@repo-toolkit/changelog 0.9.0 → 0.11.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/README.md +98 -19
- package/cli.js +296 -109
- package/index.d.ts +19 -8
- package/index.js +269 -88
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ pnpm add -D @repo-toolkit/changelog
|
|
|
11
11
|
## Config File
|
|
12
12
|
|
|
13
13
|
Use `--config` when you want repo-specific options such as custom commit `types`,
|
|
14
|
-
scope filtering, ignored commits, or custom
|
|
14
|
+
scope filtering, ignored commits, or custom URL formats.
|
|
15
15
|
|
|
16
16
|
```js
|
|
17
17
|
/** @type {import('@repo-toolkit/changelog').ChangelogConfig} */
|
|
@@ -38,9 +38,19 @@ repo-toolkit-changelog --config changelog.config.mjs
|
|
|
38
38
|
|
|
39
39
|
CLI flags override values from the config file.
|
|
40
40
|
|
|
41
|
-
Use a JavaScript config file when you need `RegExp` values such as `ignoreCommits
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
Use a JavaScript config file when you need `RegExp` values such as `ignoreCommits`.
|
|
42
|
+
JSON config files only work for plain data options.
|
|
43
|
+
|
|
44
|
+
> **Warning:** A JavaScript config file (`.mjs`, `.js`, `.cjs`) is loaded with
|
|
45
|
+
> `import()`, so it executes as trusted code in the same Node process as the
|
|
46
|
+
> CLI. Only point `--config` at files you control. JSON config files are parsed
|
|
47
|
+
> with `JSON.parse` and do not execute.
|
|
48
|
+
|
|
49
|
+
Both JSON and JS config files must export an object whose shape matches
|
|
50
|
+
`ChangelogConfig`. Runtime validation rejects unknown top-level keys, unknown
|
|
51
|
+
fields on `types` entries, and unsupported `effect` values (only `'hidden'` is
|
|
52
|
+
recognized) before the generator is built — invalid configs fail fast with an
|
|
53
|
+
actionable error instead of a silent later surprise.
|
|
44
54
|
|
|
45
55
|
## CLI
|
|
46
56
|
|
|
@@ -48,21 +58,59 @@ plain data options.
|
|
|
48
58
|
repo-toolkit-changelog
|
|
49
59
|
```
|
|
50
60
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
- `--
|
|
59
|
-
|
|
60
|
-
- `--no-
|
|
61
|
+
All flags:
|
|
62
|
+
|
|
63
|
+
| Flag | Description | Default |
|
|
64
|
+
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
|
65
|
+
| `--config <path>` | Config file (JSON or JS module) with changelog options. JS modules execute trusted code. | none |
|
|
66
|
+
| `--cwd <path>` | Working directory for reading `package.json` and git metadata. | `process.cwd()` |
|
|
67
|
+
| `--output <path>` | Output file path (absolute or relative to `--cwd`). | `CHANGELOG.md` |
|
|
68
|
+
| `--tag-prefix <prefix>` | Tag prefix to match. Pass `--tag-prefix=` for an empty prefix. | `v` |
|
|
69
|
+
| `--release-count <non-negative integer>` | Number of releases to include. `0` regenerates all releases. | latest release only |
|
|
70
|
+
| `--append` / `--no-append` | Append generated content after the existing file. Without this flag, generated content is prepended. | `--no-append` (prepend) |
|
|
71
|
+
| `--first-release` / `--no-first-release` | Include all commits back to the first tag when no prior release tag exists. Overrides `--release-count`. | `--no-first-release` |
|
|
72
|
+
| `--skip-unstable` / `--no-skip-unstable` | Skip prerelease tags (e.g. `v1.0.0-beta.1`). | `--skip-unstable` (skip) |
|
|
73
|
+
| `--output-unreleased` / `--no-output-unreleased` | Include the unreleased section. | `--output-unreleased` (include) |
|
|
74
|
+
| `-h`, `--help` | Show help and exit. | — |
|
|
75
|
+
|
|
76
|
+
Invalid numeric `--release-count` values exit nonzero with `Invalid numeric value`,
|
|
77
|
+
and unsupported config fields exit nonzero with a precise field-level message.
|
|
78
|
+
|
|
79
|
+
`--first-release` takes precedence over `--release-count` and regenerates the
|
|
80
|
+
full changelog. Unknown arguments are rejected by default; use `--` to pass
|
|
81
|
+
through trailing args to nested tooling in non-strict mode only.
|
|
82
|
+
|
|
83
|
+
## Output semantics
|
|
84
|
+
|
|
85
|
+
`generateChangelog` writes the resulting changelog atomically: it composes the
|
|
86
|
+
generated content into a sibling temporary file and renames it over the
|
|
87
|
+
destination only after the generator stream completes successfully. If the
|
|
88
|
+
generator throws, the existing file is left untouched and the temp file is
|
|
89
|
+
removed.
|
|
90
|
+
|
|
91
|
+
Existing file behavior:
|
|
92
|
+
|
|
93
|
+
- **Default (prepend):** generated content is placed above the existing content,
|
|
94
|
+
separated by a single blank line. Excess trailing newlines on either side are
|
|
95
|
+
collapsed to keep the separator stable.
|
|
96
|
+
- **`--append`:** generated content is placed below the existing content, also
|
|
97
|
+
separated by a single blank line.
|
|
98
|
+
- **No existing file:** the generated content is written directly to the
|
|
99
|
+
destination; no merge is performed.
|
|
100
|
+
- **Empty generated content:** the existing file is left unchanged.
|
|
101
|
+
|
|
102
|
+
The output always ends with a single trailing newline.
|
|
61
103
|
|
|
62
104
|
## JavaScript API
|
|
63
105
|
|
|
64
106
|
```ts
|
|
65
|
-
import {
|
|
107
|
+
import {
|
|
108
|
+
generateChangelog,
|
|
109
|
+
createGenerator,
|
|
110
|
+
createPreset,
|
|
111
|
+
getDefaultTypes,
|
|
112
|
+
DEFAULT_TYPES,
|
|
113
|
+
} from '@repo-toolkit/changelog';
|
|
66
114
|
|
|
67
115
|
await generateChangelog({
|
|
68
116
|
outputFile: 'CHANGELOG.md',
|
|
@@ -72,6 +120,36 @@ await generateChangelog({
|
|
|
72
120
|
});
|
|
73
121
|
```
|
|
74
122
|
|
|
123
|
+
### Public exports
|
|
124
|
+
|
|
125
|
+
| Export | Kind | Signature | Returns |
|
|
126
|
+
| ----------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
127
|
+
| `generateChangelog` | async function | `(options?: GenerateChangelogOptions) => Promise<string>` | Resolves with the absolute path of the written changelog file. |
|
|
128
|
+
| `createGenerator` | async function | `(options?: GenerateChangelogOptions) => Promise<ConventionalChangelog>` | Resolves with the configured `conventional-changelog` generator. |
|
|
129
|
+
| `createPreset` | async function | `(options?: CreatePresetOptions) => Promise<ConventionalCommitsPreset & { name: 'conventionalcommits' }>` | Resolves with the preset, tagged with `name: 'conventionalcommits'`. |
|
|
130
|
+
| `getDefaultTypes` | function | `() => ReadonlyArray<Readonly<ChangelogType>>` | Returns the frozen `DEFAULT_TYPES` array (same reference each call). |
|
|
131
|
+
| `DEFAULT_TYPES` | constant | `ReadonlyArray<Readonly<ChangelogType>>` | Deeply frozen default `types` array. |
|
|
132
|
+
| `GenerateChangelogOptions` | interface | extends `CreatePresetOptions` | — |
|
|
133
|
+
| `CreatePresetOptions` | type alias | = `ConventionalCommitsPresetOptions` | — |
|
|
134
|
+
| `ChangelogConfig` | type alias | = `GenerateChangelogOptions` | Config file shape. |
|
|
135
|
+
| `ChangelogType` | interface | `{ type, section?, scope?, effect?: 'hidden', hidden? }` | — |
|
|
136
|
+
| `ConventionalCommitsPresetOptions` | interface | — | Mirrors the pinned conventionalcommits API. |
|
|
137
|
+
| `ChangelogContext`, `ChangelogReference`, `ChangelogCommit` | interfaces | — | Echoed from the upstream parser/writer contracts. |
|
|
138
|
+
|
|
139
|
+
### Defaults immutability
|
|
140
|
+
|
|
141
|
+
`DEFAULT_TYPES` is deeply frozen at module load: nested entries cannot be
|
|
142
|
+
mutated, even in strict mode. Use `getDefaultTypes()` if you need a stable
|
|
143
|
+
accessor. Mutating attempts throw instead of silently corrupting later preset
|
|
144
|
+
creation.
|
|
145
|
+
|
|
146
|
+
### `effect` contract
|
|
147
|
+
|
|
148
|
+
`effect` is the preferred field for visibility control on a `ChangelogType`.
|
|
149
|
+
Only `'hidden'` is supported — it maps to `hidden: true` for the upstream
|
|
150
|
+
conventionalcommits parser. The `hidden` boolean is still accepted on a type
|
|
151
|
+
entry for compatibility with older upstream versions.
|
|
152
|
+
|
|
75
153
|
## Supported Preset Options
|
|
76
154
|
|
|
77
155
|
- `types`
|
|
@@ -80,10 +158,11 @@ await generateChangelog({
|
|
|
80
158
|
- `scope`
|
|
81
159
|
- `scopeOnly`
|
|
82
160
|
- `preMajor`
|
|
83
|
-
- `
|
|
84
|
-
- `
|
|
85
|
-
- `
|
|
86
|
-
- `
|
|
161
|
+
- `issueUrlFormat`
|
|
162
|
+
- `commitUrlFormat`
|
|
163
|
+
- `compareUrlFormat`
|
|
164
|
+
- `userUrlFormat`
|
|
165
|
+
- `bumpStrict`
|
|
87
166
|
|
|
88
167
|
## Default Sections
|
|
89
168
|
|
package/cli.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import {
|
|
4
|
+
import { pathToFileURL } from "url";
|
|
5
|
+
import { parseFlags, resolveCliOptions } from "@repo-toolkit/publish-package";
|
|
5
6
|
|
|
6
7
|
// src/index.ts
|
|
7
|
-
import {
|
|
8
|
-
import { mkdir } from "fs/promises";
|
|
8
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
9
9
|
import { dirname, isAbsolute, resolve } from "path";
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
import { isPlainObject } from "@repo-toolkit/publish-package";
|
|
10
12
|
import { ConventionalChangelog } from "conventional-changelog";
|
|
11
13
|
import createConventionalCommitsPreset from "conventional-changelog-conventionalcommits";
|
|
12
14
|
var PIPELINE_OPTION_KEYS = [
|
|
@@ -29,65 +31,67 @@ function splitPresetOptions(options) {
|
|
|
29
31
|
}
|
|
30
32
|
return presetOptions;
|
|
31
33
|
}
|
|
32
|
-
var DEFAULT_TYPES =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
34
|
+
var DEFAULT_TYPES = Object.freeze(
|
|
35
|
+
[
|
|
36
|
+
{
|
|
37
|
+
type: "feat",
|
|
38
|
+
section: "Features"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "fix",
|
|
42
|
+
scope: "deps",
|
|
43
|
+
effect: "hidden"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: "fix",
|
|
47
|
+
section: "Bug Fixes"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: "revert",
|
|
51
|
+
section: "Reverts"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: "docs",
|
|
55
|
+
section: "Documentation"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "refactor",
|
|
59
|
+
section: "Code Refactoring"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: "perf",
|
|
63
|
+
section: "Performance Improvements"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: "build",
|
|
67
|
+
section: "Build System"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: "e2e",
|
|
71
|
+
section: "End-to-end Testing"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "ci",
|
|
75
|
+
effect: "hidden"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "chore",
|
|
79
|
+
effect: "hidden"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "style",
|
|
83
|
+
effect: "hidden"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: "test",
|
|
87
|
+
effect: "hidden"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
type: "release",
|
|
91
|
+
effect: "hidden"
|
|
92
|
+
}
|
|
93
|
+
].map((entry) => Object.freeze(entry))
|
|
94
|
+
);
|
|
91
95
|
function normalizeTypes(types) {
|
|
92
96
|
return types.map((entry) => ({
|
|
93
97
|
...entry,
|
|
@@ -100,32 +104,209 @@ function resolvePresetOptions(options = {}) {
|
|
|
100
104
|
types: normalizeTypes(options.types ?? DEFAULT_TYPES)
|
|
101
105
|
};
|
|
102
106
|
}
|
|
107
|
+
function resolveTagOptions(options) {
|
|
108
|
+
const tags = {};
|
|
109
|
+
if (options.tagPrefix !== void 0) {
|
|
110
|
+
tags.prefix = options.tagPrefix;
|
|
111
|
+
}
|
|
112
|
+
if (options.skipUnstable !== void 0) {
|
|
113
|
+
tags.skipUnstable = options.skipUnstable;
|
|
114
|
+
}
|
|
115
|
+
return tags;
|
|
116
|
+
}
|
|
117
|
+
function resolveGeneratorOptions(options) {
|
|
118
|
+
const resolvedOptions = {
|
|
119
|
+
append: options.append ?? false,
|
|
120
|
+
outputUnreleased: options.outputUnreleased ?? true
|
|
121
|
+
};
|
|
122
|
+
if (options.firstRelease === true) {
|
|
123
|
+
resolvedOptions.releaseCount = 0;
|
|
124
|
+
} else if (options.releaseCount !== void 0) {
|
|
125
|
+
resolvedOptions.releaseCount = validateReleaseCount(options.releaseCount);
|
|
126
|
+
}
|
|
127
|
+
return resolvedOptions;
|
|
128
|
+
}
|
|
129
|
+
function validateGenerateChangelogOptions(options) {
|
|
130
|
+
if (options.releaseCount !== void 0) {
|
|
131
|
+
validateReleaseCount(options.releaseCount);
|
|
132
|
+
}
|
|
133
|
+
validatePresetOptions(splitPresetOptions(options));
|
|
134
|
+
}
|
|
135
|
+
function validateReleaseCount(value) {
|
|
136
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
137
|
+
throw new Error(`releaseCount must be a non-negative safe integer, got: ${value}`);
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
var PRESET_OPTION_KEYS = [
|
|
142
|
+
"types",
|
|
143
|
+
"ignoreCommits",
|
|
144
|
+
"issuePrefixes",
|
|
145
|
+
"scope",
|
|
146
|
+
"scopeOnly",
|
|
147
|
+
"preMajor",
|
|
148
|
+
"issueUrlFormat",
|
|
149
|
+
"commitUrlFormat",
|
|
150
|
+
"compareUrlFormat",
|
|
151
|
+
"userUrlFormat",
|
|
152
|
+
"bumpStrict"
|
|
153
|
+
];
|
|
154
|
+
var STRING_FORMAT_KEYS = ["issueUrlFormat", "commitUrlFormat", "compareUrlFormat", "userUrlFormat"];
|
|
155
|
+
function validatePresetOptions(options) {
|
|
156
|
+
for (const key of Object.keys(options)) {
|
|
157
|
+
if (!PRESET_OPTION_KEYS.includes(key)) {
|
|
158
|
+
throw new Error(`Unknown changelog config option: ${key}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (options.types !== void 0) {
|
|
162
|
+
validateTypes(options.types);
|
|
163
|
+
}
|
|
164
|
+
if (options.ignoreCommits !== void 0 && !(options.ignoreCommits instanceof RegExp)) {
|
|
165
|
+
throw new Error("ignoreCommits must be a RegExp");
|
|
166
|
+
}
|
|
167
|
+
if (options.issuePrefixes !== void 0) {
|
|
168
|
+
validateStringArray(options.issuePrefixes, "issuePrefixes");
|
|
169
|
+
}
|
|
170
|
+
if (options.scope !== void 0) {
|
|
171
|
+
if (typeof options.scope !== "string" && !Array.isArray(options.scope)) {
|
|
172
|
+
throw new Error("scope must be a string or an array of strings");
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(options.scope)) {
|
|
175
|
+
validateStringArray(options.scope, "scope");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (options.scopeOnly !== void 0 && typeof options.scopeOnly !== "boolean") {
|
|
179
|
+
throw new Error("scopeOnly must be a boolean");
|
|
180
|
+
}
|
|
181
|
+
if (options.preMajor !== void 0 && typeof options.preMajor !== "boolean") {
|
|
182
|
+
throw new Error("preMajor must be a boolean");
|
|
183
|
+
}
|
|
184
|
+
for (const key of STRING_FORMAT_KEYS) {
|
|
185
|
+
const value = options[key];
|
|
186
|
+
if (value !== void 0 && typeof value !== "string") {
|
|
187
|
+
throw new Error(`${key} must be a string`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (options.bumpStrict !== void 0 && typeof options.bumpStrict !== "boolean") {
|
|
191
|
+
throw new Error("bumpStrict must be a boolean");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function validateTypes(types) {
|
|
195
|
+
if (!Array.isArray(types)) {
|
|
196
|
+
throw new Error("types must be an array");
|
|
197
|
+
}
|
|
198
|
+
types.forEach((entry, index) => {
|
|
199
|
+
if (!isPlainObject(entry)) {
|
|
200
|
+
throw new Error(`types[${index}] must be an object`);
|
|
201
|
+
}
|
|
202
|
+
if (typeof entry.type !== "string" || entry.type.length === 0) {
|
|
203
|
+
throw new Error(`types[${index}].type must be a non-empty string`);
|
|
204
|
+
}
|
|
205
|
+
if (entry.section !== void 0 && typeof entry.section !== "string") {
|
|
206
|
+
throw new Error(`types[${index}].section must be a string`);
|
|
207
|
+
}
|
|
208
|
+
if (entry.scope !== void 0 && typeof entry.scope !== "string") {
|
|
209
|
+
throw new Error(`types[${index}].scope must be a string`);
|
|
210
|
+
}
|
|
211
|
+
if (entry.effect !== void 0 && entry.effect !== "hidden") {
|
|
212
|
+
throw new Error(`types[${index}].effect must be 'hidden'`);
|
|
213
|
+
}
|
|
214
|
+
if (entry.hidden !== void 0 && typeof entry.hidden !== "boolean") {
|
|
215
|
+
throw new Error(`types[${index}].hidden must be a boolean`);
|
|
216
|
+
}
|
|
217
|
+
const knownKeys = /* @__PURE__ */ new Set(["type", "section", "scope", "effect", "hidden"]);
|
|
218
|
+
for (const key of Object.keys(entry)) {
|
|
219
|
+
if (!knownKeys.has(key)) {
|
|
220
|
+
throw new Error(`types[${index}] has unknown field: ${key}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function validateStringArray(value, label) {
|
|
226
|
+
if (!Array.isArray(value)) {
|
|
227
|
+
throw new Error(`${label} must be an array of strings`);
|
|
228
|
+
}
|
|
229
|
+
value.forEach((entry, index) => {
|
|
230
|
+
if (typeof entry !== "string") {
|
|
231
|
+
throw new Error(`${label}[${index}] must be a string`);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
103
235
|
function resolveOutputPath(cwd, outputFile) {
|
|
104
236
|
return isAbsolute(outputFile) ? outputFile : resolve(cwd, outputFile);
|
|
105
237
|
}
|
|
106
|
-
function pipeGeneratorToFile(generator, outputPath) {
|
|
238
|
+
async function pipeGeneratorToFile(generator, outputPath, append) {
|
|
239
|
+
const tempPath = `${outputPath}.${randomUUID()}.tmp`;
|
|
240
|
+
const generated = await readGeneratorOutput(generator.writeStream());
|
|
241
|
+
const existing = await readExistingOutput(outputPath);
|
|
242
|
+
const contents = combineChangelogContents(existing, generated, append);
|
|
243
|
+
try {
|
|
244
|
+
await writeFile(tempPath, contents, "utf8");
|
|
245
|
+
await rename(tempPath, outputPath);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
await rm(tempPath, { force: true });
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
return outputPath;
|
|
251
|
+
}
|
|
252
|
+
function readGeneratorOutput(stream) {
|
|
107
253
|
return new Promise((resolvePromise, reject) => {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
254
|
+
const chunks = [];
|
|
255
|
+
const onData = (chunk) => {
|
|
256
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
257
|
+
};
|
|
110
258
|
const onError = (error) => {
|
|
111
|
-
|
|
112
|
-
fileStream.off("error", onError);
|
|
113
|
-
fileStream.off("finish", onFinish);
|
|
259
|
+
cleanup();
|
|
114
260
|
reject(error);
|
|
115
261
|
};
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
fileStream.off("finish", onFinish);
|
|
120
|
-
resolvePromise(outputPath);
|
|
262
|
+
const onEnd = () => {
|
|
263
|
+
cleanup();
|
|
264
|
+
resolvePromise(Buffer.concat(chunks).toString("utf8"));
|
|
121
265
|
};
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
266
|
+
const cleanup = () => {
|
|
267
|
+
stream.off("data", onData);
|
|
268
|
+
stream.off("error", onError);
|
|
269
|
+
stream.off("end", onEnd);
|
|
270
|
+
};
|
|
271
|
+
stream.on("data", onData);
|
|
272
|
+
stream.on("error", onError);
|
|
273
|
+
stream.on("end", onEnd);
|
|
126
274
|
});
|
|
127
275
|
}
|
|
276
|
+
async function readExistingOutput(outputPath) {
|
|
277
|
+
try {
|
|
278
|
+
return await readFile(outputPath, "utf8");
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error.code === "ENOENT") {
|
|
281
|
+
return void 0;
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function combineChangelogContents(existing, generated, append) {
|
|
287
|
+
const next = trimTrailingNewlines(generated);
|
|
288
|
+
const current = trimTrailingNewlines(existing ?? "");
|
|
289
|
+
if (current.length === 0) {
|
|
290
|
+
return `${next}
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
if (next.length === 0) {
|
|
294
|
+
return `${current}
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
return append ? `${current}
|
|
298
|
+
|
|
299
|
+
${next}
|
|
300
|
+
` : `${next}
|
|
301
|
+
|
|
302
|
+
${current}
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
function trimTrailingNewlines(value) {
|
|
306
|
+
return value.replace(/\n+$/u, "");
|
|
307
|
+
}
|
|
128
308
|
async function createPreset(options = {}) {
|
|
309
|
+
validatePresetOptions(options);
|
|
129
310
|
const preset = await createConventionalCommitsPreset(resolvePresetOptions(options));
|
|
130
311
|
return {
|
|
131
312
|
...preset,
|
|
@@ -134,19 +315,16 @@ async function createPreset(options = {}) {
|
|
|
134
315
|
}
|
|
135
316
|
async function createGenerator(options = {}) {
|
|
136
317
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
318
|
+
validateGenerateChangelogOptions(options);
|
|
137
319
|
const presetOptions = splitPresetOptions(options);
|
|
138
320
|
const preset = await createPreset(presetOptions);
|
|
139
321
|
const generator = new ConventionalChangelog(cwd);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
firstRelease: options.firstRelease ?? false
|
|
147
|
-
};
|
|
148
|
-
generator.readPackage(resolve(cwd, "package.json")).loadPreset(preset).options(generatorOptions).config({
|
|
149
|
-
tags: preset.tags,
|
|
322
|
+
generator.readPackage(resolve(cwd, "package.json")).loadPreset(preset).options(resolveGeneratorOptions(options)).tags(
|
|
323
|
+
resolveTagOptions({
|
|
324
|
+
tagPrefix: options.tagPrefix ?? "v",
|
|
325
|
+
skipUnstable: options.skipUnstable ?? true
|
|
326
|
+
})
|
|
327
|
+
).config({
|
|
150
328
|
commits: preset.commits,
|
|
151
329
|
parser: preset.parser,
|
|
152
330
|
writer: preset.writer
|
|
@@ -158,7 +336,7 @@ async function generateChangelog(options = {}) {
|
|
|
158
336
|
const outputPath = resolveOutputPath(cwd, options.outputFile ?? "CHANGELOG.md");
|
|
159
337
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
160
338
|
const generator = await createGenerator({ ...options, cwd });
|
|
161
|
-
return await pipeGeneratorToFile(generator, outputPath);
|
|
339
|
+
return await pipeGeneratorToFile(generator, outputPath, options.append ?? false);
|
|
162
340
|
}
|
|
163
341
|
|
|
164
342
|
// src/cli.ts
|
|
@@ -171,8 +349,7 @@ var SPECS = [
|
|
|
171
349
|
{ name: "append", boolean: true, negatable: true },
|
|
172
350
|
{ name: "first-release", boolean: true, negatable: true },
|
|
173
351
|
{ name: "skip-unstable", boolean: true, negatable: true },
|
|
174
|
-
{ name: "output-unreleased", boolean: true, negatable: true }
|
|
175
|
-
INTERACTIVE_FLAG
|
|
352
|
+
{ name: "output-unreleased", boolean: true, negatable: true }
|
|
176
353
|
];
|
|
177
354
|
function printHelp() {
|
|
178
355
|
console.log(`repo-toolkit-changelog
|
|
@@ -181,31 +358,33 @@ Usage:
|
|
|
181
358
|
repo-toolkit-changelog [options]
|
|
182
359
|
|
|
183
360
|
Options:
|
|
184
|
-
--config <path>
|
|
185
|
-
--cwd <path>
|
|
186
|
-
--output <path>
|
|
187
|
-
--tag-prefix <prefix>
|
|
188
|
-
--release-count <
|
|
189
|
-
--append
|
|
190
|
-
--first-release
|
|
191
|
-
--no-skip-unstable
|
|
192
|
-
--no-output-unreleased
|
|
193
|
-
-
|
|
194
|
-
-h, --help Show this help message
|
|
361
|
+
--config <path> Config file (JSON or JS module) with changelog options
|
|
362
|
+
--cwd <path> Working directory to read package and git metadata from
|
|
363
|
+
--output <path> Output file path (default: CHANGELOG.md)
|
|
364
|
+
--tag-prefix <prefix> Tag prefix to match (default: v; use --tag-prefix= for empty)
|
|
365
|
+
--release-count <non-negative int> Number of releases to include (default: latest release only)
|
|
366
|
+
--append / --no-append Append to or prepend to the output (default: prepend)
|
|
367
|
+
--first-release / --no-first-release Include all commits when no prior release tag exists
|
|
368
|
+
--skip-unstable / --no-skip-unstable Skip prerelease tags (default: skip)
|
|
369
|
+
--output-unreleased / --no-output-unreleased Include the unreleased section (default: include)
|
|
370
|
+
-h, --help Show this help message
|
|
195
371
|
`);
|
|
196
372
|
}
|
|
197
373
|
function parseNumber(value, flag) {
|
|
198
374
|
const parsed = Number.parseInt(value, 10);
|
|
199
|
-
if (Number.isNaN(parsed)) {
|
|
375
|
+
if (Number.isNaN(parsed) || !Number.isSafeInteger(parsed) || parsed < 0) {
|
|
200
376
|
throw new Error(`Invalid numeric value for ${flag}: ${value}`);
|
|
201
377
|
}
|
|
202
378
|
return parsed;
|
|
203
379
|
}
|
|
380
|
+
function resolveGenerateChangelogCliOptions(result) {
|
|
381
|
+
return buildOptions(result.values);
|
|
382
|
+
}
|
|
204
383
|
function buildOptions(values) {
|
|
205
384
|
const options = {};
|
|
206
385
|
if (values.cwd) options.cwd = values.cwd;
|
|
207
386
|
if (values.output) options.outputFile = values.output;
|
|
208
|
-
if (values
|
|
387
|
+
if (Object.prototype.hasOwnProperty.call(values, "tag-prefix")) options.tagPrefix = values["tag-prefix"];
|
|
209
388
|
if (values["release-count"] !== void 0)
|
|
210
389
|
options.releaseCount = parseNumber(values["release-count"], "--release-count");
|
|
211
390
|
if (values.append !== void 0) options.append = values.append === "true";
|
|
@@ -222,13 +401,21 @@ async function main() {
|
|
|
222
401
|
}
|
|
223
402
|
const merged = await resolveCliOptions({
|
|
224
403
|
result,
|
|
225
|
-
buildOptions:
|
|
404
|
+
buildOptions: resolveGenerateChangelogCliOptions
|
|
226
405
|
});
|
|
227
406
|
const outputPath = await generateChangelog(merged);
|
|
228
407
|
console.log(`Changelog generated at ${outputPath}.`);
|
|
229
408
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
409
|
+
var executedAsEntryPoint = process.argv[1] ? import.meta.url === pathToFileURL(process.argv[1]).href : false;
|
|
410
|
+
if (executedAsEntryPoint) {
|
|
411
|
+
main().catch((error) => {
|
|
412
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
413
|
+
console.error(message);
|
|
414
|
+
process.exitCode = 1;
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
export {
|
|
418
|
+
SPECS,
|
|
419
|
+
printHelp,
|
|
420
|
+
resolveGenerateChangelogCliOptions
|
|
421
|
+
};
|
package/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ interface ChangelogType {
|
|
|
4
4
|
type: string;
|
|
5
5
|
section?: string;
|
|
6
6
|
scope?: string;
|
|
7
|
-
effect?: '
|
|
7
|
+
effect?: 'hidden';
|
|
8
8
|
hidden?: boolean;
|
|
9
9
|
}
|
|
10
10
|
interface ChangelogContext {
|
|
@@ -29,10 +29,11 @@ interface ConventionalCommitsPresetOptions {
|
|
|
29
29
|
scope?: string | ReadonlyArray<string>;
|
|
30
30
|
scopeOnly?: boolean;
|
|
31
31
|
preMajor?: boolean;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
issueUrlFormat?: string;
|
|
33
|
+
commitUrlFormat?: string;
|
|
34
|
+
compareUrlFormat?: string;
|
|
35
|
+
userUrlFormat?: string;
|
|
36
|
+
bumpStrict?: boolean;
|
|
36
37
|
}
|
|
37
38
|
type CreatePresetOptions = ConventionalCommitsPresetOptions;
|
|
38
39
|
interface GenerateChangelogOptions extends CreatePresetOptions {
|
|
@@ -46,9 +47,19 @@ interface GenerateChangelogOptions extends CreatePresetOptions {
|
|
|
46
47
|
firstRelease?: boolean;
|
|
47
48
|
}
|
|
48
49
|
type ChangelogConfig = GenerateChangelogOptions;
|
|
49
|
-
declare const DEFAULT_TYPES: ReadonlyArray<ChangelogType
|
|
50
|
-
declare function
|
|
50
|
+
declare const DEFAULT_TYPES: ReadonlyArray<Readonly<ChangelogType>>;
|
|
51
|
+
declare function getDefaultTypes(): ReadonlyArray<Readonly<ChangelogType>>;
|
|
52
|
+
declare function createPreset(options?: CreatePresetOptions): Promise<{
|
|
53
|
+
name: string;
|
|
54
|
+
commits?: GetCommitsParams;
|
|
55
|
+
parser?: ParserStreamOptions;
|
|
56
|
+
writer?: WriterOptions;
|
|
57
|
+
whatBump?: (commits: Commit[]) => {
|
|
58
|
+
level: number;
|
|
59
|
+
reason: string;
|
|
60
|
+
} | null;
|
|
61
|
+
}>;
|
|
51
62
|
declare function createGenerator(options?: GenerateChangelogOptions): Promise<ConventionalChangelog>;
|
|
52
63
|
declare function generateChangelog(options?: GenerateChangelogOptions): Promise<string>;
|
|
53
64
|
|
|
54
|
-
export { type ChangelogCommit, type ChangelogConfig, type ChangelogContext, type ChangelogReference, type ChangelogType, type ConventionalCommitsPresetOptions, type CreatePresetOptions, DEFAULT_TYPES, type GenerateChangelogOptions, createGenerator, createPreset, generateChangelog };
|
|
65
|
+
export { type ChangelogCommit, type ChangelogConfig, type ChangelogContext, type ChangelogReference, type ChangelogType, type ConventionalCommitsPresetOptions, type CreatePresetOptions, DEFAULT_TYPES, type GenerateChangelogOptions, createGenerator, createPreset, generateChangelog, getDefaultTypes };
|
package/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import {
|
|
3
|
-
import { mkdir } from "fs/promises";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
4
3
|
import { dirname, isAbsolute, resolve } from "path";
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { isPlainObject } from "@repo-toolkit/publish-package";
|
|
5
6
|
import { ConventionalChangelog } from "conventional-changelog";
|
|
6
7
|
import createConventionalCommitsPreset from "conventional-changelog-conventionalcommits";
|
|
7
8
|
var PIPELINE_OPTION_KEYS = [
|
|
@@ -24,65 +25,70 @@ function splitPresetOptions(options) {
|
|
|
24
25
|
}
|
|
25
26
|
return presetOptions;
|
|
26
27
|
}
|
|
27
|
-
var DEFAULT_TYPES =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
28
|
+
var DEFAULT_TYPES = Object.freeze(
|
|
29
|
+
[
|
|
30
|
+
{
|
|
31
|
+
type: "feat",
|
|
32
|
+
section: "Features"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: "fix",
|
|
36
|
+
scope: "deps",
|
|
37
|
+
effect: "hidden"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: "fix",
|
|
41
|
+
section: "Bug Fixes"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: "revert",
|
|
45
|
+
section: "Reverts"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "docs",
|
|
49
|
+
section: "Documentation"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: "refactor",
|
|
53
|
+
section: "Code Refactoring"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: "perf",
|
|
57
|
+
section: "Performance Improvements"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: "build",
|
|
61
|
+
section: "Build System"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: "e2e",
|
|
65
|
+
section: "End-to-end Testing"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: "ci",
|
|
69
|
+
effect: "hidden"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: "chore",
|
|
73
|
+
effect: "hidden"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
type: "style",
|
|
77
|
+
effect: "hidden"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "test",
|
|
81
|
+
effect: "hidden"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: "release",
|
|
85
|
+
effect: "hidden"
|
|
86
|
+
}
|
|
87
|
+
].map((entry) => Object.freeze(entry))
|
|
88
|
+
);
|
|
89
|
+
function getDefaultTypes() {
|
|
90
|
+
return DEFAULT_TYPES;
|
|
91
|
+
}
|
|
86
92
|
function normalizeTypes(types) {
|
|
87
93
|
return types.map((entry) => ({
|
|
88
94
|
...entry,
|
|
@@ -95,32 +101,209 @@ function resolvePresetOptions(options = {}) {
|
|
|
95
101
|
types: normalizeTypes(options.types ?? DEFAULT_TYPES)
|
|
96
102
|
};
|
|
97
103
|
}
|
|
104
|
+
function resolveTagOptions(options) {
|
|
105
|
+
const tags = {};
|
|
106
|
+
if (options.tagPrefix !== void 0) {
|
|
107
|
+
tags.prefix = options.tagPrefix;
|
|
108
|
+
}
|
|
109
|
+
if (options.skipUnstable !== void 0) {
|
|
110
|
+
tags.skipUnstable = options.skipUnstable;
|
|
111
|
+
}
|
|
112
|
+
return tags;
|
|
113
|
+
}
|
|
114
|
+
function resolveGeneratorOptions(options) {
|
|
115
|
+
const resolvedOptions = {
|
|
116
|
+
append: options.append ?? false,
|
|
117
|
+
outputUnreleased: options.outputUnreleased ?? true
|
|
118
|
+
};
|
|
119
|
+
if (options.firstRelease === true) {
|
|
120
|
+
resolvedOptions.releaseCount = 0;
|
|
121
|
+
} else if (options.releaseCount !== void 0) {
|
|
122
|
+
resolvedOptions.releaseCount = validateReleaseCount(options.releaseCount);
|
|
123
|
+
}
|
|
124
|
+
return resolvedOptions;
|
|
125
|
+
}
|
|
126
|
+
function validateGenerateChangelogOptions(options) {
|
|
127
|
+
if (options.releaseCount !== void 0) {
|
|
128
|
+
validateReleaseCount(options.releaseCount);
|
|
129
|
+
}
|
|
130
|
+
validatePresetOptions(splitPresetOptions(options));
|
|
131
|
+
}
|
|
132
|
+
function validateReleaseCount(value) {
|
|
133
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
134
|
+
throw new Error(`releaseCount must be a non-negative safe integer, got: ${value}`);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
var PRESET_OPTION_KEYS = [
|
|
139
|
+
"types",
|
|
140
|
+
"ignoreCommits",
|
|
141
|
+
"issuePrefixes",
|
|
142
|
+
"scope",
|
|
143
|
+
"scopeOnly",
|
|
144
|
+
"preMajor",
|
|
145
|
+
"issueUrlFormat",
|
|
146
|
+
"commitUrlFormat",
|
|
147
|
+
"compareUrlFormat",
|
|
148
|
+
"userUrlFormat",
|
|
149
|
+
"bumpStrict"
|
|
150
|
+
];
|
|
151
|
+
var STRING_FORMAT_KEYS = ["issueUrlFormat", "commitUrlFormat", "compareUrlFormat", "userUrlFormat"];
|
|
152
|
+
function validatePresetOptions(options) {
|
|
153
|
+
for (const key of Object.keys(options)) {
|
|
154
|
+
if (!PRESET_OPTION_KEYS.includes(key)) {
|
|
155
|
+
throw new Error(`Unknown changelog config option: ${key}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (options.types !== void 0) {
|
|
159
|
+
validateTypes(options.types);
|
|
160
|
+
}
|
|
161
|
+
if (options.ignoreCommits !== void 0 && !(options.ignoreCommits instanceof RegExp)) {
|
|
162
|
+
throw new Error("ignoreCommits must be a RegExp");
|
|
163
|
+
}
|
|
164
|
+
if (options.issuePrefixes !== void 0) {
|
|
165
|
+
validateStringArray(options.issuePrefixes, "issuePrefixes");
|
|
166
|
+
}
|
|
167
|
+
if (options.scope !== void 0) {
|
|
168
|
+
if (typeof options.scope !== "string" && !Array.isArray(options.scope)) {
|
|
169
|
+
throw new Error("scope must be a string or an array of strings");
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(options.scope)) {
|
|
172
|
+
validateStringArray(options.scope, "scope");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (options.scopeOnly !== void 0 && typeof options.scopeOnly !== "boolean") {
|
|
176
|
+
throw new Error("scopeOnly must be a boolean");
|
|
177
|
+
}
|
|
178
|
+
if (options.preMajor !== void 0 && typeof options.preMajor !== "boolean") {
|
|
179
|
+
throw new Error("preMajor must be a boolean");
|
|
180
|
+
}
|
|
181
|
+
for (const key of STRING_FORMAT_KEYS) {
|
|
182
|
+
const value = options[key];
|
|
183
|
+
if (value !== void 0 && typeof value !== "string") {
|
|
184
|
+
throw new Error(`${key} must be a string`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (options.bumpStrict !== void 0 && typeof options.bumpStrict !== "boolean") {
|
|
188
|
+
throw new Error("bumpStrict must be a boolean");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function validateTypes(types) {
|
|
192
|
+
if (!Array.isArray(types)) {
|
|
193
|
+
throw new Error("types must be an array");
|
|
194
|
+
}
|
|
195
|
+
types.forEach((entry, index) => {
|
|
196
|
+
if (!isPlainObject(entry)) {
|
|
197
|
+
throw new Error(`types[${index}] must be an object`);
|
|
198
|
+
}
|
|
199
|
+
if (typeof entry.type !== "string" || entry.type.length === 0) {
|
|
200
|
+
throw new Error(`types[${index}].type must be a non-empty string`);
|
|
201
|
+
}
|
|
202
|
+
if (entry.section !== void 0 && typeof entry.section !== "string") {
|
|
203
|
+
throw new Error(`types[${index}].section must be a string`);
|
|
204
|
+
}
|
|
205
|
+
if (entry.scope !== void 0 && typeof entry.scope !== "string") {
|
|
206
|
+
throw new Error(`types[${index}].scope must be a string`);
|
|
207
|
+
}
|
|
208
|
+
if (entry.effect !== void 0 && entry.effect !== "hidden") {
|
|
209
|
+
throw new Error(`types[${index}].effect must be 'hidden'`);
|
|
210
|
+
}
|
|
211
|
+
if (entry.hidden !== void 0 && typeof entry.hidden !== "boolean") {
|
|
212
|
+
throw new Error(`types[${index}].hidden must be a boolean`);
|
|
213
|
+
}
|
|
214
|
+
const knownKeys = /* @__PURE__ */ new Set(["type", "section", "scope", "effect", "hidden"]);
|
|
215
|
+
for (const key of Object.keys(entry)) {
|
|
216
|
+
if (!knownKeys.has(key)) {
|
|
217
|
+
throw new Error(`types[${index}] has unknown field: ${key}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function validateStringArray(value, label) {
|
|
223
|
+
if (!Array.isArray(value)) {
|
|
224
|
+
throw new Error(`${label} must be an array of strings`);
|
|
225
|
+
}
|
|
226
|
+
value.forEach((entry, index) => {
|
|
227
|
+
if (typeof entry !== "string") {
|
|
228
|
+
throw new Error(`${label}[${index}] must be a string`);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
98
232
|
function resolveOutputPath(cwd, outputFile) {
|
|
99
233
|
return isAbsolute(outputFile) ? outputFile : resolve(cwd, outputFile);
|
|
100
234
|
}
|
|
101
|
-
function pipeGeneratorToFile(generator, outputPath) {
|
|
235
|
+
async function pipeGeneratorToFile(generator, outputPath, append) {
|
|
236
|
+
const tempPath = `${outputPath}.${randomUUID()}.tmp`;
|
|
237
|
+
const generated = await readGeneratorOutput(generator.writeStream());
|
|
238
|
+
const existing = await readExistingOutput(outputPath);
|
|
239
|
+
const contents = combineChangelogContents(existing, generated, append);
|
|
240
|
+
try {
|
|
241
|
+
await writeFile(tempPath, contents, "utf8");
|
|
242
|
+
await rename(tempPath, outputPath);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
await rm(tempPath, { force: true });
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
return outputPath;
|
|
248
|
+
}
|
|
249
|
+
function readGeneratorOutput(stream) {
|
|
102
250
|
return new Promise((resolvePromise, reject) => {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
251
|
+
const chunks = [];
|
|
252
|
+
const onData = (chunk) => {
|
|
253
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
254
|
+
};
|
|
105
255
|
const onError = (error) => {
|
|
106
|
-
|
|
107
|
-
fileStream.off("error", onError);
|
|
108
|
-
fileStream.off("finish", onFinish);
|
|
256
|
+
cleanup();
|
|
109
257
|
reject(error);
|
|
110
258
|
};
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
fileStream.off("finish", onFinish);
|
|
115
|
-
resolvePromise(outputPath);
|
|
259
|
+
const onEnd = () => {
|
|
260
|
+
cleanup();
|
|
261
|
+
resolvePromise(Buffer.concat(chunks).toString("utf8"));
|
|
116
262
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
263
|
+
const cleanup = () => {
|
|
264
|
+
stream.off("data", onData);
|
|
265
|
+
stream.off("error", onError);
|
|
266
|
+
stream.off("end", onEnd);
|
|
267
|
+
};
|
|
268
|
+
stream.on("data", onData);
|
|
269
|
+
stream.on("error", onError);
|
|
270
|
+
stream.on("end", onEnd);
|
|
121
271
|
});
|
|
122
272
|
}
|
|
273
|
+
async function readExistingOutput(outputPath) {
|
|
274
|
+
try {
|
|
275
|
+
return await readFile(outputPath, "utf8");
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error.code === "ENOENT") {
|
|
278
|
+
return void 0;
|
|
279
|
+
}
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function combineChangelogContents(existing, generated, append) {
|
|
284
|
+
const next = trimTrailingNewlines(generated);
|
|
285
|
+
const current = trimTrailingNewlines(existing ?? "");
|
|
286
|
+
if (current.length === 0) {
|
|
287
|
+
return `${next}
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
if (next.length === 0) {
|
|
291
|
+
return `${current}
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
return append ? `${current}
|
|
295
|
+
|
|
296
|
+
${next}
|
|
297
|
+
` : `${next}
|
|
298
|
+
|
|
299
|
+
${current}
|
|
300
|
+
`;
|
|
301
|
+
}
|
|
302
|
+
function trimTrailingNewlines(value) {
|
|
303
|
+
return value.replace(/\n+$/u, "");
|
|
304
|
+
}
|
|
123
305
|
async function createPreset(options = {}) {
|
|
306
|
+
validatePresetOptions(options);
|
|
124
307
|
const preset = await createConventionalCommitsPreset(resolvePresetOptions(options));
|
|
125
308
|
return {
|
|
126
309
|
...preset,
|
|
@@ -129,19 +312,16 @@ async function createPreset(options = {}) {
|
|
|
129
312
|
}
|
|
130
313
|
async function createGenerator(options = {}) {
|
|
131
314
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
315
|
+
validateGenerateChangelogOptions(options);
|
|
132
316
|
const presetOptions = splitPresetOptions(options);
|
|
133
317
|
const preset = await createPreset(presetOptions);
|
|
134
318
|
const generator = new ConventionalChangelog(cwd);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
firstRelease: options.firstRelease ?? false
|
|
142
|
-
};
|
|
143
|
-
generator.readPackage(resolve(cwd, "package.json")).loadPreset(preset).options(generatorOptions).config({
|
|
144
|
-
tags: preset.tags,
|
|
319
|
+
generator.readPackage(resolve(cwd, "package.json")).loadPreset(preset).options(resolveGeneratorOptions(options)).tags(
|
|
320
|
+
resolveTagOptions({
|
|
321
|
+
tagPrefix: options.tagPrefix ?? "v",
|
|
322
|
+
skipUnstable: options.skipUnstable ?? true
|
|
323
|
+
})
|
|
324
|
+
).config({
|
|
145
325
|
commits: preset.commits,
|
|
146
326
|
parser: preset.parser,
|
|
147
327
|
writer: preset.writer
|
|
@@ -153,11 +333,12 @@ async function generateChangelog(options = {}) {
|
|
|
153
333
|
const outputPath = resolveOutputPath(cwd, options.outputFile ?? "CHANGELOG.md");
|
|
154
334
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
155
335
|
const generator = await createGenerator({ ...options, cwd });
|
|
156
|
-
return await pipeGeneratorToFile(generator, outputPath);
|
|
336
|
+
return await pipeGeneratorToFile(generator, outputPath, options.append ?? false);
|
|
157
337
|
}
|
|
158
338
|
export {
|
|
159
339
|
DEFAULT_TYPES,
|
|
160
340
|
createGenerator,
|
|
161
341
|
createPreset,
|
|
162
|
-
generateChangelog
|
|
342
|
+
generateChangelog,
|
|
343
|
+
getDefaultTypes
|
|
163
344
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@repo-toolkit/changelog",
|
|
3
3
|
"description": "Shared conventional changelog preset, generator, and CLI for repository releases",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"keywords": [
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"node": ">=20"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@repo-toolkit/publish-package": "0.
|
|
30
|
+
"@repo-toolkit/publish-package": "0.11.0",
|
|
31
31
|
"conventional-changelog": "^7.2.1",
|
|
32
32
|
"conventional-changelog-conventionalcommits": "^9.3.1"
|
|
33
33
|
},
|