batch2-token-check.mjs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. /**
  2. * 票 15(批次 2)专项断言面:token 静态检查。
  3. *
  4. * 运行:node .scratch/lanhu-cockpit-asset-replacement/verify/batch2-token-check.mjs
  5. * 零依赖 Node 断言(同 T01 立场:仓库无测试运行器,不为此引入新框架)。
  6. */
  7. import assert from 'node:assert/strict'
  8. import { readFileSync, readdirSync, statSync } from 'node:fs'
  9. import { fileURLToPath } from 'node:url'
  10. import { dirname, join, relative } from 'node:path'
  11. const here = dirname(fileURLToPath(import.meta.url))
  12. const root = join(here, '..', '..', '..')
  13. const cockpitDir = join(root, 'src', 'modules', 'cockpit')
  14. const files = []
  15. ;(function walk(dir) {
  16. for (const e of readdirSync(dir)) {
  17. const p = join(dir, e)
  18. if (statSync(p).isDirectory()) walk(p)
  19. else if (/\.(vue|js|css)$/.test(e)) files.push(relative(root, p).split('\\').join('/'))
  20. }
  21. })(cockpitDir)
  22. const read = (p) => readFileSync(join(root, p), 'utf8')
  23. const problems = []
  24. function check(name, fn) {
  25. try {
  26. fn()
  27. console.log(` PASS ${name}`)
  28. } catch (error) {
  29. problems.push(`${name}: ${error.message}`)
  30. console.log(` FAIL ${name}\n ${error.message}`)
  31. }
  32. }
  33. const tokens = read('src/modules/cockpit/styles/tokens.css')
  34. const componentFiles = files.filter((f) => f !== 'src/modules/cockpit/styles/tokens.css')
  35. console.log('\n[A] token 声明唯一性(tokens.css 是唯一来源)')
  36. check('A1 语义色/主色/文字 token 只在 tokens.css 声明', () => {
  37. const offenders = []
  38. for (const f of componentFiles) {
  39. const src = read(f)
  40. for (const name of ['--point-offline', '--point-empty', '--point-unknown', '--point-unassociated', '--industry-gas', '--industry-water', '--industry-drainage', '--industry-manhole', ...Array.from({ length: 8 }, (_, i) => `--chart-c${i + 1}`), '--lv1', '--lv2', '--lv3', '--lv4', '--neutral', '--cyan', '--cyan-d', '--cyan-hi', '--text', '--text-2', '--text-strong', '--muted', '--bg-0', '--panel-top', '--panel-mid', '--panel-bot', '--panel-line', '--panel-line-hi', '--inset']) {
  41. if (new RegExp(`${name}\\s*:`).test(src)) offenders.push(`${f} 重复声明 ${name}`)
  42. }
  43. }
  44. assert.deepEqual(offenders, [], offenders.join('\n'))
  45. })
  46. check('A2 批次 2 新值已在 tokens.css 落地(设计稿 v3/v4)', () => {
  47. for (const [name, value] of [
  48. ['--u', 'clamp(1px, 0.0694vw, 2px)'],
  49. ['--bg-0', '#010514'],
  50. ['--cyan', '#01abea'],
  51. ['--cyan-hi', '#00b8ff'],
  52. ['--cyan-d', '#009de9'],
  53. ['--lv1', '#ff3b52'],
  54. ['--lv2', '#ffb020'],
  55. ['--lv3', '#01abea'],
  56. ['--lv4', '#27d17a'],
  57. ['--neutral', '#5680b0'],
  58. ['--text', '#d7ecff'],
  59. ['--text-2', '#88b7e8'],
  60. ['--text-strong', '#ffffff'],
  61. ['--muted', '#5680b0'],
  62. ['--fs-kpi', 'calc(20 * var(--u))'],
  63. ]) {
  64. const re = new RegExp(`${name}\\s*:\\s*${value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
  65. assert.match(tokens, re, `${name} 应为设计稿值 ${value}`)
  66. }
  67. })
  68. check('A3 新增 token 都有消费点(规格 03:无消费点不提交)', () => {
  69. const required = {
  70. '--cyan-hi': 1,
  71. '--text-strong': 1,
  72. '--panel-mid': 1,
  73. '--fs-kpi': 1,
  74. '--point-offline': 1,
  75. '--point-empty': 1,
  76. '--point-unknown': 1,
  77. '--point-unassociated': 1,
  78. '--industry-gas': 1,
  79. '--industry-water': 1,
  80. '--industry-drainage': 1,
  81. '--industry-manhole': 1,
  82. }
  83. for (const [name, min] of Object.entries(required)) {
  84. const uses = componentFiles.filter((f) => read(f).includes(`var(${name}`)).length
  85. assert.ok(uses >= min, `${name} 无消费点(出现于 ${uses} 个文件)`)
  86. }
  87. })
  88. check('A4 --fs-metric 未提交(设计稿 25px 档无消费点,按规格 03 不设)', () => {
  89. assert.doesNotMatch(tokens, /--fs-metric/, '--fs-metric 不应声明')
  90. })
  91. console.log('\n[B] 组件内裸主题字面量清退')
  92. const BANNED = [
  93. ['旧主色 #4fd8ff', /#4fd8ff\b/i],
  94. ['旧主色深 #1b93cf', /#1b93cf\b/i],
  95. ['旧报警 #ff4d5e', /#ff4d5e\b/i],
  96. ['旧预警 #ffa63d', /#ffa63d\b/i],
  97. ['旧正常 #35e39a', /#35e39a\b/i],
  98. ['旧中性 #6d92ae', /#6d92ae\b/i],
  99. ['旧文字 #e6f5ff', /#e6f5ff\b/i],
  100. ['旧文字次 #a8cbe2', /#a8cbe2\b/i],
  101. ['旧标题亮 #eaf9ff', /#eaf9ff\b/i],
  102. ['旧面板顶 rgba(12,48,86', /rgba\(12,\s*48,\s*86/],
  103. ['旧面板底 rgba(3,14,28', /rgba\(3,\s*14,\s*28/],
  104. ['旧面板线 rgba(58,170,226', /rgba\(58,\s*170,\s*226/],
  105. ['旧高亮线 rgba(96,220,255', /rgba\(96,\s*220,\s*255/],
  106. ]
  107. check('B1 旧 token 值字面量在面板域清零', () => {
  108. const offenders = []
  109. for (const f of componentFiles) {
  110. if (/cesiumEngine\.js$/.test(f)) continue // 地图域归批次 3
  111. if (/domain\/fog\.js$/.test(f)) continue // 雾效域:色值是 HSV 判定阈值/文档示例,非主题色
  112. const src = read(f)
  113. for (const [label, re] of BANNED) {
  114. if (re.test(src)) offenders.push(`${f} 命中 ${label}`)
  115. }
  116. }
  117. assert.deepEqual(offenders, [], offenders.join('\n'))
  118. })
  119. check('B2 裸字号已归并到语义档(无 9–15u 裸 calc 字号)', () => {
  120. const offenders = []
  121. for (const f of componentFiles) {
  122. const src = read(f)
  123. for (const m of src.matchAll(/font-size:\s*calc\(([\d.]+)\s*\*\s*var\(--u/g)) {
  124. const n = Number(m[1])
  125. if (n >= 9 && n <= 15) offenders.push(`${f}: calc(${m[1]} * var(--u))`)
  126. }
  127. }
  128. assert.deepEqual(offenders, [], offenders.join('\n'))
  129. })
  130. check('B3 裸 color: #fff/#ffffff 已清退到 --text-strong', () => {
  131. const offenders = []
  132. for (const f of componentFiles) {
  133. const src = read(f)
  134. if (/color:\s*#fff(\b|\b;)/i.test(src) || /color:\s*#ffffff\b/i.test(src)) offenders.push(f)
  135. }
  136. assert.deepEqual(offenders, [], offenders.join('\n'))
  137. })
  138. console.log('\n[C] 与既有断言面的一致性')
  139. check('C1 --u 仍是 clamp 形式且下限 1px(1280px 可读性承诺)', () => {
  140. assert.match(tokens, /--u:\s*clamp\(1px,/, '--u 下限必须保住 1px')
  141. })
  142. check('C2 token 仍声明在 .cockpit-shell 下、未挂 :root', () => {
  143. assert.match(tokens, /\.cockpit-shell\s*\{/)
  144. assert.doesNotMatch(tokens, /:root\s*\{/)
  145. })
  146. check('C3 字体槽位保留批次 4 的 Web 字体注入', () => {
  147. assert.match(tokens, /--font:\s*"Noto Sans SC Subset"/)
  148. assert.match(tokens, /--num:\s*"Rajdhani Subset"/)
  149. })
  150. if (problems.length) {
  151. console.log(`\n${problems.length} 项失败`)
  152. process.exit(1)
  153. }
  154. console.log('\n批次 2 token 静态检查全部通过。')