@peiyanlu/release 0.0.3 → 0.0.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/cli.mjs CHANGED
@@ -1,21 +1,26 @@
1
1
  import path, { join, resolve } from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
3
  import { eol, execAsync, isGitRepo, parseGitHubRepo, readJsonFile, runGit, runNpm } from "@peiyanlu/cli-utils";
4
- import { dim, gray, green, red, rgb, underline, yellow } from "ansis";
4
+ import { dim, green, red, rgb, underline, yellow } from "ansis";
5
5
  import { program } from "commander";
6
+ import { createReadStream, createWriteStream, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
7
  import { cancel, confirm, group, intro, isCancel, log, outro, select, tasks, text } from "@clack/prompts";
7
- import { clean, coerce, gt, gte, inc, parse, prerelease, valid } from "semver";
8
+ import { publint } from "publint";
9
+ import { formatMessage } from "publint/utils";
10
+ import { clean, coerce, eq, gt, inc, lt, parse, prerelease, valid } from "semver";
8
11
  import { castArray, isPlainObject, isUndefined } from "@peiyanlu/ts-utils";
9
- import { createReadStream, existsSync, statSync } from "node:fs";
10
12
  import { spawnSync } from "node:child_process";
11
- import { inspect, styleText } from "node:util";
13
+ import { inspect } from "node:util";
12
14
  import { Octokit } from "@octokit/rest";
13
15
  import { contentType } from "mime-types";
14
16
  import open from "open";
15
17
  import { glob } from "tinyglobby";
16
- import { readFile, writeFile } from "node:fs/promises";
18
+ import { ConventionalChangelog } from "conventional-changelog";
19
+ import createPreset from "conventional-changelog-conventionalcommits";
20
+ import { writeFile } from "node:fs/promises";
21
+ import { finished } from "node:stream/promises";
17
22
 
18
- //#region node_modules/.pnpm/tsdown@0.18.4_typescript@5.9.3/node_modules/tsdown/esm-shims.js
23
+ //#region node_modules/.pnpm/tsdown@0.18.4_publint@0.3.17_typescript@5.9.3/node_modules/tsdown/esm-shims.js
19
24
  const getFilename = () => fileURLToPath(import.meta.url);
20
25
  const getDirname = () => path.dirname(getFilename());
21
26
  const __dirname = /* @__PURE__ */ getDirname();
@@ -26,10 +31,8 @@ const findConfigFile = (cwd = process.cwd()) => {
26
31
  for (const ext of [
27
32
  "ts",
28
33
  "mts",
29
- "cts",
30
34
  "js",
31
- "mjs",
32
- "cjs"
35
+ "mjs"
33
36
  ]) {
34
37
  const path$1 = resolve(cwd, `release.config.${ext}`);
35
38
  if (existsSync(path$1)) return path$1;
@@ -41,11 +44,16 @@ const loadConfig = async (file) => {
41
44
  const temp = mod.default ?? mod;
42
45
  return typeof temp === "function" ? temp() : temp;
43
46
  };
44
- const resolveConfig = async (cwd, log$1) => {
47
+ const resolveConfig = async (cwd) => {
45
48
  const configFile = findConfigFile(cwd);
46
- if (!configFile) return {};
47
- log$1?.(configFile);
48
- return loadConfig(configFile);
49
+ if (!configFile) return {
50
+ configPath: "",
51
+ config: {}
52
+ };
53
+ return {
54
+ configPath: configFile,
55
+ config: await loadConfig(configFile)
56
+ };
49
57
  };
50
58
  const isFunction = (v) => typeof v === "function";
51
59
  const mergeConfig = (defaults, overrides) => {
@@ -69,8 +77,8 @@ const mergeConfig = (defaults, overrides) => {
69
77
  const n = (b) => b ? "(dry run)" : "";
70
78
  const t = () => green(`${Math.floor(process.uptime())}s`);
71
79
  const MSG = {
72
- INTRO: (pkgName, dryRun) => `🚀 Starting release ${yellow(pkgName)} ${n(dryRun)}`,
73
- OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}.`,
80
+ INTRO: (dryRun) => `🚀 Starting release ${n(dryRun)}`,
81
+ OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}`,
74
82
  LOG: {
75
83
  SHOW_VERSION: (version) => `🎉 Released ${green(version)}`,
76
84
  SHOW_CHANGELOG: "🎉 Changelog collected",
@@ -80,11 +88,11 @@ const MSG = {
80
88
  CHECK: {
81
89
  GIT: {
82
90
  CHECKING: "Checking Git repository",
83
- CHECKED: (name, url) => `Git remote resolved: ${dim(name)} → ${underline(dim(url))}`
91
+ CHECKED: (name, url) => `Git remote resolved: ${url ? `${dim(name)} → ${underline(dim(url))}` : "not found"}`
84
92
  },
85
93
  NPM: {
86
94
  CHECKING: "Checking npm registry",
87
- CHECKED: (registry, msg$1) => `NPM registry: ${underline(dim(registry))}${msg$1 ? ` ${msg$1}` : ""}`
95
+ CHECKED: (registry, msg$1) => `NPM registry: ${underline(dim(registry))} ${underline(dim(msg$1))}`
88
96
  }
89
97
  },
90
98
  TASK: {
@@ -110,6 +118,7 @@ const MSG = {
110
118
  }
111
119
  },
112
120
  PROMPT: {
121
+ SELECT_PACKAGE: "Select package release:",
113
122
  SELECT_VERSION: "Select version bump:",
114
123
  INPUT_VERSION: "Enter custom version:",
115
124
  NPM_PUBLISH: (pkg$1) => `Publish to npm ${underline(dim`(${pkg$1})`)}?`,
@@ -130,6 +139,15 @@ const MSG = {
130
139
  NPM_REGISTRY: (registry) => `[npm] Unable to reach npm registry ${underline(dim(registry))}`,
131
140
  NPM_AUTH: `[npm] Not authenticated with npm. Please run ${yellow("npm login")} and try again.`,
132
141
  NPM_USER: (user, name) => `[npm] User ${dim(user)} is not a collaborator of ${dim(name)}.`
142
+ },
143
+ ABORT: {
144
+ CANCEL: "Operation cancelled",
145
+ MONOREPO_CI_NO_PACKAGE: "CI mode requires a target package in monorepo. Please specify it via \"--package <pkg>\"",
146
+ MONOREPO_NO_PACKAGES: "Monorepo detected, but no packages found. Please configure \"config.packages\""
147
+ },
148
+ INFO: {
149
+ TOOL: (name, version) => `${name} ${dim(`v${version}`)}`,
150
+ CONFIG: (path$1) => `config file: ${underline(dim(path$1 || "not found"))}`
133
151
  }
134
152
  };
135
153
 
@@ -164,28 +182,28 @@ const uploadAssets = async (ctx, config) => {
164
182
  }));
165
183
  };
166
184
  const createWebRelease = async (ctx, config) => {
167
- const { pkg: { next, toPreRelease }, github: { owner, repo, changelog, releaseName } } = ctx;
185
+ const { pkg: { toPreRelease }, github: { owner, repo, changelog, releaseName }, git: { currentTag } } = ctx;
168
186
  const { github: { prerelease: prerelease$1 } } = config;
169
187
  const github = getGithubUrl(owner, repo);
170
188
  const url = new URL(`${github}/releases/new`);
171
- url.searchParams.set("tag", next);
172
- url.searchParams.set("title", releaseName ?? `Release ${next}`);
189
+ url.searchParams.set("tag", currentTag);
190
+ url.searchParams.set("title", releaseName);
173
191
  url.searchParams.set("body", truncateBody(changelog));
174
192
  url.searchParams.set("prelease", String(toPreRelease || prerelease$1));
175
193
  const isWindows = process.platform === "win32";
176
194
  await open(url.toString(), { wait: isWindows });
177
195
  };
178
196
  const createCiRelease = async (ctx, config) => {
179
- const { pkg: { next, toPreRelease }, github: { owner, repo, token, changelog, releaseName } } = ctx;
197
+ const { pkg: { toPreRelease }, github: { owner, repo, token, changelog, releaseName }, git: { currentTag } } = ctx;
180
198
  const { github: { prerelease: prerelease$1, draft, autoGenerate } } = config;
181
199
  const octokit = new Octokit({ auth: token });
182
200
  try {
183
201
  const { data } = await octokit.repos.createRelease({
184
202
  owner,
185
203
  repo,
186
- tag_name: next,
204
+ tag_name: currentTag,
187
205
  target_commitish: void 0,
188
- name: releaseName ?? `Release ${next}`,
206
+ name: releaseName,
189
207
  body: autoGenerate ? "" : truncateBody(changelog),
190
208
  draft: draft ?? false,
191
209
  prerelease: (toPreRelease || prerelease$1) ?? false,
@@ -202,7 +220,7 @@ const createCiRelease = async (ctx, config) => {
202
220
  discussionUrl: discussion_url
203
221
  });
204
222
  } catch (e) {
205
- if (e?.status === 422) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(next));
223
+ if (e?.status === 422) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(currentTag));
206
224
  throw e;
207
225
  }
208
226
  };
@@ -259,12 +277,17 @@ const getGithubReleaseUrl = (ctx) => {
259
277
 
260
278
  //#endregion
261
279
  //#region src/git/commit.ts
262
- const getBranchName = () => {
263
- return runGit([
264
- "rev-parse",
265
- "--abbrev-ref",
266
- "HEAD"
267
- ]);
280
+ const getBranchName = async () => {
281
+ let branch = await runGit(["branch", "--show-current"]);
282
+ if (!branch) {
283
+ branch = await runGit([
284
+ "rev-parse",
285
+ "--abbrev-ref",
286
+ "HEAD"
287
+ ]);
288
+ if (branch === "HEAD") return void 0;
289
+ }
290
+ return branch;
268
291
  };
269
292
  const getRemoteForBranch = (branch) => {
270
293
  return runGit([
@@ -273,6 +296,9 @@ const getRemoteForBranch = (branch) => {
273
296
  `branch.${branch}.remote`
274
297
  ]);
275
298
  };
299
+ const getAllRemotes = async () => {
300
+ return (await runGit(["remote"]))?.split("\n").filter(Boolean) ?? [];
301
+ };
276
302
  const getStatus = () => {
277
303
  return runGit([
278
304
  "status",
@@ -293,7 +319,7 @@ const coloredChangeset = (log$1) => {
293
319
  return log$1.split("\n").filter(Boolean).map((line) => {
294
320
  const status = line.slice(0, 2);
295
321
  const file = line.slice(3);
296
- return `${colorStatus(status)} ${styleText(["dim"], file)}`;
322
+ return `${colorStatus(status)} ${dim(file)}`;
297
323
  }).join("\n");
298
324
  };
299
325
  const getRemote = async () => {
@@ -334,7 +360,7 @@ const getPreviousTag = async (current) => {
334
360
  ])}^`
335
361
  ]);
336
362
  };
337
- const resolveChangelogRange = async (isIncrement) => {
363
+ const resolveChangelogRange = async (isIncrement = true) => {
338
364
  const latestTag = await getLatestTag();
339
365
  const previousTag = await getPreviousTag(latestTag);
340
366
  if (!latestTag) return {
@@ -350,10 +376,10 @@ const resolveChangelogRange = async (isIncrement) => {
350
376
  to: "HEAD"
351
377
  };
352
378
  };
353
- const getLog = async (print, isIncrement) => {
354
- const cmd = ["log", `--pretty=format:${print ? "* %s (%h)" : "%s %h %H"}`];
355
- const { from, to } = await resolveChangelogRange(isIncrement);
379
+ const getLog = async (from = "", to = "HEAD", scope) => {
380
+ const cmd = ["log", `--pretty=format:* %s (%h)`];
356
381
  if (from) cmd.push(`${from}...${to}`);
382
+ if (scope) cmd.push(...["--", scope]);
357
383
  return runGit(cmd, { trim: false });
358
384
  };
359
385
  const isWorkingDirClean = async () => {
@@ -372,11 +398,10 @@ const getUpstreamArgs = async (remoteName) => {
372
398
  const branch = await getBranchName();
373
399
  if (!hasUpstream) return [
374
400
  "--set-upstream",
375
- remoteName || "origin",
401
+ remoteName,
376
402
  branch
377
403
  ];
378
- if (remoteName) return [remoteName];
379
- return [];
404
+ return [remoteName, branch];
380
405
  };
381
406
  const normalizeTag = (tag) => clean(tag) ?? tag;
382
407
  const gitCheck = async (ctx, config) => {
@@ -431,16 +456,29 @@ const commitAndTag = async (ctx, config) => {
431
456
  ]);
432
457
  if (!dryRun) Object.assign(ctx.git, { isTagged: true });
433
458
  }
434
- if (push) await runGit([
435
- "push",
436
- ...await getUpstreamArgs(remoteName),
437
- "--follow-tags",
438
- ...pushArgs,
439
- ...args
440
- ]);
459
+ if (push) {
460
+ const remotes = await getAllRemotes();
461
+ for (const remoteName$1 of remotes) {
462
+ const upstreamArgs = await getUpstreamArgs(remoteName$1);
463
+ await runGit([
464
+ "push",
465
+ remoteName$1,
466
+ `refs/tags/${currentTag}`,
467
+ ...args
468
+ ]);
469
+ await runGit([
470
+ "push",
471
+ ...upstreamArgs,
472
+ ...pushArgs,
473
+ ...args
474
+ ]);
475
+ }
476
+ if (!dryRun) Object.assign(ctx.git, { isPushed: true });
477
+ }
441
478
  };
442
479
  const gitRollback$1 = (ctx) => {
443
- const { git: { currentTag, isCommitted, isTagged } } = ctx;
480
+ const { git: { currentTag, isCommitted, isTagged, isPushed } } = ctx;
481
+ if (isPushed) return;
444
482
  spawnSync("git", ["restore", "."]);
445
483
  if (isTagged) spawnSync("git", [
446
484
  "tag",
@@ -457,13 +495,14 @@ const gitRollback$1 = (ctx) => {
457
495
  //#endregion
458
496
  //#region src/utils.ts
459
497
  const info = (msg$1) => {
460
- console.log(`${rgb(33, 91, 184)(`i`)} ${gray(msg$1)}`);
498
+ console.log(`${rgb(33, 91, 184)(`i`)} ${msg$1}`);
461
499
  };
462
500
  const msg = (prefix, msg$1) => {
463
- log.message(gray`${rgb(33, 91, 184)(`[${prefix}]`)} ${msg$1}`);
501
+ log.message(`${rgb(33, 91, 184)(`[${prefix}]`)} ${dim(msg$1)}`);
464
502
  };
465
503
  const question = (msg$1, type) => {
466
504
  return `${{
505
+ package: yellow,
467
506
  version: green,
468
507
  git: rgb(220, 94, 62),
469
508
  npm: rgb(186, 70, 61),
@@ -476,7 +515,7 @@ const success = (msg$1, dryRun) => {
476
515
  const runLifeCycleHook = async (hooks, key, dryRun) => {
477
516
  const handler = hooks[key];
478
517
  if (typeof handler === "function") {
479
- if (!dryRun) await handler();
518
+ if (!dryRun) await handler(log);
480
519
  log.success(success(`${dim`run`} ${inspect(handler)}`, dryRun));
481
520
  return;
482
521
  }
@@ -504,14 +543,14 @@ const diff = (from, to, separator = ".") => {
504
543
  return to.split(separator).map((v, i) => v === a[i] ? dim(v) : green(v)).join(separator);
505
544
  };
506
545
  const formatTemplate = async (ctx, config) => {
507
- const { pkg: { next } } = ctx;
508
- const { git: { commitMessage: cm, tagMessage: tm, tagName }, github: { releaseName: rm } } = config;
509
- const f = (s, v) => s.replace("${version}", v);
546
+ const { pkg: { next }, selectedPkg } = ctx;
547
+ const { git: { commitMessage: cm, tagMessage: tm }, github: { releaseName: rm }, toTag } = config;
548
+ const f = (s, v) => s.replace("${tag}", v);
510
549
  const latestTag = await getLatestTag() ?? "";
511
- const currentTag = f(tagName || (latestTag.match(/^v/) ? "v${version}" : "${version}"), next);
512
- const commitMessage = f(cm, next);
513
- const tagMessage = f(tm, next);
514
- const releaseName = f(rm, next);
550
+ const currentTag = toTag(selectedPkg, next);
551
+ const commitMessage = f(cm, currentTag);
552
+ const tagMessage = f(tm, currentTag);
553
+ const releaseName = f(rm, currentTag);
515
554
  Object.assign(ctx.git, {
516
555
  latestTag,
517
556
  currentTag,
@@ -520,6 +559,15 @@ const formatTemplate = async (ctx, config) => {
520
559
  });
521
560
  Object.assign(ctx.github, { releaseName });
522
561
  };
562
+ const getPackageInfo = (pkgName, getPkgDir = (pkg$1) => `packages/${pkg$1}`) => {
563
+ const pkgDir = resolve(getPkgDir(pkgName));
564
+ const pkgPath = resolve(pkgDir, "package.json");
565
+ return {
566
+ pkg: readJsonFile(pkgPath),
567
+ pkgDir,
568
+ pkgPath
569
+ };
570
+ };
523
571
 
524
572
  //#endregion
525
573
  //#region src/prompts.ts
@@ -528,11 +576,11 @@ const abortTask = (msg$1) => {
528
576
  process.exit(0);
529
577
  };
530
578
  const abortSinglePrompt = (value) => {
531
- if (isCancel(value)) abortTask("Operation cancelled.");
579
+ if (isCancel(value)) abortTask(MSG.ABORT.CANCEL);
532
580
  };
533
581
  const abortGroupPrompt = (ctx) => {
534
582
  ctx && gitRollback(ctx);
535
- abortTask("Operation cancelled.");
583
+ abortTask(MSG.ABORT.CANCEL);
536
584
  };
537
585
  const abortOnError = (err, ctx) => {
538
586
  ctx && gitRollback(ctx);
@@ -679,7 +727,7 @@ const runVersionPrompts = async (ctx, config) => {
679
727
  };
680
728
  const type = await selectVersion();
681
729
  const next = type === "custom" ? await inputVersion() : type === "ignore" ? current : type;
682
- ctx.isIncrement = gte(next, current);
730
+ ctx.isIncrement = !eq(next, current);
683
731
  return { next };
684
732
  };
685
733
 
@@ -698,6 +746,7 @@ const createDefaultContext = () => {
698
746
  increment: "",
699
747
  isIncrement: false,
700
748
  configFileExists: false,
749
+ selectedPkg: "",
701
750
  pkg: {
702
751
  current: "",
703
752
  next: "",
@@ -725,9 +774,9 @@ const createDefaultContext = () => {
725
774
  currentTag: "",
726
775
  isCommitted: false,
727
776
  isTagged: false,
777
+ isPushed: false,
728
778
  commitMessage: "",
729
- tagMessage: "",
730
- tagName: ""
779
+ tagMessage: ""
731
780
  },
732
781
  github: {
733
782
  username: "",
@@ -757,14 +806,22 @@ const createDefaultConfig = (isCI) => {
757
806
  "before:release": "",
758
807
  "after:release": ""
759
808
  },
809
+ isMonorepo: false,
810
+ packages: [],
811
+ getPkgDir(pkg$1) {
812
+ return ".";
813
+ },
814
+ toTag(pkg$1, version) {
815
+ return `v${version}`;
816
+ },
817
+ changelogTagPrefix: void 0,
760
818
  git: {
761
819
  commit: isCI,
762
820
  tag: isCI,
763
821
  push: isCI,
764
- commitMessage: "chore(release): ${version}",
822
+ commitMessage: "chore(release): ${tag}",
765
823
  commitArgs: [],
766
- tagMessage: "Release ${version}",
767
- tagName: "${version}",
824
+ tagMessage: "${tag}",
768
825
  tagArgs: [],
769
826
  pushArgs: [],
770
827
  addUntrackedFiles: false,
@@ -780,17 +837,13 @@ const createDefaultConfig = (isCI) => {
780
837
  },
781
838
  github: {
782
839
  release: isCI,
783
- releaseName: "Release ${version}",
840
+ releaseName: "${tag}",
784
841
  autoGenerate: false,
785
842
  prerelease: false,
786
843
  draft: false,
787
844
  tokenRef: "GITHUB_TOKEN",
788
845
  assets: [],
789
846
  skipChecks: false
790
- },
791
- changelog: {
792
- infile: "CHANGELOG.md",
793
- types: []
794
847
  }
795
848
  };
796
849
  };
@@ -803,15 +856,25 @@ const defaultTypes = [
803
856
  section: "✨ 新功能",
804
857
  description: "新增功能"
805
858
  },
859
+ {
860
+ type: "feature",
861
+ section: "✨ 新功能",
862
+ description: "新增功能"
863
+ },
806
864
  {
807
865
  type: "fix",
808
866
  section: "🐛 Bug 修复",
809
867
  description: "修复 bug"
810
868
  },
811
869
  {
812
- type: "chore",
813
- section: "🎫 其他更新",
814
- description: "非功能性更改"
870
+ type: "perf",
871
+ section: "⚡ 性能优化",
872
+ description: "提升性能"
873
+ },
874
+ {
875
+ type: "revert",
876
+ section: "⏪ 回退",
877
+ description: "回退到之前版本"
815
878
  },
816
879
  {
817
880
  type: "docs",
@@ -823,26 +886,21 @@ const defaultTypes = [
823
886
  section: "💄 样式调整",
824
887
  description: "样式或格式修改"
825
888
  },
889
+ {
890
+ type: "chore",
891
+ section: "🎫 其他更新",
892
+ description: "非功能性更改"
893
+ },
826
894
  {
827
895
  type: "refactor",
828
896
  section: "♻ 代码重构",
829
897
  description: "重构代码"
830
898
  },
831
- {
832
- type: "perf",
833
- section: "⚡ 性能优化",
834
- description: "提升性能"
835
- },
836
899
  {
837
900
  type: "test",
838
901
  section: "✅ 测试",
839
902
  description: "测试更新"
840
903
  },
841
- {
842
- type: "revert",
843
- section: "⏪ 回退",
844
- description: "回退到之前版本"
845
- },
846
904
  {
847
905
  type: "build",
848
906
  section: "👷‍ 构建系统",
@@ -882,94 +940,80 @@ const defaultTypes = [
882
940
  type: "hotfix",
883
941
  section: "🔥 热修复",
884
942
  description: "紧急修复"
885
- },
886
- {
887
- type: "break",
888
- section: "💥 破坏性变更",
889
- description: "破坏性变更"
890
943
  }
891
944
  ];
892
- const mergeTypes = (types) => {
893
- types.forEach((item) => {
894
- const index = defaultTypes.findIndex((it) => it.type === item.type);
895
- if (index >= 0) defaultTypes.splice(index, 1, { ...item });
896
- else defaultTypes.push({ ...item });
897
- });
898
- };
899
- const findType = (type) => {
900
- return defaultTypes.find((item) => item.type === type)?.section ?? type;
901
- };
902
945
 
903
946
  //#endregion
904
947
  //#region src/git/changelog.ts
905
- const getCommits = async (isIncrement) => {
906
- return (await getLog(false, isIncrement))?.split("\n").filter(Boolean);
907
- };
908
- const parseCommit = (message) => {
909
- const match = message.match(/^(\w+):\s(.+)\s(\w{7})\s(\w{40})$/);
910
- if (!match) return;
911
- return {
912
- type: match[1],
913
- description: match[2],
914
- hash: match[3],
915
- fullHash: match[4]
916
- };
917
- };
918
- const classify = (commits) => {
919
- const sections = {};
920
- for (const msg$1 of commits) {
921
- const parsed = parseCommit(msg$1);
922
- if (!parsed) continue;
923
- const { type, description, hash, fullHash } = parsed;
924
- (sections[type] ??= []).push([
925
- description,
926
- hash,
927
- fullHash
928
- ]);
948
+ const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
949
+ const pkgDir = getPkgDir();
950
+ const infile = join(pkgDir, "CHANGELOG.md");
951
+ if (!existsSync(infile)) await writeFile(infile, "");
952
+ const originalChangelog = readFileSync(infile, "utf-8");
953
+ const preset = await createPreset({ types: defaultTypes.map((t$1) => ({
954
+ ...t$1,
955
+ hidden: false
956
+ })) });
957
+ preset.writer ??= {};
958
+ preset.writer.headerPartial = `
959
+ ## {{#if isPatch~}} <small> {{~/if~}}
960
+ {{#if @root.linkCompare~}}
961
+ [{{version}}](
962
+ {{~#if @root.repository~}}
963
+ {{~#if @root.host}}
964
+ {{~@root.host}}/
965
+ {{~/if}}
966
+ {{~#if @root.owner}}
967
+ {{~@root.owner}}/
968
+ {{~/if}}
969
+ {{~@root.repository}}
970
+ {{~else}}
971
+ {{~@root.repoUrl}}
972
+ {{~/if~}}
973
+ /compare/{{previousTag}}...{{currentTag}})
974
+ {{~else}}
975
+ {{~version}}
976
+ {{~/if}}
977
+ {{~#if title}} "{{title}}"
978
+ {{~/if}}
979
+ {{~#if date}} ({{date}})
980
+ {{~/if}}
981
+ {{~#if isPatch~}} </small> {{~/if}}
982
+ `.trim() + eol();
983
+ preset.writer.mainTemplate = `
984
+ {{> header}}
985
+ {{#if noteGroups}}
986
+ {{#each noteGroups}}
987
+
988
+ ### ⚠ {{title}}
989
+
990
+ {{#each notes}}
991
+ * {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{commit.subject}} {{#if commit.hash}}([{{commit.shortHash}}](https://github.com/{{@root.owner}}/{{@root.repository}}/commit/{{commit.hash}})){{/if}}
992
+ {{/each}}
993
+ {{/each}}
994
+ {{/if}}
995
+ {{#each commitGroups}}
996
+
997
+ {{#if title}}
998
+ ### {{title}}
999
+
1000
+ {{/if}}
1001
+ {{#each commits}}
1002
+ {{> commit root=@root}}
1003
+ {{/each}}
1004
+ {{/each}}`.trim() + eol(2);
1005
+ const generator = new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
1006
+ const writeStream = createWriteStream(infile);
1007
+ let changelog = "";
1008
+ for await (const chunk of generator.write()) {
1009
+ changelog += chunk;
1010
+ writeStream.write(chunk);
929
1011
  }
930
- return sections;
931
- };
932
- const renderChangelog = async (ctx, sections) => {
933
- const { github: { owner, repo }, git: { latestTag, currentTag } } = ctx;
934
- const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
935
- const github = getGithubUrl(owner, repo);
936
- return [
937
- latestTag ? `## [${currentTag}](${github}/compare/${latestTag}...${currentTag}) (${date})` : `## ${currentTag} (${date})`,
938
- Object.entries(sections).map((entry) => {
939
- const [type, items] = entry;
940
- return [`### ${findType(type)}`, items.map((item) => {
941
- const [desc, hash, fullHash] = item;
942
- return `* **${type}** ${desc} [${hash}](${github}/commit/${fullHash})`;
943
- }).join(eol(1))].join(eol(2));
944
- }).join(eol(2)),
945
- latestTag ? `**Full Changelog**: ${github}/compare/${latestTag}...${currentTag}` : ""
946
- ].join(eol(3));
947
- };
948
- const generateChangelog = async (ctx) => {
949
- const { isIncrement } = ctx;
950
- const commits = await getCommits(isIncrement);
951
- if (!commits) return "";
952
- const changelog = await renderChangelog(ctx, classify(commits));
953
- Object.assign(ctx.github, { changelog });
1012
+ writeStream.write(originalChangelog);
1013
+ writeStream.end();
1014
+ await finished(writeStream);
954
1015
  return changelog;
955
1016
  };
956
- const updateChangelog = async (file, newContent) => {
957
- const infile = file || "CHANGELOG.md";
958
- const target = join(process.cwd(), infile);
959
- const SHORT_TITLE = "# Changelog";
960
- const exist = (existsSync(target) ? await readFile(target, "utf-8") : "").replace(/\r\n/g, "\n");
961
- await writeFile(target, [
962
- SHORT_TITLE,
963
- newContent,
964
- exist.startsWith(SHORT_TITLE) ? exist.slice(11) : exist
965
- ].map((k) => k.trim()).join(eol(4)) + eol());
966
- await runGit(["add", infile]);
967
- };
968
- const writeChangelog = async (ctx, config) => {
969
- const { changelog: { infile, types } } = config;
970
- mergeTypes(types);
971
- await updateChangelog(infile, await generateChangelog(ctx));
972
- };
973
1017
 
974
1018
  //#endregion
975
1019
  //#region src/git/prompts.ts
@@ -1063,42 +1107,65 @@ const runNpmOptPrompts = async (ctx, config) => {
1063
1107
 
1064
1108
  //#endregion
1065
1109
  //#region src/npm/publish.ts
1066
- const getRegistry = (publishConfig = {}) => {
1067
- return (publishConfig.registry ? [publishConfig.registry] : Object.keys(publishConfig).filter((key) => key.endsWith("registry")).map((key) => publishConfig[key]))[0];
1110
+ const resolvePublishRegistry = (publishConfig = {}) => {
1111
+ if (publishConfig.registry) return publishConfig.registry;
1112
+ return Object.keys(publishConfig).filter((k) => k.endsWith("registry")).map((k) => publishConfig[k])[0];
1068
1113
  };
1069
- const isRegistryUp = async (registry) => {
1070
- return void 0 !== await runNpm(["ping", ...registry ? ["--registry", registry] : []]);
1114
+ const pingRegistry = async () => {
1115
+ return void 0 !== await runNpm(["ping"]);
1071
1116
  };
1072
- const isAuthenticated = (registry) => {
1073
- return runNpm(["whoami", ...registry ? ["--registry", registry] : []]);
1117
+ const getAuthenticatedUser = () => {
1118
+ return runNpm(["whoami"]);
1074
1119
  };
1075
- const getLatestRegistryVersion = (name, tag, registry) => {
1076
- const registryArg = registry ? ["--registry", registry] : [];
1120
+ const getVersionByTag = (name, tag) => {
1077
1121
  return runNpm([
1078
1122
  "show",
1079
1123
  `${name}@${tag}`,
1080
- "version",
1081
- ...registryArg
1124
+ "version"
1125
+ ]);
1126
+ };
1127
+ const getPublishedVersion = (name) => {
1128
+ return runNpm([
1129
+ "info",
1130
+ name,
1131
+ "version"
1082
1132
  ]);
1083
1133
  };
1084
- const bump = (version, allowSameVersion, args = []) => {
1085
- allowSameVersion && args.push(`--allow-same-version`);
1134
+ const resolvePublishTag = async (pkgName, version) => {
1135
+ const { toPreRelease, preId } = parseVersion(version);
1136
+ if (!toPreRelease) {
1137
+ const active = await getPublishedVersion(pkgName);
1138
+ return active && lt(version, active) ? "previous" : "latest";
1139
+ } else return preId || "prerelease";
1140
+ };
1141
+ const bumpPackageVersion = (version, args = [], cwd = ".") => {
1086
1142
  return runNpm([
1087
1143
  "version",
1088
1144
  version,
1089
1145
  "--no-git-tag-version",
1090
1146
  "--workspaces=false",
1147
+ "--allow-same-version",
1091
1148
  ...args
1092
- ]);
1149
+ ], { cwd });
1093
1150
  };
1094
- const resolveTag = async (version) => {
1095
- const { toPreRelease, preId } = parseVersion(version);
1096
- if (!toPreRelease) return "latest";
1097
- else return preId || "next";
1151
+ const isOtpError = (err) => err instanceof Error && /one-time password|otp/i.test(err.message);
1152
+ const publishPackage = (pkg$1 = ".", tag = "latest", args = []) => {
1153
+ return runNpm([
1154
+ "publish",
1155
+ "--access",
1156
+ "public",
1157
+ "--tag",
1158
+ tag,
1159
+ "--workspaces=false",
1160
+ ...args
1161
+ ], { cwd: pkg$1 });
1162
+ };
1163
+ const getPackageUrl = (name, next) => {
1164
+ return `https://www.npmjs.com/package/${name}/v/${next}`;
1098
1165
  };
1099
1166
  const isCollaborator = async (ctx) => {
1100
1167
  const { pkg: { name, publishConfig }, npm: { username } } = ctx;
1101
- const registry = getRegistry(publishConfig);
1168
+ const registry = resolvePublishRegistry(publishConfig);
1102
1169
  const registryArg = registry ? ["--registry", registry] : [];
1103
1170
  let npmVersion = await runNpm(["--version"]);
1104
1171
  let accessCommand;
@@ -1114,55 +1181,38 @@ const isCollaborator = async (ctx) => {
1114
1181
  name,
1115
1182
  ...registryArg
1116
1183
  ]);
1117
- if (res) {
1118
- const permissions = JSON.parse(res ?? "{}")[username];
1119
- return permissions && permissions.includes("write");
1120
- }
1121
- log.error(`Unable to verify if user ${username} is a collaborator for ${name}.`);
1122
- };
1123
- const getPackageUrl = (ctx) => {
1124
- const { pkg: { name, next } } = ctx;
1125
- return `https://www.npmjs.com/package/${name}/v/${next}`;
1184
+ if (res) return JSON.parse(res ?? "{}")[username]?.includes("write");
1185
+ return false;
1126
1186
  };
1127
1187
  const npmCheck = async (ctx, config) => {
1128
- const { pkg: { name, current, publishConfig }, npm: { username } } = ctx;
1188
+ const { pkg: { name, current, publishConfig: { registry } }, npm: { username } } = ctx;
1129
1189
  const { npm: { skipChecks } } = config;
1130
- const registry = getRegistry(publishConfig);
1131
- const tag = await resolveTag(current);
1190
+ const tag = await resolvePublishTag(name, current);
1132
1191
  Object.assign(ctx.npm, { tag });
1133
1192
  if (skipChecks) return "";
1134
- if (!await isRegistryUp()) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
1193
+ if (!await pingRegistry()) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
1135
1194
  const getUser = async () => {
1136
- const username$1 = await isAuthenticated(registry);
1195
+ const username$1 = await getAuthenticatedUser();
1137
1196
  if (!username$1) return false;
1138
1197
  Object.assign(ctx.npm, { username: username$1 });
1139
1198
  return true;
1140
1199
  };
1141
1200
  if (!await getUser()) throw new Error(MSG.ERROR.NPM_AUTH);
1142
- const latestPublish = await getLatestRegistryVersion(name, tag, registry);
1143
- if (latestPublish) {
1201
+ const latest = await getVersionByTag(name, tag);
1202
+ if (latest) {
1144
1203
  if (!await isCollaborator(ctx)) throw new Error(MSG.ERROR.NPM_USER(username, name));
1145
1204
  }
1146
- return underline(dim(latestPublish ? `(latest is ${latestPublish})` : "(new publish)"));
1205
+ return latest ? `(${tag} → ${latest})` : `(${tag} → no published version)`;
1147
1206
  };
1148
- const isOtpError = (err) => /one-time password|otp/i.test(err.message);
1149
1207
  const publishNpm = async (ctx, config) => {
1150
- const { dryRun, npm: { otp, tag }, pkg: { publishConfig } } = ctx;
1151
- const { npm: { publish, publishPath, pushArgs } } = config;
1152
- const registry = getRegistry(publishConfig);
1208
+ const { dryRun, selectedPkg, npm: { otp, tag } } = ctx;
1209
+ const { npm: { publish, pushArgs }, getPkgDir } = config;
1153
1210
  if (!publish) return;
1154
1211
  const otpArgs = otp ? ["--otp", otp] : [];
1155
1212
  const dryRunArg = dryRun ? ["--dry-run"] : [];
1156
- const registryArg = registry ? ["--registry", registry] : [];
1157
- return runNpm([
1158
- "publish",
1159
- publishPath,
1160
- "--tag",
1161
- tag,
1162
- "--workspaces=false",
1213
+ return publishPackage(getPkgDir(selectedPkg), tag, [
1163
1214
  ...otpArgs,
1164
1215
  ...pushArgs,
1165
- ...registryArg,
1166
1216
  ...dryRunArg
1167
1217
  ]);
1168
1218
  };
@@ -1172,17 +1222,38 @@ const publishNpm = async (ctx, config) => {
1172
1222
  var Action = class {
1173
1223
  async handle(cmdArgs, options) {
1174
1224
  const { version: cVersion, name: cName } = readJsonFile(join(__dirname, "../package.json"));
1175
- info(`${cName} ${dim(`v${cVersion}`)} `);
1176
- const { otp, ...others } = options;
1225
+ const { otp, package: defPkg, ...others } = options;
1177
1226
  const { showChangelog, showRelease, ci, dryRun } = Object.fromEntries(Object.entries(others).map(([k, v]) => [k, Boolean(v)]));
1178
1227
  process.env["dryRun"] = String(dryRun);
1179
- const { version: pkgVersion, private: pkgPrivate, name: pkgName, publishConfig = {} } = readJsonFile("package.json");
1180
- const nextVersion = inc(pkgVersion, cmdArgs) ?? "";
1181
- const local = await resolveConfig(process.cwd(), (path$1) => {
1182
- info(`config file: ${underline(dim(path$1))}`);
1183
- });
1228
+ const { configPath, config: local } = await resolveConfig(process.cwd());
1184
1229
  const config = mergeConfig(createDefaultConfig(ci ? true : void 0), local);
1230
+ info(MSG.INFO.TOOL(cName, cVersion));
1231
+ info(MSG.INFO.CONFIG(configPath));
1232
+ console.log();
1233
+ intro(MSG.INTRO(dryRun));
1234
+ const { isMonorepo, packages, getPkgDir } = config;
1235
+ if (isMonorepo) {
1236
+ if (ci) {
1237
+ if (!defPkg) abortTask(MSG.ABORT.MONOREPO_CI_NO_PACKAGE);
1238
+ } else if (packages.length < 1) abortTask(MSG.ABORT.MONOREPO_NO_PACKAGES);
1239
+ }
1240
+ const selectPackage = async () => {
1241
+ const pkg$2 = await select({
1242
+ message: question(MSG.PROMPT.SELECT_PACKAGE, "package"),
1243
+ options: packages.map((pkg$3) => ({
1244
+ label: pkg$3,
1245
+ value: pkg$3
1246
+ }))
1247
+ });
1248
+ abortSinglePrompt(pkg$2);
1249
+ return pkg$2;
1250
+ };
1251
+ const selectedPkg = !isMonorepo ? "" : ci ? defPkg : packages.length === 1 ? packages[0] : await selectPackage();
1252
+ const { pkg: pkg$1, pkgDir } = getPackageInfo(selectedPkg, getPkgDir);
1253
+ const { version: pkgVersion, private: pkgPrivate = false, name: pkgName, publishConfig = {} } = pkg$1;
1254
+ const nextVersion = inc(pkgVersion, cmdArgs) ?? "";
1185
1255
  const ctx = mergeConfig(createDefaultContext(), {
1256
+ selectedPkg,
1186
1257
  configFileExists: Object.keys(local).length > 0,
1187
1258
  dryRun,
1188
1259
  showRelease,
@@ -1208,8 +1279,11 @@ var Action = class {
1208
1279
  },
1209
1280
  noNpm: pkgPrivate
1210
1281
  });
1211
- console.log();
1212
- intro(MSG.INTRO(pkgName, dryRun));
1282
+ const { messages } = await publint({ pkgDir });
1283
+ for (const message of messages) {
1284
+ const formated = formatMessage(message, pkg$1);
1285
+ formated && msg("PKG", formated);
1286
+ }
1213
1287
  await this.checkTask(ctx, config);
1214
1288
  await this.bumpTask(ctx, config);
1215
1289
  await this.changelogTask(ctx, config);
@@ -1239,8 +1313,8 @@ var Action = class {
1239
1313
  }]).catch(abortOnError);
1240
1314
  }
1241
1315
  async bumpTask(ctx, config) {
1242
- const { pkg: { current }, dryRun, isCI, showRelease, showChangelog } = ctx;
1243
- const { hooks } = config;
1316
+ const { pkg: { current }, dryRun, isCI, showRelease, showChangelog, selectedPkg } = ctx;
1317
+ const { hooks, getPkgDir } = config;
1244
1318
  const need = (ctx$1) => {
1245
1319
  const { pkg: { next: next$1 }, isIncrement: isIncrement$1 } = ctx$1;
1246
1320
  return !next$1 && isIncrement$1;
@@ -1255,32 +1329,39 @@ var Action = class {
1255
1329
  await tasks([{
1256
1330
  title: MSG.TASK.VERSION.START,
1257
1331
  task: async () => {
1258
- await bump(next, isIncrement);
1332
+ await bumpPackageVersion(next, [], getPkgDir(selectedPkg));
1259
1333
  await formatTemplate(ctx, config);
1260
- const to = `(${current}...${diff(current, next)})`;
1261
- return success(MSG.TASK.VERSION.END(to), dryRun);
1334
+ const to$1 = `(${current}...${diff(current, next)})`;
1335
+ return success(MSG.TASK.VERSION.END(to$1), dryRun);
1262
1336
  }
1263
1337
  }]).catch((err) => abortOnError(err, ctx));
1264
1338
  await runLifeCycleHook(hooks, "after:bump", dryRun);
1265
- const logStr = (await getLog(true, isIncrement))?.split("\n").filter((line) => /^(\*\s+)?(\w+):\s(.+)\s\((\w{7})\)$/.test(line)).join("\n");
1339
+ const { from, to } = await resolveChangelogRange(isIncrement);
1340
+ const logStr = await getLog(from, to, getPkgDir(selectedPkg));
1266
1341
  if (logStr) {
1267
- msg("GIT", `Changelog:${eol(2)}` + dim(logStr));
1342
+ msg("GIT", `Changelog:${eol(2)}` + logStr);
1268
1343
  if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1269
1344
  } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1270
- const changeset = await getStatus();
1271
- if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredChangeset(changeset));
1272
- else msg("GIT", MSG.LOG.CHANGES_EMPTY);
1273
1345
  }
1274
1346
  async changelogTask(ctx, config) {
1275
- const { isIncrement, dryRun } = ctx;
1347
+ const { isIncrement, dryRun, selectedPkg } = ctx;
1348
+ const { getPkgDir, changelogTagPrefix } = config;
1276
1349
  await tasks([{
1277
1350
  title: MSG.TASK.CHANGELOG.START,
1278
1351
  task: async () => {
1279
- await writeChangelog(ctx, config);
1352
+ const changelog = await generateChangelog({
1353
+ getPkgDir: () => getPkgDir(selectedPkg),
1354
+ tagPrefix: changelogTagPrefix?.(selectedPkg)
1355
+ });
1356
+ await runGit(["add", `${getPkgDir(selectedPkg)}/CHANGELOG.md`]);
1357
+ Object.assign(ctx.github, { changelog });
1280
1358
  return success(MSG.TASK.CHANGELOG.END, dryRun);
1281
1359
  },
1282
1360
  enabled: isIncrement
1283
1361
  }]).catch((err) => abortOnError(err, ctx));
1362
+ const changeset = await getStatus();
1363
+ if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredChangeset(changeset));
1364
+ else msg("GIT", MSG.LOG.CHANGES_EMPTY);
1284
1365
  }
1285
1366
  async gitTask(ctx, config) {
1286
1367
  const { noGit, dryRun } = ctx;
@@ -1298,7 +1379,7 @@ var Action = class {
1298
1379
  await runLifeCycleHook(hooks, "after:push", dryRun);
1299
1380
  }
1300
1381
  async npmTask(ctx, config) {
1301
- const { noNpm, dryRun, isCI } = ctx;
1382
+ const { noNpm, dryRun, isCI, pkg: { name, next } } = ctx;
1302
1383
  const { hooks } = config;
1303
1384
  if (noNpm) return;
1304
1385
  await runNpmPublishPrompts(ctx, config);
@@ -1312,7 +1393,7 @@ var Action = class {
1312
1393
  await publishNpm(ctx, config);
1313
1394
  } else throw err;
1314
1395
  });
1315
- const url = getPackageUrl(ctx);
1396
+ const url = getPackageUrl(name, next);
1316
1397
  return success(MSG.TASK.NPM.END(url), dryRun);
1317
1398
  }
1318
1399
  }]).catch((err) => abortOnError(err, ctx));
@@ -1339,9 +1420,33 @@ var Action = class {
1339
1420
  //#endregion
1340
1421
  //#region src/cli.ts
1341
1422
  const pkg = readJsonFile(join(__dirname, "..", "package.json"));
1342
- program.name(pkg.name).description(pkg.description).version(pkg.version, "-v, --version", "Output the current version.").usage("[ARGUMENT] [OPTION]...").argument("[argument]", "patch|minor|major").option("-n, --dry-run", "Report actions that would be performed without writing out results.", false).option("--ci", "CI mode", false).option("--otp", "npm publish otp", "").option("--show-changelog", "Skip npm publish", false).option("--show-release", "Skip github release", false).action(async (argName, options) => {
1343
- await new Action().handle(argName, options);
1344
- }).helpOption("-h, --help", "Output usage information.").parse(process.argv);
1423
+ program.name("release").description(pkg.description).version(pkg.version, "-v, --version", "Print the tool version and exit.").usage("[release-type] [options]").argument("[release-type]", "Version bump type: patch | minor | major").option("-n, --dry-run", "Run in dry mode: show what would be released without making any changes.", false).option("-p, --package <pkg>", "Specify package name for mono-repo release (CI only).", "").option("--otp <code>", "One-time password for npm publish (used for 2FA).", "").option("--ci", "Enable CI mode: disable prompts and fail on missing required options.", false).option("--show-changelog", "Print the generated changelog and exit.", false).option("--show-release", "Print the version that would be released and exit.", false).helpOption("-h, --help", "Display help information.").action(async (releaseType, options) => {
1424
+ await new Action().handle(releaseType, options);
1425
+ });
1426
+ program.command("init").description("Create a release configuration file").option("-f, --force", "Overwrite existing config file", false).option("-m, --monorepo", "Mono-repo project", false).action((options) => {
1427
+ const { force = false, monorepo = false } = options;
1428
+ const cwd = process.cwd();
1429
+ const { type } = readJsonFile(join(cwd, "package.json"));
1430
+ const isESM = type === "module";
1431
+ const isTS = existsSync(join(cwd, "tsconfig.json"));
1432
+ const configPath = join(cwd, `release.config.${isESM ? "" : "m"}${isTS ? "ts" : "js"}`);
1433
+ if (existsSync(configPath) && !force) {
1434
+ console.error(`${red`Error:`} ${yellow`release.config.ts`} already exists.\nUse ${green`--force`} to overwrite.`);
1435
+ process.exit(1);
1436
+ }
1437
+ const monoConfigToString = () => `{
1438
+ isMonorepo: true,
1439
+ packages: [ 'demo-a', 'demo-b' ],
1440
+ getPkgDir: (pkg) => \`packages/\${ pkg }\`,
1441
+ toTag: (pkg, version) => \`\${ pkg }@\${ version }\`,
1442
+ changelogTagPrefix: (pkg) => \`\${ pkg }@\`,
1443
+ }`;
1444
+ writeFileSync(configPath, `import { defineConfig } from '@peiyanlu/release'
1445
+
1446
+ export default defineConfig(${monorepo ? monoConfigToString() : "{}"})`, "utf-8");
1447
+ console.log(`${green`✔ release.config.ts`} created successfully`);
1448
+ });
1449
+ program.parse(process.argv);
1345
1450
  program.on("command:*", () => {
1346
1451
  console.error(`\n${red`Error`} Invalid command: ${red`%s`}`, program.args.join(" "));
1347
1452
  console.log(`See ${red`--help`} for a list of available commands.\n`);