@cloudbase/manager-node 5.7.0-beta.0 → 5.7.0-beta.2
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/lib/cloudrun/index.js +18 -0
- package/lib/deploy/GatewayDeployer.js +100 -10
- package/lib/deploy/StaticDeployer.js +48 -2
- package/lib/projectValidator/index.js +106 -2
- package/package.json +1 -1
- package/types/cloudrun/index.d.ts +7 -1
- package/types/cloudrun/type.d.ts +22 -0
- package/types/deploy/GatewayDeployer.d.ts +19 -0
- package/types/projectValidator/index.d.ts +19 -0
package/CHANGELOG.md
CHANGED
package/lib/cloudrun/index.js
CHANGED
|
@@ -101,6 +101,21 @@ class CloudRunService {
|
|
|
101
101
|
const envConfig = this.environment.lazyEnvironmentConfig;
|
|
102
102
|
return this.tcbrService.request('DescribeServerManageTask', Object.assign({ EnvId: envConfig.EnvId, ServerName: params.serverName, TaskId: (_a = params.taskId) !== null && _a !== void 0 ? _a : 0 }, (params.operatorRemark ? { OperatorRemark: params.operatorRemark } : {})));
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* 查询发布单信息(DescribeReleaseOrder)
|
|
106
|
+
* @param {IDescribeReleaseOrderParams} params 查询参数
|
|
107
|
+
* @returns {Promise<IDescribeReleaseOrderResponse>} 发布单详情
|
|
108
|
+
*/
|
|
109
|
+
async describeReleaseOrder(params) {
|
|
110
|
+
var _a, _b;
|
|
111
|
+
const envConfig = this.environment.lazyEnvironmentConfig;
|
|
112
|
+
const serverName = (_a = params === null || params === void 0 ? void 0 : params.ServerName) === null || _a === void 0 ? void 0 : _a.trim();
|
|
113
|
+
const status = (_b = params === null || params === void 0 ? void 0 : params.Status) === null || _b === void 0 ? void 0 : _b.trim();
|
|
114
|
+
if (!serverName) {
|
|
115
|
+
throw new Error('Missing required parameters: ServerName');
|
|
116
|
+
}
|
|
117
|
+
return this.tcbrService.request('DescribeReleaseOrder', Object.assign({ EnvId: envConfig.EnvId, ServerName: serverName }, (status ? { Status: status } : {})));
|
|
118
|
+
}
|
|
104
119
|
/**
|
|
105
120
|
* 获取云托管服务列表
|
|
106
121
|
* @param {Object} [params] 查询参数
|
|
@@ -521,6 +536,9 @@ __decorate([
|
|
|
521
536
|
__decorate([
|
|
522
537
|
(0, utils_1.preLazy)()
|
|
523
538
|
], CloudRunService.prototype, "describeServerManageTask", null);
|
|
539
|
+
__decorate([
|
|
540
|
+
(0, utils_1.preLazy)()
|
|
541
|
+
], CloudRunService.prototype, "describeReleaseOrder", null);
|
|
524
542
|
__decorate([
|
|
525
543
|
(0, utils_1.preLazy)()
|
|
526
544
|
], CloudRunService.prototype, "list", null);
|
|
@@ -72,13 +72,39 @@ class GatewayDeployer {
|
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
if (toCreate.length > 0) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
Domain:
|
|
79
|
-
|
|
75
|
+
try {
|
|
76
|
+
await envService.createHttpServiceRoute({
|
|
77
|
+
EnvId: envId,
|
|
78
|
+
Domain: {
|
|
79
|
+
Domain: domain,
|
|
80
|
+
Routes: toCreate
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
// 路由已存在(getExistingRoutes 因域名格式差异漏查,导致误判 create):
|
|
86
|
+
// 幂等降级为「逐个创建,已存在的跳过」,避免整批失败
|
|
87
|
+
if (!this.isAlreadyExistsError(e)) {
|
|
88
|
+
throw e;
|
|
80
89
|
}
|
|
81
|
-
|
|
90
|
+
for (const route of toCreate) {
|
|
91
|
+
try {
|
|
92
|
+
await envService.createHttpServiceRoute({
|
|
93
|
+
EnvId: envId,
|
|
94
|
+
Domain: {
|
|
95
|
+
Domain: domain,
|
|
96
|
+
Routes: [route]
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (e2) {
|
|
101
|
+
// 该条已存在 → 跳过(已收敛);否则(环境/资源不存在等真实失败)必须抛出,避免静默吞掉
|
|
102
|
+
if (!this.isAlreadyExistsError(e2)) {
|
|
103
|
+
throw e2;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
82
108
|
}
|
|
83
109
|
deployedDomains.push(domain);
|
|
84
110
|
for (let i = 0; i < parsedRoutes.length; i++) {
|
|
@@ -197,24 +223,80 @@ class GatewayDeployer {
|
|
|
197
223
|
}
|
|
198
224
|
/**
|
|
199
225
|
* 查询指定域名下已存在的路由(幂等收敛用),返回 Map<path, 云端路由详情>
|
|
226
|
+
*
|
|
227
|
+
* 匹配策略(优先级从高到低):
|
|
228
|
+
* 1. 用 Filters 精确查询目标域名:DescribeHTTPServiceRoute 不带 Filters 时返回的 Domains
|
|
229
|
+
* 列表可能不完整(如 lowcode 环境 TotalCount=67 但 Domains 只返回部分,HTTPSERVICE 的
|
|
230
|
+
* .app.tcloudbase.com 域名缺失),导致云端已有路由查不到被误判为 create
|
|
231
|
+
* 2. 降级:不带 Filters 全量查询,按域名归一化匹配(去协议/尾斜杠/转小写)
|
|
232
|
+
* 若目标域名找不到则返回空(保留 domain 维度,避免把其他域名同 path 误判为已存在)
|
|
200
233
|
*/
|
|
201
234
|
async getExistingRoutes(envId, domain) {
|
|
235
|
+
var _a;
|
|
236
|
+
const normalizedTarget = this.normalizeDomain(domain);
|
|
237
|
+
// 1. Filters 精确查询目标域名(deleteCustomDomain 同款用法)
|
|
202
238
|
try {
|
|
203
239
|
const res = await this.environment.getEnvService().describeHttpServiceRoute({
|
|
204
|
-
EnvId: envId
|
|
240
|
+
EnvId: envId,
|
|
241
|
+
Filters: [{ Name: 'Domain', Values: [domain] }]
|
|
205
242
|
});
|
|
206
|
-
const targetDomain = (res.Domains || []).find(d => d.Domain ===
|
|
243
|
+
const targetDomain = (res.Domains || []).find((d) => this.normalizeDomain(d.Domain) === normalizedTarget);
|
|
207
244
|
const map = new Map();
|
|
208
245
|
for (const route of (targetDomain === null || targetDomain === void 0 ? void 0 : targetDomain.Routes) || []) {
|
|
209
246
|
map.set(route.Path, route);
|
|
210
247
|
}
|
|
248
|
+
if (map.size > 0) {
|
|
249
|
+
return map;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (_b) {
|
|
253
|
+
// Filters 查询失败(环境不支持)→ 走降级
|
|
254
|
+
}
|
|
255
|
+
// 2. 降级:全量查询 + 归一化匹配(严格保留域名维度)
|
|
256
|
+
try {
|
|
257
|
+
const res = await this.environment.getEnvService().describeHttpServiceRoute({
|
|
258
|
+
EnvId: envId
|
|
259
|
+
});
|
|
260
|
+
const targetDomain = (res.Domains || []).find((d) => this.normalizeDomain(d.Domain) === normalizedTarget);
|
|
261
|
+
const map = new Map();
|
|
262
|
+
if ((_a = targetDomain === null || targetDomain === void 0 ? void 0 : targetDomain.Routes) === null || _a === void 0 ? void 0 : _a.length) {
|
|
263
|
+
for (const route of targetDomain.Routes) {
|
|
264
|
+
map.set(route.Path, route);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// 不再按 path 跨域兜底归并:
|
|
268
|
+
// 路由唯一键是 (domain, path),不同域名同 path 合法;
|
|
269
|
+
// 若目标域名缺失,返回空并交由 create + AlreadyExists 幂等降级收敛,避免误判 skip/update。
|
|
211
270
|
return map;
|
|
212
271
|
}
|
|
213
|
-
catch (
|
|
272
|
+
catch (_c) {
|
|
214
273
|
// 查询失败(首次部署/环境未初始化)视为无已存在路由
|
|
215
274
|
return new Map();
|
|
216
275
|
}
|
|
217
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* 域名归一化:去协议、去尾斜杠、转小写(用于幂等收敛的域名匹配)
|
|
279
|
+
*/
|
|
280
|
+
normalizeDomain(domain) {
|
|
281
|
+
if (!domain)
|
|
282
|
+
return '';
|
|
283
|
+
return domain.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase();
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 判断错误是否属于「路由已存在/已占用」类(幂等降级专用)
|
|
287
|
+
*
|
|
288
|
+
* 只匹配明确的重复/占用语义(code 含 AlreadyExists/Duplicate/ResourceInUse/Conflict,
|
|
289
|
+
* 或 message 含 already exists),并显式排除否定语义(not exists/not found 等),
|
|
290
|
+
* 避免把「环境/资源不存在」等真实失败误判为「已存在」而被静默跳过
|
|
291
|
+
*/
|
|
292
|
+
isAlreadyExistsError(e) {
|
|
293
|
+
const msg = String((e === null || e === void 0 ? void 0 : e.message) || '') + String((e === null || e === void 0 ? void 0 : e.code) || '');
|
|
294
|
+
// 否定语义优先:资源/环境不存在 → 不是「已存在」,必须抛出让调用方感知真实失败
|
|
295
|
+
if (/not\s*exists?|doesn't?\s*exist|does\s+not\s+exist|not\s*found/i.test(msg)) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return /already\s*exists?|already[_\s-]*exists|AlreadyExists|ALREADY_EXISTS|RESOURCE_ALREADY_EXISTS|Duplicate|ResourceInUse|ResourceOccupied|Conflict|已存在/i.test(msg);
|
|
299
|
+
}
|
|
218
300
|
/**
|
|
219
301
|
* 判断期望路由与云端已存在路由是否一致
|
|
220
302
|
*
|
|
@@ -395,8 +477,16 @@ class GatewayDeployer {
|
|
|
395
477
|
`或为该路由显式配置 pathRewrite.prefix/staticStorePrefix 以引用已部署的托管实例`);
|
|
396
478
|
}
|
|
397
479
|
// 未显式配置时自动生成 Prefix = hosting 部署路径
|
|
480
|
+
// 透传会覆盖路径重写:用户显式开启 enablePathTransmission=true 时与自动重写冲突,
|
|
481
|
+
// 不再静默强制关闭(避免反向改写云端用户显式配置),明确报错让用户二选一
|
|
398
482
|
if (hosting.deployPath) {
|
|
399
|
-
|
|
483
|
+
if (route.enablePathTransmission === true) {
|
|
484
|
+
throw new error_1.CloudBaseError(`网关路由 ${route.path} 目标为 hosting:${hostingName} 且未显式配置 pathRewrite:` +
|
|
485
|
+
`部署将自动生成 PathRewrite.Prefix=${hosting.deployPath},` +
|
|
486
|
+
`但 enablePathTransmission=true 会覆盖路径重写导致请求打不到托管部署路径。` +
|
|
487
|
+
`请删除 enablePathTransmission,或显式配置 pathRewrite.prefix`);
|
|
488
|
+
}
|
|
489
|
+
return Object.assign(Object.assign({}, parsed), { EnablePathTransmission: false, PathRewrite: { Prefix: hosting.deployPath } });
|
|
400
490
|
}
|
|
401
491
|
return parsed;
|
|
402
492
|
}
|
|
@@ -57,6 +57,7 @@ class StaticDeployer {
|
|
|
57
57
|
* 访问地址 = https://{CdnDomain}{deployPath}(与 tcb hosting deploy 一致)
|
|
58
58
|
*/
|
|
59
59
|
async deployHosting(options) {
|
|
60
|
+
var _a, _b;
|
|
60
61
|
const { config, cwd = process.cwd() } = options;
|
|
61
62
|
const name = config.name || 'default';
|
|
62
63
|
const root = path_1.default.resolve(cwd, config.root || '.');
|
|
@@ -81,13 +82,49 @@ class StaticDeployer {
|
|
|
81
82
|
await this.environment.getHostingService().uploadFiles(hostingOptions);
|
|
82
83
|
// SPA 回退:
|
|
83
84
|
// - true:404 时返回 index.html(OriginalHttpStatus=Disabled 保留 200,Charity404=Disabled 防公益页面覆盖)
|
|
85
|
+
// ErrorDocument 不带 deployPath 前缀:TCB 托管会把 ErrorDocument 相对 deployPath 解析
|
|
86
|
+
// (index.html → web-pay-0804-test-ignore8/index.html),加前缀反而双重嵌套找不到
|
|
84
87
|
// - false:显式取消,重置 ErrorDocument 为空(静态托管首页 IndexDocument 保留)
|
|
85
88
|
// - undefined:不关心,保持云端现状
|
|
86
89
|
// 注意:putBucketWebsite 是整体覆盖,设置前先读取现有配置并保留 RoutingRules + AutoAddressing,
|
|
87
90
|
// 否则会清空用户之前在控制台配置的路由规则(如路径前缀重写/错误码重定向)和忽略 .html 扩展名能力
|
|
91
|
+
// 仅移除当前 deployPath 对应的 SPA 回退规则:
|
|
92
|
+
// cloudPath='/' => index.html;cloudPath='/web' => web/index.html
|
|
93
|
+
// 不能泛化删除所有 */index.html,否则会误删用户自定义的 404 重定向规则
|
|
94
|
+
const indexKey = cloudPath === '/' ? 'index.html' : `${cloudPath.replace(/^\//, '')}/index.html`;
|
|
95
|
+
const isCurrentSpaFallbackRedirect = (r) => {
|
|
96
|
+
if (r.httpErrorCodeReturnedEquals !== '404')
|
|
97
|
+
return false;
|
|
98
|
+
return r.replaceKeyWith === indexKey;
|
|
99
|
+
};
|
|
100
|
+
// 开启 SPA 回退时,清理历史上指向 index.html 的 404 重定向(含 ReplaceKeyPrefixWith)
|
|
101
|
+
// 避免 302 到 cos-website 与 ErrorDocument= index.html 冲突
|
|
102
|
+
const isLegacySpaIndexFallbackRedirect = (r) => {
|
|
103
|
+
var _a, _b;
|
|
104
|
+
if (r.httpErrorCodeReturnedEquals !== '404')
|
|
105
|
+
return false;
|
|
106
|
+
if (r.replaceKeyWith === 'index.html')
|
|
107
|
+
return true;
|
|
108
|
+
if ((_a = r.replaceKeyWith) === null || _a === void 0 ? void 0 : _a.endsWith('/index.html'))
|
|
109
|
+
return true;
|
|
110
|
+
if (r.replaceKeyPrefixWith === 'index.html')
|
|
111
|
+
return true;
|
|
112
|
+
if ((_b = r.replaceKeyPrefixWith) === null || _b === void 0 ? void 0 : _b.endsWith('/index.html'))
|
|
113
|
+
return true;
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
88
116
|
if (config.spaFallback === true) {
|
|
89
117
|
try {
|
|
90
|
-
|
|
118
|
+
const preserved = await this.preserveWebsiteConfig();
|
|
119
|
+
await this.environment.getHostingService().setWebsiteDocument({
|
|
120
|
+
indexDocument: 'index.html',
|
|
121
|
+
errorDocument: 'index.html',
|
|
122
|
+
originalHttpStatus: 'Disabled',
|
|
123
|
+
charity404: 'Disabled',
|
|
124
|
+
routingRules: (preserved.routingRules || []).filter((r) => !isLegacySpaIndexFallbackRedirect(r)),
|
|
125
|
+
// 优先保留云端 AutoAddressing(避免覆盖控制台配置),缺失时再兜底 Disabled
|
|
126
|
+
autoAddressing: (_a = preserved.autoAddressing) !== null && _a !== void 0 ? _a : 'Disabled'
|
|
127
|
+
});
|
|
91
128
|
}
|
|
92
129
|
catch (e) {
|
|
93
130
|
throw new error_1.CloudBaseError(`[${name}] 设置 SPA 回退失败(spaFallback: true):${(e === null || e === void 0 ? void 0 : e.message) || String(e)}`);
|
|
@@ -95,7 +132,16 @@ class StaticDeployer {
|
|
|
95
132
|
}
|
|
96
133
|
else if (config.spaFallback === false) {
|
|
97
134
|
try {
|
|
98
|
-
|
|
135
|
+
const preserved = await this.preserveWebsiteConfig();
|
|
136
|
+
await this.environment.getHostingService().setWebsiteDocument({
|
|
137
|
+
indexDocument: 'index.html',
|
|
138
|
+
errorDocument: '',
|
|
139
|
+
// 仅移除当前 deployPath 对应的 SPA 回退规则(404 + replaceKeyWith=indexKey)
|
|
140
|
+
// 保留用户自定义 404 规则和其他路由规则
|
|
141
|
+
routingRules: (preserved.routingRules || []).filter((r) => !isCurrentSpaFallbackRedirect(r)),
|
|
142
|
+
// 优先保留云端 AutoAddressing(避免覆盖控制台配置),缺失时再兜底 Enabled
|
|
143
|
+
autoAddressing: (_b = preserved.autoAddressing) !== null && _b !== void 0 ? _b : 'Enabled'
|
|
144
|
+
});
|
|
99
145
|
}
|
|
100
146
|
catch (e) {
|
|
101
147
|
throw new error_1.CloudBaseError(`[${name}] 取消 SPA 回退失败(spaFallback: false):${(e === null || e === void 0 ? void 0 : e.message) || String(e)}`);
|
|
@@ -42,7 +42,7 @@ class ProjectValidator {
|
|
|
42
42
|
const resourceIssues = this.validateResources(cwd, loaded.config);
|
|
43
43
|
errors.push(...resourceIssues.errors);
|
|
44
44
|
warnings.push(...resourceIssues.warnings);
|
|
45
|
-
this.validateConsistency(loaded.config, errors);
|
|
45
|
+
this.validateConsistency(loaded.config, errors, warnings);
|
|
46
46
|
}
|
|
47
47
|
const summary = this.getSummary(loaded.config);
|
|
48
48
|
return {
|
|
@@ -277,11 +277,115 @@ class ProjectValidator {
|
|
|
277
277
|
}
|
|
278
278
|
return { errors, warnings };
|
|
279
279
|
}
|
|
280
|
-
validateConsistency(config, errors) {
|
|
280
|
+
validateConsistency(config, errors, warnings = []) {
|
|
281
281
|
this.validateUniqueNames('functions', config.functions || [], errors);
|
|
282
282
|
this.validateUniqueNames('hosting', config.hosting || [], errors);
|
|
283
283
|
this.validateUniqueField('hosting', config.hosting || [], { field: 'deployPath', errors });
|
|
284
284
|
this.validateGatewayTargets(config, errors);
|
|
285
|
+
this.validateGatewayPathRewriteConflicts(config, warnings);
|
|
286
|
+
this.validateHostingSubPathBaseRisks(config, warnings);
|
|
287
|
+
this.validateSpaFallbackPrefixIsolation(config, warnings);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* 检测 hosting 路由中 enablePathTransmission=true 与自动生成 pathRewrite 的冲突
|
|
291
|
+
*
|
|
292
|
+
* hosting 目标未显式配置 pathRewrite 时,部署会自动生成 PathRewrite.Prefix = hosting.deployPath。
|
|
293
|
+
* 若同时显式开启 enablePathTransmission=true,路径透传会覆盖路径重写,导致请求打到错误的托管路径(404)。
|
|
294
|
+
*/
|
|
295
|
+
validateGatewayPathRewriteConflicts(config, warnings) {
|
|
296
|
+
var _a;
|
|
297
|
+
const hostingMap = new Map((config.hosting || []).map((h) => [h.name, h]));
|
|
298
|
+
(((_a = config.gateway) === null || _a === void 0 ? void 0 : _a.routes) || []).forEach((route, index) => {
|
|
299
|
+
var _a, _b, _c;
|
|
300
|
+
if (!((_a = route.target) === null || _a === void 0 ? void 0 : _a.startsWith('hosting:')))
|
|
301
|
+
return;
|
|
302
|
+
// 用户显式配置了 pathRewrite → 无自动重写,无冲突
|
|
303
|
+
if (((_b = route.pathRewrite) === null || _b === void 0 ? void 0 : _b.prefix) || ((_c = route.pathRewrite) === null || _c === void 0 ? void 0 : _c.staticStorePrefix))
|
|
304
|
+
return;
|
|
305
|
+
// 未显式开启透传 → 无冲突
|
|
306
|
+
if (route.enablePathTransmission !== true)
|
|
307
|
+
return;
|
|
308
|
+
const hostingName = route.target.slice('hosting:'.length);
|
|
309
|
+
const hosting = hostingMap.get(hostingName);
|
|
310
|
+
// hosting 无 deployPath → 不会自动生成 PathRewrite,不冲突
|
|
311
|
+
if (!(hosting === null || hosting === void 0 ? void 0 : hosting.deployPath))
|
|
312
|
+
return;
|
|
313
|
+
warnings.push({
|
|
314
|
+
level: 'warning',
|
|
315
|
+
resource: `gateway.routes[${index}]`,
|
|
316
|
+
field: 'enablePathTransmission',
|
|
317
|
+
message: `hosting 路由 ${route.target} 未显式配置 pathRewrite,部署将自动生成 ` +
|
|
318
|
+
`PathRewrite.Prefix=${hosting.deployPath},但 enablePathTransmission=true 会覆盖路径重写,` +
|
|
319
|
+
`导致请求打不到托管实际部署路径`,
|
|
320
|
+
fix: '删除 enablePathTransmission,让自动生成的 pathRewrite 生效;' +
|
|
321
|
+
'或显式配置 pathRewrite.prefix 指向托管部署路径',
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 子路径部署(deployPath !== '/')下,前端构建产物若仍使用根路径 base(如 Vite 默认 '/'),
|
|
327
|
+
* 会导致资源请求落到 /assets/* 而非 /{deployPath}/assets/*,出现 404/白屏。
|
|
328
|
+
*
|
|
329
|
+
* 这里在“可能存在构建步骤”的 hosting 配置上给出防坑告警。
|
|
330
|
+
*/
|
|
331
|
+
validateHostingSubPathBaseRisks(config, warnings) {
|
|
332
|
+
;
|
|
333
|
+
(config.hosting || []).forEach((hosting, index) => {
|
|
334
|
+
const deployPath = typeof hosting.deployPath === 'string' ? hosting.deployPath.trim() : '';
|
|
335
|
+
if (!deployPath || deployPath === '/')
|
|
336
|
+
return;
|
|
337
|
+
const framework = typeof hosting.framework === 'string' ? hosting.framework.trim().toLowerCase() : '';
|
|
338
|
+
const hasBuildStep = (typeof hosting.buildCommand === 'string' && hosting.buildCommand.trim().length > 0) ||
|
|
339
|
+
(!!framework && framework !== 'static' && framework !== 'custom');
|
|
340
|
+
if (!hasBuildStep)
|
|
341
|
+
return;
|
|
342
|
+
warnings.push({
|
|
343
|
+
level: 'warning',
|
|
344
|
+
resource: `hosting[${index}]`,
|
|
345
|
+
field: 'deployPath',
|
|
346
|
+
message: `静态托管部署在子路径 ${deployPath},请确认前端构建 base 与子路径匹配,` +
|
|
347
|
+
`否则资源可能请求到根路径(如 /assets/*)导致 404/白屏`,
|
|
348
|
+
fix: "建议将前端 base 配置为 './' 或与 deployPath 对齐(例如 '/serviceName/')",
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* spaFallback 为站点级回退能力,不是按网关前缀独立生效。
|
|
354
|
+
* 同一 hosting 在 spaFallback=true 下被多个网关前缀复用时,容易出现不同前缀回退串扰。
|
|
355
|
+
*/
|
|
356
|
+
validateSpaFallbackPrefixIsolation(config, warnings) {
|
|
357
|
+
var _a;
|
|
358
|
+
const routePathsByHosting = new Map();
|
|
359
|
+
(((_a = config.gateway) === null || _a === void 0 ? void 0 : _a.routes) || []).forEach((route) => {
|
|
360
|
+
var _a;
|
|
361
|
+
if (!((_a = route.target) === null || _a === void 0 ? void 0 : _a.startsWith('hosting:')))
|
|
362
|
+
return;
|
|
363
|
+
const hostingName = route.target.slice('hosting:'.length);
|
|
364
|
+
const pathValue = typeof route.path === 'string' ? route.path.trim() : '';
|
|
365
|
+
if (!pathValue)
|
|
366
|
+
return;
|
|
367
|
+
if (!routePathsByHosting.has(hostingName)) {
|
|
368
|
+
routePathsByHosting.set(hostingName, new Set());
|
|
369
|
+
}
|
|
370
|
+
routePathsByHosting.get(hostingName).add(pathValue);
|
|
371
|
+
});
|
|
372
|
+
(config.hosting || []).forEach((hosting, index) => {
|
|
373
|
+
if (hosting.spaFallback !== true)
|
|
374
|
+
return;
|
|
375
|
+
if (!hosting.name)
|
|
376
|
+
return;
|
|
377
|
+
const routePaths = [...(routePathsByHosting.get(hosting.name) || new Set())];
|
|
378
|
+
if (routePaths.length <= 1)
|
|
379
|
+
return;
|
|
380
|
+
warnings.push({
|
|
381
|
+
level: 'warning',
|
|
382
|
+
resource: `hosting[${index}]`,
|
|
383
|
+
field: 'spaFallback',
|
|
384
|
+
message: `hosting:${hosting.name} 在 spaFallback=true 下被多个网关前缀复用(${routePaths.join(', ')}),` +
|
|
385
|
+
'可能出现前缀间 404 回退串扰',
|
|
386
|
+
fix: '建议将不同前缀拆分为独立 hosting 服务,或为每个前缀显式配置独立回退策略',
|
|
387
|
+
});
|
|
388
|
+
});
|
|
285
389
|
}
|
|
286
390
|
validateUniqueNames(resource, items, errors) {
|
|
287
391
|
const names = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Environment } from '../environment';
|
|
2
2
|
import { IResponseInfo } from '../interfaces';
|
|
3
|
-
import { CloudrunServerType, ICloudrunDetailResponse, ICloudrunListResponse, ICloudrunServerBaseConfig, ICreateVpcInfo, IBuildLogResponse, IProcessLogResponse, IDiffConfigItem, ITemplate, ReleaseTypeEnum, IServerManageTaskInfo, IDescribeCloudRunDeployRecordResponse, IDescribeVersionDetailParams, IDescribeVersionDetailResponse, ISubmitServerRollbackParams, ISubmitServerRollbackResponse, IDeleteCloudRunVersionsParams, IDeleteCloudRunVersionsResponse } from './type';
|
|
3
|
+
import { CloudrunServerType, ICloudrunDetailResponse, ICloudrunListResponse, ICloudrunServerBaseConfig, ICreateVpcInfo, IBuildLogResponse, IProcessLogResponse, IDiffConfigItem, ITemplate, ReleaseTypeEnum, IServerManageTaskInfo, IDescribeReleaseOrderParams, IDescribeReleaseOrderResponse, IDescribeCloudRunDeployRecordResponse, IDescribeVersionDetailParams, IDescribeVersionDetailResponse, ISubmitServerRollbackParams, ISubmitServerRollbackResponse, IDeleteCloudRunVersionsParams, IDeleteCloudRunVersionsResponse } from './type';
|
|
4
4
|
/**
|
|
5
5
|
* 云托管服务管理类
|
|
6
6
|
* 提供云托管服务的初始化、下载、列表查询和删除等功能
|
|
@@ -58,6 +58,12 @@ export declare class CloudRunService {
|
|
|
58
58
|
Task?: IServerManageTaskInfo;
|
|
59
59
|
RequestId?: string;
|
|
60
60
|
}>;
|
|
61
|
+
/**
|
|
62
|
+
* 查询发布单信息(DescribeReleaseOrder)
|
|
63
|
+
* @param {IDescribeReleaseOrderParams} params 查询参数
|
|
64
|
+
* @returns {Promise<IDescribeReleaseOrderResponse>} 发布单详情
|
|
65
|
+
*/
|
|
66
|
+
describeReleaseOrder(params: IDescribeReleaseOrderParams): Promise<IDescribeReleaseOrderResponse>;
|
|
61
67
|
/**
|
|
62
68
|
* 获取云托管服务列表
|
|
63
69
|
* @param {Object} [params] 查询参数
|
package/types/cloudrun/type.d.ts
CHANGED
|
@@ -426,6 +426,28 @@ export interface IReleaseOrderInfo {
|
|
|
426
426
|
CreateTime?: string;
|
|
427
427
|
IsReleasing?: boolean;
|
|
428
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* DescribeReleaseOrder 请求参数
|
|
431
|
+
*/
|
|
432
|
+
export interface IDescribeReleaseOrderParams {
|
|
433
|
+
/** 服务名 */
|
|
434
|
+
ServerName: string;
|
|
435
|
+
/** 发布单状态(可选) */
|
|
436
|
+
Status?: string;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* DescribeReleaseOrder 响应
|
|
440
|
+
*/
|
|
441
|
+
export interface IDescribeReleaseOrderResponse {
|
|
442
|
+
/** 是否存在发布单 */
|
|
443
|
+
IsExist?: boolean;
|
|
444
|
+
/** 发布单详情 */
|
|
445
|
+
ReleaseOrderInfo?: IReleaseOrderInfo;
|
|
446
|
+
/** 上一次成功发布时间 */
|
|
447
|
+
LastReleasedSuccessTime?: string;
|
|
448
|
+
/** 请求 ID */
|
|
449
|
+
RequestId: string;
|
|
450
|
+
}
|
|
429
451
|
/**
|
|
430
452
|
* DescribeVersionDetail 请求参数
|
|
431
453
|
*/
|
|
@@ -104,8 +104,27 @@ export declare class GatewayDeployer {
|
|
|
104
104
|
}>>;
|
|
105
105
|
/**
|
|
106
106
|
* 查询指定域名下已存在的路由(幂等收敛用),返回 Map<path, 云端路由详情>
|
|
107
|
+
*
|
|
108
|
+
* 匹配策略(优先级从高到低):
|
|
109
|
+
* 1. 用 Filters 精确查询目标域名:DescribeHTTPServiceRoute 不带 Filters 时返回的 Domains
|
|
110
|
+
* 列表可能不完整(如 lowcode 环境 TotalCount=67 但 Domains 只返回部分,HTTPSERVICE 的
|
|
111
|
+
* .app.tcloudbase.com 域名缺失),导致云端已有路由查不到被误判为 create
|
|
112
|
+
* 2. 降级:不带 Filters 全量查询,按域名归一化匹配(去协议/尾斜杠/转小写)
|
|
113
|
+
* 若目标域名找不到则返回空(保留 domain 维度,避免把其他域名同 path 误判为已存在)
|
|
107
114
|
*/
|
|
108
115
|
private getExistingRoutes;
|
|
116
|
+
/**
|
|
117
|
+
* 域名归一化:去协议、去尾斜杠、转小写(用于幂等收敛的域名匹配)
|
|
118
|
+
*/
|
|
119
|
+
private normalizeDomain;
|
|
120
|
+
/**
|
|
121
|
+
* 判断错误是否属于「路由已存在/已占用」类(幂等降级专用)
|
|
122
|
+
*
|
|
123
|
+
* 只匹配明确的重复/占用语义(code 含 AlreadyExists/Duplicate/ResourceInUse/Conflict,
|
|
124
|
+
* 或 message 含 already exists),并显式排除否定语义(not exists/not found 等),
|
|
125
|
+
* 避免把「环境/资源不存在」等真实失败误判为「已存在」而被静默跳过
|
|
126
|
+
*/
|
|
127
|
+
private isAlreadyExistsError;
|
|
109
128
|
/**
|
|
110
129
|
* 判断期望路由与云端已存在路由是否一致
|
|
111
130
|
*
|
|
@@ -13,6 +13,25 @@ export declare class ProjectValidator {
|
|
|
13
13
|
private isPersonalCloudFunction;
|
|
14
14
|
private validateResources;
|
|
15
15
|
private validateConsistency;
|
|
16
|
+
/**
|
|
17
|
+
* 检测 hosting 路由中 enablePathTransmission=true 与自动生成 pathRewrite 的冲突
|
|
18
|
+
*
|
|
19
|
+
* hosting 目标未显式配置 pathRewrite 时,部署会自动生成 PathRewrite.Prefix = hosting.deployPath。
|
|
20
|
+
* 若同时显式开启 enablePathTransmission=true,路径透传会覆盖路径重写,导致请求打到错误的托管路径(404)。
|
|
21
|
+
*/
|
|
22
|
+
private validateGatewayPathRewriteConflicts;
|
|
23
|
+
/**
|
|
24
|
+
* 子路径部署(deployPath !== '/')下,前端构建产物若仍使用根路径 base(如 Vite 默认 '/'),
|
|
25
|
+
* 会导致资源请求落到 /assets/* 而非 /{deployPath}/assets/*,出现 404/白屏。
|
|
26
|
+
*
|
|
27
|
+
* 这里在“可能存在构建步骤”的 hosting 配置上给出防坑告警。
|
|
28
|
+
*/
|
|
29
|
+
private validateHostingSubPathBaseRisks;
|
|
30
|
+
/**
|
|
31
|
+
* spaFallback 为站点级回退能力,不是按网关前缀独立生效。
|
|
32
|
+
* 同一 hosting 在 spaFallback=true 下被多个网关前缀复用时,容易出现不同前缀回退串扰。
|
|
33
|
+
*/
|
|
34
|
+
private validateSpaFallbackPrefixIsolation;
|
|
16
35
|
private validateUniqueNames;
|
|
17
36
|
private validateUniqueField;
|
|
18
37
|
private validateGatewayTargets;
|