@peiyanlu/release 0.0.4 → 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/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
 
@@ -24,7 +25,7 @@ interface ReleaseConfig {
24
25
  /** 使用 monorepo 时使用 toTag,例如 `${pkg}@${version}`,默认为 `v${version}` 作为单仓库 */
25
26
  toTag: (pkg: string, version: string) => string;
26
27
  /** 作为 CHANGELOG 标题的 Tag 前缀,多包场景下需要 `${pkg}@` */
27
- changelogTagPrefix: ((pkg: string) => string | undefined) | undefined;
28
+ tagPrefix: ((pkg: string) => string | undefined) | undefined;
28
29
  /**
29
30
  * Git 相关配置
30
31
  * 影响提交、打 tag、推送等本地/远程 Git 行为。
@@ -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.4",
3
+ "version": "0.0.6",
4
4
  "description": "Release project tool.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,19 +14,13 @@
14
14
  "types": "./dist/index.d.mts",
15
15
  "default": "./dist/index.mjs"
16
16
  }
17
- }
18
- },
19
- "scripts": {
20
- "dev": "tsdown -w",
21
- "build": "tsdown",
22
- "prepublish": "tsdown",
23
- "test": "vitest run",
24
- "release": "tsdown && node index.js"
17
+ },
18
+ "./package.json": "./package.json"
25
19
  },
26
20
  "dependencies": {
27
21
  "@clack/prompts": "^0.11.0",
28
22
  "@octokit/rest": "^22.0.1",
29
- "@peiyanlu/cli-utils": "^0.0.4",
23
+ "@peiyanlu/cli-utils": "^0.0.6",
30
24
  "@peiyanlu/ts-utils": "^0.0.4",
31
25
  "ansis": "^4.2.0",
32
26
  "commander": "^14.0.2",
@@ -42,9 +36,10 @@
42
36
  "@types/mime-types": "^3.0.1",
43
37
  "@types/node": "^24.10.3",
44
38
  "@types/semver": "^7.7.1",
39
+ "@vitest/coverage-v8": "^4.0.15",
45
40
  "tsdown": "^0.18.4",
46
41
  "typescript": "^5.9.3",
47
- "vitest": "^4.0.15"
42
+ "vitest": "^4.0.18"
48
43
  },
49
44
  "author": {
50
45
  "name": "YanluPei",
@@ -71,5 +66,13 @@
71
66
  "bugs": {
72
67
  "url": "https://github.com/peiyanlu/release/issues"
73
68
  },
74
- "homepage": "https://github.com/peiyanlu/release#readme"
75
- }
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
+ }