| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- /**
- * generate-changelog.js — 从 git log 生成 changelog,按中版本分文件存储
- *
- * 文件结构:
- * public/CHANGELOG.md — 索引页,显示最新版本内容 + 链接到所有版本文件
- * public/CHANGELOG-v{MAJOR}.{MINOR}.md — 每个中版本一个文件
- *
- * 用法: node scripts/generate-changelog.js <version>
- */
- const fs = require('fs');
- const path = require('path');
- const { execSync } = require('child_process');
- const rootDir = path.resolve(__dirname, '..');
- const PUBLIC_DIR = path.join(rootDir, 'public');
- const version = process.argv[2];
- if (!version) {
- console.error('[generate-changelog] 用法: node scripts/generate-changelog.js <version>');
- process.exit(1);
- }
- const lastBuildFile = path.join(rootDir, '.last_build_commit');
- const changelogFile = path.join(PUBLIC_DIR, 'CHANGELOG.md');
- const versionParts = version.split('.');
- const minorVersion = `${versionParts[0]}.${versionParts[1]}`;
- const versionFileName = `CHANGELOG-v${minorVersion}.md`;
- const versionFilePath = path.join(PUBLIC_DIR, versionFileName);
- // ==================== 1. 获取 commit 日志 ====================
- let fromHash = '';
- try {
- fromHash = fs.readFileSync(lastBuildFile, 'utf-8').trim();
- } catch {
- try {
- fromHash = execSync('git rev-list --max-parents=0 HEAD', { cwd: rootDir, encoding: 'utf-8' }).trim();
- } catch {
- fromHash = '';
- }
- }
- const headHash = execSync('git rev-parse HEAD', { cwd: rootDir, encoding: 'utf-8' }).trim();
- let log = '';
- try {
- const range = fromHash ? `${fromHash}..HEAD` : 'HEAD';
- log = execSync(
- `git log --no-merges --pretty=format:"%s|||%b|||%an|||%ai" ${range}`,
- { cwd: rootDir, encoding: 'utf-8' }
- ).trim();
- } catch {
- log = '';
- }
- const commits = log ? log.split('\n').filter(Boolean).map(line => {
- const [subject, body, author, date] = line.split('|||');
- return { subject: subject || '', body: body || '', author: author || '', date: date || '' };
- }) : [];
- if (commits.length === 0) {
- console.log('[generate-changelog] 没有新 commit,跳过 changelog 生成');
- fs.writeFileSync(lastBuildFile, headHash, 'utf-8');
- return;
- }
- // ==================== 2. 分类 commit ====================
- function categorize(subject) {
- const s = subject.toLowerCase();
- if (s.startsWith('feat') || s.startsWith('feature')) return '新功能';
- if (s.startsWith('fix') || s.startsWith('bug')) return 'Bug 修复';
- if (s.startsWith('refactor')) return '重构';
- if (s.startsWith('docs') || s.startsWith('doc')) return '文档';
- if (s.startsWith('chore') || s.startsWith('build') || s.startsWith('ci')) return '工程化';
- if (s.startsWith('perf') || s.startsWith('optimize')) return '性能优化';
- if (s.startsWith('style')) return '样式';
- if (s.startsWith('test')) return '测试';
- return '其他';
- }
- const hasMeaningful = commits.some(c => categorize(c.subject) !== '工程化');
- if (!hasMeaningful) {
- console.log('[generate-changelog] 仅有工程化变更,跳过 changelog 生成');
- fs.writeFileSync(lastBuildFile, headHash, 'utf-8');
- return;
- }
- const grouped = {};
- commits.forEach(c => {
- const cat = categorize(c.subject);
- if (!grouped[cat]) grouped[cat] = [];
- let desc = c.subject.replace(/^(feat|feature|fix|refactor|docs?|chore|build|ci|perf|style|test)(\([^)]*\))?[:\s]*/i, '');
- if (!desc) desc = c.subject;
- grouped[cat].push(`- ${desc}`);
- });
- // ==================== 3. 构建版本内容块 ====================
- const now = new Date().toISOString().slice(0, 10);
- let block = `\n## v${version} (${now})\n\n`;
- for (const [cat, items] of Object.entries(grouped)) {
- if (cat === '工程化') continue;
- block += `### ${cat}\n${items.join('\n')}\n\n`;
- }
- // ==================== 4. 写入版本文件 ====================
- let versionContent = '';
- try {
- versionContent = fs.readFileSync(versionFilePath, 'utf-8');
- } catch {
- versionContent = `# v${minorVersion} 版本历史\n\n[« 返回最新版本](CHANGELOG.md)\n\n---\n`;
- }
- const versionHeaderEnd = versionContent.indexOf('\n## ');
- if (versionHeaderEnd === -1) {
- versionContent = versionContent.trimEnd() + block;
- } else {
- versionContent = versionContent.slice(0, versionHeaderEnd) + block + versionContent.slice(versionHeaderEnd);
- }
- fs.writeFileSync(versionFilePath, versionContent, 'utf-8');
- // ==================== 5. 迁移旧版 CHANGELOG.md 内容(首次运行) ====================
- // 检查是否已有版本文件(除当前文件外),若没有则从旧版 CHANGELOG.md 迁移
- const allFiles = fs.readdirSync(PUBLIC_DIR);
- const existingVersionFiles = allFiles.filter(f => /^CHANGELOG-v\d+\.\d+\.md$/.test(f) && f !== versionFileName);
- if (existingVersionFiles.length === 0) {
- try {
- const legacyContent = fs.readFileSync(changelogFile, 'utf-8');
- // 判断是否是旧版格式(包含 ## v 条目但没有版本导航链接)
- const hasLegacyEntries = /^## v\d+\.\d+\.\d+/m.test(legacyContent);
- const hasVersionNav = legacyContent.includes('版本历史](');
- if (hasLegacyEntries && !hasVersionNav) {
- // 提取所有版本条目,按中版本分组
- const versionRegex = /(## v(\d+\.\d+)\.\d+ .*?)(?=\n## v\d+\.\d+\.\d+ |\n*$)/gs;
- const entries = {};
- let match;
- while ((match = versionRegex.exec(legacyContent)) !== null) {
- const entry = match[1].trim();
- const ver = match[2];
- if (!entries[ver]) entries[ver] = [];
- entries[ver].push(entry);
- }
- for (const [ver, entryList] of Object.entries(entries)) {
- if (ver === minorVersion) continue; // 当前版本已写入
- const vf = path.join(PUBLIC_DIR, `CHANGELOG-v${ver}.md`);
- let vc = '';
- try {
- vc = fs.readFileSync(vf, 'utf-8');
- } catch {
- vc = `# v${ver} 版本历史\n\n[« 返回最新版本](CHANGELOG.md)\n\n---\n`;
- }
- const vhEnd = vc.indexOf('\n## ');
- const migratedBlock = '\n' + entryList.join('\n') + '\n';
- if (vhEnd === -1) {
- vc = vc.trimEnd() + migratedBlock;
- } else {
- vc = vc.slice(0, vhEnd) + migratedBlock + vc.slice(vhEnd);
- }
- fs.writeFileSync(vf, vc, 'utf-8');
- }
- console.log(`[generate-changelog] 已迁移 ${Object.keys(entries).length} 个中版本的历史记录`);
- }
- } catch {
- // 没有旧版 CHANGELOG.md,忽略
- }
- }
- // ==================== 6. 重建主 CHANGELOG.md ====================
- const allVersionFiles = fs.readdirSync(PUBLIC_DIR)
- .filter(f => /^CHANGELOG-v(\d+)\.(\d+)\.md$/.test(f))
- .sort((a, b) => {
- // 降序排列:1.1 > 1.0
- const ma = a.match(/^CHANGELOG-v(\d+)\.(\d+)\.md$/);
- const mb = b.match(/^CHANGELOG-v(\d+)\.(\d+)\.md$/);
- if (!ma || !mb) return 0;
- const va = parseInt(ma[1]) * 1000 + parseInt(ma[2]);
- const vb = parseInt(mb[1]) * 1000 + parseInt(mb[2]);
- return vb - va;
- });
- const links = allVersionFiles.map(f => {
- const v = f.replace('CHANGELOG-v', '').replace('.md', '');
- return `- [v${v} 版本历史](${f})`;
- }).join('\n');
- // 读取最新版本文件内容作为默认显示
- let latestContent = '';
- if (allVersionFiles.length > 0) {
- const latestFile = allVersionFiles[0];
- latestContent = fs.readFileSync(path.join(PUBLIC_DIR, latestFile), 'utf-8');
- // 去掉文件头(标题 + 返回链接 + 分隔线)
- latestContent = latestContent.replace(/^# .+?\n\n\[.+?\]\(.+?\)\n\n---\n\n/s, '');
- }
- const mainContent = `# 更新日志
- > 当前版本: v${version}
- ## 历史版本
- ${links}
- ---
- ${latestContent}
- `;
- fs.writeFileSync(changelogFile, mainContent, 'utf-8');
- // ==================== 7. 更新 LAST_BUILD ====================
- fs.writeFileSync(lastBuildFile, headHash, 'utf-8');
- console.log(`[generate-changelog] CHANGELOG.md + ${versionFileName} 已更新,新增 ${commits.length} 条记录`);
|