Files
TrueGrowth/scripts/sync-changelog.js

164 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* sync-changelog.js
* 解析 CHANGELOG.md 生成 apps/web/public/changelog.json
*
* 用法:
* node scripts/sync-changelog.js # 独立运行
* require('./sync-changelog')() # 在其他脚本中调用
*/
const fs = require('fs');
const path = require('path');
const MAX_CHANGELOG_VERSIONS = 50;
function compareSemver(a, b) {
const pa = a.split('.').map(Number);
const pb = b.split('.').map(Number);
for (let i = 0; i < 3; i++) {
if (pa[i] !== pb[i]) return pa[i] - pb[i];
}
return 0;
}
function detectType(version, prevVersion) {
if (!prevVersion) return 'patch';
const [aMaj, aMin] = version.split('.').map(Number);
const [bMaj, bMin] = prevVersion.split('.').map(Number);
if (aMaj !== bMaj) return 'major';
if (aMin !== bMin) return 'minor';
return 'patch';
}
// 去掉行末 commit hash 链接,如 ([abc1234](https://...))
function stripCommitLink(line) {
return line.replace(/\s*\(\[[a-f0-9]+\]\([^)]+\)\)\s*$/, '').trim();
}
// 判断条目是否为噪音(对用户无价值)
function isNoiseEntry(text) {
// 版本号 bump
if (/^(bump|update)\s+(app\s+)?version\s+to\s/i.test(text)) return true;
if (/^更新(应用)?版本至/i.test(text)) return true;
// docs 类CLAUDE.md、编码规则等
if (/^docs(\(.*?\))?:/i.test(text)) return true;
// 纯 refactor/style/ci/build/test/revert 前缀
if (/^(refactor|style|ci|build|test|revert)(\(.*?\))?:/i.test(text)) return true;
// 更新 build time
if (/update.*build\s*time/i.test(text)) return true;
// 过短无意义(如 "升级"、"ppt"、"简化方案"
if (text.length <= 4) return true;
// 纯英文的内部优化描述enhance/improve/optimize/unify 开头,不含中文)
const hasChinese = /[\u4e00-\u9fa5]/.test(text);
if (!hasChinese && /^(enhance|improve|optimize|unify)\s/i.test(text)) return true;
return false;
}
function parseChangelog(content) {
const versions = [];
// 按 ## VERSION (DATE) 分割
const versionRegex = /^## (\d+\.\d+\.\d+)\s+\((\d{4}-\d{2}-\d{2})\)/;
const sectionRegex = /^### (.+)/;
const lines = content.split('\n');
let current = null;
let currentSection = null;
for (const line of lines) {
const vMatch = line.match(versionRegex);
if (vMatch) {
if (current) versions.push(current);
current = {
version: vMatch[1],
date: vMatch[2],
changes: { features: [], fixes: [], improvements: [] },
};
currentSection = null;
continue;
}
if (!current) continue;
const sMatch = line.match(sectionRegex);
if (sMatch) {
const title = sMatch[1].trim();
if (/Thank You/i.test(title)) {
currentSection = null; // 跳过 Thank You 段落
} else if (/Features/i.test(title)) {
currentSection = 'features';
} else if (/Fixes/i.test(title)) {
currentSection = 'fixes';
} else if (/Chores|Improvements/i.test(title)) {
currentSection = 'improvements';
} else {
currentSection = null;
}
continue;
}
// 解析列表项
const itemMatch = line.match(/^- (.+)/);
if (itemMatch && currentSection && current.changes[currentSection]) {
const text = stripCommitLink(itemMatch[1]);
if (text) {
current.changes[currentSection].push(text);
}
}
}
if (current) versions.push(current);
// 按版本号降序排列
versions.sort((a, b) => compareSemver(b.version, a.version));
// 计算 type 和 highlights
for (let i = 0; i < versions.length; i++) {
const prev = i < versions.length - 1 ? versions[i + 1].version : null;
versions[i].type = detectType(versions[i].version, prev);
// 过滤噪音条目
const c = versions[i].changes;
c.features = c.features.filter(t => !isNoiseEntry(t));
c.fixes = c.fixes.filter(t => !isNoiseEntry(t));
c.improvements = c.improvements.filter(t => !isNoiseEntry(t));
const { features, fixes } = c;
versions[i].highlights =
(features.length > 0 && features[0]) ||
(fixes.length > 0 && fixes[0]) ||
'构建修复与稳定性改进';
// 清理空的 changes
if (!c.features.length && !c.fixes.length && !c.improvements.length) {
delete versions[i].changes;
}
}
return { versions: versions.slice(0, MAX_CHANGELOG_VERSIONS) };
}
function syncChangelog() {
const changelogPath = path.join(__dirname, '../CHANGELOG.md');
const outputPath = path.join(__dirname, '../apps/web/public/changelog.json');
if (!fs.existsSync(changelogPath)) {
console.log(' CHANGELOG.md 不存在,跳过同步');
return;
}
const content = fs.readFileSync(changelogPath, 'utf8');
const data = parseChangelog(content);
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2) + '\n');
console.log(
`✅ changelog.json 已同步(${data.versions.length} 个版本,最多保留 ${MAX_CHANGELOG_VERSIONS} 个)`
);
}
// 支持独立运行和 require() 调用
if (require.main === module) {
syncChangelog();
} else {
module.exports = syncChangelog;
}