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