pack-cockpit-dist.mjs 3.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. /**
  2. * 生产产物校验与精简(驾驶舱)。
  3. *
  4. * 用法:
  5. * node tools/pack-cockpit-dist.mjs # 只检查并报告(默认 dry-run)
  6. * node tools/pack-cockpit-dist.mjs --dist dist # 指定产物目录
  7. * node tools/pack-cockpit-dist.mjs --prune # 顺手删掉驾驶舱不需要的项
  8. *
  9. * 背景(维护者 2026-11-03):生产打包必须精简,工具与中间产物不得进入产物。
  10. * 事实依据:`fes build` 的产物 = `src/` 构建结果 + `public/` 整拷(+ `public/cesium`),
  11. * 仓库根的 `tools/`、`docs/`、`.scratch/`、`DesignAssets/`、`openspec/`、`skills/`
  12. * 都不在 `public/` 下,按构建配置不可能进入 `dist/`;本脚本把这条约束变成可执行断言,
  13. * 防止将来有人把生成物放进 `public/` 或改 publicDir 后无声污染产物。
  14. *
  15. * `--prune` 只删**确认与驾驶舱无关**的项:
  16. * · `plugins/`:TvT 插件体系的静态资源(108 MB)。驾驶舱不依赖(票 06 验收第 1 条);
  17. * 该部署若同时要跑插件页面则不要加 --prune。
  18. */
  19. import { existsSync, readdirSync, rmSync, statSync } from 'node:fs'
  20. import path from 'node:path'
  21. const argv = process.argv
  22. const arg = (name, fallback) => {
  23. const i = argv.indexOf(name)
  24. return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback
  25. }
  26. const DIST = path.resolve(arg('--dist', 'dist'))
  27. const PRUNE = argv.includes('--prune')
  28. /** 产物里出现即视为污染:工具、中间产物、源文档、审计脚本。 */
  29. const FORBIDDEN = [
  30. 'tools', 'docs', 'skills', 'openspec', 'DesignAssets', 'after-deploy',
  31. 'city-massing', 'cad-bim', 'demo', 'verify', '.scratch',
  32. ]
  33. /** 驾驶舱不依赖、可安全精简的产物项(--prune 时才删)。 */
  34. const PRUNABLE = ['plugins', 'cockpit/prototypes']
  35. const mb = (bytes) => `${(bytes / 1048576).toFixed(1)} MB`
  36. function dirSize(p) {
  37. let total = 0
  38. for (const entry of readdirSync(p, { withFileTypes: true })) {
  39. const child = path.join(p, entry.name)
  40. if (entry.isDirectory()) total += dirSize(child)
  41. else if (entry.isFile()) { try { total += statSync(child).size } catch {} }
  42. }
  43. return total
  44. }
  45. if (!existsSync(DIST)) {
  46. console.error(`产物目录不存在:${DIST}\n先跑 yarn build,或用 --dist 指定实际产物路径。`)
  47. process.exit(2)
  48. }
  49. const top = readdirSync(DIST, { withFileTypes: true })
  50. const rows = top
  51. .map((e) => {
  52. const p = path.join(DIST, e.name)
  53. const size = e.isDirectory() ? dirSize(p) : (() => { try { return statSync(p).size } catch { return 0 } })()
  54. return { name: e.name, dir: e.isDirectory(), size }
  55. })
  56. .sort((a, b) => b.size - a.size)
  57. const total = rows.reduce((s, r) => s + r.size, 0)
  58. console.log(`产物:${DIST}`)
  59. console.log(`合计:${mb(total)}\n`)
  60. console.log('按体积排序:')
  61. for (const r of rows.slice(0, 12)) console.log(` ${mb(r.size).padStart(10)} ${r.name}${r.dir ? '/' : ''}`)
  62. const leaked = rows.filter((r) => FORBIDDEN.includes(r.name))
  63. console.log('')
  64. if (leaked.length) {
  65. console.error(`✗ 产物含不该有的项:${leaked.map((r) => r.name).join(', ')}`)
  66. } else {
  67. console.log('✓ 未发现工具/中间产物/源文档(tools、city-massing、docs 等)')
  68. }
  69. const prunable = rows.filter((r) => PRUNABLE.includes(r.name))
  70. if (prunable.length) {
  71. const sum = prunable.reduce((s, r) => s + r.size, 0)
  72. console.log(`\n可精简项:${prunable.map((r) => `${r.name}(${mb(r.size)})`).join(', ')} 合计 ${mb(sum)}`)
  73. if (PRUNE) {
  74. for (const r of prunable) {
  75. rmSync(path.join(DIST, r.name), { recursive: true, force: true })
  76. console.log(` 已删除 ${r.name}`)
  77. }
  78. console.log(` 精简后合计:${mb(total - sum)}`)
  79. } else {
  80. console.log(' (加 --prune 才会删除;该部署若需要 TvT 插件页面请忽略此项)')
  81. }
  82. }
  83. process.exit(leaked.length ? 1 : 0)