const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // 直接引用本地安装的 mini-optimizer 模块 const Optimizer = require('mini-optimizer').default; // 导入 mini-optimizer 配置文件 const optimizerConfig = require('./mini-optimizer.config.js'); // 定义项目根目录 const rootDir = __dirname; // 定义输出目录 const distDir = path.join(rootDir, 'dist'); // 定义要排除的目录和文件 const excludeDirs = ['node_modules', '.git', 'dist', '.vscode']; const excludeFiles = ['.gitignore', 'package.json', 'package-lock.json', 'yarn.lock', 'release.js', 'mini-optimizer.config.js', 'babel.config.js', 'README.md']; // 获取当前日期,用于生成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压缩函数 - 采用更安全的压缩策略,接近微信小程序开发工具的压缩方式 function compressJs(content) { // 移除单行注释 content = content.replace(/\/\/.*$/gm, ''); // 移除多行注释 content = content.replace(/\/\*[\s\S]*?\*\//g, ''); // 移除行尾多余的空白字符 content = content.replace(/\s+$/gm, ''); // 移除多余的空行,只保留必要的换行 content = content.replace(/\n{2,}/g, '\n'); // 保存所有require语句,避免压缩时破坏模块路径 const requireStatements = []; let requireIndex = 0; content = content.replace(/require\s*\([^)]+\)/g, (match) => { const placeholder = `__REQUIRE_PLACEHOLDER_${requireIndex}__`; requireStatements.push({ placeholder, content: match }); requireIndex++; return placeholder; }); // 在适当的位置添加分号,避免语法错误 // 只在特定情况下添加分号 // 1. 处理return、break、continue、throw等语句 content = content.replace(/(return|break|continue|throw)\s*(.*?)\n/g, '$1 $2;\n'); // 2. 处理变量声明和赋值(简单情况) content = content.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*\s*=\s*[^;\n]+)\n/g, '$1;\n'); // 3. 处理函数调用 content = content.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*\s*\([^;\n]+\))\n/g, '$1;\n'); // 移除语句之间多余的空白字符,但保留必要的空格 content = content.replace(/\s*\{\s*/g, '{'); content = content.replace(/\s*\}\s*/g, '}'); content = content.replace(/\s*;\s*/g, ';'); content = content.replace(/\s*,\s*/g, ','); content = content.replace(/\s*=\s*/g, '='); content = content.replace(/\s*\+\s*/g, '+'); content = content.replace(/\s*-\s*/g, '-'); content = content.replace(/\s*\*\s*/g, '*'); content = content.replace(/\s*\/\s*/g, '/'); content = content.replace(/\s*\[\s*/g, '['); content = content.replace(/\s*\]\s*/g, ']'); // 移除多余的分号 content = content.replace(/;;+/g, ';'); // 移除多余的空白字符(连续的空格只保留一个) content = content.replace(/\s+/g, ' '); // 恢复之前保存的require语句 requireStatements.forEach(({ placeholder, content: requireContent }) => { content = content.replace(placeholder, requireContent + ';'); }); // 移除文件开头和结尾的分号 content = content.replace(/^;|;$/g, ''); // 移除所有多余的空格 content = content.replace(/\s+/g, ''); // 确保最后一个require语句也有分号 content = content.replace(/require\([^)]+\)$/, '$&;'); 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; } } // 递归复制目录(只复制文件,不进行压缩) function copyDir(sourceDir, targetDir) { // 创建目标目录 if (!fs.existsSync(targetDir)) { fs.mkdirSync(targetDir, { recursive: true }); } const files = fs.readdirSync(sourceDir); for (const file of files) { const sourcePath = path.join(sourceDir, file); const targetPath = path.join(targetDir, file); const stats = fs.statSync(sourcePath); // 跳过排除的目录和文件 if (excludeDirs.includes(file) || excludeFiles.includes(file)) { continue; } if (stats.isDirectory()) { // 递归处理子目录 copyDir(sourcePath, targetPath); } else { // 直接复制文件 fs.copyFileSync(sourcePath, targetPath); console.log(`Copied: ${sourcePath}`); } } } // 使用 mini-optimizer API 对 dist 目录进行统一压缩 async function compressDistDir(targetDir) { console.log('Start compressing files in dist directory using mini-optimizer API...'); // 创建优化器实例,使用配置文件中的极限压缩选项 const optimizer = new Optimizer(targetDir, 'wx', optimizerConfig); // 是否启用兼容模式 - 开启后,会使用自定义压缩函数处理压缩失败的文件 const enableCompatMode = false; // 记录哪些成功被压缩的文件 const compressedFiles = []; const unCompressedFiles = []; // 递归遍历 dist 目录,压缩所有需要的文件类型 async function compressDir(dir) { const files = fs.readdirSync(dir); for (const file of files) { const filePath = path.join(dir, file); const stats = fs.statSync(filePath); if (stats.isDirectory()) { // 递归处理子目录 await compressDir(filePath); } else { const extname = path.extname(file); try { // 读取文件内容 const content = fs.readFileSync(filePath, { encoding: 'utf8' }); let compressedContent; // 使用 mini-optimizer 压缩不同类型的文件 if (extname === '.wxml' || extname === '.xml') { // 压缩xml和wxml文件 - 禁用属性值压缩以避免Babel解析错误 try { compressedContent = optimizer.optimize_xml(content, { minifyAttributes: false }); compressedFiles.push(filePath); } catch (error) { if (!enableCompatMode) { throw error; } compressedContent = compressWxml(content); compressedFiles.push(filePath); } } else if (extname === '.js') { // 压缩js文件 try { compressedContent = optimizer.optimize_js(content); compressedFiles.push(filePath); } catch (error) { if (!enableCompatMode) { throw error; } compressedContent = compressJs(content); compressedFiles.push(filePath); } } else if (extname === '.css' || extname === '.wxss') { // 压缩css和wxss文件 try { compressedContent = await optimizer.optimize_css(content); compressedFiles.push(filePath); } catch (error) { if (!enableCompatMode) { throw error; } compressedContent = compressCss(content); compressedFiles.push(filePath); } } else if (extname === '.json') { // 压缩json文件 try { compressedContent = optimizer.optimize_json(content); compressedFiles.push(filePath); } catch (error) { if (!enableCompatMode) { throw error; } compressedContent = compressJson(content); compressedFiles.push(filePath); } } else if (extname === '.xs') { // 压缩xs文件 compressedContent = optimizer.optimize_xs(content); compressedFiles.push(filePath); } else { // 跳过不需要压缩的文件 continue; } // 写入压缩后的内容 fs.writeFileSync(filePath, compressedContent, { encoding: 'utf8' }); console.log(`Compressed: ${filePath}`); } catch (error) { console.error(`Error compressing file ${filePath}:`, error.message); unCompressedFiles.push(filePath); // 发生错误时,保留原始文件 console.log(`Kept original file: ${filePath}`); } } } } // 开始压缩整个dist目录 await compressDir(targetDir); // 打印压缩成功的文件列表 console.log('Compressed files:', compressedFiles); // 打印未压缩的文件列表 console.log('Uncompressed files:', unCompressedFiles); console.log('Files compressed successfully using mini-optimizer API!'); } // 使用系统命令创建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)'); } } // 主函数 async function main() { try { // 解析命令行参数 const args = process.argv.slice(2); const keepDist = args.includes('--keep-dist') || args.includes('-k'); // 清理dist目录(如果存在且不保留) if (fs.existsSync(distDir)) { cleanDistDir(distDir); } else { // 创建dist目录 fs.mkdirSync(distDir, { recursive: true }); console.log('Created dist directory'); } // 第一步:复制所有文件到dist目录(不压缩) console.log('Start copying files...'); copyDir(rootDir, distDir); console.log('Files copied successfully!'); // 第二步:使用mini-optimizer对dist目录进行统一压缩 console.log('Start compressing files in dist directory...'); await compressDistDir(distDir); console.log('Files compressed successfully!'); // 生成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}`); } 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'); }