feat: P2 分发 + P3 CI(含三个实测发现的缺陷修复)

执行 ROADMAP 的 S1-P2(npm 分发)与 S1-P3(CI 回归)。

P3 · CI 回归流水线
- .github/workflows/regression.yml:push/PR 自动跑回归,失败阻断
  含 playwright 浏览器缓存、零依赖自检、服务就绪轮询、报告上传、
  GitHub Step Summary 输出
- tools/run-regression.mjs 重构:
  · 路径基于脚本位置(不再依赖 cwd,CI 更稳)
  · 环境自检:服务未起退出码 2、playwright 缺失退出码 2
  · 断言超时后重试一次(吸取 _collect.html 的偶发教训)
  · 超时与断言失败分开统计(timedOut 字段)
  · 失败时打印明细并 exit 1,全部通过 exit 0

P2 · npm 分发
- package.json:aurora-admin-design,dependencies 为空(零运行时依赖铁律)
  playwright 放 devDependencies(CI 专用)
- tools/build-dist.mjs:零依赖构建脚本,产出 dist/
  tokens/{tokens.css,tokens.json(DTCG),figma.json}
  components/{<slug>.css ×79, index.css 聚合, <slug>.html ×79}
  manifest.json(含 SHA 校验和) + README.md
- .npmignore / .gitignore 更新(dist/ node_modules/ 不入库)
- npm pack 实测:115.9 KB 压缩 / 566 KB 解压 / 167 文件 / 无 site|tests 泄漏

实测发现的三个缺陷(全部修复)
1. package.json 加 "type": "module" 会让 site/dev-server.js(CJS)崩溃
   —— 移除该字段,靠 .mjs 扩展名区分模块系统
2. dist 里 HTML 引用 ../tokens/colors_and_type.css 但产物是 tokens.css
   —— 路径对但文件不存在,产物是坏的。重写全部引用规则
   (含 index.css 的 @import 从 ./tokens/ 改为 ../tokens/)
3. 断言总数不确定:icon-contrast 是条件性推入(依赖渲染时机),
   导致同一页不同次运行总数不同(960 vs 961)
   —— 改为恒定输出一条(无问题=pass,有问题=skip),
   断言数从 960 稳定为 1038,连跑两次完全一致
4. 顺带修 na 与 skip 重复计算(na = total - denom,但 skip 已是同一批)
   —— 统一为 na = skip,算术自洽:pass+fail+skip = total

验证
- runner:passRate 100% | 79 页全通过 | 1003/1003 | N/A 35 | exit 0
- collector:完全同口径(total 1038 / pass 1003 / fail 0 / skip 35)
- dist:79/79 页面浏览器实测正常(令牌解析、无加载失败)
- 连跑两次数字完全一致(可复现)
This commit is contained in:
aurora-admin
2026-09-11 23:13:25 +08:00
parent f0e0d7ad1a
commit 8934ebb440
14 changed files with 796 additions and 62 deletions
+248
View File
@@ -0,0 +1,248 @@
#!/usr/bin/env node
/**
* build-dist.mjs — 产出可分发产物(npm 包 / CDN)
*
* 设计约束:
* - 零运行时依赖:只用 Node 内置能力(fs / path)
* - 不依赖 build-site.ps1(那是 Windows 专用的文档站构建)
* - 产物自包含:dist/ 里的东西拷走就能用
*
* 输入:
* - .design_library/aurora-admin/colors_and_type.css 设计令牌
* - .design_library/aurora-admin/components.css 6 核心组件样式聚合
* - .design_library/aurora-admin/css.json 结构化令牌
* - frameworks/*.css 79 个组件样式
* - frameworks/*.html 79 个演示页
*
* 输出:
* - dist/tokens/tokens.css 设计令牌(可直接 @import)
* - dist/tokens/tokens.json W3C DTCG 格式
* - dist/tokens/figma.json Figma Tokens 格式
* - dist/components/<slug>.css 单个组件样式
* - dist/components/index.css 全部组件样式聚合
* - dist/components/<slug>.html 静态演示页(可独立打开)
* - dist/manifest.json 组件清单 + 版本 + 校验和
* - dist/README.md 用法速查
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
import { createHash } from 'crypto';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const LIB = join(ROOT, '.design_library', 'aurora-admin');
const SRC = join(ROOT, 'frameworks');
const DIST = join(ROOT, 'dist');
/* ---------- 工具 ---------- */
function ensure(dir) {
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
}
function read(p) {
return readFileSync(p, 'utf8');
}
function write(p, content) {
ensure(dirname(p));
// 统一 LF,避免跨平台差异
writeFileSync(p, content.replace(/\r\n/g, '\n'));
}
function sha256(content) {
return createHash('sha256').update(content).digest('hex').slice(0, 16);
}
/* ---------- 1. 读组件清单 ---------- */
const idxPath = join(LIB, 'components', 'index.json');
if (!existsSync(idxPath)) {
console.error('[FATAL] 找不到 components/index.json');
process.exit(1);
}
const idx = JSON.parse(read(idxPath));
const components = idx.components;
const pkg = JSON.parse(read(join(ROOT, 'package.json')));
const version = pkg.version;
console.log(`[build-dist] 版本 ${version} / ${components.length} 个组件`);
/* ---------- 2. 令牌 ---------- */
const tokenCss = read(join(LIB, 'colors_and_type.css'));
// 从令牌文件解析出 name/value/comment(与 build-site.ps1 同一套正则思路)
const tokens = [];
const TOKEN_RE = /--(au-[a-z0-9-]+)\s*:\s*([^;]+);(?:\s*\/\*\s*(.*?)\s*\*\/)?/g;
let m;
while ((m = TOKEN_RE.exec(tokenCss)) !== null) {
tokens.push({ name: m[1], value: m[2].trim(), comment: (m[3] || '').trim() });
}
write(join(DIST, 'tokens', 'tokens.css'), tokenCss);
// W3C DTCG
function groupOf(name) {
if (/color|brand|text|bg|border|success|warning|error|info|mask/.test(name)) return 'color';
if (/font|type/.test(name)) return 'typography';
if (/radius/.test(name)) return 'radius';
if (/space|gap|padding/.test(name)) return 'spacing';
if (/shadow|elevation/.test(name)) return 'shadow';
if (/duration|ease|motion/.test(name)) return 'motion';
if (/size|height|width|icon/.test(name)) return 'size';
return 'misc';
}
const dtcg = {};
for (const t of tokens) {
const g = groupOf(t.name);
dtcg[g] = dtcg[g] || {};
dtcg[g][t.name] = { $value: t.value, $type: g === 'color' ? 'color' : 'dimension', $description: t.comment || undefined };
}
write(
join(DIST, 'tokens', 'tokens.json'),
JSON.stringify(
{ $schema: 'https://schemas.design-tokens.org/draft/dtcg.json', $metadata: { name: 'Aurora Admin', version }, tokens: dtcg },
null,
2
)
);
// Figma Tokens
const figma = {};
for (const t of tokens) {
const g = groupOf(t.name);
figma[t.name] = {
value: t.value,
type: g === 'color' ? 'color' : g === 'size' ? 'sizing' : g === 'spacing' ? 'spacing' : g === 'radius' ? 'borderRadius' : 'other',
description: t.comment || '',
};
}
write(join(DIST, 'tokens', 'figma.json'), JSON.stringify({ global: figma, $themes: [], $metadata: { tokenSetOrder: ['global'] } }, null, 2));
console.log(`[build-dist] 令牌 ${tokens.length} 个 → dist/tokens/`);
/* ---------- 3. 组件样式 ---------- */
const manifest = {
name: pkg.name,
version,
generated: new Date().toISOString().slice(0, 19).replace('T', ' '),
tokens: tokens.length,
components: [],
};
const aggregate = [
'/* Aurora Admin — 全部组件样式聚合',
` * 版本 ${version} · ${components.length} 个组件`,
' * 用法:在令牌之后引入本文件即可获得全部组件样式',
' */',
'',
/* 本文件位于 dist/components/,令牌在 dist/tokens/ —— 故必须是 ../tokens/ */
`@import url('../tokens/tokens.css');`,
'',
].join('\n');
const parts = [aggregate];
for (const c of components) {
const slug = c.slug;
const prefix = c.frameworksPrefix;
// 组件 CSS
const cssPath = join(SRC, `${prefix}.css`);
if (existsSync(cssPath)) {
const css = read(cssPath);
write(join(DIST, 'components', `${slug}.css`), `/* Aurora Admin · ${c.name} */\n${css}`);
parts.push(`/* ---- ${c.name} (${slug}) ---- */`, css, '');
}
// 演示页(重写引用,使其在 dist 下能独立打开)
const htmlPath = join(SRC, `${prefix}.html`);
let hasDemo = false;
if (existsSync(htmlPath)) {
let html = read(htmlPath);
/* dist 的目录结构:
dist/components/<slug>.html
dist/components/<slug>.css
dist/tokens/tokens.css
因此所有引用都要重写。漏掉文件名是最容易犯的错 —— 会产出「路径对但文件不存在」的坏产物。 */
html = html.replace(/\.\.\/\.design_library\/aurora-admin\/colors_and_type\.css/g, '../tokens/tokens.css');
html = html.replace(/\.\.\/\.design_library\/aurora-admin\/components\.css/g, './index.css');
html = html.replace(/\.\.\/\.design_library\/aurora-admin\//g, '../tokens/');
// 组件自身样式:./<Prefix>.css → ./<slug>.css
html = html.replace(new RegExp(`(\\./)?${prefix}\\.css`, 'g'), `./${slug}.css`);
write(join(DIST, 'components', `${slug}.html`), html);
hasDemo = true;
}
manifest.components.push({
slug,
name: c.name,
category: c.category,
tier: c.tier,
css: existsSync(cssPath) ? `components/${slug}.css` : null,
demo: hasDemo ? `components/${slug}.html` : null,
cssHash: existsSync(cssPath) ? sha256(read(cssPath)) : null,
});
}
write(join(DIST, 'components', 'index.css'), parts.join('\n'));
write(join(DIST, 'manifest.json'), JSON.stringify(manifest, null, 2));
const cssCount = readdirSync(join(DIST, 'components')).filter((f) => f.endsWith('.css')).length;
const htmlCount = readdirSync(join(DIST, 'components')).filter((f) => f.endsWith('.html')).length;
console.log(`[build-dist] 组件 ${components.length} 个 → dist/components/(${cssCount} css / ${htmlCount} html)`);
/* ---------- 4. 用法速查 ---------- */
write(
join(DIST, 'README.md'),
`# Aurora Admin Design System — 分发包
版本 ${version} · ${components.length} 个组件 · ${tokens.length} 个设计令牌 · 零运行时依赖
## 最快用法
\`\`\`html
<link rel="stylesheet" href="./tokens/tokens.css">
<link rel="stylesheet" href="./components/index.css">
\`\`\`
## 按需引入
\`\`\`html
<link rel="stylesheet" href="./tokens/tokens.css">
<link rel="stylesheet" href="./components/button.css"> <!-- 只要按钮 -->
\`\`\`
## 改主题
所有色值都引用 \`--au-*\` 令牌,覆盖即可全局换色:
\`\`\`css
:root { --au-color-brand: #722ED1; }
\`\`\`
## 目录
| 路径 | 内容 |
|---|---|
| \`tokens/tokens.css\` | 设计令牌(CSS 变量) |
| \`tokens/tokens.json\` | 同上(W3C DTCG 格式,供工具消费) |
| \`tokens/figma.json\` | Figma Tokens(Tokens Studio 可导入) |
| \`components/index.css\` | 全部组件样式聚合 |
| \`components/<slug>.css\` | 单个组件样式 |
| \`components/<slug>.html\` | 该组件的静态演示页(可直接打开) |
| \`manifest.json\` | 组件清单 + 校验和 |
## 组件清单
${manifest.components.map((c) => `- \`${c.slug}\` — ${c.name}`).join('\n')}
`
);
console.log('[build-dist] 完成 → dist/');
console.log(` components/manifest.json · README.md`);