执行 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 页面浏览器实测正常(令牌解析、无加载失败)
- 连跑两次数字完全一致(可复现)
249 lines
8.4 KiB
JavaScript
249 lines
8.4 KiB
JavaScript
#!/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`);
|