keepchanges 0.0.3 → 1.0.0-beta.1

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.
@@ -0,0 +1,190 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { normalizeFull } from "verkit";
3
+ //#region src/config.ts
4
+ const defaultConfig = {
5
+ cli: {
6
+ output: "CHANGELOG.md",
7
+ to: "HEAD",
8
+ dry: false,
9
+ commit: false,
10
+ release: false,
11
+ author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>",
12
+ draft: false
13
+ },
14
+ changelog: {
15
+ emoji: true,
16
+ capitalize: true,
17
+ group: true,
18
+ breakingChanges: {
19
+ emoji: "🚨",
20
+ title: "Breaking Changes"
21
+ },
22
+ types: {
23
+ feat: {
24
+ emoji: "🚀",
25
+ title: "Features"
26
+ },
27
+ fix: {
28
+ emoji: "🐞",
29
+ title: "Bug Fixes"
30
+ },
31
+ perf: {
32
+ emoji: "🏎",
33
+ title: "Performance"
34
+ }
35
+ },
36
+ messages: {
37
+ noSignificantChanges: "No significant changes",
38
+ viewChanges: "View changes on {provider}"
39
+ }
40
+ }
41
+ };
42
+ function resolveChangelogConfig(overrides = {}) {
43
+ return {
44
+ ...defaultConfig.changelog,
45
+ ...overrides,
46
+ breakingChanges: {
47
+ ...defaultConfig.changelog.breakingChanges,
48
+ ...overrides.breakingChanges
49
+ },
50
+ types: overrides.types ?? defaultConfig.changelog.types,
51
+ messages: {
52
+ ...defaultConfig.changelog.messages,
53
+ ...overrides.messages
54
+ }
55
+ };
56
+ }
57
+ //#endregion
58
+ //#region src/changelog.ts
59
+ function generateChangelog(options, overrides = {}) {
60
+ const config = resolveChangelogConfig(overrides);
61
+ const changes = [...renderSection(options.commits.filter((commit) => commit.isBreaking), config.breakingChanges, config, options.repository), ...Object.entries(config.types).flatMap(([type, section]) => renderSection(options.commits.filter((commit) => !commit.isBreaking && commit.type === type), section, config, options.repository))];
62
+ const body = [...changes.length ? changes : [`*${config.messages.noSignificantChanges}*`], ...options.repository && options.comparisonFrom ? ["", `##### &nbsp;&nbsp;&nbsp;&nbsp;[${config.messages.viewChanges.replace("{provider}", options.repository.provider.name)}](${options.repository.provider.compareUrl(options.repository, options.comparisonFrom, `v${options.version}`)})`] : []].join("\n").trim();
63
+ return {
64
+ body,
65
+ release: [
66
+ `## v${options.version}`,
67
+ "",
68
+ body,
69
+ ""
70
+ ].join("\n")
71
+ };
72
+ }
73
+ async function readChangelog(path) {
74
+ return readFile(path, "utf8").catch((error) => {
75
+ if (error.code === "ENOENT") return "# Changelog\n";
76
+ throw error;
77
+ });
78
+ }
79
+ function writeChangelog(path, changelog) {
80
+ return writeFile(path, changelog);
81
+ }
82
+ function hasRelease(changelog, version) {
83
+ const releaseVersion = normalizeFull(version);
84
+ return releaseHeadings(changelog).some((heading) => heading.version === releaseVersion);
85
+ }
86
+ function insertRelease(changelog, release) {
87
+ const releaseVersion = releaseHeadings(release)[0]?.version;
88
+ const headings = releaseHeadings(changelog);
89
+ const existingReleaseIndex = headings.findIndex((heading) => heading.version === releaseVersion);
90
+ if (existingReleaseIndex !== -1) {
91
+ const start = headings[existingReleaseIndex].index;
92
+ const end = headings[existingReleaseIndex + 1]?.index ?? changelog.length;
93
+ changelog = changelog.slice(0, start) + changelog.slice(end);
94
+ }
95
+ const firstRelease = releaseHeadings(changelog)[0];
96
+ if (!firstRelease) return `${changelog.trimEnd()}\n\n${release.trim()}\n`;
97
+ const preamble = changelog.slice(0, firstRelease.index).trimEnd();
98
+ const history = changelog.slice(firstRelease.index).trim();
99
+ return `${preamble}\n\n${release.trim()}\n\n${history}\n`;
100
+ }
101
+ function releaseHeadings(changelog) {
102
+ return [...changelog.matchAll(/^##[^\S\r\n]+(\S+)/gm)].flatMap((heading) => {
103
+ const version = normalizeFull(heading[1]);
104
+ return version ? [{
105
+ index: heading.index,
106
+ version
107
+ }] : [];
108
+ });
109
+ }
110
+ function renderSection(commits, section, config, repository) {
111
+ const renderedCommits = commits.map((commit) => {
112
+ const reference = repository ? `[<samp>(${commit.hash.slice(0, 5)})</samp>](${repository.provider.commitUrl(repository, commit.hash)})` : "";
113
+ const authorNames = commit.authors.map((author) => author.login ? `@${author.login}` : `**${escapeHtml(author.name)}**`);
114
+ const authors = authorNames.length > 1 ? `${authorNames.slice(0, -1).join(", ")} and ${authorNames.at(-1)}` : authorNames[0] || "";
115
+ const pullRequests = commit.pullRequests.map((pullRequest) => repository ? `[${pullRequest}](${repository.provider.pullRequestUrl(repository, pullRequest)})` : pullRequest).join(", ");
116
+ const details = [
117
+ authors ? `by ${authors}` : "",
118
+ pullRequests ? `in ${pullRequests}` : "",
119
+ reference
120
+ ].filter(Boolean).join(" ");
121
+ const description = config.capitalize ? capitalize(commit.description) : commit.description;
122
+ const suffix = details ? ` &nbsp;-&nbsp; ${details}` : "";
123
+ return {
124
+ scope: commit.scope,
125
+ line: `${escapeHtml(description)}${suffix}`
126
+ };
127
+ });
128
+ if (!renderedCommits.length) return [];
129
+ const commitsByScope = /* @__PURE__ */ new Map();
130
+ for (const { scope, line } of renderedCommits) {
131
+ const lines = commitsByScope.get(scope) || [];
132
+ lines.push(line);
133
+ commitsByScope.set(scope, lines);
134
+ }
135
+ const lines = config.group && [...commitsByScope].some(([scope, lines]) => Boolean(scope) && lines.length > 1) ? [...commitsByScope.keys()].sort().flatMap((scope) => {
136
+ const scopedLines = (commitsByScope.get(scope) || []).reverse().map((line) => `${scope ? " " : ""}- ${line}`);
137
+ return scope ? [`- **${escapeHtml(scope)}**:`, ...scopedLines] : scopedLines;
138
+ }) : renderedCommits.reverse().map(({ scope, line }) => {
139
+ return `- ${scope ? `**${escapeHtml(scope)}**: ` : ""}${line}`;
140
+ });
141
+ return [
142
+ "",
143
+ `### ${[config.emoji ? section.emoji : "", section.title].filter(Boolean).join(" ")}`,
144
+ "",
145
+ ...lines
146
+ ];
147
+ }
148
+ function capitalize(value) {
149
+ return value.charAt(0).toUpperCase() + value.slice(1);
150
+ }
151
+ function escapeHtml(value) {
152
+ const htmlEntities = {
153
+ "&": "&amp;",
154
+ "<": "&lt;",
155
+ ">": "&gt;",
156
+ "\"": "&quot;",
157
+ "'": "&#39;"
158
+ };
159
+ return value.replace(/[&<>"']/g, (character) => htmlEntities[character]);
160
+ }
161
+ //#endregion
162
+ //#region src/commit.ts
163
+ function parseCommits(commits) {
164
+ return commits.map(parseCommit).filter((commit) => commit !== null);
165
+ }
166
+ function parseCommit(commit) {
167
+ const match = /^(?<type>[a-z]+)(?:\((?<scope>[^()\r\n]+)\))?(?<breaking>!)?: (?<description>.+)$/i.exec(commit.subject);
168
+ if (!match?.groups) return null;
169
+ const pullRequestPattern = /\([ a-z]*(#\d+)\s*\)/gi;
170
+ const pullRequests = [...new Set([...match.groups.description.matchAll(pullRequestPattern)].map((reference) => reference[1]))];
171
+ const authors = [commit.author];
172
+ for (const coAuthor of commit.body.matchAll(/^Co-Authored-By:([^<\r\n]+)<([^>\r\n]+)>[^\S\r\n]*$/gim)) {
173
+ const email = coAuthor[2].trim();
174
+ if (!authors.some((author) => author.email === email)) authors.push({
175
+ name: coAuthor[1].trim(),
176
+ email
177
+ });
178
+ }
179
+ return {
180
+ hash: commit.hash,
181
+ authors: authors.filter((author) => !/\[bot\]|dependabot|\(bot\)/i.test(author.name)),
182
+ type: match.groups.type.toLowerCase(),
183
+ scope: match.groups.scope || "",
184
+ description: match.groups.description.replace(pullRequestPattern, "").trim(),
185
+ pullRequests,
186
+ isBreaking: Boolean(match.groups.breaking) || /^BREAKING(?: |-)CHANGE:/im.test(commit.body)
187
+ };
188
+ }
189
+ //#endregion
190
+ export { insertRelease as a, defaultConfig as c, hasRelease as i, resolveChangelogConfig as l, parseCommits as n, readChangelog as o, generateChangelog as r, writeChangelog as s, parseCommit as t };
@@ -0,0 +1,152 @@
1
+ //#region src/repository.d.ts
2
+ interface RepositoryAuthor {
3
+ name: string;
4
+ email: string;
5
+ login?: string;
6
+ }
7
+ interface RepositoryCommit {
8
+ hash: string;
9
+ authors: RepositoryAuthor[];
10
+ }
11
+ interface Repository {
12
+ provider: RepositoryProvider;
13
+ path: string;
14
+ webUrl: string;
15
+ }
16
+ interface RepositoryRelease {
17
+ tag: string;
18
+ name: string;
19
+ body: string;
20
+ prerelease: boolean;
21
+ draft: boolean;
22
+ }
23
+ interface RepositoryReleaseResult {
24
+ url: string;
25
+ action: 'created' | 'updated';
26
+ }
27
+ interface RepositoryProvider {
28
+ name: string;
29
+ parse: (source: string) => Repository | undefined;
30
+ token: (explicit: string | undefined, env: NodeJS.ProcessEnv) => string | undefined;
31
+ commitUrl: (repository: Repository, hash: string) => string;
32
+ pullRequestUrl: (repository: Repository, reference: string) => string;
33
+ compareUrl: (repository: Repository, from: string, to: string) => string;
34
+ resolveAuthors?: (commits: RepositoryCommit[], repository: Repository, token: string, fetch: typeof globalThis.fetch) => Promise<void>;
35
+ publishRelease?: (repository: Repository, release: RepositoryRelease, token: string, fetch: typeof globalThis.fetch) => Promise<RepositoryReleaseResult>;
36
+ manualReleaseUrl?: (repository: Repository, release: RepositoryRelease) => string;
37
+ }
38
+ //#endregion
39
+ //#region src/git.d.ts
40
+ interface RawCommit {
41
+ hash: string;
42
+ subject: string;
43
+ body: string;
44
+ author: RepositoryAuthor;
45
+ }
46
+ //#endregion
47
+ //#region src/commit.d.ts
48
+ interface Commit extends RepositoryCommit {
49
+ type: string;
50
+ scope: string;
51
+ description: string;
52
+ pullRequests: string[];
53
+ isBreaking: boolean;
54
+ }
55
+ declare function parseCommits(commits: RawCommit[]): Commit[];
56
+ declare function parseCommit(commit: RawCommit): Commit | null;
57
+ //#endregion
58
+ //#region src/config.d.ts
59
+ interface ChangelogSectionConfig {
60
+ emoji: string;
61
+ title: string;
62
+ }
63
+ interface ChangelogMessages {
64
+ noSignificantChanges: string;
65
+ viewChanges: string;
66
+ }
67
+ interface ChangelogConfig {
68
+ emoji: boolean;
69
+ capitalize: boolean;
70
+ group: boolean;
71
+ breakingChanges: ChangelogSectionConfig;
72
+ types: Record<string, ChangelogSectionConfig>;
73
+ messages: ChangelogMessages;
74
+ }
75
+ interface ChangelogConfigOverrides {
76
+ emoji?: boolean;
77
+ capitalize?: boolean;
78
+ group?: boolean;
79
+ breakingChanges?: Partial<ChangelogSectionConfig>;
80
+ types?: Record<string, ChangelogSectionConfig>;
81
+ messages?: Partial<ChangelogMessages>;
82
+ }
83
+ interface KeepChangesConfig {
84
+ cli: {
85
+ output: string;
86
+ to: string;
87
+ dry: boolean;
88
+ commit: boolean;
89
+ release: boolean;
90
+ author: string;
91
+ draft: boolean;
92
+ };
93
+ changelog: ChangelogConfig;
94
+ }
95
+ declare const defaultConfig: {
96
+ cli: {
97
+ output: string;
98
+ to: string;
99
+ dry: false;
100
+ commit: false;
101
+ release: false;
102
+ author: string;
103
+ draft: false;
104
+ };
105
+ changelog: {
106
+ emoji: true;
107
+ capitalize: true;
108
+ group: true;
109
+ breakingChanges: {
110
+ emoji: string;
111
+ title: string;
112
+ };
113
+ types: {
114
+ feat: {
115
+ emoji: string;
116
+ title: string;
117
+ };
118
+ fix: {
119
+ emoji: string;
120
+ title: string;
121
+ };
122
+ perf: {
123
+ emoji: string;
124
+ title: string;
125
+ };
126
+ };
127
+ messages: {
128
+ noSignificantChanges: string;
129
+ viewChanges: string;
130
+ };
131
+ };
132
+ };
133
+ declare function resolveChangelogConfig(overrides?: ChangelogConfigOverrides): ChangelogConfig;
134
+ //#endregion
135
+ //#region src/changelog.d.ts
136
+ interface GenerateChangelogOptions {
137
+ version: string;
138
+ commits: Commit[];
139
+ repository?: Repository;
140
+ comparisonFrom?: string;
141
+ }
142
+ interface GeneratedChangelog {
143
+ body: string;
144
+ release: string;
145
+ }
146
+ declare function generateChangelog(options: GenerateChangelogOptions, overrides?: ChangelogConfigOverrides): GeneratedChangelog;
147
+ declare function readChangelog(path: string): Promise<string>;
148
+ declare function writeChangelog(path: string, changelog: string): Promise<void>;
149
+ declare function hasRelease(changelog: string, version: string): boolean;
150
+ declare function insertRelease(changelog: string, release: string): string;
151
+ //#endregion
152
+ export { type ChangelogConfig, type ChangelogConfigOverrides, type ChangelogMessages, type ChangelogSectionConfig, type Commit, type GenerateChangelogOptions, type GeneratedChangelog, type KeepChangesConfig, type RawCommit, type Repository, type RepositoryAuthor, type RepositoryCommit, type RepositoryProvider, type RepositoryRelease, type RepositoryReleaseResult, defaultConfig, generateChangelog, hasRelease, insertRelease, parseCommit, parseCommits, readChangelog, resolveChangelogConfig, writeChangelog };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as insertRelease, c as defaultConfig, i as hasRelease, l as resolveChangelogConfig, n as parseCommits, o as readChangelog, r as generateChangelog, s as writeChangelog, t as parseCommit } from "./commit-FX77u6Sj.mjs";
2
+ export { defaultConfig, generateChangelog, hasRelease, insertRelease, parseCommit, parseCommits, readChangelog, resolveChangelogConfig, writeChangelog };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "keepchanges",
3
3
  "type": "module",
4
- "version": "0.0.3",
4
+ "version": "1.0.0-beta.1",
5
5
  "description": "Generate and maintain CHANGELOG.md from Conventional Commits.",
6
6
  "author": "edram",
7
7
  "license": "MIT",
@@ -20,12 +20,11 @@
20
20
  "git"
21
21
  ],
22
22
  "exports": {
23
- ".": "./dist/cli.mjs",
24
- "./cli": "./dist/cli.mjs",
23
+ ".": "./dist/index.mjs",
25
24
  "./package.json": "./package.json"
26
25
  },
27
26
  "bin": {
28
- "changelog": "./dist/cli.mjs"
27
+ "keepchanges": "./dist/cli.mjs"
29
28
  },
30
29
  "files": [
31
30
  "dist"