@lark-apaas/miaoda-cli 0.1.18 → 0.1.19-alpha.4bf5458

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 (33) hide show
  1. package/dist/api/deploy/index.js +3 -1
  2. package/dist/api/deploy/modern-types.js +4 -1
  3. package/dist/api/deploy/modern.js +8 -0
  4. package/dist/cli/commands/app/index.js +5 -3
  5. package/dist/cli/commands/deploy/modern.js +30 -0
  6. package/dist/cli/commands/index.js +10 -0
  7. package/dist/cli/handlers/app/init.js +5 -4
  8. package/dist/cli/handlers/app/migrate.js +4 -5
  9. package/dist/cli/handlers/deploy/index.js +3 -1
  10. package/dist/cli/handlers/deploy/patch.js +16 -0
  11. package/dist/services/app/init/async-install.js +3 -7
  12. package/dist/services/app/init/index.js +1 -2
  13. package/dist/services/app/init/template.js +1 -0
  14. package/dist/services/deploy/modern/atoms/design-build.js +41 -0
  15. package/dist/services/deploy/modern/atoms/design-upload.js +74 -0
  16. package/dist/services/deploy/modern/atoms/index.js +5 -1
  17. package/dist/services/deploy/modern/atoms/tosutil.js +246 -0
  18. package/dist/services/deploy/modern/atoms/upload.js +4 -127
  19. package/dist/services/deploy/modern/check.js +28 -16
  20. package/dist/services/deploy/modern/patch/actions.js +60 -0
  21. package/dist/services/deploy/modern/patch/content.js +18 -0
  22. package/dist/services/deploy/modern/patch/index.js +41 -0
  23. package/dist/services/deploy/modern/patch/routes.js +28 -0
  24. package/dist/services/deploy/modern/patch/source-scan.js +56 -0
  25. package/dist/services/deploy/modern/pipelines/design-local.js +47 -0
  26. package/dist/services/deploy/modern/pipelines/index.js +3 -1
  27. package/dist/services/deploy/modern/protocol.js +7 -0
  28. package/dist/services/deploy/modern/run.js +10 -4
  29. package/dist/services/deploy/modern/template-key-map.js +4 -0
  30. package/dist/utils/coding-steering.js +6 -5
  31. package/dist/utils/env.js +19 -0
  32. package/dist/utils/index.js +3 -1
  33. package/package.json +1 -1
@@ -0,0 +1,246 @@
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.resolveTosutilPath = resolveTosutilPath;
7
+ exports.tosutilUploadFromTos = tosutilUploadFromTos;
8
+ exports.writeTosutilConfig = writeTosutilConfig;
9
+ exports.streamTosutilOutput = streamTosutilOutput;
10
+ exports.normalizeTosDest = normalizeTosDest;
11
+ exports.uploadDirWithCredential = uploadDirWithCredential;
12
+ exports.copyTosToTos = copyTosToTos;
13
+ exports.listRemoteKeys = listRemoteKeys;
14
+ exports.removeRemoteKeys = removeRemoteKeys;
15
+ const node_fs_1 = __importDefault(require("node:fs"));
16
+ const node_os_1 = __importDefault(require("node:os"));
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const node_child_process_1 = require("node:child_process");
19
+ const error_1 = require("../../../../utils/error");
20
+ const logger_1 = require("../../../../utils/logger");
21
+ function resolveTosutilPath() {
22
+ try {
23
+ const resolved = (0, node_child_process_1.execFileSync)('which', ['tosutil'], { encoding: 'utf-8' }).trim();
24
+ if (resolved)
25
+ return resolved;
26
+ }
27
+ catch {
28
+ /* fallthrough */
29
+ }
30
+ throw new error_1.AppError('DEPLOY_TOSUTIL_MISSING', 'tosutil is not installed or not in PATH. modern deploy requires sandbox preinstalled tosutil.');
31
+ }
32
+ function tosutilUploadFromTos(cred) {
33
+ return {
34
+ accessKeyID: cred.accessKeyID,
35
+ secretAccessKey: cred.secretAccessKey,
36
+ sessionToken: cred.sessionToken,
37
+ endpoint: cred.endpoint,
38
+ region: cred.region,
39
+ bucket: cred.bucket,
40
+ prefix: cred.prefix,
41
+ };
42
+ }
43
+ /**
44
+ * 写一个 tosutil 临时 config 文件,注入 STS 凭证 + endpoint + region。
45
+ * 返回 config 路径,调用方负责清理。
46
+ */
47
+ function writeTosutilConfig(tosutilPath, cred, label) {
48
+ const confPath = node_path_1.default.join(node_os_1.default.tmpdir(), `.tosutilconfig-miaoda-${label}-${String(process.pid)}-${String(Date.now())}`);
49
+ node_fs_1.default.writeFileSync(confPath, '');
50
+ (0, node_child_process_1.execFileSync)(tosutilPath, [
51
+ 'config',
52
+ '-conf',
53
+ confPath,
54
+ '-e',
55
+ cred.endpoint,
56
+ '-i',
57
+ cred.accessKeyID,
58
+ '-k',
59
+ cred.secretAccessKey,
60
+ '-t',
61
+ cred.sessionToken,
62
+ '-re',
63
+ cred.region,
64
+ ], { stdio: 'pipe' });
65
+ return confPath;
66
+ }
67
+ /** 把 tosutil 进程的输出按行回放到 stderr,便于线上排查(沙箱里无法 attach 进程)。 */
68
+ function streamTosutilOutput(output) {
69
+ if (!output)
70
+ return;
71
+ for (const line of output.split('\n')) {
72
+ if (line.length === 0)
73
+ continue;
74
+ process.stderr.write(`[tosutil] ${line}\n`);
75
+ }
76
+ }
77
+ function normalizeTosDest(bucket, prefix) {
78
+ const bucketPart = bucket.startsWith('tos://') ? bucket : `tos://${bucket}`;
79
+ const normalized = prefix.replace(/^\/+/, '');
80
+ return `${bucketPart.replace(/\/+$/, '')}/${normalized}`;
81
+ }
82
+ /**
83
+ * 用 tosutil 把目录批量上传到 tos://bucket/prefix(STS 凭证从 config 文件读取)。
84
+ */
85
+ function uploadDirWithCredential(tosutilPath, sourceDir, cred, label,
86
+ /**
87
+ * 传 true 时加 tosutil `-ddo`:上传时不把文件夹本身作为单独对象上传,
88
+ * 避免在 TOS 留下 `src/` 这类零字节目录占位对象(删空目录下最后一个文件后“文件夹”仍残留)。
89
+ * 仅 design 链路传 true;client/nrf 上传保持原行为(不传)。
90
+ */
91
+ noDirObjects = false) {
92
+ const entries = node_fs_1.default.readdirSync(sourceDir);
93
+ if (entries.length === 0) {
94
+ (0, logger_1.debug)(`upload: skip empty dir ${sourceDir}`);
95
+ return;
96
+ }
97
+ const dest = normalizeTosDest(cred.bucket, cred.prefix);
98
+ (0, logger_1.debug)(`upload: ${sourceDir} -> ${dest} (${String(entries.length)} entries)`);
99
+ const confPath = writeTosutilConfig(tosutilPath, cred, label);
100
+ let output;
101
+ try {
102
+ output = (0, node_child_process_1.execFileSync)(tosutilPath, [
103
+ 'cp',
104
+ sourceDir,
105
+ dest,
106
+ '-r',
107
+ '-flat',
108
+ '-j',
109
+ '5',
110
+ '-p',
111
+ '3',
112
+ '-ps',
113
+ '10485760',
114
+ '-f',
115
+ ...(noDirObjects ? ['-ddo'] : []),
116
+ '-conf',
117
+ confPath,
118
+ ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
119
+ streamTosutilOutput(output);
120
+ }
121
+ catch (err) {
122
+ const e = err;
123
+ const stderr = (e.stderr ?? '').trim();
124
+ const stdout = (e.stdout ?? '').trim();
125
+ streamTosutilOutput(stdout);
126
+ streamTosutilOutput(stderr);
127
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil cp exited with status ${String(e.status ?? -1)} for ${sourceDir}\n` +
128
+ ` stderr: ${stderr || '(empty)'}\n` +
129
+ ` stdout: ${stdout.slice(0, 500) || '(empty)'}`);
130
+ }
131
+ finally {
132
+ try {
133
+ node_fs_1.default.unlinkSync(confPath);
134
+ }
135
+ catch {
136
+ /* ignore */
137
+ }
138
+ }
139
+ const succeedMatch = /Succeed count is:\s*(\d+)/.exec(output);
140
+ const failedMatch = /Failed count is:\s*(\d+)/.exec(output);
141
+ const succeedCount = succeedMatch ? Number.parseInt(succeedMatch[1], 10) : 0;
142
+ const failedCount = failedMatch ? Number.parseInt(failedMatch[1], 10) : 0;
143
+ if (failedCount > 0) {
144
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `Upload failed: ${String(failedCount)} files failed, ${String(succeedCount)} succeeded. source=${sourceDir}`);
145
+ }
146
+ if (succeedCount === 0) {
147
+ throw new error_1.AppError('DEPLOY_UPLOAD_EMPTY', `Upload failed: 0 files uploaded. source=${sourceDir}. Check tosutil credentials and bucket permissions.`);
148
+ }
149
+ }
150
+ /** version→latest 同桶同凭证服务端复制:tosutil cp tos://src tos://dst -r -flat -f */
151
+ function copyTosToTos(tosutilPath, cred, srcPrefix, dstPrefix) {
152
+ const src = normalizeTosDest(cred.bucket, srcPrefix);
153
+ const dst = normalizeTosDest(cred.bucket, dstPrefix);
154
+ const confPath = writeTosutilConfig(tosutilPath, cred, 'tos2tos');
155
+ try {
156
+ const out = (0, node_child_process_1.execFileSync)(tosutilPath, ['cp', src, dst, '-r', '-flat', '-f', '-j', '5', '-conf', confPath], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
157
+ streamTosutilOutput(out);
158
+ }
159
+ catch (err) {
160
+ const e = err;
161
+ streamTosutilOutput((e.stdout ?? '').trim());
162
+ streamTosutilOutput((e.stderr ?? '').trim());
163
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil cp(tos→tos) exited status ${String(e.status ?? -1)}: ${src} → ${dst}`);
164
+ }
165
+ finally {
166
+ try {
167
+ node_fs_1.default.unlinkSync(confPath);
168
+ }
169
+ catch {
170
+ /* ignore */
171
+ }
172
+ }
173
+ }
174
+ /** 列 tos://bucket/prefix 下所有对象,返回相对 prefix 的 key 集合(用 ls -s 精简格式,ls 默认递归)。 */
175
+ function listRemoteKeys(tosutilPath, cred) {
176
+ const base = normalizeTosDest(cred.bucket, cred.prefix); // tos://bucket/prefix
177
+ const confPath = writeTosutilConfig(tosutilPath, cred, 'ls');
178
+ let out;
179
+ try {
180
+ // 注意:tosutil ls 没有 -r(默认就递归,-d 才是只列一层),别套 cp/rm 的 -r。
181
+ out = (0, node_child_process_1.execFileSync)(tosutilPath, ['ls', base, '-s', '-conf', confPath], {
182
+ encoding: 'utf-8',
183
+ stdio: ['pipe', 'pipe', 'pipe'],
184
+ });
185
+ }
186
+ catch (err) {
187
+ const e = err;
188
+ // tosutil 把参数类错误打到 stdout,故 stderr/stdout 都带上,避免错误信息为空难排查。
189
+ const detail = [(e.stderr ?? '').trim(), (e.stdout ?? '').trim()].filter(Boolean).join(' | ');
190
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil ls exited status ${String(e.status ?? -1)} for ${base}: ${detail || '(no output)'}`);
191
+ }
192
+ finally {
193
+ try {
194
+ node_fs_1.default.unlinkSync(confPath);
195
+ }
196
+ catch {
197
+ /* ignore */
198
+ }
199
+ }
200
+ const prefixUri = base.replace(/\/+$/, '') + '/';
201
+ const keys = [];
202
+ for (const line of out.split('\n')) {
203
+ const t = line.trim();
204
+ if (!t.startsWith(prefixUri))
205
+ continue;
206
+ const rel = t.slice(prefixUri.length);
207
+ // 含以 / 结尾的目录占位对象(如 `src/`):本地文件集永远不含此类 key,
208
+ // 故它们必落入镜像 prune 的“stale”集合被删除——清掉历史 -ddo 之前留下的空目录残留。
209
+ if (rel.length > 0)
210
+ keys.push(rel);
211
+ }
212
+ return keys;
213
+ }
214
+ /** 删除 tos://bucket/prefix 下指定相对 key(逐个 rm -f)。 */
215
+ function removeRemoteKeys(tosutilPath, cred, relKeys) {
216
+ if (relKeys.length === 0)
217
+ return;
218
+ const confPath = writeTosutilConfig(tosutilPath, cred, 'rm');
219
+ const prefix = cred.prefix.replace(/\/+$/, '');
220
+ const failed = [];
221
+ try {
222
+ for (const rel of relKeys) {
223
+ // 用与 cp/ls 同一份 normalizeTosDest 拼目标 URI,避免删除路径与其它操作漂移。
224
+ const dest = normalizeTosDest(cred.bucket, `${prefix}/${rel}`);
225
+ try {
226
+ (0, node_child_process_1.execFileSync)(tosutilPath, ['rm', dest, '-f', '-conf', confPath], {
227
+ stdio: ['pipe', 'pipe', 'pipe'],
228
+ });
229
+ }
230
+ catch {
231
+ failed.push(rel);
232
+ }
233
+ }
234
+ }
235
+ finally {
236
+ try {
237
+ node_fs_1.default.unlinkSync(confPath);
238
+ }
239
+ catch {
240
+ /* ignore */
241
+ }
242
+ }
243
+ if (failed.length > 0) {
244
+ throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `prune latest failed for: ${failed.join(', ')}`);
245
+ }
246
+ }
@@ -5,14 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.uploadArtifacts = uploadArtifacts;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
- const node_os_1 = __importDefault(require("node:os"));
9
8
  const node_path_1 = __importDefault(require("node:path"));
10
- const node_child_process_1 = require("node:child_process");
11
9
  const index_1 = require("../../../../api/deploy/index");
12
10
  const error_1 = require("../../../../utils/error");
13
11
  const logger_1 = require("../../../../utils/logger");
14
12
  const constants_1 = require("../constants");
15
13
  const protocol_1 = require("../protocol");
14
+ const tosutil_1 = require("./tosutil");
16
15
  const UPLOAD_PLAN = [
17
16
  {
18
17
  kind: 'tos',
@@ -32,28 +31,6 @@ const UPLOAD_PLAN = [
32
31
  dataKey: protocol_1.DataKey.OUTPUT_STATIC_PAAS_STORAGE_CREDENTIAL,
33
32
  },
34
33
  ];
35
- function resolveTosutilPath() {
36
- try {
37
- const resolved = (0, node_child_process_1.execFileSync)('which', ['tosutil'], { encoding: 'utf-8' }).trim();
38
- if (resolved)
39
- return resolved;
40
- }
41
- catch {
42
- /* fallthrough */
43
- }
44
- throw new error_1.AppError('DEPLOY_TOSUTIL_MISSING', 'tosutil is not installed or not in PATH. modern deploy requires sandbox preinstalled tosutil.');
45
- }
46
- function tosutilUploadFromTos(cred) {
47
- return {
48
- accessKeyID: cred.accessKeyID,
49
- secretAccessKey: cred.secretAccessKey,
50
- sessionToken: cred.sessionToken,
51
- endpoint: cred.endpoint,
52
- region: cred.region,
53
- bucket: cred.bucket,
54
- prefix: cred.prefix,
55
- };
56
- }
57
34
  function tosutilUploadFromPaasStorage(cred, dataKey) {
58
35
  const { bucket, prefix } = (0, protocol_1.parseTosUploadPrefix)(cred.uploadPrefix, dataKey);
59
36
  return {
@@ -66,106 +43,6 @@ function tosutilUploadFromPaasStorage(cred, dataKey) {
66
43
  prefix,
67
44
  };
68
45
  }
69
- /**
70
- * 写一个 tosutil 临时 config 文件,注入 STS 凭证 + endpoint + region。
71
- * 返回 config 路径,调用方负责清理。
72
- */
73
- function writeTosutilConfig(tosutilPath, cred, label) {
74
- const confPath = node_path_1.default.join(node_os_1.default.tmpdir(), `.tosutilconfig-miaoda-${label}-${String(process.pid)}-${String(Date.now())}`);
75
- node_fs_1.default.writeFileSync(confPath, '');
76
- (0, node_child_process_1.execFileSync)(tosutilPath, [
77
- 'config',
78
- '-conf',
79
- confPath,
80
- '-e',
81
- cred.endpoint,
82
- '-i',
83
- cred.accessKeyID,
84
- '-k',
85
- cred.secretAccessKey,
86
- '-t',
87
- cred.sessionToken,
88
- '-re',
89
- cred.region,
90
- ], { stdio: 'pipe' });
91
- return confPath;
92
- }
93
- /** 把 tosutil 进程的输出按行回放到 stderr,便于线上排查(沙箱里无法 attach 进程)。 */
94
- function streamTosutilOutput(output) {
95
- if (!output)
96
- return;
97
- for (const line of output.split('\n')) {
98
- if (line.length === 0)
99
- continue;
100
- process.stderr.write(`[tosutil] ${line}\n`);
101
- }
102
- }
103
- function normalizeTosDest(bucket, prefix) {
104
- const bucketPart = bucket.startsWith('tos://') ? bucket : `tos://${bucket}`;
105
- const normalized = prefix.replace(/^\/+/, '');
106
- return `${bucketPart.replace(/\/+$/, '')}/${normalized}`;
107
- }
108
- /**
109
- * 用 tosutil 把目录批量上传到 tos://bucket/prefix(STS 凭证从 config 文件读取)。
110
- */
111
- function uploadDirWithCredential(tosutilPath, sourceDir, cred, label) {
112
- const entries = node_fs_1.default.readdirSync(sourceDir);
113
- if (entries.length === 0) {
114
- (0, logger_1.debug)(`upload: skip empty dir ${sourceDir}`);
115
- return;
116
- }
117
- const dest = normalizeTosDest(cred.bucket, cred.prefix);
118
- (0, logger_1.debug)(`upload: ${sourceDir} -> ${dest} (${String(entries.length)} entries)`);
119
- const confPath = writeTosutilConfig(tosutilPath, cred, label);
120
- let output;
121
- try {
122
- output = (0, node_child_process_1.execFileSync)(tosutilPath, [
123
- 'cp',
124
- sourceDir,
125
- dest,
126
- '-r',
127
- '-flat',
128
- '-j',
129
- '5',
130
- '-p',
131
- '3',
132
- '-ps',
133
- '10485760',
134
- '-f',
135
- '-conf',
136
- confPath,
137
- ], { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
138
- streamTosutilOutput(output);
139
- }
140
- catch (err) {
141
- const e = err;
142
- const stderr = (e.stderr ?? '').trim();
143
- const stdout = (e.stdout ?? '').trim();
144
- streamTosutilOutput(stdout);
145
- streamTosutilOutput(stderr);
146
- throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `tosutil cp exited with status ${String(e.status ?? -1)} for ${sourceDir}\n` +
147
- ` stderr: ${stderr || '(empty)'}\n` +
148
- ` stdout: ${stdout.slice(0, 500) || '(empty)'}`);
149
- }
150
- finally {
151
- try {
152
- node_fs_1.default.unlinkSync(confPath);
153
- }
154
- catch {
155
- /* ignore */
156
- }
157
- }
158
- const succeedMatch = /Succeed count is:\s*(\d+)/.exec(output);
159
- const failedMatch = /Failed count is:\s*(\d+)/.exec(output);
160
- const succeedCount = succeedMatch ? Number.parseInt(succeedMatch[1], 10) : 0;
161
- const failedCount = failedMatch ? Number.parseInt(failedMatch[1], 10) : 0;
162
- if (failedCount > 0) {
163
- throw new error_1.AppError('DEPLOY_UPLOAD_FAILED', `Upload failed: ${String(failedCount)} files failed, ${String(succeedCount)} succeeded. source=${sourceDir}`);
164
- }
165
- if (succeedCount === 0) {
166
- throw new error_1.AppError('DEPLOY_UPLOAD_EMPTY', `Upload failed: 0 files uploaded. source=${sourceDir}. Check tosutil credentials and bucket permissions.`);
167
- }
168
- }
169
46
  function uploadTosTarget(tosutilPath, target, localPath, data) {
170
47
  const credRaw = target.required
171
48
  ? (0, protocol_1.requireDataKey)(data, target.dataKey)
@@ -176,7 +53,7 @@ function uploadTosTarget(tosutilPath, target, localPath, data) {
176
53
  }
177
54
  const cred = (0, protocol_1.parseTosUploadCredential)(credRaw, target.dataKey);
178
55
  (0, logger_1.log)('deploy', `Uploading ${target.dir}...`);
179
- uploadDirWithCredential(tosutilPath, localPath, tosutilUploadFromTos(cred), target.dir);
56
+ (0, tosutil_1.uploadDirWithCredential)(tosutilPath, localPath, (0, tosutil_1.tosutilUploadFromTos)(cred), target.dir);
180
57
  return true;
181
58
  }
182
59
  function uploadPaasStorageTarget(tosutilPath, target, localPath, data) {
@@ -188,7 +65,7 @@ function uploadPaasStorageTarget(tosutilPath, target, localPath, data) {
188
65
  }
189
66
  const cred = (0, protocol_1.parsePaasStorageCredential)((0, protocol_1.requireDataKey)(data, target.dataKey), target.dataKey);
190
67
  (0, logger_1.log)('deploy', `Uploading ${target.dir}...`);
191
- uploadDirWithCredential(tosutilPath, localPath, tosutilUploadFromPaasStorage(cred, target.dataKey), target.dir);
68
+ (0, tosutil_1.uploadDirWithCredential)(tosutilPath, localPath, tosutilUploadFromPaasStorage(cred, target.dataKey), target.dir);
192
69
  return cred;
193
70
  }
194
71
  /**
@@ -202,7 +79,7 @@ function uploadPaasStorageTarget(tosutilPath, target, localPath, data) {
202
79
  * 目录不存在的可选项跳过;output 不存在或凭证缺失抛错。
203
80
  */
204
81
  async function uploadArtifacts(opts) {
205
- const tosutilPath = resolveTosutilPath();
82
+ const tosutilPath = (0, tosutil_1.resolveTosutilPath)();
206
83
  const distDir = node_path_1.default.join(opts.projectDir, constants_1.DIST_DIR);
207
84
  const outcome = { uploaded: 0 };
208
85
  for (const target of UPLOAD_PLAN) {
@@ -7,16 +7,20 @@ exports.runDeployChecks = runDeployChecks;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const error_1 = require("../../../utils/error");
10
+ const template_key_map_1 = require("./template-key-map");
10
11
  /**
11
12
  * 项目级 deploy 前置检查:node_modules / .spark/meta.json / package.json.scripts.build。
12
13
  * 任一 error 级别失败抛 AppError,附错误码 DEPLOY_PROJECT_CHECK_FAILED。
14
+ *
15
+ * design buildless 链路(design_local_deploy,如 design-html)的 build 直接拷源码 + 生成
16
+ * routes.json(见 atoms/design-build.ts),不跑 `npm run build`、不读 package.json、不依赖
17
+ * node_modules,因此对其跳过 node_modules / package.json / build 脚本三项检查,只保留
18
+ * meta.json + stack 校验。
13
19
  */
14
20
  function runDeployChecks(projectDir) {
15
21
  const issues = [];
16
- const nodeModules = node_path_1.default.join(projectDir, 'node_modules');
17
- if (!node_fs_1.default.existsSync(nodeModules)) {
18
- issues.push('node_modules not found. Run `npm install` (or equivalent) first.');
19
- }
22
+ // 先读 stack 判断是否 design buildless 链路,决定后续是否豁免 node_modules / build 检查。
23
+ let stack;
20
24
  const metaPath = node_path_1.default.join(projectDir, '.spark', 'meta.json');
21
25
  if (!node_fs_1.default.existsSync(metaPath)) {
22
26
  issues.push('.spark/meta.json not found. Run `miaoda app init` first.');
@@ -24,6 +28,7 @@ function runDeployChecks(projectDir) {
24
28
  else {
25
29
  try {
26
30
  const meta = JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf-8'));
31
+ stack = meta.stack;
27
32
  if (!meta.stack) {
28
33
  issues.push('.spark/meta.json missing fields: stack');
29
34
  }
@@ -32,19 +37,26 @@ function runDeployChecks(projectDir) {
32
37
  issues.push('.spark/meta.json is not valid JSON.');
33
38
  }
34
39
  }
35
- const pkgPath = node_path_1.default.join(projectDir, 'package.json');
36
- if (!node_fs_1.default.existsSync(pkgPath)) {
37
- issues.push('package.json not found.');
38
- }
39
- else {
40
- try {
41
- const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
42
- if (!pkg.scripts?.build) {
43
- issues.push('package.json missing "build" script.');
44
- }
40
+ const isDesign = stack !== undefined && (0, template_key_map_1.resolveTemplateKey)(stack) === template_key_map_1.DESIGN_LOCAL_DEPLOY;
41
+ if (!isDesign) {
42
+ const nodeModules = node_path_1.default.join(projectDir, 'node_modules');
43
+ if (!node_fs_1.default.existsSync(nodeModules)) {
44
+ issues.push('node_modules not found. Run `npm install` (or equivalent) first.');
45
45
  }
46
- catch {
47
- issues.push('package.json is not valid JSON.');
46
+ const pkgPath = node_path_1.default.join(projectDir, 'package.json');
47
+ if (!node_fs_1.default.existsSync(pkgPath)) {
48
+ issues.push('package.json not found.');
49
+ }
50
+ else {
51
+ try {
52
+ const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
53
+ if (!pkg.scripts?.build) {
54
+ issues.push('package.json missing "build" script.');
55
+ }
56
+ }
57
+ catch {
58
+ issues.push('package.json is not valid JSON.');
59
+ }
48
60
  }
49
61
  }
50
62
  if (issues.length > 0) {
@@ -0,0 +1,60 @@
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.buildTosActions = buildTosActions;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const error_1 = require("../../../../utils/error");
10
+ const index_1 = require("../../../../api/deploy/index");
11
+ const content_1 = require("./content");
12
+ const routes_1 = require("./routes");
13
+ /** 校验相对路径:禁绝对路径、禁含 ".." 段;normalize 成 posix 相对。 */
14
+ function normalizeRel(rel) {
15
+ if (node_path_1.default.isAbsolute(rel) || rel.startsWith('/')) {
16
+ throw new error_1.AppError('ARGS_INVALID', `文件路径不能是绝对路径:${rel}`);
17
+ }
18
+ const posix = rel.split(node_path_1.default.sep).join('/').replace(/^\.\//, '');
19
+ if (posix.split('/').includes('..')) {
20
+ throw new error_1.AppError('ARGS_INVALID', `文件路径不能包含 "..":${rel}`);
21
+ }
22
+ return posix;
23
+ }
24
+ function readAction(projectDir, rel, actionType) {
25
+ const filePath = normalizeRel(rel);
26
+ const abs = node_path_1.default.join(projectDir, filePath);
27
+ if (!node_fs_1.default.existsSync(abs) || !node_fs_1.default.statSync(abs).isFile()) {
28
+ throw new error_1.AppError('ARGS_INVALID', `文件不存在:${filePath}`);
29
+ }
30
+ const { content, base64Content } = (0, content_1.encodeContent)(node_fs_1.default.readFileSync(abs));
31
+ return { actionType, filePath, content, base64Content };
32
+ }
33
+ function isHtml(rel) {
34
+ return rel.toLowerCase().endsWith('.html');
35
+ }
36
+ /**
37
+ * 把 changeset 组成 TosFileAction[]:
38
+ * - create/update 读本地文件、自动编码;delete 仅 FilePath。
39
+ * - 任一改动是 .html → 追加 routes.json(TS 重算)的 UPDATE action。
40
+ */
41
+ function buildTosActions(projectDir, cs) {
42
+ const actions = [];
43
+ for (const rel of cs.creates)
44
+ actions.push(readAction(projectDir, rel, index_1.TosActionType.CREATE));
45
+ for (const rel of cs.updates)
46
+ actions.push(readAction(projectDir, rel, index_1.TosActionType.UPDATE));
47
+ for (const rel of cs.deletes) {
48
+ actions.push({ actionType: index_1.TosActionType.DELETE, filePath: normalizeRel(rel) });
49
+ }
50
+ const touchesHtml = [...cs.creates, ...cs.updates, ...cs.deletes].some(isHtml);
51
+ if (touchesHtml) {
52
+ actions.push({
53
+ actionType: index_1.TosActionType.UPDATE,
54
+ filePath: 'routes.json',
55
+ content: (0, routes_1.generateRoutes)(projectDir),
56
+ base64Content: false,
57
+ });
58
+ }
59
+ return actions;
60
+ }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodeContent = encodeContent;
4
+ /**
5
+ * 自动判别文件内容编码:
6
+ * - 含 0x00 字节 → 二进制 → base64
7
+ * - 否则 round-trip 校验 UTF-8:能无损 decode→encode → 文本直传;否则 base64
8
+ */
9
+ function encodeContent(buf) {
10
+ if (buf.includes(0x00)) {
11
+ return { content: buf.toString('base64'), base64Content: true };
12
+ }
13
+ const text = buf.toString('utf8');
14
+ if (Buffer.from(text, 'utf8').equals(buf)) {
15
+ return { content: text, base64Content: false };
16
+ }
17
+ return { content: buf.toString('base64'), base64Content: true };
18
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTosActions = void 0;
4
+ exports.patchDesignDeploy = patchDesignDeploy;
5
+ const spark_meta_1 = require("../../../../utils/spark-meta");
6
+ const error_1 = require("../../../../utils/error");
7
+ const logger_1 = require("../../../../utils/logger");
8
+ const index_1 = require("../../../../api/deploy/index");
9
+ const template_key_map_1 = require("../template-key-map");
10
+ const actions_1 = require("./actions");
11
+ var actions_2 = require("./actions");
12
+ Object.defineProperty(exports, "buildTosActions", { enumerable: true, get: function () { return actions_2.buildTosActions; } });
13
+ /**
14
+ * design-html 增量发布:读本地文件组 TosFileAction(含 .html 时带 routes.json),
15
+ * 调后端 applyTosDiff 应用到 latest。scope 限 design_local_deploy。
16
+ */
17
+ async function patchDesignDeploy(opts) {
18
+ if (opts.creates.length + opts.updates.length + opts.deletes.length === 0) {
19
+ throw new error_1.AppError('ARGS_INVALID', '至少指定一个 --create / --update / --delete');
20
+ }
21
+ const stack = (0, spark_meta_1.readSparkMeta)(opts.projectDir).stack ?? '';
22
+ const templateKey = (0, template_key_map_1.resolveTemplateKey)(stack);
23
+ if (templateKey !== template_key_map_1.DESIGN_LOCAL_DEPLOY) {
24
+ throw new error_1.AppError('ARGS_INVALID', `deploy patch 仅支持 design-html(design_local_deploy),当前 stack=${stack || '(空)'}`);
25
+ }
26
+ const actions = (0, actions_1.buildTosActions)(opts.projectDir, {
27
+ creates: opts.creates,
28
+ updates: opts.updates,
29
+ deletes: opts.deletes,
30
+ });
31
+ const routesRegenerated = actions.some((a) => a.filePath === 'routes.json');
32
+ (0, logger_1.log)('deploy', `Applying ${String(actions.length)} file action(s)...`);
33
+ const res = await (0, index_1.applyTosDiff)({ appID: opts.appId, templateKey, actions });
34
+ (0, logger_1.log)('deploy', `Patched: upsert=${String(res.upsertCount)} delete=${String(res.deleteCount)}`);
35
+ return {
36
+ upsertCount: res.upsertCount,
37
+ deleteCount: res.deleteCount,
38
+ actionsSent: actions.length,
39
+ routesRegenerated,
40
+ };
41
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateRoutes = generateRoutes;
4
+ const source_scan_1 = require("./source-scan");
5
+ /** 与 build.sh 一致的 .html → route path 推导。rel 为 posix 相对路径。 */
6
+ function toRoute(rel, base) {
7
+ let r = '/' + rel;
8
+ r = r.replace(/\.html$/i, '');
9
+ r = r.replace(/(^|\/)index$/, '$1');
10
+ if (r.length > 1)
11
+ r = r.replace(/\/$/, '');
12
+ const full = base + r;
13
+ return full === '' ? '/' : full;
14
+ }
15
+ /**
16
+ * 扫描 projectDir 下所有 .html(套 source-scan 的 EXCLUDES),生成与 build.sh 同款 routes.json
17
+ * 文本(含尾换行)。CLIENT_BASE_PATH 取自 process.env(平台预置),去尾斜杠后作前缀。
18
+ */
19
+ function generateRoutes(projectDir) {
20
+ const base = (process.env.CLIENT_BASE_PATH ?? '').replace(/\/+$/, '');
21
+ const htmls = (0, source_scan_1.listSourceFiles)(projectDir).filter((f) => f.toLowerCase().endsWith('.html'));
22
+ const routes = [...new Set(htmls.map((rel) => toRoute(rel, base)))]
23
+ .sort()
24
+ .map((p) => ({ path: p }));
25
+ if (routes.length === 0)
26
+ routes.push({ path: base ? `${base}/` : '/' });
27
+ return JSON.stringify(routes, null, 2) + '\n';
28
+ }