Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | import { readFileSync, writeFileSync, existsSync } from 'fs';
import path from 'path';
import { getConventionalCommitsSince } from './git.js';
import { getConfig } from './config.js';
import { dryRunAction, info } from './log.js';
const CONVENTIONAL_TYPES = {
feat: 'Features',
fix: 'Bug Fixes',
perf: 'Performance Improvements',
revert: 'Reverts',
refactor: 'Code Refactoring',
docs: 'Documentation',
style: 'Styles',
chore: 'Chores',
test: 'Tests',
build: 'Build System',
ci: 'Continuous Integration',
};
const VISIBLE_TYPES = ['feat', 'fix', 'perf', 'revert'];
// PR/commit tooling routinely prepends a GUS work-item tag (e.g. "@W-12345 ") to
// the subject. That prefix is not part of the Conventional Commits grammar, so an
// authentic `fix:`/`feat:` commit written as "@W-12345 fix(scope): ..." fails to
// match and silently vanishes from the generated notes. Strip a single leading
// work-item tag before matching. (This is exactly how #1753/#1631 dropped out of
// the SLDS1 changelog until they were backfilled by hand.)
const WORK_ITEM_PREFIX = /^@?W-\d+\s+/i;
function parseConventionalCommit(subject) {
const normalized = subject.replace(WORK_ITEM_PREFIX, '');
const match = normalized.match(/^(\w+)(?:\(([^)]+)\))?(!?):\s*(.+)$/);
if (!match) return null;
return {
type: match[1],
scope: match[2] || null,
breaking: match[3] === '!',
description: match[4],
};
}
export function generateChangelogNotes(packageName, version, sinceRef, pkgPaths = [], toRef = 'HEAD') {
const commits = getConventionalCommitsSince(sinceRef, pkgPaths, toRef);
const grouped = {};
const breaking = [];
for (const commit of commits) {
const parsed = parseConventionalCommit(commit.subject);
if (!parsed) continue;
if (parsed.breaking) {
breaking.push({ ...parsed, hash: commit.hash });
}
if (!VISIBLE_TYPES.includes(parsed.type)) continue;
const heading = CONVENTIONAL_TYPES[parsed.type] || parsed.type;
if (!grouped[heading]) grouped[heading] = [];
const shortHash = commit.hash.substring(0, 7);
const scopePrefix = parsed.scope ? `**${parsed.scope}:** ` : '';
grouped[heading].push(`* ${scopePrefix}${parsed.description} (${shortHash})`);
}
const lines = [];
const shortName = packageName.split('/').pop();
const dateStr = new Date().toISOString().split('T')[0];
lines.push(`## ${version} (${dateStr})`);
lines.push('');
if (breaking.length > 0) {
lines.push('### BREAKING CHANGES');
lines.push('');
for (const b of breaking) {
const scopePrefix = b.scope ? `**${b.scope}:** ` : '';
lines.push(`* ${scopePrefix}${b.description}`);
}
lines.push('');
}
const headings = Object.keys(grouped).sort();
if (headings.length === 0 && breaking.length === 0) {
lines.push(`**Note:** Version bump only for package ${packageName}`);
lines.push('');
} else {
for (const heading of headings) {
lines.push(`### ${heading}`);
lines.push('');
for (const entry of grouped[heading]) {
lines.push(entry);
}
lines.push('');
}
}
return lines.join('\n');
}
export function prependChangelog(pkgPath, notes) {
const changelogPath = path.join(pkgPath, 'CHANGELOG.md');
if (dryRunAction(`prepend to ${changelogPath}`)) {
info(`Changelog notes:\n${notes}`);
return changelogPath;
}
let existing = '';
if (existsSync(changelogPath)) {
existing = readFileSync(changelogPath, 'utf8');
}
const header = '# Change Log\n\nAll notable changes to this project will be documented in this file.\nSee [Conventional Commits](https://conventionalcommits.org) for commit guidelines.\n';
let content;
if (existing.startsWith('# Change Log')) {
const afterHeader = existing.indexOf('\n\n', existing.indexOf('\n') + 1);
const body = afterHeader >= 0 ? existing.substring(afterHeader + 2) : '';
content = `${header}\n${notes}\n\n${body}`;
} else if (existing) {
content = `${header}\n${notes}\n\n${existing}`;
} else {
content = `${header}\n${notes}\n`;
}
writeFileSync(changelogPath, content.replace(/\n{3,}/g, '\n\n'));
return changelogPath;
}
|