Files
jiu/client/tool/check_ds_code.mjs
wangjia 23a583918d feat(tools): 代码端真相源闸 check_ds_code + 外壳硬编码颜色清零
把原型 check-ds 的「单一真源」纪律延伸到 Flutter 代码:
- client/tool/check_ds_code.mjs:扫 lib/(排除.g.dart) 硬编码 Color(0x..)/Colors.具名色,
  --strict/--changed 报错挡提交,合理特例须显式 // ds-ignore: 理由
- tokens.css(单源,client+原型同步) 加 chrome 前景色 --brand-dot/--on-accent/--on-accent-faint,
  codegen 进 AppChrome;外壳 14 处硬编码(白字/品牌点/grace橙)全部改走 token
- 外壳+3 新 widget 现 0 违规;全量存量 172→158(其余屏后续逐步清)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSKEiHsvauyxYUW2itzUXX
2026-06-25 19:14:47 +08:00

80 lines
3.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// client/tool/check_ds_code.mjs
// 代码端「设计真相源」闸 —— 与原型 check-ds.mjs 对应,把单一真源纪律延伸到 Flutter 代码。
// 红线:颜色只走 context.tokens / AppChrome / AppDims(从 tokens.css codegen 单源),
// 禁硬编码 hexColor(0x..))与具名色(Colors.red 等)。合理特例须显式 `// ds-ignore: 理由` 豁免。
//
// 用法(在 client/ 下):
// node tool/check_ds_code.mjs # 全量扫描,报存量(不 fail,便于逐步清零)
// node tool/check_ds_code.mjs --strict # 全量,有违规即 exit 1
// node tool/check_ds_code.mjs --changed # 仅扫 git 改动的 dart 文件,有违规即 exit 1pre-commit 用)
//
// 豁免:在该行加 `// ds-ignore: <理由>`(理由必填,便于审计)。
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); // client/
const LIB = path.join(ROOT, 'lib');
const args = new Set(process.argv.slice(2));
const CHANGED = args.has('--changed');
const STRICT = args.has('--strict') || CHANGED;
// 违规模式:硬编码 hex 颜色;具名色(Colors.x,除 transparent=无色合理)。
const HEX = /Color\(0x[0-9A-Fa-f]{6,8}\)/;
const NAMED = /\bColors\.(?!transparent\b)[a-zA-Z]\w*/;
const IGNORE = /\/\/\s*ds-ignore/;
function listDartFiles() {
if (CHANGED) {
let out = '';
try {
out = execFileSync(
'git', ['diff', '--name-only', '--diff-filter=ACM', 'HEAD'],
{ cwd: ROOT, encoding: 'utf8' });
} catch { out = ''; }
return out.split('\n')
.map((s) => s.trim())
.filter((f) => f.endsWith('.dart') && !f.endsWith('.g.dart'))
.map((f) => path.resolve(ROOT, '..', f)) // git 路径相对仓库根
.filter((f) => f.startsWith(LIB) && fs.existsSync(f));
}
const out = [];
(function walk(d) {
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
const p = path.join(d, e.name);
if (e.isDirectory()) walk(p);
else if (e.name.endsWith('.dart') && !e.name.endsWith('.g.dart')) out.push(p);
}
})(LIB);
return out;
}
const violations = [];
for (const file of listDartFiles()) {
const lines = fs.readFileSync(file, 'utf8').split('\n');
lines.forEach((line, i) => {
if (IGNORE.test(line)) return; // 显式豁免
const m = HEX.exec(line) || NAMED.exec(line);
if (m) {
violations.push({
file: path.relative(ROOT, file),
line: i + 1,
hit: m[0],
text: line.trim().slice(0, 90),
});
}
});
}
const banner = (s) => `\n${'─'.repeat(60)}\n${s}\n${'─'.repeat(60)}`;
if (violations.length) {
console.log(banner(`✗ 硬编码颜色 ${violations.length} 处(应走 token,或显式 // ds-ignore: 理由)`));
for (const v of violations) console.log(` ${v.file}:${v.line} [${v.hit}] ${v.text}`);
} else {
console.log(banner('✓ 通过:颜色全部走 token / 已显式豁免'));
}
if (STRICT && violations.length) process.exit(1);
console.log(`\n范围:${CHANGED ? 'git 改动文件' : '全量 lib/'} 违规:${violations.length}${STRICT ? 'strict' : '(仅报告)'}`);