From 92bc8bb78a7faa8043337a4ab0d046ee5cd91d73 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 20:02:21 +0800 Subject: [PATCH] =?UTF-8?q?iter(g221):=20verify=20=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E6=BB=9A=E5=8A=A8=E5=AE=B9=E5=99=A8=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tools/cli/bin/ua.js | 55 +++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index 42c77cd..f9b5812 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -923,6 +923,58 @@ function checkSkinCss(pkg) { return issues; } +/*** 滚动条覆盖校验:凡是 overflow: auto|scroll 的 .ua-* 容器,都必须出现在「scrollbar-width」那条统一规则里 ***/ +function checkScrollCoverage(pkg) { + const issues = []; + const dir = path.join(ROOT, pkg.dir); + if (!fs.existsSync(dir)) return issues; + for (const name of fs.readdirSync(dir)) { + if (!name.endsWith('.css')) continue; // 只查样式表 + const css = fs.readFileSync(path.join(dir, name), 'utf8').replace(/\r\n/g, '\n').replace(/\/\*[\s\S]*?\*\//g, ''); + const rules = []; // 括号配平扫出的「选择器 + 规则体」;正则切不了嵌套(@media 里有花括号) + let i = 0; + let sel = ''; + while (i < css.length) { + if (css[i] === '{') { + const start = i + 1; + let depth = 1; + i += 1; + while (i < css.length && depth > 0) { + if (css[i] === '{') depth += 1; + else if (css[i] === '}') depth -= 1; + i += 1; + } + rules.push({ sel: sel.trim(), body: css.slice(start, i - 1) }); + sel = ''; + continue; + } + if (css[i] === '}') { sel = ''; i += 1; continue; } // 顶层多余的右括号:重置选择器 + sel += css[i]; + i += 1; + } + const containers = new Set(); // 滚得动的单类容器 + for (const rule of rules) { + if (!/overflow(-[xy])?:\s*(auto|scroll)/.test(rule.body)) continue; + for (const part of rule.sel.split(',')) { + const hit = part.trim().match(/^\.(ua-[a-z0-9-]+)$/); + if (hit) containers.add(hit[1]); + } + } + if (!containers.size) continue; + const group = rules.find((rule) => rule.body.includes('scrollbar-width')); // 细条 + 令牌拇指那条统一规则 + if (!group) { + issues.push(`${pkg.dir}/${name}: 未找到滚动条统一规则(scrollbar-width),新增滚动容器会退回系统原生粗条`); + continue; + } + const covered = new Set([...group.sel.matchAll(/\.(ua-[a-z0-9-]+)/g)].map((m) => m[1])); + for (const cls of containers) { + if (covered.has(cls)) continue; + issues.push(`${pkg.dir}/${name}: 滚动容器 .${cls} 未纳入滚动条统一规则(暗色下会露出原生亮条)`); + } + } + return issues; +} + /*** 重复关键帧校验:两份 @keyframes 的帧体完全一致 = 复制粘贴的动效,语义上只应存在一份(名字不同也算) ***/ function checkDuplicateKeyframes(pkg) { const issues = []; @@ -1379,6 +1431,7 @@ function cmdVerify() { if (pkg.type === 'skin-dom') { issues.push(...checkSkinCss(pkg)); issues.push(...checkDuplicateKeyframes(pkg)); + issues.push(...checkScrollCoverage(pkg)); issues.push(...checkTokenRefs(pkg, tokenVars)); issues.push(...checkPropsImpl(pkg, comps)); issues.push(...checkPropsDoc(pkg, comps)); @@ -1424,7 +1477,7 @@ function cmdVerify() { console.error(`${issues.length} violation(s)`); process.exit(1); } - console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌、暗色令牌齐全、tokens.json ↔ tokens.css 名字与值双源一致、@keyframes 无复制粘贴的重复帧,README 组件清单与分组同步,backlog ↔ 留痕一致(编号严格递增、无重复 id、留痕时间线单调且带 +08:00),组合配方结构完整,MCP 工具清单三处一致,视觉回归抽查清单六组件 × 五维度齐全)'); + console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌、暗色令牌齐全、tokens.json ↔ tokens.css 名字与值双源一致、@keyframes 无复制粘贴的重复帧、滚动容器全在细条规则内,README 组件清单与分组同步,backlog ↔ 留痕一致(编号严格递增、无重复 id、留痕时间线单调且带 +08:00),组合配方结构完整,MCP 工具清单三处一致,视觉回归抽查清单六组件 × 五维度齐全)'); } /*** 校验 git 提交信息是否符合 COMMITTING.md 规范(参数为 message 文件路径或直接 message 文本),违规退出码 1 ***/