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; } // 检测代码是否已被压缩 function isCodeCompressed(content) { // 如果代码没有换行符或换行符很少,很可能是已压缩的 const lineBreaks = (content.match(/\n/g) || []).length; const hasManyLineBreaks = lineBreaks > content.length / 50; // 平均每50个字符至少有一个换行 // 如果代码有很多连续的分号或逗号,很可能是已压缩的 const hasManyConsecutiveSemicolons = /;{2,}/.test(content); const hasManyConsecutiveCommas = /,{2,}/.test(content); // 如果代码没有注释,很可能是已压缩的 const hasComments = /\/\/|\/\*/.test(content); return !hasManyLineBreaks || hasManyConsecutiveSemicolons || hasManyConsecutiveCommas || !hasComments; } // 定义js压缩函数 - 使用Terser去除console语句、未使用变量和注释 function compressJs(content, filePath) { return content; try { // 检测代码是否已被压缩 const isCompressed = isCodeCompressed(content); // 先尝试使用正则表达式直接去除console语句 let processedContent = content; // 匹配完整的console函数调用,支持嵌套括号 const consoleRegex = /console\.(log|warn|error|info|debug|trace|table|dir|time|timeEnd|assert|clear|count|countReset|group|groupCollapsed|groupEnd|memory|profile|profileEnd|timeStamp)\s*\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)\s*;/gi; processedContent = processedContent.replace(consoleRegex, ''); // 配置基础压缩选项 const baseOptions = { compress: { drop_console: true, // 去除所有console语句 drop_debugger: true, // 去除debugger语句 dead_code: true, // 移除死代码 unused: true, // 移除未使用的变量 toplevel: true, // 清理顶层作用域未使用的变量 passes: isCompressed ? 15 : 10, // 大幅增加压缩次数 reduce_funcs: true, // 合并或移除未使用的函数 collapse_vars: true, // 折叠定义后不再修改的变量 sequences: true, // 合并连续的变量声明 evaluate: true, // 提前计算常量表达式 unsafe: true, // 已压缩代码启用更激进的压缩策略 unsafe_comps: true, // 优化比较操作 reduce_vars: true, // 合并或移除变量 join_vars: true, // 合并变量声明 side_effects: false, // 假设函数调用没有副作用 pure_funcs: ['console.log', 'console.warn', 'console.error', 'console.info', 'console.debug'], // 标记这些函数为纯函数,可以安全移除 pure_getters: true, // 假设getter函数没有副作用 unsafe_math: true, // 允许不安全的数学优化 unsafe_proto: true, // 允许不安全的原型优化 unsafe_regexp: true, // 允许不安全的正则表达式优化 conditionals: true, // 优化条件表达式 comparisons: true, // 优化比较操作 booleans: true, // 优化布尔表达式 typeofs: true // 优化typeof操作 }, mangle: false, // 不混淆变量名,保持代码可读性 format: { ascii_only: true, // 确保输出ASCII字符 comments: false, // 去除所有注释 beautify: false // 不美化输出 }, // 为已压缩代码启用更严格的处理 parse: { bare_returns: true, // 允许顶级return语句 expression: false // 禁用表达式模式 } }; // 使用Terser进行压缩 const result = minify(processedContent, baseOptions); 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('====================================='); } // 递归处理dist目录下已有的JS文件,再次压缩以去除console语句、未使用变量和注释 function processExistingJsFiles(distDir) { console.log(`Processing directory: ${distDir}`); const files = fs.readdirSync(distDir); console.log(`Found files: ${files.join(', ')}`); files.forEach(file => { const filePath = path.join(distDir, file); const stats = fs.statSync(filePath); if (stats.isDirectory()) { // 递归处理子目录 processExistingJsFiles(filePath); } else if (path.extname(file) === '.js') { // 处理JS文件 console.log(`Found JS file: ${filePath}`); try { let content = fs.readFileSync(filePath, { encoding: 'utf8' }); // 检测是否包含console语句 const hasConsole = /console\.(log|warn|error|info|debug|trace|table|dir|time|timeEnd)/i.test(content); if (hasConsole) { filesWithConsole.push(filePath); } // 直接使用正则表达式去除console语句(优化版本,减少复杂度) let processedContent = content; // 匹配完整的console函数调用,支持嵌套括号 const consoleRegex = /console\.(log|warn|error|info|debug|trace|table|dir|time|timeEnd|assert|clear|count|countReset|group|groupCollapsed|groupEnd|memory|profile|profileEnd|timeStamp)\s*\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)\s*;/gi; processedContent = processedContent.replace(consoleRegex, ''); // 去除单行注释 processedContent = processedContent.replace(/\/\/.*$/gm, ''); // 去除多行注释(优化版本,避免过度匹配) processedContent = processedContent.replace(/\/\*[\s\S]*?\*\//g, ''); // 然后再使用terser进行压缩 const compressedContent = compressJs(processedContent, filePath); // 检测压缩后是否仍包含console语句 const hasConsoleAfterCompression = /console\.(log|warn|error|info|debug|trace|table|dir|time|timeEnd)/i.test(compressedContent); if (hasConsoleAfterCompression) { filesWithConsoleAfterCompression.push({ sourcePath: filePath, targetPath: filePath }); } // 确保即使内容变化很小也要写入文件 const finalContent = compressedContent; // 写入文件(无论是否有变化) fs.writeFileSync(filePath, finalContent, { encoding: 'utf8' }); console.log(`Processed: ${filePath}`); // 更新统计信息 const originalSize = Buffer.from(content).length; const compressedSize = Buffer.from(finalContent).length; const sizeDiff = originalSize - compressedSize; const compressionRatio = originalSize > 0 ? (sizeDiff / originalSize * 100).toFixed(2) : '0.00'; totalJsOriginalSize += originalSize; totalJsCompressedSize += compressedSize; jsFileCount++; jsSizeDetails.push({ path: filePath, originalSize, compressedSize, sizeDiff, compressionRatio }); console.log(` Original: ${originalSize} bytes`); console.log(` Compressed: ${compressedSize} bytes`); console.log(` Saved: ${sizeDiff} bytes (-${compressionRatio}%)`); } catch (error) { console.error(`Error processing file ${filePath}:`, error.message); } } }); } // 递归复制目录并压缩文件(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'); const noZip = args.includes('--no-zip') || args.includes('-n'); const onlyProcessDist = args.includes('--only-process-dist') || args.includes('-p'); // 只处理dist目录下已有的JS文件 // 清理dist目录(如果存在且不保留) if (fs.existsSync(distDir)) { if (!keepDist && !onlyProcessDist) { cleanDistDir(distDir); } } else { // 创建dist目录 fs.mkdirSync(distDir, { recursive: true }); console.log('Created dist directory'); } // 根据参数决定执行哪种流程 if (onlyProcessDist) { // 只处理dist目录下已有的JS文件 console.log('Start processing existing JS files in dist directory...'); processExistingJsFiles(distDir); console.log('Processing existing JS files completed!'); } else { // 复制并压缩文件到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压缩包(如果未指定--no-zip参数) if (!noZip) { console.log('Creating zip archive...'); createZipArchive(distDir, zipPath); } else { console.log('Skipping zip archive creation (--no-zip specified)'); } // 清理dist目录(如果不保留且不是只处理dist目录) if (!keepDist && !onlyProcessDist) { 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(`${index + 1}. ${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(' --no-zip, -n Skip zip archive creation'); console.log(' --only-process-dist, -p Only process existing JS files in dist directory'); console.log(' --help, -h Show this help message'); }