P9: #/faq 路由+renderFaq(details批量懒加载每批10个)+顶栏/侧边栏双入口
+guide-h3样式+9条i18n英文;浏览器实测240条/搜索宽度11条/0 JS错误。
规划偏差:契约已在P4-D移出data.js,FAQ走details懒加载。
P12: tools/release.mjs(检查+bump双模式,只做本地准备不自动push/tag)
+CONTRIBUTING发布5步+版本号规则;检查模式实测版本一致通过。
回归零影响:report.json 79/79、data.json未动。
102 lines
4.6 KiB
JavaScript
102 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* release.mjs — S4-P12 版本发布流程(SemVer + CHANGELOG 驱动)
|
||
*
|
||
* 只做可逆的本地准备,不自动 push/tag 到远端:
|
||
* 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 } 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(不阻塞,构建时再验证产物)');
|
||
}
|
||
|
||
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 publish(需 Q1 决策:是否公开发布)');
|