@cloudbase/manager-node 5.9.3 → 5.10.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/CHANGELOG.md +6 -0
- package/changelog_content_en.md +2 -3
- package/lib/database/index.js +76 -0
- package/lib/deploy/DeployOrchestrator.js +225 -25
- package/lib/deploy/FunctionDeployer.js +5 -2
- package/lib/deploy/StaticDeployer.js +469 -142
- package/lib/deploy/framework.js +4 -3
- package/lib/env/index.js +2 -1
- package/lib/error-types.js +8 -1
- package/lib/function/index.js +2 -2
- package/lib/hosting/index.js +9 -3
- package/lib/platform/index.js +279 -0
- package/lib/storage/index.js +46 -18
- package/lib/utils/cos-upload.js +69 -0
- package/lib/utils/fs.js +86 -0
- package/lib/utils/ignore.js +148 -0
- package/lib/utils/index.js +23 -1
- package/package.json +1 -1
- package/types/database/index.d.ts +55 -1
- package/types/database/type.d.ts +112 -0
- package/types/deploy/DeployOrchestrator.d.ts +107 -1
- package/types/deploy/StaticDeployer.d.ts +199 -35
- package/types/deploy/framework.d.ts +4 -3
- package/types/error-types.d.ts +9 -0
- package/types/platform/index.d.ts +54 -1
- package/types/platform/type.d.ts +118 -0
- package/types/storage/type.d.ts +2 -1
- package/types/utils/cos-upload.d.ts +17 -0
- package/types/utils/fs.d.ts +55 -0
- package/types/utils/ignore.d.ts +76 -0
- package/types/utils/index.d.ts +4 -0
|
@@ -4,36 +4,341 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.StaticDeployer = void 0;
|
|
7
|
+
exports.hasFilesInDir = hasFilesInDir;
|
|
8
|
+
exports.normalizeIgnore = normalizeIgnore;
|
|
9
|
+
exports.tryNormalizeDeployPath = tryNormalizeDeployPath;
|
|
10
|
+
exports.resolveInsideBase = resolveInsideBase;
|
|
11
|
+
exports.assertRealPathInsideBase = assertRealPathInsideBase;
|
|
12
|
+
exports.resolveDeployRoot = resolveDeployRoot;
|
|
13
|
+
exports.resolveArtifactDir = resolveArtifactDir;
|
|
7
14
|
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
16
|
const error_1 = require("../error");
|
|
17
|
+
const error_types_1 = require("../error-types");
|
|
18
|
+
// 默认排除清单的唯一来源:文件级上传(storage.uploadFilesCustom)也读它,
|
|
19
|
+
// 在这里复制一份就会出现「目录上传排 .env、单文件上传不排」的口径分叉
|
|
20
|
+
const ignore_1 = require("../utils/ignore");
|
|
10
21
|
const framework_1 = require("./framework");
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
/**
|
|
23
|
+
* 写部署记录时固定下发的 `Framework`:必须逐字符等于 `static`。
|
|
24
|
+
*
|
|
25
|
+
* CreateCloudApp 只有一个 `StaticConfig.Framework` 字段,「是否跳过构建」的判断
|
|
26
|
+
* 和控制台展示共用它,没法一个字段塞两个值:
|
|
27
|
+
*
|
|
28
|
+
* - BuildServer 的跳过构建分支条件写死为 `SkipBuild && framework=="static"`
|
|
29
|
+
* (cloud-tcb/cloudbasebuildserver/service/build_service.go:150)。传 `other` /
|
|
30
|
+
* `vite` / `react` 都会**真的触发 CNB 流水线** —— 产物已在本地直传,再跑一遍
|
|
31
|
+
* 既排队又重复上传,`skipBuild: true` 形同虚设。
|
|
32
|
+
* - 旧实现按 iWiki 4052401611 的枚举白名单把 `static` 收敛成 `other`,
|
|
33
|
+
* 正是上面这条失效的直接原因(枚举白名单只描述控制台展示口径,不参与该判断)。
|
|
34
|
+
*
|
|
35
|
+
* 结论:纯静态直传一律传 `static`。真实框架只用于本地推断产物目录
|
|
36
|
+
* (`resolveArtifactDir`),不再写进这个字段。
|
|
37
|
+
*/
|
|
38
|
+
const SKIP_BUILD_FRAMEWORK = 'static';
|
|
39
|
+
/**
|
|
40
|
+
* 判断目录下是否存在至少一个**真实文件**(递归)。
|
|
41
|
+
*
|
|
42
|
+
* 忽略 `.DS_Store`:只有该文件的目录在 macOS 上很常见,
|
|
43
|
+
* 若按「非空」通过,会静默发布一个空站点。
|
|
44
|
+
*
|
|
45
|
+
* 必须递归:只看顶层条目名时,「仅有空子目录」(如 `dist/assets/` 为空目录)
|
|
46
|
+
* 会被误判为有产物,于是把空站点当有效产物发布出去。
|
|
47
|
+
* 不跟随软链(`dirent.isDirectory()` 对符号链接为 false),避免环状链接无限递归。
|
|
48
|
+
*/
|
|
49
|
+
function hasFilesInDir(dir) {
|
|
50
|
+
try {
|
|
51
|
+
if (!fs_1.default.statSync(dir).isDirectory())
|
|
52
|
+
return false;
|
|
53
|
+
const stack = [dir];
|
|
54
|
+
while (stack.length > 0) {
|
|
55
|
+
const current = stack.pop();
|
|
56
|
+
for (const entry of fs_1.default.readdirSync(current, { withFileTypes: true })) {
|
|
57
|
+
if (entry.name === '.DS_Store')
|
|
58
|
+
continue;
|
|
59
|
+
if (entry.isFile())
|
|
60
|
+
return true;
|
|
61
|
+
if (entry.isDirectory())
|
|
62
|
+
stack.push(path_1.default.join(current, entry.name));
|
|
63
|
+
}
|
|
17
64
|
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
catch (_a) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 把用户配置的 `ignore` 归一成字符串数组(app / hosting / uploadCode 三处共用)。
|
|
73
|
+
*
|
|
74
|
+
* 必须与 `uploadFiles` 一样接受 `string | string[]`:cloudbaserc 的 app / hosting
|
|
75
|
+
* 配置是原文透传给部署链路的(中间没有 schema 校验),用户按 `uploadFiles` 的写法
|
|
76
|
+
* 写 `"ignore": "*.map"` 是合法的。而拼接只认数组 —— 直接 `...'*.map'` 会按字符
|
|
77
|
+
* 展开成 `['*', '.', 'm', 'a', 'p']`:用户规则失效,且这些单字符 glob 会误伤大量
|
|
78
|
+
* 产物文件(文件名含 a / m / p 就被排除),站点缺文件、线上 404,全程无报错。
|
|
79
|
+
*
|
|
80
|
+
* 与 `src/deploy/function/config-guard.ts` 的 `Array.isArray` 归一化同一口径。
|
|
81
|
+
*
|
|
82
|
+
* - `undefined` / `null` → `[]`(未配置,只用默认清单)
|
|
83
|
+
* - 字符串 → 单元素数组
|
|
84
|
+
* - 数组 → 原样(逐项校验)
|
|
85
|
+
* - 空串项丢弃:不是合法 glob,留着只会让匹配行为不可预期
|
|
86
|
+
* - 非字符串项 → `INVALID_PARAM`,报错里带字段名
|
|
87
|
+
*/
|
|
88
|
+
function normalizeIgnore(ignore, field, name) {
|
|
89
|
+
if (ignore === undefined || ignore === null)
|
|
90
|
+
return [];
|
|
91
|
+
const list = Array.isArray(ignore) ? ignore : [ignore];
|
|
92
|
+
const prefix = name ? `[${name}] ` : '';
|
|
93
|
+
return list
|
|
94
|
+
.filter(item => item !== undefined && item !== null && item !== '')
|
|
95
|
+
.map(item => {
|
|
96
|
+
if (typeof item === 'string')
|
|
97
|
+
return item;
|
|
98
|
+
return (0, error_1.throwInvalidParam)(`${prefix}${field} 必须是字符串或字符串数组,当前含 ${Array.isArray(item) ? 'array' : typeof item}: ${String(item)}`);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 会被云端构建容器按 shell 解析的字符。
|
|
103
|
+
*
|
|
104
|
+
* 与 CLI 侧同名校验保持同一套口径:空白、引号、分号、
|
|
105
|
+
* `$`、反引号、重定向/管道、通配符、`#`、`~`、反斜杠。
|
|
106
|
+
*/
|
|
107
|
+
const SHELL_UNSAFE_PATTERN = /[\s'"`$;&|<>(){}[\]*?!#~\\]/;
|
|
108
|
+
/**
|
|
109
|
+
* 校验并规范化部署路径(`app.deployPath` / `hosting[].deployPath`),
|
|
110
|
+
* 同时作为 `AppPath` / 静态托管 `cloudPath` 下发。
|
|
111
|
+
*
|
|
112
|
+
* 当前不再拼任何命令(`staticCmd` 已不下发),但校验刻意保留:
|
|
113
|
+
* - 部署路径是访问 URL、`AppPath` 与网关 `PathRewrite.Prefix` 的唯一真相源,
|
|
114
|
+
* 不能含 `..`、不能以 `-` 开头(服务端会原样存进版本记录并回显给排查者)
|
|
115
|
+
* - shell 元字符一并拒绝是纵深防御:一旦平台侧恢复「按命令部署」的链路,
|
|
116
|
+
* 这个值会被拼进 `tcb hosting deploy`,那时的防线只能在这里
|
|
117
|
+
*
|
|
118
|
+
* ⚠️ 缺前导 `/`(如 `"deployPath": "web"`)**自动补全**为 `/web`,不报错:
|
|
119
|
+
* 1. 契约要求绝对路径(cloudbaserc schema 的 `PathPattern` 就是 `^/.*`),但 schema
|
|
120
|
+
* 只在 `tcb validate` / MCP 注入侧生效,手改配置直连本方法时会被绕过
|
|
121
|
+
* 2. 补全对已部署内容是**幂等**的:`toPhysicalKey` 会 `replace(/^\/+/, '')`,
|
|
122
|
+
* `web` 与 `/web` 落到同一个 COS key,不会产生两份文件
|
|
123
|
+
* 3. 存量配置(相对写法)因此不炸,而网关 Prefix 与访问 URL 都变正确 ——
|
|
124
|
+
* 以前相对写法在网关场景会静默 404
|
|
125
|
+
*
|
|
126
|
+
* 补全**不止**加前导 `/`,还会先做分段规范化(丢掉空段与 `.` 段):
|
|
127
|
+
* 只补斜杠会把 `./web` 拼成 `/./web`,COS key 落成 `./web/...`,而浏览器访问
|
|
128
|
+
* `/./web` 会被规范化成 `/web` —— 两者不相等,整站 404 且部署「成功」无报错。
|
|
129
|
+
* `.` / `web/.` / `web//x` 一律归到 `/` / `/web` / `/web/x` 后才补前导 `/`。
|
|
130
|
+
*
|
|
131
|
+
* `../`、shell 元字符、`-` 开头仍一律拒绝(这些补斜杠救不了,且属注入面),
|
|
132
|
+
* 与 `.` 段「安全归一即可放行」的处理保持同一粒度判断:能归一的归一,不能的报错。
|
|
133
|
+
*
|
|
134
|
+
* @returns 规范化(trim + 折叠 `.`/空段 + 必要时补前导 `/`)后的值,调用方必须用返回值而非原值
|
|
135
|
+
*/
|
|
136
|
+
function normalizeDeployPath(value, field, mustBeAbsolute = false) {
|
|
137
|
+
const normalized = String(value !== null && value !== void 0 ? value : '').trim();
|
|
138
|
+
if (!normalized) {
|
|
139
|
+
(0, error_1.throwInvalidParam)(`${field} 不能为空`);
|
|
140
|
+
}
|
|
141
|
+
if (SHELL_UNSAFE_PATTERN.test(normalized)) {
|
|
142
|
+
(0, error_1.throwInvalidParam)(`${field} 含空白或 shell 特殊字符(会被当成命令参数解析): ${value}`);
|
|
143
|
+
}
|
|
144
|
+
// ⚠️ `-` 前缀检查必须在补 `/` 之前:补完后再判,`-web` 已变成 `/-web` 而躲过检查
|
|
145
|
+
if (normalized.startsWith('-')) {
|
|
146
|
+
(0, error_1.throwInvalidParam)(`${field} 不能以 - 开头(会被当成命令选项): ${value}`);
|
|
147
|
+
}
|
|
148
|
+
if (normalized.split('/').includes('..')) {
|
|
149
|
+
(0, error_1.throwInvalidParam)(`${field} 不能包含 ../: ${value}`);
|
|
150
|
+
}
|
|
151
|
+
// 折叠空段与 `.` 段(保留是否绝对路径的信息,前导 `/` 单独判断):
|
|
152
|
+
// `./web` / `web/.` / `web//x` → `web` / `web` / `web/x`;`.` / `./` → 空段
|
|
153
|
+
const isAbsolute = normalized.startsWith('/');
|
|
154
|
+
const collapsed = normalized
|
|
155
|
+
.split('/')
|
|
156
|
+
.filter(seg => seg !== '' && seg !== '.')
|
|
157
|
+
.join('/');
|
|
158
|
+
if (mustBeAbsolute) {
|
|
159
|
+
// `.` 之类全被折叠掉时即根路径,不能返回空串或 `/.`
|
|
160
|
+
return `/${collapsed}`;
|
|
161
|
+
}
|
|
162
|
+
if (isAbsolute) {
|
|
163
|
+
(0, error_1.throwInvalidParam)(`${field} 必须是相对路径: ${value}`);
|
|
164
|
+
}
|
|
165
|
+
if (!collapsed) {
|
|
166
|
+
(0, error_1.throwInvalidParam)(`${field} 不能为空`);
|
|
167
|
+
}
|
|
168
|
+
return collapsed;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* `normalizeDeployPath` 的只读版本:非法值返回 `undefined` 而不抛错。
|
|
172
|
+
*
|
|
173
|
+
* 导出给 `DeployOrchestrator.buildAppPlan` 复用:plan 必须与 apply 走**同一个**
|
|
174
|
+
* 校验函数,否则「用户显式配了非法 deployPath」这种场景会在 plan 里渲染成一条
|
|
175
|
+
* 完全正常的计划项,而 apply 必然抛 `INVALID_PARAM` —— 预演与真实发布口径分叉。
|
|
176
|
+
*
|
|
177
|
+
* plan 是只读预演,报错留给 apply 的参数校验,这里只降级为「无规范化值」。
|
|
178
|
+
*/
|
|
179
|
+
function tryNormalizeDeployPath(value, field, mustBeAbsolute = false) {
|
|
180
|
+
try {
|
|
181
|
+
return normalizeDeployPath(value, field, mustBeAbsolute);
|
|
182
|
+
}
|
|
183
|
+
catch (_a) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 解析路径并要求落在 `baseDir` 内,返回解析后的绝对路径。
|
|
189
|
+
*
|
|
190
|
+
* 防的是 `outputDir: '../..'` 或绝对路径把项目外文件递归上传并发布成站点。
|
|
191
|
+
* 这里只做字面路径判断;软链逃逸(`ln -s ~/.ssh dist`)需要调用方按需补充
|
|
192
|
+
* (CLI 侧 `safeResolveProjectPath` 有完整实现)。
|
|
193
|
+
*
|
|
194
|
+
* ⚠️ `path.isAbsolute(relative)` 不能省:`path.relative` 在找不到相对路径时
|
|
195
|
+
* 会返回 target 的绝对路径(Windows 跨盘符即如此),只判 `startsWith('..')` 会放行。
|
|
196
|
+
*
|
|
197
|
+
* 类型校验下沉到这里,而不是放在每个调用点:
|
|
198
|
+
* `app.root` / `app.outputDir` / `hosting.root` / `hosting.outputDir` 四个入口都来自
|
|
199
|
+
* 用户手改的 cloudbaserc 原文(无 schema 兜底),`"root": 123` 这类 JSON 合法值会在
|
|
200
|
+
* `path.resolve` 处抛 Node 原生 `ERR_INVALID_ARG_TYPE`,错误里不含任何字段名,用户无从定位。
|
|
201
|
+
* 逐个调用点补校验必然漏(本函数的四个调用点此前就只有一个做了),所以在唯一的收口处拦。
|
|
202
|
+
* 含 NUL 的字符串同理:能过 `path.resolve`,却在 `fs` 调用处抛 `ERR_INVALID_ARG_VALUE`。
|
|
203
|
+
*
|
|
204
|
+
* 导出给 `DeployOrchestrator.resolveBuildHint` 复用:plan 必须与 apply 用**同一个**
|
|
205
|
+
* 校验函数,否则 plan 会在越界路径上报 `artifactReady: true`,而 apply 必然拒绝。
|
|
206
|
+
*/
|
|
207
|
+
function resolveInsideBase(baseDir, target, field) {
|
|
208
|
+
if (typeof target !== 'string') {
|
|
209
|
+
(0, error_1.throwInvalidParam)(`${field} 必须是字符串,当前类型:${Array.isArray(target) ? 'array' : target === null ? 'null' : typeof target}`);
|
|
210
|
+
}
|
|
211
|
+
if (target.includes('\0')) {
|
|
212
|
+
(0, error_1.throwInvalidParam)(`${field} 含非法字符(NUL 字节)`);
|
|
213
|
+
}
|
|
214
|
+
const base = path_1.default.resolve(baseDir);
|
|
215
|
+
const resolved = path_1.default.resolve(base, target);
|
|
216
|
+
const relative = path_1.default.relative(base, resolved);
|
|
217
|
+
if (relative === '' || (!relative.startsWith('..') && !path_1.default.isAbsolute(relative))) {
|
|
218
|
+
return resolved;
|
|
219
|
+
}
|
|
220
|
+
(0, error_1.throwInvalidParam)(`${field} 解析后超出项目目录: ${target}`);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 在字面路径校验之上,再按 realpath 校验一次,挡住软链逃逸。
|
|
224
|
+
*
|
|
225
|
+
* 场景:`ln -s ~/.ssh dist` —— `resolveInsideBase` 只看字面路径,会认为 `dist`
|
|
226
|
+
* 就在项目内;实际读取时落在项目外,产物(或私钥)会被发布到公开可访问的托管桶。
|
|
227
|
+
*
|
|
228
|
+
* 目标不存在时 `realpathSync` 抛错,此时不做判断:交给调用方后续的存在性/非空校验
|
|
229
|
+
* 给出更准确的错误(例如 `BUILD_OUTPUT_NOT_FOUND`)。
|
|
230
|
+
*
|
|
231
|
+
* 只做「是否越界」的判定,返回值仍用字面解析结果 —— 避免把 `/var` → `/private/var`
|
|
232
|
+
* 这类系统级软链展开后泄漏到对外可见的路径与日志里。
|
|
233
|
+
*
|
|
234
|
+
* 与 `resolveInsideBase` 一同导出给 `DeployOrchestrator.resolveBuildHint` 复用。
|
|
235
|
+
*/
|
|
236
|
+
function assertRealPathInsideBase(options) {
|
|
237
|
+
const { baseDir, resolved, field, rawTarget } = options;
|
|
238
|
+
let realBase;
|
|
239
|
+
let realTarget;
|
|
240
|
+
try {
|
|
241
|
+
realBase = fs_1.default.realpathSync(path_1.default.resolve(baseDir));
|
|
242
|
+
realTarget = fs_1.default.realpathSync(resolved);
|
|
243
|
+
}
|
|
244
|
+
catch (_a) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const relative = path_1.default.relative(realBase, realTarget);
|
|
248
|
+
if (relative === '' || (!relative.startsWith('..') && !path_1.default.isAbsolute(relative))) {
|
|
249
|
+
return;
|
|
18
250
|
}
|
|
19
|
-
|
|
251
|
+
(0, error_1.throwInvalidParam)(`${field} 实际路径(软链解析后)超出项目目录: ${rawTarget}`);
|
|
20
252
|
}
|
|
21
253
|
/**
|
|
22
|
-
* app/hosting
|
|
254
|
+
* 解析 `app.root` / `hosting.root`:落在 `cwd` 内,且软链解析后仍在 `cwd` 内。
|
|
23
255
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
256
|
+
* 必须用 `||` 而非 `??`:`root: 0 / false / ''` 一律回落 `'.'`。
|
|
257
|
+
* 导出给 `DeployOrchestrator` 复用,plan 与 apply 用同一个函数,不会再出现口径分叉。
|
|
258
|
+
*/
|
|
259
|
+
function resolveDeployRoot(cwd, rawRoot, field) {
|
|
260
|
+
const rootField = (rawRoot || '.');
|
|
261
|
+
const root = resolveInsideBase(cwd, rootField, field);
|
|
262
|
+
assertRealPathInsideBase({ baseDir: cwd, resolved: root, field, rawTarget: rootField });
|
|
263
|
+
return root;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* 解析产物目录(绝对路径),**app 与 hosting 同一套规则**:
|
|
267
|
+
*
|
|
268
|
+
* | `outputDir` | 结果 |
|
|
269
|
+
* |---|---|
|
|
270
|
+
* | 配了(trim 后非空字符串,含 `'.'`) | 按用户配置(用 trim 后的值),不参与推断 |
|
|
271
|
+
* | 没配(`undefined` / `null` / `''` / 纯空白串) | `resolveHostingOutputDir` 推断:显式 buildCommand → `dist`;framework / package.json → 框架默认产物目录;纯静态 → root |
|
|
272
|
+
* | 非字符串 | `INVALID_PARAM`(否则会在 `path.resolve` 处以裸 TypeError 失败) |
|
|
273
|
+
*
|
|
274
|
+
* 推断规则与 `tcb app build` 的产物落点一致:构建到哪里,就从哪里上传。
|
|
275
|
+
* 只解析和做越界 / 软链校验,**不判非空** —— apply 据此抛 `BUILD_OUTPUT_NOT_FOUND`,
|
|
276
|
+
* plan(`DeployOrchestrator`)据此报 `artifactReady`,两边共用本函数。
|
|
277
|
+
*/
|
|
278
|
+
function resolveArtifactDir(options) {
|
|
279
|
+
const { config, root, kind, name } = options;
|
|
280
|
+
const field = `${kind}.outputDir`;
|
|
281
|
+
const raw = config.outputDir;
|
|
282
|
+
if (raw !== undefined && raw !== null && typeof raw !== 'string') {
|
|
283
|
+
(0, error_1.throwInvalidParam)(`${name ? `[${name}] ` : ''}${field} 必须是字符串,当前类型:${Array.isArray(raw) ? 'array' : typeof raw}`);
|
|
284
|
+
}
|
|
285
|
+
// 先 trim:纯空白串(模板清空内容后残留空格最常见)是 truthy,不归一会被当成真实配置,
|
|
286
|
+
// 走到 path.resolve(root, ' ') 变成 '<root>/ ' 这种畸形目录名 —— 字面仍在 root 内,
|
|
287
|
+
// 越界与软链校验全部放行,最终在 assertArtifactReady 抛 BUILD_OUTPUT_NOT_FOUND,
|
|
288
|
+
// 用户看到的路径根本不知道是自己多打了空格。与 normalizeDeployPath 的 trim 口径一致。
|
|
289
|
+
// 配了就校验 trim 后的值(报错文案里也是用户写的值);没配才推断,再折回相对 root 的值做越界校验
|
|
290
|
+
const trimmed = typeof raw === 'string' ? raw.trim() : raw;
|
|
291
|
+
const target = typeof trimmed === 'string' && trimmed !== ''
|
|
292
|
+
? trimmed
|
|
293
|
+
: path_1.default.relative(root, (0, framework_1.resolveHostingOutputDir)({ framework: config.framework, buildCommand: config.buildCommand }, root));
|
|
294
|
+
const outputDir = resolveInsideBase(root, target, field);
|
|
295
|
+
assertRealPathInsideBase({ baseDir: root, resolved: outputDir, field, rawTarget: target });
|
|
296
|
+
return outputDir;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* 产物必须已就绪(存在且含至少一个真实文件)。
|
|
300
|
+
*
|
|
301
|
+
* - 目录不存在:没构建,要明确告知先构建,而不是让 uploadFiles 抛一个指向内部路径的错
|
|
302
|
+
* - 目录为空:会被静默发布成空站点(线上 404,但部署「成功」)
|
|
303
|
+
*/
|
|
304
|
+
function assertArtifactReady(dir, name) {
|
|
305
|
+
if (hasFilesInDir(dir))
|
|
306
|
+
return;
|
|
307
|
+
throw new error_1.CloudBaseError(`[${name}] 未找到有效构建产物:${dir}(目录不存在或为空)。` +
|
|
308
|
+
`声明式部署只部署不构建,请先完成构建(本地执行构建命令或 tcb app build)后重试`, { type: error_types_1.ERROR_TYPES.buildOutputNotFound });
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* app/hosting 声明式部署器 —— **只部署,不构建**
|
|
312
|
+
*
|
|
313
|
+
* 声明式部署(CLI `tcb deploy` / MCP `deployApply`)的定位是「只部署」。构建由用户
|
|
314
|
+
* 自己完成,或走 `tcb app build`(本地)/ `tcb app deploy`(云端,不经过本类)。
|
|
315
|
+
* 本类不再执行任何构建命令,产物缺失即抛 `BUILD_OUTPUT_NOT_FOUND`。
|
|
316
|
+
*
|
|
317
|
+
* - app:产物直传 + `createApp(SkipBuild)` 写部署记录
|
|
318
|
+
* - hosting:产物直传,不写版本记录(保持 `tcb hosting deploy` 的既有行为)
|
|
319
|
+
*
|
|
320
|
+
* 两者的产物目录解析、就绪校验、上传排除清单完全一致(`resolveArtifactDir` /
|
|
321
|
+
* `assertArtifactReady` / `uploadArtifact`)。`buildCommand` / `framework` 仍被读取,
|
|
322
|
+
* 但只用于**推断产物目录**,不再执行;写部署记录时 `framework` 固定下发 `'static'`
|
|
323
|
+
* (跳过构建的唯一认可值,见 `SKIP_BUILD_FRAMEWORK`)。
|
|
27
324
|
*/
|
|
28
325
|
class StaticDeployer {
|
|
29
326
|
constructor(environment) {
|
|
30
327
|
this.environment = environment;
|
|
31
328
|
}
|
|
32
329
|
/**
|
|
33
|
-
* app
|
|
330
|
+
* app 部署:**只部署,不构建**。
|
|
34
331
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
332
|
+
* 无条件走「产物直传 + `createApp({ skipBuild: true })`」写部署记录,
|
|
333
|
+
* 不再根据 `framework` 分叉到云端构建。理由:声明式部署(`tcb deploy`)
|
|
334
|
+
* 的定位是只部署,构建由用户用 `tcb app build`(本地)或
|
|
335
|
+
* CLI 的 `tcb app deploy`(云端,不经过本方法)完成。
|
|
336
|
+
*
|
|
337
|
+
* 产物目录与 hosting 同一套规则(见 `resolveArtifactDir`):配了按配置,没配按
|
|
338
|
+
* buildCommand / framework / package.json 推断,纯静态回退 `app.root`。
|
|
339
|
+
*
|
|
340
|
+
* @throws CloudBaseError `INVALID_PARAM` 缺 serviceName / 路径越界 / outputDir 类型非法
|
|
341
|
+
* @throws CloudBaseError `BUILD_OUTPUT_NOT_FOUND` 产物目录不存在或为空
|
|
37
342
|
*/
|
|
38
343
|
async deployApp(options) {
|
|
39
344
|
const { config, cwd = process.cwd() } = options;
|
|
@@ -41,134 +346,165 @@ class StaticDeployer {
|
|
|
41
346
|
if (!serviceName) {
|
|
42
347
|
(0, error_1.throwInvalidParam)('app 部署需要配置 serviceName');
|
|
43
348
|
}
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
349
|
+
// app.root 也不允许越界:否则下面的 outputDir 校验会以项目外目录为基准而失去意义
|
|
350
|
+
const projectPath = resolveDeployRoot(cwd, config.root, 'app.root');
|
|
351
|
+
// 部署路径同时是直传的 cloudPath 与版本记录里的 appPath:必须逐字符一致,
|
|
352
|
+
// 否则产物挂到别的路径、页面 404。在入口处一次性规范化,两处共用同一个值
|
|
353
|
+
const resolvedDeployPath = normalizeDeployPath(config.deployPath || `/${serviceName}`, 'app.deployPath', true);
|
|
354
|
+
const uploadDir = resolveArtifactDir({ config, root: projectPath, kind: 'app', name: serviceName });
|
|
355
|
+
assertArtifactReady(uploadDir, serviceName);
|
|
356
|
+
// 入口归一次:直传托管桶与打包版本快照共用这个结果,两条路径不再各自归一
|
|
357
|
+
const userIgnore = normalizeIgnore(config.ignore, 'app.ignore', serviceName);
|
|
358
|
+
return this.deployAppStatic({
|
|
359
|
+
serviceName,
|
|
360
|
+
uploadDir,
|
|
361
|
+
resolvedDeployPath,
|
|
362
|
+
userIgnore
|
|
363
|
+
});
|
|
51
364
|
}
|
|
52
365
|
/**
|
|
53
|
-
* hosting
|
|
366
|
+
* hosting 部署:**只部署,不构建**(不写版本记录)
|
|
367
|
+
*
|
|
368
|
+
* 与 `deployApp` 同一口径:声明式部署(`tcb deploy` / MCP `deployApply`)的定位是
|
|
369
|
+
* 「只部署」,构建由用户自己完成或走 `tcb app build`(本地)。
|
|
370
|
+
*
|
|
371
|
+
* 为什么构建必须从这里移除、而不是只在 CLI 侧中立化:
|
|
372
|
+
* CLI 的 `neutralizeHostingForDeploy` 会清空 `buildCommand` 后再调用本方法,
|
|
373
|
+
* 因此从 CLI 进来看不出差别;但 MCP 的 `deployApply` 直接读 cloudbaserc 调用本方法,
|
|
374
|
+
* 不经过中立化 ⇒ 若构建留在这里,同一份 cloudbaserc 会出现
|
|
375
|
+
* 「CLI 不构建、MCP 构建」的行为分叉,且 `app` 已经不构建,
|
|
376
|
+
* 变成「app 不构建、hosting 构建」的半截状态。单一真相源只能放在本层。
|
|
377
|
+
*
|
|
378
|
+
* `buildCommand` / `installCommand` 仍被接收但**不再执行**:它们现在只是
|
|
379
|
+
* 「产物目录推断」的输入(见 `resolveArtifactDir`),配置无需改写即可平滑过渡。
|
|
54
380
|
*
|
|
55
|
-
* 支持本地构建:buildCommand 非空时自动执行 build 后上传产物(依赖由用户自行安装,
|
|
56
|
-
* 不代为执行 npm install);
|
|
57
|
-
* 纯静态(buildCommand 空/未配置且无法检测框架)时直接上传 outputDir(默认 root,保持现状)。
|
|
58
381
|
* 访问地址 = https://{CdnDomain}{deployPath}(与 tcb hosting deploy 一致)
|
|
382
|
+
*
|
|
383
|
+
* @throws CloudBaseError `INVALID_PARAM` 路径越界 / outputDir 类型非法
|
|
384
|
+
* @throws CloudBaseError `BUILD_OUTPUT_NOT_FOUND` 产物目录不存在或为空
|
|
59
385
|
*/
|
|
60
386
|
async deployHosting(options) {
|
|
61
387
|
const { config, cwd = process.cwd() } = options;
|
|
62
388
|
const name = config.name || 'default';
|
|
63
|
-
const root =
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const hostingOptions = {
|
|
75
|
-
localPath: outputDir,
|
|
389
|
+
const root = resolveDeployRoot(cwd, config.root, 'hosting.root');
|
|
390
|
+
// 与 app 同口径:缺前导 `/` 自动补全(`web` → `/web`),非法字符仍拒绝。
|
|
391
|
+
// 不校验的后果:`cloudPath` 会原样进网关 `PathRewrite.Prefix`,相对写法导致
|
|
392
|
+
// 请求重写错位、经网关访问 404,而部署本身「成功」,问题被推到线上才暴露
|
|
393
|
+
const cloudPath = normalizeDeployPath(config.deployPath || '/', 'hosting.deployPath', true);
|
|
394
|
+
const uploadDir = resolveArtifactDir({ config, root, kind: 'hosting', name });
|
|
395
|
+
assertArtifactReady(uploadDir, name);
|
|
396
|
+
// 与 deployApp 同口径:入口归一次,下游(uploadArtifact)只复用结果
|
|
397
|
+
const userIgnore = normalizeIgnore(config.ignore, 'hosting.ignore', name);
|
|
398
|
+
await this.uploadArtifact({
|
|
399
|
+
localPath: uploadDir,
|
|
76
400
|
cloudPath,
|
|
77
|
-
|
|
401
|
+
// ignore 由 uploadArtifact 从 userIgnore 统一构造,这里不再重复传原始值
|
|
78
402
|
// 声明式配置名为 entryFile,透传到 uploadFiles 时对应 entryFiles
|
|
79
403
|
entryFiles: config.entryFile,
|
|
80
404
|
// 一致性发布开关:任一开启即走一致性上传(uploadDirectoryConsistent)
|
|
81
405
|
verify: config.verify,
|
|
82
406
|
safe: config.safe,
|
|
83
407
|
prune: config.prune
|
|
84
|
-
};
|
|
85
|
-
await this.environment.getHostingService().uploadFiles(hostingOptions);
|
|
408
|
+
}, userIgnore, name);
|
|
86
409
|
const url = await this.resolveHostingUrl(cloudPath);
|
|
87
410
|
return { name, url, wroteRecord: false };
|
|
88
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* 产物直传静态托管(app / hosting 共用)。
|
|
414
|
+
*
|
|
415
|
+
* `userIgnore` 必须由调用方在入口用 `normalizeIgnore` 归一后传入:
|
|
416
|
+
* 归一化只做一次,避免「同一个 config.ignore 在两条路径上各归一一次」——
|
|
417
|
+
* 只要有一处被改回直接 spread 原始值,字符串 `'*.map'` 就会按字符展开成
|
|
418
|
+
* `['*','.','m','a','p']`,误杀大量产物且不报错。
|
|
419
|
+
*/
|
|
420
|
+
async uploadArtifact(options, userIgnore, name) {
|
|
421
|
+
// mergeArtifactIgnore 内部去重:hosting.uploadFiles 入口还会再合并一次,
|
|
422
|
+
// 两边都用同一个函数才能保证「重复合并 = 幂等」,不会把清单叠成两份
|
|
423
|
+
await this.environment.getHostingService().uploadFiles(Object.assign(Object.assign({}, options), { ignore: (0, ignore_1.mergeArtifactIgnore)(userIgnore) }));
|
|
424
|
+
}
|
|
89
425
|
/**
|
|
90
426
|
* app 纯静态直传(对齐 tcb app deploy 的 deployStaticApp 分支)
|
|
91
427
|
*
|
|
92
428
|
* 1. hosting uploadFiles 直传产物(文件级上传,立即可访问,无需打 ZIP)
|
|
93
|
-
* 2.
|
|
429
|
+
* 2. createApp(SkipBuild) 写部署记录(失败时抛错,但说明产物已可访问)
|
|
94
430
|
* 3. 跳过云端构建轮询,返回访问地址
|
|
431
|
+
*
|
|
432
|
+
* 调用前产物目录必须已解析并校验就绪(见 `deployApp`)。
|
|
95
433
|
*/
|
|
96
434
|
async deployAppStatic(options) {
|
|
97
|
-
|
|
435
|
+
var _a;
|
|
436
|
+
const { serviceName, uploadDir, resolvedDeployPath, userIgnore } = options;
|
|
98
437
|
const cloudAppService = this.environment.getCloudAppService();
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
438
|
+
// 不发 staticCmd(installCmd / buildCmd / deployCmd 都不传):
|
|
439
|
+
// skip 分支在建完版本记录后直接 return,压根不读命令;而一旦平台侧让 skip 失效,
|
|
440
|
+
// 缺 deployCmd 会退化成按 FRAMEWORK_NAME 猜目录(`static` 落到 `*` → `dist`),
|
|
441
|
+
// 而 ZIP 以产物目录为根、包内没有 dist/ → 构建阶段失败、版本 FAILED。
|
|
442
|
+
// 这是刻意的:skip 是唯一设计路径,失效必须显性失败,而不是静默多跑一次流水线。
|
|
443
|
+
// 1. 直传产物(cloudPath 与版本记录里的 appPath 共用 resolvedDeployPath,避免访问路径漂移)
|
|
444
|
+
await this.uploadArtifact({
|
|
104
445
|
localPath: uploadDir,
|
|
105
|
-
cloudPath: resolvedDeployPath
|
|
106
|
-
ignore
|
|
107
|
-
});
|
|
108
|
-
// 2.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
async deployAppCloud(config, serviceName, projectPath) {
|
|
147
|
-
const cloudAppService = this.environment.getCloudAppService();
|
|
148
|
-
const uploadResult = await cloudAppService.uploadCode({
|
|
149
|
-
deployType: 'static-hosting',
|
|
150
|
-
serviceName,
|
|
151
|
-
localPath: projectPath,
|
|
152
|
-
// 云端构建:不排除 node_modules(云端 install 需要私有/离线依赖随包上传)
|
|
153
|
-
excludeNodeModules: false,
|
|
154
|
-
ignore: ['.git/**', ...(config.ignore || [])]
|
|
155
|
-
});
|
|
156
|
-
await cloudAppService.createApp({
|
|
157
|
-
deployType: 'static-hosting',
|
|
158
|
-
serviceName,
|
|
159
|
-
buildType: 'ZIP',
|
|
160
|
-
staticConfig: {
|
|
161
|
-
cosTimestamp: uploadResult.unixTimestamp,
|
|
162
|
-
appPath: config.deployPath || `/${serviceName}`,
|
|
163
|
-
staticCmd: {
|
|
164
|
-
installCmd: config.installCommand || 'npm install',
|
|
165
|
-
buildCmd: config.buildCommand || 'npm run build',
|
|
166
|
-
deployCmd: `tcb hosting deploy ${config.outputDir || 'dist'} ${config.deployPath || `/${serviceName}`}`
|
|
446
|
+
cloudPath: resolvedDeployPath
|
|
447
|
+
// ignore 由 uploadArtifact 从入口归一后的 userIgnore 统一构造,不传原始值
|
|
448
|
+
}, userIgnore, serviceName);
|
|
449
|
+
// 2. 写部署记录(await:记录是部署结果的一部分,失败必须让调用方感知)
|
|
450
|
+
//
|
|
451
|
+
// 早期实现是 fire-and-forget + 恒返回 wroteRecord: true,导致记录写失败时
|
|
452
|
+
// 调用方仍以为成功、版本列表里却查不到,排查成本高。改为同步等待:
|
|
453
|
+
// 失败时抛错,但文案明确说明「产物已上传并可访问」,避免用户误以为要重传。
|
|
454
|
+
let wroteRecord = false;
|
|
455
|
+
// 上传阶段是否成功:决定 catch 文案能不能说「产物已上传并可访问」。
|
|
456
|
+
// 这个 try 同时包住 uploadCode 和 createApp,不区分阶段会让上传失败也报「记录写入失败」。
|
|
457
|
+
let uploaded = false;
|
|
458
|
+
try {
|
|
459
|
+
const uploadResult = await cloudAppService.uploadCode({
|
|
460
|
+
deployType: 'static-hosting',
|
|
461
|
+
serviceName,
|
|
462
|
+
// 这个 ZIP 只为满足 createApp 的 cosTimestamp 要求(skipBuild 下不会被构建),
|
|
463
|
+
// 但它就是版本快照本身,所以打包「产物目录」而不是项目根:
|
|
464
|
+
// 1. 语义正确 —— 记录里的快照应等于实际发布的内容,
|
|
465
|
+
// 打包项目根会让回滚/重建拿到源码而不是产物
|
|
466
|
+
// 2. 少传 —— 项目根会把源码、构建缓存一起传到 COS,
|
|
467
|
+
// 大仓库会明显变慢甚至撞上传超时(outputDir='.' 时两者相同)
|
|
468
|
+
localPath: uploadDir,
|
|
469
|
+
// 与直传托管桶**同一份清单**:快照是「实际发布内容」的版本记录,
|
|
470
|
+
// 两边排除范围不一致会让回滚拿到线上没有的文件(node_modules / .DS_Store)。
|
|
471
|
+
// 这里不传 excludeNodeModules: false,uploadCode 侧的
|
|
472
|
+
// node_modules 排除与本文具清单叠加,属幂等的二次保障。
|
|
473
|
+
ignore: (0, ignore_1.mergeArtifactIgnore)(userIgnore)
|
|
474
|
+
});
|
|
475
|
+
uploaded = true;
|
|
476
|
+
await cloudAppService.createApp({
|
|
477
|
+
deployType: 'static-hosting',
|
|
478
|
+
serviceName,
|
|
479
|
+
buildType: 'ZIP',
|
|
480
|
+
skipBuild: true,
|
|
481
|
+
staticConfig: {
|
|
482
|
+
cosTimestamp: uploadResult.unixTimestamp,
|
|
483
|
+
appPath: resolvedDeployPath,
|
|
484
|
+
// 必须传 static:BuildServer 只在 framework=="static" 时才跳过构建,
|
|
485
|
+
// 传真实框架(vite / react / other)会让 skipBuild 失效、真的跑流水线
|
|
486
|
+
framework: SKIP_BUILD_FRAMEWORK
|
|
167
487
|
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
488
|
+
});
|
|
489
|
+
wroteRecord = true;
|
|
490
|
+
}
|
|
491
|
+
catch (e) {
|
|
492
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
493
|
+
// 按阶段给文案:上传失败时产物根本不可访问,说「已上传并可访问」会把用户引向错误方向
|
|
494
|
+
// (以为只是记录问题、不用重传)
|
|
495
|
+
throw new error_1.CloudBaseError(uploaded
|
|
496
|
+
? `[${serviceName}] 产物已上传并可访问,但部署记录写入失败:${detail}`
|
|
497
|
+
: `[${serviceName}] 产物上传失败:${detail}`, { type: error_types_1.ERROR_TYPES.apiError });
|
|
498
|
+
}
|
|
499
|
+
// 3. 访问地址:优先云应用自己的 Domain,拿不到再回落静态托管 CDN 域名
|
|
500
|
+
//
|
|
501
|
+
// skipBuild 只跳过构建,云应用记录仍由 createApp 创建,DescribeCloudAppInfo 会返回
|
|
502
|
+
// 该应用独立的 Domain(用户可能已绑定自定义域名)。若把地址固定成托管 CDN 域名,
|
|
503
|
+
// 等于把应用域名(含自定义域名)丢掉,控制台 / CLI / MCP 展示的就不是用户实际使用的入口。
|
|
504
|
+
// 托管 CDN 仅在拿不到 Domain 时兜底,避免返回 undefined。
|
|
505
|
+
// 拼接规则与 CLI 的 buildAppUrl 同口径,见 resolveAppUrl 注释。
|
|
506
|
+
const url = (_a = (await this.resolveAppUrl(serviceName, resolvedDeployPath))) !== null && _a !== void 0 ? _a : (await this.resolveHostingUrl(resolvedDeployPath));
|
|
507
|
+
return { name: serviceName, url, wroteRecord };
|
|
172
508
|
}
|
|
173
509
|
/**
|
|
174
510
|
* 解析 hosting 访问地址:https://{CdnDomain}{deployPath}
|
|
@@ -189,32 +525,16 @@ class StaticDeployer {
|
|
|
189
525
|
}
|
|
190
526
|
}
|
|
191
527
|
/**
|
|
192
|
-
*
|
|
528
|
+
* 解析 app 访问地址(DescribeCloudAppInfo.Domain 为准,托管 CDN 仅兜底)
|
|
193
529
|
*
|
|
194
|
-
*
|
|
195
|
-
* 均不在本地构建时代装依赖)。installCommand 不再被本地执行 —— CLI 侧通过
|
|
196
|
-
* installCommand: '' 已可跳过云端安装;本地一律不装。
|
|
530
|
+
* 拼接规则与 CLI `lib/app/utils.js` 的 `buildAppUrl` 同口径:
|
|
197
531
|
*
|
|
198
|
-
* -
|
|
199
|
-
*
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
if (buildCommand) {
|
|
206
|
-
(0, child_process_1.execSync)(buildCommand, { cwd: projectPath, stdio: 'inherit', env });
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
catch (e) {
|
|
210
|
-
const detail = ((e === null || e === void 0 ? void 0 : e.stderr) || (e === null || e === void 0 ? void 0 : e.stdout) || (e === null || e === void 0 ? void 0 : e.message) || String(e)).toString().trim();
|
|
211
|
-
(0, error_1.throwApiError)(`本地构建失败:在 ${projectPath} 执行「${buildCommand}」失败。\n` +
|
|
212
|
-
`请先在项目目录手动执行该命令排查原因(常见原因:构建脚本报错、依赖未安装、lockfile 不一致)。\n` +
|
|
213
|
-
(detail ? `原始错误:${detail.slice(0, 500)}` : ''));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* 解析 app 访问地址(describeAppInfo.Domain + appPath)
|
|
532
|
+
* - **独立域名**(不含 `tcloudbaseapp.com`,如 `*.webapps.tcloudbase.com` / 自定义域名):
|
|
533
|
+
* 域名本身已定位到应用,**不拼路径**,返回 `https://{domain}/`。
|
|
534
|
+
* 控制台「访问域名」展示的就是裸域名,拼上 AppPath 反而 404
|
|
535
|
+
* (AppPath 是托管桶内的目录,不是该域名下的 URL 路径)。
|
|
536
|
+
* - **环境级共享域名**(`*.tcloudbaseapp.com`):桶被整个环境共用,必须拼路径定位应用,
|
|
537
|
+
* 优先用服务端返回的 `AppPath`(用户可能在控制台改过),本地 `appPath` 仅作 fallback。
|
|
218
538
|
*/
|
|
219
539
|
async resolveAppUrl(serviceName, appPath) {
|
|
220
540
|
try {
|
|
@@ -222,11 +542,18 @@ class StaticDeployer {
|
|
|
222
542
|
deployType: 'static-hosting',
|
|
223
543
|
serviceName
|
|
224
544
|
});
|
|
225
|
-
|
|
545
|
+
const domain = appInfo === null || appInfo === void 0 ? void 0 : appInfo.Domain;
|
|
546
|
+
if (!domain) {
|
|
226
547
|
return undefined;
|
|
227
548
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
549
|
+
const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
550
|
+
if (!cleanDomain.includes('tcloudbaseapp.com')) {
|
|
551
|
+
return `https://${cleanDomain}/`;
|
|
552
|
+
}
|
|
553
|
+
const serverAppPath = appInfo === null || appInfo === void 0 ? void 0 : appInfo.AppPath;
|
|
554
|
+
const effectivePath = serverAppPath && serverAppPath !== '/' ? serverAppPath : appPath;
|
|
555
|
+
const cleanPath = effectivePath.replace(/\/$/, '');
|
|
556
|
+
return `https://${cleanDomain}${cleanPath}/`;
|
|
230
557
|
}
|
|
231
558
|
catch (_a) {
|
|
232
559
|
return undefined;
|