@cloudbase/manager-node 5.8.3 → 5.8.4
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 +9 -0
- package/lib/deploy/DeployOrchestrator.js +165 -287
- package/lib/deploy/FunctionDeployer.js +61 -33
- package/lib/deploy/function/artifact.js +55 -41
- package/lib/deploy/function/builders/cloud.js +13 -11
- package/lib/deploy/function/cam-preflight.js +2 -1
- package/lib/deploy/function/config-guard.js +181 -36
- package/lib/deploy/function/enterprise-tcr.js +7 -6
- package/lib/deploy/function/local-builder.js +10 -9
- package/lib/deploy/function/preflight.js +3 -2
- package/lib/projectValidator/index.js +0 -3
- package/package.json +1 -1
- package/types/deploy/DeployOrchestrator.d.ts +9 -25
- package/types/deploy/FunctionDeployer.d.ts +15 -1
- package/types/deploy/function/artifact.d.ts +0 -5
- package/types/deploy/function/config-guard.d.ts +18 -0
- package/types/deploy/function/enterprise-tcr.d.ts +2 -2
- package/types/deploy/types.d.ts +76 -20
- package/types/projectValidator/types.d.ts +0 -1
- package/lib/deploy/StateStore.js +0 -328
- package/types/deploy/StateStore.d.ts +0 -174
|
@@ -22,6 +22,24 @@ export declare function parseImageReference(reference: string): IParsedImageRefe
|
|
|
22
22
|
export declare function normalizeGatewayPath(input: string): string | null;
|
|
23
23
|
/** 判断是否为自定义镜像运行时 */
|
|
24
24
|
export declare function isCustomImageRuntime(runtime?: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* 应用最小化镜像配置的可安全默认值,不执行 IO,不生成凭证。
|
|
27
|
+
* 仅填充构建上下文、仓库名、namespace 和运行时固定值;用户凭证必须显式传入。
|
|
28
|
+
*
|
|
29
|
+
* 直接在 cloudbaserc 的嵌套形状上操作:镜像字段位于 imageConfig,构建目标位于
|
|
30
|
+
* imageConfig.build,不再做字段平铺映射。
|
|
31
|
+
*/
|
|
32
|
+
export declare function applyMinimalDeployConfigDefaults(config: IFunctionDeployConfig, context?: import('../types').IMinimalDeployConfigContext): IFunctionDeployConfig;
|
|
33
|
+
/**
|
|
34
|
+
* 返回最小化部署在当前配置下仍缺失的输入字段;不执行网络、文件或凭证操作。
|
|
35
|
+
*/
|
|
36
|
+
export declare function getMissingDeployInputs(config: IFunctionDeployConfig, context?: {
|
|
37
|
+
envId?: string;
|
|
38
|
+
}): import('../types').IDeployInputField[];
|
|
39
|
+
/**
|
|
40
|
+
* 生成最小化配置准备结果:先应用安全默认值,再返回剩余缺失字段和默认值路径。
|
|
41
|
+
*/
|
|
42
|
+
export declare function prepareMinimalDeployConfig(config: IFunctionDeployConfig, context?: import('../types').IMinimalDeployConfigContext): import('../types').IMinimalDeployConfigResult;
|
|
25
43
|
/**
|
|
26
44
|
* 规范化部署配置
|
|
27
45
|
*
|
|
@@ -17,12 +17,12 @@ export interface IEnterpriseTcrCredential {
|
|
|
17
17
|
username: string;
|
|
18
18
|
token: string;
|
|
19
19
|
}
|
|
20
|
-
export declare function prepareEnterpriseTcrTarget(config:
|
|
20
|
+
export declare function prepareEnterpriseTcrTarget<T extends EnterpriseBuildConfig>(config: T, options: {
|
|
21
21
|
envId: string;
|
|
22
22
|
region: string;
|
|
23
23
|
service: ITcrApiService;
|
|
24
24
|
}): Promise<{
|
|
25
|
-
config:
|
|
25
|
+
config: T;
|
|
26
26
|
target: IEnterpriseTcrTarget;
|
|
27
27
|
}>;
|
|
28
28
|
export declare function createEnterpriseTcrCredential(service: ITcrApiService, registryId: string): Promise<IEnterpriseTcrCredential>;
|
package/types/deploy/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ICloudFunctionTrigger,
|
|
1
|
+
import { ICloudFunctionTrigger, IFunctionVPC } from '../interfaces';
|
|
2
2
|
/**
|
|
3
3
|
* 镜像 HTTP 函数固定监听端口
|
|
4
4
|
* SCF HTTP 自定义镜像函数要求容器监听 9000
|
|
@@ -295,15 +295,29 @@ export interface IImageBuildTarget {
|
|
|
295
295
|
/** 个人版 TCR 保留的最新镜像标签数量 */
|
|
296
296
|
retainedTags?: number;
|
|
297
297
|
}
|
|
298
|
-
/**
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
298
|
+
/**
|
|
299
|
+
* 声明式镜像运行时配置
|
|
300
|
+
*
|
|
301
|
+
* 直接对齐 toolbox schema 的 ICloudBaseFunctionImageConfig:镜像相关字段全部
|
|
302
|
+
* 收敛在 imageConfig 命名空间下,构建目标进一步嵌套在 imageConfig.build。
|
|
303
|
+
* manager-node 直接消费该形状,不再做字段平铺映射。
|
|
304
|
+
*/
|
|
305
|
+
export interface IDeployImageConfig {
|
|
303
306
|
/** 镜像类型;省略时有 registryId 推断为 enterprise,否则推断为 personal */
|
|
304
307
|
imageType?: 'enterprise' | 'personal';
|
|
305
|
-
/**
|
|
308
|
+
/** 已有镜像地址,image 策略必填 */
|
|
309
|
+
imageUri?: string;
|
|
310
|
+
/** 企业版 TCR 实例 ID;个人版镜像不传 */
|
|
306
311
|
registryId?: string;
|
|
312
|
+
/** 镜像构建目标,cloud/local 策略必填 */
|
|
313
|
+
build?: IImageBuildTarget;
|
|
314
|
+
/**
|
|
315
|
+
* 本地构建环境不可用时的处理方式
|
|
316
|
+
* 默认 error,避免静默改变构建环境
|
|
317
|
+
*/
|
|
318
|
+
localFallback?: 'cloud' | 'error';
|
|
319
|
+
/** 容器监听端口,仅允许 9000 */
|
|
320
|
+
imagePort?: number;
|
|
307
321
|
entryPoint?: string;
|
|
308
322
|
command?: string;
|
|
309
323
|
args?: string;
|
|
@@ -311,29 +325,59 @@ export interface IHttpImageRuntimeCommon extends IHttpFunctionDeployCommon {
|
|
|
311
325
|
argsList?: string[];
|
|
312
326
|
containerImageAccelerate?: boolean;
|
|
313
327
|
}
|
|
328
|
+
/** HTTP 自定义镜像公共配置 */
|
|
329
|
+
export interface IHttpImageRuntimeCommon extends IHttpFunctionDeployCommon {
|
|
330
|
+
runtime?: typeof CUSTOM_IMAGE_RUNTIME;
|
|
331
|
+
/** 镜像运行时配置,对齐 toolbox schema 的嵌套形状 */
|
|
332
|
+
imageConfig?: IDeployImageConfig;
|
|
333
|
+
}
|
|
314
334
|
/** 云端构建镜像部署 */
|
|
315
335
|
export interface IHttpCloudDeployConfig extends IHttpImageRuntimeCommon {
|
|
316
336
|
buildStrategy: 'cloud';
|
|
317
|
-
|
|
337
|
+
imageConfig: IDeployImageConfig;
|
|
318
338
|
}
|
|
319
339
|
/** 本地构建镜像部署 */
|
|
320
340
|
export interface IHttpLocalDeployConfig extends IHttpImageRuntimeCommon {
|
|
321
341
|
buildStrategy: 'local';
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* 本地构建环境不可用时的处理方式
|
|
325
|
-
* 默认 error,避免静默改变构建环境
|
|
326
|
-
*/
|
|
327
|
-
localFallback?: 'cloud' | 'error';
|
|
342
|
+
imageConfig: IDeployImageConfig;
|
|
328
343
|
}
|
|
329
344
|
/** 已有镜像直接部署 */
|
|
330
345
|
export interface IHttpImageDeployConfig extends IHttpImageRuntimeCommon {
|
|
331
346
|
buildStrategy: 'image';
|
|
332
|
-
imageConfig:
|
|
347
|
+
imageConfig: IDeployImageConfig;
|
|
333
348
|
}
|
|
334
349
|
export type IHttpFunctionDeployConfig = IHttpZipDeployConfig | IHttpCloudDeployConfig | IHttpLocalDeployConfig | IHttpImageDeployConfig;
|
|
335
350
|
export type IFunctionDeployConfig = IEventFunctionDeployConfig | IHttpFunctionDeployConfig;
|
|
336
|
-
|
|
351
|
+
export interface IDeployInputField {
|
|
352
|
+
/** 稳定字段名,供 CLI/MCP 决定如何收集输入 */
|
|
353
|
+
name: 'envId' | 'imageUri' | 'registryId' | 'repository' | 'namespace' | 'registryCredential.username' | 'registryCredential.password';
|
|
354
|
+
/** 配置中的字段路径 */
|
|
355
|
+
path: string;
|
|
356
|
+
/** 是否必须补齐才能继续部署 */
|
|
357
|
+
required: boolean;
|
|
358
|
+
/** 面向调用方的原因,不包含敏感信息 */
|
|
359
|
+
reason: string;
|
|
360
|
+
}
|
|
361
|
+
/** 最小化镜像配置的默认值上下文 */
|
|
362
|
+
export interface IMinimalDeployConfigContext {
|
|
363
|
+
envId?: string;
|
|
364
|
+
functionPath?: string;
|
|
365
|
+
functionRootPath?: string;
|
|
366
|
+
/** 调用方明确提供的非交互默认值;密码等敏感值只在显式传入时使用 */
|
|
367
|
+
defaults?: {
|
|
368
|
+
imageUri?: string;
|
|
369
|
+
registryId?: string;
|
|
370
|
+
repository?: string;
|
|
371
|
+
namespace?: string;
|
|
372
|
+
registryCredential?: IRegistryCredential;
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/** 最小化配置准备结果 */
|
|
376
|
+
export interface IMinimalDeployConfigResult {
|
|
377
|
+
config: IFunctionDeployConfig;
|
|
378
|
+
missing: IDeployInputField[];
|
|
379
|
+
appliedDefaults: string[];
|
|
380
|
+
}
|
|
337
381
|
export type INormalizedEventConfig = IEventFunctionDeployConfig & {
|
|
338
382
|
type: 'Event';
|
|
339
383
|
buildStrategy: 'zip';
|
|
@@ -348,22 +392,34 @@ export type INormalizedHttpCloudConfig = IHttpCloudDeployConfig & {
|
|
|
348
392
|
type: 'HTTP';
|
|
349
393
|
buildStrategy: 'cloud';
|
|
350
394
|
runtime: typeof CUSTOM_IMAGE_RUNTIME;
|
|
351
|
-
|
|
395
|
+
imageConfig: IDeployImageConfig & {
|
|
396
|
+
imagePort: number;
|
|
397
|
+
imageType: 'enterprise' | 'personal';
|
|
398
|
+
build: IImageBuildTarget;
|
|
399
|
+
};
|
|
352
400
|
};
|
|
353
401
|
/** 规范化后的 HTTP 本地构建配置 */
|
|
354
402
|
export type INormalizedHttpLocalConfig = IHttpLocalDeployConfig & {
|
|
355
403
|
type: 'HTTP';
|
|
356
404
|
buildStrategy: 'local';
|
|
357
405
|
runtime: typeof CUSTOM_IMAGE_RUNTIME;
|
|
358
|
-
|
|
359
|
-
|
|
406
|
+
imageConfig: IDeployImageConfig & {
|
|
407
|
+
imagePort: number;
|
|
408
|
+
imageType: 'enterprise' | 'personal';
|
|
409
|
+
build: IImageBuildTarget;
|
|
410
|
+
localFallback: 'cloud' | 'error';
|
|
411
|
+
};
|
|
360
412
|
};
|
|
361
413
|
/** 规范化后的 HTTP 已有镜像配置 */
|
|
362
414
|
export type INormalizedHttpImageConfig = IHttpImageDeployConfig & {
|
|
363
415
|
type: 'HTTP';
|
|
364
416
|
buildStrategy: 'image';
|
|
365
417
|
runtime: typeof CUSTOM_IMAGE_RUNTIME;
|
|
366
|
-
|
|
418
|
+
imageConfig: IDeployImageConfig & {
|
|
419
|
+
imagePort: number;
|
|
420
|
+
imageType: 'enterprise' | 'personal';
|
|
421
|
+
imageUri: string;
|
|
422
|
+
};
|
|
367
423
|
};
|
|
368
424
|
export type INormalizedDeployConfig = INormalizedEventConfig | INormalizedHttpZipConfig | INormalizedHttpCloudConfig | INormalizedHttpLocalConfig | INormalizedHttpImageConfig;
|
|
369
425
|
/** 代码类部署物 */
|
package/lib/deploy/StateStore.js
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
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
|
-
envId: state.envId,
|
|
82
|
-
resources: state.resources
|
|
83
|
-
};
|
|
84
|
-
fs_1.default.writeFileSync(filePath, JSON.stringify(full, null, 2), 'utf8');
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* 计算产物目录逐文件 sha256 指纹(仅 hash,兼容旧调用)
|
|
88
|
-
*
|
|
89
|
-
* @param projectPath 产物目录(hosting 的 outputDir / root)
|
|
90
|
-
* @param ignore 自定义忽略规则(追加到默认 node_modules / .DS_Store 之后)
|
|
91
|
-
* @param options 扫描选项(maxFiles / hashLength)
|
|
92
|
-
* @returns 相对路径(/ 分隔)→ 文件内容 sha256(默认截断 16 位)
|
|
93
|
-
*/
|
|
94
|
-
async computeHostingFiles(projectPath, ignore = [], options = {}) {
|
|
95
|
-
const fingerprints = await this.computeHostingFingerprints(projectPath, ignore, options);
|
|
96
|
-
const result = {};
|
|
97
|
-
for (const [relPath, fp] of Object.entries(fingerprints)) {
|
|
98
|
-
result[relPath] = fp.hash;
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* 计算产物目录逐文件指纹(hash + size)
|
|
104
|
-
*
|
|
105
|
-
* - 强制忽略 node_modules / .DS_Store(与传入 ignore 合并,不可关闭)
|
|
106
|
-
* - hash 默认截断为 16 位 hex(hashLength 可调,64 = 完整 sha256)
|
|
107
|
-
* - 文件数超过 maxFiles 抛 HostingFilesOverLimitError(防快照膨胀与 I/O 失控)
|
|
108
|
-
*
|
|
109
|
-
* @param projectPath 产物目录(hosting 的 outputDir / root)
|
|
110
|
-
* @param ignore 自定义忽略规则
|
|
111
|
-
* @param options 扫描选项(maxFiles / hashLength)
|
|
112
|
-
* @returns 相对路径(/ 分隔)→ { hash, size }
|
|
113
|
-
*/
|
|
114
|
-
async computeHostingFingerprints(projectPath, ignore = [], options = {}) {
|
|
115
|
-
if (!fs_1.default.existsSync(projectPath) || !fs_1.default.statSync(projectPath).isDirectory()) {
|
|
116
|
-
throw new error_1.CloudBaseError(`产物目录不存在或不是目录:${projectPath}`);
|
|
117
|
-
}
|
|
118
|
-
const { maxFiles = exports.DEFAULT_MAX_HOSTING_FILES, hashLength = exports.DEFAULT_HASH_LENGTH } = options;
|
|
119
|
-
const effectiveIgnore = this.mergeIgnore(ignore);
|
|
120
|
-
const files = {};
|
|
121
|
-
let count = 0;
|
|
122
|
-
const walk = (dir, rel) => {
|
|
123
|
-
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
124
|
-
const entryPath = path_1.default.join(dir, entry.name);
|
|
125
|
-
const relPath = rel ? path_1.default.join(rel, entry.name) : entry.name;
|
|
126
|
-
if (entry.isDirectory()) {
|
|
127
|
-
if (this.isIgnored(relPath, effectiveIgnore)) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
walk(entryPath, relPath);
|
|
131
|
-
}
|
|
132
|
-
else if (entry.isFile() && !this.isIgnored(relPath, effectiveIgnore)) {
|
|
133
|
-
count++;
|
|
134
|
-
if (maxFiles > 0 && count > maxFiles) {
|
|
135
|
-
throw new HostingFilesOverLimitError(count, maxFiles);
|
|
136
|
-
}
|
|
137
|
-
const normalized = relPath.split(path_1.default.sep).join('/');
|
|
138
|
-
files[normalized] = {
|
|
139
|
-
hash: this.hashFile(entryPath, hashLength),
|
|
140
|
-
size: fs_1.default.statSync(entryPath).size
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
walk(projectPath, '');
|
|
146
|
-
return files;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* 计算产物目录与快照的变更集(懒加载)
|
|
150
|
-
*
|
|
151
|
-
* 优化:先 stat 拿 size,size 与快照不同的文件直接判定变更(不读文件内容),
|
|
152
|
-
* 只有 size 相同(或旧快照 size 未知)的文件才读内容 hash 对比,
|
|
153
|
-
* 避免为未变文件做无谓的 I/O 与 hash。
|
|
154
|
-
* 文件数超过 maxFiles 时放弃 diff,返回 overLimit=true(调用方应降级为全量部署)。
|
|
155
|
-
*
|
|
156
|
-
* @param projectPath 产物目录(hosting 的 outputDir / root)
|
|
157
|
-
* @param previous 上次快照指纹(undefined = 首次部署,全部视为新增)
|
|
158
|
-
* @param options 选项(ignore / maxFiles / hashLength)
|
|
159
|
-
*/
|
|
160
|
-
async diffHostingFiles(projectPath, previous, options = {}) {
|
|
161
|
-
if (!fs_1.default.existsSync(projectPath) || !fs_1.default.statSync(projectPath).isDirectory()) {
|
|
162
|
-
throw new error_1.CloudBaseError(`产物目录不存在或不是目录:${projectPath}`);
|
|
163
|
-
}
|
|
164
|
-
const { ignore = [], maxFiles = exports.DEFAULT_MAX_HOSTING_FILES, hashLength = exports.DEFAULT_HASH_LENGTH } = options;
|
|
165
|
-
const effectiveIgnore = this.mergeIgnore(ignore);
|
|
166
|
-
// 归一化快照:旧格式(string)size 未知 → -1,走 hash 对比而非 size 预判
|
|
167
|
-
let prev;
|
|
168
|
-
if (previous) {
|
|
169
|
-
prev = {};
|
|
170
|
-
for (const [k, v] of Object.entries(previous)) {
|
|
171
|
-
prev[k] = this.normalizeFileValue(v);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const localFiles = new Set();
|
|
175
|
-
const added = [];
|
|
176
|
-
const modified = [];
|
|
177
|
-
let count = 0;
|
|
178
|
-
let overLimit = false;
|
|
179
|
-
const walk = (dir, rel) => {
|
|
180
|
-
if (overLimit) {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
184
|
-
const entryPath = path_1.default.join(dir, entry.name);
|
|
185
|
-
const relPath = rel ? path_1.default.join(rel, entry.name) : entry.name;
|
|
186
|
-
if (entry.isDirectory()) {
|
|
187
|
-
if (this.isIgnored(relPath, effectiveIgnore)) {
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
walk(entryPath, relPath);
|
|
191
|
-
}
|
|
192
|
-
else if (entry.isFile() && !this.isIgnored(relPath, effectiveIgnore)) {
|
|
193
|
-
count++;
|
|
194
|
-
if (maxFiles > 0 && count > maxFiles) {
|
|
195
|
-
overLimit = true;
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
const normalized = relPath.split(path_1.default.sep).join('/');
|
|
199
|
-
localFiles.add(normalized);
|
|
200
|
-
const prevEntry = prev === null || prev === void 0 ? void 0 : prev[normalized];
|
|
201
|
-
if (!prevEntry) {
|
|
202
|
-
added.push(normalized);
|
|
203
|
-
}
|
|
204
|
-
else if (prevEntry.size !== -1 && prevEntry.size !== fs_1.default.statSync(entryPath).size) {
|
|
205
|
-
// size 变化 → 内容必变,无需读文件(懒加载核心)
|
|
206
|
-
modified.push(normalized);
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
// size 相同或旧快照 size 未知 → 读内容 hash 确认
|
|
210
|
-
const hash = this.hashFile(entryPath, hashLength);
|
|
211
|
-
if (prevEntry.hash !== hash) {
|
|
212
|
-
modified.push(normalized);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
walk(projectPath, '');
|
|
219
|
-
if (overLimit) {
|
|
220
|
-
return {
|
|
221
|
-
added: [],
|
|
222
|
-
modified: [],
|
|
223
|
-
deleted: [],
|
|
224
|
-
totalChanged: 0,
|
|
225
|
-
unchanged: false,
|
|
226
|
-
overLimit: true
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
const deleted = prev
|
|
230
|
-
? Object.keys(prev).filter(k => !localFiles.has(k) && !this.isIgnored(k, effectiveIgnore))
|
|
231
|
-
: [];
|
|
232
|
-
const totalChanged = added.length + modified.length + deleted.length;
|
|
233
|
-
return { added, modified, deleted, totalChanged, unchanged: totalChanged === 0 };
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* 提取 app 配置快照(仅 diff 白名单字段)
|
|
237
|
-
* envVariables / ignore 等非 diff 字段与敏感信息不进入快照
|
|
238
|
-
*/
|
|
239
|
-
buildAppConfig(config) {
|
|
240
|
-
if (!config || typeof config !== 'object') {
|
|
241
|
-
return undefined;
|
|
242
|
-
}
|
|
243
|
-
const entry = {};
|
|
244
|
-
if (config.serviceName !== undefined) {
|
|
245
|
-
entry.serviceName = config.serviceName;
|
|
246
|
-
}
|
|
247
|
-
if (config.installCommand !== undefined) {
|
|
248
|
-
entry.installCommand = config.installCommand;
|
|
249
|
-
}
|
|
250
|
-
if (config.buildCommand !== undefined) {
|
|
251
|
-
entry.buildCommand = config.buildCommand;
|
|
252
|
-
}
|
|
253
|
-
if (config.outputDir !== undefined) {
|
|
254
|
-
entry.outputDir = config.outputDir;
|
|
255
|
-
}
|
|
256
|
-
if (config.deployPath !== undefined) {
|
|
257
|
-
entry.deployPath = config.deployPath;
|
|
258
|
-
}
|
|
259
|
-
return entry;
|
|
260
|
-
}
|
|
261
|
-
hashFile(filePath, hashLength = exports.DEFAULT_HASH_LENGTH) {
|
|
262
|
-
const content = fs_1.default.readFileSync(filePath);
|
|
263
|
-
return crypto_1.default.createHash('sha256').update(content).digest('hex').slice(0, hashLength);
|
|
264
|
-
}
|
|
265
|
-
/** 合并默认忽略规则(node_modules / .DS_Store 不可被调用方关闭) */
|
|
266
|
-
mergeIgnore(ignore) {
|
|
267
|
-
return Array.from(new Set([...DEFAULT_IGNORE, ...(ignore || [])]));
|
|
268
|
-
}
|
|
269
|
-
/** 归一化快照指纹值:旧格式(纯 string = 仅 hash,size 未知用 -1 表示) */
|
|
270
|
-
normalizeFileValue(value) {
|
|
271
|
-
if (typeof value === 'string') {
|
|
272
|
-
return { hash: value, size: -1 };
|
|
273
|
-
}
|
|
274
|
-
return value;
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* 是否命中忽略规则:
|
|
278
|
-
* - 目录模式(结尾 /):匹配该目录及其下所有
|
|
279
|
-
* - glob(含 **):转换为正则匹配相对路径
|
|
280
|
-
* - 其余:精确/前缀匹配
|
|
281
|
-
*/
|
|
282
|
-
isIgnored(relPath, ignore) {
|
|
283
|
-
const normalized = relPath.split(path_1.default.sep).join('/');
|
|
284
|
-
for (const pattern of ignore) {
|
|
285
|
-
if (this.matchPattern(normalized, pattern)) {
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
291
|
-
matchPattern(relPath, pattern) {
|
|
292
|
-
const p = pattern.trim();
|
|
293
|
-
if (!p) {
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
// 目录模式(结尾 /):匹配目录本身及下所有内容
|
|
297
|
-
if (p.endsWith('/')) {
|
|
298
|
-
const prefix = p.slice(0, -1);
|
|
299
|
-
return relPath === prefix || relPath.startsWith(prefix + '/');
|
|
300
|
-
}
|
|
301
|
-
// glob(含 **):把 ** 转成 .*,其余转义后整体匹配
|
|
302
|
-
if (p.includes('**')) {
|
|
303
|
-
const regexStr = '^' +
|
|
304
|
-
p.split('**').map(part => this.escapeRegExp(part)).join('.*') +
|
|
305
|
-
'$';
|
|
306
|
-
return new RegExp(regexStr).test(relPath);
|
|
307
|
-
}
|
|
308
|
-
// 精确匹配或前缀匹配(目录下的文件)
|
|
309
|
-
return relPath === p || relPath.startsWith(p + '/');
|
|
310
|
-
}
|
|
311
|
-
escapeRegExp(s) {
|
|
312
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
313
|
-
}
|
|
314
|
-
/** 解析当前 git commit(非 git 仓库或失败返回 undefined,不阻断) */
|
|
315
|
-
resolveGitCommit() {
|
|
316
|
-
try {
|
|
317
|
-
const out = (0, child_process_1.execSync)('git rev-parse HEAD', {
|
|
318
|
-
cwd: this.cwd,
|
|
319
|
-
stdio: 'pipe'
|
|
320
|
-
});
|
|
321
|
-
return out.toString().trim() || undefined;
|
|
322
|
-
}
|
|
323
|
-
catch (_a) {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
exports.StateStore = StateStore;
|