Files
aurora-admin/tools/build-dist.mjs
T
aurora-admin 8934ebb440 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 页面浏览器实测正常(令牌解析、无加载失败)
- 连跑两次数字完全一致(可复现)
2026-09-11 23:13:25 +08:00

249 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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`);