Files
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

119 lines
6.0 KiB
JavaScript
Raw Permalink 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
/**
* release.mjs — S4-P12 版本发布流程(SemVer + CHANGELOG 驱动)
*
* 只做可逆的本地准备,不自动 push/tag/npm publish 到远端:
* 决策冻结(本轮拍板):URL=GitHub Pages 默认域名(SITE_URL_BASE 配置化,占位禁正式发布);npm=本轮不公开发布,只做 pack 内容检查;WCAG=基线支持,不宣称正式 AA;跨端=历史差异告警、增量阻断(见 regression.yml);Card=整卡不可点击、无整卡 hover;ThemeSwitcher=6 预设色升契约,持久化键按实现现状;
* 1) 从 CHANGELOG.md 解析最新 `## [x.y.z] - date` 段(Unreleased 不算版本)
* 2) 校验 package.json 的 version 与之*一致*(不一致则报错退出,不自动改)
* 3) 校验 CHANGELOG 该版本段非空;校验 dist 可构建(tools/build-dist.mjs --dry-run 若支持)
* 4) 输出发布检查清单 + 建议的 tag/push 命令(由人执行)
*
* 另支持 --bump <major|minor|patch> --date YYYY-MM-DD:把 [Unreleased] 提升为新版本段
*(改 CHANGELOG.md 与 package.json,需人复核后提交)。
*
* 用法:
* node tools/release.mjs # 检查当前版本是否可发布
* node tools/release.mjs --bump minor --date 2026-09-14
*/
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const arg = (k) => {
const i = process.argv.indexOf(k);
return i > -1 ? process.argv[i + 1] : null;
};
const fail = (msg) => { console.error('FAIL: ' + msg); process.exit(1); };
const ok = (msg) => console.log('OK: ' + msg);
function parseChangelog() {
const src = readFileSync(join(ROOT, 'CHANGELOG.md'), 'utf8');
const vers = [...src.matchAll(/^## \[([0-9]+\.[0-9]+\.[0-9]+)\](?:\s*-\s*(\S+))?/gm)];
const hasUnreleased = /^## \[Unreleased\]/m.test(src);
const unreleasedBody = hasUnreleased
? (src.split(/^## \[Unreleased\]/m)[1] || '').split(/^## \[/m)[0].trim()
: '';
return { vers: vers.map((m) => ({ v: m[1], date: m[2] || '' })), hasUnreleased, unreleasedBody };
}
function bump(v, kind) {
const [a, b, c] = v.split('.').map(Number);
if (kind === 'major') return `${a + 1}.0.0`;
if (kind === 'minor') return `${a}.${b + 1}.0`;
if (kind === 'patch') return `${a}.${b}.${c + 1}`;
fail('未知 --bump 类型(major|minor|patch): ' + kind);
return v;
}
/* ---------- --bump 模式 ---------- */
const bumpKind = arg('--bump');
if (bumpKind) {
const date = arg('--date') || new Date().toISOString().slice(0, 10);
const pkgPath = join(ROOT, 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
const { hasUnreleased, unreleasedBody } = parseChangelog();
if (!hasUnreleased) fail('CHANGELOG.md 无 [Unreleased] 段,无内容可提升');
if (unreleasedBody.length < 10) fail('[Unreleased] 段为空或过短,先写变更条目再提升版本');
const next = bump(pkg.version, bumpKind);
let cl = readFileSync(join(ROOT, 'CHANGELOG.md'), 'utf8');
cl = cl.replace(/^## \[Unreleased\]/m, `## [Unreleased]\n\n## [${next}] - ${date}`);
writeFileSync(join(ROOT, 'CHANGELOG.md'), cl);
pkg.version = next;
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
console.log(`bumped: ${next} (${date})`);
console.log('请复核 CHANGELOG.md 与 package.json 后提交:');
console.log(` git add CHANGELOG.md package.json && git commit -m "chore(release): v${next}"`);
process.exit(0);
}
/* ---------- 检查模式 ---------- */
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
const { vers, hasUnreleased, unreleasedBody } = parseChangelog();
if (!vers.length) fail('CHANGELOG.md 无任何版本段');
const latest = vers[0];
console.log(`package.json: ${pkg.version} / CHANGELOG 最新: [${latest.v}] ${latest.date}`);
if (pkg.version !== latest.v) fail(`版本不一致:package.json=${pkg.version} vs CHANGELOG=[${latest.v}](用 --bump 提升,不要手改一边)`);
ok('版本一致');
// Unreleased 有内容 = 还有未发布变更,提示而非报错
if (hasUnreleased && unreleasedBody.length > 10) {
console.log(`NOTE: [Unreleased] 段有 ${unreleasedBody.length} 字符未发布变更,发版前确认是否纳入本次`);
} else {
ok('Unreleased 为空,无遗留变更');
}
// dist 构建检查(只验证脚本存在且可跑;产物不入库)
try {
const { execSync } = await import('child_process');
execSync('node tools/build-dist.mjs --help', { cwd: ROOT, stdio: 'pipe', timeout: 30000 });
ok('build-dist 可调用');
} catch {
console.log('NOTE: tools/build-dist.mjs 无 --help(不阻塞,构建时再验证产物)');
}
const placeholders = [];
for (const [f, c] of [['package.json', JSON.stringify(pkg)], ['build-site.ps1', (()=>{ try { return fs.readFileSync(join(ROOT,'build-site.ps1'),'utf8'); } catch { return ''; } })()]]) {
if (/YOUR-ACCOUNT/.test(c)) placeholders.push(f);
}
if (!process.env.SITE_URL_BASE && /YOUR-ACCOUNT/.test(readFileSync(join(ROOT,'build-site.ps1'),'utf8'))) {
console.log('NOTE: SITE_URL_BASE 未设置,build-site.ps1 将使用占位域名;正式发布前必须配置真实域名');
}
if (placeholders.length) fail('正式发布前必须替换占位账号/域名:' + placeholders.join(', '));
ok('无 YOUR-ACCOUNT 占位(或仅构建警告范围内)');
try {
const { execSync } = await import('child_process');
execSync('npm pack --dry-run', { cwd: ROOT, stdio: 'pipe', timeout: 60000 });
ok('npm pack --dry-run 通过(本轮不执行 npm publish)');
} catch (e) {
fail('npm pack --dry-run 失败:' + String((e && e.message) || e).slice(0, 200));
}
console.log('');
console.log('发布检查通过。建议的人工执行序列:');
console.log(` 1) git tag v${pkg.version}`);
console.log(` 2) git push origin main v${pkg.version}`);
console.log(' 3) npm run build && npm pack --dry-run(本轮决策:不执行 npm publish,如需公开发布另行决策)');