【本次核心 · 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 个 —— 历史从未跟踪且属构建产物,保持现状。
183 lines
8.1 KiB
JavaScript
183 lines
8.1 KiB
JavaScript
/* 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)');
|
||
}
|