@chamoji/core 0.1.0 → 0.2.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/dist/changelog.d.ts +11 -1
- package/dist/changelog.js +56 -23
- package/dist/config.js +13 -5
- package/dist/group.js +1 -0
- package/dist/infer.d.ts +8 -0
- package/dist/infer.js +94 -0
- package/dist/parse.js +20 -1
- package/dist/render/html.js +8 -3
- package/dist/render/markdown.js +8 -3
- package/dist/render/shared.d.ts +13 -1
- package/dist/render/shared.js +42 -2
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
package/dist/changelog.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChamojiConfig, Changelog } from './types.js';
|
|
1
|
+
import type { ChamojiConfig, Changelog, CommitClassifier } from './types.js';
|
|
2
2
|
export type BuildChangelogOptions = {
|
|
3
3
|
cwd: string;
|
|
4
4
|
config: ChamojiConfig;
|
|
@@ -10,5 +10,15 @@ export type BuildChangelogOptions = {
|
|
|
10
10
|
release?: string;
|
|
11
11
|
/** `YYYY-MM-DD`; defaults to the current UTC date. */
|
|
12
12
|
today?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Optional AI step for commits no gitmoji, type or wording rule could
|
|
15
|
+
* sort. It never sees or overrides the others.
|
|
16
|
+
*/
|
|
17
|
+
classify?: CommitClassifier;
|
|
18
|
+
/**
|
|
19
|
+
* Which commits `classify` sees: every release (default), or only the
|
|
20
|
+
* unreleased section, which is all `next-version` needs.
|
|
21
|
+
*/
|
|
22
|
+
classifyReleases?: 'all' | 'unreleased';
|
|
13
23
|
};
|
|
14
24
|
export declare function buildChangelog(options: BuildChangelogOptions): Promise<Changelog>;
|
package/dist/changelog.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import semver from 'semver';
|
|
2
2
|
import { assertGitRepo, assertRevision, getRemoteWebUrl, listReleaseTags, readCommits, } from './git.js';
|
|
3
|
+
import { getGitmoji } from './gitmojis.js';
|
|
3
4
|
import { groupCommits } from './group.js';
|
|
4
5
|
import { parseCommit } from './parse.js';
|
|
5
6
|
import { bumpFor, nextVersion } from './version.js';
|
|
@@ -24,26 +25,20 @@ export async function buildChangelog(options) {
|
|
|
24
25
|
if (from && !tags.some(tag => tag.name === from)) {
|
|
25
26
|
throw new Error(`Unknown release tag for --from: ${from}`);
|
|
26
27
|
}
|
|
27
|
-
const
|
|
28
|
-
const commits = raw.map(parseCommit);
|
|
29
|
-
const bump = bumpFor(commits);
|
|
30
|
-
return {
|
|
31
|
-
...fields,
|
|
32
|
-
bump,
|
|
33
|
-
nextVersion: isUnreleased ? nextVersion(fields.previousTag, bump) : null,
|
|
34
|
-
groups: groupCommits(commits, config.groups),
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
const releases = [];
|
|
28
|
+
const pending = [];
|
|
38
29
|
const latest = tags[0]?.name ?? null;
|
|
39
30
|
const unreleased = await readCommits(cwd, latest ? `${latest}..${to}` : to);
|
|
40
31
|
if (unreleased.length > 0) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
32
|
+
pending.push({
|
|
33
|
+
fields: {
|
|
34
|
+
version: release ?? 'Unreleased',
|
|
35
|
+
tag: release ? futureTag(release, latest) : null,
|
|
36
|
+
previousTag: latest,
|
|
37
|
+
date: release ? today : null,
|
|
38
|
+
},
|
|
39
|
+
commits: unreleased.map(parseCommit),
|
|
40
|
+
isUnreleased: true,
|
|
41
|
+
});
|
|
47
42
|
}
|
|
48
43
|
for (const [index, tag] of tags.entries()) {
|
|
49
44
|
if (tag.name === from) {
|
|
@@ -51,14 +46,52 @@ export async function buildChangelog(options) {
|
|
|
51
46
|
}
|
|
52
47
|
const previousTag = tags[index + 1]?.name ?? null;
|
|
53
48
|
const raw = await readCommits(cwd, previousTag ? `${previousTag}..${tag.name}` : tag.name);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
pending.push({
|
|
50
|
+
fields: {
|
|
51
|
+
version: tag.name,
|
|
52
|
+
tag: tag.name,
|
|
53
|
+
previousTag,
|
|
54
|
+
date: tag.date.slice(0, 10) || null,
|
|
55
|
+
},
|
|
56
|
+
commits: raw.map(parseCommit),
|
|
57
|
+
isUnreleased: false,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (options.classify) {
|
|
61
|
+
const scope = options.classifyReleases === 'unreleased'
|
|
62
|
+
? pending.filter(item => item.isUnreleased)
|
|
63
|
+
: pending;
|
|
64
|
+
await applyClassifier(options.classify, scope.flatMap(item => item.commits));
|
|
60
65
|
}
|
|
66
|
+
const releases = pending.map(({ fields, commits, isUnreleased }) => {
|
|
67
|
+
const bump = bumpFor(commits);
|
|
68
|
+
return {
|
|
69
|
+
...fields,
|
|
70
|
+
bump,
|
|
71
|
+
nextVersion: isUnreleased ? nextVersion(fields.previousTag, bump) : null,
|
|
72
|
+
groups: groupCommits(commits, config.groups),
|
|
73
|
+
};
|
|
74
|
+
});
|
|
61
75
|
const repositoryUrl = config.repositoryUrl ?? (await getRemoteWebUrl(cwd));
|
|
62
76
|
return { ...(repositoryUrl ? { repositoryUrl } : {}), releases };
|
|
63
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Runs the classifier on unsorted commits and applies its picks in place.
|
|
80
|
+
* Unknown codes are ignored, and so is `boom`: only authors declare breaking
|
|
81
|
+
* changes.
|
|
82
|
+
*/
|
|
83
|
+
async function applyClassifier(classify, commits) {
|
|
84
|
+
const unsorted = commits.filter(commit => commit.gitmojiSource === null);
|
|
85
|
+
if (unsorted.length === 0) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const picks = await classify(unsorted);
|
|
89
|
+
for (const commit of unsorted) {
|
|
90
|
+
const code = picks.get(commit.hash);
|
|
91
|
+
if (code && code !== 'boom' && getGitmoji(code)) {
|
|
92
|
+
commit.gitmoji = code;
|
|
93
|
+
commit.gitmojiSource = 'ai';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
64
97
|
//# sourceMappingURL=changelog.js.map
|
package/dist/config.js
CHANGED
|
@@ -16,7 +16,6 @@ export const DEFAULT_GROUP_CONFIG = [
|
|
|
16
16
|
'white_check_mark',
|
|
17
17
|
'construction_worker',
|
|
18
18
|
'chart_with_upwards_trend',
|
|
19
|
-
'heavy_plus_sign',
|
|
20
19
|
'loud_sound',
|
|
21
20
|
'globe_with_meridians',
|
|
22
21
|
'camera_flash',
|
|
@@ -40,9 +39,6 @@ export const DEFAULT_GROUP_CONFIG = [
|
|
|
40
39
|
'zap',
|
|
41
40
|
'lipstick',
|
|
42
41
|
'rotating_light',
|
|
43
|
-
'arrow_down',
|
|
44
|
-
'arrow_up',
|
|
45
|
-
'pushpin',
|
|
46
42
|
'recycle',
|
|
47
43
|
'wrench',
|
|
48
44
|
'rewind',
|
|
@@ -72,7 +68,7 @@ export const DEFAULT_GROUP_CONFIG = [
|
|
|
72
68
|
{
|
|
73
69
|
group: 'removed',
|
|
74
70
|
label: 'Removed',
|
|
75
|
-
emojis: ['fire', '
|
|
71
|
+
emojis: ['fire', 'mute', 'coffin'],
|
|
76
72
|
},
|
|
77
73
|
{
|
|
78
74
|
group: 'fixed',
|
|
@@ -95,6 +91,18 @@ export const DEFAULT_GROUP_CONFIG = [
|
|
|
95
91
|
label: 'Security',
|
|
96
92
|
emojis: ['lock', 'closed_lock_with_key', 'passport_control'],
|
|
97
93
|
},
|
|
94
|
+
{
|
|
95
|
+
group: 'dependencies',
|
|
96
|
+
label: 'Dependencies',
|
|
97
|
+
emojis: [
|
|
98
|
+
'arrow_up',
|
|
99
|
+
'arrow_down',
|
|
100
|
+
'pushpin',
|
|
101
|
+
'heavy_plus_sign',
|
|
102
|
+
'heavy_minus_sign',
|
|
103
|
+
],
|
|
104
|
+
collapse: true,
|
|
105
|
+
},
|
|
98
106
|
{
|
|
99
107
|
group: 'useless',
|
|
100
108
|
label: 'Useless',
|
package/dist/group.js
CHANGED
package/dist/infer.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guesses a gitmoji for a plain commit subject ("Add x", "Fix y") so repos
|
|
3
|
+
* that use neither gitmoji nor Conventional Commits still get sections.
|
|
4
|
+
* Rules are checked in order; the first match wins. Verbs are matched as
|
|
5
|
+
* whole word forms ("fix", "fixes", "fixed", "fixing"), so "Fixture" or
|
|
6
|
+
* "Dropdown" don't count, and topic words only count near the start.
|
|
7
|
+
*/
|
|
8
|
+
export declare function inferGitmoji(subject: string): string | null;
|
package/dist/infer.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guesses a gitmoji for a plain commit subject ("Add x", "Fix y") so repos
|
|
3
|
+
* that use neither gitmoji nor Conventional Commits still get sections.
|
|
4
|
+
* Rules are checked in order; the first match wins. Verbs are matched as
|
|
5
|
+
* whole word forms ("fix", "fixes", "fixed", "fixing"), so "Fixture" or
|
|
6
|
+
* "Dropdown" don't count, and topic words only count near the start.
|
|
7
|
+
*/
|
|
8
|
+
/** A version-like token: `1.2.3`, `v4`, `^7.0`. */
|
|
9
|
+
const VERSION = String.raw `[~^]?v?\d[\w.-]*`;
|
|
10
|
+
const RULES = [
|
|
11
|
+
// Version-only and release commits (hidden by default).
|
|
12
|
+
[/^v?\d+\.\d+\.\d+\S*$/, 'bookmark'],
|
|
13
|
+
[
|
|
14
|
+
/^(?:release|releasing|prepare release(?: for)?|bump version(?: to)?)\s+v?\d|^(?:release|bump version)$/i,
|
|
15
|
+
'bookmark',
|
|
16
|
+
],
|
|
17
|
+
[/^(?:initial commit|init)\b/i, 'tada'],
|
|
18
|
+
[/^\[breaking\]/i, 'boom'],
|
|
19
|
+
[/^(?:require|drop support for)\s+node(?:\.js)?\s*\d+/i, 'boom'],
|
|
20
|
+
// Dependency updates: Dependabot, Renovate and hand-written ones.
|
|
21
|
+
[/^pin\s+(?:dev-?)?dependenc(?:y|ies)\b/i, 'pushpin'],
|
|
22
|
+
[
|
|
23
|
+
/^(?:bump|update|upgrade)\s+(?:all\s+)?(?:non-major\s+|minor\s+|patch\s+)?(?:dev[- ]?)?(?:dependencies|dependency|deps)\b/i,
|
|
24
|
+
'arrow_up',
|
|
25
|
+
],
|
|
26
|
+
[/^lock\s?file maintenance\b/i, 'arrow_up'],
|
|
27
|
+
[/^bump\s+the\s+\S+\s+group\b/i, 'arrow_up'],
|
|
28
|
+
[/^update\s+\S+\s+(?:action|monorepo|docker tag|digest)\b/i, 'arrow_up'],
|
|
29
|
+
[
|
|
30
|
+
new RegExp(String.raw `^(?:bump|update|upgrade|pin)\s+\S+\s+from\s+${VERSION}\s+to\s+${VERSION}`, 'i'),
|
|
31
|
+
'arrow_up',
|
|
32
|
+
],
|
|
33
|
+
[/^bump\s+`?[@\w./-]+`?\s*$/i, 'arrow_up'],
|
|
34
|
+
[
|
|
35
|
+
new RegExp(String.raw `^upgrad(?:e|es|ed|ing)\b.*\s${VERSION}`, 'i'),
|
|
36
|
+
'arrow_up',
|
|
37
|
+
],
|
|
38
|
+
// Topics, only when they are what the subject is about.
|
|
39
|
+
[/^(?:\w+\s+){0,2}(?:the\s+)?(?:readme|docs?|documentation)\b/i, 'memo'],
|
|
40
|
+
[/^document(?:s|ed|ing)?\b/i, 'memo'],
|
|
41
|
+
[/^(?:fix(?:es|ed)?\s+(?:an?\s+|the\s+)?typos?|typos?)\b/i, 'pencil2'],
|
|
42
|
+
[/^fix(?:es|ed|ing)?\s+(?:the\s+)?ci\b/i, 'green_heart'],
|
|
43
|
+
[
|
|
44
|
+
/^(?:add|adds|added|improve|update|fix)\s+(?:more\s+)?tests?\b|^tests?\b/i,
|
|
45
|
+
'white_check_mark',
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
/^fix(?:es|ed|ing)?\b.*\b(?:security|vulnerabilit(?:y|ies)|cve-\d+)/i,
|
|
49
|
+
'lock',
|
|
50
|
+
],
|
|
51
|
+
[/^(?:security|cve-\d+)\b/i, 'lock'],
|
|
52
|
+
[
|
|
53
|
+
/^(?:improve|boost|increase)\s+(?:\w+\s+)?(?:performance|perf|speed)\b|^(?:optimi[sz]e[sd]?|speed\s+up)\b|^make\b.*\bfaster\b/i,
|
|
54
|
+
'zap',
|
|
55
|
+
],
|
|
56
|
+
[/^(?:minor\s+)?(?:code\s+style|minor\s+code|style)\s+tweaks?\b/i, 'art'],
|
|
57
|
+
[/^add(?:s|ed)?\s+missing\b/i, 'bug'],
|
|
58
|
+
// The leading verb.
|
|
59
|
+
[
|
|
60
|
+
/^(?:add(?:s|ed|ing)?|implement(?:s|ed|ing)?|introduc(?:e|es|ed|ing)|creat(?:e|es|ed|ing)|support(?:s|ed|ing)?|export(?:s|ed|ing)?|allow(?:s|ed|ing)?|enabl(?:e|es|ed|ing))\b/i,
|
|
61
|
+
'sparkles',
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
/^(?:fix(?:es|ed|ing)?|resolv(?:e|es|ed|ing)|correct(?:s|ed|ing)?|repair(?:s|ed|ing)?|hotfix(?:es|ed)?)\b/i,
|
|
65
|
+
'bug',
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
/^(?:remov(?:e|es|ed|ing)|delet(?:e|es|ed|ing)|drop(?:s|ped|ping)?)\b/i,
|
|
69
|
+
'fire',
|
|
70
|
+
],
|
|
71
|
+
[/^deprecat(?:e|es|ed|ing)\b/i, 'wastebasket'],
|
|
72
|
+
[/^(?:renam(?:e|es|ed|ing)|mov(?:e|es|ed|ing))\b/i, 'truck'],
|
|
73
|
+
[
|
|
74
|
+
/^(?:refactor(?:s|ed|ing)?|clean(?:s|ed|ing)?|cleanup|simplif(?:y|ies|ied|ying)|restructur(?:e|es|ed|ing)|reorganiz(?:e|es|ed|ing))\b/i,
|
|
75
|
+
'recycle',
|
|
76
|
+
],
|
|
77
|
+
[/^downgrad(?:e|es|ed|ing)\b/i, 'arrow_down'],
|
|
78
|
+
[
|
|
79
|
+
/^(?:updat(?:e|es|ed|ing)|upgrad(?:e|es|ed|ing)|improv(?:e|es|ed|ing)|chang(?:e|es|ed|ing)|tweak(?:s|ed|ing)?|meta|use|replac(?:e|es|ed|ing)|rewrit(?:e|es|ing|ten)|migrat(?:e|es|ed|ing)|make|ensure|switch(?:es|ed|ing)?)\b/i,
|
|
80
|
+
'wrench',
|
|
81
|
+
],
|
|
82
|
+
];
|
|
83
|
+
/** Prefixes that say nothing about the change, like "Close #14 PR: ". */
|
|
84
|
+
const NOISE_PREFIX = /^(?:close[sd]?\s+#\d+\s+pr:\s*)/i;
|
|
85
|
+
export function inferGitmoji(subject) {
|
|
86
|
+
const text = subject.trim().replace(NOISE_PREFIX, '');
|
|
87
|
+
for (const [pattern, code] of RULES) {
|
|
88
|
+
if (pattern.test(text)) {
|
|
89
|
+
return code;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=infer.js.map
|
package/dist/parse.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { findGitmoji, normalizeEmoji } from './gitmojis.js';
|
|
2
|
+
import { inferGitmoji } from './infer.js';
|
|
2
3
|
/** Conventional Commits types we recognize, and the gitmoji each implies. */
|
|
3
4
|
const TYPE_TO_GITMOJI = {
|
|
4
5
|
feat: 'sparkles',
|
|
@@ -12,7 +13,13 @@ const TYPE_TO_GITMOJI = {
|
|
|
12
13
|
ci: 'construction_worker',
|
|
13
14
|
chore: 'wrench',
|
|
14
15
|
revert: 'rewind',
|
|
16
|
+
release: 'bookmark',
|
|
17
|
+
wip: 'construction',
|
|
18
|
+
deps: 'arrow_up',
|
|
19
|
+
security: 'lock',
|
|
15
20
|
};
|
|
21
|
+
/** Conventional scopes that mean "dependency update" under any type. */
|
|
22
|
+
const DEPENDENCY_SCOPES = new Set(['deps', 'deps-dev', 'dependencies']);
|
|
16
23
|
const CONVENTIONAL_PREFIX = /^(\w+)(?:\(([^)]+)\))?(!)?:\s*/;
|
|
17
24
|
const GITMOJI_CLI_SCOPE = /^\(([^)]+)\):\s*/;
|
|
18
25
|
const BREAKING_FOOTER = /^BREAKING[ -]CHANGE:/m;
|
|
@@ -27,6 +34,7 @@ export function parseCommit(raw) {
|
|
|
27
34
|
...original,
|
|
28
35
|
body: raw.body,
|
|
29
36
|
gitmoji: 'rewind',
|
|
37
|
+
gitmojiSource: 'rule',
|
|
30
38
|
type: null,
|
|
31
39
|
description: `Revert "${original.description}"`,
|
|
32
40
|
breaking: BREAKING_FOOTER.test(raw.body),
|
|
@@ -46,9 +54,11 @@ export function parseCommit(raw) {
|
|
|
46
54
|
text = text.slice(prefix[0].length);
|
|
47
55
|
}
|
|
48
56
|
let gitmoji = null;
|
|
57
|
+
let gitmojiSource = null;
|
|
49
58
|
const found = findGitmoji(text);
|
|
50
59
|
if (found) {
|
|
51
60
|
gitmoji = found.code;
|
|
61
|
+
gitmojiSource = 'gitmoji';
|
|
52
62
|
text = (text.slice(0, found.index) + text.slice(found.index + found.length))
|
|
53
63
|
.replaceAll(/\s{2,}/g, ' ')
|
|
54
64
|
.trim();
|
|
@@ -59,12 +69,21 @@ export function parseCommit(raw) {
|
|
|
59
69
|
text = text.slice(cliScope[0].length);
|
|
60
70
|
}
|
|
61
71
|
if (!gitmoji && type) {
|
|
62
|
-
gitmoji =
|
|
72
|
+
gitmoji =
|
|
73
|
+
scope && DEPENDENCY_SCOPES.has(scope.toLowerCase())
|
|
74
|
+
? 'arrow_up'
|
|
75
|
+
: (TYPE_TO_GITMOJI[type] ?? null);
|
|
76
|
+
gitmojiSource = gitmoji ? 'type' : null;
|
|
77
|
+
}
|
|
78
|
+
if (!gitmoji) {
|
|
79
|
+
gitmoji = inferGitmoji(text);
|
|
80
|
+
gitmojiSource = gitmoji ? 'rule' : null;
|
|
63
81
|
}
|
|
64
82
|
const { trailers: rawTrailers, ...rest } = raw;
|
|
65
83
|
return {
|
|
66
84
|
...rest,
|
|
67
85
|
gitmoji,
|
|
86
|
+
gitmojiSource,
|
|
68
87
|
type,
|
|
69
88
|
scope,
|
|
70
89
|
description: text.trim(),
|
package/dist/render/html.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { commitLink, emojiFor, issueLink, releaseLink, safeRepositoryUrl, splitIssues, } from './shared.js';
|
|
1
|
+
import { commitLink, DEPENDENCY_EMOJI, emojiFor, issueLink, releaseLink, safeRepositoryUrl, splitIssues, summarizeDependencies, } from './shared.js';
|
|
2
2
|
const ESCAPES = {
|
|
3
3
|
'&': '&',
|
|
4
4
|
'<': '<',
|
|
@@ -34,8 +34,13 @@ export function renderHtml(changelog) {
|
|
|
34
34
|
out.push(`<section id="${version}">`, `<h2>${link ? anchor(link, version) : version}${date}</h2>`);
|
|
35
35
|
for (const group of release.groups) {
|
|
36
36
|
out.push(`<h3>${escapeHtml(group.label)}</h3>`, '<ul>');
|
|
37
|
-
|
|
38
|
-
out.push(`<li>${
|
|
37
|
+
if (group.collapse) {
|
|
38
|
+
out.push(`<li>${DEPENDENCY_EMOJI} ${escapeHtml(summarizeDependencies(group.commits))}</li>`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
for (const commit of group.commits) {
|
|
42
|
+
out.push(`<li>${renderCommit(commit, url)}</li>`);
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
out.push('</ul>');
|
|
41
46
|
}
|
package/dist/render/markdown.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { commitLink, emojiFor, issueLink, releaseLink, safeRepositoryUrl, splitIssues, } from './shared.js';
|
|
1
|
+
import { commitLink, DEPENDENCY_EMOJI, emojiFor, issueLink, releaseLink, safeRepositoryUrl, splitIssues, summarizeDependencies, } from './shared.js';
|
|
2
2
|
/**
|
|
3
3
|
* Makes commit text inert outside code spans: no raw HTML, and no links or
|
|
4
4
|
* images, since squash-merged PR titles can come from anyone. Backticks are
|
|
@@ -27,8 +27,13 @@ export function renderMarkdown(changelog) {
|
|
|
27
27
|
lines.push(`## ${title}${release.date ? ` - ${release.date}` : ''}`, '');
|
|
28
28
|
for (const group of release.groups) {
|
|
29
29
|
lines.push(`### ${group.label}`, '');
|
|
30
|
-
|
|
31
|
-
lines.push(`- ${
|
|
30
|
+
if (group.collapse) {
|
|
31
|
+
lines.push(`- ${DEPENDENCY_EMOJI} ${escapeMarkdown(summarizeDependencies(group.commits))}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (const commit of group.commits) {
|
|
35
|
+
lines.push(`- ${renderCommit(commit, url)}`);
|
|
36
|
+
}
|
|
32
37
|
}
|
|
33
38
|
lines.push('');
|
|
34
39
|
}
|
package/dist/render/shared.d.ts
CHANGED
|
@@ -5,8 +5,20 @@ export declare function safeRepositoryUrl(changelog: Changelog): string | undefi
|
|
|
5
5
|
export declare function releaseLink(release: Release, url: string | undefined): string | undefined;
|
|
6
6
|
export declare function commitLink(commit: ParsedCommit, url: string): string;
|
|
7
7
|
export declare function issueLink(issue: string, url: string): string;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The commit's gitmoji as a character, `:code:` for retired ones, or ''.
|
|
10
|
+
* Gitmojis that chamoji guessed (from wording or AI) are not shown, so plain
|
|
11
|
+
* histories aren't decorated with emojis their authors never wrote.
|
|
12
|
+
*/
|
|
9
13
|
export declare function emojiFor(commit: ParsedCommit): string;
|
|
14
|
+
/**
|
|
15
|
+
* One-line summary of a collapsed group of dependency updates, e.g.
|
|
16
|
+
* `9 dependency updates: jest, vite, … and 2 more`. Names come from
|
|
17
|
+
* Dependabot/Renovate-style subjects; a Dependabot group PR counts as the
|
|
18
|
+
* number of updates it bundles.
|
|
19
|
+
*/
|
|
20
|
+
export declare function summarizeDependencies(commits: ParsedCommit[]): string;
|
|
21
|
+
export declare const DEPENDENCY_EMOJI: string;
|
|
10
22
|
/**
|
|
11
23
|
* Splits text into plain parts and `#123` issue references, so renderers can
|
|
12
24
|
* escape the plain parts and link the references.
|
package/dist/render/shared.js
CHANGED
|
@@ -19,13 +19,53 @@ export function commitLink(commit, url) {
|
|
|
19
19
|
export function issueLink(issue, url) {
|
|
20
20
|
return `${url}/issues/${issue}`;
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The commit's gitmoji as a character, `:code:` for retired ones, or ''.
|
|
24
|
+
* Gitmojis that chamoji guessed (from wording or AI) are not shown, so plain
|
|
25
|
+
* histories aren't decorated with emojis their authors never wrote.
|
|
26
|
+
*/
|
|
23
27
|
export function emojiFor(commit) {
|
|
24
|
-
if (!commit.gitmoji
|
|
28
|
+
if (!commit.gitmoji ||
|
|
29
|
+
(commit.gitmojiSource !== 'gitmoji' && commit.gitmojiSource !== 'type')) {
|
|
25
30
|
return '';
|
|
26
31
|
}
|
|
27
32
|
return getGitmoji(commit.gitmoji)?.emoji ?? `:${commit.gitmoji}:`;
|
|
28
33
|
}
|
|
34
|
+
const DEPENDENCY_NAME = /^(?:bump|update|upgrade|downgrade|pin)\s+(?:the\s+)?(?:(?:dev[- ]?)?dependency\s+)?([@\w./-]+)/i;
|
|
35
|
+
const NOT_A_NAME = /^(?:dependencies|deps|dev|all|minor|patch|major)$/i;
|
|
36
|
+
/** Dependabot grouped updates: "Bump the npm group … with 5 updates". */
|
|
37
|
+
const DEPENDABOT_GROUP = /^bump\s+the\s+\S+\s+group\b.*?\bwith\s+(\d+)\s+updates?\b/i;
|
|
38
|
+
const SHOWN_NAMES = 5;
|
|
39
|
+
/**
|
|
40
|
+
* One-line summary of a collapsed group of dependency updates, e.g.
|
|
41
|
+
* `9 dependency updates: jest, vite, … and 2 more`. Names come from
|
|
42
|
+
* Dependabot/Renovate-style subjects; a Dependabot group PR counts as the
|
|
43
|
+
* number of updates it bundles.
|
|
44
|
+
*/
|
|
45
|
+
export function summarizeDependencies(commits) {
|
|
46
|
+
const names = new Set();
|
|
47
|
+
let updates = 0;
|
|
48
|
+
for (const commit of commits) {
|
|
49
|
+
const group = DEPENDABOT_GROUP.exec(commit.description);
|
|
50
|
+
if (group) {
|
|
51
|
+
updates += Number(group[1]);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
updates++;
|
|
55
|
+
const name = DEPENDENCY_NAME.exec(commit.description)?.[1];
|
|
56
|
+
if (name && !NOT_A_NAME.test(name)) {
|
|
57
|
+
names.add(name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const count = `${updates} dependency update${updates === 1 ? '' : 's'}`;
|
|
61
|
+
if (names.size === 0) {
|
|
62
|
+
return count;
|
|
63
|
+
}
|
|
64
|
+
const shown = [...names].slice(0, SHOWN_NAMES);
|
|
65
|
+
const more = names.size - shown.length;
|
|
66
|
+
return `${count}: ${shown.join(', ')}${more > 0 ? ` and ${more} more` : ''}`;
|
|
67
|
+
}
|
|
68
|
+
export const DEPENDENCY_EMOJI = getGitmoji('arrow_up')?.emoji ?? '⬆️';
|
|
29
69
|
/**
|
|
30
70
|
* Splits text into plain parts and `#123` issue references, so renderers can
|
|
31
71
|
* escape the plain parts and link the references.
|
package/dist/types.d.ts
CHANGED
|
@@ -16,9 +16,15 @@ export type Trailer = {
|
|
|
16
16
|
key: string;
|
|
17
17
|
value: string;
|
|
18
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Where a commit's gitmoji came from: written in the message, implied by a
|
|
21
|
+
* Conventional type, guessed from the wording, or picked by an AI classifier.
|
|
22
|
+
*/
|
|
23
|
+
export type GitmojiSource = 'gitmoji' | 'type' | 'rule' | 'ai' | null;
|
|
19
24
|
export type ParsedCommit = Omit<RawCommit, 'trailers'> & {
|
|
20
25
|
/** Gitmoji code without colons, e.g. `sparkles`. */
|
|
21
26
|
gitmoji: string | null;
|
|
27
|
+
gitmojiSource: GitmojiSource;
|
|
22
28
|
/** Conventional Commits type, e.g. `feat`. */
|
|
23
29
|
type: string | null;
|
|
24
30
|
scope: string | null;
|
|
@@ -34,6 +40,8 @@ export type GroupConfigItem = {
|
|
|
34
40
|
emojis: string[];
|
|
35
41
|
/** Hidden groups are matched but left out of the output. */
|
|
36
42
|
hidden?: boolean;
|
|
43
|
+
/** Render the group as one summary line (used for dependency updates). */
|
|
44
|
+
collapse?: boolean;
|
|
37
45
|
};
|
|
38
46
|
export type GroupConfig = GroupConfigItem[];
|
|
39
47
|
export type ChamojiConfig = {
|
|
@@ -43,6 +51,7 @@ export type ChamojiConfig = {
|
|
|
43
51
|
export type Group = {
|
|
44
52
|
group: string;
|
|
45
53
|
label: string;
|
|
54
|
+
collapse?: boolean;
|
|
46
55
|
commits: ParsedCommit[];
|
|
47
56
|
};
|
|
48
57
|
export type Release = {
|
|
@@ -64,3 +73,8 @@ export type Changelog = {
|
|
|
64
73
|
repositoryUrl?: string;
|
|
65
74
|
releases: Release[];
|
|
66
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Picks gitmoji codes for commits chamoji could not sort. Returns a map from
|
|
78
|
+
* commit hash to code, for the commits it is confident about.
|
|
79
|
+
*/
|
|
80
|
+
export type CommitClassifier = (commits: ParsedCommit[]) => Promise<Map<string, string>>;
|