@ruan-cat/utils 4.20.0 → 4.22.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/README.md +26 -0
- package/dist/cli/chunk-46BA5ZMG.js +2788 -0
- package/dist/cli/chunk-46BA5ZMG.js.map +1 -0
- package/dist/cli/index.js +65 -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/node-cjs/index.cjs +18 -2
- package/dist/node-cjs/index.cjs.map +1 -1
- package/dist/node-cjs/index.d.cts +11 -1
- package/dist/node-esm/index.d.ts +88 -1
- package/dist/node-esm/index.js +199 -12
- package/dist/node-esm/index.js.map +1 -1
- package/package.json +21 -15
- package/src/cli/index.ts +87 -0
- package/src/cli/move-vercel-output-to-root.ts +17 -0
- package/src/monorepo/index.ts +28 -1
- package/src/node-esm/index.ts +1 -0
- package/src/node-esm/scripts/move-vercel-output-to-root/index.test.ts +151 -0
- package/src/node-esm/scripts/move-vercel-output-to-root/index.ts +286 -0
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ruan-cat/utils",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.22.0",
|
|
4
4
|
"description": "阮喵喵工具集合。默认提供js文件,也直接提供ts文件。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"types": "./src/index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"ruan-cat-utils": "./dist/cli/index.js",
|
|
10
|
+
"move-vercel-output-to-root": "./dist/cli/move-vercel-output-to-root.js"
|
|
11
|
+
},
|
|
8
12
|
"homepage": "https://utils.ruancat6312.top",
|
|
9
13
|
"bugs": {
|
|
10
14
|
"url": "https://github.com/ruan-cat/monorepo/issues"
|
|
@@ -29,6 +33,7 @@
|
|
|
29
33
|
"types": "./dist/node-esm/index.d.ts",
|
|
30
34
|
"import": "./dist/node-esm/index.js"
|
|
31
35
|
},
|
|
36
|
+
"./monorepo": "./src/monorepo/index.ts",
|
|
32
37
|
"./unplugin-vue-router": "./src/unplugin-vue-router/index.ts",
|
|
33
38
|
"./vite-plugin-autogeneration-import-file": "./src/vite-plugin-autogeneration-import-file/index.ts",
|
|
34
39
|
"./vueuse": "./src/vueuse/index.ts",
|
|
@@ -65,31 +70,31 @@
|
|
|
65
70
|
],
|
|
66
71
|
"dependencies": {
|
|
67
72
|
"@vueuse/integrations": "^13.9.0",
|
|
68
|
-
"axios": "^1.13.
|
|
73
|
+
"axios": "^1.13.6",
|
|
69
74
|
"consola": "^3.4.2",
|
|
70
|
-
"lodash-es": "^4.17.
|
|
71
|
-
"pnpm-workspace-yaml": "^1.
|
|
75
|
+
"lodash-es": "^4.17.23",
|
|
76
|
+
"pnpm-workspace-yaml": "^1.6.0",
|
|
72
77
|
"tinyglobby": "^0.2.15"
|
|
73
78
|
},
|
|
74
79
|
"devDependencies": {
|
|
75
80
|
"@antfu/utils": "^9.3.0",
|
|
76
81
|
"@types/js-yaml": "^4.0.9",
|
|
77
82
|
"@types/lodash-es": "^4.17.12",
|
|
78
|
-
"@types/node": "^22.19.
|
|
79
|
-
"@types/qs": "^6.
|
|
80
|
-
"automd": "^0.4.
|
|
83
|
+
"@types/node": "^22.19.15",
|
|
84
|
+
"@types/qs": "^6.15.0",
|
|
85
|
+
"automd": "^0.4.3",
|
|
81
86
|
"commander": "^13.1.0",
|
|
82
|
-
"js-yaml": "^4.1.
|
|
83
|
-
"qs": "^6.
|
|
84
|
-
"tsup": "^8.5.
|
|
87
|
+
"js-yaml": "^4.1.1",
|
|
88
|
+
"qs": "^6.15.0",
|
|
89
|
+
"tsup": "^8.5.1",
|
|
85
90
|
"type-plus": "^7.6.2",
|
|
86
|
-
"typedoc": "^0.28.
|
|
87
|
-
"typedoc-plugin-frontmatter": "^1.3.
|
|
88
|
-
"typedoc-plugin-markdown": "^4.
|
|
91
|
+
"typedoc": "^0.28.17",
|
|
92
|
+
"typedoc-plugin-frontmatter": "^1.3.1",
|
|
93
|
+
"typedoc-plugin-markdown": "^4.10.0",
|
|
89
94
|
"typescript": "^5.9.3",
|
|
90
|
-
"unplugin-auto-import": "^
|
|
95
|
+
"unplugin-auto-import": "^21.0.0",
|
|
91
96
|
"unplugin-vue-router": "^0.16.1",
|
|
92
|
-
"vite-plugin-autogeneration-import-file": "^3.0.
|
|
97
|
+
"vite-plugin-autogeneration-import-file": "^3.0.1",
|
|
93
98
|
"vitepress": "^1.6.4"
|
|
94
99
|
},
|
|
95
100
|
"peerDependencies": {
|
|
@@ -120,6 +125,7 @@
|
|
|
120
125
|
"scripts": {
|
|
121
126
|
"build": "tsup",
|
|
122
127
|
"prebuild": "automd",
|
|
128
|
+
"move-vercel-output-to-root": "tsx ./src/node-esm/scripts/move-vercel-output-to-root/index.ts",
|
|
123
129
|
"docs:dev": "turbo docs:dev",
|
|
124
130
|
"build:docs": "turbo do-build-docs",
|
|
125
131
|
"typedoc": "typedoc --options typedoc.config.mjs",
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ruan-cat/utils CLI 入口
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* 本文件是 @ruan-cat/utils 包的统一 CLI 入口。
|
|
6
|
+
* 通过 package.json 的 bin 字段暴露为 `ruan-cat-utils` 命令。
|
|
7
|
+
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* npx ruan-cat-utils <command> [options]
|
|
10
|
+
*
|
|
11
|
+
* 支持的子命令:
|
|
12
|
+
* move-vercel-output-to-root 将子包的 .vercel/output 搬运到 monorepo 根目录
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import consola from "consola";
|
|
16
|
+
import {
|
|
17
|
+
getMoveVercelOutputToRootHelpText,
|
|
18
|
+
runMoveVercelOutputToRootCli,
|
|
19
|
+
} from "../node-esm/scripts/move-vercel-output-to-root/index";
|
|
20
|
+
|
|
21
|
+
const CLI_NAME = "ruan-cat-utils";
|
|
22
|
+
|
|
23
|
+
function printMainHelp() {
|
|
24
|
+
const helpText = [
|
|
25
|
+
`${CLI_NAME} - @ruan-cat/utils 命令行工具`,
|
|
26
|
+
"",
|
|
27
|
+
"用法:",
|
|
28
|
+
` ${CLI_NAME} <command> [options]`,
|
|
29
|
+
"",
|
|
30
|
+
"可用命令:",
|
|
31
|
+
" move-vercel-output-to-root 将子包的 .vercel/output 搬运到 monorepo 根目录",
|
|
32
|
+
"",
|
|
33
|
+
"全局选项:",
|
|
34
|
+
" -h, --help 查看帮助信息",
|
|
35
|
+
" -v, --version 查看版本号",
|
|
36
|
+
"",
|
|
37
|
+
"示例:",
|
|
38
|
+
` ${CLI_NAME} move-vercel-output-to-root`,
|
|
39
|
+
` ${CLI_NAME} move-vercel-output-to-root --dry-run`,
|
|
40
|
+
` ${CLI_NAME} move-vercel-output-to-root --root-dir ../../..`,
|
|
41
|
+
].join("\n");
|
|
42
|
+
|
|
43
|
+
console.log(helpText);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function printVersion() {
|
|
47
|
+
// 动态读取版本号会引入额外复杂度,这里直接输出包名提示用户查看
|
|
48
|
+
console.log(`${CLI_NAME} (from @ruan-cat/utils)`);
|
|
49
|
+
console.log("运行 'npm list @ruan-cat/utils' 查看当前安装的版本。");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function main() {
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
const command = args[0];
|
|
55
|
+
|
|
56
|
+
if (!command || command === "--help" || command === "-h") {
|
|
57
|
+
printMainHelp();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (command === "--version" || command === "-v") {
|
|
62
|
+
printVersion();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
switch (command) {
|
|
67
|
+
case "move-vercel-output-to-root": {
|
|
68
|
+
const subArgs = args.slice(1);
|
|
69
|
+
try {
|
|
70
|
+
runMoveVercelOutputToRootCli(subArgs);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
consola.error(error instanceof Error ? error.message : String(error));
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
default: {
|
|
79
|
+
consola.error(`未知命令:${command}`);
|
|
80
|
+
console.log("");
|
|
81
|
+
printMainHelp();
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* move-vercel-output-to-root 快捷命令入口
|
|
3
|
+
*
|
|
4
|
+
* @description
|
|
5
|
+
* 本文件是 `move-vercel-output-to-root` bin 命令的直接入口。
|
|
6
|
+
* 安装 @ruan-cat/utils 后,可以直接通过 `npx move-vercel-output-to-root` 调用。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import consola from "consola";
|
|
10
|
+
import { runMoveVercelOutputToRootCli } from "../node-esm/scripts/move-vercel-output-to-root/index";
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
runMoveVercelOutputToRootCli();
|
|
14
|
+
} catch (error) {
|
|
15
|
+
consola.error(error instanceof Error ? error.message : String(error));
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
}
|
package/src/monorepo/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { globSync } from "tinyglobby";
|
|
4
4
|
import { load } from "js-yaml";
|
|
@@ -17,6 +17,33 @@ function pathChange(path: string) {
|
|
|
17
17
|
// return normalizePath(path);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 从给定目录开始,向上查找 monorepo 根目录。
|
|
22
|
+
* @description
|
|
23
|
+
* 通过查找 `pnpm-workspace.yaml` 文件来定位 monorepo 根目录。
|
|
24
|
+
* 该函数适用于脚本在 monorepo 子包内运行的场景。
|
|
25
|
+
*
|
|
26
|
+
* @param startDir 起始查找目录,默认值为 `process.cwd()`
|
|
27
|
+
* @returns 找到的 monorepo 根目录绝对路径;若未找到则返回 `null`
|
|
28
|
+
*/
|
|
29
|
+
export function findMonorepoRoot(startDir: string = process.cwd()): string | null {
|
|
30
|
+
let currentDir = resolve(startDir);
|
|
31
|
+
const fileSystemRoot = parse(currentDir).root;
|
|
32
|
+
|
|
33
|
+
while (true) {
|
|
34
|
+
const workspaceConfigPath = join(currentDir, "pnpm-workspace.yaml");
|
|
35
|
+
if (fs.existsSync(workspaceConfigPath)) {
|
|
36
|
+
return currentDir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (currentDir === fileSystemRoot) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
currentDir = dirname(currentDir);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
/**
|
|
21
48
|
* 判断目标项目是否是 monorepo 格式的项目
|
|
22
49
|
* @description
|
package/src/node-esm/index.ts
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
moveVercelOutputToRoot,
|
|
7
|
+
parseMoveVercelOutputToRootCliArgs,
|
|
8
|
+
resolveMoveVercelOutputToRootOptions,
|
|
9
|
+
} from "./index";
|
|
10
|
+
|
|
11
|
+
function createMonorepoFixture() {
|
|
12
|
+
const tempRoot = fs.mkdtempSync(path.join(tmpdir(), "move-vercel-output-to-root-"));
|
|
13
|
+
const packageDir = path.join(tempRoot, "packages", "demo-app");
|
|
14
|
+
const packageOutputDir = path.join(packageDir, ".vercel", "output");
|
|
15
|
+
const rootOutputDir = path.join(tempRoot, ".vercel", "output");
|
|
16
|
+
|
|
17
|
+
fs.mkdirSync(packageOutputDir, { recursive: true });
|
|
18
|
+
fs.mkdirSync(rootOutputDir, { recursive: true });
|
|
19
|
+
fs.writeFileSync(path.join(tempRoot, "pnpm-workspace.yaml"), "packages:\n - packages/*\n", "utf8");
|
|
20
|
+
fs.writeFileSync(path.join(packageDir, "package.json"), '{"name":"demo-app"}', "utf8");
|
|
21
|
+
fs.writeFileSync(path.join(packageOutputDir, "config.json"), '{"version":3}', "utf8");
|
|
22
|
+
fs.mkdirSync(path.join(packageOutputDir, "functions"), { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(packageOutputDir, "functions", "index.func"), "hello", "utf8");
|
|
24
|
+
fs.writeFileSync(path.join(rootOutputDir, "stale.txt"), "old", "utf8");
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
tempRoot,
|
|
28
|
+
packageDir,
|
|
29
|
+
packageOutputDir,
|
|
30
|
+
rootOutputDir,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("move-vercel-output-to-root", () => {
|
|
35
|
+
const temporaryDirectories = new Set<string>();
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
temporaryDirectories.clear();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
for (const tempDirectory of temporaryDirectories) {
|
|
43
|
+
fs.rmSync(tempDirectory, {
|
|
44
|
+
force: true,
|
|
45
|
+
recursive: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("resolveMoveVercelOutputToRootOptions", () => {
|
|
51
|
+
test("应该能从子包目录自动解析 monorepo 根目录与默认路径", () => {
|
|
52
|
+
const fixture = createMonorepoFixture();
|
|
53
|
+
temporaryDirectories.add(fixture.tempRoot);
|
|
54
|
+
|
|
55
|
+
const resolvedOptions = resolveMoveVercelOutputToRootOptions({
|
|
56
|
+
cwd: fixture.packageDir,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(resolvedOptions.monorepoRoot).toBe(fixture.tempRoot);
|
|
60
|
+
expect(resolvedOptions.sourceDir).toBe(path.join(fixture.packageDir, ".vercel", "output"));
|
|
61
|
+
expect(resolvedOptions.targetDir).toBe(path.join(fixture.tempRoot, ".vercel", "output"));
|
|
62
|
+
expect(resolvedOptions.skipClean).toBe(false);
|
|
63
|
+
expect(resolvedOptions.dryRun).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("当 sourceDir 和 targetDir 解析到同一路径时应该抛出错误", () => {
|
|
67
|
+
const fixture = createMonorepoFixture();
|
|
68
|
+
temporaryDirectories.add(fixture.tempRoot);
|
|
69
|
+
|
|
70
|
+
expect(() =>
|
|
71
|
+
resolveMoveVercelOutputToRootOptions({
|
|
72
|
+
cwd: fixture.packageDir,
|
|
73
|
+
targetDir: path.relative(fixture.tempRoot, fixture.packageOutputDir),
|
|
74
|
+
}),
|
|
75
|
+
).toThrow("源目录和目标目录解析到了同一路径");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("moveVercelOutputToRoot", () => {
|
|
80
|
+
test("默认应该清空根目录旧产物并复制当前子包的 vercel 输出目录内容", () => {
|
|
81
|
+
const fixture = createMonorepoFixture();
|
|
82
|
+
temporaryDirectories.add(fixture.tempRoot);
|
|
83
|
+
|
|
84
|
+
const result = moveVercelOutputToRoot({
|
|
85
|
+
cwd: fixture.packageDir,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result.copied).toBe(true);
|
|
89
|
+
expect(fs.existsSync(path.join(fixture.rootOutputDir, "stale.txt"))).toBe(false);
|
|
90
|
+
expect(fs.readFileSync(path.join(fixture.rootOutputDir, "config.json"), "utf8")).toBe('{"version":3}');
|
|
91
|
+
expect(fs.readFileSync(path.join(fixture.rootOutputDir, "functions", "index.func"), "utf8")).toBe("hello");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("dry-run 模式下不应该修改根目录内容", () => {
|
|
95
|
+
const fixture = createMonorepoFixture();
|
|
96
|
+
temporaryDirectories.add(fixture.tempRoot);
|
|
97
|
+
|
|
98
|
+
const result = moveVercelOutputToRoot({
|
|
99
|
+
cwd: fixture.packageDir,
|
|
100
|
+
dryRun: true,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(result.copied).toBe(false);
|
|
104
|
+
expect(fs.existsSync(path.join(fixture.rootOutputDir, "stale.txt"))).toBe(true);
|
|
105
|
+
expect(fs.existsSync(path.join(fixture.rootOutputDir, "config.json"))).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("应该支持自定义 sourceDir 与 targetDir", () => {
|
|
109
|
+
const fixture = createMonorepoFixture();
|
|
110
|
+
temporaryDirectories.add(fixture.tempRoot);
|
|
111
|
+
|
|
112
|
+
const customSourceDir = path.join(fixture.packageDir, "custom-output");
|
|
113
|
+
const customTargetDir = path.join(fixture.tempRoot, "deploy-output");
|
|
114
|
+
fs.mkdirSync(customSourceDir, { recursive: true });
|
|
115
|
+
fs.writeFileSync(path.join(customSourceDir, "routes.json"), '{"routes":[]}', "utf8");
|
|
116
|
+
|
|
117
|
+
const result = moveVercelOutputToRoot({
|
|
118
|
+
cwd: fixture.packageDir,
|
|
119
|
+
sourceDir: "custom-output",
|
|
120
|
+
targetDir: "deploy-output",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(result.sourceDir).toBe(customSourceDir);
|
|
124
|
+
expect(result.targetDir).toBe(customTargetDir);
|
|
125
|
+
expect(fs.readFileSync(path.join(customTargetDir, "routes.json"), "utf8")).toBe('{"routes":[]}');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("parseMoveVercelOutputToRootCliArgs", () => {
|
|
130
|
+
test("应该正确解析命令行参数", () => {
|
|
131
|
+
const parsedOptions = parseMoveVercelOutputToRootCliArgs([
|
|
132
|
+
"--root-dir",
|
|
133
|
+
"../../..",
|
|
134
|
+
"--source-dir",
|
|
135
|
+
".vercel/output",
|
|
136
|
+
"--target-dir",
|
|
137
|
+
"deploy-output",
|
|
138
|
+
"--skip-clean",
|
|
139
|
+
"--dry-run",
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
expect(parsedOptions).toEqual({
|
|
143
|
+
rootDir: "../../..",
|
|
144
|
+
sourceDir: ".vercel/output",
|
|
145
|
+
targetDir: "deploy-output",
|
|
146
|
+
skipClean: true,
|
|
147
|
+
dryRun: true,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import consola from "consola";
|
|
5
|
+
import { findMonorepoRoot } from "../../../monorepo";
|
|
6
|
+
|
|
7
|
+
export interface MoveVercelOutputToRootOptions {
|
|
8
|
+
/**
|
|
9
|
+
* 运行目录。
|
|
10
|
+
* 默认值为 `process.cwd()`。
|
|
11
|
+
*/
|
|
12
|
+
cwd?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* monorepo 根目录。
|
|
16
|
+
* 不传时会从 `cwd` 开始,向上查找 `pnpm-workspace.yaml`。
|
|
17
|
+
*/
|
|
18
|
+
rootDir?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 子包内构建产物目录。
|
|
22
|
+
* 相对路径基于 `cwd` 解析。
|
|
23
|
+
*
|
|
24
|
+
* @default ".vercel/output"
|
|
25
|
+
*/
|
|
26
|
+
sourceDir?: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* monorepo 根目录内的目标输出目录。
|
|
30
|
+
* 相对路径基于 monorepo 根目录解析。
|
|
31
|
+
*
|
|
32
|
+
* @default ".vercel/output"
|
|
33
|
+
*/
|
|
34
|
+
targetDir?: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 是否跳过目标目录清理。
|
|
38
|
+
*
|
|
39
|
+
* @default false
|
|
40
|
+
*/
|
|
41
|
+
skipClean?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 仅打印解析结果,不实际复制文件。
|
|
45
|
+
*
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
dryRun?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResolvedMoveVercelOutputToRootOptions {
|
|
52
|
+
cwd: string;
|
|
53
|
+
monorepoRoot: string;
|
|
54
|
+
sourceDir: string;
|
|
55
|
+
targetDir: string;
|
|
56
|
+
skipClean: boolean;
|
|
57
|
+
dryRun: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MoveVercelOutputToRootResult extends ResolvedMoveVercelOutputToRootOptions {
|
|
61
|
+
copied: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 将路径解析为绝对路径。
|
|
66
|
+
*/
|
|
67
|
+
function resolvePathFromBase(baseDir: string, inputPath: string) {
|
|
68
|
+
if (path.isAbsolute(inputPath)) {
|
|
69
|
+
return path.normalize(inputPath);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return path.resolve(baseDir, inputPath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 解析 monorepo 根目录。
|
|
77
|
+
*/
|
|
78
|
+
function resolveMonorepoRoot(cwd: string, rootDir?: string) {
|
|
79
|
+
if (rootDir) {
|
|
80
|
+
const resolvedRoot = resolvePathFromBase(cwd, rootDir);
|
|
81
|
+
const workspaceConfigPath = path.join(resolvedRoot, "pnpm-workspace.yaml");
|
|
82
|
+
if (!fs.existsSync(workspaceConfigPath)) {
|
|
83
|
+
throw new Error(`指定的 rootDir 不是有效的 monorepo 根目录:${resolvedRoot}。缺少 pnpm-workspace.yaml。`);
|
|
84
|
+
}
|
|
85
|
+
return resolvedRoot;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const detectedRoot = findMonorepoRoot(cwd);
|
|
89
|
+
if (detectedRoot) {
|
|
90
|
+
return detectedRoot;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error(`无法从当前目录向上定位 monorepo 根目录:${cwd}。请显式传入 --root-dir 参数。`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 解析脚本所需的全部路径。
|
|
98
|
+
*/
|
|
99
|
+
export function resolveMoveVercelOutputToRootOptions(
|
|
100
|
+
options: MoveVercelOutputToRootOptions = {},
|
|
101
|
+
): ResolvedMoveVercelOutputToRootOptions {
|
|
102
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
103
|
+
const monorepoRoot = resolveMonorepoRoot(cwd, options.rootDir);
|
|
104
|
+
const sourceDir = resolvePathFromBase(cwd, options.sourceDir ?? ".vercel/output");
|
|
105
|
+
const targetDir = resolvePathFromBase(monorepoRoot, options.targetDir ?? ".vercel/output");
|
|
106
|
+
|
|
107
|
+
if (sourceDir === targetDir) {
|
|
108
|
+
throw new Error(`源目录和目标目录解析到了同一路径:${sourceDir}。请调整 sourceDir 或 targetDir。`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
cwd,
|
|
113
|
+
monorepoRoot,
|
|
114
|
+
sourceDir,
|
|
115
|
+
targetDir,
|
|
116
|
+
skipClean: options.skipClean ?? false,
|
|
117
|
+
dryRun: options.dryRun ?? false,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 复制目录内容,而不是把源目录本身嵌套复制进去。
|
|
123
|
+
*/
|
|
124
|
+
function copyDirectoryContents(sourceDir: string, targetDir: string) {
|
|
125
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
126
|
+
|
|
127
|
+
for (const entryName of fs.readdirSync(sourceDir)) {
|
|
128
|
+
const sourceEntry = path.join(sourceDir, entryName);
|
|
129
|
+
const targetEntry = path.join(targetDir, entryName);
|
|
130
|
+
fs.cpSync(sourceEntry, targetEntry, {
|
|
131
|
+
force: true,
|
|
132
|
+
recursive: true,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 将当前子包内的 `.vercel/output` 移动到 monorepo 根目录。
|
|
139
|
+
* @description
|
|
140
|
+
* 该函数服务于 Vercel 在 monorepo 场景下的部署:
|
|
141
|
+
* 1. 构建仍在子包目录内执行
|
|
142
|
+
* 2. 产物默认出现在子包的 `.vercel/output`
|
|
143
|
+
* 3. Vercel 却要求在 monorepo 根目录下读取 `.vercel/output`
|
|
144
|
+
*/
|
|
145
|
+
export function moveVercelOutputToRoot(options: MoveVercelOutputToRootOptions = {}): MoveVercelOutputToRootResult {
|
|
146
|
+
const resolvedOptions = resolveMoveVercelOutputToRootOptions(options);
|
|
147
|
+
|
|
148
|
+
if (!fs.existsSync(resolvedOptions.sourceDir)) {
|
|
149
|
+
throw new Error(`源目录不存在,无法搬运 Vercel 构建产物:${resolvedOptions.sourceDir}。请先在子包内完成构建。`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!fs.statSync(resolvedOptions.sourceDir).isDirectory()) {
|
|
153
|
+
throw new Error(`源路径不是目录:${resolvedOptions.sourceDir}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
consola.info("move-vercel-output-to-root 解析结果");
|
|
157
|
+
consola.log(`- cwd: ${resolvedOptions.cwd}`);
|
|
158
|
+
consola.log(`- monorepoRoot: ${resolvedOptions.monorepoRoot}`);
|
|
159
|
+
consola.log(`- sourceDir: ${resolvedOptions.sourceDir}`);
|
|
160
|
+
consola.log(`- targetDir: ${resolvedOptions.targetDir}`);
|
|
161
|
+
consola.log(`- skipClean: ${resolvedOptions.skipClean}`);
|
|
162
|
+
consola.log(`- dryRun: ${resolvedOptions.dryRun}`);
|
|
163
|
+
|
|
164
|
+
if (resolvedOptions.dryRun) {
|
|
165
|
+
consola.info("dry-run 模式:仅输出路径信息,不执行复制。");
|
|
166
|
+
return {
|
|
167
|
+
...resolvedOptions,
|
|
168
|
+
copied: false,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!resolvedOptions.skipClean) {
|
|
173
|
+
fs.rmSync(resolvedOptions.targetDir, {
|
|
174
|
+
force: true,
|
|
175
|
+
recursive: true,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
copyDirectoryContents(resolvedOptions.sourceDir, resolvedOptions.targetDir);
|
|
180
|
+
|
|
181
|
+
consola.success(`已将 ${resolvedOptions.sourceDir} 搬运到 ${resolvedOptions.targetDir}`);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...resolvedOptions,
|
|
185
|
+
copied: true,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 解析命令行参数。
|
|
191
|
+
*/
|
|
192
|
+
export function parseMoveVercelOutputToRootCliArgs(args: string[]): MoveVercelOutputToRootOptions & { help?: boolean } {
|
|
193
|
+
const options: MoveVercelOutputToRootOptions & { help?: boolean } = {};
|
|
194
|
+
const readFlagValue = (flagName: string, currentIndex: number) => {
|
|
195
|
+
const value = args[currentIndex + 1];
|
|
196
|
+
if (!value || value.startsWith("--")) {
|
|
197
|
+
throw new Error(`参数 ${flagName} 缺少对应的值。`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return value;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
204
|
+
const currentArg = args[index];
|
|
205
|
+
|
|
206
|
+
switch (currentArg) {
|
|
207
|
+
case "--root-dir":
|
|
208
|
+
options.rootDir = readFlagValue(currentArg, index);
|
|
209
|
+
index += 1;
|
|
210
|
+
break;
|
|
211
|
+
case "--source-dir":
|
|
212
|
+
options.sourceDir = readFlagValue(currentArg, index);
|
|
213
|
+
index += 1;
|
|
214
|
+
break;
|
|
215
|
+
case "--target-dir":
|
|
216
|
+
options.targetDir = readFlagValue(currentArg, index);
|
|
217
|
+
index += 1;
|
|
218
|
+
break;
|
|
219
|
+
case "--skip-clean":
|
|
220
|
+
options.skipClean = true;
|
|
221
|
+
break;
|
|
222
|
+
case "--dry-run":
|
|
223
|
+
options.dryRun = true;
|
|
224
|
+
break;
|
|
225
|
+
case "--help":
|
|
226
|
+
case "-h":
|
|
227
|
+
options.help = true;
|
|
228
|
+
break;
|
|
229
|
+
default:
|
|
230
|
+
throw new Error(`不支持的参数:${currentArg}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return options;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* CLI 帮助信息。
|
|
239
|
+
*/
|
|
240
|
+
export function getMoveVercelOutputToRootHelpText() {
|
|
241
|
+
return [
|
|
242
|
+
"move-vercel-output-to-root [options]",
|
|
243
|
+
"",
|
|
244
|
+
"选项:",
|
|
245
|
+
" --root-dir <path> 显式指定 monorepo 根目录",
|
|
246
|
+
" --source-dir <path> 指定子包内构建产物目录,默认 .vercel/output",
|
|
247
|
+
" --target-dir <path> 指定根目录内目标目录,默认 .vercel/output",
|
|
248
|
+
" --skip-clean 跳过目标目录清理",
|
|
249
|
+
" --dry-run 仅打印路径解析结果,不执行复制",
|
|
250
|
+
" -h, --help 查看帮助信息",
|
|
251
|
+
].join("\n");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 执行 CLI。
|
|
256
|
+
*/
|
|
257
|
+
export function runMoveVercelOutputToRootCli(args: string[] = process.argv.slice(2)) {
|
|
258
|
+
const cliOptions = parseMoveVercelOutputToRootCliArgs(args);
|
|
259
|
+
|
|
260
|
+
if (cliOptions.help) {
|
|
261
|
+
console.log(getMoveVercelOutputToRootHelpText());
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
moveVercelOutputToRoot(cliOptions);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function isRunningAsCli() {
|
|
269
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
270
|
+
const entryPath = process.argv[1];
|
|
271
|
+
|
|
272
|
+
if (!entryPath) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return path.resolve(entryPath) === currentFilePath;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (isRunningAsCli()) {
|
|
280
|
+
try {
|
|
281
|
+
runMoveVercelOutputToRootCli();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
consola.error(error instanceof Error ? error.message : String(error));
|
|
284
|
+
process.exitCode = 1;
|
|
285
|
+
}
|
|
286
|
+
}
|