@cloudbase/manager-node 5.7.1-beta.0 → 5.8.0-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.
Files changed (62) hide show
  1. package/lib/cloudApp/index.js +12 -3
  2. package/lib/deploy/DatabaseDeployer.js +238 -0
  3. package/lib/deploy/DeployOrchestrator.js +645 -0
  4. package/lib/deploy/FunctionDeployer.js +611 -0
  5. package/lib/deploy/GatewayDeployer.js +612 -0
  6. package/lib/deploy/StateStore.js +327 -0
  7. package/lib/deploy/StaticDeployer.js +236 -0
  8. package/lib/deploy/domain.js +41 -0
  9. package/lib/deploy/framework.js +230 -0
  10. package/lib/deploy/function/artifact.js +207 -0
  11. package/lib/deploy/function/builders/cloud.js +654 -0
  12. package/lib/deploy/function/cam-preflight.js +157 -0
  13. package/lib/deploy/function/cloud-build-service.js +191 -0
  14. package/lib/deploy/function/config-guard.js +595 -0
  15. package/lib/deploy/function/docker-preflight.js +87 -0
  16. package/lib/deploy/function/image-preflight.js +164 -0
  17. package/lib/deploy/function/local-builder.js +129 -0
  18. package/lib/deploy/function/planner.js +278 -0
  19. package/lib/deploy/function/preflight.js +329 -0
  20. package/lib/deploy/types.js +103 -0
  21. package/lib/env/index.js +0 -9
  22. package/lib/environment.js +11 -0
  23. package/lib/function/index.js +1 -1
  24. package/lib/hosting/index.js +4 -1
  25. package/lib/index.js +31 -0
  26. package/lib/projectValidator/index.js +452 -0
  27. package/lib/projectValidator/types.js +2 -0
  28. package/lib/storage/index.js +6 -7
  29. package/lib/utils/index.js +62 -5
  30. package/package.json +2 -1
  31. package/types/cloudApp/index.d.ts +4 -0
  32. package/types/cloudApp/types.d.ts +87 -6
  33. package/types/deploy/DatabaseDeployer.d.ts +70 -0
  34. package/types/deploy/DeployOrchestrator.d.ts +170 -0
  35. package/types/deploy/FunctionDeployer.d.ts +159 -0
  36. package/types/deploy/GatewayDeployer.d.ts +194 -0
  37. package/types/deploy/StateStore.d.ts +170 -0
  38. package/types/deploy/StaticDeployer.d.ts +97 -0
  39. package/types/deploy/domain.d.ts +17 -0
  40. package/types/deploy/framework.d.ts +63 -0
  41. package/types/deploy/function/artifact.d.ts +40 -0
  42. package/types/deploy/function/builders/cloud.d.ts +110 -0
  43. package/types/deploy/function/cam-preflight.d.ts +51 -0
  44. package/types/deploy/function/cloud-build-service.d.ts +10 -0
  45. package/types/deploy/function/config-guard.d.ts +41 -0
  46. package/types/deploy/function/docker-preflight.d.ts +17 -0
  47. package/types/deploy/function/image-preflight.d.ts +21 -0
  48. package/types/deploy/function/local-builder.d.ts +26 -0
  49. package/types/deploy/function/planner.d.ts +15 -0
  50. package/types/deploy/function/preflight.d.ts +9 -0
  51. package/types/deploy/types.d.ts +429 -0
  52. package/types/env/index.d.ts +1 -8
  53. package/types/env/type.d.ts +6 -97
  54. package/types/environment.d.ts +7 -0
  55. package/types/function/types.d.ts +15 -0
  56. package/types/hosting/index.d.ts +6 -0
  57. package/types/index.d.ts +18 -0
  58. package/types/interfaces/function.interface.d.ts +1 -1
  59. package/types/projectValidator/index.d.ts +38 -0
  60. package/types/projectValidator/types.d.ts +26 -0
  61. package/types/storage/index.d.ts +6 -0
  62. package/types/utils/index.d.ts +13 -0
@@ -0,0 +1,327 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.StateStore = exports.DEFAULT_HASH_LENGTH = exports.DEFAULT_MAX_HOSTING_FILES = exports.HostingFilesOverLimitError = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const child_process_1 = require("child_process");
11
+ const error_1 = require("../error");
12
+ /** hosting 指纹文件数超过上限时抛出(触发降级为全量部署) */
13
+ class HostingFilesOverLimitError extends Error {
14
+ constructor(fileCount, limit) {
15
+ super(`hosting 文件数 ${fileCount} 超过指纹上限 ${limit},跳过逐文件 diff,降级为全量部署`);
16
+ this.name = 'HostingFilesOverLimitError';
17
+ this.fileCount = fileCount;
18
+ this.limit = limit;
19
+ }
20
+ }
21
+ exports.HostingFilesOverLimitError = HostingFilesOverLimitError;
22
+ /** 默认强制忽略的目录/文件(不可被调用方关闭) */
23
+ const DEFAULT_IGNORE = ['.DS_Store', 'node_modules/'];
24
+ /** 默认指纹文件数上限(防快照膨胀与 I/O 失控) */
25
+ exports.DEFAULT_MAX_HOSTING_FILES = 50000;
26
+ /** 默认 hash 截断长度(16 位 hex = 64bit,碰撞概率可忽略;快照体积减半) */
27
+ exports.DEFAULT_HASH_LENGTH = 16;
28
+ /**
29
+ * 部署状态快照(StateStore)
30
+ *
31
+ * 职责:读写 `.cloudbase/state.json`(默认),为 dry-run 的 hosting/app diff 提供本地基准,
32
+ * 避免每次 diff 都拉取云端。
33
+ *
34
+ * 指纹算法:hosting = 产物目录逐文件内容 sha256(相对路径为 key)。
35
+ * 只存 hash 不存内容,体积 = O(文件数);app 快照不写入 envVariables(敏感过滤)。
36
+ */
37
+ class StateStore {
38
+ constructor(options = {}) {
39
+ this.cwd = options.cwd || process.cwd();
40
+ this.env = options.env;
41
+ this.stateFile = options.stateFile;
42
+ }
43
+ /**
44
+ * 计算 state 文件路径:
45
+ * - 自定义 stateFile 优先
46
+ * - 否则 {cwd}/.cloudbase/state.json(无 env)或 state.{env}.json(多环境隔离)
47
+ */
48
+ getFilePath() {
49
+ if (this.stateFile) {
50
+ return this.stateFile;
51
+ }
52
+ const base = this.env ? `state.${this.env}.json` : 'state.json';
53
+ return path_1.default.join(this.cwd, '.cloudbase', base);
54
+ }
55
+ /**
56
+ * 读取状态快照;文件不存在或 JSON 损坏返回 null(缺失降级,由调用方走云端兜底)
57
+ */
58
+ read() {
59
+ const filePath = this.getFilePath();
60
+ if (!fs_1.default.existsSync(filePath)) {
61
+ return null;
62
+ }
63
+ try {
64
+ return JSON.parse(fs_1.default.readFileSync(filePath, 'utf8'));
65
+ }
66
+ catch (_a) {
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * 写入状态快照(自动补 version/updatedAt/gitCommit,自动创建 .cloudbase 目录)
72
+ */
73
+ write(state) {
74
+ var _a;
75
+ const filePath = this.getFilePath();
76
+ fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
77
+ const full = {
78
+ version: 1,
79
+ updatedAt: new Date().toISOString(),
80
+ gitCommit: (_a = state.gitCommit) !== null && _a !== void 0 ? _a : this.resolveGitCommit(),
81
+ resources: state.resources
82
+ };
83
+ fs_1.default.writeFileSync(filePath, JSON.stringify(full, null, 2), 'utf8');
84
+ }
85
+ /**
86
+ * 计算产物目录逐文件 sha256 指纹(仅 hash,兼容旧调用)
87
+ *
88
+ * @param projectPath 产物目录(hosting 的 outputDir / root)
89
+ * @param ignore 自定义忽略规则(追加到默认 node_modules / .DS_Store 之后)
90
+ * @param options 扫描选项(maxFiles / hashLength)
91
+ * @returns 相对路径(/ 分隔)→ 文件内容 sha256(默认截断 16 位)
92
+ */
93
+ async computeHostingFiles(projectPath, ignore = [], options = {}) {
94
+ const fingerprints = await this.computeHostingFingerprints(projectPath, ignore, options);
95
+ const result = {};
96
+ for (const [relPath, fp] of Object.entries(fingerprints)) {
97
+ result[relPath] = fp.hash;
98
+ }
99
+ return result;
100
+ }
101
+ /**
102
+ * 计算产物目录逐文件指纹(hash + size)
103
+ *
104
+ * - 强制忽略 node_modules / .DS_Store(与传入 ignore 合并,不可关闭)
105
+ * - hash 默认截断为 16 位 hex(hashLength 可调,64 = 完整 sha256)
106
+ * - 文件数超过 maxFiles 抛 HostingFilesOverLimitError(防快照膨胀与 I/O 失控)
107
+ *
108
+ * @param projectPath 产物目录(hosting 的 outputDir / root)
109
+ * @param ignore 自定义忽略规则
110
+ * @param options 扫描选项(maxFiles / hashLength)
111
+ * @returns 相对路径(/ 分隔)→ { hash, size }
112
+ */
113
+ async computeHostingFingerprints(projectPath, ignore = [], options = {}) {
114
+ if (!fs_1.default.existsSync(projectPath) || !fs_1.default.statSync(projectPath).isDirectory()) {
115
+ throw new error_1.CloudBaseError(`产物目录不存在或不是目录:${projectPath}`);
116
+ }
117
+ const { maxFiles = exports.DEFAULT_MAX_HOSTING_FILES, hashLength = exports.DEFAULT_HASH_LENGTH } = options;
118
+ const effectiveIgnore = this.mergeIgnore(ignore);
119
+ const files = {};
120
+ let count = 0;
121
+ const walk = (dir, rel) => {
122
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
123
+ const entryPath = path_1.default.join(dir, entry.name);
124
+ const relPath = rel ? path_1.default.join(rel, entry.name) : entry.name;
125
+ if (entry.isDirectory()) {
126
+ if (this.isIgnored(relPath, effectiveIgnore)) {
127
+ continue;
128
+ }
129
+ walk(entryPath, relPath);
130
+ }
131
+ else if (entry.isFile() && !this.isIgnored(relPath, effectiveIgnore)) {
132
+ count++;
133
+ if (maxFiles > 0 && count > maxFiles) {
134
+ throw new HostingFilesOverLimitError(count, maxFiles);
135
+ }
136
+ const normalized = relPath.split(path_1.default.sep).join('/');
137
+ files[normalized] = {
138
+ hash: this.hashFile(entryPath, hashLength),
139
+ size: fs_1.default.statSync(entryPath).size
140
+ };
141
+ }
142
+ }
143
+ };
144
+ walk(projectPath, '');
145
+ return files;
146
+ }
147
+ /**
148
+ * 计算产物目录与快照的变更集(懒加载)
149
+ *
150
+ * 优化:先 stat 拿 size,size 与快照不同的文件直接判定变更(不读文件内容),
151
+ * 只有 size 相同(或旧快照 size 未知)的文件才读内容 hash 对比,
152
+ * 避免为未变文件做无谓的 I/O 与 hash。
153
+ * 文件数超过 maxFiles 时放弃 diff,返回 overLimit=true(调用方应降级为全量部署)。
154
+ *
155
+ * @param projectPath 产物目录(hosting 的 outputDir / root)
156
+ * @param previous 上次快照指纹(undefined = 首次部署,全部视为新增)
157
+ * @param options 选项(ignore / maxFiles / hashLength)
158
+ */
159
+ async diffHostingFiles(projectPath, previous, options = {}) {
160
+ if (!fs_1.default.existsSync(projectPath) || !fs_1.default.statSync(projectPath).isDirectory()) {
161
+ throw new error_1.CloudBaseError(`产物目录不存在或不是目录:${projectPath}`);
162
+ }
163
+ const { ignore = [], maxFiles = exports.DEFAULT_MAX_HOSTING_FILES, hashLength = exports.DEFAULT_HASH_LENGTH } = options;
164
+ const effectiveIgnore = this.mergeIgnore(ignore);
165
+ // 归一化快照:旧格式(string)size 未知 → -1,走 hash 对比而非 size 预判
166
+ let prev;
167
+ if (previous) {
168
+ prev = {};
169
+ for (const [k, v] of Object.entries(previous)) {
170
+ prev[k] = this.normalizeFileValue(v);
171
+ }
172
+ }
173
+ const localFiles = new Set();
174
+ const added = [];
175
+ const modified = [];
176
+ let count = 0;
177
+ let overLimit = false;
178
+ const walk = (dir, rel) => {
179
+ if (overLimit) {
180
+ return;
181
+ }
182
+ for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
183
+ const entryPath = path_1.default.join(dir, entry.name);
184
+ const relPath = rel ? path_1.default.join(rel, entry.name) : entry.name;
185
+ if (entry.isDirectory()) {
186
+ if (this.isIgnored(relPath, effectiveIgnore)) {
187
+ continue;
188
+ }
189
+ walk(entryPath, relPath);
190
+ }
191
+ else if (entry.isFile() && !this.isIgnored(relPath, effectiveIgnore)) {
192
+ count++;
193
+ if (maxFiles > 0 && count > maxFiles) {
194
+ overLimit = true;
195
+ return;
196
+ }
197
+ const normalized = relPath.split(path_1.default.sep).join('/');
198
+ localFiles.add(normalized);
199
+ const prevEntry = prev === null || prev === void 0 ? void 0 : prev[normalized];
200
+ if (!prevEntry) {
201
+ added.push(normalized);
202
+ }
203
+ else if (prevEntry.size !== -1 && prevEntry.size !== fs_1.default.statSync(entryPath).size) {
204
+ // size 变化 → 内容必变,无需读文件(懒加载核心)
205
+ modified.push(normalized);
206
+ }
207
+ else {
208
+ // size 相同或旧快照 size 未知 → 读内容 hash 确认
209
+ const hash = this.hashFile(entryPath, hashLength);
210
+ if (prevEntry.hash !== hash) {
211
+ modified.push(normalized);
212
+ }
213
+ }
214
+ }
215
+ }
216
+ };
217
+ walk(projectPath, '');
218
+ if (overLimit) {
219
+ return {
220
+ added: [],
221
+ modified: [],
222
+ deleted: [],
223
+ totalChanged: 0,
224
+ unchanged: false,
225
+ overLimit: true
226
+ };
227
+ }
228
+ const deleted = prev
229
+ ? Object.keys(prev).filter(k => !localFiles.has(k) && !this.isIgnored(k, effectiveIgnore))
230
+ : [];
231
+ const totalChanged = added.length + modified.length + deleted.length;
232
+ return { added, modified, deleted, totalChanged, unchanged: totalChanged === 0 };
233
+ }
234
+ /**
235
+ * 提取 app 配置快照(仅 diff 白名单字段)
236
+ * envVariables / ignore 等非 diff 字段与敏感信息不进入快照
237
+ */
238
+ buildAppConfig(config) {
239
+ if (!config || typeof config !== 'object') {
240
+ return undefined;
241
+ }
242
+ const entry = {};
243
+ if (config.serviceName !== undefined) {
244
+ entry.serviceName = config.serviceName;
245
+ }
246
+ if (config.installCommand !== undefined) {
247
+ entry.installCommand = config.installCommand;
248
+ }
249
+ if (config.buildCommand !== undefined) {
250
+ entry.buildCommand = config.buildCommand;
251
+ }
252
+ if (config.outputDir !== undefined) {
253
+ entry.outputDir = config.outputDir;
254
+ }
255
+ if (config.deployPath !== undefined) {
256
+ entry.deployPath = config.deployPath;
257
+ }
258
+ return entry;
259
+ }
260
+ hashFile(filePath, hashLength = exports.DEFAULT_HASH_LENGTH) {
261
+ const content = fs_1.default.readFileSync(filePath);
262
+ return crypto_1.default.createHash('sha256').update(content).digest('hex').slice(0, hashLength);
263
+ }
264
+ /** 合并默认忽略规则(node_modules / .DS_Store 不可被调用方关闭) */
265
+ mergeIgnore(ignore) {
266
+ return Array.from(new Set([...DEFAULT_IGNORE, ...(ignore || [])]));
267
+ }
268
+ /** 归一化快照指纹值:旧格式(纯 string = 仅 hash,size 未知用 -1 表示) */
269
+ normalizeFileValue(value) {
270
+ if (typeof value === 'string') {
271
+ return { hash: value, size: -1 };
272
+ }
273
+ return value;
274
+ }
275
+ /**
276
+ * 是否命中忽略规则:
277
+ * - 目录模式(结尾 /):匹配该目录及其下所有
278
+ * - glob(含 **):转换为正则匹配相对路径
279
+ * - 其余:精确/前缀匹配
280
+ */
281
+ isIgnored(relPath, ignore) {
282
+ const normalized = relPath.split(path_1.default.sep).join('/');
283
+ for (const pattern of ignore) {
284
+ if (this.matchPattern(normalized, pattern)) {
285
+ return true;
286
+ }
287
+ }
288
+ return false;
289
+ }
290
+ matchPattern(relPath, pattern) {
291
+ const p = pattern.trim();
292
+ if (!p) {
293
+ return false;
294
+ }
295
+ // 目录模式(结尾 /):匹配目录本身及下所有内容
296
+ if (p.endsWith('/')) {
297
+ const prefix = p.slice(0, -1);
298
+ return relPath === prefix || relPath.startsWith(prefix + '/');
299
+ }
300
+ // glob(含 **):把 ** 转成 .*,其余转义后整体匹配
301
+ if (p.includes('**')) {
302
+ const regexStr = '^' +
303
+ p.split('**').map(part => this.escapeRegExp(part)).join('.*') +
304
+ '$';
305
+ return new RegExp(regexStr).test(relPath);
306
+ }
307
+ // 精确匹配或前缀匹配(目录下的文件)
308
+ return relPath === p || relPath.startsWith(p + '/');
309
+ }
310
+ escapeRegExp(s) {
311
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
312
+ }
313
+ /** 解析当前 git commit(非 git 仓库或失败返回 undefined,不阻断) */
314
+ resolveGitCommit() {
315
+ try {
316
+ const out = (0, child_process_1.execSync)('git rev-parse HEAD', {
317
+ cwd: this.cwd,
318
+ stdio: 'pipe'
319
+ });
320
+ return out.toString().trim() || undefined;
321
+ }
322
+ catch (_a) {
323
+ return undefined;
324
+ }
325
+ }
326
+ }
327
+ exports.StateStore = StateStore;
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.StaticDeployer = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const child_process_1 = require("child_process");
9
+ const error_1 = require("../error");
10
+ const framework_1 = require("./framework");
11
+ /** 环境变量值统一转字符串(execSync env 要求 string) */
12
+ function stringifyEnvValues(envVariables) {
13
+ const result = {};
14
+ if (envVariables) {
15
+ for (const [key, value] of Object.entries(envVariables)) {
16
+ result[key] = String(value);
17
+ }
18
+ }
19
+ return result;
20
+ }
21
+ /**
22
+ * app/hosting 声明式部署器
23
+ *
24
+ * app:framework=static → 直传产物 + SkipBuild 写记录(对齐 tcb app deploy 纯静态分支);
25
+ * 其余 framework → 云端构建(上传源码 → 云端构建 → 写版本记录),与现有 tcb app deploy 一致
26
+ * hosting:固定静态流程(buildCommand 非空则本地构建后直传),不写版本记录(现有 tcb hosting deploy 行为)
27
+ */
28
+ class StaticDeployer {
29
+ constructor(environment) {
30
+ this.environment = environment;
31
+ }
32
+ /**
33
+ * app 部署(与现有 tcb app deploy 一致)
34
+ *
35
+ * framework=static:直传产物(hosting uploadFiles)+ createApp(SkipBuild) 后台写部署记录,跳过云端构建。
36
+ * 其余 framework:上传源码 ZIP → createApp 云端构建 → 写版本记录。
37
+ */
38
+ async deployApp(options) {
39
+ const { config, cwd = process.cwd() } = options;
40
+ const serviceName = config.serviceName;
41
+ if (!serviceName) {
42
+ throw new error_1.CloudBaseError('app 部署需要配置 serviceName');
43
+ }
44
+ const projectPath = path_1.default.resolve(cwd, config.root || '.');
45
+ const resolvedDeployPath = config.deployPath || `/${serviceName}`;
46
+ // 纯静态:直传产物 + SkipBuild 写记录(对齐 tcb app deploy 的 deployStaticApp 分支)
47
+ if (config.framework === 'static') {
48
+ return this.deployAppStatic({ config, serviceName, projectPath, resolvedDeployPath });
49
+ }
50
+ return this.deployAppCloud(config, serviceName, projectPath);
51
+ }
52
+ /**
53
+ * hosting 部署(固定静态流程,不写版本记录)
54
+ *
55
+ * 支持本地构建(对齐 Netlify):buildCommand 非空时自动执行 install + build 后上传产物;
56
+ * 纯静态(buildCommand 空/未配置且无法检测框架)时直接上传 outputDir(默认 root,保持现状)。
57
+ * 访问地址 = https://{CdnDomain}{deployPath}(与 tcb hosting deploy 一致)
58
+ */
59
+ async deployHosting(options) {
60
+ const { config, cwd = process.cwd() } = options;
61
+ const name = config.name || 'default';
62
+ const root = path_1.default.resolve(cwd, config.root || '.');
63
+ const cloudPath = config.deployPath || '/';
64
+ // 1. 解析构建命令:显式 buildCommand > framework 映射 > package.json 自动探测 > null(纯静态)
65
+ const buildCommand = (0, framework_1.resolveBuildCommand)(config, root);
66
+ // 2. 有构建命令 → 本地构建(install + build);纯静态 → 跳过构建直接上传
67
+ if (buildCommand) {
68
+ this.runLocalBuild(root, Object.assign(Object.assign({}, config), { buildCommand }), config.envVariables);
69
+ }
70
+ // 3. outputDir:由 framework.resolveHostingOutputDir 统一解析,
71
+ // 确保与 DeployOrchestrator(buildHostingPlans/persistState) 保持一致。
72
+ const outputDir = (0, framework_1.resolveHostingOutputDir)(config, root);
73
+ const hostingOptions = {
74
+ localPath: outputDir,
75
+ cloudPath,
76
+ ignore: config.ignore || ['.DS_Store']
77
+ };
78
+ await this.environment.getHostingService().uploadFiles(hostingOptions);
79
+ const url = await this.resolveHostingUrl(cloudPath);
80
+ return { name, url, wroteRecord: false };
81
+ }
82
+ /**
83
+ * app 纯静态直传(对齐 tcb app deploy 的 deployStaticApp 分支)
84
+ *
85
+ * 1. hosting uploadFiles 直传产物(文件级上传,立即可访问,无需打 ZIP)
86
+ * 2. 后台异步 createApp(SkipBuild) 写部署记录(失败不影响文件访问)
87
+ * 3. 跳过云端构建轮询,返回访问地址
88
+ */
89
+ async deployAppStatic(options) {
90
+ const { config, serviceName, projectPath, resolvedDeployPath } = options;
91
+ const cloudAppService = this.environment.getCloudAppService();
92
+ // 1. 直传产物(outputDir 默认项目根;cloudPath 与云端构建分支的 deployCmd 保持一致,
93
+ // 避免同一服务名切换构建/纯静态时访问路径漂移)
94
+ // 默认排除 node_modules(对齐 CLI 纯静态 DEFAULT_APP_IGNORE),避免产物目录含依赖时上传失控
95
+ const uploadDir = path_1.default.resolve(projectPath, config.outputDir || '.');
96
+ await this.environment.getHostingService().uploadFiles({
97
+ localPath: uploadDir,
98
+ cloudPath: resolvedDeployPath,
99
+ ignore: ['.DS_Store', 'node_modules/**', '**/.DS_Store', ...(config.ignore || [])]
100
+ });
101
+ // 2. 后台异步写部署记录(fire-and-forget:文件已直传,记录失败不影响访问)
102
+ const recordTask = (async () => {
103
+ try {
104
+ const uploadResult = await cloudAppService.uploadCode({
105
+ deployType: 'static-hosting',
106
+ serviceName,
107
+ localPath: projectPath,
108
+ ignore: ['.git/**', ...(config.ignore || [])]
109
+ });
110
+ await cloudAppService.createApp({
111
+ deployType: 'static-hosting',
112
+ serviceName,
113
+ buildType: 'ZIP',
114
+ skipBuild: true,
115
+ staticConfig: {
116
+ cosTimestamp: uploadResult.unixTimestamp,
117
+ appPath: resolvedDeployPath,
118
+ framework: config.framework,
119
+ staticCmd: {
120
+ installCmd: '',
121
+ buildCmd: '',
122
+ deployCmd: `tcb hosting deploy ${config.outputDir || '.'} ${resolvedDeployPath}`
123
+ }
124
+ }
125
+ });
126
+ }
127
+ catch (_a) {
128
+ // 部署记录写入失败不影响已上传的文件访问
129
+ }
130
+ })();
131
+ recordTask.catch(() => { });
132
+ // 3. 访问地址:静态托管 CDN 域名 + 部署路径(与 tcb app deploy 纯静态分支一致)
133
+ const url = await this.resolveHostingUrl(resolvedDeployPath);
134
+ return { name: serviceName, url, wroteRecord: true };
135
+ }
136
+ /**
137
+ * app 云端构建:上传源码 ZIP → createApp 云端构建 → 写版本记录
138
+ */
139
+ async deployAppCloud(config, serviceName, projectPath) {
140
+ const cloudAppService = this.environment.getCloudAppService();
141
+ const uploadResult = await cloudAppService.uploadCode({
142
+ deployType: 'static-hosting',
143
+ serviceName,
144
+ localPath: projectPath,
145
+ // 云端构建:不排除 node_modules(云端 install 需要私有/离线依赖随包上传)
146
+ excludeNodeModules: false,
147
+ ignore: ['.git/**', ...(config.ignore || [])]
148
+ });
149
+ await cloudAppService.createApp({
150
+ deployType: 'static-hosting',
151
+ serviceName,
152
+ buildType: 'ZIP',
153
+ staticConfig: {
154
+ cosTimestamp: uploadResult.unixTimestamp,
155
+ appPath: config.deployPath || `/${serviceName}`,
156
+ staticCmd: {
157
+ installCmd: config.installCommand || 'npm install',
158
+ buildCmd: config.buildCommand || 'npm run build',
159
+ deployCmd: `tcb hosting deploy ${config.outputDir || 'dist'} ${config.deployPath || `/${serviceName}`}`
160
+ }
161
+ }
162
+ });
163
+ const url = await this.resolveAppUrl(serviceName, config.deployPath || `/${serviceName}`);
164
+ return { name: serviceName, url, wroteRecord: true };
165
+ }
166
+ /**
167
+ * 解析 hosting 访问地址:https://{CdnDomain}{deployPath}
168
+ */
169
+ async resolveHostingUrl(cloudPath) {
170
+ var _a;
171
+ try {
172
+ const info = await this.environment.getHostingService().getInfo();
173
+ const cdnDomain = (_a = info === null || info === void 0 ? void 0 : info[0]) === null || _a === void 0 ? void 0 : _a.CdnDomain;
174
+ if (!cdnDomain) {
175
+ return undefined;
176
+ }
177
+ const normalizedPath = cloudPath.startsWith('/') ? cloudPath : `/${cloudPath}`;
178
+ return `https://${cdnDomain}${normalizedPath}`;
179
+ }
180
+ catch (_b) {
181
+ return undefined;
182
+ }
183
+ }
184
+ /**
185
+ * 本地构建(install + build)
186
+ *
187
+ * - installCommand:默认 npm install,显式空字符串跳过安装
188
+ * - buildCommand:由调用方保证非空(hosting 经 resolveBuildCommand 判定;app local 由 if 判定)
189
+ * - envVariables:注入构建进程环境变量(值统一转字符串)
190
+ */
191
+ runLocalBuild(projectPath, config, envVariables) {
192
+ var _a;
193
+ const installCommand = (_a = config.installCommand) !== null && _a !== void 0 ? _a : 'npm install';
194
+ const buildCommand = config.buildCommand || '';
195
+ const env = Object.assign(Object.assign({}, process.env), stringifyEnvValues(envVariables));
196
+ const commands = [];
197
+ if (installCommand) {
198
+ commands.push({ cmd: installCommand, label: '安装依赖' });
199
+ }
200
+ if (buildCommand) {
201
+ commands.push({ cmd: buildCommand, label: '构建' });
202
+ }
203
+ try {
204
+ for (const { cmd } of commands) {
205
+ (0, child_process_1.execSync)(cmd, { cwd: projectPath, stdio: 'inherit', env });
206
+ }
207
+ }
208
+ catch (e) {
209
+ 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();
210
+ const executed = commands.map(c => c.cmd).join('」或「');
211
+ throw new error_1.CloudBaseError(`本地构建失败:在 ${projectPath} 执行「${executed}」失败。\n` +
212
+ `请先在项目目录手动执行该命令排查原因(常见原因:npm registry 网络不可达、依赖安装权限、lockfile 不一致)。\n` +
213
+ (detail ? `原始错误:${detail.slice(0, 500)}` : ''));
214
+ }
215
+ }
216
+ /**
217
+ * 解析 app 访问地址(describeAppInfo.Domain + appPath)
218
+ */
219
+ async resolveAppUrl(serviceName, appPath) {
220
+ try {
221
+ const appInfo = await this.environment.getCloudAppService().describeAppInfo({
222
+ deployType: 'static-hosting',
223
+ serviceName
224
+ });
225
+ if (!appInfo.Domain) {
226
+ return undefined;
227
+ }
228
+ const normalizedPath = appPath.startsWith('/') ? appPath : `/${appPath}`;
229
+ return `https://${appInfo.Domain}${normalizedPath}`;
230
+ }
231
+ catch (_a) {
232
+ return undefined;
233
+ }
234
+ }
235
+ }
236
+ exports.StaticDeployer = StaticDeployer;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveAccessServiceDomain = resolveAccessServiceDomain;
4
+ exports.getGatewayBaseUrl = getGatewayBaseUrl;
5
+ /** 环境 HTTP 访问服务默认域名后缀(如 `xxx-1326375956.ap-shanghai.${ACCESS_DOMAIN_SUFFIX}`) */
6
+ const ACCESS_DOMAIN_SUFFIX = 'app.tcloudbase.com';
7
+ /**
8
+ * 构造环境 HTTP 访问服务默认域名 `${envId}-${AppId}.${region}.${ACCESS_DOMAIN_SUFFIX}`
9
+ *
10
+ * 关键:用 `UserInfo.AppId`(腾讯云 appid)而非 `UserInfo.Uin`(账号 uin)。
11
+ * 控制台默认域名格式是 `${envId}-${AppId}.${region}.${ACCESS_DOMAIN_SUFFIX}`
12
+ * (如 `xxx-1326375956.ap-shanghai.app.tcloudbase.com`),与 cookie 中的 `appid` 字段对应。
13
+ * AppId/Region 缺失或查询失败时返回 undefined
14
+ */
15
+ async function resolveAccessServiceDomain(environment, envId) {
16
+ var _a;
17
+ try {
18
+ const { EnvInfo } = await environment.getEnvService().getEnvInfo();
19
+ const appId = (_a = EnvInfo === null || EnvInfo === void 0 ? void 0 : EnvInfo.UserInfo) === null || _a === void 0 ? void 0 : _a.AppId;
20
+ const region = EnvInfo === null || EnvInfo === void 0 ? void 0 : EnvInfo.Region;
21
+ if (appId && region) {
22
+ return `${envId}-${appId}.${region}.${ACCESS_DOMAIN_SUFFIX}`;
23
+ }
24
+ }
25
+ catch (_b) {
26
+ // getEnvInfo 失败,忽略
27
+ }
28
+ return undefined;
29
+ }
30
+ /**
31
+ * 构造网关基础地址 `https://{envId}.api.tcloudbasegateway.com`
32
+ *
33
+ * 对齐 CLI 的 `EDomain.Gateway`:`isInternalEndpoint` 决定是否国际站域名
34
+ * (`api.intl.tcloudbasegateway.com`)。
35
+ */
36
+ function getGatewayBaseUrl(environment, envId) {
37
+ const host = environment.cloudBaseContext.isInternalEndpoint()
38
+ ? `${envId}.api.intl.tcloudbasegateway.com`
39
+ : `${envId}.api.tcloudbasegateway.com`;
40
+ return `https://${host}`;
41
+ }