@aklinker1/zero-changelog 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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/changelog-section.d.mts +7 -0
  4. package/dist/changelog-section.mjs +1 -0
  5. package/dist/conventional-commit.d.mts +28 -0
  6. package/dist/conventional-commit.mjs +1 -0
  7. package/dist/create-github-release.d.mts +15 -0
  8. package/dist/create-github-release.mjs +61 -0
  9. package/dist/detect-version-bump.d.mts +13 -0
  10. package/dist/detect-version-bump.mjs +43 -0
  11. package/dist/find-previous-tag-Cj2oZAty.mjs +27 -0
  12. package/dist/find-previous-tag.d.mts +10 -0
  13. package/dist/find-previous-tag.mjs +2 -0
  14. package/dist/get-current-version.d.mts +4 -0
  15. package/dist/get-current-version.mjs +24 -0
  16. package/dist/get-github-release.d.mts +15 -0
  17. package/dist/get-github-release.mjs +11 -0
  18. package/dist/get-github-repo.d.mts +4 -0
  19. package/dist/get-github-repo.mjs +20 -0
  20. package/dist/get-release-notes.d.mts +6 -0
  21. package/dist/get-release-notes.mjs +37 -0
  22. package/dist/git-commit.d.mts +13 -0
  23. package/dist/git-commit.mjs +1 -0
  24. package/dist/list-commits-since-DycWgHTi.mjs +44 -0
  25. package/dist/list-commits-since.d.mts +11 -0
  26. package/dist/list-commits-since.mjs +2 -0
  27. package/dist/parse-changelog.d.mts +6 -0
  28. package/dist/parse-changelog.mjs +23 -0
  29. package/dist/parse-commit.d.mts +8 -0
  30. package/dist/parse-commit.mjs +34 -0
  31. package/dist/parse-commits.d.mts +8 -0
  32. package/dist/parse-commits.mjs +8 -0
  33. package/dist/release.d.mts +384 -0
  34. package/dist/release.mjs +125 -0
  35. package/dist/semver-type--Q1lYCiZ.d.mts +11 -0
  36. package/dist/semver-type-map.d.mts +8 -0
  37. package/dist/semver-type-map.mjs +1 -0
  38. package/dist/semver-type.d.mts +2 -0
  39. package/dist/semver-type.mjs +1 -0
  40. package/dist/semver-types/aklinker1.d.mts +6 -0
  41. package/dist/semver-types/aklinker1.mjs +38 -0
  42. package/dist/semver.d.mts +37 -0
  43. package/dist/semver.mjs +142 -0
  44. package/dist/serialize-changelog.d.mts +6 -0
  45. package/dist/serialize-changelog.mjs +28 -0
  46. package/dist/summarize-unreleased-commits.d.mts +50 -0
  47. package/dist/summarize-unreleased-commits.mjs +30 -0
  48. package/dist/sync-release.d.mts +4 -0
  49. package/dist/sync-release.mjs +6 -0
  50. package/dist/sync-releases.d.mts +4 -0
  51. package/dist/sync-releases.mjs +6 -0
  52. package/dist/update-version-files.d.mts +4 -0
  53. package/dist/update-version-files.mjs +26 -0
  54. package/dist/utils-BO6byvK5.mjs +22 -0
  55. package/dist/version-regex-C82OGsTC.mjs +23 -0
  56. package/dist/wait-for-child-process-lyAoE4WE.mjs +25 -0
  57. package/package.json +134 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aaron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ <div align="center">
2
+
3
+ # `@aklinker1/zero-changelog`
4
+
5
+ [![JSR](https://jsr.io/badges/@aklinker1/zero-changelog)](https://jsr.io/@aklinker1/zero-changelog)
6
+ [![NPM Version](https://img.shields.io/npm/v/%40aklinker1%2Fzero-changelog?logo=npm&labelColor=red&color=white)](https://www.npmjs.com/package/@aklinker1/zero-changelog)
7
+ [![Docs](https://img.shields.io/badge/API%20Reference-blue?logo=readme&logoColor=white)](https://jsr.io/@aklinker1/zero-changelog/doc)
8
+ [![Install Size](https://pkg-size.dev/badge/install/61804)](https://pkg-size.dev/@aklinker1%2Fzero-changelog)
9
+
10
+ Zero-dependency, conventional commit release and changelog generator with monorepo support
11
+
12
+ </div>
13
+
14
+ ```sh
15
+ bun add @aklinker1/zero-changelog
16
+ ```
17
+
18
+ ## Features
19
+
20
+ - 📦 Zero dependencies, [e18e](https://e18e.dev) first
21
+ - 🏗️ Monorepo & sub-directory support
22
+ - 🛠️ Github Action, JS API, CLI
23
+ - 🚀 Runs custom publish scripts
24
+ - 📊 Summarize unreleased changes
25
+
26
+ ## Options
27
+
28
+ Refer to the API reference:
29
+
30
+ - [Release](https://jsr.io/@aklinker1/zero-ioc/doc/release/~/ReleaseOptions)
31
+ - [Sync Releases](https://jsr.io/@aklinker1/zero-ioc/doc/sync-releases/~/SyncReleasesOptions)
32
+ - [Summarize Unreleased Commits](https://jsr.io/@aklinker1/zero-ioc/doc/summarize-unreleased-commits/~/SummarizeUnreleasedCommitsOptions)
33
+
34
+ ## Usage
35
+
36
+ ### GitHub Action
37
+
38
+ ```yml
39
+ - uses: aklinker1/zero-changelog/actions/release@1.0.0
40
+ with:
41
+ # options...
42
+ ```
43
+
44
+ ### JS API
45
+
46
+ ```ts
47
+ import { release } from "@aklinker1/zero-changelog/release";
48
+
49
+ await release({
50
+ // options...
51
+ });
52
+ ```
53
+
54
+ ### CLI
55
+
56
+ > TODO: CLI not implemented yet.
57
+
58
+ Run any command with `--help` to see available options.
59
+
60
+ ```sh
61
+ bun zero-changelog --help
62
+ bun zero-changelog release --help
63
+ bun zero-changelog sync-releases --help
64
+ bun zero-changelog summarize-unreleased-changes --help
65
+ ```
66
+
67
+ ## Semver
68
+
69
+ `@aklinker1/zero-changelog` does not implement the entire Semver spec. It only supports the
70
+ following types of versions:
71
+
72
+ - ✅ **Unstable**: `0.[0-9]+.[0-9]+`
73
+ - Examples: `0.0.0`, `0.12.1`, `0.1.9`, `0.20.17`
74
+
75
+ - ✅ **Prerelease**: `[1-9][0-9]*.0.0-\w+.[0-9]+`
76
+ - Examples: `1.0.0-alpha.1`, `2.0.0-beta.3`, `3.0.0-rc.10`
77
+
78
+ - ✅ **Stable**: `[1-9][0-9]*.[0-9]+.[0-9]+`
79
+ - Examples: `1.1.0`, `2.0.1`, `3.0.0`
80
+
81
+ It does NOT support:
82
+
83
+ - ❌ **Truncated versions** with one or two digits:
84
+ - Examples: `1.0`, `2`
85
+ - ❌ **Unstable prereleases**:
86
+ - Examples: `0.2.0-alpha.1`
87
+ - ❌ **Non X.0.0 prereleases**:
88
+ - Examples: `1.1.0-rc.1`, `1.0.5-alpha.1`
89
+ - ❌ **Other prerelease formats**:
90
+ - Examples: `0.2.0-0.3.2`, `0.2.0-1`
91
+
92
+ This allows the package to have 0 dependencies.
93
+
94
+ ### Relative Version Bumping
95
+
96
+ When bumping the version by `major`, `minor`, or `patch`, either by setting the `bump` option or
97
+ auto-detecting the type based on commits, there are some special cases where the version may not be
98
+ bumped as expected:
99
+
100
+ 1. **Unstable**: The type of semver change will be lowered by 1, so `major` &rarr; `minor`, `minor`
101
+ &rarr; `patch`, `patch` &rarr; `patch`.
102
+ - Examples:
103
+ - `0.1.0` &rarr; `major` &rarr; `0.2.0`
104
+ - `0.1.0` &rarr; `minor` &rarr; `0.1.1`
105
+ - `0.1.0` &rarr; `patch` &rarr; `0.1.1`
106
+
107
+ 2. **Prereleases**: The final integer will always be updated.
108
+ - Examples:
109
+ - `1.0.0-alpha.1` &rarr; `major` &rarr; `1.0.0-alpha.2`
110
+ - `1.0.0-alpha.1` &rarr; `minor` &rarr; `1.0.0-alpha.2`
111
+ - `1.0.0-alpha.1` &rarr; `patch` &rarr; `1.0.0-alpha.2`
112
+
113
+ So if you want to switch between versions types (stable ↔ unstable ↔ pre-release), like `0.7.5`
114
+ &rarr; `1.0.0` or `2.0.0-rc.4` &rarr; `2.0.0`, you need to provide the next version number for the
115
+ `bump` option instead of using `major`/`minor`/`patch`:
116
+
117
+ ```ts
118
+ await release({
119
+ bump: "2.0.0",
120
+ });
121
+ ```
122
+
123
+ Otherwise stable versions will remain stable, unstable will remain unstable, and prereleases will
124
+ remain prereleases.
125
+
126
+ ## Plan
127
+
128
+ Release:
129
+
130
+ 1. List git commits
131
+ 2. Detect type of version bump
132
+ 3. Generate changelog
133
+ 4. Bump version in version files
134
+ 5. Update changelog
135
+ 6. Git commit & tag
136
+ 7. Git push
137
+ 8. Create release
138
+
139
+ Sync Releases:
140
+
141
+ 1. Parse changelog
142
+ 2. Update each release to match changelog
143
+
144
+ Summarize Unreleased Changes:
145
+
146
+ 1. List commits in each directory since the last commit
147
+ 2. Print and return the summary
148
+
149
+ ## TODO
150
+
151
+ - [x] Structure for package + action
152
+ - [x] Implement tests
153
+ - [x] Implement git logic
154
+ - [ ] Implement sync releases
155
+ - [x] Glob patterns for github action
156
+ - [x] Implement unreleased commit summary
157
+ - [ ] CLI?
158
+ - [x] Implement alpha/beta version bumping
@@ -0,0 +1,7 @@
1
+ //#region src/changelog-section.d.ts
2
+ type ChangelogSection = {
3
+ header: string;
4
+ body: string;
5
+ };
6
+ //#endregion
7
+ export { ChangelogSection };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ //#region src/conventional-commit.d.ts
2
+ type ConventionalCommit = {
3
+ type: string;
4
+ scope?: string;
5
+ description: string;
6
+ body?: string;
7
+ isBreaking: boolean;
8
+ footers: Array<{
9
+ /**
10
+ * Lowercase footer key.
11
+ *
12
+ * @example
13
+ * "Co-authored-by: Aaron <aaron@example.com>"; // key: "co-authored-by"
14
+ */
15
+ key: string;
16
+ /**
17
+ * @example
18
+ * "Co-authored-by: Aaron <aaron@example.com>"; // value: "Aaron <aaron@example.com>"
19
+ */
20
+ value: string;
21
+ }>;
22
+ authors: Array<{
23
+ name: string;
24
+ email: string;
25
+ }>;
26
+ };
27
+ //#endregion
28
+ export { ConventionalCommit };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ //#region src/create-github-release.d.ts
2
+ type CreateGithubReleaseOptions = {
3
+ repo: `${string}/${string}`;
4
+ token: string;
5
+ dryRun: boolean;
6
+ tag: string;
7
+ name: string;
8
+ body: string;
9
+ artifacts?: string[];
10
+ latest: boolean;
11
+ prerelease: boolean;
12
+ };
13
+ declare function createGithubRelease(options: CreateGithubReleaseOptions): Promise<void>;
14
+ //#endregion
15
+ export { createGithubRelease };
@@ -0,0 +1,61 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { styleText } from "node:util";
3
+ //#region src/create-github-release.ts
4
+ async function createGithubRelease(options) {
5
+ console.log("Creating GitHub release...");
6
+ if (options.dryRun) console.log(" -> Skipping, dry run");
7
+ else throw Error("TODO");
8
+ for (const artifact of options.artifacts ?? []) {
9
+ console.log(`Uploading ${styleText("cyan", artifact)}...`);
10
+ if (options.dryRun) console.log(" -> Skipping, dry run");
11
+ else {
12
+ const artifactStreams = options.artifacts?.length ? await getArtifactStreams(options.artifacts) : void 0;
13
+ await createRelease(options);
14
+ if (artifactStreams) await uploadArtifacts(options, artifactStreams);
15
+ }
16
+ }
17
+ }
18
+ async function getArtifactStreams(artifacts) {
19
+ return Promise.all(artifacts.map((artifact) => createReadStream(artifact)));
20
+ }
21
+ async function createRelease(options) {
22
+ const res = await fetch(`https://api.github.com/repos/${options.repo}/releases`, {
23
+ method: "POST",
24
+ headers: {
25
+ "Content-Type": "application/json",
26
+ Authorization: `Bearer ${options.token}`
27
+ },
28
+ body: JSON.stringify({
29
+ tag_name: options.tag,
30
+ name: options.name,
31
+ body: options.body,
32
+ make_latest: options.latest,
33
+ prerelease: options.prerelease
34
+ })
35
+ });
36
+ if (res.ok) return;
37
+ console.log("Response status: " + res.status);
38
+ console.log("Response body: " + await res.text());
39
+ throw Error(`Failed to create GitHub release`);
40
+ }
41
+ async function uploadArtifacts(options, artifactStreams) {
42
+ for (const [i, stream] of artifactStreams.entries()) {
43
+ const artifact = options.artifacts[i];
44
+ console.log(" -> Uploading ", artifact);
45
+ const res = await fetch(`https://uploads.github.com/repos/${options.repo}/releases/tags/${options.tag}/assets`, {
46
+ method: "POST",
47
+ headers: {
48
+ "Content-Type": "application/octet-stream",
49
+ Authorization: `Bearer ${options.token}`,
50
+ "Content-Disposition": `attachment; filename="${artifact}"`
51
+ },
52
+ body: stream
53
+ });
54
+ if (!res.ok) {
55
+ console.error(`Failed to upload`, artifact, res);
56
+ console.log(await res.text());
57
+ }
58
+ }
59
+ }
60
+ //#endregion
61
+ export { createGithubRelease };
@@ -0,0 +1,13 @@
1
+ import { ConventionalCommit } from "./conventional-commit.mjs";
2
+ import { RelativeBump } from "./semver.mjs";
3
+
4
+ //#region src/detect-version-bump.d.ts
5
+ /**
6
+ * Given the current version and a list of commits, detect the type of relative version bump to use.
7
+ *
8
+ * @param currentVersion Used to determine the if prerelease logic should be applied.
9
+ * @param conventionalCommits List of commits to generate the version bump from.
10
+ */
11
+ declare function detectVersionBump(conventionalCommits: ConventionalCommit[], throwOnNoChanges: boolean): RelativeBump;
12
+ //#endregion
13
+ export { detectVersionBump };
@@ -0,0 +1,43 @@
1
+ import types from "./semver-types/aklinker1.mjs";
2
+ //#region src/detect-version-bump.ts
3
+ const NONE = 0;
4
+ const PATCH = 1;
5
+ const MINOR = 2;
6
+ const PRIORITY_MAP = {
7
+ none: NONE,
8
+ patch: PATCH,
9
+ minor: MINOR
10
+ };
11
+ /**
12
+ * Given the current version and a list of commits, detect the type of relative version bump to use.
13
+ *
14
+ * @param currentVersion Used to determine the if prerelease logic should be applied.
15
+ * @param conventionalCommits List of commits to generate the version bump from.
16
+ */
17
+ function detectVersionBump(conventionalCommits, throwOnNoChanges) {
18
+ console.log("Detecting version bump based on changes...");
19
+ let priority = NONE;
20
+ for (const commit of conventionalCommits) {
21
+ if (commit.isBreaking) {
22
+ console.log(" -> major");
23
+ return "major";
24
+ }
25
+ const bumpBy = types[commit.type]?.bump ?? "none";
26
+ priority = Math.max(priority, PRIORITY_MAP[bumpBy]);
27
+ }
28
+ switch (priority) {
29
+ case MINOR:
30
+ console.log(" -> minor");
31
+ return "minor";
32
+ case PATCH:
33
+ console.log(" -> patch");
34
+ return "patch";
35
+ case NONE:
36
+ if (throwOnNoChanges) throw Error("No semver changes detected");
37
+ console.log(" -> patch");
38
+ return "patch";
39
+ default: throw Error("Unknown semver bump value: " + priority);
40
+ }
41
+ }
42
+ //#endregion
43
+ export { detectVersionBump };
@@ -0,0 +1,27 @@
1
+ import { t as waitForChildProcess } from "./wait-for-child-process-lyAoE4WE.mjs";
2
+ import { spawn } from "node:child_process";
3
+ //#region src/internal/run-git-tag.ts
4
+ async function runGitTag(prefix, cwd = process.cwd()) {
5
+ const { stdout } = await waitForChildProcess(spawn("git", [
6
+ "--no-pager",
7
+ "tag",
8
+ "-l",
9
+ `${prefix}*`,
10
+ "--sort=-version:refname"
11
+ ], { cwd }));
12
+ return stdout;
13
+ }
14
+ //#endregion
15
+ //#region src/find-previous-tag.ts
16
+ /**
17
+ * Finds the previous tag that starts with a prefix. If there are no previous tags starting with the
18
+ * prefix, this returns `undefined`.
19
+ *
20
+ * @param tagPrefix The prefix to look for.
21
+ */
22
+ async function findPreviousTag(tagPrefix) {
23
+ console.log(`Finding previous tag matching: "${tagPrefix}*"`);
24
+ return (await runGitTag(tagPrefix)).trim().split("\n")[0] || void 0;
25
+ }
26
+ //#endregion
27
+ export { findPreviousTag as t };
@@ -0,0 +1,10 @@
1
+ //#region src/find-previous-tag.d.ts
2
+ /**
3
+ * Finds the previous tag that starts with a prefix. If there are no previous tags starting with the
4
+ * prefix, this returns `undefined`.
5
+ *
6
+ * @param tagPrefix The prefix to look for.
7
+ */
8
+ declare function findPreviousTag(tagPrefix: string): Promise<string | undefined>;
9
+ //#endregion
10
+ export { findPreviousTag };
@@ -0,0 +1,2 @@
1
+ import { t as findPreviousTag } from "./find-previous-tag-Cj2oZAty.mjs";
2
+ export { findPreviousTag };
@@ -0,0 +1,4 @@
1
+ //#region src/get-current-version.d.ts
2
+ declare function getCurrentVersion(path: string, versionFiles: string[]): Promise<string>;
3
+ //#endregion
4
+ export { getCurrentVersion };
@@ -0,0 +1,24 @@
1
+ import { t as getVersionRegexFor } from "./version-regex-C82OGsTC.mjs";
2
+ import { styleText } from "node:util";
3
+ import { readFile } from "node:fs/promises";
4
+ import { join, relative } from "node:path";
5
+ //#region src/get-current-version.ts
6
+ async function getCurrentVersion(path, versionFiles) {
7
+ console.log("Getting current version...");
8
+ for (const versionFile of versionFiles) try {
9
+ const file = join(path, versionFile);
10
+ const text = await readFile(file, "utf8");
11
+ const regex = getVersionRegexFor(file);
12
+ const version = text.match(regex)?.groups?.version;
13
+ if (version) {
14
+ console.log(`Found current version (${version})) in ${styleText("cyan", relative(process.cwd(), file))}`);
15
+ return version;
16
+ }
17
+ console.log(` -> Not found in ${styleText("cyan", relative(process.cwd(), file))}`);
18
+ } catch (err) {
19
+ if (err.code !== "ENOENT") throw err;
20
+ }
21
+ throw Error("Version not found");
22
+ }
23
+ //#endregion
24
+ export { getCurrentVersion };
@@ -0,0 +1,15 @@
1
+ //#region src/get-github-release.d.ts
2
+ type GithubRelease = {
3
+ tag: string;
4
+ title: string;
5
+ body: string;
6
+ draft: boolean;
7
+ prerelease: boolean;
8
+ };
9
+ declare function getGithubRelease(options: {
10
+ repo: string;
11
+ tag: string;
12
+ token: string;
13
+ }): Promise<GithubRelease>;
14
+ //#endregion
15
+ export { GithubRelease, getGithubRelease };
@@ -0,0 +1,11 @@
1
+ //#region src/get-github-release.ts
2
+ async function getGithubRelease(options) {
3
+ const res = await fetch(`https://api.github.com/repos/${options.repo}/releases/tags/${options.tag}`, { headers: { Authorization: `Bearer ${options.token}` } });
4
+ if (res.ok) return await res.json();
5
+ console.log("Response status:", res.status);
6
+ console.log("Response body:");
7
+ console.log(await res.text());
8
+ throw Error("Failed to fetch github release");
9
+ }
10
+ //#endregion
11
+ export { getGithubRelease };
@@ -0,0 +1,4 @@
1
+ //#region src/get-github-repo.d.ts
2
+ declare function getGithubRepo(): Promise<`${string}/${string}`>;
3
+ //#endregion
4
+ export { getGithubRepo };
@@ -0,0 +1,20 @@
1
+ import { t as waitForChildProcess } from "./wait-for-child-process-lyAoE4WE.mjs";
2
+ import { spawn } from "node:child_process";
3
+ //#region src/get-github-repo.ts
4
+ const REMOTE_REGEX = [/^git@github\.com:(?<owner>\S+)\/(?<repo>\S+)\.git$/, /^https:\/\/github\.com\/(?<owner>\S+)\/(?<repo>\S+)\.git$/];
5
+ async function getGithubRepo() {
6
+ console.log("Getting current github repo...");
7
+ const { stdout } = await waitForChildProcess(spawn("git", [
8
+ "config",
9
+ "--get",
10
+ "remote.origin.url"
11
+ ]));
12
+ const url = stdout.trim();
13
+ for (const regex of REMOTE_REGEX) {
14
+ const groups = url.match(regex)?.groups;
15
+ if (groups) return `${groups.owner}/${groups.repo}`;
16
+ }
17
+ throw Error(`Could not find github repo from the origin remote's URL: ${url}`);
18
+ }
19
+ //#endregion
20
+ export { getGithubRepo };
@@ -0,0 +1,6 @@
1
+ import { ConventionalCommit } from "./conventional-commit.mjs";
2
+
3
+ //#region src/get-release-notes.d.ts
4
+ declare function getReleaseNotes(conventionalCommits: ConventionalCommit[], since: string | undefined, tag: string, repo: string): string;
5
+ //#endregion
6
+ export { getReleaseNotes };
@@ -0,0 +1,37 @@
1
+ import types from "./semver-types/aklinker1.mjs";
2
+ //#region src/get-release-notes.ts
3
+ function getReleaseNotes(conventionalCommits, since, tag, repo) {
4
+ const commitsByType = conventionalCommits.reduce((acc, commit) => {
5
+ acc[commit.type] ??= [];
6
+ acc[commit.type].push(commit);
7
+ return acc;
8
+ }, {});
9
+ const lines = [];
10
+ if (since) lines.push(`[compare changes](https://github.com/${repo}/compare/${since}...${tag})`, "");
11
+ for (const [type, details] of Object.entries(types)) {
12
+ const commits = commitsByType[type];
13
+ if (!commits?.length) continue;
14
+ lines.push(`### ${details.title}`, "");
15
+ for (const commit of commits) {
16
+ const scope = commit.scope ? `**${commit.scope}**: ` : "";
17
+ const breaking = commit.isBreaking ? "⚠️ " : "";
18
+ lines.push(`- ${breaking}${scope}${commit.description}`);
19
+ }
20
+ lines.push("");
21
+ }
22
+ const authors = conventionalCommits.flatMap((commit) => commit.authors);
23
+ if (authors.length) {
24
+ const emailNameMap = authors.reduce((acc, author) => {
25
+ acc[author.name] ??= author.email;
26
+ return acc;
27
+ }, {});
28
+ const emails = new Set(conventionalCommits.flatMap((commit) => commit.authors).map((author) => author.email));
29
+ const sortedEmails = Array.from(emails).toSorted((a, b) => b.localeCompare(a));
30
+ lines.push("### ❤️ Contributors", "");
31
+ for (const email of sortedEmails) lines.push(`- ${emailNameMap[email]} <${email}>`);
32
+ lines.push("");
33
+ }
34
+ return lines.join("\n");
35
+ }
36
+ //#endregion
37
+ export { getReleaseNotes };
@@ -0,0 +1,13 @@
1
+ //#region src/git-commit.d.ts
2
+ type GitCommit = {
3
+ hash: string;
4
+ author: {
5
+ name: string;
6
+ email: string;
7
+ };
8
+ subject: string;
9
+ body: string | undefined;
10
+ date: Date;
11
+ };
12
+ //#endregion
13
+ export { GitCommit };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import { t as waitForChildProcess } from "./wait-for-child-process-lyAoE4WE.mjs";
2
+ import { spawn } from "node:child_process";
3
+ //#region src/internal/run-git-log.ts
4
+ async function runGitLog(paths, fromRef, toRef = "HEAD", cwd = process.cwd()) {
5
+ const args = [
6
+ "--no-pager",
7
+ "log",
8
+ ...fromRef ? [`${fromRef}..${toRef}`] : [],
9
+ "--date=iso-strict",
10
+ "--no-color",
11
+ "--no-decorate",
12
+ "--no-merges",
13
+ `--format=%H%x1f%an%x1f%ae%x1f%ad%x1f%s%x1f%b%x1e`,
14
+ "--",
15
+ ...paths
16
+ ];
17
+ console.log(args);
18
+ const { stdout } = await waitForChildProcess(spawn("git", args, { cwd }));
19
+ return stdout;
20
+ }
21
+ //#endregion
22
+ //#region src/list-commits-since.ts
23
+ /** List the commits since a ref in specific dirs. */
24
+ async function listCommitsSince(options) {
25
+ console.log(`Listing commits ${options.since ? "since " + options.since : "for all time"} in:`, options.dirs);
26
+ return parseGitLog(await runGitLog(options.dirs, options.since));
27
+ }
28
+ function parseGitLog(log) {
29
+ return log.split("").map((r) => r.trim()).filter(Boolean).map((record) => {
30
+ const [hash, authorName, authorEmail, date, subject, body] = record.split("");
31
+ return {
32
+ hash,
33
+ author: {
34
+ name: authorName,
35
+ email: authorEmail
36
+ },
37
+ date: new Date(date),
38
+ subject,
39
+ body: body?.trim() || void 0
40
+ };
41
+ });
42
+ }
43
+ //#endregion
44
+ export { parseGitLog as n, listCommitsSince as t };
@@ -0,0 +1,11 @@
1
+ import { GitCommit } from "./git-commit.mjs";
2
+
3
+ //#region src/list-commits-since.d.ts
4
+ /** List the commits since a ref in specific dirs. */
5
+ declare function listCommitsSince(options: {
6
+ dirs: string[];
7
+ since: string | undefined;
8
+ }): Promise<GitCommit[]>;
9
+ declare function parseGitLog(log: string): GitCommit[];
10
+ //#endregion
11
+ export { listCommitsSince, parseGitLog };
@@ -0,0 +1,2 @@
1
+ import { n as parseGitLog, t as listCommitsSince } from "./list-commits-since-DycWgHTi.mjs";
2
+ export { listCommitsSince, parseGitLog };
@@ -0,0 +1,6 @@
1
+ import { ChangelogSection } from "./changelog-section.mjs";
2
+
3
+ //#region src/parse-changelog.d.ts
4
+ declare function parseChangelog(changelog: string): ChangelogSection[];
5
+ //#endregion
6
+ export { parseChangelog };
@@ -0,0 +1,23 @@
1
+ //#region src/parse-changelog.ts
2
+ function parseChangelog(changelog) {
3
+ const lines = changelog.split(/\n\r?/);
4
+ let results = [];
5
+ let current;
6
+ for (const line of lines) if (line.startsWith("## ")) {
7
+ if (current) results.push({
8
+ header: current.header,
9
+ body: current.lines.join("\n").trim()
10
+ });
11
+ current = {
12
+ header: line.slice(3),
13
+ lines: []
14
+ };
15
+ } else if (current) current.lines.push(line);
16
+ if (current) results.push({
17
+ header: current.header,
18
+ body: current.lines.join("\n").trim()
19
+ });
20
+ return results;
21
+ }
22
+ //#endregion
23
+ export { parseChangelog };
@@ -0,0 +1,8 @@
1
+ import { ConventionalCommit } from "./conventional-commit.mjs";
2
+ import { GitCommit } from "./git-commit.mjs";
3
+
4
+ //#region src/parse-commit.d.ts
5
+ /** Convert a commit to a {@link ConventionalCommit}, returning `undefined` for unknown formats */
6
+ declare function parseCommit(commit: GitCommit): ConventionalCommit | undefined;
7
+ //#endregion
8
+ export { parseCommit };