@ruan-cat/utils 4.21.0 → 4.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/chunk-46BA5ZMG.js +2788 -0
- package/dist/cli/chunk-46BA5ZMG.js.map +1 -0
- package/dist/cli/chunk-BPIJ4Z5J.js +233 -0
- package/dist/cli/chunk-BPIJ4Z5J.js.map +1 -0
- package/dist/cli/index.js +75 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/move-vercel-output-to-root.js +14 -0
- package/dist/cli/move-vercel-output-to-root.js.map +1 -0
- package/dist/cli/relizy-runner.js +8 -0
- package/dist/cli/relizy-runner.js.map +1 -0
- package/dist/node-esm/index.d.ts +61 -1
- package/dist/node-esm/index.js +248 -13
- package/dist/node-esm/index.js.map +1 -1
- package/package.json +7 -2
- package/src/cli/index.ts +97 -0
- package/src/cli/move-vercel-output-to-root.ts +17 -0
- package/src/cli/relizy-runner.ts +11 -0
- package/src/node-esm/index.ts +1 -0
- package/src/node-esm/scripts/move-vercel-output-to-root/index.ts +1 -1
- package/src/node-esm/scripts/relizy-runner/index.test.ts +281 -0
- package/src/node-esm/scripts/relizy-runner/index.ts +354 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import consola from "consola";
|
|
7
|
+
import { parsePnpmWorkspaceYaml } from "pnpm-workspace-yaml";
|
|
8
|
+
import type { PackageJson } from "pkg-types";
|
|
9
|
+
|
|
10
|
+
const WINDOWS_GNU_COMMANDS = ["grep", "head", "sed"] as const;
|
|
11
|
+
|
|
12
|
+
/** 发版基线 tag 校验所需的最小字段,由 {@link PackageJson} 派生。 */
|
|
13
|
+
export type WorkspacePackageInfo = Required<Pick<PackageJson, "name" | "version">>;
|
|
14
|
+
|
|
15
|
+
// ── 工作区包发现 ──────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 解析根目录 `pnpm-workspace.yaml` 并展开 glob 模式,
|
|
19
|
+
* 收集所有含 `package.json` 的子包目录,返回其 name 与 version。
|
|
20
|
+
*
|
|
21
|
+
* 使用 [pnpm-workspace-yaml](https://github.com/antfu/pnpm-workspace-utils/tree/main/packages/pnpm-workspace-yaml)
|
|
22
|
+
* 解析工作区清单,再用 `pkg-types` 的 `PackageJson` 约束子包字段。
|
|
23
|
+
*/
|
|
24
|
+
export function getWorkspacePackages(workspaceRoot?: string): WorkspacePackageInfo[] {
|
|
25
|
+
const root = workspaceRoot ?? process.cwd();
|
|
26
|
+
const yamlPath = resolve(root, "pnpm-workspace.yaml");
|
|
27
|
+
|
|
28
|
+
if (!existsSync(yamlPath)) {
|
|
29
|
+
consola.error("release:relizy:未在当前目录找到 pnpm-workspace.yaml,请从仓库根目录执行。");
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const globs = parsePnpmWorkspaceYaml(readFileSync(yamlPath, "utf8")).toJSON().packages ?? [];
|
|
34
|
+
const packages: WorkspacePackageInfo[] = [];
|
|
35
|
+
|
|
36
|
+
for (const pattern of globs) {
|
|
37
|
+
const parts = pattern.split("/");
|
|
38
|
+
|
|
39
|
+
if (parts.length !== 2 || parts[1] !== "*") {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dir = resolve(root, parts[0]);
|
|
44
|
+
|
|
45
|
+
if (!existsSync(dir)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const discovered = readdirSync(dir, { withFileTypes: true })
|
|
50
|
+
.filter((entry) => entry.isDirectory())
|
|
51
|
+
.map((entry) => join(dir, entry.name, "package.json"))
|
|
52
|
+
.filter((pkgPath) => existsSync(pkgPath))
|
|
53
|
+
.map((pkgPath) => {
|
|
54
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as PackageJson;
|
|
55
|
+
|
|
56
|
+
return { name: pkg.name, version: pkg.version };
|
|
57
|
+
})
|
|
58
|
+
.filter((pkg): pkg is WorkspacePackageInfo => typeof pkg.name === "string" && typeof pkg.version === "string");
|
|
59
|
+
|
|
60
|
+
packages.push(...discovered);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return packages;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Windows GNU 工具兼容层 ────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function runLookup(command: string, args: string[], env: NodeJS.ProcessEnv = process.env) {
|
|
69
|
+
return spawnSync(command, args, {
|
|
70
|
+
cwd: process.cwd(),
|
|
71
|
+
env,
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
stdio: "pipe",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasExecutable(command: string, env: NodeJS.ProcessEnv = process.env) {
|
|
78
|
+
const lookupCommand = process.platform === "win32" ? "where" : "which";
|
|
79
|
+
|
|
80
|
+
return runLookup(lookupCommand, [command], env).status === 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function listExecutableMatches(command: string) {
|
|
84
|
+
const lookupCommand = process.platform === "win32" ? "where" : "which";
|
|
85
|
+
const result = runLookup(lookupCommand, [command]);
|
|
86
|
+
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result.stdout
|
|
92
|
+
.split(/\r?\n/u)
|
|
93
|
+
.map((line) => line.trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveGitUsrBinPath() {
|
|
98
|
+
if (process.platform !== "win32") {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const candidates = new Set<string>();
|
|
103
|
+
|
|
104
|
+
for (const executablePath of [...listExecutableMatches("bash"), ...listExecutableMatches("git")]) {
|
|
105
|
+
const executableDir = dirname(executablePath);
|
|
106
|
+
|
|
107
|
+
candidates.add(resolve(executableDir, "..", "usr", "bin"));
|
|
108
|
+
candidates.add(resolve(executableDir, "usr", "bin"));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const candidate of candidates) {
|
|
112
|
+
const hasAllCommands = WINDOWS_GNU_COMMANDS.every((command) => existsSync(join(candidate, `${command}.exe`)));
|
|
113
|
+
|
|
114
|
+
if (hasAllCommands) {
|
|
115
|
+
return candidate;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 确保 relizy 所需的 GNU 工具(grep / head / sed)在 PATH 中可用。
|
|
124
|
+
* Windows 下会自动补齐 Git for Windows 的 `usr\bin` 路径。
|
|
125
|
+
*/
|
|
126
|
+
export function ensureRelizyShellEnv() {
|
|
127
|
+
if (process.platform !== "win32") {
|
|
128
|
+
return { ...process.env };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (WINDOWS_GNU_COMMANDS.every((command) => hasExecutable(command))) {
|
|
132
|
+
return { ...process.env };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const gitUsrBinPath = resolveGitUsrBinPath();
|
|
136
|
+
|
|
137
|
+
if (!gitUsrBinPath) {
|
|
138
|
+
consola.error("[release:relizy] 在 Windows 上未找到 relizy 所需的 GNU 工具(grep / head / sed)。");
|
|
139
|
+
consola.error("请先安装 Git for Windows,或将其安装目录下的 usr\\bin 加入 PATH。");
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const env = {
|
|
144
|
+
...process.env,
|
|
145
|
+
PATH: `${gitUsrBinPath};${process.env.PATH ?? ""}`,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (!WINDOWS_GNU_COMMANDS.every((command) => hasExecutable(command, env))) {
|
|
149
|
+
consola.error("[release:relizy] 已定位到 Git for Windows,但 grep / head / sed 仍不可用。");
|
|
150
|
+
consola.error(`请检查 PATH,或手动确认该目录是否存在所需可执行文件:${gitUsrBinPath}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
consola.info(`[release:relizy] Windows 下已补齐 GNU 工具路径:${gitUsrBinPath}`);
|
|
155
|
+
|
|
156
|
+
return env;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── independent 模式 baseline tag 检查 ───────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function getPackageTags(packageName: string, env: NodeJS.ProcessEnv) {
|
|
162
|
+
const stdout = execFileSync("git", ["tag", "--list", `${packageName}@*`], {
|
|
163
|
+
cwd: process.cwd(),
|
|
164
|
+
env,
|
|
165
|
+
encoding: "utf8",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return stdout
|
|
169
|
+
.split(/\r?\n/u)
|
|
170
|
+
.map((line) => line.trim())
|
|
171
|
+
.filter(Boolean);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 判断当前 relizy 子命令是否需要检查 independent 基线 tag。
|
|
176
|
+
* 仅 `release` 与 `bump` 需要。
|
|
177
|
+
*/
|
|
178
|
+
export function shouldCheckIndependentBootstrap(relizyArgs: string[]) {
|
|
179
|
+
const [command] = relizyArgs;
|
|
180
|
+
|
|
181
|
+
return command === "release" || command === "bump";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getPackagesMissingBootstrapTags(env: NodeJS.ProcessEnv) {
|
|
185
|
+
return getWorkspacePackages().filter((pkg) => getPackageTags(pkg.name, env).length === 0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 根据缺少基线 tag 的包列表,生成包含补打 tag 命令的提示文本。
|
|
190
|
+
*/
|
|
191
|
+
export function buildBootstrapInstructions(missingPackages: WorkspacePackageInfo[]) {
|
|
192
|
+
const tagCommands = missingPackages.map((pkg) => `git tag "${pkg.name}@${pkg.version}"`);
|
|
193
|
+
const pushArgs = missingPackages.map((pkg) => `"${pkg.name}@${pkg.version}"`).join(" ");
|
|
194
|
+
|
|
195
|
+
return [
|
|
196
|
+
"[release:relizy] 检测到本仓库尚未为以下包建立基线 tag(independent 模式首次发版前需要):",
|
|
197
|
+
...missingPackages.map((pkg) => `- ${pkg.name}@${pkg.version}`),
|
|
198
|
+
"",
|
|
199
|
+
"请按当前 package.json 版本创建基线 tag,并推送到远端:",
|
|
200
|
+
...tagCommands,
|
|
201
|
+
`git push origin ${pushArgs}`,
|
|
202
|
+
].join("\n");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function printBootstrapInstructions(missingPackages: WorkspacePackageInfo[]) {
|
|
206
|
+
consola.error(buildBootstrapInstructions(missingPackages));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── 帮助信息 ──────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 获取 relizy-runner CLI 的帮助文本。
|
|
213
|
+
*/
|
|
214
|
+
export function getRelizyRunnerHelpText() {
|
|
215
|
+
return [
|
|
216
|
+
"relizy-runner <relizy 子命令与参数>",
|
|
217
|
+
"",
|
|
218
|
+
"在 relizy 执行前补齐 Windows GNU 工具路径,并在首次 independent 发版前",
|
|
219
|
+
"校验基线 tag。不改变 relizy 自身的发版与版本计算逻辑。",
|
|
220
|
+
"",
|
|
221
|
+
"用法:",
|
|
222
|
+
" relizy-runner release --no-publish --no-provider-release",
|
|
223
|
+
" relizy-runner changelog --dry-run",
|
|
224
|
+
" relizy-runner bump",
|
|
225
|
+
"",
|
|
226
|
+
"常用参数(节选,由 relizy 处理,runner 仅透传):",
|
|
227
|
+
" --dry-run 预览,不写文件、不打 tag、不提交",
|
|
228
|
+
" --no-push 不 push 到远端",
|
|
229
|
+
" --no-publish 不执行 npm publish",
|
|
230
|
+
" --no-provider-release 不在 GitHub/GitLab 创建 Release",
|
|
231
|
+
" --no-commit 不创建提交与 tag",
|
|
232
|
+
" --no-changelog 不生成 changelog 文件",
|
|
233
|
+
" --no-verify 提交时跳过 git hooks",
|
|
234
|
+
"",
|
|
235
|
+
"以上仅为常用参数节选,完整参数请查阅 relizy 包自身文档:",
|
|
236
|
+
" npx relizy --help",
|
|
237
|
+
" npx relizy release --help",
|
|
238
|
+
" npx relizy changelog --help",
|
|
239
|
+
"",
|
|
240
|
+
"示例:",
|
|
241
|
+
" npx relizy-runner release --no-publish --no-provider-release",
|
|
242
|
+
" npx ruan-cat-utils relizy-runner release --dry-run",
|
|
243
|
+
"",
|
|
244
|
+
"选项:",
|
|
245
|
+
" -h, --help 查看帮助信息",
|
|
246
|
+
].join("\n");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── 主入口 ────────────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
function resolveRelizyEntrypoint() {
|
|
252
|
+
return resolve(process.cwd(), "node_modules", "relizy", "bin", "relizy.mjs");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 执行 relizy-runner。
|
|
257
|
+
*
|
|
258
|
+
* @description
|
|
259
|
+
* 在 relizy 执行前做两件事:
|
|
260
|
+
* 1. Windows 下自动补齐 Git for Windows 的 `usr\bin` 路径,避免 relizy 内部调用 `grep`/`head`/`sed` 失败。
|
|
261
|
+
* 2. 在 `release`/`bump` 前校验 independent 基线 tag,缺失时打印补打命令并终止。
|
|
262
|
+
*
|
|
263
|
+
* @param relizyArgs - 透传给 relizy 的子命令与参数
|
|
264
|
+
* @returns 退出码
|
|
265
|
+
*/
|
|
266
|
+
export function runRelizyRunner(relizyArgs: string[]) {
|
|
267
|
+
if (relizyArgs.length === 0) {
|
|
268
|
+
consola.error("用法:relizy-runner <relizy 子命令与参数>");
|
|
269
|
+
consola.error("示例:relizy-runner release --no-publish --no-provider-release");
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
consola.start(`[release:relizy] 执行命令:relizy ${relizyArgs.join(" ")}`);
|
|
274
|
+
|
|
275
|
+
const env = ensureRelizyShellEnv();
|
|
276
|
+
|
|
277
|
+
if (shouldCheckIndependentBootstrap(relizyArgs)) {
|
|
278
|
+
consola.info("[release:relizy] 检查 independent 基线 tag...");
|
|
279
|
+
const missingPackages = getPackagesMissingBootstrapTags(env);
|
|
280
|
+
|
|
281
|
+
if (missingPackages.length > 0) {
|
|
282
|
+
printBootstrapInstructions(missingPackages);
|
|
283
|
+
return 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
consola.success("[release:relizy] 基线 tag 检查通过。");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const relizyEntrypoint = resolveRelizyEntrypoint();
|
|
290
|
+
|
|
291
|
+
if (!existsSync(relizyEntrypoint)) {
|
|
292
|
+
consola.error("未在 node_modules 中找到 relizy 命令行入口,请先执行 pnpm install。");
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
consola.info(`[release:relizy] 调用 relizy 入口:${relizyEntrypoint}`);
|
|
297
|
+
|
|
298
|
+
const result = spawnSync(process.execPath, [relizyEntrypoint, ...relizyArgs], {
|
|
299
|
+
cwd: process.cwd(),
|
|
300
|
+
env,
|
|
301
|
+
stdio: "inherit",
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (result.status === 0) {
|
|
305
|
+
consola.success("[release:relizy] relizy 执行完毕。");
|
|
306
|
+
} else {
|
|
307
|
+
consola.error(`[release:relizy] relizy 以退出码 ${result.status} 结束。`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return result.status ?? 1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 解析 relizy-runner CLI 参数。
|
|
315
|
+
* 如果首个参数是 `--help` 或 `-h`,返回 `{ help: true }`。
|
|
316
|
+
* 否则将所有参数透传给 relizy。
|
|
317
|
+
*/
|
|
318
|
+
export function parseRelizyRunnerCliArgs(args: string[]): { help: boolean; relizyArgs: string[] } {
|
|
319
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
320
|
+
return { help: true, relizyArgs: [] };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return { help: false, relizyArgs: args };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 执行 relizy-runner CLI。
|
|
328
|
+
*/
|
|
329
|
+
export function runRelizyRunnerCli(args: string[] = process.argv.slice(2)) {
|
|
330
|
+
const parsed = parseRelizyRunnerCliArgs(args);
|
|
331
|
+
|
|
332
|
+
if (parsed.help) {
|
|
333
|
+
console.log(getRelizyRunnerHelpText());
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const exitCode = runRelizyRunner(parsed.relizyArgs);
|
|
338
|
+
process.exitCode = exitCode;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function isRunningAsCli() {
|
|
342
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
343
|
+
const entryPath = process.argv[1];
|
|
344
|
+
|
|
345
|
+
if (!entryPath) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return resolve(entryPath) === currentFilePath;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (isRunningAsCli()) {
|
|
353
|
+
runRelizyRunnerCli();
|
|
354
|
+
}
|