@ckeditor/ckeditor5-dev-changelog 50.3.1 → 51.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/dist/index.js CHANGED
@@ -18,6 +18,7 @@ const CHANGELOG_HEADER = 'Changelog\n=========';
18
18
  const NPM_URL = 'https://www.npmjs.com/package';
19
19
  const VERSIONING_POLICY_URL = 'https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html';
20
20
  const CHANGESET_DIRECTORY = '.changelog';
21
+ const PRE_RELEASE_DIRECTORY = 'pre-release';
21
22
  upath.join(import.meta.dirname, '../template/template.md');
22
23
  const SECTIONS = {
23
24
  major: {
@@ -187,14 +188,16 @@ function groupEntriesBySection(options) {
187
188
  return files.reduce((sections, entry) => {
188
189
  const { validatedEntry, isValid } = validateEntry(entry, packageNames, isSinglePackage);
189
190
  const validatedData = validatedEntry.data;
191
+ const closesLinks = filterLinks(validatedData.closes, validatedEntry);
192
+ const closes = getIssuesLinks(closesLinks, 'Closes');
193
+ const seeLinks = filterLinks(validatedData.see, validatedEntry);
194
+ const see = getIssuesLinks(seeLinks, 'See');
190
195
  const scope = isSinglePackage ? null : getScopesLinks(validatedData.scope, transformScope);
191
- const closes = getIssuesLinks(validatedData.closes, 'Closes', validatedEntry.gitHubUrl);
192
- const see = getIssuesLinks(validatedData.see, 'See', validatedEntry.gitHubUrl);
193
196
  const section = getSection({ entry: validatedEntry, isSinglePackage, isValid });
194
197
  const contentWithCommunityCredits = getContentWithCommunityCredits(validatedEntry.content, validatedData.communityCredits);
195
198
  const content = linkToGitHubUser(contentWithCommunityCredits);
196
199
  const [mainContent, ...restContent] = formatContent(content);
197
- const changeMessage = getChangeMessage({ restContent, scope, mainContent, entry, see, closes });
200
+ const changeMessage = getChangeMessage({ restContent, scope, mainContent, see, closes });
198
201
  const newEntry = {
199
202
  message: changeMessage,
200
203
  data: {
@@ -202,8 +205,8 @@ function groupEntriesBySection(options) {
202
205
  restContent,
203
206
  type: validatedData.type,
204
207
  scope: validatedData.scope,
205
- see: validatedData.see.map(see => getIssueLinkObject(see, validatedEntry.gitHubUrl)),
206
- closes: validatedData.closes.map(closes => getIssueLinkObject(closes, validatedEntry.gitHubUrl)),
208
+ see: seeLinks,
209
+ closes: closesLinks,
207
210
  validations: validatedData.validations,
208
211
  communityCredits: validatedData.communityCredits
209
212
  },
@@ -216,13 +219,18 @@ function groupEntriesBySection(options) {
216
219
  return sections;
217
220
  }, getInitialSectionsWithEntries());
218
221
  }
219
- function getChangeMessage({ restContent, scope, mainContent, entry, see, closes }) {
222
+ function filterLinks(links, entry) {
223
+ return links
224
+ .map(link => getIssueLinkObject(link, entry.gitHubUrl))
225
+ .filter(({ link }) => entry.linkFilter(link));
226
+ }
227
+ function getChangeMessage({ restContent, scope, mainContent, see, closes }) {
220
228
  const messageFirstLine = [
221
229
  '*',
222
230
  scope ? `**${scope}**:` : null,
223
231
  mainContent,
224
- !entry.shouldSkipLinks && see.length ? see : null,
225
- !entry.shouldSkipLinks && closes.length ? closes : null
232
+ see.length ? see : null,
233
+ closes.length ? closes : null
226
234
  ].filter(Boolean).join(' ');
227
235
  if (!restContent || !restContent.length) {
228
236
  return messageFirstLine;
@@ -237,7 +245,8 @@ function getChangeMessage({ restContent, scope, mainContent, entry, see, closes
237
245
  function formatContent(content) {
238
246
  const lines = content.trim()
239
247
  .split('\n')
240
- .map(line => line.trimEnd());
248
+ .map(line => line.trimEnd())
249
+ .map(line => normalizeListMarker(line));
241
250
  const mainIndex = lines.findIndex(line => line.trim() !== '');
242
251
  const mainContent = lines.at(mainIndex);
243
252
  let restContent = lines.slice(mainIndex + 1);
@@ -282,18 +291,18 @@ function getIssueLinkObject(issue, gitHubUrl) {
282
291
  const repoUrlMatch = issue.match(ISSUE_URL_PATTERN);
283
292
  if (repoUrlMatch) {
284
293
  const { owner, repository, number } = repoUrlMatch.groups;
294
+ if (issue.startsWith(gitHubUrl)) {
295
+ return { displayName: `#${number}`, link: issue };
296
+ }
285
297
  return { displayName: `${owner}/${repository}#${number}`, link: issue };
286
298
  }
287
299
  return { displayName: '', link: '' };
288
300
  }
289
- function getIssuesLinks(issues, prefix, gitHubUrl) {
301
+ function getIssuesLinks(issues, prefix) {
290
302
  if (!issues.length) {
291
303
  return '';
292
304
  }
293
- const links = issues.map(String).map(issue => {
294
- const { displayName, link } = getIssueLinkObject(issue, gitHubUrl);
295
- return `[${displayName}](${link})`;
296
- });
305
+ const links = issues.map(issue => `[${issue.displayName}](${issue.link})`);
297
306
  return `${prefix} ${links.join(', ')}.`;
298
307
  }
299
308
  function getSection(options) {
@@ -325,6 +334,10 @@ function getInitialSectionsWithEntries() {
325
334
  }
326
335
  return sections;
327
336
  }
337
+ function normalizeListMarker(line) {
338
+ const listMarkerRegexp = /^(\s*)[-+](\s*)/;
339
+ return line.replace(listMarkerRegexp, '$1*$2');
340
+ }
328
341
 
329
342
  /**
330
343
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
@@ -529,6 +542,49 @@ function prepareChangelogContent(existingChangelog, newChangelog) {
529
542
  class UserAbortError extends Error {
530
543
  }
531
544
 
545
+ /**
546
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
547
+ * For licensing, see LICENSE.md.
548
+ */
549
+ async function validateInputVersion(options) {
550
+ const { newVersion, version, releaseType, packageName, suggestedVersion } = options;
551
+ const [newChannel] = semver.prerelease(newVersion) || ['latest'];
552
+ const [currentChannel] = semver.prerelease(version) || ['latest'];
553
+ // Generic semantic‑version checks.
554
+ if (!semver.valid(newVersion)) {
555
+ return 'Please provide a valid version.';
556
+ }
557
+ if (!semver.gt(newVersion, version)) {
558
+ return `Provided version must be higher than "${version}".`;
559
+ }
560
+ if (!(await npm.checkVersionAvailability(newVersion, packageName))) {
561
+ return 'Given version is already taken.';
562
+ }
563
+ // Rules that depend on release type.
564
+ const isPrerelease = releaseType === 'prerelease';
565
+ const isPrereleasePromote = releaseType === 'prerelease-promote';
566
+ const isLatest = releaseType === 'latest';
567
+ // Pre‑release types must always include a channel suffix.
568
+ if ((isPrerelease || isPrereleasePromote) && newChannel === 'latest') {
569
+ return 'You chose the "pre-release" release type. Please provide a version with a channel suffix.';
570
+ }
571
+ // Promoting a pre‑release: new version ≥ suggested version.
572
+ if (isPrereleasePromote && !semver.gte(newVersion, suggestedVersion)) {
573
+ return `Provided version must be higher or equal to "${suggestedVersion}".`;
574
+ }
575
+ // Continuing a pre‑release stream: channel cannot change.
576
+ if (isPrerelease &&
577
+ currentChannel !== 'latest' &&
578
+ currentChannel !== newChannel) {
579
+ return `Provided channel must be the same existing channel ${currentChannel}.`;
580
+ }
581
+ // Latest release must not carry a channel suffix.
582
+ if (isLatest && newChannel !== 'latest') {
583
+ return 'You chose the "latest" release type. Please provide a version without a channel suffix.';
584
+ }
585
+ return true;
586
+ }
587
+
532
588
  /**
533
589
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
534
590
  * For licensing, see LICENSE.md.
@@ -584,39 +640,69 @@ async function askContinueConfirmation(indentLevel = 0) {
584
640
  * Creates a prompt question for version input with validation.
585
641
  */
586
642
  function createVersionQuestion(options) {
587
- const { version, packageName, bumpType, indentLevel = 0 } = options;
588
- const suggestedVersion = semver.inc(version, bumpType) || version;
643
+ const { version, packageName, bumpType, releaseChannel, releaseType, indentLevel = 0 } = options;
644
+ const suggestedVersion = getSuggestedVersion(bumpType, version, releaseChannel) || version;
589
645
  const message = 'Type the new version ' +
590
- `(current: "${version}", suggested: "${suggestedVersion}", or "internal" for internal changes):`;
646
+ `(current: "${version}", suggested: "${suggestedVersion}":`;
591
647
  return [{
592
648
  type: 'input',
593
649
  name: 'version',
594
650
  default: suggestedVersion,
595
651
  message,
596
652
  filter: (newVersion) => newVersion.trim(),
597
- async validate(newVersion) {
598
- // Allow 'internal' as a special version.
599
- if (newVersion === 'internal') {
600
- return true;
601
- }
602
- // Require a semver valid version, e.g., `1.0.0`, `1.0.0-alpha.0`, etc.
603
- if (!semver.valid(newVersion)) {
604
- return 'Please provide a valid version or "internal" for internal changes.';
605
- }
606
- // The provided version must be higher than the current version.
607
- if (!semver.gt(newVersion, version)) {
608
- return `Provided version must be higher than "${version}".`;
609
- }
610
- const isAvailable = await npm.checkVersionAvailability(newVersion, packageName);
611
- // Check against availability in the npm registry.
612
- if (!isAvailable) {
613
- return 'Given version is already taken.';
614
- }
615
- return true;
616
- },
653
+ validate: (newVersion) => validateInputVersion({ newVersion, version, releaseType, packageName, suggestedVersion }),
617
654
  prefix: ' '.repeat(indentLevel * CLI_INDENT_SIZE) + chalk.cyan('?')
618
655
  }];
619
656
  }
657
+ function getSuggestedVersion(bumpType, version, releaseChannel) {
658
+ if (bumpType === 'prerelease' && releaseChannel !== 'latest') {
659
+ return semver.inc(version, bumpType, releaseChannel);
660
+ }
661
+ else if (bumpType === 'prerelease' && releaseChannel === 'latest') {
662
+ // Using 'premajor` and `alpha` channel for a case, when introducing a prerelease for the next major.
663
+ // E.g. 1.0.0 -> 2.0.0-alpha.0.
664
+ return semver.inc(version, 'premajor', 'alpha');
665
+ }
666
+ else {
667
+ return semver.inc(version, bumpType);
668
+ }
669
+ }
670
+
671
+ /**
672
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
673
+ * For licensing, see LICENSE.md.
674
+ */
675
+ /**
676
+ * Detects the release channel from a version string.
677
+ */
678
+ function detectReleaseChannel(version, promotePrerelease = false) {
679
+ const prerelease = semver.prerelease(version);
680
+ if (!prerelease) {
681
+ return 'latest';
682
+ }
683
+ const currentChannel = prerelease[0];
684
+ if (promotePrerelease) {
685
+ if (currentChannel === 'alpha') {
686
+ return 'beta';
687
+ }
688
+ if (currentChannel === 'beta') {
689
+ return 'rc';
690
+ }
691
+ logInfo(chalk.yellow(`Warning! Unknown release channel to promote from ${currentChannel}.`));
692
+ return 'alpha';
693
+ }
694
+ return currentChannel;
695
+ }
696
+
697
+ /**
698
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
699
+ * For licensing, see LICENSE.md.
700
+ */
701
+ /**
702
+ * Custom error class for handling validation errors in the changelog generation process.
703
+ */
704
+ class InternalError extends Error {
705
+ }
620
706
 
621
707
  /**
622
708
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
@@ -624,27 +710,39 @@ function createVersionQuestion(options) {
624
710
  */
625
711
  /**
626
712
  * Determines the next version for a single package or a mono-repository setup based on
627
- * the change sections, * user input, and semantic versioning rules.
713
+ * the change sections, user input, and semantic versioning rules.
628
714
  *
629
715
  * The function handles:
630
716
  * * Automatic version bump calculation from categorized changelog sections (major, minor, patch).
631
- * * Accepting explicit next version overrides, including a special `internal` version bump.
717
+ * * Version bump for prerelease channels.
632
718
  * * User prompts for version input when no explicit version is provided.
633
719
  */
634
720
  async function determineNextVersion(options) {
635
- const { sections, currentVersion, packageName, nextVersion } = options;
636
- if (nextVersion === 'internal') {
637
- const internalVersionBump = getInternalVersionBump(currentVersion);
638
- logInfo(`○ ${chalk.cyan(`Determined the next version to be ${internalVersionBump.newVersion}.`)}`);
639
- return internalVersionBump;
640
- }
721
+ const { sections, currentVersion, packageName, nextVersion, releaseType } = options;
641
722
  if (nextVersion) {
642
723
  logInfo(`○ ${chalk.cyan(`Determined the next version to be ${nextVersion}.`)}`);
643
- return { newVersion: nextVersion, isInternal: false };
724
+ const isNightlyVersion = nextVersion.startsWith('0.0.0-');
725
+ if (isNightlyVersion) {
726
+ return nextVersion;
727
+ }
728
+ const validationResult = await validateInputVersion({
729
+ newVersion: nextVersion,
730
+ suggestedVersion: nextVersion,
731
+ version: currentVersion,
732
+ releaseType,
733
+ packageName
734
+ });
735
+ if (typeof validationResult === 'string') {
736
+ throw new InternalError(validationResult);
737
+ }
738
+ return nextVersion;
644
739
  }
645
740
  logInfo(`○ ${chalk.cyan('Determining the new version...')}`);
646
741
  let bumpType = 'patch';
647
- if (sections.major.entries.length || sections.breaking.entries.length) {
742
+ if (releaseType === 'prerelease' || releaseType === 'prerelease-promote') {
743
+ bumpType = 'prerelease';
744
+ }
745
+ else if (sections.major.entries.length || sections.breaking.entries.length) {
648
746
  bumpType = 'major';
649
747
  }
650
748
  else if (sections.minor.entries.length || sections.feature.entries.length) {
@@ -655,20 +753,12 @@ async function determineNextVersion(options) {
655
753
  const userProvidedVersion = await provideNewVersion({
656
754
  packageName,
657
755
  bumpType,
756
+ releaseType,
658
757
  version: currentVersion,
758
+ releaseChannel: detectReleaseChannel(currentVersion, releaseType === 'prerelease-promote'),
659
759
  displayValidationWarning: areErrorsPresent || areWarningsPresent
660
760
  });
661
- if (userProvidedVersion === 'internal') {
662
- return getInternalVersionBump(currentVersion);
663
- }
664
- return { newVersion: userProvidedVersion, isInternal: false };
665
- }
666
- function getInternalVersionBump(currentVersion) {
667
- const version = semver.inc(currentVersion, 'patch');
668
- if (!version) {
669
- throw new Error('Unable to determine new version based on the version in root package.json.');
670
- }
671
- return { newVersion: version, isInternal: true };
761
+ return userProvidedVersion;
672
762
  }
673
763
 
674
764
  /**
@@ -819,37 +909,44 @@ function getPackageName(value) {
819
909
  * Gathers changelog entry file paths (Markdown files) from the main repository and any configured external repositories.
820
910
  */
821
911
  async function findChangelogEntryPaths(options) {
822
- const { cwd, externalRepositories, shouldSkipLinks } = options;
912
+ const { cwd, externalRepositories, includeSubdirectories = true } = options;
913
+ const globPattern = includeSubdirectories ? '**/*.md' : '*.md';
823
914
  return AsyncArray
824
915
  .from(Promise.resolve(externalRepositories))
825
916
  .map(async (repo) => {
826
- const changesetGlob = await glob('**/*.md', {
917
+ const changesetGlob = await glob(globPattern, {
827
918
  cwd: upath.join(repo.cwd, CHANGESET_DIRECTORY),
828
919
  absolute: true
829
920
  });
830
921
  return {
831
922
  filePaths: changesetGlob.map(p => upath.normalize(p)),
832
923
  gitHubUrl: await workspaces.getRepositoryUrl(repo.cwd, { async: true }),
833
- shouldSkipLinks: !!repo.shouldSkipLinks,
924
+ linkFilter: getLinkFilter(repo),
834
925
  cwd: repo.cwd,
835
926
  isRoot: false
836
927
  };
837
928
  })
838
929
  .then(async (externalResults) => {
839
- const mainChangesetGlob = await glob('**/*.md', {
930
+ const mainChangesetGlob = await glob(globPattern, {
840
931
  cwd: upath.join(cwd, CHANGESET_DIRECTORY),
841
932
  absolute: true
842
933
  });
843
934
  const mainEntry = {
844
935
  filePaths: mainChangesetGlob.map(p => upath.normalize(p)),
845
936
  gitHubUrl: await workspaces.getRepositoryUrl(cwd, { async: true }),
846
- shouldSkipLinks,
937
+ linkFilter: getLinkFilter(options),
847
938
  cwd,
848
939
  isRoot: true
849
940
  };
850
941
  return [mainEntry, ...externalResults];
851
942
  });
852
943
  }
944
+ function getLinkFilter(options) {
945
+ if (typeof options.shouldSkipLinks === 'boolean') {
946
+ return () => !options.shouldSkipLinks;
947
+ }
948
+ return options.linkFilter || (() => true);
949
+ }
853
950
 
854
951
  /**
855
952
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
@@ -955,19 +1052,19 @@ function toArray(input) {
955
1052
  * Reads and processes input files to extract changelog entries.
956
1053
  */
957
1054
  function parseChangelogEntries(entryPaths, isSinglePackage) {
958
- const fileEntries = entryPaths.reduce((acc, { filePaths, gitHubUrl, shouldSkipLinks }) => {
1055
+ const fileEntries = entryPaths.reduce((acc, { filePaths, gitHubUrl, linkFilter }) => {
959
1056
  for (const changesetPath of filePaths) {
960
- acc.push({ changesetPath, gitHubUrl, shouldSkipLinks });
1057
+ acc.push({ changesetPath, gitHubUrl, linkFilter });
961
1058
  }
962
1059
  return acc;
963
1060
  }, []);
964
1061
  return AsyncArray
965
1062
  .from(Promise.resolve(fileEntries))
966
- .map(async ({ changesetPath, gitHubUrl, shouldSkipLinks }) => ({
1063
+ .map(async ({ changesetPath, gitHubUrl, linkFilter }) => ({
967
1064
  ...matter(await fs$1.readFile(changesetPath, 'utf-8')),
968
1065
  gitHubUrl,
969
1066
  changesetPath,
970
- shouldSkipLinks,
1067
+ linkFilter,
971
1068
  createdAt: extractDateFromFilename(changesetPath)
972
1069
  }))
973
1070
  .map(entry => normalizeEntry(entry, isSinglePackage))
@@ -994,25 +1091,6 @@ function extractDateFromFilename(changesetPath) {
994
1091
  return parsedDate;
995
1092
  }
996
1093
 
997
- /**
998
- * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
999
- * For licensing, see LICENSE.md.
1000
- */
1001
- /**
1002
- * Custom error class for handling validation errors in the changelog generation process.
1003
- */
1004
- class InternalError extends Error {
1005
- constructor() {
1006
- const message = 'No valid entries were found. Please ensure that:\n' +
1007
- '1) Input files exist in the `.changelog/` directory.\n' +
1008
- '2) The `cwd` parameter points to the root of your project.\n' +
1009
- '3) The `packagesDirectory` parameter correctly specifies the packages folder.\n' +
1010
- 'If no errors appear in the console but inputs are present, your project configuration may be incorrect.\n' +
1011
- 'If validation errors are shown, please resolve them according to the details provided.\n';
1012
- super(message);
1013
- }
1014
- }
1015
-
1016
1094
  /**
1017
1095
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
1018
1096
  * For licensing, see LICENSE.md.
@@ -1031,7 +1109,13 @@ function filterVisibleSections(sectionsWithEntries) {
1031
1109
  })
1032
1110
  .map(([, section]) => section);
1033
1111
  if (!sectionsToDisplay.length) {
1034
- throw new InternalError();
1112
+ const message = 'No valid entries were found. Please ensure that:\n' +
1113
+ '1) Input files exist in the `.changelog/` directory.\n' +
1114
+ '2) The `cwd` parameter points to the root of your project.\n' +
1115
+ '3) The `packagesDirectory` parameter correctly specifies the packages folder.\n' +
1116
+ 'If no errors appear in the console but inputs are present, your project configuration may be incorrect.\n' +
1117
+ 'If validation errors are shown, please resolve them according to the details provided.\n';
1118
+ throw new InternalError(message);
1035
1119
  }
1036
1120
  return sectionsToDisplay;
1037
1121
  }
@@ -1058,13 +1142,12 @@ function getDateFormatted(date) {
1058
1142
  * * A version header with a link to the GitHub comparison view (except for an initial version).
1059
1143
  * * Sections with grouped changelog entries and their messages.
1060
1144
  * * A collapsible summary of released packages and their version bumps for a mono-repository setup.
1061
- * * Special handling for internal-only releases and single-package repositories.
1145
+ * * Special handling for single-package repositories.
1062
1146
  */
1063
1147
  async function composeChangelog(options) {
1064
- const { cwd, date, currentVersion, newVersion, sections, releasedPackagesInfo, isInternal, isSinglePackage, packagesMetadata } = options;
1148
+ const { cwd, date, currentVersion, newVersion, sections, releasedPackagesInfo, isSinglePackage } = options;
1065
1149
  const gitHubUrl = await workspaces.getRepositoryUrl(cwd, { async: true });
1066
1150
  const dateFormatted = getDateFormatted(date);
1067
- const packagesNames = [...packagesMetadata.keys()];
1068
1151
  const header = currentVersion === '0.0.1' ?
1069
1152
  `## ${newVersion} (${dateFormatted})` :
1070
1153
  `## [${newVersion}](${gitHubUrl}/compare/v${currentVersion}...v${newVersion}) (${dateFormatted})`;
@@ -1080,19 +1163,13 @@ async function composeChangelog(options) {
1080
1163
  '',
1081
1164
  ...packages.map(packageName => `* [${packageName}](${NPM_URL}/${packageName}/v/${newVersion}): ${version}`)
1082
1165
  ])).flat().join('\n');
1083
- const internalVersionsBumps = [
1084
- '',
1085
- SECTIONS.other.title + ':',
1086
- '',
1087
- packagesNames.map(name => `* [${name}](${NPM_URL}/${name}/v/${newVersion}): v${currentVersion} => v${newVersion}`)
1088
- ].flat().join('\n');
1089
1166
  const changelog = [
1090
1167
  header,
1091
1168
  '',
1092
- isInternal ? 'Internal changes only (updated dependencies, documentation, etc.).\n' : sectionsAsString
1169
+ sectionsAsString
1093
1170
  ];
1094
1171
  if (!isSinglePackage) {
1095
- 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>', '');
1172
+ changelog.push('### Released packages', '', `Check out the [Versioning policy](${VERSIONING_POLICY_URL}) guide for more information.`, '', '<details>', '<summary>Released packages (summary)</summary>', packagesVersionBumps, '</details>', '');
1096
1173
  }
1097
1174
  return changelog.join('\n');
1098
1175
  }
@@ -1112,6 +1189,40 @@ async function removeChangelogEntryFiles(entryPaths) {
1112
1189
  .map(file => fs$1.unlink(file)));
1113
1190
  }
1114
1191
 
1192
+ /**
1193
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
1194
+ * For licensing, see LICENSE.md.
1195
+ */
1196
+ /**
1197
+ * Moves changelog entry files to cycle-specific directories instead of deleting them.
1198
+ * This preserves the history of changes across prerelease cycles.
1199
+ * Returns an array of entry paths that were modified by the move operation
1200
+ */
1201
+ async function moveChangelogEntryFiles(entryPaths) {
1202
+ const targetDir = PRE_RELEASE_DIRECTORY;
1203
+ const modifiedEntryPaths = [];
1204
+ logInfo(`○ ${chalk.cyan(`Moving changelog entries to ${targetDir}/ directory...`)}`);
1205
+ for (const repo of entryPaths) {
1206
+ const { cwd, filePaths } = repo;
1207
+ const changelogDir = upath.join(cwd, CHANGESET_DIRECTORY);
1208
+ const targetPath = upath.join(changelogDir, targetDir);
1209
+ await fs$1.ensureDir(targetPath);
1210
+ const modifiedFilePaths = [];
1211
+ for (const filePath of filePaths) {
1212
+ const fileName = upath.basename(filePath);
1213
+ const targetFilePath = upath.join(targetPath, fileName);
1214
+ await fs$1.rename(filePath, targetFilePath);
1215
+ modifiedFilePaths.push(targetFilePath);
1216
+ modifiedFilePaths.push(filePath);
1217
+ }
1218
+ modifiedEntryPaths.push({
1219
+ ...repo,
1220
+ filePaths: modifiedFilePaths
1221
+ });
1222
+ }
1223
+ return modifiedEntryPaths;
1224
+ }
1225
+
1115
1226
  /**
1116
1227
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
1117
1228
  * For licensing, see LICENSE.md.
@@ -1134,6 +1245,61 @@ async function commitChanges(version, repositories) {
1134
1245
  }
1135
1246
  }
1136
1247
 
1248
+ /**
1249
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
1250
+ * For licensing, see LICENSE.md.
1251
+ */
1252
+ /**
1253
+ * Prompts the user to choose between latest or prerelease
1254
+ */
1255
+ async function promptReleaseType(currentVersion) {
1256
+ const { releaseType } = await inquirer.prompt([
1257
+ {
1258
+ type: 'list',
1259
+ name: 'releaseType',
1260
+ message: 'Please select the release type.',
1261
+ choices: getQuestions(currentVersion)
1262
+ }
1263
+ ]);
1264
+ return releaseType;
1265
+ }
1266
+ function getQuestions(currentVersion) {
1267
+ const currentVersionPrerelease = semver.prerelease(currentVersion);
1268
+ if (!currentVersionPrerelease) {
1269
+ return [
1270
+ { name: 'Latest (stable) release (e.g. 1.0.0 -> 2.0.0)', value: 'latest' },
1271
+ { name: 'Pre-release (e.g. 1.0.0 -> 2.0.0-alpha.0)', value: 'prerelease' }
1272
+ ];
1273
+ }
1274
+ if (currentVersionPrerelease[0] === 'rc') {
1275
+ return [
1276
+ { name: 'Latest (stable) release (e.g. 1.0.0-beta.2 -> 1.0.0)', value: 'latest' },
1277
+ { name: 'Pre-release continuation (e.g. 1.0.0-alpha.0 -> 1.0.0-alpha.1)', value: 'prerelease' }
1278
+ ];
1279
+ }
1280
+ return [
1281
+ { name: 'Latest (stable) release (e.g. 1.0.0-beta.2 -> 1.0.0)', value: 'latest' },
1282
+ { name: 'Pre-release continuation (e.g. 1.0.0-alpha.0 -> 1.0.0-alpha.1)', value: 'prerelease' },
1283
+ { name: 'Pre-release promotion (e.g. 1.0.0-alpha.1 -> 1.0.0-beta.0)', value: 'prerelease-promote' }
1284
+ ];
1285
+ }
1286
+
1287
+ /**
1288
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
1289
+ * For licensing, see LICENSE.md.
1290
+ */
1291
+ function getReleaseType(currentVersion, nextVersion) {
1292
+ const [currentChannel] = semver.prerelease(currentVersion) || ['latest'];
1293
+ const [nextChannel] = semver.prerelease(nextVersion) || ['latest'];
1294
+ if (nextChannel === 'latest') {
1295
+ return 'latest';
1296
+ }
1297
+ if (nextChannel === currentChannel) {
1298
+ return 'prerelease';
1299
+ }
1300
+ return 'prerelease-promote';
1301
+ }
1302
+
1137
1303
  /**
1138
1304
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
1139
1305
  * For licensing, see LICENSE.md.
@@ -1156,7 +1322,7 @@ async function commitChanges(version, repositories) {
1156
1322
  * and the assembled changelog object will be returned instead.
1157
1323
  */
1158
1324
  const main = async (options) => {
1159
- const { nextVersion, packagesDirectory, isSinglePackage, transformScope, npmPackageToCheck, cwd = process.cwd(), externalRepositories = [], date = format(new Date(), 'yyyy-MM-dd'), shouldSkipLinks = false, shouldIgnoreRootPackage = false, disableFilesystemOperations = false } = options;
1325
+ const { nextVersion, packagesDirectory, isSinglePackage, transformScope, npmPackageToCheck, linkFilter, shouldSkipLinks, cwd = process.cwd(), externalRepositories = [], date = format(new Date(), 'yyyy-MM-dd'), shouldIgnoreRootPackage = false, disableFilesystemOperations = false } = options;
1160
1326
  const { version: currentVersion, name: rootPackageName } = await workspaces.getPackageJson(cwd, { async: true });
1161
1327
  const packagesMetadata = await findPackages({
1162
1328
  cwd,
@@ -1164,10 +1330,13 @@ const main = async (options) => {
1164
1330
  shouldIgnoreRootPackage,
1165
1331
  externalRepositories
1166
1332
  });
1333
+ const releaseType = nextVersion ? getReleaseType(currentVersion, nextVersion) : await promptReleaseType(currentVersion);
1167
1334
  const entryPaths = await findChangelogEntryPaths({
1168
1335
  cwd,
1169
1336
  externalRepositories,
1170
- shouldSkipLinks
1337
+ linkFilter,
1338
+ shouldSkipLinks,
1339
+ includeSubdirectories: releaseType === 'latest' || releaseType === 'prerelease-promote'
1171
1340
  });
1172
1341
  const parsedChangesetFiles = await parseChangelogEntries(entryPaths, isSinglePackage);
1173
1342
  const sectionsWithEntries = groupEntriesBySection({
@@ -1191,9 +1360,10 @@ const main = async (options) => {
1191
1360
  });
1192
1361
  }
1193
1362
  // Display a prompt to provide a new version in the console.
1194
- const { isInternal, newVersion } = await determineNextVersion({
1363
+ const newVersion = await determineNextVersion({
1195
1364
  currentVersion,
1196
1365
  nextVersion,
1366
+ releaseType,
1197
1367
  sections: sectionsWithEntries,
1198
1368
  packageName: shouldIgnoreRootPackage ? npmPackageToCheck : rootPackageName
1199
1369
  });
@@ -1208,18 +1378,23 @@ const main = async (options) => {
1208
1378
  cwd,
1209
1379
  date,
1210
1380
  newVersion,
1211
- isInternal,
1212
1381
  isSinglePackage,
1213
- packagesMetadata,
1214
1382
  releasedPackagesInfo,
1215
1383
  sections: filterVisibleSections(sectionsWithEntries)
1216
1384
  });
1217
1385
  if (disableFilesystemOperations) {
1218
1386
  return newChangelog;
1219
1387
  }
1220
- await removeChangelogEntryFiles(entryPaths);
1388
+ let pathsToCommit = entryPaths;
1389
+ // Handle changelog entry files based on release type.
1390
+ if (releaseType === 'latest') {
1391
+ await removeChangelogEntryFiles(entryPaths);
1392
+ }
1393
+ else {
1394
+ pathsToCommit = await moveChangelogEntryFiles(entryPaths);
1395
+ }
1221
1396
  await modifyChangelog(newChangelog, cwd);
1222
- await commitChanges(newVersion, entryPaths.map(({ cwd, isRoot, filePaths }) => ({ cwd, isRoot, filePaths })));
1397
+ await commitChanges(newVersion, pathsToCommit.map(({ cwd, isRoot, filePaths }) => ({ cwd, isRoot, filePaths })));
1223
1398
  logInfo('○ ' + chalk.green('Done!'));
1224
1399
  };
1225
1400
  /**
@@ -1263,7 +1438,7 @@ function isExpectedError(error) {
1263
1438
  * For licensing, see LICENSE.md.
1264
1439
  */
1265
1440
  const generateChangelogForMonoRepository = async (options) => {
1266
- const { date, cwd, externalRepositories, nextVersion, disableFilesystemOperations, npmPackageToCheck, packagesDirectory, shouldSkipLinks, shouldIgnoreRootPackage, transformScope } = options;
1441
+ const { date, cwd, externalRepositories, nextVersion, disableFilesystemOperations, npmPackageToCheck, packagesDirectory, linkFilter, shouldSkipLinks, shouldIgnoreRootPackage, transformScope } = options;
1267
1442
  return generateChangelog({
1268
1443
  nextVersion,
1269
1444
  cwd,
@@ -1271,6 +1446,7 @@ const generateChangelogForMonoRepository = async (options) => {
1271
1446
  externalRepositories,
1272
1447
  transformScope,
1273
1448
  date,
1449
+ linkFilter,
1274
1450
  shouldSkipLinks,
1275
1451
  disableFilesystemOperations,
1276
1452
  ...(shouldIgnoreRootPackage && npmPackageToCheck ?
@@ -1285,12 +1461,13 @@ const generateChangelogForMonoRepository = async (options) => {
1285
1461
  * For licensing, see LICENSE.md.
1286
1462
  */
1287
1463
  const generateChangelogForSingleRepository = async (options) => {
1288
- const { cwd, date, externalRepositories, nextVersion, disableFilesystemOperations, shouldSkipLinks } = options;
1464
+ const { cwd, date, externalRepositories, nextVersion, disableFilesystemOperations, linkFilter, shouldSkipLinks } = options;
1289
1465
  return generateChangelog({
1290
1466
  nextVersion,
1291
1467
  cwd,
1292
1468
  externalRepositories,
1293
1469
  date,
1470
+ linkFilter,
1294
1471
  shouldSkipLinks,
1295
1472
  disableFilesystemOperations,
1296
1473
  isSinglePackage: true,
package/dist/types.d.ts CHANGED
@@ -6,9 +6,9 @@ import type { SECTIONS } from './utils/constants.js';
6
6
  export type ConfigBase = RepositoryConfig & {
7
7
  /**
8
8
  * The next version number to use. If not provided, will be calculated based on changes.
9
- * Can be a semver string or 'internal' for internal changes only.
9
+ * Must be a valid version in terms of the semantic versioning specification.
10
10
  */
11
- nextVersion?: string | 'internal';
11
+ nextVersion?: string;
12
12
  /**
13
13
  * Array of external repository configurations to include in the changelog.
14
14
  */
@@ -41,6 +41,12 @@ export type RepositoryConfig = {
41
41
  * The directory containing the packages. Defaults to 'packages'.
42
42
  */
43
43
  packagesDirectory: null | string;
44
+ /**
45
+ * Function that decides whether to filter out a link in the changelog entry.
46
+ * If `shouldSkipLinks` flag is set, the `shouldSkipLinks` flag takes precedence over the `linkFilter` function.
47
+ * No links are skipped by default.
48
+ */
49
+ linkFilter?: LinkFilter;
44
50
  /**
45
51
  * Whether to skip links in the changelog entries. Defaults to false.
46
52
  */
@@ -73,7 +79,7 @@ export type ParsedFile<T = FileMetadata> = {
73
79
  changesetPath: string;
74
80
  createdAt: Date;
75
81
  gitHubUrl: string;
76
- shouldSkipLinks: boolean;
82
+ linkFilter: LinkFilter;
77
83
  };
78
84
  export type Section = {
79
85
  entries: Array<Entry>;
@@ -94,7 +100,7 @@ export type TransformScope = (name: string) => {
94
100
  export type ChangesetPathsWithGithubUrl = {
95
101
  filePaths: Array<string>;
96
102
  gitHubUrl: string;
97
- shouldSkipLinks: boolean;
103
+ linkFilter: LinkFilter;
98
104
  cwd: string;
99
105
  isRoot: boolean;
100
106
  };
@@ -109,6 +115,7 @@ export type LinkObject = {
109
115
  displayName: string;
110
116
  link: string;
111
117
  };
118
+ export type LinkFilter = (resourceUrl: string) => boolean;
112
119
  export type FileMetadata = {
113
120
  type: string;
114
121
  scope: Array<string>;
@@ -117,4 +124,8 @@ export type FileMetadata = {
117
124
  communityCredits: Array<string>;
118
125
  validations: Array<string>;
119
126
  };
127
+ export type ChangelogReleaseType = 'latest' | // e.g. 1.0.0 -> 2.0.0 or 2.0.0-beta.1 -> 2.0.0.
128
+ 'prerelease' | // e.g. 1.0.0 -> 2.0.0-alpha.0 or 2.0.0-alpha.0 -> 2.0.0-alpha.1.
129
+ 'prerelease-promote';
130
+ export type ReleaseChannel = 'alpha' | 'beta' | 'rc' | 'latest';
120
131
  export {};
@@ -3,16 +3,14 @@
3
3
  * For licensing, see LICENSE.md.
4
4
  */
5
5
  import type { ReleaseInfo, Section } from '../types.js';
6
- type ComposeChangelogOptions = {
6
+ export type ComposeChangelogOptions = {
7
7
  cwd: string;
8
8
  date: string;
9
9
  currentVersion: string;
10
10
  newVersion: string;
11
11
  sections: Array<Section>;
12
12
  releasedPackagesInfo: Array<ReleaseInfo>;
13
- isInternal: boolean;
14
13
  isSinglePackage: boolean;
15
- packagesMetadata: Map<string, string>;
16
14
  };
17
15
  /**
18
16
  * Generates a formatted changelog string for a new version release.
@@ -21,7 +19,6 @@ type ComposeChangelogOptions = {
21
19
  * * A version header with a link to the GitHub comparison view (except for an initial version).
22
20
  * * Sections with grouped changelog entries and their messages.
23
21
  * * A collapsible summary of released packages and their version bumps for a mono-repository setup.
24
- * * Special handling for internal-only releases and single-package repositories.
22
+ * * Special handling for single-package repositories.
25
23
  */
26
24
  export declare function composeChangelog(options: ComposeChangelogOptions): Promise<string>;
27
- export {};
@@ -7,6 +7,7 @@ export declare const CHANGELOG_HEADER = "Changelog\n=========";
7
7
  export declare const NPM_URL = "https://www.npmjs.com/package";
8
8
  export declare const VERSIONING_POLICY_URL = "https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/versioning-policy.html";
9
9
  export declare const CHANGESET_DIRECTORY = ".changelog";
10
+ export declare const PRE_RELEASE_DIRECTORY = "pre-release";
10
11
  export declare const TEMPLATE_FILE: string;
11
12
  export declare const SECTIONS: {
12
13
  readonly major: {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+ import type { ReleaseChannel } from '../types.js';
6
+ /**
7
+ * Detects the release channel from a version string.
8
+ */
9
+ export declare function detectReleaseChannel(version: string, promotePrerelease?: boolean): ReleaseChannel;
@@ -2,25 +2,21 @@
2
2
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md.
4
4
  */
5
- import type { SectionsWithEntries } from '../types.js';
6
- type NextVersionOutput = {
7
- isInternal: boolean;
8
- newVersion: string;
9
- };
5
+ import type { ChangelogReleaseType, SectionsWithEntries } from '../types.js';
10
6
  export type DetermineNextVersionOptions = {
11
7
  sections: SectionsWithEntries;
12
8
  currentVersion: string;
13
9
  packageName: string;
14
10
  nextVersion: string | undefined;
11
+ releaseType: ChangelogReleaseType;
15
12
  };
16
13
  /**
17
14
  * Determines the next version for a single package or a mono-repository setup based on
18
- * the change sections, * user input, and semantic versioning rules.
15
+ * the change sections, user input, and semantic versioning rules.
19
16
  *
20
17
  * The function handles:
21
18
  * * Automatic version bump calculation from categorized changelog sections (major, minor, patch).
22
- * * Accepting explicit next version overrides, including a special `internal` version bump.
19
+ * * Version bump for prerelease channels.
23
20
  * * User prompts for version input when no explicit version is provided.
24
21
  */
25
- export declare function determineNextVersion(options: DetermineNextVersionOptions): Promise<NextVersionOutput>;
26
- export {};
22
+ export declare function determineNextVersion(options: DetermineNextVersionOptions): Promise<string>;
@@ -2,11 +2,13 @@
2
2
  * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md.
4
4
  */
5
- import type { ChangesetPathsWithGithubUrl, RepositoryConfig } from '../types.js';
5
+ import type { ChangesetPathsWithGithubUrl, LinkFilter, RepositoryConfig } from '../types.js';
6
6
  type FindChangelogEntryPathsOptions = {
7
7
  cwd: string;
8
8
  externalRepositories: Array<RepositoryConfig>;
9
- shouldSkipLinks: boolean;
9
+ linkFilter?: LinkFilter;
10
+ shouldSkipLinks?: boolean;
11
+ includeSubdirectories?: boolean;
10
12
  };
11
13
  /**
12
14
  * Gathers changelog entry file paths (Markdown files) from the main repository and any configured external repositories.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+ import { type ChangelogReleaseType } from '../types.js';
6
+ export declare function getReleaseType(currentVersion: string, nextVersion: string): ChangelogReleaseType;
@@ -6,5 +6,4 @@
6
6
  * Custom error class for handling validation errors in the changelog generation process.
7
7
  */
8
8
  export declare class InternalError extends Error {
9
- constructor();
10
9
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+ import type { ChangesetPathsWithGithubUrl } from '../types.js';
6
+ /**
7
+ * Moves changelog entry files to cycle-specific directories instead of deleting them.
8
+ * This preserves the history of changes across prerelease cycles.
9
+ * Returns an array of entry paths that were modified by the move operation
10
+ */
11
+ export declare function moveChangelogEntryFiles(entryPaths: Array<ChangesetPathsWithGithubUrl>): Promise<Array<ChangesetPathsWithGithubUrl>>;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+ import type { ChangelogReleaseType } from '../types.js';
6
+ /**
7
+ * Prompts the user to choose between latest or prerelease
8
+ */
9
+ export declare function promptReleaseType(currentVersion: string): Promise<ChangelogReleaseType>;
@@ -3,12 +3,15 @@
3
3
  * For licensing, see LICENSE.md.
4
4
  */
5
5
  import { type ReleaseType } from 'semver';
6
+ import type { ChangelogReleaseType, ReleaseChannel } from '../types.js';
6
7
  type Options = {
7
8
  packageName: string;
8
9
  version: string;
9
10
  bumpType: ReleaseType;
11
+ releaseChannel: ReleaseChannel;
10
12
  indentLevel?: number;
11
13
  displayValidationWarning: boolean;
14
+ releaseType: ChangelogReleaseType;
12
15
  };
13
16
  /**
14
17
  * Prompts the user to provide a new version for a package.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+ import type { ChangelogReleaseType } from '../types.js';
6
+ type ValidateOptions = {
7
+ newVersion: string;
8
+ version: string;
9
+ releaseType: ChangelogReleaseType;
10
+ packageName: string;
11
+ suggestedVersion: string;
12
+ };
13
+ export declare function validateInputVersion(options: ValidateOptions): Promise<string | true>;
14
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-dev-changelog",
3
- "version": "50.3.1",
3
+ "version": "51.1.0",
4
4
  "description": "A CKEditor 5 development tool for handling changelogs.",
5
5
  "keywords": [],
6
6
  "author": "CKSource (http://cksource.com/)",
@@ -28,7 +28,7 @@
28
28
  "ckeditor5-dev-changelog-create-entry": "bin/generate-template.js"
29
29
  },
30
30
  "dependencies": {
31
- "@ckeditor/ckeditor5-dev-utils": "^50.3.1",
31
+ "@ckeditor/ckeditor5-dev-utils": "^51.1.0",
32
32
  "chalk": "^5.0.0",
33
33
  "date-fns": "^4.0.0",
34
34
  "fs-extra": "^11.0.0",
@@ -9,10 +9,10 @@
9
9
  #
10
10
  # For guidance on breaking changes, see:
11
11
  # https://ckeditor.com/docs/ckeditor5/latest/updating/versioning-policy.html#major-and-minor-breaking-changes
12
- type:
12
+ type:
13
13
 
14
14
  # Optional: Affected package(s), using short names.
15
- # Can be skipped when processing a non-mono-repository.
15
+ # Leave empty when used in a single-package repository.
16
16
  # Example: ckeditor5-core
17
17
  scope:
18
18
  -