@lark-apaas/miaoda-cli 0.1.33-alpha.e318de2 → 0.1.33-alpha.fbcdd20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/cli/commands/app/index.js +64 -0
- package/dist/cli/commands/deploy/modern.js +6 -5
- package/dist/cli/handlers/app/export-standalone.js +171 -0
- package/dist/cli/handlers/app/index.js +4 -1
- package/dist/cli/handlers/app/migrate.js +58 -133
- package/dist/services/app/export-standalone/archive.js +33 -0
- package/dist/services/app/export-standalone/copy-tree.js +51 -0
- package/dist/services/app/export-standalone/download.js +52 -0
- package/dist/services/app/export-standalone/index.js +21 -0
- package/dist/services/app/export-standalone/naming.js +65 -0
- package/dist/services/app/export-standalone/rewrite.js +72 -0
- package/dist/services/app/export-standalone/scan.js +72 -0
- package/dist/services/app/export-standalone/strategies.js +161 -0
- package/dist/services/deploy/modern/patch/actions.js +6 -4
- package/dist/services/deploy/modern/patch/html-title.js +75 -0
- package/dist/services/deploy/modern/patch/index.js +1 -1
- package/dist/services/deploy/modern/patch/routes.js +13 -3
- package/dist/services/deploy/modern/patch/source-scan.js +3 -40
- package/dist/utils/exclude-patterns.js +49 -0
- package/dist/utils/index.js +1 -5
- package/package.json +1 -1
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/lint.js +43 -7
- package/dist/utils/preview-control.js +0 -139
package/README.md
CHANGED
|
@@ -48,9 +48,31 @@ miaoda file ls --output json
|
|
|
48
48
|
| `miaoda observability ...` | 线上日志、链路、前端源码堆栈反查、监控指标、运营指标 |
|
|
49
49
|
| `miaoda comment ...` | 应用评论:查看评论列表、解决评论(仅 design-html 场景开放) |
|
|
50
50
|
| `miaoda registry ...` | 组件 registry:列出 / copy-in SDK 文件(本地执行) |
|
|
51
|
+
| `miaoda app ...` | 应用生命周期:元数据、初始化、平台同步、源码导出、静态产物导出 |
|
|
51
52
|
|
|
52
53
|
完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
|
|
53
54
|
|
|
55
|
+
## 导出可本地访问的静态产物
|
|
56
|
+
|
|
57
|
+
`miaoda app export-standalone` 把当前应用构建成一份自包含静态产物,可脱离妙搭平台本地打开。
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
miaoda app export-standalone # → dist/standalone/ + dist/standalone.zip
|
|
61
|
+
miaoda app export-standalone --out dist/share
|
|
62
|
+
miaoda app export-standalone --json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
支持 `design-html` / `html` / `vite-react` / `nestjs-react-fullstack`。页面与代码里字面以
|
|
66
|
+
`/spark/app` 开头的存储图片会经 dev server 代理下载到 `images/` 并改写成相对路径。
|
|
67
|
+
|
|
68
|
+
默认输出落在 `dist/` 下是刻意的:`dist` 已在部署扫描的 EXCLUDES 与各模板 `.gitignore` 中,
|
|
69
|
+
产物不会污染部署包、不会被误提交;代价是下一次构建会清掉它。
|
|
70
|
+
|
|
71
|
+
**只保证纯静态可看** —— 接口调用会失败,不做数据快照。多路由 SPA 在 `file://` 下不可用
|
|
72
|
+
(`BrowserRouter` 匹配不上路径),需用本地 HTTP server 打开,命令会在 `warnings` 里提示。
|
|
73
|
+
|
|
74
|
+
> 与 `miaoda app export` **语义无关**:后者导出的是源码归档(创意应用升级链路的只读参考物)。
|
|
75
|
+
|
|
54
76
|
## 组件 registry
|
|
55
77
|
|
|
56
78
|
`miaoda registry` 提供 shadcn 式的「组件 registry」:把 SDK 文件**拷进**当前项目(copy-in),而非作为依赖安装。registry 是独立 npm 包,按运行时分发(design-html / html → buildless runtime → `@lark-apaas/coding-registry-buildless`)。
|
|
@@ -22,6 +22,7 @@ function registerAppCommands(program, opts = {}) {
|
|
|
22
22
|
registerAppGet(appCmd);
|
|
23
23
|
registerAppUpdate(appCmd);
|
|
24
24
|
registerAppExport(appCmd);
|
|
25
|
+
registerAppExportStandalone(appCmd);
|
|
25
26
|
if (opts.includeInit) {
|
|
26
27
|
registerAppInit(appCmd);
|
|
27
28
|
registerAppSync(appCmd);
|
|
@@ -127,6 +128,10 @@ function registerAppExport(parent) {
|
|
|
127
128
|
.option('--out <dir>', '输出目录(相对当前目录)', 'source_package/creative')
|
|
128
129
|
.option('--checkpoint <n>', '按检查点版本导出;缺省为最新提交(HEAD)')
|
|
129
130
|
.addHelpText('after', `
|
|
131
|
+
与 'miaoda app export-standalone' 的区别(**两个命令语义无关,别选错**)
|
|
132
|
+
app export 导出源码归档(本命令)
|
|
133
|
+
app export-standalone 导出可本地访问的构建产物
|
|
134
|
+
|
|
130
135
|
说明
|
|
131
136
|
从服务端导出源应用(可以不是当前应用)的源码归档并解压到 --out 目录,导出完成后写
|
|
132
137
|
--out/.upgrade-manifest.json(含 sourceAppId / 导出时间 / 文件数),供升级链路幂等
|
|
@@ -173,6 +178,65 @@ JSON 输出
|
|
|
173
178
|
});
|
|
174
179
|
}));
|
|
175
180
|
}
|
|
181
|
+
function registerAppExportStandalone(parent) {
|
|
182
|
+
const cmd = parent
|
|
183
|
+
.command('export-standalone')
|
|
184
|
+
.description('把当前应用构建成自包含静态产物(目录 + zip),可脱离妙搭平台本地访问')
|
|
185
|
+
.option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
|
|
186
|
+
.option('--out <dir>', '输出目录(相对项目根)', index_1.DEFAULT_STANDALONE_OUT)
|
|
187
|
+
.option('--dev-server <url>', 'dev server 基址,默认 http://localhost:${CLIENT_DEV_PORT:-8080}')
|
|
188
|
+
.addHelpText('after', `
|
|
189
|
+
与 'miaoda app export' 的区别(**两个命令语义无关,别选错**)
|
|
190
|
+
app export 导出源码归档(创意应用升级链路的只读参考物)
|
|
191
|
+
app export-standalone 导出可本地访问的构建产物(本命令)
|
|
192
|
+
|
|
193
|
+
支持的 stack
|
|
194
|
+
design-html / html / vite-react / nestjs-react-fullstack
|
|
195
|
+
|
|
196
|
+
产物
|
|
197
|
+
<out>/ index.html + assets/ + images/(+ static/,vite 系有 shared/static 时)
|
|
198
|
+
<out>.zip 同内容的压缩包
|
|
199
|
+
默认 <out> 是 dist/standalone —— 落在 dist/ 下是刻意的:dist 已在部署扫描的
|
|
200
|
+
EXCLUDES 与各模板 .gitignore 中,产物不会污染部署包、不会被误提交。
|
|
201
|
+
代价:下一次构建会 rm -rf dist,导出产物是易失的。
|
|
202
|
+
|
|
203
|
+
图片本地化
|
|
204
|
+
只处理页面/代码里字面以 /spark/app 开头的 URL,经 dev server 代理下载到
|
|
205
|
+
<out>/images/ 并改写成相对路径。其它挂载前缀(/spark/p/、/app/<id>、反代)不处理。
|
|
206
|
+
接口运行时返回的 download_url 属动态 URL,静态扫描覆盖不到。
|
|
207
|
+
|
|
208
|
+
已知限制
|
|
209
|
+
只保证纯静态可看——接口调用会失败,不做数据快照。
|
|
210
|
+
多路由 SPA 在 file:// 下不可用(BrowserRouter 匹配不上路径、pushState 抛 SecurityError),
|
|
211
|
+
请用本地 HTTP server 打开;检测到时会在 warnings 里给 SPA_ROUTER_FILE_PROTOCOL。
|
|
212
|
+
design-html 的 cd-elements SDK 是 CDN 外链,断网打开自定义元素不渲染。
|
|
213
|
+
|
|
214
|
+
JSON 输出
|
|
215
|
+
{"data": {"appId": "...", "stack": "...", "out": "dist/standalone", "zip": "dist/standalone.zip",
|
|
216
|
+
"fileCount": N, "bytes": N,
|
|
217
|
+
"images": {"matched": N, "downloaded": N, "failed": [{"url": "...", "reason": "..."}]},
|
|
218
|
+
"rewrittenFiles": [...],
|
|
219
|
+
"warnings": [{"code": "...", "message": "..."}]}}
|
|
220
|
+
|
|
221
|
+
失败
|
|
222
|
+
单张图片下载失败 → 跳过并计入 images.failed + warnings,退出码 0。
|
|
223
|
+
命中 > 0 但全部失败 → EXPORT_STANDALONE_IMAGE_ALL_FAILED,退出码 1(系统性问题)。
|
|
224
|
+
stack 不支持 → EXPORT_STANDALONE_STACK_UNSUPPORTED。
|
|
225
|
+
构建失败 → EXPORT_STANDALONE_BUILD_FAILED。
|
|
226
|
+
|
|
227
|
+
示例
|
|
228
|
+
$ miaoda app export-standalone
|
|
229
|
+
$ miaoda app export-standalone --out dist/share
|
|
230
|
+
$ miaoda app export-standalone --json
|
|
231
|
+
`);
|
|
232
|
+
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
233
|
+
await (0, index_1.handleAppExportStandalone)({
|
|
234
|
+
dir: rawOpts.dir,
|
|
235
|
+
out: rawOpts.out,
|
|
236
|
+
devServer: rawOpts.devServer,
|
|
237
|
+
});
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
176
240
|
function registerAppGet(parent) {
|
|
177
241
|
const cmd = parent
|
|
178
242
|
.command('get')
|
|
@@ -72,7 +72,7 @@ JSON 输出(stdout)
|
|
|
72
72
|
}));
|
|
73
73
|
const patchCmd = deployCmd
|
|
74
74
|
.command('patch')
|
|
75
|
-
.description('design-html 增量发布:按文件 create/update/delete,.html
|
|
75
|
+
.description('design-html 增量发布:按文件 create/update/delete,.html 变动时同步 routes.json')
|
|
76
76
|
.option('--create <relpath>', '新增文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
77
77
|
.option('--update <relpath>', '覆盖已有文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
78
78
|
.option('--delete <relpath>', '删除文件(可重复)', shared_1.collectRepeatedOption, [])
|
|
@@ -80,10 +80,11 @@ JSON 输出(stdout)
|
|
|
80
80
|
.addHelpText('after', `
|
|
81
81
|
--dir 为项目目录(与 deploy 同名参数,默认当前目录)。
|
|
82
82
|
仅支持 design-html(design_local_deploy)。路径为工程根相对路径(= 服务端 latest 下 key),
|
|
83
|
-
禁绝对路径与 ".."。--create/--update
|
|
84
|
-
时自动重算并随包更新 routes.json
|
|
85
|
-
routes.json 每项为 {path, file}:path 为相对路径(不含 base 前缀,由消费侧拼接),
|
|
86
|
-
|
|
83
|
+
禁绝对路径与 ".."。--create/--update 的本地文件必须存在。改动里含任意 .html(增 / 改 / 删)
|
|
84
|
+
时自动重算并随包更新 routes.json;纯非 .html 改动不动 routes.json。
|
|
85
|
+
routes.json 每项为 {path, file, name?}:path 为相对路径(不含 base 前缀,由消费侧拼接),
|
|
86
|
+
file 为对应源文件(如 / → index.html),name 为该 HTML 的 <title> 文本(方案名,可缺省,
|
|
87
|
+
缺省时由消费侧 fallback 到 path)。
|
|
87
88
|
|
|
88
89
|
JSON 输出
|
|
89
90
|
{"data": {"upsertCount": <n>, "deleteCount": <n>, "actionsSent": <n>, "routesRegenerated": <bool>}}
|
|
@@ -0,0 +1,171 @@
|
|
|
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.DEFAULT_STANDALONE_OUT = void 0;
|
|
7
|
+
exports.handleAppExportStandalone = handleAppExportStandalone;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
11
|
+
const error_1 = require("../../../utils/error");
|
|
12
|
+
const output_1 = require("../../../utils/output");
|
|
13
|
+
const logger_1 = require("../../../utils/logger");
|
|
14
|
+
const index_1 = require("../../../services/app/export-standalone/index");
|
|
15
|
+
exports.DEFAULT_STANDALONE_OUT = 'dist/standalone';
|
|
16
|
+
/** 校验相对输出路径:禁绝对路径、禁含 ".." 段(与 app export 的 normalizeOutDir 同口径)。 */
|
|
17
|
+
function normalizeOutDir(rel) {
|
|
18
|
+
if (node_path_1.default.isAbsolute(rel) || rel.startsWith('/')) {
|
|
19
|
+
throw new error_1.AppError('ARGS_INVALID', `输出目录不能是绝对路径:${rel}`);
|
|
20
|
+
}
|
|
21
|
+
const posix = rel.split(node_path_1.default.sep).join('/').replace(/^\.\//, '');
|
|
22
|
+
if (posix.split('/').includes('..')) {
|
|
23
|
+
throw new error_1.AppError('ARGS_INVALID', `输出目录不能包含 "..":${rel}`);
|
|
24
|
+
}
|
|
25
|
+
return posix;
|
|
26
|
+
}
|
|
27
|
+
function resolveDevServerBase(explicit) {
|
|
28
|
+
if (explicit)
|
|
29
|
+
return explicit.replace(/\/+$/, '');
|
|
30
|
+
const port = process.env.CLIENT_DEV_PORT ?? '8080';
|
|
31
|
+
return `http://localhost:${port}`;
|
|
32
|
+
}
|
|
33
|
+
/** 遍历产物树里的文本文件,返回 posix 相对路径。 */
|
|
34
|
+
function* walkText(dir, rel = '') {
|
|
35
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
36
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
37
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
38
|
+
if (entry.isDirectory())
|
|
39
|
+
yield* walkText(abs, childRel);
|
|
40
|
+
else if (index_1.TEXT_EXTENSIONS.has(node_path_1.default.extname(entry.name).toLowerCase()))
|
|
41
|
+
yield childRel;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** 统计产物树的文件数与总字节数。 */
|
|
45
|
+
function measure(dir) {
|
|
46
|
+
let fileCount = 0;
|
|
47
|
+
let bytes = 0;
|
|
48
|
+
const walk = (cur) => {
|
|
49
|
+
for (const entry of node_fs_1.default.readdirSync(cur, { withFileTypes: true })) {
|
|
50
|
+
const abs = node_path_1.default.join(cur, entry.name);
|
|
51
|
+
if (entry.isDirectory())
|
|
52
|
+
walk(abs);
|
|
53
|
+
else {
|
|
54
|
+
fileCount += 1;
|
|
55
|
+
bytes += node_fs_1.default.statSync(abs).size;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(dir);
|
|
60
|
+
return { fileCount, bytes };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 扫产物里是否残留 BrowserRouter —— standalone 不做 HashRouter 改造(明确决策),
|
|
64
|
+
* 多路由 SPA 在 file:// 下 `location.pathname` 是完整文件系统路径,匹配不上任何 Route,
|
|
65
|
+
* 且 pushState 在 opaque origin 下抛 SecurityError。检测到就告知用户改用本地 server。
|
|
66
|
+
*/
|
|
67
|
+
function detectBrowserRouter(outDir) {
|
|
68
|
+
for (const rel of walkText(outDir)) {
|
|
69
|
+
const text = node_fs_1.default.readFileSync(node_path_1.default.join(outDir, rel), 'utf-8');
|
|
70
|
+
if (/\b(BrowserRouter|createBrowserRouter)\b/.test(text))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* miaoda app export-standalone [--out <dir>]
|
|
77
|
+
*
|
|
78
|
+
* 把当前应用构建成自包含静态产物:按 stack 走各自的 strategy 产出可静态服务的目录树,
|
|
79
|
+
* 再扫出 `/spark/app` 图片经 dev server 代理下载到 `images/` 并改写成相对路径,最后打 zip。
|
|
80
|
+
*
|
|
81
|
+
* 设计文档:docs/superpowers/specs/2026-08-19-app-export-standalone-design.md
|
|
82
|
+
*/
|
|
83
|
+
async function handleAppExportStandalone(opts) {
|
|
84
|
+
const projectDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
85
|
+
const meta = (0, spark_meta_1.readSparkMeta)(projectDir);
|
|
86
|
+
if (!meta.stack) {
|
|
87
|
+
throw new error_1.AppError('ARGS_INVALID', '.spark/meta.json 缺少 stack —— 请先跑 miaoda app init', {
|
|
88
|
+
next_actions: ['cd 到应用根目录后重试,或先执行 miaoda app init'],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const strategy = (0, index_1.resolveStrategy)(meta.stack);
|
|
92
|
+
const outRel = normalizeOutDir(opts.out ?? exports.DEFAULT_STANDALONE_OUT);
|
|
93
|
+
const outDir = node_path_1.default.join(projectDir, outRel);
|
|
94
|
+
const zipRel = `${outRel}.zip`;
|
|
95
|
+
const zipPath = node_path_1.default.join(projectDir, zipRel);
|
|
96
|
+
// 1. stack-specific:产出可静态服务的目录树
|
|
97
|
+
strategy({ projectDir, outDir });
|
|
98
|
+
if (!node_fs_1.default.existsSync(outDir) || node_fs_1.default.readdirSync(outDir).length === 0) {
|
|
99
|
+
throw new error_1.AppError('EXPORT_STANDALONE_EMPTY_OUTPUT', '构建产物为空', {
|
|
100
|
+
next_actions: ['先手动跑一次该 stack 的构建命令确认工程本身可构建'],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// 2. 扫描
|
|
104
|
+
const urls = [];
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (const rel of walkText(outDir)) {
|
|
107
|
+
for (const url of (0, index_1.scanSparkAppUrls)(node_fs_1.default.readFileSync(node_path_1.default.join(outDir, rel), 'utf-8'))) {
|
|
108
|
+
if (seen.has(url))
|
|
109
|
+
continue;
|
|
110
|
+
seen.add(url);
|
|
111
|
+
urls.push(url);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// 3. 下载
|
|
115
|
+
const devServerBase = resolveDevServerBase(opts.devServer);
|
|
116
|
+
(0, logger_1.log)('export-standalone', `Matched ${String(urls.length)} image url(s), fetching via ${devServerBase}`);
|
|
117
|
+
const { mapping, failed } = await (0, index_1.downloadImages)(urls, {
|
|
118
|
+
imagesDir: node_path_1.default.join(outDir, 'images'),
|
|
119
|
+
devServerBase,
|
|
120
|
+
});
|
|
121
|
+
// 命中 > 0 且全部失败 = 系统性问题(代理挂了 / 路径口径错),静默成功会误导调用方
|
|
122
|
+
if (urls.length > 0 && mapping.size === 0) {
|
|
123
|
+
throw new error_1.AppError('EXPORT_STANDALONE_IMAGE_ALL_FAILED', `命中 ${String(urls.length)} 张图片但全部下载失败`, {
|
|
124
|
+
next_actions: [
|
|
125
|
+
'确认 dev server 正在运行且能代理 /spark/app 路径',
|
|
126
|
+
`首个失败:${failed[0]?.url ?? '?'} → ${failed[0]?.reason ?? 'unknown'}`,
|
|
127
|
+
...(failed.length > 1
|
|
128
|
+
? [`其余 ${String(failed.length - 1)} 条见 --verbose(每条请求地址都会打到 stderr)`]
|
|
129
|
+
: []),
|
|
130
|
+
`实际请求前缀:${devServerBase}(来自 ${opts.devServer ? '--dev-server' : process.env.CLIENT_DEV_PORT ? 'CLIENT_DEV_PORT env' : '默认 8080'})`,
|
|
131
|
+
'若端口不对,可用 --dev-server http://localhost:<port> 覆盖',
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// 4. 改写
|
|
136
|
+
const rewrittenFiles = (0, index_1.rewriteTree)(outDir, mapping);
|
|
137
|
+
// 5. 打包
|
|
138
|
+
(0, index_1.zipDir)(outDir, zipPath);
|
|
139
|
+
// 6. 输出
|
|
140
|
+
const warnings = [];
|
|
141
|
+
if (failed.length > 0) {
|
|
142
|
+
warnings.push({
|
|
143
|
+
code: 'IMAGE_DOWNLOAD_PARTIAL_FAILURE',
|
|
144
|
+
message: `${String(failed.length)} 张图片下载失败,产物中保留原始绝对路径(离线打不开)`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (detectBrowserRouter(outDir)) {
|
|
148
|
+
warnings.push({
|
|
149
|
+
code: 'SPA_ROUTER_FILE_PROTOCOL',
|
|
150
|
+
message: '检测到 BrowserRouter:产物在 file:// 下不可用(路由匹配不上、pushState 抛 SecurityError),请用本地 HTTP server 打开',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const { fileCount, bytes } = measure(outDir);
|
|
154
|
+
(0, output_1.emit)({
|
|
155
|
+
data: {
|
|
156
|
+
appId: meta.app_id ?? '',
|
|
157
|
+
stack: meta.stack,
|
|
158
|
+
out: outRel,
|
|
159
|
+
zip: zipRel,
|
|
160
|
+
fileCount,
|
|
161
|
+
bytes,
|
|
162
|
+
images: {
|
|
163
|
+
matched: urls.length,
|
|
164
|
+
downloaded: mapping.size,
|
|
165
|
+
failed,
|
|
166
|
+
},
|
|
167
|
+
rewrittenFiles,
|
|
168
|
+
warnings,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.UPGRADE_MANIFEST_FILENAME = exports.DEFAULT_EXPORT_OUT = exports.handleAppExport = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
|
|
3
|
+
exports.DEFAULT_STANDALONE_OUT = exports.handleAppExportStandalone = exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.UPGRADE_MANIFEST_FILENAME = exports.DEFAULT_EXPORT_OUT = exports.handleAppExport = 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");
|
|
@@ -19,3 +19,6 @@ Object.defineProperty(exports, "handleAppMigrate", { enumerable: true, get: func
|
|
|
19
19
|
// commands 层渲染 help 时需要这份枚举;从 handler barrel 转发,避免 commands → services 越界
|
|
20
20
|
var index_1 = require("../../../services/app/init/index");
|
|
21
21
|
Object.defineProperty(exports, "SUPPORTED_STACKS", { enumerable: true, get: function () { return index_1.SUPPORTED_STACKS; } });
|
|
22
|
+
var export_standalone_1 = require("./export-standalone");
|
|
23
|
+
Object.defineProperty(exports, "handleAppExportStandalone", { enumerable: true, get: function () { return export_standalone_1.handleAppExportStandalone; } });
|
|
24
|
+
Object.defineProperty(exports, "DEFAULT_STANDALONE_OUT", { enumerable: true, get: function () { return export_standalone_1.DEFAULT_STANDALONE_OUT; } });
|
|
@@ -15,7 +15,6 @@ const spark_meta_1 = require("../../../utils/spark-meta");
|
|
|
15
15
|
const error_1 = require("../../../utils/error");
|
|
16
16
|
const output_1 = require("../../../utils/output");
|
|
17
17
|
const env_1 = require("../../../utils/env");
|
|
18
|
-
const preview_control_1 = require("../../../utils/preview-control");
|
|
19
18
|
const logger_1 = require("../../../utils/logger");
|
|
20
19
|
/**
|
|
21
20
|
* miaoda app migrate --to <stack> [--from <stack>] [--dir <path>]
|
|
@@ -100,139 +99,78 @@ async function handleAppMigrate(opts) {
|
|
|
100
99
|
// 的 miaodaTemplate.archType 拿到的动态值,rule 配置时不知道,由 handler 在拿到
|
|
101
100
|
// renderTemplate 结果后单独写。version 也一并写回方便 sync 后续诊断。
|
|
102
101
|
(0, spark_meta_1.writeSparkMeta)(targetDir, { archType: templateArchType, version: templateVersion });
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let previewStopped = false;
|
|
113
|
-
let previewMayBeSuspended = false;
|
|
114
|
-
let previewStopError;
|
|
115
|
-
if ((0, env_1.isSandboxEnv)()) {
|
|
116
|
-
(0, logger_1.log)('migrate', '停止 dev server(预览)以便安全重装依赖...');
|
|
117
|
-
const stopped = await (0, preview_control_1.stopPreview)({ logPrefix: 'migrate' });
|
|
118
|
-
previewStopped = stopped.ok;
|
|
119
|
-
previewMayBeSuspended = stopped.mayBeSuspended;
|
|
120
|
-
if (!stopped.ok) {
|
|
121
|
-
previewStopError = stopped.error;
|
|
122
|
-
const tail = previewMayBeSuspended ? '结束时仍会尝试恢复' : '降级为不停 dev 直接重装';
|
|
123
|
-
(0, logger_1.log)('migrate', `⚠ 停止预览失败 (continuing, ${tail}): ${stopped.error ?? ''}`);
|
|
124
|
-
}
|
|
102
|
+
// 清掉 user app 的 node_modules —— migrate 改了 dependencies 集合(删 lite + 加 NestJS
|
|
103
|
+
// 全套)+ rule 已经把 package-lock.json 删了。如果 node_modules 留着,npm install 时旧
|
|
104
|
+
// 包还在 node_modules 顶层 hoist 树里,可能跟新 package.json spec 撞 peer 冲突
|
|
105
|
+
// (比如旧 coding-preset-vite-react@1.0.8 peer vite@^8 跟新 vite@^7 撞)。
|
|
106
|
+
// 软失败:node_modules 不存在不报错。
|
|
107
|
+
const nodeModulesPath = node_path_1.default.join(targetDir, 'node_modules');
|
|
108
|
+
if (node_fs_1.default.existsSync(nodeModulesPath)) {
|
|
109
|
+
(0, logger_1.log)('migrate', '清理 node_modules(dependencies 集合已变)...');
|
|
110
|
+
node_fs_1.default.rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
125
111
|
}
|
|
112
|
+
// 跑 npm install 物化新依赖集合 + 把 followLatestPackages 钉 latest
|
|
113
|
+
// --ignore-scripts 绕开 action-plugin postinstall 在缺平台 env 时的 ENOENT
|
|
114
|
+
// --registry 钉同一份 npmmirror(跟 init / sync 行为一致)
|
|
115
|
+
// 软失败:install 挂了不阻断 emit;用户拿到详细 error,自行 npm install 兜底
|
|
126
116
|
const followLatest = config.followLatestPackages ?? [];
|
|
117
|
+
const installArgs = [
|
|
118
|
+
'install',
|
|
119
|
+
'--no-audit',
|
|
120
|
+
'--no-fund',
|
|
121
|
+
'--ignore-scripts',
|
|
122
|
+
'--registry',
|
|
123
|
+
(0, index_2.resolveNpmInstallRegistry)(),
|
|
124
|
+
...followLatest.map((pkg) => `${pkg}@latest`),
|
|
125
|
+
];
|
|
126
|
+
(0, logger_1.log)('migrate', `Running npm ${installArgs.join(' ')}...`);
|
|
127
127
|
let installError;
|
|
128
|
-
let nodeModulesCleanError;
|
|
129
|
-
let previewRestarted = false;
|
|
130
|
-
let previewRestartError;
|
|
131
128
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
129
|
+
(0, node_child_process_1.execFileSync)('npm', installArgs, {
|
|
130
|
+
cwd: targetDir,
|
|
131
|
+
stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
installError = err instanceof Error ? err.message : String(err);
|
|
136
|
+
(0, logger_1.log)('migrate', `⚠ npm install failed (continuing): ${installError}`);
|
|
137
|
+
}
|
|
138
|
+
// 重启 dev process 前清 vite cache —— migrate 改了 package.json + lockfile,
|
|
139
|
+
// 老 vite 进程的 .vite/deps cache 的 lockfileHash 跟新 lockfile 不一致。
|
|
140
|
+
// 不清的话 supervisor 拉起的新 vite cold start 会立即 "Re-optimizing because
|
|
141
|
+
// lockfile has changed", re-optimize 期间 ModuleGraph transform cache 跟
|
|
142
|
+
// deps middleware metadata 短暂不同步, 浏览器请求老 hash 触发
|
|
143
|
+
// ERR_OUTDATED_OPTIMIZED_DEP → 504 死锁(详见 fullstack-plugin#1143 的 504
|
|
144
|
+
// ignoreOutdatedRequests fix 注释)。直接 rm -rf 让新 vite 从干净状态 cold
|
|
145
|
+
// optimize, 一次写到位 lockfile 一致的 metadata, 不再触发中途 re-optimize。
|
|
146
|
+
//
|
|
147
|
+
// - 只在 SANDBOX_ID 非空时做(本地一般不会撞这条链路)
|
|
148
|
+
// - 只在 install 成功时做(install 挂了 node_modules 状态不对, 清 cache 也救不回来)
|
|
149
|
+
// - 软失败:.vite 不存在 / 删失败都不阻断
|
|
150
|
+
if (installError === undefined &&
|
|
151
|
+
process.env.SANDBOX_ID !== undefined &&
|
|
152
|
+
process.env.SANDBOX_ID !== '') {
|
|
153
|
+
const viteCacheDir = node_path_1.default.join(targetDir, 'node_modules', '.vite');
|
|
154
|
+
if (node_fs_1.default.existsSync(viteCacheDir)) {
|
|
155
|
+
(0, logger_1.log)('migrate', '清理 node_modules/.vite (vite cache 跟新 lockfile 错位会 504)...');
|
|
143
156
|
try {
|
|
144
|
-
node_fs_1.default.rmSync(
|
|
145
|
-
recursive: true,
|
|
146
|
-
force: true,
|
|
147
|
-
maxRetries: 5,
|
|
148
|
-
retryDelay: 300,
|
|
149
|
-
});
|
|
157
|
+
node_fs_1.default.rmSync(viteCacheDir, { recursive: true, force: true });
|
|
150
158
|
}
|
|
151
159
|
catch (err) {
|
|
152
|
-
|
|
153
|
-
(0, logger_1.log)('migrate', `⚠ 清理 node_modules 失败 (continuing): ${nodeModulesCleanError}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
// 跑 npm install 物化新依赖集合 + 把 followLatestPackages 钉 latest
|
|
157
|
-
// --ignore-scripts 绕开 action-plugin postinstall 在缺平台 env 时的 ENOENT
|
|
158
|
-
// --registry 钉同一份 npmmirror(跟 init / sync 行为一致)
|
|
159
|
-
// 软失败:install 挂了不阻断 emit;用户拿到详细 error,自行 npm install 兜底
|
|
160
|
-
const installArgs = [
|
|
161
|
-
'install',
|
|
162
|
-
'--no-audit',
|
|
163
|
-
'--no-fund',
|
|
164
|
-
'--ignore-scripts',
|
|
165
|
-
'--registry',
|
|
166
|
-
(0, index_2.resolveNpmInstallRegistry)(),
|
|
167
|
-
...followLatest.map((pkg) => `${pkg}@latest`),
|
|
168
|
-
];
|
|
169
|
-
(0, logger_1.log)('migrate', `Running npm ${installArgs.join(' ')}...`);
|
|
170
|
-
try {
|
|
171
|
-
(0, node_child_process_1.execFileSync)('npm', installArgs, {
|
|
172
|
-
cwd: targetDir,
|
|
173
|
-
stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
catch (err) {
|
|
177
|
-
installError = err instanceof Error ? err.message : String(err);
|
|
178
|
-
(0, logger_1.log)('migrate', `⚠ npm install failed (continuing): ${installError}`);
|
|
179
|
-
}
|
|
180
|
-
// 重启 dev process 前清 vite cache —— migrate 改了 package.json + lockfile,
|
|
181
|
-
// 老 vite 进程的 .vite/deps cache 的 lockfileHash 跟新 lockfile 不一致。
|
|
182
|
-
// 不清的话拉起的新 vite cold start 会立即 "Re-optimizing because
|
|
183
|
-
// lockfile has changed", re-optimize 期间 ModuleGraph transform cache 跟
|
|
184
|
-
// deps middleware metadata 短暂不同步, 浏览器请求老 hash 触发
|
|
185
|
-
// ERR_OUTDATED_OPTIMIZED_DEP → 504 死锁(详见 fullstack-plugin#1143 的 504
|
|
186
|
-
// ignoreOutdatedRequests fix 注释)。直接 rm -rf 让新 vite 从干净状态 cold
|
|
187
|
-
// optimize, 一次写到位 lockfile 一致的 metadata, 不再触发中途 re-optimize。
|
|
188
|
-
//
|
|
189
|
-
// - 只在 SANDBOX_ID 非空时做(本地一般不会撞这条链路)
|
|
190
|
-
// - 只在 install 成功时做(install 挂了 node_modules 状态不对, 清 cache 也救不回来)
|
|
191
|
-
// - 软失败:.vite 不存在 / 删失败都不阻断
|
|
192
|
-
if (installError === undefined &&
|
|
193
|
-
process.env.SANDBOX_ID !== undefined &&
|
|
194
|
-
process.env.SANDBOX_ID !== '') {
|
|
195
|
-
const viteCacheDir = node_path_1.default.join(targetDir, 'node_modules', '.vite');
|
|
196
|
-
if (node_fs_1.default.existsSync(viteCacheDir)) {
|
|
197
|
-
(0, logger_1.log)('migrate', '清理 node_modules/.vite (vite cache 跟新 lockfile 错位会 504)...');
|
|
198
|
-
try {
|
|
199
|
-
node_fs_1.default.rmSync(viteCacheDir, { recursive: true, force: true });
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
(0, logger_1.log)('migrate', `⚠ 清 .vite 失败 (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
// 恢复 dev server —— 只要暂停位可能已生效就必须调,且必须在 finally 里:
|
|
209
|
-
// rule 应用 / install / 清 cache 任何一步抛异常都不能把保活留在关闭状态,否则用户沙箱
|
|
210
|
-
// 的预览再也起不来(比迁移失败严重得多)。
|
|
211
|
-
// 无条件恢复(不看 installError):dev 进程已经被停了,留着不起来只会更糟;新 scripts/dev.js
|
|
212
|
-
// + 新依赖也正是靠这次 start 生效。
|
|
213
|
-
// 用 previewMayBeSuspended 而非 previewStopped:stop 超时/500 时服务端暂停位已经置上了。
|
|
214
|
-
// 多余的 start 无害(服务端未暂停且进程在跑时 no-op)。
|
|
215
|
-
if (previewMayBeSuspended) {
|
|
216
|
-
(0, logger_1.log)('migrate', '恢复 dev server(预览)...');
|
|
217
|
-
const started = await (0, preview_control_1.startPreview)({ logPrefix: 'migrate' });
|
|
218
|
-
previewRestarted = started.ok;
|
|
219
|
-
if (!started.ok) {
|
|
220
|
-
previewRestartError = started.error;
|
|
221
|
-
(0, logger_1.log)('migrate', `✗ 恢复预览失败, 需人工处理: ${started.error ?? ''}`);
|
|
160
|
+
(0, logger_1.log)('migrate', `⚠ 清 .vite 失败 (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
222
161
|
}
|
|
223
162
|
}
|
|
224
163
|
}
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
//
|
|
164
|
+
// 沙箱环境下重启 dev process —— scripts/dev.js 被 fullstack 版本覆盖了,但当前正在
|
|
165
|
+
// 跑的 node 进程仍按老逻辑工作(只起 vite,没起 nest),整体没切到 fullstack 模式。
|
|
166
|
+
// 通过 pkill 杀掉 dev.js 主进程,依赖沙箱平台 supervisor 自动重新 exec dev.sh → dev.js,
|
|
167
|
+
// 新进程用新 deps + 新 scripts,正常进入 fullstack 模式。
|
|
228
168
|
// - 只在沙箱内做(isSandboxEnv;本地环境用户进程混杂,主动 pkill 风险大)
|
|
229
|
-
// - previewMayBeSuspended 时不做 —— 新 code-server 上暂停位管着重建,pkill 无意义;
|
|
230
|
-
// 进程该由上面的 startPreview 拉起
|
|
231
169
|
// - install 挂了不做 —— 新依赖不全, 杀旧 dev 后新 dev 起不来反而更糟,
|
|
232
170
|
// 留着旧进程让用户先处理 installError
|
|
233
171
|
// - 软失败:pkill 无匹配进程退出 1,catch 吞掉
|
|
234
|
-
let devRestarted =
|
|
235
|
-
if (
|
|
172
|
+
let devRestarted = false;
|
|
173
|
+
if (installError === undefined && (0, env_1.isSandboxEnv)()) {
|
|
236
174
|
(0, logger_1.log)('migrate', '沙箱环境,重启 dev process(平台 supervisor 会自动拉起)...');
|
|
237
175
|
try {
|
|
238
176
|
(0, node_child_process_1.execFileSync)('pkill', ['-f', 'node.*scripts/dev\\.js'], { stdio: 'ignore' });
|
|
@@ -256,24 +194,11 @@ async function handleAppMigrate(opts) {
|
|
|
256
194
|
...summarizeResults(results),
|
|
257
195
|
followLatestPackages: followLatest,
|
|
258
196
|
installError,
|
|
259
|
-
nodeModulesCleanError,
|
|
260
197
|
devRestarted,
|
|
261
|
-
previewStopped,
|
|
262
|
-
previewStopError,
|
|
263
|
-
previewMayBeSuspended,
|
|
264
|
-
previewRestarted,
|
|
265
|
-
previewRestartError,
|
|
266
198
|
nextActions: (0, env_1.isSandboxEnv)()
|
|
267
199
|
? [
|
|
268
200
|
'git status / git diff 评估改动并 commit',
|
|
269
201
|
'miaoda skills sync 同步到新 stack 的 agent skills',
|
|
270
|
-
// 预览恢复失败时补一条人工兜底动作 —— 暂停位还挂着,预览不会自己起来。
|
|
271
|
-
// 兜底自愈:暂停位只存 code-server 内存,重启 code-server 也能恢复。
|
|
272
|
-
...(previewRestartError === undefined
|
|
273
|
-
? []
|
|
274
|
-
: [
|
|
275
|
-
`⚠ dev server(预览)未恢复成功,需手动 curl -X POST ${(0, preview_control_1.resolveCodeServerBaseUrl)()}/api/v1/preview/start 恢复保活(暂停状态只存内存,重启 code-server 亦可自愈)`,
|
|
276
|
-
]),
|
|
277
202
|
]
|
|
278
203
|
: [
|
|
279
204
|
'git status / git diff 评估改动并 commit',
|
|
@@ -0,0 +1,33 @@
|
|
|
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.zipDir = zipDir;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const error_1 = require("../../../utils/error");
|
|
11
|
+
/**
|
|
12
|
+
* 把 `dir` 的**内容**打成 zip(解压后直接是 index.html,不多一层目录)。
|
|
13
|
+
*
|
|
14
|
+
* 走系统 `zip` 而非引 npm 依赖:仓库现有的 `app export` 也是 shell 出去调 `unzip`/`tar`,
|
|
15
|
+
* 保持一致,且避免为一个收尾动作增加运行时依赖。
|
|
16
|
+
*
|
|
17
|
+
* 覆盖语义:`zip` 默认是**增量更新**已存在的归档(会残留上次的文件),
|
|
18
|
+
* 所以先删掉目标文件再打,保证重跑导出幂等。
|
|
19
|
+
*/
|
|
20
|
+
function zipDir(dir, zipPath) {
|
|
21
|
+
if (!node_fs_1.default.existsSync(dir)) {
|
|
22
|
+
throw new error_1.AppError('EXPORT_STANDALONE_ARCHIVE_FAILED', `待打包目录不存在:${dir}`);
|
|
23
|
+
}
|
|
24
|
+
node_fs_1.default.rmSync(zipPath, { force: true });
|
|
25
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(zipPath), { recursive: true });
|
|
26
|
+
try {
|
|
27
|
+
// cwd=dir + '.' → 归档内是目录内容而非目录本身
|
|
28
|
+
(0, node_child_process_1.execFileSync)('zip', ['-qr', zipPath, '.'], { cwd: dir, stdio: 'pipe' });
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
throw new error_1.AppError('EXPORT_STANDALONE_ARCHIVE_FAILED', `zip failed: ${err.message}`, { next_actions: ['确认运行环境有 zip 命令(沙箱镜像应自带)'] });
|
|
32
|
+
}
|
|
33
|
+
}
|