@peiyanlu/release 0.0.14 → 0.0.16

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/README.md CHANGED
@@ -122,7 +122,9 @@ export default defineConfig({
122
122
  packages: [],
123
123
  getPkgDir: (pkg) => `.`,
124
124
  toTag: (pkg: string, version: string) => `v${ version }`,
125
- tagPrefix: undefined,
125
+ changelog: {
126
+ tagPrefix: undefined,
127
+ },
126
128
 
127
129
  git: {
128
130
  commit: true,
@@ -157,7 +159,9 @@ export default defineConfig({
157
159
  packages: [ 'demo-a', 'demo-b' ],
158
160
  getPkgDir: (pkg) => `packages/${ pkg }`,
159
161
  toTag: (pkg: string, version: string) => `${ pkg }@${ version }`,
160
- tagPrefix: (pkg: string) => `${ pkg }@`,
162
+ changelog: {
163
+ tagPrefix: (pkg: string) => `${ pkg }@`,
164
+ },
161
165
 
162
166
  git: {
163
167
  commit: true,
package/dist/cli.mjs CHANGED
@@ -1,15 +1,14 @@
1
1
  import path, { join, resolve } from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
- import { bumpPackageVersion, coloredChangeset, eol, execAsync, getAllRemotes, getAuthenticatedUser, getCurrentBranch, getGithubReleaseUrl, getGithubUrl, getLatestTag, getLog, getPackageInfo, getPackageUrl, getPublishedVersion, getRemote, getStatus, gitAddAll, gitAddTracked, gitCommit, gitTagAnnotated, hasWriteAccess, isGitRepo, isOtpError, isPrerelease, isValid, isWorkingDirClean, parseGitHubRepo, parseVersion, pingRegistry, publishPackage, pushBranch, pushTag, readJsonFile, resolveChangelogRange, resolvePublishTag } from "@peiyanlu/cli-utils";
3
+ import { bumpPackageVersion, canPublish, coloredStatus, eol, execAsync, getAuthenticatedUser, getGithubReleaseUrl, getGithubUrl, getLatestTag, getLogSince, getPackageInfo, getPackageUrl, getPublishedVersion, getRemoteList, getRemoteNames, getShortStatus, gitAddAll, gitAddTracked, gitBranchCurrent, gitCommitMessage, gitPushBranch, gitPushTag, gitResetHardSync, gitTagAnnotated, gitTagDeleteSync, hasWriteAccess, isGitRepo, isOtpError, isPrerelease, isValidVersion, isWorkingTreeClean, parseGitHubRepo, parseVersion, pingRegistry, publishPackage, readJsonFile, resolveChangelogRange, resolvePublishTag } from "@peiyanlu/cli-utils";
4
4
  import { dim, green, red, rgb, underline, yellow } from "ansis";
5
5
  import { program } from "commander";
6
6
  import { createReadStream, createWriteStream, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
7
7
  import { cancel, confirm, group, intro, isCancel, log, outro, select, tasks, text } from "@clack/prompts";
8
+ import { castArray, isFunction, isPlainObject, isUndefined, isZero } from "@peiyanlu/ts-utils";
8
9
  import { publint } from "publint";
9
10
  import { formatMessage } from "publint/utils";
10
11
  import { inc, neq } from "semver";
11
- import { castArray, isFunction, isPlainObject, isUndefined } from "@peiyanlu/ts-utils";
12
- import { spawnSync } from "node:child_process";
13
12
  import { inspect } from "node:util";
14
13
  import { ConventionalChangelog } from "conventional-changelog";
15
14
  import createPreset from "conventional-changelog-conventionalcommits";
@@ -19,7 +18,7 @@ import { Octokit } from "@octokit/rest";
19
18
  import { contentType } from "mime-types";
20
19
  import open from "open";
21
20
  import { glob } from "tinyglobby";
22
- //#region node_modules/.pnpm/tsdown@0.22.1_publint@0.3.21_typescript@6.0.3/node_modules/tsdown/esm-shims.js
21
+ //#region node_modules/.pnpm/tsdown@0.22.3_publint@0.3.21_typescript@6.0.3/node_modules/tsdown/esm-shims.js
23
22
  const getFilename = () => fileURLToPath(import.meta.url);
24
23
  const getDirname = () => path.dirname(getFilename());
25
24
  const __dirname = /* @__PURE__ */ getDirname();
@@ -45,22 +44,21 @@ const loadConfig = async (file) => {
45
44
  const resolveConfig = async (cwd) => {
46
45
  const configFile = findConfigFile(cwd);
47
46
  if (!configFile) return {
48
- configPath: "",
47
+ configFile: "",
49
48
  config: {}
50
49
  };
51
50
  return {
52
- configPath: configFile,
51
+ configFile,
53
52
  config: await loadConfig(configFile)
54
53
  };
55
54
  };
56
55
  const mergeConfig = (defaults, overrides) => {
57
- if (!overrides) return structuredClone(defaults);
58
- if (Object.keys(defaults).length === 0) return structuredClone(overrides);
59
56
  const result = {};
60
- for (const key in defaults) {
57
+ for (const key of Object.keys(defaults)) {
61
58
  const defaultValue = defaults[key];
62
59
  const overrideValue = overrides[key];
63
- if (overrideValue === void 0) if (isFunction(defaultValue)) result[key] = defaultValue;
60
+ if (isUndefined(overrideValue)) if (isFunction(defaultValue)) result[key] = defaultValue;
61
+ else if (isPlainObject(defaultValue)) result[key] = mergeConfig(defaultValue, {});
64
62
  else result[key] = structuredClone(defaultValue);
65
63
  else if (isPlainObject(defaultValue) && isPlainObject(overrideValue)) result[key] = mergeConfig(defaultValue, overrideValue);
66
64
  else if (isFunction(overrideValue)) result[key] = overrideValue;
@@ -134,13 +132,14 @@ const MSG = {
134
132
  GIT_WORKDIR: `[git] Working dir must be clean`,
135
133
  GIT_REGISTRY: (name) => `[git] Repository not found for ${dim(name)}`,
136
134
  GIT_REMOTE: (name) => `[git] Remote "${dim(name)}" not found`,
137
- GITHUB_TAG_EXIT: (tag) => `[github] Release or tag "${tag}" already exists`,
135
+ GITHUB_TAG_EXIT: (tag) => `[github] Release for tag "${tag}" already exists and is not a draft`,
138
136
  GITHUB_TOKEN: (tokenRef) => `[github] Missing GitHub token. Please set ${yellow(tokenRef ?? "GITHUB_TOKEN")}`,
139
137
  GITHUB_AUTH: "[github] Invalid GitHub token or insufficient permissions",
140
138
  GITHUB_USER: "[github] User does not have permission to create releases",
141
139
  NPM_REGISTRY: (registry) => `[npm] Unable to reach npm registry ${underline(dim(registry))}`,
142
- NPM_AUTH: `[npm] Not authenticated with npm. Please run ${yellow("npm login")} and try again.`,
143
- NPM_USER: (user, name) => `[npm] User ${dim(user)} is not a collaborator of ${dim(name)}.`
140
+ NPM_AUTH: `[npm] Not authenticated with npm. Please run ${yellow("npm login")} and try again`,
141
+ NPM_PERMISSION: `[npm] Missing publish permission. Please use a token with publish access and try again`,
142
+ NPM_USER: (user, name) => `[npm] User ${dim(user)} is not a collaborator of ${dim(name)}`
144
143
  },
145
144
  ABORT: {
146
145
  CANCEL: "Operation cancelled",
@@ -168,12 +167,12 @@ const gitCheck = async (ctx, config) => {
168
167
  ctx.noGitHub = true;
169
168
  return;
170
169
  }
171
- if (requireWorkDirClean && !await isWorkingDirClean()) throw new Error(MSG.ERROR.GIT_WORKDIR);
170
+ if (requireWorkDirClean && !await isWorkingTreeClean()) throw new Error(MSG.ERROR.GIT_WORKDIR);
172
171
  if (ignoreGithub) {
173
172
  ctx.noGitHub = true;
174
173
  return;
175
174
  }
176
- const { remoteName, remoteUrl } = await getRemote();
175
+ const { name: remoteName, url: remoteUrl } = (await getRemoteList())[0];
177
176
  if (!remoteUrl) if (requireRemote) throw new Error(MSG.ERROR.GIT_REMOTE(name));
178
177
  else {
179
178
  ctx.noGitHub = true;
@@ -194,18 +193,18 @@ const commitAndTag = async (ctx, config) => {
194
193
  const { git: { commit, tag, push, addUntrackedFiles, commitArgs, tagArgs, pushArgs } } = config;
195
194
  if (!commit) return;
196
195
  const args = dryRun ? ["--dry-run"] : [];
197
- addUntrackedFiles ? await gitAddAll() : await gitAddTracked(args);
198
- await gitCommit(commitMessage, [...commitArgs, ...args]);
196
+ addUntrackedFiles ? await gitAddAll(args) : await gitAddTracked(args);
197
+ await gitCommitMessage(commitMessage, [...commitArgs, ...args]);
199
198
  if (!dryRun) Object.assign(ctx.git, { isCommitted: true });
200
199
  if (tag) {
201
200
  await gitTagAnnotated(currentTag, tagMessage, [...tagArgs, ...args]);
202
201
  if (!dryRun) Object.assign(ctx.git, { isTagged: true });
203
202
  }
204
203
  if (push) {
205
- const remotes = await getAllRemotes();
204
+ const remotes = await getRemoteNames();
206
205
  for (const remoteName of remotes) {
207
- await pushTag(remoteName, currentTag, args);
208
- await pushBranch(remoteName, await getCurrentBranch(), [...pushArgs, ...args]);
206
+ await gitPushTag(remoteName, currentTag, args);
207
+ await gitPushBranch(remoteName, await gitBranchCurrent(), [...pushArgs, ...args]);
209
208
  }
210
209
  if (!dryRun) Object.assign(ctx.git, { isPushed: true });
211
210
  }
@@ -213,40 +212,8 @@ const commitAndTag = async (ctx, config) => {
213
212
  const gitRollback = (ctx) => {
214
213
  const { git: { currentTag, isCommitted, isTagged, isPushed } } = ctx;
215
214
  if (isPushed) return;
216
- spawnSync("git", ["restore", "."]);
217
- if (isTagged) spawnSync("git", [
218
- "tag",
219
- "--delete",
220
- currentTag
221
- ]);
222
- spawnSync("git", [
223
- "reset",
224
- "--hard",
225
- isCommitted ? "HEAD~1" : "HEAD"
226
- ]);
227
- };
228
- //#endregion
229
- //#region src/prompts.ts
230
- const abortTask = (msg) => {
231
- cancel(`${red("✕")} ${msg}`);
232
- process.exit(0);
233
- };
234
- const abortSinglePrompt = (value) => {
235
- if (isCancel(value)) abortTask(MSG.ABORT.CANCEL);
236
- };
237
- const abortGroupPrompt = (ctx) => {
238
- ctx && gitRollback(ctx);
239
- abortTask(MSG.ABORT.CANCEL);
240
- };
241
- const abortOnError = (err, ctx, exit = true) => {
242
- ctx && gitRollback(ctx);
243
- const msg = err.message.replace(/^\[(github|npm|git)]/, (a) => red(a));
244
- log.message(`\n${msg}\n`);
245
- exit && process.exit(1);
246
- };
247
- const taskEnd = (msg) => {
248
- outro(msg);
249
- process.exit(0);
215
+ if (isTagged) gitTagDeleteSync(currentTag);
216
+ gitResetHardSync(isCommitted ? 1 : 0);
250
217
  };
251
218
  //#endregion
252
219
  //#region src/utils.ts
@@ -301,6 +268,27 @@ const formatTemplate = async (ctx, config) => {
301
268
  });
302
269
  Object.assign(ctx.github, { releaseName });
303
270
  };
271
+ const abortTask = (msg) => {
272
+ cancel(`${red("✕")} ${msg}`);
273
+ process.exit(0);
274
+ };
275
+ const abortSinglePrompt = (value) => {
276
+ if (isCancel(value)) abortTask(MSG.ABORT.CANCEL);
277
+ };
278
+ const abortGroupPrompt = (ctx) => {
279
+ ctx && gitRollback(ctx);
280
+ abortTask(MSG.ABORT.CANCEL);
281
+ };
282
+ const abortOnError = (err, ctx, exit = true) => {
283
+ ctx && gitRollback(ctx);
284
+ const msg = err.message.replace(/^\[(github|npm|git)]/, (a) => red(a));
285
+ log.message(`\n${msg}\n`);
286
+ exit && process.exit(1);
287
+ };
288
+ const taskEnd = (msg) => {
289
+ outro(msg);
290
+ process.exit(0);
291
+ };
304
292
  //#endregion
305
293
  //#region src/version/prompts.ts
306
294
  const createVersionMap = (current) => ({
@@ -348,7 +336,7 @@ const runVersionPrompts = async (ctx, config) => {
348
336
  message: question(MSG.PROMPT.INPUT_VERSION, "version"),
349
337
  initialValue: current,
350
338
  validate: (val) => {
351
- if (!isValid(val)) return "Invalid version";
339
+ if (isUndefined(val) || !isValidVersion(val)) return "Invalid version";
352
340
  }
353
341
  });
354
342
  abortSinglePrompt(version);
@@ -444,8 +432,11 @@ const createDefaultConfig = (isCI) => {
444
432
  toTag(pkg, version) {
445
433
  return `v${version}`;
446
434
  },
447
- tagPrefix(pkg) {
448
- return "";
435
+ changelog: {
436
+ tagPrefix(pkg) {
437
+ return "";
438
+ },
439
+ releaseCount: 1
449
440
  },
450
441
  ignoreGit: false,
451
442
  ignoreNpm: false,
@@ -521,42 +512,42 @@ const defaultTypes = [
521
512
  },
522
513
  {
523
514
  type: "chore",
524
- section: "🎫 其他更新",
515
+ section: "🧹 其他更新",
525
516
  description: "非功能性更改"
526
517
  },
527
518
  {
528
519
  type: "refactor",
529
- section: "♻ 代码重构",
520
+ section: "♻️ 代码重构",
530
521
  description: "重构代码"
531
522
  },
532
523
  {
533
524
  type: "test",
534
- section: "✅ 测试",
525
+ section: "🧪 测试",
535
526
  description: "测试更新"
536
527
  },
537
528
  {
538
529
  type: "build",
539
- section: "👷‍ 构建系统",
530
+ section: "🏗️ 构建系统",
540
531
  description: "构建工具和系统修改"
541
532
  },
542
533
  {
543
534
  type: "ci",
544
- section: "🔧 CI 配置",
535
+ section: "🤖 CI",
545
536
  description: "持续集成配置"
546
537
  },
547
538
  {
548
539
  type: "config",
549
- section: "🔨 配置",
540
+ section: "⚙️ 配置",
550
541
  description: "配置文件更新"
551
542
  },
552
543
  {
553
544
  type: "deps",
554
- section: "🔗 依赖更新",
545
+ section: "📦 依赖更新",
555
546
  description: "依赖版本变更"
556
547
  },
557
548
  {
558
549
  type: "security",
559
- section: "🔒 安全修复",
550
+ section: "🛡️ 安全修复",
560
551
  description: "安全性更新"
561
552
  },
562
553
  {
@@ -566,13 +557,18 @@ const defaultTypes = [
566
557
  },
567
558
  {
568
559
  type: "ux",
569
- section: "🖥️ 用户体验",
560
+ section: "🎨 用户体验",
570
561
  description: "用户体验改进"
571
562
  },
572
563
  {
573
564
  type: "hotfix",
574
- section: "🔥 热修复",
565
+ section: "🚑 热修复",
575
566
  description: "紧急修复"
567
+ },
568
+ {
569
+ type: "tool",
570
+ section: "🛠️ 工具链",
571
+ description: "工具链变更"
576
572
  }
577
573
  ];
578
574
  //#endregion
@@ -583,8 +579,7 @@ const parsePreset = async () => {
583
579
  hidden: false
584
580
  })) });
585
581
  preset.writer ??= {};
586
- preset.writer.headerPartial = `
587
- ## {{#if isPatch~}} <small> {{~/if~}}
582
+ preset.writer.headerPartial = `## {{#if isPatch~}} <small> {{~/if~}}
588
583
  {{#if @root.linkCompare~}}
589
584
  [{{version}}](
590
585
  {{~#if @root.repository~}}
@@ -608,8 +603,7 @@ const parsePreset = async () => {
608
603
  {{~/if}}
609
604
  {{~#if isPatch~}} </small> {{~/if}}
610
605
  `.trim() + eol();
611
- preset.writer.mainTemplate = `
612
- {{> header}}
606
+ preset.writer.mainTemplate = `{{> header}}
613
607
  {{#if noteGroups}}
614
608
  {{#each noteGroups}}
615
609
 
@@ -632,32 +626,37 @@ const parsePreset = async () => {
632
626
  {{/each}}`.trim();
633
627
  return preset;
634
628
  };
635
- const createGenerator = async ({ getPkgDir, tagPrefix }) => {
629
+ const createGenerator = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
636
630
  const pkgDir = getPkgDir();
637
631
  const preset = await parsePreset();
638
- return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
632
+ return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
639
633
  };
640
- const getChangelog = async ({ getPkgDir, tagPrefix }) => {
634
+ const getChangelog = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
641
635
  const generator = await createGenerator({
642
636
  getPkgDir,
643
- tagPrefix
637
+ tagPrefix,
638
+ releaseCount
644
639
  });
645
640
  let changelog = "";
646
641
  for await (const chunk of generator.write()) changelog += chunk;
647
- return changelog;
642
+ return changelog.trimEnd();
648
643
  };
649
- const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
644
+ const generateChangelog = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
650
645
  const infile = join(getPkgDir(), "CHANGELOG.md");
651
646
  if (!existsSync(infile)) await writeFile(infile, "");
652
647
  const exist = readFileSync(infile, "utf-8");
648
+ const writeStream = createWriteStream(infile);
653
649
  const generator = await createGenerator({
654
650
  getPkgDir,
655
- tagPrefix
651
+ tagPrefix,
652
+ releaseCount
656
653
  });
657
- const writeStream = createWriteStream(infile);
658
654
  for await (const chunk of generator.write()) writeStream.write(chunk);
659
- if (exist.trim().length) writeStream.write(eol(2));
660
- writeStream.write(exist);
655
+ const override = isZero(releaseCount);
656
+ if (!(isZero(exist.trim().length) || override)) {
657
+ writeStream.write(eol());
658
+ writeStream.write(exist);
659
+ }
661
660
  writeStream.end();
662
661
  await finished(writeStream);
663
662
  };
@@ -776,7 +775,7 @@ const createCReleaseByTag = async (octokit, options) => {
776
775
  const existing = await getReleaseByTagSafe(octokit, options);
777
776
  if (!existing) return createCRelease(octokit, { ...options });
778
777
  const { draft, id: releaseId } = existing;
779
- if (!draft) throw new Error(`Release for tag "${options.tag}" already exists and is not a draft`);
778
+ if (!draft) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(options.tag));
780
779
  return updateCRelease(octokit, {
781
780
  releaseId,
782
781
  ...options
@@ -925,9 +924,14 @@ const npmCheck = async (ctx, config) => {
925
924
  const tag = await resolvePublishTag(name, current);
926
925
  Object.assign(ctx.npm, { tag });
927
926
  if (skipChecks) return "";
928
- const [pinged, username] = await Promise.all([pingRegistry(registry), getAuthenticatedUser(registry)]);
927
+ const [pinged, username, canPublished] = await Promise.all([
928
+ pingRegistry(registry),
929
+ getAuthenticatedUser(registry),
930
+ canPublish(registry)
931
+ ]);
929
932
  if (!pinged) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
930
933
  if (!username) throw new Error(MSG.ERROR.NPM_AUTH);
934
+ if (!canPublished) throw new Error(MSG.ERROR.NPM_PERMISSION);
931
935
  Object.assign(ctx.npm, { username });
932
936
  const latest = await getPublishedVersion(`${name}@${tag}`);
933
937
  if (latest) {
@@ -979,10 +983,10 @@ var Action = class {
979
983
  const { otp, package: defPkg, ...others } = options;
980
984
  const { showChangelog, showRelease, ci, dryRun } = Object.fromEntries(Object.entries(others).map(([k, v]) => [k, Boolean(v)]));
981
985
  process.env["dryRun"] = String(dryRun);
982
- const { configPath, config: local } = await resolveConfig(process.cwd());
986
+ const { configFile, config: local } = await resolveConfig(process.cwd());
983
987
  const config = mergeConfig(createDefaultConfig(ci ? true : void 0), local);
984
988
  info(MSG.INFO.TOOL(cName, cVersion));
985
- info(MSG.INFO.CONFIG(configPath));
989
+ info(MSG.INFO.CONFIG(configFile));
986
990
  console.log();
987
991
  intro(prepare ? MSG.INTRO_PREPARE(dryRun) : MSG.INTRO(dryRun));
988
992
  const { isMonorepo, packages, getPkgDir, ignoreNpm } = config;
@@ -1077,7 +1081,7 @@ var Action = class {
1077
1081
  }
1078
1082
  async bumpTask(ctx, config) {
1079
1083
  const { pkg: { current }, dryRun, isCI, showRelease, showChangelog, selectedPkg, noGit } = ctx;
1080
- const { isMonorepo, hooks, getPkgDir, tagPrefix } = config;
1084
+ const { isMonorepo, hooks, getPkgDir, changelog: { tagPrefix, releaseCount } } = config;
1081
1085
  const need = (ctx) => {
1082
1086
  const { pkg: { next }, isIncrement } = ctx;
1083
1087
  return !next && isIncrement;
@@ -1107,37 +1111,43 @@ var Action = class {
1107
1111
  }]).catch((err) => abortOnError(err, ctx));
1108
1112
  await runLifeCycleHook(hooks, "after:bump", dryRun);
1109
1113
  if (noGit) return;
1110
- const { from, to } = await resolveChangelogRange(isIncrement, isMonorepo ? `${tagPrefix?.(selectedPkg)}*` : "*");
1111
- const commits = await getLog(from, to, getPkgDir(selectedPkg));
1114
+ const match = isMonorepo ? `${tagPrefix?.(selectedPkg)}*` : "*";
1115
+ const { from, to } = isZero(releaseCount) ? {
1116
+ from: "",
1117
+ to: "HEAD"
1118
+ } : await resolveChangelogRange(isIncrement, match);
1119
+ const commits = await getLogSince(from, to, getPkgDir(selectedPkg));
1112
1120
  const changelog = await getChangelog({
1113
1121
  getPkgDir: () => getPkgDir(selectedPkg),
1114
- tagPrefix: tagPrefix?.(selectedPkg)
1122
+ tagPrefix: tagPrefix?.(selectedPkg),
1123
+ releaseCount
1115
1124
  });
1116
1125
  Object.assign(ctx.github, { changelog });
1117
1126
  if (commits) {
1118
- msg("GIT", `Changelog(${from}...${to}):${eol(2)}` + changelog.trimEnd());
1127
+ msg("GIT", `Changelog(${from}...${to}):${eol(2)}` + changelog);
1119
1128
  msg("GIT", `Commits(${from}...${to}):${eol(2)}` + commits);
1120
1129
  if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1121
1130
  } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1122
1131
  }
1123
1132
  async changelogTask(ctx, config) {
1124
1133
  const { isIncrement, dryRun, selectedPkg, noGit } = ctx;
1125
- const { getPkgDir, tagPrefix } = config;
1134
+ const { getPkgDir, changelog: { tagPrefix, releaseCount } } = config;
1126
1135
  if (noGit) return;
1127
1136
  await tasks([{
1128
1137
  title: MSG.TASK.CHANGELOG.START,
1129
1138
  task: async () => {
1130
1139
  await generateChangelog({
1131
1140
  getPkgDir: () => getPkgDir(selectedPkg),
1132
- tagPrefix: tagPrefix?.(selectedPkg)
1141
+ tagPrefix: tagPrefix?.(selectedPkg),
1142
+ releaseCount
1133
1143
  });
1134
1144
  await gitAddAll();
1135
1145
  return success(MSG.TASK.CHANGELOG.END, dryRun);
1136
1146
  },
1137
1147
  enabled: isIncrement
1138
1148
  }]).catch((err) => abortOnError(err, ctx));
1139
- const changeset = await getStatus();
1140
- if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredChangeset(changeset));
1149
+ const changeset = await getShortStatus();
1150
+ if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredStatus(changeset));
1141
1151
  else msg("GIT", MSG.LOG.CHANGES_EMPTY);
1142
1152
  }
1143
1153
  async gitTask(ctx, config) {
@@ -1212,8 +1222,8 @@ program.command("init").description("Create a release configuration file").optio
1212
1222
  const isESM = type === "module";
1213
1223
  const isTS = existsSync(join(cwd, "tsconfig.json"));
1214
1224
  const infile = `release.config.${isESM ? "" : "m"}${isTS ? "ts" : "js"}`;
1215
- const configPath = join(cwd, infile);
1216
- if (existsSync(configPath) && !force) {
1225
+ const configFile = join(cwd, infile);
1226
+ if (existsSync(configFile) && !force) {
1217
1227
  console.error(`${red`Error:`} ${yellow(infile)} already exists.\nUse ${green`--force`} to overwrite.`);
1218
1228
  process.exit(1);
1219
1229
  }
@@ -1222,9 +1232,11 @@ program.command("init").description("Create a release configuration file").optio
1222
1232
  packages: [ 'demo-a', 'demo-b' ],
1223
1233
  getPkgDir: (pkg) => \`packages/\${ pkg }\`,
1224
1234
  toTag: (pkg, version) => \`\${ pkg }@\${ version }\`,
1225
- tagPrefix: (pkg) => \`\${ pkg }@\`,
1235
+ changelog: {
1236
+ tagPrefix: (pkg) => \`\${ pkg }@\`,
1237
+ },
1226
1238
  }`;
1227
- writeFileSync(configPath, [`import { defineConfig } from '@peiyanlu/release'`, `export default defineConfig(${monorepo ? monoConfigToString() : "{}"})`].join(eol(2)), "utf-8");
1239
+ writeFileSync(configFile, [`import { defineConfig } from '@peiyanlu/release'`, `export default defineConfig(${monorepo ? monoConfigToString() : "{}"})`].join(eol(2)), "utf-8");
1228
1240
  console.log(`${green`✔`} ${infile} created successfully`);
1229
1241
  });
1230
1242
  program.parse(process.argv);
package/dist/index.d.mts CHANGED
@@ -23,14 +23,20 @@ interface ReleaseConfig {
23
23
  getPkgDir: (pkg: string) => string;
24
24
  /** 使用 monorepo 时使用 toTag,例如 `${pkg}@${version}`,默认为 `v${version}` 作为单仓库 */
25
25
  toTag: (pkg: string, version: string) => string;
26
- /** 作为 CHANGELOG 标题的 Tag 前缀,多包场景下需要 `${pkg}@` */
27
- tagPrefix: ((pkg: string) => string) | undefined;
26
+ /**
27
+ * CHANGELOG 相关配置
28
+ * 影响生成的日志数量、日志标题(monorepo)。
29
+ */
30
+ changelog: {
31
+ /** 作为 CHANGELOG 标题的 Tag 前缀,多包场景下需要 `${pkg}@` */tagPrefix: ((pkg: string) => string); /** 生成多少个版本的变更日志,详见 {@link Options} */
32
+ releaseCount: number;
33
+ };
28
34
  /** 忽略 git、github 环境检查以及后续操作 */
29
- ignoreGit: boolean | undefined;
35
+ ignoreGit: boolean;
30
36
  /** 忽略 npm 环境检查以及后续操作 */
31
- ignoreNpm: boolean | undefined;
37
+ ignoreNpm: boolean;
32
38
  /** 忽略 github 环境检查以及后续操作 */
33
- ignoreGithub: boolean | undefined;
39
+ ignoreGithub: boolean;
34
40
  /**
35
41
  * Git 相关配置
36
42
  * 影响提交、打 tag、推送等本地/远程 Git 行为。
@@ -156,27 +162,32 @@ declare function defineConfig(config: UserConfigFnObject): UserConfigFnObject;
156
162
  declare function defineConfig(config: UserConfigFnPromise): UserConfigFnPromise;
157
163
  declare function defineConfig(config: UserConfigFn): UserConfigFn;
158
164
  declare function defineConfig(config: UserConfigExport): UserConfigExport;
159
- declare const mergeConfig: <T extends object>(defaults: DeepRequired<T>, overrides?: DeepPartial<T>) => DeepRequired<T>;
165
+ declare const mergeConfig: <T extends object>(defaults: DeepRequired<T>, overrides: DeepPartial<T>) => DeepRequired<T>;
160
166
  //#endregion
161
167
  //#region src/git/changelog.d.ts
162
- interface Options {
168
+ interface GenerateOptions {
163
169
  /** @example () => `packages/${pkg}` */
164
170
  getPkgDir: () => string;
165
171
  /** @example `${pkg}@` */
166
172
  tagPrefix?: string;
173
+ /** 生成多少个变更日志,详见 {@link Options} */
174
+ releaseCount?: number;
167
175
  }
168
176
  declare const createGenerator: ({
169
177
  getPkgDir,
170
- tagPrefix
171
- }: Options) => Promise<ConventionalChangelog>;
178
+ tagPrefix,
179
+ releaseCount
180
+ }: GenerateOptions) => Promise<ConventionalChangelog>;
172
181
  declare const getChangelog: ({
173
182
  getPkgDir,
174
- tagPrefix
175
- }: Options) => Promise<string>;
183
+ tagPrefix,
184
+ releaseCount
185
+ }: GenerateOptions) => Promise<string>;
176
186
  declare const generateChangelog: ({
177
187
  getPkgDir,
178
- tagPrefix
179
- }: Options) => Promise<void>;
188
+ tagPrefix,
189
+ releaseCount
190
+ }: GenerateOptions) => Promise<void>;
180
191
  //#endregion
181
192
  //#region src/npm/publish.d.ts
182
193
  interface PublishTagOptions {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { isFunction, isPlainObject } from "@peiyanlu/ts-utils";
1
+ import { isFunction, isPlainObject, isUndefined, isZero } from "@peiyanlu/ts-utils";
2
2
  import { createWriteStream, existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { eol, getPackageInfo, publishPackage, resolvePublishTag } from "@peiyanlu/cli-utils";
@@ -12,13 +12,12 @@ function defineConfig(config) {
12
12
  return config;
13
13
  }
14
14
  const mergeConfig = (defaults, overrides) => {
15
- if (!overrides) return structuredClone(defaults);
16
- if (Object.keys(defaults).length === 0) return structuredClone(overrides);
17
15
  const result = {};
18
- for (const key in defaults) {
16
+ for (const key of Object.keys(defaults)) {
19
17
  const defaultValue = defaults[key];
20
18
  const overrideValue = overrides[key];
21
- if (overrideValue === void 0) if (isFunction(defaultValue)) result[key] = defaultValue;
19
+ if (isUndefined(overrideValue)) if (isFunction(defaultValue)) result[key] = defaultValue;
20
+ else if (isPlainObject(defaultValue)) result[key] = mergeConfig(defaultValue, {});
22
21
  else result[key] = structuredClone(defaultValue);
23
22
  else if (isPlainObject(defaultValue) && isPlainObject(overrideValue)) result[key] = mergeConfig(defaultValue, overrideValue);
24
23
  else if (isFunction(overrideValue)) result[key] = overrideValue;
@@ -66,42 +65,42 @@ const defaultTypes = [
66
65
  },
67
66
  {
68
67
  type: "chore",
69
- section: "🎫 其他更新",
68
+ section: "🧹 其他更新",
70
69
  description: "非功能性更改"
71
70
  },
72
71
  {
73
72
  type: "refactor",
74
- section: "♻ 代码重构",
73
+ section: "♻️ 代码重构",
75
74
  description: "重构代码"
76
75
  },
77
76
  {
78
77
  type: "test",
79
- section: "✅ 测试",
78
+ section: "🧪 测试",
80
79
  description: "测试更新"
81
80
  },
82
81
  {
83
82
  type: "build",
84
- section: "👷‍ 构建系统",
83
+ section: "🏗️ 构建系统",
85
84
  description: "构建工具和系统修改"
86
85
  },
87
86
  {
88
87
  type: "ci",
89
- section: "🔧 CI 配置",
88
+ section: "🤖 CI",
90
89
  description: "持续集成配置"
91
90
  },
92
91
  {
93
92
  type: "config",
94
- section: "🔨 配置",
93
+ section: "⚙️ 配置",
95
94
  description: "配置文件更新"
96
95
  },
97
96
  {
98
97
  type: "deps",
99
- section: "🔗 依赖更新",
98
+ section: "📦 依赖更新",
100
99
  description: "依赖版本变更"
101
100
  },
102
101
  {
103
102
  type: "security",
104
- section: "🔒 安全修复",
103
+ section: "🛡️ 安全修复",
105
104
  description: "安全性更新"
106
105
  },
107
106
  {
@@ -111,13 +110,18 @@ const defaultTypes = [
111
110
  },
112
111
  {
113
112
  type: "ux",
114
- section: "🖥️ 用户体验",
113
+ section: "🎨 用户体验",
115
114
  description: "用户体验改进"
116
115
  },
117
116
  {
118
117
  type: "hotfix",
119
- section: "🔥 热修复",
118
+ section: "🚑 热修复",
120
119
  description: "紧急修复"
120
+ },
121
+ {
122
+ type: "tool",
123
+ section: "🛠️ 工具链",
124
+ description: "工具链变更"
121
125
  }
122
126
  ];
123
127
  //#endregion
@@ -128,8 +132,7 @@ const parsePreset = async () => {
128
132
  hidden: false
129
133
  })) });
130
134
  preset.writer ??= {};
131
- preset.writer.headerPartial = `
132
- ## {{#if isPatch~}} <small> {{~/if~}}
135
+ preset.writer.headerPartial = `## {{#if isPatch~}} <small> {{~/if~}}
133
136
  {{#if @root.linkCompare~}}
134
137
  [{{version}}](
135
138
  {{~#if @root.repository~}}
@@ -153,8 +156,7 @@ const parsePreset = async () => {
153
156
  {{~/if}}
154
157
  {{~#if isPatch~}} </small> {{~/if}}
155
158
  `.trim() + eol();
156
- preset.writer.mainTemplate = `
157
- {{> header}}
159
+ preset.writer.mainTemplate = `{{> header}}
158
160
  {{#if noteGroups}}
159
161
  {{#each noteGroups}}
160
162
 
@@ -177,32 +179,37 @@ const parsePreset = async () => {
177
179
  {{/each}}`.trim();
178
180
  return preset;
179
181
  };
180
- const createGenerator = async ({ getPkgDir, tagPrefix }) => {
182
+ const createGenerator = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
181
183
  const pkgDir = getPkgDir();
182
184
  const preset = await parsePreset();
183
- return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
185
+ return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
184
186
  };
185
- const getChangelog = async ({ getPkgDir, tagPrefix }) => {
187
+ const getChangelog = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
186
188
  const generator = await createGenerator({
187
189
  getPkgDir,
188
- tagPrefix
190
+ tagPrefix,
191
+ releaseCount
189
192
  });
190
193
  let changelog = "";
191
194
  for await (const chunk of generator.write()) changelog += chunk;
192
- return changelog;
195
+ return changelog.trimEnd();
193
196
  };
194
- const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
197
+ const generateChangelog = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
195
198
  const infile = join(getPkgDir(), "CHANGELOG.md");
196
199
  if (!existsSync(infile)) await writeFile(infile, "");
197
200
  const exist = readFileSync(infile, "utf-8");
201
+ const writeStream = createWriteStream(infile);
198
202
  const generator = await createGenerator({
199
203
  getPkgDir,
200
- tagPrefix
204
+ tagPrefix,
205
+ releaseCount
201
206
  });
202
- const writeStream = createWriteStream(infile);
203
207
  for await (const chunk of generator.write()) writeStream.write(chunk);
204
- if (exist.trim().length) writeStream.write(eol(2));
205
- writeStream.write(exist);
208
+ const override = isZero(releaseCount);
209
+ if (!(isZero(exist.trim().length) || override)) {
210
+ writeStream.write(eol());
211
+ writeStream.write(exist);
212
+ }
206
213
  writeStream.end();
207
214
  await finished(writeStream);
208
215
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peiyanlu/release",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "description": "Release project tool.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "dev": "tsdown -w",
22
22
  "build": "tsdown",
23
- "prepublish": "tsdown",
23
+ "prepublishOnly": "tsdown",
24
24
  "release": "node index.js",
25
25
  "test": "vitest run",
26
26
  "test:cov": "vitest run --coverage"
@@ -30,16 +30,17 @@
30
30
  "@actions/github": "^9.1.1",
31
31
  "@clack/prompts": "^1.5.0",
32
32
  "@octokit/rest": "^22.0.1",
33
- "@peiyanlu/cli-utils": "^0.0.8",
34
- "@peiyanlu/ts-utils": "^0.0.5",
33
+ "@peiyanlu/cli-utils": "^0.0.12",
34
+ "@peiyanlu/ts-utils": "^0.0.7",
35
35
  "ansis": "^4.3.1",
36
36
  "commander": "^15.0.0",
37
- "conventional-changelog": "^7.2.0",
37
+ "conventional-changelog": "^7.2.1",
38
38
  "conventional-changelog-conventionalcommits": "^9.3.1",
39
+ "conventional-changelog-writer": "^8.3.0",
39
40
  "mime-types": "^3.0.2",
40
41
  "open": "^11.0.0",
41
42
  "publint": "^0.3.21",
42
- "semver": "^7.8.1",
43
+ "semver": "^7.8.5",
43
44
  "tinyglobby": "^0.2.17"
44
45
  },
45
46
  "devDependencies": {
@@ -47,9 +48,9 @@
47
48
  "@types/node": "^24.12.4",
48
49
  "@types/semver": "^7.7.1",
49
50
  "@vitest/coverage-v8": "^4.1.8",
50
- "tsdown": "^0.22.1",
51
+ "tsdown": "^0.22.3",
51
52
  "typescript": "^6.0.3",
52
- "vitest": "^4.1.8"
53
+ "vitest": "^4.1.9"
53
54
  },
54
55
  "author": {
55
56
  "name": "YanluPei",