@lark-apaas/miaoda-cli 0.1.16-alpha.f013f1c → 0.1.16-alpha.fdc0db1
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/dist/cli/commands/app/index.js +0 -49
- package/dist/cli/commands/db/index.js +0 -3
- package/dist/cli/commands/file/index.js +4 -3
- package/dist/cli/handlers/app/index.js +1 -3
- package/dist/main.js +3 -0
- package/package.json +1 -1
- package/dist/cli/handlers/app/migrate.js +0 -182
- package/dist/config/migrate-configs/index.js +0 -35
- package/dist/config/migrate-configs/vite-react-to-nestjs-react-fullstack.js +0 -202
- package/dist/config/migrate.js +0 -15
- package/dist/utils/codemod-client-toolkit-lite.js +0 -106
- package/dist/utils/migrate-rule.js +0 -507
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.registerAppCommands = registerAppCommands;
|
|
4
4
|
const shared_1 = require("../../../cli/commands/shared");
|
|
5
5
|
const index_1 = require("../../../cli/handlers/app/index");
|
|
6
|
-
const index_2 = require("../../../config/migrate-configs/index");
|
|
7
6
|
const error_1 = require("../../../utils/error");
|
|
8
7
|
function registerAppCommands(program, opts = {}) {
|
|
9
8
|
const description = opts.includeInit
|
|
@@ -23,56 +22,8 @@ function registerAppCommands(program, opts = {}) {
|
|
|
23
22
|
if (opts.includeInit) {
|
|
24
23
|
registerAppInit(appCmd);
|
|
25
24
|
registerAppSync(appCmd);
|
|
26
|
-
registerAppMigrate(appCmd);
|
|
27
25
|
}
|
|
28
26
|
}
|
|
29
|
-
function registerAppMigrate(parent) {
|
|
30
|
-
const supportedDesc = (0, index_2.listSupportedMigrations)()
|
|
31
|
-
.map((m) => `${m.from} → ${m.to}`)
|
|
32
|
-
.join(', ');
|
|
33
|
-
const cmd = parent
|
|
34
|
-
.command('migrate')
|
|
35
|
-
.description('跨 stack 原地迁移:按 MigrateConfig 全套 apply(move + delete + 模板覆盖 + 字段 merge + set-stack)')
|
|
36
|
-
.requiredOption('--to <stack>', '目标 stack 短名')
|
|
37
|
-
.option('--from <stack>', '源 stack;默认读 .spark/meta.json 当前 stack')
|
|
38
|
-
.option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
|
|
39
|
-
.addHelpText('after', `
|
|
40
|
-
已支持的迁移路径
|
|
41
|
-
${supportedDesc || '(none)'}
|
|
42
|
-
|
|
43
|
-
执行流
|
|
44
|
-
1. 校验 .spark/meta.json 存在且当前 stack 匹配 --from(或自动读 meta)
|
|
45
|
-
2. 按 (from, to) 在 src/config/migrate-configs/ 取 MigrateConfig
|
|
46
|
-
3. 顺序执行 rules:
|
|
47
|
-
move-directory / move-file → 用户领地搬到新布局(src → client/src 等)
|
|
48
|
-
delete-file / delete-directory → 清理老 stack 的残留文件
|
|
49
|
-
delete-json-keys → 从 user package.json 删 vite-react 专属字段
|
|
50
|
-
file / directory → 拷新 stack 的模板资产覆盖
|
|
51
|
-
merge-json → package.json 等字段级深合并
|
|
52
|
-
add-script / patch-script / add-line / remove-line → 局部修补
|
|
53
|
-
set-stack → 收尾切 .spark/meta.json 的 stack 字段
|
|
54
|
-
4. 不做 npm install —— 让用户自行 review 改动后再装依赖(package-lock.json 已删)
|
|
55
|
-
|
|
56
|
-
JSON 输出
|
|
57
|
-
{"data": {"from": "...", "to": "...",
|
|
58
|
-
"appliedRules": [{"type": "...", "action": "...", "path": "...", "detail": "..."}],
|
|
59
|
-
"moved": [...], "deleted": [...], "synced": [...], "merged": [...], "patched": [...],
|
|
60
|
-
"skipped": N,
|
|
61
|
-
"nextActions": [...]}}
|
|
62
|
-
|
|
63
|
-
示例
|
|
64
|
-
$ miaoda app migrate --to nestjs-react-fullstack
|
|
65
|
-
$ miaoda app migrate --from vite-react --to nestjs-react-fullstack
|
|
66
|
-
$ miaoda app migrate --to nestjs-react-fullstack --dir /path/to/app
|
|
67
|
-
`);
|
|
68
|
-
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
69
|
-
await (0, index_1.handleAppMigrate)({
|
|
70
|
-
dir: rawOpts.dir,
|
|
71
|
-
from: rawOpts.from,
|
|
72
|
-
to: rawOpts.to,
|
|
73
|
-
});
|
|
74
|
-
}));
|
|
75
|
-
}
|
|
76
27
|
function registerAppSync(parent) {
|
|
77
28
|
const cmd = parent
|
|
78
29
|
.command('sync')
|
|
@@ -481,7 +481,6 @@ Examples:
|
|
|
481
481
|
'应用到 online;online 将无法直接更改数据库结构(仍可进行数据 DML 操作)。')
|
|
482
482
|
.usage('[flags]')
|
|
483
483
|
.option('--sync-data', '启用时将现有数据同步一份到 dev 环境')
|
|
484
|
-
.option('-y, --yes', '跳过确认提示直接执行')
|
|
485
484
|
.action(async function () {
|
|
486
485
|
await (0, index_1.handleDbMigrationInit)(this.optsWithGlobals());
|
|
487
486
|
})
|
|
@@ -562,7 +561,6 @@ Examples:
|
|
|
562
561
|
.summary('将 dev 的变更发布到 online(单事务原子)')
|
|
563
562
|
.description('将 dev 的变更发布到 online。')
|
|
564
563
|
.usage('[flags]')
|
|
565
|
-
.option('-y, --yes', '跳过确认提示直接执行')
|
|
566
564
|
.action(async function () {
|
|
567
565
|
await (0, index_1.handleDbMigrationApply)(this.optsWithGlobals());
|
|
568
566
|
})
|
|
@@ -681,7 +679,6 @@ Examples:
|
|
|
681
679
|
.description('将数据库恢复到指定时间点的状态。')
|
|
682
680
|
.usage('<timestamp> [flags]')
|
|
683
681
|
.argument('<timestamp>', '目标时间')
|
|
684
|
-
.option('-y, --yes', '跳过确认提示直接执行')
|
|
685
682
|
.action(async function (target) {
|
|
686
683
|
await (0, index_1.handleDbRecoveryApply)(target, this.optsWithGlobals());
|
|
687
684
|
})
|
|
@@ -146,9 +146,10 @@ Examples:
|
|
|
146
146
|
.usage('[paths...] [flags]')
|
|
147
147
|
.argument('[paths...]', '要删除的文件,每项可填路径或文件名(自动识别)')
|
|
148
148
|
.option('-n, --name <name>', '按文件名删除(可重复指定)', (value, prev) => [...(prev ?? []), value])
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
// --yes 是 root 全局 option(见 main.ts),不在此重复声明;必须经 optsWithGlobals
|
|
150
|
+
// 读取(值挂在 root 上,本地 this.opts().yes 为 undefined),否则非交互场景确认门会误拦。
|
|
151
|
+
.action(async function (paths) {
|
|
152
|
+
await (0, index_1.handleFileRm)(paths, { ...this.optsWithGlobals(), appId: (0, shared_1.resolveAppId)({}) });
|
|
152
153
|
})
|
|
153
154
|
.addHelpText('after', `
|
|
154
155
|
Notes:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SUPPORTED_STACKS = exports.
|
|
3
|
+
exports.SUPPORTED_STACKS = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
|
|
4
4
|
var get_1 = require("./get");
|
|
5
5
|
Object.defineProperty(exports, "handleAppGet", { enumerable: true, get: function () { return get_1.handleAppGet; } });
|
|
6
6
|
var update_1 = require("./update");
|
|
@@ -10,8 +10,6 @@ Object.defineProperty(exports, "handleAppInit", { enumerable: true, get: functio
|
|
|
10
10
|
var sync_1 = require("./sync");
|
|
11
11
|
Object.defineProperty(exports, "handleAppSync", { enumerable: true, get: function () { return sync_1.handleAppSync; } });
|
|
12
12
|
Object.defineProperty(exports, "handleAppUpgrade", { enumerable: true, get: function () { return sync_1.handleAppUpgrade; } });
|
|
13
|
-
var migrate_1 = require("./migrate");
|
|
14
|
-
Object.defineProperty(exports, "handleAppMigrate", { enumerable: true, get: function () { return migrate_1.handleAppMigrate; } });
|
|
15
13
|
// commands 层渲染 help 时需要这份枚举;从 handler barrel 转发,避免 commands → services 越界
|
|
16
14
|
var index_1 = require("../../../services/app/init/index");
|
|
17
15
|
Object.defineProperty(exports, "SUPPORTED_STACKS", { enumerable: true, get: function () { return index_1.SUPPORTED_STACKS; } });
|
package/dist/main.js
CHANGED
|
@@ -30,6 +30,9 @@ program
|
|
|
30
30
|
.argParser((0, shared_1.caseInsensitiveChoice)(['pretty', 'json']))
|
|
31
31
|
.default('pretty'))
|
|
32
32
|
.option('--verbose', 'debug 日志到 stderr')
|
|
33
|
+
// 全局接受 -y/--yes:作为 root 全局 option 下发到所有子命令解析(同 --json/--verbose),
|
|
34
|
+
// 让任意命令带 --yes 都不报 unknown option。对有确认门的破坏性命令生效,其余命令忽略。
|
|
35
|
+
.option('-y, --yes', '跳过确认提示(对有确认门的命令生效,其余命令忽略)')
|
|
33
36
|
.helpOption('-h, --help', '显示帮助信息')
|
|
34
37
|
.hook('preAction', (_thisCmd, actionCmd) => {
|
|
35
38
|
(0, log_id_1.generateLogId)();
|
package/package.json
CHANGED
|
@@ -1,182 +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.handleAppMigrate = handleAppMigrate;
|
|
7
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
-
const index_1 = require("../../../config/migrate-configs/index");
|
|
11
|
-
const index_2 = require("../../../services/app/init/index");
|
|
12
|
-
const migrate_rule_1 = require("../../../utils/migrate-rule");
|
|
13
|
-
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
14
|
-
const error_1 = require("../../../utils/error");
|
|
15
|
-
const output_1 = require("../../../utils/output");
|
|
16
|
-
const logger_1 = require("../../../utils/logger");
|
|
17
|
-
/**
|
|
18
|
-
* miaoda app migrate --to <stack> [--from <stack>] [--dir <path>]
|
|
19
|
-
*
|
|
20
|
-
* 跨 stack 原地迁移:把 user app 从源 stack(默认 .spark/meta.json 当前 stack)转到目标 stack。
|
|
21
|
-
* 同一个 git 仓库内进行,不动 git history,所有改动作为 working tree 变更体现,用户跑完后
|
|
22
|
-
* 自行 `git status` / `git diff` 评估并 commit。
|
|
23
|
-
*
|
|
24
|
-
* 跟 sync 的区别:sync 是同 stack 内的版本对齐,migrate 是跨 stack 切换(布局重排、依赖
|
|
25
|
-
* 集合切换、配置文件全套替换)。两条命令的 rule 引擎共享(applyMigrateRules 内部对
|
|
26
|
-
* SyncRule 委派给 applySyncRules)。
|
|
27
|
-
*
|
|
28
|
-
* 执行流:
|
|
29
|
-
* 1. 校验 user app 已 init(.spark/meta.json 存在)+ 当前 stack 匹配 from
|
|
30
|
-
* 2. 拿到 (from, to) 对应的 MigrateConfig 与模板源根目录
|
|
31
|
-
* 3. 顺序执行 rules:move-* → delete-* → delete-json-keys → file/directory →
|
|
32
|
-
* merge-json → set-stack
|
|
33
|
-
* 4. emit 结果;不做 npm install(用户接下来跑 `npm install` 或 `miaoda app sync` 时自然装)
|
|
34
|
-
*
|
|
35
|
-
* 不做事:
|
|
36
|
-
* - 不自动 commit / stash —— 让用户自己用 git review 改动
|
|
37
|
-
* - 不跑 npm install —— 留给后续 sync 或用户手动
|
|
38
|
-
* - 不动 .agent/skills/(那是 miaoda skills sync 的事,迁移后用户应该单独跑一次切到新 stack 的 skills)
|
|
39
|
-
*/
|
|
40
|
-
async function handleAppMigrate(opts) {
|
|
41
|
-
await Promise.resolve();
|
|
42
|
-
const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
43
|
-
const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
|
|
44
|
-
if (meta.stack === undefined || meta.stack === '') {
|
|
45
|
-
throw new error_1.AppError('MIGRATE_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
|
|
46
|
-
}
|
|
47
|
-
const from = opts.from ?? meta.stack;
|
|
48
|
-
if (opts.from !== undefined && opts.from !== meta.stack) {
|
|
49
|
-
throw new error_1.AppError('MIGRATE_FROM_MISMATCH', `--from '${opts.from}' but current stack is '${meta.stack}'`, {
|
|
50
|
-
next_actions: ['省略 --from 让 cli 自动读 meta.json,或确认 user app 状态'],
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
if (from === opts.to) {
|
|
54
|
-
throw new error_1.AppError('MIGRATE_NOOP', `from and to are both '${from}', nothing to do`);
|
|
55
|
-
}
|
|
56
|
-
const config = (0, index_1.getMigrateConfig)(from, opts.to);
|
|
57
|
-
if (config === null) {
|
|
58
|
-
const supported = (0, index_1.listSupportedMigrations)()
|
|
59
|
-
.map((m) => `${m.from} → ${m.to}`)
|
|
60
|
-
.join(', ');
|
|
61
|
-
throw new error_1.AppError('MIGRATE_PATH_NOT_SUPPORTED', `no migrate config for '${from}' → '${opts.to}'`, {
|
|
62
|
-
next_actions: [`已支持:${supported || '(none)'}`],
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
// migrate 用 init 同款的 renderTemplate 拉目标 stack 的 npm template tarball 到 tmp,
|
|
66
|
-
// 再把 tmp 当作 sourceRoot 喂给 applyMigrateRules。这样 migrate config 里 file/directory
|
|
67
|
-
// rule 的 `from` 引用的是完整的 npm template 渲染产物,不需要在 upgrade/templates/ 重复
|
|
68
|
-
// 维护一份 server/ + client/ + 全套配置 —— 复杂度由 init 那条链路 amortize。
|
|
69
|
-
const tmpRoot = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-migrate-'));
|
|
70
|
-
const projectName = node_path_1.default.basename(targetDir);
|
|
71
|
-
let templateVersion;
|
|
72
|
-
let templateArchType;
|
|
73
|
-
let results;
|
|
74
|
-
try {
|
|
75
|
-
const tpl = (0, index_2.renderTemplate)({ stack: opts.to, targetDir: tmpRoot, projectName });
|
|
76
|
-
templateVersion = tpl.version;
|
|
77
|
-
templateArchType = tpl.archType;
|
|
78
|
-
// template package.json 的 user 私有字段(name / version / private 等)不参与 merge —— 否
|
|
79
|
-
// 则会被 deepMergeJson 当成 template 端"权威值"覆盖 user 项目原本的字段。这里在 tmp 端
|
|
80
|
-
// 直接 strip 这些字段,让 merge-json rule 只对 dependencies / devDependencies / scripts
|
|
81
|
-
// 等 schema-level 字段生效。
|
|
82
|
-
stripUserOwnedFields(node_path_1.default.join(tmpRoot, 'package.json'));
|
|
83
|
-
(0, logger_1.log)('migrate', `Rendered ${opts.to}@${tpl.version} to tmp, applying ${config.rules.length.toString()} rule(s)`);
|
|
84
|
-
results = (0, migrate_rule_1.applyMigrateRules)(config.rules, {
|
|
85
|
-
sourceRoot: tmpRoot,
|
|
86
|
-
targetDir,
|
|
87
|
-
logPrefix: 'migrate',
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
finally {
|
|
91
|
-
node_fs_1.default.rmSync(tmpRoot, { recursive: true, force: true });
|
|
92
|
-
}
|
|
93
|
-
// archType / version 字段同步:set-stack rule 只切 stack,archType 是从 npm template 包
|
|
94
|
-
// 的 miaodaTemplate.archType 拿到的动态值,rule 配置时不知道,由 handler 在拿到
|
|
95
|
-
// renderTemplate 结果后单独写。version 也一并写回方便 sync 后续诊断。
|
|
96
|
-
(0, spark_meta_1.writeSparkMeta)(targetDir, { archType: templateArchType, version: templateVersion });
|
|
97
|
-
(0, output_1.emit)({
|
|
98
|
-
data: {
|
|
99
|
-
from,
|
|
100
|
-
to: opts.to,
|
|
101
|
-
templateVersion,
|
|
102
|
-
appliedRules: results.map((r) => ({
|
|
103
|
-
type: r.rule.type,
|
|
104
|
-
action: r.action,
|
|
105
|
-
path: r.path,
|
|
106
|
-
detail: r.detail,
|
|
107
|
-
})),
|
|
108
|
-
...summarizeResults(results),
|
|
109
|
-
nextActions: [
|
|
110
|
-
'git status / git diff 评估改动并 commit',
|
|
111
|
-
'npm install 装新依赖(package-lock.json 已被删除,将重新生成)',
|
|
112
|
-
'miaoda skills sync 同步到新 stack 的 agent skills',
|
|
113
|
-
],
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* 从 tmp 端 template package.json 中删除 user 项目通常拥有的字段,避免 merge-json 时这些
|
|
119
|
-
* 字段被 template 值覆盖。需要 strip 的字段以 npm 模板里"通常专属于具体应用"的为准。
|
|
120
|
-
*/
|
|
121
|
-
function stripUserOwnedFields(pkgPath) {
|
|
122
|
-
if (!node_fs_1.default.existsSync(pkgPath))
|
|
123
|
-
return;
|
|
124
|
-
const json = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
125
|
-
const userOwned = [
|
|
126
|
-
'name',
|
|
127
|
-
'version',
|
|
128
|
-
'private',
|
|
129
|
-
'description',
|
|
130
|
-
'author',
|
|
131
|
-
'keywords',
|
|
132
|
-
'license',
|
|
133
|
-
'repository',
|
|
134
|
-
'homepage',
|
|
135
|
-
'bugs',
|
|
136
|
-
];
|
|
137
|
-
let changed = false;
|
|
138
|
-
for (const key of userOwned) {
|
|
139
|
-
if (Reflect.deleteProperty(json, key))
|
|
140
|
-
changed = true;
|
|
141
|
-
}
|
|
142
|
-
if (changed) {
|
|
143
|
-
node_fs_1.default.writeFileSync(pkgPath, JSON.stringify(json, null, 2) + '\n');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function summarizeResults(results) {
|
|
147
|
-
const moved = [];
|
|
148
|
-
const deleted = [];
|
|
149
|
-
const synced = [];
|
|
150
|
-
const merged = [];
|
|
151
|
-
const patched = [];
|
|
152
|
-
let skipped = 0;
|
|
153
|
-
for (const r of results) {
|
|
154
|
-
if (r.path === undefined)
|
|
155
|
-
continue;
|
|
156
|
-
switch (r.action) {
|
|
157
|
-
case 'moved':
|
|
158
|
-
moved.push(r.path);
|
|
159
|
-
break;
|
|
160
|
-
case 'deleted':
|
|
161
|
-
deleted.push(r.path);
|
|
162
|
-
break;
|
|
163
|
-
case 'synced':
|
|
164
|
-
case 'created':
|
|
165
|
-
synced.push(r.path);
|
|
166
|
-
break;
|
|
167
|
-
case 'merged':
|
|
168
|
-
merged.push(r.path);
|
|
169
|
-
break;
|
|
170
|
-
case 'patched':
|
|
171
|
-
patched.push(r.path);
|
|
172
|
-
break;
|
|
173
|
-
case 'skipped':
|
|
174
|
-
skipped++;
|
|
175
|
-
break;
|
|
176
|
-
case 'appended':
|
|
177
|
-
case 'noop':
|
|
178
|
-
break;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return { moved, deleted, synced, merged, patched, skipped };
|
|
182
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* 跨 stack 迁移注册表 —— 按 (from, to) 二元组查找对应的 MigrateConfig。
|
|
4
|
-
*
|
|
5
|
-
* 跟 sync 注册表 (src/config/sync-configs/index.ts) 的关系:sync 一份配置对应一个 stack,
|
|
6
|
-
* migrate 一份配置对应 (源 stack, 目标 stack) 二元组;模板资产 migrate 在 runtime 从 npm
|
|
7
|
-
* 拉目标 stack 的 template tarball(init 用同一套 renderTemplate),不再用 upgrade/templates/
|
|
8
|
-
* 的子集。
|
|
9
|
-
*
|
|
10
|
-
* 新增迁移路径:
|
|
11
|
-
* 1. src/config/migrate-configs/<from>-to-<to>.ts 里 export default MIGRATE_CONFIG
|
|
12
|
-
* 2. 在 MIGRATE_REGISTRY 加一条
|
|
13
|
-
*/
|
|
14
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
-
};
|
|
17
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.getMigrateConfig = getMigrateConfig;
|
|
19
|
-
exports.listSupportedMigrations = listSupportedMigrations;
|
|
20
|
-
const vite_react_to_nestjs_react_fullstack_1 = __importDefault(require("./vite-react-to-nestjs-react-fullstack"));
|
|
21
|
-
/** key 格式:`<from>:<to>` */
|
|
22
|
-
const MIGRATE_REGISTRY = {
|
|
23
|
-
'vite-react:nestjs-react-fullstack': vite_react_to_nestjs_react_fullstack_1.default,
|
|
24
|
-
};
|
|
25
|
-
/** 返回 (from, to) 对应的 MigrateConfig;未注册时返回 null。 */
|
|
26
|
-
function getMigrateConfig(from, to) {
|
|
27
|
-
return MIGRATE_REGISTRY[`${from}:${to}`] ?? null;
|
|
28
|
-
}
|
|
29
|
-
/** 列出已支持的迁移路径,给 handler / 命令 help 用 */
|
|
30
|
-
function listSupportedMigrations() {
|
|
31
|
-
return Object.keys(MIGRATE_REGISTRY).map((key) => {
|
|
32
|
-
const [from, to] = key.split(':');
|
|
33
|
-
return { from, to };
|
|
34
|
-
});
|
|
35
|
-
}
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* vite-react → nestjs-react-fullstack 的跨 stack 迁移规则。
|
|
4
|
-
*
|
|
5
|
-
* 设计原则:**最小改动**。只动 fullstack 形态必须的文件 / package.json key,user 已有
|
|
6
|
-
* 的 vite.config / tsconfig / eslint / prettier / tailwind / postcss / components.json
|
|
7
|
-
* 等业务配置一律**保留**,不被 template 整体覆盖。
|
|
8
|
-
*
|
|
9
|
-
* 步骤:
|
|
10
|
-
* 1. src/ public/ 搬到 client/ 下(fullstack 布局)
|
|
11
|
-
* 2. 清掉 vite-react 时代的根 index.html / eslint.config.mjs / package-lock.json
|
|
12
|
-
* 3. 删 user package.json 里 lite SDK 相关 key(保留 coding-preset-vite-react)
|
|
13
|
-
* 4. 把 NestJS server runtime 拷进来(server/ + nest-cli.json + tsconfig.node.json
|
|
14
|
-
* + client/index.html 等 fullstack 形态新增的资产)
|
|
15
|
-
* 5. 把 fullstack 必需的 scripts 加到 package.json(dev / build / type:check 等)
|
|
16
|
-
* 6. 从 fullstack template/package.json 取版本号,精确加 NestJS 强依赖白名单
|
|
17
|
-
* (client-toolkit + 几个 @nestjs/* + fullstack-nestjs-core + hbs 等;不全量 merge)
|
|
18
|
-
* 7. codemod 业务代码:@lark-apaas/client-toolkit-lite → @lark-apaas/client-toolkit
|
|
19
|
-
* 8. codemod vite.config:defineConfig(...) → defineConfig(..., { fullstack: true }),
|
|
20
|
-
* 让 coding-preset-vite-react 启用 fullstack dev-proxy / html-output / basename /
|
|
21
|
-
* server-log 4 件套,vite 8080 反代 HTML / API 到 nest 3000
|
|
22
|
-
* 9. set-stack 切 .spark/meta.json
|
|
23
|
-
*
|
|
24
|
-
* 跟 SDK 的协作:client-toolkit@1.2.52+ 顶层 single-entry 完整 re-export 了
|
|
25
|
-
* client-toolkit-lite 全部 48 项 API(fullstack-plugin#1124),所以 codemod 只换
|
|
26
|
-
* 包名、specifier 列表不动就够。
|
|
27
|
-
*/
|
|
28
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.MIGRATE_CONFIG = void 0;
|
|
30
|
-
exports.MIGRATE_CONFIG = {
|
|
31
|
-
from: 'vite-react',
|
|
32
|
-
to: 'nestjs-react-fullstack',
|
|
33
|
-
rules: [
|
|
34
|
-
// ===== 1. 用户领地搬迁 =====
|
|
35
|
-
{ type: 'move-directory', from: 'src', to: 'client/src' },
|
|
36
|
-
{ type: 'move-directory', from: 'public', to: 'client/public' },
|
|
37
|
-
// shared/ 不动(两边布局相同);.env / .spark_project 同理。
|
|
38
|
-
// ===== 2. 清理 vite-react 残留 =====
|
|
39
|
-
// 根 index.html 被 client/index.html 取代(fullstack 用 HBS 渲染)
|
|
40
|
-
{ type: 'delete-file', to: 'index.html' },
|
|
41
|
-
// ESLint 9 flat config 文件名变了(user eslint.config.mjs → eslint.config.js)
|
|
42
|
-
{ type: 'delete-file', to: 'eslint.config.mjs' },
|
|
43
|
-
// 依赖集合变了,lockfile 重新生成
|
|
44
|
-
{ type: 'delete-file', to: 'package-lock.json' },
|
|
45
|
-
// ===== 3. 从 user package.json 删 vite-react 专属字段 =====
|
|
46
|
-
// 注意保留 devDependencies.@lark-apaas/coding-preset-vite-react —— user 迁移后
|
|
47
|
-
// vite.config 仍用它(开 fullstack 模式即可),不切到 fullstack-vite-preset。
|
|
48
|
-
{
|
|
49
|
-
type: 'delete-json-keys',
|
|
50
|
-
to: 'package.json',
|
|
51
|
-
keys: [
|
|
52
|
-
// 顶层 "type": "module" —— vite-react ESM 入口约定,nestjs CommonJS 不需要
|
|
53
|
-
'type',
|
|
54
|
-
// lite SDK:codemod 后 import 走 client-toolkit 顶层
|
|
55
|
-
'dependencies.@lark-apaas/client-toolkit-lite',
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
// ===== 4. NestJS server runtime 资产(新增,不覆盖 user 已有同名文件) =====
|
|
59
|
-
// server/ 整目录:main.ts / app.module.ts / common/ / modules/view/ 等(fullstack 必需)
|
|
60
|
-
{ type: 'directory', from: 'server', to: 'server', overwrite: true },
|
|
61
|
-
// nest-cli.json:nest build 入口
|
|
62
|
-
{ type: 'file', from: 'nest-cli.json', to: 'nest-cli.json', overwrite: true },
|
|
63
|
-
// tsconfig.node.json:server 端 ts 编译配置
|
|
64
|
-
{ type: 'file', from: 'tsconfig.node.json', to: 'tsconfig.node.json', overwrite: true },
|
|
65
|
-
// client/index.html:HBS 模板(fullstack 用,vite-react 用根 index.html 已删)
|
|
66
|
-
{ type: 'file', from: 'client/index.html', to: 'client/index.html', overwrite: true },
|
|
67
|
-
// scripts/dev.sh + build.sh:并发起 nest + vite 的脚本(fullstack 形态必需)
|
|
68
|
-
{ type: 'file', from: 'scripts/dev.sh', to: 'scripts/dev.sh', overwrite: true },
|
|
69
|
-
{ type: 'file', from: 'scripts/build.sh', to: 'scripts/build.sh', overwrite: true },
|
|
70
|
-
// .githooks(fallback,user 已有则保留)
|
|
71
|
-
{ type: 'directory', from: '.githooks', to: '.githooks', overwrite: false },
|
|
72
|
-
// shared/ fallback(user 已有同名文件保留,template 新增文件加入)
|
|
73
|
-
{ type: 'directory', from: 'shared', to: 'shared', overwrite: false },
|
|
74
|
-
// .env fallback(user 没有时用 template 默认)
|
|
75
|
-
{ type: 'file', from: '.env', to: '.env', overwrite: false },
|
|
76
|
-
// ===== 5. 加 fullstack 形态必需的 scripts =====
|
|
77
|
-
// 这些 scripts 用 user 现有的可能性低(vite-react 没有 nest 相关 script),直接覆盖
|
|
78
|
-
{
|
|
79
|
-
type: 'add-script',
|
|
80
|
-
name: 'dev',
|
|
81
|
-
command: './scripts/dev.sh',
|
|
82
|
-
overwrite: true,
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
type: 'add-script',
|
|
86
|
-
name: 'dev:server',
|
|
87
|
-
command: 'NODE_ENV=development nest start --watch',
|
|
88
|
-
overwrite: true,
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
type: 'add-script',
|
|
92
|
-
name: 'dev:client',
|
|
93
|
-
command: 'NODE_ENV=development vite --config vite.config.ts',
|
|
94
|
-
overwrite: true,
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
type: 'add-script',
|
|
98
|
-
name: 'build',
|
|
99
|
-
command: './scripts/build.sh',
|
|
100
|
-
overwrite: true,
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
type: 'add-script',
|
|
104
|
-
name: 'build:server',
|
|
105
|
-
command: 'NODE_ENV=production nest build',
|
|
106
|
-
overwrite: true,
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
type: 'add-script',
|
|
110
|
-
name: 'build:client',
|
|
111
|
-
command: 'NODE_ENV=production vite build --config vite.config.ts',
|
|
112
|
-
overwrite: true,
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
type: 'add-script',
|
|
116
|
-
name: 'build:prod',
|
|
117
|
-
command: 'npm run build:server && npm run build:client',
|
|
118
|
-
overwrite: true,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
type: 'add-script',
|
|
122
|
-
name: 'start',
|
|
123
|
-
command: 'NODE_ENV=production node main.js',
|
|
124
|
-
overwrite: true,
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
type: 'add-script',
|
|
128
|
-
name: 'type:check:server',
|
|
129
|
-
command: 'tsc --noEmit --project tsconfig.node.json',
|
|
130
|
-
overwrite: true,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
type: 'add-script',
|
|
134
|
-
name: 'type:check:client',
|
|
135
|
-
command: 'tsc --noEmit --project tsconfig.app.json',
|
|
136
|
-
overwrite: true,
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
type: 'add-script',
|
|
140
|
-
name: 'type:check',
|
|
141
|
-
command: 'concurrently -n "server,client" -c "blue,green" "npm run type:check:server" "npm run type:check:client"',
|
|
142
|
-
overwrite: true,
|
|
143
|
-
},
|
|
144
|
-
// ===== 6. 从 fullstack template 取版本号加 NestJS 强依赖(精确白名单) =====
|
|
145
|
-
// 不走 merge-json(template 130+ devDeps 全 merge 会污染 user 业务依赖集合);
|
|
146
|
-
// 白名单只列 fullstack 启动能跑起来必需的最小集。
|
|
147
|
-
{
|
|
148
|
-
type: 'add-deps-from-template',
|
|
149
|
-
from: 'package.json',
|
|
150
|
-
to: 'package.json',
|
|
151
|
-
dependencies: [
|
|
152
|
-
// NestJS server 核心
|
|
153
|
-
'@nestjs/core',
|
|
154
|
-
'@nestjs/common',
|
|
155
|
-
'@nestjs/platform-express',
|
|
156
|
-
'@nestjs/config',
|
|
157
|
-
'@nestjs/axios',
|
|
158
|
-
'@nestjs/swagger',
|
|
159
|
-
// 妙搭 fullstack server SDK(main.ts / view module 等都依赖)
|
|
160
|
-
'@lark-apaas/fullstack-nestjs-core',
|
|
161
|
-
// HBS 模板引擎(client/index.html 渲染)
|
|
162
|
-
'hbs',
|
|
163
|
-
// NestJS runtime 必需
|
|
164
|
-
'reflect-metadata',
|
|
165
|
-
'rxjs',
|
|
166
|
-
'tslib',
|
|
167
|
-
// DTO 校验 / 转换
|
|
168
|
-
'class-transformer',
|
|
169
|
-
'class-validator',
|
|
170
|
-
// .env 加载(scripts/dev.sh 用到 dotenv-cli 风格)
|
|
171
|
-
'dotenv',
|
|
172
|
-
// client-toolkit:替代 lite,业务代码 codemod 后走它
|
|
173
|
-
'@lark-apaas/client-toolkit',
|
|
174
|
-
],
|
|
175
|
-
devDependencies: [
|
|
176
|
-
// nest start / nest build 必需
|
|
177
|
-
'@nestjs/cli',
|
|
178
|
-
'@nestjs/testing',
|
|
179
|
-
// server ts 编译
|
|
180
|
-
'ts-node',
|
|
181
|
-
'@types/express',
|
|
182
|
-
'@types/hbs',
|
|
183
|
-
'@types/node',
|
|
184
|
-
// 并发跑 nest + vite(scripts/dev.sh 用 concurrently)
|
|
185
|
-
'concurrently',
|
|
186
|
-
],
|
|
187
|
-
},
|
|
188
|
-
// ===== 7. 业务代码 codemod:lite → client-toolkit 包名替换 =====
|
|
189
|
-
// client-toolkit@1.2.52+ 顶层完整 re-export lite 全部 48 项,specifier 列表不动。
|
|
190
|
-
// 同时扫 shared/(client/server 共享代码也可能引 lite 类型 / utils)。
|
|
191
|
-
{ type: 'codemod-client-toolkit-lite', root: 'client/src' },
|
|
192
|
-
{ type: 'codemod-client-toolkit-lite', root: 'shared' },
|
|
193
|
-
// ===== 8. vite.config codemod:加 { fullstack: true } 第二参 =====
|
|
194
|
-
// user vite.config 现在是 defineConfig({...}) 单参;fullstack 形态需要 vite 8080
|
|
195
|
-
// 反代 HTML / API 到 nest 3000,coding-preset-vite-react 用 fullstack option 启用
|
|
196
|
-
// 这一行为(详见 fullstack-plugin coding-preset-vite-react#fullstack)。
|
|
197
|
-
{ type: 'codemod-vite-config-fullstack', to: 'vite.config.ts' },
|
|
198
|
-
// ===== 9. 切 stack =====
|
|
199
|
-
{ type: 'set-stack', stack: 'nestjs-react-fullstack' },
|
|
200
|
-
],
|
|
201
|
-
};
|
|
202
|
-
exports.default = exports.MIGRATE_CONFIG;
|
package/dist/config/migrate.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* MigrateRule 类型定义 —— miaoda app migrate(跨 stack 迁移)的核心抽象。
|
|
4
|
-
*
|
|
5
|
-
* 跟 sync 的关系:sync 是**同 stack 内**的版本同步(脚手架更新、平台依赖钉版本);
|
|
6
|
-
* migrate 是**跨 stack 切换**(如 vite-react → nestjs-react-fullstack)。
|
|
7
|
-
*
|
|
8
|
-
* 复用:MigrateRule 把 SyncRule 全部纳入联合类型(10 种)—— migrate 也要做覆盖 / 删除 /
|
|
9
|
-
* merge-json / patch-script 等动作,没必要重新定义。
|
|
10
|
-
*
|
|
11
|
-
* 新增:MoveRule 和 SetStackRule 是 migrate 专有的语义,sync 用不上:
|
|
12
|
-
* - MoveRule:把用户领地从老布局搬到新布局(如 src/ → client/src/),完成后源消失。
|
|
13
|
-
* - SetStackRule:把 .spark/meta.json 的 stack 字段切到新 stack,migrate 必跑的收尾。
|
|
14
|
-
*/
|
|
15
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Codemod:把 `@lark-apaas/client-toolkit-lite` 的 import 改写到 `@lark-apaas/client-toolkit`。
|
|
4
|
-
*
|
|
5
|
-
* 用于 `miaoda app migrate vite-react → nestjs-react-fullstack`:迁移后让两个包共存属于
|
|
6
|
-
* lib 冗余(两份 react / 两套主题 / 两套 axios),所以扫 user 代码把 lite import 改写到
|
|
7
|
-
* full,然后用 delete-json-keys 移除 lite dep。
|
|
8
|
-
*
|
|
9
|
-
* 实现策略:单行包名替换。
|
|
10
|
-
*
|
|
11
|
-
* client-toolkit 自 v1.2.48 起在顶层 single-entry 完整 re-export 了 client-toolkit-lite
|
|
12
|
-
* 的全部 48 项(components / hooks / utils / integrations / logger / trace / types /
|
|
13
|
-
* constants),跟 lite 同名同签 —— 所以 codemod 只需把 import 来源包名从 `-lite` 换掉,
|
|
14
|
-
* specifier 列表完全不动。CSS `@import` 同理(client-toolkit 顶层加了 ./styles.css 别名)。
|
|
15
|
-
*
|
|
16
|
-
* 边界:
|
|
17
|
-
* - 仅处理形如 `import [type] { A, B as C } from '@lark-apaas/client-toolkit-lite'` 的
|
|
18
|
-
* named import;default import / namespace import / 跨多行 specifier 当前不识别,
|
|
19
|
-
* 但只要 `from '...lite'` 在同一行,包名替换仍有效(regex 抓 from 段)。
|
|
20
|
-
*/
|
|
21
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
|
-
};
|
|
24
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.rewriteLiteImports = rewriteLiteImports;
|
|
26
|
-
exports.rewriteCssText = rewriteCssText;
|
|
27
|
-
exports.rewriteText = rewriteText;
|
|
28
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
29
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
30
|
-
const LITE_PKG = '@lark-apaas/client-toolkit-lite';
|
|
31
|
-
const FULL_PKG = '@lark-apaas/client-toolkit';
|
|
32
|
-
/**
|
|
33
|
-
* 递归扫描 rootDir 下的 .ts/.tsx/.js/.jsx/.css 文件,把 lite import 改写到 full。
|
|
34
|
-
*/
|
|
35
|
-
function rewriteLiteImports(rootDir) {
|
|
36
|
-
const result = {
|
|
37
|
-
filesScanned: 0,
|
|
38
|
-
filesChanged: [],
|
|
39
|
-
rewrittenSymbols: 0,
|
|
40
|
-
unmapped: [],
|
|
41
|
-
};
|
|
42
|
-
if (!node_fs_1.default.existsSync(rootDir))
|
|
43
|
-
return result;
|
|
44
|
-
for (const file of walk(rootDir)) {
|
|
45
|
-
const isJs = /\.(ts|tsx|js|jsx)$/.test(file);
|
|
46
|
-
const isCss = file.endsWith('.css');
|
|
47
|
-
if (!isJs && !isCss)
|
|
48
|
-
continue;
|
|
49
|
-
result.filesScanned++;
|
|
50
|
-
const before = node_fs_1.default.readFileSync(file, 'utf-8');
|
|
51
|
-
if (!before.includes(LITE_PKG))
|
|
52
|
-
continue;
|
|
53
|
-
const rel = node_path_1.default.relative(rootDir, file);
|
|
54
|
-
const { text, count } = isJs ? rewriteText(before, rel) : rewriteCssText(before, rel);
|
|
55
|
-
if (count > 0) {
|
|
56
|
-
node_fs_1.default.writeFileSync(file, text);
|
|
57
|
-
result.filesChanged.push(rel);
|
|
58
|
-
result.rewrittenSymbols += count;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return result;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* css 改写器:单行包名替换。`@import "@lark-apaas/client-toolkit-lite/<path>"` →
|
|
65
|
-
* `@import "@lark-apaas/client-toolkit/<path>"`。其中 styles.css 在 full 端已加同名别名
|
|
66
|
-
* 指向 ./lib/index.css。
|
|
67
|
-
*/
|
|
68
|
-
function rewriteCssText(source, _fileLabel) {
|
|
69
|
-
let count = 0;
|
|
70
|
-
const re = /(@import\s+['"])@lark-apaas\/client-toolkit-lite(\/[^'"]*)?(['"])/g;
|
|
71
|
-
const text = source.replace(re, (_match, p1, sub, p3) => {
|
|
72
|
-
count++;
|
|
73
|
-
return `${p1}${FULL_PKG}${sub ?? ''}${p3}`;
|
|
74
|
-
});
|
|
75
|
-
return { text, count, unmapped: [] };
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* JS/TS 文件改写器:把任何 `from '@lark-apaas/client-toolkit-lite'` 改成 full 包名。
|
|
79
|
-
* specifier 列表完全保留 —— full 顶层 single-entry 已经兼容 lite 全部 48 项 export。
|
|
80
|
-
*
|
|
81
|
-
* 同样兼容 dynamic import / require / `export ... from` 等其他形式(只要包名字符串在)。
|
|
82
|
-
*/
|
|
83
|
-
function rewriteText(source, _fileLabel) {
|
|
84
|
-
let count = 0;
|
|
85
|
-
// 既匹配 `from 'pkg'` 也匹配 `from "pkg"`,单双引号都管。
|
|
86
|
-
// 同时覆盖 `import('pkg')` / `require('pkg')` 等其他常见形态。
|
|
87
|
-
const re = /(['"])@lark-apaas\/client-toolkit-lite(['"])/g;
|
|
88
|
-
const text = source.replace(re, (_match, p1, p2) => {
|
|
89
|
-
count++;
|
|
90
|
-
return `${p1}${FULL_PKG}${p2}`;
|
|
91
|
-
});
|
|
92
|
-
return { text, count, unmapped: [] };
|
|
93
|
-
}
|
|
94
|
-
function* walk(dir) {
|
|
95
|
-
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
96
|
-
if (entry.name === 'node_modules' || entry.name.startsWith('.'))
|
|
97
|
-
continue;
|
|
98
|
-
const full = node_path_1.default.join(dir, entry.name);
|
|
99
|
-
if (entry.isDirectory()) {
|
|
100
|
-
yield* walk(full);
|
|
101
|
-
}
|
|
102
|
-
else if (entry.isFile()) {
|
|
103
|
-
yield full;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
@@ -1,507 +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.applyMigrateRules = applyMigrateRules;
|
|
7
|
-
exports.rewriteViteConfigForFullstack = rewriteViteConfigForFullstack;
|
|
8
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
-
const sync_rule_1 = require("./sync-rule");
|
|
11
|
-
const spark_meta_1 = require("./spark-meta");
|
|
12
|
-
const codemod_client_toolkit_lite_1 = require("./codemod-client-toolkit-lite");
|
|
13
|
-
const logger_1 = require("./logger");
|
|
14
|
-
/**
|
|
15
|
-
* 顺序执行 MigrateRule[]:
|
|
16
|
-
* - move-* / set-stack 是 migrate 专有 rule,本模块直接处理。
|
|
17
|
-
* - 其余(directory / file / merge-json / delete-* / add-script / ...)全部委派给
|
|
18
|
-
* applySyncRules —— 单条调用,逻辑跟 sync 完全对齐,避免行为漂移。
|
|
19
|
-
*
|
|
20
|
-
* 一条 rule 报错不影响后续(applySyncRules 内部已有 try/catch;本模块的 applyMove / applySetStack
|
|
21
|
-
* 当前不 catch —— 如果 move 失败说明状态已经被破坏,应当直接抛错让 handler 决定回滚或上报)。
|
|
22
|
-
*/
|
|
23
|
-
function applyMigrateRules(rules, opts) {
|
|
24
|
-
const results = [];
|
|
25
|
-
for (const rule of rules) {
|
|
26
|
-
if (rule.type === 'move-directory' || rule.type === 'move-file') {
|
|
27
|
-
results.push(applyMove(rule, opts));
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (rule.type === 'set-stack') {
|
|
31
|
-
results.push(applySetStack(rule, opts));
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (rule.type === 'delete-json-keys') {
|
|
35
|
-
results.push(applyDeleteJsonKeys(rule, opts));
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (rule.type === 'codemod-client-toolkit-lite') {
|
|
39
|
-
results.push(applyCodemodLite(rule, opts));
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
if (rule.type === 'codemod-vite-config-fullstack') {
|
|
43
|
-
results.push(applyCodemodViteConfigFullstack(rule, opts));
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (rule.type === 'add-deps-from-template') {
|
|
47
|
-
results.push(applyAddDepsFromTemplate(rule, opts));
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
// 前面 if 已经把 migrate 专用 rule 都 narrow 掉,剩下的必然是 SyncRule
|
|
51
|
-
const [syncResult] = (0, sync_rule_1.applySyncRules)([rule], opts);
|
|
52
|
-
results.push(syncResult);
|
|
53
|
-
}
|
|
54
|
-
return results;
|
|
55
|
-
}
|
|
56
|
-
function applyMove(rule, opts) {
|
|
57
|
-
const logPrefix = opts.logPrefix ?? 'migrate';
|
|
58
|
-
const src = node_path_1.default.join(opts.targetDir, rule.from);
|
|
59
|
-
const dest = node_path_1.default.join(opts.targetDir, rule.to);
|
|
60
|
-
const onConflict = rule.onConflict ?? 'merge';
|
|
61
|
-
if (!node_fs_1.default.existsSync(src)) {
|
|
62
|
-
(0, logger_1.log)(logPrefix, ` ○ ${rule.from} (source not found, skip)`);
|
|
63
|
-
return { rule, action: 'skipped', path: rule.to };
|
|
64
|
-
}
|
|
65
|
-
if (node_fs_1.default.existsSync(dest)) {
|
|
66
|
-
if (onConflict === 'skip') {
|
|
67
|
-
(0, logger_1.log)(logPrefix, ` ○ ${rule.to} (already exists, skip per onConflict=skip)`);
|
|
68
|
-
return { rule, action: 'skipped', path: rule.to };
|
|
69
|
-
}
|
|
70
|
-
if (onConflict === 'overwrite') {
|
|
71
|
-
node_fs_1.default.rmSync(dest, { recursive: true, force: true });
|
|
72
|
-
moveAtomic(src, dest);
|
|
73
|
-
(0, logger_1.log)(logPrefix, ` ✓ ${rule.from} → ${rule.to} (overwritten)`);
|
|
74
|
-
return { rule, action: 'moved', path: rule.to, detail: rule.from };
|
|
75
|
-
}
|
|
76
|
-
// 'merge':把 src 的内容递归 overlay 到 dest,源 entry 完成后整体清掉
|
|
77
|
-
mergeOverlay(src, dest);
|
|
78
|
-
node_fs_1.default.rmSync(src, { recursive: true, force: true });
|
|
79
|
-
(0, logger_1.log)(logPrefix, ` ✓ ${rule.from} → ${rule.to} (merged)`);
|
|
80
|
-
return { rule, action: 'moved', path: rule.to, detail: rule.from };
|
|
81
|
-
}
|
|
82
|
-
// 目标不存在 —— 直接 rename
|
|
83
|
-
const destDir = node_path_1.default.dirname(dest);
|
|
84
|
-
if (destDir !== '.' && !node_fs_1.default.existsSync(destDir)) {
|
|
85
|
-
node_fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
86
|
-
}
|
|
87
|
-
moveAtomic(src, dest);
|
|
88
|
-
(0, logger_1.log)(logPrefix, ` ✓ ${rule.from} → ${rule.to}`);
|
|
89
|
-
return { rule, action: 'moved', path: rule.to, detail: rule.from };
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* fs.renameSync 在跨设备 / 跨文件系统时报 EXDEV(Linux/macOS),fallback 到 cp -r + rm。
|
|
93
|
-
* worktree 内通常同一卷不会触发,但 tmp 目录跟 user app 可能不同卷,留这层兜底。
|
|
94
|
-
*/
|
|
95
|
-
function moveAtomic(src, dest) {
|
|
96
|
-
try {
|
|
97
|
-
node_fs_1.default.renameSync(src, dest);
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
if (err.code !== 'EXDEV') {
|
|
101
|
-
throw err;
|
|
102
|
-
}
|
|
103
|
-
node_fs_1.default.cpSync(src, dest, { recursive: true });
|
|
104
|
-
node_fs_1.default.rmSync(src, { recursive: true, force: true });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* 把 src 的每个 entry 复制到 dest 同路径下(src 是文件则覆盖 dest 同名文件;src 是目录则
|
|
109
|
-
* 递归创建 + 复制)。同名冲突一律 src 覆盖 dest。本函数不删 src;调用方负责清理。
|
|
110
|
-
*/
|
|
111
|
-
function mergeOverlay(src, dest) {
|
|
112
|
-
const stat = node_fs_1.default.statSync(src);
|
|
113
|
-
if (stat.isFile()) {
|
|
114
|
-
node_fs_1.default.mkdirSync(node_path_1.default.dirname(dest), { recursive: true });
|
|
115
|
-
node_fs_1.default.copyFileSync(src, dest);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (!node_fs_1.default.existsSync(dest)) {
|
|
119
|
-
node_fs_1.default.mkdirSync(dest, { recursive: true });
|
|
120
|
-
}
|
|
121
|
-
for (const entry of node_fs_1.default.readdirSync(src)) {
|
|
122
|
-
mergeOverlay(node_path_1.default.join(src, entry), node_path_1.default.join(dest, entry));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function applySetStack(rule, opts) {
|
|
126
|
-
const logPrefix = opts.logPrefix ?? 'migrate';
|
|
127
|
-
(0, spark_meta_1.writeSparkMeta)(opts.targetDir, { stack: rule.stack });
|
|
128
|
-
(0, logger_1.log)(logPrefix, ` ✓ .spark/meta.json stack → ${rule.stack}`);
|
|
129
|
-
return { rule, action: 'patched', path: '.spark/meta.json', detail: rule.stack };
|
|
130
|
-
}
|
|
131
|
-
function applyDeleteJsonKeys(rule, opts) {
|
|
132
|
-
const logPrefix = opts.logPrefix ?? 'migrate';
|
|
133
|
-
const dest = node_path_1.default.join(opts.targetDir, rule.to);
|
|
134
|
-
if (!node_fs_1.default.existsSync(dest)) {
|
|
135
|
-
(0, logger_1.log)(logPrefix, ` ○ ${rule.to} (not found, skip delete-json-keys)`);
|
|
136
|
-
return { rule, action: 'skipped', path: rule.to };
|
|
137
|
-
}
|
|
138
|
-
const text = node_fs_1.default.readFileSync(dest, 'utf-8');
|
|
139
|
-
const json = JSON.parse(text);
|
|
140
|
-
const deleted = [];
|
|
141
|
-
for (const dotKey of rule.keys) {
|
|
142
|
-
if (deleteAtDotPath(json, dotKey))
|
|
143
|
-
deleted.push(dotKey);
|
|
144
|
-
}
|
|
145
|
-
if (deleted.length === 0) {
|
|
146
|
-
(0, logger_1.log)(logPrefix, ` ○ ${rule.to} (no listed keys present)`);
|
|
147
|
-
return { rule, action: 'noop', path: rule.to };
|
|
148
|
-
}
|
|
149
|
-
// 保持原文件末尾换行约定(package.json 一般以 \n 结尾)
|
|
150
|
-
const trailing = text.endsWith('\n') ? '\n' : '';
|
|
151
|
-
node_fs_1.default.writeFileSync(dest, JSON.stringify(json, null, 2) + trailing);
|
|
152
|
-
(0, logger_1.log)(logPrefix, ` ✓ ${rule.to} (deleted ${deleted.length.toString()} key(s): ${deleted.join(', ')})`);
|
|
153
|
-
return { rule, action: 'patched', path: rule.to, detail: deleted.join(',') };
|
|
154
|
-
}
|
|
155
|
-
function applyCodemodLite(rule, opts) {
|
|
156
|
-
const logPrefix = opts.logPrefix ?? 'migrate';
|
|
157
|
-
const root = node_path_1.default.join(opts.targetDir, rule.root ?? 'client/src');
|
|
158
|
-
const r = (0, codemod_client_toolkit_lite_1.rewriteLiteImports)(root);
|
|
159
|
-
if (r.filesChanged.length === 0 && r.unmapped.length === 0) {
|
|
160
|
-
(0, logger_1.log)(logPrefix, ` ○ ${rule.root ?? 'client/src'} (no @lark-apaas/client-toolkit-lite imports found)`);
|
|
161
|
-
return { rule, action: 'noop', path: rule.root ?? 'client/src' };
|
|
162
|
-
}
|
|
163
|
-
const detail = `${r.rewrittenSymbols.toString()} symbol(s) in ${r.filesChanged.length.toString()} file(s)` +
|
|
164
|
-
(r.unmapped.length > 0 ? `; ${r.unmapped.length.toString()} unmapped` : '');
|
|
165
|
-
(0, logger_1.log)(logPrefix, ` ✓ ${rule.root ?? 'client/src'} (codemod: ${detail})`);
|
|
166
|
-
for (const u of r.unmapped) {
|
|
167
|
-
(0, logger_1.log)(logPrefix, ` ⚠ unmapped: ${u.file}:${u.line.toString()} '${u.symbol}'`);
|
|
168
|
-
}
|
|
169
|
-
return {
|
|
170
|
-
rule,
|
|
171
|
-
action: r.rewrittenSymbols > 0 ? 'patched' : 'noop',
|
|
172
|
-
path: rule.root ?? 'client/src',
|
|
173
|
-
detail,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
function applyCodemodViteConfigFullstack(rule, opts) {
|
|
177
|
-
const logPrefix = opts.logPrefix ?? 'migrate';
|
|
178
|
-
const rel = rule.to ?? 'vite.config.ts';
|
|
179
|
-
const dest = node_path_1.default.join(opts.targetDir, rel);
|
|
180
|
-
if (!node_fs_1.default.existsSync(dest)) {
|
|
181
|
-
(0, logger_1.log)(logPrefix, ` ○ ${rel} (not found, skip)`);
|
|
182
|
-
return { rule, action: 'skipped', path: rel };
|
|
183
|
-
}
|
|
184
|
-
const before = node_fs_1.default.readFileSync(dest, 'utf-8');
|
|
185
|
-
const { text, changed, alreadyDone } = rewriteViteConfigForFullstack(before);
|
|
186
|
-
if (alreadyDone) {
|
|
187
|
-
(0, logger_1.log)(logPrefix, ` ○ ${rel} (fullstack: true already present)`);
|
|
188
|
-
return { rule, action: 'noop', path: rel };
|
|
189
|
-
}
|
|
190
|
-
if (!changed) {
|
|
191
|
-
(0, logger_1.log)(logPrefix, ` ⚠ ${rel} (defineConfig call not found; please add { fullstack: true } manually)`);
|
|
192
|
-
return { rule, action: 'noop', path: rel, detail: 'defineConfig call not found' };
|
|
193
|
-
}
|
|
194
|
-
node_fs_1.default.writeFileSync(dest, text);
|
|
195
|
-
(0, logger_1.log)(logPrefix, ` ✓ ${rel} (added { fullstack: true } to defineConfig)`);
|
|
196
|
-
return { rule, action: 'patched', path: rel };
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* 给 vite.config.ts 的 `defineConfig(...)` 调用加上 `{ fullstack: true }` 第二参。
|
|
200
|
-
*
|
|
201
|
-
* 支持的形态:
|
|
202
|
-
* defineConfig({ ... }) → defineConfig({ ... }, { fullstack: true })
|
|
203
|
-
* defineConfig({ ... }, { ... }) → 已有第二参,把 fullstack: true 注入对象字面量
|
|
204
|
-
* defineConfig() → defineConfig({}, { fullstack: true })
|
|
205
|
-
*
|
|
206
|
-
* 跨多行 + 嵌套 arrow function / 字符串 / 模板字符串都要保留,所以用括号匹配而非正则。
|
|
207
|
-
* 已经含 `fullstack: true` 字面量则跳过(视为已迁移)。
|
|
208
|
-
*/
|
|
209
|
-
function rewriteViteConfigForFullstack(source) {
|
|
210
|
-
const callRe = /defineConfig\s*\(/g;
|
|
211
|
-
const m = callRe.exec(source);
|
|
212
|
-
if (m === null) {
|
|
213
|
-
return { text: source, changed: false, alreadyDone: false };
|
|
214
|
-
}
|
|
215
|
-
const openIdx = m.index + m[0].length - 1; // 指向 '('
|
|
216
|
-
const closeIdx = findMatchingParen(source, openIdx);
|
|
217
|
-
if (closeIdx < 0) {
|
|
218
|
-
return { text: source, changed: false, alreadyDone: false };
|
|
219
|
-
}
|
|
220
|
-
const argsRaw = source.slice(openIdx + 1, closeIdx);
|
|
221
|
-
if (/fullstack\s*:\s*true/.test(argsRaw)) {
|
|
222
|
-
return { text: source, changed: false, alreadyDone: true };
|
|
223
|
-
}
|
|
224
|
-
const { firstArgEnd, secondArgRange } = splitFirstTwoArgs(source, openIdx + 1, closeIdx);
|
|
225
|
-
if (secondArgRange === null) {
|
|
226
|
-
// 单参 / 无参:在第一参之后插 ", { fullstack: true }"
|
|
227
|
-
const insertAt = firstArgEnd ?? openIdx + 1;
|
|
228
|
-
const head = source.slice(0, insertAt);
|
|
229
|
-
const tail = source.slice(insertAt, closeIdx);
|
|
230
|
-
// 第一参是否非空(去空白)
|
|
231
|
-
const arg1 = source.slice(openIdx + 1, insertAt).trim();
|
|
232
|
-
const sep = arg1.length === 0 ? '{}, { fullstack: true }' : ', { fullstack: true }';
|
|
233
|
-
return {
|
|
234
|
-
text: head + (arg1.length === 0 ? sep : sep) + tail + source.slice(closeIdx),
|
|
235
|
-
changed: true,
|
|
236
|
-
alreadyDone: false,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
// 双参:在第二参对象 `{` 之后插 ` fullstack: true,`
|
|
240
|
-
const arg2 = source.slice(secondArgRange.start, secondArgRange.end);
|
|
241
|
-
const objOpen = arg2.indexOf('{');
|
|
242
|
-
if (objOpen < 0) {
|
|
243
|
-
// 第二参不是对象字面量(罕见),保守跳过
|
|
244
|
-
return { text: source, changed: false, alreadyDone: false };
|
|
245
|
-
}
|
|
246
|
-
const absObjOpen = secondArgRange.start + objOpen;
|
|
247
|
-
const inserted = '\n fullstack: true,';
|
|
248
|
-
return {
|
|
249
|
-
text: source.slice(0, absObjOpen + 1) + inserted + source.slice(absObjOpen + 1),
|
|
250
|
-
changed: true,
|
|
251
|
-
alreadyDone: false,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* 从 openIdx(指向 `(`)开始扫到匹配的 `)`,跳过 string / template / 括号嵌套。
|
|
256
|
-
* 找不到匹配返回 -1。
|
|
257
|
-
*/
|
|
258
|
-
function findMatchingParen(text, openIdx) {
|
|
259
|
-
let depth = 0;
|
|
260
|
-
let i = openIdx;
|
|
261
|
-
let inSingle = false;
|
|
262
|
-
let inDouble = false;
|
|
263
|
-
let inTpl = false;
|
|
264
|
-
let inLineComment = false;
|
|
265
|
-
let inBlockComment = false;
|
|
266
|
-
while (i < text.length) {
|
|
267
|
-
const c = text[i];
|
|
268
|
-
const next = i + 1 < text.length ? text[i + 1] : '';
|
|
269
|
-
if (inLineComment) {
|
|
270
|
-
if (c === '\n')
|
|
271
|
-
inLineComment = false;
|
|
272
|
-
i++;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (inBlockComment) {
|
|
276
|
-
if (c === '*' && next === '/') {
|
|
277
|
-
inBlockComment = false;
|
|
278
|
-
i += 2;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
i++;
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (inSingle) {
|
|
285
|
-
if (c === '\\')
|
|
286
|
-
i += 2;
|
|
287
|
-
else if (c === "'") {
|
|
288
|
-
inSingle = false;
|
|
289
|
-
i++;
|
|
290
|
-
}
|
|
291
|
-
else
|
|
292
|
-
i++;
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
if (inDouble) {
|
|
296
|
-
if (c === '\\')
|
|
297
|
-
i += 2;
|
|
298
|
-
else if (c === '"') {
|
|
299
|
-
inDouble = false;
|
|
300
|
-
i++;
|
|
301
|
-
}
|
|
302
|
-
else
|
|
303
|
-
i++;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
if (inTpl) {
|
|
307
|
-
if (c === '\\')
|
|
308
|
-
i += 2;
|
|
309
|
-
else if (c === '`') {
|
|
310
|
-
inTpl = false;
|
|
311
|
-
i++;
|
|
312
|
-
}
|
|
313
|
-
else
|
|
314
|
-
i++;
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
if (c === '/' && next === '/') {
|
|
318
|
-
inLineComment = true;
|
|
319
|
-
i += 2;
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
if (c === '/' && next === '*') {
|
|
323
|
-
inBlockComment = true;
|
|
324
|
-
i += 2;
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
if (c === "'") {
|
|
328
|
-
inSingle = true;
|
|
329
|
-
i++;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
if (c === '"') {
|
|
333
|
-
inDouble = true;
|
|
334
|
-
i++;
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
if (c === '`') {
|
|
338
|
-
inTpl = true;
|
|
339
|
-
i++;
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
if (c === '(' || c === '{' || c === '[') {
|
|
343
|
-
depth++;
|
|
344
|
-
i++;
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
if (c === ')' || c === '}' || c === ']') {
|
|
348
|
-
depth--;
|
|
349
|
-
if (depth === 0 && c === ')')
|
|
350
|
-
return i;
|
|
351
|
-
i++;
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
i++;
|
|
355
|
-
}
|
|
356
|
-
return -1;
|
|
357
|
-
}
|
|
358
|
-
/**
|
|
359
|
-
* 在 `argsStart`(`(` 之后第一字符)到 `argsEnd`(`)` 位置)范围内识别顶层 `,`,
|
|
360
|
-
* 拿到第一参结束位置和(如果存在)第二参的范围。
|
|
361
|
-
*/
|
|
362
|
-
function splitFirstTwoArgs(text, argsStart, argsEnd) {
|
|
363
|
-
let depth = 0;
|
|
364
|
-
let i = argsStart;
|
|
365
|
-
let inSingle = false;
|
|
366
|
-
let inDouble = false;
|
|
367
|
-
let inTpl = false;
|
|
368
|
-
let firstComma = -1;
|
|
369
|
-
while (i < argsEnd) {
|
|
370
|
-
const c = text[i];
|
|
371
|
-
if (inSingle) {
|
|
372
|
-
if (c === '\\') {
|
|
373
|
-
i += 2;
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
if (c === "'")
|
|
377
|
-
inSingle = false;
|
|
378
|
-
i++;
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
if (inDouble) {
|
|
382
|
-
if (c === '\\') {
|
|
383
|
-
i += 2;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
if (c === '"')
|
|
387
|
-
inDouble = false;
|
|
388
|
-
i++;
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
if (inTpl) {
|
|
392
|
-
if (c === '\\') {
|
|
393
|
-
i += 2;
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (c === '`')
|
|
397
|
-
inTpl = false;
|
|
398
|
-
i++;
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
if (c === "'") {
|
|
402
|
-
inSingle = true;
|
|
403
|
-
i++;
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
if (c === '"') {
|
|
407
|
-
inDouble = true;
|
|
408
|
-
i++;
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
if (c === '`') {
|
|
412
|
-
inTpl = true;
|
|
413
|
-
i++;
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
if (c === '(' || c === '{' || c === '[') {
|
|
417
|
-
depth++;
|
|
418
|
-
i++;
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (c === ')' || c === '}' || c === ']') {
|
|
422
|
-
depth--;
|
|
423
|
-
i++;
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
if (c === ',' && depth === 0) {
|
|
427
|
-
firstComma = i;
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
i++;
|
|
431
|
-
}
|
|
432
|
-
if (firstComma < 0) {
|
|
433
|
-
return { firstArgEnd: argsEnd, secondArgRange: null };
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
firstArgEnd: firstComma,
|
|
437
|
-
secondArgRange: { start: firstComma + 1, end: argsEnd },
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
function applyAddDepsFromTemplate(rule, opts) {
|
|
441
|
-
const logPrefix = opts.logPrefix ?? 'migrate';
|
|
442
|
-
const userPath = node_path_1.default.join(opts.targetDir, rule.to ?? 'package.json');
|
|
443
|
-
const tplPath = node_path_1.default.join(opts.sourceRoot, rule.from ?? 'package.json');
|
|
444
|
-
if (!node_fs_1.default.existsSync(userPath)) {
|
|
445
|
-
(0, logger_1.log)(logPrefix, ` ○ ${rule.to ?? 'package.json'} (not found, skip add-deps-from-template)`);
|
|
446
|
-
return { rule, action: 'skipped', path: rule.to ?? 'package.json' };
|
|
447
|
-
}
|
|
448
|
-
if (!node_fs_1.default.existsSync(tplPath)) {
|
|
449
|
-
(0, logger_1.log)(logPrefix, ` ⚠ template ${rule.from ?? 'package.json'} not found, skip add-deps-from-template`);
|
|
450
|
-
return { rule, action: 'skipped', path: rule.to ?? 'package.json' };
|
|
451
|
-
}
|
|
452
|
-
const userText = node_fs_1.default.readFileSync(userPath, 'utf-8');
|
|
453
|
-
const userJson = JSON.parse(userText);
|
|
454
|
-
const tplJson = JSON.parse(node_fs_1.default.readFileSync(tplPath, 'utf-8'));
|
|
455
|
-
const added = [];
|
|
456
|
-
const missingInTpl = [];
|
|
457
|
-
for (const grp of ['dependencies', 'devDependencies']) {
|
|
458
|
-
const whitelist = rule[grp] ?? [];
|
|
459
|
-
if (whitelist.length === 0)
|
|
460
|
-
continue;
|
|
461
|
-
userJson[grp] ??= {};
|
|
462
|
-
for (const dep of whitelist) {
|
|
463
|
-
// 在 template 里找版本号:优先 tpl 同 grp,其次另一 grp(template 可能把 dep 写在
|
|
464
|
-
// dependencies/devDependencies 任一处,跟 user 当前 grp 不一定一致)
|
|
465
|
-
const tplVersion = tplJson[grp]?.[dep] ??
|
|
466
|
-
(grp === 'dependencies' ? tplJson.devDependencies?.[dep] : tplJson.dependencies?.[dep]);
|
|
467
|
-
if (tplVersion === undefined) {
|
|
468
|
-
missingInTpl.push(dep);
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
userJson[grp][dep] = tplVersion;
|
|
472
|
-
added.push(`${grp}.${dep}`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
if (added.length === 0 && missingInTpl.length === 0) {
|
|
476
|
-
(0, logger_1.log)(logPrefix, ` ○ ${rule.to ?? 'package.json'} (no deps to add)`);
|
|
477
|
-
return { rule, action: 'noop', path: rule.to ?? 'package.json' };
|
|
478
|
-
}
|
|
479
|
-
const trailing = userText.endsWith('\n') ? '\n' : '';
|
|
480
|
-
node_fs_1.default.writeFileSync(userPath, JSON.stringify(userJson, null, 2) + trailing);
|
|
481
|
-
(0, logger_1.log)(logPrefix, ` ✓ ${rule.to ?? 'package.json'} (added ${added.length.toString()} dep(s)${missingInTpl.length > 0 ? `; ${missingInTpl.length.toString()} missing in template: ${missingInTpl.join(', ')}` : ''})`);
|
|
482
|
-
return {
|
|
483
|
-
rule,
|
|
484
|
-
action: 'patched',
|
|
485
|
-
path: rule.to ?? 'package.json',
|
|
486
|
-
detail: added.join(','),
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* 按 dot path 删除嵌套对象里的一个 leaf key。返回是否真的删了一项。
|
|
491
|
-
* 不支持包含 `.` 的字段名(实际 dep 名 / scripts 名不会含 `.`,够用)。
|
|
492
|
-
*/
|
|
493
|
-
function deleteAtDotPath(obj, dotPath) {
|
|
494
|
-
const parts = dotPath.split('.');
|
|
495
|
-
let cursor = obj;
|
|
496
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
497
|
-
const next = cursor[parts[i]];
|
|
498
|
-
if (next === undefined || next === null || typeof next !== 'object')
|
|
499
|
-
return false;
|
|
500
|
-
cursor = next;
|
|
501
|
-
}
|
|
502
|
-
const leaf = parts[parts.length - 1];
|
|
503
|
-
if (!Object.prototype.hasOwnProperty.call(cursor, leaf))
|
|
504
|
-
return false;
|
|
505
|
-
// 用 Reflect.deleteProperty 避开 eslint 对 `delete obj[computedKey]` 的禁用规则
|
|
506
|
-
return Reflect.deleteProperty(cursor, leaf);
|
|
507
|
-
}
|