@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
package/CHANGELOG.md
CHANGED
package/changelog_content_en.md
CHANGED
package/lib/database/index.js
CHANGED
|
@@ -325,6 +325,73 @@ class DatabaseService {
|
|
|
325
325
|
}
|
|
326
326
|
return this.dbOpService.request('ExecutePGSql', params);
|
|
327
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* 开通 PG 数据库 Realtime 能力
|
|
330
|
+
* 用于服务首次开通;已开通时重复调用仍返回 Enabled: true(幂等)
|
|
331
|
+
*
|
|
332
|
+
* @param options 开通参数
|
|
333
|
+
* @param options.EnvId 环境 ID(可选,不传则自动使用当前环境 ID)
|
|
334
|
+
* @returns 开通状态
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* // 使用当前环境 ID 开通
|
|
338
|
+
* const res = await database.enablePGRealtime()
|
|
339
|
+
* console.log(res.Enabled) // true
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* // 指定环境 ID 开通
|
|
343
|
+
* const res = await database.enablePGRealtime({ EnvId: 'pg-shared-d4gq5c0ne1a30f155' })
|
|
344
|
+
*/
|
|
345
|
+
async enablePGRealtime(options = {}) {
|
|
346
|
+
return this.dbOpService.request('EnablePGRealtime', {
|
|
347
|
+
EnvId: options.EnvId || this.envId
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* 查询 PG Realtime 设置
|
|
352
|
+
* 返回当前挂起状态、频道策略、限流阈值、连接池大小及创建/更新时间
|
|
353
|
+
*
|
|
354
|
+
* @param options 查询参数
|
|
355
|
+
* @param options.EnvId 环境 ID(可选,不传则自动使用当前环境 ID)
|
|
356
|
+
* @returns Realtime 当前配置
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* const settings = await database.describePGRealtimeSettings()
|
|
360
|
+
* console.log(settings.Suspend, settings.MaxConcurrentUsers)
|
|
361
|
+
*/
|
|
362
|
+
async describePGRealtimeSettings(options = {}) {
|
|
363
|
+
return this.dbOpService.request('DescribePGRealtimeSettings', {
|
|
364
|
+
EnvId: options.EnvId || this.envId
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* 修改 PG Realtime 设置
|
|
369
|
+
*
|
|
370
|
+
* 增量修改:只需传要变更的字段,未传字段保持服务端当前值;
|
|
371
|
+
* 建议先用 describePGRealtimeSettings 获取当前配置再按需调整。
|
|
372
|
+
* 关闭/挂起 Realtime 会断开所有已有连接,客户端需重连。
|
|
373
|
+
*
|
|
374
|
+
* @param options 需要变更的设置项
|
|
375
|
+
* @param options.EnvId 环境 ID(可选,不传则自动使用当前环境 ID)
|
|
376
|
+
* @returns RequestId
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* // 启用 Realtime(取消挂起)
|
|
380
|
+
* await database.modifyPGRealtimeSettings({ Suspend: false })
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* // 更新最大并发用户数与限流阈值
|
|
384
|
+
* await database.modifyPGRealtimeSettings({
|
|
385
|
+
* MaxConcurrentUsers: 50002,
|
|
386
|
+
* MaxEventsPerSecond: 200
|
|
387
|
+
* })
|
|
388
|
+
*/
|
|
389
|
+
async modifyPGRealtimeSettings(options = {}) {
|
|
390
|
+
const { EnvId, Suspend, PrivateOnly, PresenceEnabled, MaxConcurrentUsers, MaxEventsPerSecond, MaxJoinsPerSecond, MaxBytesPerSecond, MaxChannelsPerClient, MaxPresenceEventsPerSecond, MaxPayloadSizeInKB, ConnectionPool, PostgresChangesPool } = options;
|
|
391
|
+
return this.dbOpService.request('ModifyPGRealtimeSettings', Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ EnvId: EnvId || this.envId }, (Suspend !== undefined ? { Suspend } : {})), (PrivateOnly !== undefined ? { PrivateOnly } : {})), (PresenceEnabled !== undefined ? { PresenceEnabled } : {})), (MaxConcurrentUsers !== undefined ? { MaxConcurrentUsers } : {})), (MaxEventsPerSecond !== undefined ? { MaxEventsPerSecond } : {})), (MaxJoinsPerSecond !== undefined ? { MaxJoinsPerSecond } : {})), (MaxBytesPerSecond !== undefined ? { MaxBytesPerSecond } : {})), (MaxChannelsPerClient !== undefined ? { MaxChannelsPerClient } : {})), (MaxPresenceEventsPerSecond !== undefined
|
|
392
|
+
? { MaxPresenceEventsPerSecond }
|
|
393
|
+
: {})), (MaxPayloadSizeInKB !== undefined ? { MaxPayloadSizeInKB } : {})), (ConnectionPool !== undefined ? { ConnectionPool } : {})), (PostgresChangesPool !== undefined ? { PostgresChangesPool } : {})));
|
|
394
|
+
}
|
|
328
395
|
/**
|
|
329
396
|
* 预览 PG 用户 migrations 的远端执行计划(不实际执行 SQL)
|
|
330
397
|
* @param options 预览参数
|
|
@@ -548,6 +615,15 @@ __decorate([
|
|
|
548
615
|
__decorate([
|
|
549
616
|
preLazy()
|
|
550
617
|
], DatabaseService.prototype, "executePGSql", null);
|
|
618
|
+
__decorate([
|
|
619
|
+
preLazy()
|
|
620
|
+
], DatabaseService.prototype, "enablePGRealtime", null);
|
|
621
|
+
__decorate([
|
|
622
|
+
preLazy()
|
|
623
|
+
], DatabaseService.prototype, "describePGRealtimeSettings", null);
|
|
624
|
+
__decorate([
|
|
625
|
+
preLazy()
|
|
626
|
+
], DatabaseService.prototype, "modifyPGRealtimeSettings", null);
|
|
551
627
|
__decorate([
|
|
552
628
|
preLazy()
|
|
553
629
|
], DatabaseService.prototype, "previewPGUserMigrations", null);
|
|
@@ -10,6 +10,7 @@ const DatabaseDeployer_1 = require("./DatabaseDeployer");
|
|
|
10
10
|
const FunctionDeployer_1 = require("./FunctionDeployer");
|
|
11
11
|
const StaticDeployer_1 = require("./StaticDeployer");
|
|
12
12
|
const GatewayDeployer_1 = require("./GatewayDeployer");
|
|
13
|
+
const framework_1 = require("./framework");
|
|
13
14
|
const config_guard_1 = require("./function/config-guard");
|
|
14
15
|
const parallel_1 = require("../utils/parallel");
|
|
15
16
|
/** 部署顺序(按依赖:database 最先,函数依赖新 Schema;gateway 最后,依赖函数/hosting) */
|
|
@@ -31,7 +32,7 @@ class DeployOrchestrator {
|
|
|
31
32
|
* 计算部署计划(dry-run 输出)
|
|
32
33
|
*/
|
|
33
34
|
async deployPlan(options) {
|
|
34
|
-
const config = this.
|
|
35
|
+
const config = this.normalizeConfigInput(options.config, options.envId, options.cwd);
|
|
35
36
|
return this.buildPlan(config, options);
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
@@ -46,7 +47,7 @@ class DeployOrchestrator {
|
|
|
46
47
|
async deploy(options) {
|
|
47
48
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
48
49
|
const { config: inputConfig, envId, dryRun, cwd = process.cwd(), log, concurrency = 5, continueOnError = false } = options;
|
|
49
|
-
const config = this.
|
|
50
|
+
const config = this.normalizeConfigInput(inputConfig, envId, cwd);
|
|
50
51
|
// 1. 轻量 inline 守卫(validateProject 未接入时的兜底)
|
|
51
52
|
this.guardConfig(config);
|
|
52
53
|
const plan = await this.buildPlan(config, options);
|
|
@@ -173,6 +174,31 @@ class DeployOrchestrator {
|
|
|
173
174
|
}
|
|
174
175
|
return { plan, results };
|
|
175
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* 部署入口归一化:cloudbaserc 原文 → 编排器内部契约(唯一的输入形状收口处)
|
|
179
|
+
*
|
|
180
|
+
* cloudbaserc 是用户手改的 JSON,schema 只在 `tcb validate` / MCP 注入侧生效
|
|
181
|
+
* (见 `StaticDeployer.resolveInsideBase` 注释里 `"root": 123` 的例子),因此
|
|
182
|
+
* `"functions": ""` / `0` / `false` / `{}`、`"hosting": 123`、`"gateway": []`
|
|
183
|
+
* 都是真实可达输入。
|
|
184
|
+
*
|
|
185
|
+
* 为什么必须收口在**入口**而不是各消费点自己防:
|
|
186
|
+
* `(config.hosting || []).map(...)` 对 `''` 恰好安全,对 `{}` / `123` 会抛
|
|
187
|
+
* `x.map is not a function` —— 不含字段名,用户无从定位。这类守卫此前分散在
|
|
188
|
+
* `buildHostingPlans` / `executeStep` / 网关归一化三处,每新增一个消费点就漏一处。
|
|
189
|
+
* 归一化后,消费点可以假设「顶层资源声明一定是对象数组」。
|
|
190
|
+
*
|
|
191
|
+
* 降级口径与历史 `config.hosting || []` 一致 —— **非数组一律降级为空数组且不报错**,
|
|
192
|
+
* 存量把手写错的配置仍能跑通其余资源的部署;数组元素非对象则没有静默降级的合理语义
|
|
193
|
+
* (会漏部署一个站点/一条路由),显式报 `INVALID_PARAM` 并带字段名与下标。
|
|
194
|
+
*/
|
|
195
|
+
normalizeConfigInput(config, envId, cwd = process.cwd()) {
|
|
196
|
+
this.guardConfigShape(config);
|
|
197
|
+
// 先收口形状,再跑函数归一化:`normalizeFunctionConfigs` 会直接 `.map` functions,
|
|
198
|
+
// 元素非对象时同样会抛不含字段名的原生错误
|
|
199
|
+
const shaped = Object.assign(Object.assign({}, config), { functions: normalizeResourceList(config.functions, 'functions'), hosting: normalizeResourceList(config.hosting, 'hosting'), gateway: normalizeGatewayConfig(config.gateway) });
|
|
200
|
+
return this.normalizeFunctionConfigs(shaped, envId, cwd);
|
|
201
|
+
}
|
|
176
202
|
/**
|
|
177
203
|
* 归一化声明式函数配置:直接消费 cloudbaserc 的嵌套形状(imageConfig.build),
|
|
178
204
|
* 不做字段平铺映射;非 HTTP 函数原样返回。
|
|
@@ -312,10 +338,10 @@ class DeployOrchestrator {
|
|
|
312
338
|
});
|
|
313
339
|
}
|
|
314
340
|
if (type === 'app') {
|
|
315
|
-
return this.buildAppPlan({ config, envId });
|
|
341
|
+
return this.buildAppPlan({ config, envId, cwd });
|
|
316
342
|
}
|
|
317
343
|
if (type === 'hosting') {
|
|
318
|
-
return this.buildHostingPlans(config);
|
|
344
|
+
return this.buildHostingPlans(config, cwd);
|
|
319
345
|
}
|
|
320
346
|
return this.buildGatewayPlans(config, envId);
|
|
321
347
|
}
|
|
@@ -373,53 +399,190 @@ class DeployOrchestrator {
|
|
|
373
399
|
}
|
|
374
400
|
/**
|
|
375
401
|
* app 计划:按云端存在性 → create / update
|
|
402
|
+
*
|
|
403
|
+
* 附带 buildHint:声明式部署不构建,调用方需要据此自行构建(见 IBuildHint)。
|
|
376
404
|
*/
|
|
377
405
|
async buildAppPlan(options) {
|
|
378
|
-
const { config, envId } = options;
|
|
406
|
+
const { config, envId, cwd } = options;
|
|
379
407
|
if (!config.app)
|
|
380
408
|
return [];
|
|
381
409
|
const name = config.app.serviceName || 'app';
|
|
382
410
|
const appExists = await this.appExists(name);
|
|
411
|
+
const buildHint = this.resolveBuildHint(config.app, cwd, 'app');
|
|
412
|
+
// 与 buildHostingPlans 对称:框架 / 构建 / 部署路径三行必须都给,否则 `--only app`
|
|
413
|
+
// 的预演比 `--only hosting` 少一段,用户读不到「产物按什么推断、会挂到哪个路径」。
|
|
414
|
+
// ⚠️ 部署路径必须与 apply 同口径(StaticDeployer.deployApp 的默认 `/${serviceName}`
|
|
415
|
+
// 与 `normalizeDeployPath` 校验),写错了会让预演与真实发布路径对不上:
|
|
416
|
+
// 不仅默认值要一致,用户显式配的非法值(相对路径 / 带空格 / 含 shell 元字符 / 含 `..`)
|
|
417
|
+
// 也必须在预演里点出来 —— apply 会对同一个值抛 INVALID_PARAM,把必然失败的配置
|
|
418
|
+
// 渲染成正常计划,比「少渲染一行」更误导。
|
|
419
|
+
const resolvedDeployPath = (0, StaticDeployer_1.tryNormalizeDeployPath)(config.app.deployPath || `/${name}`, 'app.deployPath', true);
|
|
420
|
+
const details = [
|
|
421
|
+
// 这里的框架是**配置项**,只用于推断产物目录;写进版本记录的固定是 `static`
|
|
422
|
+
// (skipBuild 的唯一认可值),两者不是同一个概念,不要合并
|
|
423
|
+
{ label: '框架', value: config.app.framework || 'static' },
|
|
424
|
+
{
|
|
425
|
+
label: '构建',
|
|
426
|
+
value: !buildHint
|
|
427
|
+
? '不构建;产物目录无法按部署口径解析(配置可能非法),apply 时会报错'
|
|
428
|
+
: buildHint.buildCommand
|
|
429
|
+
? '不构建,上传已有产物(产物目录按框架/构建配置推断)'
|
|
430
|
+
: '不构建,直接上传'
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
label: '部署路径',
|
|
434
|
+
value: resolvedDeployPath !== null && resolvedDeployPath !== void 0 ? resolvedDeployPath :
|
|
435
|
+
// 非法值不回显原值:原值可能含 shell 元字符,回显到计划里等于把
|
|
436
|
+
// 用户可能误粘贴的命令片段再渲染一遍;点明「apply 会报错」即可。
|
|
437
|
+
// 注意这里不再提「必须以 / 开头」:缺 `/` 会被 normalizeDeployPath 补全,
|
|
438
|
+
// 真正会让 apply 失败的是这些字符本身
|
|
439
|
+
'部署路径非法(含空格 / shell 特殊字符 / ../,或以 - 开头),apply 时会报错'
|
|
440
|
+
}
|
|
441
|
+
];
|
|
442
|
+
// 产物未就绪 / 产物目录解析不出来时,在计划里点明 apply 必定失败。
|
|
443
|
+
// 只靠 buildHint 字段表达的话,读纯文本计划的人看不到任何异常 ——
|
|
444
|
+
// 计划项会渲染成与正常部署别无二致的一条。
|
|
445
|
+
if (!buildHint) {
|
|
446
|
+
// buildHint 缺省 = 未能按 apply 的口径解析出产物目录(outputDir 类型非法 /
|
|
447
|
+
// 路径越界 / 软链逃逸),apply 会抛 INVALID_PARAM。
|
|
448
|
+
details.push({
|
|
449
|
+
label: '产物',
|
|
450
|
+
value: '产物目录无法按部署口径解析(app.outputDir 类型非法或路径越界),apply 时会报错'
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
else if (!buildHint.artifactReady) {
|
|
454
|
+
details.push({
|
|
455
|
+
label: '产物',
|
|
456
|
+
value: `缺失或为空:${buildHint.outputDir}(需先完成构建)`
|
|
457
|
+
});
|
|
458
|
+
}
|
|
383
459
|
return [
|
|
384
460
|
{
|
|
385
461
|
type: 'app',
|
|
386
462
|
name,
|
|
387
463
|
status: appExists ? 'update' : 'create',
|
|
388
464
|
action: appExists
|
|
389
|
-
?
|
|
390
|
-
:
|
|
465
|
+
? `覆盖更新静态托管应用 ${name}`
|
|
466
|
+
: `新建静态托管应用 ${name}`,
|
|
467
|
+
// 框架 / 构建 / 部署路径恒有三行,无需再判空
|
|
468
|
+
details,
|
|
469
|
+
buildHint
|
|
391
470
|
}
|
|
392
471
|
];
|
|
393
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* 解析 app / hosting 的构建上下文(见 IBuildHint)
|
|
475
|
+
*
|
|
476
|
+
* ⚠️ root / outputDir 必须复用 `StaticDeployer` 导出的 `resolveDeployRoot` /
|
|
477
|
+
* `resolveArtifactDir`,**不能自己再写一份解析**:
|
|
478
|
+
* 1. plan 与 apply 口径一旦分叉,`artifactReady` 的契约就会反转 —— 例如 apply 拒绝的配置
|
|
479
|
+
* plan 报「可以 apply」,或 apply 能部署的配置 plan 报「配置非法」
|
|
480
|
+
* 2. 不做越界校验时 `hasFilesInDir` 会**同步递归遍历项目外的目录树**;
|
|
481
|
+
* `outputDir: '/'` 或用户主目录会让 plan 直接卡死
|
|
482
|
+
*
|
|
483
|
+
* 非法值(非字符串 / 含 NUL / 越界 / 软链逃逸)一律返回 `undefined` 而不抛错:
|
|
484
|
+
* plan 是只读预演,报错留给 apply 的参数校验,让 plan 始终能完整渲染。
|
|
485
|
+
* `null` / `''` 与「未配置」同义,走推断,不属非法值。
|
|
486
|
+
*/
|
|
487
|
+
resolveBuildHint(resourceConfig, cwd, kind) {
|
|
488
|
+
// 校验函数在非法输入上抛 CloudBaseError(apply 的行为),plan 只需降级为「无上下文」
|
|
489
|
+
try {
|
|
490
|
+
const root = (0, StaticDeployer_1.resolveDeployRoot)(cwd, resourceConfig.root, `${kind}.root`);
|
|
491
|
+
const outputDir = (0, StaticDeployer_1.resolveArtifactDir)({ config: resourceConfig, root, kind });
|
|
492
|
+
return {
|
|
493
|
+
buildCommand: (0, framework_1.resolveBuildCommand)(resourceConfig, root),
|
|
494
|
+
outputDir,
|
|
495
|
+
artifactReady: (0, StaticDeployer_1.hasFilesInDir)(outputDir)
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
catch (_a) {
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
394
502
|
/**
|
|
395
503
|
* hosting 计划:每次直传覆盖(无本地指纹 skip)
|
|
504
|
+
*
|
|
505
|
+
* 声明式部署只部署不构建,因此 `buildCommand` / `framework` 在计划里只用来说明
|
|
506
|
+
* **产物目录是怎么推断出来的**,不代表会执行构建(历史文案「本地构建后直传」已废除)。
|
|
507
|
+
* 机器可读的构建上下文见每项的 `buildHint`。
|
|
396
508
|
*/
|
|
397
|
-
buildHostingPlans(config) {
|
|
509
|
+
buildHostingPlans(config, cwd) {
|
|
398
510
|
const plans = [];
|
|
399
|
-
|
|
511
|
+
// 形状由 normalizeConfigInput 保证(一定是对象数组),这里不再需要 `|| []` 守卫
|
|
512
|
+
for (const h of config.hosting) {
|
|
400
513
|
const name = h.name || 'default';
|
|
401
|
-
const
|
|
402
|
-
|
|
514
|
+
const buildHint = this.resolveBuildHint(h, cwd, 'hosting');
|
|
515
|
+
// 三分支而非 `!!buildHint?.buildCommand` 二分:buildHint 缺省意味着「产物目录
|
|
516
|
+
// 没能按 apply 的口径解析出来」(配置类型非法 / 路径越界),此时若落到
|
|
517
|
+
// 「不构建,直接上传」会把一个 apply 必然报错的配置渲染成最正常的那条文案。
|
|
403
518
|
const details = [
|
|
404
519
|
{ label: '框架', value: h.framework || 'static' },
|
|
405
520
|
{
|
|
406
521
|
label: '构建',
|
|
407
|
-
value:
|
|
522
|
+
value: !buildHint
|
|
523
|
+
? '不构建;产物目录无法按部署口径解析(配置可能非法),apply 时会报错'
|
|
524
|
+
: buildHint.buildCommand
|
|
525
|
+
? '不构建,上传已有产物(产物目录按框架/构建配置推断)'
|
|
526
|
+
: '不构建,直接上传'
|
|
408
527
|
},
|
|
409
|
-
{
|
|
528
|
+
{
|
|
529
|
+
label: '部署路径',
|
|
530
|
+
// 与 apply 同口径(`deployHosting` 的 normalizeDeployPath):预演里显示的
|
|
531
|
+
// 必须是**实际生效**的路径,`"deployPath": "web"` 会补全成 `/web`
|
|
532
|
+
value: (0, StaticDeployer_1.tryNormalizeDeployPath)(h.deployPath || '/', 'hosting.deployPath', true) || '部署路径非法(apply 时会报错)'
|
|
533
|
+
}
|
|
410
534
|
];
|
|
535
|
+
// 产物未就绪时在计划里直接点明:apply 必定失败,让用户在预演阶段就看到
|
|
536
|
+
if (buildHint && !buildHint.artifactReady) {
|
|
537
|
+
details.push({
|
|
538
|
+
label: '产物',
|
|
539
|
+
value: `缺失或为空:${buildHint.outputDir}(需先完成构建)`
|
|
540
|
+
});
|
|
541
|
+
}
|
|
411
542
|
plans.push({
|
|
412
543
|
type: 'hosting',
|
|
413
544
|
name,
|
|
414
545
|
status: 'deploy',
|
|
415
|
-
action:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
details
|
|
546
|
+
action: `直传覆盖静态托管 ${name}`,
|
|
547
|
+
details,
|
|
548
|
+
buildHint
|
|
419
549
|
});
|
|
420
550
|
}
|
|
421
551
|
return plans;
|
|
422
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* 传给网关的 hosting 配置:deployPath 必须是**归一化后**的值
|
|
555
|
+
*
|
|
556
|
+
* 同一个字段被两处消费,必须同源:GatewayDeployer 用它生成 `PathRewrite.Prefix`
|
|
557
|
+
* (parseRouteWithType),StaticDeployer 用它算上传的 `cloudPath`
|
|
558
|
+
* (`deployHosting` 里的 normalizeDeployPath)。只归一化预演显示的那一份时,
|
|
559
|
+
* `"deployPath": "web"` 会让产物传到 `/web`、网关前缀却写成 `web`,请求重写后
|
|
560
|
+
* 落到存储根路径 → 线上 404,而 plan 显示 `/web`、apply 也不报错。
|
|
561
|
+
*
|
|
562
|
+
* 判据是「归一化是否成功」,不是「原始值是否为真」—— 下游 `if (hosting.deployPath)`
|
|
563
|
+
* 只判真值,`123` 与 `' '` 都算真值,原样下发会得到 `Prefix: 123` / `Prefix: ' '`。
|
|
564
|
+
*
|
|
565
|
+
* 三类输入分别处理:
|
|
566
|
+
* 1. 未配置(null/undefined/''/0/false):保持原样,与 `deployHosting` 的
|
|
567
|
+
* `deployPath || '/'` 同口径,网关侧 falsy → 不生成 PathRewrite(不能补成
|
|
568
|
+
* `Prefix: '/'`,否则给存量配置制造出路由 diff)
|
|
569
|
+
* 2. 非字符串(手改 JSON 的 `"deployPath": 123`):先 `String()` 收口再归一化,
|
|
570
|
+
* 与 `deployHosting` 内部 `String(value)` 同源,两侧都得到 `/123`
|
|
571
|
+
* 3. 纯空白 / 归一化失败(`' '`、`'../web'`):**不回落原值**,置为 undefined
|
|
572
|
+
* 让网关不生成 Prefix,失败出口收敛到 apply 的 INVALID_PARAM,避免「部署成功
|
|
573
|
+
* 但前缀与真实上传路径不一致」的静默 404
|
|
574
|
+
*
|
|
575
|
+
* 入参形状由 `normalizeConfigInput` 保证(一定是对象数组),因此这里不再需要
|
|
576
|
+
* `Array.isArray` / `|| []` 守卫 —— 守卫分散在各消费点正是「修一处漏一处」的来源。
|
|
577
|
+
*/
|
|
578
|
+
normalizeHostingsForGateway(hostings) {
|
|
579
|
+
return hostings.map(h => {
|
|
580
|
+
if (!h || !h.deployPath)
|
|
581
|
+
return h;
|
|
582
|
+
const normalized = (0, StaticDeployer_1.tryNormalizeDeployPath)(String(h.deployPath), 'hosting.deployPath', true);
|
|
583
|
+
return normalized ? Object.assign(Object.assign({}, h), { deployPath: normalized }) : Object.assign(Object.assign({}, h), { deployPath: undefined });
|
|
584
|
+
});
|
|
585
|
+
}
|
|
423
586
|
/**
|
|
424
587
|
* gateway 计划:planRoutes 对比云端路由 → create / update / skip(含变更字段明细)
|
|
425
588
|
*/
|
|
@@ -431,7 +594,7 @@ class DeployOrchestrator {
|
|
|
431
594
|
const routePlans = await this.gatewayDeployer.planRoutes({
|
|
432
595
|
routes,
|
|
433
596
|
envId,
|
|
434
|
-
hostings: config.hosting
|
|
597
|
+
hostings: this.normalizeHostingsForGateway(config.hosting)
|
|
435
598
|
});
|
|
436
599
|
const routeConfigMap = new Map(routes.map(r => [r.path, r]));
|
|
437
600
|
return routePlans.map(rp => {
|
|
@@ -464,7 +627,7 @@ class DeployOrchestrator {
|
|
|
464
627
|
}
|
|
465
628
|
}
|
|
466
629
|
/**
|
|
467
|
-
*
|
|
630
|
+
* 判断静态托管应用是否已存在(查询失败返回 false,降级为新建判定)
|
|
468
631
|
*/
|
|
469
632
|
async appExists(name) {
|
|
470
633
|
try {
|
|
@@ -511,7 +674,7 @@ class DeployOrchestrator {
|
|
|
511
674
|
return result.url;
|
|
512
675
|
}
|
|
513
676
|
case 'hosting': {
|
|
514
|
-
const h =
|
|
677
|
+
const h = config.hosting.find((x) => (x.name || 'default') === item.name);
|
|
515
678
|
const result = await this.staticDeployer.deployHosting({ config: h, cwd });
|
|
516
679
|
return result.url;
|
|
517
680
|
}
|
|
@@ -519,8 +682,9 @@ class DeployOrchestrator {
|
|
|
519
682
|
const result = await this.gatewayDeployer.deploy({
|
|
520
683
|
routes: config.gateway.routes,
|
|
521
684
|
envId,
|
|
522
|
-
// 传入 hosting 配置:hosting 路由自动生成 PathRewrite.Prefix = deployPath
|
|
523
|
-
|
|
685
|
+
// 传入 hosting 配置:hosting 路由自动生成 PathRewrite.Prefix = deployPath。
|
|
686
|
+
// 与 plan / StaticDeployer 同口径先归一化,避免前缀与真实上传路径不一致而 404
|
|
687
|
+
hostings: this.normalizeHostingsForGateway(config.hosting)
|
|
524
688
|
});
|
|
525
689
|
return result.domain;
|
|
526
690
|
}
|
|
@@ -530,12 +694,48 @@ class DeployOrchestrator {
|
|
|
530
694
|
* 轻量 inline 守卫:配置合法 + 目录存在(validateProject 接线点)
|
|
531
695
|
*/
|
|
532
696
|
guardConfig(config) {
|
|
533
|
-
|
|
534
|
-
(0, error_1.throwInvalidParam)('无效的配置:config 必须为对象');
|
|
535
|
-
}
|
|
697
|
+
this.guardConfigShape(config);
|
|
536
698
|
if (!config.envId) {
|
|
537
699
|
(0, error_1.throwInvalidParam)('无效的配置:缺少 envId');
|
|
538
700
|
}
|
|
539
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* config 必须是普通对象(不含数组)。
|
|
704
|
+
*
|
|
705
|
+
* 拆出来给 plan / apply 共用:此前只有 `deploy` 走 `guardConfig`,`deployPlan`
|
|
706
|
+
* 完全没校验,`deployPlan({ config: null })` 会在 `buildPlanForType` 里抛原生
|
|
707
|
+
* TypeError。`envId` 的校验仍留在 `guardConfig` —— plan 侧的 envId 真相源是
|
|
708
|
+
* `options.envId`(`config.envId` 允许缺省)。
|
|
709
|
+
*/
|
|
710
|
+
guardConfigShape(config) {
|
|
711
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
712
|
+
(0, error_1.throwInvalidParam)('无效的配置:config 必须为对象');
|
|
713
|
+
}
|
|
714
|
+
}
|
|
540
715
|
}
|
|
541
716
|
exports.DeployOrchestrator = DeployOrchestrator;
|
|
717
|
+
/**
|
|
718
|
+
* 顶层资源声明(`functions` / `hosting` / `gateway.routes`)的入口归一化。
|
|
719
|
+
*
|
|
720
|
+
* 非数组一律降级为空数组:与历史 `config.hosting || []` 同口径,存量把手写错的
|
|
721
|
+
* 配置(`"hosting": ""`)仍能跑通其余资源;数组元素非对象则显式报错 —— 静默丢弃
|
|
722
|
+
* 会少部署一个站点/一条路由,比报错更难排查。
|
|
723
|
+
*/
|
|
724
|
+
function normalizeResourceList(value, field) {
|
|
725
|
+
if (!Array.isArray(value))
|
|
726
|
+
return [];
|
|
727
|
+
return value.map((item, index) => {
|
|
728
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
729
|
+
const actualType = item === null ? 'null' : Array.isArray(item) ? 'array' : typeof item;
|
|
730
|
+
(0, error_1.throwInvalidParam)(`${field}[${index}] 必须是对象,当前类型:${actualType}`);
|
|
731
|
+
}
|
|
732
|
+
return item;
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
/** `gateway` 声明归一化:非对象视为「未配置网关」,`routes` 缺省/非数组降级为空数组 */
|
|
736
|
+
function normalizeGatewayConfig(value) {
|
|
737
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
738
|
+
return undefined;
|
|
739
|
+
const gateway = value;
|
|
740
|
+
return Object.assign(Object.assign({}, gateway), { routes: normalizeResourceList(gateway.routes, 'gateway.routes') });
|
|
741
|
+
}
|
|
@@ -160,8 +160,11 @@ class FunctionDeployer {
|
|
|
160
160
|
return undefined;
|
|
161
161
|
}
|
|
162
162
|
const pathValue = config.path || config.gatewayPath;
|
|
163
|
-
// 推荐方式:通过 API
|
|
164
|
-
|
|
163
|
+
// 推荐方式:通过 API 网关调用云函数(无需配置访问服务)。
|
|
164
|
+
// `webfn=true` 必填:网关据此区分「HTTP 云函数」与普通(事件)云函数,
|
|
165
|
+
// 缺了它请求会被当成事件函数处理、进不到 HTTP 路由。见官方文档「HTTP API 调用格式」:
|
|
166
|
+
// https://docs.cloudbase.net/cloud-function/function-calls/#api-调用格式
|
|
167
|
+
const recommendedUrl = `${(0, domain_1.getGatewayBaseUrl)(this.environment, envId)}/v1/functions/${config.name}?webfn=true`;
|
|
165
168
|
if (!pathValue) {
|
|
166
169
|
return recommendedUrl;
|
|
167
170
|
}
|