@pob/version 1.0.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 (69) hide show
  1. package/.editorconfig +13 -0
  2. package/.yo-rc.json +13 -0
  3. package/CHANGELOG.md +35 -0
  4. package/LICENSE +21 -0
  5. package/README.md +163 -0
  6. package/dist/index-node.mjs +1419 -0
  7. package/dist/index-node.mjs.map +1 -0
  8. package/package.json +81 -0
  9. package/rollup.config.mjs +12 -0
  10. package/src/__fixtures__/bun-basic/package.json +4 -0
  11. package/src/__fixtures__/bun-no-version/package.json +3 -0
  12. package/src/__fixtures__/yarn-basic/package.json +4 -0
  13. package/src/__fixtures__/yarn-basic/yarn.lock +12 -0
  14. package/src/__fixtures__/yarn-monorepo/package.json +7 -0
  15. package/src/__fixtures__/yarn-monorepo/packages/package-1/package.json +4 -0
  16. package/src/__fixtures__/yarn-monorepo/packages/package-2/package.json +7 -0
  17. package/src/__fixtures__/yarn-monorepo/packages/package-3/package.json +7 -0
  18. package/src/__fixtures__/yarn-monorepo/packages/package-4/package.json +7 -0
  19. package/src/__fixtures__/yarn-monorepo/packages/package-5/package.json +8 -0
  20. package/src/__fixtures__/yarn-monorepo/yarn.lock +0 -0
  21. package/src/__fixtures__/yarn-monorepo-duplicate-names/package.json +7 -0
  22. package/src/__fixtures__/yarn-monorepo-duplicate-names/packages/pkg-a/package.json +4 -0
  23. package/src/__fixtures__/yarn-monorepo-duplicate-names/packages/pkg-b/package.json +4 -0
  24. package/src/__fixtures__/yarn-monorepo-invalid-package-json/package.json +7 -0
  25. package/src/__fixtures__/yarn-monorepo-invalid-package-json/packages/pkg-with-invalid-json/package.json +5 -0
  26. package/src/__fixtures__/yarn-monorepo-nested/package.json +9 -0
  27. package/src/__fixtures__/yarn-monorepo-nested/packages/deep-nested/package.json +4 -0
  28. package/src/__fixtures__/yarn-monorepo-nested/packages/nested-1/packages/package-nested.json +4 -0
  29. package/src/__fixtures__/yarn-monorepo-nested/packages/nested-1/packages/package.json +4 -0
  30. package/src/__fixtures__/yarn-monorepo-nested/packages/package-1/package.json +4 -0
  31. package/src/__fixtures__/yarn-monorepo-no-name/package.json +7 -0
  32. package/src/__fixtures__/yarn-monorepo-no-name/packages/no-name-package/package.json +3 -0
  33. package/src/__fixtures__/yarn-monorepo-object-form/package.json +9 -0
  34. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-1/package.json +4 -0
  35. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-2/package.json +4 -0
  36. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-3/package.json +4 -0
  37. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-4/package.json +4 -0
  38. package/src/__fixtures__/yarn-monorepo-object-form/packages/package-5/package.json +4 -0
  39. package/src/__fixtures__/yarn-no-version/package.json +3 -0
  40. package/src/__fixtures__/yarn-no-version/yarn.lock +0 -0
  41. package/src/__fixtures__/yarn-with-lifecycle/.yarn/install-state.gz +0 -0
  42. package/src/__fixtures__/yarn-with-lifecycle/package.json +7 -0
  43. package/src/__fixtures__/yarn-with-lifecycle/yarn.lock +11 -0
  44. package/src/commands/version.test.ts +114 -0
  45. package/src/commands/version.ts +936 -0
  46. package/src/index.ts +13 -0
  47. package/src/missing-types.d.ts +10 -0
  48. package/src/pm/BunPackageManager.ts +12 -0
  49. package/src/pm/PackageManager.ts +66 -0
  50. package/src/pm/YarnPackageManager.ts +34 -0
  51. package/src/utils/UsageError.ts +6 -0
  52. package/src/utils/bumpTypeUtils.ts +113 -0
  53. package/src/utils/conventionalChangelogUtils.test.ts +762 -0
  54. package/src/utils/conventionalChangelogUtils.ts +104 -0
  55. package/src/utils/conventionalCommitConfigUtils.ts +20 -0
  56. package/src/utils/execCommand.ts +214 -0
  57. package/src/utils/generatorUtils.ts +11 -0
  58. package/src/utils/gitUtils.ts +193 -0
  59. package/src/utils/githubUtils.ts +114 -0
  60. package/src/utils/mapUtils.ts +7 -0
  61. package/src/utils/mapWorkspaces.test.ts +89 -0
  62. package/src/utils/packageDependenciesUtils.test.ts +25 -0
  63. package/src/utils/packageDependenciesUtils.ts +80 -0
  64. package/src/utils/packageUtils.test.ts +32 -0
  65. package/src/utils/packageUtils.ts +127 -0
  66. package/src/utils/updateChangelog.ts +39 -0
  67. package/src/utils/workspaceUtils.ts +142 -0
  68. package/tsconfig.json +21 -0
  69. package/vitest.config.js +9 -0
@@ -0,0 +1,936 @@
1
+ /* eslint-disable complexity */
2
+
3
+ import { Option, program } from "commander";
4
+ import { LoggerCLI } from "nightingale";
5
+ import { satisfies } from "semver";
6
+ import {
7
+ autoDetectPackageManager,
8
+ getPackageManager,
9
+ } from "../pm/PackageManager.ts";
10
+ import { UsageError } from "../utils/UsageError.ts";
11
+ import type { BumpType } from "../utils/bumpTypeUtils.ts";
12
+ import {
13
+ calcBumpRange,
14
+ calcBumpType,
15
+ getHighestBumpType,
16
+ incrementVersion,
17
+ } from "../utils/bumpTypeUtils.ts";
18
+ import {
19
+ createCommitsParser,
20
+ generateChangelog,
21
+ recommendBump,
22
+ } from "../utils/conventionalChangelogUtils.ts";
23
+ import { loadConventionalCommitConfig } from "../utils/conventionalCommitConfigUtils.ts";
24
+ import { asyncIterableToArray } from "../utils/generatorUtils.ts";
25
+ import type { GitTagVersion } from "../utils/gitUtils.ts";
26
+ import {
27
+ createGitCommit,
28
+ createGitTag,
29
+ getDirtyFiles,
30
+ getGitCommits,
31
+ getGitCurrentBranch,
32
+ getGitLatestTagVersion,
33
+ isBehindRemote,
34
+ pushCommitsAndTags,
35
+ } from "../utils/gitUtils.ts";
36
+ import {
37
+ createGhRelease,
38
+ ensureGhCliAvailable,
39
+ parseGithubRepoUrl,
40
+ } from "../utils/githubUtils.ts";
41
+ import {
42
+ PackageDependencyDescriptorUtils,
43
+ PackageDescriptorNameUtils,
44
+ } from "../utils/packageDependenciesUtils.ts";
45
+ import type { PackageDependencyDescriptor } from "../utils/packageDependenciesUtils.ts";
46
+ import type { Workspace } from "../utils/packageUtils.ts";
47
+ import {
48
+ createProjectWorkspace,
49
+ createWorkspace,
50
+ findRootWorkspace,
51
+ writePkg,
52
+ } from "../utils/packageUtils.ts";
53
+ import { updateChangelogFile } from "../utils/updateChangelog.ts";
54
+ import type { DependencyType } from "../utils/workspaceUtils.ts";
55
+ import {
56
+ buildDependenciesMaps,
57
+ buildTopologicalOrderBatches,
58
+ getWorkspaceName,
59
+ } from "../utils/workspaceUtils.ts";
60
+
61
+ export interface VersionCommandOptions {
62
+ cwd: string;
63
+ includesRoot: boolean;
64
+ dryRun: boolean;
65
+ force?: "major" | "minor" | "patch";
66
+ prerelease?: string;
67
+ json: boolean;
68
+ preset: string;
69
+ tagVersionPrefix: string;
70
+ changelog: string;
71
+ commitMessage: string;
72
+ createRelease: boolean;
73
+ bumpDependentsHighestAs: "major" | "minor" | "patch";
74
+ alwaysBumpPeerDependencies: boolean;
75
+ gitRemote: string;
76
+ ignoreChanges?: string;
77
+ verbose?: boolean;
78
+ cwdIsRoot?: boolean;
79
+ packageManager?: "bun" | "yarn";
80
+ publish?: boolean;
81
+ }
82
+
83
+ interface BumpableWorkspace {
84
+ workspace: Workspace;
85
+ previousTag?: string;
86
+ workspaceName: string;
87
+ isRoot: boolean;
88
+ version: string;
89
+ }
90
+
91
+ interface ChangedWorkspace {
92
+ bumpReason?: string;
93
+ bumpType: BumpType;
94
+ }
95
+
96
+ interface NoVersionToUpdateWorkspace {
97
+ // for legacy configuration not using workspaces ranges https://yarnpkg.com/features/workspaces#workspace-ranges-workspace
98
+ dependenciesToBump: [DependencyType, PackageDependencyDescriptor, string][];
99
+ }
100
+
101
+ interface BumpedWorkspace extends ChangedWorkspace, NoVersionToUpdateWorkspace {
102
+ bumpForDependenciesReasons?: string[];
103
+ hasChanged: boolean;
104
+ currentVersion: string;
105
+ newVersion: string;
106
+ newTag: string | null;
107
+ }
108
+
109
+ const todayInYYYYMMDD = (): string => {
110
+ // sv-SEis used for yyyy-mm-dd format
111
+ const dateFormatter = Intl.DateTimeFormat("sv-SE", {
112
+ timeZone: "UTC",
113
+ });
114
+ return dateFormatter.format(new Date());
115
+ };
116
+
117
+ export const versionCommandAction = async (
118
+ options: VersionCommandOptions,
119
+ { logger = new LoggerCLI("@pob/version", { json: options.json }) } = {},
120
+ ): Promise<void> => {
121
+ const rootWorkspace = await (options.cwdIsRoot
122
+ ? createWorkspace(options.cwd)
123
+ : findRootWorkspace(options.cwd));
124
+
125
+ if (!rootWorkspace) {
126
+ throw new UsageError("Could not find root workspace from this path.");
127
+ }
128
+
129
+ const packageManager = options.packageManager
130
+ ? getPackageManager(rootWorkspace, options.packageManager)
131
+ : autoDetectPackageManager(rootWorkspace);
132
+
133
+ const project = await createProjectWorkspace(rootWorkspace);
134
+
135
+ if (!options.dryRun) {
136
+ const dirtyFiles = await getDirtyFiles(project.root);
137
+ if (dirtyFiles) {
138
+ throw new Error(
139
+ `Dirty Files:\n${dirtyFiles}\n\nThere are uncommitted changes in the git repository. Please commit or stash them first.`,
140
+ );
141
+ }
142
+ }
143
+
144
+ const rootWorkspaceChildren = [...project.children.values()];
145
+ let rootNewVersion = "";
146
+ let rootNewTag = "";
147
+ const isMonorepo = rootWorkspaceChildren.length > 0;
148
+ const isMonorepoVersionIndependent = isMonorepo && !rootWorkspace.pkg.version;
149
+ const workspaces =
150
+ !(isMonorepo && !rootWorkspaceChildren) || options.includesRoot
151
+ ? [rootWorkspace, ...rootWorkspaceChildren]
152
+ : rootWorkspaceChildren;
153
+
154
+ if (options.prerelease) {
155
+ throw new UsageError("--prerelease is not supported yet.");
156
+ }
157
+ if (options.ignoreChanges) {
158
+ throw new UsageError("--ignore-changes is not supported yet.");
159
+ }
160
+
161
+ const [conventionalCommitConfig, parsedRepoUrl, gitCurrentBranch] =
162
+ await Promise.all([
163
+ loadConventionalCommitConfig(rootWorkspace, options.preset),
164
+ options.createRelease ? parseGithubRepoUrl(rootWorkspace) : undefined,
165
+ getGitCurrentBranch(rootWorkspace),
166
+ // ensure gh CLI is available early to fail fast if necessary
167
+ options.createRelease ? ensureGhCliAvailable(rootWorkspace) : undefined,
168
+ ]);
169
+
170
+ const rootPreviousVersionTagPromise =
171
+ options.force || isMonorepoVersionIndependent
172
+ ? null
173
+ : getGitLatestTagVersion(rootWorkspace, {
174
+ prefix: options.tagVersionPrefix,
175
+ skipUnstable: true,
176
+ });
177
+
178
+ const buildTagName = (workspace: Workspace, version: string): string =>
179
+ `${
180
+ isMonorepo && workspace !== rootWorkspace
181
+ ? `${getWorkspaceName(workspace)}@`
182
+ : options.tagVersionPrefix
183
+ }${version}`;
184
+
185
+ const changedWorkspaces = new Map<Workspace, ChangedWorkspace>();
186
+ const dependenciesMap = isMonorepo ? buildDependenciesMaps(project) : null;
187
+
188
+ const bumpableWorkspaces: BumpableWorkspace[] = [];
189
+
190
+ logger.group(
191
+ options.force
192
+ ? "Check all workspaces (force option)"
193
+ : "Finding changed workspaces",
194
+ () => {
195
+ // check workspaces and create bumpableWorkspaces
196
+
197
+ for (const workspace of workspaces) {
198
+ const workspaceName = getWorkspaceName(workspace);
199
+ const isRoot = workspace === rootWorkspace;
200
+ if (isRoot && isMonorepo && isMonorepoVersionIndependent) continue;
201
+
202
+ const version = workspace.pkg.version;
203
+
204
+ if (!version || version === "0.0.0") {
205
+ if (
206
+ (isRoot || isMonorepoVersionIndependent) &&
207
+ (!isMonorepo || !isMonorepoVersionIndependent)
208
+ ) {
209
+ throw new UsageError(
210
+ 'package.json has no version in its manifest. For the first release, set to "1.0.0-pre" or "0.1.0-pre".',
211
+ );
212
+ }
213
+
214
+ logger.info(`${workspaceName}: skipped (no version)`);
215
+ continue;
216
+ }
217
+
218
+ bumpableWorkspaces.push({
219
+ workspace,
220
+ workspaceName,
221
+ isRoot,
222
+ version,
223
+ });
224
+ }
225
+
226
+ if (bumpableWorkspaces.length > 0) {
227
+ logger.info("Found bumpable workspaces", {
228
+ count: bumpableWorkspaces.length,
229
+ });
230
+ }
231
+ },
232
+ );
233
+
234
+ const previousTagVersionByWorkspace = new Map<
235
+ Workspace,
236
+ GitTagVersion | null
237
+ >(
238
+ await Promise.all(
239
+ bumpableWorkspaces.map(async ({ workspace, workspaceName, isRoot }) => {
240
+ const packageOption =
241
+ isMonorepo && isMonorepoVersionIndependent
242
+ ? workspaceName
243
+ : undefined;
244
+
245
+ const tagPrefix = packageOption
246
+ ? `${packageOption}@`
247
+ : options.tagVersionPrefix;
248
+
249
+ const previousTagAndVersion = await (isRoot ||
250
+ !isMonorepoVersionIndependent
251
+ ? rootPreviousVersionTagPromise
252
+ : getGitLatestTagVersion(rootWorkspace, {
253
+ path: workspace.relativeCwd,
254
+ prefix: tagPrefix,
255
+ skipUnstable: true,
256
+ }));
257
+
258
+ return [workspace, previousTagAndVersion] as const;
259
+ }),
260
+ ),
261
+ );
262
+
263
+ if (options.dryRun) {
264
+ logger.info("Previous tags", {
265
+ previousTagByWorkspace: Object.fromEntries(
266
+ [...previousTagVersionByWorkspace.entries()].map(
267
+ ([workspace, previousTagVersion]) => [
268
+ getWorkspaceName(workspace),
269
+ previousTagVersion?.tag,
270
+ ],
271
+ ),
272
+ ),
273
+ });
274
+ }
275
+
276
+ const parseCommits = createCommitsParser(conventionalCommitConfig);
277
+
278
+ const commitsByWorkspace = options.force
279
+ ? undefined
280
+ : new Map(
281
+ await Promise.all(
282
+ bumpableWorkspaces.map(async ({ workspace }) => {
283
+ const previousTagVersion =
284
+ previousTagVersionByWorkspace.get(workspace);
285
+
286
+ const commits = await asyncIterableToArray(
287
+ parseCommits(
288
+ getGitCommits(rootWorkspace, {
289
+ path: workspace.relativeCwd,
290
+ from: previousTagVersion?.tag || undefined,
291
+ }),
292
+ ),
293
+ );
294
+
295
+ return [workspace, commits] as const;
296
+ }),
297
+ ),
298
+ );
299
+
300
+ await logger.group("Finding changed workspaces", async () => {
301
+ for (const { workspace, workspaceName } of bumpableWorkspaces) {
302
+ let bumpType: BumpType | null = null;
303
+ let bumpReason: string | undefined;
304
+
305
+ if (options.force) {
306
+ bumpType = options.force;
307
+ bumpReason = "forced by --force flag";
308
+ } else {
309
+ const commits = commitsByWorkspace?.get(workspace);
310
+
311
+ // No changes found for this package
312
+ if (!commits || commits.length === 0) {
313
+ logger.info(`${workspaceName}: skipped (no changes)`);
314
+ continue;
315
+ }
316
+
317
+ const { releaseType, reason } = await recommendBump(
318
+ commits,
319
+ conventionalCommitConfig,
320
+ );
321
+ bumpReason = reason;
322
+
323
+ if (releaseType) {
324
+ bumpType = releaseType as BumpType;
325
+ }
326
+ }
327
+
328
+ if (bumpType) {
329
+ if (isMonorepo && !workspace.pkg.name) {
330
+ throw new Error("Workspace name is required");
331
+ }
332
+
333
+ const currentVersion = workspace.pkg.version;
334
+
335
+ if (!currentVersion) {
336
+ throw new UsageError(
337
+ `Invalid "${getWorkspaceName(workspace)}" version`,
338
+ );
339
+ }
340
+
341
+ changedWorkspaces.set(workspace, {
342
+ bumpType,
343
+ bumpReason,
344
+ });
345
+ }
346
+ }
347
+
348
+ if (changedWorkspaces.size === 0) {
349
+ logger.info("No changed workspaces");
350
+ }
351
+ });
352
+
353
+ const bumpedWorkspaces = new Map<Workspace, BumpedWorkspace>();
354
+ const noVersionToUpdateWorkspaces = new Map<
355
+ Workspace,
356
+ NoVersionToUpdateWorkspace
357
+ >();
358
+ const batches = dependenciesMap
359
+ ? buildTopologicalOrderBatches(project, dependenciesMap)
360
+ : [[rootWorkspace]];
361
+
362
+ logger.group("Preparing bumping", () => {
363
+ for (const batch of batches) {
364
+ for (const workspace of batch) {
365
+ const currentVersion = workspace.pkg.version;
366
+
367
+ if (!currentVersion && !workspace.pkg.private) {
368
+ throw new UsageError(
369
+ `Invalid "${getWorkspaceName(workspace)}" version`,
370
+ );
371
+ }
372
+
373
+ const changedWorkspace = changedWorkspaces.get(workspace);
374
+ let bumpType: BumpType | null = null;
375
+ const bumpReasons: string[] = [];
376
+ const dependenciesToBump: BumpedWorkspace["dependenciesToBump"] = [];
377
+
378
+ if (changedWorkspace) {
379
+ bumpType = changedWorkspace.bumpType;
380
+ bumpReasons.push(changedWorkspace.bumpReason || "by commits");
381
+ }
382
+
383
+ const dependencies = dependenciesMap?.get(workspace);
384
+
385
+ if (dependencies) {
386
+ for (const [
387
+ dependencyWorkspace,
388
+ dependencyType,
389
+ dependencyDescriptor,
390
+ ] of dependencies) {
391
+ const dependencyBumpedWorkspace =
392
+ bumpedWorkspaces.get(dependencyWorkspace);
393
+
394
+ if (!dependencyBumpedWorkspace) {
395
+ continue;
396
+ }
397
+
398
+ if (
399
+ dependencyType === "peerDependencies" &&
400
+ !options.alwaysBumpPeerDependencies &&
401
+ // skip when peerdependency with a new version satisfied by the existing range.
402
+ satisfies(
403
+ dependencyBumpedWorkspace.newVersion,
404
+ dependencyDescriptor.selector,
405
+ { includePrerelease: true },
406
+ )
407
+ ) {
408
+ continue;
409
+ }
410
+
411
+ const newRange = calcBumpRange(
412
+ workspace,
413
+ dependencyDescriptor.selector,
414
+ dependencyBumpedWorkspace.newVersion,
415
+ );
416
+
417
+ if (dependencyDescriptor.selector === newRange) {
418
+ continue;
419
+ }
420
+
421
+ dependenciesToBump.push([
422
+ dependencyType,
423
+ dependencyDescriptor,
424
+ newRange,
425
+ ]);
426
+
427
+ bumpType = getHighestBumpType([
428
+ bumpType ?? "patch",
429
+ calcBumpType(
430
+ dependencyBumpedWorkspace.bumpType,
431
+ options.bumpDependentsHighestAs,
432
+ ),
433
+ ]);
434
+
435
+ bumpReasons.push(
436
+ `Version bump for dependency: ${PackageDescriptorNameUtils.stringify(dependencyDescriptor.name)}`,
437
+ );
438
+ }
439
+ }
440
+
441
+ const workspaceName = getWorkspaceName(workspace);
442
+ if (!currentVersion) {
443
+ logger.info(`${workspaceName}: skipped (no version)`);
444
+ if (workspace !== rootWorkspace) {
445
+ noVersionToUpdateWorkspaces.set(workspace, {
446
+ dependenciesToBump,
447
+ });
448
+ }
449
+ } else if (!bumpType) {
450
+ logger.info(
451
+ `${workspaceName}: skipped (${
452
+ changedWorkspace
453
+ ? `no bump recommended by ${options.preset}`
454
+ : "no changes"
455
+ })`,
456
+ );
457
+ } else {
458
+ const newVersion = incrementVersion(
459
+ workspace,
460
+ currentVersion,
461
+ bumpType,
462
+ );
463
+
464
+ const tagName = buildTagName(workspace, newVersion);
465
+
466
+ if (workspace === rootWorkspace) {
467
+ rootNewVersion = newVersion;
468
+ rootNewTag = tagName;
469
+ }
470
+
471
+ if (workspace !== rootWorkspace || !isMonorepo) {
472
+ const bumpReason = bumpReasons.join("\n");
473
+ bumpedWorkspaces.set(workspace, {
474
+ currentVersion,
475
+ bumpType,
476
+ bumpReason,
477
+ bumpForDependenciesReasons: changedWorkspace
478
+ ? bumpReasons.slice(1)
479
+ : bumpReasons,
480
+ newVersion,
481
+ newTag: tagName,
482
+ hasChanged: changedWorkspace !== undefined,
483
+ dependenciesToBump,
484
+ });
485
+
486
+ logger.info(
487
+ `${workspaceName}: ${currentVersion} -> ${
488
+ !isMonorepo || isMonorepoVersionIndependent
489
+ ? newVersion
490
+ : "bump"
491
+ } (${bumpReason.replaceAll("\n", " ; ")})`,
492
+ );
493
+ // TODO Json only:
494
+ // logger.info({
495
+ // cwd: npath.fromPortablePath(workspace.cwd),
496
+ // ident: workspaceName,
497
+ // oldVersion: currentVersion,
498
+ // ...(isMonorepoVersionIndependent ? { newVersion } : {}),
499
+ // bumpType,
500
+ // bumpReasons,
501
+ // });
502
+ }
503
+ }
504
+ }
505
+ }
506
+ });
507
+
508
+ // TODO ask for confirmation
509
+ // if (!this.yes) {
510
+ // }
511
+
512
+ // TODO do this entire phase here, not override for not independent only
513
+ if (isMonorepo && !isMonorepoVersionIndependent) {
514
+ const currentVersion = rootWorkspace.pkg.version!;
515
+ const highestBumpType = getHighestBumpType(
516
+ [...bumpedWorkspaces.values()].map(({ bumpType }) => bumpType),
517
+ );
518
+ const newVersion = incrementVersion(
519
+ rootWorkspace,
520
+ currentVersion,
521
+ highestBumpType,
522
+ );
523
+ rootNewVersion = newVersion;
524
+ rootNewTag = buildTagName(rootWorkspace, newVersion);
525
+
526
+ [...bumpedWorkspaces.entries()].forEach(([workspace, bumpedWorkspace]) => {
527
+ const isRoot = workspace === rootWorkspace;
528
+ if (isRoot) {
529
+ throw new Error("Unexpected root found in bumped workspaces");
530
+ }
531
+ bumpedWorkspace.bumpType = highestBumpType;
532
+ bumpedWorkspace.newVersion = newVersion;
533
+ bumpedWorkspace.newTag = null;
534
+ });
535
+
536
+ [
537
+ ...bumpedWorkspaces.entries(),
538
+ ...noVersionToUpdateWorkspaces.entries(),
539
+ ].forEach(([workspace, { dependenciesToBump }]) => {
540
+ dependenciesToBump.forEach((dependencyToBump) => {
541
+ dependencyToBump[2] = calcBumpRange(
542
+ workspace,
543
+ dependencyToBump[1].selector,
544
+ newVersion,
545
+ );
546
+ });
547
+ });
548
+
549
+ bumpedWorkspaces.set(rootWorkspace, {
550
+ currentVersion,
551
+ bumpType: highestBumpType,
552
+ hasChanged: true,
553
+ newVersion: rootNewVersion,
554
+ newTag: rootNewTag,
555
+ dependenciesToBump: [],
556
+ });
557
+
558
+ logger.info(
559
+ `${currentVersion} -> ${newVersion}`,
560
+ // TODO Json only:
561
+ // {
562
+ // oldVersion: currentVersion,
563
+ // newVersion, },
564
+ );
565
+ }
566
+
567
+ await logger.group("Applying changes", async () => {
568
+ // Update lockfile ; must be done to make sure preversion script can be ran
569
+
570
+ if (!options.dryRun) {
571
+ if (packageManager.installOnPackageContentChange) {
572
+ logger.info(`${getWorkspaceName(rootWorkspace)}: Running install`);
573
+ await packageManager.installOnPackageContentChange(rootWorkspace);
574
+ }
575
+
576
+ logger.info("Lifecycle script: preversion");
577
+
578
+ if (
579
+ isMonorepoVersionIndependent &&
580
+ rootWorkspace.pkg.scripts?.preversion
581
+ ) {
582
+ await packageManager.runScript(rootWorkspace, "preversion");
583
+ }
584
+
585
+ // lifecycle: preversion
586
+ for (const workspace of bumpedWorkspaces.keys()) {
587
+ if (workspace.pkg.scripts?.preversion) {
588
+ await packageManager.runScript(workspace, "preversion");
589
+ }
590
+ }
591
+
592
+ logger.info("Modifying versions in package.json");
593
+ // update versions
594
+
595
+ await Promise.all(
596
+ [...bumpedWorkspaces.entries()].map(
597
+ ([workspace, { newVersion, dependenciesToBump }]) => {
598
+ workspace.pkg.version = newVersion;
599
+
600
+ for (const [
601
+ dependencyType,
602
+ dependencyDescriptor,
603
+ dependencyNewRange,
604
+ ] of dependenciesToBump) {
605
+ const newDescriptor = PackageDependencyDescriptorUtils.make(
606
+ dependencyDescriptor,
607
+ dependencyNewRange,
608
+ );
609
+ const [key, newValue] =
610
+ PackageDependencyDescriptorUtils.stringify(newDescriptor);
611
+ workspace.pkg[dependencyType]![key] = newValue;
612
+ }
613
+
614
+ return writePkg(workspace);
615
+ },
616
+ ),
617
+ );
618
+
619
+ if (packageManager.installOnPackageContentChange) {
620
+ // Update lockfile ; must be done before running again lifecycle scripts
621
+ logger.info(`${getWorkspaceName(rootWorkspace)}: Running install`);
622
+ await packageManager.installOnPackageContentChange(rootWorkspace);
623
+ }
624
+
625
+ // lifecycle: version
626
+ logger.info("Lifecycle script: version");
627
+ for (const workspace of bumpedWorkspaces.keys()) {
628
+ if (workspace.pkg.scripts?.version) {
629
+ await packageManager.runScript(workspace, "version");
630
+ }
631
+ }
632
+ }
633
+
634
+ if (isMonorepoVersionIndependent && rootWorkspace.pkg.scripts?.version) {
635
+ await packageManager.runScript(rootWorkspace, "version");
636
+ }
637
+ });
638
+
639
+ const changelogs = new Map<Workspace, string>();
640
+
641
+ await Promise.all(
642
+ [...bumpedWorkspaces.entries()].map(
643
+ async ([
644
+ workspace,
645
+ { newTag, hasChanged, bumpReason, bumpForDependenciesReasons },
646
+ ]) => {
647
+ // const workspaceRelativePath =
648
+ // rootWorkspace === workspace
649
+ // ? undefined
650
+ // : path.relative(rootWorkspace.cwd, workspace.cwd);
651
+
652
+ const commits = commitsByWorkspace?.get(workspace);
653
+
654
+ let changelog = await generateChangelog(
655
+ rootWorkspace,
656
+ workspace.pkg,
657
+ conventionalCommitConfig,
658
+ previousTagVersionByWorkspace.get(workspace)?.tag || null,
659
+ isMonorepoVersionIndependent ? newTag : rootNewTag,
660
+ commits,
661
+ todayInYYYYMMDD(),
662
+ // {
663
+ // path: workspaceRelativePath,
664
+ // previousTag: previousTagByWorkspace.get(workspace) || undefined,
665
+ // verbose: options.verbose,
666
+ // tagPrefix: options.tagVersionPrefix,
667
+ // lernaPackage:
668
+ // rootWorkspace === workspace
669
+ // ? undefined
670
+ // : getWorkspaceName(workspace),
671
+ // },
672
+ );
673
+
674
+ if (bumpForDependenciesReasons && workspace !== rootWorkspace) {
675
+ if (bumpForDependenciesReasons.length > 0) {
676
+ changelog += `${!changelog.endsWith("\n\n") ? "\n" : ""}${bumpForDependenciesReasons.join("\n")}\n\n`;
677
+ }
678
+ }
679
+
680
+ if (changelog.slice(changelog.indexOf("\n")).trim().length === 0) {
681
+ changelog += `${!changelog.endsWith("\n\n") ? "\n" : ""}Note: no notable changes\n\n`;
682
+ }
683
+
684
+ if (!changelog && rootWorkspace === workspace) {
685
+ throw new Error("No changelog found for root workspace");
686
+ }
687
+
688
+ changelogs.set(workspace, changelog);
689
+
690
+ if (options.changelog) {
691
+ if (options.dryRun) {
692
+ logger.info(
693
+ `${getWorkspaceName(workspace)}: ${
694
+ options.changelog
695
+ }\n${changelog}`,
696
+ );
697
+ } else {
698
+ await updateChangelogFile(
699
+ changelog,
700
+ options.tagVersionPrefix,
701
+ `${workspace.cwd}/${options.changelog}`,
702
+ );
703
+ }
704
+ }
705
+ },
706
+ ),
707
+ );
708
+
709
+ if (!options.dryRun) {
710
+ logger.separator();
711
+
712
+ if (packageManager.installOnPackageContentChange) {
713
+ // install to update versions in lock file
714
+ logger.info(`${getWorkspaceName(rootWorkspace)}: Running install`);
715
+ await packageManager.installOnPackageContentChange(rootWorkspace);
716
+ }
717
+
718
+ logger.separator();
719
+
720
+ const changedFiles = await getDirtyFiles(project.root);
721
+
722
+ if (changedFiles) {
723
+ logger.info("Commit, tag and push", {
724
+ changedFiles,
725
+ });
726
+
727
+ const tagsSet = new Set<string>(
728
+ [...bumpedWorkspaces.values()]
729
+ .map(({ newTag }) => newTag)
730
+ .filter((newTag) => newTag !== null),
731
+ );
732
+
733
+ const tagsInCommitMessage = [...tagsSet]
734
+ .map((tag) => `- ${tag}`)
735
+ .join("\n");
736
+ const message = options.commitMessage
737
+ .replace(/\\n/g, "\n")
738
+ .replace(
739
+ /%a/g,
740
+ isMonorepoVersionIndependent
741
+ ? `\n\n${tagsInCommitMessage}`
742
+ : rootNewVersion,
743
+ )
744
+ .replace(/%s/g, rootNewTag)
745
+ .replace(/%v/g, rootNewVersion)
746
+ .replace(/%t/g, tagsInCommitMessage);
747
+
748
+ await createGitCommit(rootWorkspace, message);
749
+
750
+ for (const [workspace, { newTag }] of bumpedWorkspaces.entries()) {
751
+ if (newTag === null) continue;
752
+ await createGitTag(workspace, newTag);
753
+ }
754
+
755
+ if (
756
+ await isBehindRemote(rootWorkspace, options.gitRemote, gitCurrentBranch)
757
+ ) {
758
+ logger.error("Remote is ahead, aborting");
759
+ // eslint-disable-next-line unicorn/no-process-exit
760
+ process.exit(1);
761
+ }
762
+
763
+ await pushCommitsAndTags(
764
+ rootWorkspace,
765
+ options.gitRemote,
766
+ gitCurrentBranch,
767
+ );
768
+
769
+ // run postversion
770
+ if (rootWorkspace.pkg.scripts?.postversion) {
771
+ logger.info("Lifecycle script: postversion");
772
+ await packageManager.runScript(rootWorkspace, "postversion");
773
+ }
774
+
775
+ // TODO open github PR
776
+
777
+ if (options.createRelease && parsedRepoUrl) {
778
+ logger.info("Create git release");
779
+
780
+ await Promise.all(
781
+ [...bumpedWorkspaces.entries()].map(([workspace, { newTag }]) => {
782
+ if (newTag === null) return undefined;
783
+ const changelog = changelogs.get(workspace);
784
+ if (!changelog) {
785
+ logger.warn(
786
+ `No changelog found for workspace: ${getWorkspaceName(
787
+ workspace,
788
+ )}`,
789
+ );
790
+ return undefined;
791
+ }
792
+ return createGhRelease(workspace, {
793
+ parsedRepoUrl,
794
+ tag: newTag,
795
+ body: changelog,
796
+ prerelease: !!options.prerelease,
797
+ });
798
+ }),
799
+ );
800
+ }
801
+
802
+ if (!options.publish) {
803
+ return;
804
+ }
805
+ await logger.group("Publishing packages", async () => {
806
+ if (packageManager.publishWorkspaces) {
807
+ await packageManager.publishWorkspaces(rootWorkspace);
808
+ } else {
809
+ for (const [workspace] of bumpedWorkspaces.entries()) {
810
+ if (workspace.pkg.private) {
811
+ logger.info(
812
+ `Skipping private workspace ${getWorkspaceName(workspace)}`,
813
+ );
814
+ continue;
815
+ }
816
+
817
+ logger.info(`Publishing ${getWorkspaceName(workspace)}`);
818
+ await packageManager.publish(workspace);
819
+ }
820
+ }
821
+ });
822
+ } else {
823
+ logger.warn("No changes made. Nothing to commit.");
824
+ }
825
+ }
826
+ };
827
+
828
+ export const Defaults: VersionCommandOptions = {
829
+ cwd: process.cwd(),
830
+ includesRoot: false,
831
+ dryRun: false,
832
+ json: false,
833
+ preset: "conventional-changelog-conventionalcommits",
834
+ tagVersionPrefix: "v",
835
+ changelog: "CHANGELOG.md",
836
+ commitMessage: "chore: release %a",
837
+ createRelease: false,
838
+ bumpDependentsHighestAs: "major",
839
+ alwaysBumpPeerDependencies: false,
840
+ gitRemote: "origin",
841
+ verbose: false,
842
+ publish: false,
843
+ };
844
+
845
+ export default program
846
+ .command("version", { isDefault: true })
847
+ .usage("Bump package version using conventional commit")
848
+ .addOption(
849
+ new Option("--includes-root", "Release root workspace [untested]").default(
850
+ Defaults.includesRoot,
851
+ ),
852
+ )
853
+ .addOption(new Option("--cwd", "working directory").default(process.cwd()))
854
+ .addOption(
855
+ new Option(
856
+ "--dry-run",
857
+ "Print the versions without actually generating the package archive",
858
+ ).default(Defaults.dryRun),
859
+ )
860
+ .addOption(new Option("-v,--verbose").default(Defaults.verbose))
861
+ .addOption(
862
+ new Option("--force <type>", "Specify the release type").choices([
863
+ "major",
864
+ "minor",
865
+ "patch",
866
+ ]),
867
+ )
868
+ .addOption(
869
+ new Option(
870
+ "--prerelease [releaseType]",
871
+ "Add a prerelease identifier to new versions",
872
+ ),
873
+ )
874
+ .addOption(
875
+ new Option(
876
+ "--preset <presetName>",
877
+ "Conventional Changelog preset to require. Defaults to conventional-changelog-conventionalcommits.",
878
+ ).default("conventional-changelog-conventionalcommits"),
879
+ )
880
+ .addOption(
881
+ new Option("--tag-version-prefix <prefix>", "Tag version prefix").default(
882
+ Defaults.tagVersionPrefix,
883
+ ),
884
+ )
885
+ .addOption(
886
+ new Option(
887
+ "--changelog <path>",
888
+ "Changelog path. Default to CHANGELOG.md.",
889
+ ).default(Defaults.changelog),
890
+ )
891
+ .addOption(
892
+ new Option(
893
+ "-m,--commit-message <message>",
894
+ 'Commit message. Default to "chore: release %a". You can use %v for the version, %s for the version with prefix, %t to list tags, %a for auto best display.',
895
+ ).default(Defaults.commitMessage),
896
+ )
897
+ .addOption(
898
+ new Option("--create-release <type>", "Create a release").choices([
899
+ "github",
900
+ ]),
901
+ )
902
+ .addOption(
903
+ new Option(
904
+ "--bump-dependents-highest-as <type>",
905
+ "Bump dependents highest version as major, minor or patch",
906
+ )
907
+ .choices(["major", "minor", "patch"])
908
+ .default(Defaults.bumpDependentsHighestAs),
909
+ )
910
+ .addOption(
911
+ new Option(
912
+ "--always-bump-peer-dependencies",
913
+ "Always bump peer dependencies. Default to bumping only if the version doesn't satisfies the peer dependency range.",
914
+ ).default(Defaults.alwaysBumpPeerDependencies),
915
+ )
916
+ .addOption(
917
+ new Option(
918
+ "--git-remote <remote-name>",
919
+ "Git remote to push commits and tags to",
920
+ ).default(Defaults.gitRemote),
921
+ )
922
+ .addOption(
923
+ new Option(
924
+ "--ignore-changes <glob>",
925
+ 'Ignore changes in files matching the glob. Example: "**/*.test.js"',
926
+ ),
927
+ )
928
+ .addOption(new Option("--json", "Logger json"))
929
+ .addOption(
930
+ new Option(
931
+ "--package-manager <manager>",
932
+ "Specify the package manager to use (bun or yarn). Autodetected if not specified.",
933
+ ).choices(["bun", "yarn"]),
934
+ )
935
+ .addOption(new Option("--publish", "Publish after versioning").default(false))
936
+ .action((options) => versionCommandAction(options));