@williamthorsen/release-kit 5.2.1 → 5.3.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 (46) hide show
  1. package/CHANGELOG.md +76 -26
  2. package/README.md +223 -31
  3. package/cliff.toml.template +4 -39
  4. package/dist/esm/.cache +1 -1
  5. package/dist/esm/bin/release-kit.js +59 -0
  6. package/dist/esm/buildChangelogEntries.js +6 -0
  7. package/dist/esm/buildEmptyReleaseEntry.d.ts +2 -0
  8. package/dist/esm/buildEmptyReleaseEntry.js +16 -0
  9. package/dist/esm/changelogJsonFile.d.ts +2 -0
  10. package/dist/esm/changelogJsonFile.js +11 -1
  11. package/dist/esm/changelogJsonUtils.d.ts +2 -1
  12. package/dist/esm/changelogJsonUtils.js +9 -0
  13. package/dist/esm/changelogOverrides.d.ts +53 -0
  14. package/dist/esm/changelogOverrides.js +424 -0
  15. package/dist/esm/checkWorkTypesDrift.js +3 -2
  16. package/dist/esm/defaults.js +1 -1
  17. package/dist/esm/generateChangelogs.d.ts +2 -3
  18. package/dist/esm/generateChangelogs.js +4 -34
  19. package/dist/esm/index.d.ts +2 -0
  20. package/dist/esm/index.js +8 -0
  21. package/dist/esm/loadConfig.d.ts +1 -1
  22. package/dist/esm/releasePrepare.js +68 -11
  23. package/dist/esm/releasePrepareMono.js +103 -59
  24. package/dist/esm/releasePrepareProject.d.ts +4 -1
  25. package/dist/esm/releasePrepareProject.js +74 -18
  26. package/dist/esm/renderChangelogMarkdown.d.ts +12 -0
  27. package/dist/esm/renderChangelogMarkdown.js +32 -0
  28. package/dist/esm/renderReleaseNotes.js +6 -1
  29. package/dist/esm/resolveReleaseNotesConfig.js +3 -1
  30. package/dist/esm/runGitCliff.d.ts +1 -0
  31. package/dist/esm/runGitCliff.js +7 -0
  32. package/dist/esm/syncWorkTypes.js +3 -2
  33. package/dist/esm/types.d.ts +98 -31
  34. package/dist/esm/types.js +60 -0
  35. package/dist/esm/validateConfig.js +84 -345
  36. package/dist/esm/validateOverridesCommand.d.ts +14 -0
  37. package/dist/esm/validateOverridesCommand.js +136 -0
  38. package/dist/esm/work-types.json +23 -17
  39. package/dist/esm/work-types.schema.json +28 -1
  40. package/dist/esm/workTypesData.d.ts +8 -0
  41. package/dist/esm/workTypesData.js +20 -17
  42. package/dist/esm/workTypesUtils.d.ts +1 -0
  43. package/dist/esm/workTypesUtils.js +8 -0
  44. package/package.json +5 -4
  45. package/dist/esm/writeSyntheticChangelog.d.ts +0 -9
  46. package/dist/esm/writeSyntheticChangelog.js +0 -27
@@ -1,37 +1,23 @@
1
1
  import { isRecord } from "./typeGuards.js";
2
+ import { releaseKitConfigSchema } from "./types.js";
2
3
  function validateConfig(raw) {
3
- const errors = [];
4
4
  if (!isRecord(raw)) {
5
5
  return { config: {}, errors: ["Config must be an object"], warnings: [] };
6
6
  }
7
- const config = {};
8
- const knownFields = /* @__PURE__ */ new Set([
9
- "changelogJson",
10
- "cliffConfigPath",
11
- "formatCommand",
12
- "project",
13
- "releaseNotes",
14
- "retiredPackages",
15
- "scopeAliases",
16
- "versionPatterns",
17
- "workspaces",
18
- "workTypes"
19
- ]);
20
- for (const key of Object.keys(raw)) {
21
- if (!knownFields.has(key)) {
22
- errors.push(`Unknown field: '${key}'`);
23
- }
24
- }
25
- validateChangelogJson(raw.changelogJson, config, errors);
26
- validateWorkspaces(raw.workspaces, config, errors);
27
- validateReleaseNotes(raw.releaseNotes, config, errors);
28
- validateVersionPatterns(raw.versionPatterns, config, errors);
29
- validateWorkTypes(raw.workTypes, config, errors);
30
- validateStringField("formatCommand", raw.formatCommand, config, errors);
31
- validateStringField("cliffConfigPath", raw.cliffConfigPath, config, errors);
32
- validateScopeAliases(raw.scopeAliases, config, errors);
33
- validateRetiredPackages(raw.retiredPackages, config, errors);
34
- validateProjectConfig(raw.project, config, errors);
7
+ const { cleaned, deprecationErrors } = preprocessDeprecatedKeys(raw);
8
+ const parseResult = releaseKitConfigSchema.safeParse(cleaned);
9
+ if (!parseResult.success) {
10
+ return {
11
+ config: {},
12
+ errors: [...deprecationErrors, ...parseResult.error.issues.map(formatZodIssue)],
13
+ warnings: []
14
+ };
15
+ }
16
+ const config = parseResult.data;
17
+ const errors = [...deprecationErrors];
18
+ detectLegacyIdentityDuplicates(config, errors);
19
+ detectRetiredPackageDuplicates(config, errors);
20
+ detectRetiredVsLegacyCollisions(config, errors);
35
21
  const warnings = [];
36
22
  const changelogJsonEnabled = config.changelogJson?.enabled ?? true;
37
23
  if (!changelogJsonEnabled && config.releaseNotes?.shouldInjectIntoReadme) {
@@ -41,257 +27,93 @@ function validateConfig(raw) {
41
27
  }
42
28
  return { config, errors, warnings };
43
29
  }
44
- function validateChangelogJson(value, config, errors) {
45
- if (value === void 0) return;
46
- if (!isRecord(value)) {
47
- errors.push("'changelogJson' must be an object");
48
- return;
49
- }
50
- const knownChangelogJsonFields = /* @__PURE__ */ new Set(["enabled", "outputPath", "devOnlySections"]);
51
- for (const key of Object.keys(value)) {
52
- if (!knownChangelogJsonFields.has(key)) {
53
- errors.push(`changelogJson: unknown field '${key}'`);
54
- }
55
- }
56
- const result = {};
57
- if (value.enabled !== void 0) {
58
- if (typeof value.enabled === "boolean") {
59
- result.enabled = value.enabled;
60
- } else {
61
- errors.push("changelogJson.enabled: must be a boolean");
62
- }
63
- }
64
- if (value.outputPath !== void 0) {
65
- if (typeof value.outputPath === "string") {
66
- result.outputPath = value.outputPath;
67
- } else {
68
- errors.push("changelogJson.outputPath: must be a string");
69
- }
70
- }
71
- if (value.devOnlySections !== void 0) {
72
- if (isStringArray(value.devOnlySections)) {
73
- result.devOnlySections = value.devOnlySections;
74
- } else {
75
- errors.push("changelogJson.devOnlySections: must be a string array");
76
- }
77
- }
78
- config.changelogJson = result;
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
- }
104
- function validateReleaseNotes(value, config, errors) {
105
- if (value === void 0) return;
106
- if (!isRecord(value)) {
107
- errors.push("'releaseNotes' must be an object");
108
- return;
109
- }
110
- const knownReleaseNotesFields = /* @__PURE__ */ new Set(["shouldInjectIntoReadme"]);
111
- for (const key of Object.keys(value)) {
112
- if (!knownReleaseNotesFields.has(key)) {
113
- if (key === "shouldCreateGithubRelease") {
30
+ function preprocessDeprecatedKeys(raw) {
31
+ if (!isRecord(raw)) return { cleaned: raw, deprecationErrors: [] };
32
+ const errors = [];
33
+ const cleaned = { ...raw };
34
+ if (isRecord(cleaned.releaseNotes) && "shouldCreateGithubRelease" in cleaned.releaseNotes) {
35
+ errors.push(
36
+ "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."
37
+ );
38
+ const releaseNotesCopy = { ...cleaned.releaseNotes };
39
+ delete releaseNotesCopy.shouldCreateGithubRelease;
40
+ cleaned.releaseNotes = releaseNotesCopy;
41
+ }
42
+ if (Array.isArray(cleaned.workspaces)) {
43
+ cleaned.workspaces = cleaned.workspaces.map((ws, i) => {
44
+ if (!isRecord(ws)) return ws;
45
+ const wsCopy = { ...ws };
46
+ if ("tagPrefix" in wsCopy) {
47
+ const dir = typeof wsCopy.dir === "string" && wsCopy.dir !== "" ? wsCopy.dir : "<dir>";
48
+ errors.push(`workspaces[${i}]: 'tagPrefix' is no longer supported; remove it to use the default '${dir}-v'`);
49
+ delete wsCopy.tagPrefix;
50
+ }
51
+ if ("legacyTagPrefixes" in wsCopy) {
114
52
  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."
53
+ `workspaces[${i}]: 'legacyTagPrefixes' is no longer supported; use 'legacyIdentities: [{ name, tagPrefix }, ...]' instead`
116
54
  );
117
- } else {
118
- errors.push(`releaseNotes: unknown field '${key}'`);
55
+ delete wsCopy.legacyTagPrefixes;
119
56
  }
120
- }
57
+ return wsCopy;
58
+ });
121
59
  }
122
- const result = {};
123
- if (value.shouldInjectIntoReadme !== void 0) {
124
- if (typeof value.shouldInjectIntoReadme === "boolean") {
125
- result.shouldInjectIntoReadme = value.shouldInjectIntoReadme;
126
- } else {
127
- errors.push("releaseNotes.shouldInjectIntoReadme: must be a boolean");
128
- }
129
- }
130
- config.releaseNotes = result;
60
+ return { cleaned, deprecationErrors: errors };
131
61
  }
132
- function isStringArray(value) {
133
- return Array.isArray(value) && value.every((item) => typeof item === "string");
62
+ function formatZodIssue(issue) {
63
+ const path = renderPath(issue.path);
64
+ const message = customizeMessage(issue);
65
+ return path === "" ? message : `${path}: ${message}`;
134
66
  }
135
- function validateWorkspaces(value, config, errors) {
136
- if (value === void 0) return;
137
- if (!Array.isArray(value)) {
138
- errors.push("'workspaces' must be an array");
139
- return;
67
+ function customizeMessage(issue) {
68
+ if (issue.code === "too_small" && issue.origin === "string") {
69
+ return "must be a non-empty string";
140
70
  }
141
- const workspaces = [];
142
- const knownWorkspaceFields = /* @__PURE__ */ new Set(["dir", "shouldExclude", "legacyIdentities"]);
143
- for (const [i, entry] of value.entries()) {
144
- if (!isRecord(entry)) {
145
- errors.push(`workspaces[${i}]: must be an object`);
146
- continue;
147
- }
148
- if (typeof entry.dir !== "string" || entry.dir === "") {
149
- errors.push(`workspaces[${i}]: 'dir' is required`);
150
- continue;
151
- }
152
- for (const key of Object.keys(entry)) {
153
- if (!knownWorkspaceFields.has(key)) {
154
- if (key === "tagPrefix") {
155
- errors.push(
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`
161
- );
162
- } else {
163
- errors.push(`workspaces[${i}]: unknown field '${key}'`);
164
- }
165
- }
166
- }
167
- const workspace = { dir: entry.dir };
168
- if (entry.shouldExclude !== void 0) {
169
- if (typeof entry.shouldExclude === "boolean") {
170
- workspace.shouldExclude = entry.shouldExclude;
171
- } else {
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
- }
71
+ return issue.message;
72
+ }
73
+ function renderPath(path) {
74
+ let rendered = "";
75
+ for (const segment of path) {
76
+ if (typeof segment === "number") {
77
+ rendered += `[${segment}]`;
78
+ } else if (rendered === "") {
79
+ rendered += String(segment);
80
+ } else {
81
+ rendered += `.${String(segment)}`;
180
82
  }
181
- workspaces.push(workspace);
182
83
  }
183
- config.workspaces = workspaces;
84
+ return rendered;
184
85
  }
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;
86
+ function detectLegacyIdentityDuplicates(config, errors) {
87
+ if (config.workspaces === void 0) return;
88
+ for (const [wsIndex, workspace] of config.workspaces.entries()) {
89
+ if (workspace.legacyIdentities === void 0) continue;
90
+ const seen = /* @__PURE__ */ new Set();
91
+ for (const [entryIndex, identity] of workspace.legacyIdentities.entries()) {
92
+ const key = `${identity.name}\0${identity.tagPrefix}`;
93
+ if (seen.has(key)) {
94
+ errors.push(
95
+ `workspaces[${wsIndex}].legacyIdentities[${entryIndex}]: duplicate identity (name='${identity.name}', tagPrefix='${identity.tagPrefix}')`
96
+ );
203
97
  }
98
+ seen.add(key);
204
99
  }
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
100
  }
235
- return identities;
236
101
  }
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;
102
+ function detectRetiredPackageDuplicates(config, errors) {
103
+ if (config.retiredPackages === void 0) return;
104
+ const seen = /* @__PURE__ */ new Set();
105
+ for (const [index, retired] of config.retiredPackages.entries()) {
248
106
  const key = `${retired.name}\0${retired.tagPrefix}`;
249
- if (seenTuples.has(key)) {
107
+ if (seen.has(key)) {
250
108
  errors.push(
251
- `retiredPackages[${i}]: duplicate package (name='${retired.name}', tagPrefix='${retired.tagPrefix}')`
109
+ `retiredPackages[${index}]: duplicate package (name='${retired.name}', tagPrefix='${retired.tagPrefix}')`
252
110
  );
253
- continue;
254
111
  }
255
- seenTuples.add(key);
256
- validEntries.push({ entry: retired, rawIndex: i });
112
+ seen.add(key);
257
113
  }
258
- detectRetiredVsLegacyCollisions(validEntries, config, errors);
259
- config.retiredPackages = validEntries.map(({ entry }) => entry);
260
114
  }
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;
115
+ function detectRetiredVsLegacyCollisions(config, errors) {
116
+ if (config.retiredPackages === void 0 || config.workspaces === void 0) return;
295
117
  const legacyPrefixToWorkspace = /* @__PURE__ */ new Map();
296
118
  for (const workspace of config.workspaces) {
297
119
  if (workspace.legacyIdentities === void 0) continue;
@@ -301,98 +123,15 @@ function detectRetiredVsLegacyCollisions(retiredPackages, config, errors) {
301
123
  }
302
124
  }
303
125
  }
304
- for (const { entry: retired, rawIndex } of retiredPackages) {
126
+ for (const [index, retired] of config.retiredPackages.entries()) {
305
127
  const collidingDir = legacyPrefixToWorkspace.get(retired.tagPrefix);
306
128
  if (collidingDir !== void 0) {
307
129
  errors.push(
308
- `retiredPackages[${rawIndex}]: tagPrefix '${retired.tagPrefix}' collides with a declared legacyIdentities[].tagPrefix on workspace '${collidingDir}'`
130
+ `retiredPackages[${index}]: tagPrefix '${retired.tagPrefix}' collides with a declared legacyIdentities[].tagPrefix on workspace '${collidingDir}'`
309
131
  );
310
132
  }
311
133
  }
312
134
  }
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;
323
- }
324
- function validateVersionPatterns(value, config, errors) {
325
- if (value === void 0) return;
326
- if (!isRecord(value)) {
327
- errors.push("'versionPatterns' must be an object");
328
- return;
329
- }
330
- if (!isStringArray(value.major)) {
331
- errors.push("versionPatterns.major: expected string array");
332
- }
333
- if (!isStringArray(value.minor)) {
334
- errors.push("versionPatterns.minor: expected string array");
335
- }
336
- if (isStringArray(value.major) && isStringArray(value.minor)) {
337
- config.versionPatterns = { major: value.major, minor: value.minor };
338
- }
339
- }
340
- function validateWorkTypes(value, config, errors) {
341
- if (value === void 0) return;
342
- if (!isRecord(value) || Array.isArray(value)) {
343
- errors.push("'workTypes' must be a record (object)");
344
- return;
345
- }
346
- const workTypes = {};
347
- for (const [key, entry] of Object.entries(value)) {
348
- if (!isRecord(entry)) {
349
- errors.push(`workTypes.${key}: must be an object`);
350
- continue;
351
- }
352
- if (typeof entry.header !== "string") {
353
- errors.push(`workTypes.${key}: 'header' is required and must be a string`);
354
- continue;
355
- }
356
- const wtEntry = { header: entry.header };
357
- if (entry.aliases !== void 0) {
358
- if (isStringArray(entry.aliases)) {
359
- wtEntry.aliases = entry.aliases;
360
- } else {
361
- errors.push(`workTypes.${key}: 'aliases' must be a string array`);
362
- }
363
- }
364
- workTypes[key] = wtEntry;
365
- }
366
- config.workTypes = workTypes;
367
- }
368
- function validateStringField(fieldName, value, config, errors) {
369
- if (value === void 0) return;
370
- if (typeof value !== "string") {
371
- errors.push(`'${fieldName}' must be a string`);
372
- return;
373
- }
374
- config[fieldName] = value;
375
- }
376
- function validateScopeAliases(value, config, errors) {
377
- if (value === void 0) return;
378
- if (!isRecord(value)) {
379
- errors.push("'scopeAliases' must be a record (object)");
380
- return;
381
- }
382
- const aliases = {};
383
- let valid = true;
384
- for (const [key, v] of Object.entries(value)) {
385
- if (typeof v === "string") {
386
- aliases[key] = v;
387
- } else {
388
- errors.push(`scopeAliases.${key}: value must be a string`);
389
- valid = false;
390
- }
391
- }
392
- if (valid) {
393
- config.scopeAliases = aliases;
394
- }
395
- }
396
135
  export {
397
136
  validateConfig
398
137
  };
@@ -0,0 +1,14 @@
1
+ import { type ValidateAllChangelogOverridesInputs, type ValidateAllChangelogOverridesResult } from './changelogOverrides.ts';
2
+ import type { ChangelogEntry, ReleaseConfig } from './types.ts';
3
+ export interface ValidateOverridesCommandResult {
4
+ exitCode: 0 | 1 | 2;
5
+ message: string;
6
+ }
7
+ export interface ValidateOverridesCommandDependencies {
8
+ discoverWorkspaces?: () => Promise<string[] | undefined>;
9
+ loadConfig?: () => Promise<unknown>;
10
+ buildEntries?: (config: Pick<ReleaseConfig, 'cliffConfigPath' | 'changelogJson'>, tagPattern?: string, includePaths?: string[]) => ChangelogEntry[];
11
+ validate?: (inputs: ValidateAllChangelogOverridesInputs) => ValidateAllChangelogOverridesResult;
12
+ }
13
+ export declare function validateOverridesCommand(dependencies?: ValidateOverridesCommandDependencies): Promise<ValidateOverridesCommandResult>;
14
+ export declare function formatValidateOverridesResult(result: ValidateAllChangelogOverridesResult): ValidateOverridesCommandResult;
@@ -0,0 +1,136 @@
1
+ import { buildChangelogEntries } from "./buildChangelogEntries.js";
2
+ import {
3
+ resolveOverridePath,
4
+ validateAllChangelogOverrides
5
+ } from "./changelogOverrides.js";
6
+ import { discoverWorkspaces } from "./discoverWorkspaces.js";
7
+ import { buildTagPattern, getAllTagPrefixes } from "./generateChangelogs.js";
8
+ import { loadConfig, mergeMonorepoConfig, mergeSinglePackageConfig, readRootPackageVersion } from "./loadConfig.js";
9
+ import { validateConfig } from "./validateConfig.js";
10
+ const SYNTHETIC_VALIDATE_TAG = "validate-only";
11
+ async function validateOverridesCommand(dependencies = {}) {
12
+ const discover = dependencies.discoverWorkspaces ?? discoverWorkspaces;
13
+ const load = dependencies.loadConfig ?? loadConfig;
14
+ const buildEntries = dependencies.buildEntries ?? defaultBuildEntries;
15
+ const validate = dependencies.validate ?? validateAllChangelogOverrides;
16
+ let userConfig;
17
+ try {
18
+ userConfig = await loadAndValidateConfig(load);
19
+ } catch (error) {
20
+ return { exitCode: 2, message: errorMessage(error) };
21
+ }
22
+ let discoveredPaths;
23
+ try {
24
+ discoveredPaths = await discover();
25
+ } catch (error) {
26
+ return { exitCode: 2, message: `Error discovering workspaces: ${errorMessage(error)}` };
27
+ }
28
+ let inputs;
29
+ try {
30
+ inputs = discoveredPaths === void 0 ? buildSinglePackageInputs(userConfig, buildEntries) : buildMonorepoInputs(discoveredPaths, userConfig, buildEntries);
31
+ } catch (error) {
32
+ return { exitCode: 2, message: `Error resolving overrides scope: ${errorMessage(error)}` };
33
+ }
34
+ const result = validate(inputs);
35
+ return formatValidateOverridesResult(result);
36
+ }
37
+ function formatValidateOverridesResult(result) {
38
+ const { errors, warnings } = result;
39
+ if (errors.length === 0 && warnings.length === 0) {
40
+ return { exitCode: 0, message: "All override files are valid (no errors, no stale keys)." };
41
+ }
42
+ const exitCode = errors.length > 0 ? 2 : 1;
43
+ const summary = formatSummaryLine(errors.length, warnings.length);
44
+ const errorLines = errors.map((message2) => ` \u274C ${message2}`);
45
+ const warningLines = warnings.map((message2) => ` \u26A0\uFE0F ${message2}`);
46
+ const message = [summary, "", ...errorLines, ...warningLines].join("\n");
47
+ return { exitCode, message };
48
+ }
49
+ function formatSummaryLine(errorCount, warningCount) {
50
+ const parts = [];
51
+ if (errorCount > 0) {
52
+ parts.push(pluralize(errorCount, "error"));
53
+ }
54
+ if (warningCount > 0) {
55
+ parts.push(pluralize(warningCount, "warning"));
56
+ }
57
+ return `Found ${parts.join(" and ")}:`;
58
+ }
59
+ function pluralize(count, noun) {
60
+ return count === 1 ? `${count} ${noun}` : `${count} ${noun}s`;
61
+ }
62
+ function errorMessage(error) {
63
+ return error instanceof Error ? error.message : String(error);
64
+ }
65
+ function defaultBuildEntries(config, tagPattern, includePaths) {
66
+ const options = {};
67
+ if (tagPattern !== void 0) {
68
+ options.tagPattern = tagPattern;
69
+ }
70
+ if (includePaths !== void 0) {
71
+ options.includePaths = includePaths;
72
+ }
73
+ return buildChangelogEntries(config, SYNTHETIC_VALIDATE_TAG, options);
74
+ }
75
+ function flattenEntriesToHashes(entries) {
76
+ const hashes = [];
77
+ for (const entry of entries) {
78
+ for (const section of entry.sections) {
79
+ for (const item of section.items) {
80
+ if (item.hash !== void 0) {
81
+ hashes.push(item.hash);
82
+ }
83
+ }
84
+ }
85
+ }
86
+ return hashes;
87
+ }
88
+ async function loadAndValidateConfig(load) {
89
+ let rawConfig;
90
+ try {
91
+ rawConfig = await load();
92
+ } catch (error) {
93
+ throw new Error(`Error loading config: ${errorMessage(error)}`);
94
+ }
95
+ if (rawConfig === void 0) {
96
+ return void 0;
97
+ }
98
+ const { config, errors } = validateConfig(rawConfig);
99
+ if (errors.length > 0) {
100
+ throw new Error(`Invalid config:
101
+ - ${errors.join("\n - ")}`);
102
+ }
103
+ return config;
104
+ }
105
+ function buildSinglePackageInputs(userConfig, buildEntries) {
106
+ const config = mergeSinglePackageConfig(userConfig);
107
+ const hashes = flattenEntriesToHashes(buildEntries(config));
108
+ return {
109
+ project: { filePath: resolveOverridePath("."), hashes }
110
+ };
111
+ }
112
+ function buildMonorepoInputs(discoveredPaths, userConfig, buildEntries) {
113
+ const rootPackage = readRootPackageVersion();
114
+ const config = mergeMonorepoConfig(discoveredPaths, userConfig, rootPackage);
115
+ const workspaces = config.workspaces.map((workspace) => {
116
+ const tagPattern = buildTagPattern(getAllTagPrefixes(workspace));
117
+ return {
118
+ filePath: resolveOverridePath(workspace.workspacePath),
119
+ hashes: flattenEntriesToHashes(buildEntries(config, tagPattern, workspace.paths))
120
+ };
121
+ });
122
+ const project = config.project;
123
+ const projectScope = {
124
+ filePath: resolveOverridePath(".")
125
+ };
126
+ if (project !== void 0) {
127
+ const contributingPaths = config.workspaces.flatMap((workspace) => workspace.paths);
128
+ const projectTagPattern = buildTagPattern([project.tagPrefix]);
129
+ projectScope.hashes = flattenEntriesToHashes(buildEntries(config, projectTagPattern, contributingPaths));
130
+ }
131
+ return { project: projectScope, workspaces };
132
+ }
133
+ export {
134
+ formatValidateOverridesResult,
135
+ validateOverridesCommand
136
+ };