@williamthorsen/release-kit 4.8.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +134 -4
  2. package/README.md +404 -40
  3. package/cliff.toml.template +2 -1
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/assertCleanWorkingTree.js +1 -1
  6. package/dist/esm/bin/release-kit.js +45 -14
  7. package/dist/esm/buildChangelogEntries.d.ts +3 -0
  8. package/dist/esm/{generateChangelogJson.js → buildChangelogEntries.js} +40 -77
  9. package/dist/esm/buildDependencyGraph.d.ts +4 -3
  10. package/dist/esm/buildDependencyGraph.js +18 -11
  11. package/dist/esm/buildReleaseSummary.js +12 -4
  12. package/dist/esm/buildSyntheticChangelogEntry.d.ts +5 -0
  13. package/dist/esm/buildSyntheticChangelogEntry.js +13 -0
  14. package/dist/esm/bumpAllVersions.d.ts +1 -0
  15. package/dist/esm/bumpAllVersions.js +16 -2
  16. package/dist/esm/bumpVersion.js +3 -0
  17. package/dist/esm/changelogJsonFile.d.ts +4 -0
  18. package/dist/esm/changelogJsonFile.js +68 -0
  19. package/dist/esm/commitCommand.js +1 -1
  20. package/dist/esm/compareVersions.d.ts +1 -0
  21. package/dist/esm/compareVersions.js +27 -0
  22. package/dist/esm/createGithubRelease.d.ts +6 -2
  23. package/dist/esm/createGithubRelease.js +17 -17
  24. package/dist/esm/createGithubReleaseCommand.d.ts +1 -0
  25. package/dist/esm/createGithubReleaseCommand.js +41 -0
  26. package/dist/esm/decideRelease.d.ts +25 -0
  27. package/dist/esm/decideRelease.js +28 -0
  28. package/dist/esm/defaults.d.ts +1 -0
  29. package/dist/esm/defaults.js +7 -3
  30. package/dist/esm/deriveWorkspaceConfig.d.ts +2 -0
  31. package/dist/esm/deriveWorkspaceConfig.js +37 -0
  32. package/dist/esm/detectUndeclaredTagPrefixes.d.ts +7 -0
  33. package/dist/esm/detectUndeclaredTagPrefixes.js +46 -0
  34. package/dist/esm/generateChangelogs.d.ts +1 -1
  35. package/dist/esm/generateChangelogs.js +14 -3
  36. package/dist/esm/getCommitsSinceTarget.d.ts +1 -1
  37. package/dist/esm/getCommitsSinceTarget.js +8 -4
  38. package/dist/esm/index.d.ts +2 -39
  39. package/dist/esm/index.js +0 -75
  40. package/dist/esm/init/initCommand.js +1 -1
  41. package/dist/esm/init/scaffold.d.ts +1 -1
  42. package/dist/esm/init/scaffold.js +8 -5
  43. package/dist/esm/init/templates.d.ts +1 -0
  44. package/dist/esm/init/templates.js +35 -5
  45. package/dist/esm/injectReleaseNotesIntoReadme.d.ts +6 -1
  46. package/dist/esm/injectReleaseNotesIntoReadme.js +20 -7
  47. package/dist/esm/loadConfig.d.ts +12 -2
  48. package/dist/esm/loadConfig.js +161 -14
  49. package/dist/esm/parseRequestedTags.d.ts +1 -0
  50. package/dist/esm/parseRequestedTags.js +10 -0
  51. package/dist/esm/prepareCommand.d.ts +3 -1
  52. package/dist/esm/prepareCommand.js +121 -31
  53. package/dist/esm/previewTagPrefixes.d.ts +30 -0
  54. package/dist/esm/previewTagPrefixes.js +120 -0
  55. package/dist/esm/propagateBumps.d.ts +1 -0
  56. package/dist/esm/propagateBumps.js +1 -1
  57. package/dist/esm/publish.d.ts +0 -1
  58. package/dist/esm/publish.js +3 -3
  59. package/dist/esm/publishCommand.js +18 -14
  60. package/dist/esm/pushCommand.js +5 -4
  61. package/dist/esm/readCurrentVersion.d.ts +1 -0
  62. package/dist/esm/readCurrentVersion.js +21 -0
  63. package/dist/esm/releasePrepare.d.ts +2 -0
  64. package/dist/esm/releasePrepare.js +140 -54
  65. package/dist/esm/releasePrepareMono.js +312 -143
  66. package/dist/esm/releasePrepareProject.d.ts +9 -0
  67. package/dist/esm/releasePrepareProject.js +109 -0
  68. package/dist/esm/renderReleaseNotes.d.ts +1 -0
  69. package/dist/esm/renderReleaseNotes.js +29 -2
  70. package/dist/esm/reportPrepare.js +146 -73
  71. package/dist/esm/resolveCliffConfigPath.js +1 -1
  72. package/dist/esm/resolveCommandTags.d.ts +1 -1
  73. package/dist/esm/resolveCommandTags.js +17 -13
  74. package/dist/esm/resolveReleaseNotesConfig.d.ts +8 -1
  75. package/dist/esm/resolveReleaseNotesConfig.js +17 -7
  76. package/dist/esm/resolveReleaseTags.d.ts +2 -1
  77. package/dist/esm/resolveReleaseTags.js +19 -14
  78. package/dist/esm/showTagPrefixesCommand.d.ts +1 -0
  79. package/dist/esm/showTagPrefixesCommand.js +84 -0
  80. package/dist/esm/sync-labels/initCommand.js +1 -1
  81. package/dist/esm/sync-labels/presets.js +1 -1
  82. package/dist/esm/tagCommand.js +1 -1
  83. package/dist/esm/types.d.ts +77 -19
  84. package/dist/esm/validateConfig.js +205 -36
  85. package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -0
  86. package/dist/esm/validateOnlyExcludesStrandedDependents.js +109 -0
  87. package/dist/esm/version.d.ts +1 -1
  88. package/dist/esm/version.js +1 -1
  89. package/dist/esm/writeReleaseNotesPreviews.d.ts +18 -0
  90. package/dist/esm/writeReleaseNotesPreviews.js +65 -0
  91. package/package.json +5 -2
  92. package/presets/labels/common.yaml +9 -6
  93. package/schemas/label-map.json +24 -0
  94. package/dist/esm/component.d.ts +0 -2
  95. package/dist/esm/component.js +0 -14
  96. package/dist/esm/findPackageRoot.d.ts +0 -1
  97. package/dist/esm/findPackageRoot.js +0 -17
  98. package/dist/esm/generateChangelogJson.d.ts +0 -7
  99. package/dist/esm/githubReleaseCommand.d.ts +0 -1
  100. package/dist/esm/githubReleaseCommand.js +0 -35
@@ -0,0 +1,84 @@
1
+ import { detectRepoType } from "./init/detectRepoType.js";
2
+ import { previewTagPrefixes } from "./previewTagPrefixes.js";
3
+ async function showTagPrefixesCommand() {
4
+ if (detectRepoType() === "single-package") {
5
+ process.stdout.write(renderSinglePackage());
6
+ return 0;
7
+ }
8
+ const preview = await previewTagPrefixes();
9
+ process.stdout.write(renderMonorepo(preview));
10
+ return computeExitCode(preview);
11
+ }
12
+ function renderSinglePackage() {
13
+ const lines = [
14
+ "Workspace Derived prefix Status",
15
+ ". v (single-package mode)",
16
+ ""
17
+ ];
18
+ return lines.join("\n");
19
+ }
20
+ function renderMonorepo(preview) {
21
+ const lines = ["Workspace tag prefixes:", ""];
22
+ for (const row of preview.workspaces) {
23
+ lines.push(...renderWorkspaceRow(row));
24
+ }
25
+ if (preview.collisions.length > 0) {
26
+ lines.push(
27
+ "",
28
+ ...preview.collisions.map(
29
+ (collision) => `\u26D4 tag prefix collision: '${collision.tagPrefix}' used by ${collision.workspacePaths.join(", ")}`
30
+ )
31
+ );
32
+ }
33
+ if (preview.undeclaredCandidates.length > 0) {
34
+ lines.push(
35
+ "",
36
+ "Undeclared tag prefixes:",
37
+ "",
38
+ ...preview.undeclaredCandidates.map(
39
+ (candidate) => ` '${candidate.prefix}' \u2014 ${candidate.tagCount} tags (e.g., ${candidate.exampleTags.join(", ")})`
40
+ ),
41
+ "",
42
+ "Suggested config snippet (adjust `dir` to match your workspace if the guess is wrong, and replace the `name` placeholder with the legacy npm name):",
43
+ "",
44
+ renderSuggestedSnippet(preview.undeclaredCandidates),
45
+ "",
46
+ "If the suggested `dir` does not match your workspace, adjust before pasting. Each legacy identity requires a `name` \u2014 replace the `TODO-fill-in-legacy-npm-name` placeholder with the package's prior npm name."
47
+ );
48
+ }
49
+ lines.push("");
50
+ return lines.join("\n");
51
+ }
52
+ function renderWorkspaceRow(row) {
53
+ const lines = [];
54
+ if (row.derivedPrefix === null) {
55
+ lines.push(` ${row.workspacePath} \u2014 \u26D4 derivation failed: ${row.derivationError ?? "unknown error"}`);
56
+ return lines;
57
+ }
58
+ const statusMarker = row.derivedTagCount > 0 ? `\u2705 ${row.derivedTagCount} tags` : "\u26A0\uFE0F no existing tags";
59
+ lines.push(` ${row.workspacePath} \u2014 derived prefix '${row.derivedPrefix}', ${statusMarker}`);
60
+ for (const entry of row.legacyEntries) {
61
+ if (entry.tagCount > 0) {
62
+ lines.push(` \u2705 ${entry.tagCount} legacy tags with '${entry.prefix}' prefix (recognized)`);
63
+ } else {
64
+ lines.push(` \u26A0\uFE0F recorded legacy prefix '${entry.prefix}' has no tags`);
65
+ }
66
+ }
67
+ return lines;
68
+ }
69
+ function renderSuggestedSnippet(candidates) {
70
+ const entries = candidates.map(
71
+ (candidate) => ` { dir: '${candidate.suggestedDir}', legacyIdentities: [{ name: 'TODO-fill-in-legacy-npm-name', tagPrefix: '${candidate.prefix}' }] },`
72
+ ).join("\n");
73
+ return ` workspaces: [
74
+ ${entries}
75
+ ],`;
76
+ }
77
+ function computeExitCode(preview) {
78
+ const hasDerivationFailure = preview.workspaces.some((row) => row.derivedPrefix === null);
79
+ const hasCollision = preview.collisions.length > 0;
80
+ return hasDerivationFailure || hasCollision ? 1 : 0;
81
+ }
82
+ export {
83
+ showTagPrefixesCommand
84
+ };
@@ -1,4 +1,4 @@
1
- import { reportWriteResult, writeFileWithCheck } from "@williamthorsen/node-monorepo-core";
1
+ import { reportWriteResult, writeFileWithCheck } from "@williamthorsen/nmr-core";
2
2
  import { discoverWorkspaces } from "../discoverWorkspaces.js";
3
3
  import { generateCommand, LABELS_OUTPUT_PATH } from "./generateCommand.js";
4
4
  import { SYNC_LABELS_CONFIG_PATH } from "./loadSyncLabelsConfig.js";
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
+ import { findPackageRoot } from "@williamthorsen/nmr-core";
4
5
  import { load } from "js-yaml";
5
- import { findPackageRoot } from "../findPackageRoot.js";
6
6
  import { isRecord } from "../typeGuards.js";
7
7
  function resolvePresetPath(presetName) {
8
8
  const root = findPackageRoot(import.meta.url);
@@ -1,4 +1,4 @@
1
- import { parseArgs, translateParseError } from "@williamthorsen/node-monorepo-core";
1
+ import { parseArgs, translateParseError } from "@williamthorsen/nmr-core";
2
2
  import { createTags } from "./createTags.js";
3
3
  const tagFlagSchema = {
4
4
  dryRun: { long: "--dry-run", type: "boolean" },
@@ -2,6 +2,7 @@ export type ReleaseType = 'major' | 'minor' | 'patch';
2
2
  export type ChangelogAudience = 'all' | 'dev';
3
3
  export interface ChangelogItem {
4
4
  description: string;
5
+ body?: string;
5
6
  }
6
7
  export interface ChangelogSection {
7
8
  title: string;
@@ -20,7 +21,12 @@ export interface ChangelogJsonConfig {
20
21
  }
21
22
  export interface ReleaseNotesConfig {
22
23
  shouldInjectIntoReadme: boolean;
23
- shouldCreateGithubRelease: boolean;
24
+ }
25
+ export interface ProjectConfig {
26
+ tagPrefix?: string;
27
+ }
28
+ export interface ResolvedProjectConfig {
29
+ tagPrefix: string;
24
30
  }
25
31
  export interface PropagationSource {
26
32
  packageName: string;
@@ -31,25 +37,60 @@ export interface BumpResult {
31
37
  newVersion: string;
32
38
  files: string[];
33
39
  }
34
- export interface ComponentPrepareResult {
35
- name?: string | undefined;
36
- status: 'released' | 'skipped';
37
- previousTag?: string | undefined;
40
+ export interface ReleasedWorkspaceResult {
41
+ status: 'released';
42
+ name?: string;
43
+ previousTag?: string;
38
44
  commitCount: number;
39
- parsedCommitCount?: number | undefined;
40
- releaseType?: ReleaseType | undefined;
41
- currentVersion?: string | undefined;
42
- newVersion?: string | undefined;
43
- tag?: string | undefined;
45
+ parsedCommitCount?: number;
46
+ unparseableCommits?: Commit[];
47
+ releaseType?: ReleaseType;
48
+ currentVersion: string;
49
+ newVersion: string;
50
+ tag: string;
44
51
  bumpedFiles: string[];
45
52
  changelogFiles: string[];
46
- commits?: Commit[] | undefined;
47
- unparseableCommits?: Commit[] | undefined;
48
- propagatedFrom?: PropagationSource[] | undefined;
49
- skipReason?: string | undefined;
53
+ commits?: Commit[];
54
+ bumpOverride?: ReleaseType;
55
+ propagatedFrom?: PropagationSource[];
56
+ setVersion?: string;
57
+ }
58
+ export interface SkippedWorkspaceResult {
59
+ status: 'skipped';
60
+ name?: string;
61
+ previousTag?: string;
62
+ commitCount: number;
63
+ parsedCommitCount?: number;
64
+ unparseableCommits?: Commit[];
65
+ skipReason: string;
66
+ }
67
+ export type WorkspacePrepareResult = ReleasedWorkspaceResult | SkippedWorkspaceResult;
68
+ export interface ReleasedProjectResult {
69
+ status: 'released';
70
+ previousTag?: string;
71
+ commitCount: number;
72
+ parsedCommitCount: number;
73
+ unparseableCommits?: Commit[];
74
+ releaseType: ReleaseType;
75
+ currentVersion: string;
76
+ newVersion: string;
77
+ tag: string;
78
+ bumpedFiles: string[];
79
+ changelogFiles: string[];
80
+ commits: Commit[];
81
+ bumpOverride?: ReleaseType;
82
+ }
83
+ export interface SkippedProjectResult {
84
+ status: 'skipped';
85
+ previousTag?: string;
86
+ commitCount: number;
87
+ parsedCommitCount: number;
88
+ unparseableCommits?: Commit[];
89
+ skipReason: string;
50
90
  }
91
+ export type ProjectPrepareResult = ReleasedProjectResult | SkippedProjectResult;
51
92
  export interface PrepareResult {
52
- components: ComponentPrepareResult[];
93
+ workspaces: WorkspacePrepareResult[];
53
94
  tags: string[];
54
95
  formatCommand?: {
55
96
  command: string;
@@ -58,6 +99,7 @@ export interface PrepareResult {
58
99
  } | undefined;
59
100
  dryRun: boolean;
60
101
  warnings?: string[] | undefined;
102
+ project?: ProjectPrepareResult | undefined;
61
103
  }
62
104
  export interface WorkTypeConfig {
63
105
  header: string;
@@ -68,7 +110,7 @@ export interface VersionPatterns {
68
110
  minor: string[];
69
111
  }
70
112
  export interface ReleaseKitConfig {
71
- components?: ComponentOverride[];
113
+ workspaces?: WorkspaceOverride[];
72
114
  versionPatterns?: VersionPatterns;
73
115
  workTypes?: Record<string, WorkTypeConfig>;
74
116
  formatCommand?: string;
@@ -76,10 +118,22 @@ export interface ReleaseKitConfig {
76
118
  scopeAliases?: Record<string, string>;
77
119
  changelogJson?: Partial<ChangelogJsonConfig>;
78
120
  releaseNotes?: Partial<ReleaseNotesConfig>;
121
+ retiredPackages?: RetiredPackage[];
122
+ project?: ProjectConfig;
123
+ }
124
+ export interface LegacyIdentity {
125
+ name: string;
126
+ tagPrefix: string;
127
+ }
128
+ export interface RetiredPackage {
129
+ name: string;
130
+ tagPrefix: string;
131
+ successor?: string;
79
132
  }
80
- export interface ComponentOverride {
133
+ export interface WorkspaceOverride {
81
134
  dir: string;
82
135
  shouldExclude?: boolean;
136
+ legacyIdentities?: LegacyIdentity[];
83
137
  }
84
138
  export interface Commit {
85
139
  message: string;
@@ -93,15 +147,18 @@ export interface ParsedCommit {
93
147
  scope?: string;
94
148
  breaking: boolean;
95
149
  }
96
- export interface ComponentConfig {
150
+ export interface WorkspaceConfig {
97
151
  dir: string;
152
+ name: string;
98
153
  tagPrefix: string;
154
+ workspacePath: string;
99
155
  packageFiles: string[];
100
156
  changelogPaths: string[];
101
157
  paths: string[];
158
+ legacyIdentities?: LegacyIdentity[];
102
159
  }
103
160
  export interface MonorepoReleaseConfig {
104
- components: ComponentConfig[];
161
+ workspaces: WorkspaceConfig[];
105
162
  workTypes?: Record<string, WorkTypeConfig>;
106
163
  versionPatterns?: VersionPatterns;
107
164
  formatCommand?: string;
@@ -109,6 +166,7 @@ export interface MonorepoReleaseConfig {
109
166
  scopeAliases?: Record<string, string>;
110
167
  changelogJson: ChangelogJsonConfig;
111
168
  releaseNotes: ReleaseNotesConfig;
169
+ project?: ResolvedProjectConfig;
112
170
  }
113
171
  export interface ReleaseConfig {
114
172
  tagPrefix: string;
@@ -8,11 +8,13 @@ function validateConfig(raw) {
8
8
  const knownFields = /* @__PURE__ */ new Set([
9
9
  "changelogJson",
10
10
  "cliffConfigPath",
11
- "components",
12
11
  "formatCommand",
12
+ "project",
13
13
  "releaseNotes",
14
+ "retiredPackages",
14
15
  "scopeAliases",
15
16
  "versionPatterns",
17
+ "workspaces",
16
18
  "workTypes"
17
19
  ]);
18
20
  for (const key of Object.keys(raw)) {
@@ -21,26 +23,21 @@ function validateConfig(raw) {
21
23
  }
22
24
  }
23
25
  validateChangelogJson(raw.changelogJson, config, errors);
24
- validateComponents(raw.components, config, errors);
26
+ validateWorkspaces(raw.workspaces, config, errors);
25
27
  validateReleaseNotes(raw.releaseNotes, config, errors);
26
28
  validateVersionPatterns(raw.versionPatterns, config, errors);
27
29
  validateWorkTypes(raw.workTypes, config, errors);
28
30
  validateStringField("formatCommand", raw.formatCommand, config, errors);
29
31
  validateStringField("cliffConfigPath", raw.cliffConfigPath, config, errors);
30
32
  validateScopeAliases(raw.scopeAliases, config, errors);
33
+ validateRetiredPackages(raw.retiredPackages, config, errors);
34
+ validateProjectConfig(raw.project, config, errors);
31
35
  const warnings = [];
32
36
  const changelogJsonEnabled = config.changelogJson?.enabled ?? true;
33
- if (!changelogJsonEnabled) {
34
- if (config.releaseNotes?.shouldCreateGithubRelease) {
35
- warnings.push(
36
- "releaseNotes.shouldCreateGithubRelease is enabled but changelogJson.enabled is false; GitHub Releases will be skipped at runtime"
37
- );
38
- }
39
- if (config.releaseNotes?.shouldInjectIntoReadme) {
40
- warnings.push(
41
- "releaseNotes.shouldInjectIntoReadme is enabled but changelogJson.enabled is false; README injection will be skipped at runtime"
42
- );
43
- }
37
+ if (!changelogJsonEnabled && config.releaseNotes?.shouldInjectIntoReadme) {
38
+ warnings.push(
39
+ "releaseNotes.shouldInjectIntoReadme is enabled but changelogJson.enabled is false; README injection will be skipped at runtime"
40
+ );
44
41
  }
45
42
  return { config, errors, warnings };
46
43
  }
@@ -80,16 +77,46 @@ function validateChangelogJson(value, config, errors) {
80
77
  }
81
78
  config.changelogJson = result;
82
79
  }
80
+ function validateProjectConfig(value, config, errors) {
81
+ if (value === void 0) return;
82
+ if (!isRecord(value)) {
83
+ errors.push("'project' must be an object");
84
+ return;
85
+ }
86
+ const knownProjectFields = /* @__PURE__ */ new Set(["tagPrefix"]);
87
+ for (const key of Object.keys(value)) {
88
+ if (!knownProjectFields.has(key)) {
89
+ errors.push(`project: unknown field '${key}'`);
90
+ }
91
+ }
92
+ const result = {};
93
+ if (value.tagPrefix !== void 0) {
94
+ if (typeof value.tagPrefix !== "string") {
95
+ errors.push("project.tagPrefix: must be a string");
96
+ } else if (value.tagPrefix === "") {
97
+ errors.push("project.tagPrefix: must be a non-empty string");
98
+ } else {
99
+ result.tagPrefix = value.tagPrefix;
100
+ }
101
+ }
102
+ config.project = result;
103
+ }
83
104
  function validateReleaseNotes(value, config, errors) {
84
105
  if (value === void 0) return;
85
106
  if (!isRecord(value)) {
86
107
  errors.push("'releaseNotes' must be an object");
87
108
  return;
88
109
  }
89
- const knownReleaseNotesFields = /* @__PURE__ */ new Set(["shouldInjectIntoReadme", "shouldCreateGithubRelease"]);
110
+ const knownReleaseNotesFields = /* @__PURE__ */ new Set(["shouldInjectIntoReadme"]);
90
111
  for (const key of Object.keys(value)) {
91
112
  if (!knownReleaseNotesFields.has(key)) {
92
- errors.push(`releaseNotes: unknown field '${key}'`);
113
+ if (key === "shouldCreateGithubRelease") {
114
+ errors.push(
115
+ "releaseNotes.shouldCreateGithubRelease is no longer supported. Adoption is now signaled by installing the create-github-release workflow. Remove this field from your config; see README for the updated workflow."
116
+ );
117
+ } else {
118
+ errors.push(`releaseNotes: unknown field '${key}'`);
119
+ }
93
120
  }
94
121
  }
95
122
  const result = {};
@@ -100,57 +127,199 @@ function validateReleaseNotes(value, config, errors) {
100
127
  errors.push("releaseNotes.shouldInjectIntoReadme: must be a boolean");
101
128
  }
102
129
  }
103
- if (value.shouldCreateGithubRelease !== void 0) {
104
- if (typeof value.shouldCreateGithubRelease === "boolean") {
105
- result.shouldCreateGithubRelease = value.shouldCreateGithubRelease;
106
- } else {
107
- errors.push("releaseNotes.shouldCreateGithubRelease: must be a boolean");
108
- }
109
- }
110
130
  config.releaseNotes = result;
111
131
  }
112
132
  function isStringArray(value) {
113
133
  return Array.isArray(value) && value.every((item) => typeof item === "string");
114
134
  }
115
- function validateComponents(value, config, errors) {
135
+ function validateWorkspaces(value, config, errors) {
116
136
  if (value === void 0) return;
117
137
  if (!Array.isArray(value)) {
118
- errors.push("'components' must be an array");
138
+ errors.push("'workspaces' must be an array");
119
139
  return;
120
140
  }
121
- const components = [];
122
- const knownComponentFields = /* @__PURE__ */ new Set(["dir", "shouldExclude"]);
141
+ const workspaces = [];
142
+ const knownWorkspaceFields = /* @__PURE__ */ new Set(["dir", "shouldExclude", "legacyIdentities"]);
123
143
  for (const [i, entry] of value.entries()) {
124
144
  if (!isRecord(entry)) {
125
- errors.push(`components[${i}]: must be an object`);
145
+ errors.push(`workspaces[${i}]: must be an object`);
126
146
  continue;
127
147
  }
128
148
  if (typeof entry.dir !== "string" || entry.dir === "") {
129
- errors.push(`components[${i}]: 'dir' is required`);
149
+ errors.push(`workspaces[${i}]: 'dir' is required`);
130
150
  continue;
131
151
  }
132
152
  for (const key of Object.keys(entry)) {
133
- if (!knownComponentFields.has(key)) {
153
+ if (!knownWorkspaceFields.has(key)) {
134
154
  if (key === "tagPrefix") {
135
155
  errors.push(
136
- `components[${i}]: 'tagPrefix' is no longer supported; remove it to use the default '${entry.dir}-v'`
156
+ `workspaces[${i}]: 'tagPrefix' is no longer supported; remove it to use the default '${entry.dir}-v'`
157
+ );
158
+ } else if (key === "legacyTagPrefixes") {
159
+ errors.push(
160
+ `workspaces[${i}]: 'legacyTagPrefixes' is no longer supported; use 'legacyIdentities: [{ name, tagPrefix }, ...]' instead`
137
161
  );
138
162
  } else {
139
- errors.push(`components[${i}]: unknown field '${key}'`);
163
+ errors.push(`workspaces[${i}]: unknown field '${key}'`);
140
164
  }
141
165
  }
142
166
  }
143
- const component = { dir: entry.dir };
167
+ const workspace = { dir: entry.dir };
144
168
  if (entry.shouldExclude !== void 0) {
145
169
  if (typeof entry.shouldExclude === "boolean") {
146
- component.shouldExclude = entry.shouldExclude;
170
+ workspace.shouldExclude = entry.shouldExclude;
147
171
  } else {
148
- errors.push(`components[${i}]: 'shouldExclude' must be a boolean`);
172
+ errors.push(`workspaces[${i}]: 'shouldExclude' must be a boolean`);
173
+ }
174
+ }
175
+ if (entry.legacyIdentities !== void 0) {
176
+ const identities = validateLegacyIdentities(entry.legacyIdentities, i, errors);
177
+ if (identities !== void 0) {
178
+ workspace.legacyIdentities = identities;
179
+ }
180
+ }
181
+ workspaces.push(workspace);
182
+ }
183
+ config.workspaces = workspaces;
184
+ }
185
+ function validateLegacyIdentities(value, workspaceIndex, errors) {
186
+ if (!Array.isArray(value)) {
187
+ errors.push(`workspaces[${workspaceIndex}]: 'legacyIdentities' must be an array`);
188
+ return void 0;
189
+ }
190
+ const knownIdentityFields = /* @__PURE__ */ new Set(["name", "tagPrefix"]);
191
+ const identities = [];
192
+ const seenTuples = /* @__PURE__ */ new Set();
193
+ for (const [entryIndex, entry] of value.entries()) {
194
+ if (!isRecord(entry)) {
195
+ errors.push(`workspaces[${workspaceIndex}].legacyIdentities[${entryIndex}]: must be an object`);
196
+ continue;
197
+ }
198
+ let entryValid = true;
199
+ for (const key2 of Object.keys(entry)) {
200
+ if (!knownIdentityFields.has(key2)) {
201
+ errors.push(`workspaces[${workspaceIndex}].legacyIdentities[${entryIndex}]: unknown field '${key2}'`);
202
+ entryValid = false;
203
+ }
204
+ }
205
+ const { name, tagPrefix } = entry;
206
+ if (typeof name !== "string") {
207
+ errors.push(`workspaces[${workspaceIndex}].legacyIdentities[${entryIndex}].name: must be a string`);
208
+ entryValid = false;
209
+ } else if (name === "") {
210
+ errors.push(`workspaces[${workspaceIndex}].legacyIdentities[${entryIndex}].name: must be a non-empty string`);
211
+ entryValid = false;
212
+ }
213
+ if (typeof tagPrefix !== "string") {
214
+ errors.push(`workspaces[${workspaceIndex}].legacyIdentities[${entryIndex}].tagPrefix: must be a string`);
215
+ entryValid = false;
216
+ } else if (tagPrefix === "") {
217
+ errors.push(
218
+ `workspaces[${workspaceIndex}].legacyIdentities[${entryIndex}].tagPrefix: must be a non-empty string`
219
+ );
220
+ entryValid = false;
221
+ }
222
+ if (!entryValid || typeof name !== "string" || typeof tagPrefix !== "string") {
223
+ continue;
224
+ }
225
+ const key = `${name}\0${tagPrefix}`;
226
+ if (seenTuples.has(key)) {
227
+ errors.push(
228
+ `workspaces[${workspaceIndex}].legacyIdentities[${entryIndex}]: duplicate identity (name='${name}', tagPrefix='${tagPrefix}')`
229
+ );
230
+ continue;
231
+ }
232
+ seenTuples.add(key);
233
+ identities.push({ name, tagPrefix });
234
+ }
235
+ return identities;
236
+ }
237
+ function validateRetiredPackages(value, config, errors) {
238
+ if (value === void 0) return;
239
+ if (!Array.isArray(value)) {
240
+ errors.push("'retiredPackages' must be an array");
241
+ return;
242
+ }
243
+ const validEntries = [];
244
+ const seenTuples = /* @__PURE__ */ new Set();
245
+ for (const [i, entry] of value.entries()) {
246
+ const retired = validateRetiredPackageEntry(entry, i, errors);
247
+ if (retired === void 0) continue;
248
+ const key = `${retired.name}\0${retired.tagPrefix}`;
249
+ if (seenTuples.has(key)) {
250
+ errors.push(
251
+ `retiredPackages[${i}]: duplicate package (name='${retired.name}', tagPrefix='${retired.tagPrefix}')`
252
+ );
253
+ continue;
254
+ }
255
+ seenTuples.add(key);
256
+ validEntries.push({ entry: retired, rawIndex: i });
257
+ }
258
+ detectRetiredVsLegacyCollisions(validEntries, config, errors);
259
+ config.retiredPackages = validEntries.map(({ entry }) => entry);
260
+ }
261
+ function validateRetiredPackageEntry(entry, i, errors) {
262
+ if (!isRecord(entry)) {
263
+ errors.push(`retiredPackages[${i}]: must be an object`);
264
+ return void 0;
265
+ }
266
+ const knownRetiredFields = /* @__PURE__ */ new Set(["name", "tagPrefix", "successor"]);
267
+ let entryValid = true;
268
+ for (const key of Object.keys(entry)) {
269
+ if (!knownRetiredFields.has(key)) {
270
+ errors.push(`retiredPackages[${i}]: unknown field '${key}'`);
271
+ entryValid = false;
272
+ }
273
+ }
274
+ const { name, tagPrefix, successor } = entry;
275
+ if (!validateNonEmptyString(name, `retiredPackages[${i}].name`, errors)) {
276
+ entryValid = false;
277
+ }
278
+ if (!validateNonEmptyString(tagPrefix, `retiredPackages[${i}].tagPrefix`, errors)) {
279
+ entryValid = false;
280
+ }
281
+ if (successor !== void 0 && !validateNonEmptyString(successor, `retiredPackages[${i}].successor`, errors)) {
282
+ entryValid = false;
283
+ }
284
+ if (!entryValid || typeof name !== "string" || typeof tagPrefix !== "string") {
285
+ return void 0;
286
+ }
287
+ const retired = { name, tagPrefix };
288
+ if (typeof successor === "string" && successor !== "") {
289
+ retired.successor = successor;
290
+ }
291
+ return retired;
292
+ }
293
+ function detectRetiredVsLegacyCollisions(retiredPackages, config, errors) {
294
+ if (config.workspaces === void 0) return;
295
+ const legacyPrefixToWorkspace = /* @__PURE__ */ new Map();
296
+ for (const workspace of config.workspaces) {
297
+ if (workspace.legacyIdentities === void 0) continue;
298
+ for (const identity of workspace.legacyIdentities) {
299
+ if (!legacyPrefixToWorkspace.has(identity.tagPrefix)) {
300
+ legacyPrefixToWorkspace.set(identity.tagPrefix, workspace.dir);
149
301
  }
150
302
  }
151
- components.push(component);
152
303
  }
153
- config.components = components;
304
+ for (const { entry: retired, rawIndex } of retiredPackages) {
305
+ const collidingDir = legacyPrefixToWorkspace.get(retired.tagPrefix);
306
+ if (collidingDir !== void 0) {
307
+ errors.push(
308
+ `retiredPackages[${rawIndex}]: tagPrefix '${retired.tagPrefix}' collides with a declared legacyIdentities[].tagPrefix on workspace '${collidingDir}'`
309
+ );
310
+ }
311
+ }
312
+ }
313
+ function validateNonEmptyString(value, fieldPath, errors) {
314
+ if (typeof value !== "string") {
315
+ errors.push(`${fieldPath}: must be a string`);
316
+ return false;
317
+ }
318
+ if (value === "") {
319
+ errors.push(`${fieldPath}: must be a non-empty string`);
320
+ return false;
321
+ }
322
+ return true;
154
323
  }
155
324
  function validateVersionPatterns(value, config, errors) {
156
325
  if (value === void 0) return;
@@ -0,0 +1,14 @@
1
+ import type { DependencyGraph } from './buildDependencyGraph.ts';
2
+ import type { WorkspaceConfig } from './types.ts';
3
+ export interface CommitsProbeResult {
4
+ has: boolean;
5
+ tag: string | undefined;
6
+ }
7
+ export interface StrandedDependentViolation {
8
+ dir: string;
9
+ downstreamOf: string;
10
+ tag: string | undefined;
11
+ }
12
+ type CommitsProbe = (workspace: WorkspaceConfig) => CommitsProbeResult;
13
+ export declare function validateOnlyExcludesStrandedDependents(workspaces: readonly WorkspaceConfig[], only: readonly string[], graph: DependencyGraph, hasCommits: CommitsProbe): StrandedDependentViolation[] | undefined;
14
+ export {};