@cloudbase/manager-node 5.7.0-beta.3 → 5.7.1-beta.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/jest.config.unit.js +14 -0
- package/lib/cloudApp/index.js +3 -12
- package/lib/env/index.js +9 -0
- package/lib/environment.js +0 -11
- package/lib/function/index.js +67 -26
- package/lib/hosting/index.js +1 -4
- package/lib/index.js +0 -31
- package/lib/storage/index.js +7 -6
- package/lib/utils/index.js +5 -62
- package/package.json +1 -2
- package/types/cloudApp/index.d.ts +0 -4
- package/types/cloudApp/types.d.ts +6 -87
- package/types/env/index.d.ts +8 -1
- package/types/env/type.d.ts +97 -6
- package/types/environment.d.ts +0 -7
- package/types/function/index.d.ts +3 -0
- package/types/function/types.d.ts +0 -15
- package/types/hosting/index.d.ts +0 -6
- package/types/index.d.ts +0 -18
- package/types/interfaces/function.interface.d.ts +1 -1
- package/types/storage/index.d.ts +0 -6
- package/types/utils/index.d.ts +0 -13
- package/lib/deploy/DatabaseDeployer.js +0 -238
- package/lib/deploy/DeployOrchestrator.js +0 -632
- package/lib/deploy/FunctionDeployer.js +0 -611
- package/lib/deploy/GatewayDeployer.js +0 -612
- package/lib/deploy/StateStore.js +0 -327
- package/lib/deploy/StaticDeployer.js +0 -239
- package/lib/deploy/domain.js +0 -41
- package/lib/deploy/framework.js +0 -105
- package/lib/deploy/function/artifact.js +0 -207
- package/lib/deploy/function/builders/cloud.js +0 -536
- package/lib/deploy/function/cam-preflight.js +0 -157
- package/lib/deploy/function/cloud-build-service.js +0 -170
- package/lib/deploy/function/config-guard.js +0 -592
- package/lib/deploy/function/docker-preflight.js +0 -87
- package/lib/deploy/function/image-preflight.js +0 -164
- package/lib/deploy/function/local-builder.js +0 -129
- package/lib/deploy/function/planner.js +0 -282
- package/lib/deploy/function/preflight.js +0 -264
- package/lib/deploy/types.js +0 -99
- package/lib/projectValidator/index.js +0 -440
- package/lib/projectValidator/types.js +0 -2
- package/types/deploy/DatabaseDeployer.d.ts +0 -70
- package/types/deploy/DeployOrchestrator.d.ts +0 -168
- package/types/deploy/FunctionDeployer.d.ts +0 -158
- package/types/deploy/GatewayDeployer.d.ts +0 -194
- package/types/deploy/StateStore.d.ts +0 -170
- package/types/deploy/StaticDeployer.d.ts +0 -97
- package/types/deploy/domain.d.ts +0 -17
- package/types/deploy/framework.d.ts +0 -23
- package/types/deploy/function/artifact.d.ts +0 -40
- package/types/deploy/function/builders/cloud.d.ts +0 -110
- package/types/deploy/function/cam-preflight.d.ts +0 -51
- package/types/deploy/function/cloud-build-service.d.ts +0 -10
- package/types/deploy/function/config-guard.d.ts +0 -41
- package/types/deploy/function/docker-preflight.d.ts +0 -17
- package/types/deploy/function/image-preflight.d.ts +0 -21
- package/types/deploy/function/local-builder.d.ts +0 -26
- package/types/deploy/function/planner.d.ts +0 -15
- package/types/deploy/function/preflight.d.ts +0 -9
- package/types/deploy/types.d.ts +0 -429
- package/types/projectValidator/index.d.ts +0 -38
- package/types/projectValidator/types.d.ts +0 -26
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
-
};
|
|
19
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.ProjectValidator = void 0;
|
|
21
|
-
const fs_1 = __importDefault(require("fs"));
|
|
22
|
-
const path_1 = __importDefault(require("path"));
|
|
23
|
-
const toolbox_1 = require("@cloudbase/toolbox");
|
|
24
|
-
class ProjectValidator {
|
|
25
|
-
async validate(options = {}) {
|
|
26
|
-
var _a;
|
|
27
|
-
const cwd = path_1.default.resolve(options.cwd || process.cwd());
|
|
28
|
-
const errors = [];
|
|
29
|
-
const warnings = [];
|
|
30
|
-
const loaded = await this.loadProjectConfig(cwd, {
|
|
31
|
-
configPath: options.configPath,
|
|
32
|
-
mode: options.mode,
|
|
33
|
-
errors,
|
|
34
|
-
});
|
|
35
|
-
const version = String(((_a = loaded.rawConfig) === null || _a === void 0 ? void 0 : _a.version) || '2.1');
|
|
36
|
-
if (loaded.rawConfig) {
|
|
37
|
-
this.validateSchema(loaded.rawConfig, errors);
|
|
38
|
-
}
|
|
39
|
-
if (loaded.config) {
|
|
40
|
-
this.validateEnvId(cwd, { raw: loaded.rawConfig || {}, resolved: loaded.config }, errors);
|
|
41
|
-
this.validatePersonalRegistryConfig(loaded.config, errors);
|
|
42
|
-
const resourceIssues = this.validateResources(cwd, loaded.config);
|
|
43
|
-
errors.push(...resourceIssues.errors);
|
|
44
|
-
warnings.push(...resourceIssues.warnings);
|
|
45
|
-
this.validateConsistency(loaded.config, errors, warnings);
|
|
46
|
-
}
|
|
47
|
-
const summary = this.getSummary(loaded.config);
|
|
48
|
-
return {
|
|
49
|
-
valid: errors.length === 0,
|
|
50
|
-
version,
|
|
51
|
-
errors,
|
|
52
|
-
warnings,
|
|
53
|
-
summary,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
async loadProjectConfig(cwd, options) {
|
|
57
|
-
const { configPath, mode, errors } = options;
|
|
58
|
-
let filepath = configPath ? path_1.default.resolve(cwd, configPath) : undefined;
|
|
59
|
-
try {
|
|
60
|
-
if (!filepath) {
|
|
61
|
-
const result = await (0, toolbox_1.searchConfig)(cwd);
|
|
62
|
-
filepath = result === null || result === void 0 ? void 0 : result.filepath;
|
|
63
|
-
}
|
|
64
|
-
if (!filepath) {
|
|
65
|
-
errors.push({
|
|
66
|
-
level: 'error',
|
|
67
|
-
field: 'cloudbaserc',
|
|
68
|
-
message: '未找到 cloudbaserc 配置文件',
|
|
69
|
-
fix: '在项目根目录新增 cloudbaserc.json / cloudbaserc.js / cloudbaserc.yaml / cloudbaserc.yml',
|
|
70
|
-
});
|
|
71
|
-
return {};
|
|
72
|
-
}
|
|
73
|
-
const rawConfig = (await (0, toolbox_1.loadConfig)({ configPath: filepath }));
|
|
74
|
-
const config = (await toolbox_1.ConfigParser.parseRawConfig(rawConfig || {}, cwd, {
|
|
75
|
-
mode,
|
|
76
|
-
}));
|
|
77
|
-
return {
|
|
78
|
-
filepath,
|
|
79
|
-
rawConfig,
|
|
80
|
-
config,
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
catch (e) {
|
|
84
|
-
errors.push({
|
|
85
|
-
level: 'error',
|
|
86
|
-
field: 'cloudbaserc',
|
|
87
|
-
message: `配置文件解析失败:${this.getErrorMessage(e)}`,
|
|
88
|
-
fix: '检查 cloudbaserc 配置文件语法是否正确',
|
|
89
|
-
});
|
|
90
|
-
return { filepath };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
validateSchema(config, errors) {
|
|
94
|
-
const result = (0, toolbox_1.validateCloudBaseConfigBySchema)(config, { version: '2.1' });
|
|
95
|
-
result.errors.forEach((error) => {
|
|
96
|
-
errors.push({
|
|
97
|
-
level: 'error',
|
|
98
|
-
field: this.normalizeSchemaPath(error.dataPath),
|
|
99
|
-
message: `配置不符合 v2.1 Schema:${error.message}`,
|
|
100
|
-
fix: `请检查 ${this.normalizeSchemaPath(error.dataPath) || 'cloudbaserc'} 字段`,
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
validateEnvId(cwd, config, errors) {
|
|
105
|
-
if (this.hasPersonalCloudFunction(config.resolved)) {
|
|
106
|
-
if (typeof config.raw.envId !== 'string' || !config.raw.envId.trim()) {
|
|
107
|
-
errors.push({
|
|
108
|
-
level: 'error',
|
|
109
|
-
field: 'envId',
|
|
110
|
-
message: '个人版 TCR 云端构建必须在 cloudbaserc 中显式声明 envId',
|
|
111
|
-
fix: '声明 envId: "{{env.ENV_ID}}",并在 .env* 中设置 ENV_ID',
|
|
112
|
-
});
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (typeof config.resolved.envId !== 'string' || !config.resolved.envId.trim()) {
|
|
116
|
-
errors.push({
|
|
117
|
-
level: 'error',
|
|
118
|
-
field: 'envId',
|
|
119
|
-
message: '个人版 TCR 云端构建的 envId 未赋值',
|
|
120
|
-
fix: '在 .env* 中设置 cloudbaserc 通过 {{env.ENV_ID}} 引用的 ENV_ID',
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (config.resolved.envId || this.getLinkedEnvId(cwd)) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
errors.push({
|
|
129
|
-
level: 'error',
|
|
130
|
-
field: 'envId',
|
|
131
|
-
message: '缺少 envId,且未检测到 .tcb/project.json 关联环境',
|
|
132
|
-
fix: '在 cloudbaserc 中配置 envId,或先执行环境关联流程',
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* 个人版 TCR 的 username/password/namespace 必须在 cloudbaserc 中显式声明。
|
|
137
|
-
* ConfigParser 已先解析 {{env.*}},因此这里同时承担缺失 env 值的前置校验。
|
|
138
|
-
*/
|
|
139
|
-
validatePersonalRegistryConfig(config, errors) {
|
|
140
|
-
;
|
|
141
|
-
(config.functions || []).forEach((func, index) => {
|
|
142
|
-
var _a;
|
|
143
|
-
if (!this.isPersonalCloudFunction(func)) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const build = (_a = func.imageConfig) === null || _a === void 0 ? void 0 : _a.build;
|
|
147
|
-
const namespace = build === null || build === void 0 ? void 0 : build.namespace;
|
|
148
|
-
if (typeof namespace !== 'string' || !namespace.trim()) {
|
|
149
|
-
errors.push({
|
|
150
|
-
level: 'error',
|
|
151
|
-
resource: `functions[${index}]`,
|
|
152
|
-
field: 'imageConfig.build.namespace',
|
|
153
|
-
message: '个人版 TCR 缺少 namespace,或对应环境变量未赋值',
|
|
154
|
-
fix: '声明 namespace: "{{env.TCB_TCR_NAMESPACE}}",并在 .env* 中设置该变量',
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
const credential = build === null || build === void 0 ? void 0 : build.registryCredential;
|
|
158
|
-
if (!credential || typeof credential !== 'object') {
|
|
159
|
-
errors.push({
|
|
160
|
-
level: 'error',
|
|
161
|
-
resource: `functions[${index}]`,
|
|
162
|
-
field: 'imageConfig.build.registryCredential',
|
|
163
|
-
message: '个人版 TCR 缺少 username/password 凭证声明',
|
|
164
|
-
fix: '在 registryCredential 中使用 {{env.TCB_TCR_USERNAME}} 与 {{env.TCB_TCR_PASSWORD}}',
|
|
165
|
-
});
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
const { username, password } = credential;
|
|
169
|
-
if (typeof username !== 'string' || !/^\d{5,20}$/.test(username.trim())) {
|
|
170
|
-
errors.push({
|
|
171
|
-
level: 'error',
|
|
172
|
-
resource: `functions[${index}]`,
|
|
173
|
-
field: 'imageConfig.build.registryCredential.username',
|
|
174
|
-
message: '个人版 TCR username 未赋值或不是有效腾讯云账号 ID',
|
|
175
|
-
fix: '声明 username: "{{env.TCB_TCR_USERNAME}}",并在 .env* 中设置该变量',
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
if (typeof password !== 'string' || !password || password.length > 16 * 1024) {
|
|
179
|
-
errors.push({
|
|
180
|
-
level: 'error',
|
|
181
|
-
resource: `functions[${index}]`,
|
|
182
|
-
field: 'imageConfig.build.registryCredential.password',
|
|
183
|
-
message: '个人版 TCR password 未赋值或长度不合法',
|
|
184
|
-
fix: '声明 password: "{{env.TCB_TCR_PASSWORD}}",并在 .env* 中设置该变量',
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
hasPersonalCloudFunction(config) {
|
|
190
|
-
return (config.functions || []).some((func) => this.isPersonalCloudFunction(func));
|
|
191
|
-
}
|
|
192
|
-
isPersonalCloudFunction(func) {
|
|
193
|
-
var _a;
|
|
194
|
-
return (func.type === 'HTTP' &&
|
|
195
|
-
func.buildStrategy === 'cloud' &&
|
|
196
|
-
((_a = func.imageConfig) === null || _a === void 0 ? void 0 : _a.imageType) === 'personal');
|
|
197
|
-
}
|
|
198
|
-
validateResources(cwd, config) {
|
|
199
|
-
const errors = [];
|
|
200
|
-
const warnings = [];
|
|
201
|
-
const functionRoot = config.functionRoot || './functions';
|
|
202
|
-
(config.functions || []).forEach((func, index) => {
|
|
203
|
-
const resource = `functions[${index}]`;
|
|
204
|
-
const functionDir = path_1.default.resolve(cwd, func.dir || path_1.default.join(functionRoot, func.name));
|
|
205
|
-
if (!fs_1.default.existsSync(functionDir)) {
|
|
206
|
-
errors.push({
|
|
207
|
-
level: 'error',
|
|
208
|
-
resource,
|
|
209
|
-
field: 'dir',
|
|
210
|
-
message: `函数目录不存在:${path_1.default.relative(cwd, functionDir)}`,
|
|
211
|
-
fix: '创建函数目录,或修正 functions[].dir / functionRoot 配置',
|
|
212
|
-
});
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
const functionType = func.type || 'Event';
|
|
216
|
-
const buildStrategy = func.buildStrategy || 'zip';
|
|
217
|
-
if (functionType === 'HTTP' && buildStrategy === 'zip') {
|
|
218
|
-
const bootstrapPath = path_1.default.join(functionDir, 'scf_bootstrap');
|
|
219
|
-
if (!fs_1.default.existsSync(bootstrapPath)) {
|
|
220
|
-
errors.push({
|
|
221
|
-
level: 'error',
|
|
222
|
-
resource,
|
|
223
|
-
field: 'scf_bootstrap',
|
|
224
|
-
message: '缺少 scf_bootstrap 文件(HTTP 函数 zip 部署必须)',
|
|
225
|
-
fix: '在函数目录中新增 scf_bootstrap,并确保其具备执行权限',
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (functionType === 'Event') {
|
|
230
|
-
const handlerFile = `${(func.handler || 'index.main').split('.')[0]}.js`;
|
|
231
|
-
if (!fs_1.default.existsSync(path_1.default.join(functionDir, handlerFile))) {
|
|
232
|
-
warnings.push({
|
|
233
|
-
level: 'warning',
|
|
234
|
-
resource,
|
|
235
|
-
field: 'handler',
|
|
236
|
-
message: `未找到入口文件 ${handlerFile}`,
|
|
237
|
-
fix: '确认 handler 配置与函数入口文件一致',
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
(config.hosting || []).forEach((hosting, index) => {
|
|
243
|
-
const root = path_1.default.resolve(cwd, hosting.root || '.');
|
|
244
|
-
if (!fs_1.default.existsSync(root)) {
|
|
245
|
-
errors.push({
|
|
246
|
-
level: 'error',
|
|
247
|
-
resource: `hosting[${index}]`,
|
|
248
|
-
field: 'root',
|
|
249
|
-
message: `静态托管根目录不存在:${path_1.default.relative(cwd, root)}`,
|
|
250
|
-
fix: '创建目录,或修正 hosting[].root 配置',
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
if (config.database) {
|
|
255
|
-
const migrations = config.database.migrations || './cloudbase/migrations';
|
|
256
|
-
if (path_1.default.isAbsolute(migrations) || migrations.startsWith('/')) {
|
|
257
|
-
errors.push({
|
|
258
|
-
level: 'error',
|
|
259
|
-
resource: 'database',
|
|
260
|
-
field: 'migrations',
|
|
261
|
-
message: 'database.migrations 仅支持相对路径',
|
|
262
|
-
fix: '请使用相对路径,例如 ./cloudbase/migrations',
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
const migrationsDir = path_1.default.resolve(cwd, migrations);
|
|
267
|
-
if (!fs_1.default.existsSync(migrationsDir)) {
|
|
268
|
-
errors.push({
|
|
269
|
-
level: 'error',
|
|
270
|
-
resource: 'database',
|
|
271
|
-
field: 'migrations',
|
|
272
|
-
message: `数据库迁移目录不存在:${path_1.default.relative(cwd, migrationsDir)}`,
|
|
273
|
-
fix: '创建迁移目录,或修正 database.migrations 配置',
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return { errors, warnings };
|
|
279
|
-
}
|
|
280
|
-
validateConsistency(config, errors, warnings = []) {
|
|
281
|
-
this.validateUniqueNames('functions', config.functions || [], errors);
|
|
282
|
-
this.validateUniqueNames('hosting', config.hosting || [], errors);
|
|
283
|
-
this.validateUniqueField('hosting', config.hosting || [], { field: 'deployPath', errors });
|
|
284
|
-
this.validateGatewayTargets(config, errors);
|
|
285
|
-
this.validateGatewayPathRewriteConflicts(config, warnings);
|
|
286
|
-
this.validateHostingSubPathBaseRisks(config, warnings);
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* 检测 hosting 路由中 enablePathTransmission=true 与自动生成 pathRewrite 的冲突
|
|
290
|
-
*
|
|
291
|
-
* hosting 目标未显式配置 pathRewrite 时,部署会自动生成 PathRewrite.Prefix = hosting.deployPath。
|
|
292
|
-
* 若同时显式开启 enablePathTransmission=true,路径透传会覆盖路径重写,导致请求打到错误的托管路径(404)。
|
|
293
|
-
*/
|
|
294
|
-
validateGatewayPathRewriteConflicts(config, warnings) {
|
|
295
|
-
var _a;
|
|
296
|
-
const hostingMap = new Map((config.hosting || []).map((h) => [h.name, h]));
|
|
297
|
-
(((_a = config.gateway) === null || _a === void 0 ? void 0 : _a.routes) || []).forEach((route, index) => {
|
|
298
|
-
var _a, _b, _c;
|
|
299
|
-
if (!((_a = route.target) === null || _a === void 0 ? void 0 : _a.startsWith('hosting:')))
|
|
300
|
-
return;
|
|
301
|
-
// 用户显式配置了 pathRewrite → 无自动重写,无冲突
|
|
302
|
-
if (((_b = route.pathRewrite) === null || _b === void 0 ? void 0 : _b.prefix) || ((_c = route.pathRewrite) === null || _c === void 0 ? void 0 : _c.staticStorePrefix))
|
|
303
|
-
return;
|
|
304
|
-
// 未显式开启透传 → 无冲突
|
|
305
|
-
if (route.enablePathTransmission !== true)
|
|
306
|
-
return;
|
|
307
|
-
const hostingName = route.target.slice('hosting:'.length);
|
|
308
|
-
const hosting = hostingMap.get(hostingName);
|
|
309
|
-
// hosting 无 deployPath → 不会自动生成 PathRewrite,不冲突
|
|
310
|
-
if (!(hosting === null || hosting === void 0 ? void 0 : hosting.deployPath))
|
|
311
|
-
return;
|
|
312
|
-
warnings.push({
|
|
313
|
-
level: 'warning',
|
|
314
|
-
resource: `gateway.routes[${index}]`,
|
|
315
|
-
field: 'enablePathTransmission',
|
|
316
|
-
message: `hosting 路由 ${route.target} 未显式配置 pathRewrite,部署将自动生成 ` +
|
|
317
|
-
`PathRewrite.Prefix=${hosting.deployPath},但 enablePathTransmission=true 会覆盖路径重写,` +
|
|
318
|
-
`导致请求打不到托管实际部署路径`,
|
|
319
|
-
fix: '删除 enablePathTransmission,让自动生成的 pathRewrite 生效;' +
|
|
320
|
-
'或显式配置 pathRewrite.prefix 指向托管部署路径',
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* 子路径部署(deployPath !== '/')下,前端构建产物若仍使用根路径 base(如 Vite 默认 '/'),
|
|
326
|
-
* 会导致资源请求落到 /assets/* 而非 /{deployPath}/assets/*,出现 404/白屏。
|
|
327
|
-
*
|
|
328
|
-
* 这里在“可能存在构建步骤”的 hosting 配置上给出防坑告警。
|
|
329
|
-
*/
|
|
330
|
-
validateHostingSubPathBaseRisks(config, warnings) {
|
|
331
|
-
;
|
|
332
|
-
(config.hosting || []).forEach((hosting, index) => {
|
|
333
|
-
const deployPath = typeof hosting.deployPath === 'string' ? hosting.deployPath.trim() : '';
|
|
334
|
-
if (!deployPath || deployPath === '/')
|
|
335
|
-
return;
|
|
336
|
-
const framework = typeof hosting.framework === 'string' ? hosting.framework.trim().toLowerCase() : '';
|
|
337
|
-
const hasBuildStep = (typeof hosting.buildCommand === 'string' && hosting.buildCommand.trim().length > 0) ||
|
|
338
|
-
(!!framework && framework !== 'static' && framework !== 'custom');
|
|
339
|
-
if (!hasBuildStep)
|
|
340
|
-
return;
|
|
341
|
-
warnings.push({
|
|
342
|
-
level: 'warning',
|
|
343
|
-
resource: `hosting[${index}]`,
|
|
344
|
-
field: 'deployPath',
|
|
345
|
-
message: `静态托管部署在子路径 ${deployPath},请确认前端构建 base 与子路径匹配,` +
|
|
346
|
-
`否则资源可能请求到根路径(如 /assets/*)导致 404/白屏`,
|
|
347
|
-
fix: "建议将前端 base 配置为 './' 或与 deployPath 对齐(例如 '/serviceName/')",
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
validateUniqueNames(resource, items, errors) {
|
|
352
|
-
const names = new Map();
|
|
353
|
-
items.forEach((item, index) => {
|
|
354
|
-
if (!item.name)
|
|
355
|
-
return;
|
|
356
|
-
const prev = names.get(item.name);
|
|
357
|
-
if (typeof prev === 'number') {
|
|
358
|
-
errors.push({
|
|
359
|
-
level: 'error',
|
|
360
|
-
resource: `${resource}[${index}]`,
|
|
361
|
-
field: 'name',
|
|
362
|
-
message: `${resource} 名称重复:${item.name}`,
|
|
363
|
-
fix: `修改 ${resource}[${index}].name,避免与 ${resource}[${prev}].name 重复`,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
names.set(item.name, index);
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
validateUniqueField(resource, items, options) {
|
|
372
|
-
const { field, errors } = options;
|
|
373
|
-
const values = new Map();
|
|
374
|
-
items.forEach((item, index) => {
|
|
375
|
-
const value = item[field];
|
|
376
|
-
if (!value)
|
|
377
|
-
return;
|
|
378
|
-
const prev = values.get(value);
|
|
379
|
-
if (typeof prev === 'number') {
|
|
380
|
-
errors.push({
|
|
381
|
-
level: 'error',
|
|
382
|
-
resource: `${resource}[${index}]`,
|
|
383
|
-
field,
|
|
384
|
-
message: `${resource}.${field} 配置重复:${value}`,
|
|
385
|
-
fix: `修改 ${resource}[${index}].${field},避免与 ${resource}[${prev}].${field} 重复`,
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
else {
|
|
389
|
-
values.set(value, index);
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
validateGatewayTargets(config, errors) {
|
|
394
|
-
var _a;
|
|
395
|
-
const functionNames = new Set((config.functions || []).map((item) => item.name));
|
|
396
|
-
const hostingNames = new Set((config.hosting || []).map((item) => item.name));
|
|
397
|
-
(((_a = config.gateway) === null || _a === void 0 ? void 0 : _a.routes) || []).forEach((route, index) => {
|
|
398
|
-
const [type, name] = route.target.split(':');
|
|
399
|
-
if ((type === 'function' && !functionNames.has(name)) || (type === 'hosting' && !hostingNames.has(name))) {
|
|
400
|
-
errors.push({
|
|
401
|
-
level: 'error',
|
|
402
|
-
resource: `gateway.routes[${index}]`,
|
|
403
|
-
field: 'target',
|
|
404
|
-
message: `网关目标不存在:${route.target}`,
|
|
405
|
-
fix: '确认 gateway.routes[].target 引用了已声明的 function 或 hosting 资源',
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
getSummary(config) {
|
|
411
|
-
var _a, _b, _c, _d;
|
|
412
|
-
return {
|
|
413
|
-
functions: ((_a = config === null || config === void 0 ? void 0 : config.functions) === null || _a === void 0 ? void 0 : _a.length) || 0,
|
|
414
|
-
hosting: ((_b = config === null || config === void 0 ? void 0 : config.hosting) === null || _b === void 0 ? void 0 : _b.length) || 0,
|
|
415
|
-
databases: (config === null || config === void 0 ? void 0 : config.database) ? 1 : 0,
|
|
416
|
-
gateways: ((_d = (_c = config === null || config === void 0 ? void 0 : config.gateway) === null || _c === void 0 ? void 0 : _c.routes) === null || _d === void 0 ? void 0 : _d.length) || 0,
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
getLinkedEnvId(cwd) {
|
|
420
|
-
const projectPath = path_1.default.join(cwd, '.tcb', 'project.json');
|
|
421
|
-
if (!fs_1.default.existsSync(projectPath)) {
|
|
422
|
-
return '';
|
|
423
|
-
}
|
|
424
|
-
try {
|
|
425
|
-
const project = JSON.parse(fs_1.default.readFileSync(projectPath, 'utf8'));
|
|
426
|
-
return project.envId || project.EnvId || '';
|
|
427
|
-
}
|
|
428
|
-
catch (e) {
|
|
429
|
-
return '';
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
normalizeSchemaPath(dataPath) {
|
|
433
|
-
return dataPath.replace(/^\//, '').replace(/\//g, '.');
|
|
434
|
-
}
|
|
435
|
-
getErrorMessage(error) {
|
|
436
|
-
return (error === null || error === void 0 ? void 0 : error.message) || String(error);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
exports.ProjectValidator = ProjectValidator;
|
|
440
|
-
__exportStar(require("./types"), exports);
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { Environment } from '../environment';
|
|
2
|
-
import { IPGUserMigrationInput } from '../database/type';
|
|
3
|
-
/** 数据库迁移部署配置(对应 cloudbaserc.json 中的 database 块) */
|
|
4
|
-
export interface IDatabaseDeployConfig {
|
|
5
|
-
/** 数据库类型(v2.1 schema: 目前仅支持 postgresql) */
|
|
6
|
-
type: 'postgresql';
|
|
7
|
-
/** migration SQL 文件目录(相对项目根) */
|
|
8
|
-
migrations?: string;
|
|
9
|
-
}
|
|
10
|
-
/** Plan 中单条 migration 的状态 */
|
|
11
|
-
export interface IDatabasePlanItem {
|
|
12
|
-
/** 14位数字时间串 */
|
|
13
|
-
version: string;
|
|
14
|
-
/** migration 名称 */
|
|
15
|
-
name: string;
|
|
16
|
-
/** pending=待执行;applied=已应用;conflict=冲突 */
|
|
17
|
-
status: 'pending' | 'applied' | 'conflict';
|
|
18
|
-
/** 冲突原因(仅 conflict 时有值) */
|
|
19
|
-
reason?: string;
|
|
20
|
-
}
|
|
21
|
-
/** deploy 返回结果 */
|
|
22
|
-
export interface IDatabaseDeployResult {
|
|
23
|
-
/** 异步任务 ID(无pending时为空字符串) */
|
|
24
|
-
taskId: string;
|
|
25
|
-
}
|
|
26
|
-
export declare class DatabaseDeployer {
|
|
27
|
-
private environment;
|
|
28
|
-
constructor(environment: Environment);
|
|
29
|
-
/**
|
|
30
|
-
* 从本地目录解析 migration 文件列表
|
|
31
|
-
*
|
|
32
|
-
* 文件名格式:{14位时间戳}_{小写下划线名称}.sql
|
|
33
|
-
* 例如:20260526000000_create_users.sql
|
|
34
|
-
*
|
|
35
|
-
* 按文件名升序排列(即按版本号从早到晚)
|
|
36
|
-
*/
|
|
37
|
-
parseMigrationFiles(migrationsDir: string): IPGUserMigrationInput[];
|
|
38
|
-
/**
|
|
39
|
-
* 生成部署计划
|
|
40
|
-
*
|
|
41
|
-
* 调用 previewPGUserMigrations 获取云端对比结果,
|
|
42
|
-
* 返回每条migration 的状态(pending / applied / conflict)
|
|
43
|
-
*/
|
|
44
|
-
plan(config: IDatabaseDeployConfig, cwd: string, envId: string): Promise<IDatabasePlanItem[]>;
|
|
45
|
-
/**
|
|
46
|
-
* 执行数据库迁移
|
|
47
|
-
*
|
|
48
|
-
* 流程:
|
|
49
|
-
* 1. 解析本地 migration 文件
|
|
50
|
-
* 2. 预览确认无冲突(有冲突直接 throw,中断整个部署)
|
|
51
|
-
* 3. 无 pending 则跳过
|
|
52
|
-
* 4. 提交 pushPGUserMigrations
|
|
53
|
-
* 5. 轮询describeTaskResult 等待完成
|
|
54
|
-
*/
|
|
55
|
-
deploy(config: IDatabaseDeployConfig, cwd: string, envId: string): Promise<IDatabaseDeployResult>;
|
|
56
|
-
/**
|
|
57
|
-
* 解析 migrations 目录路径
|
|
58
|
-
* - 未配置:默认 ./cloudbase/migrations
|
|
59
|
-
* - 仅允许相对路径(禁止 / 开头)
|
|
60
|
-
*/
|
|
61
|
-
private resolveMigrationsDir;
|
|
62
|
-
/**
|
|
63
|
-
* 校验 database.type(当前仅支持 postgresql)
|
|
64
|
-
*/
|
|
65
|
-
private assertDatabaseType;
|
|
66
|
-
/**
|
|
67
|
-
* 轮询等待异步任务完成
|
|
68
|
-
*/
|
|
69
|
-
private waitForTask;
|
|
70
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { Environment } from '../environment';
|
|
2
|
-
/** 可编排的资源类型 */
|
|
3
|
-
export type ResourceType = 'database' | 'functions' | 'app' | 'hosting' | 'gateway';
|
|
4
|
-
/** 声明式部署选项 */
|
|
5
|
-
export interface IDeployOptions {
|
|
6
|
-
/** 解析后的 cloudbaserc 配置(ConfigParser 输出,含 envOverrides 已应用) */
|
|
7
|
-
config: Record<string, any>;
|
|
8
|
-
envId: string;
|
|
9
|
-
/** 只部署指定类型 */
|
|
10
|
-
only?: ResourceType[];
|
|
11
|
-
/** 跳过指定类型 */
|
|
12
|
-
skip?: ResourceType[];
|
|
13
|
-
/** dry-run:只输出计划不实际部署 */
|
|
14
|
-
dryRun?: boolean;
|
|
15
|
-
/** 项目根目录(默认 process.cwd()) */
|
|
16
|
-
cwd?: string;
|
|
17
|
-
/** 已存在资源(函数)的覆盖更新:yes=true 直接放行(AI Agent / CI 场景) */
|
|
18
|
-
yes?: boolean;
|
|
19
|
-
/** 已存在资源(函数)的覆盖更新确认回调:返回 true 部署、false 跳过(CLI 注入 autoConfirm) */
|
|
20
|
-
confirmUpdate?: (item: IDeployPlanItem) => Promise<boolean>;
|
|
21
|
-
/** 强制云端对比(漂移检测):忽略本地 state 快照的 skip 判定,强制重新对比/部署 */
|
|
22
|
-
refresh?: boolean;
|
|
23
|
-
/** 同类型资源最大并行数,默认 1(串行)。仅作用于同类型连续实例之间,跨类型依赖顺序不变 */
|
|
24
|
-
concurrency?: number;
|
|
25
|
-
/** 失败后继续部署其余资源(database 失败仍强制中断) */
|
|
26
|
-
continueOnError?: boolean;
|
|
27
|
-
log?: {
|
|
28
|
-
info?: (msg: string) => void;
|
|
29
|
-
success?: (msg: string) => void;
|
|
30
|
-
warn?: (msg: string) => void;
|
|
31
|
-
error?: (msg: string) => void;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/** 部署计划项(资源级) */
|
|
35
|
-
export interface IDeployPlanItem {
|
|
36
|
-
type: ResourceType;
|
|
37
|
-
name: string;
|
|
38
|
-
/** 动作分类:create=新建;update=覆盖更新已有资源;skip=已存在且配置一致;conflict=检测到冲突(需中断);deploy=直传覆盖(无存在性概念) */
|
|
39
|
-
status: 'create' | 'update' | 'skip' | 'conflict' | 'deploy';
|
|
40
|
-
/** 动作说明(用户可读) */
|
|
41
|
-
action: string;
|
|
42
|
-
/** 变更字段明细(如 database 迁移聚合、gateway 路由差异等) */
|
|
43
|
-
changes?: Array<{
|
|
44
|
-
field: string;
|
|
45
|
-
from?: string;
|
|
46
|
-
to?: string;
|
|
47
|
-
}>;
|
|
48
|
-
/** hosting 文件级 diff(新增/修改/删除,前 N 条 + 统计) */
|
|
49
|
-
fileDiff?: {
|
|
50
|
-
added: string[];
|
|
51
|
-
modified: string[];
|
|
52
|
-
deleted: string[];
|
|
53
|
-
totalChanged: number;
|
|
54
|
-
};
|
|
55
|
-
/** 资源级详情子行(dry-run 树状渲染用):如函数 runtime/构建策略、迁移逐条状态等 */
|
|
56
|
-
details?: Array<{
|
|
57
|
-
label: string;
|
|
58
|
-
value: string;
|
|
59
|
-
}>;
|
|
60
|
-
}
|
|
61
|
-
/** 部署结果 */
|
|
62
|
-
export interface IDeployResult {
|
|
63
|
-
plan: IDeployPlanItem[];
|
|
64
|
-
results: Array<{
|
|
65
|
-
type: ResourceType;
|
|
66
|
-
name: string;
|
|
67
|
-
ok: boolean;
|
|
68
|
-
url?: string;
|
|
69
|
-
error?: string;
|
|
70
|
-
/** 未执行原因:skip=未变更自动跳过;no-confirm=无确认机制保守跳过;skipped-by-user=用户拒绝覆盖 */
|
|
71
|
-
reason?: 'skip' | 'no-confirm' | 'skipped-by-user';
|
|
72
|
-
}>;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* 声明式部署编排器
|
|
76
|
-
* 顺序:database → functions → app → hosting → gateway
|
|
77
|
-
* 支持 dry-run / only / skip / 幂等重试(database 失败/冲突会中断后续步骤)
|
|
78
|
-
*/
|
|
79
|
-
export declare class DeployOrchestrator {
|
|
80
|
-
private environment;
|
|
81
|
-
private databaseDeployer;
|
|
82
|
-
private functionDeployer;
|
|
83
|
-
private staticDeployer;
|
|
84
|
-
private gatewayDeployer;
|
|
85
|
-
constructor(environment: Environment);
|
|
86
|
-
/**
|
|
87
|
-
* 计算部署计划(dry-run 输出)
|
|
88
|
-
*/
|
|
89
|
-
deployPlan(options: IDeployOptions): Promise<IDeployPlanItem[]>;
|
|
90
|
-
/**
|
|
91
|
-
* 执行部署
|
|
92
|
-
*
|
|
93
|
-
* 编排模型:plan 按「连续相同资源类型」切成 segments,segment 之间严格按
|
|
94
|
-
* DEPLOY_ORDER 串行;segment 内部按 concurrency 并行执行。失败中断策略:
|
|
95
|
-
* - 默认 fail-fast:任一资源失败即中断后续(return)
|
|
96
|
-
* - continueOnError:非 database 资源失败仍继续,最后汇总失败数
|
|
97
|
-
* - database 失败始终强制中断(无论是否 continueOnError)
|
|
98
|
-
*/
|
|
99
|
-
deploy(options: IDeployOptions): Promise<IDeployResult>;
|
|
100
|
-
/**
|
|
101
|
-
* 将扁平 plan 按「连续相同资源类型」切成有序 segments
|
|
102
|
-
*
|
|
103
|
-
* segment 之间保留 DEPLOY_ORDER 依赖顺序(严格串行);segment 内部为同类型
|
|
104
|
-
* 多实例,可并行执行。例如 [db, fnA, fnB, hostingX, hostingY, gateway]
|
|
105
|
-
* → [[db], [fnA, fnB], [hostingX, hostingY], [gateway]]
|
|
106
|
-
*/
|
|
107
|
-
private segmentPlan;
|
|
108
|
-
/**
|
|
109
|
-
* 部署成功后写回本地 state 快照
|
|
110
|
-
*
|
|
111
|
-
* - hosting:成功部署/未变更(skip)的项重算产物指纹;失败项保留旧快照
|
|
112
|
-
* - app:成功部署写 buildAppConfig 快照;skip 复用旧快照
|
|
113
|
-
* - gateway:保留旧快照(本轮不做路由级实时快照)
|
|
114
|
-
*/
|
|
115
|
-
private persistState;
|
|
116
|
-
/**
|
|
117
|
-
* 构建完整部署计划(按依赖顺序 + only/skip 过滤)
|
|
118
|
-
*
|
|
119
|
-
* 通过查询云端状态判定每个资源的动作:
|
|
120
|
-
* - database:先按迁移文件做 preview,再聚合成单条资源级计划项(pending→create,存在 conflict→conflict,全部 applied→不出现在 plan)
|
|
121
|
-
* - functions:ListFunctions 判断是否已存在 → create / update(不做本地 hash / 字段 diff)
|
|
122
|
-
* - app:describeAppInfo 判断是否已存在 → create / update;本地 state 配置一致 → skip
|
|
123
|
-
* - hosting:本地 state 指纹 diff → 一致 skip(不重新上传)/ 有差异 deploy(带文件级明细)
|
|
124
|
-
* - gateway:planRoutes 对比云端路由 → create / update / skip(含变更字段明细)
|
|
125
|
-
* 云端查询失败时降级为保守判定(全 create),不影响 dry-run 展示。
|
|
126
|
-
*/
|
|
127
|
-
private buildPlan;
|
|
128
|
-
/**
|
|
129
|
-
* 按资源类型构建计划(拆分自 buildPlan,控制圈复杂度)
|
|
130
|
-
*/
|
|
131
|
-
private buildPlanForType;
|
|
132
|
-
/**
|
|
133
|
-
* database 计划:preview 聚合成单条资源级计划项
|
|
134
|
-
*/
|
|
135
|
-
private buildDatabasePlan;
|
|
136
|
-
/**
|
|
137
|
-
* app 计划:本地 state 配置一致 → skip;否则按云端存在性 → create / update
|
|
138
|
-
*/
|
|
139
|
-
private buildAppPlan;
|
|
140
|
-
/**
|
|
141
|
-
* hosting 计划:本地 state 指纹 diff → 一致 skip / 有差异 deploy(带文件级明细)
|
|
142
|
-
*/
|
|
143
|
-
private buildHostingPlans;
|
|
144
|
-
/**
|
|
145
|
-
* gateway 计划:planRoutes 对比云端路由 → create / update / skip(含变更字段明细)
|
|
146
|
-
*/
|
|
147
|
-
private buildGatewayPlans;
|
|
148
|
-
/**
|
|
149
|
-
* 查询环境中已存在的函数名集合(查询失败返回空集,降级为全新建判定)
|
|
150
|
-
*/
|
|
151
|
-
private listExistingFunctionNames;
|
|
152
|
-
/**
|
|
153
|
-
* 判断云应用是否已存在(查询失败返回 false,降级为新建判定)
|
|
154
|
-
*/
|
|
155
|
-
private appExists;
|
|
156
|
-
/**
|
|
157
|
-
* 执行单个计划步骤
|
|
158
|
-
*/
|
|
159
|
-
private executeStep;
|
|
160
|
-
/** 深比较(JSON 快照语义):仅用于 app 配置快照等简单对象 */
|
|
161
|
-
private deepEqual;
|
|
162
|
-
/** app 配置差异明细(from → to) */
|
|
163
|
-
private appConfigChanges;
|
|
164
|
-
/**
|
|
165
|
-
* 轻量 inline 守卫:配置合法 + 目录存在(validateProject 接线点)
|
|
166
|
-
*/
|
|
167
|
-
private guardConfig;
|
|
168
|
-
}
|