@ucdjs/release-scripts 0.1.0-beta.11 → 0.1.0-beta.12

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,5 @@
1
+ import "commit-parser";
2
+
1
3
  //#region src/workspace.d.ts
2
4
  interface WorkspacePackage {
3
5
  name: string;
@@ -10,6 +12,7 @@ interface WorkspacePackage {
10
12
  //#endregion
11
13
  //#region src/types.d.ts
12
14
  type BumpKind = "none" | "patch" | "minor" | "major";
15
+ type GlobalCommitMode = false | "dependencies" | "all";
13
16
  interface SharedOptions {
14
17
  /**
15
18
  * Repository identifier (e.g., "owner/repo")
@@ -99,6 +102,26 @@ interface VersionUpdate {
99
102
  interface PublishOptions extends SharedOptions {}
100
103
  declare function publish(_options: PublishOptions): void;
101
104
  //#endregion
105
+ //#region src/changelog.d.ts
106
+ interface ChangelogOptions {
107
+ /**
108
+ * Whether to generate changelogs
109
+ * @default false
110
+ */
111
+ enabled?: boolean;
112
+ /**
113
+ * Transform function to customize the changelog content
114
+ */
115
+ transform?: (changelog: string, pkg: WorkspacePackage) => string | Promise<string>;
116
+ /**
117
+ * Repository information for generating links
118
+ */
119
+ repository?: {
120
+ owner: string;
121
+ repo: string;
122
+ };
123
+ }
124
+ //#endregion
102
125
  //#region src/release.d.ts
103
126
  interface ReleaseOptions extends SharedOptions {
104
127
  branch?: {
@@ -139,6 +162,11 @@ interface ReleaseOptions extends SharedOptions {
139
162
  */
140
163
  body?: string;
141
164
  };
165
+ /**
166
+ * Changelog configuration
167
+ */
168
+ changelog?: ChangelogOptions;
169
+ globalCommitMode?: GlobalCommitMode;
142
170
  }
143
171
  interface ReleaseResult {
144
172
  /**
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  import { t as Eta } from "./eta-Boh7yPZi.mjs";
2
2
  import farver from "farver";
3
- import { getCommits } from "commit-parser";
3
+ import { existsSync } from "node:fs";
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+ import { join } from "node:path";
4
6
  import process from "node:process";
5
7
  import { exec } from "tinyexec";
8
+ import { getCommits } from "commit-parser";
6
9
  import { dedent } from "@luxass/utils";
7
- import { join } from "node:path";
8
- import { readFile, writeFile } from "node:fs/promises";
9
10
  import prompts from "prompts";
10
11
 
11
12
  //#region src/publish.ts
@@ -77,6 +78,114 @@ function normalizeSharedOptions(options) {
77
78
  };
78
79
  }
79
80
 
81
+ //#endregion
82
+ //#region src/changelog.ts
83
+ /**
84
+ * Get section label for commit type
85
+ */
86
+ function getSectionLabel(type) {
87
+ return {
88
+ feat: "Features",
89
+ fix: "Bug Fixes",
90
+ docs: "Documentation",
91
+ style: "Styles",
92
+ refactor: "Code Refactoring",
93
+ perf: "Performance Improvements",
94
+ test: "Tests",
95
+ build: "Build System",
96
+ ci: "Continuous Integration",
97
+ chore: "Miscellaneous Chores",
98
+ revert: "Reverts"
99
+ }[type] || "Other Changes";
100
+ }
101
+ /**
102
+ * Generate changelog content from commits
103
+ */
104
+ function generateChangelog(pkg, newVersion, commits, previousVersion, repository) {
105
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
106
+ let versionHeader = `## `;
107
+ if (repository && previousVersion) {
108
+ const compareUrl = `https://github.com/${repository.owner}/${repository.repo}/compare/${pkg.name}@${previousVersion}...${pkg.name}@${newVersion}`;
109
+ versionHeader += `[${newVersion}](${compareUrl})`;
110
+ } else versionHeader += newVersion;
111
+ versionHeader += ` (${date})\n\n`;
112
+ let changelog = versionHeader;
113
+ const grouped = /* @__PURE__ */ new Map();
114
+ for (const commit of commits) {
115
+ if (!commit.isConventional || !commit.type) continue;
116
+ const type = commit.type;
117
+ if (!grouped.has(type)) grouped.set(type, []);
118
+ grouped.get(type).push(commit);
119
+ }
120
+ for (const type of [
121
+ "feat",
122
+ "fix",
123
+ "perf",
124
+ "refactor",
125
+ "docs",
126
+ "test",
127
+ "build",
128
+ "ci",
129
+ "chore",
130
+ "revert",
131
+ "style"
132
+ ]) {
133
+ const commits$1 = grouped.get(type);
134
+ if (!commits$1 || commits$1.length === 0) continue;
135
+ const label = getSectionLabel(type);
136
+ changelog += `### ${label}\n\n`;
137
+ for (const commit of commits$1) {
138
+ const scope = commit.scope ? `**${commit.scope}:** ` : "";
139
+ const breaking = commit.isBreaking ? " **BREAKING CHANGE**" : "";
140
+ let entry = `* ${scope}${commit.description}${breaking}`;
141
+ if (repository) {
142
+ const commitUrl = `https://github.com/${repository.owner}/${repository.repo}/commit/${commit.shortHash}`;
143
+ entry += ` ([${commit.shortHash}](${commitUrl}))`;
144
+ } else entry += ` (${commit.shortHash})`;
145
+ changelog += `${entry}\n`;
146
+ }
147
+ changelog += "\n";
148
+ }
149
+ return changelog.trim();
150
+ }
151
+ /**
152
+ * Write changelog to package's CHANGELOG.md file
153
+ */
154
+ async function writeChangelog(pkg, newContent, version) {
155
+ const changelogPath = join(pkg.path, "CHANGELOG.md");
156
+ let existingContent = "";
157
+ if (existsSync(changelogPath)) existingContent = await readFile(changelogPath, "utf-8");
158
+ let updatedContent;
159
+ if (existingContent) {
160
+ const withoutTitle = existingContent.replace(/^# Changelog\n\n/, "");
161
+ if (new RegExp(`^## ${version.replace(/\./g, "\\.")}(\\s|$)`, "m").test(withoutTitle)) {
162
+ const versionSectionRegex = new RegExp(`^## ${version.replace(/\./g, "\\.")}[\\s\\S]*?(?=^## |$)`, "m");
163
+ updatedContent = `# Changelog\n\n${withoutTitle.replace(versionSectionRegex, `${newContent}\n\n`)}`;
164
+ } else updatedContent = `# Changelog\n\n${newContent}\n\n${withoutTitle}`;
165
+ } else updatedContent = `# Changelog\n\n${newContent}\n`;
166
+ await writeFile(changelogPath, updatedContent, "utf-8");
167
+ logger.log(`Updated changelog: ${changelogPath}`);
168
+ }
169
+ /**
170
+ * Generate and write changelogs for all updated packages
171
+ */
172
+ async function updateChangelogs(updates, packageCommits, options) {
173
+ if (!options?.enabled) {
174
+ logger.log("Changelog generation is disabled");
175
+ return;
176
+ }
177
+ logger.log("Generating changelogs...");
178
+ for (const update of updates) {
179
+ if (!update.hasDirectChanges) continue;
180
+ const commits = packageCommits.get(update.package.name) || [];
181
+ if (commits.length === 0) continue;
182
+ let changelog = generateChangelog(update.package, update.newVersion, commits, update.currentVersion, options.repository);
183
+ if (options.transform) changelog = await options.transform(changelog, update.package);
184
+ logger.info(`Generating changelog for package ${update.package.name}`);
185
+ await writeChangelog(update.package, changelog, update.newVersion);
186
+ }
187
+ }
188
+
80
189
  //#endregion
81
190
  //#region src/commits.ts
82
191
  async function getLastPackageTag(packageName, workspaceRoot) {
@@ -140,6 +249,58 @@ async function getWorkspacePackageCommits(workspaceRoot, packages) {
140
249
  for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
141
250
  return changedPackages;
142
251
  }
252
+ /**
253
+ * Get all commits for the workspace (not filtered by package)
254
+ */
255
+ async function getAllWorkspaceCommits(workspaceRoot, lastTag) {
256
+ return getCommits({
257
+ from: lastTag,
258
+ to: "HEAD",
259
+ cwd: workspaceRoot
260
+ });
261
+ }
262
+ /**
263
+ * Get files changed in a specific commit
264
+ */
265
+ async function getFilesChangedInCommit(commitHash, workspaceRoot) {
266
+ try {
267
+ const { stdout } = await run("git", [
268
+ "diff-tree",
269
+ "--no-commit-id",
270
+ "--name-only",
271
+ "-r",
272
+ commitHash
273
+ ], { nodeOptions: {
274
+ cwd: workspaceRoot,
275
+ stdio: "pipe"
276
+ } });
277
+ return stdout.split("\n").map((file) => file.trim()).filter(Boolean);
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+ /**
283
+ * Filter and combine package commits with global commits
284
+ */
285
+ function combineWithGlobalCommits(workspaceRoot, packageCommits, allCommits, mode) {
286
+ if (!mode) return packageCommits;
287
+ const packageCommitShas = new Set(packageCommits.map((c) => c.shortHash));
288
+ const globalCommits = allCommits.filter((c) => !packageCommitShas.has(c.shortHash));
289
+ if (mode === "all") return [...packageCommits, ...globalCommits];
290
+ if (mode === "dependencies") {
291
+ const dependencyCommits = globalCommits.filter(async (c) => {
292
+ const affectedFiles = await getFilesChangedInCommit(c.shortHash, workspaceRoot);
293
+ if (affectedFiles == null) return false;
294
+ return affectedFiles.some((file) => [
295
+ "package.json",
296
+ "pnpm-lock.yaml",
297
+ "pnpm-workspace.yaml"
298
+ ].includes(file));
299
+ });
300
+ return [...packageCommits, ...dependencyCommits];
301
+ }
302
+ return packageCommits;
303
+ }
143
304
  function determineBumpType(commit) {
144
305
  if (commit.isBreaking) return "major";
145
306
  if (!commit.isConventional || !commit.type) return "none";
@@ -599,19 +760,15 @@ function createVersionUpdate(pkg, bump, hasDirectChanges) {
599
760
  /**
600
761
  * Infer version updates from package commits with optional interactive overrides
601
762
  *
602
- * @param workspacePackages - All workspace packages
603
- * @param packageCommits - Map of package names to their commits
604
- * @param workspaceRoot - Root directory for prompts
605
- * @param showPrompt - Whether to show prompts for version overrides
606
763
  * @returns Version updates for packages with changes
607
764
  */
608
- async function inferVersionUpdates(workspacePackages, packageCommits, workspaceRoot, showPrompt) {
765
+ async function inferVersionUpdates({ workspacePackages, packageCommits, allCommits, workspaceRoot, showPrompt, globalCommitMode }) {
609
766
  const versionUpdates = [];
610
- for (const [pkgName, commits] of packageCommits) {
611
- if (commits.length === 0) continue;
767
+ for (const [pkgName, pkgCommits] of packageCommits) {
768
+ if (pkgCommits.length === 0) continue;
612
769
  const pkg = workspacePackages.find((p) => p.name === pkgName);
613
770
  if (!pkg) continue;
614
- const bump = determineHighestBump(commits);
771
+ const bump = determineHighestBump(combineWithGlobalCommits(workspaceRoot, pkgCommits, allCommits, globalCommitMode));
615
772
  if (bump === "none") {
616
773
  logger.info(`No version bump needed for package ${pkg.name}`);
617
774
  continue;
@@ -841,6 +998,8 @@ async function release(options) {
841
998
  normalizedOptions.branch.release ??= "release/next";
842
999
  normalizedOptions.branch.default = await getDefaultBranch();
843
1000
  normalizedOptions.safeguards ??= true;
1001
+ normalizedOptions.changelog ??= { enabled: true };
1002
+ normalizedOptions.globalCommitMode ??= "dependencies";
844
1003
  globalOptions.dryRun = normalizedOptions.dryRun;
845
1004
  const workspaceRoot = normalizedOptions.workspaceRoot;
846
1005
  if (normalizedOptions.safeguards && !await isWorkingDirectoryClean(workspaceRoot)) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
@@ -849,7 +1008,15 @@ async function release(options) {
849
1008
  logger.log("No packages found to release.");
850
1009
  return null;
851
1010
  }
852
- const versionUpdates = await inferVersionUpdates(workspacePackages, await getWorkspacePackageCommits(workspaceRoot, workspacePackages), workspaceRoot, options.prompts?.versions !== false);
1011
+ const packageCommits = await getWorkspacePackageCommits(workspaceRoot, workspacePackages);
1012
+ const versionUpdates = await inferVersionUpdates({
1013
+ workspacePackages,
1014
+ packageCommits,
1015
+ workspaceRoot,
1016
+ showPrompt: options.prompts?.versions !== false,
1017
+ allCommits: await getAllWorkspaceCommits(workspaceRoot),
1018
+ globalCommitMode: options.globalCommitMode
1019
+ });
853
1020
  if (versionUpdates.length === 0) logger.warn("No packages have changes requiring a release");
854
1021
  const allUpdates = createDependentUpdates(buildPackageDependencyGraph(workspacePackages), workspacePackages, versionUpdates);
855
1022
  const currentBranch = await getCurrentBranch(workspaceRoot);
@@ -876,6 +1043,13 @@ async function release(options) {
876
1043
  logger.log("Rebasing release branch onto", normalizedOptions.branch.default);
877
1044
  await rebaseBranch(normalizedOptions.branch.default, workspaceRoot);
878
1045
  await updateAllPackageJsonFiles(allUpdates);
1046
+ await updateChangelogs(versionUpdates, packageCommits, {
1047
+ ...options.changelog,
1048
+ repository: options.changelog?.repository || {
1049
+ owner: normalizedOptions.owner,
1050
+ repo: normalizedOptions.repo
1051
+ }
1052
+ });
879
1053
  const hasCommitted = await commitChanges("chore: update release versions", workspaceRoot);
880
1054
  const isBranchAhead = await isBranchAheadOfRemote(normalizedOptions.branch.release, workspaceRoot);
881
1055
  if (!hasCommitted && !isBranchAhead) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.11",
3
+ "version": "0.1.0-beta.12",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",