cross-check-icons.mjs 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. // 交叉校验:注册表 ↔ manifest ↔ 组件引用 ↔ 面板映射,四处必须一致
  2. import { readFileSync, readdirSync } from 'node:fs'
  3. import { join } from 'node:path'
  4. const read = (p) => readFileSync(p, 'utf8')
  5. const cockpit = 'src/modules/cockpit'
  6. const manifest = JSON.parse(read('public/cockpit/assets/manifest.json'))
  7. const manifestKeys = new Set(manifest.entries.map((e) => e.key))
  8. const iconsSrc = read(join(cockpit, 'domain/icons.js'))
  9. // 注册表条目形如 ` name: 'icon.key',`
  10. const registry = new Map()
  11. for (const m of iconsSrc.matchAll(/^\s{4}([a-zA-Z]+):\s*'([^']+)'/gm)) registry.set(m[1], m[2])
  12. console.log('注册表图标数:', registry.size)
  13. console.log('manifest 条目数:', manifestKeys.size)
  14. // ① 注册表引用的 key 必须在 manifest 中
  15. const missingInManifest = [...registry.entries()].filter(([, k]) => !manifestKeys.has(k))
  16. console.log('① 注册表引用但 manifest 缺失:', missingInManifest.length ? missingInManifest.map(([n, k]) => `${n}->${k}`).join(', ') : '无')
  17. // ② manifest 中未被注册表引用的 key(冗余)
  18. // 票 18(批次 5)修正既有盲区:`marker.*` 六键由位置符号合成层(services/markerAssets.js
  19. // 经 resolveAsset('marker.master.*'/'marker.inner.*'))直连消费,不经过 icons 注册表——
  20. // 此前每次跑都会把批次 3 的正式条目误报为「冗余」。判定依据:markerAssets.js 源码里
  21. // 真实出现这些 key 前缀;若该接缝改名,此处会重新报冗余(不掩盖真实漂移)。
  22. const usedKeys = new Set(registry.values())
  23. const markerAssetsSrc = read(join(cockpit, 'services/markerAssets.js'))
  24. const consumedElsewhere = (k) =>
  25. (k.startsWith('marker.master.') || k.startsWith('marker.inner.')) && /marker\.(master|inner)\./.test(markerAssetsSrc)
  26. const unused = [...manifestKeys].filter((k) => !usedKeys.has(k) && !consumedElsewhere(k))
  27. const coveredBySeams = [...manifestKeys].filter((k) => !usedKeys.has(k) && consumedElsewhere(k))
  28. console.log('② manifest 中冗余 key:', unused.length ? unused.join(', ') : '无')
  29. if (coveredBySeams.length) console.log(` (另有 ${coveredBySeams.length} 个 key 经位置符号合成层消费,不在注册表:${coveredBySeams.join(', ')})`)
  30. // ③ 组件里 <CockpitIcon name="x"> 的 x 必须已注册
  31. const vueFiles = []
  32. ;(function walk(dir) {
  33. for (const e of readdirSync(dir, { withFileTypes: true })) {
  34. const p = join(dir, e.name)
  35. if (e.isDirectory()) walk(p)
  36. else if (e.name.endsWith('.vue')) vueFiles.push(p)
  37. }
  38. })(cockpit)
  39. let badRefs = 0
  40. for (const f of vueFiles) {
  41. const s = read(f)
  42. for (const m of s.matchAll(/<CockpitIcon[^>]*\sname="([a-zA-Z-]+)"/g)) {
  43. if (!registry.has(m[1])) {
  44. console.log(` ✗ 未注册引用: ${m[1]} in ${f}`)
  45. badRefs++
  46. }
  47. }
  48. }
  49. console.log('③ 组件内未注册图标引用:', badRefs)
  50. // ④ PANEL_ICONS 的每个值必须已注册
  51. const panelSrc = read(join(cockpit, 'domain/panelCatalog.js'))
  52. const panelBlock = panelSrc.split('PANEL_ICONS = {')[1].split('\n}')[0]
  53. let badPanels = 0
  54. let panelCount = 0
  55. for (const m of panelBlock.matchAll(/(\w+):\s*'([a-zA-Z]+)'/g)) {
  56. panelCount++
  57. if (!registry.has(m[2])) {
  58. console.log(` ✗ 面板映射缺图标: ${m[1]} -> ${m[2]}`)
  59. badPanels++
  60. }
  61. }
  62. console.log(`④ 面板图标映射: ${panelCount} 项,缺失 ${badPanels}`)
  63. // ⑤ INDUSTRY_ICONS 同理
  64. const filterSrc = read(join(cockpit, 'domain/filterOptions.js'))
  65. const indBlock = filterSrc.split('INDUSTRY_ICONS = {')[1].split('\n}')[0]
  66. let badInd = 0
  67. let indCount = 0
  68. for (const m of indBlock.matchAll(/(\w+):\s*'([a-zA-Z]+)'/g)) {
  69. indCount++
  70. if (!registry.has(m[2])) {
  71. console.log(` ✗ 行业映射缺图标: ${m[1]} -> ${m[2]}`)
  72. badInd++
  73. }
  74. }
  75. console.log(`⑤ 行业图标映射: ${indCount} 项,缺失 ${badInd}`)
  76. const ok = !missingInManifest.length && !unused.length && !badRefs && !badPanels && !badInd
  77. console.log(ok ? '\n✅ 四处交叉引用完全一致' : '\n❌ 存在不一致,见上')
  78. if (!ok) process.exitCode = 1