@ckeditor/ckeditor5-dev-changelog 50.1.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.md +16 -0
- package/README.md +46 -0
- package/bin/generate-template.js +12 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1284 -0
- package/dist/tasks/generatechangelogformonorepository.d.ts +11 -0
- package/dist/tasks/generatechangelogforsinglerepository.d.ts +8 -0
- package/dist/template.d.ts +20 -0
- package/dist/template.js +108 -0
- package/dist/types.d.ts +120 -0
- package/dist/utils/asyncarray.d.ts +35 -0
- package/dist/utils/commitchanges.d.ts +8 -0
- package/dist/utils/composechangelog.d.ts +27 -0
- package/dist/utils/composereleasesummary.d.ts +23 -0
- package/dist/utils/constants.d.ts +56 -0
- package/dist/utils/determinenextversion.d.ts +26 -0
- package/dist/utils/displaychanges.d.ts +22 -0
- package/dist/utils/filtervisiblesections.d.ts +13 -0
- package/dist/utils/findchangelogentrypaths.d.ts +15 -0
- package/dist/utils/findpackages.d.ts +16 -0
- package/dist/utils/generatechangelog.d.ts +18 -0
- package/dist/utils/getdateformatted.d.ts +8 -0
- package/dist/utils/groupentriesbysection.d.ts +16 -0
- package/dist/utils/internalerror.d.ts +10 -0
- package/dist/utils/linktogithubuser.d.ts +11 -0
- package/dist/utils/loginfo.d.ts +12 -0
- package/dist/utils/modifychangelog.d.ts +11 -0
- package/dist/utils/normalizeentry.d.ts +6 -0
- package/dist/utils/parsechangelogentries.d.ts +9 -0
- package/dist/utils/providenewversion.d.ts +21 -0
- package/dist/utils/removechangelogentryfiles.d.ts +10 -0
- package/dist/utils/sortentriesbyscopeanddate.d.ts +12 -0
- package/dist/utils/truncatechangelog.d.ts +8 -0
- package/dist/utils/useraborterror.d.ts +9 -0
- package/dist/utils/validateentry.d.ts +17 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { parse, isValid, format } from 'date-fns';
|
|
3
|
+
import { workspaces, npm, tools } from '@ckeditor/ckeditor5-dev-utils';
|
|
4
|
+
import upath from 'upath';
|
|
5
|
+
import fs$1 from 'fs-extra';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import semver from 'semver';
|
|
8
|
+
import inquirer from 'inquirer';
|
|
9
|
+
import { glob } from 'glob';
|
|
10
|
+
import matter from 'gray-matter';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
14
|
+
* For licensing, see LICENSE.md.
|
|
15
|
+
*/
|
|
16
|
+
const CHANGELOG_FILE = 'CHANGELOG.md';
|
|
17
|
+
const CHANGELOG_HEADER = 'Changelog\n=========';
|
|
18
|
+
const NPM_URL = 'https://www.npmjs.com/package';
|
|
19
|
+
const VERSIONING_POLICY_URL = 'https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html';
|
|
20
|
+
const CHANGESET_DIRECTORY = '.changelog';
|
|
21
|
+
upath.join(import.meta.dirname, '../template/template.md');
|
|
22
|
+
const SECTIONS = {
|
|
23
|
+
major: {
|
|
24
|
+
title: `MAJOR BREAKING CHANGES [ℹ️](${VERSIONING_POLICY_URL}#major-and-minor-breaking-changes)`,
|
|
25
|
+
titleInLogs: 'MAJOR BREAKING CHANGES'
|
|
26
|
+
},
|
|
27
|
+
minor: {
|
|
28
|
+
title: `MINOR BREAKING CHANGES [ℹ️](${VERSIONING_POLICY_URL}#major-and-minor-breaking-changes)`,
|
|
29
|
+
titleInLogs: 'MINOR BREAKING CHANGES'
|
|
30
|
+
},
|
|
31
|
+
breaking: { title: 'BREAKING CHANGES' },
|
|
32
|
+
feature: { title: 'Features' },
|
|
33
|
+
fix: { title: 'Bug fixes' },
|
|
34
|
+
other: { title: 'Other changes' },
|
|
35
|
+
warning: {
|
|
36
|
+
title: 'Incorrect values',
|
|
37
|
+
excludeInChangelog: true
|
|
38
|
+
},
|
|
39
|
+
invalid: {
|
|
40
|
+
title: 'Invalid files',
|
|
41
|
+
excludeInChangelog: true
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const ISSUE_SLUG_PATTERN = /^(?<owner>[a-z0-9.-]+)\/(?<repository>[a-z0-9.-]+)#(?<number>\d+)$/;
|
|
45
|
+
const ISSUE_PATTERN = /^\d+$/;
|
|
46
|
+
const ISSUE_URL_PATTERN = /^(?<base>https:\/\/github\.com)\/(?<owner>[a-z0-9.-]+)\/(?<repository>[a-z0-9.-]+)\/issues\/(?<number>\d+)$/;
|
|
47
|
+
const TYPES = [
|
|
48
|
+
{ name: 'Feature' },
|
|
49
|
+
{ name: 'Other' },
|
|
50
|
+
{ name: 'Fix' },
|
|
51
|
+
{ name: 'Major breaking change' },
|
|
52
|
+
{ name: 'Minor breaking change' },
|
|
53
|
+
{ name: 'Breaking change' }
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
58
|
+
* For licensing, see LICENSE.md.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* This function enhances changelog entries by linking contributor usernames to their GitHub profiles.
|
|
62
|
+
*
|
|
63
|
+
* It searches for occurrences of GitHub-style mentions (e.g., @username) in the given comment string
|
|
64
|
+
* and transforms them into Markdown links pointing to the corresponding GitHub user page.
|
|
65
|
+
*/
|
|
66
|
+
function linkToGitHubUser(comment) {
|
|
67
|
+
return comment.replace(/(^|[\s(])@([\w-]+)(?![/\w-])/ig, (_, charBefore, nickName) => {
|
|
68
|
+
return `${charBefore}[@${nickName}](https://github.com/${nickName})`;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
74
|
+
* For licensing, see LICENSE.md.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Validates a changelog entry against expected types, scopes, and issue references.
|
|
78
|
+
*
|
|
79
|
+
* It checks if the type is valid and consistent with single or multi-package modes,
|
|
80
|
+
* verifies scopes against known package names, and ensures issue references are correctly formatted.
|
|
81
|
+
*
|
|
82
|
+
* Returns whether the entry is valid along with a validated version including any validation messages.
|
|
83
|
+
*/
|
|
84
|
+
function validateEntry(entry, packagesNames, singlePackage) {
|
|
85
|
+
const noScopePackagesNames = packagesNames.map(packageName => packageName.replace(/@.*\//, ''));
|
|
86
|
+
const data = entry.data;
|
|
87
|
+
const validations = [];
|
|
88
|
+
let isValid = true;
|
|
89
|
+
const allowedTypesArray = TYPES.map(({ name }) => name);
|
|
90
|
+
const allowedTypesList = getAllowedTypesList();
|
|
91
|
+
if (typeof data.type === 'undefined') {
|
|
92
|
+
validations.push(`Provide a type with one of the values: ${allowedTypesList} (case insensitive).`);
|
|
93
|
+
isValid = false;
|
|
94
|
+
}
|
|
95
|
+
else if (!allowedTypesArray.includes(data.type)) {
|
|
96
|
+
validations.push(`Type is required and should be one of: ${allowedTypesList} (case insensitive).`);
|
|
97
|
+
isValid = false;
|
|
98
|
+
}
|
|
99
|
+
if (singlePackage && ['Major breaking change', 'Minor breaking change'].includes(data.type)) {
|
|
100
|
+
validations.push(`Breaking change "${data.type}" should be generic: "Breaking change", for a single package mode (case insensitive).`);
|
|
101
|
+
isValid = false;
|
|
102
|
+
}
|
|
103
|
+
if (!singlePackage && data.type === 'Breaking change') {
|
|
104
|
+
validations.push(`Breaking change "${data.type}" should be one of: "Minor breaking change", "Major breaking change" ` +
|
|
105
|
+
'for a monorepo (case insensitive).');
|
|
106
|
+
isValid = false;
|
|
107
|
+
}
|
|
108
|
+
const scopeValidated = [];
|
|
109
|
+
if (singlePackage) {
|
|
110
|
+
// Skip scope validation for single package mode
|
|
111
|
+
scopeValidated.push(...data.scope);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
for (const scopeName of data.scope) {
|
|
115
|
+
if (!noScopePackagesNames.includes(scopeName)) {
|
|
116
|
+
validations.push(`Scope "${scopeName}" is not recognized as a valid package in the repository.`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
scopeValidated.push(scopeName);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
data.scope = scopeValidated;
|
|
124
|
+
const seeValidated = [];
|
|
125
|
+
for (const see of data.see) {
|
|
126
|
+
if (!(see.match(ISSUE_PATTERN) || see.match(ISSUE_SLUG_PATTERN) || see.match(ISSUE_URL_PATTERN))) {
|
|
127
|
+
validations.push([
|
|
128
|
+
`See "${see}" is not a valid issue reference. Provide either:`,
|
|
129
|
+
'issue number, repository-slug#id or full issue link URL.'
|
|
130
|
+
].join(' '));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
seeValidated.push(see);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
data.see = seeValidated;
|
|
137
|
+
const closesValidated = [];
|
|
138
|
+
for (const closes of data.closes) {
|
|
139
|
+
if (!(closes.match(ISSUE_PATTERN) || closes.match(ISSUE_SLUG_PATTERN) || closes.match(ISSUE_URL_PATTERN))) {
|
|
140
|
+
validations.push([
|
|
141
|
+
`Closes "${closes}" is not a valid issue reference. Provide either:`,
|
|
142
|
+
'issue number, repository-slug#id or full issue link URL.'
|
|
143
|
+
].join(' '));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
closesValidated.push(closes);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
data.closes = closesValidated;
|
|
150
|
+
const validatedEntry = {
|
|
151
|
+
...entry,
|
|
152
|
+
data: {
|
|
153
|
+
...data,
|
|
154
|
+
validations,
|
|
155
|
+
type: data.type
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
return { isValid, validatedEntry };
|
|
159
|
+
}
|
|
160
|
+
function getAllowedTypesList() {
|
|
161
|
+
const formatter = new Intl.ListFormat('en-US', { style: 'long', type: 'disjunction' });
|
|
162
|
+
const items = TYPES.map(type => `"${type.name}"`);
|
|
163
|
+
return formatter.format(items);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
168
|
+
* For licensing, see LICENSE.md.
|
|
169
|
+
*/
|
|
170
|
+
/**
|
|
171
|
+
* This function categorizes changelog entries based on their types and packages.
|
|
172
|
+
*/
|
|
173
|
+
function groupEntriesBySection(options) {
|
|
174
|
+
const { files, packagesMetadata, transformScope, isSinglePackage } = options;
|
|
175
|
+
const packageNames = [...packagesMetadata.keys()];
|
|
176
|
+
return files.reduce((sections, entry) => {
|
|
177
|
+
const { validatedEntry, isValid } = validateEntry(entry, packageNames, isSinglePackage);
|
|
178
|
+
const validatedData = validatedEntry.data;
|
|
179
|
+
const scope = isSinglePackage ? null : getScopesLinks(validatedData.scope, transformScope);
|
|
180
|
+
const closes = getIssuesLinks(validatedData.closes, 'Closes', validatedEntry.gitHubUrl);
|
|
181
|
+
const see = getIssuesLinks(validatedData.see, 'See', validatedEntry.gitHubUrl);
|
|
182
|
+
const section = getSection({ entry: validatedEntry, isSinglePackage, isValid });
|
|
183
|
+
const contentWithCommunityCredits = getContentWithCommunityCredits(validatedEntry.content, validatedData.communityCredits);
|
|
184
|
+
const content = linkToGitHubUser(contentWithCommunityCredits);
|
|
185
|
+
const [mainContent, ...restContent] = formatContent(content);
|
|
186
|
+
const changeMessage = getChangeMessage({ restContent, scope, mainContent, entry, see, closes });
|
|
187
|
+
const newEntry = {
|
|
188
|
+
message: changeMessage,
|
|
189
|
+
data: {
|
|
190
|
+
mainContent,
|
|
191
|
+
restContent,
|
|
192
|
+
type: validatedData.type,
|
|
193
|
+
scope: validatedData.scope,
|
|
194
|
+
see: validatedData.see.map(see => getIssueLinkObject(see, validatedEntry.gitHubUrl)),
|
|
195
|
+
closes: validatedData.closes.map(closes => getIssueLinkObject(closes, validatedEntry.gitHubUrl)),
|
|
196
|
+
validations: validatedData.validations,
|
|
197
|
+
communityCredits: validatedData.communityCredits
|
|
198
|
+
},
|
|
199
|
+
changesetPath: validatedEntry.changesetPath
|
|
200
|
+
};
|
|
201
|
+
sections[section].entries.push(newEntry);
|
|
202
|
+
if (isValid && newEntry.data.validations?.length) {
|
|
203
|
+
sections.warning.entries.push(newEntry);
|
|
204
|
+
}
|
|
205
|
+
return sections;
|
|
206
|
+
}, getInitialSectionsWithEntries());
|
|
207
|
+
}
|
|
208
|
+
function getChangeMessage({ restContent, scope, mainContent, entry, see, closes }) {
|
|
209
|
+
const messageFirstLine = [
|
|
210
|
+
'*',
|
|
211
|
+
scope ? `**${scope}**:` : null,
|
|
212
|
+
mainContent,
|
|
213
|
+
!entry.shouldSkipLinks && see.length ? see : null,
|
|
214
|
+
!entry.shouldSkipLinks && closes.length ? closes : null
|
|
215
|
+
].filter(Boolean).join(' ');
|
|
216
|
+
if (!restContent || !restContent.length) {
|
|
217
|
+
return messageFirstLine;
|
|
218
|
+
}
|
|
219
|
+
return `${messageFirstLine}\n\n${restContent.map(line => {
|
|
220
|
+
if (line.length) {
|
|
221
|
+
return ` ${line}`;
|
|
222
|
+
}
|
|
223
|
+
return line;
|
|
224
|
+
}).join('\n')}`;
|
|
225
|
+
}
|
|
226
|
+
function formatContent(content) {
|
|
227
|
+
const lines = content.trim()
|
|
228
|
+
.split('\n')
|
|
229
|
+
.map(line => line.trimEnd());
|
|
230
|
+
const mainIndex = lines.findIndex(line => line.trim() !== '');
|
|
231
|
+
const mainContent = lines.at(mainIndex);
|
|
232
|
+
let restContent = lines.slice(mainIndex + 1);
|
|
233
|
+
if (restContent.at(0)?.trim() === '') {
|
|
234
|
+
restContent = restContent.slice(1);
|
|
235
|
+
}
|
|
236
|
+
const cleanedRestContent = restContent.reduce((acc, line) => {
|
|
237
|
+
if (line.trim() === '') {
|
|
238
|
+
// Only add empty line if the last item is not already an empty line
|
|
239
|
+
if (acc.length > 0 && acc.at(-1) !== '') {
|
|
240
|
+
acc.push('');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
acc.push(line);
|
|
245
|
+
}
|
|
246
|
+
return acc;
|
|
247
|
+
}, []);
|
|
248
|
+
return [mainContent, ...cleanedRestContent];
|
|
249
|
+
}
|
|
250
|
+
function getContentWithCommunityCredits(content, communityCredits) {
|
|
251
|
+
if (!communityCredits?.length) {
|
|
252
|
+
return content;
|
|
253
|
+
}
|
|
254
|
+
return content.concat(`\n\nThanks to ${communityCredits?.join(', ')}.`);
|
|
255
|
+
}
|
|
256
|
+
function getScopesLinks(scope, transformScope) {
|
|
257
|
+
return scope
|
|
258
|
+
.map(scope => transformScope(scope))
|
|
259
|
+
.map(({ displayName, npmUrl }) => `[${displayName}](${npmUrl})`)
|
|
260
|
+
.join(', ');
|
|
261
|
+
}
|
|
262
|
+
function getIssueLinkObject(issue, gitHubUrl) {
|
|
263
|
+
if (issue.match(ISSUE_PATTERN)) {
|
|
264
|
+
return { displayName: `#${issue}`, link: `${gitHubUrl}/issues/${issue}` };
|
|
265
|
+
}
|
|
266
|
+
const differentRepoMatch = issue.match(ISSUE_SLUG_PATTERN);
|
|
267
|
+
if (differentRepoMatch) {
|
|
268
|
+
const { owner, repository, number } = differentRepoMatch.groups;
|
|
269
|
+
return { displayName: issue, link: `https://github.com/${owner}/${repository}/issues/${number}` };
|
|
270
|
+
}
|
|
271
|
+
const repoUrlMatch = issue.match(ISSUE_URL_PATTERN);
|
|
272
|
+
if (repoUrlMatch) {
|
|
273
|
+
const { owner, repository, number } = repoUrlMatch.groups;
|
|
274
|
+
return { displayName: `${owner}/${repository}#${number}`, link: issue };
|
|
275
|
+
}
|
|
276
|
+
return { displayName: '', link: '' };
|
|
277
|
+
}
|
|
278
|
+
function getIssuesLinks(issues, prefix, gitHubUrl) {
|
|
279
|
+
if (!issues.length) {
|
|
280
|
+
return '';
|
|
281
|
+
}
|
|
282
|
+
const links = issues.map(String).map(issue => {
|
|
283
|
+
const { displayName, link } = getIssueLinkObject(issue, gitHubUrl);
|
|
284
|
+
return `[${displayName}](${link})`;
|
|
285
|
+
});
|
|
286
|
+
return `${prefix} ${links.join(', ')}.`;
|
|
287
|
+
}
|
|
288
|
+
function getSection(options) {
|
|
289
|
+
const { entry, isSinglePackage, isValid } = options;
|
|
290
|
+
if (!isValid) {
|
|
291
|
+
return 'invalid';
|
|
292
|
+
}
|
|
293
|
+
// If someone tries to use minor/major breaking change in a single package, we simply cast it to a generic breaking change.
|
|
294
|
+
if (isSinglePackage) {
|
|
295
|
+
const breakingChangeTypes = ['Minor breaking change', 'Major breaking change', 'Breaking change'];
|
|
296
|
+
if (breakingChangeTypes.includes(entry.data.type)) {
|
|
297
|
+
return 'breaking';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
if (entry.data.type === 'Minor breaking change') {
|
|
302
|
+
return 'minor';
|
|
303
|
+
}
|
|
304
|
+
if (entry.data.type === 'Major breaking change') {
|
|
305
|
+
return 'major';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return entry.data.type.toLowerCase();
|
|
309
|
+
}
|
|
310
|
+
function getInitialSectionsWithEntries() {
|
|
311
|
+
const sections = structuredClone(SECTIONS);
|
|
312
|
+
for (const key in sections) {
|
|
313
|
+
sections[key].entries = [];
|
|
314
|
+
}
|
|
315
|
+
return sections;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
320
|
+
* For licensing, see LICENSE.md.
|
|
321
|
+
*/
|
|
322
|
+
/**
|
|
323
|
+
* This function provides a consistent logging format for the changelog generation process.
|
|
324
|
+
*
|
|
325
|
+
* It logs the given text to the console, optionally indenting it by a specified number of levels.
|
|
326
|
+
*/
|
|
327
|
+
function logInfo(text, { indent } = { indent: 0 }) {
|
|
328
|
+
console.log(' '.repeat(indent * 3) + text);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
333
|
+
* For licensing, see LICENSE.md.
|
|
334
|
+
*/
|
|
335
|
+
/**
|
|
336
|
+
* Displays a formatted summary of all changelog entries grouped by sections (e.g., Features, Fixes, Breaking changes).
|
|
337
|
+
*
|
|
338
|
+
* This function:
|
|
339
|
+
* * Lists all non-empty changelog sections with appropriate formatting.
|
|
340
|
+
* * Differentiates between valid and invalid entries using visual indicators (`+` for valid, `x` for invalid).
|
|
341
|
+
* * Supports both mono-repository and single package repositories through the `isSinglePackage` flag.
|
|
342
|
+
* * Applies a transformation to scope names using the `transformScope` callback for a mono-repository setup.
|
|
343
|
+
* * Outputs a helpful legend for interpreting the entry indicators.
|
|
344
|
+
*/
|
|
345
|
+
function displayChanges(options) {
|
|
346
|
+
const { sections, isSinglePackage, transformScope } = options;
|
|
347
|
+
let numberOfEntries = 0;
|
|
348
|
+
logInfo(`○ ${chalk.cyan('Listing the changes...')}`);
|
|
349
|
+
const nonEmptySections = Object.entries(sections)
|
|
350
|
+
.filter(([, section]) => section.entries.length);
|
|
351
|
+
for (const [sectionName, section] of nonEmptySections) {
|
|
352
|
+
const color = getTitleColor(sectionName);
|
|
353
|
+
logInfo('◌ ' + color(chalk.underline(`${section.titleInLogs || section.title}:`)), { indent: 1 });
|
|
354
|
+
const displayCallback = sectionName === 'invalid' || sectionName === 'warning' ?
|
|
355
|
+
displayWarningEntry :
|
|
356
|
+
displayValidEntry;
|
|
357
|
+
if (sectionName !== 'warning') {
|
|
358
|
+
numberOfEntries += section.entries.length;
|
|
359
|
+
}
|
|
360
|
+
section.entries.forEach(entry => displayCallback(entry, sectionName, isSinglePackage, transformScope));
|
|
361
|
+
logInfo('');
|
|
362
|
+
}
|
|
363
|
+
logInfo('◌ ' + chalk.underline('Legend:'), { indent: 1 });
|
|
364
|
+
logInfo(`- Entries marked with ${chalk.green('+')} symbol are included in the changelog.`, { indent: 2 });
|
|
365
|
+
logInfo('- Entries marked with ' + chalk.yellow('x') + ' symbol include invalid references (see and/or closes) ' +
|
|
366
|
+
'or scope definitions. Please ensure that:', { indent: 2 });
|
|
367
|
+
logInfo('* Reference entries match one of the following formats:', { indent: 3 });
|
|
368
|
+
logInfo('1. An issue number (e.g., 1000)', { indent: 4 });
|
|
369
|
+
logInfo('2. Repository-slug#id (e.g., org/repo#1000)', { indent: 4 });
|
|
370
|
+
logInfo('3. A full issue link URL', { indent: 4 });
|
|
371
|
+
logInfo('* A scope field consists of existing packages.', { indent: 3 });
|
|
372
|
+
logInfo('');
|
|
373
|
+
logInfo(`Found ${numberOfEntries} entries to parse.`, { indent: 1 });
|
|
374
|
+
logInfo('');
|
|
375
|
+
}
|
|
376
|
+
function getTitleColor(sectionName) {
|
|
377
|
+
if (sectionName === 'warning') {
|
|
378
|
+
return chalk.yellow;
|
|
379
|
+
}
|
|
380
|
+
if (sectionName === 'invalid') {
|
|
381
|
+
return chalk.red;
|
|
382
|
+
}
|
|
383
|
+
let defaultColor = chalk.blue;
|
|
384
|
+
if (isBreakingChangeSection(sectionName)) {
|
|
385
|
+
// To avoid tricks in tests, let's simplify the implementation.
|
|
386
|
+
defaultColor = (value) => {
|
|
387
|
+
return chalk.bold(chalk.blue(value));
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return defaultColor;
|
|
391
|
+
}
|
|
392
|
+
function isBreakingChangeSection(sectionName) {
|
|
393
|
+
return sectionName === 'breaking' || sectionName === 'major' || sectionName === 'minor';
|
|
394
|
+
}
|
|
395
|
+
function displayWarningEntry(entry) {
|
|
396
|
+
logInfo(`» file://${entry.changesetPath}`, { indent: 2 });
|
|
397
|
+
for (const validationMessage of (entry.data.validations || [])) {
|
|
398
|
+
logInfo(`- ${validationMessage}`, { indent: 3 });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function displayValidEntry(entry, sectionName, isSinglePackage, transformScope) {
|
|
402
|
+
const isEntryFullyValid = !entry.data.validations?.length;
|
|
403
|
+
const scopeFormatted = transformScope ?
|
|
404
|
+
entry.data.scope.map(scope => transformScope(scope).displayName) :
|
|
405
|
+
entry.data.scope;
|
|
406
|
+
const scope = entry.data.scope.length ?
|
|
407
|
+
chalk.grey(scopeFormatted?.join(', ')) :
|
|
408
|
+
`${chalk.italic(chalk.grey('(no scope)'))}`;
|
|
409
|
+
const validationIndicator = isEntryFullyValid ? chalk.green('+') : chalk.yellow('x');
|
|
410
|
+
const shouldTrimMessage = String(entry.data.mainContent).length > 100;
|
|
411
|
+
const trimmedMessageContent = shouldTrimMessage ? entry.data.mainContent?.slice(0, 100) + '...' : entry.data.mainContent;
|
|
412
|
+
if (isSinglePackage) {
|
|
413
|
+
logInfo(`${validationIndicator} ${trimmedMessageContent}`, { indent: 2 });
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
logInfo(`${validationIndicator} ${scope}: ${trimmedMessageContent}`, { indent: 2 });
|
|
417
|
+
}
|
|
418
|
+
if (!isBreakingChangeSection(sectionName)) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
entry.data.see.forEach(({ link }) => {
|
|
422
|
+
logInfo(`- See: ${link}`, { indent: 3 });
|
|
423
|
+
});
|
|
424
|
+
entry.data.closes.forEach(({ link }) => {
|
|
425
|
+
logInfo(`- Closes: ${link}`, { indent: 3 });
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
431
|
+
* For licensing, see LICENSE.md.
|
|
432
|
+
*/
|
|
433
|
+
/**
|
|
434
|
+
* This function limits the size of the changelog by removing older entries.
|
|
435
|
+
*/
|
|
436
|
+
function truncateChangelog(length, cwd) {
|
|
437
|
+
const changelog = getChangelog(cwd);
|
|
438
|
+
if (!changelog) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const entryHeader = '## [\\s\\S]+?';
|
|
442
|
+
const entryHeaderRegexp = new RegExp(`\\n(${entryHeader})(?=\\n${entryHeader}|$)`, 'g');
|
|
443
|
+
const entries = [...changelog.matchAll(entryHeaderRegexp)]
|
|
444
|
+
.filter(match => match && match[1])
|
|
445
|
+
.map(match => match[1]);
|
|
446
|
+
if (!entries.length) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const truncatedEntries = entries.slice(0, length);
|
|
450
|
+
const repositoryUrl = workspaces.getRepositoryUrl(cwd);
|
|
451
|
+
const changelogFooter = entries.length > truncatedEntries.length ?
|
|
452
|
+
`\n\n---\n\nTo see all releases, visit the [release page](${repositoryUrl}/releases).\n` :
|
|
453
|
+
'\n';
|
|
454
|
+
const truncatedChangelog = CHANGELOG_HEADER + '\n\n' + truncatedEntries.join('\n').trim() + changelogFooter;
|
|
455
|
+
saveChangelog(truncatedChangelog, cwd);
|
|
456
|
+
}
|
|
457
|
+
function getChangelog(cwd) {
|
|
458
|
+
const changelogFile = upath.join(cwd, CHANGELOG_FILE);
|
|
459
|
+
if (!fs.existsSync(changelogFile)) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
return fs.readFileSync(changelogFile, 'utf-8');
|
|
463
|
+
}
|
|
464
|
+
function saveChangelog(content, cwd) {
|
|
465
|
+
const changelogFile = upath.join(cwd, CHANGELOG_FILE);
|
|
466
|
+
fs.writeFileSync(changelogFile, content, 'utf-8');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
471
|
+
* For licensing, see LICENSE.md.
|
|
472
|
+
*/
|
|
473
|
+
/**
|
|
474
|
+
* This function writes the generated changelog content to the repository's changelog file.
|
|
475
|
+
*
|
|
476
|
+
* It reads the existing changelog (if any), inserts the new changelog content after the defined header,
|
|
477
|
+
* writes the updated content back to the changelog file, and truncates the changelog to keep a manageable length.
|
|
478
|
+
*/
|
|
479
|
+
async function modifyChangelog(newChangelog, cwd) {
|
|
480
|
+
const changelogPath = upath.join(cwd, CHANGELOG_FILE);
|
|
481
|
+
const existingChangelog = await readExistingChangelog(changelogPath);
|
|
482
|
+
const updatedChangelog = prepareChangelogContent(existingChangelog, newChangelog);
|
|
483
|
+
logInfo(`○ ${chalk.cyan('Appending changes to the existing changelog...')}`);
|
|
484
|
+
await fs$1.writeFile(changelogPath, updatedChangelog, 'utf-8');
|
|
485
|
+
await truncateChangelog(5, cwd);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Reads the existing changelog file or returns an empty string if the file doesn't exist.
|
|
489
|
+
*/
|
|
490
|
+
async function readExistingChangelog(changelogPath) {
|
|
491
|
+
try {
|
|
492
|
+
return await fs$1.readFile(changelogPath, 'utf-8');
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
logInfo(`○ ${chalk.yellow('CHANGELOG.md not found.')}`);
|
|
496
|
+
return '';
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Prepares the new changelog content by inserting it after the header or at the beginning if header is missing.
|
|
501
|
+
*/
|
|
502
|
+
function prepareChangelogContent(existingChangelog, newChangelog) {
|
|
503
|
+
const headerIndex = existingChangelog.indexOf(CHANGELOG_HEADER);
|
|
504
|
+
if (headerIndex === -1) {
|
|
505
|
+
return `${CHANGELOG_HEADER}\n\n${newChangelog}${existingChangelog}`;
|
|
506
|
+
}
|
|
507
|
+
const insertPosition = headerIndex + CHANGELOG_HEADER.length;
|
|
508
|
+
return existingChangelog.slice(0, insertPosition) + '\n\n' + newChangelog + existingChangelog.slice(insertPosition);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
513
|
+
* For licensing, see LICENSE.md.
|
|
514
|
+
*/
|
|
515
|
+
/**
|
|
516
|
+
* Custom error class for handling a case when a user aborts the process (at any stage).
|
|
517
|
+
*/
|
|
518
|
+
class UserAbortError extends Error {
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
523
|
+
* For licensing, see LICENSE.md.
|
|
524
|
+
*/
|
|
525
|
+
const CLI_INDENT_SIZE = 3;
|
|
526
|
+
/**
|
|
527
|
+
* Prompts the user to provide a new version for a package.
|
|
528
|
+
*
|
|
529
|
+
* Validates the input (version format, version higher than current, availability).
|
|
530
|
+
*
|
|
531
|
+
* Optionally shows warnings for invalid changes and allows user to abort.
|
|
532
|
+
*/
|
|
533
|
+
async function provideNewVersion(options) {
|
|
534
|
+
if (options.displayValidationWarning) {
|
|
535
|
+
// Display warning about invalid changes
|
|
536
|
+
displayInvalidChangesWarning();
|
|
537
|
+
// Ask for confirmation to continue
|
|
538
|
+
const shouldContinue = await askContinueConfirmation(options.indentLevel);
|
|
539
|
+
if (!shouldContinue) {
|
|
540
|
+
throw new UserAbortError('Aborted while detecting invalid changes.');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const question = createVersionQuestion(options);
|
|
544
|
+
const answers = await inquirer.prompt(question);
|
|
545
|
+
return answers.version;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Displays a warning message about invalid changes in a visible color.
|
|
549
|
+
*/
|
|
550
|
+
function displayInvalidChangesWarning() {
|
|
551
|
+
logInfo('');
|
|
552
|
+
logInfo(chalk.yellow(chalk.bold(`⚠️ ${chalk.underline('WARNING: Invalid changes detected!')}`)));
|
|
553
|
+
logInfo('');
|
|
554
|
+
logInfo(chalk.yellow('You can cancel the process, fix the invalid files, and run the tool again.'));
|
|
555
|
+
logInfo(chalk.yellow('Alternatively, you can continue - but invalid values will be lost.'));
|
|
556
|
+
logInfo('');
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Asks the user if they want to continue with the version bump process.
|
|
560
|
+
*/
|
|
561
|
+
async function askContinueConfirmation(indentLevel = 0) {
|
|
562
|
+
const question = {
|
|
563
|
+
type: 'confirm',
|
|
564
|
+
name: 'continue',
|
|
565
|
+
message: 'Should continue?',
|
|
566
|
+
default: false,
|
|
567
|
+
prefix: ' '.repeat(indentLevel * CLI_INDENT_SIZE) + chalk.cyan('?')
|
|
568
|
+
};
|
|
569
|
+
const answers = await inquirer.prompt(question);
|
|
570
|
+
return answers.continue;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Creates a prompt question for version input with validation.
|
|
574
|
+
*/
|
|
575
|
+
function createVersionQuestion(options) {
|
|
576
|
+
const { version, packageName, bumpType, indentLevel = 0 } = options;
|
|
577
|
+
const suggestedVersion = semver.inc(version, bumpType) || version;
|
|
578
|
+
const message = 'Type the new version ' +
|
|
579
|
+
`(current: "${version}", suggested: "${suggestedVersion}", or "internal" for internal changes):`;
|
|
580
|
+
return [{
|
|
581
|
+
type: 'input',
|
|
582
|
+
name: 'version',
|
|
583
|
+
default: suggestedVersion,
|
|
584
|
+
message,
|
|
585
|
+
filter: (newVersion) => newVersion.trim(),
|
|
586
|
+
async validate(newVersion) {
|
|
587
|
+
// Allow 'internal' as a special version.
|
|
588
|
+
if (newVersion === 'internal') {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
// Require a semver valid version, e.g., `1.0.0`, `1.0.0-alpha.0`, etc.
|
|
592
|
+
if (!semver.valid(newVersion)) {
|
|
593
|
+
return 'Please provide a valid version or "internal" for internal changes.';
|
|
594
|
+
}
|
|
595
|
+
// The provided version must be higher than the current version.
|
|
596
|
+
if (!semver.gt(newVersion, version)) {
|
|
597
|
+
return `Provided version must be higher than "${version}".`;
|
|
598
|
+
}
|
|
599
|
+
const isAvailable = await npm.checkVersionAvailability(newVersion, packageName);
|
|
600
|
+
// Check against availability in the npm registry.
|
|
601
|
+
if (!isAvailable) {
|
|
602
|
+
return 'Given version is already taken.';
|
|
603
|
+
}
|
|
604
|
+
return true;
|
|
605
|
+
},
|
|
606
|
+
prefix: ' '.repeat(indentLevel * CLI_INDENT_SIZE) + chalk.cyan('?')
|
|
607
|
+
}];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
612
|
+
* For licensing, see LICENSE.md.
|
|
613
|
+
*/
|
|
614
|
+
/**
|
|
615
|
+
* Determines the next version for a single package or a mono-repository setup based on
|
|
616
|
+
* the change sections, * user input, and semantic versioning rules.
|
|
617
|
+
*
|
|
618
|
+
* The function handles:
|
|
619
|
+
* * Automatic version bump calculation from categorized changelog sections (major, minor, patch).
|
|
620
|
+
* * Accepting explicit next version overrides, including a special `internal` version bump.
|
|
621
|
+
* * User prompts for version input when no explicit version is provided.
|
|
622
|
+
*/
|
|
623
|
+
async function determineNextVersion(options) {
|
|
624
|
+
const { sections, currentVersion, packageName, nextVersion } = options;
|
|
625
|
+
if (nextVersion === 'internal') {
|
|
626
|
+
const internalVersionBump = getInternalVersionBump(currentVersion);
|
|
627
|
+
logInfo(`○ ${chalk.cyan(`Determined the next version to be ${internalVersionBump.newVersion}.`)}`);
|
|
628
|
+
return internalVersionBump;
|
|
629
|
+
}
|
|
630
|
+
if (nextVersion) {
|
|
631
|
+
logInfo(`○ ${chalk.cyan(`Determined the next version to be ${nextVersion}.`)}`);
|
|
632
|
+
return { newVersion: nextVersion, isInternal: false };
|
|
633
|
+
}
|
|
634
|
+
logInfo(`○ ${chalk.cyan('Determining the new version...')}`);
|
|
635
|
+
let bumpType = 'patch';
|
|
636
|
+
if (sections.minor.entries.length || sections.feature.entries.length) {
|
|
637
|
+
bumpType = 'minor';
|
|
638
|
+
}
|
|
639
|
+
if (sections.major.entries.length) {
|
|
640
|
+
bumpType = 'major';
|
|
641
|
+
}
|
|
642
|
+
const areErrorsPresent = !!sections.invalid.entries.length;
|
|
643
|
+
const areWarningsPresent = Object.values(sections).some(section => section.entries.some(entry => entry.data.validations && entry.data.validations.length > 0));
|
|
644
|
+
const userProvidedVersion = await provideNewVersion({
|
|
645
|
+
packageName,
|
|
646
|
+
bumpType,
|
|
647
|
+
version: currentVersion,
|
|
648
|
+
displayValidationWarning: areErrorsPresent || areWarningsPresent
|
|
649
|
+
});
|
|
650
|
+
if (userProvidedVersion === 'internal') {
|
|
651
|
+
return getInternalVersionBump(currentVersion);
|
|
652
|
+
}
|
|
653
|
+
return { newVersion: userProvidedVersion, isInternal: false };
|
|
654
|
+
}
|
|
655
|
+
function getInternalVersionBump(currentVersion) {
|
|
656
|
+
const version = semver.inc(currentVersion, 'patch');
|
|
657
|
+
if (!version) {
|
|
658
|
+
throw new Error('Unable to determine new version based on the version in root package.json.');
|
|
659
|
+
}
|
|
660
|
+
return { newVersion: version, isInternal: true };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
665
|
+
* For licensing, see LICENSE.md.
|
|
666
|
+
*/
|
|
667
|
+
/**
|
|
668
|
+
* A utility class that wraps a Promise of an array and provides async array-like operations.
|
|
669
|
+
*/
|
|
670
|
+
class AsyncArray {
|
|
671
|
+
promise;
|
|
672
|
+
/**
|
|
673
|
+
* Creates a new `AsyncArray` instance.
|
|
674
|
+
*/
|
|
675
|
+
constructor(promise) {
|
|
676
|
+
this.promise = promise;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Creates an `AsyncArray` from a given promise.
|
|
680
|
+
*/
|
|
681
|
+
static from(promise) {
|
|
682
|
+
return new AsyncArray(promise);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Asynchronously maps each item in the array using the provided callback.
|
|
686
|
+
*/
|
|
687
|
+
map(fn) {
|
|
688
|
+
const newPromise = this.promise.then(arr => Promise.all(arr.map(fn)));
|
|
689
|
+
return new AsyncArray(newPromise);
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Flattens one level of nesting in an array of arrays.
|
|
693
|
+
*/
|
|
694
|
+
flat() {
|
|
695
|
+
const newPromise = this.promise.then(arr => arr.flat());
|
|
696
|
+
return new AsyncArray(newPromise);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Maps each item using a callback that returns an array (or promise of an array),
|
|
700
|
+
* then flattens the result by one level.
|
|
701
|
+
*/
|
|
702
|
+
flatMap(fn) {
|
|
703
|
+
const newPromise = this.promise.then(async (arr) => {
|
|
704
|
+
const mapped = await Promise.all(arr.map(fn));
|
|
705
|
+
return mapped.flat();
|
|
706
|
+
});
|
|
707
|
+
return new AsyncArray(newPromise);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Allows chaining or awaiting the result of the internal promise.
|
|
711
|
+
*/
|
|
712
|
+
then(onfulfilled) {
|
|
713
|
+
return this.promise.then(onfulfilled);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
719
|
+
* For licensing, see LICENSE.md.
|
|
720
|
+
*/
|
|
721
|
+
/**
|
|
722
|
+
* Retrieves the names and versions of packages found in both the main repository and any external repositories.
|
|
723
|
+
*/
|
|
724
|
+
async function findPackages(options) {
|
|
725
|
+
const { cwd, packagesDirectory, externalRepositories, shouldIgnoreRootPackage = false } = options;
|
|
726
|
+
const externalPackagesPromises = externalRepositories.map(externalRepository => {
|
|
727
|
+
return workspaces.findPathsToPackages(externalRepository.cwd, externalRepository.packagesDirectory, { includePackageJson: true });
|
|
728
|
+
});
|
|
729
|
+
const promise = Promise.all([
|
|
730
|
+
workspaces.findPathsToPackages(cwd, packagesDirectory, { includeCwd: !shouldIgnoreRootPackage, includePackageJson: true }),
|
|
731
|
+
...externalPackagesPromises
|
|
732
|
+
]);
|
|
733
|
+
return AsyncArray.from(promise)
|
|
734
|
+
.flat()
|
|
735
|
+
.map(packagePath => fs$1.readJson(packagePath))
|
|
736
|
+
.map(({ name, version }) => [name, version])
|
|
737
|
+
.then(entries => new Map(entries.sort(([a], [b]) => a.localeCompare(b))));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
742
|
+
* For licensing, see LICENSE.md.
|
|
743
|
+
*/
|
|
744
|
+
/**
|
|
745
|
+
* Generates a categorized summary of packages released in the new version,
|
|
746
|
+
* including new packages, major, minor, feature, and other releases.
|
|
747
|
+
*
|
|
748
|
+
* This function analyzes changelog sections and package metadata to:
|
|
749
|
+
* * Identify new packages introduced with version '0.0.1'.
|
|
750
|
+
* * Group packages by release type based on the changelog sections: major, minor, and features.
|
|
751
|
+
* * Exclude packages already accounted for in higher-priority release categories from lower ones.
|
|
752
|
+
* * Provide a fallback category for "other releases" that don't fall into the above groups.
|
|
753
|
+
*/
|
|
754
|
+
async function composeReleaseSummary(options) {
|
|
755
|
+
const { sections, currentVersion, newVersion, packagesMetadata } = options;
|
|
756
|
+
const versionUpgradeText = `v${currentVersion} => v${newVersion}`;
|
|
757
|
+
const packageNames = [...packagesMetadata.keys()];
|
|
758
|
+
const newVersionReleases = getNewVersionReleases(packagesMetadata);
|
|
759
|
+
const majorReleases = getScopeWithOrgNamespace(sections.major.entries, { packagesToRemove: newVersionReleases, packageNames });
|
|
760
|
+
const minorReleases = getScopeWithOrgNamespace(sections.minor.entries, {
|
|
761
|
+
packagesToRemove: [...majorReleases, ...newVersionReleases],
|
|
762
|
+
packageNames
|
|
763
|
+
});
|
|
764
|
+
const newFeaturesReleases = getScopeWithOrgNamespace(sections.feature.entries, {
|
|
765
|
+
packagesToRemove: [...minorReleases, ...majorReleases, ...newVersionReleases],
|
|
766
|
+
packageNames
|
|
767
|
+
});
|
|
768
|
+
const packagesToRemoveFromOtherReleases = [majorReleases, minorReleases, newFeaturesReleases, newVersionReleases].flat();
|
|
769
|
+
const otherReleases = packageNames
|
|
770
|
+
.filter(packageName => !packagesToRemoveFromOtherReleases.includes(packageName))
|
|
771
|
+
.sort();
|
|
772
|
+
return [
|
|
773
|
+
{ title: 'New packages:', version: `v${newVersion}`, packages: newVersionReleases },
|
|
774
|
+
{ title: 'Major releases (contain major breaking changes):', version: versionUpgradeText, packages: majorReleases },
|
|
775
|
+
{ title: 'Minor releases (contain minor breaking changes):', version: versionUpgradeText, packages: minorReleases },
|
|
776
|
+
{ title: 'Releases containing new features:', version: versionUpgradeText, packages: newFeaturesReleases },
|
|
777
|
+
{ title: 'Other releases:', version: versionUpgradeText, packages: otherReleases }
|
|
778
|
+
].filter(release => release.packages?.length > 0);
|
|
779
|
+
}
|
|
780
|
+
function getNewVersionReleases(packages) {
|
|
781
|
+
return [...packages]
|
|
782
|
+
.filter(([, version]) => version === '0.0.1')
|
|
783
|
+
.map(([packageName]) => packageName)
|
|
784
|
+
.sort();
|
|
785
|
+
}
|
|
786
|
+
function getScopeWithOrgNamespace(entries = [], { packagesToRemove, packageNames }) {
|
|
787
|
+
const uniqueScopes = entries
|
|
788
|
+
.flatMap(entry => entry.data.scope)
|
|
789
|
+
.filter(Boolean)
|
|
790
|
+
.filter((item, index, array) => array.indexOf(item) === index);
|
|
791
|
+
const packagesFullNames = uniqueScopes.map(scope => {
|
|
792
|
+
return packageNames.find(packageName => getPackageName(packageName) === scope);
|
|
793
|
+
});
|
|
794
|
+
return packagesFullNames.filter(packageName => !packagesToRemove.includes(packageName));
|
|
795
|
+
}
|
|
796
|
+
function getPackageName(value) {
|
|
797
|
+
if (value.includes('/')) {
|
|
798
|
+
return value.split('/').at(1);
|
|
799
|
+
}
|
|
800
|
+
return value;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
805
|
+
* For licensing, see LICENSE.md.
|
|
806
|
+
*/
|
|
807
|
+
/**
|
|
808
|
+
* Gathers changelog entry file paths (Markdown files) from the main repository and any configured external repositories.
|
|
809
|
+
*/
|
|
810
|
+
async function findChangelogEntryPaths(options) {
|
|
811
|
+
const { cwd, externalRepositories, shouldSkipLinks } = options;
|
|
812
|
+
return AsyncArray
|
|
813
|
+
.from(Promise.resolve(externalRepositories))
|
|
814
|
+
.map(async (repo) => {
|
|
815
|
+
const changesetGlob = await glob('**/*.md', {
|
|
816
|
+
cwd: upath.join(repo.cwd, CHANGESET_DIRECTORY),
|
|
817
|
+
absolute: true
|
|
818
|
+
});
|
|
819
|
+
return {
|
|
820
|
+
filePaths: changesetGlob.map(p => upath.normalize(p)),
|
|
821
|
+
gitHubUrl: await workspaces.getRepositoryUrl(repo.cwd, { async: true }),
|
|
822
|
+
shouldSkipLinks: !!repo.shouldSkipLinks,
|
|
823
|
+
cwd: repo.cwd,
|
|
824
|
+
isRoot: false
|
|
825
|
+
};
|
|
826
|
+
})
|
|
827
|
+
.then(async (externalResults) => {
|
|
828
|
+
const mainChangesetGlob = await glob('**/*.md', {
|
|
829
|
+
cwd: upath.join(cwd, CHANGESET_DIRECTORY),
|
|
830
|
+
absolute: true
|
|
831
|
+
});
|
|
832
|
+
const mainEntry = {
|
|
833
|
+
filePaths: mainChangesetGlob.map(p => upath.normalize(p)),
|
|
834
|
+
gitHubUrl: await workspaces.getRepositoryUrl(cwd, { async: true }),
|
|
835
|
+
shouldSkipLinks,
|
|
836
|
+
cwd,
|
|
837
|
+
isRoot: true
|
|
838
|
+
};
|
|
839
|
+
return [mainEntry, ...externalResults];
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
845
|
+
* For licensing, see LICENSE.md.
|
|
846
|
+
*/
|
|
847
|
+
/**
|
|
848
|
+
* Sorts parsed files according to the rules:
|
|
849
|
+
* 1. Entries with more scopes at the top.
|
|
850
|
+
* 2. Entries with single scope grouped by scope and sorted by date within a group.
|
|
851
|
+
* 3. Entries with no scope at the bottom.
|
|
852
|
+
*/
|
|
853
|
+
function sortEntriesByScopeAndDate(entries) {
|
|
854
|
+
return entries.sort((itemBefore, itemAfter) => {
|
|
855
|
+
const beforeScopeCount = itemBefore.data.scope.length;
|
|
856
|
+
const afterScopeCount = itemAfter.data.scope.length;
|
|
857
|
+
// Both have single scope - group by scope name first.
|
|
858
|
+
if (beforeScopeCount === 1 && afterScopeCount === 1) {
|
|
859
|
+
const firstScope = itemBefore.data.scope.at(0);
|
|
860
|
+
const secondScope = itemAfter.data.scope.at(0);
|
|
861
|
+
if (firstScope !== secondScope) {
|
|
862
|
+
return firstScope.localeCompare(secondScope); // Alphabetical scope order.
|
|
863
|
+
}
|
|
864
|
+
// Same scope - sort by date (older first).
|
|
865
|
+
return itemBefore.createdAt.getTime() - itemAfter.createdAt.getTime();
|
|
866
|
+
}
|
|
867
|
+
// Both have the same number of scopes - sort by date (older first).
|
|
868
|
+
if (beforeScopeCount === afterScopeCount) {
|
|
869
|
+
return itemBefore.createdAt.getTime() - itemAfter.createdAt.getTime();
|
|
870
|
+
}
|
|
871
|
+
// Sort by number of scopes.
|
|
872
|
+
return afterScopeCount - beforeScopeCount;
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
878
|
+
* For licensing, see LICENSE.md.
|
|
879
|
+
*/
|
|
880
|
+
function normalizeEntry(entry, isSinglePackage) {
|
|
881
|
+
// Normalize type.
|
|
882
|
+
const typeNormalized = getTypeNormalized(entry.data.type, isSinglePackage);
|
|
883
|
+
// Normalize scope.
|
|
884
|
+
const scopeNormalized = toArray(entry.data.scope)
|
|
885
|
+
.filter(scope => scope)
|
|
886
|
+
.map(scopeEntry => String(scopeEntry).toLowerCase())
|
|
887
|
+
.sort();
|
|
888
|
+
// Normalize closes.
|
|
889
|
+
const closesNormalized = toArray(entry.data.closes)
|
|
890
|
+
.filter(closes => closes)
|
|
891
|
+
.map(closes => String(closes));
|
|
892
|
+
// Normalize see.
|
|
893
|
+
const seeNormalized = toArray(entry.data.see)
|
|
894
|
+
.filter(see => see).map(see => String(see));
|
|
895
|
+
// Normalize community credits.
|
|
896
|
+
const communityCreditsNormalized = toArray(entry.data.communityCredits)
|
|
897
|
+
.filter(see => see)
|
|
898
|
+
.map(credits => ensureAt(String(credits)));
|
|
899
|
+
return {
|
|
900
|
+
...entry,
|
|
901
|
+
data: {
|
|
902
|
+
type: typeNormalized,
|
|
903
|
+
scope: deduplicate(scopeNormalized),
|
|
904
|
+
closes: deduplicate(closesNormalized),
|
|
905
|
+
see: deduplicate(seeNormalized),
|
|
906
|
+
communityCredits: deduplicate(communityCreditsNormalized),
|
|
907
|
+
validations: []
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function getTypeNormalized(type, isSinglePackage) {
|
|
912
|
+
const typeCapitalized = capitalize(type);
|
|
913
|
+
const expectedBreakingChangeTypes = ['Major breaking change', 'Minor breaking change'];
|
|
914
|
+
if (isSinglePackage && expectedBreakingChangeTypes.includes(typeCapitalized)) {
|
|
915
|
+
return 'Breaking change';
|
|
916
|
+
}
|
|
917
|
+
return typeCapitalized;
|
|
918
|
+
}
|
|
919
|
+
function capitalize(value) {
|
|
920
|
+
const valueStr = String(value);
|
|
921
|
+
return valueStr.charAt(0).toUpperCase() + valueStr.slice(1).toLowerCase();
|
|
922
|
+
}
|
|
923
|
+
function ensureAt(str) {
|
|
924
|
+
return str.startsWith('@') ? str : '@' + str;
|
|
925
|
+
}
|
|
926
|
+
function deduplicate(packageNames) {
|
|
927
|
+
return [...new Set(packageNames)];
|
|
928
|
+
}
|
|
929
|
+
function toArray(input) {
|
|
930
|
+
if (!input) {
|
|
931
|
+
return [];
|
|
932
|
+
}
|
|
933
|
+
if (typeof input === 'string') {
|
|
934
|
+
return [input];
|
|
935
|
+
}
|
|
936
|
+
return input;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
941
|
+
* For licensing, see LICENSE.md.
|
|
942
|
+
*/
|
|
943
|
+
/**
|
|
944
|
+
* Reads and processes input files to extract changelog entries.
|
|
945
|
+
*/
|
|
946
|
+
function parseChangelogEntries(entryPaths, isSinglePackage) {
|
|
947
|
+
const fileEntries = entryPaths.reduce((acc, { filePaths, gitHubUrl, shouldSkipLinks }) => {
|
|
948
|
+
for (const changesetPath of filePaths) {
|
|
949
|
+
acc.push({ changesetPath, gitHubUrl, shouldSkipLinks });
|
|
950
|
+
}
|
|
951
|
+
return acc;
|
|
952
|
+
}, []);
|
|
953
|
+
return AsyncArray
|
|
954
|
+
.from(Promise.resolve(fileEntries))
|
|
955
|
+
.map(async ({ changesetPath, gitHubUrl, shouldSkipLinks }) => ({
|
|
956
|
+
...matter(await fs$1.readFile(changesetPath, 'utf-8')),
|
|
957
|
+
gitHubUrl,
|
|
958
|
+
changesetPath,
|
|
959
|
+
shouldSkipLinks,
|
|
960
|
+
createdAt: extractDateFromFilename(changesetPath)
|
|
961
|
+
}))
|
|
962
|
+
.map(entry => normalizeEntry(entry, isSinglePackage))
|
|
963
|
+
.then(entries => sortEntriesByScopeAndDate(entries));
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Extracts date from an entry filename (`YYYYMMDDHHMMSS_*.md`).
|
|
967
|
+
*
|
|
968
|
+
* Defaults to the current date if the filename does not match the expected format.
|
|
969
|
+
*/
|
|
970
|
+
function extractDateFromFilename(changesetPath) {
|
|
971
|
+
const now = new Date();
|
|
972
|
+
const filename = changesetPath.split('/').pop() || '';
|
|
973
|
+
const dateMatch = filename.match(/^(\d{14})_/);
|
|
974
|
+
if (!dateMatch || !dateMatch[1]) {
|
|
975
|
+
// Fallback to the current date if no date pattern found.
|
|
976
|
+
return now;
|
|
977
|
+
}
|
|
978
|
+
const parsedDate = parse(dateMatch[1], 'yyyyMMddHHmmss', now);
|
|
979
|
+
// Validate the parsed date and fallback to the current date when failed.
|
|
980
|
+
if (!isValid(parsedDate)) {
|
|
981
|
+
return now;
|
|
982
|
+
}
|
|
983
|
+
return parsedDate;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
988
|
+
* For licensing, see LICENSE.md.
|
|
989
|
+
*/
|
|
990
|
+
/**
|
|
991
|
+
* Custom error class for handling validation errors in the changelog generation process.
|
|
992
|
+
*/
|
|
993
|
+
class InternalError extends Error {
|
|
994
|
+
constructor() {
|
|
995
|
+
const message = 'No valid entries were found. Please ensure that:\n' +
|
|
996
|
+
'1) Input files exist in the `.changelog/` directory.\n' +
|
|
997
|
+
'2) The `cwd` parameter points to the root of your project.\n' +
|
|
998
|
+
'3) The `packagesDirectory` parameter correctly specifies the packages folder.\n' +
|
|
999
|
+
'If no errors appear in the console but inputs are present, your project configuration may be incorrect.\n' +
|
|
1000
|
+
'If validation errors are shown, please resolve them according to the details provided.\n';
|
|
1001
|
+
super(message);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
1007
|
+
* For licensing, see LICENSE.md.
|
|
1008
|
+
*/
|
|
1009
|
+
/**
|
|
1010
|
+
* Filters and returns only those changelog sections that:
|
|
1011
|
+
* * Have at least one entry.
|
|
1012
|
+
* * Are not explicitly marked to be excluded from the final changelog.
|
|
1013
|
+
*
|
|
1014
|
+
* This is used to determine which sections should be displayed or processed for changelog generation.
|
|
1015
|
+
*/
|
|
1016
|
+
function filterVisibleSections(sectionsWithEntries) {
|
|
1017
|
+
const sectionsToDisplay = Object.entries(sectionsWithEntries)
|
|
1018
|
+
.filter(([, { entries, excludeInChangelog }]) => {
|
|
1019
|
+
return entries?.length && !excludeInChangelog;
|
|
1020
|
+
})
|
|
1021
|
+
.map(([, section]) => section);
|
|
1022
|
+
if (!sectionsToDisplay.length) {
|
|
1023
|
+
throw new InternalError();
|
|
1024
|
+
}
|
|
1025
|
+
return sectionsToDisplay;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
1030
|
+
* For licensing, see LICENSE.md.
|
|
1031
|
+
*/
|
|
1032
|
+
/**
|
|
1033
|
+
* Formats a date string `YYYY-MM-DD` into a human-readable format for the changelog.
|
|
1034
|
+
*/
|
|
1035
|
+
function getDateFormatted(date) {
|
|
1036
|
+
return format(parse(date, 'yyyy-MM-dd', new Date()), 'LLLL d, yyyy');
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
1041
|
+
* For licensing, see LICENSE.md.
|
|
1042
|
+
*/
|
|
1043
|
+
/**
|
|
1044
|
+
* Generates a formatted changelog string for a new version release.
|
|
1045
|
+
*
|
|
1046
|
+
* This function constructs the changelog content including
|
|
1047
|
+
* * A version header with a link to the GitHub comparison view (except for an initial version).
|
|
1048
|
+
* * Sections with grouped changelog entries and their messages.
|
|
1049
|
+
* * A collapsible summary of released packages and their version bumps for a mono-repository setup.
|
|
1050
|
+
* * Special handling for internal-only releases and single-package repositories.
|
|
1051
|
+
*/
|
|
1052
|
+
async function composeChangelog(options) {
|
|
1053
|
+
const { cwd, date, currentVersion, newVersion, sections, releasedPackagesInfo, isInternal, isSinglePackage, packagesMetadata } = options;
|
|
1054
|
+
const gitHubUrl = await workspaces.getRepositoryUrl(cwd, { async: true });
|
|
1055
|
+
const dateFormatted = getDateFormatted(date);
|
|
1056
|
+
const packagesNames = [...packagesMetadata.keys()];
|
|
1057
|
+
const header = currentVersion === '0.0.1' ?
|
|
1058
|
+
`## ${newVersion} (${dateFormatted})` :
|
|
1059
|
+
`## [${newVersion}](${gitHubUrl}/compare/v${currentVersion}...v${newVersion}) (${dateFormatted})`;
|
|
1060
|
+
const sectionsAsString = sections.map(({ title, entries }) => ([
|
|
1061
|
+
`### ${title}`,
|
|
1062
|
+
'',
|
|
1063
|
+
...entries.map(entry => entry.message),
|
|
1064
|
+
''
|
|
1065
|
+
])).flat().join('\n');
|
|
1066
|
+
const packagesVersionBumps = releasedPackagesInfo.map(({ title, version, packages }) => ([
|
|
1067
|
+
'',
|
|
1068
|
+
title,
|
|
1069
|
+
'',
|
|
1070
|
+
...packages.map(packageName => `* [${packageName}](${NPM_URL}/${packageName}/v/${newVersion}): ${version}`)
|
|
1071
|
+
])).flat().join('\n');
|
|
1072
|
+
const internalVersionsBumps = [
|
|
1073
|
+
'',
|
|
1074
|
+
SECTIONS.other.title + ':',
|
|
1075
|
+
'',
|
|
1076
|
+
packagesNames.map(name => `* [${name}](${NPM_URL}/${name}/v/${newVersion}): v${currentVersion} => v${newVersion}`)
|
|
1077
|
+
].flat().join('\n');
|
|
1078
|
+
const changelog = [
|
|
1079
|
+
header,
|
|
1080
|
+
'',
|
|
1081
|
+
isInternal ? 'Internal changes only (updated dependencies, documentation, etc.).\n' : sectionsAsString
|
|
1082
|
+
];
|
|
1083
|
+
if (!isSinglePackage) {
|
|
1084
|
+
changelog.push('### Released packages', '', `Check out the [Versioning policy](${VERSIONING_POLICY_URL}) guide for more information.`, '', '<details>', '<summary>Released packages (summary)</summary>', isInternal ? internalVersionsBumps : packagesVersionBumps, '</details>', '');
|
|
1085
|
+
}
|
|
1086
|
+
return changelog.join('\n');
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
1091
|
+
* For licensing, see LICENSE.md.
|
|
1092
|
+
*/
|
|
1093
|
+
/**
|
|
1094
|
+
* Cleans up the input files that have been incorporated into the changelog by deleting them
|
|
1095
|
+
* and removing any resulting empty directories both in the current repository and in any external repositories.
|
|
1096
|
+
*/
|
|
1097
|
+
async function removeChangelogEntryFiles(entryPaths) {
|
|
1098
|
+
logInfo(`○ ${chalk.cyan('Removing the changeset files...')}`);
|
|
1099
|
+
await Promise.all(entryPaths
|
|
1100
|
+
.flatMap(repo => repo.filePaths)
|
|
1101
|
+
.map(file => fs$1.unlink(file)));
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
1106
|
+
* For licensing, see LICENSE.md.
|
|
1107
|
+
*/
|
|
1108
|
+
async function commitChanges(version, repositories) {
|
|
1109
|
+
const message = `Changelog for v${version}. [skip ci]`;
|
|
1110
|
+
logInfo(`○ ${chalk.cyan('Committing changes...')}`);
|
|
1111
|
+
for (const { cwd, isRoot, filePaths } of repositories) {
|
|
1112
|
+
// Copy to avoid modifying the original array.
|
|
1113
|
+
const files = filePaths.slice(0);
|
|
1114
|
+
if (isRoot) {
|
|
1115
|
+
files.unshift(upath.join(cwd, CHANGELOG_FILE));
|
|
1116
|
+
}
|
|
1117
|
+
logInfo(`◌ Processing "${cwd}".`, { indent: 1 });
|
|
1118
|
+
await tools.commit({ cwd, message, files })
|
|
1119
|
+
.catch(error => {
|
|
1120
|
+
logInfo('An error occurred while committing changes.', { indent: 2 });
|
|
1121
|
+
logInfo(chalk.red(error.message), { indent: 2 });
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
1128
|
+
* For licensing, see LICENSE.md.
|
|
1129
|
+
*/
|
|
1130
|
+
/**
|
|
1131
|
+
* Orchestrates the full changelog generation workflow.
|
|
1132
|
+
*
|
|
1133
|
+
* This function:
|
|
1134
|
+
* * Reads the current package version and metadata.
|
|
1135
|
+
* * Locates all changelog entry files from the main and external repositories.
|
|
1136
|
+
* * Parses, validates, and groups entries by their section.
|
|
1137
|
+
* * Optionally displays the changes for manual inspection.
|
|
1138
|
+
* * Prompts for the next version if not provided via `options.nextVersion`.
|
|
1139
|
+
* * Computes the released package information based on version changes.
|
|
1140
|
+
* * Assembles a new changelog based on the visible entries.
|
|
1141
|
+
* * Optionally writes the new changelog to disk and removes the processed entry files.
|
|
1142
|
+
* * Commits the changes (changelog and removed files) to the Git repository.
|
|
1143
|
+
*
|
|
1144
|
+
* If `disableFilesystemOperations` is enabled, file operations (writing/committing) will be skipped,
|
|
1145
|
+
* and the assembled changelog object will be returned instead.
|
|
1146
|
+
*/
|
|
1147
|
+
const main = async (options) => {
|
|
1148
|
+
const { nextVersion, packagesDirectory, isSinglePackage, transformScope, npmPackageToCheck, cwd = process.cwd(), externalRepositories = [], date = format(new Date(), 'yyyy-MM-dd'), shouldSkipLinks = false, shouldIgnoreRootPackage = false, disableFilesystemOperations = false } = options;
|
|
1149
|
+
const { version: currentVersion, name: rootPackageName } = await workspaces.getPackageJson(cwd, { async: true });
|
|
1150
|
+
const packagesMetadata = await findPackages({
|
|
1151
|
+
cwd,
|
|
1152
|
+
packagesDirectory,
|
|
1153
|
+
shouldIgnoreRootPackage,
|
|
1154
|
+
externalRepositories
|
|
1155
|
+
});
|
|
1156
|
+
const entryPaths = await findChangelogEntryPaths({
|
|
1157
|
+
cwd,
|
|
1158
|
+
externalRepositories,
|
|
1159
|
+
shouldSkipLinks
|
|
1160
|
+
});
|
|
1161
|
+
const parsedChangesetFiles = await parseChangelogEntries(entryPaths, isSinglePackage);
|
|
1162
|
+
const sectionsWithEntries = groupEntriesBySection({
|
|
1163
|
+
packagesMetadata,
|
|
1164
|
+
transformScope,
|
|
1165
|
+
isSinglePackage,
|
|
1166
|
+
files: parsedChangesetFiles
|
|
1167
|
+
});
|
|
1168
|
+
// Log changes in the console only when `nextVersion` is not provided.
|
|
1169
|
+
if (!nextVersion) {
|
|
1170
|
+
displayChanges({
|
|
1171
|
+
isSinglePackage,
|
|
1172
|
+
transformScope,
|
|
1173
|
+
sections: sectionsWithEntries
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
// Display a prompt to provide a new version in the console.
|
|
1177
|
+
const { isInternal, newVersion } = await determineNextVersion({
|
|
1178
|
+
currentVersion,
|
|
1179
|
+
nextVersion,
|
|
1180
|
+
sections: sectionsWithEntries,
|
|
1181
|
+
packageName: shouldIgnoreRootPackage ? npmPackageToCheck : rootPackageName
|
|
1182
|
+
});
|
|
1183
|
+
const releasedPackagesInfo = await composeReleaseSummary({
|
|
1184
|
+
currentVersion,
|
|
1185
|
+
newVersion,
|
|
1186
|
+
packagesMetadata,
|
|
1187
|
+
sections: sectionsWithEntries
|
|
1188
|
+
});
|
|
1189
|
+
const newChangelog = await composeChangelog({
|
|
1190
|
+
currentVersion,
|
|
1191
|
+
cwd,
|
|
1192
|
+
date,
|
|
1193
|
+
newVersion,
|
|
1194
|
+
isInternal,
|
|
1195
|
+
isSinglePackage,
|
|
1196
|
+
packagesMetadata,
|
|
1197
|
+
releasedPackagesInfo,
|
|
1198
|
+
sections: filterVisibleSections(sectionsWithEntries)
|
|
1199
|
+
});
|
|
1200
|
+
if (disableFilesystemOperations) {
|
|
1201
|
+
return newChangelog;
|
|
1202
|
+
}
|
|
1203
|
+
await removeChangelogEntryFiles(entryPaths);
|
|
1204
|
+
await modifyChangelog(newChangelog, cwd);
|
|
1205
|
+
await commitChanges(newVersion, entryPaths.map(({ cwd, isRoot, filePaths }) => ({ cwd, isRoot, filePaths })));
|
|
1206
|
+
logInfo('○ ' + chalk.green('Done!'));
|
|
1207
|
+
};
|
|
1208
|
+
/**
|
|
1209
|
+
* Entry point for generating a changelog with error handling.
|
|
1210
|
+
*
|
|
1211
|
+
* This wrapper ensures that:
|
|
1212
|
+
* * Interruptions from the user (e.g., Ctrl+C or intentional aborts) exit silently with code 0.
|
|
1213
|
+
* * Expected and unexpected internal errors are logged to the console and exit with code 1.
|
|
1214
|
+
* * Other unexpected errors are re-thrown for higher-level handling.
|
|
1215
|
+
*/
|
|
1216
|
+
const generateChangelog = async (options) => {
|
|
1217
|
+
return main(options)
|
|
1218
|
+
.catch(error => {
|
|
1219
|
+
if (isExpectedError(error)) {
|
|
1220
|
+
process.exit(0);
|
|
1221
|
+
}
|
|
1222
|
+
else if (!(error instanceof InternalError)) {
|
|
1223
|
+
throw error;
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
console.error(chalk.red('Error: ' + error.message));
|
|
1227
|
+
process.exit(1);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
};
|
|
1231
|
+
function isError(error) {
|
|
1232
|
+
return typeof error === 'object' && error !== null && 'message' in error;
|
|
1233
|
+
}
|
|
1234
|
+
function isExpectedError(error) {
|
|
1235
|
+
if (isError(error) && error.message.includes('User force closed the prompt with SIGINT')) {
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
if (error instanceof UserAbortError) {
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
1246
|
+
* For licensing, see LICENSE.md.
|
|
1247
|
+
*/
|
|
1248
|
+
const generateChangelogForMonoRepository = async (options) => {
|
|
1249
|
+
const { date, cwd, externalRepositories, nextVersion, disableFilesystemOperations, npmPackageToCheck, packagesDirectory, shouldSkipLinks, shouldIgnoreRootPackage, transformScope } = options;
|
|
1250
|
+
return generateChangelog({
|
|
1251
|
+
nextVersion,
|
|
1252
|
+
cwd,
|
|
1253
|
+
packagesDirectory,
|
|
1254
|
+
externalRepositories,
|
|
1255
|
+
transformScope,
|
|
1256
|
+
date,
|
|
1257
|
+
shouldSkipLinks,
|
|
1258
|
+
disableFilesystemOperations,
|
|
1259
|
+
...(shouldIgnoreRootPackage && npmPackageToCheck ?
|
|
1260
|
+
{ shouldIgnoreRootPackage: true, npmPackageToCheck } :
|
|
1261
|
+
{ shouldIgnoreRootPackage: false }),
|
|
1262
|
+
isSinglePackage: false
|
|
1263
|
+
});
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
|
|
1268
|
+
* For licensing, see LICENSE.md.
|
|
1269
|
+
*/
|
|
1270
|
+
const generateChangelogForSingleRepository = async (options) => {
|
|
1271
|
+
const { cwd, date, externalRepositories, nextVersion, disableFilesystemOperations, shouldSkipLinks } = options;
|
|
1272
|
+
return generateChangelog({
|
|
1273
|
+
nextVersion,
|
|
1274
|
+
cwd,
|
|
1275
|
+
externalRepositories,
|
|
1276
|
+
date,
|
|
1277
|
+
shouldSkipLinks,
|
|
1278
|
+
disableFilesystemOperations,
|
|
1279
|
+
isSinglePackage: true,
|
|
1280
|
+
packagesDirectory: null
|
|
1281
|
+
});
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
export { generateChangelogForMonoRepository, generateChangelogForSingleRepository };
|