208d0cda12
真相源整固: - design/index.html 登记簿:token 色板(var() 实时渲染)/ 组件登记卡 / 图标与跨端映射 / guidelines / ui_kits 单一入口,check-ds 强制登记 - design/icons.js 图标单源(Lucide 线条)+ icon-map.json 跨端映射表 (web/iOS/Android 三列,iOS 12 个 SF Symbol 全部收编登记) - components/core/Icon.jsx 单源图标渲染器;MicBar/RecognitionOverlay 接入 - tokens 新增 --brand-wechat(-press)(微信官方绿)与 --overlay-wave - fonts.css 弃 Google Fonts CDN,Outfit 可变字体自托管 (assets/outfit-latin.woff2,40KB;中国网络下 CDN 不可用会卡字体) - design/serve.mjs 零依赖预览服务器(登记簿与原型评审入口) codegen 强化(export-tokens.mjs): - 产物直写消费位置:ios/dudu/Shared/DuduTheme.swift、 android/.../design/DuduTheme.kt——删除 design-pipeline/generated/ 中间拷贝层(此前 app 内是手工拷贝,--check 守不到真实消费文件已漂移) - 新增 web/tokens.css 产物(custom props 透传 + Outfit data URI 内嵌) - tokens 文件遍历排序,产物确定性 存量收编: - desktop:三个窗口内联 SVG → Icon 组件;tray 裸 rgba → token+opacity - Android:LoginScreen 微信绿常量 → DuduPalette.brandWechat(Press), Color.White → textOnAccent - 官网 web/index.html:接入 tokens.css,34 处硬编码 hex 全部 var() 化; 深色板块(metrics/cta/footer)改挂 data-theme="dark" 复用 dark 令牌; 移除 Google Fonts CDN - design 包内 8 处裸色:微信绿 token 化、装饰色 ds-ignore 白名单、 同值色改 var() 引用 防漂移闸(零依赖 Node): - design-pipeline/check-ds.mjs:真相源自检 7 道(dark⊆light / var 链 / 裸色∈tokens 值集 / 组件登记 / icons 唯一 / icon-map 双向同集 / 禁 emoji) - design-pipeline/check-code.mjs:代码侧单源(desktop 禁字面色+内联 path / iOS 禁字面色+SF Symbol 同集 / Android 禁 Color(0x / web 禁字面色), ds-ignore 行级豁免制 - .githooks/pre-commit(启用:git config core.hooksPath .githooks) - CI design job 扩展为三道闸全绿 - 反向验证通过:注入裸色/孤儿 token/未登记组件/未登记 symbol 均被拦截 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
4.9 KiB
JavaScript
106 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* check-code.mjs — 代码侧颜色/图标单源闸(分端扫描,零依赖,Node >= 18)
|
||
*
|
||
* 各端规则(违规 exit 1;行内 `ds-ignore: 理由` 豁免):
|
||
* desktop/src(jsx/css/js):禁字面色(hex/rgb/hsl),须 var(--*);
|
||
* 禁内联 <path>(图标须经 @dudu/design 的 Icon/icons.js)
|
||
* ios(swift) :禁 Color(red:/UIColor(/Color(0x/#colorLiteral,须 DuduTheme;
|
||
* systemName 用的 SF Symbol 须 ∈ design/icon-map.json 的 ios 列
|
||
* android(kt) :禁 Color(0x,须 DuduTheme(生成文件本身豁免)
|
||
* web(html/css) :禁字面色(令牌由 GENERATED tokens.css 提供,页面只允许 var(--*))
|
||
*
|
||
* 用法:node design-pipeline/check-code.mjs
|
||
*/
|
||
|
||
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
|
||
import { resolve, dirname, join, relative, basename } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const __dir = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = resolve(__dir, '..');
|
||
const errors = [];
|
||
const err = (file, line, msg) => errors.push(`${relative(ROOT, file)}:${line} ${msg}`);
|
||
|
||
function walk(dir, exts, out = []) {
|
||
if (!existsSync(dir)) return out;
|
||
for (const name of readdirSync(dir)) {
|
||
if (name === 'node_modules' || name === 'dist' || name === 'target' || name.startsWith('.')) continue;
|
||
const p = join(dir, name);
|
||
if (statSync(p).isDirectory()) walk(p, exts, out);
|
||
else if (exts.some((e) => name.endsWith(e))) out.push(p);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const COLOR_RE = /#[0-9a-fA-F]{3,8}\b|(?:rgba?|hsla?)\(/g;
|
||
|
||
function scanColors(files, label) {
|
||
for (const f of files) {
|
||
readFileSync(f, 'utf8').split('\n').forEach((line, i) => {
|
||
if (line.includes('ds-ignore:')) return;
|
||
const stripped = line.replace(/var\(--[\w-]+\)/g, ''); // var() 里不含字面色,先剥掉便于报错定位
|
||
const m = stripped.match(COLOR_RE);
|
||
if (m) err(f, i + 1, `${label} 字面色 ${m[0]}(改用设计令牌,或加 ds-ignore: 理由)`);
|
||
});
|
||
}
|
||
}
|
||
|
||
// ─── desktop:字面色 + 内联 <path> ───────────────────────────────────────────
|
||
|
||
const desktopFiles = walk(join(ROOT, 'desktop/src'), ['.jsx', '.js', '.css', '.html']);
|
||
scanColors(desktopFiles, 'desktop');
|
||
for (const f of desktopFiles) {
|
||
readFileSync(f, 'utf8').split('\n').forEach((line, i) => {
|
||
if (line.includes('ds-ignore:')) return;
|
||
if (/<path[\s>]/.test(line))
|
||
err(f, i + 1, 'desktop 内联 <path>(图标须登记 design/icons.js 并经 Icon 组件渲染)');
|
||
});
|
||
}
|
||
|
||
// ─── iOS:字面色 + SF Symbol 同集 ────────────────────────────────────────────
|
||
|
||
const iconMap = JSON.parse(readFileSync(join(ROOT, 'design/icon-map.json'), 'utf8'));
|
||
const iosSymbols = new Set(
|
||
Object.entries(iconMap).filter(([k]) => k !== '$schema').map(([, v]) => v.ios).filter(Boolean)
|
||
);
|
||
|
||
for (const f of walk(join(ROOT, 'ios'), ['.swift'])) {
|
||
const isGenerated = basename(f) === 'DuduTheme.swift';
|
||
readFileSync(f, 'utf8').split('\n').forEach((line, i) => {
|
||
if (line.includes('ds-ignore:')) return;
|
||
if (!isGenerated && /Color\(red:|UIColor\(red:|Color\(\s*0x|#colorLiteral/.test(line))
|
||
err(f, i + 1, 'iOS 字面色(改用 DuduTheme.*,或加 ds-ignore: 理由)');
|
||
for (const m of line.matchAll(/systemName:\s*"([^"]+)"/g)) {
|
||
if (!iosSymbols.has(m[1]))
|
||
err(f, i + 1, `iOS SF Symbol "${m[1]}" 未登记 design/icon-map.json(ios 列)`);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ─── Android:字面色 ─────────────────────────────────────────────────────────
|
||
|
||
for (const f of walk(join(ROOT, 'android/app/src'), ['.kt'])) {
|
||
if (basename(f) === 'DuduTheme.kt') continue; // codegen 产物
|
||
readFileSync(f, 'utf8').split('\n').forEach((line, i) => {
|
||
if (line.includes('ds-ignore:')) return;
|
||
if (/Color\(\s*0x/.test(line))
|
||
err(f, i + 1, 'Android 字面色(改用 DuduTheme / DuduPalette,或加 ds-ignore: 理由)');
|
||
});
|
||
}
|
||
|
||
// ─── web:字面色(tokens.css 为 GENERATED 豁免)──────────────────────────────
|
||
|
||
scanColors(
|
||
walk(join(ROOT, 'web'), ['.html', '.css']).filter((f) => basename(f) !== 'tokens.css'),
|
||
'web'
|
||
);
|
||
|
||
// ─── 汇总 ────────────────────────────────────────────────────────────────────
|
||
|
||
if (errors.length) {
|
||
console.error(`❌ check-code:${errors.length} 处违规\n` + errors.map((e) => ' ' + e).join('\n'));
|
||
process.exit(1);
|
||
}
|
||
console.log('✅ check-code:desktop / iOS / Android / web 单源全绿');
|