/** * 生产产物校验与精简(驾驶舱)。 * * 用法: * node tools/pack-cockpit-dist.mjs # 只检查并报告(默认 dry-run) * node tools/pack-cockpit-dist.mjs --dist dist # 指定产物目录 * node tools/pack-cockpit-dist.mjs --prune # 顺手删掉驾驶舱不需要的项 * * 背景(维护者 2026-11-03):生产打包必须精简,工具与中间产物不得进入产物。 * 事实依据:`fes build` 的产物 = `src/` 构建结果 + `public/` 整拷(+ `public/cesium`), * 仓库根的 `tools/`、`docs/`、`.scratch/`、`DesignAssets/`、`openspec/`、`skills/` * 都不在 `public/` 下,按构建配置不可能进入 `dist/`;本脚本把这条约束变成可执行断言, * 防止将来有人把生成物放进 `public/` 或改 publicDir 后无声污染产物。 * * `--prune` 只删**确认与驾驶舱无关**的项: * · `plugins/`:TvT 插件体系的静态资源(108 MB)。驾驶舱不依赖(票 06 验收第 1 条); * 该部署若同时要跑插件页面则不要加 --prune。 */ import { existsSync, readdirSync, rmSync, statSync } from 'node:fs' import path from 'node:path' const argv = process.argv const arg = (name, fallback) => { const i = argv.indexOf(name) return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback } const DIST = path.resolve(arg('--dist', 'dist')) const PRUNE = argv.includes('--prune') /** 产物里出现即视为污染:工具、中间产物、源文档、审计脚本。 */ const FORBIDDEN = [ 'tools', 'docs', 'skills', 'openspec', 'DesignAssets', 'after-deploy', 'city-massing', 'cad-bim', 'demo', 'verify', '.scratch', ] /** 驾驶舱不依赖、可安全精简的产物项(--prune 时才删)。 */ const PRUNABLE = ['plugins', 'cockpit/prototypes'] const mb = (bytes) => `${(bytes / 1048576).toFixed(1)} MB` function dirSize(p) { let total = 0 for (const entry of readdirSync(p, { withFileTypes: true })) { const child = path.join(p, entry.name) if (entry.isDirectory()) total += dirSize(child) else if (entry.isFile()) { try { total += statSync(child).size } catch {} } } return total } if (!existsSync(DIST)) { console.error(`产物目录不存在:${DIST}\n先跑 yarn build,或用 --dist 指定实际产物路径。`) process.exit(2) } const top = readdirSync(DIST, { withFileTypes: true }) const rows = top .map((e) => { const p = path.join(DIST, e.name) const size = e.isDirectory() ? dirSize(p) : (() => { try { return statSync(p).size } catch { return 0 } })() return { name: e.name, dir: e.isDirectory(), size } }) .sort((a, b) => b.size - a.size) const total = rows.reduce((s, r) => s + r.size, 0) console.log(`产物:${DIST}`) console.log(`合计:${mb(total)}\n`) console.log('按体积排序:') for (const r of rows.slice(0, 12)) console.log(` ${mb(r.size).padStart(10)} ${r.name}${r.dir ? '/' : ''}`) const leaked = rows.filter((r) => FORBIDDEN.includes(r.name)) console.log('') if (leaked.length) { console.error(`✗ 产物含不该有的项:${leaked.map((r) => r.name).join(', ')}`) } else { console.log('✓ 未发现工具/中间产物/源文档(tools、city-massing、docs 等)') } const prunable = rows.filter((r) => PRUNABLE.includes(r.name)) if (prunable.length) { const sum = prunable.reduce((s, r) => s + r.size, 0) console.log(`\n可精简项:${prunable.map((r) => `${r.name}(${mb(r.size)})`).join(', ')} 合计 ${mb(sum)}`) if (PRUNE) { for (const r of prunable) { rmSync(path.join(DIST, r.name), { recursive: true, force: true }) console.log(` 已删除 ${r.name}`) } console.log(` 精简后合计:${mb(total - sum)}`) } else { console.log(' (加 --prune 才会删除;该部署若需要 TvT 插件页面请忽略此项)') } } process.exit(leaked.length ? 1 : 0)