keepachangelog-fmt 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # tsdown-starter
2
+
3
+ A starter for creating a TypeScript package.
4
+
5
+ ## Development
6
+
7
+ - Install dependencies:
8
+
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ - Run the unit tests:
14
+
15
+ ```bash
16
+ npm run test
17
+ ```
18
+
19
+ - Build the library:
20
+
21
+ ```bash
22
+ npm run build
23
+ ```
@@ -0,0 +1 @@
1
+ export { };
package/dist/index.mjs ADDED
@@ -0,0 +1,154 @@
1
+ import fs from "node:fs/promises";
2
+ //#region src/constants.ts
3
+ const RELEASE_HEADING_REGEX = /^## \[(.+)\](?=(?: - (\d{4}-\d{2}-\d{2})?\s?(?:\((.+)\))?)?$)/;
4
+ //#endregion
5
+ //#region src/parse.ts
6
+ const SECTION_TITLES = [
7
+ "Added",
8
+ "Changed",
9
+ "Removed",
10
+ "Fixed"
11
+ ];
12
+ const isTitleHeading = (line) => line.startsWith("# ");
13
+ const isReleaseHeading = (line) => line.startsWith("## [");
14
+ const isSectionHeading = (line) => line.startsWith("### ") && SECTION_TITLES.includes(line.replace("### ", ""));
15
+ const isEmptyLine = (line) => line === "";
16
+ const isValidPreambleLine = (line) => !isTitleHeading(line) && !isReleaseHeading(line) && !isSectionHeading(line);
17
+ const isBullet = (line) => line.startsWith("- ");
18
+ const removeTrailingNewLines = (lines) => {
19
+ let newArray = [...lines];
20
+ while (isEmptyLine(newArray[0])) newArray = newArray.slice(1);
21
+ while (isEmptyLine(newArray[newArray.length - 1])) newArray = newArray.slice(0, -1);
22
+ return newArray;
23
+ };
24
+ function parse(text) {
25
+ const changelog = {
26
+ type: "changelog",
27
+ title: "# Changelog",
28
+ preamble: [],
29
+ releases: []
30
+ };
31
+ const lines = text.split("\n");
32
+ let lineNumber = 0;
33
+ const parseTitle = () => {
34
+ const titleLine = lines[lineNumber];
35
+ if (isTitleHeading(titleLine)) {
36
+ changelog.title = titleLine;
37
+ lineNumber++;
38
+ }
39
+ };
40
+ const parsePreamble = () => {
41
+ while (lineNumber < lines.length) {
42
+ const line = lines[lineNumber];
43
+ if (!isValidPreambleLine(line)) break;
44
+ changelog.preamble.push(line);
45
+ lineNumber++;
46
+ }
47
+ changelog.preamble = removeTrailingNewLines(changelog.preamble);
48
+ };
49
+ const parseChanges = () => {
50
+ const changes = [];
51
+ while (lineNumber < lines.length) {
52
+ const line = lines[lineNumber];
53
+ if (isEmptyLine(line)) {
54
+ lineNumber++;
55
+ continue;
56
+ }
57
+ if (!isBullet(line)) break;
58
+ changes.push({
59
+ type: "change",
60
+ text: [line.replace("- ", "")]
61
+ });
62
+ lineNumber++;
63
+ }
64
+ return changes;
65
+ };
66
+ const parseSection = () => {
67
+ const title = lines[lineNumber].replace("### ", "");
68
+ lineNumber++;
69
+ return {
70
+ type: "section",
71
+ title,
72
+ changes: parseChanges()
73
+ };
74
+ };
75
+ const parseSections = () => {
76
+ const sections = [];
77
+ while (lineNumber < lines.length) {
78
+ const line = lines[lineNumber];
79
+ if (isEmptyLine(line)) {
80
+ lineNumber++;
81
+ continue;
82
+ }
83
+ if (!isSectionHeading(line)) break;
84
+ sections.push(parseSection());
85
+ }
86
+ return sections;
87
+ };
88
+ const parseRelease = () => {
89
+ const line = lines[lineNumber];
90
+ const matches = RELEASE_HEADING_REGEX.exec(line.trim());
91
+ if (!matches) throw new Error(`Invalid release heading on line ${lineNumber}: "${line}"`);
92
+ const [_, version, date, label] = matches;
93
+ lineNumber++;
94
+ const release = {
95
+ type: "release",
96
+ version,
97
+ date,
98
+ label,
99
+ sections: parseSections()
100
+ };
101
+ changelog.releases.push(release);
102
+ };
103
+ const parseReleases = () => {
104
+ while (lineNumber < lines.length) {
105
+ const line = lines[lineNumber];
106
+ if (isEmptyLine(line)) {
107
+ lineNumber++;
108
+ continue;
109
+ }
110
+ if (!isReleaseHeading(line)) {
111
+ lineNumber++;
112
+ break;
113
+ }
114
+ parseRelease();
115
+ }
116
+ };
117
+ parseTitle();
118
+ parsePreamble();
119
+ parseReleases();
120
+ return changelog;
121
+ }
122
+ //#endregion
123
+ //#region src/format.ts
124
+ function format(changelog) {
125
+ const preambleText = changelog.preamble.length !== 0 ? `\n\n${changelog.preamble.join("\n")}\n` : "";
126
+ const releasesText = changelog.releases.map((release) => {
127
+ let title = `## [${release.version.trim()}]`;
128
+ if (release.date || release.label) {
129
+ title += " -";
130
+ if (release.date) title += ` ${release.date}`;
131
+ if (release.label) title += ` (${release.label.trim()})`;
132
+ }
133
+ const sections = release.sections.map((section) => {
134
+ const title = `### ${section.title.trim()}`;
135
+ const changes = section.changes.map((change) => {
136
+ const line = change.text[0].trim();
137
+ return `- ${line}${line[line.length - 1] === "." ? "" : "."}`;
138
+ }).join("\n");
139
+ if (changes === "") return title;
140
+ return `${title}\n\n${changes}`;
141
+ }).join("\n\n");
142
+ return `${title}\n\n${sections}`;
143
+ }).join("\n\n\n");
144
+ return `${changelog.title}${preambleText}\n\n${releasesText}\n`;
145
+ }
146
+ //#endregion
147
+ //#region src/index.ts
148
+ async function main() {
149
+ const formatted = format(parse(await fs.readFile("CHANGELOG.md", { encoding: "utf8" })));
150
+ await fs.writeFile("CHANGELOG.md", formatted, { encoding: "utf8" });
151
+ }
152
+ main();
153
+ //#endregion
154
+ export {};
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "keepachangelog-fmt",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "A formatter for changelogs following the \"keep a changelog\" format.",
6
+ "author": "Bram <bram@brams.dev>",
7
+ "license": "MIT",
8
+ "exports": {
9
+ ".": "./dist/index.mjs",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsdown",
17
+ "dev": "tsdown --watch",
18
+ "test": "vitest",
19
+ "typecheck": "tsc --noEmit",
20
+ "release": "bumpp",
21
+ "prepublishOnly": "pnpm run build",
22
+ "publish": "npm publish",
23
+ "format": "bun run src/index.ts"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^25.5.0",
27
+ "@typescript/native-preview": "7.0.0-dev.20260328.1",
28
+ "bumpp": "^11.0.1",
29
+ "tsdown": "^0.21.7",
30
+ "typescript": "^6.0.2",
31
+ "vitest": "^4.1.3"
32
+ }
33
+ }