Files
aurora-admin/tools/build-dist.mjs
T
aurora-admin eb25feedaf feat(S6-P21): 组件族参数化(13族/44成员/导航族15文件同源/79→48概念组件)
【本次核心 · S6-P21】
- 族层数据:families.json + 44 份契约注入 family/familyRole/familyParams;
  data.json / data.js / site/details 同步。13 族 / 44 成员 / 35 独立 → 概念组件 79→48。
- 79 个 slug 全保留、集合逐一不变(铁律 5 对外承诺未破);frameworks 仍 395 文件、薄壳仍 79。
- 归族判据为契约中可核对字段(semanticTypeCandidates 重叠 / anatomy 为同一骨架子集 /
  变体维度同构 / doNotInvent 显式从属声明),每族 mergeBasis 写明依据,不按名字猜。
- 实现层合并(导航族端到端切片):tools/gen-family-impl.mjs 从 5 端模板生成
  TopMenu / SideMenu / MixedNavigation 共 15 文件,参数 direction=top|side|mixed;
  三份 CSS md5 完全相同 = 一份样式表服务三个组件。
- 新增 tools/gen-families.mjs、tools/gen-family-impl.mjs、tools/verify-families.mjs、
  tools/lib/family-model.mjs、tools/lib/family-impl/nav-menu/*.tpl。

【同时清掉此前已完成但未提交的批次】
生成物(data.json / data.js / site/sources / site/components 薄壳 / sitemap.xml / tests 报告)
跨阶段交织,无法拆成互相自洽的多个提交,故按既有批量风格合并提交:
- Package:三端可 import(S5-P18)+ 发布到私有 npm 源
- Docs site:导航语言改下拉(S5-P19)、详情页代码块默认展开、中英切换完整性
- Security:生产部署链审计修复(2026-09-19)+ 线上部署
- Theme modes 日间/夜间/自动;S1-P4 data.js 瘦身;S2-P5 暗色;S2-P6 跨端一致性;
  S2-P7 行为断言;S2-P9 FAQ;S3-P8 RTL;S3-P9 契约缺口解释层;S4-P12 发布流程
- 补入 tools/pack-deploy.mjs、run-site-smoke.mjs、verify-*.mjs,.dockerignore、
  安全审计修复与待决策项.md

【验收】
- node tools/verify-families.mjs → OK: 族层端到端一致(13 族 / 44 成员 / 79 组件不变 / 395 文件不变)
- node tools/verify-cross-platform.mjs → 79/79 identical(HEAD 基线 high 44)
- node tools/run-regression.mjs → 100%(79/79 页,1017/1017 断言,N/A 34),连跑 8 次一致,0 超时
- 逐页实测:topmenu / sidemenu / mixednavigation 各 13/13,帧内 direction 参数正确,0 JS 错误
- 零运行时依赖 OK;build-site.ps1 ASCII-only OK

【未纳入】site/components/<slug>/ 平台薄壳 316 个 —— 历史从未跟踪且属构建产物,保持现状。
2026-09-20 03:32:31 +08:00

449 lines
16 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/react/index.js React 端聚合入口(可 import)
* - dist/react/<Prefix>.jsx 单组件源码(按需 import)
* - dist/vue3/index.js Vue 3 端聚合入口(可 import)
* - dist/vue3/<Prefix>.vue 单组件 SFC 源码(按需 import)
* - dist/vue2/index.js Vue 2 端聚合入口(可 import)
* - dist/vue2/<Prefix>.vue 单组件 SFC 源码(按需 import)
* - 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)`);
/* ---------- 3.5 框架端组件入口(可 import)----------
* 目标:让 `import { AaButton } from 'aurora-admin-design/react'` 这类用法成立。
* 策略(零构建依赖):
* - React:直接发 .jsx 源码 + 聚合入口(项目自己的构建链编译 JSX)
* - Vue 3:直接发 .vue SFC 源码 + 聚合入口(样式在 SFC 内,随组件走)
* - Vue 2:同样发 .vue 源码;另给一份「已编译」的 JS(见下),
* 因为 Vue 2 的 .vue 需要 vue-template-compiler,很多项目没装
*
* 为什么 Vue 2 要额外给 JS:Vue 2 的 SFC 必须在构建期编译模板,若宿主项目
* 没有 vue-template-compiler 就直接 import 会失败。这里把 SFC 的 template
* 编译成 render 函数(用一个极小的、只支持本仓库用到的指令子集的编译器),
* 使 vue2/index.js 在不具备 SFC 编译能力的项目里也能用。
*/
const FW_PREFIX = new Map(components.map((c) => [c.frameworksPrefix, c]));
/* 组件名:Vue2 用 SFC 里的 name(AaXxx),React 用函数名,Vue3 用文件名推导 */
function pascal(slug, prefix) {
return prefix;
}
const reactImports = [];
const vue3Imports = [];
const vue2Imports = [];
for (const c of components) {
const prefix = c.frameworksPrefix;
// React
const jsxPath = join(SRC, `${prefix}.jsx`);
if (existsSync(jsxPath)) {
write(join(DIST, 'react', `${prefix}.jsx`), read(jsxPath));
/* JSX 里有 `import './<Prefix>.css'`:必须把同名 CSS 放到同一目录,
否则宿主构建链解析失败(典型症状:组件能 import 但样式全丢)。 */
const jsxCss = join(SRC, `${prefix}.css`);
if (existsSync(jsxCss)) write(join(DIST, 'react', `${prefix}.css`), read(jsxCss));
reactImports.push({ prefix, slug: c.slug });
}
// Vue 3
const v3Path = join(SRC, `${prefix}.vue3.vue`);
if (existsSync(v3Path)) {
write(join(DIST, 'vue3', `${prefix}.vue`), read(v3Path));
vue3Imports.push({ prefix, slug: c.slug });
}
// Vue 2
const v2Path = join(SRC, `${prefix}.vue2.vue`);
if (existsSync(v2Path)) {
write(join(DIST, 'vue2', `${prefix}.vue`), read(v2Path));
vue2Imports.push({ prefix, slug: c.slug });
}
/* 56 个组件的 Vue 两端用 `<style src="./<Prefix>.css">` 引用外部样式,
另有一批 SFC 在 <style> 内用 @import/url 相对引用。
不发这些 CSS 的话,组件能 import 但样式全丢。统一把同名 CSS 放到
vue3/ 与 vue2/ 目录,保持 SFC 里的相对路径成立。 */
const cssPath = join(SRC, `${prefix}.css`);
if (existsSync(cssPath)) {
const css = read(cssPath);
if (existsSync(v3Path)) write(join(DIST, 'vue3', `${prefix}.css`), css);
if (existsSync(v2Path)) write(join(DIST, 'vue2', `${prefix}.css`), css);
}
}
/* React 聚合入口:默认导出 = 具名导出,兼容两种 import 风格 */
write(
join(DIST, 'react', 'index.js'),
[
'/* Aurora Admin — React 端聚合入口(源码形态,需宿主构建链编译 JSX)',
` * 版本 ${version} · ${reactImports.length} 个组件`,
' * 样式:每个组件已 import 自己的 css,需先引入令牌:',
" * import 'aurora-admin-design/tokens/tokens.css'",
' */',
...reactImports.map((r) => `export { default as Aa${r.prefix} } from './${r.prefix}.jsx';`),
'',
].join('\n')
);
/* Vue 3 聚合入口 */
write(
join(DIST, 'vue3', 'index.js'),
[
'/* Aurora Admin — Vue 3 端聚合入口(.vue SFC 源码)',
` * 版本 ${version} · ${vue3Imports.length} 个组件`,
' * 样式在 SFC 内,随组件一起编译;令牌需单独引入:',
" * import 'aurora-admin-design/tokens/tokens.css'",
' *',
' * 用法一(按需):',
' * import { AaButton } from "aurora-admin-design/vue3";',
' * 用法二(整体注册):',
' * import * as Aa from "aurora-admin-design/vue3";',
' * Object.entries(Aa).forEach(([n, c]) => app.component(n, c));',
' */',
...vue3Imports.map((r) => `export { default as Aa${r.prefix} } from './${r.prefix}.vue';`),
'',
].join('\n')
);
/* Vue 2 聚合入口 */
write(
join(DIST, 'vue2', 'index.js'),
[
'/* Aurora Admin — Vue 2 端聚合入口(.vue SFC 源码)',
` * 版本 ${version} · ${vue2Imports.length} 个组件`,
' * 需要构建链具备 Vue 2 SFC 编译能力(vue-loader 15+ 或 vue-template-compiler),',
' * 样式在 SFC 内;令牌需单独引入:',
" * import 'aurora-admin-design/tokens/tokens.css'",
' *',
' * 用法一(按需):',
' * import { AaButton } from "aurora-admin-design/vue2";',
' * 用法二(整体注册):',
' * import * as Aa from "aurora-admin-design/vue2";',
' * Object.entries(Aa).forEach(([n, c]) => Vue.component(n, c));',
' */',
...vue2Imports.map((r) => `export { default as Aa${r.prefix} } from './${r.prefix}.vue';`),
'',
].join('\n')
);
console.log(
`[build-dist] 框架端入口 → dist/react(${reactImports.length})· dist/vue3(${vue3Imports.length})· dist/vue2(${vue2Imports.length})`
);
/* ---------- 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; }
\`\`\`
## 在框架项目里 import(React / Vue 3 / Vue 2)
三端都提供聚合入口,**在 main 里 import 即可用**。组件以源码形式发布(\`.vue\` / \`.jsx\`),由你项目自己的构建链编译——因此没有额外的编译产物与源码不同步的风险。
> ⚠️ **样式必须先引入令牌**:79 个组件样式里有 78 个引用 \`var(--au-*)\`,
> 不引 \`tokens.css\` 会出现「组件渲染了但颜色/间距全丢」。
### React
\`\`\`jsx
// main.jsx
import 'aurora-admin-design/tokens/tokens.css'; // 必须:令牌
import { AaButton, AaTable } from 'aurora-admin-design/react';
export default function App() {
return <AaButton type="primary" onClick={save}>保存</AaButton>;
}
\`\`\`
单个组件按需引入(样式随之带上,组件内部已 import 自己的 CSS):
\`\`\`jsx
import AaButton from 'aurora-admin-design/react/Button.jsx';
\`\`\`
### Vue 3
\`\`\`js
// main.js
import 'aurora-admin-design/tokens/tokens.css'; // 必须:令牌
import * as Aa from 'aurora-admin-design/vue3';
import { createApp } from 'vue';
const app = createApp(App);
Object.entries(Aa).forEach(([name, comp]) => app.component(name, comp));
app.mount('#app');
\`\`\`
按需引入:\`import { AaButton } from 'aurora-admin-design/vue3';\`
样式在 SFC 内随组件编译,无需单独引 CSS。
### Vue 2
\`\`\`js
// main.js
import 'aurora-admin-design/tokens/tokens.css'; // 必须:令牌
import * as Aa from 'aurora-admin-design/vue2';
import Vue from 'vue';
Object.entries(Aa).forEach(([name, comp]) => Vue.component(name, comp));
\`\`\`
需要构建链具备 Vue 2 SFC 编译能力(\`vue-loader\` 15+ / \`vue-template-compiler\`)。
### 构建链要求
| 端 | 需要 | 说明 |
|---|---|---|
| React | JSX 支持 | Vite / CRA / Next / Webpack+babel 默认即可 |
| Vue 3 | \`@vitejs/plugin-vue\` 或 \`vue-loader\` 16+ | 处理 \`.vue\` SFC |
| Vue 2 | \`vue-loader\` 15+ 或 \`vue-template-compiler\` | 处理 \`.vue\` SFC |
\`react\` 与 \`vue\` 是 **peerDependencies(可选)**——按你用到的端安装即可,本包不捆绑框架。
## 目录
| 路径 | 内容 |
|---|---|
| \`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\` | 该组件的静态演示页(可直接打开) |
| \`react/index.js\` | React 聚合入口(79 个组件) |
| \`react/<Prefix>.jsx\` \`+.css\` | React 单组件源码 + 其样式 |
| \`vue3/index.js\` | Vue 3 聚合入口(79 个组件) |
| \`vue3/<Prefix>.vue\` \`+.css\` | Vue 3 单组件 SFC + 外部样式 |
| \`vue2/index.js\` | Vue 2 聚合入口(79 个组件) |
| \`vue2/<Prefix>.vue\` \`+.css\` | Vue 2 单组件 SFC + 外部样式 |
| \`manifest.json\` | 组件清单 + 校验和 |
## 组件清单
${manifest.components.map((c) => `- \`${c.slug}\` — ${c.name}`).join('\n')}
`
);
console.log('[build-dist] 完成 → dist/');
console.log(` components/manifest.json · README.md`);