166 lines
5.6 KiB
TypeScript
166 lines
5.6 KiB
TypeScript
import * as plugins from "./plugins.js";
|
|
|
|
export type TChangelogBucket =
|
|
| "Breaking Changes"
|
|
| "Features"
|
|
| "Fixes"
|
|
| "Documentation"
|
|
| "Maintenance";
|
|
|
|
export interface IChangelogEntry {
|
|
type: string;
|
|
scope: string;
|
|
message: string;
|
|
details?: string[];
|
|
}
|
|
|
|
export interface IPendingChangelog {
|
|
block: string;
|
|
isEmpty: boolean;
|
|
}
|
|
|
|
const bucketForCommitType = (commitType: string): TChangelogBucket => {
|
|
switch (commitType) {
|
|
case "BREAKING CHANGE":
|
|
return "Breaking Changes";
|
|
case "feat":
|
|
return "Features";
|
|
case "fix":
|
|
return "Fixes";
|
|
case "docs":
|
|
return "Documentation";
|
|
default:
|
|
return "Maintenance";
|
|
}
|
|
};
|
|
|
|
const readChangelog = async (filePath: string): Promise<string> => {
|
|
if (!(await plugins.smartfs.file(filePath).exists())) {
|
|
return "# Changelog\n\n";
|
|
}
|
|
return (await plugins.smartfs.file(filePath).encoding("utf8").read()) as string;
|
|
};
|
|
|
|
const writeChangelog = async (filePath: string, content: string): Promise<void> => {
|
|
await plugins.smartfs.file(filePath).encoding("utf8").write(content.endsWith("\n") ? content : `${content}\n`);
|
|
};
|
|
|
|
const findPendingSection = (
|
|
content: string,
|
|
sectionName: string,
|
|
): { start: number; bodyStart: number; end: number } | null => {
|
|
const headingRegex = new RegExp(`^##\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
|
const match = headingRegex.exec(content);
|
|
if (!match || match.index === undefined) {
|
|
return null;
|
|
}
|
|
|
|
const bodyStart = match.index + match[0].length;
|
|
const rest = content.slice(bodyStart);
|
|
const nextHeadingMatch = /^##\s+/m.exec(rest);
|
|
const end = nextHeadingMatch ? bodyStart + nextHeadingMatch.index : content.length;
|
|
return { start: match.index, bodyStart, end };
|
|
};
|
|
|
|
export const ensurePendingSection = async (
|
|
filePath: string,
|
|
sectionName = "Pending",
|
|
): Promise<string> => {
|
|
let content = await readChangelog(filePath);
|
|
if (findPendingSection(content, sectionName)) {
|
|
return content;
|
|
}
|
|
|
|
const pendingSection = `## ${sectionName}\n\n`;
|
|
const titleMatch = /^#\s+.+$/m.exec(content);
|
|
if (titleMatch && titleMatch.index !== undefined) {
|
|
const insertAt = titleMatch.index + titleMatch[0].length;
|
|
content = `${content.slice(0, insertAt)}\n\n${pendingSection}${content.slice(insertAt).replace(/^\n+/, "")}`;
|
|
} else {
|
|
content = `# Changelog\n\n${pendingSection}${content}`;
|
|
}
|
|
|
|
await writeChangelog(filePath, content);
|
|
return content;
|
|
};
|
|
|
|
export const appendPendingChangelogEntry = async (
|
|
filePath: string,
|
|
sectionName: string,
|
|
entry: IChangelogEntry,
|
|
): Promise<void> => {
|
|
let content = await ensurePendingSection(filePath, sectionName);
|
|
const pendingSection = findPendingSection(content, sectionName)!;
|
|
let pendingBody = content.slice(pendingSection.bodyStart, pendingSection.end);
|
|
const bucket = bucketForCommitType(entry.type);
|
|
const bucketHeading = `### ${bucket}`;
|
|
|
|
const entryLines = [`- ${entry.message}${entry.scope ? ` (${entry.scope})` : ""}`];
|
|
for (const detail of entry.details || []) {
|
|
entryLines.push(` - ${detail}`);
|
|
}
|
|
const renderedEntry = entryLines.join("\n");
|
|
|
|
const bucketRegex = new RegExp(`^###\\s+${bucket.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
|
const bucketMatch = bucketRegex.exec(pendingBody);
|
|
if (!bucketMatch || bucketMatch.index === undefined) {
|
|
pendingBody = `${pendingBody.trimEnd()}\n\n${bucketHeading}\n\n${renderedEntry}\n`;
|
|
} else {
|
|
const bucketBodyStart = bucketMatch.index + bucketMatch[0].length;
|
|
const afterBucket = pendingBody.slice(bucketBodyStart);
|
|
const nextBucketMatch = /^###\s+/m.exec(afterBucket);
|
|
const insertAt = nextBucketMatch ? bucketBodyStart + nextBucketMatch.index : pendingBody.length;
|
|
const beforeInsert = pendingBody.slice(0, insertAt).trimEnd();
|
|
const afterInsert = pendingBody.slice(insertAt).replace(/^\n+/, "");
|
|
pendingBody = `${beforeInsert}\n${renderedEntry}\n\n${afterInsert}`;
|
|
}
|
|
|
|
content = `${content.slice(0, pendingSection.bodyStart)}\n${pendingBody.trim()}\n\n${content.slice(pendingSection.end).replace(/^\n+/, "")}`;
|
|
await writeChangelog(filePath, content);
|
|
};
|
|
|
|
export const readPendingChangelog = async (
|
|
filePath: string,
|
|
sectionName = "Pending",
|
|
): Promise<IPendingChangelog> => {
|
|
const content = await ensurePendingSection(filePath, sectionName);
|
|
const pendingSection = findPendingSection(content, sectionName)!;
|
|
const block = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
|
return {
|
|
block,
|
|
isEmpty: block.length === 0,
|
|
};
|
|
};
|
|
|
|
export const inferVersionTypeFromPending = (pendingBlock: string): "patch" | "minor" | "major" => {
|
|
if (/^###\s+Breaking Changes\s*$/m.test(pendingBlock)) {
|
|
return "major";
|
|
}
|
|
if (/^###\s+Features\s*$/m.test(pendingBlock)) {
|
|
return "minor";
|
|
}
|
|
return "patch";
|
|
};
|
|
|
|
export const movePendingToVersion = async (
|
|
filePath: string,
|
|
sectionName: string,
|
|
versionHeading: string,
|
|
version: string,
|
|
dateString: string,
|
|
): Promise<void> => {
|
|
let content = await ensurePendingSection(filePath, sectionName);
|
|
const pendingSection = findPendingSection(content, sectionName)!;
|
|
const pendingBlock = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
|
|
if (!pendingBlock) {
|
|
throw new Error("No pending changelog entries. Nothing to release.");
|
|
}
|
|
|
|
const renderedHeading = versionHeading
|
|
.replaceAll("{{version}}", version)
|
|
.replaceAll("{{date}}", dateString);
|
|
const nextContent = content.slice(pendingSection.end).replace(/^\n+/, "");
|
|
content = `${content.slice(0, pendingSection.bodyStart)}\n\n${renderedHeading}\n\n${pendingBlock}\n\n${nextContent}`;
|
|
await writeChangelog(filePath, content);
|
|
};
|