githublogen 0.0.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.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # githublogen
2
+
3
+ Generate changelog for GitHub releases from [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), powered by [changelogithub](https://github.com/antfu/changelogithub).
4
+
5
+ ## Usage
6
+
7
+ In GitHub Actions:
8
+
9
+ ```yml
10
+ # .github/workflows/release.yml
11
+
12
+ name: Release
13
+
14
+ permissions:
15
+ contents: write
16
+
17
+ on:
18
+ push:
19
+ tags:
20
+ - "v*"
21
+
22
+ jobs:
23
+ release:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v3
27
+ with:
28
+ fetch-depth: 0
29
+
30
+ - uses: actions/setup-node@v3
31
+ with:
32
+ node-version: 16.x
33
+
34
+ - run: npx githublogen
35
+ env:
36
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
37
+ ```
38
+
39
+ It will be trigged whenever you push a tag to GitHub that starts with `v`.
40
+
41
+ ## Configuration
42
+
43
+ You can put a configuration file in the project root, named as `githublogen.config.{json,ts,js,mjs,cjs}`, `.githublogenrc` or use the `githublogen` field in `package.json`.
44
+
45
+ ## Preview Locally
46
+
47
+ ```bash
48
+ npx githublogen --dry
49
+ ```
50
+
51
+
52
+
53
+
54
+
package/dist/cli.cjs ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs/promises');
5
+ const kolorist = require('kolorist');
6
+ const cac = require('cac');
7
+ const index = require('./index.cjs');
8
+ require('ohmyfetch');
9
+ require('convert-gitmoji');
10
+
11
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
12
+
13
+ const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
14
+ const cac__default = /*#__PURE__*/_interopDefaultCompat(cac);
15
+
16
+ const version = "0.0.1";
17
+
18
+ const cli = cac__default("githublogen");
19
+ cli.version(version).option("-t, --token <path>", "GitHub Token").option("--from <ref>", "From tag").option("--to <ref>", "To tag").option("--github <path>", "GitHub Repository, e.g. soybeanjs/githublogen").option("--name <name>", "Name of the release").option("--contributors", "Show contributors section").option("--prerelease", "Mark release as prerelease").option("-d, --draft", "Mark release as draft").option("--output <path>", "Output to file instead of sending to GitHub").option("--capitalize", "Should capitalize for each comment message").option("--emoji", "Use emojis in section titles", { default: true }).option("--group", "Nest commit messages under their scopes").option("--dry", "Dry run").help();
20
+ cli.command("").action(async (args) => {
21
+ args.token = args.token || process.env.GITHUB_TOKEN;
22
+ try {
23
+ console.log();
24
+ console.log(kolorist.dim(`${kolorist.bold("github")}logen `) + kolorist.dim(`v${version}`));
25
+ const { config, md, commits } = await index.generate(args);
26
+ console.log(kolorist.cyan(config.from) + kolorist.dim(" -> ") + kolorist.blue(config.to) + kolorist.dim(` (${commits.length} commits)`));
27
+ console.log(kolorist.dim("--------------"));
28
+ console.log();
29
+ console.log(md.replace(/&nbsp;/g, ""));
30
+ console.log();
31
+ console.log(kolorist.dim("--------------"));
32
+ if (config.dry) {
33
+ console.log(kolorist.yellow("Dry run. Release skipped."));
34
+ return;
35
+ }
36
+ if (!config.token) {
37
+ console.error(kolorist.red("No GitHub token found, specify it via GITHUB_TOKEN env. Release skipped."));
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+ if (typeof config.output === "string") {
42
+ await fs__default.writeFile(config.output, md, "utf-8");
43
+ console.log(kolorist.yellow(`Saved to ${config.output}`));
44
+ return;
45
+ }
46
+ if (!await index.hasTagOnGitHub(config.to, config)) {
47
+ console.error(kolorist.yellow(`Current ref "${kolorist.bold(config.to)}" is not available as tags on GitHub. Release skipped.`));
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ if (!commits.length && await index.isRepoShallow()) {
52
+ console.error(
53
+ kolorist.yellow(
54
+ "The repo seems to be clone shallowly, which make changelog failed to generate. You might want to specify `fetch-depth: 0` in your CI config."
55
+ )
56
+ );
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+ await index.sendRelease(config, md);
61
+ } catch (e) {
62
+ console.error(kolorist.red(String(e)));
63
+ if (e?.stack)
64
+ console.error(kolorist.dim(e.stack?.split("\n").slice(1).join("\n")));
65
+ process.exit(1);
66
+ }
67
+ });
68
+ cli.parse();
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.mjs ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import { dim, bold, cyan, blue, yellow, red } from 'kolorist';
4
+ import cac from 'cac';
5
+ import { generate, hasTagOnGitHub, isRepoShallow, sendRelease } from './index.mjs';
6
+ import 'ohmyfetch';
7
+ import 'convert-gitmoji';
8
+
9
+ const version = "0.0.1";
10
+
11
+ const cli = cac("githublogen");
12
+ cli.version(version).option("-t, --token <path>", "GitHub Token").option("--from <ref>", "From tag").option("--to <ref>", "To tag").option("--github <path>", "GitHub Repository, e.g. soybeanjs/githublogen").option("--name <name>", "Name of the release").option("--contributors", "Show contributors section").option("--prerelease", "Mark release as prerelease").option("-d, --draft", "Mark release as draft").option("--output <path>", "Output to file instead of sending to GitHub").option("--capitalize", "Should capitalize for each comment message").option("--emoji", "Use emojis in section titles", { default: true }).option("--group", "Nest commit messages under their scopes").option("--dry", "Dry run").help();
13
+ cli.command("").action(async (args) => {
14
+ args.token = args.token || process.env.GITHUB_TOKEN;
15
+ try {
16
+ console.log();
17
+ console.log(dim(`${bold("github")}logen `) + dim(`v${version}`));
18
+ const { config, md, commits } = await generate(args);
19
+ console.log(cyan(config.from) + dim(" -> ") + blue(config.to) + dim(` (${commits.length} commits)`));
20
+ console.log(dim("--------------"));
21
+ console.log();
22
+ console.log(md.replace(/&nbsp;/g, ""));
23
+ console.log();
24
+ console.log(dim("--------------"));
25
+ if (config.dry) {
26
+ console.log(yellow("Dry run. Release skipped."));
27
+ return;
28
+ }
29
+ if (!config.token) {
30
+ console.error(red("No GitHub token found, specify it via GITHUB_TOKEN env. Release skipped."));
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ if (typeof config.output === "string") {
35
+ await fs.writeFile(config.output, md, "utf-8");
36
+ console.log(yellow(`Saved to ${config.output}`));
37
+ return;
38
+ }
39
+ if (!await hasTagOnGitHub(config.to, config)) {
40
+ console.error(yellow(`Current ref "${bold(config.to)}" is not available as tags on GitHub. Release skipped.`));
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ if (!commits.length && await isRepoShallow()) {
45
+ console.error(
46
+ yellow(
47
+ "The repo seems to be clone shallowly, which make changelog failed to generate. You might want to specify `fetch-depth: 0` in your CI config."
48
+ )
49
+ );
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+ await sendRelease(config, md);
54
+ } catch (e) {
55
+ console.error(red(String(e)));
56
+ if (e?.stack)
57
+ console.error(dim(e.stack?.split("\n").slice(1).join("\n")));
58
+ process.exit(1);
59
+ }
60
+ });
61
+ cli.parse();
package/dist/index.cjs ADDED
@@ -0,0 +1,420 @@
1
+ 'use strict';
2
+
3
+ const ohmyfetch = require('ohmyfetch');
4
+ const kolorist = require('kolorist');
5
+ const convertGitmoji = require('convert-gitmoji');
6
+
7
+ function partition(array, ...filters) {
8
+ const result = new Array(filters.length + 1).fill(null).map(() => []);
9
+ array.forEach((e, idx, arr) => {
10
+ let i = 0;
11
+ for (const filter of filters) {
12
+ if (filter(e, idx, arr)) {
13
+ result[i].push(e);
14
+ return;
15
+ }
16
+ i += 1;
17
+ }
18
+ result[i].push(e);
19
+ });
20
+ return result;
21
+ }
22
+ function notNullish(v) {
23
+ return v !== null && v !== void 0;
24
+ }
25
+ function groupBy(items, key, groups = {}) {
26
+ for (const item of items) {
27
+ const v = item[key];
28
+ groups[v] = groups[v] || [];
29
+ groups[v].push(item);
30
+ }
31
+ return groups;
32
+ }
33
+ function capitalize(str) {
34
+ return str.charAt(0).toUpperCase() + str.slice(1);
35
+ }
36
+ function join(array, glue = ", ", finalGlue = " and ") {
37
+ if (!array || array.length === 0)
38
+ return "";
39
+ if (array.length === 1)
40
+ return array[0];
41
+ if (array.length === 2)
42
+ return array.join(finalGlue);
43
+ return `${array.slice(0, -1).join(glue)}${finalGlue}${array.slice(-1)}`;
44
+ }
45
+
46
+ async function sendRelease(options, content) {
47
+ const headers = getHeaders(options);
48
+ let url = `https://api.github.com/repos/${options.github}/releases`;
49
+ let method = "POST";
50
+ try {
51
+ const exists = await ohmyfetch.$fetch(`https://api.github.com/repos/${options.github}/releases/tags/${options.to}`, {
52
+ headers
53
+ });
54
+ if (exists.url) {
55
+ url = exists.url;
56
+ method = "PATCH";
57
+ }
58
+ } catch (e) {
59
+ }
60
+ const body = {
61
+ body: content,
62
+ draft: options.draft || false,
63
+ name: options.name || options.to,
64
+ prerelease: options.prerelease,
65
+ tag_name: options.to
66
+ };
67
+ const webUrl = `https://github.com/${options.github}/releases/new?title=${encodeURIComponent(
68
+ String(body.name)
69
+ )}&body=${encodeURIComponent(String(body.body))}&tag=${encodeURIComponent(String(options.to))}&prerelease=${options.prerelease}`;
70
+ try {
71
+ console.log(kolorist.cyan(method === "POST" ? "Creating release notes..." : "Updating release notes..."));
72
+ const res = await ohmyfetch.$fetch(url, {
73
+ method,
74
+ body: JSON.stringify(body),
75
+ headers
76
+ });
77
+ console.log(kolorist.green(`Released on ${res.html_url}`));
78
+ } catch (e) {
79
+ console.log();
80
+ console.error(kolorist.red("Failed to create the release. Using the following link to create it manually:"));
81
+ console.error(kolorist.yellow(webUrl));
82
+ console.log();
83
+ throw e;
84
+ }
85
+ }
86
+ function getHeaders(options) {
87
+ return {
88
+ accept: "application/vnd.github.v3+json",
89
+ authorization: `token ${options.token}`
90
+ };
91
+ }
92
+ async function resolveAuthorInfo(options, info) {
93
+ if (info.login)
94
+ return info;
95
+ if (!options.token)
96
+ return info;
97
+ const authorInfo = { ...info };
98
+ try {
99
+ const data = await ohmyfetch.$fetch(`https://api.github.com/search/users?q=${encodeURIComponent(info.email)}`, {
100
+ headers: getHeaders(options)
101
+ });
102
+ authorInfo.login = data.items[0].login;
103
+ } catch {
104
+ }
105
+ if (info.login)
106
+ return info;
107
+ if (info.commits.length) {
108
+ try {
109
+ const data = await ohmyfetch.$fetch(`https://api.github.com/repos/${options.github}/commits/${info.commits[0]}`, {
110
+ headers: getHeaders(options)
111
+ });
112
+ authorInfo.login = data.author.login;
113
+ } catch (e) {
114
+ }
115
+ }
116
+ return authorInfo;
117
+ }
118
+ async function resolveAuthors(commits, options) {
119
+ const map = /* @__PURE__ */ new Map();
120
+ commits.forEach((commit) => {
121
+ commit.resolvedAuthors = commit.authors.map((a, idx) => {
122
+ if (!a.email || !a.name)
123
+ return null;
124
+ if (!map.has(a.email)) {
125
+ map.set(a.email, {
126
+ commits: [],
127
+ name: a.name,
128
+ email: a.email
129
+ });
130
+ }
131
+ const info = map.get(a.email);
132
+ if (idx === 0)
133
+ info.commits.push(commit.shortHash);
134
+ return info;
135
+ }).filter(notNullish);
136
+ });
137
+ const authors = Array.from(map.values());
138
+ const resolved = await Promise.all(authors.map((info) => resolveAuthorInfo(options, info)));
139
+ const loginSet = /* @__PURE__ */ new Set();
140
+ const nameSet = /* @__PURE__ */ new Set();
141
+ return resolved.sort((a, b) => (a.login || a.name).localeCompare(b.login || b.name)).filter((i) => {
142
+ if (i.login && loginSet.has(i.login))
143
+ return false;
144
+ if (i.login) {
145
+ loginSet.add(i.login);
146
+ } else {
147
+ if (nameSet.has(i.name))
148
+ return false;
149
+ nameSet.add(i.name);
150
+ }
151
+ return true;
152
+ });
153
+ }
154
+ async function hasTagOnGitHub(tag, options) {
155
+ try {
156
+ await ohmyfetch.$fetch(`https://api.github.com/repos/${options.github}/git/ref/tags/${tag}`, {
157
+ headers: getHeaders(options)
158
+ });
159
+ return true;
160
+ } catch (e) {
161
+ return false;
162
+ }
163
+ }
164
+
165
+ async function getGitHubRepo() {
166
+ const url = await execCommand("git", ["config", "--get", "remote.origin.url"]);
167
+ const match = url.match(/github\.com[\/:]([\w\d._-]+?)\/([\w\d._-]+?)(\.git)?$/i);
168
+ if (!match)
169
+ throw new Error(`Can not parse GitHub repo from url ${url}`);
170
+ return `${match[1]}/${match[2]}`;
171
+ }
172
+ async function getCurrentGitBranch() {
173
+ const result1 = await execCommand("git", ["tag", "--points-at", "HEAD"]);
174
+ const result2 = await execCommand("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
175
+ return result1 || result2;
176
+ }
177
+ async function isRepoShallow() {
178
+ return (await execCommand("git", ["rev-parse", "--is-shallow-repository"])).trim() === "true";
179
+ }
180
+ async function getLastGitTag(delta = 0) {
181
+ const tags = await execCommand("git", ["--no-pager", "tag", "-l", "--sort=creatordate"]).then((r) => r.split("\n"));
182
+ return tags[tags.length + delta - 1];
183
+ }
184
+ async function isRefGitTag(to) {
185
+ const { execa } = await import('execa');
186
+ try {
187
+ await execa("git", ["show-ref", "--verify", `refs/tags/${to}`], { reject: true });
188
+ return true;
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
193
+ function getFirstGitCommit() {
194
+ return execCommand("git", ["rev-list", "--max-parents=0", "HEAD"]);
195
+ }
196
+ function isPrerelease(version) {
197
+ return !/^[^.]*[\d.]+$/.test(version);
198
+ }
199
+ async function execCommand(cmd, args) {
200
+ const { execa } = await import('execa');
201
+ const res = await execa(cmd, args);
202
+ return res.stdout.trim();
203
+ }
204
+ async function getGitDiff(from, to = "HEAD") {
205
+ const r = await execCommand("git", [
206
+ "--no-pager",
207
+ "log",
208
+ `${from ? `${from}...` : ""}${to}`,
209
+ '--pretty="----%n%s|%h|%an|%ae%n%b"',
210
+ "--name-status"
211
+ ]);
212
+ return r.split("----\n").splice(1).map((line) => {
213
+ const [firstLine, ..._body] = line.split("\n");
214
+ const [message, shortHash, authorName, authorEmail] = firstLine.split("|");
215
+ const $r = {
216
+ message,
217
+ shortHash,
218
+ author: { name: authorName, email: authorEmail },
219
+ body: _body.join("\n")
220
+ };
221
+ return $r;
222
+ });
223
+ }
224
+
225
+ const emojisRE = /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g;
226
+ function formatReferences(references, github, type) {
227
+ const refs = references.filter((i) => {
228
+ if (type === "issues")
229
+ return i.type === "issue" || i.type === "pull-request";
230
+ return i.type === "hash";
231
+ }).map((ref) => {
232
+ if (!github)
233
+ return ref.value;
234
+ if (ref.type === "pull-request" || ref.type === "issue")
235
+ return `https://github.com/${github}/issues/${ref.value.slice(1)}`;
236
+ return `[<samp>(${ref.value.slice(0, 5)})</samp>](https://github.com/${github}/commit/${ref.value})`;
237
+ });
238
+ const referencesString = join(refs).trim();
239
+ if (type === "issues")
240
+ return referencesString && `in ${referencesString}`;
241
+ return referencesString;
242
+ }
243
+ function formatLine(commit, options) {
244
+ const prRefs = formatReferences(commit.references, options.github, "issues");
245
+ const hashRefs = formatReferences(commit.references, options.github, "hash");
246
+ let authors = join([
247
+ ...new Set(commit.resolvedAuthors?.map((i) => i.login ? `@${i.login}` : `**${i.name}**`))
248
+ ])?.trim();
249
+ if (authors)
250
+ authors = `by ${authors}`;
251
+ let refs = [authors, prRefs, hashRefs].filter((i) => i?.trim()).join(" ");
252
+ if (refs)
253
+ refs = `&nbsp;-&nbsp; ${refs}`;
254
+ const description = options.capitalize ? capitalize(commit.description) : commit.description;
255
+ return [description, refs].filter((i) => i?.trim()).join(" ");
256
+ }
257
+ function formatTitle(name, options) {
258
+ let formatName = name.trim();
259
+ if (!options.emoji) {
260
+ formatName = name.replace(emojisRE, "").trim();
261
+ }
262
+ return `### &nbsp;&nbsp;&nbsp;${formatName}`;
263
+ }
264
+ function formatSection(commits, sectionName, options) {
265
+ if (!commits.length)
266
+ return [];
267
+ const lines = ["", formatTitle(sectionName, options), ""];
268
+ const scopes = groupBy(commits, "scope");
269
+ let useScopeGroup = options.group;
270
+ if (!Object.entries(scopes).some(([k, v]) => k && v.length > 1))
271
+ useScopeGroup = false;
272
+ Object.keys(scopes).sort().forEach((scope) => {
273
+ let padding = "";
274
+ let prefix = "";
275
+ const scopeText = `**${options.scopeMap[scope] || scope}**`;
276
+ if (scope && (useScopeGroup === true || useScopeGroup === "multiple" && scopes[scope].length > 1)) {
277
+ lines.push(`- ${scopeText}:`);
278
+ padding = " ";
279
+ } else if (scope) {
280
+ prefix = `${scopeText}: `;
281
+ }
282
+ lines.push(...scopes[scope].reverse().map((commit) => `${padding}- ${prefix}${formatLine(commit, options)}`));
283
+ });
284
+ return lines;
285
+ }
286
+ function generateMarkdown(commits, options) {
287
+ const lines = [];
288
+ const [breaking, changes] = partition(commits, (c) => c.isBreaking);
289
+ const group = groupBy(changes, "type");
290
+ lines.push(...formatSection(breaking, options.titles.breakingChanges, options));
291
+ for (const type of Object.keys(options.types)) {
292
+ const items = group[type] || [];
293
+ lines.push(...formatSection(items, options.types[type].title, options));
294
+ }
295
+ if (!lines.length)
296
+ lines.push("*No significant changes*");
297
+ const url = `https://github.com/${options.github}/compare/${options.from}...${options.to}`;
298
+ lines.push("", `##### &nbsp;&nbsp;&nbsp;&nbsp;[View changes on GitHub](${url})`);
299
+ return convertGitmoji.convert(lines.join("\n").trim(), true);
300
+ }
301
+
302
+ function defineConfig(config) {
303
+ return config;
304
+ }
305
+ const defaultConfig = {
306
+ scopeMap: {},
307
+ types: {
308
+ feat: { title: "\u{1F680} Features" },
309
+ fix: { title: "\u{1F41E} Bug Fixes" },
310
+ perf: { title: "\u{1F525} Performance" },
311
+ refactor: { title: "\u{1F485} Refactors" },
312
+ docs: { title: "\u{1F4D6} Documentation" },
313
+ build: { title: "\u{1F4E6} Build" },
314
+ types: { title: "\u{1F30A} Types" },
315
+ chore: { title: "\u{1F3E1} Chore" },
316
+ examples: { title: "\u{1F3C0} Examples" },
317
+ test: { title: "\u2705 Tests" },
318
+ style: { title: "\u{1F3A8} Styles" },
319
+ ci: { title: "\u{1F916} CI" }
320
+ },
321
+ titles: {
322
+ breakingChanges: "\u{1F6A8} Breaking Changes"
323
+ },
324
+ contributors: true,
325
+ capitalize: true,
326
+ group: true
327
+ };
328
+ async function resolveConfig(options) {
329
+ const { loadConfig } = await import('c12');
330
+ const config = await loadConfig({
331
+ name: "githublogen",
332
+ defaults: defaultConfig,
333
+ overrides: options
334
+ }).then((r) => r.config || defaultConfig);
335
+ config.from = config.from || await getLastGitTag();
336
+ config.to = config.to || await getCurrentGitBranch();
337
+ config.github = config.github || await getGitHubRepo();
338
+ config.prerelease = config.prerelease ?? isPrerelease(config.to);
339
+ if (config.to === config.from)
340
+ config.from = await getLastGitTag(-1) || await getFirstGitCommit();
341
+ return config;
342
+ }
343
+
344
+ const ConventionalCommitRegex = /(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
345
+ const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gim;
346
+ const PullRequestRE = /\([a-z]*(#\d+)\s*\)/gm;
347
+ const IssueRE = /(#\d+)/gm;
348
+ function parseGitCommit(commit, config) {
349
+ const match = commit.message.match(ConventionalCommitRegex);
350
+ if (!match?.groups) {
351
+ return null;
352
+ }
353
+ const type = match.groups.type;
354
+ let scope = match.groups.scope || "";
355
+ scope = config.scopeMap[scope] || scope;
356
+ const isBreaking = Boolean(match.groups.breaking);
357
+ let description = match.groups.description;
358
+ const references = [];
359
+ for (const m of description.matchAll(PullRequestRE)) {
360
+ references.push({ type: "pull-request", value: m[1] });
361
+ }
362
+ for (const m of description.matchAll(IssueRE)) {
363
+ if (!references.some((i) => i.value === m[1])) {
364
+ references.push({ type: "issue", value: m[1] });
365
+ }
366
+ }
367
+ references.push({ value: commit.shortHash, type: "hash" });
368
+ description = description.replace(PullRequestRE, "").trim();
369
+ const authors = [commit.author];
370
+ const matchs = commit.body.matchAll(CoAuthoredByRegex);
371
+ for (const $match of matchs) {
372
+ const { name = "", email = "" } = $match.groups || {};
373
+ const author = {
374
+ name: name.trim(),
375
+ email: email.trim()
376
+ };
377
+ authors.push(author);
378
+ }
379
+ return {
380
+ ...commit,
381
+ authors,
382
+ description,
383
+ type,
384
+ scope,
385
+ references,
386
+ isBreaking
387
+ };
388
+ }
389
+ function parseCommits(commits, config) {
390
+ return commits.map((commit) => parseGitCommit(commit, config)).filter(notNullish);
391
+ }
392
+
393
+ async function generate(options) {
394
+ const resolved = await resolveConfig(options);
395
+ const rawCommits = await getGitDiff(resolved.from, resolved.to);
396
+ const commits = parseCommits(rawCommits, resolved);
397
+ if (resolved.contributors)
398
+ await resolveAuthors(commits, resolved);
399
+ const md = generateMarkdown(commits, resolved);
400
+ return { config: resolved, md, commits };
401
+ }
402
+
403
+ exports.defineConfig = defineConfig;
404
+ exports.generate = generate;
405
+ exports.generateMarkdown = generateMarkdown;
406
+ exports.getCurrentGitBranch = getCurrentGitBranch;
407
+ exports.getFirstGitCommit = getFirstGitCommit;
408
+ exports.getGitDiff = getGitDiff;
409
+ exports.getGitHubRepo = getGitHubRepo;
410
+ exports.getLastGitTag = getLastGitTag;
411
+ exports.hasTagOnGitHub = hasTagOnGitHub;
412
+ exports.isPrerelease = isPrerelease;
413
+ exports.isRefGitTag = isRefGitTag;
414
+ exports.isRepoShallow = isRepoShallow;
415
+ exports.parseCommits = parseCommits;
416
+ exports.parseGitCommit = parseGitCommit;
417
+ exports.resolveAuthorInfo = resolveAuthorInfo;
418
+ exports.resolveAuthors = resolveAuthors;
419
+ exports.resolveConfig = resolveConfig;
420
+ exports.sendRelease = sendRelease;
@@ -0,0 +1,125 @@
1
+ type SemverBumpType = 'major' | 'premajor' | 'minor' | 'preminor' | 'patch' | 'prepatch' | 'prerelease';
2
+ interface ChangelogConfig {
3
+ cwd: string;
4
+ types: Record<string, {
5
+ title: string;
6
+ semver?: SemverBumpType;
7
+ }>;
8
+ scopeMap: Record<string, string>;
9
+ github: string;
10
+ from: string;
11
+ to: string;
12
+ newVersion?: string;
13
+ output: string | boolean;
14
+ }
15
+ interface GitCommitAuthor {
16
+ name: string;
17
+ email: string;
18
+ }
19
+ interface RawGitCommit {
20
+ message: string;
21
+ body: string;
22
+ shortHash: string;
23
+ author: GitCommitAuthor;
24
+ }
25
+ interface Reference {
26
+ type: 'hash' | 'issue' | 'pull-request';
27
+ value: string;
28
+ }
29
+ interface GitCommit extends RawGitCommit {
30
+ description: string;
31
+ type: string;
32
+ scope: string;
33
+ references: Reference[];
34
+ authors: GitCommitAuthor[];
35
+ isBreaking: boolean;
36
+ }
37
+ interface Commit extends GitCommit {
38
+ resolvedAuthors?: AuthorInfo[];
39
+ }
40
+ interface ChangelogOptions extends Partial<ChangelogConfig> {
41
+ /**
42
+ * Dry run. Skip releasing to GitHub.
43
+ */
44
+ dry?: boolean;
45
+ /**
46
+ * Whether to include contributors in release notes.
47
+ *
48
+ * @default true
49
+ */
50
+ contributors?: boolean;
51
+ /**
52
+ * Name of the release
53
+ */
54
+ name?: string;
55
+ /**
56
+ * Mark the release as a draft
57
+ */
58
+ draft?: boolean;
59
+ /**
60
+ * Mark the release as prerelease
61
+ */
62
+ prerelease?: boolean;
63
+ /**
64
+ * GitHub Token
65
+ */
66
+ token?: string;
67
+ /**
68
+ * Custom titles
69
+ */
70
+ titles?: {
71
+ breakingChanges?: string;
72
+ };
73
+ /**
74
+ * Capitalize commit messages
75
+ * @default true
76
+ */
77
+ capitalize?: boolean;
78
+ /**
79
+ * Nest commit messages under their scopes
80
+ * @default true
81
+ */
82
+ group?: boolean | 'multiple';
83
+ /**
84
+ * Use emojis in section titles
85
+ * @default true
86
+ */
87
+ emoji?: boolean;
88
+ }
89
+ type ResolvedChangelogOptions = Required<ChangelogOptions>;
90
+ interface AuthorInfo {
91
+ commits: string[];
92
+ login?: string;
93
+ email: string;
94
+ name: string;
95
+ }
96
+
97
+ declare function sendRelease(options: ChangelogOptions, content: string): Promise<void>;
98
+ declare function resolveAuthorInfo(options: ChangelogOptions, info: AuthorInfo): Promise<AuthorInfo>;
99
+ declare function resolveAuthors(commits: Commit[], options: ChangelogOptions): Promise<AuthorInfo[]>;
100
+ declare function hasTagOnGitHub(tag: string, options: ChangelogOptions): Promise<boolean>;
101
+
102
+ declare function getGitHubRepo(): Promise<string>;
103
+ declare function getCurrentGitBranch(): Promise<string>;
104
+ declare function isRepoShallow(): Promise<boolean>;
105
+ declare function getLastGitTag(delta?: number): Promise<string>;
106
+ declare function isRefGitTag(to: string): Promise<boolean>;
107
+ declare function getFirstGitCommit(): Promise<string>;
108
+ declare function isPrerelease(version: string): boolean;
109
+ declare function getGitDiff(from: string | undefined, to?: string): Promise<RawGitCommit[]>;
110
+
111
+ declare function generateMarkdown(commits: Commit[], options: ResolvedChangelogOptions): string;
112
+
113
+ declare function generate(options: ChangelogOptions): Promise<{
114
+ config: Required<ChangelogOptions>;
115
+ md: string;
116
+ commits: GitCommit[];
117
+ }>;
118
+
119
+ declare function defineConfig(config: ChangelogOptions): ChangelogOptions;
120
+ declare function resolveConfig(options: ChangelogOptions): Promise<Required<ChangelogOptions>>;
121
+
122
+ declare function parseGitCommit(commit: RawGitCommit, config: ChangelogConfig): GitCommit | null;
123
+ declare function parseCommits(commits: RawGitCommit[], config: ChangelogConfig): GitCommit[];
124
+
125
+ export { AuthorInfo, ChangelogConfig, ChangelogOptions, Commit, GitCommit, GitCommitAuthor, RawGitCommit, Reference, ResolvedChangelogOptions, SemverBumpType, defineConfig, generate, generateMarkdown, getCurrentGitBranch, getFirstGitCommit, getGitDiff, getGitHubRepo, getLastGitTag, hasTagOnGitHub, isPrerelease, isRefGitTag, isRepoShallow, parseCommits, parseGitCommit, resolveAuthorInfo, resolveAuthors, resolveConfig, sendRelease };
package/dist/index.mjs ADDED
@@ -0,0 +1,401 @@
1
+ import { $fetch } from 'ohmyfetch';
2
+ import { cyan, green, red, yellow } from 'kolorist';
3
+ import { convert } from 'convert-gitmoji';
4
+
5
+ function partition(array, ...filters) {
6
+ const result = new Array(filters.length + 1).fill(null).map(() => []);
7
+ array.forEach((e, idx, arr) => {
8
+ let i = 0;
9
+ for (const filter of filters) {
10
+ if (filter(e, idx, arr)) {
11
+ result[i].push(e);
12
+ return;
13
+ }
14
+ i += 1;
15
+ }
16
+ result[i].push(e);
17
+ });
18
+ return result;
19
+ }
20
+ function notNullish(v) {
21
+ return v !== null && v !== void 0;
22
+ }
23
+ function groupBy(items, key, groups = {}) {
24
+ for (const item of items) {
25
+ const v = item[key];
26
+ groups[v] = groups[v] || [];
27
+ groups[v].push(item);
28
+ }
29
+ return groups;
30
+ }
31
+ function capitalize(str) {
32
+ return str.charAt(0).toUpperCase() + str.slice(1);
33
+ }
34
+ function join(array, glue = ", ", finalGlue = " and ") {
35
+ if (!array || array.length === 0)
36
+ return "";
37
+ if (array.length === 1)
38
+ return array[0];
39
+ if (array.length === 2)
40
+ return array.join(finalGlue);
41
+ return `${array.slice(0, -1).join(glue)}${finalGlue}${array.slice(-1)}`;
42
+ }
43
+
44
+ async function sendRelease(options, content) {
45
+ const headers = getHeaders(options);
46
+ let url = `https://api.github.com/repos/${options.github}/releases`;
47
+ let method = "POST";
48
+ try {
49
+ const exists = await $fetch(`https://api.github.com/repos/${options.github}/releases/tags/${options.to}`, {
50
+ headers
51
+ });
52
+ if (exists.url) {
53
+ url = exists.url;
54
+ method = "PATCH";
55
+ }
56
+ } catch (e) {
57
+ }
58
+ const body = {
59
+ body: content,
60
+ draft: options.draft || false,
61
+ name: options.name || options.to,
62
+ prerelease: options.prerelease,
63
+ tag_name: options.to
64
+ };
65
+ const webUrl = `https://github.com/${options.github}/releases/new?title=${encodeURIComponent(
66
+ String(body.name)
67
+ )}&body=${encodeURIComponent(String(body.body))}&tag=${encodeURIComponent(String(options.to))}&prerelease=${options.prerelease}`;
68
+ try {
69
+ console.log(cyan(method === "POST" ? "Creating release notes..." : "Updating release notes..."));
70
+ const res = await $fetch(url, {
71
+ method,
72
+ body: JSON.stringify(body),
73
+ headers
74
+ });
75
+ console.log(green(`Released on ${res.html_url}`));
76
+ } catch (e) {
77
+ console.log();
78
+ console.error(red("Failed to create the release. Using the following link to create it manually:"));
79
+ console.error(yellow(webUrl));
80
+ console.log();
81
+ throw e;
82
+ }
83
+ }
84
+ function getHeaders(options) {
85
+ return {
86
+ accept: "application/vnd.github.v3+json",
87
+ authorization: `token ${options.token}`
88
+ };
89
+ }
90
+ async function resolveAuthorInfo(options, info) {
91
+ if (info.login)
92
+ return info;
93
+ if (!options.token)
94
+ return info;
95
+ const authorInfo = { ...info };
96
+ try {
97
+ const data = await $fetch(`https://api.github.com/search/users?q=${encodeURIComponent(info.email)}`, {
98
+ headers: getHeaders(options)
99
+ });
100
+ authorInfo.login = data.items[0].login;
101
+ } catch {
102
+ }
103
+ if (info.login)
104
+ return info;
105
+ if (info.commits.length) {
106
+ try {
107
+ const data = await $fetch(`https://api.github.com/repos/${options.github}/commits/${info.commits[0]}`, {
108
+ headers: getHeaders(options)
109
+ });
110
+ authorInfo.login = data.author.login;
111
+ } catch (e) {
112
+ }
113
+ }
114
+ return authorInfo;
115
+ }
116
+ async function resolveAuthors(commits, options) {
117
+ const map = /* @__PURE__ */ new Map();
118
+ commits.forEach((commit) => {
119
+ commit.resolvedAuthors = commit.authors.map((a, idx) => {
120
+ if (!a.email || !a.name)
121
+ return null;
122
+ if (!map.has(a.email)) {
123
+ map.set(a.email, {
124
+ commits: [],
125
+ name: a.name,
126
+ email: a.email
127
+ });
128
+ }
129
+ const info = map.get(a.email);
130
+ if (idx === 0)
131
+ info.commits.push(commit.shortHash);
132
+ return info;
133
+ }).filter(notNullish);
134
+ });
135
+ const authors = Array.from(map.values());
136
+ const resolved = await Promise.all(authors.map((info) => resolveAuthorInfo(options, info)));
137
+ const loginSet = /* @__PURE__ */ new Set();
138
+ const nameSet = /* @__PURE__ */ new Set();
139
+ return resolved.sort((a, b) => (a.login || a.name).localeCompare(b.login || b.name)).filter((i) => {
140
+ if (i.login && loginSet.has(i.login))
141
+ return false;
142
+ if (i.login) {
143
+ loginSet.add(i.login);
144
+ } else {
145
+ if (nameSet.has(i.name))
146
+ return false;
147
+ nameSet.add(i.name);
148
+ }
149
+ return true;
150
+ });
151
+ }
152
+ async function hasTagOnGitHub(tag, options) {
153
+ try {
154
+ await $fetch(`https://api.github.com/repos/${options.github}/git/ref/tags/${tag}`, {
155
+ headers: getHeaders(options)
156
+ });
157
+ return true;
158
+ } catch (e) {
159
+ return false;
160
+ }
161
+ }
162
+
163
+ async function getGitHubRepo() {
164
+ const url = await execCommand("git", ["config", "--get", "remote.origin.url"]);
165
+ const match = url.match(/github\.com[\/:]([\w\d._-]+?)\/([\w\d._-]+?)(\.git)?$/i);
166
+ if (!match)
167
+ throw new Error(`Can not parse GitHub repo from url ${url}`);
168
+ return `${match[1]}/${match[2]}`;
169
+ }
170
+ async function getCurrentGitBranch() {
171
+ const result1 = await execCommand("git", ["tag", "--points-at", "HEAD"]);
172
+ const result2 = await execCommand("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
173
+ return result1 || result2;
174
+ }
175
+ async function isRepoShallow() {
176
+ return (await execCommand("git", ["rev-parse", "--is-shallow-repository"])).trim() === "true";
177
+ }
178
+ async function getLastGitTag(delta = 0) {
179
+ const tags = await execCommand("git", ["--no-pager", "tag", "-l", "--sort=creatordate"]).then((r) => r.split("\n"));
180
+ return tags[tags.length + delta - 1];
181
+ }
182
+ async function isRefGitTag(to) {
183
+ const { execa } = await import('execa');
184
+ try {
185
+ await execa("git", ["show-ref", "--verify", `refs/tags/${to}`], { reject: true });
186
+ return true;
187
+ } catch {
188
+ return false;
189
+ }
190
+ }
191
+ function getFirstGitCommit() {
192
+ return execCommand("git", ["rev-list", "--max-parents=0", "HEAD"]);
193
+ }
194
+ function isPrerelease(version) {
195
+ return !/^[^.]*[\d.]+$/.test(version);
196
+ }
197
+ async function execCommand(cmd, args) {
198
+ const { execa } = await import('execa');
199
+ const res = await execa(cmd, args);
200
+ return res.stdout.trim();
201
+ }
202
+ async function getGitDiff(from, to = "HEAD") {
203
+ const r = await execCommand("git", [
204
+ "--no-pager",
205
+ "log",
206
+ `${from ? `${from}...` : ""}${to}`,
207
+ '--pretty="----%n%s|%h|%an|%ae%n%b"',
208
+ "--name-status"
209
+ ]);
210
+ return r.split("----\n").splice(1).map((line) => {
211
+ const [firstLine, ..._body] = line.split("\n");
212
+ const [message, shortHash, authorName, authorEmail] = firstLine.split("|");
213
+ const $r = {
214
+ message,
215
+ shortHash,
216
+ author: { name: authorName, email: authorEmail },
217
+ body: _body.join("\n")
218
+ };
219
+ return $r;
220
+ });
221
+ }
222
+
223
+ const emojisRE = /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g;
224
+ function formatReferences(references, github, type) {
225
+ const refs = references.filter((i) => {
226
+ if (type === "issues")
227
+ return i.type === "issue" || i.type === "pull-request";
228
+ return i.type === "hash";
229
+ }).map((ref) => {
230
+ if (!github)
231
+ return ref.value;
232
+ if (ref.type === "pull-request" || ref.type === "issue")
233
+ return `https://github.com/${github}/issues/${ref.value.slice(1)}`;
234
+ return `[<samp>(${ref.value.slice(0, 5)})</samp>](https://github.com/${github}/commit/${ref.value})`;
235
+ });
236
+ const referencesString = join(refs).trim();
237
+ if (type === "issues")
238
+ return referencesString && `in ${referencesString}`;
239
+ return referencesString;
240
+ }
241
+ function formatLine(commit, options) {
242
+ const prRefs = formatReferences(commit.references, options.github, "issues");
243
+ const hashRefs = formatReferences(commit.references, options.github, "hash");
244
+ let authors = join([
245
+ ...new Set(commit.resolvedAuthors?.map((i) => i.login ? `@${i.login}` : `**${i.name}**`))
246
+ ])?.trim();
247
+ if (authors)
248
+ authors = `by ${authors}`;
249
+ let refs = [authors, prRefs, hashRefs].filter((i) => i?.trim()).join(" ");
250
+ if (refs)
251
+ refs = `&nbsp;-&nbsp; ${refs}`;
252
+ const description = options.capitalize ? capitalize(commit.description) : commit.description;
253
+ return [description, refs].filter((i) => i?.trim()).join(" ");
254
+ }
255
+ function formatTitle(name, options) {
256
+ let formatName = name.trim();
257
+ if (!options.emoji) {
258
+ formatName = name.replace(emojisRE, "").trim();
259
+ }
260
+ return `### &nbsp;&nbsp;&nbsp;${formatName}`;
261
+ }
262
+ function formatSection(commits, sectionName, options) {
263
+ if (!commits.length)
264
+ return [];
265
+ const lines = ["", formatTitle(sectionName, options), ""];
266
+ const scopes = groupBy(commits, "scope");
267
+ let useScopeGroup = options.group;
268
+ if (!Object.entries(scopes).some(([k, v]) => k && v.length > 1))
269
+ useScopeGroup = false;
270
+ Object.keys(scopes).sort().forEach((scope) => {
271
+ let padding = "";
272
+ let prefix = "";
273
+ const scopeText = `**${options.scopeMap[scope] || scope}**`;
274
+ if (scope && (useScopeGroup === true || useScopeGroup === "multiple" && scopes[scope].length > 1)) {
275
+ lines.push(`- ${scopeText}:`);
276
+ padding = " ";
277
+ } else if (scope) {
278
+ prefix = `${scopeText}: `;
279
+ }
280
+ lines.push(...scopes[scope].reverse().map((commit) => `${padding}- ${prefix}${formatLine(commit, options)}`));
281
+ });
282
+ return lines;
283
+ }
284
+ function generateMarkdown(commits, options) {
285
+ const lines = [];
286
+ const [breaking, changes] = partition(commits, (c) => c.isBreaking);
287
+ const group = groupBy(changes, "type");
288
+ lines.push(...formatSection(breaking, options.titles.breakingChanges, options));
289
+ for (const type of Object.keys(options.types)) {
290
+ const items = group[type] || [];
291
+ lines.push(...formatSection(items, options.types[type].title, options));
292
+ }
293
+ if (!lines.length)
294
+ lines.push("*No significant changes*");
295
+ const url = `https://github.com/${options.github}/compare/${options.from}...${options.to}`;
296
+ lines.push("", `##### &nbsp;&nbsp;&nbsp;&nbsp;[View changes on GitHub](${url})`);
297
+ return convert(lines.join("\n").trim(), true);
298
+ }
299
+
300
+ function defineConfig(config) {
301
+ return config;
302
+ }
303
+ const defaultConfig = {
304
+ scopeMap: {},
305
+ types: {
306
+ feat: { title: "\u{1F680} Features" },
307
+ fix: { title: "\u{1F41E} Bug Fixes" },
308
+ perf: { title: "\u{1F525} Performance" },
309
+ refactor: { title: "\u{1F485} Refactors" },
310
+ docs: { title: "\u{1F4D6} Documentation" },
311
+ build: { title: "\u{1F4E6} Build" },
312
+ types: { title: "\u{1F30A} Types" },
313
+ chore: { title: "\u{1F3E1} Chore" },
314
+ examples: { title: "\u{1F3C0} Examples" },
315
+ test: { title: "\u2705 Tests" },
316
+ style: { title: "\u{1F3A8} Styles" },
317
+ ci: { title: "\u{1F916} CI" }
318
+ },
319
+ titles: {
320
+ breakingChanges: "\u{1F6A8} Breaking Changes"
321
+ },
322
+ contributors: true,
323
+ capitalize: true,
324
+ group: true
325
+ };
326
+ async function resolveConfig(options) {
327
+ const { loadConfig } = await import('c12');
328
+ const config = await loadConfig({
329
+ name: "githublogen",
330
+ defaults: defaultConfig,
331
+ overrides: options
332
+ }).then((r) => r.config || defaultConfig);
333
+ config.from = config.from || await getLastGitTag();
334
+ config.to = config.to || await getCurrentGitBranch();
335
+ config.github = config.github || await getGitHubRepo();
336
+ config.prerelease = config.prerelease ?? isPrerelease(config.to);
337
+ if (config.to === config.from)
338
+ config.from = await getLastGitTag(-1) || await getFirstGitCommit();
339
+ return config;
340
+ }
341
+
342
+ const ConventionalCommitRegex = /(?<type>[a-z]+)(\((?<scope>.+)\))?(?<breaking>!)?: (?<description>.+)/i;
343
+ const CoAuthoredByRegex = /co-authored-by:\s*(?<name>.+)(<(?<email>.+)>)/gim;
344
+ const PullRequestRE = /\([a-z]*(#\d+)\s*\)/gm;
345
+ const IssueRE = /(#\d+)/gm;
346
+ function parseGitCommit(commit, config) {
347
+ const match = commit.message.match(ConventionalCommitRegex);
348
+ if (!match?.groups) {
349
+ return null;
350
+ }
351
+ const type = match.groups.type;
352
+ let scope = match.groups.scope || "";
353
+ scope = config.scopeMap[scope] || scope;
354
+ const isBreaking = Boolean(match.groups.breaking);
355
+ let description = match.groups.description;
356
+ const references = [];
357
+ for (const m of description.matchAll(PullRequestRE)) {
358
+ references.push({ type: "pull-request", value: m[1] });
359
+ }
360
+ for (const m of description.matchAll(IssueRE)) {
361
+ if (!references.some((i) => i.value === m[1])) {
362
+ references.push({ type: "issue", value: m[1] });
363
+ }
364
+ }
365
+ references.push({ value: commit.shortHash, type: "hash" });
366
+ description = description.replace(PullRequestRE, "").trim();
367
+ const authors = [commit.author];
368
+ const matchs = commit.body.matchAll(CoAuthoredByRegex);
369
+ for (const $match of matchs) {
370
+ const { name = "", email = "" } = $match.groups || {};
371
+ const author = {
372
+ name: name.trim(),
373
+ email: email.trim()
374
+ };
375
+ authors.push(author);
376
+ }
377
+ return {
378
+ ...commit,
379
+ authors,
380
+ description,
381
+ type,
382
+ scope,
383
+ references,
384
+ isBreaking
385
+ };
386
+ }
387
+ function parseCommits(commits, config) {
388
+ return commits.map((commit) => parseGitCommit(commit, config)).filter(notNullish);
389
+ }
390
+
391
+ async function generate(options) {
392
+ const resolved = await resolveConfig(options);
393
+ const rawCommits = await getGitDiff(resolved.from, resolved.to);
394
+ const commits = parseCommits(rawCommits, resolved);
395
+ if (resolved.contributors)
396
+ await resolveAuthors(commits, resolved);
397
+ const md = generateMarkdown(commits, resolved);
398
+ return { config: resolved, md, commits };
399
+ }
400
+
401
+ export { defineConfig, generate, generateMarkdown, getCurrentGitBranch, getFirstGitCommit, getGitDiff, getGitHubRepo, getLastGitTag, hasTagOnGitHub, isPrerelease, isRefGitTag, isRepoShallow, parseCommits, parseGitCommit, resolveAuthorInfo, resolveAuthors, resolveConfig, sendRelease };
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "githublogen",
3
+ "type": "module",
4
+ "version": "0.0.1",
5
+ "sideEffects": false,
6
+ "author": {
7
+ "name": "SoybeanJS",
8
+ "email": "honghuangdc@gmail.com",
9
+ "url": "https://github.com/soybeanjs"
10
+ },
11
+ "license": "MIT",
12
+ "homepage": "https://github.com/soybeanjs/githublogen#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/soybeanjs/githublogen.git"
16
+ },
17
+ "bugs": "https://github.com/soybeanjs/githublogen/issues",
18
+ "publishConfig": {
19
+ "registry": "https://registry.npmjs.org/"
20
+ },
21
+ "keywords": [
22
+ "github",
23
+ "release",
24
+ "releases",
25
+ "conventional",
26
+ "changelog",
27
+ "log"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "require": "./dist/index.cjs",
33
+ "import": "./dist/index.mjs"
34
+ }
35
+ },
36
+ "main": "./dist/index.mjs",
37
+ "module": "./dist/index.mjs",
38
+ "types": "./dist/index.d.ts",
39
+ "bin": {
40
+ "githublogen": "./dist/cli.mjs"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "engines": {
46
+ "node": ">=12.0.0"
47
+ },
48
+ "dependencies": {
49
+ "c12": "1.4.1",
50
+ "cac": "6.7.14",
51
+ "convert-gitmoji": "0.1.3",
52
+ "execa": "7.1.1",
53
+ "kolorist": "1.8.0",
54
+ "ohmyfetch": "0.4.21"
55
+ },
56
+ "devDependencies": {
57
+ "@soybeanjs/cli": "0.2.11",
58
+ "@types/node": "20.2.5",
59
+ "bumpp": "9.1.0",
60
+ "eslint": "8.41.0",
61
+ "eslint-config-soybeanjs": "0.4.6",
62
+ "lint-staged": "13.2.2",
63
+ "simple-git-hooks": "2.8.1",
64
+ "typescript": "5.0.4",
65
+ "unbuild": "1.2.1"
66
+ },
67
+ "simple-git-hooks": {
68
+ "commit-msg": "pnpm soy git-commit-verify",
69
+ "pre-commit": "pnpm lint-staged"
70
+ },
71
+ "lint-staged": {
72
+ "*.{js,mjs,jsx,ts,mts,tsx,json,vue,svelte}": "eslint . --fix",
73
+ "*.!{js,mjs,jsx,ts,mts,tsx,json,vue,svelte}": "format"
74
+ },
75
+ "scripts": {
76
+ "build": "unbuild",
77
+ "lint": "eslint . --fix",
78
+ "format": "soy prettier-format",
79
+ "commit": "soy git-commit",
80
+ "cleanup": "soy cleanup",
81
+ "update-pkg": "soy update-pkg",
82
+ "update-version": "bumpp --commit --push --tag",
83
+ "publish-pkg": "pnpm -r publish --access public",
84
+ "release": "pnpm update-version && pnpm publish-pkg"
85
+ }
86
+ }