@varlet/release 1.0.5-alpha.1773908865255 → 1.1.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.
@@ -0,0 +1,432 @@
1
+ import { createWriteStream, existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { styleText } from "node:util";
4
+ import { cancel, confirm, isCancel, select, spinner } from "@clack/prompts";
5
+ import { logger } from "rslog";
6
+ import semver from "semver";
7
+ import { x } from "tinyexec";
8
+ import conventionalChangelog from "conventional-changelog";
9
+ //#region src/commitLint.ts
10
+ const COMMIT_HEADER_RE = /^(revert|fix|feat|docs|perf|test|types|style|build|chore|release|refactor|merge|wip)(?:\(([^)]+)\))?(!)?:\s(.+)$/;
11
+ const COMMIT_MESSAGE_RE = new RegExp(COMMIT_HEADER_RE.source.replace(/\$$/, "") + "(?:\\n[\\s\\S]+)?$");
12
+ const ERROR_MESSAGE = "Commit message invalid.";
13
+ const WARNING_MESSAGE = `\
14
+ The rules for commit messages are as follows
15
+
16
+ Example:
17
+
18
+ feat: add a new feature
19
+ feat(ui/button): add a new feature in the ui/button scope
20
+
21
+ fix: fix a bug
22
+ fix(ui/button): fix a bug in the ui/button scope
23
+
24
+ docs: fix an error in the documentation
25
+ docs(ui/button): fix a documentation error in the ui/button scope
26
+
27
+ Allowed types:
28
+ - fix
29
+ - feat
30
+ - docs
31
+ - perf
32
+ - test
33
+ - types
34
+ - style
35
+ - build
36
+ - chore
37
+ - release
38
+ - refactor
39
+ - revert
40
+ - merge
41
+ - wip
42
+
43
+ Commit message reference: https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y
44
+ 参考阮一峰Commit message编写指南: https://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html`;
45
+ function isVersionCommitMessage(message) {
46
+ return Boolean(message.startsWith("v") && semver.valid(message.slice(1)));
47
+ }
48
+ function getCommitMessage(commitMessagePath) {
49
+ return readFileSync(commitMessagePath, "utf-8").trim();
50
+ }
51
+ function commitLint(options) {
52
+ const { commitMessagePath, commitMessageRe = COMMIT_MESSAGE_RE, errorMessage = ERROR_MESSAGE, warningMessage = WARNING_MESSAGE } = options;
53
+ if (!commitMessagePath) {
54
+ logger.error("commitMessagePath is required");
55
+ process.exit(1);
56
+ }
57
+ const commitMessage = getCommitMessage(commitMessagePath);
58
+ const isValidCommitMessage = new RegExp(commitMessageRe).test(commitMessage);
59
+ if (!isVersionCommitMessage(commitMessage) && !isValidCommitMessage) {
60
+ logger.error(errorMessage);
61
+ logger.warn(warningMessage);
62
+ process.exit(1);
63
+ }
64
+ }
65
+ //#endregion
66
+ //#region src/changelog.ts
67
+ const COMMIT_TYPE_MAP = {
68
+ feat: "Features",
69
+ fix: "Bug Fixes",
70
+ perf: "Performance Improvements",
71
+ revert: "Reverts",
72
+ refactor: "Code Refactoring",
73
+ docs: "Documentation",
74
+ style: "Styles",
75
+ test: "Tests",
76
+ build: "Build System",
77
+ ci: "Continuous Integration"
78
+ };
79
+ const ALWAYS_SHOW_TYPES = new Set([
80
+ "feat",
81
+ "fix",
82
+ "perf",
83
+ "revert",
84
+ "refactor"
85
+ ]);
86
+ const BREAKING_CHANGE_RE = /BREAKING CHANGES?:\s*([\s\S]+)/;
87
+ const MAIN_TEMPLATE = `{{> header}}
88
+
89
+ {{> footer}}
90
+ {{#each commitGroups}}
91
+
92
+ {{#if title}}
93
+ ### {{title}}
94
+
95
+ {{/if}}
96
+ {{#each commits}}
97
+ {{> commit root=@root}}
98
+ {{/each}}
99
+
100
+ {{/each}}
101
+ `;
102
+ function linkify(text, context, issues) {
103
+ let result = text;
104
+ const repoUrl = context.repository ? `${context.host}/${context.owner}/${context.repository}` : context.repoUrl;
105
+ if (repoUrl) {
106
+ const issueBaseUrl = `${repoUrl}/issues/`;
107
+ result = result.replace(/#([0-9]+)/g, (_, issue) => {
108
+ issues.push(issue);
109
+ return `[#${issue}](${issueBaseUrl}${issue})`;
110
+ });
111
+ }
112
+ if (context.host) result = result.replace(/\B@([a-z0-9](?:-?[a-z0-9/]){0,38})/g, (_, username) => username.includes("/") ? `@${username}` : `[@${username}](${context.host}/${username})`);
113
+ return result;
114
+ }
115
+ function extractBreakingText(commit) {
116
+ const body = typeof commit.body === "string" ? commit.body : "";
117
+ const footer = typeof commit.footer === "string" ? commit.footer : "";
118
+ const match = BREAKING_CHANGE_RE.exec(`${footer}\n${body}`);
119
+ if (match?.[1]) return match[1].trim();
120
+ if (typeof commit.subject === "string" && commit.subject) return commit.subject;
121
+ if (typeof commit.header === "string" && commit.header) return commit.header;
122
+ return "";
123
+ }
124
+ function tryParseHeader(commit) {
125
+ if (typeof commit.header !== "string") return null;
126
+ const match = COMMIT_HEADER_RE.exec(commit.header.trim());
127
+ if (!match) return null;
128
+ return {
129
+ type: match[1],
130
+ scope: match[2] || void 0,
131
+ subject: match[4] || void 0,
132
+ isBreaking: Boolean(match[3])
133
+ };
134
+ }
135
+ function processBreakingChanges(commit, context, issues) {
136
+ let discard = true;
137
+ commit.notes.forEach((note) => {
138
+ note.title = "BREAKING CHANGES";
139
+ discard = false;
140
+ });
141
+ const hadBreakingNotes = commit.notes.length > 0;
142
+ const addBreakingNote = () => {
143
+ if (!hadBreakingNotes) {
144
+ const text = linkify(extractBreakingText(commit), context, issues);
145
+ commit.notes.push({
146
+ title: "BREAKING CHANGES",
147
+ text
148
+ });
149
+ }
150
+ discard = false;
151
+ };
152
+ const parsed = tryParseHeader(commit);
153
+ if (parsed) {
154
+ if (!commit.type) commit.type = parsed.type;
155
+ if (!commit.scope && parsed.scope) commit.scope = parsed.scope;
156
+ if (!commit.subject && parsed.subject) commit.subject = parsed.subject;
157
+ if (parsed.isBreaking) addBreakingNote();
158
+ }
159
+ if (typeof commit.type === "string" && commit.type.endsWith("!")) {
160
+ commit.type = commit.type.slice(0, -1);
161
+ addBreakingNote();
162
+ }
163
+ return discard;
164
+ }
165
+ function mapCommitType(commit, discard) {
166
+ if (commit.revert) {
167
+ commit.type = "Reverts";
168
+ return true;
169
+ }
170
+ const mapped = COMMIT_TYPE_MAP[commit.type];
171
+ if (mapped) {
172
+ if (ALWAYS_SHOW_TYPES.has(commit.type) || !discard) {
173
+ commit.type = mapped;
174
+ return true;
175
+ }
176
+ }
177
+ return !discard;
178
+ }
179
+ function createDefaultWriterOpts() {
180
+ return {
181
+ mainTemplate: MAIN_TEMPLATE,
182
+ transform(commit, context) {
183
+ const issues = [];
184
+ if (!mapCommitType(commit, processBreakingChanges(commit, context, issues))) return false;
185
+ if (commit.scope === "*") commit.scope = "";
186
+ if (typeof commit.hash === "string") commit.shortHash = commit.hash.substring(0, 7);
187
+ if (typeof commit.subject === "string") commit.subject = linkify(commit.subject, context, issues);
188
+ commit.references = commit.references.filter((ref) => !issues.includes(ref.issue));
189
+ return commit;
190
+ }
191
+ };
192
+ }
193
+ function changelog({ releaseCount = 0, file = "CHANGELOG.md", preset = "angular", writerOpts = createDefaultWriterOpts() } = {}) {
194
+ const s = spinner();
195
+ s.start("Generating changelog");
196
+ return new Promise((resolve$1) => {
197
+ conventionalChangelog({
198
+ preset,
199
+ releaseCount
200
+ }, void 0, void 0, void 0, writerOpts).pipe(createWriteStream(resolve(process.cwd(), file))).on("close", () => {
201
+ s.stop("Changelog generated successfully!");
202
+ resolve$1();
203
+ });
204
+ });
205
+ }
206
+ //#endregion
207
+ //#region src/utils.ts
208
+ function readJSONSync(path) {
209
+ const content = readFileSync(path, "utf-8").replace(/^\uFEFF/, "");
210
+ try {
211
+ return JSON.parse(content);
212
+ } catch (err) {
213
+ err.message = `${path}: ${err.message}`;
214
+ throw err;
215
+ }
216
+ }
217
+ //#endregion
218
+ //#region src/release.ts
219
+ const cwd = () => process.cwd();
220
+ const releaseTypes = [
221
+ "patch",
222
+ "minor",
223
+ "major",
224
+ "prepatch",
225
+ "preminor",
226
+ "premajor"
227
+ ];
228
+ const BACK_HINT = "Back to previous step";
229
+ async function isWorktreeEmpty() {
230
+ return !(await x("git", ["status", "--porcelain"])).stdout;
231
+ }
232
+ async function isSameVersion(version) {
233
+ const s = spinner();
234
+ s.start("Check remote version...");
235
+ const packageJsones = getPackageJsons();
236
+ const packageJson = packageJsones.find((packageJson) => !packageJson.config.private) || packageJsones[0];
237
+ if (packageJson) {
238
+ const { config } = packageJson;
239
+ try {
240
+ await x("npm", [
241
+ "view",
242
+ `${config.name}@${version ?? config.version}`,
243
+ "version"
244
+ ], { throwOnError: true });
245
+ s.cancel();
246
+ logger.warn(`The npm package has a same remote version ${version ?? config.version}.`);
247
+ return true;
248
+ } catch {
249
+ s.stop();
250
+ return false;
251
+ }
252
+ }
253
+ }
254
+ async function publish({ preRelease, checkRemoteVersion, npmTag }) {
255
+ const s = spinner();
256
+ s.start("Publishing all packages");
257
+ const args = [
258
+ "-r",
259
+ "publish",
260
+ "--no-git-checks",
261
+ "--access",
262
+ "public"
263
+ ];
264
+ if (checkRemoteVersion && await isSameVersion()) {
265
+ logger.error("publishing automatically skipped.");
266
+ return;
267
+ }
268
+ if (preRelease) args.push("--tag", "alpha");
269
+ else if (npmTag) args.push("--tag", npmTag);
270
+ try {
271
+ const ret = await x("pnpm", args, { throwOnError: true });
272
+ s.stop("Publish all packages successfully");
273
+ ret.stdout && logger.log(ret.stdout);
274
+ } catch (error) {
275
+ s.cancel("Publish all packages failed");
276
+ throw error?.output?.stderr ?? error;
277
+ }
278
+ }
279
+ async function pushGit(version, remote = "origin", skipGitTag = false) {
280
+ const s = spinner();
281
+ s.start("Pushing to remote git repository");
282
+ await x("git", ["add", "."], { throwOnError: true });
283
+ await x("git", [
284
+ "commit",
285
+ "-m",
286
+ `v${version}`
287
+ ], { throwOnError: true });
288
+ if (!skipGitTag) {
289
+ await x("git", ["tag", `v${version}`], { throwOnError: true });
290
+ await x("git", [
291
+ "push",
292
+ remote,
293
+ `v${version}`
294
+ ], { throwOnError: true });
295
+ }
296
+ const ret = await x("git", ["push"], { throwOnError: true });
297
+ s.stop("Push remote repository successfully");
298
+ ret.stdout && logger.log(ret.stdout);
299
+ }
300
+ function getPackageJsons() {
301
+ const packageJsons = [resolve(cwd(), "package.json")];
302
+ const packagesDir = resolve(cwd(), "packages");
303
+ if (existsSync(packagesDir)) for (const name of readdirSync(packagesDir)) {
304
+ const pkgPath = resolve(packagesDir, name, "package.json");
305
+ if (existsSync(pkgPath)) packageJsons.push(pkgPath);
306
+ }
307
+ return packageJsons.map((path) => {
308
+ return {
309
+ filePath: path,
310
+ config: readJSONSync(path)
311
+ };
312
+ });
313
+ }
314
+ function updateVersion(version) {
315
+ getPackageJsons().forEach(({ config, filePath }) => {
316
+ config.version = version;
317
+ writeFileSync(filePath, JSON.stringify(config, null, 2));
318
+ });
319
+ }
320
+ async function confirmRegistry() {
321
+ const registry = (await x("npm", [
322
+ "config",
323
+ "get",
324
+ "registry"
325
+ ])).stdout;
326
+ const ret = await confirm({ message: `Current registry is: ${registry}` });
327
+ if (isCancel(ret)) {
328
+ cancel("Operation cancelled.");
329
+ process.exit(0);
330
+ }
331
+ return ret;
332
+ }
333
+ async function confirmVersion(currentVersion, expectVersion) {
334
+ const ret = await select({
335
+ message: "Version confirm",
336
+ options: [`All packages version ${currentVersion} -> ${expectVersion}`, BACK_HINT].map((value) => ({
337
+ label: value,
338
+ value
339
+ }))
340
+ });
341
+ if (isCancel(ret)) {
342
+ cancel("Operation cancelled.");
343
+ process.exit(0);
344
+ }
345
+ return ret;
346
+ }
347
+ async function confirmRefs(remote = "origin") {
348
+ const { stdout } = await x("git", ["remote", "-v"]);
349
+ const reg = new RegExp(`${remote}\t(.*) \\(push`);
350
+ const repo = stdout.match(reg)?.[1];
351
+ const { stdout: branch } = await x("git", ["branch", "--show-current"]);
352
+ const ret = await confirm({ message: `Current refs ${repo}:refs/for/${styleText("blue", branch)}` });
353
+ if (isCancel(ret)) {
354
+ cancel("Operation cancelled.");
355
+ process.exit(0);
356
+ }
357
+ return ret;
358
+ }
359
+ async function getReleaseType() {
360
+ const releaseType = await select({
361
+ message: "Please select release type",
362
+ options: releaseTypes.map((type) => ({
363
+ label: type,
364
+ value: type
365
+ }))
366
+ });
367
+ if (isCancel(releaseType)) {
368
+ cancel("Operation cancelled.");
369
+ process.exit(0);
370
+ }
371
+ return releaseType;
372
+ }
373
+ async function getReleaseVersion(currentVersion) {
374
+ let isPreRelease = false;
375
+ let expectVersion = "";
376
+ let confirmVersionRet = "";
377
+ do {
378
+ const type = await getReleaseType();
379
+ isPreRelease = type.startsWith("pre");
380
+ expectVersion = semver.inc(currentVersion, type, `alpha.${Date.now()}`);
381
+ expectVersion = isPreRelease ? expectVersion.slice(0, -2) : expectVersion;
382
+ confirmVersionRet = await confirmVersion(currentVersion, expectVersion);
383
+ } while (confirmVersionRet === BACK_HINT);
384
+ return {
385
+ isPreRelease,
386
+ expectVersion
387
+ };
388
+ }
389
+ async function release(options) {
390
+ try {
391
+ const currentVersion = readJSONSync(resolve(cwd(), "package.json")).version;
392
+ if (!currentVersion) {
393
+ logger.error("Your package is missing the version field");
394
+ return;
395
+ }
396
+ if (!await isWorktreeEmpty()) {
397
+ logger.error("Git worktree is not empty, please commit changed");
398
+ return;
399
+ }
400
+ if (!await confirmRefs(options.remote)) return;
401
+ if (!options.skipNpmPublish && !await confirmRegistry()) return;
402
+ const { isPreRelease, expectVersion } = await getReleaseVersion(currentVersion);
403
+ if (options.checkRemoteVersion && await isSameVersion(expectVersion)) {
404
+ logger.error("Please check remote version.");
405
+ return;
406
+ }
407
+ updateVersion(expectVersion);
408
+ if (options.task) await options.task(expectVersion, currentVersion);
409
+ if (!options.skipNpmPublish) await publish({
410
+ preRelease: isPreRelease,
411
+ npmTag: options.npmTag
412
+ });
413
+ if (!isPreRelease) {
414
+ if (!options.skipChangelog) await changelog();
415
+ await pushGit(expectVersion, options.remote, options.skipGitTag);
416
+ }
417
+ logger.success(`Release version ${expectVersion} successfully!`);
418
+ if (isPreRelease) {
419
+ try {
420
+ await x("git", ["restore", "**/package.json"], { throwOnError: true });
421
+ } catch {}
422
+ try {
423
+ await x("git", ["restore", "package.json"], { throwOnError: true });
424
+ } catch {}
425
+ }
426
+ } catch (error) {
427
+ logger.error(error);
428
+ process.exit(1);
429
+ }
430
+ }
431
+ //#endregion
432
+ export { updateVersion as a, COMMIT_MESSAGE_RE as c, isVersionCommitMessage as d, release as i, commitLint as l, isSameVersion as n, changelog as o, publish as r, COMMIT_HEADER_RE as s, getPackageJsons as t, getCommitMessage as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@varlet/release",
3
- "version": "1.0.5-alpha.1773908865255",
3
+ "version": "1.1.0",
4
4
  "description": "publish all packages, generate changelogs and check commit messages",
5
5
  "keywords": [
6
6
  "changelog",
@@ -42,33 +42,30 @@
42
42
  "package.json": "prettier --write"
43
43
  },
44
44
  "dependencies": {
45
- "@clack/prompts": "^1.0.0",
45
+ "@clack/prompts": "^1.1.0",
46
46
  "commander": "^11.1.0",
47
47
  "conventional-changelog": "^5.1.0",
48
- "fs-extra": "^11.3.3",
49
- "rslog": "^2.0.0",
48
+ "rslog": "^2.0.1",
50
49
  "semver": "^7.5.4",
51
- "tinyexec": "^1.0.2"
50
+ "tinyexec": "^1.0.4"
52
51
  },
53
52
  "devDependencies": {
54
53
  "@configurajs/eslint": "^0.1.2",
55
54
  "@configurajs/prettier": "^0.1.4",
56
55
  "@ianvs/prettier-plugin-sort-imports": "^4.7.1",
57
56
  "@types/conventional-changelog": "^3.1.5",
58
- "@types/fs-extra": "^11.0.4",
59
57
  "@types/node": "^22.14.0",
60
58
  "@types/semver": "^7.5.5",
61
- "@vitest/coverage-v8": "^4.0.18",
59
+ "@vitest/coverage-v8": "^4.1.0",
62
60
  "eslint": "^9.39.2",
63
61
  "lint-staged": "^16.2.7",
64
62
  "prettier": "^3.8.1",
65
63
  "prettier-plugin-packagejson": "^3.0.0",
66
64
  "prettier-plugin-tailwindcss": "^0.7.2",
67
- "rimraf": "^6.1.2",
68
65
  "simple-git-hooks": "^2.13.1",
69
- "tsdown": "^0.20.3",
66
+ "tsdown": "^0.21.4",
70
67
  "typescript": "^5.9.3",
71
- "vitest": "^4.0.18"
68
+ "vitest": "^4.1.0"
72
69
  },
73
70
  "engines": {
74
71
  "node": "^20.19.0 || >=22.12.0",
@@ -76,7 +73,6 @@
76
73
  },
77
74
  "scripts": {
78
75
  "build": "tsdown",
79
- "clean": "rimraf node_modules dist",
80
76
  "commit-lint": "node dist/cli.js commit-lint",
81
77
  "dev": "tsdown --watch",
82
78
  "format": "eslint . --fix && prettier --write .",