/** * 票 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 静态检查全部通过。')