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 个 —— 历史从未跟踪且属构建产物,保持现状。
This commit is contained in:
aurora-admin
2026-09-20 03:32:31 +08:00
parent 7e3d442dab
commit eb25feedaf
372 changed files with 16928 additions and 12810 deletions
+182
View File
@@ -0,0 +1,182 @@
/* Aurora Admin —— 部署打包器(.dockerignore 是唯一真源)
*
* 背景(实测事故):
* 远端 /opt/aurora-admin 之前靠手工拷贝维护,没有 git、也没有 .dockerignore,
* 于是 ① .design_library/aurora-admin/specs 等目录随镜像发布并对外可下载;
* ② 曾经修好的 nginx.conf(absolute_redirect off)被下一次拷贝覆盖丢失。
* 本脚本把「该发什么、不该发什么」固化为可复现的一步:
* - 文件集只包含 docker build 真正需要的顶层条目;
* - 排除规则**解析 .dockerignore**(含 Docker 的匹配语义),不再维护第二份清单;
* - 硬断言:规范原文/agent-reports/preview/ui_kits 绝不出现,站点运行必需文件必须出现。
*
* 用法:
* node tools/pack-deploy.mjs # 生成 dist-deploy/aurora-admin/ + manifest
* node tools/pack-deploy.mjs --tar # 额外打出 dist-deploy/aurora-admin-deploy.tar.gz
*/
'use strict';
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
const ROOT = process.cwd();
const OUT_DIR = path.join(ROOT, 'dist-deploy');
const STAGE = path.join(OUT_DIR, 'aurora-admin');
/* 部署目录必需文件:与 Dockerfile 的 COPY 对应,且必须留在 /opt 供 docker compose 使用。
注意:它们**始终**随包发布,不受 .dockerignore 影响——.dockerignore 只决定
「哪些文件进入镜像构建上下文」,而 docker-compose.yml 这类文件必须留在部署目录里
(compose 从磁盘读取它,不从构建上下文读取)。 */
const TOP_FILES = ['Dockerfile', '.dockerignore', 'nginx.conf', 'docker-compose.yml', 'sitemap.xml'];
/* 受 .dockerignore 约束的内容目录(镜像里真正要用的资源) */
const CONTENT_DIRS = ['site', 'frameworks', 'tests', '.design_library'];
/* 永不打包:版本库、依赖、本地工具与产物(与 .dockerignore 无关,属于打包器自身边界) */
const NEVER = [/^\.git(\/|$)/, /^node_modules(\/|$)/, /^\.zcode(\/|$)/, /^\.playwright-mcp(\/|$)/,
/^dist(-deploy)?(\/|$)/, /^\.gitignore$/];
/* ---------- Docker .dockerignore 语义 ----------
* 实测确认:不含 "/" 的模式(如 *.md)只匹配**构建上下文根目录**,不匹配嵌套路径;
* 故一律按「完整相对路径」匹配,其中 * 不跨 "/",** 可跨任意层(含零层)。
* 命中某目录即视为命中其下全部内容。 */
function patternToRegExp(pat) {
let re = '';
for (let i = 0; i < pat.length; i++) {
const c = pat[i];
if (c === '*') {
if (pat[i + 1] === '*') {
i++;
if (pat[i + 1] === '/') { i++; re += '(?:[^/]*/)*'; }
else re += '.*';
} else re += '[^/]*';
} else if (c === '?') re += '[^/]';
else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
}
return new RegExp('^' + re + '$');
}
function loadDockerignore() {
const file = path.join(ROOT, '.dockerignore');
if (!fs.existsSync(file)) throw new Error('.dockerignore 不存在——排除规则无法确定,拒绝打包');
const rules = [];
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const negated = line.startsWith('!');
const body = (negated ? line.slice(1) : line).replace(/^\.\//, '').replace(/\/+$/, '');
if (!body) continue;
rules.push({ negated, re: patternToRegExp(body) });
}
return rules;
}
function isIgnored(rules, rel) {
const segments = rel.split('/');
let excluded = false;
for (const rule of rules) {
let hit = rule.re.test(rel);
if (!hit) {
for (let i = 1; i < segments.length && !hit; i++) hit = rule.re.test(segments.slice(0, i).join('/'));
}
if (hit) excluded = !rule.negated;
}
return excluded;
}
/* ---------- 采集文件 ---------- */
function walk(absDir, relPrefix, out) {
for (const e of fs.readdirSync(absDir, { withFileTypes: true })) {
const rel = relPrefix ? relPrefix + '/' + e.name : e.name;
if (NEVER.some((re) => re.test(rel))) continue;
if (e.isDirectory()) walk(path.join(absDir, e.name), rel, out);
else out.push(rel);
}
}
const rules = loadDockerignore();
const candidates = [];
for (const top of CONTENT_DIRS) {
const abs = path.join(ROOT, top);
if (!fs.existsSync(abs)) throw new Error('缺少部署必需目录:' + top);
walk(abs, top, candidates);
}
for (const f of TOP_FILES) {
if (!fs.existsSync(path.join(ROOT, f))) throw new Error('缺少部署必需文件:' + f);
}
const kept = [...TOP_FILES, ...candidates.filter((f) => !isIgnored(rules, f))].sort();
const excludedCount = candidates.length - (kept.length - TOP_FILES.length);
/* ---------- 硬断言:不该发的绝不允许出现 ---------- */
const MUST_ABSENT = [
'.design_library/aurora-admin/specs/组件1.txt',
'.design_library/aurora-admin/agent-reports/phase3-component-modal.json',
'.design_library/aurora-admin/preview',
'.design_library/aurora-admin/ui_kits',
'.design_library/aurora-admin/SKILL.md',
'AGENTS.md', 'CHANGELOG.md', 'ROADMAP.md', 'TESTING.md',
'docker-compose.yml.bak-20260916', 'build-site.ps1',
];
const MUST_PRESENT = [
'site/index.html', 'site/app.js', 'site/logger.js', 'site/data.json', 'site/data.js',
'site/tokens/tokens.css', 'site/style.css', 'site/i18n.js',
'frameworks/Button.css', 'frameworks/button.html', 'frameworks/Button.vue3.vue',
'tests/index.html',
'.design_library/aurora-admin/colors_and_type.css', '.design_library/aurora-admin/components.css',
'.design_library/aurora-admin/css.json', '.design_library/aurora-admin/components/button.json',
'Dockerfile', '.dockerignore', 'nginx.conf', 'docker-compose.yml', 'sitemap.xml',
];
const problems = [];
for (const f of MUST_ABSENT) {
const bad = kept.some((k) => k === f || k.startsWith(f + '/'));
if (bad) problems.push('不该出现:' + f);
}
for (const f of MUST_PRESENT) {
if (!kept.includes(f)) problems.push('缺失必需文件:' + f);
}
const shells = kept.filter((f) => /^site\/components\/[^/]+\.html$/.test(f)).length;
if (shells !== 79) problems.push('site/components 薄壳数应为 79,实际 ' + shells);
const fw = kept.filter((f) => f.startsWith('frameworks/')).length;
if (fw !== 395) problems.push('frameworks 实现文件应为 395,实际 ' + fw);
if (problems.length) {
console.error('[pack-deploy] FAIL');
for (const p of problems) console.error(' - ' + p);
process.exit(1);
}
/* ---------- 落盘 ---------- */
fs.rmSync(STAGE, { recursive: true, force: true });
let bytes = 0;
for (const rel of kept) {
const dst = path.join(STAGE, rel);
fs.mkdirSync(path.dirname(dst), { recursive: true });
const buf = fs.readFileSync(path.join(ROOT, rel));
fs.writeFileSync(dst, buf);
bytes += buf.length;
}
const manifest = {
generatedAt: new Date().toISOString(),
files: kept.length,
bytes,
components: shells,
frameworks: fw,
excludedByDockerignore: excludedCount,
list: kept,
};
fs.writeFileSync(path.join(OUT_DIR, 'deploy-manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
console.log('[pack-deploy] OK');
console.log(' staging : ' + STAGE);
console.log(' files : ' + kept.length + '(内容文件 ' + candidates.length + ',按 .dockerignore 排除 ' + excludedCount + ';部署配置文件 ' + TOP_FILES.length + ' 个始终保留)');
console.log(' bytes : ' + (bytes / 1024).toFixed(1) + ' KB');
console.log(' components : ' + shells + ' 个薄壳 / frameworks ' + fw + ' 个实现文件');
console.log(' 断言 : 排除项 ' + MUST_ABSENT.length + ' 条全部未出现;必需项 ' + MUST_PRESENT.length + ' 条全部存在');
if (process.argv.includes('--tar')) {
/* 用相对路径调用 tar:bsdtar(Windows 自带)对含中文的绝对路径 + -C 组合会报 status 2 */
const tgzRel = path.join('dist-deploy', 'aurora-admin-deploy.tar.gz');
const tgz = path.join(ROOT, tgzRel);
fs.rmSync(tgz, { force: true });
execFileSync('tar', ['-czf', tgzRel, '-C', 'dist-deploy', 'aurora-admin'], { cwd: ROOT, stdio: 'inherit' });
console.log(' tarball : ' + tgz + '(' + (fs.statSync(tgz).size / 1024).toFixed(1) + ' KB)');
}