@lark-apaas/miaoda-cli 0.1.16-alpha.0 → 0.1.16-alpha.452772a

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.
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerAppCommands = registerAppCommands;
4
+ const commander_1 = require("commander");
4
5
  const shared_1 = require("../../../cli/commands/shared");
5
6
  const index_1 = require("../../../cli/handlers/app/index");
6
7
  const index_2 = require("../../../config/migrate-configs/index");
@@ -30,8 +31,13 @@ function registerAppMigrate(parent) {
30
31
  const supportedDesc = (0, index_2.listSupportedMigrations)()
31
32
  .map((m) => `${m.from} → ${m.to}`)
32
33
  .join(', ');
33
- const cmd = parent
34
- .command('migrate')
34
+ // 渐进式披露:migrate 是给妙搭平台 / 用户主动触发的命令,**不暴露给 Agent**。
35
+ // - `miaoda app -h` / `miaoda app help` 都不列出此命令
36
+ // - 但 `miaoda app migrate --to xxx` 仍能直接调用(仅命令注册时打 hidden 标记,行为不变)
37
+ // - `--help` 也不显示 ——commander hidden 命令仅显式 invoke 时才暴露
38
+ // 用 addCommand(cmd, { hidden: true }) 而非 parent.command(...),因为 commander 13
39
+ // 的 fluent `.command()` 不支持设置 hidden。
40
+ const cmd = new commander_1.Command('migrate')
35
41
  .description('跨 stack 原地迁移:按 MigrateConfig 全套 apply(move + delete + 模板覆盖 + 字段 merge + set-stack)')
36
42
  .requiredOption('--to <stack>', '目标 stack 短名')
37
43
  .option('--from <stack>', '源 stack;默认读 .spark/meta.json 当前 stack')
@@ -40,19 +46,6 @@ function registerAppMigrate(parent) {
40
46
  已支持的迁移路径
41
47
  ${supportedDesc || '(none)'}
42
48
 
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
49
  JSON 输出
57
50
  {"data": {"from": "...", "to": "...",
58
51
  "appliedRules": [{"type": "...", "action": "...", "path": "...", "detail": "..."}],
@@ -72,6 +65,7 @@ JSON 输出
72
65
  to: rawOpts.to,
73
66
  });
74
67
  }));
68
+ parent.addCommand(cmd, { hidden: true });
75
69
  }
76
70
  function registerAppSync(parent) {
77
71
  const cmd = parent
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.handleAppMigrate = handleAppMigrate;
7
+ const node_child_process_1 = require("node:child_process");
7
8
  const node_fs_1 = __importDefault(require("node:fs"));
8
9
  const node_os_1 = __importDefault(require("node:os"));
9
10
  const node_path_1 = __importDefault(require("node:path"));
@@ -30,11 +31,12 @@ const logger_1 = require("../../../utils/logger");
30
31
  * 2. 拿到 (from, to) 对应的 MigrateConfig 与模板源根目录
31
32
  * 3. 顺序执行 rules:move-* → delete-* → delete-json-keys → file/directory →
32
33
  * merge-json → set-stack
33
- * 4. emit 结果;不做 npm install(用户接下来跑 `npm install` 或 `miaoda app sync` 时自然装)
34
+ * 4. 清掉 node_modules(package-lock.json 已经在 rules 里 delete),跑 npm install
35
+ * 物化新依赖集合 —— migrate 改了 dependencies / devDependencies 集合(删 lite +
36
+ * 加 NestJS 全套),不重装就跑不起来
34
37
  *
35
38
  * 不做事:
36
39
  * - 不自动 commit / stash —— 让用户自己用 git review 改动
37
- * - 不跑 npm install —— 留给后续 sync 或用户手动
38
40
  * - 不动 .agent/skills/(那是 miaoda skills sync 的事,迁移后用户应该单独跑一次切到新 stack 的 skills)
39
41
  */
40
42
  async function handleAppMigrate(opts) {
@@ -94,6 +96,40 @@ async function handleAppMigrate(opts) {
94
96
  // 的 miaodaTemplate.archType 拿到的动态值,rule 配置时不知道,由 handler 在拿到
95
97
  // renderTemplate 结果后单独写。version 也一并写回方便 sync 后续诊断。
96
98
  (0, spark_meta_1.writeSparkMeta)(targetDir, { archType: templateArchType, version: templateVersion });
99
+ // 清掉 user app 的 node_modules —— migrate 改了 dependencies 集合(删 lite + 加 NestJS
100
+ // 全套)+ rule 已经把 package-lock.json 删了。如果 node_modules 留着,npm install 时旧
101
+ // 包还在 node_modules 顶层 hoist 树里,可能跟新 package.json spec 撞 peer 冲突
102
+ // (比如旧 coding-preset-vite-react@1.0.8 peer vite@^8 跟新 vite@^7 撞)。
103
+ // 软失败:node_modules 不存在不报错。
104
+ const nodeModulesPath = node_path_1.default.join(targetDir, 'node_modules');
105
+ if (node_fs_1.default.existsSync(nodeModulesPath)) {
106
+ (0, logger_1.log)('migrate', '清理 node_modules(dependencies 集合已变)...');
107
+ node_fs_1.default.rmSync(nodeModulesPath, { recursive: true, force: true });
108
+ }
109
+ // 跑 npm install 物化新依赖集合
110
+ // --ignore-scripts 绕开 action-plugin postinstall 在缺平台 env 时的 ENOENT
111
+ // --registry 钉同一份 npmmirror(跟 init / sync 行为一致)
112
+ // 软失败:install 挂了不阻断 emit;用户拿到详细 error,自行 npm install 兜底
113
+ const installArgs = [
114
+ 'install',
115
+ '--no-audit',
116
+ '--no-fund',
117
+ '--ignore-scripts',
118
+ '--registry',
119
+ (0, index_2.resolveNpmInstallRegistry)(),
120
+ ];
121
+ (0, logger_1.log)('migrate', `Running npm ${installArgs.join(' ')}...`);
122
+ let installError;
123
+ try {
124
+ (0, node_child_process_1.execFileSync)('npm', installArgs, {
125
+ cwd: targetDir,
126
+ stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
127
+ });
128
+ }
129
+ catch (err) {
130
+ installError = err instanceof Error ? err.message : String(err);
131
+ (0, logger_1.log)('migrate', `⚠ npm install failed (continuing): ${installError}`);
132
+ }
97
133
  (0, output_1.emit)({
98
134
  data: {
99
135
  from,
@@ -106,9 +142,9 @@ async function handleAppMigrate(opts) {
106
142
  detail: r.detail,
107
143
  })),
108
144
  ...summarizeResults(results),
145
+ installError,
109
146
  nextActions: [
110
147
  'git status / git diff 评估改动并 commit',
111
- 'npm install 装新依赖(package-lock.json 已被删除,将重新生成)',
112
148
  'miaoda skills sync 同步到新 stack 的 agent skills',
113
149
  ],
114
150
  },
@@ -2,16 +2,28 @@
2
2
  /**
3
3
  * vite-react → nestjs-react-fullstack 的跨 stack 迁移规则。
4
4
  *
5
- * 分类原则(与飞书 wiki 设计文档对齐):
6
- * - 用户领地(src / public / shared / .env / .spark_project):原样保留,搬到新布局位置;
7
- * - 模板领地(vite.config / tsconfig / scripts / .githooks / lint 配置 / 入口 html 等):
8
- * 用 fullstack 模板版本整体替换;
9
- * - package.json:字段级 merge —— 顶层 user 字段(name/version/private)保留,依赖列表
10
- * 做 set union(template 同名包覆盖版本号 → 避免依赖冲突),scripts 同名替换为 template
11
- * 版本。
5
+ * 设计原则:**最小改动**。只动 fullstack 形态必须的文件 / package.json key,user 已有
6
+ * vite.config / tsconfig / eslint / prettier / tailwind / postcss / components.json
7
+ * 等业务配置一律**保留**,不被 template 整体覆盖。
12
8
  *
13
- * 模板资产来源:upgrade/templates/nestjs-react-fullstack/templates/(cli 内嵌的脚手架资产,
14
- * sync 共享同一份)。
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 列表不动就够。
15
27
  */
16
28
  Object.defineProperty(exports, "__esModule", { value: true });
17
29
  exports.MIGRATE_CONFIG = void 0;
@@ -20,92 +32,184 @@ exports.MIGRATE_CONFIG = {
20
32
  to: 'nestjs-react-fullstack',
21
33
  rules: [
22
34
  // ===== 1. 用户领地搬迁 =====
23
- // src/ 是 vite-react 的业务代码根,fullstack 拆 client/server,全部归 client/src/
24
35
  { type: 'move-directory', from: 'src', to: 'client/src' },
25
- // public/ 是 vite 端的静态资源根,跟随 client/
26
36
  { type: 'move-directory', from: 'public', to: 'client/public' },
27
- // shared/ 在两边布局相同,不动。.env / .spark_project 同理,不写规则即可。
28
- // ===== 2. 清理 vite-react 专属残留 =====
29
- // 根 index.html 被 client/index.html 取代(fullstack 模板自己带)
37
+ // shared/ 不动(两边布局相同);.env / .spark_project 同理。
38
+ // ===== 2. 清理 vite-react 残留 =====
39
+ // 根 index.html 被 client/index.html 取代(fullstack 用 HBS 渲染)
30
40
  { type: 'delete-file', to: 'index.html' },
31
- // ESLint 9 flat config 文件名变了
41
+ // ESLint 9 flat config 文件名变了(user eslint.config.mjs → eslint.config.js)
32
42
  { type: 'delete-file', to: 'eslint.config.mjs' },
33
- // 依赖集合变了,lockfile 重新生成(首次 npm install 时)
43
+ // 依赖集合变了,lockfile 重新生成
34
44
  { type: 'delete-file', to: 'package-lock.json' },
35
- // ===== 3. 从 user package.json 删除 vite-react 专属字段 =====
36
- // 这步在 merge-json 之前,确保 vite-react preset 不残留。
37
- // merge-json 是"template 覆盖同名 key + user 多出 key 保留",所以 user 的 vite-react
38
- // 专属字段不会被自动清理 —— 必须显式删。
39
- //
40
- // 注意:不删 dependencies.@lark-apaas/client-toolkit-lite —— lite 是 vite-react 时代的
41
- // 入门 wrapper(顶层 38 个 export),完整版 @lark-apaas/client-toolkit 只暴露 ~20 个
42
- // 对等出口(components/* / hooks/* / utils/* / tools/* / 顶层 alias),剩下 ~18 个 lite
43
- // symbol(copyToClipboard / getAppId / clsxWithTw / isMobile / avatarImages 等)在 full
44
- // 公共 API 里没有对等物。lite 跟 full 不是简单替换关系,而是不同抽象层级。强行替换
45
- // 会让 build 失败;让两个包共存(lite 很轻量)是当前的现实选择。后续若 client-toolkit
46
- // 补全 utility 出口,可启用 src/utils/codemod-client-toolkit-lite.ts 里已实现的 codemod
47
- // rule(在 MigrateRule 联合里有 CodemodClientToolkitLiteRule 类型)。
45
+ // ===== 3. 从 user package.json vite-react 专属字段 =====
46
+ // 注意保留 devDependencies.@lark-apaas/coding-preset-vite-react —— user 迁移后
47
+ // vite.config 仍用它(开 fullstack 模式即可),不切到 fullstack-vite-preset。
48
48
  {
49
49
  type: 'delete-json-keys',
50
50
  to: 'package.json',
51
51
  keys: [
52
52
  // 顶层 "type": "module" —— vite-react ESM 入口约定,nestjs CommonJS 不需要
53
53
  'type',
54
- // vite-react vite preset,nestjs fullstack-vite-preset
55
- 'devDependencies.@lark-apaas/coding-preset-vite-react',
56
- // vite-react 的 ESLint preset
57
- 'devDependencies.@lark-apaas/coding-presets-react',
54
+ // lite SDK:codemod import client-toolkit 顶层
55
+ 'dependencies.@lark-apaas/client-toolkit-lite',
58
56
  ],
59
57
  },
60
- // ===== 4. 模板资产覆盖 / 新增 =====
61
- // 顶层配置文件:直接用 fullstack 模板版本覆盖(用户改过的话靠 git diff 兜底自查)
62
- { type: 'file', from: 'vite.config.ts', to: 'vite.config.ts', overwrite: true },
58
+ // ===== 4. fullstack 形态必需资产(覆盖 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.* —— vite-react 时代 tsconfig 配 include: ["src"],迁完代码搬到 client/src,
64
+ // include 路径不对 + 老 tsconfig.app 还 extends @lark-apaas/coding-presets-react;
65
+ // 整体覆盖到 fullstack 版本(include 改 client/** + shared/**、extends fullstack-presets)。
66
+ // user 自己加的 paths / strict 等设置如有需要,按 git diff 手动加回。
63
67
  { type: 'file', from: 'tsconfig.json', to: 'tsconfig.json', overwrite: true },
64
68
  { type: 'file', from: 'tsconfig.app.json', to: 'tsconfig.app.json', overwrite: true },
65
69
  { type: 'file', from: 'tsconfig.node.json', to: 'tsconfig.node.json', overwrite: true },
70
+ // ESLint 9 flat config —— vite-react 用 eslint.config.mjs 已删,fullstack 用
71
+ // eslint.config.js (commonjs) + extends @lark-apaas/fullstack-presets;user 通常不改
66
72
  { type: 'file', from: 'eslint.config.js', to: 'eslint.config.js', overwrite: true },
67
- { type: 'file', from: 'nest-cli.json', to: 'nest-cli.json', overwrite: true },
68
- { type: 'file', from: 'postcss.config.js', to: 'postcss.config.js', overwrite: true },
69
- { type: 'file', from: 'tailwind.config.ts', to: 'tailwind.config.ts', overwrite: true },
70
- { type: 'file', from: 'components.json', to: 'components.json', overwrite: true },
71
- { type: 'file', from: '.prettierrc', to: '.prettierrc', overwrite: true },
72
- { type: 'file', from: '.stylelintrc.js', to: '.stylelintrc.js', overwrite: true },
73
- // renderTemplate 拉 tarball 时会把 _gitignore / _npmrc 占位名重命名回 .gitignore / .npmrc,
74
- // 所以 from 也写 dot-form
75
- { type: 'file', from: '.gitignore', to: '.gitignore', overwrite: true },
76
- { type: 'file', from: '.npmrc', to: '.npmrc', overwrite: true },
77
- // client 入口:用 nestjs 模板的 index.tsx 覆盖(含 Toaster / defaultTheme),原 vite-react
78
- // 入口在前面 move-directory 时已搬到 client/src/index.tsx
73
+ // client/index.html:HBS 模板(fullstack 用,vite-react 用根 index.html 已删)
79
74
  { type: 'file', from: 'client/index.html', to: 'client/index.html', overwrite: true },
80
- { type: 'file', from: 'client/src/index.tsx', to: 'client/src/index.tsx', overwrite: true },
81
- // 整目录复制(不存在则创建,存在则递归覆盖同名文件)
82
- // server/:NestJS SSR runtime,固定模板,不需要业务定制
83
- { type: 'directory', from: 'server', to: 'server', overwrite: true },
84
- // scripts/:dev / build / lint / hooks 全套脚本
75
+ // scripts/ 整目录:fullstack 形态启动 / 构建 / lint 所需的 8 个平台脚本(dev.sh /
76
+ // dev.js / dev-local.js / build.sh / lint.js / prune-smart.js / run.sh / hooks/)。
77
+ // dev.sh exec node dev.js,dev.js 才是并发起 nest + vite 的核心 —— 漏一个 dev 跑不起来。
78
+ // overwrite: true:这些脚本由平台 cli 维护,user 一般不改;按 fullstack 版本覆盖。
85
79
  { type: 'directory', from: 'scripts', to: 'scripts', overwrite: true },
86
- // .githooks/:pre-commit
87
- { type: 'directory', from: '.githooks', to: '.githooks', overwrite: true },
88
- // shared/ 是用户领地(不在 move 阶段动),但 fullstack 模板带了 shared/api.interface.ts 之类
89
- // 的 fallback 文件 —— 用 overwrite: false 把 template 新增的文件加入 user shared/,user
90
- // 已有同名文件保持不动。
80
+ // .githooks(fallback,user 已有则保留)
81
+ { type: 'directory', from: '.githooks', to: '.githooks', overwrite: false },
82
+ // shared/ fallback(user 已有同名文件保留,template 新增文件加入)
91
83
  { type: 'directory', from: 'shared', to: 'shared', overwrite: false },
92
- // .env 同样 fallback —— user 已有 .env 保留,否则用 template 的(默认 logger 配置)
84
+ // .env fallbackuser 没有时用 template 默认)
93
85
  { type: 'file', from: '.env', to: '.env', overwrite: false },
94
- // ===== 5. package.json 字段级 merge =====
95
- // template partial pkg(只含 dependencies / devDependencies / scripts)deep merge
96
- // 进 user pkg:
97
- // - 顶层 user 字段(name / version / private)保留 —— template 不写这些字段
98
- // - dependencies / devDependencies:template 覆盖同名版本号,user 业务 deps 保留
99
- // - scripts:template 同名脚本覆盖 user 同名脚本,user 自定义脚本保留
100
- // 模板源是 upgrade/templates/nestjs-react-fullstack/templates/package.json
86
+ // ===== 5. fullstack 形态必需的 scripts =====
87
+ // 这些 scripts user 现有的可能性低(vite-react 没有 nest 相关 script),直接覆盖
88
+ {
89
+ type: 'add-script',
90
+ name: 'dev',
91
+ command: './scripts/dev.sh',
92
+ overwrite: true,
93
+ },
94
+ {
95
+ type: 'add-script',
96
+ name: 'dev:server',
97
+ command: 'NODE_ENV=development nest start --watch',
98
+ overwrite: true,
99
+ },
100
+ {
101
+ type: 'add-script',
102
+ name: 'dev:client',
103
+ command: 'NODE_ENV=development vite --config vite.config.ts',
104
+ overwrite: true,
105
+ },
106
+ {
107
+ type: 'add-script',
108
+ name: 'build',
109
+ command: './scripts/build.sh',
110
+ overwrite: true,
111
+ },
112
+ {
113
+ type: 'add-script',
114
+ name: 'build:server',
115
+ command: 'NODE_ENV=production nest build',
116
+ overwrite: true,
117
+ },
101
118
  {
102
- type: 'merge-json',
119
+ type: 'add-script',
120
+ name: 'build:client',
121
+ command: 'NODE_ENV=production vite build --config vite.config.ts',
122
+ overwrite: true,
123
+ },
124
+ {
125
+ type: 'add-script',
126
+ name: 'build:prod',
127
+ command: 'npm run build:server && npm run build:client',
128
+ overwrite: true,
129
+ },
130
+ {
131
+ type: 'add-script',
132
+ name: 'start',
133
+ command: 'NODE_ENV=production node main.js',
134
+ overwrite: true,
135
+ },
136
+ {
137
+ type: 'add-script',
138
+ name: 'type:check:server',
139
+ command: 'tsc --noEmit --project tsconfig.node.json',
140
+ overwrite: true,
141
+ },
142
+ {
143
+ type: 'add-script',
144
+ name: 'type:check:client',
145
+ command: 'tsc --noEmit --project tsconfig.app.json',
146
+ overwrite: true,
147
+ },
148
+ {
149
+ type: 'add-script',
150
+ name: 'type:check',
151
+ command: 'concurrently -n "server,client" -c "blue,green" "npm run type:check:server" "npm run type:check:client"',
152
+ overwrite: true,
153
+ },
154
+ // ===== 6. 从 fullstack template 取版本号加 NestJS 强依赖(精确白名单) =====
155
+ // 不走 merge-json(template 130+ devDeps 全 merge 会污染 user 业务依赖集合);
156
+ // 白名单只列 fullstack 启动能跑起来必需的最小集。
157
+ {
158
+ type: 'add-deps-from-template',
103
159
  from: 'package.json',
104
160
  to: 'package.json',
161
+ dependencies: [
162
+ // NestJS server 核心
163
+ '@nestjs/core',
164
+ '@nestjs/common',
165
+ '@nestjs/platform-express',
166
+ '@nestjs/config',
167
+ '@nestjs/axios',
168
+ '@nestjs/swagger',
169
+ // 妙搭 fullstack server SDK(main.ts / view module 等都依赖)
170
+ '@lark-apaas/fullstack-nestjs-core',
171
+ // HBS 模板引擎(client/index.html 渲染)
172
+ 'hbs',
173
+ // NestJS runtime 必需
174
+ 'reflect-metadata',
175
+ 'rxjs',
176
+ 'tslib',
177
+ // DTO 校验 / 转换
178
+ 'class-transformer',
179
+ 'class-validator',
180
+ // .env 加载(scripts/dev.sh 用到 dotenv-cli 风格)
181
+ 'dotenv',
182
+ // client-toolkit:替代 lite,业务代码 codemod 后走它
183
+ '@lark-apaas/client-toolkit',
184
+ ],
185
+ devDependencies: [
186
+ // nest start / nest build 必需
187
+ '@nestjs/cli',
188
+ '@nestjs/testing',
189
+ // server ts 编译
190
+ 'ts-node',
191
+ '@types/express',
192
+ '@types/hbs',
193
+ '@types/node',
194
+ // 并发跑 nest + vite(scripts/dev.sh 用 concurrently)
195
+ 'concurrently',
196
+ // fullstack server 端 tsconfig.node.json 通过 extends 引这个包
197
+ // (@lark-apaas/fullstack-presets/lib/simple/tsconfig/tsconfig.node.json);
198
+ // 不装会导致 nest build / type:check:server / vite dev 解析 tsconfig 时 ENOENT
199
+ '@lark-apaas/fullstack-presets',
200
+ ],
105
201
  },
106
- // ===== 6. stack =====
107
- // 收尾:把 .spark/meta.json stack 切到 nestjs-react-fullstack,后续 miaoda app sync
108
- // 会按新 stack SyncConfig
202
+ // ===== 7. 业务代码 codemod:lite → client-toolkit 包名替换 =====
203
+ // client-toolkit@1.2.52+ 顶层完整 re-export lite 全部 48 项,specifier 列表不动。
204
+ // 同时扫 shared/(client/server 共享代码也可能引 lite 类型 / utils)。
205
+ { type: 'codemod-client-toolkit-lite', root: 'client/src' },
206
+ { type: 'codemod-client-toolkit-lite', root: 'shared' },
207
+ // ===== 8. vite.config codemod:加 { fullstack: true } 第二参 =====
208
+ // user vite.config 现在是 defineConfig({...}) 单参;fullstack 形态需要 vite 8080
209
+ // 反代 HTML / API 到 nest 3000,coding-preset-vite-react 用 fullstack option 启用
210
+ // 这一行为(详见 fullstack-plugin coding-preset-vite-react#fullstack)。
211
+ { type: 'codemod-vite-config-fullstack', to: 'vite.config.ts' },
212
+ // ===== 9. 切 stack =====
109
213
  { type: 'set-stack', stack: 'nestjs-react-fullstack' },
110
214
  ],
111
215
  };
@@ -42,7 +42,7 @@ exports.SYNC_CONFIG = {
42
42
  command: 'node scripts/hooks/run-precommit.js',
43
43
  overwrite: false,
44
44
  },
45
- // 2. .gitignore 卫生:移除 package-lock.json + 加 .agent/
45
+ // 2. .gitignore 卫生:移除 package-lock.json + 加 .agent/ / .npm_cache
46
46
  {
47
47
  type: 'remove-line',
48
48
  to: '.gitignore',
@@ -53,6 +53,11 @@ exports.SYNC_CONFIG = {
53
53
  to: '.gitignore',
54
54
  line: '.agent/',
55
55
  },
56
+ {
57
+ type: 'add-line',
58
+ to: '.gitignore',
59
+ line: '.npm_cache',
60
+ },
56
61
  // 3. 老 npm scripts 迁移(裸 fullstack-cli → 钉版 npx)。两条 ifStartsWith 分别覆盖
57
62
  // "裸 fullstack-cli"(最老形态)和 "npx 未钉版" 形态,统一升到 FULLSTACK_CLI_PIN_SPEC。
58
63
  {
@@ -113,6 +113,12 @@ exports.SYNC_CONFIG = {
113
113
  to: '.gitignore',
114
114
  line: '.agent/',
115
115
  },
116
+ // 7a. 确保 .gitignore 包含 .npm_cache 目录
117
+ {
118
+ type: 'add-line',
119
+ to: '.gitignore',
120
+ line: '.npm_cache',
121
+ },
116
122
  // 8. 同步 .spark_project 配置文件(总是覆盖)
117
123
  {
118
124
  type: 'file',
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.installDependencies = exports.writeSparkMeta = exports.readSparkMeta = exports.TEMPLATE_PACKAGE_BY_STACK = exports.SUPPORTED_STACKS = exports.renderTemplate = void 0;
3
+ exports.resolveNpmInstallRegistry = exports.installDependencies = exports.writeSparkMeta = exports.readSparkMeta = exports.TEMPLATE_PACKAGE_BY_STACK = exports.SUPPORTED_STACKS = exports.renderTemplate = void 0;
4
4
  var template_1 = require("./template");
5
5
  Object.defineProperty(exports, "renderTemplate", { enumerable: true, get: function () { return template_1.renderTemplate; } });
6
6
  Object.defineProperty(exports, "SUPPORTED_STACKS", { enumerable: true, get: function () { return template_1.SUPPORTED_STACKS; } });
@@ -10,3 +10,4 @@ Object.defineProperty(exports, "readSparkMeta", { enumerable: true, get: functio
10
10
  Object.defineProperty(exports, "writeSparkMeta", { enumerable: true, get: function () { return spark_meta_1.writeSparkMeta; } });
11
11
  var install_1 = require("./install");
12
12
  Object.defineProperty(exports, "installDependencies", { enumerable: true, get: function () { return install_1.installDependencies; } });
13
+ Object.defineProperty(exports, "resolveNpmInstallRegistry", { enumerable: true, get: function () { return install_1.resolveNpmInstallRegistry; } });
@@ -1,21 +1,22 @@
1
1
  "use strict";
2
2
  /**
3
- * Codemod:把 `@lark-apaas/client-toolkit-lite` 的 import 改写到 `@lark-apaas/client-toolkit`
4
- * 的对应子路径。
3
+ * Codemod:把 `@lark-apaas/client-toolkit-lite` 的 import 改写到 `@lark-apaas/client-toolkit`。
5
4
  *
6
5
  * 用于 `miaoda app migrate vite-react → nestjs-react-fullstack`:迁移后让两个包共存属于
7
- * lib 冗余(两份 react / 两套主题 / 两套 axios),所以扫 client/src lite import 改写到
8
- * full 等价子路径,然后用 delete-json-keys 移除 lite dep。
6
+ * lib 冗余(两份 react / 两套主题 / 两套 axios),所以扫 user 代码把 lite import 改写到
7
+ * full,然后用 delete-json-keys 移除 lite dep。
9
8
  *
10
- * 表的来源:grep client-toolkit lib/ 目录定位每个 lite symbol 的 d.ts 路径,subpath 取相对
11
- * 路径。同 symbol 多个 d.ts 时按优先级选最合理的——例如 AppContainer 优先 `components/` 而
12
- * `apis/components/`(跟 fullstack template 自带 client/src/index.tsx 的约定一致)。
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 别名)。
13
15
  *
14
16
  * 边界:
15
17
  * - 仅处理形如 `import [type] { A, B as C } from '@lark-apaas/client-toolkit-lite'` 的
16
- * 单行 named importdefault import / namespace import / 跨多行 specifier 表当作 unmapped。
17
- * - 表里没有的 symbol:保留原 import + 加一行 `// TODO[migrate]: ...` 注释;后续 build
18
- * 会因为 lite dep 被删而失败,提示用户手动处理。
18
+ * named importdefault import / namespace import / 跨多行 specifier 当前不识别,
19
+ * 但只要 `from '...lite'` 在同一行,包名替换仍有效(regex from 段)。
19
20
  */
20
21
  var __importDefault = (this && this.__importDefault) || function (mod) {
21
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -26,68 +27,10 @@ exports.rewriteCssText = rewriteCssText;
26
27
  exports.rewriteText = rewriteText;
27
28
  const node_fs_1 = __importDefault(require("node:fs"));
28
29
  const node_path_1 = __importDefault(require("node:path"));
29
- /** lite 38 个 export → full 子路径的映射表 */
30
- const SYMBOL_MAP = {
31
- // === 组件(apis/components 是 lite 兼容层,components 是基础层) ===
32
- ActiveLink: { subpath: 'apis/components/ActiveLink' },
33
- AppContainer: { subpath: 'components/AppContainer' },
34
- ErrorRender: { subpath: 'components/ErrorRender' },
35
- NavLink: { subpath: 'apis/components/NavLink' },
36
- PagePlaceholder: { subpath: 'components/PagePlaceholder' },
37
- QueryProvider: { subpath: 'components/QueryProvider' },
38
- UniversalLink: { subpath: 'apis/components/UniversalLink' },
39
- Welcome: { subpath: 'apis/components/Welcome' },
40
- // === 类型 ===
41
- AppInfoPayload: { subpath: 'types/common' },
42
- IFileAttachment: { subpath: 'apis/udt-types' },
43
- IJson: { subpath: 'apis/udt-types' },
44
- IUserProfile: { subpath: 'apis/udt-types' },
45
- LogLevel: { subpath: 'logger/log-types' },
46
- LogWithMeta: { subpath: 'logger/log-types' },
47
- TrackKey: { subpath: 'types/tea' },
48
- TrackParams: { subpath: 'types/tea' },
49
- // === 图片资源(lite 端用了 alias:avatar→avatarImages 等) ===
50
- avatarImages: { subpath: 'apis/constants/img-resources/avatar', sourceName: 'avatar' },
51
- bannerImages: { subpath: 'apis/constants/img-resources/banner', sourceName: 'banner' },
52
- coverImages: { subpath: 'apis/constants/img-resources/cover', sourceName: 'cover' },
53
- // === Hooks ===
54
- useAppInfo: { subpath: 'hooks/useAppInfo' },
55
- useCurrentUserProfile: { subpath: 'hooks/useCurrentUserProfile' },
56
- useIsMobile: { subpath: 'hooks/useIsMobile' },
57
- useLogout: { subpath: 'hooks/useLogout' },
58
- // === Utils / 工具函数 ===
59
- axiosForBackend: { subpath: 'utils/getAxiosForBackend' },
60
- capabilityClient: { subpath: '' }, // 在 full 顶层 export
61
- clsxWithTw: { subpath: 'utils/utils' },
62
- copyToClipboard: { subpath: 'utils/copyToClipboard' },
63
- getAppId: { subpath: 'utils/getAppId' },
64
- getAppInfo: { subpath: 'integrations/getAppInfo' },
65
- getAxiosForBackend: { subpath: 'utils/getAxiosForBackend' },
66
- getCsrfToken: { subpath: 'utils/getCsrfToken' },
67
- getCurrentUserProfile: { subpath: 'integrations/getCurrentUserProfile' },
68
- getDataloom: { subpath: 'apis/dataloom' },
69
- getEnvPath: { subpath: 'utils/getEnvPath' },
70
- getWsPath: { subpath: 'utils/utils' },
71
- initAxiosConfig: { subpath: 'utils/axiosConfig' },
72
- initObservable: { subpath: 'runtime/observable' },
73
- isIOS: { subpath: 'utils/deviceType' },
74
- isIpad: { subpath: 'utils/deviceType' },
75
- isMobile: { subpath: 'utils/deviceType' },
76
- isPreview: { subpath: 'utils/utils' },
77
- logger: { subpath: 'apis/logger' },
78
- normalizeBasePath: { subpath: 'utils/utils' },
79
- reportTeaEvent: { subpath: 'components/AppContainer/utils/tea' },
80
- resolveAppUrl: { subpath: 'utils/resolveAppUrl' },
81
- safeStringify: { subpath: 'utils/safeStringify' },
82
- scopedStorage: { subpath: 'apis/utils/scopedStorage' },
83
- trace: { subpath: 'trace/index' },
84
- };
85
30
  const LITE_PKG = '@lark-apaas/client-toolkit-lite';
86
31
  const FULL_PKG = '@lark-apaas/client-toolkit';
87
32
  /**
88
33
  * 递归扫描 rootDir 下的 .ts/.tsx/.js/.jsx/.css 文件,把 lite import 改写到 full。
89
- * css 里 `@import "@lark-apaas/client-toolkit-lite/styles.css"` 同样要重写到
90
- * `@lark-apaas/client-toolkit/lib/index.css`,否则 css build 会因 lite dep 被删而 ENOENT。
91
34
  */
92
35
  function rewriteLiteImports(rootDir) {
93
36
  const result = {
@@ -108,8 +51,7 @@ function rewriteLiteImports(rootDir) {
108
51
  if (!before.includes(LITE_PKG))
109
52
  continue;
110
53
  const rel = node_path_1.default.relative(rootDir, file);
111
- const { text, count, unmapped } = isJs ? rewriteText(before, rel) : rewriteCssText(before, rel);
112
- result.unmapped.push(...unmapped);
54
+ const { text, count } = isJs ? rewriteText(before, rel) : rewriteCssText(before, rel);
113
55
  if (count > 0) {
114
56
  node_fs_1.default.writeFileSync(file, text);
115
57
  result.filesChanged.push(rel);
@@ -119,128 +61,35 @@ function rewriteLiteImports(rootDir) {
119
61
  return result;
120
62
  }
121
63
  /**
122
- * css 改写器:把 `@import "@lark-apaas/client-toolkit-lite/styles.css"` 重写到 full 的入口
123
- * `@lark-apaas/client-toolkit/lib/index.css`;其他 lite styles 子路径(如 `styles/*`)
124
- * full 里没有对等物,留下 TODO 注释让用户手动处理。
64
+ * css 改写器:单行包名替换。`@import "@lark-apaas/client-toolkit-lite/<path>"`
65
+ * `@import "@lark-apaas/client-toolkit/<path>"`。其中 styles.css full 端已加同名别名
66
+ * 指向 ./lib/index.css。
125
67
  */
126
- function rewriteCssText(source, fileLabel) {
127
- const unmapped = [];
68
+ function rewriteCssText(source, _fileLabel) {
128
69
  let count = 0;
129
- const lines = source.split('\n');
130
- const out = [];
131
- // 匹配 `@import "@lark-apaas/client-toolkit-lite/<path>";` 或 single quote
132
- const re = /^(\s*)@import\s+['"]@lark-apaas\/client-toolkit-lite\/([^'"]+)['"]\s*;?\s*$/;
133
- for (let i = 0; i < lines.length; i++) {
134
- const line = lines[i];
135
- const m = re.exec(line);
136
- if (!m) {
137
- out.push(line);
138
- continue;
139
- }
140
- const captures = m;
141
- const indent = captures[1] ?? '';
142
- const subpath = captures[2] ?? '';
143
- if (subpath === 'styles.css') {
144
- out.push(`${indent}@import "${FULL_PKG}/lib/index.css";`);
145
- count++;
146
- }
147
- else {
148
- // styles/* 等子路径 full 没对等物,保留原行 + TODO(build 会失败)
149
- out.push(line);
150
- out.push(`${indent}/* TODO[migrate]: @lark-apaas/client-toolkit-lite/${subpath} has no known mapping in @lark-apaas/client-toolkit; replace manually. */`);
151
- unmapped.push({ file: fileLabel, symbol: subpath, line: i + 1 });
152
- }
153
- }
154
- return { text: out.join('\n'), count, unmapped };
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: [] };
155
76
  }
156
- /** 对单个文件文本做改写。导出供单测直接验证。 */
157
- function rewriteText(source, fileLabel) {
158
- const unmapped = [];
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) {
159
84
  let count = 0;
160
- const lines = source.split('\n');
161
- const out = [];
162
- // 匹配单行 named import:捕获 indent / `type ` 修饰符 / specifier 段
163
- const reImport = /^(\s*)import\s+(type\s+)?\{([^}]+)\}\s+from\s+['"]@lark-apaas\/client-toolkit-lite['"]\s*;?\s*$/;
164
- for (let i = 0; i < lines.length; i++) {
165
- const line = lines[i];
166
- const m = reImport.exec(line);
167
- if (!m) {
168
- out.push(line);
169
- continue;
170
- }
171
- // RegExp.exec 返回 string[],但 optional capture group 实际可能是 undefined。
172
- // 显式 cast 让 TS narrow 配合 eslint no-unnecessary-condition 不报"永远 truthy"。
173
- const captures = m;
174
- const indent = captures[1] ?? '';
175
- const wholeIsType = captures[2] !== undefined;
176
- const specifiers = parseSpecifiers(captures[3] ?? '');
177
- // 按 target subpath 分组(同 subpath 的 specifier 合并到一条 import)
178
- const groups = new Map();
179
- const localUnmapped = [];
180
- for (const spec of specifiers) {
181
- // SYMBOL_MAP 是 Record,TS 默认推断索引返回非 undefined;用 hasOwnProperty 做 narrow
182
- if (!Object.prototype.hasOwnProperty.call(SYMBOL_MAP, spec.imported)) {
183
- localUnmapped.push(spec.imported);
184
- unmapped.push({ file: fileLabel, symbol: spec.imported, line: i + 1 });
185
- continue;
186
- }
187
- const info = SYMBOL_MAP[spec.imported];
188
- const target = info.subpath ? `${FULL_PKG}/${info.subpath}` : FULL_PKG;
189
- const sourceName = info.sourceName ?? spec.imported;
190
- const isType = wholeIsType || spec.isType;
191
- const entry = groups.get(target) ?? { items: [], allTypes: true };
192
- entry.items.push({ source: sourceName, local: spec.local, isType });
193
- entry.allTypes = entry.allTypes && isType;
194
- groups.set(target, entry);
195
- count++;
196
- }
197
- if (groups.size === 0) {
198
- // 全部 unmapped:保留原行 + TODO 注释
199
- out.push(line);
200
- out.push(`${indent}// TODO[migrate]: '${localUnmapped.join(', ')}' has no known mapping from '${LITE_PKG}' to '${FULL_PKG}'; replace manually before building.`);
201
- continue;
202
- }
203
- // 输出 group 化的新 import 行(同 target 下区分 type-only / 混合)
204
- for (const [target, { items, allTypes }] of groups) {
205
- if (allTypes) {
206
- const inner = items.map(fmtSpec).join(', ');
207
- out.push(`${indent}import type { ${inner} } from '${target}';`);
208
- }
209
- else {
210
- // 把 type 和非 type 分两条输出更稳:避免 `import { type X, Y }` 在某些工具链下不被支持
211
- const typeItems = items.filter((it) => it.isType);
212
- const valItems = items.filter((it) => !it.isType);
213
- if (typeItems.length > 0) {
214
- out.push(`${indent}import type { ${typeItems.map(fmtSpec).join(', ')} } from '${target}';`);
215
- }
216
- out.push(`${indent}import { ${valItems.map(fmtSpec).join(', ')} } from '${target}';`);
217
- }
218
- }
219
- if (localUnmapped.length > 0) {
220
- out.push(`${indent}// TODO[migrate]: '${localUnmapped.join(', ')}' has no known mapping from '${LITE_PKG}' to '${FULL_PKG}'; replace manually before building.`);
221
- }
222
- }
223
- return { text: out.join('\n'), count, unmapped };
224
- }
225
- function fmtSpec(s) {
226
- return s.local === s.source ? s.source : `${s.source} as ${s.local}`;
227
- }
228
- function parseSpecifiers(raw) {
229
- const out = [];
230
- for (const part of raw.split(',')) {
231
- const t = part.trim();
232
- if (!t)
233
- continue;
234
- const m = /^(type\s+)?([A-Za-z_$][A-Za-z0-9_$]*)(?:\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*))?$/.exec(t);
235
- if (!m)
236
- continue;
237
- const captures = m;
238
- const imported = captures[2];
239
- if (imported === undefined)
240
- continue;
241
- out.push({ isType: captures[1] !== undefined, imported, local: captures[3] ?? imported });
242
- }
243
- return out;
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: [] };
244
93
  }
245
94
  function* walk(dir) {
246
95
  for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.applyMigrateRules = applyMigrateRules;
7
+ exports.rewriteViteConfigForFullstack = rewriteViteConfigForFullstack;
8
+ exports.rewriteAliasSrcPaths = rewriteAliasSrcPaths;
7
9
  const node_fs_1 = __importDefault(require("node:fs"));
8
10
  const node_path_1 = __importDefault(require("node:path"));
9
11
  const sync_rule_1 = require("./sync-rule");
@@ -38,6 +40,14 @@ function applyMigrateRules(rules, opts) {
38
40
  results.push(applyCodemodLite(rule, opts));
39
41
  continue;
40
42
  }
43
+ if (rule.type === 'codemod-vite-config-fullstack') {
44
+ results.push(applyCodemodViteConfigFullstack(rule, opts));
45
+ continue;
46
+ }
47
+ if (rule.type === 'add-deps-from-template') {
48
+ results.push(applyAddDepsFromTemplate(rule, opts));
49
+ continue;
50
+ }
41
51
  // 前面 if 已经把 migrate 专用 rule 都 narrow 掉,剩下的必然是 SyncRule
42
52
  const [syncResult] = (0, sync_rule_1.applySyncRules)([rule], opts);
43
53
  results.push(syncResult);
@@ -164,6 +174,130 @@ function applyCodemodLite(rule, opts) {
164
174
  detail,
165
175
  };
166
176
  }
177
+ function applyCodemodViteConfigFullstack(rule, opts) {
178
+ const logPrefix = opts.logPrefix ?? 'migrate';
179
+ const rel = rule.to ?? 'vite.config.ts';
180
+ const dest = node_path_1.default.join(opts.targetDir, rel);
181
+ if (!node_fs_1.default.existsSync(dest)) {
182
+ (0, logger_1.log)(logPrefix, ` ○ ${rel} (not found, skip)`);
183
+ return { rule, action: 'skipped', path: rel };
184
+ }
185
+ const before = node_fs_1.default.readFileSync(dest, 'utf-8');
186
+ const { text, aliasRewriteCount } = rewriteViteConfigForFullstack(before);
187
+ if (aliasRewriteCount === 0) {
188
+ (0, logger_1.log)(logPrefix, ` ○ ${rel} (no src alias path to rewrite)`);
189
+ return { rule, action: 'noop', path: rel };
190
+ }
191
+ node_fs_1.default.writeFileSync(dest, text);
192
+ (0, logger_1.log)(logPrefix, ` ✓ ${rel} (rewrote ${aliasRewriteCount.toString()} alias path(s) src → client/src)`);
193
+ return {
194
+ rule,
195
+ action: 'patched',
196
+ path: rel,
197
+ detail: `${aliasRewriteCount.toString()} alias path(s)`,
198
+ };
199
+ }
200
+ /**
201
+ * 把 vite.config.ts 里 alias 字符串里 `src` 起头的路径重写到 `client/src`。
202
+ *
203
+ * vite-react 时代 user 业务代码在 `src/`,迁移后搬到 `client/src/`,vite.config
204
+ * 里的 alias(典型如 `'@': path.resolve(__dirname, 'src')`)必须同步改:
205
+ * path.resolve(<args>, 'src') → 'src' 替换为 'client/src'
206
+ * path.resolve(<args>, 'src/<rest>') → 同上
207
+ * path.join(<args>, 'src') → 同上
208
+ * './src' / './src/<rest>' 字符串 → './client/src' / './client/src/<rest>'
209
+ * 边界:仅匹配整段 'src' 起头(避免 'sub/src' 误改),保留单/双引号原样。
210
+ *
211
+ * 注:fullstack 形态识别 **不再** 通过 `defineConfig` 的 option 第二参,而是
212
+ * 由 `coding-preset-vite-react` 内部读 `process.env.MIAODA_APP_TYPE === '3'`
213
+ * 自动判断(fullstack stack 的 archType 是 3)—— 所以 vite.config 完全不需要
214
+ * 加 option,只需 alias 路径同步即可。
215
+ */
216
+ function rewriteViteConfigForFullstack(source) {
217
+ const { text, count } = rewriteAliasSrcPaths(source);
218
+ return { text, aliasRewriteCount: count };
219
+ }
220
+ /**
221
+ * vite.config 里 alias 路径重写:`src` 起头的路径加 `client/` 前缀。
222
+ *
223
+ * 匹配的形态(不限制在 resolve.alias 块内 —— vite.config 顶层很少出现非 alias 用的
224
+ * `src` 字符串,业务 import 走 `@/...`,所以全文匹配是安全的):
225
+ *
226
+ * path.resolve(<args>, 'src') → path.resolve(<args>, 'client/src')
227
+ * path.resolve(<args>, 'src/<rest>') → path.resolve(<args>, 'client/src/<rest>')
228
+ * path.join(<args>, 'src') → 同上
229
+ * './src' / './src/<rest>'(独立字符串) → './client/src' / './client/src/<rest>'
230
+ *
231
+ * 已含 `client/src` 的不动(视为已迁移),`sub/src` 之类(src 不在词首)不动。
232
+ */
233
+ function rewriteAliasSrcPaths(source) {
234
+ let count = 0;
235
+ // path.resolve(...) / path.join(...) 的最后一个参数是 'src' 或 'src/...' 起头
236
+ // 匹配 ['"]src[/'"],保留引号
237
+ const pathCallRe = /(\bpath\s*\.\s*(?:resolve|join)\s*\([^)]*?,\s*)(['"`])(src)(\/[^'"`]*)?(['"`])/g;
238
+ let next = source.replace(pathCallRe, (_match, prefix, q1, _src, rest, q2) => {
239
+ count++;
240
+ return `${prefix}${q1}client/src${rest ?? ''}${q2}`;
241
+ });
242
+ // 独立的相对路径字符串字面量 './src' 或 './src/...'(不在 path.resolve 参数里)
243
+ // 注意:path.resolve / join 已被上一步处理,这里只匹配剩下的 './src...' 字面量。
244
+ // 这种用法在 vite.config 较少(典型是直接给 alias 字符串值),但兜底处理。
245
+ const relativeRe = /(['"`])\.\/(src)(\/[^'"`]*)?(['"`])/g;
246
+ next = next.replace(relativeRe, (_match, q1, _src, rest, q2) => {
247
+ count++;
248
+ return `${q1}./client/src${rest ?? ''}${q2}`;
249
+ });
250
+ return { text: next, count };
251
+ }
252
+ function applyAddDepsFromTemplate(rule, opts) {
253
+ const logPrefix = opts.logPrefix ?? 'migrate';
254
+ const userPath = node_path_1.default.join(opts.targetDir, rule.to ?? 'package.json');
255
+ const tplPath = node_path_1.default.join(opts.sourceRoot, rule.from ?? 'package.json');
256
+ if (!node_fs_1.default.existsSync(userPath)) {
257
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to ?? 'package.json'} (not found, skip add-deps-from-template)`);
258
+ return { rule, action: 'skipped', path: rule.to ?? 'package.json' };
259
+ }
260
+ if (!node_fs_1.default.existsSync(tplPath)) {
261
+ (0, logger_1.log)(logPrefix, ` ⚠ template ${rule.from ?? 'package.json'} not found, skip add-deps-from-template`);
262
+ return { rule, action: 'skipped', path: rule.to ?? 'package.json' };
263
+ }
264
+ const userText = node_fs_1.default.readFileSync(userPath, 'utf-8');
265
+ const userJson = JSON.parse(userText);
266
+ const tplJson = JSON.parse(node_fs_1.default.readFileSync(tplPath, 'utf-8'));
267
+ const added = [];
268
+ const missingInTpl = [];
269
+ for (const grp of ['dependencies', 'devDependencies']) {
270
+ const whitelist = rule[grp] ?? [];
271
+ if (whitelist.length === 0)
272
+ continue;
273
+ userJson[grp] ??= {};
274
+ for (const dep of whitelist) {
275
+ // 在 template 里找版本号:优先 tpl 同 grp,其次另一 grp(template 可能把 dep 写在
276
+ // dependencies/devDependencies 任一处,跟 user 当前 grp 不一定一致)
277
+ const tplVersion = tplJson[grp]?.[dep] ??
278
+ (grp === 'dependencies' ? tplJson.devDependencies?.[dep] : tplJson.dependencies?.[dep]);
279
+ if (tplVersion === undefined) {
280
+ missingInTpl.push(dep);
281
+ continue;
282
+ }
283
+ userJson[grp][dep] = tplVersion;
284
+ added.push(`${grp}.${dep}`);
285
+ }
286
+ }
287
+ if (added.length === 0 && missingInTpl.length === 0) {
288
+ (0, logger_1.log)(logPrefix, ` ○ ${rule.to ?? 'package.json'} (no deps to add)`);
289
+ return { rule, action: 'noop', path: rule.to ?? 'package.json' };
290
+ }
291
+ const trailing = userText.endsWith('\n') ? '\n' : '';
292
+ node_fs_1.default.writeFileSync(userPath, JSON.stringify(userJson, null, 2) + trailing);
293
+ (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(', ')}` : ''})`);
294
+ return {
295
+ rule,
296
+ action: 'patched',
297
+ path: rule.to ?? 'package.json',
298
+ detail: added.join(','),
299
+ };
300
+ }
167
301
  /**
168
302
  * 按 dot path 删除嵌套对象里的一个 leaf key。返回是否真的删了一项。
169
303
  * 不支持包含 `.` 的字段名(实际 dep 名 / scripts 名不会含 `.`,够用)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.16-alpha.0",
3
+ "version": "0.1.16-alpha.452772a",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -17,20 +17,6 @@
17
17
  "dist",
18
18
  "upgrade"
19
19
  ],
20
- "scripts": {
21
- "build": "bash scripts/build.sh",
22
- "typecheck": "tsc --noEmit -p tsconfig.json",
23
- "lint": "eslint src/ --max-warnings 0",
24
- "format": "prettier --write src/",
25
- "format:check": "prettier --check src/",
26
- "test": "vitest run --project=unit",
27
- "test:watch": "vitest --project=unit",
28
- "test:integration": "vitest run --project=integration",
29
- "dev": "node --import tsx src/main.ts",
30
- "cli": "node --env-file-if-exists=integration/.env --import tsx src/main.ts",
31
- "prepare": "husky",
32
- "prepublishOnly": "pnpm format:check && pnpm lint && pnpm build && pnpm test"
33
- },
34
20
  "keywords": [
35
21
  "miaoda",
36
22
  "cli",
@@ -65,5 +51,16 @@
65
51
  "vitest": "^4.1.4",
66
52
  "xml2js": "^0.6.2"
67
53
  },
68
- "packageManager": "pnpm@10.16.1+sha512.0e155aa2629db8672b49e8475da6226aa4bdea85fdcdfdc15350874946d4f3c91faaf64cbdc4a5d1ab8002f473d5c3fcedcd197989cf0390f9badd3c04678706"
69
- }
54
+ "scripts": {
55
+ "build": "bash scripts/build.sh",
56
+ "typecheck": "tsc --noEmit -p tsconfig.json",
57
+ "lint": "eslint src/ --max-warnings 0",
58
+ "format": "prettier --write src/",
59
+ "format:check": "prettier --check src/",
60
+ "test": "vitest run --project=unit",
61
+ "test:watch": "vitest --project=unit",
62
+ "test:integration": "vitest run --project=integration",
63
+ "dev": "node --import tsx src/main.ts",
64
+ "cli": "node --env-file-if-exists=integration/.env --import tsx src/main.ts"
65
+ }
66
+ }