14a9836882
5.1 client/tool/check_ds_code.mjs(照 jiu):禁 Flutter 业务代码裸 Color(0x)/ 具名 Colors.x;豁免 token 生成层(pangolin_tokens.gen.dart)+ 实现层 (pangolin_theme.dart);--changed(pre-commit)/--strict(CI)/全量三模式; 剥行尾注释防误报。现状 --strict 通过(0 违规,UI 层零裸色)。 5.2 design/prototype/tools/check-ds.mjs:原型单源守门,5 道——硬编码色(仅严格 扫 atoms.css)/未定义 token/font-family 走 var(--font*)/图标走 icons.js sprite/ 组件原子在 index.html 登记;剥 CSS+HTML 注释防误报,SVG fill/stroke 豁免。 现状通过(serve.mjs 改动时自动跑)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
93 lines
4.0 KiB
JavaScript
93 lines
4.0 KiB
JavaScript
// client/tool/check_ds_code.mjs
|
|
// Flutter 代码端「设计真相源」闸 —— 与原型 check-ds.mjs 对应,把颜色单源纪律延伸到
|
|
// Flutter 业务代码。红线:颜色只走语义 token(PangolinScheme via context / PangolinColors
|
|
// / PangolinShadow 等,均从 design/prototype/tokens.css codegen 单源),禁硬编码 hex
|
|
// (Color(0x..))与具名 Material 色(Colors.red 等)。合理特例须显式 `// ds-ignore: 理由`。
|
|
//
|
|
// 用法(在 client/ 下):
|
|
// node tool/check_ds_code.mjs # 全量扫描,仅报告(不 fail)
|
|
// node tool/check_ds_code.mjs --strict # 全量,有违规即 exit 1(CI)
|
|
// node tool/check_ds_code.mjs --changed # 仅扫 git 改动的 dart,有违规即 exit 1(pre-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 颜色;具名 Material 色(Colors.x,transparent 除外=无色合理)。
|
|
// 注:PangolinColors.x 不会命中(\bColors 前有词字符 n,无词边界)。
|
|
const HEX = /Color\(0x[0-9A-Fa-f]{6,8}\)/;
|
|
const NAMED = /\bColors\.(?!transparent\b)[a-zA-Z]\w*/;
|
|
const IGNORE = /\/\/\s*ds-ignore/;
|
|
|
|
// 文件级豁免:token 生成层 + 主题实现层——它们**定义** token 的原始色值(合法用
|
|
// Color(0x..)),是颜色单源的落地位,不该被自己的闸拦。
|
|
const EXEMPT_FILES = [
|
|
'lib/pangolin_tokens.gen.dart', // codegen 产物(色阶原始 hex 定义位)
|
|
'lib/pangolin_theme.dart', // 实现层(PangolinScheme 语义色映射)
|
|
];
|
|
|
|
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('.gen.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('.gen.dart') && !e.name.endsWith('.g.dart')) out.push(p);
|
|
}
|
|
})(LIB);
|
|
return out;
|
|
}
|
|
|
|
const isExempt = (f) => EXEMPT_FILES.some((e) => f.replaceAll(path.sep, '/').endsWith(e));
|
|
|
|
const violations = [];
|
|
for (const file of listDartFiles()) {
|
|
if (isExempt(file)) continue;
|
|
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
lines.forEach((line, i) => {
|
|
if (IGNORE.test(line)) return; // 显式豁免
|
|
// 只扫代码部分:剥掉行尾 `//` 注释(注释里提及 Colors.x 作说明不算违规)。
|
|
const code = line.split('//')[0];
|
|
const m = HEX.exec(code) || NAMED.exec(code);
|
|
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(`✗ Flutter 硬编码颜色 ${violations.length} 处(应走 token,或显式 // ds-ignore: 理由)`));
|
|
for (const v of violations) console.log(` ${v.file}:${v.line} [${v.hit}] ${v.text}`);
|
|
} else {
|
|
console.log(banner('✓ 通过:Flutter 颜色全部走 token / 已显式豁免'));
|
|
}
|
|
|
|
if (STRICT && violations.length) process.exit(1);
|
|
console.log(`\n范围:${CHANGED ? 'git 改动文件' : '全量 lib/'} 违规:${violations.length}${STRICT ? '(strict)' : '(仅报告)'}`);
|