Files
novalon-website/scripts/audit/brand-usage-scan.mjs
T
zhangxiang 441a748f32 chore(audit): UI 审计工具链增强与噪音根治
- ui-audit.mjs:路由发现扩展至 33 路由(standalone 构建产物 + erp-upgrade 系列);
  GA/gtm 请求 mock(gtag/js 空 JS + collect 204);第三方统计噪音与同源
  ERR_ABORTED(重调度成功/prefetch)降级 INFO;LCP/CLS 断言按系统负载门控
  (load >= 5 跳过并注明,防测量环境假阳性);LCP 超阈值热缓存 reload 复测
  取优(lcpCold 留存冷值);clip 探针 sr-only 白名单;onReqFail 防御性修复
- ui-audit-report.mjs(新增):多主题探针聚合 HTML 报告,含截图画廊与 INFO 分级
- hardcoded-color-audit.mjs:bg-black/50 半透明遮罩误报修正(负向前瞻)+ 白名单
- 新增 brand-usage-scan.mjs / brand-ink-migrate.mjs(品牌令牌迁移辅助)
2026-09-20 11:50:58 +08:00

61 lines
2.3 KiB
JavaScript

/**
* 品牌色 utility 用量盘点(为「文本令牌 / 底色令牌」拆分提供决策依据)
*
* 统计 src 下所有 `(?:变体前缀:)?(属性)-brand(?:-子token)?(/透明度)?` 的出现次数与文件分布。
* 用法:node scripts/audit/brand-usage-scan.mjs [--files]
*/
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)), '../..');
const EXTS = new Set(['.tsx', '.ts', '.css', '.jsx', '.js', '.mdx']);
const SKIP = new Set(['node_modules', '.next', '.git', '_archive', 'dist', 'coverage']);
function walk(dir, out = []) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) { if (!SKIP.has(e.name)) walk(p, out); }
else if (EXTS.has(path.extname(e.name))) out.push(p);
}
return out;
}
const VARIANTS = '(?:hover:|focus:|focus-visible:|active:|group-hover:|dark:|sm:|md:|lg:|xl:)*';
const PROPS = '(bg|text|border|from|to|via|fill|stroke|ring|divide|decoration|outline|shadow|placeholder|caret|accent)';
const re = new RegExp(`(?:^|[\\s\`"'])${VARIANTS}${PROPS}-(brand(?:-[a-z]+)?)(?:/[0-9]+)?(?=[\\s\`"':/]|$)`, 'g');
const byToken = new Map();
const byFile = new Map();
const byPropToken = new Map();
for (const f of walk(path.join(ROOT, 'src'))) {
let s;
try { s = fs.readFileSync(f, 'utf8'); } catch { continue; }
let m;
while ((m = re.exec(s))) {
const key = `${m[1]}-${m[2]}`;
byToken.set(key, (byToken.get(key) || 0) + 1);
byPropToken.set(m[1], (byPropToken.get(m[1]) || 0) + 1);
byFile.set(f, (byFile.get(f) || 0) + 1);
}
}
const sortDesc = a => [...a.entries()].sort((x, y) => y[1] - x[1]);
console.log('=== brand utility 用量(src 全量)===');
for (const [k, n] of sortDesc(byToken)) console.log(String(n).padStart(5), k);
const total = [...byToken.values()].reduce((s, n) => s + n, 0);
console.log('总计'.padStart(5), total);
console.log('\n=== 按属性聚合 ===');
for (const [k, n] of sortDesc(byPropToken)) console.log(String(n).padStart(5), k);
if (process.argv.includes('--files')) {
console.log('\n=== 文件分布 Top 25 ===');
for (const [f, n] of sortDesc(byFile).slice(0, 25)) {
console.log(String(n).padStart(5), path.relative(ROOT, f));
}
}