@lark-apaas/miaoda-cli 0.1.46 → 0.1.48
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/README.md +20 -0
- package/dist/cli/commands/app/index.js +87 -15
- package/dist/cli/handlers/app/init.js +74 -2
- package/dist/cli/handlers/app/migrate.js +131 -212
- package/dist/cli/handlers/app/sync.js +41 -4
- package/dist/config/fullstack-managed-files.js +67 -0
- package/dist/config/migrate-configs/html-to-design-html.js +1 -1
- package/dist/config/migrate-configs/index.js +9 -0
- package/dist/config/migrate-configs/vite-react-to-nestjs-react-fullstack.js +227 -197
- package/dist/config/sync-configs/design-stack.js +3 -4
- package/dist/config/sync-configs/nestjs-react-fullstack.js +11 -23
- package/dist/services/app/init/async-install.js +21 -0
- package/dist/services/app/init/index.js +3 -1
- package/dist/services/app/init/workspace-check.js +105 -0
- package/dist/services/app/migrate/detect.js +34 -0
- package/dist/services/app/migrate/index.js +8 -0
- package/dist/services/app/migrate/restart.js +59 -0
- package/dist/services/app/migrate/run.js +565 -0
- package/dist/services/app/migrate/symlinks.js +146 -0
- package/dist/utils/engineering-state.js +52 -0
- package/dist/utils/migrate-adapters.js +634 -0
- package/dist/utils/migrate-rule.js +801 -36
- package/dist/utils/project-snapshot.js +99 -0
- package/dist/utils/spark-meta.js +31 -10
- package/dist/utils/sync-rule.js +80 -11
- package/package.json +1 -1
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 工程转换原语:在原工程目录内把工程转换(或修复)到目标 stack,并返回可验证的结构化结果。
|
|
4
|
+
*
|
|
5
|
+
* 流程(任一阶段失败即停止,保留当前目录现场,不自动回滚):
|
|
6
|
+
* preflight 严格读 meta、核对 app_id(沙箱内不一致则在写入 meta 时改写)、按磁盘布局识别源 stack、
|
|
7
|
+
* 选迁移 / 修复配置、检查符号链接
|
|
8
|
+
* render 把目标模板渲染到临时模板目录(不复制用户工程;固定版本号进入结果)
|
|
9
|
+
* precheck 在原目录只读预检规则冲突;有冲突 → 返回具体路径 / ID,工程未写入
|
|
10
|
+
* apply 在原目录执行规则(遇冲突即停)
|
|
11
|
+
* install 在原目录安装依赖(失败 = 转换失败,不软成功;manual 调用的退出码由 handler 放宽)
|
|
12
|
+
* verify 按迁移配置在原目录执行验证命令;未配置则跳过(verify.status = not_configured)
|
|
13
|
+
* restart 仅沙箱:清 vite cache、向在本目录运行的 dev 进程发重启信号(只是信号,不是健康结果)
|
|
14
|
+
*
|
|
15
|
+
* 冲突策略(conflictPolicy)按调用方区分,见 resolveConflictPolicy。precheck / apply 只对该策略下
|
|
16
|
+
* 仍是冲突的规则停止;按策略覆盖的用户内容列在 overwritten,保留未适配的进 warnings。
|
|
17
|
+
*
|
|
18
|
+
* apply 之后的任何失败都会留下部分转换的工程(originalIntact=false)。同目标重跑按磁盘真实
|
|
19
|
+
* 布局进入 repair,补齐适配后接续安装 / 构建;恢复原工程走调用方的代码版本能力。
|
|
20
|
+
* 执行期间的并发编辑由调用方现有互斥与调度保证,本原语不加锁。
|
|
21
|
+
*
|
|
22
|
+
* 平台 readiness(沙箱进程真正健康、预览可用)不在本原语内判定,由调用方(M04/M08)核验;
|
|
23
|
+
* 结果里明确标为 pending,不伪造。
|
|
24
|
+
*
|
|
25
|
+
* service 层:不产生终端输出,结果由 handler emit。
|
|
26
|
+
*/
|
|
27
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
28
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
29
|
+
};
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.MIGRATION_PURPOSES = void 0;
|
|
32
|
+
exports.runMigration = runMigration;
|
|
33
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
34
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
35
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
36
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
37
|
+
const index_1 = require("../../../config/migrate-configs/index");
|
|
38
|
+
const index_2 = require("../../../services/app/init/index");
|
|
39
|
+
const migrate_rule_1 = require("../../../utils/migrate-rule");
|
|
40
|
+
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
41
|
+
const project_snapshot_1 = require("../../../utils/project-snapshot");
|
|
42
|
+
const engineering_state_1 = require("../../../utils/engineering-state");
|
|
43
|
+
const run_npm_1 = require("../../../utils/run-npm");
|
|
44
|
+
const error_1 = require("../../../utils/error");
|
|
45
|
+
const logger_1 = require("../../../utils/logger");
|
|
46
|
+
const detect_1 = require("./detect");
|
|
47
|
+
const restart_1 = require("./restart");
|
|
48
|
+
const symlinks_1 = require("./symlinks");
|
|
49
|
+
/** 调用目的:用于记录与关联,并决定冲突策略(resolveConflictPolicy);不据此重新判灰度 */
|
|
50
|
+
exports.MIGRATION_PURPOSES = ['manual', 'upgrade', 'history_restore'];
|
|
51
|
+
/**
|
|
52
|
+
* 冲突策略按调用方区分:
|
|
53
|
+
* - upgrade / history_restore(平台调用)→ stop:平台迁移按结构化结果决定后续流程,冲突时保持
|
|
54
|
+
* 工程原样交给平台处理,转换本身不替用户丢内容。
|
|
55
|
+
* - manual(feida-ai 升级全栈、本机手动执行)跨 stack 转换 → overwrite:调用方没有处理冲突的
|
|
56
|
+
* 环节,停下来会让应用卡在"类型已切、代码未转"的中间态;工程在 git 下,被替换的用户改动
|
|
57
|
+
* 可从历史找回,结果里逐条列出。
|
|
58
|
+
* - manual 且磁盘已是目标布局(repair)→ keep:不同之处是用户在已转换工程上的改动,不能当成
|
|
59
|
+
* 旧布局残留覆盖;也不能停,否则调用方对已转换工程的重入(安装失败后重试等)会失败。
|
|
60
|
+
*/
|
|
61
|
+
function resolveConflictPolicy(purpose, mode) {
|
|
62
|
+
if (purpose !== 'manual')
|
|
63
|
+
return 'stop';
|
|
64
|
+
return mode === 'migrate' ? 'overwrite' : 'keep';
|
|
65
|
+
}
|
|
66
|
+
const DEFAULT_STEP_TIMEOUT_MS = 20 * 60 * 1000;
|
|
67
|
+
/** template package.json 里专属于具体应用的字段,不参与 merge(避免覆盖用户值) */
|
|
68
|
+
const USER_OWNED_PKG_FIELDS = [
|
|
69
|
+
'name',
|
|
70
|
+
'version',
|
|
71
|
+
'private',
|
|
72
|
+
'description',
|
|
73
|
+
'author',
|
|
74
|
+
'keywords',
|
|
75
|
+
'license',
|
|
76
|
+
'repository',
|
|
77
|
+
'homepage',
|
|
78
|
+
'bugs',
|
|
79
|
+
];
|
|
80
|
+
function stripUserOwnedFields(pkgPath) {
|
|
81
|
+
if (!node_fs_1.default.existsSync(pkgPath))
|
|
82
|
+
return;
|
|
83
|
+
const json = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
84
|
+
let changed = false;
|
|
85
|
+
for (const key of USER_OWNED_PKG_FIELDS) {
|
|
86
|
+
if (Reflect.deleteProperty(json, key))
|
|
87
|
+
changed = true;
|
|
88
|
+
}
|
|
89
|
+
if (changed)
|
|
90
|
+
node_fs_1.default.writeFileSync(pkgPath, JSON.stringify(json, null, 2) + '\n');
|
|
91
|
+
}
|
|
92
|
+
function errMessage(err) {
|
|
93
|
+
return err instanceof Error ? err.message : String(err);
|
|
94
|
+
}
|
|
95
|
+
function isTimeout(err) {
|
|
96
|
+
return /signal SIG(TERM|KILL)/.test(errMessage(err));
|
|
97
|
+
}
|
|
98
|
+
function newRunId() {
|
|
99
|
+
return `${Date.now().toString(36)}-${node_crypto_1.default.randomBytes(3).toString('hex')}`;
|
|
100
|
+
}
|
|
101
|
+
function flattenConflicts(results) {
|
|
102
|
+
return results.flatMap((r) => (r.action === 'conflict' ? (r.conflicts ?? []) : []));
|
|
103
|
+
}
|
|
104
|
+
function resolveSource(opts) {
|
|
105
|
+
const meta = (0, spark_meta_1.readSparkMetaStrict)(opts.targetDir);
|
|
106
|
+
let rewriteAppId;
|
|
107
|
+
if (opts.appId !== undefined &&
|
|
108
|
+
opts.appId !== '' &&
|
|
109
|
+
meta?.app_id !== undefined &&
|
|
110
|
+
meta.app_id !== opts.appId) {
|
|
111
|
+
// 沙箱里的工程就是当前应用的代码:不一致只会来自复制(本机 `+init` 写进 meta 的源应用
|
|
112
|
+
// app_id 随代码进了副本),拒绝会让副本永远迁移不了。本机目录可能放错,仍拒绝。
|
|
113
|
+
if (opts.sandbox) {
|
|
114
|
+
rewriteAppId = { from: meta.app_id, to: opts.appId };
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
throw new error_1.AppError('MIGRATE_APP_ID_MISMATCH', `app_id '${opts.appId}' does not match .spark/meta.json app_id '${meta.app_id}'`, { next_actions: ['确认在正确的应用工程目录执行,或修正 --app-id / MIAODA_APP_ID'] });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const metaStack = meta?.stack !== undefined && meta.stack !== '' ? meta.stack : undefined;
|
|
121
|
+
const detectedStack = (0, detect_1.detectDiskStack)(opts.targetDir);
|
|
122
|
+
if (opts.from !== undefined &&
|
|
123
|
+
detectedStack !== 'unknown' &&
|
|
124
|
+
detectedStack !== opts.from &&
|
|
125
|
+
detectedStack !== opts.to) {
|
|
126
|
+
throw new error_1.AppError('MIGRATE_FROM_MISMATCH', `--from '${opts.from}' but project layout on disk is '${detectedStack}'`, { next_actions: ['省略 --from,按磁盘实际布局转换'] });
|
|
127
|
+
}
|
|
128
|
+
const from = detectedStack !== 'unknown' ? detectedStack : (opts.from ?? metaStack);
|
|
129
|
+
if (from === undefined) {
|
|
130
|
+
throw new error_1.AppError('MIGRATE_META_INCOMPLETE', 'cannot determine source stack: .spark/meta.json missing stack and layout not recognized', { next_actions: ['先执行 `miaoda app init`,或用 --from 指明源 stack'] });
|
|
131
|
+
}
|
|
132
|
+
if (opts.from !== undefined &&
|
|
133
|
+
detectedStack === 'unknown' &&
|
|
134
|
+
metaStack !== undefined &&
|
|
135
|
+
opts.from !== metaStack) {
|
|
136
|
+
throw new error_1.AppError('MIGRATE_FROM_MISMATCH', `--from '${opts.from}' but current stack is '${metaStack}'`, { next_actions: ['省略 --from 让 cli 自动读 meta.json,或确认 user app 状态'] });
|
|
137
|
+
}
|
|
138
|
+
const mode = from === opts.to ? 'repair' : 'migrate';
|
|
139
|
+
const config = mode === 'repair' ? (0, index_1.getRepairConfig)(opts.to) : (0, index_1.getMigrateConfig)(from, opts.to);
|
|
140
|
+
if (config === null) {
|
|
141
|
+
const supported = (0, index_1.listSupportedMigrations)()
|
|
142
|
+
.map((m) => `${m.from} → ${m.to}`)
|
|
143
|
+
.join(', ');
|
|
144
|
+
throw new error_1.AppError('MIGRATE_PATH_NOT_SUPPORTED', `no migrate config for '${from}' → '${opts.to}'`, { next_actions: [`已支持:${supported || '(none)'}`] });
|
|
145
|
+
}
|
|
146
|
+
return { metaStack, detectedStack, from, mode, config, rewriteAppId };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 执行一次原地工程转换。参数 / 绑定 / 路径类错误在任何副作用之前抛 AppError;
|
|
150
|
+
* 其余失败以 status=failed|conflict 的结果返回(工程是否被写入见 originalIntact)。
|
|
151
|
+
*/
|
|
152
|
+
function runMigration(opts) {
|
|
153
|
+
const targetDir = node_path_1.default.resolve(opts.targetDir);
|
|
154
|
+
const source = resolveSource({ ...opts, targetDir });
|
|
155
|
+
const runId = newRunId();
|
|
156
|
+
const stepTimeout = opts.stepTimeoutMs ??
|
|
157
|
+
(Number(process.env.MIAODA_MIGRATE_STEP_TIMEOUT_MS) || DEFAULT_STEP_TIMEOUT_MS);
|
|
158
|
+
const npmStdio = opts.quietStdout ? ['ignore', 2, 'inherit'] : 'inherit';
|
|
159
|
+
const conflictPolicy = resolveConflictPolicy(opts.purpose, source.mode);
|
|
160
|
+
const outcome = {
|
|
161
|
+
status: 'failed',
|
|
162
|
+
mode: source.mode,
|
|
163
|
+
runId,
|
|
164
|
+
purpose: opts.purpose,
|
|
165
|
+
conflictPolicy,
|
|
166
|
+
operationRef: opts.operationRef,
|
|
167
|
+
source: {
|
|
168
|
+
metaStack: source.metaStack,
|
|
169
|
+
detectedStack: source.detectedStack,
|
|
170
|
+
stack: source.from,
|
|
171
|
+
},
|
|
172
|
+
target: { stack: opts.to, cliVersion: opts.cliVersion },
|
|
173
|
+
stages: [
|
|
174
|
+
{
|
|
175
|
+
name: 'preflight',
|
|
176
|
+
status: 'ok',
|
|
177
|
+
durationMs: 0,
|
|
178
|
+
detail: `mode=${source.mode}, conflictPolicy=${conflictPolicy}`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
conflicts: [],
|
|
182
|
+
overwritten: [],
|
|
183
|
+
warnings: [],
|
|
184
|
+
changes: { added: [], modified: [], deleted: [] },
|
|
185
|
+
results: [],
|
|
186
|
+
followLatestPackages: source.config.followLatestPackages ?? [],
|
|
187
|
+
install: { status: 'not_run' },
|
|
188
|
+
verify: {
|
|
189
|
+
status: source.config.verify === undefined ? 'not_configured' : 'not_run',
|
|
190
|
+
steps: [],
|
|
191
|
+
},
|
|
192
|
+
originalIntact: true,
|
|
193
|
+
restartSignalSent: false,
|
|
194
|
+
readiness: {
|
|
195
|
+
status: 'not_applicable',
|
|
196
|
+
detail: 'conversion did not complete; runtime not touched',
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
const statePaths = [
|
|
200
|
+
'.spark/local/.gitignore',
|
|
201
|
+
'.spark/local/engineering-state.json',
|
|
202
|
+
`.spark/local/engineering-state.json.${process.pid.toString()}.tmp`,
|
|
203
|
+
];
|
|
204
|
+
const record = (status, stage, extra) => {
|
|
205
|
+
try {
|
|
206
|
+
const conflicts = (0, symlinks_1.writePathSymlinkConflicts)(targetDir, statePaths);
|
|
207
|
+
if (conflicts.length > 0) {
|
|
208
|
+
outcome.warnings.push(...conflicts.map((conflict) => ({
|
|
209
|
+
path: conflict.path,
|
|
210
|
+
code: 'MIGRATE_STATE_UNSAFE_SYMLINK',
|
|
211
|
+
message: 'engineering state not written: unsafe symlink',
|
|
212
|
+
})));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
(0, engineering_state_1.writeEngineeringState)(targetDir, {
|
|
216
|
+
operation: 'migrate',
|
|
217
|
+
status,
|
|
218
|
+
stage,
|
|
219
|
+
runId,
|
|
220
|
+
code: extra?.code,
|
|
221
|
+
message: extra?.message,
|
|
222
|
+
fromStack: source.from,
|
|
223
|
+
toStack: opts.to,
|
|
224
|
+
templateVersion: outcome.target.templateVersion,
|
|
225
|
+
purpose: opts.purpose,
|
|
226
|
+
operationRef: opts.operationRef,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
outcome.warnings.push({
|
|
231
|
+
path: node_path_1.default.join(targetDir, '.spark/local'),
|
|
232
|
+
code: err.code,
|
|
233
|
+
message: `failed to record engineering state: ${errMessage(err)}`,
|
|
234
|
+
});
|
|
235
|
+
(0, logger_1.log)('migrate', `⚠ failed to record engineering state: ${errMessage(err)}`);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const stage = (name, fn) => {
|
|
239
|
+
const start = Date.now();
|
|
240
|
+
try {
|
|
241
|
+
const value = fn();
|
|
242
|
+
if (outcome.failure?.stage !== name) {
|
|
243
|
+
outcome.stages.push({ name, status: 'ok', durationMs: Date.now() - start });
|
|
244
|
+
}
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
outcome.stages.push({
|
|
249
|
+
name,
|
|
250
|
+
status: 'failed',
|
|
251
|
+
durationMs: Date.now() - start,
|
|
252
|
+
detail: errMessage(err),
|
|
253
|
+
});
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
// 进入 apply 后记录已写入工程的变更;快照失败只影响诊断,不影响结果判定
|
|
258
|
+
let before;
|
|
259
|
+
const captureChanges = () => {
|
|
260
|
+
if (before === undefined)
|
|
261
|
+
return;
|
|
262
|
+
try {
|
|
263
|
+
outcome.changes = (0, project_snapshot_1.diffSnapshots)(before, (0, project_snapshot_1.snapshotTree)(targetDir));
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
outcome.warnings.push({
|
|
267
|
+
path: targetDir,
|
|
268
|
+
code: err.code,
|
|
269
|
+
message: `failed to list written changes: ${errMessage(err)}`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const fail = (name, code, err, retryable) => {
|
|
274
|
+
outcome.status = 'failed';
|
|
275
|
+
outcome.failure = {
|
|
276
|
+
stage: name,
|
|
277
|
+
code,
|
|
278
|
+
message: errMessage(err),
|
|
279
|
+
retryable,
|
|
280
|
+
...(isTimeout(err) ? { timedOut: true } : {}),
|
|
281
|
+
};
|
|
282
|
+
captureChanges();
|
|
283
|
+
record('failed', name, { code, message: outcome.failure.message });
|
|
284
|
+
return outcome;
|
|
285
|
+
};
|
|
286
|
+
const conflict = (name, code, conflicts) => {
|
|
287
|
+
outcome.status = 'conflict';
|
|
288
|
+
outcome.conflicts = conflicts;
|
|
289
|
+
outcome.failure = {
|
|
290
|
+
stage: name,
|
|
291
|
+
code,
|
|
292
|
+
message: conflicts.map((c) => `${c.path}: ${c.reason}`).join('; '),
|
|
293
|
+
retryable: false,
|
|
294
|
+
};
|
|
295
|
+
captureChanges();
|
|
296
|
+
if (name !== 'preflight') {
|
|
297
|
+
record('conflict', name, { code, message: outcome.failure.message });
|
|
298
|
+
}
|
|
299
|
+
return outcome;
|
|
300
|
+
};
|
|
301
|
+
const writePaths = [...(0, symlinks_1.migrationWritePaths)(source.config.rules, source.mode), ...statePaths];
|
|
302
|
+
const rejectSymlinks = (name, root, paths = writePaths, installed = false) => {
|
|
303
|
+
let conflicts;
|
|
304
|
+
try {
|
|
305
|
+
conflicts = (0, symlinks_1.migrationSymlinkConflicts)(root, paths, installed);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
outcome.stages.push({ name, status: 'failed', durationMs: 0, detail: errMessage(err) });
|
|
309
|
+
fail(name, 'MIGRATE_SYMLINK_CHECK_FAILED', err, true);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (conflicts.length === 0)
|
|
313
|
+
return false;
|
|
314
|
+
const message = conflicts.map((c) => `${c.path}: ${c.reason}`).join('; ');
|
|
315
|
+
outcome.stages.push({ name, status: 'conflict', durationMs: 0, detail: message });
|
|
316
|
+
conflict(name, 'MIGRATE_UNSAFE_SYMLINK', conflicts);
|
|
317
|
+
return true;
|
|
318
|
+
};
|
|
319
|
+
const hasFailure = () => outcome.failure !== undefined;
|
|
320
|
+
if (rejectSymlinks('preflight', targetDir))
|
|
321
|
+
return outcome;
|
|
322
|
+
record('running', 'preflight');
|
|
323
|
+
(0, logger_1.log)('migrate', `run ${runId}: ${source.from} → ${opts.to} (mode=${source.mode}, purpose=${opts.purpose}, conflictPolicy=${conflictPolicy})`);
|
|
324
|
+
// 临时目录只放渲染后的目标模板;用户工程始终在原目录处理
|
|
325
|
+
const tmpRoot = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), `miaoda-migrate-${runId}-`));
|
|
326
|
+
const templateDir = node_path_1.default.join(tmpRoot, 'template');
|
|
327
|
+
try {
|
|
328
|
+
// ---- render ----
|
|
329
|
+
try {
|
|
330
|
+
stage('render', () => {
|
|
331
|
+
const tpl = (0, index_2.renderTemplate)({
|
|
332
|
+
stack: opts.to,
|
|
333
|
+
targetDir: templateDir,
|
|
334
|
+
projectName: node_path_1.default.basename(targetDir),
|
|
335
|
+
});
|
|
336
|
+
outcome.target.templatePackage = tpl.packageName;
|
|
337
|
+
outcome.target.templateVersion = tpl.version;
|
|
338
|
+
outcome.target.archType = tpl.archType;
|
|
339
|
+
if (rejectSymlinks('render', templateDir))
|
|
340
|
+
return;
|
|
341
|
+
stripUserOwnedFields(node_path_1.default.join(templateDir, 'package.json'));
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
return fail('render', 'MIGRATE_TEMPLATE_UNAVAILABLE', err, true);
|
|
346
|
+
}
|
|
347
|
+
if (hasFailure())
|
|
348
|
+
return outcome;
|
|
349
|
+
// ---- precheck:只读预检,任何可识别冲突都在写入前返回 ----
|
|
350
|
+
const ruleOpts = {
|
|
351
|
+
sourceRoot: templateDir,
|
|
352
|
+
targetDir,
|
|
353
|
+
logPrefix: 'migrate',
|
|
354
|
+
mode: source.mode,
|
|
355
|
+
conflictPolicy,
|
|
356
|
+
};
|
|
357
|
+
try {
|
|
358
|
+
outcome.results = stage('precheck', () => (0, migrate_rule_1.applyMigrateRules)(source.config.rules, { ...ruleOpts, dryRun: true }));
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
return fail('precheck', 'MIGRATE_RULE_FAILED', err, false);
|
|
362
|
+
}
|
|
363
|
+
const planned = flattenConflicts(outcome.results);
|
|
364
|
+
if (planned.length > 0) {
|
|
365
|
+
markLastStage(outcome, 'conflict', `${planned.length.toString()} conflict(s)`);
|
|
366
|
+
return conflict('precheck', 'MIGRATE_CONFLICT', planned);
|
|
367
|
+
}
|
|
368
|
+
// ---- apply:从这里开始写入原工程 ----
|
|
369
|
+
try {
|
|
370
|
+
before = (0, project_snapshot_1.snapshotTree)(targetDir);
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
outcome.stages.push({ name: 'apply', status: 'failed', durationMs: 0 });
|
|
374
|
+
return fail('apply', 'MIGRATE_RULE_FAILED', err, true);
|
|
375
|
+
}
|
|
376
|
+
outcome.originalIntact = false;
|
|
377
|
+
try {
|
|
378
|
+
outcome.results = stage('apply', () => (0, migrate_rule_1.applyMigrateRules)(source.config.rules, { ...ruleOpts, stopOnConflict: true }));
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
return fail('apply', 'MIGRATE_RULE_FAILED', err, false);
|
|
382
|
+
}
|
|
383
|
+
collectPolicyResults(outcome);
|
|
384
|
+
const late = flattenConflicts(outcome.results);
|
|
385
|
+
if (late.length > 0) {
|
|
386
|
+
// 依赖前序规则写入结果的冲突只在实际执行时出现:已停止,后续规则未执行
|
|
387
|
+
markLastStage(outcome, 'conflict', `${late.length.toString()} conflict(s)`);
|
|
388
|
+
return conflict('apply', 'MIGRATE_CONFLICT', late);
|
|
389
|
+
}
|
|
390
|
+
// 规则搬迁后的链接也必须仍在工程内,安装 / 构建才不会越界
|
|
391
|
+
if (rejectSymlinks('install', targetDir))
|
|
392
|
+
return outcome;
|
|
393
|
+
(0, spark_meta_1.writeSparkMeta)(targetDir, {
|
|
394
|
+
archType: outcome.target.archType,
|
|
395
|
+
version: outcome.target.templateVersion,
|
|
396
|
+
app_id: source.rewriteAppId?.to,
|
|
397
|
+
});
|
|
398
|
+
if (source.rewriteAppId !== undefined) {
|
|
399
|
+
outcome.warnings.push({
|
|
400
|
+
path: '.spark/meta.json',
|
|
401
|
+
code: 'MIGRATE_APP_ID_REWRITTEN',
|
|
402
|
+
message: `meta app_id '${source.rewriteAppId.from}' rewritten to current app '${source.rewriteAppId.to}' (copied project)`,
|
|
403
|
+
});
|
|
404
|
+
(0, logger_1.log)('migrate', `⚠ .spark/meta.json app_id '${source.rewriteAppId.from}' → '${source.rewriteAppId.to}'(复制来的工程)`);
|
|
405
|
+
}
|
|
406
|
+
// ---- install ----
|
|
407
|
+
const installArgs = [
|
|
408
|
+
'install',
|
|
409
|
+
'--no-audit',
|
|
410
|
+
'--no-fund',
|
|
411
|
+
'--registry',
|
|
412
|
+
(0, index_2.resolveNpmInstallRegistry)(),
|
|
413
|
+
];
|
|
414
|
+
try {
|
|
415
|
+
stage('install', () => {
|
|
416
|
+
// 跨 stack 转换后依赖集合已变(lockfile 已由规则删除):旧依赖树留着会与新 spec 撞 peer。
|
|
417
|
+
// repair 保留现有 node_modules,从上次中断处接续安装
|
|
418
|
+
if (source.mode === 'migrate') {
|
|
419
|
+
node_fs_1.default.rmSync(node_path_1.default.join(targetDir, 'node_modules'), RM_WITH_RETRY);
|
|
420
|
+
}
|
|
421
|
+
if (outcome.followLatestPackages.length > 0) {
|
|
422
|
+
const upgradeArgs = [
|
|
423
|
+
...installArgs,
|
|
424
|
+
...outcome.followLatestPackages.map((pkg) => `${pkg}@latest`),
|
|
425
|
+
];
|
|
426
|
+
(0, logger_1.log)('migrate', `Running npm ${upgradeArgs.join(' ')}...`);
|
|
427
|
+
(0, run_npm_1.execNpmSync)(upgradeArgs, { cwd: targetDir, stdio: npmStdio, timeout: stepTimeout });
|
|
428
|
+
if (rejectSymlinks('install', targetDir, writePaths, true))
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
(0, logger_1.log)('migrate', `Running npm ${installArgs.join(' ')}...`);
|
|
432
|
+
(0, run_npm_1.execNpmSync)(installArgs, { cwd: targetDir, stdio: npmStdio, timeout: stepTimeout });
|
|
433
|
+
});
|
|
434
|
+
if (hasFailure())
|
|
435
|
+
return outcome;
|
|
436
|
+
outcome.install = { status: 'ok' };
|
|
437
|
+
}
|
|
438
|
+
catch (err) {
|
|
439
|
+
outcome.install = { status: 'failed', error: errMessage(err) };
|
|
440
|
+
return fail('install', 'MIGRATE_INSTALL_FAILED', err, true);
|
|
441
|
+
}
|
|
442
|
+
// ---- verify ----
|
|
443
|
+
if (rejectSymlinks('verify', targetDir, writePaths, true))
|
|
444
|
+
return outcome;
|
|
445
|
+
if (source.config.verify !== undefined) {
|
|
446
|
+
try {
|
|
447
|
+
stage('verify', () => {
|
|
448
|
+
for (const step of source.config.verify ?? []) {
|
|
449
|
+
const command = `npm ${step.join(' ')}`;
|
|
450
|
+
(0, logger_1.log)('migrate', `Verifying: ${command}...`);
|
|
451
|
+
try {
|
|
452
|
+
(0, run_npm_1.execNpmSync)(step, { cwd: targetDir, stdio: npmStdio, timeout: stepTimeout });
|
|
453
|
+
outcome.verify.steps.push({ command, status: 'ok' });
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
outcome.verify.steps.push({ command, status: 'failed', error: errMessage(err) });
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
outcome.verify.status = 'ok';
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
outcome.verify.status = 'failed';
|
|
465
|
+
return fail('verify', 'MIGRATE_VERIFY_FAILED', err, true);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
captureChanges();
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
try {
|
|
472
|
+
node_fs_1.default.rmSync(tmpRoot, { recursive: true, force: true });
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
outcome.warnings.push({
|
|
476
|
+
path: tmpRoot,
|
|
477
|
+
code: err.code,
|
|
478
|
+
message: errMessage(err),
|
|
479
|
+
});
|
|
480
|
+
(0, logger_1.log)('migrate', `⚠ cleanup failed for ${tmpRoot}: ${errMessage(err)}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
// ---- restart (sandbox only) ----
|
|
484
|
+
const restartStart = Date.now();
|
|
485
|
+
if (opts.sandbox) {
|
|
486
|
+
clearViteCache(targetDir);
|
|
487
|
+
outcome.restartSignalSent = (0, restart_1.signalDevRestart)(targetDir).length > 0;
|
|
488
|
+
outcome.stages.push({
|
|
489
|
+
name: 'restart',
|
|
490
|
+
status: 'ok',
|
|
491
|
+
durationMs: Date.now() - restartStart,
|
|
492
|
+
detail: outcome.restartSignalSent
|
|
493
|
+
? 'restart signal sent'
|
|
494
|
+
: 'no dev process running in this directory',
|
|
495
|
+
});
|
|
496
|
+
outcome.readiness = {
|
|
497
|
+
status: 'pending_caller_verification',
|
|
498
|
+
detail: 'files and dependencies ready; sandbox process health / preview readiness must be verified by the caller',
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
outcome.stages.push({
|
|
503
|
+
name: 'restart',
|
|
504
|
+
status: 'skipped',
|
|
505
|
+
durationMs: 0,
|
|
506
|
+
detail: 'local run: restart the dev process manually',
|
|
507
|
+
});
|
|
508
|
+
outcome.readiness = {
|
|
509
|
+
status: 'pending_caller_verification',
|
|
510
|
+
detail: 'local project converted; start `npm run dev` and verify the preview',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
outcome.status = 'converted';
|
|
514
|
+
record('succeeded', opts.sandbox ? 'restart' : 'verify');
|
|
515
|
+
return outcome;
|
|
516
|
+
}
|
|
517
|
+
/** 汇总按冲突策略覆盖 / 保留的位置:覆盖的进 overwritten,保留未适配的作为 warning 交给调用方 */
|
|
518
|
+
function collectPolicyResults(outcome) {
|
|
519
|
+
for (const r of outcome.results) {
|
|
520
|
+
outcome.overwritten.push(...(r.overwritten ?? []));
|
|
521
|
+
for (const c of r.kept ?? []) {
|
|
522
|
+
const ref = [
|
|
523
|
+
c.with === undefined ? '' : ` with ${c.with}`,
|
|
524
|
+
c.id === undefined ? '' : ` id=${c.id}`,
|
|
525
|
+
];
|
|
526
|
+
outcome.warnings.push({
|
|
527
|
+
path: c.path,
|
|
528
|
+
code: 'MIGRATE_CONFLICT_KEPT',
|
|
529
|
+
message: `${c.reason}${ref.join('')}`,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function markLastStage(outcome, status, detail) {
|
|
535
|
+
const last = outcome.stages[outcome.stages.length - 1];
|
|
536
|
+
last.status = status;
|
|
537
|
+
last.detail = detail;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* 删依赖目录时的重试:沙箱里 dev 进程(vite 依赖预构建写 node_modules/.vite)可能与删除并发,
|
|
541
|
+
* 递归删到父目录时子项刚被写入 → ENOTEMPTY。Node 对 ENOTEMPTY / EBUSY / EPERM 等按线性退避
|
|
542
|
+
* 重试(retryDelay × 第 n 次),6 次合计约 10s。并发写入方不停时仍会失败,由调用方先停 dev。
|
|
543
|
+
*/
|
|
544
|
+
const RM_WITH_RETRY = {
|
|
545
|
+
recursive: true,
|
|
546
|
+
force: true,
|
|
547
|
+
maxRetries: 6,
|
|
548
|
+
retryDelay: 500,
|
|
549
|
+
};
|
|
550
|
+
/**
|
|
551
|
+
* 清 vite 预构建缓存:依赖与 lockfile 变化后,旧 .vite/deps 的 lockfileHash 与新 lockfile
|
|
552
|
+
* 不一致,新 vite 冷启动时会中途 re-optimize,期间浏览器请求旧 hash 触发
|
|
553
|
+
* ERR_OUTDATED_OPTIMIZED_DEP → 504(见 fullstack-plugin#1143)。软失败。
|
|
554
|
+
*/
|
|
555
|
+
function clearViteCache(targetDir) {
|
|
556
|
+
const viteCacheDir = node_path_1.default.join(targetDir, 'node_modules', '.vite');
|
|
557
|
+
if (!node_fs_1.default.existsSync(viteCacheDir))
|
|
558
|
+
return;
|
|
559
|
+
try {
|
|
560
|
+
node_fs_1.default.rmSync(viteCacheDir, RM_WITH_RETRY);
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
(0, logger_1.log)('migrate', `⚠ 清 .vite 失败 (continuing): ${errMessage(err)}`);
|
|
564
|
+
}
|
|
565
|
+
}
|