@chamoji/core 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +14 -2
- package/dist/changelog.d.ts +24 -0
- package/dist/changelog.js +97 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +187 -0
- package/dist/git.d.ts +20 -0
- package/dist/git.js +133 -0
- package/dist/gitmojis.d.ts +22 -0
- package/dist/gitmojis.js +47 -0
- package/dist/group.d.ts +7 -0
- package/dist/group.js +33 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/infer.d.ts +8 -0
- package/dist/infer.js +94 -0
- package/dist/parse.d.ts +2 -0
- package/dist/parse.js +104 -0
- package/dist/render/html.d.ts +3 -0
- package/dist/render/html.js +65 -0
- package/dist/render/index.d.ts +6 -0
- package/dist/render/index.js +19 -0
- package/dist/render/markdown.d.ts +2 -0
- package/dist/render/markdown.js +60 -0
- package/dist/render/shared.d.ts +29 -0
- package/dist/render/shared.js +79 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.js +2 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +22 -0
- package/package.json +37 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chamoji contributors
|
|
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
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @chamoji/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The engine behind [`@chamoji/cli`](https://www.npmjs.com/package/@chamoji/cli). It reads git history, parses gitmoji and Conventional Commits messages, groups commits into changelog sections, suggests the next semver version, and renders Markdown, HTML or JSON. It has no UI and makes no network calls. Needs Node.js 22+ and git.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import {buildChangelog, loadConfig, render} from '@chamoji/core';
|
|
7
|
+
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const changelog = await buildChangelog({cwd, config: await loadConfig(cwd)});
|
|
10
|
+
console.log(render(changelog, 'markdown'));
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Lower-level pieces are exported too: `parseCommit`, `findGitmoji`, `groupCommits`, `bumpFor`, `nextVersion`, `readCommits`, `listReleaseTags`, `renderMarkdown`, `renderHtml` and `renderJson`.
|
|
14
|
+
|
|
15
|
+
MIT licensed.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ChamojiConfig, Changelog, CommitClassifier } from './types.js';
|
|
2
|
+
export type BuildChangelogOptions = {
|
|
3
|
+
cwd: string;
|
|
4
|
+
config: ChamojiConfig;
|
|
5
|
+
/** Ref treated as the head of history. Defaults to `HEAD`. */
|
|
6
|
+
to?: string;
|
|
7
|
+
/** Stop before the release with this tag (it and older ones are left out). */
|
|
8
|
+
from?: string;
|
|
9
|
+
/** Version to give the Unreleased section, dated `today`. */
|
|
10
|
+
release?: string;
|
|
11
|
+
/** `YYYY-MM-DD`; defaults to the current UTC date. */
|
|
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';
|
|
23
|
+
};
|
|
24
|
+
export declare function buildChangelog(options: BuildChangelogOptions): Promise<Changelog>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { assertGitRepo, assertRevision, getRemoteWebUrl, listReleaseTags, readCommits, } from './git.js';
|
|
3
|
+
import { getGitmoji } from './gitmojis.js';
|
|
4
|
+
import { groupCommits } from './group.js';
|
|
5
|
+
import { parseCommit } from './parse.js';
|
|
6
|
+
import { bumpFor, nextVersion } from './version.js';
|
|
7
|
+
/** The tag a `--release` version will get, following the latest tag's `v` style. */
|
|
8
|
+
function futureTag(release, latestTag) {
|
|
9
|
+
if (release.startsWith('v')) {
|
|
10
|
+
return release;
|
|
11
|
+
}
|
|
12
|
+
return latestTag === null || latestTag.startsWith('v')
|
|
13
|
+
? `v${release}`
|
|
14
|
+
: release;
|
|
15
|
+
}
|
|
16
|
+
export async function buildChangelog(options) {
|
|
17
|
+
const { cwd, config, to = 'HEAD', from, release } = options;
|
|
18
|
+
const today = options.today ?? new Date().toISOString().slice(0, 10);
|
|
19
|
+
if (release !== undefined && semver.valid(release) === null) {
|
|
20
|
+
throw new Error(`Invalid release version: ${release}`);
|
|
21
|
+
}
|
|
22
|
+
await assertGitRepo(cwd);
|
|
23
|
+
await assertRevision(cwd, to);
|
|
24
|
+
const tags = await listReleaseTags(cwd, to);
|
|
25
|
+
if (from && !tags.some(tag => tag.name === from)) {
|
|
26
|
+
throw new Error(`Unknown release tag for --from: ${from}`);
|
|
27
|
+
}
|
|
28
|
+
const pending = [];
|
|
29
|
+
const latest = tags[0]?.name ?? null;
|
|
30
|
+
const unreleased = await readCommits(cwd, latest ? `${latest}..${to}` : to);
|
|
31
|
+
if (unreleased.length > 0) {
|
|
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
|
+
});
|
|
42
|
+
}
|
|
43
|
+
for (const [index, tag] of tags.entries()) {
|
|
44
|
+
if (tag.name === from) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
const previousTag = tags[index + 1]?.name ?? null;
|
|
48
|
+
const raw = await readCommits(cwd, previousTag ? `${previousTag}..${tag.name}` : tag.name);
|
|
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));
|
|
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
|
+
});
|
|
75
|
+
const repositoryUrl = config.repositoryUrl ?? (await getRemoteWebUrl(cwd));
|
|
76
|
+
return { ...(repositoryUrl ? { repositoryUrl } : {}), releases };
|
|
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
|
+
}
|
|
97
|
+
//# sourceMappingURL=changelog.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ChamojiConfig, GroupConfig } from './types.js';
|
|
2
|
+
export declare const CONFIG_FILE = "chamoji.config.json";
|
|
3
|
+
/**
|
|
4
|
+
* Keep a Changelog style sections. Every official gitmoji appears exactly once
|
|
5
|
+
* (enforced by a test). The retired `apple`, `penguin`, `checkered_flag`,
|
|
6
|
+
* `robot` and `green_apple` codes stay under Fixed for older histories.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_GROUP_CONFIG: GroupConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Loads `explicitPath`, or `chamoji.config.json` in `cwd` when present.
|
|
11
|
+
* A JSON array is treated as a legacy group config.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadConfig(cwd: string, explicitPath?: string): Promise<ChamojiConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export const CONFIG_FILE = 'chamoji.config.json';
|
|
4
|
+
/**
|
|
5
|
+
* Keep a Changelog style sections. Every official gitmoji appears exactly once
|
|
6
|
+
* (enforced by a test). The retired `apple`, `penguin`, `checkered_flag`,
|
|
7
|
+
* `robot` and `green_apple` codes stay under Fixed for older histories.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_GROUP_CONFIG = [
|
|
10
|
+
{
|
|
11
|
+
group: 'added',
|
|
12
|
+
label: 'Added',
|
|
13
|
+
emojis: [
|
|
14
|
+
'sparkles',
|
|
15
|
+
'tada',
|
|
16
|
+
'white_check_mark',
|
|
17
|
+
'construction_worker',
|
|
18
|
+
'chart_with_upwards_trend',
|
|
19
|
+
'loud_sound',
|
|
20
|
+
'globe_with_meridians',
|
|
21
|
+
'camera_flash',
|
|
22
|
+
'seedling',
|
|
23
|
+
'test_tube',
|
|
24
|
+
'necktie',
|
|
25
|
+
'triangular_flag_on_post',
|
|
26
|
+
'egg',
|
|
27
|
+
'money_with_wings',
|
|
28
|
+
'thread',
|
|
29
|
+
'safety_vest',
|
|
30
|
+
'airplane',
|
|
31
|
+
'stethoscope',
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
group: 'changed',
|
|
36
|
+
label: 'Changed',
|
|
37
|
+
emojis: [
|
|
38
|
+
'art',
|
|
39
|
+
'zap',
|
|
40
|
+
'lipstick',
|
|
41
|
+
'rotating_light',
|
|
42
|
+
'recycle',
|
|
43
|
+
'wrench',
|
|
44
|
+
'rewind',
|
|
45
|
+
'alien',
|
|
46
|
+
'truck',
|
|
47
|
+
'bento',
|
|
48
|
+
'wheelchair',
|
|
49
|
+
'speech_balloon',
|
|
50
|
+
'card_file_box',
|
|
51
|
+
'children_crossing',
|
|
52
|
+
'building_construction',
|
|
53
|
+
'iphone',
|
|
54
|
+
'hammer',
|
|
55
|
+
'package',
|
|
56
|
+
'label',
|
|
57
|
+
'mag',
|
|
58
|
+
'dizzy',
|
|
59
|
+
'goal_net',
|
|
60
|
+
'bricks',
|
|
61
|
+
'technologist',
|
|
62
|
+
'page_facing_up',
|
|
63
|
+
'poop',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{ group: 'breaking_changes', label: 'Breaking changes', emojis: ['boom'] },
|
|
67
|
+
{ group: 'deprecated', label: 'Deprecated', emojis: ['wastebasket'] },
|
|
68
|
+
{
|
|
69
|
+
group: 'removed',
|
|
70
|
+
label: 'Removed',
|
|
71
|
+
emojis: ['fire', 'mute', 'coffin'],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
group: 'fixed',
|
|
75
|
+
label: 'Fixed',
|
|
76
|
+
emojis: [
|
|
77
|
+
'bug',
|
|
78
|
+
'ambulance',
|
|
79
|
+
'green_heart',
|
|
80
|
+
'pencil2',
|
|
81
|
+
'adhesive_bandage',
|
|
82
|
+
'apple',
|
|
83
|
+
'penguin',
|
|
84
|
+
'checkered_flag',
|
|
85
|
+
'robot',
|
|
86
|
+
'green_apple',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
group: 'security',
|
|
91
|
+
label: 'Security',
|
|
92
|
+
emojis: ['lock', 'closed_lock_with_key', 'passport_control'],
|
|
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
|
+
},
|
|
106
|
+
{
|
|
107
|
+
group: 'useless',
|
|
108
|
+
label: 'Useless',
|
|
109
|
+
emojis: ['bookmark', 'twisted_rightwards_arrows'],
|
|
110
|
+
hidden: true,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
group: 'misc',
|
|
114
|
+
label: 'Miscellaneous',
|
|
115
|
+
emojis: [
|
|
116
|
+
'memo',
|
|
117
|
+
'bulb',
|
|
118
|
+
'rocket',
|
|
119
|
+
'construction',
|
|
120
|
+
'beers',
|
|
121
|
+
'busts_in_silhouette',
|
|
122
|
+
'clown_face',
|
|
123
|
+
'see_no_evil',
|
|
124
|
+
'alembic',
|
|
125
|
+
'monocle_face',
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
/**
|
|
130
|
+
* Loads `explicitPath`, or `chamoji.config.json` in `cwd` when present.
|
|
131
|
+
* A JSON array is treated as a legacy group config.
|
|
132
|
+
*/
|
|
133
|
+
export async function loadConfig(cwd, explicitPath) {
|
|
134
|
+
const file = path.resolve(cwd, explicitPath ?? CONFIG_FILE);
|
|
135
|
+
let text;
|
|
136
|
+
try {
|
|
137
|
+
text = await readFile(file, 'utf8');
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error.code !== 'ENOENT') {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
if (explicitPath) {
|
|
144
|
+
throw new Error(`Config file not found: ${file}`);
|
|
145
|
+
}
|
|
146
|
+
return { groups: DEFAULT_GROUP_CONFIG };
|
|
147
|
+
}
|
|
148
|
+
let data;
|
|
149
|
+
try {
|
|
150
|
+
data = JSON.parse(text);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
throw new Error(`Invalid config ${file}: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(data)) {
|
|
156
|
+
return { groups: validateGroups(data, file) };
|
|
157
|
+
}
|
|
158
|
+
if (!data || typeof data !== 'object') {
|
|
159
|
+
throw new Error(`Invalid config ${file}: expected an object or an array`);
|
|
160
|
+
}
|
|
161
|
+
const { groups, repositoryUrl } = data;
|
|
162
|
+
if (repositoryUrl !== undefined && typeof repositoryUrl !== 'string') {
|
|
163
|
+
throw new Error(`Invalid config ${file}: repositoryUrl must be a string`);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
groups: groups === undefined
|
|
167
|
+
? DEFAULT_GROUP_CONFIG
|
|
168
|
+
: validateGroups(groups, file),
|
|
169
|
+
...(repositoryUrl ? { repositoryUrl } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function validateGroups(value, file) {
|
|
173
|
+
const valid = Array.isArray(value) &&
|
|
174
|
+
value.every(item => item &&
|
|
175
|
+
typeof item.group === 'string' &&
|
|
176
|
+
typeof item.label === 'string' &&
|
|
177
|
+
Array.isArray(item.emojis) &&
|
|
178
|
+
item.emojis.every((code) => typeof code === 'string'));
|
|
179
|
+
if (!valid) {
|
|
180
|
+
throw new Error(`Invalid config ${file}: groups must be [{group, label, emojis: string[]}]`);
|
|
181
|
+
}
|
|
182
|
+
return value.map(item => ({
|
|
183
|
+
...item,
|
|
184
|
+
emojis: item.emojis.map(code => code.replaceAll(':', '')),
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=config.js.map
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RawCommit } from './types.js';
|
|
2
|
+
export type ReleaseTag = {
|
|
3
|
+
name: string;
|
|
4
|
+
date: string;
|
|
5
|
+
};
|
|
6
|
+
/** Runs git without a shell and returns stdout. */
|
|
7
|
+
export declare function runGit(args: string[], cwd: string): Promise<string>;
|
|
8
|
+
export declare function assertGitRepo(cwd: string): Promise<void>;
|
|
9
|
+
export declare function assertRevision(cwd: string, ref: string): Promise<void>;
|
|
10
|
+
/** Non-merge commits in `range` (anything `git log` accepts), newest first. */
|
|
11
|
+
export declare function readCommits(cwd: string, range: string): Promise<RawCommit[]>;
|
|
12
|
+
/**
|
|
13
|
+
* Semver tags reachable from `to`, newest first. `date` is the tagged
|
|
14
|
+
* commit's committer date, so annotated and lightweight tags agree.
|
|
15
|
+
*/
|
|
16
|
+
export declare function listReleaseTags(cwd: string, to: string): Promise<ReleaseTag[]>;
|
|
17
|
+
/** `origin` as an https web URL, or undefined when there is none. */
|
|
18
|
+
export declare function getRemoteWebUrl(cwd: string): Promise<string | undefined>;
|
|
19
|
+
/** Converts an ssh, scp-style or http(s) git remote to a browsable URL. */
|
|
20
|
+
export declare function toWebUrl(remote: string): string | undefined;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const FIELD = '\u001F';
|
|
6
|
+
const RECORD = '\u001E';
|
|
7
|
+
/** Runs git without a shell and returns stdout. */
|
|
8
|
+
export async function runGit(args, cwd) {
|
|
9
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
10
|
+
cwd,
|
|
11
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
12
|
+
});
|
|
13
|
+
return stdout;
|
|
14
|
+
}
|
|
15
|
+
export async function assertGitRepo(cwd) {
|
|
16
|
+
try {
|
|
17
|
+
await runGit(['rev-parse', '--is-inside-work-tree'], cwd);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
const { code, stderr } = error;
|
|
21
|
+
if (code === 'ENOENT') {
|
|
22
|
+
throw new Error('git is not installed or not on PATH');
|
|
23
|
+
}
|
|
24
|
+
if (stderr?.includes('not a git repository')) {
|
|
25
|
+
throw new Error(`Not a git repository: ${cwd}`);
|
|
26
|
+
}
|
|
27
|
+
throw new Error(stderr?.trim() || error.message);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function assertRevision(cwd, ref) {
|
|
31
|
+
try {
|
|
32
|
+
await runGit(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], cwd);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new Error(ref === 'HEAD'
|
|
36
|
+
? 'Repository has no commits yet'
|
|
37
|
+
: `Unknown revision: ${ref}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Non-merge commits in `range` (anything `git log` accepts), newest first. */
|
|
41
|
+
export async function readCommits(cwd, range) {
|
|
42
|
+
const format = [
|
|
43
|
+
'%H',
|
|
44
|
+
'%h',
|
|
45
|
+
'%an',
|
|
46
|
+
'%ae',
|
|
47
|
+
'%aI',
|
|
48
|
+
'%s',
|
|
49
|
+
'%b',
|
|
50
|
+
'%(trailers:only,unfold)',
|
|
51
|
+
].join('%x1f');
|
|
52
|
+
const stdout = await runGit([
|
|
53
|
+
'log',
|
|
54
|
+
'--no-merges',
|
|
55
|
+
'--no-show-signature',
|
|
56
|
+
`--format=${format}%x1e`,
|
|
57
|
+
'--end-of-options',
|
|
58
|
+
range,
|
|
59
|
+
'--',
|
|
60
|
+
], cwd);
|
|
61
|
+
return stdout
|
|
62
|
+
.split(RECORD)
|
|
63
|
+
.map(record => record.replace(/^\n/, ''))
|
|
64
|
+
.filter(record => record.length > 0)
|
|
65
|
+
.map(record => {
|
|
66
|
+
const [hash, shortHash, authorName, authorEmail, date, subject, body, trailers,] = record.split(FIELD);
|
|
67
|
+
return {
|
|
68
|
+
hash: hash,
|
|
69
|
+
shortHash: shortHash,
|
|
70
|
+
authorName: authorName,
|
|
71
|
+
authorEmail: authorEmail,
|
|
72
|
+
date: date,
|
|
73
|
+
subject: subject,
|
|
74
|
+
body: body,
|
|
75
|
+
trailers: trailers ?? '',
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Semver tags reachable from `to`, newest first. `date` is the tagged
|
|
81
|
+
* commit's committer date, so annotated and lightweight tags agree.
|
|
82
|
+
*/
|
|
83
|
+
export async function listReleaseTags(cwd, to) {
|
|
84
|
+
const stdout = await runGit([
|
|
85
|
+
'tag',
|
|
86
|
+
`--merged=${to}`,
|
|
87
|
+
'--format=%(refname:strip=2)%1f%(committerdate:iso-strict)%1f%(*committerdate:iso-strict)',
|
|
88
|
+
], cwd);
|
|
89
|
+
return stdout
|
|
90
|
+
.split('\n')
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map(line => {
|
|
93
|
+
const [name, direct, dereferenced] = line.split(FIELD);
|
|
94
|
+
return { name: name, date: dereferenced || direct || '' };
|
|
95
|
+
})
|
|
96
|
+
.filter(tag => semver.valid(tag.name) !== null)
|
|
97
|
+
.sort((a, b) => semver.rcompare(a.name, b.name));
|
|
98
|
+
}
|
|
99
|
+
/** `origin` as an https web URL, or undefined when there is none. */
|
|
100
|
+
export async function getRemoteWebUrl(cwd) {
|
|
101
|
+
try {
|
|
102
|
+
return toWebUrl((await runGit(['remote', 'get-url', 'origin'], cwd)).trim());
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Converts an ssh, scp-style or http(s) git remote to a browsable URL. */
|
|
109
|
+
export function toWebUrl(remote) {
|
|
110
|
+
const scp = /^(?:[^@\s/]+@)?([^:/\s]+):(?!\/)(.+)$/.exec(remote);
|
|
111
|
+
if (scp && !/^[a-z][a-z\d+.-]*:\/\//i.test(remote)) {
|
|
112
|
+
return `https://${scp[1]}/${stripGitSuffix(scp[2])}`;
|
|
113
|
+
}
|
|
114
|
+
let url;
|
|
115
|
+
try {
|
|
116
|
+
url = new URL(remote);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
if (!['https:', 'http:', 'ssh:', 'git:'].includes(url.protocol)) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
const protocol = url.protocol === 'http:' ? 'http' : 'https';
|
|
125
|
+
const host = url.protocol === 'https:' || url.protocol === 'http:'
|
|
126
|
+
? url.host
|
|
127
|
+
: url.hostname;
|
|
128
|
+
return `${protocol}://${host}/${stripGitSuffix(url.pathname.replace(/^\/+/, ''))}`;
|
|
129
|
+
}
|
|
130
|
+
function stripGitSuffix(repoPath) {
|
|
131
|
+
return repoPath.replace(/\/+$/, '').replace(/\.git$/, '');
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SemverBump } from './types.js';
|
|
2
|
+
export type GitmojiInfo = {
|
|
3
|
+
/** Code without colons, e.g. `white_check_mark`. */
|
|
4
|
+
code: string;
|
|
5
|
+
emoji: string;
|
|
6
|
+
description: string;
|
|
7
|
+
semver: SemverBump | null;
|
|
8
|
+
};
|
|
9
|
+
export type GitmojiMatch = {
|
|
10
|
+
code: string;
|
|
11
|
+
index: number;
|
|
12
|
+
length: number;
|
|
13
|
+
};
|
|
14
|
+
/** Removes U+FE0F so `⚡` and `⚡️` compare equal. */
|
|
15
|
+
export declare function normalizeEmoji(text: string): string;
|
|
16
|
+
export declare const GITMOJIS: readonly GitmojiInfo[];
|
|
17
|
+
export declare function getGitmoji(code: string): GitmojiInfo | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Finds the earliest gitmoji in already-normalized `text`, either as a known
|
|
20
|
+
* `:code:` or as its unicode character.
|
|
21
|
+
*/
|
|
22
|
+
export declare function findGitmoji(text: string): GitmojiMatch | null;
|
package/dist/gitmojis.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { gitmojis } from 'gitmojis';
|
|
2
|
+
/** Removes U+FE0F so `⚡` and `⚡️` compare equal. */
|
|
3
|
+
export function normalizeEmoji(text) {
|
|
4
|
+
return text.replaceAll('️', '');
|
|
5
|
+
}
|
|
6
|
+
export const GITMOJIS = gitmojis.map(gitmoji => ({
|
|
7
|
+
code: gitmoji.code.slice(1, -1),
|
|
8
|
+
emoji: gitmoji.emoji,
|
|
9
|
+
description: gitmoji.description,
|
|
10
|
+
semver: gitmoji.semver,
|
|
11
|
+
}));
|
|
12
|
+
const byCode = new Map(GITMOJIS.map(gitmoji => [gitmoji.code, gitmoji]));
|
|
13
|
+
const byEmoji = GITMOJIS.map(gitmoji => ({
|
|
14
|
+
code: gitmoji.code,
|
|
15
|
+
emoji: normalizeEmoji(gitmoji.emoji),
|
|
16
|
+
}));
|
|
17
|
+
export function getGitmoji(code) {
|
|
18
|
+
return byCode.get(code);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Finds the earliest gitmoji in already-normalized `text`, either as a known
|
|
22
|
+
* `:code:` or as its unicode character.
|
|
23
|
+
*/
|
|
24
|
+
export function findGitmoji(text) {
|
|
25
|
+
let best = null;
|
|
26
|
+
const consider = (match) => {
|
|
27
|
+
if (!best ||
|
|
28
|
+
match.index < best.index ||
|
|
29
|
+
(match.index === best.index && match.length > best.length)) {
|
|
30
|
+
best = match;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
for (const match of text.matchAll(/:([a-z\d_+-]+):/g)) {
|
|
34
|
+
if (byCode.has(match[1])) {
|
|
35
|
+
consider({ code: match[1], index: match.index, length: match[0].length });
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const { code, emoji } of byEmoji) {
|
|
40
|
+
const index = text.indexOf(emoji);
|
|
41
|
+
if (index !== -1) {
|
|
42
|
+
consider({ code, index, length: emoji.length });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return best;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=gitmojis.js.map
|
package/dist/group.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Group, GroupConfig, ParsedCommit } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sorts commits into the configured sections. Breaking commits go to
|
|
4
|
+
* Breaking changes whatever their gitmoji; unmatched ones go to misc.
|
|
5
|
+
* Returns groups in config order, without empty or hidden ones.
|
|
6
|
+
*/
|
|
7
|
+
export declare function groupCommits(commits: ParsedCommit[], config: GroupConfig): Group[];
|
package/dist/group.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const BREAKING_GROUP = 'breaking_changes';
|
|
2
|
+
const FALLBACK_GROUP = 'misc';
|
|
3
|
+
/**
|
|
4
|
+
* Sorts commits into the configured sections. Breaking commits go to
|
|
5
|
+
* Breaking changes whatever their gitmoji; unmatched ones go to misc.
|
|
6
|
+
* Returns groups in config order, without empty or hidden ones.
|
|
7
|
+
*/
|
|
8
|
+
export function groupCommits(commits, config) {
|
|
9
|
+
const buckets = new Map(config.map(item => [item.group, []]));
|
|
10
|
+
const groupByCode = new Map();
|
|
11
|
+
for (const item of config) {
|
|
12
|
+
for (const code of item.emojis) {
|
|
13
|
+
if (!groupByCode.has(code)) {
|
|
14
|
+
groupByCode.set(code, item.group);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const commit of commits) {
|
|
19
|
+
const target = commit.breaking && buckets.has(BREAKING_GROUP)
|
|
20
|
+
? BREAKING_GROUP
|
|
21
|
+
: (commit.gitmoji && groupByCode.get(commit.gitmoji)) || FALLBACK_GROUP;
|
|
22
|
+
buckets.get(target)?.push(commit);
|
|
23
|
+
}
|
|
24
|
+
return config
|
|
25
|
+
.filter(item => !item.hidden && buckets.get(item.group).length > 0)
|
|
26
|
+
.map(item => ({
|
|
27
|
+
group: item.group,
|
|
28
|
+
label: item.label,
|
|
29
|
+
...(item.collapse ? { collapse: true } : {}),
|
|
30
|
+
commits: buckets.get(item.group),
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=group.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { buildChangelog, type BuildChangelogOptions } from './changelog.js';
|
|
2
|
+
export { CONFIG_FILE, DEFAULT_GROUP_CONFIG, loadConfig } from './config.js';
|
|
3
|
+
export { getRemoteWebUrl, listReleaseTags, readCommits, runGit, toWebUrl, type ReleaseTag, } from './git.js';
|
|
4
|
+
export { GITMOJIS, findGitmoji, getGitmoji, normalizeEmoji, type GitmojiInfo, type GitmojiMatch, } from './gitmojis.js';
|
|
5
|
+
export { groupCommits } from './group.js';
|
|
6
|
+
export { parseCommit } from './parse.js';
|
|
7
|
+
export { escapeHtml, render, renderHtml, renderJson, renderMarkdown, type OutputFormat, } from './render/index.js';
|
|
8
|
+
export type * from './types.js';
|
|
9
|
+
export { bumpFor, nextVersion } from './version.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { buildChangelog } from './changelog.js';
|
|
2
|
+
export { CONFIG_FILE, DEFAULT_GROUP_CONFIG, loadConfig } from './config.js';
|
|
3
|
+
export { getRemoteWebUrl, listReleaseTags, readCommits, runGit, toWebUrl, } from './git.js';
|
|
4
|
+
export { GITMOJIS, findGitmoji, getGitmoji, normalizeEmoji, } from './gitmojis.js';
|
|
5
|
+
export { groupCommits } from './group.js';
|
|
6
|
+
export { parseCommit } from './parse.js';
|
|
7
|
+
export { escapeHtml, render, renderHtml, renderJson, renderMarkdown, } from './render/index.js';
|
|
8
|
+
export { bumpFor, nextVersion } from './version.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
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.d.ts
ADDED
package/dist/parse.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { findGitmoji, normalizeEmoji } from './gitmojis.js';
|
|
2
|
+
import { inferGitmoji } from './infer.js';
|
|
3
|
+
/** Conventional Commits types we recognize, and the gitmoji each implies. */
|
|
4
|
+
const TYPE_TO_GITMOJI = {
|
|
5
|
+
feat: 'sparkles',
|
|
6
|
+
fix: 'bug',
|
|
7
|
+
docs: 'memo',
|
|
8
|
+
style: 'art',
|
|
9
|
+
refactor: 'recycle',
|
|
10
|
+
perf: 'zap',
|
|
11
|
+
test: 'white_check_mark',
|
|
12
|
+
build: 'package',
|
|
13
|
+
ci: 'construction_worker',
|
|
14
|
+
chore: 'wrench',
|
|
15
|
+
revert: 'rewind',
|
|
16
|
+
release: 'bookmark',
|
|
17
|
+
wip: 'construction',
|
|
18
|
+
deps: 'arrow_up',
|
|
19
|
+
security: 'lock',
|
|
20
|
+
};
|
|
21
|
+
/** Conventional scopes that mean "dependency update" under any type. */
|
|
22
|
+
const DEPENDENCY_SCOPES = new Set(['deps', 'deps-dev', 'dependencies']);
|
|
23
|
+
const CONVENTIONAL_PREFIX = /^(\w+)(?:\(([^)]+)\))?(!)?:\s*/;
|
|
24
|
+
const GITMOJI_CLI_SCOPE = /^\(([^)]+)\):\s*/;
|
|
25
|
+
const BREAKING_FOOTER = /^BREAKING[ -]CHANGE:/m;
|
|
26
|
+
const GIT_REVERT = /^Revert "(.+)"$/;
|
|
27
|
+
export function parseCommit(raw) {
|
|
28
|
+
const revert = GIT_REVERT.exec(raw.subject.trim());
|
|
29
|
+
if (revert) {
|
|
30
|
+
// `git revert` quotes the original subject; classify by the revert, not
|
|
31
|
+
// by the reverted commit's gitmoji.
|
|
32
|
+
const original = parseCommit({ ...raw, subject: revert[1], body: '' });
|
|
33
|
+
return {
|
|
34
|
+
...original,
|
|
35
|
+
body: raw.body,
|
|
36
|
+
gitmoji: 'rewind',
|
|
37
|
+
gitmojiSource: 'rule',
|
|
38
|
+
type: null,
|
|
39
|
+
description: `Revert "${original.description}"`,
|
|
40
|
+
breaking: BREAKING_FOOTER.test(raw.body),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
let text = normalizeEmoji(raw.subject).trim();
|
|
44
|
+
let type = null;
|
|
45
|
+
let scope = null;
|
|
46
|
+
let bang = false;
|
|
47
|
+
const prefix = CONVENTIONAL_PREFIX.exec(text);
|
|
48
|
+
// Known types always count; any other word counts only with a `!`, so
|
|
49
|
+
// prose like "Note: …" is left alone.
|
|
50
|
+
if (prefix && (prefix[1].toLowerCase() in TYPE_TO_GITMOJI || prefix[3])) {
|
|
51
|
+
type = prefix[1].toLowerCase();
|
|
52
|
+
scope = prefix[2] ?? null;
|
|
53
|
+
bang = Boolean(prefix[3]);
|
|
54
|
+
text = text.slice(prefix[0].length);
|
|
55
|
+
}
|
|
56
|
+
let gitmoji = null;
|
|
57
|
+
let gitmojiSource = null;
|
|
58
|
+
const found = findGitmoji(text);
|
|
59
|
+
if (found) {
|
|
60
|
+
gitmoji = found.code;
|
|
61
|
+
gitmojiSource = 'gitmoji';
|
|
62
|
+
text = (text.slice(0, found.index) + text.slice(found.index + found.length))
|
|
63
|
+
.replaceAll(/\s{2,}/g, ' ')
|
|
64
|
+
.trim();
|
|
65
|
+
}
|
|
66
|
+
const cliScope = scope ? null : GITMOJI_CLI_SCOPE.exec(text);
|
|
67
|
+
if (cliScope) {
|
|
68
|
+
scope = cliScope[1];
|
|
69
|
+
text = text.slice(cliScope[0].length);
|
|
70
|
+
}
|
|
71
|
+
if (!gitmoji && type) {
|
|
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;
|
|
81
|
+
}
|
|
82
|
+
const { trailers: rawTrailers, ...rest } = raw;
|
|
83
|
+
return {
|
|
84
|
+
...rest,
|
|
85
|
+
gitmoji,
|
|
86
|
+
gitmojiSource,
|
|
87
|
+
type,
|
|
88
|
+
scope,
|
|
89
|
+
description: text.trim(),
|
|
90
|
+
breaking: gitmoji === 'boom' || bang || BREAKING_FOOTER.test(raw.body),
|
|
91
|
+
trailers: parseTrailers(rawTrailers),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function parseTrailers(text) {
|
|
95
|
+
const trailers = [];
|
|
96
|
+
for (const line of text.split('\n')) {
|
|
97
|
+
const match = /^([\w-]+):\s*(.*)$/.exec(line.trim());
|
|
98
|
+
if (match) {
|
|
99
|
+
trailers.push({ key: match[1], value: match[2] });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return trailers;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { commitLink, DEPENDENCY_EMOJI, emojiFor, issueLink, releaseLink, safeRepositoryUrl, splitIssues, summarizeDependencies, } from './shared.js';
|
|
2
|
+
const ESCAPES = {
|
|
3
|
+
'&': '&',
|
|
4
|
+
'<': '<',
|
|
5
|
+
'>': '>',
|
|
6
|
+
'"': '"',
|
|
7
|
+
"'": ''',
|
|
8
|
+
};
|
|
9
|
+
export const escapeHtml = (text) => text.replaceAll(/[&<>"']/g, char => ESCAPES[char]);
|
|
10
|
+
const anchor = (href, content) => `<a href="${escapeHtml(href)}">${content}</a>`;
|
|
11
|
+
const STYLE = `body{font-family:system-ui,sans-serif;line-height:1.6;max-width:48rem;margin:2rem auto;padding:0 1rem}
|
|
12
|
+
h2{margin-top:2rem;border-bottom:1px solid #ddd}time{color:#666;font-size:.9em;font-weight:normal}
|
|
13
|
+
code{font-size:.85em}`;
|
|
14
|
+
export function renderHtml(changelog) {
|
|
15
|
+
const url = safeRepositoryUrl(changelog);
|
|
16
|
+
const out = [
|
|
17
|
+
'<!doctype html>',
|
|
18
|
+
'<html lang="en">',
|
|
19
|
+
'<head>',
|
|
20
|
+
'<meta charset="utf-8">',
|
|
21
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
22
|
+
'<title>Changelog</title>',
|
|
23
|
+
`<style>${STYLE}</style>`,
|
|
24
|
+
'</head>',
|
|
25
|
+
'<body>',
|
|
26
|
+
'<h1>Changelog</h1>',
|
|
27
|
+
];
|
|
28
|
+
for (const release of changelog.releases) {
|
|
29
|
+
const link = releaseLink(release, url);
|
|
30
|
+
const version = escapeHtml(release.version);
|
|
31
|
+
const date = release.date
|
|
32
|
+
? ` <time datetime="${escapeHtml(release.date)}">${escapeHtml(release.date)}</time>`
|
|
33
|
+
: '';
|
|
34
|
+
out.push(`<section id="${version}">`, `<h2>${link ? anchor(link, version) : version}${date}</h2>`);
|
|
35
|
+
for (const group of release.groups) {
|
|
36
|
+
out.push(`<h3>${escapeHtml(group.label)}</h3>`, '<ul>');
|
|
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
|
+
}
|
|
44
|
+
}
|
|
45
|
+
out.push('</ul>');
|
|
46
|
+
}
|
|
47
|
+
out.push('</section>');
|
|
48
|
+
}
|
|
49
|
+
out.push('</body>', '</html>', '');
|
|
50
|
+
return out.join('\n');
|
|
51
|
+
}
|
|
52
|
+
function renderCommit(commit, url) {
|
|
53
|
+
const emoji = emojiFor(commit);
|
|
54
|
+
const scope = commit.scope
|
|
55
|
+
? `<strong>${escapeHtml(commit.scope)}:</strong> `
|
|
56
|
+
: '';
|
|
57
|
+
const description = splitIssues(commit.description)
|
|
58
|
+
.map(part => part.issue && url
|
|
59
|
+
? anchor(issueLink(part.issue, url), escapeHtml(part.text))
|
|
60
|
+
: escapeHtml(part.text))
|
|
61
|
+
.join('');
|
|
62
|
+
const hash = `<code>${escapeHtml(commit.shortHash)}</code>`;
|
|
63
|
+
return `${emoji ? `${escapeHtml(emoji)} ` : ''}${scope}${description} (${url ? anchor(commitLink(commit, url), hash) : hash})`;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=html.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Changelog } from '../types.js';
|
|
2
|
+
export type OutputFormat = 'markdown' | 'json' | 'html';
|
|
3
|
+
export declare const renderJson: (changelog: Changelog) => string;
|
|
4
|
+
export declare function render(changelog: Changelog, format: OutputFormat): string;
|
|
5
|
+
export { escapeHtml, renderHtml } from './html.js';
|
|
6
|
+
export { renderMarkdown } from './markdown.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { renderHtml } from './html.js';
|
|
2
|
+
import { renderMarkdown } from './markdown.js';
|
|
3
|
+
export const renderJson = (changelog) => `${JSON.stringify(changelog, null, 2)}\n`;
|
|
4
|
+
export function render(changelog, format) {
|
|
5
|
+
switch (format) {
|
|
6
|
+
case 'markdown': {
|
|
7
|
+
return renderMarkdown(changelog);
|
|
8
|
+
}
|
|
9
|
+
case 'html': {
|
|
10
|
+
return renderHtml(changelog);
|
|
11
|
+
}
|
|
12
|
+
case 'json': {
|
|
13
|
+
return renderJson(changelog);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export { escapeHtml, renderHtml } from './html.js';
|
|
18
|
+
export { renderMarkdown } from './markdown.js';
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { commitLink, DEPENDENCY_EMOJI, emojiFor, issueLink, releaseLink, safeRepositoryUrl, splitIssues, summarizeDependencies, } from './shared.js';
|
|
2
|
+
/**
|
|
3
|
+
* Makes commit text inert outside code spans: no raw HTML, and no links or
|
|
4
|
+
* images, since squash-merged PR titles can come from anyone. Backticks are
|
|
5
|
+
* escaped too: any left here are unpaired, and an unpaired run could open a
|
|
6
|
+
* code span a Markdown parser sees differently from `splitCodeSpans`.
|
|
7
|
+
*/
|
|
8
|
+
const escapeMarkdown = (text) => text
|
|
9
|
+
.replaceAll(/[\\[\]`]/g, char => `\\${char}`)
|
|
10
|
+
.replaceAll('<', '<')
|
|
11
|
+
.replaceAll('>', '>');
|
|
12
|
+
/**
|
|
13
|
+
* Splits text into single-backtick `code spans` and the parts between. A span
|
|
14
|
+
* holds no backticks, so emitting it as-is is safe once the surrounding text
|
|
15
|
+
* has its stray backticks escaped.
|
|
16
|
+
*/
|
|
17
|
+
const splitCodeSpans = (text) => text
|
|
18
|
+
.split(/(`[^`\n]+`)/)
|
|
19
|
+
.filter(part => part.length > 0)
|
|
20
|
+
.map(part => ({ text: part, code: /^`[^`\n]+`$/.test(part) }));
|
|
21
|
+
export function renderMarkdown(changelog) {
|
|
22
|
+
const url = safeRepositoryUrl(changelog);
|
|
23
|
+
const lines = ['# Changelog', ''];
|
|
24
|
+
for (const release of changelog.releases) {
|
|
25
|
+
const link = releaseLink(release, url);
|
|
26
|
+
const title = link ? `[${release.version}](${link})` : release.version;
|
|
27
|
+
lines.push(`## ${title}${release.date ? ` - ${release.date}` : ''}`, '');
|
|
28
|
+
for (const group of release.groups) {
|
|
29
|
+
lines.push(`### ${group.label}`, '');
|
|
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
|
+
}
|
|
37
|
+
}
|
|
38
|
+
lines.push('');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return lines.join('\n').replace(/\n+$/, '\n');
|
|
42
|
+
}
|
|
43
|
+
function renderCommit(commit, url) {
|
|
44
|
+
const emoji = emojiFor(commit);
|
|
45
|
+
const scope = commit.scope ? `**${escapeMarkdown(commit.scope)}:** ` : '';
|
|
46
|
+
const description = splitCodeSpans(commit.description)
|
|
47
|
+
.map(span => span.code
|
|
48
|
+
? span.text
|
|
49
|
+
: splitIssues(span.text)
|
|
50
|
+
.map(part => part.issue && url
|
|
51
|
+
? `[${part.text}](${issueLink(part.issue, url)})`
|
|
52
|
+
: escapeMarkdown(part.text))
|
|
53
|
+
.join(''))
|
|
54
|
+
.join('');
|
|
55
|
+
const hash = url
|
|
56
|
+
? `[${commit.shortHash}](${commitLink(commit, url)})`
|
|
57
|
+
: commit.shortHash;
|
|
58
|
+
return `${emoji ? `${emoji} ` : ''}${scope}${description} (${hash})`;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Changelog, ParsedCommit, Release } from '../types.js';
|
|
2
|
+
/** The repository URL if it is plain http(s), without a trailing slash. */
|
|
3
|
+
export declare function safeRepositoryUrl(changelog: Changelog): string | undefined;
|
|
4
|
+
/** Where a release heading links to: a compare view, when one exists. */
|
|
5
|
+
export declare function releaseLink(release: Release, url: string | undefined): string | undefined;
|
|
6
|
+
export declare function commitLink(commit: ParsedCommit, url: string): string;
|
|
7
|
+
export declare function issueLink(issue: string, url: string): string;
|
|
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
|
+
*/
|
|
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;
|
|
22
|
+
/**
|
|
23
|
+
* Splits text into plain parts and `#123` issue references, so renderers can
|
|
24
|
+
* escape the plain parts and link the references.
|
|
25
|
+
*/
|
|
26
|
+
export declare function splitIssues(text: string): Array<{
|
|
27
|
+
text: string;
|
|
28
|
+
issue?: string;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getGitmoji } from '../gitmojis.js';
|
|
2
|
+
/** The repository URL if it is plain http(s), without a trailing slash. */
|
|
3
|
+
export function safeRepositoryUrl(changelog) {
|
|
4
|
+
const url = changelog.repositoryUrl;
|
|
5
|
+
return url && /^https?:\/\/[^\s"'<>]+$/.test(url)
|
|
6
|
+
? url.replace(/\/+$/, '')
|
|
7
|
+
: undefined;
|
|
8
|
+
}
|
|
9
|
+
/** Where a release heading links to: a compare view, when one exists. */
|
|
10
|
+
export function releaseLink(release, url) {
|
|
11
|
+
if (!url || !release.previousTag) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return `${url}/compare/${release.previousTag}...${release.tag ?? 'HEAD'}`;
|
|
15
|
+
}
|
|
16
|
+
export function commitLink(commit, url) {
|
|
17
|
+
return `${url}/commit/${commit.hash}`;
|
|
18
|
+
}
|
|
19
|
+
export function issueLink(issue, url) {
|
|
20
|
+
return `${url}/issues/${issue}`;
|
|
21
|
+
}
|
|
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
|
+
*/
|
|
27
|
+
export function emojiFor(commit) {
|
|
28
|
+
if (!commit.gitmoji ||
|
|
29
|
+
(commit.gitmojiSource !== 'gitmoji' && commit.gitmojiSource !== 'type')) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
return getGitmoji(commit.gitmoji)?.emoji ?? `:${commit.gitmoji}:`;
|
|
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 ?? '⬆️';
|
|
69
|
+
/**
|
|
70
|
+
* Splits text into plain parts and `#123` issue references, so renderers can
|
|
71
|
+
* escape the plain parts and link the references.
|
|
72
|
+
*/
|
|
73
|
+
export function splitIssues(text) {
|
|
74
|
+
return text
|
|
75
|
+
.split(/(#\d+)\b/)
|
|
76
|
+
.filter(part => part.length > 0)
|
|
77
|
+
.map(part => /^#\d+$/.test(part) ? { text: part, issue: part.slice(1) } : { text: part });
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=shared.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type SemverBump = 'major' | 'minor' | 'patch';
|
|
2
|
+
/** One commit as read from `git log`, before any interpretation. */
|
|
3
|
+
export type RawCommit = {
|
|
4
|
+
hash: string;
|
|
5
|
+
shortHash: string;
|
|
6
|
+
authorName: string;
|
|
7
|
+
authorEmail: string;
|
|
8
|
+
/** ISO 8601 author date. */
|
|
9
|
+
date: string;
|
|
10
|
+
subject: string;
|
|
11
|
+
body: string;
|
|
12
|
+
/** Raw `%(trailers:only,unfold)` output: one `Key: value` per line. */
|
|
13
|
+
trailers: string;
|
|
14
|
+
};
|
|
15
|
+
export type Trailer = {
|
|
16
|
+
key: string;
|
|
17
|
+
value: string;
|
|
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;
|
|
24
|
+
export type ParsedCommit = Omit<RawCommit, 'trailers'> & {
|
|
25
|
+
/** Gitmoji code without colons, e.g. `sparkles`. */
|
|
26
|
+
gitmoji: string | null;
|
|
27
|
+
gitmojiSource: GitmojiSource;
|
|
28
|
+
/** Conventional Commits type, e.g. `feat`. */
|
|
29
|
+
type: string | null;
|
|
30
|
+
scope: string | null;
|
|
31
|
+
/** Subject with the type, scope and gitmoji removed. */
|
|
32
|
+
description: string;
|
|
33
|
+
breaking: boolean;
|
|
34
|
+
trailers: Trailer[];
|
|
35
|
+
};
|
|
36
|
+
export type GroupConfigItem = {
|
|
37
|
+
group: string;
|
|
38
|
+
label: string;
|
|
39
|
+
/** Gitmoji codes without colons. */
|
|
40
|
+
emojis: string[];
|
|
41
|
+
/** Hidden groups are matched but left out of the output. */
|
|
42
|
+
hidden?: boolean;
|
|
43
|
+
/** Render the group as one summary line (used for dependency updates). */
|
|
44
|
+
collapse?: boolean;
|
|
45
|
+
};
|
|
46
|
+
export type GroupConfig = GroupConfigItem[];
|
|
47
|
+
export type ChamojiConfig = {
|
|
48
|
+
groups: GroupConfig;
|
|
49
|
+
repositoryUrl?: string;
|
|
50
|
+
};
|
|
51
|
+
export type Group = {
|
|
52
|
+
group: string;
|
|
53
|
+
label: string;
|
|
54
|
+
collapse?: boolean;
|
|
55
|
+
commits: ParsedCommit[];
|
|
56
|
+
};
|
|
57
|
+
export type Release = {
|
|
58
|
+
/** Tag name, the `--release` version, or `Unreleased`. */
|
|
59
|
+
version: string;
|
|
60
|
+
/**
|
|
61
|
+
* The release's tag. For a `--release` section, the tag it will get
|
|
62
|
+
* (e.g. `v1.2.0`); null for an unnamed Unreleased section.
|
|
63
|
+
*/
|
|
64
|
+
tag: string | null;
|
|
65
|
+
previousTag: string | null;
|
|
66
|
+
/** `YYYY-MM-DD`, or null for an undated Unreleased section. */
|
|
67
|
+
date: string | null;
|
|
68
|
+
bump: SemverBump | null;
|
|
69
|
+
nextVersion: string | null;
|
|
70
|
+
groups: Group[];
|
|
71
|
+
};
|
|
72
|
+
export type Changelog = {
|
|
73
|
+
repositoryUrl?: string;
|
|
74
|
+
releases: Release[];
|
|
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>>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ParsedCommit, SemverBump } from './types.js';
|
|
2
|
+
/** The release bump implied by `commits`, from gitmoji semver data. */
|
|
3
|
+
export declare function bumpFor(commits: ParsedCommit[]): SemverBump | null;
|
|
4
|
+
/** `current` bumped by `bump`, without a `v` prefix. */
|
|
5
|
+
export declare function nextVersion(current: string | null, bump: SemverBump | null): string | null;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { getGitmoji } from './gitmojis.js';
|
|
3
|
+
const RANK = { patch: 1, minor: 2, major: 3 };
|
|
4
|
+
/** The release bump implied by `commits`, from gitmoji semver data. */
|
|
5
|
+
export function bumpFor(commits) {
|
|
6
|
+
let bump = null;
|
|
7
|
+
for (const commit of commits) {
|
|
8
|
+
if (commit.breaking) {
|
|
9
|
+
return 'major';
|
|
10
|
+
}
|
|
11
|
+
const level = commit.gitmoji ? getGitmoji(commit.gitmoji)?.semver : null;
|
|
12
|
+
if (level && (!bump || RANK[level] > RANK[bump])) {
|
|
13
|
+
bump = level;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return bump;
|
|
17
|
+
}
|
|
18
|
+
/** `current` bumped by `bump`, without a `v` prefix. */
|
|
19
|
+
export function nextVersion(current, bump) {
|
|
20
|
+
return bump ? semver.inc(current ?? '0.0.0', bump) : null;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=version.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chamoji/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Changelog engine for gitmoji and Conventional Commits",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"!dist/**/*.map"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"gitmojis": "^3.15.0",
|
|
22
|
+
"semver": "^7.7.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/semver": "^7.7.1"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"changelog",
|
|
29
|
+
"gitmoji",
|
|
30
|
+
"conventional-commits",
|
|
31
|
+
"semver",
|
|
32
|
+
"git"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p ."
|
|
39
|
+
}
|
|
6
40
|
}
|