@williamthorsen/release-kit 14.2.2 → 14.2.3

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.
Files changed (84) hide show
  1. package/.meta/changelog.json +50 -0
  2. package/.readyup/kits/default.js +135 -145
  3. package/.readyup/kits/npm-auto-publish.js +179 -180
  4. package/.readyup/manifest.json +31 -17
  5. package/CHANGELOG.md +16 -0
  6. package/README.md +7 -1
  7. package/dist/esm/applyChangelogOverrides.d.ts +50 -0
  8. package/dist/esm/applyChangelogOverrides.js +162 -0
  9. package/dist/esm/attachChangelogDiagnostics.d.ts +1 -1
  10. package/dist/esm/bin/release-kit.js +6 -238
  11. package/dist/esm/buildChangelogEntries.d.ts +20 -31
  12. package/dist/esm/buildChangelogEntries.js +71 -188
  13. package/dist/esm/buildChangelogItem.d.ts +56 -0
  14. package/dist/esm/buildChangelogItem.js +119 -0
  15. package/dist/esm/buildReleasedResult.d.ts +20 -0
  16. package/dist/esm/buildReleasedResult.js +78 -0
  17. package/dist/esm/changelogJsonUtils.d.ts +1 -1
  18. package/dist/esm/changelogOverrides.d.ts +29 -143
  19. package/dist/esm/changelogOverrides.js +35 -467
  20. package/dist/esm/configSchema.d.ts +225 -0
  21. package/dist/esm/configSchema.js +103 -0
  22. package/dist/esm/decideDirectReleases.d.ts +30 -0
  23. package/dist/esm/decideDirectReleases.js +66 -0
  24. package/dist/esm/defineConfig.d.ts +2 -2
  25. package/dist/esm/deriveReleaseNotesConfig.d.ts +2 -1
  26. package/dist/esm/deriveReleaseNotesConfig.js +1 -1
  27. package/dist/esm/formatPrepareFiles.d.ts +11 -0
  28. package/dist/esm/formatPrepareFiles.js +55 -0
  29. package/dist/esm/formatPrepareWarnings.d.ts +26 -0
  30. package/dist/esm/formatPrepareWarnings.js +72 -0
  31. package/dist/esm/help/initHelp.d.ts +4 -0
  32. package/dist/esm/help/initHelp.js +31 -0
  33. package/dist/esm/help/overridesHelp.d.ts +4 -0
  34. package/dist/esm/help/overridesHelp.js +32 -0
  35. package/dist/esm/help/releaseStepsHelp.d.ts +12 -0
  36. package/dist/esm/help/releaseStepsHelp.js +91 -0
  37. package/dist/esm/help/syncLabelsHelp.d.ts +8 -0
  38. package/dist/esm/help/syncLabelsHelp.js +55 -0
  39. package/dist/esm/help/usage.d.ts +2 -0
  40. package/dist/esm/help/usage.js +29 -0
  41. package/dist/esm/index.d.ts +1 -1
  42. package/dist/esm/index.js +1 -1
  43. package/dist/esm/loadChangelogOverrides.d.ts +31 -0
  44. package/dist/esm/loadChangelogOverrides.js +138 -0
  45. package/dist/esm/loadConfig.d.ts +0 -40
  46. package/dist/esm/loadConfig.js +0 -240
  47. package/dist/esm/loadValidatedConfig.d.ts +1 -1
  48. package/dist/esm/mergeConfig.d.ts +41 -0
  49. package/dist/esm/mergeConfig.js +118 -0
  50. package/dist/esm/monorepoConfigAssertions.d.ts +49 -0
  51. package/dist/esm/monorepoConfigAssertions.js +126 -0
  52. package/dist/esm/planChangelogFiles.d.ts +16 -0
  53. package/dist/esm/planChangelogFiles.js +41 -0
  54. package/dist/esm/prepareArgs.d.ts +55 -0
  55. package/dist/esm/prepareArgs.js +69 -0
  56. package/dist/esm/prepareCommand.d.ts +0 -48
  57. package/dist/esm/prepareCommand.js +6 -73
  58. package/dist/esm/prepareComposite.d.ts +16 -0
  59. package/dist/esm/prepareComposite.js +39 -0
  60. package/dist/esm/prepareRunContext.d.ts +38 -0
  61. package/dist/esm/prepareRunContext.js +39 -0
  62. package/dist/esm/prepareTarget.d.ts +2 -7
  63. package/dist/esm/prepareTarget.js +2 -118
  64. package/dist/esm/previewTagPrefixes.d.ts +1 -1
  65. package/dist/esm/releasePrepareTargets.js +13 -205
  66. package/dist/esm/reportPrepare.js +3 -125
  67. package/dist/esm/resolveCommandTags.d.ts +1 -1
  68. package/dist/esm/resolveCommandTags.js +2 -1
  69. package/dist/esm/resolveReleaseDeclaration.d.ts +2 -1
  70. package/dist/esm/resolveReleaseTargets.d.ts +2 -1
  71. package/dist/esm/resolveReleaseTargets.js +2 -1
  72. package/dist/esm/sync-labels/generateCommand.d.ts +1 -1
  73. package/dist/esm/sync-labels/resolveLabels.d.ts +1 -1
  74. package/dist/esm/topologicalSort.d.ts +12 -0
  75. package/dist/esm/topologicalSort.js +60 -0
  76. package/dist/esm/types.d.ts +1 -225
  77. package/dist/esm/types.js +1 -103
  78. package/dist/esm/validateAllChangelogOverrides.d.ts +56 -0
  79. package/dist/esm/validateAllChangelogOverrides.js +138 -0
  80. package/dist/esm/validateConfig.d.ts +1 -1
  81. package/dist/esm/validateConfig.js +7 -9
  82. package/dist/esm/validateOverridesCommand.d.ts +1 -1
  83. package/dist/esm/validateOverridesCommand.js +2 -1
  84. package/package.json +2 -2
@@ -10,10 +10,6 @@ import { existsSync, readFileSync } from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { pathToFileURL } from 'node:url';
12
12
  import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
13
- import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_PROJECT_TAG_PREFIX, DEFAULT_RELEASE_NOTES_CONFIG, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES, } from "./defaults.js";
14
- import { deriveDirectoryConfig } from "./deriveDirectoryConfig.js";
15
- import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
16
- import { getTargetName, normalizeWorkspacePath, resolveReleaseDeclaration, } from "./resolveReleaseDeclaration.js";
17
13
  import { isRecord } from "./typeGuards.js";
18
14
  export const ROOT_PACKAGE_JSON_PATH = 'package.json';
19
15
  export function readRootPackageVersion() {
@@ -56,239 +52,3 @@ export async function loadConfig(configPath) {
56
52
  }
57
53
  return resolved;
58
54
  }
59
- export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
60
- const discovered = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
61
- assertUniqueTagPrefixes(discovered);
62
- const { retained: workspaces, excluded, directories: declaredDirectories, composite, } = resolveReleaseDeclaration(discovered, userConfig);
63
- const directories = declaredDirectories.map((declared) => deriveDirectoryConfig(declared));
64
- for (const workspace of workspaces) {
65
- if (workspace.legacyIdentities !== undefined) {
66
- assertLegacyIdentityDoesNotMatchCurrent(workspace.dir, workspace.name, workspace.tagPrefix, workspace.legacyIdentities);
67
- }
68
- }
69
- if (userConfig?.retiredPackages !== undefined) {
70
- assertRetiredPackagesDoNotCollideWithActive(workspaces, userConfig.retiredPackages);
71
- }
72
- const project = resolveProjectConfig(composite, rootPackage, workspaces);
73
- const workTypes = resolveWorkTypes(userConfig?.workTypes);
74
- const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
75
- const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
76
- const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
77
- assertNoSharedVersionFiles(workspaces, directories, project);
78
- assertNoTagPrefixCollisions(workspaces, directories, userConfig?.retiredPackages, project);
79
- const result = {
80
- workspaces,
81
- workTypes,
82
- versionPatterns,
83
- changelogJson,
84
- releaseNotes,
85
- };
86
- if (excluded.length > 0) {
87
- result.excludedWorkspaces = excluded;
88
- }
89
- if (directories.length > 0) {
90
- result.directories = directories;
91
- }
92
- if (project !== undefined) {
93
- result.project = project;
94
- }
95
- applyOptionalPassthroughFields(result, userConfig);
96
- return result;
97
- }
98
- function applyOptionalPassthroughFields(result, userConfig) {
99
- if (userConfig?.changelogMarkdown !== undefined) {
100
- result.changelogMarkdown = userConfig.changelogMarkdown;
101
- }
102
- if (userConfig?.formatCommand !== undefined) {
103
- result.formatCommand = userConfig.formatCommand;
104
- }
105
- if (userConfig?.scopeAliases !== undefined) {
106
- result.scopeAliases = userConfig.scopeAliases;
107
- }
108
- if (userConfig?.breakingPolicies !== undefined) {
109
- result.breakingPolicies = userConfig.breakingPolicies;
110
- }
111
- }
112
- export function mergeSinglePackageConfig(userConfig) {
113
- if (userConfig?.project !== undefined) {
114
- throw new Error('project block is not supported in single-package mode');
115
- }
116
- if (userConfig?.releases !== undefined) {
117
- throw new Error('releases block is not supported in single-package mode');
118
- }
119
- const workTypes = resolveWorkTypes(userConfig?.workTypes);
120
- const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
121
- const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
122
- const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
123
- const result = {
124
- workTypes,
125
- versionPatterns,
126
- changelogJson,
127
- releaseNotes,
128
- };
129
- applyOptionalPassthroughFields(result, userConfig);
130
- return result;
131
- }
132
- export function resolveWorkTypes(userWorkTypes) {
133
- return userWorkTypes === undefined ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userWorkTypes };
134
- }
135
- function mergeChangelogJsonConfig(partial) {
136
- if (partial === undefined) {
137
- return { ...DEFAULT_CHANGELOG_JSON_CONFIG };
138
- }
139
- return {
140
- enabled: partial.enabled ?? DEFAULT_CHANGELOG_JSON_CONFIG.enabled,
141
- outputPath: partial.outputPath ?? DEFAULT_CHANGELOG_JSON_CONFIG.outputPath,
142
- devOnlySections: partial.devOnlySections ?? [...DEFAULT_CHANGELOG_JSON_CONFIG.devOnlySections],
143
- };
144
- }
145
- function assertLegacyIdentityDoesNotMatchCurrent(dir, currentName, currentTagPrefix, legacyIdentities) {
146
- for (const identity of legacyIdentities) {
147
- if (identity.name === currentName && identity.tagPrefix === currentTagPrefix) {
148
- throw new Error(`Workspace '${dir}': legacyIdentities must not match the current identity ` +
149
- `(name='${currentName}', tagPrefix='${currentTagPrefix}'). ` +
150
- 'The current identity is always searched; listing it again is a no-op.');
151
- }
152
- }
153
- }
154
- function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredPackages) {
155
- const workspaceByDerivedPrefix = new Map();
156
- for (const workspace of workspaces) {
157
- workspaceByDerivedPrefix.set(workspace.tagPrefix, workspace);
158
- }
159
- for (const retired of retiredPackages) {
160
- const active = workspaceByDerivedPrefix.get(retired.tagPrefix);
161
- if (active !== undefined) {
162
- throw new Error(`retiredPackages: tagPrefix '${retired.tagPrefix}' collides with active workspace '${active.dir}' ` +
163
- `(derived prefix '${active.tagPrefix}'). A retired package's tagPrefix cannot belong to an active workspace.`);
164
- }
165
- }
166
- }
167
- function resolveProjectConfig(composite, rootPackage, workspaces) {
168
- if (composite === undefined) {
169
- return undefined;
170
- }
171
- const subject = composite.name === undefined ? 'project block' : `composite release '${composite.name}'`;
172
- if (rootPackage === undefined || !rootPackage.exists) {
173
- throw new Error(`${subject} requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`);
174
- }
175
- if (rootPackage.version === undefined) {
176
- throw new Error(`${subject} requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to the root package.json`);
177
- }
178
- return {
179
- paths: composite.paths ?? workspaces.flatMap((workspace) => workspace.paths),
180
- tagPrefix: composite.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX,
181
- ...(composite.name !== undefined && { name: composite.name }),
182
- ...(composite.members !== undefined && { members: composite.members }),
183
- };
184
- }
185
- function assertNoSharedVersionFiles(workspaces, directories, project) {
186
- const owners = [
187
- ...directories.map((directory) => ({
188
- label: `directory release '${directory.name}'`,
189
- versionFiles: [directory.versionFile],
190
- })),
191
- ...workspaces.map((workspace) => ({
192
- label: `workspace '${getTargetName(workspace)}'`,
193
- versionFiles: workspace.packageFiles.map(normalizeWorkspacePath),
194
- })),
195
- ...(project === undefined
196
- ? []
197
- : [
198
- {
199
- label: project.name === undefined ? 'the project release' : `composite release '${project.name}'`,
200
- versionFiles: [ROOT_PACKAGE_JSON_PATH],
201
- },
202
- ]),
203
- ];
204
- for (const [index, directory] of directories.entries()) {
205
- const peer = owners.slice(index + 1).find((owner) => owner.versionFiles.includes(directory.versionFile));
206
- if (peer !== undefined) {
207
- throw new Error(`directory release '${directory.name}' and ${peer.label} share the version file '${directory.versionFile}', ` +
208
- "so each release would overwrite the other's version and changelog; declare only one of them");
209
- }
210
- }
211
- }
212
- function assertNoTagPrefixCollisions(workspaces, directories, retiredPackages, project) {
213
- const sources = [];
214
- for (const workspace of workspaces) {
215
- const owner = `ws:${workspace.dir}`;
216
- sources.push({ prefix: workspace.tagPrefix, label: `workspace '${workspace.dir}'`, owner });
217
- const legacyIdentities = workspace.legacyIdentities ?? [];
218
- for (const identity of legacyIdentities) {
219
- sources.push({
220
- prefix: identity.tagPrefix,
221
- label: `workspace '${workspace.dir}' legacyIdentities entry (name='${identity.name}')`,
222
- owner,
223
- });
224
- }
225
- }
226
- for (const directory of directories) {
227
- const owner = `dir:${directory.path}`;
228
- sources.push({ prefix: directory.tagPrefix, label: `directory release '${directory.name}'`, owner });
229
- const legacyIdentities = directory.legacyIdentities ?? [];
230
- for (const identity of legacyIdentities) {
231
- sources.push({
232
- prefix: identity.tagPrefix,
233
- label: `directory release '${directory.name}' legacyIdentities entry (name='${identity.name}')`,
234
- owner,
235
- });
236
- }
237
- }
238
- const retiredEntries = (retiredPackages ?? []).entries();
239
- for (const [index, retired] of retiredEntries) {
240
- sources.push({
241
- prefix: retired.tagPrefix,
242
- label: `retiredPackages entry (name='${retired.name}')`,
243
- owner: `retired:${index}`,
244
- });
245
- }
246
- if (project !== undefined) {
247
- sources.push({
248
- prefix: project.tagPrefix,
249
- label: project.name === undefined ? 'project' : `composite release '${project.name}'`,
250
- owner: 'project',
251
- });
252
- }
253
- for (let i = 0; i < sources.length; i++) {
254
- for (let j = i + 1; j < sources.length; j++) {
255
- const a = sources[i];
256
- const b = sources[j];
257
- if (a === undefined || b === undefined || a.owner === b.owner)
258
- continue;
259
- if (isPrefixCollision(a.prefix, b.prefix)) {
260
- throw new Error(`Tag prefix collision: '${a.prefix}' (${a.label}) and '${b.prefix}' (${b.label}). ` +
261
- 'One prefix is identical to or a strict prefix of the other; ' +
262
- 'a tag under the longer prefix can then match the shorter one and be counted among its releases.');
263
- }
264
- }
265
- }
266
- }
267
- function isPrefixCollision(a, b) {
268
- return a === b || a.startsWith(b) || b.startsWith(a);
269
- }
270
- function assertUniqueTagPrefixes(workspaces) {
271
- const pathsByPrefix = new Map();
272
- for (const workspace of workspaces) {
273
- const existing = pathsByPrefix.get(workspace.tagPrefix);
274
- if (existing === undefined) {
275
- pathsByPrefix.set(workspace.tagPrefix, [workspace.workspacePath]);
276
- }
277
- else {
278
- existing.push(workspace.workspacePath);
279
- }
280
- }
281
- for (const [prefix, paths] of pathsByPrefix) {
282
- if (paths.length > 1) {
283
- throw new Error(`Duplicate tag prefix '${prefix}' for workspaces: ${paths.join(', ')}`);
284
- }
285
- }
286
- }
287
- function mergeReleaseNotesConfig(partial) {
288
- if (partial === undefined) {
289
- return { ...DEFAULT_RELEASE_NOTES_CONFIG };
290
- }
291
- return {
292
- shouldInjectIntoReadme: partial.shouldInjectIntoReadme ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldInjectIntoReadme,
293
- };
294
- }
@@ -1,5 +1,5 @@
1
1
  import { type OutputStyle } from '@williamthorsen/nmr-core';
2
- import type { ReleaseKitConfig } from './types.js';
2
+ import type { ReleaseKitConfig } from './configSchema.js';
3
3
  /**
4
4
  * Why a config file could not be used: The import threw, or the contents failed the schema.
5
5
  *
@@ -0,0 +1,41 @@
1
+ import type { ReleaseKitConfig } from './configSchema.js';
2
+ import type { MonorepoReleaseConfig, ReleaseSettings, WorkTypeConfig } from './types.js';
3
+ /**
4
+ * Resolves a final monorepo config from discovered workspaces and an optional user config overlay.
5
+ *
6
+ * Merging rules:
7
+ * - Release targets: `resolveReleaseDeclaration` projects the `releases` block, or the `workspaces` overrides and the
8
+ * `project` block, onto the discovered workspaces. Excluded workspaces move to `excludedWorkspaces`, and the
9
+ * directories that `releases` declares resolve through `deriveDirectoryConfig` into `directories`, which must not
10
+ * share a version file with any other target.
11
+ * - `workTypes`: Shallow merge; consumer entries override or add to defaults by key.
12
+ * - `versionPatterns`: Consumer value replaces defaults entirely.
13
+ * - Pass-through fields (see `applyOptionalPassthroughFields`): Consumer value wins.
14
+ * - `project`: Present iff the config declares a composite. Resolves `tagPrefix` to `DEFAULT_PROJECT_TAG_PREFIX` and
15
+ * `paths` to the union of its members' `paths` when omitted. Requires `rootPackage` to be passed and to contain a
16
+ * valid `version` field; throws otherwise. The resolved prefix is included in the strict-prefix collision check
17
+ * across all workspace and retired-package prefixes.
18
+ */
19
+ export declare function mergeMonorepoConfig(discoveredPaths: string[], userConfig: ReleaseKitConfig | undefined, rootPackage?: RootPackageInfo): MonorepoReleaseConfig;
20
+ /**
21
+ * Resolves the release settings of a single-package repo from an optional user config overlay.
22
+ *
23
+ * Rejects a configured `project` or `releases` block: Both declare the targets of a monorepo, and a single-package
24
+ * repo has one target, its root package.
25
+ */
26
+ export declare function mergeSinglePackageConfig(userConfig: ReleaseKitConfig | undefined): ReleaseSettings;
27
+ /**
28
+ * Merges consumer work-type overrides onto `DEFAULT_WORK_TYPES`.
29
+ *
30
+ * Preserves the declaration order of defaults; net-new consumer keys append at the end.
31
+ */
32
+ export declare function resolveWorkTypes(userWorkTypes?: ReleaseKitConfig['workTypes']): Record<string, WorkTypeConfig>;
33
+ /**
34
+ * Information about the root `package.json` passed into `mergeMonorepoConfig` when a
35
+ * `project` block is configured. `readRootPackageVersion` performs the read, so that
36
+ * `mergeMonorepoConfig` does not perform any I/O.
37
+ */
38
+ export interface RootPackageInfo {
39
+ exists: boolean;
40
+ version: string | undefined;
41
+ }
@@ -0,0 +1,118 @@
1
+ import { DEFAULT_CHANGELOG_JSON_CONFIG, DEFAULT_PROJECT_TAG_PREFIX, DEFAULT_RELEASE_NOTES_CONFIG, DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES, } from "./defaults.js";
2
+ import { deriveDirectoryConfig } from "./deriveDirectoryConfig.js";
3
+ import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
4
+ import { ROOT_PACKAGE_JSON_PATH } from "./loadConfig.js";
5
+ import { assertLegacyIdentityDoesNotMatchCurrent, assertNoSharedVersionFiles, assertNoTagPrefixCollisions, assertRetiredPackagesDoNotCollideWithActive, assertUniqueTagPrefixes, } from "./monorepoConfigAssertions.js";
6
+ import { resolveReleaseDeclaration } from "./resolveReleaseDeclaration.js";
7
+ export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
8
+ const discovered = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
9
+ assertUniqueTagPrefixes(discovered);
10
+ const { retained: workspaces, excluded, directories: declaredDirectories, composite, } = resolveReleaseDeclaration(discovered, userConfig);
11
+ const directories = declaredDirectories.map((declared) => deriveDirectoryConfig(declared));
12
+ for (const workspace of workspaces) {
13
+ if (workspace.legacyIdentities !== undefined) {
14
+ assertLegacyIdentityDoesNotMatchCurrent(workspace.dir, workspace.name, workspace.tagPrefix, workspace.legacyIdentities);
15
+ }
16
+ }
17
+ if (userConfig?.retiredPackages !== undefined) {
18
+ assertRetiredPackagesDoNotCollideWithActive(workspaces, userConfig.retiredPackages);
19
+ }
20
+ const project = resolveProjectConfig(composite, rootPackage, workspaces);
21
+ const workTypes = resolveWorkTypes(userConfig?.workTypes);
22
+ const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
23
+ const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
24
+ const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
25
+ assertNoSharedVersionFiles(workspaces, directories, project);
26
+ assertNoTagPrefixCollisions(workspaces, directories, userConfig?.retiredPackages, project);
27
+ const result = {
28
+ workspaces,
29
+ workTypes,
30
+ versionPatterns,
31
+ changelogJson,
32
+ releaseNotes,
33
+ };
34
+ if (excluded.length > 0) {
35
+ result.excludedWorkspaces = excluded;
36
+ }
37
+ if (directories.length > 0) {
38
+ result.directories = directories;
39
+ }
40
+ if (project !== undefined) {
41
+ result.project = project;
42
+ }
43
+ applyOptionalPassthroughFields(result, userConfig);
44
+ return result;
45
+ }
46
+ export function mergeSinglePackageConfig(userConfig) {
47
+ if (userConfig?.project !== undefined) {
48
+ throw new Error('project block is not supported in single-package mode');
49
+ }
50
+ if (userConfig?.releases !== undefined) {
51
+ throw new Error('releases block is not supported in single-package mode');
52
+ }
53
+ const workTypes = resolveWorkTypes(userConfig?.workTypes);
54
+ const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
55
+ const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
56
+ const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
57
+ const result = {
58
+ workTypes,
59
+ versionPatterns,
60
+ changelogJson,
61
+ releaseNotes,
62
+ };
63
+ applyOptionalPassthroughFields(result, userConfig);
64
+ return result;
65
+ }
66
+ export function resolveWorkTypes(userWorkTypes) {
67
+ return userWorkTypes === undefined ? { ...DEFAULT_WORK_TYPES } : { ...DEFAULT_WORK_TYPES, ...userWorkTypes };
68
+ }
69
+ function applyOptionalPassthroughFields(result, userConfig) {
70
+ if (userConfig?.changelogMarkdown !== undefined) {
71
+ result.changelogMarkdown = userConfig.changelogMarkdown;
72
+ }
73
+ if (userConfig?.formatCommand !== undefined) {
74
+ result.formatCommand = userConfig.formatCommand;
75
+ }
76
+ if (userConfig?.scopeAliases !== undefined) {
77
+ result.scopeAliases = userConfig.scopeAliases;
78
+ }
79
+ if (userConfig?.breakingPolicies !== undefined) {
80
+ result.breakingPolicies = userConfig.breakingPolicies;
81
+ }
82
+ }
83
+ function mergeChangelogJsonConfig(partial) {
84
+ if (partial === undefined) {
85
+ return { ...DEFAULT_CHANGELOG_JSON_CONFIG };
86
+ }
87
+ return {
88
+ enabled: partial.enabled ?? DEFAULT_CHANGELOG_JSON_CONFIG.enabled,
89
+ outputPath: partial.outputPath ?? DEFAULT_CHANGELOG_JSON_CONFIG.outputPath,
90
+ devOnlySections: partial.devOnlySections ?? [...DEFAULT_CHANGELOG_JSON_CONFIG.devOnlySections],
91
+ };
92
+ }
93
+ function mergeReleaseNotesConfig(partial) {
94
+ if (partial === undefined) {
95
+ return { ...DEFAULT_RELEASE_NOTES_CONFIG };
96
+ }
97
+ return {
98
+ shouldInjectIntoReadme: partial.shouldInjectIntoReadme ?? DEFAULT_RELEASE_NOTES_CONFIG.shouldInjectIntoReadme,
99
+ };
100
+ }
101
+ function resolveProjectConfig(composite, rootPackage, workspaces) {
102
+ if (composite === undefined) {
103
+ return undefined;
104
+ }
105
+ const subject = composite.name === undefined ? 'project block' : `composite release '${composite.name}'`;
106
+ if (rootPackage === undefined || !rootPackage.exists) {
107
+ throw new Error(`${subject} requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`);
108
+ }
109
+ if (rootPackage.version === undefined) {
110
+ throw new Error(`${subject} requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to the root package.json`);
111
+ }
112
+ return {
113
+ paths: composite.paths ?? workspaces.flatMap((workspace) => workspace.paths),
114
+ tagPrefix: composite.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX,
115
+ ...(composite.name !== undefined && { name: composite.name }),
116
+ ...(composite.members !== undefined && { members: composite.members }),
117
+ };
118
+ }
@@ -0,0 +1,49 @@
1
+ import type { LegacyIdentity, RetiredPackage } from './configSchema.js';
2
+ import type { DirectoryConfig, ResolvedProjectConfig, WorkspaceConfig } from './types.js';
3
+ /**
4
+ * Throws when a workspace's `legacyIdentities` contains its current identity.
5
+ *
6
+ * An identity whose full `(name, tagPrefix)` tuple equals the current workspace's
7
+ * `(name, tagPrefix)` is a guaranteed no-op duplicate, almost always a copy-paste mistake.
8
+ * An entry whose `tagPrefix` matches but whose `name` differs is valid: It documents a prior
9
+ * rename that reused the same tag shape.
10
+ */
11
+ export declare function assertLegacyIdentityDoesNotMatchCurrent(dir: string, currentName: string, currentTagPrefix: string, legacyIdentities: readonly LegacyIdentity[]): void;
12
+ /**
13
+ * Throws when a directory target shares its version file with another target, whose release would bump the same
14
+ * `version` and write the same `CHANGELOG.md`: the repo root beside a composite, which the root `package.json` also
15
+ * versions. Pairs of other targets keep the behavior that they had before directories existed, so the check covers
16
+ * only pairs that include a directory.
17
+ */
18
+ export declare function assertNoSharedVersionFiles(workspaces: readonly WorkspaceConfig[], directories: readonly DirectoryConfig[], project: ResolvedProjectConfig | undefined): void;
19
+ /**
20
+ * Throws when any pair of tag prefixes from distinct owners is identical or one is a strict
21
+ * prefix of the other.
22
+ *
23
+ * A tag matches a prefix when the rest of its name is a complete SemVer version, so a prefix
24
+ * `'pkg-'` matches `'pkg-1.0.0-v1.2.3'` through its pre-release suffix. The owners are each
25
+ * workspace, each directory, each retired package, and the project. A workspace or a directory owns
26
+ * both its current prefix and its legacy-identity prefixes, because a prior identity may reuse the
27
+ * current tag shape under a different name.
28
+ */
29
+ export declare function assertNoTagPrefixCollisions(workspaces: readonly WorkspaceConfig[], directories: readonly DirectoryConfig[], retiredPackages: readonly RetiredPackage[] | undefined, project: ResolvedProjectConfig | undefined): void;
30
+ /**
31
+ * Throws when a retired package's `tagPrefix` matches an active workspace's derived prefix.
32
+ *
33
+ * A retired package is, by definition, no longer hosted by any active workspace in this repo.
34
+ * If its declared `tagPrefix` equals an active workspace's derived prefix, new tags from that
35
+ * workspace would collide with the retired package's historical tags.
36
+ */
37
+ export declare function assertRetiredPackagesDoNotCollideWithActive(workspaces: readonly WorkspaceConfig[], retiredPackages: readonly RetiredPackage[]): void;
38
+ /**
39
+ * Throws when two or more workspaces share the same `tagPrefix`.
40
+ *
41
+ * A collision means two workspaces would produce indistinguishable tags, breaking both tag
42
+ * creation and tag resolution. The error lists every colliding workspace path so that the author
43
+ * can rename one of the conflicting `package.json` `name` fields.
44
+ *
45
+ * This guards the pre-merge state when only workspaces have been derived. The broader
46
+ * strict-prefix check across legacy, retired, and project prefixes runs later in
47
+ * `assertNoTagPrefixCollisions`.
48
+ */
49
+ export declare function assertUniqueTagPrefixes(workspaces: readonly WorkspaceConfig[]): void;
@@ -0,0 +1,126 @@
1
+ import { ROOT_PACKAGE_JSON_PATH } from "./loadConfig.js";
2
+ import { getTargetName, normalizeWorkspacePath } from "./resolveReleaseDeclaration.js";
3
+ export function assertLegacyIdentityDoesNotMatchCurrent(dir, currentName, currentTagPrefix, legacyIdentities) {
4
+ for (const identity of legacyIdentities) {
5
+ if (identity.name === currentName && identity.tagPrefix === currentTagPrefix) {
6
+ throw new Error(`Workspace '${dir}': legacyIdentities must not match the current identity ` +
7
+ `(name='${currentName}', tagPrefix='${currentTagPrefix}'). ` +
8
+ 'The current identity is always searched; listing it again is a no-op.');
9
+ }
10
+ }
11
+ }
12
+ export function assertNoSharedVersionFiles(workspaces, directories, project) {
13
+ const owners = [
14
+ ...directories.map((directory) => ({
15
+ label: `directory release '${directory.name}'`,
16
+ versionFiles: [directory.versionFile],
17
+ })),
18
+ ...workspaces.map((workspace) => ({
19
+ label: `workspace '${getTargetName(workspace)}'`,
20
+ versionFiles: workspace.packageFiles.map(normalizeWorkspacePath),
21
+ })),
22
+ ...(project === undefined
23
+ ? []
24
+ : [
25
+ {
26
+ label: project.name === undefined ? 'the project release' : `composite release '${project.name}'`,
27
+ versionFiles: [ROOT_PACKAGE_JSON_PATH],
28
+ },
29
+ ]),
30
+ ];
31
+ for (const [index, directory] of directories.entries()) {
32
+ const peer = owners.slice(index + 1).find((owner) => owner.versionFiles.includes(directory.versionFile));
33
+ if (peer !== undefined) {
34
+ throw new Error(`directory release '${directory.name}' and ${peer.label} share the version file '${directory.versionFile}', ` +
35
+ "so each release would overwrite the other's version and changelog; declare only one of them");
36
+ }
37
+ }
38
+ }
39
+ export function assertNoTagPrefixCollisions(workspaces, directories, retiredPackages, project) {
40
+ const sources = [];
41
+ for (const workspace of workspaces) {
42
+ const owner = `ws:${workspace.dir}`;
43
+ sources.push({ prefix: workspace.tagPrefix, label: `workspace '${workspace.dir}'`, owner });
44
+ const legacyIdentities = workspace.legacyIdentities ?? [];
45
+ for (const identity of legacyIdentities) {
46
+ sources.push({
47
+ prefix: identity.tagPrefix,
48
+ label: `workspace '${workspace.dir}' legacyIdentities entry (name='${identity.name}')`,
49
+ owner,
50
+ });
51
+ }
52
+ }
53
+ for (const directory of directories) {
54
+ const owner = `dir:${directory.path}`;
55
+ sources.push({ prefix: directory.tagPrefix, label: `directory release '${directory.name}'`, owner });
56
+ const legacyIdentities = directory.legacyIdentities ?? [];
57
+ for (const identity of legacyIdentities) {
58
+ sources.push({
59
+ prefix: identity.tagPrefix,
60
+ label: `directory release '${directory.name}' legacyIdentities entry (name='${identity.name}')`,
61
+ owner,
62
+ });
63
+ }
64
+ }
65
+ const retiredEntries = (retiredPackages ?? []).entries();
66
+ for (const [index, retired] of retiredEntries) {
67
+ sources.push({
68
+ prefix: retired.tagPrefix,
69
+ label: `retiredPackages entry (name='${retired.name}')`,
70
+ owner: `retired:${index}`,
71
+ });
72
+ }
73
+ if (project !== undefined) {
74
+ sources.push({
75
+ prefix: project.tagPrefix,
76
+ label: project.name === undefined ? 'project' : `composite release '${project.name}'`,
77
+ owner: 'project',
78
+ });
79
+ }
80
+ for (let i = 0; i < sources.length; i++) {
81
+ for (let j = i + 1; j < sources.length; j++) {
82
+ const a = sources[i];
83
+ const b = sources[j];
84
+ if (a === undefined || b === undefined || a.owner === b.owner)
85
+ continue;
86
+ if (isPrefixCollision(a.prefix, b.prefix)) {
87
+ throw new Error(`Tag prefix collision: '${a.prefix}' (${a.label}) and '${b.prefix}' (${b.label}). ` +
88
+ 'One prefix is identical to or a strict prefix of the other; ' +
89
+ 'a tag under the longer prefix can then match the shorter one and be counted among its releases.');
90
+ }
91
+ }
92
+ }
93
+ }
94
+ export function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredPackages) {
95
+ const workspaceByDerivedPrefix = new Map();
96
+ for (const workspace of workspaces) {
97
+ workspaceByDerivedPrefix.set(workspace.tagPrefix, workspace);
98
+ }
99
+ for (const retired of retiredPackages) {
100
+ const active = workspaceByDerivedPrefix.get(retired.tagPrefix);
101
+ if (active !== undefined) {
102
+ throw new Error(`retiredPackages: tagPrefix '${retired.tagPrefix}' collides with active workspace '${active.dir}' ` +
103
+ `(derived prefix '${active.tagPrefix}'). A retired package's tagPrefix cannot belong to an active workspace.`);
104
+ }
105
+ }
106
+ }
107
+ export function assertUniqueTagPrefixes(workspaces) {
108
+ const pathsByPrefix = new Map();
109
+ for (const workspace of workspaces) {
110
+ const existing = pathsByPrefix.get(workspace.tagPrefix);
111
+ if (existing === undefined) {
112
+ pathsByPrefix.set(workspace.tagPrefix, [workspace.workspacePath]);
113
+ }
114
+ else {
115
+ existing.push(workspace.workspacePath);
116
+ }
117
+ }
118
+ for (const [prefix, paths] of pathsByPrefix) {
119
+ if (paths.length > 1) {
120
+ throw new Error(`Duplicate tag prefix '${prefix}' for workspaces: ${paths.join(', ')}`);
121
+ }
122
+ }
123
+ }
124
+ function isPrefixCollision(a, b) {
125
+ return a === b || a.startsWith(b) || b.startsWith(a);
126
+ }
@@ -0,0 +1,16 @@
1
+ import type { PlannedWrite } from './releasePlan.js';
2
+ import type { ChangelogEntry, ChangelogPreservation, ReleaseSettings } from './types.js';
3
+ /**
4
+ * Plans the `changelog.json` (when enabled), `CHANGELOG.md`, and archive files of one changelog directory from the new
5
+ * entries merged with the entries on disk, so that the markdown includes prior releases whether or not the JSON is
6
+ * written. Also plans the removal of every existing archive file that the render does not produce.
7
+ */
8
+ export declare function planChangelogFiles(changelogDir: string, newEntries: ChangelogEntry[], settings: Pick<ReleaseSettings, 'changelogJson' | 'changelogMarkdown'>, sectionOrder: string[]): {
9
+ /** The `CHANGELOG.md`, then its archive files. */
10
+ changelogFiles: string[];
11
+ changelogJsonFiles: string[];
12
+ entries: ChangelogEntry[];
13
+ preservation: ChangelogPreservation | undefined;
14
+ removals: string[];
15
+ writes: PlannedWrite[];
16
+ };
@@ -0,0 +1,41 @@
1
+ import { join as joinPath } from 'node:path';
2
+ import { describeArchivePath, listArchiveFiles } from "./changelogArchive.js";
3
+ import { mergeChangelogEntriesWithDisk, renderChangelogJson, resolveChangelogJsonPath } from "./changelogJsonFile.js";
4
+ import { planPreservedSections } from "./readChangelogSections.js";
5
+ import { renderChangelogFiles } from "./renderChangelogMarkdown.js";
6
+ export function planChangelogFiles(changelogDir, newEntries, settings, sectionOrder) {
7
+ const jsonPath = resolveChangelogJsonPath(settings, changelogDir);
8
+ const entries = mergeChangelogEntriesWithDisk(jsonPath, newEntries);
9
+ const writes = [];
10
+ const changelogJsonFiles = [];
11
+ if (settings.changelogJson.enabled) {
12
+ writes.push({ path: jsonPath, content: renderChangelogJson(entries) });
13
+ changelogJsonFiles.push(jsonPath);
14
+ }
15
+ const changelogFile = joinPath(changelogDir, 'CHANGELOG.md');
16
+ const preserved = planPreservedSections(changelogDir, entries);
17
+ const keepVersions = settings.changelogMarkdown?.keepVersions;
18
+ const rendered = renderChangelogFiles(entries, {
19
+ sectionOrder,
20
+ preservedSections: preserved.sections,
21
+ ...(keepVersions !== undefined && { keepVersions }),
22
+ });
23
+ writes.push({ path: changelogFile, content: rendered.main });
24
+ const archiveFiles = [];
25
+ for (const { major, content } of rendered.archives) {
26
+ const path = joinPath(changelogDir, describeArchivePath(major));
27
+ writes.push({ path, content });
28
+ archiveFiles.push(path);
29
+ }
30
+ const removals = listArchiveFiles(changelogDir)
31
+ .map((archive) => archive.path)
32
+ .filter((path) => !archiveFiles.includes(path));
33
+ return {
34
+ changelogFiles: [changelogFile, ...archiveFiles],
35
+ changelogJsonFiles,
36
+ entries,
37
+ preservation: preserved.preservation,
38
+ removals,
39
+ writes,
40
+ };
41
+ }