@williamthorsen/release-kit 14.0.0 → 14.2.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.
Files changed (110) hide show
  1. package/.meta/changelog.json +173 -0
  2. package/.readyup/kits/default.js +64 -7
  3. package/.readyup/kits/npm-auto-publish.js +127 -75
  4. package/.readyup/manifest.json +22 -7
  5. package/CHANGELOG.md +47 -0
  6. package/README.md +18 -4
  7. package/dist/esm/assertReleasesOnNpm.d.ts +7 -0
  8. package/dist/esm/assertReleasesOnNpm.js +56 -0
  9. package/dist/esm/assertTaggedBaseline.d.ts +2 -2
  10. package/dist/esm/attachChangelogDiagnostics.d.ts +2 -2
  11. package/dist/esm/bin/release-kit.js +10 -6
  12. package/dist/esm/buildChangelogEntries.d.ts +3 -3
  13. package/dist/esm/buildChangelogEntries.js +3 -3
  14. package/dist/esm/buildDependencyGraph.d.ts +11 -11
  15. package/dist/esm/buildDependencyGraph.js +13 -13
  16. package/dist/esm/buildReleaseSummary.d.ts +3 -3
  17. package/dist/esm/buildReleaseSummary.js +4 -12
  18. package/dist/esm/changelogArchive.d.ts +14 -0
  19. package/dist/esm/changelogArchive.js +21 -0
  20. package/dist/esm/changelogJsonFile.d.ts +2 -2
  21. package/dist/esm/changelogOverrides.d.ts +9 -8
  22. package/dist/esm/changelogOverrides.js +3 -6
  23. package/dist/esm/createGithubReleaseCommand.d.ts +2 -1
  24. package/dist/esm/createGithubReleaseCommand.js +7 -4
  25. package/dist/esm/deriveDirectoryConfig.d.ts +13 -0
  26. package/dist/esm/deriveDirectoryConfig.js +59 -0
  27. package/dist/esm/deriveWorkspaceConfig.js +2 -3
  28. package/dist/esm/discoverWorkspaces.d.ts +3 -3
  29. package/dist/esm/enumerateReleaseWindows.d.ts +1 -1
  30. package/dist/esm/enumerateReleaseWindows.js +1 -3
  31. package/dist/esm/findTransitiveDependents.js +3 -3
  32. package/dist/esm/findUnroutedEntryScopes.d.ts +2 -2
  33. package/dist/esm/formatDirectoryPublishSkip.d.ts +3 -0
  34. package/dist/esm/formatDirectoryPublishSkip.js +3 -0
  35. package/dist/esm/formatExcludedSkip.d.ts +5 -2
  36. package/dist/esm/formatExcludedSkip.js +2 -2
  37. package/dist/esm/formatProjectPublishSkip.d.ts +3 -0
  38. package/dist/esm/formatProjectPublishSkip.js +3 -0
  39. package/dist/esm/generateChangelogs.d.ts +7 -7
  40. package/dist/esm/generateChangelogs.js +2 -2
  41. package/dist/esm/help/prepareHelp.d.ts +1 -1
  42. package/dist/esm/help/prepareHelp.js +7 -5
  43. package/dist/esm/init/hasPublishablePackage.d.ts +10 -0
  44. package/dist/esm/init/hasPublishablePackage.js +28 -0
  45. package/dist/esm/init/initCommand.js +23 -13
  46. package/dist/esm/init/scaffold.d.ts +6 -1
  47. package/dist/esm/init/scaffold.js +10 -7
  48. package/dist/esm/init/templates.js +1 -1
  49. package/dist/esm/init/updateTemplatesCommand.d.ts +2 -1
  50. package/dist/esm/init/updateTemplatesCommand.js +12 -6
  51. package/dist/esm/isPublishableManifest.d.ts +2 -0
  52. package/dist/esm/isPublishableManifest.js +5 -0
  53. package/dist/esm/loadConfig.d.ts +12 -12
  54. package/dist/esm/loadConfig.js +79 -30
  55. package/dist/esm/matchesTagPrefix.d.ts +2 -0
  56. package/dist/esm/matchesTagPrefix.js +4 -0
  57. package/dist/esm/npmRegistry.d.ts +35 -0
  58. package/dist/esm/npmRegistry.js +58 -0
  59. package/dist/esm/prepareCommand.d.ts +1 -1
  60. package/dist/esm/prepareCommand.js +50 -36
  61. package/dist/esm/prepareTarget.d.ts +103 -0
  62. package/dist/esm/prepareTarget.js +274 -0
  63. package/dist/esm/previewTagPrefixes.d.ts +5 -3
  64. package/dist/esm/previewTagPrefixes.js +60 -14
  65. package/dist/esm/propagateBumps.js +1 -1
  66. package/dist/esm/publishCommand.js +13 -1
  67. package/dist/esm/readChangelogSections.d.ts +6 -4
  68. package/dist/esm/readChangelogSections.js +18 -4
  69. package/dist/esm/releasePlan.d.ts +4 -2
  70. package/dist/esm/releasePlan.js +34 -12
  71. package/dist/esm/releasePrepareTargets.d.ts +47 -0
  72. package/dist/esm/releasePrepareTargets.js +335 -0
  73. package/dist/esm/renderChangelogMarkdown.d.ts +23 -0
  74. package/dist/esm/renderChangelogMarkdown.js +47 -11
  75. package/dist/esm/reportPrepare.d.ts +2 -3
  76. package/dist/esm/reportPrepare.js +24 -12
  77. package/dist/esm/resolveCommandTags.d.ts +3 -1
  78. package/dist/esm/resolveCommandTags.js +15 -22
  79. package/dist/esm/resolveReleaseDeclaration.d.ts +45 -0
  80. package/dist/esm/resolveReleaseDeclaration.js +153 -0
  81. package/dist/esm/resolveReleaseTags.d.ts +14 -3
  82. package/dist/esm/resolveReleaseTags.js +36 -15
  83. package/dist/esm/resolveReleaseTargets.d.ts +29 -0
  84. package/dist/esm/resolveReleaseTargets.js +131 -0
  85. package/dist/esm/sync-labels/generateCommand.js +2 -1
  86. package/dist/esm/sync-labels/resolveLabels.d.ts +4 -3
  87. package/dist/esm/sync-labels/resolveLabels.js +30 -1
  88. package/dist/esm/sync-labels/templates.js +1 -1
  89. package/dist/esm/sync-labels/types.d.ts +2 -0
  90. package/dist/esm/types.d.ts +251 -167
  91. package/dist/esm/types.js +22 -0
  92. package/dist/esm/validateConfig.js +80 -21
  93. package/dist/esm/validateOverridesCommand.d.ts +1 -1
  94. package/dist/esm/validateOverridesCommand.js +17 -28
  95. package/docs/api.md +25 -58
  96. package/docs/changelogs.md +26 -6
  97. package/docs/configuration.md +80 -17
  98. package/docs/labels.md +30 -5
  99. package/docs/project-releases.md +42 -2
  100. package/docs/publishing.md +3 -3
  101. package/docs/releasing.md +12 -4
  102. package/docs/work-types.md +1 -1
  103. package/package.json +6 -6
  104. package/presets/labels/common.yaml +11 -0
  105. package/dist/esm/releasePrepare.d.ts +0 -36
  106. package/dist/esm/releasePrepare.js +0 -230
  107. package/dist/esm/releasePrepareMono.d.ts +0 -29
  108. package/dist/esm/releasePrepareMono.js +0 -503
  109. package/dist/esm/releasePrepareProject.d.ts +0 -47
  110. package/dist/esm/releasePrepareProject.js +0 -164
@@ -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++) {
@@ -0,0 +1,2 @@
1
+ /** Checks whether a tag name is one of the prefixes followed by a complete SemVer version. */
2
+ export declare function matchesTagPrefix(tagName: string, tagPrefixes: readonly string[]): boolean;
@@ -0,0 +1,4 @@
1
+ const SEMVER_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
2
+ export function matchesTagPrefix(tagName, tagPrefixes) {
3
+ return tagPrefixes.some((prefix) => tagName.startsWith(prefix) && SEMVER_VERSION_PATTERN.test(tagName.slice(prefix.length)));
4
+ }
@@ -0,0 +1,35 @@
1
+ export declare const AUTH_ERROR_CODES: ReadonlySet<string>;
2
+ export declare const UNREACHABLE_ERROR_CODES: ReadonlySet<string>;
3
+ /**
4
+ * Classifies the outcome of `npm view <package> version --json`.
5
+ *
6
+ * Only `E404` means that the registry doesn't know the package; any other failure leaves the question open.
7
+ */
8
+ export declare function classifyNpmPackageLookup(result: NpmCommandResult): NpmPackageLookup;
9
+ /** Looks up whether a package exists on the npm registry, querying `registry` when one is given. */
10
+ export declare function lookUpNpmPackage(packageName: string, registry?: string): NpmPackageLookup;
11
+ export interface NpmCommandResult {
12
+ exitOk: boolean;
13
+ stdout: string;
14
+ }
15
+ export interface NpmErrorPayload {
16
+ code: string;
17
+ summary: string;
18
+ }
19
+ export type NpmPackageLookup = {
20
+ status: 'published';
21
+ } | {
22
+ status: 'unpublished';
23
+ } | {
24
+ status: 'unverifiable';
25
+ detail: string;
26
+ };
27
+ /** Reads npm's `--json` error envelope, which npm writes to stdout on both the success and failure paths. */
28
+ export declare function readNpmError(stdout: string): NpmErrorPayload | undefined;
29
+ /**
30
+ * Runs npm with the given arguments, capturing stdout whether or not the command exits zero.
31
+ *
32
+ * The arguments are expected to include `--json`: npm writes its machine-readable error envelope to stdout,
33
+ * which `execFileSync` attaches to the thrown error instead of returning it.
34
+ */
35
+ export declare function runNpmJson(args: readonly string[]): NpmCommandResult;
@@ -0,0 +1,58 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { isRecord } from "./typeGuards.js";
3
+ export const AUTH_ERROR_CODES = new Set(['E401', 'ENEEDAUTH']);
4
+ export const UNREACHABLE_ERROR_CODES = new Set([
5
+ 'EAI_AGAIN',
6
+ 'ECONNREFUSED',
7
+ 'ECONNRESET',
8
+ 'ENETUNREACH',
9
+ 'ENOTFOUND',
10
+ 'ERR_SOCKET_TIMEOUT',
11
+ 'ETIMEDOUT',
12
+ ]);
13
+ export function classifyNpmPackageLookup(result) {
14
+ if (result.exitOk) {
15
+ return { status: 'published' };
16
+ }
17
+ const error = readNpmError(result.stdout);
18
+ if (error === undefined) {
19
+ return { status: 'unverifiable', detail: 'The npm registry query failed without a readable error payload' };
20
+ }
21
+ if (error.code === 'E404') {
22
+ return { status: 'unpublished' };
23
+ }
24
+ if (AUTH_ERROR_CODES.has(error.code)) {
25
+ return { status: 'unverifiable', detail: `The npm registry rejected the session (${error.code})` };
26
+ }
27
+ if (UNREACHABLE_ERROR_CODES.has(error.code)) {
28
+ return { status: 'unverifiable', detail: `Cannot reach the npm registry (${error.code})` };
29
+ }
30
+ return { status: 'unverifiable', detail: `The npm registry query failed (${error.code}): ${error.summary}` };
31
+ }
32
+ export function lookUpNpmPackage(packageName, registry) {
33
+ const args = ['view', packageName, 'version', '--json', ...(registry === undefined ? [] : ['--registry', registry])];
34
+ return classifyNpmPackageLookup(runNpmJson(args));
35
+ }
36
+ export function readNpmError(stdout) {
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(stdout);
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ if (!isRecord(parsed) || !isRecord(parsed['error']) || typeof parsed['error']['code'] !== 'string') {
45
+ return undefined;
46
+ }
47
+ const { code, summary } = parsed['error'];
48
+ return { code, summary: typeof summary === 'string' ? summary : '' };
49
+ }
50
+ export function runNpmJson(args) {
51
+ try {
52
+ return { exitOk: true, stdout: execFileSync('npm', args, { encoding: 'utf8', stdio: 'pipe' }) };
53
+ }
54
+ catch (error) {
55
+ const stdout = isRecord(error) && typeof error['stdout'] === 'string' ? error['stdout'] : '';
56
+ return { exitOk: false, stdout };
57
+ }
58
+ }
@@ -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
  */
@@ -2,18 +2,18 @@ import { execSync } from 'node:child_process';
2
2
  import { formatGlyphLine, parseArgsOrExit, reportError, } from '@williamthorsen/nmr-core';
3
3
  import { describeError } from '@williamthorsen/toolbelt.errors';
4
4
  import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
5
+ import { assertReleasesOnNpm } from "./assertReleasesOnNpm.js";
5
6
  import { configFlagSchema } from "./configFlagSchema.js";
6
7
  import { describeEmptyWorkspace, discoverWorkspaces } from "./discoverWorkspaces.js";
7
8
  import { dim } from "./format.js";
8
9
  import { RELEASE_GLYPHS } from "./glyphs.js";
9
- import { mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
10
10
  import { loadValidatedConfig, reportConfigProblem, reportConfigWarnings } from "./loadValidatedConfig.js";
11
11
  import { RELEASE_SUMMARY_FILE, RELEASE_TAGS_FILE } from "./releaseFiles.js";
12
12
  import { applyReleasePlan } from "./releasePlan.js";
13
- import { releasePrepare } from "./releasePrepare.js";
14
- import { releasePrepareMono } from "./releasePrepareMono.js";
13
+ import { releasePrepareTargets } from "./releasePrepareTargets.js";
15
14
  import { reportPrepare } from "./reportPrepare.js";
16
15
  import { resolveConfigFlag } from "./resolveConfigFlag.js";
16
+ import { isSinglePackageTargets, PROJECT_TARGET_ID, resolveReleaseTargets, SINGLE_PACKAGE_TARGET_ID, } from "./resolveReleaseTargets.js";
17
17
  const VALID_BUMP_TYPES = ['major', 'minor', 'patch'];
18
18
  const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
19
19
  function isReleaseType(value) {
@@ -99,56 +99,70 @@ export async function prepareCommand(argv, styles, invocationDir) {
99
99
  reportError(`No workspace package to release. ${describeEmptyWorkspace(workspace)}`);
100
100
  process.exit(1);
101
101
  }
102
- if (workspace.kind === 'single-package') {
103
- runSinglePackageMode(userConfig, options, only, setVersion, dryRun, styles.stdout);
104
- }
105
- else {
106
- runMonorepoMode(workspace.packageDirs, userConfig, options, only, setVersion, dryRun, styles.stdout);
107
- }
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
+ };
108
+ runAndReport(() => {
109
+ const plan = releasePrepareTargets(config, targetOptions);
110
+ assertReleasesOnNpm(findReleasedPackagePaths(plan, config.targets));
111
+ return plan;
112
+ }, dryRun, styles.stdout);
108
113
  }
109
- function runSinglePackageMode(userConfig, options, only, setVersion, dryRun, style) {
110
- if (only !== undefined) {
111
- reportError('--only is only supported for monorepo configurations');
112
- process.exit(1);
113
- }
114
- if (setVersion !== undefined && !CANONICAL_SEMVER_PATTERN.test(setVersion)) {
115
- reportError(`Invalid --set-version value "${setVersion}". Must be canonical semver (N.N.N, no pre-release suffix).`);
116
- process.exit(1);
117
- }
118
- const config = mergeSinglePackageConfig(userConfig);
119
- runAndReport(() => releasePrepare(config, { ...options, ...(setVersion !== undefined && { setVersion }) }), dryRun, style);
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] : []);
120
117
  }
121
- 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
+ }
122
130
  let config;
123
131
  try {
124
- const rootPackage = readRootPackageVersion();
125
- config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
132
+ config = resolveReleaseTargets(userConfig, discovery);
126
133
  }
127
134
  catch (error) {
128
135
  reportError(`Failed to resolve workspaces: ${describeError(error)}`);
129
136
  process.exit(1);
130
137
  }
131
- if (setVersion !== undefined && config.project !== undefined) {
138
+ if (setVersion !== undefined && config.targets.some((target) => target.kind === 'composite')) {
132
139
  reportError('--set-version cannot be combined with a project release. ' +
133
140
  "--set-version sets individual workspaces' versions; a project release rolls up every " +
134
- '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.');
135
142
  process.exit(1);
136
143
  }
137
- const knownNames = config.workspaces.map((w) => w.dir);
144
+ const knownNames = listSelectableIds(config.targets);
138
145
  const onlyNames = only ?? [];
139
146
  for (const name of onlyNames) {
140
147
  if (knownNames.includes(name)) {
141
148
  continue;
142
149
  }
143
- reportError(`Unknown workspace "${name}". Known workspaces: ${knownNames.join(', ')}`);
150
+ reportError(`Unknown release target "${name}". Known targets: ${knownNames.join(', ')}`);
144
151
  process.exit(1);
145
152
  }
146
- const monorepoOptions = {
147
- ...options,
148
- ...(only !== undefined && { only }),
149
- ...(setVersion !== undefined && { setVersions: parseSetVersions(setVersion, knownNames) }),
150
- };
151
- runAndReport(() => releasePrepareMono(config, monorepoOptions), 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]));
163
+ }
164
+ function listMemberIds(targets) {
165
+ return targets.flatMap((target) => (target.kind === 'composite' ? [] : [target.id]));
152
166
  }
153
167
  function parseSetVersions(value, knownNames) {
154
168
  const setVersions = new Map();
@@ -157,16 +171,16 @@ function parseSetVersions(value, knownNames) {
157
171
  const name = entry.slice(0, Math.max(0, separatorIndex));
158
172
  const version = entry.slice(separatorIndex + 1);
159
173
  if (separatorIndex <= 0 || !CANONICAL_SEMVER_PATTERN.test(version)) {
160
- 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 ` +
161
175
  '(canonical semver, no pre-release suffix), e.g. --set-version=arrays@1.0.0.');
162
176
  process.exit(1);
163
177
  }
164
178
  if (!knownNames.includes(name)) {
165
- reportError(`Unknown workspace "${name}" in --set-version. Known workspaces: ${knownNames.join(', ')}`);
179
+ reportError(`Unknown release target "${name}" in --set-version. Known targets: ${knownNames.join(', ')}`);
166
180
  process.exit(1);
167
181
  }
168
182
  if (setVersions.has(name)) {
169
- reportError(`--set-version names workspace "${name}" more than once`);
183
+ reportError(`--set-version names target "${name}" more than once`);
170
184
  process.exit(1);
171
185
  }
172
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;