#!/usr/bin/env node /** * verify-brand-mark.mjs — 项目品牌图标静态门禁。 * 组件 Icon registry 是另一套系统,本脚本只检查 favicon 与文档站品牌标记。 */ import fs from 'node:fs'; import path from 'node:path'; import { collectPublishSet } from './lib/publish-set.mjs'; import { generatedData, readBrandSpec, renderFaviconSvg, renderMonoSvg, validateBrandSpec, BRAND_ASSET_REL, BRAND_DATA_REL, BRAND_MONO_REL, BRAND_SPEC_REL, } from './lib/brand-mark.mjs'; const ROOT = process.cwd(); const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/^\uFEFF/, ''); const exists = (rel) => fs.existsSync(path.join(ROOT, rel)); let pass = 0; const failures = []; function check(ok, label, detail = '') { if (ok) { pass++; console.log(` PASS ${label}${detail ? ` — ${detail}` : ''}`); } else { failures.push(`${label}${detail ? ` — ${detail}` : ''}`); console.log(` FAIL ${label}${detail ? ` — ${detail}` : ''}`); } } console.log('Kole UI 项目品牌图标门禁'); check(exists(BRAND_SPEC_REL), 'B1 品牌规格存在', BRAND_SPEC_REL); check(exists(BRAND_ASSET_REL), 'B2 favicon SVG 存在', BRAND_ASSET_REL); check(exists(BRAND_MONO_REL), 'B3 单色 SVG 存在', BRAND_MONO_REL); check(exists(BRAND_DATA_REL), 'B4 生成数据存在', BRAND_DATA_REL); const spec = readBrandSpec(ROOT); const specErrors = validateBrandSpec(spec); check(specErrors.length === 0, 'B5 品牌规格结构有效', specErrors.join(';')); const expectedFavicon = renderFaviconSvg(spec) + '\n'; const expectedMono = renderMonoSvg(spec) + '\n'; const expectedData = JSON.stringify(generatedData(spec), null, 2) + '\n'; const favicon = exists(BRAND_ASSET_REL) ? read(BRAND_ASSET_REL) : ''; const mono = exists(BRAND_MONO_REL) ? read(BRAND_MONO_REL) : ''; const data = exists(BRAND_DATA_REL) ? read(BRAND_DATA_REL) : ''; check(favicon === expectedFavicon, 'B6 favicon 是规格生成物'); check(mono === expectedMono, 'B7 单色标记是规格生成物'); check(data === expectedData, 'B8 generated JSON 是规格生成物'); check(!/ p && typeof p === 'object' && p.evenodd); const subpathCount = (d) => (String(d).match(/M/gi) || []).length; check( negativePaths.length === 0 || (favicon.includes('fill-rule="evenodd"') && mono.includes('fill-rule="evenodd"')), 'B9b 负空间路径带 fill-rule="evenodd"(否则 K 会被填实)', negativePaths.length ? `${negativePaths.length} 条负空间路径 · 子路径 ${negativePaths.map((p) => subpathCount(p.d)).join('+')}` : '无负空间路径' ); check((favicon.match(/= 2, 'B20 PC theme-color 保留亮暗两态'); const mobileIndex = JSON.parse(read('.design_library/kole-ui-mobile/components/index.json')); const mobilePages = [ ...['index', 'guide', 'design', 'faq', 'changelog', 'platform'].map((name) => `site/m/${name}.html`), ...mobileIndex.components.map((component) => `site/m/component/${component.slug}.html`), ]; const pageProblems = []; for (const rel of mobilePages) { if (!exists(rel)) { pageProblems.push(`${rel}: 文件缺失`); continue; } const html = read(rel); const head = (html.match(/]*>([\s\S]*?)<\/head>/i) || [])[1] || ''; const icons = [...head.matchAll(/]*rel="icon"[^>]*>/g)]; const href = icons.length === 1 ? (icons[0][0].match(/\bhref="([^"]+)"/) || [])[1] : ''; if (!href || path.posix.normalize(path.posix.join(path.posix.dirname(rel), href)) !== BRAND_ASSET_REL) { pageProblems.push(`${rel}: favicon 未指向统一资产`); } const header = (html.match(/
([\s\S]*?)<\/header>/) || [])[1] || ''; if (!header.includes('')) { pageProblems.push(`${rel}: 顶栏标记或装饰性语义缺失`); } if (header.includes('M7 5.5V18.5') || (head.match(/name="theme-color"/g) || []).length !== 2) { pageProblems.push(`${rel}: 旧几何残留或 theme-color 缺失`); } } check(pageProblems.length === 0, 'B21 移动端全部生成页使用统一品牌资产', pageProblems.slice(0, 5).join(';') || `${mobilePages.length} 页`); const pcStyle = read('site/style.css'); const pcMask = (pcStyle.match(/\.logo-mark\[data-brand-mark="mono"\]::before\s*\{([^}]+)\}/) || [])[1] || ''; const mobileMask = (mobileStyle.match(/\.m-logo-mark\[data-brand-mark="mono"\]::before\s*\{([^}]+)\}/) || [])[1] || ''; check(pcMask.includes('background: currentColor') && pcMask.includes("url('assets/kole-mark-mono.svg')") && mobileMask.includes('background: currentColor') && mobileMask.includes("url('../assets/kole-mark-mono.svg')"), 'B22 两端 mask 路径同源且继承 currentColor'); const published = new Set(collectPublishSet(ROOT).kept); check([BRAND_ASSET_REL, BRAND_MONO_REL, BRAND_DATA_REL].every((rel) => published.has(rel)), 'B23 发布集包含全部品牌产物'); const scripts = JSON.parse(read('package.json')).scripts; check(scripts['build:brand'] === 'node tools/build-brand-mark.mjs' && scripts['verify:brand'] === 'node tools/verify-brand-mark.mjs' && scripts.build.startsWith('node tools/build-brand-mark.mjs && '), 'B24 品牌构建与门禁入口完整'); if (failures.length) { console.error(`\n[FAIL] ${failures.length} 条品牌图标断言失败(通过 ${pass} 条)`); failures.forEach((failure) => console.error(' - ' + failure)); process.exit(1); } console.log(`\n[OK] 品牌图标门禁全部通过(${pass} 条断言)`);