const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const { minify } = require('terser'); // 定义项目根目录 const rootDir = __dirname; // 定义输出目录 const distDir = path.join(rootDir, 'dist'); // 定义要排除的目录和文件 const excludeDirs = ['node_modules', '.git', 'dist', '.vscode', 'scripts']; const excludeFiles = ['.gitignore', 'package-lock.json', 'yarn.lock', 'release.js', 'README.md']; // 统计信息变量 let totalJsOriginalSize = 0; let totalJsCompressedSize = 0; let jsFileCount = 0; const jsSizeDetails = []; // 存储包含console语句的JS文件 const filesWithConsole = []; // 存储压缩错误的JS文件 const filesWithCompressionError = []; // 存储压缩后仍含console语句的JS文件 const filesWithConsoleAfterCompression = []; // 获取当前日期,用于生成zip文件名(格式:YYYY-MM-DD) function getCurrentDate() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } // 定义wxml压缩函数 function compressWxml(content) { // 移除HTML注释 content = content.replace(//g, ''); // 移除多余的空白字符(空格、换行、制表符) // 保留标签内的必要空格 content = content.replace(/\s+/g, ' '); // 移除标签前后的空格 content = content.replace(/\s*\s*/g, '>'); return content; } // 定义js压缩函数 - 使用Terser去除console语句 function compressJs(content, filePath) { return content; try { // 使用Terser进行压缩 const result = minify(content, { compress: { drop_console: true, // 去除所有console语句 drop_debugger: true, // 去除debugger语句 dead_code: true, passes: 3 // 增加压缩次数 }, mangle: false, // 不混淆变量名,保持代码可读性 output: { ascii_only: true, // 确保输出ASCII字符 comments: false // 去除所有注释 } }); if (result.error) { console.warn('Terser compression failed for', filePath, ':', result.error.message); if (filePath && !filesWithCompressionError.includes(filePath)) { filesWithCompressionError.push(filePath); } return content; // 出错时返回原始内容 } return result.code || content; } catch (error) { console.warn('Error in compressJs for', filePath, ':', error.message); if (filePath && !filesWithCompressionError.includes(filePath)) { filesWithCompressionError.push(filePath); } return content; // 捕获异常时返回原始内容 } } // 定义css压缩函数 function compressCss(content) { // 移除单行注释 content = content.replace(/\/\/.*$/gm, ''); // 移除多行注释 content = content.replace(/\/\*[\s\S]*?\*\//g, ''); // 移除多余的空白字符 content = content.replace(/\s+/g, ' '); // 移除CSS属性前后的空格 content = content.replace(/\s*([{:;}])\s*/g, '$1'); // 移除行首和行尾的空格 content = content.replace(/^\s+|\s+$/gm, ''); return content; } // 定义json压缩函数 function compressJson(content) { try { // 使用JSON.parse和JSON.stringify进行压缩 // 确保中文等非ASCII字符不会被转义 const jsonObj = JSON.parse(content); return JSON.stringify(jsonObj, null, 0); } catch (error) { // 如果JSON解析失败,返回原始内容 console.warn('Failed to compress JSON, using original content:', error.message); return content; } } // 输出JS文件压缩统计结果 function outputJsCompressionStats() { if (jsFileCount === 0) { console.log('\nNo JS files were processed.'); return; } console.log('\n=== JS File Compression Statistics ==='); // 输出每个文件的详细信息 console.log('\nIndividual File Statistics:'); jsSizeDetails.forEach(file => { console.log(`${file.path}`); console.log(` Original: ${file.originalSize} bytes`); console.log(` Compressed: ${file.compressedSize} bytes`); console.log(` Saved: ${file.sizeDiff} bytes (-${file.compressionRatio}%)`); console.log(''); }); // 计算整体统计信息 const totalSaved = totalJsOriginalSize - totalJsCompressedSize; const overallRatio = totalJsOriginalSize > 0 ? (totalSaved / totalJsOriginalSize * 100).toFixed(2) : '0.00'; // 输出整体统计结果 console.log('=== Overall JS Compression Results ==='); console.log(`Total JS files processed: ${jsFileCount}`); console.log(`Total original size: ${totalJsOriginalSize} bytes`); console.log(`Total compressed size: ${totalJsCompressedSize} bytes`); console.log(`Total saved: ${totalSaved} bytes (-${overallRatio}%)`); console.log('====================================='); } // 递归复制目录并压缩文件(wxml, js, css, json) function copyAndCompressDir(sourceDir, targetDir) { // 创建目标目录 if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } const files = fs.readdirSync(sourceDir); files.forEach(file => { const sourcePath = path.join(sourceDir, file); const targetPath = path.join(targetDir, file); const stats = fs.statSync(sourcePath); // 跳过排除的目录和文件 if (excludeDirs.includes(file) || excludeFiles.includes(file)) { return; } if (stats.isDirectory()) { // 递归处理子目录 copyAndCompressDir(sourcePath, targetPath); } else { const extname = path.extname(file); let compressedContent; let content; try { // 根据文件类型选择不同的处理方式 if (extname === '.wxml') { // 压缩wxml文件 content = fs.readFileSync(sourcePath, { encoding: 'utf8' }); compressedContent = compressWxml(content); fs.writeFileSync(targetPath, compressedContent, { encoding: 'utf8' }); console.log(`Compressed and copied: ${sourcePath}`); } else if (extname === '.js') { // 压缩js文件 content = fs.readFileSync(sourcePath, { encoding: 'utf8' }); // 检测是否包含console语句 const hasConsole = /console\.(log|warn|error|info|debug|trace|table|dir|time|timeEnd)/i.test(content); if (hasConsole) { filesWithConsole.push(sourcePath); } compressedContent = compressJs(content, sourcePath); // 检测压缩后是否仍包含console语句 const hasConsoleAfterCompression = /console\.(log|warn|error|info|debug|trace|table|dir|time|timeEnd)/i.test(compressedContent); if (hasConsoleAfterCompression) { filesWithConsoleAfterCompression.push({ sourcePath, targetPath }); } fs.writeFileSync(targetPath, compressedContent, { encoding: 'utf8' }); // 统计JS文件大小变化 const originalSize = Buffer.from(content).length; const compressedSize = Buffer.from(compressedContent).length; const sizeDiff = originalSize - compressedSize; const compressionRatio = originalSize > 0 ? (sizeDiff / originalSize * 100).toFixed(2) : '0.00'; totalJsOriginalSize += originalSize; totalJsCompressedSize += compressedSize; jsFileCount++; jsSizeDetails.push({ path: sourcePath, originalSize, compressedSize, sizeDiff, compressionRatio }); console.log(`Compressed and copied: ${sourcePath} - ${originalSize} → ${compressedSize} bytes (-${compressionRatio}%)`); } else if (extname === '.css') { // 压缩css文件 content = fs.readFileSync(sourcePath, { encoding: 'utf8' }); compressedContent = compressCss(content); fs.writeFileSync(targetPath, compressedContent, { encoding: 'utf8' }); console.log(`Compressed and copied: ${sourcePath}`); } else if (extname === '.json') { // 压缩json文件 content = fs.readFileSync(sourcePath, { encoding: 'utf8' }); compressedContent = compressJson(content); fs.writeFileSync(targetPath, compressedContent, { encoding: 'utf8' }); console.log(`Compressed and copied: ${sourcePath}`); } else { // 直接复制其他文件 fs.copyFileSync(sourcePath, targetPath); console.log(`Copied: ${sourcePath}`); } } catch (error) { console.error(`Error processing file ${sourcePath}:`, error.message); // 发生错误时,直接复制原始文件 fs.copyFileSync(sourcePath, targetPath); console.log(`Fallback: Copied original file ${sourcePath}`); } } }); } // 使用系统命令创建zip压缩包 function createZipArchive(sourceDir, outputPath) { try { // 根据操作系统选择命令 if (process.platform === 'win32') { // Windows系统使用PowerShell命令,排除文件名后缀为 -mp-weixin.zip 的文件 // 使用PowerShell的管道功能和Where-Object来排除特定文件 const command = `PowerShell.exe -Command "Get-ChildItem -Path \"${sourceDir}\" | Where-Object { $_.Name -notlike '*\-mp\-weixin\.zip' } | Compress-Archive -DestinationPath \"${outputPath}\" -Force"`; execSync(command, { shell: 'cmd.exe' }); } else { // Linux/Mac系统使用zip命令,排除文件名后缀为 -mp-weixin.zip 的文件 const command = `zip -r \"${outputPath}\" * -x \"*-mp-weixin.zip\"`; execSync(command, { cwd: sourceDir }); } console.log(`Zip archive created: ${outputPath}`); } catch (error) { console.error('Error creating zip archive:', error.message); throw error; } } function cleanDistDir(distDir) { if (fs.existsSync(distDir)) { // 清理dist目录下除**-mp-weixin.zip文件外的所有内容 const files = fs.readdirSync(distDir); files.forEach(file => { const filePath = path.join(distDir, file); const stats = fs.statSync(filePath); // 只保留符合特定命名模式的zip文件(**-mp-weixin.zip) if (stats.isFile() && path.extname(file) === '.zip' && file.endsWith('-mp-weixin.zip')) { console.log(`Keeping zip file: ${file}`); } else { if (stats.isDirectory()) { fs.rmSync(filePath, { recursive: true, force: true }); console.log(`Removed directory: ${file}`); } else { fs.unlinkSync(filePath); console.log(`Removed file: ${file}`); } } }); console.log('Cleaned dist directory (excluding -mp-weixin.zip files)'); } } // 主函数 function main() { try { // 解析命令行参数 const args = process.argv.slice(2); const keepDist = args.includes('--keep-dist') || args.includes('-k') || true; // 清理dist目录(如果存在且不保留) if (fs.existsSync(distDir)) { cleanDistDir(distDir); } else { // 创建dist目录 fs.mkdirSync(distDir, { recursive: true }); console.log('Created dist directory'); } // 复制并压缩文件到dist目录 console.log('Start copying and compressing files...'); copyAndCompressDir(rootDir, distDir); console.log('Files copied and compressed successfully!'); // 输出JS文件压缩统计结果 outputJsCompressionStats(); // 生成zip文件名(格式:POCT检测分析平台-定制化-YYYY-MM-DD-mp-weixin.zip) const currentDate = getCurrentDate(); const zipFileName = `POCT检测分析平台-定制化-${currentDate}-mp-weixin.zip`; const zipPath = path.join(distDir, zipFileName); // 创建zip压缩包 console.log('Creating zip archive...'); createZipArchive(distDir, zipPath); // 清理dist目录(如果不保留) if (!keepDist) { cleanDistDir(distDir); } console.log('\nAll tasks completed successfully!'); console.log(`Dist directory: ${distDir}`); console.log(`Zip file: ${zipPath}`); // 输出包含console语句的JS文件列表 if (filesWithConsole.length > 0) { console.log('\n=== JS Files with Console Statements ==='); console.log(`Found ${filesWithConsole.length} file(s) containing console statements:`); filesWithConsole.forEach(file => { console.log(`- ${file}`); }); console.log('======================================'); } else { console.log('\nNo JS files contain console statements.'); } // 输出压缩错误的JS文件列表 if (filesWithCompressionError.length > 0) { console.log('\n=== JS Files with Compression Errors ==='); console.log(`Found ${filesWithCompressionError.length} file(s) with compression errors:`); filesWithCompressionError.forEach(file => { console.log(`- ${file}`); }); console.log('======================================'); } else { console.log('\nNo JS files had compression errors.'); } // 输出压缩后仍含有console语句的JS文件列表 if (filesWithConsoleAfterCompression.length > 0) { console.log('\n=== JS Files with Console Statements After Compression ==='); console.log(`Found ${filesWithConsoleAfterCompression.length} file(s) still containing console statements after compression:`); filesWithConsoleAfterCompression.forEach((file, index) => { console.log(`\n${index + 1}. Source: ${file.sourcePath}`); console.log(` Target: ${file.targetPath}`); }); console.log('\n================================================='); } else { console.log('\nNo JS files contain console statements after compression.'); } } catch (error) { console.error('Error occurred:', error); process.exit(1); } } // 执行主函数 main(); // 输出帮助信息 if (process.argv.includes('--help') || process.argv.includes('-h')) { console.log('Usage: node release.js [options]'); console.log('Options:'); console.log(' --keep-dist, -k Keep existing dist directory contents'); console.log(' --help, -h Show this help message'); }