@williamthorsen/release-kit 14.1.0 → 14.2.1

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 (92) hide show
  1. package/.meta/changelog.json +136 -0
  2. package/.readyup/kits/default.js +63 -6
  3. package/.readyup/kits/npm-auto-publish.js +47 -35
  4. package/.readyup/manifest.json +12 -7
  5. package/CHANGELOG.md +39 -0
  6. package/README.md +2 -16
  7. package/dist/esm/assertTaggedBaseline.d.ts +2 -2
  8. package/dist/esm/attachChangelogDiagnostics.d.ts +2 -2
  9. package/dist/esm/bin/release-kit.js +5 -4
  10. package/dist/esm/buildChangelogEntries.d.ts +3 -3
  11. package/dist/esm/buildChangelogEntries.js +3 -3
  12. package/dist/esm/buildDependencyGraph.d.ts +11 -11
  13. package/dist/esm/buildDependencyGraph.js +13 -13
  14. package/dist/esm/buildReleaseSummary.d.ts +3 -3
  15. package/dist/esm/buildReleaseSummary.js +4 -12
  16. package/dist/esm/changelogArchive.d.ts +14 -0
  17. package/dist/esm/changelogArchive.js +21 -0
  18. package/dist/esm/changelogJsonFile.d.ts +2 -2
  19. package/dist/esm/changelogOverrides.d.ts +9 -8
  20. package/dist/esm/changelogOverrides.js +3 -6
  21. package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
  22. package/dist/esm/createGithubReleaseCommand.js +7 -4
  23. package/dist/esm/deriveDirectoryConfig.d.ts +13 -0
  24. package/dist/esm/deriveDirectoryConfig.js +59 -0
  25. package/dist/esm/deriveWorkspaceConfig.js +0 -1
  26. package/dist/esm/discoverWorkspaces.d.ts +3 -3
  27. package/dist/esm/findTransitiveDependents.js +3 -3
  28. package/dist/esm/findUnroutedEntryScopes.d.ts +2 -2
  29. package/dist/esm/formatDirectoryPublishSkip.d.ts +3 -0
  30. package/dist/esm/formatDirectoryPublishSkip.js +3 -0
  31. package/dist/esm/formatExcludedSkip.d.ts +5 -2
  32. package/dist/esm/formatExcludedSkip.js +2 -2
  33. package/dist/esm/formatProjectPublishSkip.d.ts +3 -0
  34. package/dist/esm/formatProjectPublishSkip.js +3 -0
  35. package/dist/esm/generateChangelogs.d.ts +7 -7
  36. package/dist/esm/generateChangelogs.js +2 -2
  37. package/dist/esm/help/prepareHelp.d.ts +1 -1
  38. package/dist/esm/help/prepareHelp.js +7 -5
  39. package/dist/esm/init/templates.js +1 -1
  40. package/dist/esm/loadConfig.d.ts +12 -12
  41. package/dist/esm/loadConfig.js +79 -30
  42. package/dist/esm/prepareCommand.d.ts +1 -1
  43. package/dist/esm/prepareCommand.js +47 -46
  44. package/dist/esm/prepareTarget.d.ts +103 -0
  45. package/dist/esm/prepareTarget.js +274 -0
  46. package/dist/esm/previewTagPrefixes.d.ts +5 -3
  47. package/dist/esm/previewTagPrefixes.js +58 -13
  48. package/dist/esm/propagateBumps.js +1 -1
  49. package/dist/esm/publishCommand.js +13 -1
  50. package/dist/esm/readChangelogSections.d.ts +6 -4
  51. package/dist/esm/readChangelogSections.js +18 -4
  52. package/dist/esm/releasePlan.d.ts +4 -2
  53. package/dist/esm/releasePlan.js +34 -12
  54. package/dist/esm/releasePrepareTargets.d.ts +47 -0
  55. package/dist/esm/releasePrepareTargets.js +335 -0
  56. package/dist/esm/renderChangelogMarkdown.d.ts +23 -0
  57. package/dist/esm/renderChangelogMarkdown.js +47 -11
  58. package/dist/esm/reportPrepare.d.ts +2 -3
  59. package/dist/esm/reportPrepare.js +24 -12
  60. package/dist/esm/resolveCommandTags.d.ts +3 -1
  61. package/dist/esm/resolveCommandTags.js +15 -22
  62. package/dist/esm/resolveReleaseDeclaration.d.ts +45 -0
  63. package/dist/esm/resolveReleaseDeclaration.js +153 -0
  64. package/dist/esm/resolveReleaseTags.d.ts +14 -3
  65. package/dist/esm/resolveReleaseTags.js +36 -15
  66. package/dist/esm/resolveReleaseTargets.d.ts +29 -0
  67. package/dist/esm/resolveReleaseTargets.js +131 -0
  68. package/dist/esm/sync-labels/generateCommand.js +2 -1
  69. package/dist/esm/sync-labels/resolveLabels.d.ts +4 -3
  70. package/dist/esm/sync-labels/resolveLabels.js +30 -1
  71. package/dist/esm/sync-labels/types.d.ts +2 -0
  72. package/dist/esm/types.d.ts +251 -167
  73. package/dist/esm/types.js +22 -0
  74. package/dist/esm/validateConfig.js +80 -21
  75. package/dist/esm/validateOverridesCommand.d.ts +1 -1
  76. package/dist/esm/validateOverridesCommand.js +17 -28
  77. package/docs/api.md +25 -58
  78. package/docs/changelogs.md +26 -6
  79. package/docs/configuration.md +79 -16
  80. package/docs/labels.md +25 -2
  81. package/docs/project-releases.md +42 -2
  82. package/docs/publishing.md +3 -3
  83. package/docs/releasing.md +4 -4
  84. package/docs/work-types.md +1 -1
  85. package/package.json +3 -3
  86. package/presets/labels/common.yaml +11 -0
  87. package/dist/esm/releasePrepare.d.ts +0 -36
  88. package/dist/esm/releasePrepare.js +0 -230
  89. package/dist/esm/releasePrepareMono.d.ts +0 -29
  90. package/dist/esm/releasePrepareMono.js +0 -503
  91. package/dist/esm/releasePrepareProject.d.ts +0 -47
  92. package/dist/esm/releasePrepareProject.js +0 -164
@@ -1,4 +1,4 @@
1
- import type { MonorepoReleaseConfig, ReleaseConfig, ReleaseKitConfig, WorkTypeConfig } from './types.js';
1
+ import type { MonorepoReleaseConfig, ReleaseKitConfig, ReleaseSettings, WorkTypeConfig } from './types.js';
2
2
  /** Path of the root `package.json` consulted when validating a `project` block. */
3
3
  export declare const ROOT_PACKAGE_JSON_PATH = "package.json";
4
4
  /**
@@ -38,26 +38,26 @@ export interface RootPackageInfo {
38
38
  * Resolves a final monorepo config from discovered workspaces and an optional user config overlay.
39
39
  *
40
40
  * Merging rules:
41
- * - `workspaces`: Match overlay entries by `dir` against discovered list; `shouldExclude: true`
42
- * removes the workspace; unlisted packages keep defaults.
41
+ * - Release targets: `resolveReleaseDeclaration` projects the `releases` block, or the `workspaces` overrides and the
42
+ * `project` block, onto the discovered workspaces. Excluded workspaces move to `excludedWorkspaces`, and the
43
+ * directories that `releases` declares resolve through `deriveDirectoryConfig` into `directories`, which must not
44
+ * share a version file with any other target.
43
45
  * - `workTypes`: Shallow merge; consumer entries override or add to defaults by key.
44
46
  * - `versionPatterns`: Consumer value replaces defaults entirely.
45
47
  * - Pass-through fields (see `applyOptionalPassthroughFields`): Consumer value wins.
46
- * - `project`: Present iff `userConfig.project` is declared. Resolves `tagPrefix` to
47
- * `DEFAULT_PROJECT_TAG_PREFIX` and `paths` to the union of the retained workspaces' `paths`
48
- * when omitted. Requires `rootPackage` to be passed and to contain a valid `version` field;
49
- * throws otherwise. The resolved prefix is included in the strict-prefix collision check
48
+ * - `project`: Present iff the config declares a composite. Resolves `tagPrefix` to `DEFAULT_PROJECT_TAG_PREFIX` and
49
+ * `paths` to the union of its members' `paths` when omitted. Requires `rootPackage` to be passed and to contain a
50
+ * valid `version` field; throws otherwise. The resolved prefix is included in the strict-prefix collision check
50
51
  * across all workspace and retired-package prefixes.
51
52
  */
52
53
  export declare function mergeMonorepoConfig(discoveredPaths: string[], userConfig: ReleaseKitConfig | undefined, rootPackage?: RootPackageInfo): MonorepoReleaseConfig;
53
54
  /**
54
- * Resolves a final single-package config from an optional user config overlay.
55
+ * Resolves the release settings of a single-package repo from an optional user config overlay.
55
56
  *
56
- * Rejects a configured `project` block: Project-level releases are a monorepo-only feature,
57
- * since the implicit "all non-excluded workspaces contribute" rule is meaningless in a
58
- * single-package repo.
57
+ * Rejects a configured `project` or `releases` block: Both declare the targets of a monorepo, and a single-package
58
+ * repo has one target, its root package.
59
59
  */
60
- export declare function mergeSinglePackageConfig(userConfig: ReleaseKitConfig | undefined): ReleaseConfig;
60
+ export declare function mergeSinglePackageConfig(userConfig: ReleaseKitConfig | undefined): ReleaseSettings;
61
61
  /**
62
62
  * Merges consumer work-type overrides onto `DEFAULT_WORK_TYPES`.
63
63
  *
@@ -11,7 +11,9 @@ import path from 'node:path';
11
11
  import { pathToFileURL } from 'node:url';
12
12
  import { chainError } from '@williamthorsen/toolbelt.errors/candidate';
13
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";
14
15
  import { deriveWorkspaceConfig } from "./deriveWorkspaceConfig.js";
16
+ import { getTargetName, normalizeWorkspacePath, resolveReleaseDeclaration, } from "./resolveReleaseDeclaration.js";
15
17
  import { isRecord } from "./typeGuards.js";
16
18
  export const ROOT_PACKAGE_JSON_PATH = 'package.json';
17
19
  export function readRootPackageVersion() {
@@ -55,33 +57,25 @@ export async function loadConfig(configPath) {
55
57
  return resolved;
56
58
  }
57
59
  export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
58
- let workspaces = discoveredPaths.map((workspacePath) => deriveWorkspaceConfig(workspacePath));
59
- assertUniqueTagPrefixes(workspaces);
60
- if (userConfig?.workspaces !== undefined) {
61
- const overrides = new Map(userConfig.workspaces.map((w) => [w.dir, w]));
62
- workspaces = workspaces
63
- .filter((w) => {
64
- const override = overrides.get(w.dir);
65
- return override?.shouldExclude !== true;
66
- })
67
- .map((w) => {
68
- const override = overrides.get(w.dir);
69
- if (override?.legacyIdentities === undefined) {
70
- return w;
71
- }
72
- assertLegacyIdentityDoesNotMatchCurrent(w.dir, w.name, w.tagPrefix, override.legacyIdentities);
73
- return { ...w, legacyIdentities: override.legacyIdentities.map((identity) => ({ ...identity })) };
74
- });
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
+ }
75
68
  }
76
69
  if (userConfig?.retiredPackages !== undefined) {
77
70
  assertRetiredPackagesDoNotCollideWithActive(workspaces, userConfig.retiredPackages);
78
71
  }
79
- const project = resolveProjectConfig(userConfig?.project, rootPackage, workspaces);
72
+ const project = resolveProjectConfig(composite, rootPackage, workspaces);
80
73
  const workTypes = resolveWorkTypes(userConfig?.workTypes);
81
74
  const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
82
75
  const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
83
76
  const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
84
- assertNoTagPrefixCollisions(workspaces, userConfig?.retiredPackages, project);
77
+ assertNoSharedVersionFiles(workspaces, directories, project);
78
+ assertNoTagPrefixCollisions(workspaces, directories, userConfig?.retiredPackages, project);
85
79
  const result = {
86
80
  workspaces,
87
81
  workTypes,
@@ -89,6 +83,12 @@ export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
89
83
  changelogJson,
90
84
  releaseNotes,
91
85
  };
86
+ if (excluded.length > 0) {
87
+ result.excludedWorkspaces = excluded;
88
+ }
89
+ if (directories.length > 0) {
90
+ result.directories = directories;
91
+ }
92
92
  if (project !== undefined) {
93
93
  result.project = project;
94
94
  }
@@ -96,6 +96,9 @@ export function mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage) {
96
96
  return result;
97
97
  }
98
98
  function applyOptionalPassthroughFields(result, userConfig) {
99
+ if (userConfig?.changelogMarkdown !== undefined) {
100
+ result.changelogMarkdown = userConfig.changelogMarkdown;
101
+ }
99
102
  if (userConfig?.formatCommand !== undefined) {
100
103
  result.formatCommand = userConfig.formatCommand;
101
104
  }
@@ -110,14 +113,14 @@ export function mergeSinglePackageConfig(userConfig) {
110
113
  if (userConfig?.project !== undefined) {
111
114
  throw new Error('project block is not supported in single-package mode');
112
115
  }
116
+ if (userConfig?.releases !== undefined) {
117
+ throw new Error('releases block is not supported in single-package mode');
118
+ }
113
119
  const workTypes = resolveWorkTypes(userConfig?.workTypes);
114
120
  const versionPatterns = userConfig?.versionPatterns === undefined ? { ...DEFAULT_VERSION_PATTERNS } : { ...userConfig.versionPatterns };
115
121
  const changelogJson = mergeChangelogJsonConfig(userConfig?.changelogJson);
116
122
  const releaseNotes = mergeReleaseNotesConfig(userConfig?.releaseNotes);
117
123
  const result = {
118
- tagPrefix: 'v',
119
- packageFiles: ['package.json'],
120
- changelogPaths: ['.'],
121
124
  workTypes,
122
125
  versionPatterns,
123
126
  changelogJson,
@@ -161,22 +164,52 @@ function assertRetiredPackagesDoNotCollideWithActive(workspaces, retiredPackages
161
164
  }
162
165
  }
163
166
  }
164
- function resolveProjectConfig(userProject, rootPackage, workspaces) {
165
- if (userProject === undefined) {
167
+ function resolveProjectConfig(composite, rootPackage, workspaces) {
168
+ if (composite === undefined) {
166
169
  return undefined;
167
170
  }
171
+ const subject = composite.name === undefined ? 'project block' : `composite release '${composite.name}'`;
168
172
  if (rootPackage === undefined || !rootPackage.exists) {
169
- throw new Error(`project block requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`);
173
+ throw new Error(`${subject} requires a root ${ROOT_PACKAGE_JSON_PATH}; create one with a 'version' field at the repo root`);
170
174
  }
171
175
  if (rootPackage.version === undefined) {
172
- throw new Error(`project block requires root ${ROOT_PACKAGE_JSON_PATH} to have a 'version' field; add a 'version' field to the root package.json`);
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`);
173
177
  }
174
178
  return {
175
- paths: userProject.paths ?? workspaces.flatMap((workspace) => workspace.paths),
176
- tagPrefix: userProject.tagPrefix ?? DEFAULT_PROJECT_TAG_PREFIX,
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 }),
177
183
  };
178
184
  }
179
- function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
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) {
180
213
  const sources = [];
181
214
  for (const workspace of workspaces) {
182
215
  const owner = `ws:${workspace.dir}`;
@@ -190,6 +223,18 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
190
223
  });
191
224
  }
192
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
+ }
193
238
  const retiredEntries = (retiredPackages ?? []).entries();
194
239
  for (const [index, retired] of retiredEntries) {
195
240
  sources.push({
@@ -199,7 +244,11 @@ function assertNoTagPrefixCollisions(workspaces, retiredPackages, project) {
199
244
  });
200
245
  }
201
246
  if (project !== undefined) {
202
- sources.push({ prefix: project.tagPrefix, label: 'project', owner: 'project' });
247
+ sources.push({
248
+ prefix: project.tagPrefix,
249
+ label: project.name === undefined ? 'project' : `composite release '${project.name}'`,
250
+ owner: 'project',
251
+ });
203
252
  }
204
253
  for (let i = 0; i < sources.length; i++) {
205
254
  for (let j = i + 1; j < sources.length; j++) {
@@ -49,7 +49,7 @@ export declare function parseArgs(argv: string[]): {
49
49
  };
50
50
  /**
51
51
  * Orchestrates the CLI `prepare` command: checks the working tree, loads and validates the config, discovers the
52
- * workspaces, and prepares the release in single-package or monorepo mode.
52
+ * workspaces, and prepares the release over the targets of the repo shape.
53
53
  *
54
54
  * A relative `--config` resolves against `invocationDir`.
55
55
  */
@@ -7,14 +7,13 @@ import { configFlagSchema } from "./configFlagSchema.js";
7
7
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
8
8
  import { dim } from "./format.js";
9
9
  import { RELEASE_GLYPHS } from "./glyphs.js";
10
- import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
11
10
  import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
12
11
  import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
13
12
  import { applyReleasePlan } from "./releasePlan.js";
14
- import { releasePrepare } from "./releasePrepare.js";
15
- import { releasePrepareMono } from "./releasePrepareMono.js";
13
+ import { releasePrepareTargets } from "./releasePrepareTargets.js";
16
14
  import { reportPrepare } from "./reportPrepare.js";
17
15
  import { resolveConfigFlag } from "./resolveConfigFlag.js";
16
+ import { isSinglePackageTargets, PROJECT_TARGET_ID, resolveReleaseTargets, SINGLE_PACKAGE_TARGET_ID, } from "./resolveReleaseTargets.js";
18
17
  const VALID_BUMP_TYPES = ['major', 'minor', 'patch'];
19
18
  const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
20
19
  function isReleaseType(value) {
@@ -100,68 +99,70 @@ export async function prepareCommand(argv, styles, invocationDir) {
100
99
  reportError(`No workspace package to release. ${describeEmptyWorkspace(workspace)}`);
101
100
  process.exit(1);
102
101
  }
103
- if (workspace.kind === 'single-package') {
104
- runSinglePackageMode(userConfig, options, only, setVersion, dryRun, styles.stdout);
105
- }
106
- else {
107
- runMonorepoMode(workspace.packageDirs, userConfig, options, only, setVersion, dryRun, styles.stdout);
108
- }
109
- }
110
- function runSinglePackageMode(userConfig, options, only, setVersion, dryRun, style) {
111
- if (only !== undefined) {
112
- reportError('--only is only supported for monorepo configurations');
113
- process.exit(1);
114
- }
115
- if (setVersion !== undefined && !CANONICAL_SEMVER_PATTERN.test(setVersion)) {
116
- reportError(`Invalid --set-version value "${setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
117
- process.exit(1);
118
- }
119
- const config = mergeSinglePackageConfig(userConfig);
102
+ const config = resolveTargetsOrExit(userConfig, workspace, only, setVersion);
103
+ const targetOptions = {
104
+ ...options,
105
+ ...(only !== undefined && { only }),
106
+ ...(setVersion !== undefined && { setVersions: resolveSetVersions(setVersion, config.targets) }),
107
+ };
120
108
  runAndReport(() => {
121
- const plan = releasePrepare(config, { ...options, ...(setVersion !== undefined && { setVersion }) });
122
- assertReleasesOnNpm(plan.workspaces.some((result) => result.status === 'released') ? ['.'] : []);
109
+ const plan = releasePrepareTargets(config, targetOptions);
110
+ assertReleasesOnNpm(findReleasedPackagePaths(plan, config.targets));
123
111
  return plan;
124
- }, dryRun, style);
112
+ }, dryRun, styles.stdout);
113
+ }
114
+ function findReleasedPackagePaths(plan, targets) {
115
+ const releasedIds = new Set(plan.targets.flatMap((result) => result.status === 'released' && result.kind === 'package' ? [result.targetId] : []));
116
+ return targets.flatMap((target) => target.kind === 'package' && releasedIds.has(target.id) ? [target.packagePath] : []);
125
117
  }
126
- function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion, dryRun, style) {
118
+ function resolveTargetsOrExit(userConfig, discovery, only, setVersion) {
119
+ if (discovery.kind === 'single-package') {
120
+ if (only !== undefined) {
121
+ reportError('--only is only supported for monorepo configurations');
122
+ process.exit(1);
123
+ }
124
+ if (setVersion !== undefined && !CANONICAL_SEMVER_PATTERN.test(setVersion)) {
125
+ reportError(`Invalid --set-version value "${setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
126
+ process.exit(1);
127
+ }
128
+ return resolveReleaseTargets(userConfig, discovery);
129
+ }
127
130
  let config;
128
131
  try {
129
- const rootPackage = readRootPackageVersion();
130
- config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
132
+ config = resolveReleaseTargets(userConfig, discovery);
131
133
  }
132
134
  catch (error) {
133
135
  reportError(`Failed to resolve workspaces: ${describeError(error)}`);
134
136
  process.exit(1);
135
137
  }
136
- if (setVersion !== undefined && config.project !== undefined) {
138
+ if (setVersion !== undefined && config.targets.some((target) => target.kind === 'composite')) {
137
139
  reportError('--set-version cannot be combined with a project release. ' +
138
140
  "--set-version sets individual workspaces' versions; a project release rolls up every " +
139
- 'contributing workspace. To use --set-version, run on a config without a `project` block.');
141
+ 'contributing workspace. To use --set-version, run on a config without a `project` block or a `releases` composite.');
140
142
  process.exit(1);
141
143
  }
142
- const knownNames = config.workspaces.map((w) => w.dir);
144
+ const knownNames = listSelectableIds(config.targets);
143
145
  const onlyNames = only ?? [];
144
146
  for (const name of onlyNames) {
145
147
  if (knownNames.includes(name)) {
146
148
  continue;
147
149
  }
148
- reportError(`Unknown workspace "${name}". Known workspaces: ${knownNames.join(', ')}`);
150
+ reportError(`Unknown release target "${name}". Known targets: ${knownNames.join(', ')}`);
149
151
  process.exit(1);
150
152
  }
151
- const monorepoOptions = {
152
- ...options,
153
- ...(only !== undefined && { only }),
154
- ...(setVersion !== undefined && { setVersions: parseSetVersions(setVersion, knownNames) }),
155
- };
156
- runAndReport(() => {
157
- const plan = releasePrepareMono(config, monorepoOptions);
158
- assertReleasesOnNpm(findReleasedWorkspacePaths(plan, config.workspaces));
159
- return plan;
160
- }, dryRun, style);
153
+ return config;
154
+ }
155
+ function resolveSetVersions(setVersion, targets) {
156
+ if (isSinglePackageTargets(targets)) {
157
+ return new Map([[SINGLE_PACKAGE_TARGET_ID, setVersion]]);
158
+ }
159
+ return parseSetVersions(setVersion, listMemberIds(targets));
160
+ }
161
+ function listSelectableIds(targets) {
162
+ return targets.flatMap((target) => (target.id === PROJECT_TARGET_ID ? [] : [target.id]));
161
163
  }
162
- function findReleasedWorkspacePaths(plan, workspaces) {
163
- const releasedDirs = new Set(plan.workspaces.flatMap((result) => result.status === 'released' && result.name !== undefined ? [result.name] : []));
164
- return workspaces.filter((workspace) => releasedDirs.has(workspace.dir)).map((workspace) => workspace.workspacePath);
164
+ function listMemberIds(targets) {
165
+ return targets.flatMap((target) => (target.kind === 'composite' ? [] : [target.id]));
165
166
  }
166
167
  function parseSetVersions(value, knownNames) {
167
168
  const setVersions = new Map();
@@ -170,16 +171,16 @@ function parseSetVersions(value, knownNames) {
170
171
  const name = entry.slice(0, Math.max(0, separatorIndex));
171
172
  const version = entry.slice(separatorIndex + 1);
172
173
  if (separatorIndex <= 0 || !CANONICAL_SEMVER_PATTERN.test(version)) {
173
- reportError(`Invalid --set-version entry "${entry}". In monorepo mode, each entry must be <workspace>@N.N.N ` +
174
+ reportError(`Invalid --set-version entry "${entry}". In monorepo mode, each entry must be <target>@N.N.N ` +
174
175
  '(canonical semver, no pre-release suffix), e.g. --set-version=arrays@1.0.0.');
175
176
  process.exit(1);
176
177
  }
177
178
  if (!knownNames.includes(name)) {
178
- reportError(`Unknown workspace "${name}" in --set-version. Known workspaces: ${knownNames.join(', ')}`);
179
+ reportError(`Unknown release target "${name}" in --set-version. Known targets: ${knownNames.join(', ')}`);
179
180
  process.exit(1);
180
181
  }
181
182
  if (setVersions.has(name)) {
182
- reportError(`--set-version names workspace "${name}" more than once`);
183
+ reportError(`--set-version names target "${name}" more than once`);
183
184
  process.exit(1);
184
185
  }
185
186
  setVersions.set(name, version);
@@ -0,0 +1,103 @@
1
+ import { type UntaggedBaseline } from './assertTaggedBaseline.js';
2
+ import { type ReleaseHistory } from './buildChangelogEntries.js';
3
+ import { type OverrideContext } from './changelogOverrides.js';
4
+ import { type GenerateChangelogOptions } from './generateChangelogs.js';
5
+ import type { PlannedWrite } from './releasePlan.js';
6
+ import type { DirectoryTarget, PropagationSource, ReleasedTargetResult, ReleaseSettings, ReleaseTarget, ReleaseType, TargetPrepareResult } from './types.js';
7
+ /** The shared settings that the prepare step reads, which do not include the release-notes settings. */
8
+ export type TargetPrepareSettings = Omit<ReleaseSettings, 'releaseNotes'>;
9
+ /** One read of a target's release windows, and the untagged baseline that it found, if any. */
10
+ export interface TargetReading {
11
+ history: ReleaseHistory;
12
+ untagged: UntaggedBaseline | undefined;
13
+ }
14
+ /** Outcome of {@link decideTargetRelease}: a release at a decided level or at an explicit version, or a skip. */
15
+ export type TargetDecision = {
16
+ outcome: 'release';
17
+ releaseType: ReleaseType;
18
+ bumpOverride: ReleaseType | undefined;
19
+ setVersion: undefined;
20
+ } | {
21
+ outcome: 'release';
22
+ releaseType: undefined;
23
+ bumpOverride: undefined;
24
+ setVersion: string;
25
+ } | {
26
+ outcome: 'skip';
27
+ skipReason: string;
28
+ };
29
+ /** The release level that a target takes, whether its own commits or a dependency's bump decided it. */
30
+ export interface TargetRelease {
31
+ releaseType: ReleaseType;
32
+ /** Dependencies whose bumps this release records; present for a propagated or mixed release. */
33
+ propagatedFrom?: PropagationSource[];
34
+ }
35
+ /** Inputs to {@link planTargetRelease}. */
36
+ export interface PlanTargetReleaseArgs {
37
+ target: ReleaseTarget;
38
+ settings: TargetPrepareSettings;
39
+ /** The level of the release and, for a propagated release, the dependencies that set it. */
40
+ release: TargetRelease;
41
+ /**
42
+ * The decision behind a direct release; undefined for a release that propagation alone set, whose own window called
43
+ * for none.
44
+ */
45
+ decision: Extract<TargetDecision, {
46
+ outcome: 'release';
47
+ }> | undefined;
48
+ /** The window that the target's read produced. */
49
+ history: ReleaseHistory;
50
+ overrideContext: OverrideContext;
51
+ /** Section titles in priority order. */
52
+ sectionOrder: string[];
53
+ /** Header of the section under which a propagation-only entry lists its dependencies. */
54
+ dependenciesHeader: string;
55
+ /** True when release-notes previews are requested and `changelogJson.enabled` is true. */
56
+ shouldPlanPreviews: boolean;
57
+ /** The release date, as `YYYY-MM-DD`. */
58
+ today: string;
59
+ }
60
+ /** A target's planned release: its result and the tag, writes, and modified files that the run collects. */
61
+ export interface PlannedTargetRelease {
62
+ result: ReleasedTargetResult;
63
+ tag: string;
64
+ writes: PlannedWrite[];
65
+ /** Archive files of the target's `CHANGELOG.md` that the release removes. */
66
+ removals: string[];
67
+ /**
68
+ * Files that the format command covers: the version files, the `CHANGELOG.md` and its archive files, and any
69
+ * changelog JSON.
70
+ */
71
+ modifiedFiles: string[];
72
+ /** Warnings raised while planning the release-notes previews. */
73
+ warnings: string[];
74
+ }
75
+ /** Reads a target's release windows under its tag prefixes and commit window, and finds its untagged baseline. */
76
+ export declare function readTargetHistory(target: ReleaseTarget, settings: TargetPrepareSettings): TargetReading;
77
+ /**
78
+ * Returns the bounds of a target's history read: its current and legacy tag prefixes, its commit window, and, for a
79
+ * workspace or a directory other than the repo root, the scope to which change-record entries route, which is its name.
80
+ */
81
+ export declare function describeHistoryWindow(target: ReleaseTarget): GenerateChangelogOptions;
82
+ /**
83
+ * Decides a target's direct release from its window: `setVersion` releases at that version, and otherwise the
84
+ * window's bump, `force`, and `bumpOverride` decide as {@link decideRelease} states.
85
+ */
86
+ export declare function decideTargetRelease(target: ReleaseTarget, history: ReleaseHistory, options: {
87
+ force: boolean;
88
+ bumpOverride: ReleaseType | undefined;
89
+ setVersion: string | undefined;
90
+ }): TargetDecision;
91
+ /**
92
+ * Plans a target's release: the version bump, the changelogs, and the release-notes previews, and builds its result.
93
+ *
94
+ * The changelog entries come from the target's release windows, with the synthetic "Forced version bump." entry for a
95
+ * window that doesn't yield any item, or, for a release that propagation alone set, from one synthetic entry listing
96
+ * the dependencies. Overrides apply before `changelog.json` and `CHANGELOG.md` render from the merged set, and
97
+ * `CHANGELOG.md` keeps the existing sections whose versions the merged set lacks.
98
+ */
99
+ export declare function planTargetRelease(args: PlanTargetReleaseArgs): PlannedTargetRelease;
100
+ /** Builds the result of a target that skips, from the window that its read produced. */
101
+ export declare function buildSkippedTargetResult(target: ReleaseTarget, history: ReleaseHistory, skipReason: string): TargetPrepareResult;
102
+ /** Reports whether a directory target is the repo root, which reads the whole repo and renders every entry. */
103
+ export declare function isRepoRoot(target: DirectoryTarget): boolean;