| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- /**
- * 票 15(批次 2)专项断言面:token 静态检查。
- *
- * 运行:node .scratch/lanhu-cockpit-asset-replacement/verify/batch2-token-check.mjs
- * 零依赖 Node 断言(同 T01 立场:仓库无测试运行器,不为此引入新框架)。
- */
- import assert from 'node:assert/strict'
- import { readFileSync, readdirSync, statSync } from 'node:fs'
- import { fileURLToPath } from 'node:url'
- import { dirname, join, relative } from 'node:path'
- const here = dirname(fileURLToPath(import.meta.url))
- const root = join(here, '..', '..', '..')
- const cockpitDir = join(root, 'src', 'modules', 'cockpit')
- const files = []
- ;(function walk(dir) {
- for (const e of readdirSync(dir)) {
- const p = join(dir, e)
- if (statSync(p).isDirectory()) walk(p)
- else if (/\.(vue|js|css)$/.test(e)) files.push(relative(root, p).split('\\').join('/'))
- }
- })(cockpitDir)
- const read = (p) => readFileSync(join(root, p), 'utf8')
- const problems = []
- function check(name, fn) {
- try {
- fn()
- console.log(` PASS ${name}`)
- } catch (error) {
- problems.push(`${name}: ${error.message}`)
- console.log(` FAIL ${name}\n ${error.message}`)
- }
- }
- const tokens = read('src/modules/cockpit/styles/tokens.css')
- const componentFiles = files.filter((f) => f !== 'src/modules/cockpit/styles/tokens.css')
- console.log('\n[A] token 声明唯一性(tokens.css 是唯一来源)')
- check('A1 语义色/主色/文字 token 只在 tokens.css 声明', () => {
- const offenders = []
- for (const f of componentFiles) {
- const src = read(f)
- 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']) {
- if (new RegExp(`${name}\\s*:`).test(src)) offenders.push(`${f} 重复声明 ${name}`)
- }
- }
- assert.deepEqual(offenders, [], offenders.join('\n'))
- })
- check('A2 批次 2 新值已在 tokens.css 落地(设计稿 v3/v4)', () => {
- for (const [name, value] of [
- ['--u', 'clamp(1px, 0.0694vw, 2px)'],
- ['--bg-0', '#010514'],
- ['--cyan', '#01abea'],
- ['--cyan-hi', '#00b8ff'],
- ['--cyan-d', '#009de9'],
- ['--lv1', '#ff3b52'],
- ['--lv2', '#ffb020'],
- ['--lv3', '#01abea'],
- ['--lv4', '#27d17a'],
- ['--neutral', '#5680b0'],
- ['--text', '#d7ecff'],
- ['--text-2', '#88b7e8'],
- ['--text-strong', '#ffffff'],
- ['--muted', '#5680b0'],
- ['--fs-kpi', 'calc(20 * var(--u))'],
- ]) {
- const re = new RegExp(`${name}\\s*:\\s*${value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
- assert.match(tokens, re, `${name} 应为设计稿值 ${value}`)
- }
- })
- check('A3 新增 token 都有消费点(规格 03:无消费点不提交)', () => {
- const required = {
- '--cyan-hi': 1,
- '--text-strong': 1,
- '--panel-mid': 1,
- '--fs-kpi': 1,
- '--point-offline': 1,
- '--point-empty': 1,
- '--point-unknown': 1,
- '--point-unassociated': 1,
- '--industry-gas': 1,
- '--industry-water': 1,
- '--industry-drainage': 1,
- '--industry-manhole': 1,
- }
- for (const [name, min] of Object.entries(required)) {
- const uses = componentFiles.filter((f) => read(f).includes(`var(${name}`)).length
- assert.ok(uses >= min, `${name} 无消费点(出现于 ${uses} 个文件)`)
- }
- })
- check('A4 --fs-metric 未提交(设计稿 25px 档无消费点,按规格 03 不设)', () => {
- assert.doesNotMatch(tokens, /--fs-metric/, '--fs-metric 不应声明')
- })
- console.log('\n[B] 组件内裸主题字面量清退')
- const BANNED = [
- ['旧主色 #4fd8ff', /#4fd8ff\b/i],
- ['旧主色深 #1b93cf', /#1b93cf\b/i],
- ['旧报警 #ff4d5e', /#ff4d5e\b/i],
- ['旧预警 #ffa63d', /#ffa63d\b/i],
- ['旧正常 #35e39a', /#35e39a\b/i],
- ['旧中性 #6d92ae', /#6d92ae\b/i],
- ['旧文字 #e6f5ff', /#e6f5ff\b/i],
- ['旧文字次 #a8cbe2', /#a8cbe2\b/i],
- ['旧标题亮 #eaf9ff', /#eaf9ff\b/i],
- ['旧面板顶 rgba(12,48,86', /rgba\(12,\s*48,\s*86/],
- ['旧面板底 rgba(3,14,28', /rgba\(3,\s*14,\s*28/],
- ['旧面板线 rgba(58,170,226', /rgba\(58,\s*170,\s*226/],
- ['旧高亮线 rgba(96,220,255', /rgba\(96,\s*220,\s*255/],
- ]
- check('B1 旧 token 值字面量在面板域清零', () => {
- const offenders = []
- for (const f of componentFiles) {
- if (/cesiumEngine\.js$/.test(f)) continue // 地图域归批次 3
- if (/domain\/fog\.js$/.test(f)) continue // 雾效域:色值是 HSV 判定阈值/文档示例,非主题色
- const src = read(f)
- for (const [label, re] of BANNED) {
- if (re.test(src)) offenders.push(`${f} 命中 ${label}`)
- }
- }
- assert.deepEqual(offenders, [], offenders.join('\n'))
- })
- check('B2 裸字号已归并到语义档(无 9–15u 裸 calc 字号)', () => {
- const offenders = []
- for (const f of componentFiles) {
- const src = read(f)
- for (const m of src.matchAll(/font-size:\s*calc\(([\d.]+)\s*\*\s*var\(--u/g)) {
- const n = Number(m[1])
- if (n >= 9 && n <= 15) offenders.push(`${f}: calc(${m[1]} * var(--u))`)
- }
- }
- assert.deepEqual(offenders, [], offenders.join('\n'))
- })
- check('B3 裸 color: #fff/#ffffff 已清退到 --text-strong', () => {
- const offenders = []
- for (const f of componentFiles) {
- const src = read(f)
- if (/color:\s*#fff(\b|\b;)/i.test(src) || /color:\s*#ffffff\b/i.test(src)) offenders.push(f)
- }
- assert.deepEqual(offenders, [], offenders.join('\n'))
- })
- console.log('\n[C] 与既有断言面的一致性')
- check('C1 --u 仍是 clamp 形式且下限 1px(1280px 可读性承诺)', () => {
- assert.match(tokens, /--u:\s*clamp\(1px,/, '--u 下限必须保住 1px')
- })
- check('C2 token 仍声明在 .cockpit-shell 下、未挂 :root', () => {
- assert.match(tokens, /\.cockpit-shell\s*\{/)
- assert.doesNotMatch(tokens, /:root\s*\{/)
- })
- check('C3 字体槽位保留批次 4 的 Web 字体注入', () => {
- assert.match(tokens, /--font:\s*"Noto Sans SC Subset"/)
- assert.match(tokens, /--num:\s*"Rajdhani Subset"/)
- })
- if (problems.length) {
- console.log(`\n${problems.length} 项失败`)
- process.exit(1)
- }
- console.log('\n批次 2 token 静态检查全部通过。')
|