Add CHANGELOG function addBodyLinesToUnreleasedSection

This will be used by the `pr-checks/changenotes.mts` script to "compile" the latest release entry of CHANGELOG.md.
This commit is contained in:
Mario Campos
2026-09-18 09:07:44 -05:00
parent 47d607e2c1
commit bb1dc5460b
2 changed files with 132 additions and 0 deletions

View File

@@ -9,13 +9,17 @@ import * as fs from "node:fs";
import { describe, it } from "node:test";
import {
addBodyLinesToUnreleasedSection,
ChangelogSection,
EMPTY_CHANGELOG,
getHeader,
getReleaseDateString,
NO_CHANGES_STR,
parseChangelog,
processChangelogForBackports,
renderChangelog,
setVersionAndDate,
UNRELEASED_PLACEHOLDER,
} from "./changelog";
import { CHANGELOG_FILE } from "./config";
@@ -89,3 +93,78 @@ describe("processChangelogForBackports", async () => {
assert.deepEqual(result.split("\n"), testChangelogResult.split("\n"));
});
});
describe("addBodyLinesToUnreleasedSection", async () => {
function newChangelogWithSections(sections: ChangelogSection[]) {
return {
preamble: [],
sections,
};
}
await it("throws error if '[UNRELEASED]' section does not exist", async () => {
const emptyChangelog = newChangelogWithSections([]);
assert.throws(() => addBodyLinesToUnreleasedSection(emptyChangelog, []));
const releasedChangelog = newChangelogWithSections([
{
headerLine: "## Release 1.0.0",
bodyLines: [],
},
{
headerLine: "## Release 2.0.0",
bodyLines: [],
},
{
headerLine: "## Release 3.0.0",
bodyLines: [],
},
]);
assert.throws(() => addBodyLinesToUnreleasedSection(releasedChangelog, []));
});
await it("overwrites 'No user facing changes.'", async () => {
const changelog = newChangelogWithSections([
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: ["", NO_CHANGES_STR, ""],
},
]);
addBodyLinesToUnreleasedSection(changelog, ["- foo"]);
assert.equal(changelog.sections[0].bodyLines.length, 3);
assert.deepEqual(changelog.sections[0].bodyLines, ["", "- foo", ""]);
});
await it("does nothing if lines is empty", async () => {
const changelog = newChangelogWithSections([
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: ["", NO_CHANGES_STR, ""],
},
]);
const changelogClone = structuredClone(changelog);
addBodyLinesToUnreleasedSection(changelog, []);
assert.deepEqual(changelog, changelogClone);
});
await it("inserts a line", async () => {
const changelog = newChangelogWithSections([
{
headerLine: `## ${UNRELEASED_PLACEHOLDER}`,
bodyLines: ["", "- Added a new dependency.", ""],
},
]);
const lineToInsert = "- foo";
addBodyLinesToUnreleasedSection(changelog, [lineToInsert]);
assert.equal(changelog.sections[0].bodyLines.length, 4);
assert.ok(
changelog.sections[0].bodyLines.some((line) => line === lineToInsert),
);
});
});

View File

@@ -132,6 +132,59 @@ export function parseChangelog(content: string): Changelog {
return { preamble, sections };
}
/**
* Inserts the changenotes `notes` under the `[UNRELEASED]` section of `changelog`.
* If the section contains the stock message {@link NO_CHANGES_STR}, then
* `notes` will be inserted in place and the stock message will be deleted.
*
* This function will throw an exception if `[UNRELEASED]` does not exist.
*
* @param changelog The CHANGELOG object to modify.
* @param lines The changenotes to insert.
*/
export function addBodyLinesToUnreleasedSection(
changelog: Changelog,
lines: string[],
) {
// Find the '[UNRELEASED]' section.
let unreleasedSection: ChangelogSection | undefined;
for (const section of changelog.sections) {
if (getHeader(section.headerLine) === UNRELEASED_PLACEHOLDER) {
unreleasedSection = section;
break;
}
}
// Ensure that the '[UNRELEASED]' section exists first.
if (unreleasedSection === undefined) {
throw Error(
"Cannot put changenotes into CHANGELOG.md's '[UNRELEASED]' section because it does not exist",
);
}
let insertAtIndex = 0;
let deleteCount = 0;
// If the section contains an empty line, preserve it -- insert afterward.
if (
unreleasedSection.bodyLines.length > 0 &&
unreleasedSection.bodyLines[0] === ""
) {
insertAtIndex++;
}
// If the section contains the stock message 'No user facing changes.'
if (
lines.length > 0 &&
unreleasedSection.bodyLines.length > insertAtIndex &&
unreleasedSection.bodyLines[insertAtIndex].trim() === NO_CHANGES_STR
) {
deleteCount++; // Delete the line by incrementing the delete marker.
}
unreleasedSection.bodyLines.splice(insertAtIndex, deleteCount, ...lines);
}
/**
* Combines an array of lines into a single string by adding line breaks.
*/