feat(ds-flow): Phase 5.1-5.2 — Flutter 裸色闸 + 原型校验闸
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>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env node
|
||||
// design/prototype/tools/check-ds.mjs — 原型单源守门检查(纯 Node 零依赖)。
|
||||
// 扫 atoms.css + index.html,强制颜色/字体/圆角走 tokens.css 的 var(--token);
|
||||
// 图标走 icons.js sprite;每个原子类在 index.html 登记。违规 exit 1。
|
||||
// node design/prototype/tools/check-ds.mjs
|
||||
// 行内 `ds-allow` 豁免;SVG 内 fill/stroke 豁免。
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); // design/prototype/
|
||||
const read = (f) => fs.readFileSync(path.join(ROOT, f), 'utf8');
|
||||
|
||||
// token 定义(tokens.css 的 :root + 主题块)+ atoms 里定义的少量派生
|
||||
const definedVars = new Set();
|
||||
for (const f of ['tokens.css', 'atoms.css']) for (const m of read(f).matchAll(/--([a-z0-9-]+)\s*:/gi)) definedVars.add('--' + m[1]);
|
||||
// atoms 原子类
|
||||
const atomClasses = new Set();
|
||||
for (const m of read('atoms.css').matchAll(/\.([a-zA-Z][\w-]*)/g)) atomClasses.add(m[1]);
|
||||
// icons.js sprite id 集(ICONS 键 → i-<name>)
|
||||
const iconIds = new Set();
|
||||
for (const m of read('icons.js').matchAll(/'([a-z0-9-]+)'\s*:\s*'</g)) iconIds.add('i-' + m[1]);
|
||||
const indexSrc = read('index.html');
|
||||
|
||||
// 颜色白名单:纯白/纯黑 + 品牌 logo 固定色(与 check-l1-sync 口径一致)
|
||||
const COLOR_ALLOW = new Set(['#fff', '#ffffff', '#000', '#000000', '#b96a3d', '#faf3ed', '#f4efe8', '#9e5630', '#3d2213']);
|
||||
const COLOR_RE = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)/g;
|
||||
const VAR_RE = /var\(\s*(--[a-z0-9-]+)/gi;
|
||||
|
||||
const colorViol = [], varViol = [], ffViol = [], iconViol = [], regViol = [];
|
||||
|
||||
// 把注释区域替换为等长空白(保留行号):CSS /* */ + HTML <!-- --> —— 避免注释里
|
||||
// 的示例(如 header 注释里的 var(--token))被误判。
|
||||
function stripComments(src) {
|
||||
return src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '))
|
||||
.replace(/<!--[\s\S]*?-->/g, (m) => m.replace(/[^\n]/g, ' '));
|
||||
}
|
||||
|
||||
// ── 值扫描。strictColor=true 时严格查硬编码色(仅 atoms.css:原子必须走 token);
|
||||
// index.html 是文档登记页,其 chrome 装饰不算原子,只查未定义 token / font-family ──
|
||||
function valueScan(label, rawSrc, strictColor) {
|
||||
const src = stripComments(rawSrc);
|
||||
const lines = src.split('\n');
|
||||
let svgDepth = 0;
|
||||
lines.forEach((line, i) => {
|
||||
const depthBefore = svgDepth;
|
||||
if (!line.includes('ds-allow')) {
|
||||
if (strictColor) {
|
||||
for (const m of line.matchAll(COLOR_RE)) {
|
||||
const c = m[0];
|
||||
if (COLOR_ALLOW.has(c.toLowerCase())) continue;
|
||||
const before = line.slice(0, m.index);
|
||||
if (depthBefore > 0 || /(?:fill|stroke)\s*=\s*["']?$/.test(before)) continue; // SVG fill/stroke 豁免
|
||||
colorViol.push(`${label}:${i + 1} ${c}`);
|
||||
}
|
||||
}
|
||||
for (const m of line.matchAll(VAR_RE)) if (!definedVars.has(m[1])) varViol.push(`${label}:${i + 1} var(${m[1]}) 未定义`);
|
||||
for (const m of line.matchAll(/font-family:\s*([^;}\n]+)/g)) if (!/var\(--font/.test(m[1]) && !/inherit|monospace|sans-serif|system-ui/.test(m[1])) ffViol.push(`${label}:${i + 1} font-family 未走 var(--font*)`);
|
||||
}
|
||||
svgDepth += (line.match(/<svg/g) || []).length - (line.match(/<\/svg>/g) || []).length;
|
||||
if (svgDepth < 0) svgDepth = 0;
|
||||
});
|
||||
}
|
||||
valueScan('atoms.css', read('atoms.css'), true);
|
||||
valueScan('index.html', indexSrc, false);
|
||||
|
||||
// ── 图标必须走 sprite:index.html 的 <use href="#i-*"> id 须在 icons.js 登记 ──
|
||||
for (const m of indexSrc.matchAll(/<use[^>]*href="#(i-[\w-]+)"/g)) {
|
||||
if (!iconIds.has(m[1])) iconViol.push(`index.html <use #${m[1]}> 未在 icons.js 登记`);
|
||||
}
|
||||
|
||||
// ── 组件原子登记:atoms.css 每个 class 须在 index.html 出现(活文档完整性)──
|
||||
// 跳过明显的修饰/状态子类(-primary/-ghost 等变体随基类展示即可),只查基类原子。
|
||||
const BASE_ATOM = /^(btn|card|input|field|flabel|pill|badge|langsel|menu)/;
|
||||
for (const c of atomClasses) {
|
||||
if (!BASE_ATOM.test(c)) continue;
|
||||
const re = new RegExp(`class="[^"]*\\b${c}\\b|\\.${c}[\\s{:.,]`);
|
||||
if (!re.test(indexSrc)) regViol.push(`.${c} ← 未在 index.html 登记展示`);
|
||||
}
|
||||
|
||||
const banner = (s) => `\n${'='.repeat(60)}\n${s}\n${'='.repeat(60)}`;
|
||||
const sec = (n, arr) => { console.log(banner(`${n} 共 ${arr.length}`)); console.log(arr.length ? arr.join('\n') : '(无)✓'); };
|
||||
console.log(banner('原型单源守门检查'));
|
||||
console.log(`token: ${definedVars.size} · 原子类: ${atomClasses.size} · 图标: ${iconIds.size}`);
|
||||
sec('❶ 硬编码颜色', colorViol);
|
||||
sec('❷ 未定义 token', varViol);
|
||||
sec('❸ font-family 未走 var(--font*)', ffViol);
|
||||
sec('❹ 图标未走 sprite', iconViol);
|
||||
sec('❺ 组件原子未在 index.html 登记', regViol);
|
||||
|
||||
const fail = [colorViol, varViol, ffViol, iconViol, regViol].reduce((s, a) => s + a.length, 0);
|
||||
console.log(banner(fail ? `✗ 未通过:${fail} 处违规` : '✓ 通过:原型颜色/字体/图标走单一来源,组件已登记'));
|
||||
process.exit(fail ? 1 : 0);
|
||||
Reference in New Issue
Block a user