@peiyanlu/release 1.0.0 → 1.1.1

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
@@ -56,14 +56,21 @@ pnpm release minor --ci
56
56
 
57
57
  ### 通用参数
58
58
 
59
- | 参数 | 说明 |
60
- |--------------------|------------------------------------------------------------|
61
- | `-v, --version` | 输出当前 CLI 版本并退出 |
62
- | `-h, --help` | 显示帮助信息 |
63
- | `-n, --dry-run` | 空跑模式,展示将要执行的发布流程但不做任何修改 |
64
- | `--show-release` | 打印即将发布的版本号并退出 |
65
- | `--show-changelog` | 打印生成的 Changelog 并退出 |
66
- | `--ci` | 启用 CI 模式:禁用交互提示,缺少必要参数时直接失败;GitHub Actions 等 CI/CD 环境中默认启用 |
59
+ | 参数 | 说明 |
60
+ |---------------------------------------|------------------------------------------------------------|
61
+ | `-v, --version` | 输出当前 CLI 版本并退出 |
62
+ | `-h, --help` | 显示帮助信息 |
63
+ | `-n, --dry-run` | 空跑模式,展示将要执行的发布流程但不做任何修改 |
64
+ | `--show-release` | 打印即将发布的版本号并退出 |
65
+ | `--show-changelog` | 打印生成的 Changelog 并退出 |
66
+ | `--ci` | 启用 CI 模式:禁用交互提示,缺少必要参数时直接失败;GitHub Actions 等 CI/CD 环境中默认启用 |
67
+ | `-r, --release-count <count>` | 生成多少个版本的变更日志 |
68
+ | `--include-hidden` | 生成 CHANGELOG 时包含默认隐藏的 commit 类型 |
69
+ | `--only-changelog` | 仅更新 CHANGELOG |
70
+ | `-C, --no-require-clean-working-tree` | 是否要求工作目录保持干净(无未提交的更改) |
71
+ | `--skip-git` | 跳过 git、github 环境检查以及后续操作 |
72
+ | `--skip-npm` | 跳过 npm 环境检查以及后续操作 |
73
+ | `--skip-github` | 跳过 github 环境检查以及后续操作 |
67
74
 
68
75
  ### Prepare 相关参数
69
76
 
@@ -81,6 +88,7 @@ pnpm release minor --prepare --ci --package pkg-a
81
88
 
82
89
  | 参数 | 说明 |
83
90
  |-----------------------|----------------------------------|
91
+ | `-m, --is-monorepo` | Monorepo 项目 |
84
92
  | `-p, --package <pkg>` | 指定要发布的子包名称(仅用于 Monorepo 的 CI 场景) |
85
93
 
86
94
  ```bash
@@ -194,6 +202,7 @@ release init
194
202
  |------------------|---------------------|
195
203
  | `-f, --force` | 覆盖已存在的配置文件 |
196
204
  | `-m, --monorepo` | 生成 Monorepo 项目的配置模板 |
205
+ | `-a, --add` | 自动暂存配置文件 |
197
206
 
198
207
  #### 行为说明
199
208
 
@@ -232,6 +241,12 @@ COMMIT_TYPES:
232
241
  - 默认:`feat | feature | fix | perf | revert | docs | style | chore | refactor | test | build | ci`
233
242
  - 自定义:`config | deps | security | i18n | ux | hotfix`
234
243
 
244
+ | Effect | Types | 说明 |
245
+ |-----------------|----------------------------------------------------------------|-----------------------------------|
246
+ | **`bump`** | `feat`, `feature`, `fix`, `perf`, `revert` | 影响版本号(Minor/Patch),同时写入 CHANGELOG |
247
+ | **`changelog`** | `docs`, `refactor`, `deps`, `security`, `hotfix`, `i18n`, `ux` | 不影响版本号,仅写入 CHANGELOG |
248
+ | **`hidden`** | `style`, `chore`, `test`, `build`, `ci`, `config`, `tool` | 不影响版本号,也不会写入 CHANGELOG |
249
+
235
250
  支持 **--show-changelog** 预览模式:
236
251
 
237
252
  - 仅展示用于生成 Changelog 的 Commits
package/dist/cli.mjs CHANGED
@@ -12,8 +12,10 @@ import { inc, neq } from "semver";
12
12
  import { inspect } from "node:util";
13
13
  import { ConventionalChangelog } from "conventional-changelog";
14
14
  import createPreset from "conventional-changelog-conventionalcommits";
15
+ import { Bumper } from "conventional-recommended-bump";
15
16
  import { writeFile } from "node:fs/promises";
16
17
  import { finished } from "node:stream/promises";
18
+ import { bold, compareUrl, each, heading, link, list, newline, segments, small, words } from "@conventional-changelog/template";
17
19
  import { Octokit } from "@octokit/rest";
18
20
  import { contentType } from "mime-types";
19
21
  import open from "open";
@@ -79,7 +81,8 @@ const MSG = {
79
81
  SHOW_VERSION: (version) => `🎉 Released ${green(version)}`,
80
82
  SHOW_CHANGELOG: "🎉 Changelog collected",
81
83
  CHANGELOG_EMPTY: "No commits found since last release",
82
- CHANGES_EMPTY: "Working tree clean. No files changed"
84
+ CHANGES_EMPTY: "Working tree clean. No files changed",
85
+ ONLY_CHANGELOG: "🎉 Changelog generation completed"
83
86
  },
84
87
  CHECK: {
85
88
  GIT: {
@@ -306,7 +309,7 @@ const option = (label, value, hint) => ({
306
309
  value,
307
310
  hint: hint ?? value
308
311
  });
309
- const runVersionPrompts = async (ctx, config) => {
312
+ const runVersionPrompts = async (ctx, config, inferred) => {
310
313
  const { pkg: { current, fromPreRelease } } = ctx;
311
314
  const selectVersion = async () => {
312
315
  const versions = createVersionMap(current);
@@ -326,7 +329,7 @@ const runVersionPrompts = async (ctx, config) => {
326
329
  const type = await select({
327
330
  message: question(MSG.PROMPT.SELECT_VERSION, "version"),
328
331
  options,
329
- initialValue: fromPreRelease ? "prerelease" : "patch"
332
+ initialValue: inferred ?? (fromPreRelease ? "prerelease" : "patch")
330
333
  });
331
334
  abortSinglePrompt(type);
332
335
  return type;
@@ -357,6 +360,7 @@ const createDefaultContext = () => {
357
360
  release: false,
358
361
  showRelease: false,
359
362
  showChangelog: false,
363
+ onlyChangelog: false,
360
364
  dryRun: false,
361
365
  noGit: false,
362
366
  noNpm: false,
@@ -436,7 +440,9 @@ const createDefaultConfig = (isCI) => {
436
440
  tagPrefix(pkg) {
437
441
  return "";
438
442
  },
439
- releaseCount: 1
443
+ releaseCount: 1,
444
+ includeHidden: false,
445
+ transformTypes: (t) => t
440
446
  },
441
447
  skipGit: false,
442
448
  skipNpm: false,
@@ -478,179 +484,178 @@ const defaultTypes = [
478
484
  {
479
485
  type: "feat",
480
486
  section: "✨ 新功能",
487
+ effect: "bump",
481
488
  description: "新增功能"
482
489
  },
483
490
  {
484
491
  type: "feature",
485
492
  section: "✨ 新功能",
493
+ effect: "bump",
486
494
  description: "新增功能"
487
495
  },
488
496
  {
489
497
  type: "fix",
490
498
  section: "🐛 Bug 修复",
499
+ effect: "bump",
491
500
  description: "修复 bug"
492
501
  },
493
502
  {
494
503
  type: "perf",
495
504
  section: "⚡ 性能优化",
505
+ effect: "bump",
496
506
  description: "提升性能"
497
507
  },
498
508
  {
499
509
  type: "revert",
500
510
  section: "⏪ 回退",
511
+ effect: "bump",
501
512
  description: "回退到之前版本"
502
513
  },
503
514
  {
504
515
  type: "docs",
505
516
  section: "📝 文档",
517
+ effect: "changelog",
506
518
  description: "文档更新"
507
519
  },
508
520
  {
509
521
  type: "style",
510
522
  section: "💄 样式调整",
523
+ effect: "hidden",
511
524
  description: "样式或格式修改"
512
525
  },
513
526
  {
514
527
  type: "chore",
515
528
  section: "🧹 其他更新",
529
+ effect: "hidden",
516
530
  description: "非功能性更改"
517
531
  },
518
532
  {
519
533
  type: "refactor",
520
534
  section: "♻️ 代码重构",
535
+ effect: "changelog",
521
536
  description: "重构代码"
522
537
  },
523
538
  {
524
539
  type: "test",
525
540
  section: "🧪 测试",
541
+ effect: "hidden",
526
542
  description: "测试更新"
527
543
  },
528
544
  {
529
545
  type: "build",
530
546
  section: "🏗️ 构建系统",
547
+ effect: "hidden",
531
548
  description: "构建工具和系统修改"
532
549
  },
533
550
  {
534
551
  type: "ci",
535
552
  section: "🤖 CI",
553
+ effect: "hidden",
536
554
  description: "持续集成配置"
537
555
  },
538
- {
539
- type: "config",
540
- section: "⚙️ 配置",
541
- description: "配置文件更新"
542
- },
543
556
  {
544
557
  type: "deps",
545
558
  section: "📦 依赖更新",
559
+ effect: "changelog",
546
560
  description: "依赖版本变更"
547
561
  },
548
562
  {
549
563
  type: "security",
550
564
  section: "🛡️ 安全修复",
565
+ effect: "changelog",
551
566
  description: "安全性更新"
552
567
  },
568
+ {
569
+ type: "hotfix",
570
+ section: "🚑 热修复",
571
+ effect: "changelog",
572
+ description: "紧急修复"
573
+ },
553
574
  {
554
575
  type: "i18n",
555
576
  section: "🌐 国际化",
577
+ effect: "changelog",
556
578
  description: "国际化相关更新"
557
579
  },
558
580
  {
559
581
  type: "ux",
560
582
  section: "🎨 用户体验",
583
+ effect: "changelog",
561
584
  description: "用户体验改进"
562
585
  },
563
586
  {
564
- type: "hotfix",
565
- section: "🚑 热修复",
566
- description: "紧急修复"
587
+ type: "config",
588
+ section: "⚙️ 配置",
589
+ effect: "hidden",
590
+ description: "配置文件更新"
567
591
  },
568
592
  {
569
593
  type: "tool",
570
594
  section: "🛠️ 工具链",
595
+ effect: "hidden",
571
596
  description: "工具链变更"
572
597
  }
573
598
  ];
574
599
  //#endregion
600
+ //#region src/git/templates.ts
601
+ const isLikelyHash = (value) => /^[0-9a-f]{7,40}$/i.test(value);
602
+ function headerPartial(context) {
603
+ const { isPatch, linkCompare, version, title, date, previousTag } = context;
604
+ const fullTitle = words(linkCompare && !isLikelyHash(previousTag) ? link(version, compareUrl(context)) : version, title && `"${title}"`, date && `(${date})`);
605
+ return heading(2, isPatch ? small(fullTitle) : fullTitle);
606
+ }
607
+ function template(context) {
608
+ const { headerPartial, preamblePartial, commitPartial, footerPartial, noteGroups, commitGroups } = context;
609
+ return segments(headerPartial(context), preamblePartial(context), each(noteGroups, (group) => segments(heading(3, words("⚠", group.title)), list(group.notes, (note) => {
610
+ return each([words(note.commit.scope && bold(`${note.commit.scope}:`), commitPartial(context, note.commit)), note.commit.subject.trim() !== note.text.trim() ? list([note.text], (t) => t) : ""], (t) => t, newline());
611
+ })), newline(2)), each(commitGroups, (group) => segments(group.title && heading(3, group.title), list(group.commits, (commit) => commitPartial(context, commit))), newline(2)), footerPartial(context));
612
+ }
613
+ //#endregion
575
614
  //#region src/git/changelog.ts
576
- const parsePreset = async () => {
577
- const preset = await createPreset({ types: defaultTypes.map((t) => ({
578
- ...t,
579
- hidden: false
580
- })) });
615
+ const parsePreset = (options = {}) => {
616
+ const { includeHidden, transformTypes } = options;
617
+ let types = [...defaultTypes];
618
+ if (includeHidden) types.forEach((type) => {
619
+ if (type.effect === "hidden") type.effect = "changelog";
620
+ });
621
+ types = transformTypes?.(types) ?? types;
622
+ const preset = createPreset({ types });
581
623
  preset.writer ??= {};
582
- preset.writer.headerPartial = `## {{#if isPatch~}} <small> {{~/if~}}
583
- {{#if @root.linkCompare~}}
584
- [{{version}}](
585
- {{~#if @root.repository~}}
586
- {{~#if @root.host}}
587
- {{~@root.host}}/
588
- {{~/if}}
589
- {{~#if @root.owner}}
590
- {{~@root.owner}}/
591
- {{~/if}}
592
- {{~@root.repository}}
593
- {{~else}}
594
- {{~@root.repoUrl}}
595
- {{~/if~}}
596
- /compare/{{previousTag}}...{{currentTag}})
597
- {{~else}}
598
- {{~version}}
599
- {{~/if}}
600
- {{~#if title}} "{{title}}"
601
- {{~/if}}
602
- {{~#if date}} ({{date}})
603
- {{~/if}}
604
- {{~#if isPatch~}} </small> {{~/if}}
605
- `.trim() + eol();
606
- preset.writer.mainTemplate = `{{> header}}
607
- {{#if noteGroups}}
608
- {{#each noteGroups}}
609
-
610
- ### ⚠ {{title}}
611
-
612
- {{#each notes}}
613
- * {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{commit.subject}} {{#if commit.hash}}([{{commit.shortHash}}](https://github.com/{{@root.owner}}/{{@root.repository}}/commit/{{commit.hash}})){{/if}}
614
- {{/each}}
615
- {{/each}}
616
- {{/if}}
617
- {{#each commitGroups}}
618
-
619
- {{#if title}}
620
- ### {{title}}
621
-
622
- {{/if}}
623
- {{#each commits}}
624
- {{> commit root=@root}}
625
- {{/each}}
626
- {{/each}}`.trim();
624
+ preset.writer.headerPartial = headerPartial;
625
+ preset.writer.template = template;
627
626
  return preset;
628
627
  };
629
- const createGenerator = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
628
+ const inferReleaseType = async (options) => {
629
+ const preset = parsePreset(options);
630
+ const res = await new Bumper().config(preset).bump(preset.whatBump);
631
+ if (!("releaseType" in res)) return void 0;
632
+ const { releaseType, reason } = res;
633
+ return options?.json ? {
634
+ releaseType,
635
+ reason
636
+ } : releaseType;
637
+ };
638
+ const createGenerator = async (options) => {
639
+ const { getPkgDir, tagPrefix, releaseCount = 1, includeHidden, transformTypes } = options;
630
640
  const pkgDir = getPkgDir();
631
- const preset = await parsePreset();
632
- return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
641
+ return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(parsePreset({
642
+ includeHidden,
643
+ transformTypes
644
+ })).options({ releaseCount }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
633
645
  };
634
- const getChangelog = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
635
- const generator = await createGenerator({
636
- getPkgDir,
637
- tagPrefix,
638
- releaseCount
639
- });
646
+ const getChangelog = async (options) => {
647
+ const generator = await createGenerator(options);
640
648
  let changelog = "";
641
649
  for await (const chunk of generator.write()) changelog += chunk;
642
650
  return changelog.trimEnd();
643
651
  };
644
- const generateChangelog = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
652
+ const generateChangelog = async (options) => {
653
+ const { getPkgDir, releaseCount = 1 } = options;
645
654
  const infile = join(getPkgDir(), "CHANGELOG.md");
646
655
  if (!existsSync(infile)) await writeFile(infile, "");
647
656
  const exist = readFileSync(infile, "utf-8");
648
657
  const writeStream = createWriteStream(infile);
649
- const generator = await createGenerator({
650
- getPkgDir,
651
- tagPrefix,
652
- releaseCount
653
- });
658
+ const generator = await createGenerator(options);
654
659
  for await (const chunk of generator.write()) writeStream.write(chunk);
655
660
  const override = isZero(releaseCount);
656
661
  if (!(isZero(exist.trim().length) || override)) {
@@ -981,16 +986,17 @@ var Action = class {
981
986
  async createContext(cmdArgs, options, prepare = false) {
982
987
  const { version: cVersion, name: cName } = readJsonFile(join(__dirname, "../package.json"));
983
988
  const { otp, package: defPkg, releaseCount, ...others } = options;
984
- const { showChangelog, showRelease, ci, dryRun } = mapObjectValues(others, (v) => Boolean(v));
989
+ const { showChangelog, showRelease, ci, dryRun, onlyChangelog } = mapObjectValues(others, (v) => Boolean(v));
985
990
  process.env["dryRun"] = String(dryRun);
986
991
  const { configFile, config: local } = await resolveConfig(process.cwd());
987
992
  const config = mergeConfig(createDefaultConfig(ci ? true : void 0), local);
988
993
  info(MSG.INFO.TOOL(cName, cVersion));
989
994
  info(MSG.INFO.CONFIG(configFile));
990
995
  {
991
- const { requireCleanWorkingTree, skipGit, skipNpm, skipGithub, isMonorepo } = mapObjectValues(others, (v) => Boolean(v));
996
+ const { requireCleanWorkingTree, skipGit, skipNpm, skipGithub, isMonorepo, includeHidden } = mapObjectValues(others, (v) => Boolean(v));
992
997
  const count = Number(releaseCount);
993
998
  config.changelog.releaseCount = isNaN(count) ? 0 : count;
999
+ config.changelog.includeHidden = includeHidden;
994
1000
  config.git.requireCleanWorkingTree = requireCleanWorkingTree;
995
1001
  config.skipGit = skipGit;
996
1002
  config.skipNpm = skipNpm;
@@ -1026,6 +1032,7 @@ var Action = class {
1026
1032
  dryRun,
1027
1033
  showRelease,
1028
1034
  showChangelog,
1035
+ onlyChangelog,
1029
1036
  npm: { otp: String(otp) },
1030
1037
  increment: cmdArgs,
1031
1038
  isIncrement: true,
@@ -1057,8 +1064,32 @@ var Action = class {
1057
1064
  config
1058
1065
  };
1059
1066
  }
1067
+ async printChangelog(ctx, config) {
1068
+ const { showChangelog, selectedPkg, noGit, isIncrement } = ctx;
1069
+ const { isMonorepo, getPkgDir, changelog: { tagPrefix, releaseCount, includeHidden, transformTypes } } = config;
1070
+ if (noGit) return;
1071
+ const match = isMonorepo ? `${tagPrefix?.(selectedPkg)}*` : "*";
1072
+ const { from, to } = isZero(releaseCount) ? {
1073
+ from: "",
1074
+ to: "HEAD"
1075
+ } : await resolveChangelogRange(isIncrement, match);
1076
+ const commits = await getLogSince(from, to, getPkgDir(selectedPkg));
1077
+ const changelog = await getChangelog({
1078
+ getPkgDir: () => getPkgDir(selectedPkg),
1079
+ tagPrefix: tagPrefix?.(selectedPkg),
1080
+ releaseCount,
1081
+ includeHidden,
1082
+ transformTypes
1083
+ });
1084
+ Object.assign(ctx.github, { changelog });
1085
+ if (commits) {
1086
+ msg("GIT", `Changelog(${from}...${to}):${eol(2)}` + changelog);
1087
+ msg("GIT", `Commits(${from}...${to}):${eol(2)}` + commits);
1088
+ if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1089
+ } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1090
+ }
1060
1091
  async checkTask(ctx, config) {
1061
- const { dryRun } = ctx;
1092
+ const { dryRun, onlyChangelog } = ctx;
1062
1093
  await tasks([
1063
1094
  {
1064
1095
  title: MSG.CHECK.GIT.CHECKING,
@@ -1075,7 +1106,7 @@ var Action = class {
1075
1106
  const { pkg: { publishConfig: { registry } } } = ctx;
1076
1107
  return success(MSG.CHECK.NPM.CHECKED(registry, msg), dryRun);
1077
1108
  },
1078
- enabled: !ctx.noNpm
1109
+ enabled: !(ctx.noNpm || onlyChangelog)
1079
1110
  },
1080
1111
  {
1081
1112
  title: MSG.CHECK.GITHUB.CHECKING,
@@ -1085,19 +1116,27 @@ var Action = class {
1085
1116
  const repository = getGithubUrl(owner, repo);
1086
1117
  return success(MSG.CHECK.GITHUB.CHECKED(repository, msg), dryRun);
1087
1118
  },
1088
- enabled: !ctx.noGitHub
1119
+ enabled: !(ctx.noGitHub || onlyChangelog)
1089
1120
  }
1090
1121
  ]).catch(abortOnError);
1091
1122
  }
1092
1123
  async bumpTask(ctx, config) {
1093
- const { pkg: { current }, dryRun, isCI, showRelease, showChangelog, selectedPkg, noGit } = ctx;
1094
- const { isMonorepo, hooks, getPkgDir, changelog: { tagPrefix, releaseCount } } = config;
1124
+ const { pkg: { current }, dryRun, isCI, showRelease, selectedPkg, onlyChangelog } = ctx;
1125
+ const { hooks, getPkgDir, changelog: { includeHidden, transformTypes } } = config;
1126
+ if (onlyChangelog) return;
1095
1127
  const need = (ctx) => {
1096
1128
  const { pkg: { next }, isIncrement } = ctx;
1097
1129
  return !next && isIncrement;
1098
1130
  };
1099
1131
  if (need(ctx)) {
1100
- const nextVersion = (isCI ? inc(current, "patch") : void 0) || await runVersionPrompts(ctx, config);
1132
+ const res = await inferReleaseType({
1133
+ includeHidden,
1134
+ transformTypes,
1135
+ json: true
1136
+ });
1137
+ if (res) msg("BUMP", res.reason);
1138
+ const inferred = res?.releaseType;
1139
+ const nextVersion = (isCI ? inferred ?? inc(current, "patch") : void 0) || await runVersionPrompts(ctx, config, inferred);
1101
1140
  ctx.isIncrement = neq(current, nextVersion);
1102
1141
  const { version: next, isPrerelease: toPreRelease, preId, preBase } = parseVersion(nextVersion);
1103
1142
  Object.assign(ctx.pkg, {
@@ -1107,7 +1146,7 @@ var Action = class {
1107
1146
  preBase
1108
1147
  });
1109
1148
  }
1110
- const { pkg: { next }, isIncrement } = ctx;
1149
+ const { pkg: { next } } = ctx;
1111
1150
  if (showRelease) taskEnd(MSG.LOG.SHOW_VERSION(next));
1112
1151
  await runLifeCycleHook(hooks, "before:bump", dryRun);
1113
1152
  await tasks([{
@@ -1117,31 +1156,15 @@ var Action = class {
1117
1156
  await formatTemplate(ctx, config);
1118
1157
  const to = `(${current}...${diff(current, next)})`;
1119
1158
  return success(MSG.TASK.VERSION.END(to), dryRun);
1120
- }
1159
+ },
1160
+ enabled: true
1121
1161
  }]).catch((err) => abortOnError(err, ctx));
1122
1162
  await runLifeCycleHook(hooks, "after:bump", dryRun);
1123
- if (noGit) return;
1124
- const match = isMonorepo ? `${tagPrefix?.(selectedPkg)}*` : "*";
1125
- const { from, to } = isZero(releaseCount) ? {
1126
- from: "",
1127
- to: "HEAD"
1128
- } : await resolveChangelogRange(isIncrement, match);
1129
- const commits = await getLogSince(from, to, getPkgDir(selectedPkg));
1130
- const changelog = await getChangelog({
1131
- getPkgDir: () => getPkgDir(selectedPkg),
1132
- tagPrefix: tagPrefix?.(selectedPkg),
1133
- releaseCount
1134
- });
1135
- Object.assign(ctx.github, { changelog });
1136
- if (commits) {
1137
- msg("GIT", `Changelog(${from}...${to}):${eol(2)}` + changelog);
1138
- msg("GIT", `Commits(${from}...${to}):${eol(2)}` + commits);
1139
- if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1140
- } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1163
+ await this.printChangelog(ctx, config);
1141
1164
  }
1142
1165
  async changelogTask(ctx, config) {
1143
- const { isIncrement, dryRun, selectedPkg, noGit } = ctx;
1144
- const { getPkgDir, changelog: { tagPrefix, releaseCount } } = config;
1166
+ const { isIncrement, dryRun, selectedPkg, noGit, onlyChangelog } = ctx;
1167
+ const { getPkgDir, changelog: { tagPrefix, releaseCount, includeHidden, transformTypes } } = config;
1145
1168
  if (noGit) return;
1146
1169
  await tasks([{
1147
1170
  title: MSG.TASK.CHANGELOG.START,
@@ -1149,7 +1172,9 @@ var Action = class {
1149
1172
  await generateChangelog({
1150
1173
  getPkgDir: () => getPkgDir(selectedPkg),
1151
1174
  tagPrefix: tagPrefix?.(selectedPkg),
1152
- releaseCount
1175
+ releaseCount,
1176
+ includeHidden,
1177
+ transformTypes
1153
1178
  });
1154
1179
  await gitAddAll();
1155
1180
  return success(MSG.TASK.CHANGELOG.END, dryRun);
@@ -1159,6 +1184,7 @@ var Action = class {
1159
1184
  const changeset = await getShortStatus();
1160
1185
  if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredStatus(changeset));
1161
1186
  else msg("GIT", MSG.LOG.CHANGES_EMPTY);
1187
+ if (onlyChangelog) taskEnd(MSG.LOG.ONLY_CHANGELOG);
1162
1188
  }
1163
1189
  async gitTask(ctx, config) {
1164
1190
  const { noGit, dryRun } = ctx;
@@ -1220,7 +1246,7 @@ var Action = class {
1220
1246
  //#endregion
1221
1247
  //#region src/cli.ts
1222
1248
  const pkg = readJsonFile(join(__dirname, "..", "package.json"));
1223
- 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", false).option("-p, --package <pkg>", "Specify package name (Mono-repo CI only).", "").option("--otp <code>", "One-time password for npm publish.", "").option("--prepare", "Prepare a release.", false).option("--ci", "Enable CI mode.", false).option("--show-changelog", "Print changelog and exit.", false).option("--show-release", "Print release version and exit.", false).option("-r, --release-count <count>", "Release count for release.", (value) => Number(value), 1).option("-C, --no-require-clean-working-tree", "Allow releasing with uncommitted changes.", true).option("-m, --is-monorepo", "Mono-repo project", false).option("--skip-git", "Skip all Git-related checks and operations.", false).option("--skip-npm", "Skip all npm-related checks and operations.", false).option("--skip-github", "Skip all GitHub-related checks and operations.", false).helpOption("-h, --help", "Display help information.").action(async (releaseType, options) => {
1249
+ 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", false).option("-m, --is-monorepo", "Mono-repo project", false).option("-p, --package <pkg>", "Specify package name (Mono-repo CI only).", "").option("--otp <code>", "One-time password for npm publish.", "").option("--prepare", "Prepare a release.", false).option("--ci", "Enable CI mode.", false).option("--show-changelog", "Print changelog and exit.", false).option("--show-release", "Print release version and exit.", false).option("-r, --release-count <count>", "Release count for release.", (value) => Number(value), 1).option("--include-hidden", "Include hidden commit types in the changelog.", false).option("--only-changelog", "Only update the changelog.", false).option("-C, --no-require-clean-working-tree", "Allow releasing with uncommitted changes.", true).option("--skip-git", "Skip all Git-related checks and operations.", false).option("--skip-npm", "Skip all npm-related checks and operations.", false).option("--skip-github", "Skip all GitHub-related checks and operations.", false).helpOption("-h, --help", "Display help information.").action(async (releaseType, options) => {
1224
1250
  const { prepare } = options;
1225
1251
  const action = new Action();
1226
1252
  prepare ? await action.handlePrepareRelease(releaseType, options) : await action.handleRelease(releaseType, options);
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DeepPartial, DeepRequired } from "@peiyanlu/ts-utils";
2
2
  import { ConventionalChangelog } from "conventional-changelog";
3
+ import { CommitType } from "conventional-changelog-conventionalcommits";
3
4
  import { log } from "@clack/prompts";
4
5
 
5
6
  //#region src/types.d.ts
@@ -36,6 +37,14 @@ interface ReleaseConfig {
36
37
  * 生成多少个版本的变更日志,详见 {@link Options}
37
38
  */
38
39
  releaseCount: number;
40
+ /**
41
+ * 生成 CHANGELOG 时包含默认隐藏的 commit 类型
42
+ */
43
+ includeHidden: boolean;
44
+ /**
45
+ * 对默认 commit types 进行转换
46
+ */
47
+ transformTypes: (types: CommitType[]) => CommitType[];
39
48
  };
40
49
  /** 跳过 git、github 环境检查以及后续操作 */
41
50
  skipGit: boolean;
@@ -179,21 +188,15 @@ interface GenerateOptions {
179
188
  /** 生成多少个变更日志,详见 {@link Options} */
180
189
  releaseCount?: number;
181
190
  }
182
- declare const createGenerator: ({
183
- getPkgDir,
184
- tagPrefix,
185
- releaseCount
186
- }: GenerateOptions) => Promise<ConventionalChangelog>;
187
- declare const getChangelog: ({
188
- getPkgDir,
189
- tagPrefix,
190
- releaseCount
191
- }: GenerateOptions) => Promise<string>;
192
- declare const generateChangelog: ({
193
- getPkgDir,
194
- tagPrefix,
195
- releaseCount
196
- }: GenerateOptions) => Promise<void>;
191
+ interface ParsePresetOptions {
192
+ /** 生成 CHANGELOG 时包含默认隐藏的 commit 类型 */
193
+ includeHidden?: boolean;
194
+ /** 对默认 commit types 进行转换 */
195
+ transformTypes?: (types: CommitType[]) => CommitType[];
196
+ }
197
+ declare const createGenerator: (options: GenerateOptions & ParsePresetOptions) => Promise<ConventionalChangelog>;
198
+ declare const getChangelog: (options: GenerateOptions & ParsePresetOptions) => Promise<string>;
199
+ declare const generateChangelog: (options: GenerateOptions & ParsePresetOptions) => Promise<void>;
197
200
  //#endregion
198
201
  //#region src/npm/publish.d.ts
199
202
  interface PublishTagOptions {
package/dist/index.mjs CHANGED
@@ -4,8 +4,10 @@ import { join } from "node:path";
4
4
  import { eol, getPackageInfo, publishPackage, resolvePublishTag } from "@peiyanlu/cli-utils";
5
5
  import { ConventionalChangelog } from "conventional-changelog";
6
6
  import createPreset from "conventional-changelog-conventionalcommits";
7
+ import "conventional-recommended-bump";
7
8
  import { writeFile } from "node:fs/promises";
8
9
  import { finished } from "node:stream/promises";
10
+ import { bold, compareUrl, each, heading, link, list, newline, segments, small, words } from "@conventional-changelog/template";
9
11
  import { blue, cyan, green, magenta, red, underline, yellow } from "ansis";
10
12
  //#region src/config.ts
11
13
  function defineConfig(config) {
@@ -31,179 +33,168 @@ const defaultTypes = [
31
33
  {
32
34
  type: "feat",
33
35
  section: "✨ 新功能",
36
+ effect: "bump",
34
37
  description: "新增功能"
35
38
  },
36
39
  {
37
40
  type: "feature",
38
41
  section: "✨ 新功能",
42
+ effect: "bump",
39
43
  description: "新增功能"
40
44
  },
41
45
  {
42
46
  type: "fix",
43
47
  section: "🐛 Bug 修复",
48
+ effect: "bump",
44
49
  description: "修复 bug"
45
50
  },
46
51
  {
47
52
  type: "perf",
48
53
  section: "⚡ 性能优化",
54
+ effect: "bump",
49
55
  description: "提升性能"
50
56
  },
51
57
  {
52
58
  type: "revert",
53
59
  section: "⏪ 回退",
60
+ effect: "bump",
54
61
  description: "回退到之前版本"
55
62
  },
56
63
  {
57
64
  type: "docs",
58
65
  section: "📝 文档",
66
+ effect: "changelog",
59
67
  description: "文档更新"
60
68
  },
61
69
  {
62
70
  type: "style",
63
71
  section: "💄 样式调整",
72
+ effect: "hidden",
64
73
  description: "样式或格式修改"
65
74
  },
66
75
  {
67
76
  type: "chore",
68
77
  section: "🧹 其他更新",
78
+ effect: "hidden",
69
79
  description: "非功能性更改"
70
80
  },
71
81
  {
72
82
  type: "refactor",
73
83
  section: "♻️ 代码重构",
84
+ effect: "changelog",
74
85
  description: "重构代码"
75
86
  },
76
87
  {
77
88
  type: "test",
78
89
  section: "🧪 测试",
90
+ effect: "hidden",
79
91
  description: "测试更新"
80
92
  },
81
93
  {
82
94
  type: "build",
83
95
  section: "🏗️ 构建系统",
96
+ effect: "hidden",
84
97
  description: "构建工具和系统修改"
85
98
  },
86
99
  {
87
100
  type: "ci",
88
101
  section: "🤖 CI",
102
+ effect: "hidden",
89
103
  description: "持续集成配置"
90
104
  },
91
- {
92
- type: "config",
93
- section: "⚙️ 配置",
94
- description: "配置文件更新"
95
- },
96
105
  {
97
106
  type: "deps",
98
107
  section: "📦 依赖更新",
108
+ effect: "changelog",
99
109
  description: "依赖版本变更"
100
110
  },
101
111
  {
102
112
  type: "security",
103
113
  section: "🛡️ 安全修复",
114
+ effect: "changelog",
104
115
  description: "安全性更新"
105
116
  },
117
+ {
118
+ type: "hotfix",
119
+ section: "🚑 热修复",
120
+ effect: "changelog",
121
+ description: "紧急修复"
122
+ },
106
123
  {
107
124
  type: "i18n",
108
125
  section: "🌐 国际化",
126
+ effect: "changelog",
109
127
  description: "国际化相关更新"
110
128
  },
111
129
  {
112
130
  type: "ux",
113
131
  section: "🎨 用户体验",
132
+ effect: "changelog",
114
133
  description: "用户体验改进"
115
134
  },
116
135
  {
117
- type: "hotfix",
118
- section: "🚑 热修复",
119
- description: "紧急修复"
136
+ type: "config",
137
+ section: "⚙️ 配置",
138
+ effect: "hidden",
139
+ description: "配置文件更新"
120
140
  },
121
141
  {
122
142
  type: "tool",
123
143
  section: "🛠️ 工具链",
144
+ effect: "hidden",
124
145
  description: "工具链变更"
125
146
  }
126
147
  ];
127
148
  //#endregion
149
+ //#region src/git/templates.ts
150
+ const isLikelyHash = (value) => /^[0-9a-f]{7,40}$/i.test(value);
151
+ function headerPartial(context) {
152
+ const { isPatch, linkCompare, version, title, date, previousTag } = context;
153
+ const fullTitle = words(linkCompare && !isLikelyHash(previousTag) ? link(version, compareUrl(context)) : version, title && `"${title}"`, date && `(${date})`);
154
+ return heading(2, isPatch ? small(fullTitle) : fullTitle);
155
+ }
156
+ function template(context) {
157
+ const { headerPartial, preamblePartial, commitPartial, footerPartial, noteGroups, commitGroups } = context;
158
+ return segments(headerPartial(context), preamblePartial(context), each(noteGroups, (group) => segments(heading(3, words("⚠", group.title)), list(group.notes, (note) => {
159
+ return each([words(note.commit.scope && bold(`${note.commit.scope}:`), commitPartial(context, note.commit)), note.commit.subject.trim() !== note.text.trim() ? list([note.text], (t) => t) : ""], (t) => t, newline());
160
+ })), newline(2)), each(commitGroups, (group) => segments(group.title && heading(3, group.title), list(group.commits, (commit) => commitPartial(context, commit))), newline(2)), footerPartial(context));
161
+ }
162
+ //#endregion
128
163
  //#region src/git/changelog.ts
129
- const parsePreset = async () => {
130
- const preset = await createPreset({ types: defaultTypes.map((t) => ({
131
- ...t,
132
- hidden: false
133
- })) });
164
+ const parsePreset = (options = {}) => {
165
+ const { includeHidden, transformTypes } = options;
166
+ let types = [...defaultTypes];
167
+ if (includeHidden) types.forEach((type) => {
168
+ if (type.effect === "hidden") type.effect = "changelog";
169
+ });
170
+ types = transformTypes?.(types) ?? types;
171
+ const preset = createPreset({ types });
134
172
  preset.writer ??= {};
135
- preset.writer.headerPartial = `## {{#if isPatch~}} <small> {{~/if~}}
136
- {{#if @root.linkCompare~}}
137
- [{{version}}](
138
- {{~#if @root.repository~}}
139
- {{~#if @root.host}}
140
- {{~@root.host}}/
141
- {{~/if}}
142
- {{~#if @root.owner}}
143
- {{~@root.owner}}/
144
- {{~/if}}
145
- {{~@root.repository}}
146
- {{~else}}
147
- {{~@root.repoUrl}}
148
- {{~/if~}}
149
- /compare/{{previousTag}}...{{currentTag}})
150
- {{~else}}
151
- {{~version}}
152
- {{~/if}}
153
- {{~#if title}} "{{title}}"
154
- {{~/if}}
155
- {{~#if date}} ({{date}})
156
- {{~/if}}
157
- {{~#if isPatch~}} </small> {{~/if}}
158
- `.trim() + eol();
159
- preset.writer.mainTemplate = `{{> header}}
160
- {{#if noteGroups}}
161
- {{#each noteGroups}}
162
-
163
- ### ⚠ {{title}}
164
-
165
- {{#each notes}}
166
- * {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{commit.subject}} {{#if commit.hash}}([{{commit.shortHash}}](https://github.com/{{@root.owner}}/{{@root.repository}}/commit/{{commit.hash}})){{/if}}
167
- {{/each}}
168
- {{/each}}
169
- {{/if}}
170
- {{#each commitGroups}}
171
-
172
- {{#if title}}
173
- ### {{title}}
174
-
175
- {{/if}}
176
- {{#each commits}}
177
- {{> commit root=@root}}
178
- {{/each}}
179
- {{/each}}`.trim();
173
+ preset.writer.headerPartial = headerPartial;
174
+ preset.writer.template = template;
180
175
  return preset;
181
176
  };
182
- const createGenerator = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
177
+ const createGenerator = async (options) => {
178
+ const { getPkgDir, tagPrefix, releaseCount = 1, includeHidden, transformTypes } = options;
183
179
  const pkgDir = getPkgDir();
184
- const preset = await parsePreset();
185
- return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
180
+ return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(parsePreset({
181
+ includeHidden,
182
+ transformTypes
183
+ })).options({ releaseCount }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
186
184
  };
187
- const getChangelog = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
188
- const generator = await createGenerator({
189
- getPkgDir,
190
- tagPrefix,
191
- releaseCount
192
- });
185
+ const getChangelog = async (options) => {
186
+ const generator = await createGenerator(options);
193
187
  let changelog = "";
194
188
  for await (const chunk of generator.write()) changelog += chunk;
195
189
  return changelog.trimEnd();
196
190
  };
197
- const generateChangelog = async ({ getPkgDir, tagPrefix, releaseCount = 1 }) => {
191
+ const generateChangelog = async (options) => {
192
+ const { getPkgDir, releaseCount = 1 } = options;
198
193
  const infile = join(getPkgDir(), "CHANGELOG.md");
199
194
  if (!existsSync(infile)) await writeFile(infile, "");
200
195
  const exist = readFileSync(infile, "utf-8");
201
196
  const writeStream = createWriteStream(infile);
202
- const generator = await createGenerator({
203
- getPkgDir,
204
- tagPrefix,
205
- releaseCount
206
- });
197
+ const generator = await createGenerator(options);
207
198
  for await (const chunk of generator.write()) writeStream.write(chunk);
208
199
  const override = isZero(releaseCount);
209
200
  if (!(isZero(exist.trim().length) || override)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peiyanlu/release",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Release project tool.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,9 +34,10 @@
34
34
  "@peiyanlu/ts-utils": "^0.0.8",
35
35
  "ansis": "^4.3.1",
36
36
  "commander": "^15.0.0",
37
- "conventional-changelog": "^7.2.1",
38
- "conventional-changelog-conventionalcommits": "^9.3.1",
39
- "conventional-changelog-writer": "^8.3.0",
37
+ "conventional-changelog": "^8.0.0",
38
+ "conventional-changelog-conventionalcommits": "^10.2.0",
39
+ "conventional-recommended-bump": "^12.0.0",
40
+ "@conventional-changelog/template": "^1.2.0",
40
41
  "mime-types": "^3.0.2",
41
42
  "open": "^11.0.0",
42
43
  "publint": "^0.3.21",