@ucdjs/release-scripts 0.1.0-beta.2 → 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
@@ -99,6 +102,21 @@ interface ReleaseOptions {
99
102
  * GitHub token for authentication
100
103
  */
101
104
  githubToken: string;
105
+ pullRequest?: {
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;
119
+ };
102
120
  }
103
121
  interface ReleaseResult {
104
122
  /**
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
  /**
@@ -407,34 +292,6 @@ async function pushBranch(branch, workspaceRoot, options) {
407
292
  else if (options?.force) args.push("--force");
408
293
  await run("git", args, { nodeOptions: { cwd: workspaceRoot } });
409
294
  }
410
- /**
411
- * Generate PR body from version updates
412
- * @param updates - Array of version updates to include in the PR body
413
- * @returns Formatted PR body as a string
414
- */
415
- function generatePRBody(updates) {
416
- const lines = [];
417
- lines.push("## Packages");
418
- lines.push("");
419
- const directChanges = updates.filter((u) => u.hasDirectChanges);
420
- const dependencyUpdates = updates.filter((u) => !u.hasDirectChanges);
421
- if (directChanges.length > 0) {
422
- lines.push("### Direct Changes");
423
- lines.push("");
424
- for (const update of directChanges) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (${update.bumpType})`);
425
- lines.push("");
426
- }
427
- if (dependencyUpdates.length > 0) {
428
- lines.push("### Dependency Updates");
429
- lines.push("");
430
- for (const update of dependencyUpdates) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (dependencies changed)`);
431
- lines.push("");
432
- }
433
- lines.push("---");
434
- lines.push("");
435
- lines.push("This release PR was automatically generated.");
436
- return lines.join("\n");
437
- }
438
295
 
439
296
  //#endregion
440
297
  //#region src/github.ts
@@ -502,26 +359,311 @@ async function upsertPullRequest({ owner, repo, title, body, head, base, pullNum
502
359
  throw err;
503
360
  }
504
361
  }
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
+ }));
570
+ }
505
571
 
506
572
  //#endregion
507
573
  //#region src/prompts.ts
508
- 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) {
509
648
  const response = await prompts({
510
649
  type: "multiselect",
511
650
  name: "selectedPackages",
512
651
  message: "Select packages to release",
513
652
  choices: packages.map((pkg) => ({
514
- title: `${pkg.name} (${pkg.version})`,
653
+ title: `${pkg.name} (${farver.bold(pkg.version)})`,
515
654
  value: pkg.name,
516
655
  selected: true
517
656
  })),
518
657
  min: 1,
519
- hint: "Space to select/deselect. Return to submit."
658
+ hint: "Space to select/deselect. Return to submit.",
659
+ instructions: false
520
660
  });
521
- if (!response.selectedPackages || response.selectedPackages.length === 0) throw new Error("No packages selected");
661
+ if (!response.selectedPackages || response.selectedPackages.length === 0) return [];
522
662
  return response.selectedPackages;
523
663
  }
524
- 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`);
525
667
  const choices = [{
526
668
  title: `Use suggested: ${suggestedVersion} (${suggestedBumpType})`,
527
669
  value: "suggested"
@@ -544,7 +686,7 @@ async function promptVersionOverride(packageName, currentVersion, suggestedVersi
544
686
  const response = await prompts([{
545
687
  type: "select",
546
688
  name: "choice",
547
- message: `${packageName} (${currentVersion}):`,
689
+ message: `${pkg.name} (${currentVersion}):`,
548
690
  choices,
549
691
  initial: 0
550
692
  }, {
@@ -560,18 +702,85 @@ async function promptVersionOverride(packageName, currentVersion, suggestedVersi
560
702
  else if (response.choice === "custom") return response.customVersion;
561
703
  else return calculateNewVersion(currentVersion, response.choice);
562
704
  }
563
- async function promptVersionOverrides(packages) {
705
+ async function promptVersionOverrides(packages, workspaceRoot) {
564
706
  const overrides = /* @__PURE__ */ new Map();
565
- for (const pkg of packages) {
566
- const newVersion = await promptVersionOverride(pkg.name, pkg.currentVersion, pkg.suggestedVersion, pkg.bumpType);
567
- 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);
568
710
  }
569
711
  return overrides;
570
712
  }
571
713
 
572
714
  //#endregion
573
715
  //#region src/workspace.ts
574
- 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
+ }
575
784
  function shouldIncludePackage(pkg, options) {
576
785
  if (!options) return true;
577
786
  if (options.excludePrivate && pkg.private) return false;
@@ -581,130 +790,45 @@ function shouldIncludePackage(pkg, options) {
581
790
  if (options.excluded?.includes(pkg.name)) return false;
582
791
  return true;
583
792
  }
584
- async function findWorkspacePackages(workspaceRoot, options) {
585
- const result = await run("pnpm", [
586
- "-r",
587
- "ls",
588
- "--json"
589
- ], { nodeOptions: {
590
- cwd: workspaceRoot,
591
- stdio: "pipe"
592
- } });
593
- const rawProjects = JSON.parse(result.stdout);
594
- const packages = [];
595
- const allPackageNames = new Set(rawProjects.map((p) => p.name));
596
- for (const rawProject of rawProjects) {
597
- const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
598
- const packageJson = JSON.parse(content);
599
- if (!shouldIncludePackage(packageJson, options)) {
600
- debug?.(`Excluding package ${rawProject.name}`);
601
- continue;
602
- }
603
- const workspaceDeps = extractWorkspaceDependencies(rawProject.dependencies, allPackageNames);
604
- const workspaceDevDeps = extractWorkspaceDependencies(rawProject.devDependencies, allPackageNames);
605
- packages.push({
606
- name: rawProject.name,
607
- version: rawProject.version,
608
- path: rawProject.path,
609
- packageJson,
610
- workspaceDependencies: workspaceDeps,
611
- workspaceDevDependencies: workspaceDevDeps
612
- });
613
- }
614
- return packages;
615
- }
616
793
  function extractWorkspaceDependencies(dependencies, workspacePackages) {
617
794
  if (!dependencies) return [];
618
795
  return Object.keys(dependencies).filter((dep) => {
619
796
  return workspacePackages.has(dep);
620
797
  });
621
798
  }
622
- function buildDependencyGraph(packages) {
623
- const packagesMap = /* @__PURE__ */ new Map();
624
- const dependents = /* @__PURE__ */ new Map();
625
- for (const pkg of packages) {
626
- packagesMap.set(pkg.name, pkg);
627
- dependents.set(pkg.name, /* @__PURE__ */ new Set());
628
- }
629
- for (const pkg of packages) {
630
- const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
631
- for (const dep of allDeps) {
632
- const depSet = dependents.get(dep);
633
- if (depSet) depSet.add(pkg.name);
634
- }
635
- }
636
- return {
637
- packages: packagesMap,
638
- dependents
639
- };
640
- }
641
- function getPackageUpdateOrder(graph, changedPackages) {
642
- const result = [];
643
- const visited = /* @__PURE__ */ new Set();
644
- const toUpdate = new Set(changedPackages);
645
- const packagesToProcess = new Set(changedPackages);
646
- for (const pkg of changedPackages) {
647
- const deps = graph.dependents.get(pkg);
648
- if (deps) for (const dep of deps) {
649
- packagesToProcess.add(dep);
650
- toUpdate.add(dep);
651
- }
652
- }
653
- function visit(pkgName, level) {
654
- if (visited.has(pkgName)) return;
655
- visited.add(pkgName);
656
- const pkg = graph.packages.get(pkgName);
657
- if (!pkg) return;
658
- const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
659
- let maxDepLevel = level;
660
- for (const dep of allDeps) if (toUpdate.has(dep)) {
661
- visit(dep, level);
662
- const depResult = result.find((r) => r.package.name === dep);
663
- if (depResult && depResult.level >= maxDepLevel) maxDepLevel = depResult.level + 1;
664
- }
665
- result.push({
666
- package: pkg,
667
- level: maxDepLevel
668
- });
669
- }
670
- for (const pkg of toUpdate) visit(pkg, 0);
671
- result.sort((a, b) => a.level - b.level);
672
- return result;
673
- }
674
799
 
675
800
  //#endregion
676
801
  //#region src/release.ts
677
- const isCI = process.env.CI === "true";
678
802
  async function release(options) {
679
803
  const { dryRun: dryRun$1 = false, safeguards = true, workspaceRoot = process.cwd(), releaseBranch = "release/next", githubToken } = options;
680
804
  globalOptions.dryRun = dryRun$1;
681
805
  if (githubToken.trim() === "" || githubToken == null) throw new Error("GitHub token is required");
682
806
  const [owner, repo] = options.repo.split("/");
683
807
  if (!owner || !repo) throw new Error(`Invalid repo format: ${options.repo}. Expected "owner/repo".`);
684
- if (safeguards && !isWorkingDirectoryClean(workspaceRoot)) {
808
+ if (safeguards && !await isWorkingDirectoryClean(workspaceRoot)) {
685
809
  console.error("Working directory is not clean. Please commit or stash your changes before proceeding.");
686
810
  return null;
687
811
  }
688
- const { workspacePackages, packagesToAnalyze: initialPackages } = await discoverPackages(workspaceRoot, options);
689
- if (initialPackages.length === 0) return null;
690
- const isPackagePromptEnabled = options.prompts?.packages !== false;
691
- const isPackagesPreConfigured = Array.isArray(options.packages) || typeof options.packages === "object" && options.packages.included != null;
692
- let packagesToAnalyze = initialPackages;
693
- if (!isCI && isPackagePromptEnabled && !isPackagesPreConfigured) {
694
- const selectedNames = await promptPackageSelection(initialPackages);
695
- 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;
696
816
  }
697
817
  const changedPackages = await analyzeCommits(packagesToAnalyze, workspaceRoot);
698
818
  if (changedPackages.size === 0) throw new Error("No packages have changes requiring a release");
699
- 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
+ }
700
824
  const isVersionPromptEnabled = options.prompts?.versions !== false;
701
825
  if (!isCI && isVersionPromptEnabled) {
702
826
  const versionOverrides = await promptVersionOverrides(versionUpdates.map((u) => ({
703
- name: u.package.name,
827
+ package: u.package,
704
828
  currentVersion: u.currentVersion,
705
829
  suggestedVersion: u.newVersion,
706
830
  bumpType: u.bumpType
707
- })));
831
+ })), workspaceRoot);
708
832
  versionUpdates = versionUpdates.map((update) => {
709
833
  const overriddenVersion = versionOverrides.get(update.package.name);
710
834
  if (overriddenVersion && overriddenVersion !== update.newVersion) return {
@@ -714,7 +838,7 @@ async function release(options) {
714
838
  return update;
715
839
  });
716
840
  }
717
- const allUpdates = createDependentUpdates(getPackageUpdateOrder(buildDependencyGraph(workspacePackages), new Set(versionUpdates.map((u) => u.package.name))), versionUpdates);
841
+ const allUpdates = createDependentUpdates(buildPackageDependencyGraph(workspacePackages), workspacePackages, versionUpdates);
718
842
  const currentBranch = await getCurrentBranch(workspaceRoot);
719
843
  const existingPullRequest = await getExistingPullRequest({
720
844
  owner,
@@ -737,7 +861,7 @@ async function release(options) {
737
861
  }
738
862
  console.log("Rebasing release branch onto", currentBranch);
739
863
  await rebaseBranch(currentBranch, workspaceRoot);
740
- await updatePackageJsonFiles(allUpdates);
864
+ await updateAllPackageJsonFiles(allUpdates);
741
865
  const hasCommitted = await commitChanges("chore: update release versions", workspaceRoot);
742
866
  const isBranchAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot);
743
867
  if (!hasCommitted && !isBranchAhead) {
@@ -757,8 +881,8 @@ async function release(options) {
757
881
  }
758
882
  console.log("Pushing changes to remote");
759
883
  await pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
760
- const prTitle = existingPullRequest?.title || "Release: Update package versions";
761
- const prBody = generatePRBody(allUpdates);
884
+ const prTitle = existingPullRequest?.title || options.pullRequest?.title || "chore: update package versions";
885
+ const prBody = generatePullRequestBody(allUpdates, options.pullRequest?.body);
762
886
  const pullRequest = await upsertPullRequest({
763
887
  owner,
764
888
  repo,
@@ -777,64 +901,6 @@ async function release(options) {
777
901
  created: !prExists
778
902
  };
779
903
  }
780
- async function discoverPackages(workspaceRoot, options) {
781
- let workspacePackages;
782
- let packagesToAnalyze;
783
- if (typeof options.packages === "boolean" && options.packages === true) {
784
- workspacePackages = await findWorkspacePackages(workspaceRoot, { excludePrivate: false });
785
- packagesToAnalyze = workspacePackages;
786
- return {
787
- workspacePackages,
788
- packagesToAnalyze
789
- };
790
- }
791
- if (Array.isArray(options.packages)) {
792
- const packageNames = options.packages;
793
- workspacePackages = await findWorkspacePackages(workspaceRoot, {
794
- excludePrivate: false,
795
- included: packageNames
796
- });
797
- packagesToAnalyze = workspacePackages.filter((pkg) => packageNames.includes(pkg.name));
798
- if (packagesToAnalyze.length !== packageNames.length) {
799
- const found = new Set(packagesToAnalyze.map((p) => p.name));
800
- const missing = packageNames.filter((p) => !found.has(p));
801
- throw new Error(`Packages not found in workspace: ${missing.join(", ")}`);
802
- }
803
- return {
804
- workspacePackages,
805
- packagesToAnalyze
806
- };
807
- }
808
- workspacePackages = await findWorkspacePackages(workspaceRoot, options.packages);
809
- packagesToAnalyze = workspacePackages;
810
- return {
811
- workspacePackages,
812
- packagesToAnalyze
813
- };
814
- }
815
- async function analyzeCommits(packages, workspaceRoot) {
816
- const changedPackages = /* @__PURE__ */ new Map();
817
- for (const pkg of packages) {
818
- const bump = await analyzePackageCommits(pkg, workspaceRoot);
819
- if (bump !== "none") changedPackages.set(pkg.name, bump);
820
- }
821
- return changedPackages;
822
- }
823
- function calculateVersions(allPackages, changedPackages) {
824
- const updates = [];
825
- for (const [pkgName, bump] of changedPackages) {
826
- const pkg = allPackages.find((p) => p.name === pkgName);
827
- if (!pkg) continue;
828
- updates.push(createVersionUpdate(pkg, bump, true));
829
- }
830
- return updates;
831
- }
832
- async function updatePackageJsonFiles(updates) {
833
- await Promise.all(updates.map(async (update) => {
834
- const depUpdates = getDependencyUpdates(update.package, updates);
835
- await updatePackageJson(update.package, update.newVersion, depUpdates);
836
- }));
837
- }
838
904
 
839
905
  //#endregion
840
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.2",
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",