@zhipu/zp-cli 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +119 -8
- package/dist/cli.d.mts +1 -4
- package/dist/cli.mjs +698 -11
- package/dist/config-BZc9pg7J.mjs +105 -0
- package/dist/index.d.mts +23 -3
- package/dist/index.mjs +2 -6
- package/package.json +8 -3
- package/src/templates/nginx-proxy.conf.template +16 -0
- package/src/templates/zpc.config.cjs.template +20 -0
- package/src/templates/zpc.config.cts.template +21 -0
- package/src/templates/zpc.config.js.template +21 -0
- package/src/templates/zpc.config.json.template +20 -0
- package/src/templates/zpc.config.mjs.template +21 -0
- package/src/templates/zpc.config.mts.template +21 -0
- package/src/templates/zpc.config.schema.json +67 -0
- package/src/templates/zpc.config.ts.template +21 -0
package/README.md
CHANGED
|
@@ -1,23 +1,134 @@
|
|
|
1
|
-
#
|
|
1
|
+
# zp-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
内部单命令部署工具(`@zhipu/zp-cli`)。用 `zpc pack` 构建打包、`zpc deploy` 在服务器上解压并挂到 Nginx / PM2。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
路线图见 [docs/ROADMAP.md](./docs/ROADMAP.md)。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 使用
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
# 在项目根生成 zpc.config
|
|
11
|
+
zpc init
|
|
12
|
+
|
|
13
|
+
# 构建并打出 dist.zip(全量)
|
|
14
|
+
zpc pack
|
|
15
|
+
|
|
16
|
+
# 仅 api 增量包
|
|
17
|
+
zpc pack -i
|
|
18
|
+
|
|
19
|
+
# 在服务器上:目录内需有 dist.zip + zpc.config.*(配置不要打进压缩包)
|
|
20
|
+
zpc deploy
|
|
21
|
+
zpc deploy -i # 增量(api 保留 public,跳过 web)
|
|
22
|
+
zpc deploy --full # 强制全量
|
|
23
|
+
zpc deploy --force # 允许抢占冲突的 nginx proxy
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
可选环境变量:`DEPLOY_DIR`、`DEPLOY_INCREMENTAL`、`DEPLOY_FORCE`;测试可用 `ZPC_API_ROOT` / `ZPC_WEB_ROOT` / `ZPC_NGINX_DEFAULT_D` / `ZPC_DEPLOY_SKIP_RUNTIME`。
|
|
27
|
+
|
|
28
|
+
## 部署约定
|
|
29
|
+
|
|
30
|
+
`zpc.config` 中的 `projectName` 即下文 `<system-tag>`。
|
|
31
|
+
|
|
32
|
+
### 目录结构
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
─┬─ etc/nginx/
|
|
36
|
+
│ ├─ default.d/
|
|
37
|
+
│ │ └─ <system-tag>.conf
|
|
38
|
+
│ └─ nginx.conf
|
|
39
|
+
└─ usr/share/nginx/
|
|
40
|
+
├─ api/
|
|
41
|
+
│ └─ <system-tag>/
|
|
42
|
+
│ ├─ <api-1>/
|
|
43
|
+
│ │ ├─ dist
|
|
44
|
+
│ │ ├─ public
|
|
45
|
+
│ │ ├─ bootstrap.js
|
|
46
|
+
│ │ └─ package.json
|
|
47
|
+
│ ├─ <api-2>/
|
|
48
|
+
│ │ ├─ dist
|
|
49
|
+
│ │ ├─ public
|
|
50
|
+
│ │ ├─ bootstrap.js
|
|
51
|
+
│ │ └─ package.json
|
|
52
|
+
│ └─ ...
|
|
53
|
+
└─ html/
|
|
54
|
+
└─ <system-tag>/
|
|
55
|
+
├─ <web-1>/
|
|
56
|
+
│ ├─ assets
|
|
57
|
+
│ └─ index.html
|
|
58
|
+
├─ <web-2>/
|
|
59
|
+
│ ├─ assets
|
|
60
|
+
│ └─ index.html
|
|
61
|
+
└─ ...
|
|
11
62
|
```
|
|
12
63
|
|
|
13
|
-
|
|
64
|
+
### Nginx 配置
|
|
65
|
+
|
|
66
|
+
1. 单系统配置放在 `/etc/nginx/default.d/`,文件名为 `<system-tag>.conf`(当前实现尚未完全对齐,见路线图「进行中」):
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
location /<API_PROXY>/ {
|
|
70
|
+
proxy_pass http://127.0.0.1:<PORT>/;
|
|
71
|
+
proxy_http_version 1.1;
|
|
72
|
+
proxy_set_header Host $host;
|
|
73
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
74
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
75
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
76
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
77
|
+
proxy_set_header Connection "upgrade";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
location = /<API_PROXY> {
|
|
81
|
+
return 301 /<API_PROXY>/;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
2. 宿主机 `nginx.conf` 的 `server` 块须包含 `include`,以便加载系统配置(机器初始化时完成,非 `zpc deploy` 职责):
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
server {
|
|
89
|
+
listen 80;
|
|
90
|
+
listen [::]:80;
|
|
91
|
+
server_name _;
|
|
92
|
+
root /usr/share/nginx/html;
|
|
93
|
+
|
|
94
|
+
# Load configuration files for the default server block.
|
|
95
|
+
include /etc/nginx/default.d/*.conf;
|
|
96
|
+
|
|
97
|
+
error_page 404 /404.html;
|
|
98
|
+
location = /404.html {
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
error_page 500 502 503 504 /50x.html;
|
|
102
|
+
location = /50x.html {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
服务器软件栈(Nginx、Node、pnpm、PM2 等)的一键安装不在本仓库范围,见路线图「展望」。
|
|
108
|
+
|
|
109
|
+
## 开发
|
|
14
110
|
|
|
15
111
|
```bash
|
|
112
|
+
# 安装依赖
|
|
113
|
+
vp install
|
|
114
|
+
# 校验(格式 / lint / 类型)
|
|
115
|
+
vp check
|
|
116
|
+
# 单元测试
|
|
16
117
|
vp test
|
|
118
|
+
# 构建可发布产物
|
|
119
|
+
vp pack
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 发布
|
|
123
|
+
|
|
124
|
+
因 pnpm 安全策略,access token 需配在用户级 npmrc(`~/.npmrc`):
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
|
17
128
|
```
|
|
18
129
|
|
|
19
|
-
|
|
130
|
+
在环境变量中设置 `NPM_TOKEN` 后发布:
|
|
20
131
|
|
|
21
132
|
```bash
|
|
22
|
-
|
|
133
|
+
pnpm publish
|
|
23
134
|
```
|
package/dist/cli.d.mts
CHANGED
package/dist/cli.mjs
CHANGED
|
@@ -1,15 +1,702 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { a as findConfigFile, c as loadConfig, l as templateFileName, n as SCHEMA_FILE_NAME, o as isConfigFormat, r as configFileName, s as listConfigFiles, t as CONFIG_FORMATS } from "./config-BZc9pg7J.mjs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Command } from "@commander-js/extra-typings";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import zlib from "node:zlib";
|
|
8
|
+
//#region package.json
|
|
9
|
+
var version = "0.0.1";
|
|
10
|
+
var description = "内部单命令部署工具";
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/utils/deploy-paths.ts
|
|
13
|
+
/** 增量包标记(打进 dist.zip 根目录;deploy 据此拒绝误全量)。 */
|
|
14
|
+
const INCREMENTAL_MARKER = ".deploy-incremental";
|
|
15
|
+
function envPath(name, fallback) {
|
|
16
|
+
const value = process.env[name];
|
|
17
|
+
return value && value.trim() !== "" ? value : fallback;
|
|
18
|
+
}
|
|
19
|
+
/** API 静态/产物根目录(可用 ZPC_API_ROOT 覆盖,便于测试) */
|
|
20
|
+
function getApiRoot() {
|
|
21
|
+
return envPath("ZPC_API_ROOT", "/usr/share/nginx/api");
|
|
22
|
+
}
|
|
23
|
+
/** Web 静态根目录 */
|
|
24
|
+
function getWebRoot() {
|
|
25
|
+
return envPath("ZPC_WEB_ROOT", "/usr/share/nginx/html");
|
|
26
|
+
}
|
|
27
|
+
/** nginx location 片段目录(需被 server 块 include) */
|
|
28
|
+
function getNginxDefaultD() {
|
|
29
|
+
return envPath("ZPC_NGINX_DEFAULT_D", "/etc/nginx/default.d");
|
|
30
|
+
}
|
|
31
|
+
/** 测试用:跳过 pnpm / pm2 / nginx */
|
|
32
|
+
function shouldSkipDeployRuntime() {
|
|
33
|
+
return process.env.ZPC_DEPLOY_SKIP_RUNTIME === "1";
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/utils/run.ts
|
|
37
|
+
function runCommand(command, args, opts = {}) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const useCmd = opts.windowsCmd && process.platform === "win32";
|
|
40
|
+
const child = spawn(useCmd ? process.env.ComSpec || "cmd.exe" : command, useCmd ? [
|
|
41
|
+
"/d",
|
|
42
|
+
"/s",
|
|
43
|
+
"/c",
|
|
44
|
+
command,
|
|
45
|
+
...args
|
|
46
|
+
] : args, {
|
|
47
|
+
cwd: opts.cwd,
|
|
48
|
+
stdio: opts.stdio ?? "inherit",
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
env: opts.env ? {
|
|
51
|
+
...process.env,
|
|
52
|
+
...opts.env
|
|
53
|
+
} : process.env
|
|
54
|
+
});
|
|
55
|
+
child.on("error", (err) => {
|
|
56
|
+
reject(/* @__PURE__ */ new Error(`无法启动 ${command}: ${err.message}`));
|
|
57
|
+
});
|
|
58
|
+
child.on("exit", (code) => {
|
|
59
|
+
if (code === 0 || opts.ignoreExit) {
|
|
60
|
+
resolve();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
reject(/* @__PURE__ */ new Error(`${command} ${args.join(" ")} 退出码 ${code}${opts.cwd ? ` (cwd: ${opts.cwd})` : ""}`));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/utils/templates.ts
|
|
69
|
+
const require = createRequire(import.meta.url);
|
|
70
|
+
function resolveTemplatesDir() {
|
|
71
|
+
return path.join(path.dirname(require.resolve("@zhipu/zp-cli/package.json")), "src", "templates");
|
|
72
|
+
}
|
|
73
|
+
function resolveTemplateFile(format) {
|
|
74
|
+
const file = path.join(resolveTemplatesDir(), templateFileName(format));
|
|
75
|
+
if (!fs.existsSync(file)) throw new Error(`[init] 缺少模板 ${templateFileName(format)}`);
|
|
76
|
+
return file;
|
|
77
|
+
}
|
|
78
|
+
function resolveSchemaFile() {
|
|
79
|
+
const file = path.join(resolveTemplatesDir(), SCHEMA_FILE_NAME);
|
|
80
|
+
if (!fs.existsSync(file)) throw new Error(`[init] 缺少 ${SCHEMA_FILE_NAME}`);
|
|
81
|
+
return file;
|
|
82
|
+
}
|
|
83
|
+
const NGINX_PROXY_TEMPLATE = "nginx-proxy.conf.template";
|
|
84
|
+
function resolveNginxProxyTemplate() {
|
|
85
|
+
const file = path.join(resolveTemplatesDir(), NGINX_PROXY_TEMPLATE);
|
|
86
|
+
if (!fs.existsSync(file)) throw new Error(`[deploy] 缺少 ${NGINX_PROXY_TEMPLATE}`);
|
|
87
|
+
return file;
|
|
88
|
+
}
|
|
89
|
+
function renderTemplate(template, vars) {
|
|
90
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
91
|
+
if (!(key in vars)) throw new Error(`[template] 缺少变量 ${key}`);
|
|
92
|
+
return String(vars[key]);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/utils/deploy-ops.ts
|
|
97
|
+
function isSafeProxy(proxy) {
|
|
98
|
+
if (!proxy.startsWith("/") || proxy.length < 2) return false;
|
|
99
|
+
if (proxy.includes("..") || proxy.includes("//") || proxy.includes("\\")) return false;
|
|
100
|
+
return /^\/[A-Za-z0-9._/-]+$/.test(proxy);
|
|
101
|
+
}
|
|
102
|
+
function parsePort(env) {
|
|
103
|
+
if (!env || env.PORT == null || env.PORT === "") return null;
|
|
104
|
+
const n = Number(env.PORT);
|
|
105
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) return null;
|
|
106
|
+
return n;
|
|
107
|
+
}
|
|
108
|
+
function toEnvMap(env) {
|
|
109
|
+
const out = { NODE_ENV: "production" };
|
|
110
|
+
if (!env) return out;
|
|
111
|
+
for (const [key, value] of Object.entries(env)) {
|
|
112
|
+
if (value == null) continue;
|
|
113
|
+
out[key] = String(value);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function nginxLocation(proxy) {
|
|
118
|
+
return proxy.endsWith("/") ? proxy : `${proxy}/`;
|
|
119
|
+
}
|
|
120
|
+
function nginxOwnerId(projectName, name) {
|
|
121
|
+
return `${projectName}/${name}`;
|
|
122
|
+
}
|
|
123
|
+
function parseNginxOwnedBy(text) {
|
|
124
|
+
return text.match(/^#\s*owned-by:\s*(\S+)/m)?.[1] ?? null;
|
|
125
|
+
}
|
|
126
|
+
let nginxProxyTemplateCache;
|
|
127
|
+
function loadNginxProxyTemplate() {
|
|
128
|
+
if (nginxProxyTemplateCache === void 0) nginxProxyTemplateCache = fs.readFileSync(resolveNginxProxyTemplate(), "utf8");
|
|
129
|
+
return nginxProxyTemplateCache;
|
|
130
|
+
}
|
|
131
|
+
function renderNginxProxy(proxy, port, ownerId) {
|
|
132
|
+
const loc = nginxLocation(proxy);
|
|
133
|
+
const exact = loc.slice(0, -1);
|
|
134
|
+
return renderTemplate(loadNginxProxyTemplate(), {
|
|
135
|
+
ownerId,
|
|
136
|
+
loc,
|
|
137
|
+
exact,
|
|
138
|
+
port
|
|
139
|
+
}).trimEnd() + "\n";
|
|
140
|
+
}
|
|
141
|
+
function confDefinesProxy(text, proxy) {
|
|
142
|
+
const loc = nginxLocation(proxy);
|
|
143
|
+
const exact = loc.slice(0, -1);
|
|
144
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
145
|
+
return new RegExp(`location\\s*=\\s*${esc(exact)}(?:\\s|\\{)`).test(text) || new RegExp(`location\\s+${esc(loc)}(?:\\s|\\{)`).test(text) || new RegExp(`location\\s+${esc(exact)}(?:\\s|\\{)`).test(text);
|
|
146
|
+
}
|
|
147
|
+
function assertNginxProxyAvailable(proxy, keepPath, ownerId, force, log) {
|
|
148
|
+
if (!fs.existsSync(getNginxDefaultD())) return;
|
|
149
|
+
const keep = path.resolve(keepPath);
|
|
150
|
+
const loc = nginxLocation(proxy);
|
|
151
|
+
for (const name of fs.readdirSync(getNginxDefaultD())) {
|
|
152
|
+
if (!name.endsWith(".conf")) continue;
|
|
153
|
+
const full = path.join(getNginxDefaultD(), name);
|
|
154
|
+
if (path.resolve(full) === keep) continue;
|
|
155
|
+
let text;
|
|
156
|
+
try {
|
|
157
|
+
text = fs.readFileSync(full, "utf8");
|
|
158
|
+
} catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!confDefinesProxy(text, proxy)) continue;
|
|
162
|
+
const owned = parseNginxOwnedBy(text);
|
|
163
|
+
if (force) {
|
|
164
|
+
log(`--force:删除冲突 nginx 配置 ${full}(与 ${loc} 重复,原归属 ${owned ?? "未知"})`);
|
|
165
|
+
fs.unlinkSync(full);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const who = owned ? `归属 ${owned}` : "无归属标记(可能为其它项目或旧配置)";
|
|
169
|
+
throw new Error(`[deploy] nginx proxy ${loc} 已被占用: ${full}(${who})。当前部署归属 ${ownerId}。若确认抢占请加 --force`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** 部署期额外校验:proxy 合法性、配 proxy 时必须有 PORT。 */
|
|
173
|
+
function assertDeployConfig(config) {
|
|
174
|
+
for (const item of config.web ?? []) if (item.proxy != null) throw new Error(`[deploy] 只有 api 项可以配置 proxy(web/${item.name})`);
|
|
175
|
+
for (const item of config.api ?? []) {
|
|
176
|
+
if (item.proxy == null) continue;
|
|
177
|
+
const proxy = item.proxy;
|
|
178
|
+
if (!isSafeProxy(proxy)) throw new Error(`[deploy] api/${item.name} 的 proxy 不合法: ${proxy}`);
|
|
179
|
+
if (parsePort(item.env) == null) throw new Error(`[deploy] api/${item.name} 配置了 proxy,需要 env.PORT(1–65535)`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function isSafeDirName(name) {
|
|
183
|
+
if (!name || name === "." || name === "..") return false;
|
|
184
|
+
return !name.includes("/") && !name.includes("\\") && !name.includes("\0");
|
|
185
|
+
}
|
|
186
|
+
function listNamedDirs(parent) {
|
|
187
|
+
if (!fs.existsSync(parent)) return [];
|
|
188
|
+
return fs.readdirSync(parent, { withFileTypes: true }).filter((d) => d.isDirectory() && isSafeDirName(d.name) && !d.name.startsWith(".")).map((d) => d.name);
|
|
189
|
+
}
|
|
190
|
+
/** 原子风格替换目录:删旧 `.bak` → 现目录改名为 `.bak` → 拷入新内容。 */
|
|
191
|
+
function deployDir(src, dest, log) {
|
|
192
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
193
|
+
const bak = `${dest}.bak`;
|
|
194
|
+
if (fs.existsSync(bak)) {
|
|
195
|
+
log(`删除旧备份 ${bak}`);
|
|
196
|
+
fs.rmSync(bak, {
|
|
197
|
+
recursive: true,
|
|
198
|
+
force: true
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (fs.existsSync(dest)) {
|
|
202
|
+
log(`备份 ${dest} -> ${bak}`);
|
|
203
|
+
fs.renameSync(dest, bak);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
207
|
+
log(`全量部署 ${src} -> ${dest}`);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (fs.existsSync(bak) && !fs.existsSync(dest)) {
|
|
210
|
+
fs.renameSync(bak, dest);
|
|
211
|
+
log(`部署失败,已从备份恢复 ${dest}`);
|
|
212
|
+
}
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const API_INCREMENTAL_FILES = ["package.json", "bootstrap.js"];
|
|
217
|
+
/** api 增量部署:只替换 dist 与根部 package.json / bootstrap.js,不碰 public。 */
|
|
218
|
+
function deployApiIncremental(src, dest, log) {
|
|
219
|
+
const distSrc = path.join(src, "dist");
|
|
220
|
+
if (!fs.existsSync(distSrc)) throw new Error(`[deploy] ${src} 缺少 dist,无法增量部署`);
|
|
221
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
222
|
+
const distDest = path.join(dest, "dist");
|
|
223
|
+
const distBak = `${distDest}.bak`;
|
|
224
|
+
if (fs.existsSync(distBak)) fs.rmSync(distBak, {
|
|
225
|
+
recursive: true,
|
|
226
|
+
force: true
|
|
227
|
+
});
|
|
228
|
+
if (fs.existsSync(distDest)) fs.renameSync(distDest, distBak);
|
|
229
|
+
try {
|
|
230
|
+
fs.cpSync(distSrc, distDest, { recursive: true });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
if (fs.existsSync(distBak) && !fs.existsSync(distDest)) fs.renameSync(distBak, distDest);
|
|
233
|
+
throw err;
|
|
234
|
+
}
|
|
235
|
+
if (fs.existsSync(distBak)) fs.rmSync(distBak, {
|
|
236
|
+
recursive: true,
|
|
237
|
+
force: true
|
|
238
|
+
});
|
|
239
|
+
for (const file of API_INCREMENTAL_FILES) {
|
|
240
|
+
const from = path.join(src, file);
|
|
241
|
+
if (!fs.existsSync(from)) throw new Error(`[deploy] ${src} 缺少 ${file},无法增量部署`);
|
|
242
|
+
fs.cpSync(from, path.join(dest, file));
|
|
243
|
+
}
|
|
244
|
+
fs.mkdirSync(path.join(dest, "public"), { recursive: true });
|
|
245
|
+
log(`增量部署 ${src} -> ${dest}(更新 dist、${API_INCREMENTAL_FILES.join("、")},保留 public)`);
|
|
246
|
+
}
|
|
247
|
+
async function installAndReloadApi(name, dest, item, log) {
|
|
248
|
+
if (!fs.existsSync(path.join(dest, "package.json"))) throw new Error(`[deploy] ${dest} 缺少 package.json,无法 pnpm i`);
|
|
249
|
+
if (!fs.existsSync(path.join(dest, "bootstrap.js"))) throw new Error(`[deploy] ${dest} 缺少 bootstrap.js,无法用 pm2 启动`);
|
|
250
|
+
log(`pnpm i --prod @ ${dest}`);
|
|
251
|
+
await runCommand("pnpm", [
|
|
252
|
+
"i",
|
|
253
|
+
"--prod",
|
|
254
|
+
"--dangerously-allow-all-builds"
|
|
255
|
+
], { cwd: dest });
|
|
256
|
+
const env = toEnvMap(item.env);
|
|
257
|
+
const ecoPath = path.join(dest, "ecosystem.config.cjs");
|
|
258
|
+
fs.writeFileSync(ecoPath, `module.exports = ${JSON.stringify({ apps: [{
|
|
259
|
+
name,
|
|
260
|
+
script: "bootstrap.js",
|
|
261
|
+
env
|
|
262
|
+
}] }, null, 2)};\n`);
|
|
263
|
+
log(`pm2 startOrReload ${name},注入 ${Object.keys(env).join(", ")}`);
|
|
264
|
+
await runCommand("pm2", [
|
|
265
|
+
"startOrReload",
|
|
266
|
+
ecoPath,
|
|
267
|
+
"--update-env"
|
|
268
|
+
], {
|
|
269
|
+
cwd: dest,
|
|
270
|
+
env: {
|
|
271
|
+
...process.env,
|
|
272
|
+
...env
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
async function configureNginxProxies(projectName, apis, force, log) {
|
|
277
|
+
const items = (apis ?? []).filter((item) => item.proxy);
|
|
278
|
+
if (!items.length) {
|
|
279
|
+
log("未配置 api.proxy,跳过 nginx location");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const seen = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const item of items) {
|
|
284
|
+
const key = nginxLocation(item.proxy);
|
|
285
|
+
if (seen.has(key)) throw new Error(`[deploy] 同一份配置里 proxy 重复: ${key}(${seen.get(key)} 与 ${item.name})`);
|
|
286
|
+
seen.set(key, item.name);
|
|
287
|
+
}
|
|
288
|
+
fs.mkdirSync(getNginxDefaultD(), { recursive: true });
|
|
289
|
+
for (const item of items) {
|
|
290
|
+
const port = parsePort(item.env);
|
|
291
|
+
if (port == null) throw new Error(`[deploy] ${item.name} 缺少合法 env.PORT`);
|
|
292
|
+
const ownerId = nginxOwnerId(projectName, item.name);
|
|
293
|
+
const confPath = path.join(getNginxDefaultD(), `${projectName}-${item.name}.conf`);
|
|
294
|
+
assertNginxProxyAvailable(item.proxy, confPath, ownerId, force, log);
|
|
295
|
+
fs.writeFileSync(confPath, renderNginxProxy(item.proxy, port, ownerId));
|
|
296
|
+
log(`nginx ${nginxLocation(item.proxy)} -> 127.0.0.1:${port}(bypass 前缀,owned-by ${ownerId})`);
|
|
297
|
+
}
|
|
298
|
+
await runCommand("nginx", ["-t"]);
|
|
299
|
+
try {
|
|
300
|
+
await runCommand("nginx", ["-s", "reload"]);
|
|
301
|
+
} catch {
|
|
302
|
+
await runCommand("systemctl", ["reload", "nginx"]);
|
|
303
|
+
}
|
|
304
|
+
log("nginx 已 reload");
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/utils/zip.ts
|
|
308
|
+
/** ZIP End of Central Directory 签名 */
|
|
309
|
+
const SIG_EOCD = 101010256;
|
|
310
|
+
/** ZIP Central Directory 文件头签名 */
|
|
311
|
+
const SIG_CD = 33639248;
|
|
312
|
+
/** ZIP Local file header 签名 */
|
|
313
|
+
const SIG_LOCAL = 67324752;
|
|
314
|
+
function readHead(filePath, length = 4) {
|
|
315
|
+
const fd = fs.openSync(filePath, "r");
|
|
316
|
+
try {
|
|
317
|
+
const buf = Buffer.alloc(length);
|
|
318
|
+
const n = fs.readSync(fd, buf, 0, length, 0);
|
|
319
|
+
return buf.subarray(0, n);
|
|
320
|
+
} finally {
|
|
321
|
+
fs.closeSync(fd);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function isZip(filePath) {
|
|
325
|
+
const head = readHead(filePath);
|
|
326
|
+
return head.length >= 4 && head[0] === 80 && head[1] === 75;
|
|
327
|
+
}
|
|
328
|
+
function findEocdOffset(buf) {
|
|
329
|
+
const min = 22;
|
|
330
|
+
if (buf.length < min) throw new Error("zip 文件过小");
|
|
331
|
+
const maxScan = Math.min(buf.length, 65557);
|
|
332
|
+
for (let i = buf.length - min; i >= buf.length - maxScan; i--) if (buf.readUInt32LE(i) === SIG_EOCD) return i;
|
|
333
|
+
throw new Error("找不到 zip 目录结尾(EOCD)");
|
|
334
|
+
}
|
|
335
|
+
function inflateZipData(method, data) {
|
|
336
|
+
if (method === 0) return data;
|
|
337
|
+
if (method === 8) return zlib.inflateRawSync(data);
|
|
338
|
+
throw new Error(`不支持的 zip 压缩方式: ${method}`);
|
|
339
|
+
}
|
|
340
|
+
function resolveZipEntry(dest, name) {
|
|
341
|
+
const normalized = name.replace(/\\/g, "/");
|
|
342
|
+
const parts = normalized.split("/").filter((p) => p && p !== ".");
|
|
343
|
+
if (parts.includes("..")) throw new Error(`非法 zip 路径: ${name}`);
|
|
344
|
+
const target = path.resolve(dest, ...parts);
|
|
345
|
+
const root = path.resolve(dest);
|
|
346
|
+
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
|
|
347
|
+
if (target !== root && !target.startsWith(prefix)) throw new Error(`非法 zip 路径: ${name}`);
|
|
348
|
+
return {
|
|
349
|
+
target,
|
|
350
|
+
isDir: normalized.endsWith("/")
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** 用 Node 解析 ZIP 并解压到 dest(兼容 Windows `tar -a`;不依赖系统 unzip)。 */
|
|
354
|
+
function unzipTo(zipPath, dest) {
|
|
355
|
+
const buf = fs.readFileSync(zipPath);
|
|
356
|
+
const eocd = findEocdOffset(buf);
|
|
357
|
+
const entryCount = buf.readUInt16LE(eocd + 10);
|
|
358
|
+
const cdSize = buf.readUInt32LE(eocd + 12);
|
|
359
|
+
const cdOffset = buf.readUInt32LE(eocd + 16);
|
|
360
|
+
if (cdOffset === 4294967295 || cdSize === 4294967295) throw new Error("不支持 Zip64 压缩包");
|
|
361
|
+
let off = cdOffset;
|
|
362
|
+
const cdEnd = cdOffset + cdSize;
|
|
363
|
+
for (let i = 0; i < entryCount && off + 46 <= buf.length && off < cdEnd; i++) {
|
|
364
|
+
if (buf.readUInt32LE(off) !== SIG_CD) throw new Error("zip central directory 损坏");
|
|
365
|
+
const method = buf.readUInt16LE(off + 10);
|
|
366
|
+
const compSize = buf.readUInt32LE(off + 20);
|
|
367
|
+
const nameLen = buf.readUInt16LE(off + 28);
|
|
368
|
+
const extraLen = buf.readUInt16LE(off + 30);
|
|
369
|
+
const commentLen = buf.readUInt16LE(off + 32);
|
|
370
|
+
const localOff = buf.readUInt32LE(off + 42);
|
|
371
|
+
const name = buf.toString("utf8", off + 46, off + 46 + nameLen);
|
|
372
|
+
off += 46 + nameLen + extraLen + commentLen;
|
|
373
|
+
const { target, isDir } = resolveZipEntry(dest, name);
|
|
374
|
+
if (isDir) {
|
|
375
|
+
fs.mkdirSync(target, { recursive: true });
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (buf.readUInt32LE(localOff) !== SIG_LOCAL) throw new Error(`zip local header 损坏: ${name}`);
|
|
379
|
+
const localNameLen = buf.readUInt16LE(localOff + 26);
|
|
380
|
+
const localExtraLen = buf.readUInt16LE(localOff + 28);
|
|
381
|
+
const dataStart = localOff + 30 + localNameLen + localExtraLen;
|
|
382
|
+
const data = buf.subarray(dataStart, dataStart + compSize);
|
|
383
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
384
|
+
fs.writeFileSync(target, inflateZipData(method, data));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** 解压压缩包到 dest:ZIP 走 Node 实现,否则尝试系统 tar。 */
|
|
388
|
+
async function extractArchive(zipPath, dest) {
|
|
389
|
+
fs.rmSync(dest, {
|
|
390
|
+
recursive: true,
|
|
391
|
+
force: true
|
|
392
|
+
});
|
|
393
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
394
|
+
if (isZip(zipPath)) {
|
|
395
|
+
unzipTo(zipPath, dest);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
await runCommand("tar", [
|
|
399
|
+
"-xf",
|
|
400
|
+
zipPath,
|
|
401
|
+
"-C",
|
|
402
|
+
dest
|
|
403
|
+
]);
|
|
404
|
+
}
|
|
405
|
+
/** 将 staging 打成 zip(Windows / 通用: tar -a)。 */
|
|
406
|
+
async function zipDirectory(stagingDir, zipPath, cwd) {
|
|
407
|
+
if (fs.existsSync(zipPath)) fs.rmSync(zipPath);
|
|
408
|
+
await runCommand("tar", [
|
|
409
|
+
"-a",
|
|
410
|
+
"-cf",
|
|
411
|
+
zipPath,
|
|
412
|
+
"-C",
|
|
413
|
+
stagingDir,
|
|
414
|
+
"."
|
|
415
|
+
], {
|
|
416
|
+
cwd,
|
|
417
|
+
windowsCmd: true
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
//#endregion
|
|
421
|
+
//#region src/commands/deploy.ts
|
|
422
|
+
function log$1(message) {
|
|
423
|
+
console.log(`[deploy] ${message}`);
|
|
424
|
+
}
|
|
425
|
+
function envFlag(name) {
|
|
426
|
+
const value = process.env[name];
|
|
427
|
+
return value === "1" || value === "true";
|
|
428
|
+
}
|
|
429
|
+
function resolveFlags(options) {
|
|
430
|
+
const forceFull = Boolean(options.full);
|
|
431
|
+
const force = Boolean(options.force) || envFlag("DEPLOY_FORCE");
|
|
432
|
+
let wantIncremental = Boolean(options.incremental) || envFlag("DEPLOY_INCREMENTAL");
|
|
433
|
+
if (forceFull) wantIncremental = false;
|
|
434
|
+
return {
|
|
435
|
+
wantIncremental,
|
|
436
|
+
forceFull,
|
|
437
|
+
force
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function hasProjectFiles(dir) {
|
|
441
|
+
return fs.existsSync(path.join(dir, "dist.zip")) && Boolean(findConfigFile(dir));
|
|
442
|
+
}
|
|
443
|
+
function resolveProjectDir(dirArg) {
|
|
444
|
+
let dir;
|
|
445
|
+
if (dirArg) {
|
|
446
|
+
const abs = path.resolve(dirArg);
|
|
447
|
+
if (!fs.existsSync(abs)) throw new Error(`[deploy] 找不到路径: ${abs}`);
|
|
448
|
+
dir = fs.statSync(abs).isDirectory() ? abs : path.dirname(abs);
|
|
449
|
+
} else if (process.env.DEPLOY_DIR) dir = path.resolve(process.env.DEPLOY_DIR);
|
|
450
|
+
else dir = process.cwd();
|
|
451
|
+
if (!hasProjectFiles(dir)) throw new Error(`[deploy] ${dir} 中需要同时有 dist.zip 和 zpc.config.*(配置请单独上传,不要打进压缩包)`);
|
|
452
|
+
return dir;
|
|
453
|
+
}
|
|
454
|
+
async function deployKind(stagingDir, items, kind, destRoot, projectName, incremental) {
|
|
455
|
+
const packed = new Set(listNamedDirs(path.join(stagingDir, kind)));
|
|
456
|
+
const configured = items ?? [];
|
|
457
|
+
for (const extra of packed) if (!configured.some((item) => item.name === extra)) log$1(`跳过压缩包中的 ${kind}/${extra}(未在配置中列出)`);
|
|
458
|
+
for (const item of configured) {
|
|
459
|
+
const src = path.join(stagingDir, kind, item.name);
|
|
460
|
+
if (!fs.existsSync(src)) throw new Error(`[deploy] 配置中有 ${kind}/${item.name},但压缩包中没有对应目录`);
|
|
461
|
+
const dest = path.join(destRoot, projectName, item.name);
|
|
462
|
+
const pm2Name = `${projectName}-${item.name}`;
|
|
463
|
+
if (kind === "api" && incremental && fs.existsSync(dest)) deployApiIncremental(src, dest, log$1);
|
|
464
|
+
else {
|
|
465
|
+
if (kind === "api" && incremental && !fs.existsSync(dest)) log$1(`${dest} 不存在,增量改为全量部署`);
|
|
466
|
+
deployDir(src, dest, log$1);
|
|
467
|
+
}
|
|
468
|
+
if (kind === "api" && !shouldSkipDeployRuntime()) await installAndReloadApi(pm2Name, dest, item, log$1);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function runDeploy(dir, options) {
|
|
472
|
+
const { wantIncremental, forceFull, force } = resolveFlags(options);
|
|
473
|
+
const projectDir = resolveProjectDir(dir);
|
|
474
|
+
const stagingDir = path.join(projectDir, ".deploy-staging");
|
|
475
|
+
const zipPath = path.join(projectDir, "dist.zip");
|
|
476
|
+
const { file, config } = await loadConfig(projectDir);
|
|
477
|
+
assertDeployConfig(config);
|
|
478
|
+
log$1(`项目目录: ${projectDir}`);
|
|
479
|
+
log$1(`压缩包: ${zipPath}`);
|
|
480
|
+
log$1(`配置: ${file}`);
|
|
481
|
+
log$1(`项目: ${config.projectName}`);
|
|
482
|
+
try {
|
|
483
|
+
log$1(`解压 ${zipPath}`);
|
|
484
|
+
await extractArchive(zipPath, stagingDir);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
fs.rmSync(stagingDir, {
|
|
487
|
+
recursive: true,
|
|
488
|
+
force: true
|
|
489
|
+
});
|
|
490
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
491
|
+
throw new Error(`[deploy] 解压失败: ${message}`);
|
|
7
492
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
493
|
+
const packIsIncremental = fs.existsSync(path.join(stagingDir, INCREMENTAL_MARKER));
|
|
494
|
+
let incremental = wantIncremental || packIsIncremental;
|
|
495
|
+
try {
|
|
496
|
+
if (packIsIncremental && forceFull) throw new Error("[deploy] 当前 dist.zip 是增量包(含 .deploy-incremental),不能 --full,请使用 --incremental,或重新 pack 全量包");
|
|
497
|
+
if (packIsIncremental && !wantIncremental) throw new Error("[deploy] 当前 dist.zip 是增量包(由 zpc pack -i 生成),请加上 --incremental / -i,以免全量覆盖清空 public");
|
|
498
|
+
incremental = wantIncremental || packIsIncremental;
|
|
499
|
+
log$1(`模式: ${incremental ? "增量(api 保留 public,跳过 web)" : "全量"}`);
|
|
500
|
+
if (force) log$1("已启用 --force(允许抢占冲突的 nginx proxy)");
|
|
501
|
+
await deployKind(stagingDir, config.api, "api", getApiRoot(), config.projectName, incremental);
|
|
502
|
+
if (!incremental) await deployKind(stagingDir, config.web, "web", getWebRoot(), config.projectName, false);
|
|
503
|
+
else if (config.web?.length) log$1("增量模式跳过 web 部署");
|
|
504
|
+
} finally {
|
|
505
|
+
fs.rmSync(stagingDir, {
|
|
506
|
+
recursive: true,
|
|
507
|
+
force: true
|
|
508
|
+
});
|
|
11
509
|
}
|
|
510
|
+
if (!shouldSkipDeployRuntime()) {
|
|
511
|
+
await configureNginxProxies(config.projectName, config.api, force, log$1);
|
|
512
|
+
if (config.api?.length) await runCommand("pm2", ["save"], { ignoreExit: true });
|
|
513
|
+
}
|
|
514
|
+
const apiCount = config.api?.length ?? 0;
|
|
515
|
+
const webCount = incremental ? 0 : config.web?.length ?? 0;
|
|
516
|
+
const prefix = `${config.projectName}/<name>`;
|
|
517
|
+
log$1(`完成: api ${apiCount} 个 -> ${getApiRoot()}/${prefix},web ${webCount} 个 -> ${getWebRoot()}/${prefix}`);
|
|
518
|
+
}
|
|
519
|
+
function createDeployCommand() {
|
|
520
|
+
return new Command("deploy").description("按 zpc.config 将 dist.zip 部署到服务器").argument("[dir]", "项目目录,默认为当前目录").option("-i, --incremental", "增量部署(api 保留 public,跳过 web)").option("--full", "全量部署").option("--force", "允许抢占冲突的 nginx proxy").action(async (dir, options) => {
|
|
521
|
+
await runDeploy(dir, options);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
//#endregion
|
|
525
|
+
//#region src/commands/init.ts
|
|
526
|
+
async function runInit(dir, options) {
|
|
527
|
+
const format = resolveFormat(options.template);
|
|
528
|
+
const root = path.resolve(dir ?? process.cwd());
|
|
529
|
+
const dest = path.join(root, configFileName(format));
|
|
530
|
+
const schemaDest = path.join(root, SCHEMA_FILE_NAME);
|
|
531
|
+
const existing = listConfigFiles(root);
|
|
532
|
+
if (existing.length > 0 && !options.force) {
|
|
533
|
+
const names = existing.map((file) => path.basename(file)).join("、");
|
|
534
|
+
throw new Error(`[init] 已存在 ${names},如需覆盖请加 --force`);
|
|
535
|
+
}
|
|
536
|
+
fs.mkdirSync(root, { recursive: true });
|
|
537
|
+
for (const file of existing) if (file !== dest) fs.rmSync(file);
|
|
538
|
+
fs.copyFileSync(resolveTemplateFile(format), dest);
|
|
539
|
+
console.log(`[init] 已写入 ${dest}`);
|
|
540
|
+
if (format === "json") {
|
|
541
|
+
fs.copyFileSync(resolveSchemaFile(), schemaDest);
|
|
542
|
+
console.log(`[init] 已写入 ${schemaDest}`);
|
|
543
|
+
} else if (fs.existsSync(schemaDest)) fs.rmSync(schemaDest);
|
|
544
|
+
}
|
|
545
|
+
function resolveFormat(template) {
|
|
546
|
+
const format = template ?? "json";
|
|
547
|
+
if (!isConfigFormat(format)) throw new Error(`[init] 不支持的模板 ${format},可选:${CONFIG_FORMATS.join(", ")}`);
|
|
548
|
+
return format;
|
|
12
549
|
}
|
|
13
|
-
|
|
550
|
+
function createInitCommand() {
|
|
551
|
+
return new Command("init").description("在项目目录生成 zpc.config 配置文件").argument("[dir]", "项目目录,默认为当前目录").option("-f, --force", "覆盖已有 zpc.config 文件").option("-t, --template <format>", `配置文件格式:${CONFIG_FORMATS.join(" / ")}`, "json").action(async (dir, options) => {
|
|
552
|
+
await runInit(dir, options);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
//#endregion
|
|
556
|
+
//#region src/commands/pack.ts
|
|
557
|
+
function log(message) {
|
|
558
|
+
console.log(`[pack] ${message}`);
|
|
559
|
+
}
|
|
560
|
+
function resolveIncremental(options) {
|
|
561
|
+
if (options.full) return false;
|
|
562
|
+
return Boolean(options.incremental);
|
|
563
|
+
}
|
|
564
|
+
function resolveProjects(root, config, incremental) {
|
|
565
|
+
const projects = [];
|
|
566
|
+
const kinds = incremental ? ["api"] : ["api", "web"];
|
|
567
|
+
for (const kind of kinds) {
|
|
568
|
+
const items = config[kind];
|
|
569
|
+
if (items == null) continue;
|
|
570
|
+
for (const item of items) {
|
|
571
|
+
const absPath = path.resolve(root, item.path);
|
|
572
|
+
if (!fs.existsSync(absPath)) throw new Error(`[pack] 项目目录不存在: ${absPath} (${kind}/${item.name})`);
|
|
573
|
+
const pkgJson = path.join(absPath, "package.json");
|
|
574
|
+
if (!fs.existsSync(pkgJson)) throw new Error(`[pack] 缺少 package.json: ${pkgJson} (${kind}/${item.name})`);
|
|
575
|
+
projects.push({
|
|
576
|
+
...item,
|
|
577
|
+
kind,
|
|
578
|
+
absPath,
|
|
579
|
+
distPath: path.join(absPath, "dist")
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (projects.length === 0) throw new Error(incremental ? "[pack] 增量打包需要配置中至少有一个 api 项目" : "[pack] 配置中没有 api / web 项目");
|
|
584
|
+
return projects;
|
|
585
|
+
}
|
|
586
|
+
function resolveBuildEnv(project, config) {
|
|
587
|
+
const env = { ...process.env };
|
|
588
|
+
if (project.buildEnv) for (const [key, value] of Object.entries(project.buildEnv)) env[key] = String(value);
|
|
589
|
+
if (project.kind === "web" && !env.VITE_API_PROXY) {
|
|
590
|
+
let proxy = project.proxy;
|
|
591
|
+
if (!proxy) {
|
|
592
|
+
const apis = (config.api ?? []).filter((a) => typeof a.proxy === "string" && a.proxy);
|
|
593
|
+
if (apis.length === 1) proxy = apis[0].proxy;
|
|
594
|
+
}
|
|
595
|
+
if (proxy) env.VITE_API_PROXY = proxy.endsWith("/") ? proxy.slice(0, -1) : proxy;
|
|
596
|
+
}
|
|
597
|
+
return env;
|
|
598
|
+
}
|
|
599
|
+
async function buildAll(projects, config) {
|
|
600
|
+
log(`并行构建 ${projects.length} 个项目`);
|
|
601
|
+
await Promise.all(projects.map(async (project) => {
|
|
602
|
+
const env = resolveBuildEnv(project, config);
|
|
603
|
+
if (project.kind === "web" && env.VITE_API_PROXY) log(`build ${project.kind}/${project.name} VITE_API_PROXY=${env.VITE_API_PROXY}`);
|
|
604
|
+
else log(`build ${project.kind}/${project.name} -> ${project.absPath}`);
|
|
605
|
+
await runCommand("npm", ["run", "build"], {
|
|
606
|
+
cwd: project.absPath,
|
|
607
|
+
env,
|
|
608
|
+
windowsCmd: true
|
|
609
|
+
});
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
function copyRequiredFile(src, destDir, label) {
|
|
613
|
+
if (!fs.existsSync(src)) throw new Error(`[pack] 缺少 ${label}: ${src}`);
|
|
614
|
+
fs.cpSync(src, path.join(destDir, path.basename(src)));
|
|
615
|
+
}
|
|
616
|
+
function stageApi(project, dest, incremental) {
|
|
617
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
618
|
+
fs.cpSync(project.distPath, path.join(dest, "dist"), { recursive: true });
|
|
619
|
+
copyRequiredFile(path.join(project.absPath, "package.json"), dest, "package.json");
|
|
620
|
+
copyRequiredFile(path.join(project.absPath, "bootstrap.js"), dest, "bootstrap.js");
|
|
621
|
+
if (incremental) {
|
|
622
|
+
log(`api/${project.name} 增量:跳过 public`);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const publicDir = path.join(project.absPath, "public");
|
|
626
|
+
const destPublic = path.join(dest, "public");
|
|
627
|
+
if (fs.existsSync(publicDir)) fs.cpSync(publicDir, destPublic, { recursive: true });
|
|
628
|
+
else fs.mkdirSync(destPublic, { recursive: true });
|
|
629
|
+
}
|
|
630
|
+
function stageDist(stagingDir, root, projects, incremental) {
|
|
631
|
+
fs.rmSync(stagingDir, {
|
|
632
|
+
recursive: true,
|
|
633
|
+
force: true
|
|
634
|
+
});
|
|
635
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
636
|
+
if (incremental) fs.writeFileSync(path.join(stagingDir, INCREMENTAL_MARKER), "1\n");
|
|
637
|
+
for (const project of projects) {
|
|
638
|
+
if (!fs.existsSync(project.distPath)) throw new Error(`[pack] 构建产物不存在: ${project.distPath} (${project.kind}/${project.name})`);
|
|
639
|
+
const dest = path.join(stagingDir, project.kind, project.name);
|
|
640
|
+
if (project.kind === "api") stageApi(project, dest, incremental);
|
|
641
|
+
else fs.cpSync(project.distPath, dest, { recursive: true });
|
|
642
|
+
log(`收集 ${project.kind}/${project.name} -> ${path.relative(root, dest)}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function runPack(dir, options) {
|
|
646
|
+
const root = path.resolve(dir ?? process.cwd());
|
|
647
|
+
if (!fs.existsSync(root)) throw new Error(`[pack] 找不到路径: ${root}`);
|
|
648
|
+
const projectRoot = fs.statSync(root).isDirectory() ? root : path.dirname(root);
|
|
649
|
+
const incremental = resolveIncremental(options);
|
|
650
|
+
const stagingDir = path.join(projectRoot, ".packup-staging");
|
|
651
|
+
const zipPath = path.join(projectRoot, "dist.zip");
|
|
652
|
+
const { file, config } = await loadConfig(projectRoot);
|
|
653
|
+
log(`项目目录: ${projectRoot}`);
|
|
654
|
+
log(`配置: ${file}`);
|
|
655
|
+
log(`项目: ${config.projectName}`);
|
|
656
|
+
log(`模式: ${incremental ? "增量(仅 api,不含 public)" : "全量"}`);
|
|
657
|
+
const projects = resolveProjects(projectRoot, config, incremental);
|
|
658
|
+
await buildAll(projects, config);
|
|
659
|
+
stageDist(stagingDir, projectRoot, projects, incremental);
|
|
660
|
+
try {
|
|
661
|
+
log(`压缩 ${path.relative(projectRoot, zipPath)}`);
|
|
662
|
+
await zipDirectory(stagingDir, zipPath, projectRoot);
|
|
663
|
+
} catch (err) {
|
|
664
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
665
|
+
throw new Error(`[pack] 打包失败(需要系统 tar): ${message}`);
|
|
666
|
+
} finally {
|
|
667
|
+
fs.rmSync(stagingDir, {
|
|
668
|
+
recursive: true,
|
|
669
|
+
force: true
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
log(`完成: ${zipPath}`);
|
|
673
|
+
if (incremental) log("请使用: zpc deploy --incremental");
|
|
674
|
+
}
|
|
675
|
+
function createPackCommand() {
|
|
676
|
+
return new Command("pack").description("按 zpc.config 构建并打包 dist.zip").argument("[dir]", "项目目录,默认为当前目录").option("-i, --incremental", "增量打包(仅 api,不含 public)").option("--full", "全量打包").action(async (dir, options) => {
|
|
677
|
+
await runPack(dir, options);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/utils/program.ts
|
|
682
|
+
function createProgram() {
|
|
683
|
+
const program = new Command().name("zpc").description(description).version(version).showHelpAfterError().addCommand(createInitCommand()).addCommand(createPackCommand()).addCommand(createDeployCommand());
|
|
684
|
+
program.action(() => {
|
|
685
|
+
program.outputHelp();
|
|
686
|
+
});
|
|
687
|
+
return program;
|
|
688
|
+
}
|
|
689
|
+
async function runCli(argv = process.argv) {
|
|
690
|
+
try {
|
|
691
|
+
await createProgram().parseAsync(argv);
|
|
692
|
+
} catch (err) {
|
|
693
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
694
|
+
console.error(message);
|
|
695
|
+
process.exitCode = 1;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/cli.ts
|
|
700
|
+
runCli();
|
|
14
701
|
//#endregion
|
|
15
|
-
export {
|
|
702
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
//#region src/utils/config.ts
|
|
5
|
+
const CONFIG_FILE_PREFIX = "zpc.config";
|
|
6
|
+
const SCHEMA_FILE_NAME = `${CONFIG_FILE_PREFIX}.schema.json`;
|
|
7
|
+
const CONFIG_FORMATS = [
|
|
8
|
+
"ts",
|
|
9
|
+
"mts",
|
|
10
|
+
"cts",
|
|
11
|
+
"js",
|
|
12
|
+
"mjs",
|
|
13
|
+
"cjs",
|
|
14
|
+
"json"
|
|
15
|
+
];
|
|
16
|
+
function defineConfig(config) {
|
|
17
|
+
return config;
|
|
18
|
+
}
|
|
19
|
+
function isConfigFormat(value) {
|
|
20
|
+
return CONFIG_FORMATS.includes(value);
|
|
21
|
+
}
|
|
22
|
+
function configFileName(format) {
|
|
23
|
+
return `${CONFIG_FILE_PREFIX}.${format}`;
|
|
24
|
+
}
|
|
25
|
+
function templateFileName(format) {
|
|
26
|
+
return `${configFileName(format)}.template`;
|
|
27
|
+
}
|
|
28
|
+
const jiti = createJiti(import.meta.url);
|
|
29
|
+
function listConfigFiles(dir) {
|
|
30
|
+
return CONFIG_FORMATS.map((format) => path.join(dir, configFileName(format))).filter((file) => fs.existsSync(file));
|
|
31
|
+
}
|
|
32
|
+
function findConfigFile(dir) {
|
|
33
|
+
return listConfigFiles(dir)[0];
|
|
34
|
+
}
|
|
35
|
+
async function loadConfig(dir) {
|
|
36
|
+
const file = findConfigFile(dir);
|
|
37
|
+
if (!file) throw new Error(`[config] 在 ${dir} 找不到 ${CONFIG_FILE_PREFIX}.{${CONFIG_FORMATS.join(",")}},请先运行 zpc init`);
|
|
38
|
+
let loaded;
|
|
39
|
+
try {
|
|
40
|
+
loaded = await readConfigFile(file);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
+
throw new Error(`[config] 无法加载 ${file}: ${message}`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
file,
|
|
47
|
+
config: validateConfig(loaded, file)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function readConfigFile(file) {
|
|
51
|
+
if (file.endsWith(".json")) return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
52
|
+
return await jiti.import(file, { default: true });
|
|
53
|
+
}
|
|
54
|
+
function validateConfig(config, source) {
|
|
55
|
+
if (!isPlainObject(config)) throw new Error(`[config] ${source} 必须导出一个对象`);
|
|
56
|
+
if (!isSafeName(config.projectName)) throw new Error(`[config] ${source} 缺少合法 projectName`);
|
|
57
|
+
const api = optionalAppList(config.api, "api", source);
|
|
58
|
+
const web = optionalAppList(config.web, "web", source);
|
|
59
|
+
if (api.length === 0 && web.length === 0) throw new Error(`[config] ${source} 中没有 api / web 项目`);
|
|
60
|
+
return {
|
|
61
|
+
projectName: config.projectName,
|
|
62
|
+
...api.length ? { api } : {},
|
|
63
|
+
...web.length ? { web } : {}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function optionalAppList(value, kind, source) {
|
|
67
|
+
if (value == null) return [];
|
|
68
|
+
if (!Array.isArray(value)) throw new Error(`[config] ${source} 中 ${kind} 必须是数组`);
|
|
69
|
+
return value.map((item, index) => parseAppConfig(item, `${source} ${kind}[${index}]`));
|
|
70
|
+
}
|
|
71
|
+
function parseAppConfig(item, label) {
|
|
72
|
+
if (!isPlainObject(item)) throw new Error(`[config] ${label} 必须是对象`);
|
|
73
|
+
if (typeof item.path !== "string" || item.path.trim() === "") throw new Error(`[config] ${label} 缺少 path`);
|
|
74
|
+
if (!isSafeName(item.name)) throw new Error(`[config] ${label} 缺少合法 name`);
|
|
75
|
+
const parsed = {
|
|
76
|
+
path: item.path,
|
|
77
|
+
name: item.name
|
|
78
|
+
};
|
|
79
|
+
if (item.proxy != null) {
|
|
80
|
+
if (typeof item.proxy !== "string") throw new Error(`[config] ${label} 的 proxy 必须是字符串`);
|
|
81
|
+
parsed.proxy = item.proxy;
|
|
82
|
+
}
|
|
83
|
+
if (item.buildEnv != null) parsed.buildEnv = parseEnvMap(item.buildEnv, `${label} buildEnv`);
|
|
84
|
+
if (item.env != null) parsed.env = parseEnvMap(item.env, `${label} env`);
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
function parseEnvMap(value, label) {
|
|
88
|
+
if (!isPlainObject(value)) throw new Error(`[config] ${label} 必须是对象`);
|
|
89
|
+
const out = {};
|
|
90
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
91
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`[config] ${label} 含非法环境变量名: ${key}`);
|
|
92
|
+
if (typeof entry !== "string" && typeof entry !== "number" && typeof entry !== "boolean") throw new Error(`[config] ${label}.${key} 必须是 string / number / boolean`);
|
|
93
|
+
out[key] = entry;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
function isPlainObject(value) {
|
|
98
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
99
|
+
}
|
|
100
|
+
function isSafeName(name) {
|
|
101
|
+
if (typeof name !== "string" || name === "" || name === "." || name === "..") return false;
|
|
102
|
+
return !name.includes("/") && !name.includes("\\") && !name.includes(String.fromCharCode(0));
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { findConfigFile as a, loadConfig as c, defineConfig as i, templateFileName as l, SCHEMA_FILE_NAME as n, isConfigFormat as o, configFileName as r, listConfigFiles as s, CONFIG_FORMATS as t };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
//#region src/
|
|
2
|
-
declare
|
|
1
|
+
//#region src/utils/config.d.ts
|
|
2
|
+
declare const CONFIG_FORMATS: readonly ["ts", "mts", "cts", "js", "mjs", "cjs", "json"];
|
|
3
|
+
type ConfigFormat = (typeof CONFIG_FORMATS)[number];
|
|
4
|
+
type ZpcEnvMap = Record<string, string | number | boolean>;
|
|
5
|
+
interface ZpcAppConfig {
|
|
6
|
+
/** 相对项目根的源码目录 */
|
|
7
|
+
path: string;
|
|
8
|
+
/** 打包 / 部署目录名 */
|
|
9
|
+
name: string;
|
|
10
|
+
/** api 对外路径前缀;web 未写 buildEnv.VITE_API_PROXY 时也可作回退 */
|
|
11
|
+
proxy?: string;
|
|
12
|
+
/** 仅构建期注入 */
|
|
13
|
+
buildEnv?: ZpcEnvMap;
|
|
14
|
+
/** 仅运行时注入(pm2) */
|
|
15
|
+
env?: ZpcEnvMap;
|
|
16
|
+
}
|
|
17
|
+
interface ZpcConfig {
|
|
18
|
+
projectName: string;
|
|
19
|
+
api?: ZpcAppConfig[];
|
|
20
|
+
web?: ZpcAppConfig[];
|
|
21
|
+
}
|
|
22
|
+
declare function defineConfig(config: ZpcConfig): ZpcConfig;
|
|
3
23
|
//#endregion
|
|
4
|
-
export {
|
|
24
|
+
export { type ConfigFormat, type ZpcAppConfig, type ZpcConfig, type ZpcEnvMap, defineConfig };
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhipu/zp-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "内部单命令部署工具",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zpc": "./dist/cli.mjs"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"src/templates"
|
|
10
11
|
],
|
|
11
12
|
"type": "module",
|
|
12
13
|
"exports": {
|
|
13
14
|
".": "./dist/index.mjs",
|
|
14
|
-
"./cli": "./dist/cli.mjs",
|
|
15
15
|
"./package.json": "./package.json"
|
|
16
16
|
},
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@commander-js/extra-typings": "^15.0.0",
|
|
22
|
+
"commander": "^15.0.0",
|
|
23
|
+
"jiti": "^2.7.0"
|
|
24
|
+
},
|
|
20
25
|
"devDependencies": {
|
|
21
26
|
"@types/node": "^26.1.1",
|
|
22
27
|
"bumpp": "^11.1.0",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# generated by zpc deploy
|
|
2
|
+
# owned-by: {{ownerId}}
|
|
3
|
+
location {{loc}} {
|
|
4
|
+
proxy_pass http://127.0.0.1:{{port}}/;
|
|
5
|
+
proxy_http_version 1.1;
|
|
6
|
+
proxy_set_header Host $host;
|
|
7
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
8
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
9
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
10
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
11
|
+
proxy_set_header Connection "upgrade";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
location = {{exact}} {
|
|
15
|
+
return 301 {{loc}};
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** @type {import("@zhipu/zp-cli").ZpcConfig} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
projectName: "my-project",
|
|
4
|
+
api: [
|
|
5
|
+
{
|
|
6
|
+
path: "api",
|
|
7
|
+
name: "api",
|
|
8
|
+
proxy: "/api",
|
|
9
|
+
env: {
|
|
10
|
+
PORT: 3000,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
web: [
|
|
15
|
+
{
|
|
16
|
+
path: "web",
|
|
17
|
+
name: "web",
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "@zhipu/zp-cli";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
projectName: "my-project",
|
|
5
|
+
api: [
|
|
6
|
+
{
|
|
7
|
+
path: "api",
|
|
8
|
+
name: "api",
|
|
9
|
+
proxy: "/api",
|
|
10
|
+
env: {
|
|
11
|
+
PORT: 3000,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
web: [
|
|
16
|
+
{
|
|
17
|
+
path: "web",
|
|
18
|
+
name: "web",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "@zhipu/zp-cli";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
projectName: "my-project",
|
|
5
|
+
api: [
|
|
6
|
+
{
|
|
7
|
+
path: "api",
|
|
8
|
+
name: "api",
|
|
9
|
+
proxy: "/api",
|
|
10
|
+
env: {
|
|
11
|
+
PORT: 3000,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
web: [
|
|
16
|
+
{
|
|
17
|
+
path: "web",
|
|
18
|
+
name: "web",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./zpc.config.schema.json",
|
|
3
|
+
"projectName": "my-project",
|
|
4
|
+
"api": [
|
|
5
|
+
{
|
|
6
|
+
"path": "api",
|
|
7
|
+
"name": "api",
|
|
8
|
+
"proxy": "/api",
|
|
9
|
+
"env": {
|
|
10
|
+
"PORT": 3000
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"web": [
|
|
15
|
+
{
|
|
16
|
+
"path": "web",
|
|
17
|
+
"name": "web"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "@zhipu/zp-cli";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
projectName: "my-project",
|
|
5
|
+
api: [
|
|
6
|
+
{
|
|
7
|
+
path: "api",
|
|
8
|
+
name: "api",
|
|
9
|
+
proxy: "/api",
|
|
10
|
+
env: {
|
|
11
|
+
PORT: 3000,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
web: [
|
|
16
|
+
{
|
|
17
|
+
path: "web",
|
|
18
|
+
name: "web",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "@zhipu/zp-cli";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
projectName: "my-project",
|
|
5
|
+
api: [
|
|
6
|
+
{
|
|
7
|
+
path: "api",
|
|
8
|
+
name: "api",
|
|
9
|
+
proxy: "/api",
|
|
10
|
+
env: {
|
|
11
|
+
PORT: 3000,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
web: [
|
|
16
|
+
{
|
|
17
|
+
path: "web",
|
|
18
|
+
name: "web",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "zpc.config",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": ["projectName"],
|
|
7
|
+
"anyOf": [{ "required": ["api"] }, { "required": ["web"] }],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string"
|
|
11
|
+
},
|
|
12
|
+
"projectName": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"minLength": 1,
|
|
15
|
+
"description": "项目名,部署路径为 <root>/<projectName>/<name>"
|
|
16
|
+
},
|
|
17
|
+
"api": {
|
|
18
|
+
"$ref": "#/definitions/appList"
|
|
19
|
+
},
|
|
20
|
+
"web": {
|
|
21
|
+
"$ref": "#/definitions/appList"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"definitions": {
|
|
25
|
+
"appList": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"items": {
|
|
28
|
+
"$ref": "#/definitions/app"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"app": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"required": ["path", "name"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"path": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"minLength": 1,
|
|
39
|
+
"description": "相对项目根的源码目录"
|
|
40
|
+
},
|
|
41
|
+
"name": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"minLength": 1,
|
|
44
|
+
"description": "打包 / 部署目录名"
|
|
45
|
+
},
|
|
46
|
+
"proxy": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "api 对外路径前缀;web 未写 buildEnv.VITE_API_PROXY 时也可作回退"
|
|
49
|
+
},
|
|
50
|
+
"buildEnv": {
|
|
51
|
+
"$ref": "#/definitions/envMap",
|
|
52
|
+
"description": "仅构建期注入"
|
|
53
|
+
},
|
|
54
|
+
"env": {
|
|
55
|
+
"$ref": "#/definitions/envMap",
|
|
56
|
+
"description": "仅运行时注入(pm2)"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"envMap": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"additionalProperties": {
|
|
63
|
+
"type": ["string", "number", "boolean"]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from "@zhipu/zp-cli";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
projectName: "my-project",
|
|
5
|
+
api: [
|
|
6
|
+
{
|
|
7
|
+
path: "api",
|
|
8
|
+
name: "api",
|
|
9
|
+
proxy: "/api",
|
|
10
|
+
env: {
|
|
11
|
+
PORT: 3000,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
web: [
|
|
16
|
+
{
|
|
17
|
+
path: "web",
|
|
18
|
+
name: "web",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
});
|