@peiyanlu/release 0.0.5 → 0.0.6

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) $YEAR $OWNER
3
+ Copyright (c) 2026 离殇
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/cli.mjs CHANGED
@@ -1,24 +1,24 @@
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, isWorkingDirClean, parseGitHubRepo, pingRegistry, publishPackage, pushBranch, pushTag, readJsonFile, resolveChangelogRange, runGit } from "@peiyanlu/cli-utils";
3
+ import { bumpPackageVersion, coloredChangeset, eol, execAsync, getAllRemotes, getAuthenticatedUser, getCurrentBranch, getGithubReleaseUrl, getGithubUrl, getLatestTag, getLog, getPackageInfo, getPackageUrl, getPublishedVersion, getRemote, getStatus, gitAddAll, gitAddTracked, gitCommit, gitTagAnnotated, hasWriteAccess, isGitRepo, isWorkingDirClean, parseGitHubRepo, pingRegistry, publishPackage, pushBranch, pushTag, readJsonFile, resolveChangelogRange } 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
8
  import { publint } from "publint";
9
9
  import { formatMessage } from "publint/utils";
10
- import { coerce, eq, inc, lt, parse, prerelease, valid } from "semver";
10
+ import { coerce, inc, lt, neq, parse, prerelease, valid } from "semver";
11
11
  import { castArray, isPlainObject, isUndefined } from "@peiyanlu/ts-utils";
12
12
  import { spawnSync } from "node:child_process";
13
- import { Octokit } from "@octokit/rest";
14
- import { contentType } from "mime-types";
15
- import open from "open";
16
- import { glob } from "tinyglobby";
17
13
  import { inspect } from "node:util";
18
14
  import { ConventionalChangelog } from "conventional-changelog";
19
15
  import createPreset from "conventional-changelog-conventionalcommits";
20
16
  import { writeFile } from "node:fs/promises";
21
17
  import { finished } from "node:stream/promises";
18
+ import { Octokit } from "@octokit/rest";
19
+ import { contentType } from "mime-types";
20
+ import open from "open";
21
+ import { glob } from "tinyglobby";
22
22
 
23
23
  //#region node_modules/.pnpm/tsdown@0.18.4_publint@0.3.17_typescript@5.9.3/node_modules/tsdown/esm-shims.js
24
24
  const getFilename = () => fileURLToPath(import.meta.url);
@@ -79,6 +79,7 @@ const t = () => green(`${Math.floor(process.uptime())}s`);
79
79
  const MSG = {
80
80
  INTRO: (dryRun) => `🚀 Starting release ${n(dryRun)}`,
81
81
  OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}`,
82
+ OUTRO_PREPARE: (dryRun) => `🎉 PrepareRelease finished successfully in ${t()} ${n(dryRun)}`,
82
83
  LOG: {
83
84
  SHOW_VERSION: (version) => `🎉 Released ${green(version)}`,
84
85
  SHOW_CHANGELOG: "🎉 Changelog collected",
@@ -93,6 +94,10 @@ const MSG = {
93
94
  NPM: {
94
95
  CHECKING: "Checking npm registry",
95
96
  CHECKED: (registry, msg$1) => `NPM registry: ${underline(dim(registry))} ${underline(dim(msg$1))}`
97
+ },
98
+ GITHUB: {
99
+ CHECKING: "Checking github repository",
100
+ CHECKED: (repository, msg$1) => `GitHub repository: ${underline(dim(repository))} ${underline(dim(msg$1))}`
96
101
  }
97
102
  },
98
103
  TASK: {
@@ -151,123 +156,6 @@ const MSG = {
151
156
  }
152
157
  };
153
158
 
154
- //#endregion
155
- //#region src/github/release.ts
156
- const truncateBody = (body) => {
157
- if (body && body.length >= 124e3) return body.substring(0, 124e3) + "...";
158
- return body;
159
- };
160
- const uploadAssets = async (ctx, config) => {
161
- const { github: { assets } } = config;
162
- const { github: { isReleased, owner, repo, uploadUrl, releaseId, token }, pkg: { name } } = ctx;
163
- if (!assets?.length || !isReleased) return;
164
- const octokit = new Octokit({ auth: token });
165
- const files = await glob(assets);
166
- await Promise.all(files.map((file) => {
167
- return octokit.repos.uploadReleaseAsset({
168
- owner,
169
- repo,
170
- name,
171
- url: uploadUrl,
172
- release_id: releaseId,
173
- data: createReadStream(file),
174
- headers: {
175
- "content-type": contentType(file) || "application/octet-stream",
176
- "content-length": statSync(file).size
177
- }
178
- });
179
- }));
180
- };
181
- const createWebRelease = async (ctx, config) => {
182
- const { pkg: { toPreRelease }, github: { owner, repo, changelog, releaseName }, git: { currentTag } } = ctx;
183
- const { github: { prerelease: prerelease$1 } } = config;
184
- const github = getGithubUrl(owner, repo);
185
- const url = new URL(`${github}/releases/new`);
186
- url.searchParams.set("tag", currentTag);
187
- url.searchParams.set("title", releaseName);
188
- url.searchParams.set("body", truncateBody(changelog));
189
- url.searchParams.set("prelease", String(toPreRelease || prerelease$1));
190
- const isWindows = process.platform === "win32";
191
- await open(url.toString(), { wait: isWindows });
192
- };
193
- const createCiRelease = async (ctx, config) => {
194
- const { pkg: { toPreRelease }, github: { owner, repo, token, changelog, releaseName }, git: { currentTag } } = ctx;
195
- const { github: { prerelease: prerelease$1, draft, autoGenerate } } = config;
196
- const octokit = new Octokit({ auth: token });
197
- try {
198
- const { data } = await octokit.repos.createRelease({
199
- owner,
200
- repo,
201
- tag_name: currentTag,
202
- target_commitish: void 0,
203
- name: releaseName,
204
- body: autoGenerate ? "" : truncateBody(changelog),
205
- draft: draft ?? false,
206
- prerelease: (toPreRelease || prerelease$1) ?? false,
207
- discussion_category_name: void 0,
208
- generate_release_notes: autoGenerate,
209
- make_latest: "true"
210
- });
211
- const { html_url, upload_url, id, discussion_url } = data;
212
- Object.assign(ctx.github, {
213
- isReleased: true,
214
- releaseId: id,
215
- releaseUrl: html_url,
216
- uploadUrl: upload_url,
217
- discussionUrl: discussion_url
218
- });
219
- } catch (e) {
220
- if (e?.status === 422) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(currentTag));
221
- throw e;
222
- }
223
- };
224
- const parseGithubUrl = (ctx) => {
225
- const { git: { remoteUrl } } = ctx;
226
- const [owner, repo] = parseGitHubRepo(remoteUrl);
227
- Object.assign(ctx.github, {
228
- owner,
229
- repo
230
- });
231
- };
232
- const createRelease = async (ctx, config) => {
233
- const { github: { owner, repo }, isCI } = ctx;
234
- const { github: { tokenRef, release, skipChecks } } = config;
235
- if (!release) return;
236
- const token = tokenRef ? process.env[tokenRef] : process.env.GITHUB_TOKEN;
237
- if (!token) {
238
- Object.assign(ctx.github, { isWeb: true });
239
- if (isCI && process.env.GITHUB_ACTIONS) throw new Error(MSG.ERROR.GITHUB_TOKEN(tokenRef));
240
- }
241
- Object.assign(ctx.github, { token });
242
- const octokit = new Octokit({ auth: token });
243
- if (token && !skipChecks) if (process.env.GITHUB_ACTIONS) Object.assign(ctx.github, { username: process.env.GITHUB_ACTOR });
244
- else {
245
- try {
246
- const { data: { login: username } } = await octokit.users.getAuthenticated();
247
- Object.assign(ctx.github, { username });
248
- } catch (e) {
249
- throw new Error(MSG.ERROR.GITHUB_AUTH);
250
- }
251
- try {
252
- const { github: { username } } = ctx;
253
- await octokit.repos.checkCollaborator({
254
- owner,
255
- repo,
256
- username
257
- });
258
- } catch (e) {
259
- throw new Error(MSG.ERROR.GITHUB_USER);
260
- }
261
- }
262
- const { github: { isWeb }, dryRun } = ctx;
263
- if (dryRun) return true;
264
- if (isWeb) await createWebRelease(ctx, config);
265
- else if (isCI) {
266
- await createCiRelease(ctx, config);
267
- await uploadAssets(ctx, config);
268
- }
269
- };
270
-
271
159
  //#endregion
272
160
  //#region src/git/commit.ts
273
161
  const gitCheck = async (ctx, config) => {
@@ -289,7 +177,11 @@ const gitCheck = async (ctx, config) => {
289
177
  remoteName,
290
178
  remoteUrl
291
179
  });
292
- parseGithubUrl(ctx);
180
+ const [owner, repo] = parseGitHubRepo(remoteUrl);
181
+ Object.assign(ctx.github, {
182
+ owner,
183
+ repo
184
+ });
293
185
  };
294
186
  const commitAndTag = async (ctx, config) => {
295
187
  const { git: { remoteName, currentTag, commitMessage, tagMessage }, dryRun } = ctx;
@@ -428,7 +320,10 @@ const parseVersion = (raw) => {
428
320
  };
429
321
  };
430
322
  const getCIVersion = (current) => {
431
- return inc(current, Release.PATCH) ?? "";
323
+ return inc(current, Release.PATCH);
324
+ };
325
+ const isNeq = (current, next) => {
326
+ return neq(current, next);
432
327
  };
433
328
 
434
329
  //#endregion
@@ -437,6 +332,7 @@ let Release = /* @__PURE__ */ function(Release$1) {
437
332
  Release$1["MAJOR"] = "major";
438
333
  Release$1["MINOR"] = "minor";
439
334
  Release$1["PATCH"] = "patch";
335
+ Release$1["RELEASE"] = "release";
440
336
  return Release$1;
441
337
  }({});
442
338
  let PreRelease = /* @__PURE__ */ function(PreRelease$1) {
@@ -456,76 +352,45 @@ let PreId = /* @__PURE__ */ function(PreId$1) {
456
352
  PreId$1["DEV"] = "dev";
457
353
  return PreId$1;
458
354
  }({});
355
+ const createVersionMap = (current) => ({
356
+ patch: inc(current, Release.PATCH),
357
+ minor: inc(current, Release.MINOR),
358
+ major: inc(current, Release.MAJOR),
359
+ release: inc(current, Release.RELEASE),
360
+ prerelease: inc(current, PreRelease.RELEASE),
361
+ prepatch: inc(current, PreRelease.PATCH, PreId.BETA),
362
+ preminor: inc(current, PreRelease.MINOR, PreId.BETA),
363
+ premajor: inc(current, PreRelease.MAJOR, PreId.BETA)
364
+ });
365
+ const option = (label, value, hint) => ({
366
+ label,
367
+ value,
368
+ hint: hint ?? value
369
+ });
459
370
  const runVersionPrompts = async (ctx, config) => {
460
371
  const { pkg: { current, fromPreRelease } } = ctx;
461
372
  const selectVersion = async () => {
462
- const patch = inc(current, Release.PATCH);
463
- const minor = inc(current, Release.MINOR);
464
- const major = inc(current, Release.MAJOR);
465
- const prerelease$1 = inc(current, PreRelease.RELEASE);
466
- const prepatch = inc(current, PreRelease.PATCH, PreId.BETA);
467
- const preminor = inc(current, PreRelease.MINOR, PreId.BETA);
468
- const premajor = inc(current, PreRelease.MAJOR, PreId.BETA);
469
- const pr = [{
470
- label: "Pre-Release",
471
- value: prerelease$1,
472
- hint: prerelease$1
473
- }];
474
- const pre = [
475
- {
476
- label: "Pre-Patch",
477
- value: prepatch,
478
- hint: prepatch
479
- },
480
- {
481
- label: "Pre-Minor",
482
- value: preminor,
483
- hint: preminor
484
- },
485
- {
486
- label: "Pre-Major",
487
- value: premajor,
488
- hint: premajor
489
- }
490
- ];
491
- const release = [
492
- {
493
- label: "Patch",
494
- value: patch,
495
- hint: patch
496
- },
497
- {
498
- label: "Minor",
499
- value: minor,
500
- hint: minor
501
- },
502
- {
503
- label: "Major",
504
- value: major,
505
- hint: major
506
- }
373
+ const versions = createVersionMap(current);
374
+ const options = [
375
+ ...fromPreRelease ? [option("Pre-Release", "prerelease", versions.prerelease)] : [],
376
+ option("Patch", "patch", versions.patch),
377
+ option("Minor", "minor", versions.minor),
378
+ option("Major", "major", versions.major),
379
+ ...!fromPreRelease ? [
380
+ option("Pre-Patch", "prepatch", versions.prepatch),
381
+ option("Pre-Minor", "preminor", versions.preminor),
382
+ option("Pre-Major", "premajor", versions.premajor)
383
+ ] : [],
384
+ option("As-Is", "ignore", current),
385
+ option("Custom", "custom", "custom specified")
507
386
  ];
508
- const custom = [{
509
- label: "As-Is",
510
- value: "ignore",
511
- hint: current
512
- }, {
513
- label: "Custom",
514
- value: "custom",
515
- hint: "custom specified"
516
- }];
517
- const version = await select({
387
+ const type = await select({
518
388
  message: question(MSG.PROMPT.SELECT_VERSION, "version"),
519
- options: [
520
- ...fromPreRelease ? pr : [],
521
- ...release,
522
- ...fromPreRelease ? [] : pre,
523
- ...custom
524
- ],
525
- initialValue: fromPreRelease ? prerelease$1 : patch
389
+ options,
390
+ initialValue: fromPreRelease ? "prerelease" : "patch"
526
391
  });
527
- abortSinglePrompt(version);
528
- return version;
392
+ abortSinglePrompt(type);
393
+ return type;
529
394
  };
530
395
  const inputVersion = async () => {
531
396
  const version = await text({
@@ -538,10 +403,12 @@ const runVersionPrompts = async (ctx, config) => {
538
403
  abortSinglePrompt(version);
539
404
  return version;
540
405
  };
541
- const type = await selectVersion();
542
- const next = type === "custom" ? await inputVersion() : type === "ignore" ? current : type;
543
- ctx.isIncrement = !eq(next, current);
544
- return { next };
406
+ const resolveNextVersion = async (type, current$1) => {
407
+ if (type === "ignore") return current$1;
408
+ if (type === "custom") return inputVersion();
409
+ return createVersionMap(current$1)[type];
410
+ };
411
+ return await resolveNextVersion(await selectVersion(), current);
545
412
  };
546
413
 
547
414
  //#endregion
@@ -644,8 +511,7 @@ const createDefaultConfig = (isCI) => {
644
511
  },
645
512
  npm: {
646
513
  publish: isCI,
647
- publishPath: ".",
648
- pushArgs: [],
514
+ publishArgs: [],
649
515
  skipChecks: false
650
516
  },
651
517
  github: {
@@ -758,11 +624,8 @@ const defaultTypes = [
758
624
 
759
625
  //#endregion
760
626
  //#region src/git/changelog.ts
761
- const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
627
+ const createGenerator = async ({ getPkgDir, tagPrefix }) => {
762
628
  const pkgDir = getPkgDir();
763
- const infile = join(pkgDir, "CHANGELOG.md");
764
- if (!existsSync(infile)) await writeFile(infile, "");
765
- const originalChangelog = readFileSync(infile, "utf-8");
766
629
  const preset = await createPreset({ types: defaultTypes.map((t$1) => ({
767
630
  ...t$1,
768
631
  hidden: false
@@ -815,17 +678,30 @@ const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
815
678
  {{> commit root=@root}}
816
679
  {{/each}}
817
680
  {{/each}}`.trim() + eol(2);
818
- const generator = new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
819
- const writeStream = createWriteStream(infile);
681
+ return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
682
+ };
683
+ const getChangelog = async ({ getPkgDir, tagPrefix }) => {
684
+ const generator = await createGenerator({
685
+ getPkgDir,
686
+ tagPrefix
687
+ });
820
688
  let changelog = "";
821
- for await (const chunk of generator.write()) {
822
- changelog += chunk;
823
- writeStream.write(chunk);
824
- }
689
+ for await (const chunk of generator.write()) changelog += chunk;
690
+ return changelog;
691
+ };
692
+ const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
693
+ const infile = join(getPkgDir(), "CHANGELOG.md");
694
+ if (!existsSync(infile)) await writeFile(infile, "");
695
+ const originalChangelog = readFileSync(infile, "utf-8");
696
+ const generator = await createGenerator({
697
+ getPkgDir,
698
+ tagPrefix
699
+ });
700
+ const writeStream = createWriteStream(infile);
701
+ for await (const chunk of generator.write()) writeStream.write(chunk);
825
702
  writeStream.write(originalChangelog);
826
703
  writeStream.end();
827
704
  await finished(writeStream);
828
- return changelog;
829
705
  };
830
706
 
831
707
  //#endregion
@@ -887,6 +763,172 @@ const runGithubPrompts = async (ctx, config) => {
887
763
  return res;
888
764
  };
889
765
 
766
+ //#endregion
767
+ //#region src/github/release.ts
768
+ const truncateBody = (body) => {
769
+ if (body && body.length >= 124e3) return body.substring(0, 124e3) + "...";
770
+ return body;
771
+ };
772
+ const getReleaseByTagSafe = async (octokit, options) => {
773
+ try {
774
+ const { data } = await octokit.repos.getReleaseByTag({ ...options });
775
+ return data;
776
+ } catch (e) {
777
+ if (e?.status === 404) return null;
778
+ throw e;
779
+ }
780
+ };
781
+ const createCRelease = async (octokit, options) => {
782
+ const { owner, repo, tag, name, body, draft, prerelease: prerelease$1, generateReleaseNotes, makeLatest, targetCommitish, discussionCategoryName } = options;
783
+ const { data } = await octokit.repos.createRelease({
784
+ owner,
785
+ repo,
786
+ tag_name: tag,
787
+ name,
788
+ body,
789
+ draft,
790
+ prerelease: prerelease$1,
791
+ generate_release_notes: generateReleaseNotes,
792
+ make_latest: makeLatest,
793
+ target_commitish: targetCommitish,
794
+ discussion_category_name: discussionCategoryName
795
+ });
796
+ return data;
797
+ };
798
+ const updateCRelease = async (octokit, options) => {
799
+ const { owner, repo, tag, name, body, draft, prerelease: prerelease$1, generateReleaseNotes, makeLatest, targetCommitish, discussionCategoryName, releaseId } = options;
800
+ const { data } = await octokit.repos.updateRelease({
801
+ owner,
802
+ repo,
803
+ release_id: releaseId,
804
+ tag_name: tag,
805
+ name,
806
+ body,
807
+ draft,
808
+ prerelease: prerelease$1,
809
+ generate_release_notes: generateReleaseNotes,
810
+ make_latest: makeLatest,
811
+ target_commitish: targetCommitish,
812
+ discussion_category_name: discussionCategoryName
813
+ });
814
+ return data;
815
+ };
816
+ const createCReleaseByTag = async (octokit, options) => {
817
+ const existing = await getReleaseByTagSafe(octokit, options);
818
+ if (!existing) return createCRelease(octokit, { ...options });
819
+ const { draft, id: releaseId } = existing;
820
+ if (!draft) throw new Error(`Release for tag "${options.tag}" already exists and is not a draft`);
821
+ return updateCRelease(octokit, {
822
+ releaseId,
823
+ ...options
824
+ });
825
+ };
826
+ const uploadAssets = async (octokit, options) => {
827
+ const { owner, repo, name, uploadUrl, releaseId, assets } = options;
828
+ const files = await glob(assets);
829
+ await Promise.all(files.map((file) => {
830
+ return octokit.repos.uploadReleaseAsset({
831
+ owner,
832
+ repo,
833
+ name,
834
+ url: uploadUrl,
835
+ release_id: releaseId,
836
+ data: createReadStream(file),
837
+ headers: {
838
+ "content-type": contentType(file) || "application/octet-stream",
839
+ "content-length": statSync(file).size
840
+ }
841
+ });
842
+ }));
843
+ };
844
+ const createWebRelease = async (options) => {
845
+ const { owner, repo, tag, title, body, prerelease: prerelease$1 } = options;
846
+ const url = new URL(`${getGithubUrl(owner, repo)}/releases/new`);
847
+ Object.entries({
848
+ tag,
849
+ title,
850
+ body,
851
+ prerelease: prerelease$1
852
+ }).forEach(([key, value]) => {
853
+ url.searchParams.set(key, value);
854
+ });
855
+ const isWindows = process.platform === "win32";
856
+ await open(url.toString(), { wait: isWindows });
857
+ };
858
+ const githubCheck = async (ctx, config) => {
859
+ const { github: { owner, repo }, isCI } = ctx;
860
+ const { github: { tokenRef, skipChecks } } = config;
861
+ const token = tokenRef ? process.env[tokenRef] : process.env.GITHUB_TOKEN;
862
+ if (!token) {
863
+ Object.assign(ctx.github, { isWeb: true });
864
+ if (isCI && process.env.GITHUB_ACTIONS) throw new Error(MSG.ERROR.GITHUB_TOKEN(tokenRef));
865
+ return "";
866
+ }
867
+ Object.assign(ctx.github, { token });
868
+ if (skipChecks) return "";
869
+ let username = "";
870
+ if (process.env.GITHUB_ACTIONS) username = process.env.GITHUB_ACTOR;
871
+ else {
872
+ const octokit = new Octokit({ auth: token });
873
+ try {
874
+ const { data: { login } } = await octokit.users.getAuthenticated();
875
+ username = login;
876
+ } catch (e) {
877
+ throw new Error(MSG.ERROR.GITHUB_AUTH);
878
+ }
879
+ try {
880
+ await octokit.repos.checkCollaborator({
881
+ owner,
882
+ repo,
883
+ username
884
+ });
885
+ } catch (e) {
886
+ throw new Error(MSG.ERROR.GITHUB_USER);
887
+ }
888
+ }
889
+ return username;
890
+ };
891
+ const createRelease = async (ctx, config) => {
892
+ const { pkg: { name, toPreRelease }, git: { currentTag }, isCI, dryRun } = ctx;
893
+ const { github: { isWeb, token, owner, repo, changelog, releaseName } } = ctx;
894
+ const { github: { release, assets, prerelease: prerelease$1, autoGenerate, draft } } = config;
895
+ if (!release || dryRun) return;
896
+ const octokit = new Octokit({ auth: token });
897
+ if (isWeb) await createWebRelease({
898
+ owner,
899
+ repo,
900
+ tag: currentTag,
901
+ title: releaseName,
902
+ body: truncateBody(changelog),
903
+ prerelease: String(toPreRelease || prerelease$1)
904
+ });
905
+ else if (isCI) {
906
+ const res = await createCReleaseByTag(octokit, {
907
+ owner,
908
+ repo,
909
+ tag: currentTag,
910
+ name: releaseName,
911
+ body: autoGenerate ? "" : truncateBody(changelog),
912
+ draft,
913
+ prerelease: toPreRelease || prerelease$1,
914
+ generateReleaseNotes: autoGenerate,
915
+ makeLatest: "true"
916
+ });
917
+ if (!res) return;
918
+ if (assets.length) {
919
+ const { id: releaseId, upload_url: uploadUrl } = res;
920
+ await uploadAssets(octokit, {
921
+ owner,
922
+ repo,
923
+ name,
924
+ uploadUrl,
925
+ releaseId,
926
+ assets
927
+ });
928
+ }
929
+ }
930
+ };
931
+
890
932
  //#endregion
891
933
  //#region src/npm/prompts.ts
892
934
  const runNpmPublishPrompts = async (ctx, config) => {
@@ -922,10 +964,10 @@ const runNpmOptPrompts = async (ctx, config) => {
922
964
  //#region src/npm/publish.ts
923
965
  const resolvePublishTag = async (pkgName, version) => {
924
966
  const { toPreRelease, preId } = parseVersion(version);
925
- if (!toPreRelease) {
926
- const active = await getPublishedVersion(pkgName);
927
- return active && lt(version, active) ? "previous" : "latest";
928
- } else return preId || "prerelease";
967
+ if (toPreRelease) return preId || "next";
968
+ const active = await getPublishedVersion(pkgName);
969
+ if (!active) return "latest";
970
+ return lt(version, active) ? "previous" : "latest";
929
971
  };
930
972
  const isOtpError = (err) => err instanceof Error && /one-time password|otp/i.test(err.message);
931
973
  const npmCheck = async (ctx, config) => {
@@ -946,7 +988,7 @@ const npmCheck = async (ctx, config) => {
946
988
  };
947
989
  const publishNpm = async (ctx, config) => {
948
990
  const { dryRun, selectedPkg, npm: { otp, tag } } = ctx;
949
- const { npm: { publish, pushArgs }, getPkgDir } = config;
991
+ const { npm: { publish, publishArgs }, getPkgDir } = config;
950
992
  if (!publish) return;
951
993
  const otpArgs = otp ? ["--otp", otp] : [];
952
994
  const dryRunArg = dryRun ? ["--dry-run"] : [];
@@ -954,7 +996,7 @@ const publishNpm = async (ctx, config) => {
954
996
  tag,
955
997
  args: [
956
998
  ...otpArgs,
957
- ...pushArgs,
999
+ ...publishArgs,
958
1000
  ...dryRunArg
959
1001
  ],
960
1002
  cwd: getPkgDir(selectedPkg)
@@ -964,7 +1006,27 @@ const publishNpm = async (ctx, config) => {
964
1006
  //#endregion
965
1007
  //#region src/action.ts
966
1008
  var Action = class {
967
- async handle(cmdArgs, options) {
1009
+ async handleRelease(cmdArgs, options) {
1010
+ const { ctx, config } = await this.createContext(cmdArgs, options);
1011
+ await this.checkTask(ctx, config);
1012
+ await this.bumpTask(ctx, config);
1013
+ await this.changelogTask(ctx, config);
1014
+ await this.gitTask(ctx, config);
1015
+ await this.npmTask(ctx, config);
1016
+ await this.githubTask(ctx, config);
1017
+ if (ctx.dryRun) gitRollback(ctx);
1018
+ taskEnd(MSG.OUTRO(ctx.dryRun));
1019
+ }
1020
+ async handlePrepareRelease(cmdArgs, options) {
1021
+ const { ctx, config } = await this.createContext(cmdArgs, options);
1022
+ await this.checkTask(ctx, config);
1023
+ await this.bumpTask(ctx, config);
1024
+ await this.changelogTask(ctx, config);
1025
+ await this.gitTask(ctx, config);
1026
+ if (ctx.dryRun) gitRollback(ctx);
1027
+ taskEnd(MSG.OUTRO_PREPARE(ctx.dryRun));
1028
+ }
1029
+ async createContext(cmdArgs, options) {
968
1030
  const { version: cVersion, name: cName } = readJsonFile(join(__dirname, "../package.json"));
969
1031
  const { otp, package: defPkg, ...others } = options;
970
1032
  const { showChangelog, showRelease, ci, dryRun } = Object.fromEntries(Object.entries(others).map(([k, v]) => [k, Boolean(v)]));
@@ -1028,33 +1090,42 @@ var Action = class {
1028
1090
  const formated = formatMessage(message, pkg$1);
1029
1091
  formated && msg("PKG", formated);
1030
1092
  }
1031
- await this.checkTask(ctx, config);
1032
- await this.bumpTask(ctx, config);
1033
- await this.changelogTask(ctx, config);
1034
- await this.gitTask(ctx, config);
1035
- await this.npmTask(ctx, config);
1036
- await this.githubTask(ctx, config);
1037
- if (dryRun) gitRollback(ctx);
1038
- taskEnd(MSG.OUTRO(dryRun));
1093
+ return {
1094
+ ctx,
1095
+ config
1096
+ };
1039
1097
  }
1040
1098
  async checkTask(ctx, config) {
1041
- const { noNpm, dryRun } = ctx;
1042
- await tasks([{
1043
- title: MSG.CHECK.GIT.CHECKING,
1044
- task: async () => {
1045
- await gitCheck(ctx, config);
1046
- const { git: { remoteName, remoteUrl } } = ctx;
1047
- return success(MSG.CHECK.GIT.CHECKED(remoteName, remoteUrl), dryRun);
1048
- }
1049
- }, {
1050
- title: MSG.CHECK.NPM.CHECKING,
1051
- task: async () => {
1052
- const msg$1 = await npmCheck(ctx, config);
1053
- const { pkg: { publishConfig: { registry } } } = ctx;
1054
- return success(MSG.CHECK.NPM.CHECKED(registry, msg$1), dryRun);
1099
+ const { dryRun } = ctx;
1100
+ await tasks([
1101
+ {
1102
+ title: MSG.CHECK.GIT.CHECKING,
1103
+ task: async () => {
1104
+ await gitCheck(ctx, config);
1105
+ const { git: { remoteName, remoteUrl } } = ctx;
1106
+ return success(MSG.CHECK.GIT.CHECKED(remoteName, remoteUrl), dryRun);
1107
+ }
1108
+ },
1109
+ {
1110
+ title: MSG.CHECK.NPM.CHECKING,
1111
+ task: async () => {
1112
+ const msg$1 = await npmCheck(ctx, config);
1113
+ const { pkg: { publishConfig: { registry } } } = ctx;
1114
+ return success(MSG.CHECK.NPM.CHECKED(registry, msg$1), dryRun);
1115
+ },
1116
+ enabled: !ctx.noNpm
1055
1117
  },
1056
- enabled: !noNpm
1057
- }]).catch(abortOnError);
1118
+ {
1119
+ title: MSG.CHECK.GITHUB.CHECKING,
1120
+ task: async () => {
1121
+ const msg$1 = await githubCheck(ctx, config);
1122
+ const { github: { owner, repo } } = ctx;
1123
+ const repository = getGithubUrl(owner, repo);
1124
+ return success(MSG.CHECK.GITHUB.CHECKED(repository, msg$1), dryRun);
1125
+ },
1126
+ enabled: !ctx.noGitHub
1127
+ }
1128
+ ]).catch(abortOnError);
1058
1129
  }
1059
1130
  async bumpTask(ctx, config) {
1060
1131
  const { pkg: { current }, dryRun, isCI, showRelease, showChangelog, selectedPkg } = ctx;
@@ -1064,7 +1135,9 @@ var Action = class {
1064
1135
  return !next$1 && isIncrement$1;
1065
1136
  };
1066
1137
  if (need(ctx)) {
1067
- const parsed = parseVersion(isCI && getCIVersion(current) || (await runVersionPrompts(ctx, config)).next);
1138
+ const next$1 = (isCI ? getCIVersion(current) : void 0) || await runVersionPrompts(ctx, config);
1139
+ ctx.isIncrement = isNeq(current, next$1);
1140
+ const parsed = parseVersion(next$1);
1068
1141
  Object.assign(ctx.pkg, { ...parsed });
1069
1142
  }
1070
1143
  const { pkg: { next }, isIncrement } = ctx;
@@ -1081,9 +1154,15 @@ var Action = class {
1081
1154
  }]).catch((err) => abortOnError(err, ctx));
1082
1155
  await runLifeCycleHook(hooks, "after:bump", dryRun);
1083
1156
  const { from, to } = await resolveChangelogRange(isIncrement, isMonorepo ? `${tagPrefix?.(selectedPkg)}*` : "*");
1084
- const logStr = await getLog(from, to, getPkgDir(selectedPkg));
1085
- if (logStr) {
1086
- msg("GIT", `Changelog(${from}...${to}):${eol(2)}` + logStr);
1157
+ const commits = await getLog(from, to, getPkgDir(selectedPkg));
1158
+ const changelog = await getChangelog({
1159
+ getPkgDir: () => getPkgDir(selectedPkg),
1160
+ tagPrefix: tagPrefix?.(selectedPkg)
1161
+ });
1162
+ Object.assign(ctx.github, { changelog });
1163
+ if (commits) {
1164
+ msg("GIT", `Changelog(${from}...${to}):${eol(2)}` + changelog.trimEnd());
1165
+ msg("GIT", `Commits(${from}...${to}):${eol(2)}` + commits);
1087
1166
  if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1088
1167
  } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1089
1168
  }
@@ -1093,12 +1172,11 @@ var Action = class {
1093
1172
  await tasks([{
1094
1173
  title: MSG.TASK.CHANGELOG.START,
1095
1174
  task: async () => {
1096
- const changelog = await generateChangelog({
1175
+ await generateChangelog({
1097
1176
  getPkgDir: () => getPkgDir(selectedPkg),
1098
1177
  tagPrefix: tagPrefix?.(selectedPkg)
1099
1178
  });
1100
- await runGit(["add", `${getPkgDir(selectedPkg)}/CHANGELOG.md`]);
1101
- Object.assign(ctx.github, { changelog });
1179
+ await gitAddAll();
1102
1180
  return success(MSG.TASK.CHANGELOG.END, dryRun);
1103
1181
  },
1104
1182
  enabled: isIncrement
@@ -1165,8 +1243,14 @@ var Action = class {
1165
1243
  //#endregion
1166
1244
  //#region src/cli.ts
1167
1245
  const pkg = readJsonFile(join(__dirname, "..", "package.json"));
1168
- 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) => {
1169
- await new Action().handle(releaseType, options);
1246
+ function withCommonOptions(cmd) {
1247
+ return cmd.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 (CI only).", "").option("--otp <code>", "One-time password for npm publish.", "").option("--ci", "Enable CI mode.", false).option("--show-changelog", "Print changelog and exit.", false).option("--show-release", "Print release version and exit.", false).helpOption("-h, --help", "Display help information.");
1248
+ }
1249
+ withCommonOptions(program.name("release").description(pkg.description)).action(async (releaseType, options) => {
1250
+ await new Action().handleRelease(releaseType, options);
1251
+ });
1252
+ withCommonOptions(program.command("prepare").description("Prepare a release (bump version, commit, tag)")).action(async (releaseType, options) => {
1253
+ await new Action().handlePrepareRelease(releaseType, options);
1170
1254
  });
1171
1255
  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) => {
1172
1256
  const { force = false, monorepo = false } = options;
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
- import { DeepPartial, DeepRequired } from "@peiyanlu/ts-utils";
2
1
  import { log } from "@clack/prompts";
2
+ import { DeepPartial, DeepRequired } from "@peiyanlu/ts-utils";
3
+ import { ConventionalChangelog } from "conventional-changelog";
3
4
 
4
5
  //#region src/types.d.ts
5
6
 
@@ -88,14 +89,10 @@ interface ReleaseConfig {
88
89
  * 是否启用 npm 发布流程
89
90
  */
90
91
  publish: boolean | undefined;
91
- /**
92
- * npm 发布目标路径,对应 `npm publish <package-spec>`
93
- */
94
- publishPath: string;
95
92
  /**
96
93
  * 额外的 npm publish 参数(透传给 npm CLI)
97
94
  */
98
- pushArgs: string[];
95
+ publishArgs: string[];
99
96
  /**
100
97
  * 是否跳过发布前检查(如 ping、whoami 等校验)
101
98
  */
@@ -155,4 +152,48 @@ declare function defineConfig(config: UserConfigFn): UserConfigFn;
155
152
  declare function defineConfig(config: UserConfigExport): UserConfigExport;
156
153
  declare const mergeConfig: <T extends object>(defaults: DeepRequired<T>, overrides?: DeepPartial<T>) => DeepRequired<T>;
157
154
  //#endregion
158
- export { type Logger, type UserConfig, defineConfig, mergeConfig };
155
+ //#region src/git/changelog.d.ts
156
+ interface Options {
157
+ /** @example () => `packages/${pkg}` */
158
+ getPkgDir: () => string;
159
+ /** @example `${pkg}@` */
160
+ tagPrefix?: string;
161
+ }
162
+ declare const createGenerator: ({
163
+ getPkgDir,
164
+ tagPrefix
165
+ }: Options) => Promise<ConventionalChangelog>;
166
+ declare const getChangelog: ({
167
+ getPkgDir,
168
+ tagPrefix
169
+ }: Options) => Promise<string>;
170
+ declare const generateChangelog: ({
171
+ getPkgDir,
172
+ tagPrefix
173
+ }: Options) => Promise<void>;
174
+ //#endregion
175
+ //#region src/index.d.ts
176
+ interface PublishTagOptions {
177
+ /**
178
+ * 支持通过命令行参数提供
179
+ * @example v1.0.0; pkg@1.0.1;
180
+ * @example my-cli v1.0.0;
181
+ */
182
+ gitTag?: string;
183
+ /** @defaults `@` */
184
+ tagSeparator?: string;
185
+ /**
186
+ * monorepo 仓库子包不使用 `pkg@version` 格式创建 tag 时需要指定
187
+ * @example pkg-demo
188
+ */
189
+ defaultPackage?: string;
190
+ /**
191
+ * @example (pkg) => `packages/${pkg}`; (pkg) => '.';
192
+ */
193
+ getPkgDir?: (pkg: string) => string;
194
+ /** npm Trusted Publishing(OIDC) */
195
+ provenance?: boolean;
196
+ }
197
+ declare const publishTagToNpm: (options: PublishTagOptions) => Promise<void>;
198
+ //#endregion
199
+ export { type Logger, type UserConfig, createGenerator, defineConfig, generateChangelog, getChangelog, mergeConfig, publishTagToNpm };
package/dist/index.mjs CHANGED
@@ -1,5 +1,128 @@
1
+ import { eol, getPackageInfo, getPublishedVersion, publishPackage } from "@peiyanlu/cli-utils";
2
+ import { coerce, lt, parse, valid } from "semver";
3
+ import { dim, green, underline, yellow } from "ansis";
4
+ import { log } from "@clack/prompts";
1
5
  import { isPlainObject } from "@peiyanlu/ts-utils";
6
+ import { createWriteStream, existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { ConventionalChangelog } from "conventional-changelog";
9
+ import createPreset from "conventional-changelog-conventionalcommits";
10
+ import { writeFile } from "node:fs/promises";
11
+ import { finished } from "node:stream/promises";
2
12
 
13
+ //#region src/messages.ts
14
+ const n = (b) => b ? "(dry run)" : "";
15
+ const t = () => green(`${Math.floor(process.uptime())}s`);
16
+ const MSG = {
17
+ INTRO: (dryRun) => `🚀 Starting release ${n(dryRun)}`,
18
+ OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}`,
19
+ OUTRO_PREPARE: (dryRun) => `🎉 PrepareRelease finished successfully in ${t()} ${n(dryRun)}`,
20
+ LOG: {
21
+ SHOW_VERSION: (version) => `🎉 Released ${green(version)}`,
22
+ SHOW_CHANGELOG: "🎉 Changelog collected",
23
+ CHANGELOG_EMPTY: "No commits found since last release",
24
+ CHANGES_EMPTY: "Working tree clean. No files changed"
25
+ },
26
+ CHECK: {
27
+ GIT: {
28
+ CHECKING: "Checking Git repository",
29
+ CHECKED: (name, url) => `Git remote resolved: ${url ? `${dim(name)} → ${underline(dim(url))}` : "not found"}`
30
+ },
31
+ NPM: {
32
+ CHECKING: "Checking npm registry",
33
+ CHECKED: (registry, msg) => `NPM registry: ${underline(dim(registry))} ${underline(dim(msg))}`
34
+ },
35
+ GITHUB: {
36
+ CHECKING: "Checking github repository",
37
+ CHECKED: (repository, msg) => `GitHub repository: ${underline(dim(repository))} ${underline(dim(msg))}`
38
+ }
39
+ },
40
+ TASK: {
41
+ VERSION: {
42
+ START: "Bumping version",
43
+ END: (to) => `Version bumped: ${underline(dim(to))}`
44
+ },
45
+ CHANGELOG: {
46
+ START: "Generating changelog",
47
+ END: `Changelog generated`
48
+ },
49
+ NPM: {
50
+ START: "Publishing npm package",
51
+ END: (url) => `NPM package published: ${underline(dim(url))}`
52
+ },
53
+ GIT: {
54
+ START: "Creating git commit, tag & push",
55
+ END: "Git commit, tag & push completed"
56
+ },
57
+ GITHUB: {
58
+ START: "Creating GitHub release",
59
+ END: (url) => `GitHub release created: ${underline(dim(url))}`
60
+ }
61
+ },
62
+ PROMPT: {
63
+ SELECT_PACKAGE: "Select package release:",
64
+ SELECT_VERSION: "Select version bump:",
65
+ INPUT_VERSION: "Enter custom version:",
66
+ NPM_PUBLISH: (pkg) => `Publish to npm ${underline(dim`(${pkg})`)}?`,
67
+ NPM_OTP: "Enter npm one-time password (OTP):",
68
+ GITHUB_RELEASE: (title) => `Create a new GitHub release ${underline(dim`(${title})`)}?`,
69
+ GIT_COMMIT: (msg) => `Create git commit ${underline(dim`(${msg})`)}?`,
70
+ GIT_TAG: (msg) => `Create git tag ${underline(dim`(${msg})`)}?`,
71
+ GIT_PUSH: "Push commits and tags to remote?"
72
+ },
73
+ ERROR: {
74
+ GIT_WORKDIR: `[git] Working dir must be clean`,
75
+ GIT_REGISTRY: (name) => `[git] Repository not found for ${dim(name)}`,
76
+ GIT_REMOTE: (name) => `[git] Remote "${dim(name)}" not found`,
77
+ GITHUB_TAG_EXIT: (tag) => `[github] Release or tag "${tag}" already exists`,
78
+ GITHUB_TOKEN: (tokenRef) => `[github] Missing GitHub token. Please set ${yellow(tokenRef ?? "GITHUB_TOKEN")}`,
79
+ GITHUB_AUTH: "[github] Invalid GitHub token or insufficient permissions",
80
+ GITHUB_USER: "[github] User does not have permission to create releases",
81
+ NPM_REGISTRY: (registry) => `[npm] Unable to reach npm registry ${underline(dim(registry))}`,
82
+ NPM_AUTH: `[npm] Not authenticated with npm. Please run ${yellow("npm login")} and try again.`,
83
+ NPM_USER: (user, name) => `[npm] User ${dim(user)} is not a collaborator of ${dim(name)}.`
84
+ },
85
+ ABORT: {
86
+ CANCEL: "Operation cancelled",
87
+ MONOREPO_CI_NO_PACKAGE: "CI mode requires a target package in monorepo. Please specify it via \"--package <pkg>\"",
88
+ MONOREPO_NO_PACKAGES: "Monorepo detected, but no packages found. Please configure \"config.packages\""
89
+ },
90
+ INFO: {
91
+ TOOL: (name, version) => `${name} ${dim(`v${version}`)}`,
92
+ CONFIG: (path) => `config file: ${underline(dim(path || "not found"))}`
93
+ }
94
+ };
95
+
96
+ //#endregion
97
+ //#region src/version/bump.ts
98
+ const isValid = (version) => {
99
+ return Boolean(valid(version));
100
+ };
101
+ const parseVersion = (raw) => {
102
+ const next = isValid(raw) ? raw : coerce(raw)?.toString();
103
+ if (!next) return {};
104
+ const { prerelease } = parse(next);
105
+ const toPreRelease = Boolean(prerelease.length);
106
+ const [preId, preBase] = prerelease.map(String);
107
+ return {
108
+ next,
109
+ toPreRelease,
110
+ preId,
111
+ preBase
112
+ };
113
+ };
114
+
115
+ //#endregion
116
+ //#region src/npm/publish.ts
117
+ const resolvePublishTag = async (pkgName, version) => {
118
+ const { toPreRelease, preId } = parseVersion(version);
119
+ if (toPreRelease) return preId || "next";
120
+ const active = await getPublishedVersion(pkgName);
121
+ if (!active) return "latest";
122
+ return lt(version, active) ? "previous" : "latest";
123
+ };
124
+
125
+ //#endregion
3
126
  //#region src/config.ts
4
127
  function defineConfig(config) {
5
128
  return config;
@@ -22,4 +145,218 @@ const mergeConfig = (defaults, overrides) => {
22
145
  };
23
146
 
24
147
  //#endregion
25
- export { defineConfig, mergeConfig };
148
+ //#region src/git/changetype.ts
149
+ const defaultTypes = [
150
+ {
151
+ type: "feat",
152
+ section: "✨ 新功能",
153
+ description: "新增功能"
154
+ },
155
+ {
156
+ type: "feature",
157
+ section: "✨ 新功能",
158
+ description: "新增功能"
159
+ },
160
+ {
161
+ type: "fix",
162
+ section: "🐛 Bug 修复",
163
+ description: "修复 bug"
164
+ },
165
+ {
166
+ type: "perf",
167
+ section: "⚡ 性能优化",
168
+ description: "提升性能"
169
+ },
170
+ {
171
+ type: "revert",
172
+ section: "⏪ 回退",
173
+ description: "回退到之前版本"
174
+ },
175
+ {
176
+ type: "docs",
177
+ section: "📝 文档",
178
+ description: "文档更新"
179
+ },
180
+ {
181
+ type: "style",
182
+ section: "💄 样式调整",
183
+ description: "样式或格式修改"
184
+ },
185
+ {
186
+ type: "chore",
187
+ section: "🎫 其他更新",
188
+ description: "非功能性更改"
189
+ },
190
+ {
191
+ type: "refactor",
192
+ section: "♻ 代码重构",
193
+ description: "重构代码"
194
+ },
195
+ {
196
+ type: "test",
197
+ section: "✅ 测试",
198
+ description: "测试更新"
199
+ },
200
+ {
201
+ type: "build",
202
+ section: "👷‍ 构建系统",
203
+ description: "构建工具和系统修改"
204
+ },
205
+ {
206
+ type: "ci",
207
+ section: "🔧 CI 配置",
208
+ description: "持续集成配置"
209
+ },
210
+ {
211
+ type: "config",
212
+ section: "🔨 配置",
213
+ description: "配置文件更新"
214
+ },
215
+ {
216
+ type: "deps",
217
+ section: "🔗 依赖更新",
218
+ description: "依赖版本变更"
219
+ },
220
+ {
221
+ type: "security",
222
+ section: "🔒 安全修复",
223
+ description: "安全性更新"
224
+ },
225
+ {
226
+ type: "i18n",
227
+ section: "🌐 国际化",
228
+ description: "国际化相关更新"
229
+ },
230
+ {
231
+ type: "ux",
232
+ section: "🖥️ 用户体验",
233
+ description: "用户体验改进"
234
+ },
235
+ {
236
+ type: "hotfix",
237
+ section: "🔥 热修复",
238
+ description: "紧急修复"
239
+ }
240
+ ];
241
+
242
+ //#endregion
243
+ //#region src/git/changelog.ts
244
+ const createGenerator = async ({ getPkgDir, tagPrefix }) => {
245
+ const pkgDir = getPkgDir();
246
+ const preset = await createPreset({ types: defaultTypes.map((t$1) => ({
247
+ ...t$1,
248
+ hidden: false
249
+ })) });
250
+ preset.writer ??= {};
251
+ preset.writer.headerPartial = `
252
+ ## {{#if isPatch~}} <small> {{~/if~}}
253
+ {{#if @root.linkCompare~}}
254
+ [{{version}}](
255
+ {{~#if @root.repository~}}
256
+ {{~#if @root.host}}
257
+ {{~@root.host}}/
258
+ {{~/if}}
259
+ {{~#if @root.owner}}
260
+ {{~@root.owner}}/
261
+ {{~/if}}
262
+ {{~@root.repository}}
263
+ {{~else}}
264
+ {{~@root.repoUrl}}
265
+ {{~/if~}}
266
+ /compare/{{previousTag}}...{{currentTag}})
267
+ {{~else}}
268
+ {{~version}}
269
+ {{~/if}}
270
+ {{~#if title}} "{{title}}"
271
+ {{~/if}}
272
+ {{~#if date}} ({{date}})
273
+ {{~/if}}
274
+ {{~#if isPatch~}} </small> {{~/if}}
275
+ `.trim() + eol();
276
+ preset.writer.mainTemplate = `
277
+ {{> header}}
278
+ {{#if noteGroups}}
279
+ {{#each noteGroups}}
280
+
281
+ ### ⚠ {{title}}
282
+
283
+ {{#each notes}}
284
+ * {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{commit.subject}} {{#if commit.hash}}([{{commit.shortHash}}](https://github.com/{{@root.owner}}/{{@root.repository}}/commit/{{commit.hash}})){{/if}}
285
+ {{/each}}
286
+ {{/each}}
287
+ {{/if}}
288
+ {{#each commitGroups}}
289
+
290
+ {{#if title}}
291
+ ### {{title}}
292
+
293
+ {{/if}}
294
+ {{#each commits}}
295
+ {{> commit root=@root}}
296
+ {{/each}}
297
+ {{/each}}`.trim() + eol(2);
298
+ return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
299
+ };
300
+ const getChangelog = async ({ getPkgDir, tagPrefix }) => {
301
+ const generator = await createGenerator({
302
+ getPkgDir,
303
+ tagPrefix
304
+ });
305
+ let changelog = "";
306
+ for await (const chunk of generator.write()) changelog += chunk;
307
+ return changelog;
308
+ };
309
+ const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
310
+ const infile = join(getPkgDir(), "CHANGELOG.md");
311
+ if (!existsSync(infile)) await writeFile(infile, "");
312
+ const originalChangelog = readFileSync(infile, "utf-8");
313
+ const generator = await createGenerator({
314
+ getPkgDir,
315
+ tagPrefix
316
+ });
317
+ const writeStream = createWriteStream(infile);
318
+ for await (const chunk of generator.write()) writeStream.write(chunk);
319
+ writeStream.write(originalChangelog);
320
+ writeStream.end();
321
+ await finished(writeStream);
322
+ };
323
+
324
+ //#endregion
325
+ //#region src/index.ts
326
+ const publishTagToNpm = async (options) => {
327
+ const { gitTag, defaultPackage, tagSeparator = "@", getPkgDir = () => ".", provenance } = options;
328
+ const tag = process.argv.slice(2)[0] || gitTag;
329
+ if (!tag) throw new Error(`❌ No git tag specified.`);
330
+ console.log(`\n📦 Start publishing from git tag: ${tag}`);
331
+ let pkgName = defaultPackage;
332
+ let version;
333
+ if (tag.includes(tagSeparator)) {
334
+ [pkgName, version] = tag.split(tagSeparator);
335
+ console.log(`🔍 Parsed tag → package: "${pkgName}", version: "${version}"`);
336
+ } else {
337
+ version = tag;
338
+ console.log(`🔍 Parsed tag → version only: "${version}"`);
339
+ }
340
+ if (version.startsWith("v")) {
341
+ version = version.slice(1);
342
+ console.log(`✂️ Normalized version (strip v): ${version}`);
343
+ }
344
+ if (!pkgName) throw new Error(`❌ Package name should be specified in tag "${tag}" when defaultPackage is not set`);
345
+ console.log(`📁 Resolving package info for "${pkgName}"...`);
346
+ const { pkg, pkgDir } = getPackageInfo(pkgName, getPkgDir);
347
+ console.log(`📄 package.json → name: ${pkg.name}, version: ${pkg.version}`);
348
+ console.log(`📂 Package directory: ${pkgDir}`);
349
+ if (pkg.version !== version) throw new Error(`❌ Package version from tag "${version}" mismatches with current version "${pkg.version}"`);
350
+ const publishTag = await resolvePublishTag(pkg.name, version);
351
+ console.log(`🏷 Resolved npm dist-tag: "${publishTag}" (version ${version})`);
352
+ console.log(`🚀 Publishing "${pkg.name}@${version}" to npm...`);
353
+ await publishPackage({
354
+ tag: publishTag,
355
+ args: provenance ? ["--provenance"] : [],
356
+ cwd: pkgDir
357
+ });
358
+ console.log(`✅ Successfully published ${pkg.name}@${version} with dist-tag "${publishTag}"\n`);
359
+ };
360
+
361
+ //#endregion
362
+ export { createGenerator, defineConfig, generateChangelog, getChangelog, mergeConfig, publishTagToNpm };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peiyanlu/release",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Release project tool.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,14 +17,6 @@
17
17
  },
18
18
  "./package.json": "./package.json"
19
19
  },
20
- "scripts": {
21
- "dev": "tsdown -w",
22
- "build": "tsdown",
23
- "prepublish": "tsdown",
24
- "release": "tsdown && node index.js",
25
- "test": "vitest run",
26
- "test:cov": "vitest run --coverage"
27
- },
28
20
  "dependencies": {
29
21
  "@clack/prompts": "^0.11.0",
30
22
  "@octokit/rest": "^22.0.1",
@@ -74,5 +66,13 @@
74
66
  "bugs": {
75
67
  "url": "https://github.com/peiyanlu/release/issues"
76
68
  },
77
- "homepage": "https://github.com/peiyanlu/release#readme"
78
- }
69
+ "homepage": "https://github.com/peiyanlu/release#readme",
70
+ "scripts": {
71
+ "dev": "tsdown -w",
72
+ "build": "tsdown",
73
+ "prepublish": "tsdown",
74
+ "release": "node index.js",
75
+ "test": "vitest run",
76
+ "test:cov": "vitest run --coverage"
77
+ }
78
+ }