@ucdjs/release-scripts 0.1.0-beta.3 → 0.1.0-beta.4

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.
package/dist/index.d.mts CHANGED
@@ -1,3 +1,13 @@
1
+ //#region src/workspace.d.ts
2
+ interface WorkspacePackage {
3
+ name: string;
4
+ version: string;
5
+ path: string;
6
+ packageJson: PackageJson;
7
+ workspaceDependencies: string[];
8
+ workspaceDevDependencies: string[];
9
+ }
10
+ //#endregion
1
11
  //#region src/types.d.ts
2
12
  type BumpKind = "none" | "patch" | "minor" | "major";
3
13
  interface PackageJson {
@@ -6,16 +16,9 @@ interface PackageJson {
6
16
  dependencies?: Record<string, string>;
7
17
  devDependencies?: Record<string, string>;
8
18
  peerDependencies?: Record<string, string>;
19
+ private?: boolean;
9
20
  [key: string]: unknown;
10
21
  }
11
- interface WorkspacePackage {
12
- name: string;
13
- version: string;
14
- path: string;
15
- packageJson: PackageJson;
16
- workspaceDependencies: string[];
17
- workspaceDevDependencies: string[];
18
- }
19
22
  interface FindWorkspacePackagesOptions {
20
23
  /**
21
24
  * Package names to exclude
@@ -100,8 +103,19 @@ interface ReleaseOptions {
100
103
  */
101
104
  githubToken: string;
102
105
  pullRequest?: {
103
- title: string;
104
- body: string;
106
+ /**
107
+ * Title for the release pull request
108
+ */
109
+ title?: string;
110
+ /**
111
+ * Body for the release pull request
112
+ *
113
+ * If not provided, a default body will be generated.
114
+ *
115
+ * NOTE:
116
+ * You can use custom template expressions, see [h3js/rendu](https://github.com/h3js/rendu)
117
+ */
118
+ body?: string;
105
119
  };
106
120
  }
107
121
  interface ReleaseResult {
package/dist/index.mjs CHANGED
@@ -1,21 +1,16 @@
1
1
  import process from "node:process";
2
2
  import { getCommits } from "commit-parser";
3
- import createDebug from "debug";
4
3
  import farver from "farver";
5
4
  import { exec } from "tinyexec";
5
+ import { dedent } from "@luxass/utils";
6
+ import { Eta } from "eta";
6
7
  import { readFile, writeFile } from "node:fs/promises";
7
8
  import { join } from "node:path";
8
9
  import prompts from "prompts";
9
10
 
10
- //#region src/logger.ts
11
- function createDebugger(namespace) {
12
- const debug$2 = createDebug(namespace);
13
- if (debug$2.enabled) return debug$2;
14
- }
15
-
16
- //#endregion
17
11
  //#region src/utils.ts
18
12
  const globalOptions = { dryRun: false };
13
+ const isCI = typeof process.env.CI === "string" && process.env.CI !== "" && process.env.CI.toLowerCase() !== "false";
19
14
  async function run(bin, args, opts = {}) {
20
15
  return exec(bin, args, {
21
16
  throwOnError: true,
@@ -33,7 +28,6 @@ const runIfNotDry = globalOptions.dryRun ? dryRun : run;
33
28
 
34
29
  //#endregion
35
30
  //#region src/commits.ts
36
- const debug$1 = createDebugger("ucdjs:release-scripts:commits");
37
31
  async function getLastPackageTag(packageName, workspaceRoot) {
38
32
  const { stdout } = await run("git", ["tag", "--list"], { nodeOptions: {
39
33
  cwd: workspaceRoot,
@@ -58,16 +52,31 @@ async function getPackageCommits(pkg, workspaceRoot) {
58
52
  from: lastTag,
59
53
  to: "HEAD"
60
54
  });
61
- debug$1?.(`Found ${allCommits.length} commits for ${pkg.name} since ${lastTag || "beginning"}`);
55
+ console.log(`Found ${allCommits.length} commits for ${pkg.name} since ${lastTag || "beginning"}`);
62
56
  const touchedCommitHashes = await getCommitsTouchingPackage(lastTag || "HEAD", "HEAD", pkg.path, workspaceRoot);
63
57
  const touchedSet = new Set(touchedCommitHashes);
64
58
  const packageCommits = allCommits.filter((commit) => touchedSet.has(commit.shortHash));
65
- debug$1?.(`${packageCommits.length} commits affect ${pkg.name}`);
59
+ console.log(`${packageCommits.length} commits affect ${pkg.name}`);
66
60
  return packageCommits;
67
61
  }
68
62
  async function analyzePackageCommits(pkg, workspaceRoot) {
69
63
  return determineHighestBump(await getPackageCommits(pkg, workspaceRoot));
70
64
  }
65
+ /**
66
+ * Analyze commits for multiple packages to determine version bumps
67
+ *
68
+ * @param packages - Packages to analyze
69
+ * @param workspaceRoot - Root directory of the workspace
70
+ * @returns Map of package names to their bump types
71
+ */
72
+ async function analyzeCommits(packages, workspaceRoot) {
73
+ const changedPackages = /* @__PURE__ */ new Map();
74
+ for (const pkg of packages) {
75
+ const bump = await analyzePackageCommits(pkg, workspaceRoot);
76
+ if (bump !== "none") changedPackages.set(pkg.name, bump);
77
+ }
78
+ return changedPackages;
79
+ }
71
80
  function determineBumpType(commit) {
72
81
  if (commit.isBreaking) return "major";
73
82
  if (!commit.isConventional || !commit.type) return "none";
@@ -100,135 +109,11 @@ async function getCommitsTouchingPackage(from, to, packagePath, workspaceRoot) {
100
109
  } });
101
110
  return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
102
111
  } catch (error) {
103
- debug$1?.(`Error getting commits touching package: ${error}`);
112
+ console.error(`Error getting commits touching package: ${error}`);
104
113
  return [];
105
114
  }
106
115
  }
107
116
 
108
- //#endregion
109
- //#region src/validation.ts
110
- /**
111
- * Validation utilities for release scripts
112
- */
113
- function isValidSemver(version) {
114
- return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
115
- }
116
- function validateSemver(version) {
117
- if (!isValidSemver(version)) throw new Error(`Invalid semver version: ${version}`);
118
- }
119
-
120
- //#endregion
121
- //#region src/version.ts
122
- /**
123
- * Calculate the new version based on current version and bump type
124
- * Pure function - no side effects, easily testable
125
- */
126
- function calculateNewVersion(currentVersion, bump) {
127
- if (bump === "none") return currentVersion;
128
- validateSemver(currentVersion);
129
- const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
130
- if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
131
- const [, major, minor, patch, suffix] = match;
132
- let newMajor = Number.parseInt(major, 10);
133
- let newMinor = Number.parseInt(minor, 10);
134
- let newPatch = Number.parseInt(patch, 10);
135
- switch (bump) {
136
- case "major":
137
- newMajor += 1;
138
- newMinor = 0;
139
- newPatch = 0;
140
- break;
141
- case "minor":
142
- newMinor += 1;
143
- newPatch = 0;
144
- break;
145
- case "patch":
146
- newPatch += 1;
147
- break;
148
- }
149
- return `${newMajor}.${newMinor}.${newPatch}`;
150
- }
151
- /**
152
- * Create a version update object
153
- */
154
- function createVersionUpdate(pkg, bump, hasDirectChanges) {
155
- const newVersion = calculateNewVersion(pkg.version, bump);
156
- return {
157
- package: pkg,
158
- currentVersion: pkg.version,
159
- newVersion,
160
- bumpType: bump,
161
- hasDirectChanges
162
- };
163
- }
164
- /**
165
- * Update a package.json file with new version and dependency versions
166
- */
167
- async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
168
- const packageJsonPath = join(pkg.path, "package.json");
169
- const content = await readFile(packageJsonPath, "utf-8");
170
- const packageJson = JSON.parse(content);
171
- packageJson.version = newVersion;
172
- for (const [depName, depVersion] of dependencyUpdates) {
173
- if (packageJson.dependencies?.[depName]) {
174
- if (packageJson.dependencies[depName] === "workspace:*") continue;
175
- packageJson.dependencies[depName] = `^${depVersion}`;
176
- }
177
- if (packageJson.devDependencies?.[depName]) {
178
- if (packageJson.devDependencies[depName] === "workspace:*") continue;
179
- packageJson.devDependencies[depName] = `^${depVersion}`;
180
- }
181
- if (packageJson.peerDependencies?.[depName]) {
182
- if (packageJson.peerDependencies[depName] === "workspace:*") continue;
183
- packageJson.peerDependencies[depName] = `^${depVersion}`;
184
- }
185
- }
186
- await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
187
- }
188
- /**
189
- * Get all dependency updates needed for a package
190
- */
191
- function getDependencyUpdates(pkg, allUpdates) {
192
- const updates = /* @__PURE__ */ new Map();
193
- const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
194
- for (const dep of allDeps) {
195
- const update = allUpdates.find((u) => u.package.name === dep);
196
- if (update) updates.set(dep, update.newVersion);
197
- }
198
- return updates;
199
- }
200
-
201
- //#endregion
202
- //#region src/dependencies.ts
203
- /**
204
- * Pure function: Determine which packages need updates due to dependency changes
205
- *
206
- * When a package is updated, all packages that depend on it should also be updated.
207
- * This function calculates which additional packages need patch bumps.
208
- *
209
- * @param updateOrder - Packages in topological order with their dependency levels
210
- * @param directUpdates - Packages with direct code changes
211
- * @returns All updates including dependent packages
212
- */
213
- function createDependentUpdates(updateOrder, directUpdates) {
214
- const allUpdates = [...directUpdates];
215
- const updatedPackages = new Set(directUpdates.map((u) => u.package.name));
216
- for (const { package: pkg } of updateOrder) {
217
- if (updatedPackages.has(pkg.name)) continue;
218
- if (hasUpdatedDependencies(pkg, updatedPackages)) {
219
- allUpdates.push(createVersionUpdate(pkg, "patch", false));
220
- updatedPackages.add(pkg.name);
221
- }
222
- }
223
- return allUpdates;
224
- }
225
- /**
226
- * Pure function: Check if a package has any updated dependencies
227
- */
228
- function hasUpdatedDependencies(pkg, updatedPackages) {
229
- return [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies].some((dep) => updatedPackages.has(dep));
230
- }
231
-
232
117
  //#endregion
233
118
  //#region src/git.ts
234
119
  /**
@@ -474,49 +359,311 @@ async function upsertPullRequest({ owner, repo, title, body, head, base, pullNum
474
359
  throw err;
475
360
  }
476
361
  }
477
- function generatePullRequestBody(updates, _body) {
478
- const lines = [];
479
- lines.push("## Packages");
480
- lines.push("");
481
- const directChanges = updates.filter((u) => u.hasDirectChanges);
482
- const dependencyUpdates = updates.filter((u) => !u.hasDirectChanges);
483
- if (directChanges.length > 0) {
484
- lines.push("### Direct Changes");
485
- lines.push("");
486
- for (const update of directChanges) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (${update.bumpType})`);
487
- lines.push("");
488
- }
489
- if (dependencyUpdates.length > 0) {
490
- lines.push("### Dependency Updates");
491
- lines.push("");
492
- for (const update of dependencyUpdates) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (dependencies changed)`);
493
- lines.push("");
494
- }
495
- lines.push("---");
496
- lines.push("");
497
- lines.push("This release PR was automatically generated.");
498
- return lines.join("\n");
362
+ const defaultTemplate = dedent`
363
+ This PR was automatically generated by the release script.
364
+
365
+ The following packages have been prepared for release:
366
+
367
+ <% it.packages.forEach((pkg) => { %>
368
+ - **<%= pkg.name %>**: <%= pkg.currentVersion %> → <%= pkg.newVersion %> (<%= pkg.bumpType %>)
369
+ <% }) %>
370
+
371
+ Please review the changes and merge when ready.
372
+
373
+ For a more in-depth look at the changes, please refer to the individual package changelogs.
374
+
375
+ > [!NOTE]
376
+ > When this PR is merged, the release process will be triggered automatically, publishing the new package versions to the registry.
377
+ `;
378
+ function dedentString(str) {
379
+ const lines = str.split("\n");
380
+ const minIndent = lines.filter((line) => line.trim().length > 0).reduce((min, line) => Math.min(min, line.search(/\S/)), Infinity);
381
+ return lines.map((line) => minIndent === Infinity ? line : line.slice(minIndent)).join("\n").trim();
382
+ }
383
+ function generatePullRequestBody(updates, body) {
384
+ const eta = new Eta();
385
+ const bodyTemplate = body ? dedentString(body) : defaultTemplate;
386
+ return eta.renderString(bodyTemplate, { packages: updates.map((u) => ({
387
+ name: u.package.name,
388
+ currentVersion: u.currentVersion,
389
+ newVersion: u.newVersion,
390
+ bumpType: u.bumpType,
391
+ hasDirectChanges: u.hasDirectChanges
392
+ })) });
393
+ }
394
+
395
+ //#endregion
396
+ //#region src/version.ts
397
+ function isValidSemver(version) {
398
+ return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
399
+ }
400
+ function validateSemver(version) {
401
+ if (!isValidSemver(version)) throw new Error(`Invalid semver version: ${version}`);
402
+ }
403
+ /**
404
+ * Calculate the new version based on current version and bump type
405
+ * Pure function - no side effects, easily testable
406
+ */
407
+ function calculateNewVersion(currentVersion, bump) {
408
+ if (bump === "none") return currentVersion;
409
+ validateSemver(currentVersion);
410
+ const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
411
+ if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
412
+ const [, major, minor, patch] = match;
413
+ let newMajor = Number.parseInt(major, 10);
414
+ let newMinor = Number.parseInt(minor, 10);
415
+ let newPatch = Number.parseInt(patch, 10);
416
+ switch (bump) {
417
+ case "major":
418
+ newMajor += 1;
419
+ newMinor = 0;
420
+ newPatch = 0;
421
+ break;
422
+ case "minor":
423
+ newMinor += 1;
424
+ newPatch = 0;
425
+ break;
426
+ case "patch":
427
+ newPatch += 1;
428
+ break;
429
+ }
430
+ return `${newMajor}.${newMinor}.${newPatch}`;
431
+ }
432
+ /**
433
+ * Create a version update object
434
+ */
435
+ function createVersionUpdate(pkg, bump, hasDirectChanges) {
436
+ const newVersion = calculateNewVersion(pkg.version, bump);
437
+ return {
438
+ package: pkg,
439
+ currentVersion: pkg.version,
440
+ newVersion,
441
+ bumpType: bump,
442
+ hasDirectChanges
443
+ };
444
+ }
445
+ /**
446
+ * Update a package.json file with new version and dependency versions
447
+ */
448
+ async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
449
+ const packageJsonPath = join(pkg.path, "package.json");
450
+ const content = await readFile(packageJsonPath, "utf-8");
451
+ const packageJson = JSON.parse(content);
452
+ packageJson.version = newVersion;
453
+ for (const [depName, depVersion] of dependencyUpdates) {
454
+ if (packageJson.dependencies?.[depName]) {
455
+ if (packageJson.dependencies[depName] === "workspace:*") continue;
456
+ packageJson.dependencies[depName] = `^${depVersion}`;
457
+ }
458
+ if (packageJson.devDependencies?.[depName]) {
459
+ if (packageJson.devDependencies[depName] === "workspace:*") continue;
460
+ packageJson.devDependencies[depName] = `^${depVersion}`;
461
+ }
462
+ if (packageJson.peerDependencies?.[depName]) {
463
+ if (packageJson.peerDependencies[depName] === "workspace:*") continue;
464
+ packageJson.peerDependencies[depName] = `^${depVersion}`;
465
+ }
466
+ }
467
+ await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
468
+ }
469
+ /**
470
+ * Get all dependency updates needed for a package
471
+ */
472
+ function getDependencyUpdates(pkg, allUpdates) {
473
+ const updates = /* @__PURE__ */ new Map();
474
+ const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
475
+ for (const dep of allDeps) {
476
+ const update = allUpdates.find((u) => u.package.name === dep);
477
+ if (update) updates.set(dep, update.newVersion);
478
+ }
479
+ return updates;
480
+ }
481
+
482
+ //#endregion
483
+ //#region src/package.ts
484
+ /**
485
+ * Build a dependency graph from workspace packages
486
+ *
487
+ * Creates a bidirectional graph that maps:
488
+ * - packages: Map of package name → WorkspacePackage
489
+ * - dependents: Map of package name → Set of packages that depend on it
490
+ *
491
+ * @param packages - All workspace packages
492
+ * @returns Dependency graph with packages and dependents maps
493
+ */
494
+ function buildPackageDependencyGraph(packages) {
495
+ const packagesMap = /* @__PURE__ */ new Map();
496
+ const dependents = /* @__PURE__ */ new Map();
497
+ for (const pkg of packages) {
498
+ packagesMap.set(pkg.name, pkg);
499
+ dependents.set(pkg.name, /* @__PURE__ */ new Set());
500
+ }
501
+ for (const pkg of packages) {
502
+ const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
503
+ for (const dep of allDeps) {
504
+ const depSet = dependents.get(dep);
505
+ if (depSet) depSet.add(pkg.name);
506
+ }
507
+ }
508
+ return {
509
+ packages: packagesMap,
510
+ dependents
511
+ };
512
+ }
513
+ /**
514
+ * Get all packages affected by changes (including transitive dependents)
515
+ *
516
+ * Uses graph traversal to find all packages that need updates:
517
+ * - Packages with direct changes
518
+ * - All packages that depend on changed packages (transitively)
519
+ *
520
+ * @param graph - Dependency graph
521
+ * @param changedPackages - Set of package names with direct changes
522
+ * @returns Set of all package names that need updates
523
+ */
524
+ function getAllAffectedPackages(graph, changedPackages) {
525
+ const affected = /* @__PURE__ */ new Set();
526
+ function visitDependents(pkgName) {
527
+ if (affected.has(pkgName)) return;
528
+ affected.add(pkgName);
529
+ const dependents = graph.dependents.get(pkgName);
530
+ if (dependents) for (const dependent of dependents) visitDependents(dependent);
531
+ }
532
+ for (const pkg of changedPackages) visitDependents(pkg);
533
+ return affected;
534
+ }
535
+ /**
536
+ * Create version updates for all packages affected by dependency changes
537
+ *
538
+ * When a package is updated, all packages that depend on it should also be updated.
539
+ * This function calculates which additional packages need patch bumps due to dependency changes.
540
+ *
541
+ * @param graph - Dependency graph
542
+ * @param workspacePackages - All workspace packages
543
+ * @param directUpdates - Packages with direct code changes
544
+ * @returns All updates including dependent packages that need patch bumps
545
+ */
546
+ function createDependentUpdates(graph, workspacePackages, directUpdates) {
547
+ const allUpdates = [...directUpdates];
548
+ const directUpdateMap = new Map(directUpdates.map((u) => [u.package.name, u]));
549
+ const affectedPackages = getAllAffectedPackages(graph, new Set(directUpdates.map((u) => u.package.name)));
550
+ for (const pkgName of affectedPackages) {
551
+ if (directUpdateMap.has(pkgName)) continue;
552
+ const pkg = workspacePackages.find((p) => p.name === pkgName);
553
+ if (!pkg) continue;
554
+ allUpdates.push(createVersionUpdate(pkg, "patch", false));
555
+ }
556
+ return allUpdates;
557
+ }
558
+ /**
559
+ * Update all package.json files with new versions and dependency updates
560
+ *
561
+ * Updates are performed in parallel for better performance.
562
+ *
563
+ * @param updates - Version updates to apply
564
+ */
565
+ async function updateAllPackageJsonFiles(updates) {
566
+ await Promise.all(updates.map(async (update) => {
567
+ const depUpdates = getDependencyUpdates(update.package, updates);
568
+ await updatePackageJson(update.package, update.newVersion, depUpdates);
569
+ }));
499
570
  }
500
571
 
501
572
  //#endregion
502
573
  //#region src/prompts.ts
503
- async function promptPackageSelection(packages) {
574
+ /**
575
+ * Get commits for a package grouped by conventional commit type
576
+ *
577
+ * @param pkg - The workspace package
578
+ * @param workspaceRoot - Root directory of the workspace
579
+ * @param limit - Maximum number of commits to return (default: 10)
580
+ * @returns Commits grouped by type
581
+ */
582
+ async function getCommitsForPackage(pkg, workspaceRoot, limit = 10) {
583
+ const limitedCommits = (await getPackageCommits(pkg, workspaceRoot)).slice(0, limit);
584
+ const grouped = {
585
+ feat: [],
586
+ fix: [],
587
+ perf: [],
588
+ chore: [],
589
+ docs: [],
590
+ style: [],
591
+ refactor: [],
592
+ test: [],
593
+ build: [],
594
+ ci: [],
595
+ revert: [],
596
+ other: []
597
+ };
598
+ for (const commit of limitedCommits) if (commit.type && commit.type in grouped) grouped[commit.type].push(commit);
599
+ else grouped.other.push(commit);
600
+ return grouped;
601
+ }
602
+ /**
603
+ * Format grouped commits into a readable string
604
+ */
605
+ function formatCommitGroups(grouped) {
606
+ const lines = [];
607
+ const typeLabels = {
608
+ feat: "Features",
609
+ fix: "Bug Fixes",
610
+ perf: "Performance",
611
+ chore: "Chores",
612
+ docs: "Documentation",
613
+ style: "Styling",
614
+ refactor: "Refactoring",
615
+ test: "Tests",
616
+ build: "Build",
617
+ ci: "CI",
618
+ revert: "Reverts",
619
+ other: "Other"
620
+ };
621
+ for (const type of [
622
+ "feat",
623
+ "fix",
624
+ "perf",
625
+ "refactor",
626
+ "test",
627
+ "docs",
628
+ "style",
629
+ "build",
630
+ "ci",
631
+ "chore",
632
+ "revert",
633
+ "other"
634
+ ]) {
635
+ const commits = grouped[type];
636
+ if (commits.length > 0) {
637
+ lines.push(`\n${typeLabels[type]}:`);
638
+ for (const commit of commits) {
639
+ const scope = commit.scope ? `(${commit.scope})` : "";
640
+ const breaking = commit.isBreaking ? " ⚠️ BREAKING" : "";
641
+ lines.push(` • ${commit.type}${scope}: ${commit.message}${breaking}`);
642
+ }
643
+ }
644
+ }
645
+ return lines.join("\n");
646
+ }
647
+ async function selectPackagePrompt(packages) {
504
648
  const response = await prompts({
505
649
  type: "multiselect",
506
650
  name: "selectedPackages",
507
651
  message: "Select packages to release",
508
652
  choices: packages.map((pkg) => ({
509
- title: `${pkg.name} (${pkg.version})`,
653
+ title: `${pkg.name} (${farver.bold(pkg.version)})`,
510
654
  value: pkg.name,
511
655
  selected: true
512
656
  })),
513
657
  min: 1,
514
- hint: "Space to select/deselect. Return to submit."
658
+ hint: "Space to select/deselect. Return to submit.",
659
+ instructions: false
515
660
  });
516
- if (!response.selectedPackages || response.selectedPackages.length === 0) throw new Error("No packages selected");
661
+ if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
517
662
  return response.selectedPackages;
518
663
  }
519
- async function promptVersionOverride(packageName, currentVersion, suggestedVersion, suggestedBumpType) {
664
+ async function promptVersionOverride(pkg, workspaceRoot, currentVersion, suggestedVersion, suggestedBumpType) {
665
+ const commitSummary = formatCommitGroups(await getCommitsForPackage(pkg, workspaceRoot));
666
+ if (commitSummary.trim()) console.log(`\nRecent changes in ${pkg.name}:${commitSummary}\n`);
520
667
  const choices = [{
521
668
  title: `Use suggested: ${suggestedVersion} (${suggestedBumpType})`,
522
669
  value: "suggested"
@@ -539,7 +686,7 @@ async function promptVersionOverride(packageName, currentVersion, suggestedVersi
539
686
  const response = await prompts([{
540
687
  type: "select",
541
688
  name: "choice",
542
- message: `${packageName} (${currentVersion}):`,
689
+ message: `${pkg.name} (${currentVersion}):`,
543
690
  choices,
544
691
  initial: 0
545
692
  }, {
@@ -555,18 +702,85 @@ async function promptVersionOverride(packageName, currentVersion, suggestedVersi
555
702
  else if (response.choice === "custom") return response.customVersion;
556
703
  else return calculateNewVersion(currentVersion, response.choice);
557
704
  }
558
- async function promptVersionOverrides(packages) {
705
+ async function promptVersionOverrides(packages, workspaceRoot) {
559
706
  const overrides = /* @__PURE__ */ new Map();
560
- for (const pkg of packages) {
561
- const newVersion = await promptVersionOverride(pkg.name, pkg.currentVersion, pkg.suggestedVersion, pkg.bumpType);
562
- overrides.set(pkg.name, newVersion);
707
+ for (const item of packages) {
708
+ const newVersion = await promptVersionOverride(item.package, workspaceRoot, item.currentVersion, item.suggestedVersion, item.bumpType);
709
+ overrides.set(item.package.name, newVersion);
563
710
  }
564
711
  return overrides;
565
712
  }
566
713
 
567
714
  //#endregion
568
715
  //#region src/workspace.ts
569
- const debug = createDebugger("ucdjs:release-scripts:workspace");
716
+ async function discoverWorkspacePackages(workspaceRoot, options) {
717
+ let workspaceOptions;
718
+ let explicitPackages;
719
+ if (options.packages == null || options.packages === true) workspaceOptions = { excludePrivate: false };
720
+ else if (Array.isArray(options.packages)) {
721
+ workspaceOptions = {
722
+ excludePrivate: false,
723
+ included: options.packages
724
+ };
725
+ explicitPackages = options.packages;
726
+ } else {
727
+ workspaceOptions = options.packages;
728
+ if (options.packages.included) explicitPackages = options.packages.included;
729
+ }
730
+ const workspacePackages = await findWorkspacePackages(workspaceRoot, workspaceOptions);
731
+ if (explicitPackages) {
732
+ const foundNames = new Set(workspacePackages.map((p) => p.name));
733
+ const missing = explicitPackages.filter((p) => !foundNames.has(p));
734
+ if (missing.length > 0) throw new Error(`Packages not found in workspace: ${missing.join(", ")}`);
735
+ }
736
+ let packagesToAnalyze = workspacePackages;
737
+ const isPackagePromptEnabled = options.prompts?.packages !== false;
738
+ if (!isCI && isPackagePromptEnabled && !explicitPackages) {
739
+ const selectedNames = await selectPackagePrompt(workspacePackages);
740
+ packagesToAnalyze = workspacePackages.filter((pkg) => selectedNames.includes(pkg.name));
741
+ }
742
+ return {
743
+ workspacePackages,
744
+ packagesToAnalyze
745
+ };
746
+ }
747
+ async function findWorkspacePackages(workspaceRoot, options) {
748
+ try {
749
+ const result = await run("pnpm", [
750
+ "-r",
751
+ "ls",
752
+ "--json"
753
+ ], { nodeOptions: {
754
+ cwd: workspaceRoot,
755
+ stdio: "pipe"
756
+ } });
757
+ const rawProjects = JSON.parse(result.stdout);
758
+ const allPackageNames = new Set(rawProjects.map((p) => p.name));
759
+ const excludedPackages = /* @__PURE__ */ new Set();
760
+ const promises = rawProjects.map(async (rawProject) => {
761
+ const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
762
+ const packageJson = JSON.parse(content);
763
+ if (!shouldIncludePackage(packageJson, options)) {
764
+ excludedPackages.add(rawProject.name);
765
+ return null;
766
+ }
767
+ return {
768
+ name: rawProject.name,
769
+ version: rawProject.version,
770
+ path: rawProject.path,
771
+ packageJson,
772
+ workspaceDependencies: extractWorkspaceDependencies(rawProject.dependencies, allPackageNames),
773
+ workspaceDevDependencies: extractWorkspaceDependencies(rawProject.devDependencies, allPackageNames)
774
+ };
775
+ });
776
+ const packages = await Promise.all(promises);
777
+ if (excludedPackages.size > 0) console.info(`${farver.cyan("[info]:")} Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
778
+ return packages.filter((pkg) => pkg !== null);
779
+ } catch (err) {
780
+ console.error("Error discovering workspace packages:", err);
781
+ throw err;
782
+ }
783
+ }
570
784
  function shouldIncludePackage(pkg, options) {
571
785
  if (!options) return true;
572
786
  if (options.excludePrivate && pkg.private) return false;
@@ -576,130 +790,45 @@ function shouldIncludePackage(pkg, options) {
576
790
  if (options.excluded?.includes(pkg.name)) return false;
577
791
  return true;
578
792
  }
579
- async function findWorkspacePackages(workspaceRoot, options) {
580
- const result = await run("pnpm", [
581
- "-r",
582
- "ls",
583
- "--json"
584
- ], { nodeOptions: {
585
- cwd: workspaceRoot,
586
- stdio: "pipe"
587
- } });
588
- const rawProjects = JSON.parse(result.stdout);
589
- const packages = [];
590
- const allPackageNames = new Set(rawProjects.map((p) => p.name));
591
- for (const rawProject of rawProjects) {
592
- const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
593
- const packageJson = JSON.parse(content);
594
- if (!shouldIncludePackage(packageJson, options)) {
595
- debug?.(`Excluding package ${rawProject.name}`);
596
- continue;
597
- }
598
- const workspaceDeps = extractWorkspaceDependencies(rawProject.dependencies, allPackageNames);
599
- const workspaceDevDeps = extractWorkspaceDependencies(rawProject.devDependencies, allPackageNames);
600
- packages.push({
601
- name: rawProject.name,
602
- version: rawProject.version,
603
- path: rawProject.path,
604
- packageJson,
605
- workspaceDependencies: workspaceDeps,
606
- workspaceDevDependencies: workspaceDevDeps
607
- });
608
- }
609
- return packages;
610
- }
611
793
  function extractWorkspaceDependencies(dependencies, workspacePackages) {
612
794
  if (!dependencies) return [];
613
795
  return Object.keys(dependencies).filter((dep) => {
614
796
  return workspacePackages.has(dep);
615
797
  });
616
798
  }
617
- function buildDependencyGraph(packages) {
618
- const packagesMap = /* @__PURE__ */ new Map();
619
- const dependents = /* @__PURE__ */ new Map();
620
- for (const pkg of packages) {
621
- packagesMap.set(pkg.name, pkg);
622
- dependents.set(pkg.name, /* @__PURE__ */ new Set());
623
- }
624
- for (const pkg of packages) {
625
- const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
626
- for (const dep of allDeps) {
627
- const depSet = dependents.get(dep);
628
- if (depSet) depSet.add(pkg.name);
629
- }
630
- }
631
- return {
632
- packages: packagesMap,
633
- dependents
634
- };
635
- }
636
- function getPackageUpdateOrder(graph, changedPackages) {
637
- const result = [];
638
- const visited = /* @__PURE__ */ new Set();
639
- const toUpdate = new Set(changedPackages);
640
- const packagesToProcess = new Set(changedPackages);
641
- for (const pkg of changedPackages) {
642
- const deps = graph.dependents.get(pkg);
643
- if (deps) for (const dep of deps) {
644
- packagesToProcess.add(dep);
645
- toUpdate.add(dep);
646
- }
647
- }
648
- function visit(pkgName, level) {
649
- if (visited.has(pkgName)) return;
650
- visited.add(pkgName);
651
- const pkg = graph.packages.get(pkgName);
652
- if (!pkg) return;
653
- const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
654
- let maxDepLevel = level;
655
- for (const dep of allDeps) if (toUpdate.has(dep)) {
656
- visit(dep, level);
657
- const depResult = result.find((r) => r.package.name === dep);
658
- if (depResult && depResult.level >= maxDepLevel) maxDepLevel = depResult.level + 1;
659
- }
660
- result.push({
661
- package: pkg,
662
- level: maxDepLevel
663
- });
664
- }
665
- for (const pkg of toUpdate) visit(pkg, 0);
666
- result.sort((a, b) => a.level - b.level);
667
- return result;
668
- }
669
799
 
670
800
  //#endregion
671
801
  //#region src/release.ts
672
- const isCI = process.env.CI === "true";
673
802
  async function release(options) {
674
803
  const { dryRun: dryRun$1 = false, safeguards = true, workspaceRoot = process.cwd(), releaseBranch = "release/next", githubToken } = options;
675
804
  globalOptions.dryRun = dryRun$1;
676
805
  if (githubToken.trim() === "" || githubToken == null) throw new Error("GitHub token is required");
677
806
  const [owner, repo] = options.repo.split("/");
678
807
  if (!owner || !repo) throw new Error(`Invalid repo format: ${options.repo}. Expected "owner/repo".`);
679
- if (safeguards && !isWorkingDirectoryClean(workspaceRoot)) {
808
+ if (safeguards && !await isWorkingDirectoryClean(workspaceRoot)) {
680
809
  console.error("Working directory is not clean. Please commit or stash your changes before proceeding.");
681
810
  return null;
682
811
  }
683
- const { workspacePackages, packagesToAnalyze: initialPackages } = await discoverPackages(workspaceRoot, options);
684
- if (initialPackages.length === 0) return null;
685
- const isPackagePromptEnabled = options.prompts?.packages !== false;
686
- const isPackagesPreConfigured = Array.isArray(options.packages) || typeof options.packages === "object" && options.packages.included != null;
687
- let packagesToAnalyze = initialPackages;
688
- if (!isCI && isPackagePromptEnabled && !isPackagesPreConfigured) {
689
- const selectedNames = await promptPackageSelection(initialPackages);
690
- packagesToAnalyze = initialPackages.filter((pkg) => selectedNames.includes(pkg.name));
812
+ const { workspacePackages, packagesToAnalyze } = await discoverWorkspacePackages(workspaceRoot, options);
813
+ if (packagesToAnalyze.length === 0) {
814
+ console.log("No packages found to analyze for release.");
815
+ return null;
691
816
  }
692
817
  const changedPackages = await analyzeCommits(packagesToAnalyze, workspaceRoot);
693
818
  if (changedPackages.size === 0) throw new Error("No packages have changes requiring a release");
694
- let versionUpdates = calculateVersions(workspacePackages, changedPackages);
819
+ let versionUpdates = [];
820
+ for (const [pkgName, bump] of changedPackages) {
821
+ const pkg = workspacePackages.find((p) => p.name === pkgName);
822
+ if (pkg) versionUpdates.push(createVersionUpdate(pkg, bump, true));
823
+ }
695
824
  const isVersionPromptEnabled = options.prompts?.versions !== false;
696
825
  if (!isCI && isVersionPromptEnabled) {
697
826
  const versionOverrides = await promptVersionOverrides(versionUpdates.map((u) => ({
698
- name: u.package.name,
827
+ package: u.package,
699
828
  currentVersion: u.currentVersion,
700
829
  suggestedVersion: u.newVersion,
701
830
  bumpType: u.bumpType
702
- })));
831
+ })), workspaceRoot);
703
832
  versionUpdates = versionUpdates.map((update) => {
704
833
  const overriddenVersion = versionOverrides.get(update.package.name);
705
834
  if (overriddenVersion && overriddenVersion !== update.newVersion) return {
@@ -709,7 +838,7 @@ async function release(options) {
709
838
  return update;
710
839
  });
711
840
  }
712
- const allUpdates = createDependentUpdates(getPackageUpdateOrder(buildDependencyGraph(workspacePackages), new Set(versionUpdates.map((u) => u.package.name))), versionUpdates);
841
+ const allUpdates = createDependentUpdates(buildPackageDependencyGraph(workspacePackages), workspacePackages, versionUpdates);
713
842
  const currentBranch = await getCurrentBranch(workspaceRoot);
714
843
  const existingPullRequest = await getExistingPullRequest({
715
844
  owner,
@@ -732,7 +861,7 @@ async function release(options) {
732
861
  }
733
862
  console.log("Rebasing release branch onto", currentBranch);
734
863
  await rebaseBranch(currentBranch, workspaceRoot);
735
- await updatePackageJsonFiles(allUpdates);
864
+ await updateAllPackageJsonFiles(allUpdates);
736
865
  const hasCommitted = await commitChanges("chore: update release versions", workspaceRoot);
737
866
  const isBranchAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot);
738
867
  if (!hasCommitted && !isBranchAhead) {
@@ -772,64 +901,6 @@ async function release(options) {
772
901
  created: !prExists
773
902
  };
774
903
  }
775
- async function discoverPackages(workspaceRoot, options) {
776
- let workspacePackages;
777
- let packagesToAnalyze;
778
- if (typeof options.packages === "boolean" && options.packages === true) {
779
- workspacePackages = await findWorkspacePackages(workspaceRoot, { excludePrivate: false });
780
- packagesToAnalyze = workspacePackages;
781
- return {
782
- workspacePackages,
783
- packagesToAnalyze
784
- };
785
- }
786
- if (Array.isArray(options.packages)) {
787
- const packageNames = options.packages;
788
- workspacePackages = await findWorkspacePackages(workspaceRoot, {
789
- excludePrivate: false,
790
- included: packageNames
791
- });
792
- packagesToAnalyze = workspacePackages.filter((pkg) => packageNames.includes(pkg.name));
793
- if (packagesToAnalyze.length !== packageNames.length) {
794
- const found = new Set(packagesToAnalyze.map((p) => p.name));
795
- const missing = packageNames.filter((p) => !found.has(p));
796
- throw new Error(`Packages not found in workspace: ${missing.join(", ")}`);
797
- }
798
- return {
799
- workspacePackages,
800
- packagesToAnalyze
801
- };
802
- }
803
- workspacePackages = await findWorkspacePackages(workspaceRoot, options.packages);
804
- packagesToAnalyze = workspacePackages;
805
- return {
806
- workspacePackages,
807
- packagesToAnalyze
808
- };
809
- }
810
- async function analyzeCommits(packages, workspaceRoot) {
811
- const changedPackages = /* @__PURE__ */ new Map();
812
- for (const pkg of packages) {
813
- const bump = await analyzePackageCommits(pkg, workspaceRoot);
814
- if (bump !== "none") changedPackages.set(pkg.name, bump);
815
- }
816
- return changedPackages;
817
- }
818
- function calculateVersions(allPackages, changedPackages) {
819
- const updates = [];
820
- for (const [pkgName, bump] of changedPackages) {
821
- const pkg = allPackages.find((p) => p.name === pkgName);
822
- if (!pkg) continue;
823
- updates.push(createVersionUpdate(pkg, bump, true));
824
- }
825
- return updates;
826
- }
827
- async function updatePackageJsonFiles(updates) {
828
- await Promise.all(updates.map(async (update) => {
829
- const depUpdates = getDependencyUpdates(update.package, updates);
830
- await updatePackageJson(update.package, update.newVersion, depUpdates);
831
- }));
832
- }
833
904
 
834
905
  //#endregion
835
906
  export { release };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,15 +19,15 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
+ "@luxass/utils": "2.7.2",
22
23
  "commit-parser": "0.4.5",
23
- "debug": "4.4.3",
24
+ "eta": "4.0.1",
24
25
  "farver": "1.0.0-beta.1",
25
26
  "prompts": "2.4.2",
26
27
  "tinyexec": "1.0.2"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@luxass/eslint-config": "6.0.1",
30
- "@types/debug": "4.1.12",
31
31
  "@types/node": "22.18.12",
32
32
  "@types/prompts": "2.4.9",
33
33
  "eslint": "9.39.1",