/** * 打包脚本:压缩并拷贝静态资源到 dist 目录。 * * 压缩策略: * - HTML → html-minifier(移除空白、注释、可省略引号) * - CSS → clean-css * - JS → terser * - 图片 → 直接拷贝 * * 排除:token.js、.env*、welfare-config.example.js、隐藏目录、node_modules、docs、scripts */ const fs = require('fs'); const path = require('path'); const { minify: minifyHtml } = require('html-minifier'); const CleanCSS = require('clean-css'); const { minify: minifyJs } = require('terser'); const ROOT = path.resolve(__dirname, '..'); const DIST = path.join(ROOT, 'dist'); // ───────────────── 排除规则 ───────────────── const EXCLUDE_NAMES = new Set(['token.js', '.env', '.env.local', 'welfare-config.example.js']); function shouldExclude(relativePath) { const name = path.basename(relativePath); if (EXCLUDE_NAMES.has(name)) return true; // 隐藏目录 / 黑名单目录 for (const part of relativePath.split(path.sep)) { if (part.startsWith('.') || ['node_modules', 'docs', 'dist', 'scripts'].includes(part)) return true; } return false; } // ───────────────── 压缩器实例 ───────────────── const htmlMinifyOpts = { collapseWhitespace: true, // 合并空白 removeComments: true, // 删除注释 removeAttributeQuotes: false, // 保留引号(避免兼容性问题) minifyCSS: true, // 内联 CSS 也压缩 minifyJS: false, // 内联 JS 不压缩(避免变量名冲突) removeEmptyAttributes: true, keepClosingSlash: false, }; const cleanCSS = new CleanCSS({ level: 2 }); // ───────────────── 文件处理 ───────────────── const extHandlers = { '.html': handleHtml, '.css': handleCss, '.js': handleJs, }; function handleHtml(src, dest) { const raw = fs.readFileSync(src, 'utf8'); const result = minifyHtml(raw, htmlMinifyOpts); fs.writeFileSync(dest, result, 'utf8'); logFile(src, dest, raw.length, result.length); } async function handleJs(src, dest) { const raw = fs.readFileSync(src, 'utf8'); const result = await minifyJs(raw, { compress: { drop_console: false }, mangle: false, // 不混淆变量名(i18n key 等动态引用) output: { comments: false }, }); if (result.code) { fs.writeFileSync(dest, result.code, 'utf8'); logFile(src, dest, raw.length, result.code.length); } else { // terser 压缩失败时直接拷贝 fs.copyFileSync(src, dest); console.warn('[build] ⚠ terser 跳过: ' + path.relative(ROOT, src)); } } function handleCss(src, dest) { const raw = fs.readFileSync(src, 'utf8'); const output = cleanCSS.minify(raw); if (output.styles) { fs.writeFileSync(dest, output.styles, 'utf8'); logFile(src, dest, raw.length, output.styles.length); } else { fs.copyFileSync(src, dest); } } function logFile(src, dest, before, after) { const name = path.relative(ROOT, src); const ratio = before > 0 ? ((1 - after / before) * 100).toFixed(1) : '0.0'; console.log(`[build] ${name} ${(before / 1024).toFixed(1)} KB → ${(after / 1024).toFixed(1)} KB (${ratio}%)`); } // ───────────────── 目录拷贝(含压缩) ───────────────── async function buildDir(srcDir, destDir) { if (!fs.existsSync(srcDir)) return; const entries = fs.readdirSync(srcDir, { withFileTypes: true }); for (const entry of entries) { const src = path.join(srcDir, entry.name); const relative = path.relative(ROOT, src); if (shouldExclude(relative)) continue; const dest = path.join(DIST, relative); if (entry.isDirectory()) { fs.mkdirSync(dest, { recursive: true }); await buildDir(src, dest); } else { fs.mkdirSync(path.dirname(dest), { recursive: true }); const handler = extHandlers[path.extname(entry.name)]; if (handler) { await handler(src, dest); } else { fs.copyFileSync(src, dest); } } } } // ───────────────── 统计 ───────────────── function countFiles(dir) { let c = 0; if (!fs.existsSync(dir)) return 0; for (const e of fs.readdirSync(dir, { withFileTypes: true })) { c += e.isDirectory() ? countFiles(path.join(dir, e.name)) : 1; } return c; } // ───────────────── 主流程 ───────────────── (async () => { const start = Date.now(); // 1. 清理 console.log('[build] 清理 dist…'); if (fs.existsSync(DIST)) fs.rmSync(DIST, { recursive: true }); fs.mkdirSync(DIST, { recursive: true }); // 2. html/ 子目录(含压缩) console.log('[build] 打包 html/…'); await buildDir(path.join(ROOT, 'html'), path.join(DIST, 'html')); // 3. css / js / images for (const dir of ['css', 'js', 'images']) { console.log('[build] 打包 ' + dir + '/…'); await buildDir(path.join(ROOT, dir), path.join(DIST, dir)); } // 4. 根目录 *.html(压缩) console.log('[build] 打包根目录 HTML…'); for (const entry of fs.readdirSync(ROOT, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith('.html')) continue; const src = path.join(ROOT, entry.name); const dest = path.join(DIST, entry.name); await handleHtml(src, dest); } const total = countFiles(DIST); const elapsed = ((Date.now() - start) / 1000).toFixed(2); console.log('[build] 完成! ' + total + ' 个文件 → dist/ (' + elapsed + 's)'); })();