@qualcomm-ui/changesets-cli 1.2.0 → 2.0.0
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/changelog-formatter.cjs +43 -88
- package/dist/changelog-formatter.cjs.map +1 -7
- package/dist/cli.js +1003 -0
- package/dist/cli.js.map +1 -0
- package/dist/create-github-releases.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/tsbuildinfo +1 -1
- package/lib/cli.js +6 -0
- package/package.json +2 -2
- package/dist/cli.cjs +0 -162805
- package/dist/cli.cjs.map +0 -7
- package/lib/cli.cjs +0 -3
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
import { program } from "@commander-js/extra-typings";
|
|
2
|
+
import { stdin, stdout } from "node:process";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import * as core from "@actions/core";
|
|
5
|
+
import { getPackages, getPackagesSync } from "@manypkg/get-packages";
|
|
6
|
+
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
7
|
+
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
|
+
import dayjs from "dayjs";
|
|
9
|
+
import advancedFormat from "dayjs/plugin/advancedFormat.js";
|
|
10
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
11
|
+
import { Octokit } from "@octokit/rest";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import readChangeset from "@changesets/read";
|
|
14
|
+
import writeChangeset from "@changesets/write";
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { Node, Project, ts } from "ts-morph";
|
|
17
|
+
//#region src/publishable-packages.ts
|
|
18
|
+
var DEFAULT_CONFIG_LOCATION = join(".changeset", "config.json");
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves all packages in the monorepo that should be published.
|
|
21
|
+
* Filters out packages marked as private and ignored packages.
|
|
22
|
+
*
|
|
23
|
+
* @param configPath - Path to the changesets config file, relative to cwd
|
|
24
|
+
* @param cwd - Working directory (defaults to process.cwd())
|
|
25
|
+
* @returns Array of package objects that are eligible for publishing
|
|
26
|
+
*/
|
|
27
|
+
async function getPublishablePackages(configPath, cwd = process.cwd()) {
|
|
28
|
+
const { packages } = await getPackages(cwd);
|
|
29
|
+
const changesetConfig = JSON.parse(await readFile(resolve(cwd, configPath ?? DEFAULT_CONFIG_LOCATION), "utf-8"));
|
|
30
|
+
const ignoredPackages = new Set(changesetConfig.ignored ?? []);
|
|
31
|
+
return packages.filter((pkg) => {
|
|
32
|
+
if (ignoredPackages.has(pkg.packageJson.name)) return false;
|
|
33
|
+
if (!pkg.packageJson.version) return false;
|
|
34
|
+
return !pkg.packageJson.private;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/check-versions.ts
|
|
39
|
+
async function getPublishedVersion(packageName) {
|
|
40
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
|
|
41
|
+
if (response.status === 404) return null;
|
|
42
|
+
if (!response.ok) throw new Error(`Failed to fetch ${packageName}: ${response.status}`);
|
|
43
|
+
return (await response.json())["dist-tags"]?.latest || null;
|
|
44
|
+
}
|
|
45
|
+
function compareVersions(current, published) {
|
|
46
|
+
const [cMajor, cMinor, cPatch] = current.split(".").map(Number);
|
|
47
|
+
const [pMajor, pMinor, pPatch] = published.split(".").map(Number);
|
|
48
|
+
if (cMajor !== pMajor) return cMajor - pMajor;
|
|
49
|
+
if (cMinor !== pMinor) return cMinor - pMinor;
|
|
50
|
+
return cPatch - pPatch;
|
|
51
|
+
}
|
|
52
|
+
async function checkVersions(options) {
|
|
53
|
+
const packages = await getPublishablePackages(options?.configPath);
|
|
54
|
+
const newer = (await Promise.all(packages.map(async (pkg) => {
|
|
55
|
+
const { packageJson } = pkg;
|
|
56
|
+
const { name, version } = packageJson;
|
|
57
|
+
const published = await getPublishedVersion(name);
|
|
58
|
+
if (!published) return {
|
|
59
|
+
name,
|
|
60
|
+
unpublished: true,
|
|
61
|
+
version
|
|
62
|
+
};
|
|
63
|
+
const comparison = compareVersions(version, published);
|
|
64
|
+
return {
|
|
65
|
+
current: version,
|
|
66
|
+
isNewer: comparison > 0,
|
|
67
|
+
isOlder: comparison < 0,
|
|
68
|
+
isSame: comparison === 0,
|
|
69
|
+
name,
|
|
70
|
+
published
|
|
71
|
+
};
|
|
72
|
+
}))).filter((r) => "isNewer" in r && r.isNewer === true);
|
|
73
|
+
if (newer.length > 0) {
|
|
74
|
+
console.log("The following packages will be published:");
|
|
75
|
+
newer.forEach((r) => console.log(` ${r.name}: ${r.published} -> ${r.current}`));
|
|
76
|
+
core.setOutput("should-publish", true);
|
|
77
|
+
} else {
|
|
78
|
+
console.log("No packages need publishing.");
|
|
79
|
+
core.setOutput("should-publish", false);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/consolidate-changelogs.ts
|
|
84
|
+
dayjs.extend(advancedFormat);
|
|
85
|
+
var DATE_FORMAT = "MMM Do, YYYY";
|
|
86
|
+
function getChangedChangelogs() {
|
|
87
|
+
try {
|
|
88
|
+
return execSync("git diff --name-only HEAD", { encoding: "utf-8" }).trim().split("\n").filter(Boolean).filter((file) => file.endsWith("CHANGELOG.md"));
|
|
89
|
+
} catch {
|
|
90
|
+
console.error("Failed to get changed files from git diff");
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function consolidateChangelog(changelogPath) {
|
|
95
|
+
const lines = (await readFile(changelogPath, "utf-8")).split("\n");
|
|
96
|
+
const firstReleaseIndex = lines.findIndex((line) => line.startsWith("## "));
|
|
97
|
+
if (firstReleaseIndex === -1) return;
|
|
98
|
+
const secondReleaseIndex = lines.findIndex((line, i) => i > firstReleaseIndex && line.startsWith("## "));
|
|
99
|
+
const endIndex = secondReleaseIndex === -1 ? lines.length : secondReleaseIndex;
|
|
100
|
+
const before = lines.slice(0, firstReleaseIndex);
|
|
101
|
+
const releaseLines = lines.slice(firstReleaseIndex, endIndex);
|
|
102
|
+
const after = lines.slice(endIndex);
|
|
103
|
+
const sections = /* @__PURE__ */ new Map();
|
|
104
|
+
let versionLine = "";
|
|
105
|
+
let dateLine = "";
|
|
106
|
+
let currentSection = "";
|
|
107
|
+
for (const line of releaseLines) {
|
|
108
|
+
if (line.startsWith("## ")) {
|
|
109
|
+
const dateMatch = line.match(/\((\d{4}\/\d{2}\/\d{2})\)/);
|
|
110
|
+
if (dateMatch) {
|
|
111
|
+
versionLine = line.replace(` (${dateMatch[1]})`, "");
|
|
112
|
+
dateLine = dayjs(dateMatch[1]).format(DATE_FORMAT);
|
|
113
|
+
} else {
|
|
114
|
+
versionLine = line;
|
|
115
|
+
dateLine = dayjs().format(DATE_FORMAT);
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (line.startsWith("### Patch Changes") || line.startsWith("### Minor Changes") || line.startsWith("### Major Changes")) continue;
|
|
120
|
+
if (line.startsWith("### ")) {
|
|
121
|
+
currentSection = line;
|
|
122
|
+
if (!sections.has(currentSection)) sections.set(currentSection, []);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (currentSection && line.trim()) sections.get(currentSection)?.push(line.trim());
|
|
126
|
+
}
|
|
127
|
+
const output = [];
|
|
128
|
+
output.push(versionLine);
|
|
129
|
+
output.push("");
|
|
130
|
+
output.push(dateLine);
|
|
131
|
+
output.push("");
|
|
132
|
+
for (const [section, items] of sections) {
|
|
133
|
+
if (items.length === 0) continue;
|
|
134
|
+
output.push(section);
|
|
135
|
+
output.push("");
|
|
136
|
+
output.push(...items);
|
|
137
|
+
output.push("");
|
|
138
|
+
}
|
|
139
|
+
await writeFile(changelogPath, [
|
|
140
|
+
...before,
|
|
141
|
+
...output,
|
|
142
|
+
...after
|
|
143
|
+
].join("\n"));
|
|
144
|
+
}
|
|
145
|
+
async function consolidateChangelogs() {
|
|
146
|
+
const changedChangelogs = getChangedChangelogs();
|
|
147
|
+
if (changedChangelogs.length === 0) {
|
|
148
|
+
console.log("No changelogs changed");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
console.log(`Consolidating ${changedChangelogs.length} changelog(s)...`);
|
|
152
|
+
for (const changelogPath of changedChangelogs) {
|
|
153
|
+
console.log(` - ${changelogPath}`);
|
|
154
|
+
await consolidateChangelog(changelogPath);
|
|
155
|
+
}
|
|
156
|
+
console.log("Done");
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
159
|
+
//#region src/create-github-releases.ts
|
|
160
|
+
/**
|
|
161
|
+
* Parses a changelog file to extract the latest version entry.
|
|
162
|
+
*
|
|
163
|
+
* @param path - Path to the CHANGELOG.md file
|
|
164
|
+
* @returns Object containing version, date, and body of the latest entry, or null if parsing fails
|
|
165
|
+
*/
|
|
166
|
+
async function parseChangelog(path) {
|
|
167
|
+
const lines = (await readFile(path, "utf-8")).split("\n");
|
|
168
|
+
const firstVersionIndex = lines.findIndex((l) => l.startsWith("## "));
|
|
169
|
+
if (firstVersionIndex === -1) {
|
|
170
|
+
console.log(` No version header found`);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const headerLine = lines[firstVersionIndex];
|
|
174
|
+
const match = headerLine.match(/^## ([\d.]+)/);
|
|
175
|
+
if (!match) {
|
|
176
|
+
console.log(` Invalid version format: ${headerLine}`);
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
const [, version, date] = match;
|
|
180
|
+
const endIndex = lines.findIndex((l, i) => i > firstVersionIndex && l.startsWith("## "));
|
|
181
|
+
return {
|
|
182
|
+
body: lines.slice(firstVersionIndex + 1, endIndex === -1 ? void 0 : endIndex).join("\n").trim(),
|
|
183
|
+
date,
|
|
184
|
+
version
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function getRepoFromGitRemote() {
|
|
188
|
+
const remoteUrl = execSync("git remote get-url origin").toString().trim();
|
|
189
|
+
const match = remoteUrl.match(/github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
190
|
+
if (!match) throw new Error(`Could not parse GitHub owner/repo from remote URL: ${remoteUrl}, use --repo to specify explicitly.`);
|
|
191
|
+
return {
|
|
192
|
+
owner: match[1],
|
|
193
|
+
repo: match[2]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async function createGitHubReleases(options) {
|
|
197
|
+
const octokit = new Octokit({ auth: options.token });
|
|
198
|
+
const repoOpts = options.repo ? {
|
|
199
|
+
owner: options.repo.split("/")[0],
|
|
200
|
+
repo: options.repo.split("/")[1]
|
|
201
|
+
} : getRepoFromGitRemote();
|
|
202
|
+
const packages = await getPublishablePackages(options.configPath);
|
|
203
|
+
for (const pkg of packages) {
|
|
204
|
+
const changelog = await parseChangelog(join(pkg.dir, "CHANGELOG.md")).catch(() => null);
|
|
205
|
+
if (!changelog) {
|
|
206
|
+
console.warn("no changelog found, skipping package:", pkg.packageJson.name);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (changelog.version !== pkg.packageJson.version) {
|
|
210
|
+
console.log(`Skipping ${pkg.packageJson.name}: changelog ${changelog.version} !== package.json ${pkg.packageJson.version}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const tag = `${pkg.packageJson.name}@${changelog.version}`;
|
|
214
|
+
const release = await octokit.repos.getReleaseByTag({
|
|
215
|
+
...repoOpts,
|
|
216
|
+
tag
|
|
217
|
+
}).catch(() => null);
|
|
218
|
+
if (release) {
|
|
219
|
+
console.log(`Release \x1b[93m${release.data.name}\x1b[0m already exists, skipping`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
console.log(`Creating release: \x1b[96m${tag}\x1b[0m`);
|
|
223
|
+
await octokit.repos.createRelease({
|
|
224
|
+
...repoOpts,
|
|
225
|
+
body: changelog.body,
|
|
226
|
+
name: tag,
|
|
227
|
+
tag_name: tag
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/generate-release-notes.ts
|
|
233
|
+
var RELEASE_NOTES_FILENAME = "release-notes.md";
|
|
234
|
+
function extractLatestEntry(content, fallbackName) {
|
|
235
|
+
const lines = content.split("\n");
|
|
236
|
+
const packageName = (lines[0]?.match(/^# (.+) Changelog$/))?.[1] ?? fallbackName;
|
|
237
|
+
const firstVersionIndex = lines.findIndex((l) => l.startsWith("## "));
|
|
238
|
+
if (firstVersionIndex === -1) return null;
|
|
239
|
+
const versionMatch = lines[firstVersionIndex].match(/^## ([\d.]+)/);
|
|
240
|
+
if (!versionMatch) return null;
|
|
241
|
+
const version = versionMatch[1];
|
|
242
|
+
const endIndex = lines.findIndex((l, i) => i > firstVersionIndex && l.startsWith("## "));
|
|
243
|
+
const bodyLines = lines.slice(firstVersionIndex + 1, endIndex === -1 ? void 0 : endIndex);
|
|
244
|
+
const date = bodyLines.find((l) => /^[A-Z][a-z]{2} \d/.test(l.trim()))?.trim() ?? "";
|
|
245
|
+
return {
|
|
246
|
+
date,
|
|
247
|
+
packageName,
|
|
248
|
+
sections: bodyLines.filter((l) => l.trim() !== date).join("\n").trim(),
|
|
249
|
+
version
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function isDepsOnlyEntry(sections) {
|
|
253
|
+
const lines = sections.split("\n").filter((l) => l.trim());
|
|
254
|
+
const hasOnlyChoresHeader = lines.filter((l) => l.startsWith("### ")).every((l) => l === "### Miscellaneous Chores");
|
|
255
|
+
const bulletLines = lines.filter((l) => l.startsWith("- "));
|
|
256
|
+
const allDepsBullets = bulletLines.every((l) => l.includes("**deps:** update dependencies"));
|
|
257
|
+
return hasOnlyChoresHeader && allDepsBullets && bulletLines.length > 0;
|
|
258
|
+
}
|
|
259
|
+
function getReleaseNotesPath() {
|
|
260
|
+
return join(tmpdir(), RELEASE_NOTES_FILENAME);
|
|
261
|
+
}
|
|
262
|
+
async function generateReleaseNotes() {
|
|
263
|
+
const changedChangelogs = getChangedChangelogs();
|
|
264
|
+
if (changedChangelogs.length === 0) {
|
|
265
|
+
console.log("No changed changelogs, skipping release notes generation");
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
const entries = [];
|
|
269
|
+
for (const changelogPath of changedChangelogs) {
|
|
270
|
+
const entry = extractLatestEntry(await readFile(changelogPath, "utf-8"), changelogPath);
|
|
271
|
+
if (entry) entries.push(entry);
|
|
272
|
+
}
|
|
273
|
+
if (entries.length === 0) {
|
|
274
|
+
console.log("No parseable changelog entries found");
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
const substantive = entries.filter((e) => !isDepsOnlyEntry(e.sections));
|
|
278
|
+
const depsOnly = entries.filter((e) => isDepsOnlyEntry(e.sections));
|
|
279
|
+
const lines = ["# Release Notes", ""];
|
|
280
|
+
for (const entry of substantive) lines.push(`## ${entry.packageName} — ${entry.version} (${entry.date})`, "", entry.sections, "");
|
|
281
|
+
if (depsOnly.length > 0) {
|
|
282
|
+
lines.push("---", "", "<details>", "<summary>Dependency-only updates</summary>", "");
|
|
283
|
+
for (const entry of depsOnly) lines.push(`- ${entry.packageName} — ${entry.version}`);
|
|
284
|
+
lines.push("", "</details>", "");
|
|
285
|
+
}
|
|
286
|
+
const markdown = lines.join("\n");
|
|
287
|
+
const outPath = getReleaseNotesPath();
|
|
288
|
+
await writeFile(outPath, markdown);
|
|
289
|
+
console.log(`Release notes written to ${outPath}`);
|
|
290
|
+
console.log(` ${substantive.length} package(s) with changes, ${depsOnly.length} dependency-only update(s)`);
|
|
291
|
+
return outPath;
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/utils.ts
|
|
295
|
+
var defaultCommitTypes = [
|
|
296
|
+
{
|
|
297
|
+
section: "Features",
|
|
298
|
+
type: "feat"
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
section: "Features",
|
|
302
|
+
type: "feature"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
section: "Bug Fixes",
|
|
306
|
+
type: "fix"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
section: "Performance Improvements",
|
|
310
|
+
type: "perf"
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
section: "Reverts",
|
|
314
|
+
type: "revert"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
section: "Documentation",
|
|
318
|
+
type: "docs"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
section: "Styles",
|
|
322
|
+
type: "style"
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
section: "Styles",
|
|
326
|
+
type: "styles"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
section: "Miscellaneous Chores",
|
|
330
|
+
type: "chore"
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
section: "Code Refactoring",
|
|
334
|
+
type: "refactor"
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
section: "Tests",
|
|
338
|
+
type: "test"
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
section: "Build System",
|
|
342
|
+
type: "build"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
section: "Continuous Integration",
|
|
346
|
+
type: "ci"
|
|
347
|
+
}
|
|
348
|
+
];
|
|
349
|
+
/**
|
|
350
|
+
* Normalizes conventional commit format by removing whitespace between type and
|
|
351
|
+
* scope. Transforms "fix (scope): message" to "fix(scope): message"
|
|
352
|
+
* @param commit - The commit message to normalize
|
|
353
|
+
* @returns Normalized commit message
|
|
354
|
+
*/
|
|
355
|
+
function normalizeConventionalCommit(commit) {
|
|
356
|
+
const normalized = commit.replace(/^(\w+)\s+(\(.*?\))/, "$1$2");
|
|
357
|
+
if (normalized.startsWith("- ")) return normalized.substring(2);
|
|
358
|
+
return normalized;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Checks if a commit message follows conventional commit format
|
|
362
|
+
* @param commit - The commit message to check
|
|
363
|
+
* @returns True if the commit follows conventional commit format
|
|
364
|
+
*/
|
|
365
|
+
function isConventionalCommit(commit) {
|
|
366
|
+
const normalized = normalizeConventionalCommit(commit);
|
|
367
|
+
return defaultCommitTypes.some((commitType) => normalized.match(new RegExp(`^(?:-\\s)?${commitType.type}\\s*(?:\(.*\))?!?:`)));
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Checks if a commit message indicates a breaking change
|
|
371
|
+
* @param commit - The commit message to check
|
|
372
|
+
* @returns True if the commit contains a breaking change indicator
|
|
373
|
+
*/
|
|
374
|
+
function isBreakingChange(commit) {
|
|
375
|
+
const normalized = normalizeConventionalCommit(commit);
|
|
376
|
+
return normalized.includes("BREAKING CHANGE:") || defaultCommitTypes.some((commitType) => normalized.match(new RegExp(`^${commitType.type}\\s*(?:\(.*\))?!:`)));
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Filters commits to only conventional commits and maps them to changelog format
|
|
380
|
+
* @param commits - Array of commits to translate
|
|
381
|
+
* @returns Array of conventional commit messages with their associated commit hashes
|
|
382
|
+
*/
|
|
383
|
+
function translateCommitsToConventionalCommitMessages(commits) {
|
|
384
|
+
return commits.filter((commit) => isConventionalCommit(commit.commitMessage)).map((commit) => ({
|
|
385
|
+
changelogMessage: normalizeConventionalCommit(commit.commitMessage),
|
|
386
|
+
commitHashes: [commit.commitHash]
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Gets the list of files changed between two git refs
|
|
391
|
+
* @param opts - Object containing from and to git refs
|
|
392
|
+
* @returns Array of file paths that changed
|
|
393
|
+
*/
|
|
394
|
+
function getFilesChangedSince(opts) {
|
|
395
|
+
return execSync(`git diff --name-only ${opts.from}~1...${opts.to}`).toString().trim().split("\n");
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Gets the absolute path to the git repository root
|
|
399
|
+
* @returns Absolute path to repository root
|
|
400
|
+
*/
|
|
401
|
+
function getRepoRoot() {
|
|
402
|
+
return execSync("git rev-parse --show-toplevel").toString().trim().replace(/\n|\r/g, "");
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Gets the full commit message for a given commit hash
|
|
406
|
+
* @param commitHash - The git commit hash
|
|
407
|
+
* @returns The full commit message
|
|
408
|
+
*/
|
|
409
|
+
function getCommitMessage(commitHash) {
|
|
410
|
+
return execSync(`git log -1 --pretty=%B ${commitHash}`).toString().trim();
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Extracts all conventional commit messages from a multi-line commit message
|
|
414
|
+
* @param commitMessage - The commit message to parse
|
|
415
|
+
* @returns Array of conventional commit messages found
|
|
416
|
+
*/
|
|
417
|
+
function extractConventionalCommits(commitMessage) {
|
|
418
|
+
const lines = commitMessage.split("\n");
|
|
419
|
+
const conventionalCommits = [];
|
|
420
|
+
for (const line of lines) {
|
|
421
|
+
const trimmed = line.trim();
|
|
422
|
+
if (trimmed && isConventionalCommit(trimmed)) conventionalCommits.push(normalizeConventionalCommit(trimmed));
|
|
423
|
+
}
|
|
424
|
+
return conventionalCommits;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Converts conventional commit messages to changesets based on affected packages
|
|
428
|
+
* @param conventionalMessagesToCommits - Array of conventional messages with their commit hashes
|
|
429
|
+
* @param options - Configuration options including ignored files and packages
|
|
430
|
+
* @returns Array of changesets for affected packages
|
|
431
|
+
*/
|
|
432
|
+
function conventionalMessagesWithCommitsToChangesets(conventionalMessagesToCommits, options) {
|
|
433
|
+
const { ignoredFiles = [], includeCommitLinks, packages } = options;
|
|
434
|
+
const repoRoot = getRepoRoot();
|
|
435
|
+
return conventionalMessagesToCommits.flatMap((entry) => {
|
|
436
|
+
const filesChanged = getFilesChangedSince({
|
|
437
|
+
from: entry.commitHashes[0],
|
|
438
|
+
to: entry.commitHashes[entry.commitHashes.length - 1]
|
|
439
|
+
}).filter((file) => {
|
|
440
|
+
return ignoredFiles.every((ignoredPattern) => !file.match(ignoredPattern));
|
|
441
|
+
});
|
|
442
|
+
const packagesChanged = packages.filter((pkg) => {
|
|
443
|
+
const pkgPath = pkg.dir.replace(/\\/g, "/").replace(`${repoRoot}/`, "");
|
|
444
|
+
return filesChanged.some((file) => file.startsWith(`${pkgPath}/`));
|
|
445
|
+
});
|
|
446
|
+
if (packagesChanged.length === 0) return [];
|
|
447
|
+
return entry.commitHashes.flatMap((hash) => {
|
|
448
|
+
return extractConventionalCommits(getCommitMessage(hash)).map((msg) => ({
|
|
449
|
+
hash,
|
|
450
|
+
message: msg
|
|
451
|
+
}));
|
|
452
|
+
}).filter((item, index, self) => index === self.findIndex((other) => other.message === item.message)).flatMap(({ hash, message: conventionalCommit }) => {
|
|
453
|
+
const changeType = isBreakingChange(conventionalCommit) ? "major" : conventionalCommit.startsWith("feat") ? "minor" : "patch";
|
|
454
|
+
return {
|
|
455
|
+
packagesChanged,
|
|
456
|
+
releases: packagesChanged.map((pkg) => ({
|
|
457
|
+
name: pkg.packageJson.name,
|
|
458
|
+
type: changeType
|
|
459
|
+
})),
|
|
460
|
+
summary: includeCommitLinks ? `${conventionalCommit}\n\ncommit: ${hash.slice(0, 7)}` : conventionalCommit
|
|
461
|
+
};
|
|
462
|
+
});
|
|
463
|
+
}).filter(Boolean);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Fetches the latest changes from a remote branch
|
|
467
|
+
* @param branch - The branch name to fetch
|
|
468
|
+
*/
|
|
469
|
+
function gitFetch(branch) {
|
|
470
|
+
execSync(`git fetch origin ${branch}`);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Gets all commit hashes since a specific commit
|
|
474
|
+
* @param sha - The commit SHA to compare against
|
|
475
|
+
* @returns Array of commit hashes since the given commit
|
|
476
|
+
*/
|
|
477
|
+
function getCommitsSinceCommit(sha) {
|
|
478
|
+
return execSync(`git rev-list ${sha}..HEAD`).toString().split("\n").filter(Boolean).reverse();
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Gets all commit hashes since a reference branch or tag
|
|
482
|
+
* @param branch - The branch to compare against
|
|
483
|
+
* @returns Array of commit hashes since the reference point
|
|
484
|
+
*/
|
|
485
|
+
function getCommitsSinceBranch(branch) {
|
|
486
|
+
gitFetch(branch);
|
|
487
|
+
return execSync(`git rev-list ${`origin/${branch}`}..HEAD`).toString().split("\n").filter(Boolean).reverse();
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Compares two changesets for equality
|
|
491
|
+
* @param a - First changeset
|
|
492
|
+
* @param b - Second changeset
|
|
493
|
+
* @returns True if changesets are equal
|
|
494
|
+
*/
|
|
495
|
+
function compareChangeSet(a, b) {
|
|
496
|
+
return a.summary.replace(/\n$/, "") === b.summary && JSON.stringify(a.releases) === JSON.stringify(b.releases);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Returns changesets in array a that are not in array b
|
|
500
|
+
* @param a - Array of changesets to filter
|
|
501
|
+
* @param b - Array of changesets to compare against
|
|
502
|
+
* @returns Changesets that exist in a but not in b
|
|
503
|
+
*/
|
|
504
|
+
function difference(a, b) {
|
|
505
|
+
return a.filter((changeA) => !b.some((changeB) => compareChangeSet(changeA, changeB)));
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/main.ts
|
|
509
|
+
var CHANGESET_CONFIG_LOCATION$1 = join(".changeset", "config.json");
|
|
510
|
+
function getCommitsWithMessages(commitHashes) {
|
|
511
|
+
return commitHashes.map((commitHash) => {
|
|
512
|
+
return {
|
|
513
|
+
commitHash,
|
|
514
|
+
commitMessage: execSync(`git log -n 1 --pretty=format:%B ${commitHash}`).toString()
|
|
515
|
+
};
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
async function conventionalCommitChangeset(options, cwd = process.cwd()) {
|
|
519
|
+
const configLocation = options.configPath ?? CHANGESET_CONFIG_LOCATION$1;
|
|
520
|
+
const changesetConfig = JSON.parse(readFileSync(join(cwd, configLocation)).toString());
|
|
521
|
+
const ignored = changesetConfig.ignore ?? [];
|
|
522
|
+
const packages = getPackagesSync(cwd).packages.filter((pkg) => Boolean(pkg.packageJson.version) && !ignored.includes(pkg.packageJson.name));
|
|
523
|
+
const { baseBranch = "main" } = changesetConfig;
|
|
524
|
+
const { commitSha, includeCommitLinks } = options;
|
|
525
|
+
const changesets = conventionalMessagesWithCommitsToChangesets(translateCommitsToConventionalCommitMessages(getCommitsWithMessages(commitSha ? getCommitsSinceCommit(commitSha) : getCommitsSinceBranch(baseBranch))), {
|
|
526
|
+
ignoredFiles: ignored,
|
|
527
|
+
includeCommitLinks,
|
|
528
|
+
packages
|
|
529
|
+
});
|
|
530
|
+
const currentChangesets = await readChangeset(cwd);
|
|
531
|
+
const newChangesets = currentChangesets.length === 0 ? changesets : difference(changesets, currentChangesets);
|
|
532
|
+
await Promise.all(newChangesets.map((changeset) => writeChangeset(changeset, cwd)));
|
|
533
|
+
}
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/update-jsdoc-since-tags.ts
|
|
536
|
+
var NEXT_RELEASE = "next-release";
|
|
537
|
+
var NEXT_RELEASE_TAG_PATTERN = /@since\s+next-release\b/;
|
|
538
|
+
var SOURCE_EXTENSIONS = new Set([
|
|
539
|
+
".cjs",
|
|
540
|
+
".cts",
|
|
541
|
+
".js",
|
|
542
|
+
".jsx",
|
|
543
|
+
".mjs",
|
|
544
|
+
".mts",
|
|
545
|
+
".ts",
|
|
546
|
+
".tsx"
|
|
547
|
+
]);
|
|
548
|
+
var EXCLUDED_DIRECTORIES = new Set([
|
|
549
|
+
".turbo",
|
|
550
|
+
"__tests__",
|
|
551
|
+
"build",
|
|
552
|
+
"coverage",
|
|
553
|
+
"dist",
|
|
554
|
+
"lib",
|
|
555
|
+
"node_modules"
|
|
556
|
+
]);
|
|
557
|
+
var TEST_FILE_PATTERN = /\.(spec|test)\.[cm]?[jt]sx?$/;
|
|
558
|
+
function pluralize(count, singular, plural) {
|
|
559
|
+
return count === 1 ? singular : plural;
|
|
560
|
+
}
|
|
561
|
+
function formatJsDocSinceUpdateStartMessage() {
|
|
562
|
+
return "Updating JSDoc @since tags...";
|
|
563
|
+
}
|
|
564
|
+
function formatJsDocSincePackageUpdateProgress({ name, version }) {
|
|
565
|
+
return `Processing package ${name} ${version}...`;
|
|
566
|
+
}
|
|
567
|
+
function normalizeRelativePath(cwd, filePath) {
|
|
568
|
+
return relative(cwd, filePath).split(sep).join("/");
|
|
569
|
+
}
|
|
570
|
+
function hasNextReleaseSinceTag(content) {
|
|
571
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, content);
|
|
572
|
+
let token = scanner.scan();
|
|
573
|
+
while (token !== ts.SyntaxKind.EndOfFileToken) {
|
|
574
|
+
if (token === ts.SyntaxKind.MultiLineCommentTrivia) {
|
|
575
|
+
const text = scanner.getTokenText();
|
|
576
|
+
if (text.startsWith("/**") && NEXT_RELEASE_TAG_PATTERN.test(text)) return true;
|
|
577
|
+
}
|
|
578
|
+
token = scanner.scan();
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
function getNodeName(node) {
|
|
583
|
+
if (Node.isClassDeclaration(node) || Node.isEnumDeclaration(node) || Node.isFunctionDeclaration(node) || Node.isInterfaceDeclaration(node) || Node.isMethodDeclaration(node) || Node.isMethodSignature(node) || Node.isPropertyDeclaration(node) || Node.isPropertySignature(node) || Node.isTypeAliasDeclaration(node)) return node.getName();
|
|
584
|
+
if (Node.isVariableStatement(node)) return node.getDeclarationList().getDeclarations().map((declaration) => declaration.getName()).join(", ");
|
|
585
|
+
}
|
|
586
|
+
function getQualifiedEntityName(node) {
|
|
587
|
+
const name = getNodeName(node);
|
|
588
|
+
if (!name) return "unknown";
|
|
589
|
+
const parent = node.getParent();
|
|
590
|
+
const parentName = parent ? getNodeName(parent) : void 0;
|
|
591
|
+
if (parentName) return `${parentName}.${name}`;
|
|
592
|
+
return name;
|
|
593
|
+
}
|
|
594
|
+
function getVersionedPackages(packages) {
|
|
595
|
+
return packages.flatMap((pkg) => {
|
|
596
|
+
if (!pkg.version) return [];
|
|
597
|
+
return [{
|
|
598
|
+
name: pkg.name,
|
|
599
|
+
root: pkg.root,
|
|
600
|
+
version: pkg.version
|
|
601
|
+
}];
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function isSourceFile(filePath) {
|
|
605
|
+
return SOURCE_EXTENSIONS.has(extname(filePath)) && !TEST_FILE_PATTERN.test(filePath);
|
|
606
|
+
}
|
|
607
|
+
async function getSourceFiles(dir) {
|
|
608
|
+
const entries = (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
609
|
+
const files = [];
|
|
610
|
+
for (const entry of entries) {
|
|
611
|
+
const entryPath = join(dir, entry.name);
|
|
612
|
+
if (entry.isDirectory()) {
|
|
613
|
+
if (!EXCLUDED_DIRECTORIES.has(entry.name)) files.push(...await getSourceFiles(entryPath));
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (entry.isFile() && isSourceFile(entry.name)) files.push(entryPath);
|
|
617
|
+
}
|
|
618
|
+
return files;
|
|
619
|
+
}
|
|
620
|
+
async function updateSourceFile(filePath, version) {
|
|
621
|
+
const sourceFile = new Project({
|
|
622
|
+
compilerOptions: {
|
|
623
|
+
allowJs: true,
|
|
624
|
+
checkJs: false
|
|
625
|
+
},
|
|
626
|
+
manipulationSettings: {
|
|
627
|
+
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: false,
|
|
628
|
+
useTrailingCommas: true
|
|
629
|
+
},
|
|
630
|
+
skipAddingFilesFromTsConfig: true
|
|
631
|
+
}).addSourceFileAtPath(filePath);
|
|
632
|
+
let tagCount = 0;
|
|
633
|
+
sourceFile.forEachDescendant((node) => {
|
|
634
|
+
if (!Node.isJSDocable(node)) return;
|
|
635
|
+
for (const jsDoc of node.getJsDocs()) for (const tag of jsDoc.getTags()) if (tag.getTagName() === "since" && tag.getCommentText()?.trim() === NEXT_RELEASE) {
|
|
636
|
+
tag.set({
|
|
637
|
+
tagName: "since",
|
|
638
|
+
text: version
|
|
639
|
+
});
|
|
640
|
+
tagCount += 1;
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
if (tagCount > 0) await sourceFile.save();
|
|
644
|
+
return tagCount;
|
|
645
|
+
}
|
|
646
|
+
async function findUnresolvedFiles(cwd, sourceFiles) {
|
|
647
|
+
const unresolvedFiles = [];
|
|
648
|
+
for (const filePath of sourceFiles) if (hasNextReleaseSinceTag(await readFile(filePath, "utf-8"))) unresolvedFiles.push(normalizeRelativePath(cwd, filePath));
|
|
649
|
+
return unresolvedFiles;
|
|
650
|
+
}
|
|
651
|
+
function collectSourceFileSinceTagLocations(cwd, sourceFile) {
|
|
652
|
+
const filePath = normalizeRelativePath(cwd, sourceFile.getFilePath());
|
|
653
|
+
const locations = [];
|
|
654
|
+
sourceFile.forEachDescendant((node) => {
|
|
655
|
+
if (!Node.isJSDocable(node)) return;
|
|
656
|
+
for (const jsDoc of node.getJsDocs()) for (const tag of jsDoc.getTags()) if (tag.getTagName() === "since" && tag.getCommentText()?.trim() === NEXT_RELEASE) locations.push({
|
|
657
|
+
entityName: getQualifiedEntityName(node),
|
|
658
|
+
filePath
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
return locations;
|
|
662
|
+
}
|
|
663
|
+
function createProject() {
|
|
664
|
+
return new Project({
|
|
665
|
+
compilerOptions: {
|
|
666
|
+
allowJs: true,
|
|
667
|
+
checkJs: false
|
|
668
|
+
},
|
|
669
|
+
manipulationSettings: {
|
|
670
|
+
insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: false,
|
|
671
|
+
useTrailingCommas: true
|
|
672
|
+
},
|
|
673
|
+
skipAddingFilesFromTsConfig: true
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
function getFileSinceTagLocations(cwd, filePath) {
|
|
677
|
+
return collectSourceFileSinceTagLocations(cwd, createProject().addSourceFileAtPath(filePath));
|
|
678
|
+
}
|
|
679
|
+
async function checkJsDocSinceTags({ cwd = process.cwd(), packages }) {
|
|
680
|
+
const unresolvedTags = [];
|
|
681
|
+
for (const pkg of packages) {
|
|
682
|
+
const sourceFiles = await getSourceFiles(pkg.root);
|
|
683
|
+
for (const filePath of sourceFiles) unresolvedTags.push(...getFileSinceTagLocations(cwd, filePath));
|
|
684
|
+
}
|
|
685
|
+
return { unresolvedTags };
|
|
686
|
+
}
|
|
687
|
+
async function updatePackages(cwd, packages, onProgress) {
|
|
688
|
+
const updatedPackages = [];
|
|
689
|
+
const unresolvedFiles = [];
|
|
690
|
+
for (const pkg of packages) {
|
|
691
|
+
onProgress?.(formatJsDocSincePackageUpdateProgress(pkg));
|
|
692
|
+
const sourceFiles = await getSourceFiles(pkg.root);
|
|
693
|
+
let fileCount = 0;
|
|
694
|
+
let tagCount = 0;
|
|
695
|
+
for (const filePath of sourceFiles) {
|
|
696
|
+
const updatedTags = await updateSourceFile(filePath, pkg.version);
|
|
697
|
+
if (updatedTags > 0) {
|
|
698
|
+
fileCount += 1;
|
|
699
|
+
tagCount += updatedTags;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (tagCount > 0) updatedPackages.push({
|
|
703
|
+
fileCount,
|
|
704
|
+
name: pkg.name,
|
|
705
|
+
tagCount,
|
|
706
|
+
version: pkg.version
|
|
707
|
+
});
|
|
708
|
+
unresolvedFiles.push(...await findUnresolvedFiles(cwd, sourceFiles));
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
unresolvedFiles,
|
|
712
|
+
updatedPackages
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
async function updateJsDocSinceTagsForPackages({ cwd = process.cwd(), onProgress, packages }) {
|
|
716
|
+
return updatePackages(cwd, getVersionedPackages(packages), onProgress);
|
|
717
|
+
}
|
|
718
|
+
function formatJsDocSinceUpdateResult(result) {
|
|
719
|
+
const lines = [];
|
|
720
|
+
if (result.updatedPackages.length > 0) {
|
|
721
|
+
lines.push("Updated JSDoc @since tags:");
|
|
722
|
+
for (const pkg of result.updatedPackages) lines.push(` ${pkg.name} ${pkg.version}: ${pkg.tagCount} ${pluralize(pkg.tagCount, "tag", "tags")} in ${pkg.fileCount} ${pluralize(pkg.fileCount, "file", "files")}`);
|
|
723
|
+
}
|
|
724
|
+
if (result.unresolvedFiles.length > 0) {
|
|
725
|
+
if (lines.length > 0) lines.push("");
|
|
726
|
+
lines.push("Warning: unresolved @since next-release tags remain after JSDoc version update:");
|
|
727
|
+
lines.push(...result.unresolvedFiles.map((filePath) => ` ${filePath}`));
|
|
728
|
+
lines.push("");
|
|
729
|
+
lines.push("These tags were not rewritten automatically. This is non-blocking, but the updater may need to support another JSDoc shape.");
|
|
730
|
+
}
|
|
731
|
+
return lines;
|
|
732
|
+
}
|
|
733
|
+
function formatJsDocSinceCheckResult(result) {
|
|
734
|
+
if (result.unresolvedTags.length === 0) return ["No @since next-release tags found."];
|
|
735
|
+
return ["Found @since next-release tags:", ...result.unresolvedTags.map(({ entityName, filePath }) => ` ${filePath}: ${entityName}`)];
|
|
736
|
+
}
|
|
737
|
+
//#endregion
|
|
738
|
+
//#region src/version-bump.ts
|
|
739
|
+
var CHANGESET_CONFIG_LOCATION = join(".changeset", "config.json");
|
|
740
|
+
function normalizePath(path) {
|
|
741
|
+
return resolve(path);
|
|
742
|
+
}
|
|
743
|
+
function isWithinDirectory(parent, child) {
|
|
744
|
+
const relativePath = relative(parent, child);
|
|
745
|
+
return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
746
|
+
}
|
|
747
|
+
function getPackageLabel(directory) {
|
|
748
|
+
return directory.replaceAll("\\", "/");
|
|
749
|
+
}
|
|
750
|
+
function getSelectedDirectory(cwd, directory) {
|
|
751
|
+
return resolve(cwd, directory);
|
|
752
|
+
}
|
|
753
|
+
function getSelectedDirectories({ cwd, directories, directory }) {
|
|
754
|
+
return (directories && directories.length > 0 ? directories : directory ? [directory] : []).map((selectedDirectory) => ({
|
|
755
|
+
input: selectedDirectory,
|
|
756
|
+
root: getSelectedDirectory(cwd, selectedDirectory)
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
function getPackageSourceSnapshot(pkg, versionOverride) {
|
|
760
|
+
return {
|
|
761
|
+
...pkg,
|
|
762
|
+
root: resolve(pkg.root, "src"),
|
|
763
|
+
version: versionOverride ?? pkg.version
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function getPackageSourceSnapshots(packages, versionOverride) {
|
|
767
|
+
return packages.map((pkg) => getPackageSourceSnapshot(pkg, versionOverride));
|
|
768
|
+
}
|
|
769
|
+
function normalizeGitPath(path) {
|
|
770
|
+
return path.split(sep).join("/");
|
|
771
|
+
}
|
|
772
|
+
function getBaseBranch({ configPath = CHANGESET_CONFIG_LOCATION, cwd }) {
|
|
773
|
+
return JSON.parse(readFileSync(join(cwd, configPath), "utf-8")).baseBranch ?? "main";
|
|
774
|
+
}
|
|
775
|
+
function getPackageVersionAtRef(pkg, diffRef, cwd) {
|
|
776
|
+
const packageJsonPath = normalizeGitPath(relative(cwd, join(pkg.root, "package.json")));
|
|
777
|
+
try {
|
|
778
|
+
const packageJson = execFileSync("git", ["show", `${diffRef}:${packageJsonPath}`], {
|
|
779
|
+
cwd,
|
|
780
|
+
encoding: "utf-8"
|
|
781
|
+
});
|
|
782
|
+
return JSON.parse(packageJson).version;
|
|
783
|
+
} catch {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function isGitRefResolvable(diffRef, cwd) {
|
|
788
|
+
try {
|
|
789
|
+
execFileSync("git", [
|
|
790
|
+
"rev-parse",
|
|
791
|
+
"--verify",
|
|
792
|
+
`${diffRef}^{commit}`
|
|
793
|
+
], {
|
|
794
|
+
cwd,
|
|
795
|
+
stdio: "ignore"
|
|
796
|
+
});
|
|
797
|
+
return true;
|
|
798
|
+
} catch {
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function getPackagesChangedSinceRef({ cwd, diffRef, packages }) {
|
|
803
|
+
if (!isGitRefResolvable(diffRef, cwd)) throw new Error(`Git ref "${diffRef}" could not be resolved.`);
|
|
804
|
+
return packages.filter((pkg) => getPackageVersionAtRef(pkg, diffRef, cwd) !== pkg.version);
|
|
805
|
+
}
|
|
806
|
+
function findContainingPackage(directory, packages) {
|
|
807
|
+
return packages.filter((pkg) => isWithinDirectory(normalizePath(pkg.root), directory)).sort((a, b) => normalizePath(b.root).length - normalizePath(a.root).length).at(0);
|
|
808
|
+
}
|
|
809
|
+
function selectCustomCheckSnapshots(directories, packages) {
|
|
810
|
+
return directories.map(({ input, root }) => {
|
|
811
|
+
const containingPackage = findContainingPackage(root, packages);
|
|
812
|
+
return {
|
|
813
|
+
name: containingPackage?.name ?? getPackageLabel(input),
|
|
814
|
+
root,
|
|
815
|
+
version: containingPackage?.version
|
|
816
|
+
};
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
function selectCustomUpdateSnapshots(directories, packages, versionOverride) {
|
|
820
|
+
return directories.map(({ input, root }) => {
|
|
821
|
+
const containingPackage = findContainingPackage(root, packages);
|
|
822
|
+
const version = versionOverride ?? containingPackage?.version;
|
|
823
|
+
if (!version) throw new Error(`Directory "${input}" is not within a package with a version. Pass --version to update arbitrary directories.`);
|
|
824
|
+
return {
|
|
825
|
+
name: containingPackage?.name ?? getPackageLabel(input),
|
|
826
|
+
root,
|
|
827
|
+
version
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
function selectDefaultPackageSourceSnapshots({ configPath, cwd = process.cwd(), diffRef, directories, directory, packages, version }) {
|
|
832
|
+
const selectedDirectories = getSelectedDirectories({
|
|
833
|
+
cwd,
|
|
834
|
+
directories,
|
|
835
|
+
directory
|
|
836
|
+
});
|
|
837
|
+
if (selectedDirectories.length === 0) return getPackageSourceSnapshots(getPackagesChangedSinceRef({
|
|
838
|
+
cwd,
|
|
839
|
+
diffRef: diffRef ?? getBaseBranch({
|
|
840
|
+
configPath,
|
|
841
|
+
cwd
|
|
842
|
+
}),
|
|
843
|
+
packages
|
|
844
|
+
}), version);
|
|
845
|
+
return selectCustomUpdateSnapshots(selectedDirectories, packages, version);
|
|
846
|
+
}
|
|
847
|
+
function selectCheckPackageSnapshots({ cwd = process.cwd(), directories, directory, packages }) {
|
|
848
|
+
const selectedDirectories = getSelectedDirectories({
|
|
849
|
+
cwd,
|
|
850
|
+
directories,
|
|
851
|
+
directory
|
|
852
|
+
});
|
|
853
|
+
if (selectedDirectories.length === 0) return getPackageSourceSnapshots(packages);
|
|
854
|
+
return selectCustomCheckSnapshots(selectedDirectories, packages);
|
|
855
|
+
}
|
|
856
|
+
function getPackageSnapshots(cwd = process.cwd()) {
|
|
857
|
+
return getPackagesSync(cwd).packages.flatMap((pkg) => {
|
|
858
|
+
const { name, version } = pkg.packageJson;
|
|
859
|
+
if (!name) return [];
|
|
860
|
+
if (!existsSync(join(pkg.dir, "src"))) return [];
|
|
861
|
+
return [{
|
|
862
|
+
name,
|
|
863
|
+
root: pkg.dir,
|
|
864
|
+
version
|
|
865
|
+
}];
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
function getCheckPackageSnapshots({ cwd = process.cwd(), directories, directory, packages = getPackageSnapshots(cwd) } = {}) {
|
|
869
|
+
return selectCheckPackageSnapshots({
|
|
870
|
+
cwd,
|
|
871
|
+
directories,
|
|
872
|
+
directory,
|
|
873
|
+
packages
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
function getUpdatePackageSnapshots({ configPath, cwd = process.cwd(), diffRef, directories, directory, packages = getPackageSnapshots(cwd), version } = {}) {
|
|
877
|
+
return selectDefaultPackageSourceSnapshots({
|
|
878
|
+
configPath,
|
|
879
|
+
cwd,
|
|
880
|
+
diffRef,
|
|
881
|
+
directories,
|
|
882
|
+
directory,
|
|
883
|
+
packages,
|
|
884
|
+
version
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
function bumpVersionsAndMaybeUpdateJsDocSinceTags({ exec = (command) => execSync(command, { stdio: "inherit" }), packageManager = "pnpm" } = {}) {
|
|
888
|
+
exec(`${packageManager} changeset version`);
|
|
889
|
+
}
|
|
890
|
+
//#endregion
|
|
891
|
+
//#region src/cli.ts
|
|
892
|
+
function buildSteps(options) {
|
|
893
|
+
const pm = options.packageManager ?? "pnpm";
|
|
894
|
+
return [
|
|
895
|
+
{
|
|
896
|
+
description: "Generate changesets from conventional commits",
|
|
897
|
+
name: "changeset-generate",
|
|
898
|
+
run: () => conventionalCommitChangeset({
|
|
899
|
+
commitSha: options.commitSha,
|
|
900
|
+
configPath: options.config,
|
|
901
|
+
includeCommitLinks: options.includeCommitLinks
|
|
902
|
+
})
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
description: "Bump versions and generate changelogs",
|
|
906
|
+
name: "bump",
|
|
907
|
+
run: () => bumpVersionsAndMaybeUpdateJsDocSinceTags({ packageManager: pm })
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
description: "Consolidate changelog formatting",
|
|
911
|
+
name: "consolidate-changelogs",
|
|
912
|
+
run: () => consolidateChangelogs()
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
description: "Generate combined release notes",
|
|
916
|
+
name: "generate-release-notes",
|
|
917
|
+
run: async () => {
|
|
918
|
+
await generateReleaseNotes();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
async function waitForConfirmation(stepName) {
|
|
924
|
+
const rl = createInterface({
|
|
925
|
+
input: stdin,
|
|
926
|
+
output: stdout
|
|
927
|
+
});
|
|
928
|
+
try {
|
|
929
|
+
return (await rl.question(`\nStep "${stepName}" completed. Continue to next step? [Y/n] `)).trim().toLowerCase() !== "n";
|
|
930
|
+
} finally {
|
|
931
|
+
rl.close();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
async function run(options) {
|
|
935
|
+
const steps = buildSteps(options);
|
|
936
|
+
for (let i = 0; i < steps.length; i++) {
|
|
937
|
+
const step = steps[i];
|
|
938
|
+
const stepLabel = `[${i + 1}/${steps.length}]`;
|
|
939
|
+
console.log(`\n${stepLabel} ${step.description}...`);
|
|
940
|
+
try {
|
|
941
|
+
await step.run();
|
|
942
|
+
} catch (e) {
|
|
943
|
+
console.error(`\nStep "${step.name}" failed. Aborting prep-release.`);
|
|
944
|
+
console.error(e);
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
947
|
+
if (options.inSteps && i < steps.length - 1) {
|
|
948
|
+
if (!await waitForConfirmation(step.name)) {
|
|
949
|
+
console.log("Aborted by user.");
|
|
950
|
+
process.exit(0);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
console.log("\nAll prep-release steps completed.");
|
|
955
|
+
}
|
|
956
|
+
program.allowUnknownOption(false);
|
|
957
|
+
program.command("prep-release").description("Run all prep-release steps sequentially").option("--in-steps", "Pause after each step and wait for confirmation before continuing", false).option("--commit-sha <sha>", "Diff each package against the target commit instead of the repository's base branch").option("--include-commit-links", "Embed commit hashes in changeset summaries for changelog links", false).option("--package-manager <command>", "Package manager command to use for changeset version", "pnpm").option("--config <path>", "Path to the changesets config file, relative to the project root").action((options) => run(options));
|
|
958
|
+
program.command("changeset-generate").description("Generate changesets from conventional commits").option("--commit-sha <sha>", "Diff each package against the target commit instead of the repository's base branch").option("--include-commit-links", "Embed commit hashes in changeset summaries for changelog links", false).option("--config <path>", "Path to the changesets config file, relative to the project root").action(({ config, ...options }) => conventionalCommitChangeset({
|
|
959
|
+
...options,
|
|
960
|
+
configPath: config
|
|
961
|
+
}));
|
|
962
|
+
program.command("consolidate-changelogs").description("Consolidate changelog formatting").action(() => consolidateChangelogs());
|
|
963
|
+
program.command("generate-release-notes").description("Generate combined release notes").action(async () => {
|
|
964
|
+
await generateReleaseNotes();
|
|
965
|
+
});
|
|
966
|
+
program.command("check-versions").description("Check which packages have newer versions than what is published on npm").option("--config <path>", "Path to the changesets config file, relative to the project root").action((options) => checkVersions({ configPath: options.config }));
|
|
967
|
+
program.command("check-jsdoc-since-tags").argument("[directories...]", "Directories to scan instead of package sources. If this is omitted, the `src` folder will be used by default").description("Check for JSDoc @since next-release tags without modifying files").action(async (directories) => {
|
|
968
|
+
const result = await checkJsDocSinceTags({ packages: getCheckPackageSnapshots({ directories }) });
|
|
969
|
+
for (const line of formatJsDocSinceCheckResult(result)) console.log(line);
|
|
970
|
+
});
|
|
971
|
+
program.command("update-jsdoc-since-tags").argument("[directories...]", "Directories to update instead of package sources").option("--version <version>", "Version to use instead of each containing package's current version").option("--diff-ref <git-ref>", "Git ref to compare package versions against (defaults to the changesets base branch)").description("Replace JSDoc @since next-release tags with package or explicit versions").action(async (directories, options) => {
|
|
972
|
+
console.log(formatJsDocSinceUpdateStartMessage());
|
|
973
|
+
const lines = formatJsDocSinceUpdateResult(await updateJsDocSinceTagsForPackages({
|
|
974
|
+
onProgress: console.log,
|
|
975
|
+
packages: getUpdatePackageSnapshots({
|
|
976
|
+
diffRef: options.diffRef,
|
|
977
|
+
directories,
|
|
978
|
+
version: options.version
|
|
979
|
+
})
|
|
980
|
+
}));
|
|
981
|
+
if (lines.length === 0) {
|
|
982
|
+
console.log("No JSDoc @since next-release tags updated.");
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
for (const line of lines) console.log(line);
|
|
986
|
+
});
|
|
987
|
+
program.command("create-github-releases").description("Create GitHub releases for published packages from changelogs").option("--token <token>", "GitHub token for authentication (falls back to TOKEN or GITHUB_TOKEN env vars)").option("--repo <owner/repo>", "GitHub repository in owner/repo format (defaults to git remote origin)").option("--config <path>", "Path to the changesets config file, relative to the project root").action((options) => {
|
|
988
|
+
const token = options.token ?? process.env.TOKEN ?? process.env.GITHUB_TOKEN;
|
|
989
|
+
if (!token) {
|
|
990
|
+
console.error("GitHub token is required. Use --token, or set TOKEN or GITHUB_TOKEN env var.");
|
|
991
|
+
process.exit(1);
|
|
992
|
+
}
|
|
993
|
+
return createGitHubReleases({
|
|
994
|
+
configPath: options.config,
|
|
995
|
+
repo: options.repo,
|
|
996
|
+
token
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
program.parse(process.argv);
|
|
1000
|
+
//#endregion
|
|
1001
|
+
export {};
|
|
1002
|
+
|
|
1003
|
+
//# sourceMappingURL=cli.js.map
|