@lark-apaas/miaoda-cli 0.1.33-alpha.155f17e → 0.1.33-alpha.2ce4ff9
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 +7 -6
- package/dist/cli/commands/app/index.js +35 -17
- package/dist/cli/handlers/app/index.js +6 -4
- package/dist/cli/handlers/app/{export-standalone.js → pack.js} +112 -25
- package/dist/services/app/{export-standalone → pack}/archive.js +4 -2
- package/dist/services/app/{export-standalone → pack}/copy-tree.js +1 -1
- package/dist/services/app/{export-standalone → pack}/download.js +2 -2
- package/dist/services/app/{export-standalone → pack}/index.js +6 -2
- package/dist/services/app/{export-standalone → pack}/inline-babel.js +1 -1
- package/dist/services/app/{export-standalone → pack}/strategies.js +75 -17
- package/dist/services/app/pack/upload.js +110 -0
- package/dist/utils/exclude-patterns.js +1 -1
- package/package.json +1 -1
- /package/dist/services/app/{export-standalone → pack}/naming.js +0 -0
- /package/dist/services/app/{export-standalone → pack}/rewrite.js +0 -0
- /package/dist/services/app/{export-standalone → pack}/scan.js +0 -0
package/README.md
CHANGED
|
@@ -52,14 +52,15 @@ miaoda file ls --output json
|
|
|
52
52
|
|
|
53
53
|
完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
|
|
54
54
|
|
|
55
|
-
##
|
|
55
|
+
## 打包可本地访问的静态产物
|
|
56
56
|
|
|
57
|
-
`miaoda app
|
|
57
|
+
`miaoda app pack`(默认 `--mode standalone`)把当前应用构建成一份自包含静态产物,可脱离妙搭平台本地打开。
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
miaoda app
|
|
61
|
-
miaoda app
|
|
62
|
-
miaoda app
|
|
60
|
+
miaoda app pack # → dist/standalone/ + dist/standalone.zip
|
|
61
|
+
miaoda app pack --out dist/share
|
|
62
|
+
miaoda app pack --json
|
|
63
|
+
miaoda app pack --upload-url 'https://tos.example.com/x.zip?sign=...'
|
|
63
64
|
```
|
|
64
65
|
|
|
65
66
|
支持 `design-html` / `html` / `vite-react` / `nestjs-react-fullstack`。页面与代码里字面以
|
|
@@ -71,7 +72,7 @@ miaoda app export-standalone --json
|
|
|
71
72
|
**只保证纯静态可看** —— 接口调用会失败,不做数据快照。多路由 SPA 在 `file://` 下不可用
|
|
72
73
|
(`BrowserRouter` 匹配不上路径),需用本地 HTTP server 打开,命令会在 `warnings` 里提示。
|
|
73
74
|
|
|
74
|
-
> 与 `miaoda app export`
|
|
75
|
+
> 与 `miaoda app export` **语义无关**:后者拉的是**其他应用**的源码归档(创意应用升级链路的只读参考物)。
|
|
75
76
|
|
|
76
77
|
## 组件 registry
|
|
77
78
|
|
|
@@ -22,7 +22,7 @@ function registerAppCommands(program, opts = {}) {
|
|
|
22
22
|
registerAppGet(appCmd);
|
|
23
23
|
registerAppUpdate(appCmd);
|
|
24
24
|
registerAppExport(appCmd);
|
|
25
|
-
|
|
25
|
+
registerAppPack(appCmd);
|
|
26
26
|
if (opts.includeInit) {
|
|
27
27
|
registerAppInit(appCmd);
|
|
28
28
|
registerAppSync(appCmd);
|
|
@@ -128,9 +128,9 @@ function registerAppExport(parent) {
|
|
|
128
128
|
.option('--out <dir>', '输出目录(相对当前目录)', 'source_package/creative')
|
|
129
129
|
.option('--checkpoint <n>', '按检查点版本导出;缺省为最新提交(HEAD)')
|
|
130
130
|
.addHelpText('after', `
|
|
131
|
-
与 'miaoda app
|
|
132
|
-
app export
|
|
133
|
-
app
|
|
131
|
+
与 'miaoda app pack' 的区别(**两个命令语义无关,别选错**)
|
|
132
|
+
app export 拉「其他应用」的源码归档(本命令)
|
|
133
|
+
app pack 把「当前应用」打包成产物(默认 standalone 形态)
|
|
134
134
|
|
|
135
135
|
说明
|
|
136
136
|
从服务端导出源应用(可以不是当前应用)的源码归档并解压到 --out 目录,导出完成后写
|
|
@@ -178,17 +178,23 @@ JSON 输出
|
|
|
178
178
|
});
|
|
179
179
|
}));
|
|
180
180
|
}
|
|
181
|
-
function
|
|
181
|
+
function registerAppPack(parent) {
|
|
182
182
|
const cmd = parent
|
|
183
|
-
.command('
|
|
184
|
-
.description('
|
|
183
|
+
.command('pack')
|
|
184
|
+
.description('打包应用产物。默认 standalone 模式:自包含静态产物(目录 + zip),可脱离妙搭平台本地访问')
|
|
185
185
|
.option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
|
|
186
|
-
.option('--
|
|
186
|
+
.option('--mode <mode>', `打包形态(${index_1.PACK_MODES.join(' / ')})`, index_1.DEFAULT_PACK_MODE)
|
|
187
|
+
.option('--out <dir>', '输出目录(相对项目根)', index_1.DEFAULT_PACK_OUT)
|
|
187
188
|
.option('--dev-server <url>', '图片下载的请求前缀,默认沙箱网关 http://localhost:8080')
|
|
189
|
+
.option('--upload-url <url>', '打包后把 zip 以 PUT 上传到该预签名 URL(http/https);上传成功后清掉本地产物')
|
|
188
190
|
.addHelpText('after', `
|
|
189
191
|
与 'miaoda app export' 的区别(**两个命令语义无关,别选错**)
|
|
190
|
-
app export
|
|
191
|
-
app
|
|
192
|
+
app export 拉「其他应用」的源码归档(创意应用升级链路的只读参考物)
|
|
193
|
+
app pack 把「当前应用」打包成产物(本命令)
|
|
194
|
+
|
|
195
|
+
打包形态(--mode)
|
|
196
|
+
standalone(默认) 自包含静态产物,可脱离妙搭平台本地访问
|
|
197
|
+
后续新增形态时在此列出;不传 --mode 的行为永远等同 standalone。
|
|
192
198
|
|
|
193
199
|
支持的 stack
|
|
194
200
|
design-html / html / vite-react / nestjs-react-fullstack
|
|
@@ -196,6 +202,11 @@ function registerAppExportStandalone(parent) {
|
|
|
196
202
|
产物
|
|
197
203
|
<out>/ index.html + assets/ + images/(+ static/,vite 系有 shared/static 时)
|
|
198
204
|
<out>.zip 同内容的压缩包
|
|
205
|
+
传了 --upload-url 时,zip 会以 PUT 裸 body(Content-Type: application/zip)
|
|
206
|
+
上传到该地址——预签名 URL 的标准形态,鉴权由 URL 自带签名承担,不加额外 header。
|
|
207
|
+
**上传成功后本地产物(目录 + zip)会被清掉**,交付物以远端为准,JSON 里
|
|
208
|
+
localRemoved=true;此时不要再去读 out / zip 路径。
|
|
209
|
+
上传失败则保留本地产物,便于修正后重跑或手动上传。
|
|
199
210
|
默认 <out> 是 dist/standalone —— 落在 dist/ 下是刻意的:dist 已在部署扫描的
|
|
200
211
|
EXCLUDES 与各模板 .gitignore 中,产物不会污染部署包、不会被误提交。
|
|
201
212
|
代价:下一次构建会 rm -rf dist,导出产物是易失的。
|
|
@@ -215,25 +226,32 @@ JSON 输出
|
|
|
215
226
|
{"data": {"appId": "...", "stack": "...", "out": "dist/standalone", "zip": "dist/standalone.zip",
|
|
216
227
|
"fileCount": N, "bytes": N,
|
|
217
228
|
"images": {"matched": N, "downloaded": N, "failed": [{"url": "...", "reason": "..."}]},
|
|
229
|
+
"localRemoved": false,
|
|
218
230
|
"rewrittenFiles": [...],
|
|
219
231
|
"warnings": [{"code": "...", "message": "..."}]}}
|
|
220
232
|
|
|
221
233
|
失败
|
|
222
234
|
单张图片下载失败 → 跳过并计入 images.failed + warnings,退出码 0。
|
|
223
|
-
命中 > 0 但全部失败 →
|
|
224
|
-
stack 不支持 →
|
|
225
|
-
构建失败 →
|
|
235
|
+
命中 > 0 但全部失败 → PACK_IMAGE_ALL_FAILED,退出码 1(系统性问题)。
|
|
236
|
+
stack 不支持 → PACK_STACK_UNSUPPORTED。
|
|
237
|
+
构建失败 → PACK_BUILD_FAILED。
|
|
238
|
+
上传失败(非 2xx / 网络异常)→ PACK_UPLOAD_FAILED,退出码 1。
|
|
239
|
+
本地产物保留,可修正 URL 后重跑或手动上传。
|
|
240
|
+
--upload-url 非 http/https → ARGS_INVALID,退出码 2。
|
|
226
241
|
|
|
227
242
|
示例
|
|
228
|
-
$ miaoda app
|
|
229
|
-
$ miaoda app
|
|
230
|
-
$ miaoda app
|
|
243
|
+
$ miaoda app pack
|
|
244
|
+
$ miaoda app pack --out dist/share
|
|
245
|
+
$ miaoda app pack --json
|
|
246
|
+
$ miaoda app pack --upload-url 'https://tos.example.com/x.zip?sign=...'
|
|
231
247
|
`);
|
|
232
248
|
cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
|
|
233
|
-
await (0, index_1.
|
|
249
|
+
await (0, index_1.handleAppPack)({
|
|
234
250
|
dir: rawOpts.dir,
|
|
235
251
|
out: rawOpts.out,
|
|
236
252
|
devServer: rawOpts.devServer,
|
|
253
|
+
uploadUrl: rawOpts.uploadUrl,
|
|
254
|
+
mode: rawOpts.mode,
|
|
237
255
|
});
|
|
238
256
|
}));
|
|
239
257
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.PACK_MODES = exports.DEFAULT_PACK_MODE = exports.DEFAULT_PACK_OUT = exports.handleAppPack = 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,6 +19,8 @@ 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
|
|
23
|
-
Object.defineProperty(exports, "
|
|
24
|
-
Object.defineProperty(exports, "
|
|
22
|
+
var pack_1 = require("./pack");
|
|
23
|
+
Object.defineProperty(exports, "handleAppPack", { enumerable: true, get: function () { return pack_1.handleAppPack; } });
|
|
24
|
+
Object.defineProperty(exports, "DEFAULT_PACK_OUT", { enumerable: true, get: function () { return pack_1.DEFAULT_PACK_OUT; } });
|
|
25
|
+
Object.defineProperty(exports, "DEFAULT_PACK_MODE", { enumerable: true, get: function () { return pack_1.DEFAULT_PACK_MODE; } });
|
|
26
|
+
Object.defineProperty(exports, "PACK_MODES", { enumerable: true, get: function () { return pack_1.PACK_MODES; } });
|
|
@@ -3,16 +3,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
exports.
|
|
6
|
+
exports.DEFAULT_PACK_OUT = exports.DEFAULT_PACK_MODE = exports.PACK_MODES = void 0;
|
|
7
|
+
exports.handleAppPack = handleAppPack;
|
|
8
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const exclude_patterns_1 = require("../../../utils/exclude-patterns");
|
|
10
11
|
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
11
12
|
const error_1 = require("../../../utils/error");
|
|
12
13
|
const output_1 = require("../../../utils/output");
|
|
13
14
|
const logger_1 = require("../../../utils/logger");
|
|
14
|
-
const index_1 = require("../../../services/app/
|
|
15
|
-
|
|
15
|
+
const index_1 = require("../../../services/app/pack/index");
|
|
16
|
+
/**
|
|
17
|
+
* 支持的打包形态。当前只有 `standalone`;留成枚举是为了让扩展点显式可见 ——
|
|
18
|
+
* 新增形态时只需往这里加,命令 help 会自动列出。
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ 不传 `--mode` 的行为永远等同 `standalone`,这是对调用方的稳定契约:
|
|
21
|
+
* 将来加了新形态也不能改默认值,否则存量调用会静默换语义。
|
|
22
|
+
*/
|
|
23
|
+
exports.PACK_MODES = ['standalone'];
|
|
24
|
+
exports.DEFAULT_PACK_MODE = 'standalone';
|
|
25
|
+
/** standalone 形态的默认输出目录。新增形态时各自用自己的目录名。 */
|
|
26
|
+
exports.DEFAULT_PACK_OUT = 'dist/standalone';
|
|
16
27
|
/** 校验相对输出路径:禁绝对路径、禁含 ".." 段(与 app export 的 normalizeOutDir 同口径)。 */
|
|
17
28
|
function normalizeOutDir(rel) {
|
|
18
29
|
if (node_path_1.default.isAbsolute(rel) || rel.startsWith('/')) {
|
|
@@ -69,27 +80,66 @@ function measure(dir) {
|
|
|
69
80
|
return { fileCount, bytes };
|
|
70
81
|
}
|
|
71
82
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
83
|
+
* 扫**项目源码**里是否用了 BrowserRouter。
|
|
84
|
+
*
|
|
85
|
+
* preset 在 standalone 下会把它编译期换成 HashRouter(`standaloneRouterPlugin`),
|
|
86
|
+
* 换完 file:// 下路由完全可用。这里保留检测是为了**覆盖不到时给提示**:插件只替换
|
|
87
|
+
* 用户源码里字面的 `BrowserRouter` / `createBrowserRouter`,碰不到的情况有——
|
|
88
|
+
* 用户二次封装(`export const Router = BrowserRouter`)、非 react-router 的路由库、
|
|
89
|
+
* 代码里硬编码 `window.location.href = '/about'`。
|
|
90
|
+
*
|
|
91
|
+
* 命中时只是提示"若路由异常请用本地 server",不代表一定坏。
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ **必须扫源码,不能扫产物**:编译型 stack 的产物经过 minify,`BrowserRouter`
|
|
94
|
+
* 这个标识符会被改名,扫产物永远命中不了 —— 而那恰恰是最需要这条警告的场景。
|
|
95
|
+
* 真机踩过:vite-react 应用源码里明明有 BrowserRouter、产物首屏渲染成 404,
|
|
96
|
+
* 但因为扫的是产物,warnings 是空的。
|
|
97
|
+
*
|
|
98
|
+
* 扫描面套 EXCLUDES(跳过 node_modules / dist 等),四个 stack 统一按项目根扫。
|
|
75
99
|
*/
|
|
76
|
-
function detectBrowserRouter(
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
function detectBrowserRouter(projectDir) {
|
|
101
|
+
const walk = (dir) => {
|
|
102
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
103
|
+
if (exclude_patterns_1.EXCLUDES.has(entry.name))
|
|
104
|
+
continue;
|
|
105
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
106
|
+
let isDir;
|
|
107
|
+
try {
|
|
108
|
+
isDir = node_fs_1.default.statSync(abs).isDirectory();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (isDir) {
|
|
114
|
+
if (walk(abs))
|
|
115
|
+
return true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!index_1.TEXT_EXTENSIONS.has(node_path_1.default.extname(entry.name).toLowerCase()))
|
|
119
|
+
continue;
|
|
120
|
+
if (/\b(BrowserRouter|createBrowserRouter)\b/.test(node_fs_1.default.readFileSync(abs, 'utf-8'))) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
126
|
+
return walk(projectDir);
|
|
83
127
|
}
|
|
84
128
|
/**
|
|
85
|
-
* miaoda app
|
|
129
|
+
* miaoda app pack [--mode standalone] [--out <dir>]
|
|
86
130
|
*
|
|
87
131
|
* 把当前应用构建成自包含静态产物:按 stack 走各自的 strategy 产出可静态服务的目录树,
|
|
88
132
|
* 再扫出 `/spark/app` 图片经 dev server 代理下载到 `images/` 并改写成相对路径,最后打 zip。
|
|
89
133
|
*
|
|
90
|
-
* 设计文档:docs/superpowers/specs/2026-08-19-app-
|
|
134
|
+
* 设计文档:docs/superpowers/specs/2026-08-19-app-pack-design.md
|
|
91
135
|
*/
|
|
92
|
-
async function
|
|
136
|
+
async function handleAppPack(opts) {
|
|
137
|
+
const mode = opts.mode ?? exports.DEFAULT_PACK_MODE;
|
|
138
|
+
if (!exports.PACK_MODES.includes(mode)) {
|
|
139
|
+
throw new error_1.AppError('ARGS_INVALID', `不支持的打包形态:${mode}`, {
|
|
140
|
+
next_actions: [`可用形态:${exports.PACK_MODES.join(', ')}`],
|
|
141
|
+
});
|
|
142
|
+
}
|
|
93
143
|
const projectDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
|
|
94
144
|
const meta = (0, spark_meta_1.readSparkMeta)(projectDir);
|
|
95
145
|
if (!meta.stack) {
|
|
@@ -98,14 +148,17 @@ async function handleAppExportStandalone(opts) {
|
|
|
98
148
|
});
|
|
99
149
|
}
|
|
100
150
|
const strategy = (0, index_1.resolveStrategy)(meta.stack);
|
|
101
|
-
|
|
151
|
+
// 参数校验一律前置:URL 写错不该等到构建 + 下载 + 打包跑完才报(那是几十秒)
|
|
152
|
+
if (opts.uploadUrl)
|
|
153
|
+
(0, index_1.assertValidUploadUrl)(opts.uploadUrl);
|
|
154
|
+
const outRel = normalizeOutDir(opts.out ?? exports.DEFAULT_PACK_OUT);
|
|
102
155
|
const outDir = node_path_1.default.join(projectDir, outRel);
|
|
103
156
|
const zipRel = `${outRel}.zip`;
|
|
104
157
|
const zipPath = node_path_1.default.join(projectDir, zipRel);
|
|
105
158
|
// 1. stack-specific:产出可静态服务的目录树
|
|
106
159
|
strategy({ projectDir, outDir });
|
|
107
160
|
if (!node_fs_1.default.existsSync(outDir) || node_fs_1.default.readdirSync(outDir).length === 0) {
|
|
108
|
-
throw new error_1.AppError('
|
|
161
|
+
throw new error_1.AppError('PACK_EMPTY_OUTPUT', '构建产物为空', {
|
|
109
162
|
next_actions: ['先手动跑一次该 stack 的构建命令确认工程本身可构建'],
|
|
110
163
|
});
|
|
111
164
|
}
|
|
@@ -122,14 +175,14 @@ async function handleAppExportStandalone(opts) {
|
|
|
122
175
|
}
|
|
123
176
|
// 3. 下载
|
|
124
177
|
const devServerBase = resolveDevServerBase(opts.devServer);
|
|
125
|
-
(0, logger_1.log)('
|
|
178
|
+
(0, logger_1.log)('pack', `Matched ${String(urls.length)} image url(s), fetching via ${devServerBase}`);
|
|
126
179
|
const { mapping, failed } = await (0, index_1.downloadImages)(urls, {
|
|
127
180
|
imagesDir: node_path_1.default.join(outDir, 'images'),
|
|
128
181
|
devServerBase,
|
|
129
182
|
});
|
|
130
183
|
// 命中 > 0 且全部失败 = 系统性问题(代理挂了 / 路径口径错),静默成功会误导调用方
|
|
131
184
|
if (urls.length > 0 && mapping.size === 0) {
|
|
132
|
-
throw new error_1.AppError('
|
|
185
|
+
throw new error_1.AppError('PACK_IMAGE_ALL_FAILED', `命中 ${String(urls.length)} 张图片但全部下载失败`, {
|
|
133
186
|
next_actions: [
|
|
134
187
|
'确认 dev server 正在运行且能代理 /spark/app 路径',
|
|
135
188
|
`首个失败:${failed[0]?.url ?? '?'} → ${failed[0]?.reason ?? 'unknown'}`,
|
|
@@ -145,27 +198,59 @@ async function handleAppExportStandalone(opts) {
|
|
|
145
198
|
const rewrittenFiles = (0, index_1.rewriteTree)(outDir, mapping);
|
|
146
199
|
// 5. 打包
|
|
147
200
|
(0, index_1.zipDir)(outDir, zipPath);
|
|
148
|
-
// 6.
|
|
201
|
+
// 6. 统计。**必须在清理之前**——上传成功后本地产物会被删掉,那时就量不到了。
|
|
202
|
+
const { fileCount, bytes } = measure(outDir);
|
|
203
|
+
// 7. 上传(可选)。放在 emit 之前——传了 --upload-url 就说明上传是目的,
|
|
204
|
+
// 失败要让整个命令失败(退出码 1),不能先 emit 成功再抛。
|
|
205
|
+
// JSON 输出里**不回显 URL**(凭证类信息),只给状态码与服务端 log_id。
|
|
206
|
+
const upload = opts.uploadUrl ? await (0, index_1.uploadZip)(zipPath, opts.uploadUrl) : undefined;
|
|
149
207
|
const warnings = [];
|
|
208
|
+
// 8. 上传成功后清掉本地产物:交付物已在远端,沙箱磁盘不必留一份目录 + 一份 zip。
|
|
209
|
+
//
|
|
210
|
+
// **只在上传成功后清**。上传失败时产物必须留着——错误提示里明确写了「本地产物已保留,
|
|
211
|
+
// 可修正 --upload-url 后重跑,或手动上传」,清掉会让那句话变成谎话,且用户得重跑
|
|
212
|
+
// 整轮构建。失败路径走 uploadZip 抛错,根本到不了这里。
|
|
213
|
+
//
|
|
214
|
+
// 清理失败不算命令失败:上传已经成功,目的达到了,删不掉只是留了点垃圾 → 降级为 warning。
|
|
215
|
+
let localRemoved = false;
|
|
216
|
+
if (upload) {
|
|
217
|
+
try {
|
|
218
|
+
node_fs_1.default.rmSync(outDir, { recursive: true, force: true });
|
|
219
|
+
node_fs_1.default.rmSync(zipPath, { force: true });
|
|
220
|
+
localRemoved = true;
|
|
221
|
+
(0, logger_1.log)('pack', `Uploaded, local artifacts removed (${outRel}, ${zipRel})`);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
warnings.push({
|
|
225
|
+
code: 'LOCAL_CLEANUP_FAILED',
|
|
226
|
+
message: `上传成功但本地产物清理失败:${err instanceof Error ? err.message : String(err)}`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
150
230
|
if (failed.length > 0) {
|
|
151
231
|
warnings.push({
|
|
152
232
|
code: 'IMAGE_DOWNLOAD_PARTIAL_FAILURE',
|
|
153
233
|
message: `${String(failed.length)} 张图片下载失败,产物中保留原始绝对路径(离线打不开)`,
|
|
154
234
|
});
|
|
155
235
|
}
|
|
156
|
-
if (detectBrowserRouter(
|
|
236
|
+
if (detectBrowserRouter(projectDir)) {
|
|
157
237
|
warnings.push({
|
|
158
238
|
code: 'SPA_ROUTER_FILE_PROTOCOL',
|
|
159
|
-
message: '检测到 BrowserRouter
|
|
239
|
+
message: '检测到 BrowserRouter:standalone 构建已自动换成 HashRouter,路由形如 index.html#/about。' +
|
|
240
|
+
'若你对 router 做过二次封装或用了非 react-router 的方案,替换可能覆盖不到 —— ' +
|
|
241
|
+
'此时产物在 file:// 下路由会失效(首屏落 404 页),请改用本地 HTTP server 并从 / 访问。',
|
|
160
242
|
});
|
|
161
243
|
}
|
|
162
|
-
const { fileCount, bytes } = measure(outDir);
|
|
163
244
|
(0, output_1.emit)({
|
|
164
245
|
data: {
|
|
165
246
|
appId: meta.app_id ?? '',
|
|
166
247
|
stack: meta.stack,
|
|
248
|
+
mode,
|
|
167
249
|
out: outRel,
|
|
168
250
|
zip: zipRel,
|
|
251
|
+
// 上传成功后本地产物已被清掉——调用方**不要**再去读 out / zip 这两个路径,
|
|
252
|
+
// 它们只描述"产出过什么",不代表现在还在。
|
|
253
|
+
localRemoved,
|
|
169
254
|
fileCount,
|
|
170
255
|
bytes,
|
|
171
256
|
images: {
|
|
@@ -174,6 +259,8 @@ async function handleAppExportStandalone(opts) {
|
|
|
174
259
|
failed,
|
|
175
260
|
},
|
|
176
261
|
rewrittenFiles,
|
|
262
|
+
// 只在传了 --upload-url 时出现:{ status, logId? }。刻意不含 URL。
|
|
263
|
+
...(upload ? { upload } : {}),
|
|
177
264
|
warnings,
|
|
178
265
|
},
|
|
179
266
|
});
|
|
@@ -19,7 +19,7 @@ const error_1 = require("../../../utils/error");
|
|
|
19
19
|
*/
|
|
20
20
|
function zipDir(dir, zipPath) {
|
|
21
21
|
if (!node_fs_1.default.existsSync(dir)) {
|
|
22
|
-
throw new error_1.AppError('
|
|
22
|
+
throw new error_1.AppError('PACK_ARCHIVE_FAILED', `待打包目录不存在:${dir}`);
|
|
23
23
|
}
|
|
24
24
|
node_fs_1.default.rmSync(zipPath, { force: true });
|
|
25
25
|
node_fs_1.default.mkdirSync(node_path_1.default.dirname(zipPath), { recursive: true });
|
|
@@ -28,6 +28,8 @@ function zipDir(dir, zipPath) {
|
|
|
28
28
|
(0, node_child_process_1.execFileSync)('zip', ['-qr', zipPath, '.'], { cwd: dir, stdio: 'pipe' });
|
|
29
29
|
}
|
|
30
30
|
catch (err) {
|
|
31
|
-
throw new error_1.AppError('
|
|
31
|
+
throw new error_1.AppError('PACK_ARCHIVE_FAILED', `zip failed: ${err.message}`, {
|
|
32
|
+
next_actions: ['确认运行环境有 zip 命令(沙箱镜像应自带)'],
|
|
33
|
+
});
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -18,7 +18,7 @@ const error_1 = require("../../../utils/error");
|
|
|
18
18
|
*/
|
|
19
19
|
function copyTree(root, out, excludes) {
|
|
20
20
|
if (!node_fs_1.default.existsSync(root)) {
|
|
21
|
-
throw new error_1.AppError('
|
|
21
|
+
throw new error_1.AppError('PACK_EMPTY_OUTPUT', `源目录不存在:${root}`, {
|
|
22
22
|
next_actions: ['确认已在应用根目录下执行,且项目结构完整'],
|
|
23
23
|
});
|
|
24
24
|
}
|
|
@@ -26,7 +26,7 @@ async function downloadImages(urls, opts) {
|
|
|
26
26
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
27
27
|
for (const url of urls) {
|
|
28
28
|
const target = `${opts.devServerBase}${url}`;
|
|
29
|
-
(0, logger_1.debug)(`
|
|
29
|
+
(0, logger_1.debug)(`pack: GET ${target}`);
|
|
30
30
|
try {
|
|
31
31
|
// 刻意串行(见函数注释):图片量级通常个位数到几十,并发收益有限;
|
|
32
32
|
// 串行能让每张图的失败归因清晰,且不对沙箱 dev server 造成并发压力。
|
|
@@ -42,7 +42,7 @@ async function downloadImages(urls, opts) {
|
|
|
42
42
|
const name = (0, naming_1.localImageName)(url, resp.headers.get('content-type') ?? undefined);
|
|
43
43
|
node_fs_1.default.writeFileSync(node_path_1.default.join(opts.imagesDir, name), buf);
|
|
44
44
|
mapping.set(url, name);
|
|
45
|
-
(0, logger_1.debug)(`
|
|
45
|
+
(0, logger_1.debug)(`pack: downloaded ${url} -> images/${name} (${String(buf.length)}B)`);
|
|
46
46
|
}
|
|
47
47
|
catch (err) {
|
|
48
48
|
failed.push({ url, reason: err instanceof Error ? err.message : String(err) });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.PACK_SUPPORTED_STACKS = exports.resolveStrategy = exports.buildStandaloneEnv = exports.uploadZip = exports.redactUrl = exports.assertValidUploadUrl = exports.scanSparkAppUrls = exports.TEXT_EXTENSIONS = exports.rewriteTree = exports.localImageName = exports.extFromContentType = exports.inlineBabelScripts = exports.downloadImages = exports.copyTree = exports.zipDir = void 0;
|
|
4
4
|
var archive_1 = require("./archive");
|
|
5
5
|
Object.defineProperty(exports, "zipDir", { enumerable: true, get: function () { return archive_1.zipDir; } });
|
|
6
6
|
var copy_tree_1 = require("./copy-tree");
|
|
@@ -17,7 +17,11 @@ Object.defineProperty(exports, "rewriteTree", { enumerable: true, get: function
|
|
|
17
17
|
Object.defineProperty(exports, "TEXT_EXTENSIONS", { enumerable: true, get: function () { return rewrite_1.TEXT_EXTENSIONS; } });
|
|
18
18
|
var scan_1 = require("./scan");
|
|
19
19
|
Object.defineProperty(exports, "scanSparkAppUrls", { enumerable: true, get: function () { return scan_1.scanSparkAppUrls; } });
|
|
20
|
+
var upload_1 = require("./upload");
|
|
21
|
+
Object.defineProperty(exports, "assertValidUploadUrl", { enumerable: true, get: function () { return upload_1.assertValidUploadUrl; } });
|
|
22
|
+
Object.defineProperty(exports, "redactUrl", { enumerable: true, get: function () { return upload_1.redactUrl; } });
|
|
23
|
+
Object.defineProperty(exports, "uploadZip", { enumerable: true, get: function () { return upload_1.uploadZip; } });
|
|
20
24
|
var strategies_1 = require("./strategies");
|
|
21
25
|
Object.defineProperty(exports, "buildStandaloneEnv", { enumerable: true, get: function () { return strategies_1.buildStandaloneEnv; } });
|
|
22
26
|
Object.defineProperty(exports, "resolveStrategy", { enumerable: true, get: function () { return strategies_1.resolveStrategy; } });
|
|
23
|
-
Object.defineProperty(exports, "
|
|
27
|
+
Object.defineProperty(exports, "PACK_SUPPORTED_STACKS", { enumerable: true, get: function () { return strategies_1.PACK_SUPPORTED_STACKS; } });
|
|
@@ -77,7 +77,7 @@ function inlineBabelScripts(root) {
|
|
|
77
77
|
// `</script` 出现在 JS 字符串里会提前闭合标签,必须转义
|
|
78
78
|
const code = node_fs_1.default.readFileSync(target, 'utf-8').replace(/<\/script/gi, '<\\/script');
|
|
79
79
|
const keptAttrs = attrs.replace(SRC_ATTR_RE, '').replace(/\s+/g, ' ').trimEnd();
|
|
80
|
-
(0, logger_1.debug)(`
|
|
80
|
+
(0, logger_1.debug)(`pack: inlined ${src} into ${rel}`);
|
|
81
81
|
return `<script${keptAttrs} data-inlined-from="${src}">\n${code}\n</script>`;
|
|
82
82
|
});
|
|
83
83
|
// 用「文本是否变化」判定,不用外层 flag —— 回调里的赋值 TS 控制流分析看不到,
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.PACK_SUPPORTED_STACKS = void 0;
|
|
7
7
|
exports.buildStandaloneEnv = buildStandaloneEnv;
|
|
8
8
|
exports.liftNestedEntry = liftNestedEntry;
|
|
9
9
|
exports.resolveStrategy = resolveStrategy;
|
|
@@ -11,11 +11,12 @@ const node_child_process_1 = require("node:child_process");
|
|
|
11
11
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
13
|
const error_1 = require("../../../utils/error");
|
|
14
|
+
const output_1 = require("../../../utils/output");
|
|
14
15
|
const exclude_patterns_1 = require("../../../utils/exclude-patterns");
|
|
15
16
|
const logger_1 = require("../../../utils/logger");
|
|
16
17
|
const copy_tree_1 = require("./copy-tree");
|
|
17
18
|
const inline_babel_1 = require("./inline-babel");
|
|
18
|
-
exports.
|
|
19
|
+
exports.PACK_SUPPORTED_STACKS = new Set([
|
|
19
20
|
'design-html',
|
|
20
21
|
'html',
|
|
21
22
|
'vite-react',
|
|
@@ -36,9 +37,15 @@ function buildStandaloneEnv(base) {
|
|
|
36
37
|
delete env.MIAODA_APP_ID;
|
|
37
38
|
delete env.CLIENT_BASE_PATH;
|
|
38
39
|
env.MIAODA_BUILD_TARGET = 'standalone';
|
|
40
|
+
env.NODE_ENV = 'production';
|
|
41
|
+
// vite-react 走 build.sh,由它把 MIAODA_RESOURCE_CDN_PREFIX 映射成 ASSETS_CDN_PATH
|
|
39
42
|
env.MIAODA_RESOURCE_CDN_PREFIX = './';
|
|
43
|
+
// nestjs 走 `npm run build:client` 直接调 vite,**没有 build.sh 那层映射** ——
|
|
44
|
+
// 必须直接给 preset 认的名字,否则 publicPath 为空 → base '/' → 产物里是
|
|
45
|
+
// `/assets/x.js` 绝对路径,file:// 下指向文件系统根,页面白屏(真机踩过)。
|
|
46
|
+
// vite-react 那边 build.sh 会用同样的值覆盖它,两条路结果一致。
|
|
47
|
+
env.ASSETS_CDN_PATH = './';
|
|
40
48
|
env.STATIC_ASSETS_BASE_URL = './static';
|
|
41
|
-
env.NODE_ENV = 'production';
|
|
42
49
|
return env;
|
|
43
50
|
}
|
|
44
51
|
/** 把 `from` 目录内容并入 `into`(into 已存在,不清空)。from 不存在则静默跳过。 */
|
|
@@ -77,6 +84,31 @@ function liftNestedEntry(outDir) {
|
|
|
77
84
|
if (node_fs_1.default.readdirSync(nestedDir).length === 0)
|
|
78
85
|
node_fs_1.default.rmdirSync(nestedDir);
|
|
79
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* 从产物树里删掉 sourcemap(`*.map`)。
|
|
89
|
+
*
|
|
90
|
+
* preset 的 prod 配置是 `sourcemap: 'hidden'` —— map 文件会产出,但产物里**没有**
|
|
91
|
+
* `sourceMappingURL` 注释,即没人引用它。留在 standalone 包里有两个实打实的问题:
|
|
92
|
+
*
|
|
93
|
+
* 1. **体积**:实测真实应用 4.7MB map / 5.9MB 总体积,占 80%
|
|
94
|
+
* 2. **泄源码**:map 的 `sourcesContent` 内嵌原始源码(实测含 12 个业务文件,
|
|
95
|
+
* 如 `src/lib/utils.ts`)。standalone 包是拿来分发给人的,等于附赠全部源码
|
|
96
|
+
*
|
|
97
|
+
* 只对有编译步骤的 stack 调用;buildless 栈(design-html / html)的产物就是源码,
|
|
98
|
+
* 那里的 `.map`(若有)是用户自己的文件,不该动。
|
|
99
|
+
*/
|
|
100
|
+
function dropSourceMaps(outDir) {
|
|
101
|
+
const walk = (dir) => {
|
|
102
|
+
for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
103
|
+
const abs = node_path_1.default.join(dir, entry.name);
|
|
104
|
+
if (entry.isDirectory())
|
|
105
|
+
walk(abs);
|
|
106
|
+
else if (entry.name.toLowerCase().endsWith('.map'))
|
|
107
|
+
node_fs_1.default.rmSync(abs, { force: true });
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
walk(outDir);
|
|
111
|
+
}
|
|
80
112
|
/** 从产物树里删掉 routes.json —— 平台/server 路由用,standalone 无人读取。 */
|
|
81
113
|
function dropRoutesJson(outDir) {
|
|
82
114
|
node_fs_1.default.rmSync(node_path_1.default.join(outDir, 'routes.json'), { force: true });
|
|
@@ -85,17 +117,22 @@ function runProjectBuild(projectDir, command) {
|
|
|
85
117
|
try {
|
|
86
118
|
(0, node_child_process_1.execSync)(command, {
|
|
87
119
|
cwd: projectDir,
|
|
88
|
-
|
|
120
|
+
// JSON 模式下把子进程 stdout 重定向到父进程 stderr(fd 2)——构建工具会往
|
|
121
|
+
// stdout 打一大堆进度,会把最终 emit 的 JSON 信封冲掉,下游 parse 直接失败。
|
|
122
|
+
// 终端仍看得到进度(走 stderr)。与 app init 的 stdioFor 同一套做法。
|
|
123
|
+
stdio: (0, output_1.isJsonMode)() ? ['ignore', 2, 'inherit'] : 'inherit',
|
|
89
124
|
env: buildStandaloneEnv(process.env),
|
|
90
125
|
});
|
|
91
126
|
}
|
|
92
127
|
catch (err) {
|
|
93
|
-
throw new error_1.AppError('
|
|
128
|
+
throw new error_1.AppError('PACK_BUILD_FAILED', `${command} failed: ${err.message}`, {
|
|
129
|
+
next_actions: ['先手动跑一次该构建命令确认工程本身可构建'],
|
|
130
|
+
});
|
|
94
131
|
}
|
|
95
132
|
}
|
|
96
133
|
/** design-html:项目根纯拷贝,套 deploy 同一份 EXCLUDES。 */
|
|
97
134
|
const designHtmlStrategy = ({ projectDir, outDir }) => {
|
|
98
|
-
(0, logger_1.log)('
|
|
135
|
+
(0, logger_1.log)('pack', 'Copying source (design-html)...');
|
|
99
136
|
(0, copy_tree_1.copyTree)(projectDir, outDir, exclude_patterns_1.EXCLUDES);
|
|
100
137
|
(0, inline_babel_1.inlineBabelScripts)(outDir);
|
|
101
138
|
dropRoutesJson(outDir);
|
|
@@ -106,7 +143,7 @@ const designHtmlStrategy = ({ projectDir, outDir }) => {
|
|
|
106
143
|
* 调它再逐项撤销不如不调(设计文档「html 跳过 coding-html-devserver build」)。
|
|
107
144
|
*/
|
|
108
145
|
const htmlStrategy = ({ projectDir, outDir }) => {
|
|
109
|
-
(0, logger_1.log)('
|
|
146
|
+
(0, logger_1.log)('pack', 'Copying src/ (html)...');
|
|
110
147
|
(0, copy_tree_1.copyTree)(node_path_1.default.join(projectDir, 'src'), outDir);
|
|
111
148
|
(0, inline_babel_1.inlineBabelScripts)(outDir);
|
|
112
149
|
dropRoutesJson(outDir);
|
|
@@ -117,7 +154,7 @@ const htmlStrategy = ({ projectDir, outDir }) => {
|
|
|
117
154
|
* `virtual:capabilities` 虚拟模块,产物目录不引用它。
|
|
118
155
|
*/
|
|
119
156
|
const viteReactStrategy = ({ projectDir, outDir }) => {
|
|
120
|
-
(0, logger_1.log)('
|
|
157
|
+
(0, logger_1.log)('pack', 'Building (vite-react)...');
|
|
121
158
|
runProjectBuild(projectDir, 'bash scripts/build.sh');
|
|
122
159
|
const dist = node_path_1.default.join(projectDir, 'dist');
|
|
123
160
|
node_fs_1.default.rmSync(outDir, { recursive: true, force: true });
|
|
@@ -125,6 +162,7 @@ const viteReactStrategy = ({ projectDir, outDir }) => {
|
|
|
125
162
|
mergeInto(node_path_1.default.join(dist, 'output'), outDir);
|
|
126
163
|
mergeInto(node_path_1.default.join(dist, 'output_resource', 'assets'), node_path_1.default.join(outDir, 'assets'));
|
|
127
164
|
mergeInto(node_path_1.default.join(dist, 'output_static'), node_path_1.default.join(outDir, 'static'));
|
|
165
|
+
dropSourceMaps(outDir);
|
|
128
166
|
dropRoutesJson(outDir);
|
|
129
167
|
};
|
|
130
168
|
/**
|
|
@@ -137,16 +175,34 @@ const viteReactStrategy = ({ projectDir, outDir }) => {
|
|
|
137
175
|
* (standalone 不挂 preset 的 vite-html-output 插件),需 `liftNestedEntry` 提到根。
|
|
138
176
|
*/
|
|
139
177
|
const nestjsStrategy = ({ projectDir, outDir }) => {
|
|
140
|
-
(0, logger_1.log)('
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
178
|
+
(0, logger_1.log)('pack', 'Building client (nestjs-react-fullstack)...');
|
|
179
|
+
// 构建到**独立临时目录**,不碰 `dist/client`。
|
|
180
|
+
//
|
|
181
|
+
// 两个原因:
|
|
182
|
+
// 1. preset 设了 `emptyOutDir: false`(它指望 `scripts/build.sh` 先 `rm -rf dist`),
|
|
183
|
+
// 而我们绕过 build.sh 直接跑 `build:client`,没人清目录 —— 复用 `dist/client`
|
|
184
|
+
// 会把上次构建的 chunk 一起带进产物(真机见过新旧两个 index-<hash>.js 并存)。
|
|
185
|
+
// 2. `dist/client` 是平台链路的产物目录(dev 期 html-output 会往里写、NestJS 从
|
|
186
|
+
// 那儿取 HTML),导出这种旁路动作**不该清它**,否则破坏既有目录结构。
|
|
187
|
+
//
|
|
188
|
+
// vite CLI 的 `--outDir` 优先级高于 config,`--emptyOutDir` 覆盖 `emptyOutDir: false`。
|
|
189
|
+
// 放在 dist/ 下是为了留在 vite root 内(root 外 vite 会告警)。
|
|
190
|
+
const tmpDist = node_path_1.default.join(projectDir, 'dist', '.standalone-client');
|
|
191
|
+
try {
|
|
192
|
+
runProjectBuild(projectDir, `npm run build:client -- --outDir ${JSON.stringify(node_path_1.default.relative(projectDir, tmpDist))} --emptyOutDir`);
|
|
193
|
+
if (!node_fs_1.default.existsSync(tmpDist)) {
|
|
194
|
+
throw new error_1.AppError('PACK_EMPTY_OUTPUT', `client 构建产物 ${node_path_1.default.relative(projectDir, tmpDist)} 不存在`);
|
|
195
|
+
}
|
|
196
|
+
node_fs_1.default.rmSync(outDir, { recursive: true, force: true });
|
|
197
|
+
node_fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
198
|
+
mergeInto(tmpDist, outDir);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
// 临时构建目录不留在用户项目里
|
|
202
|
+
node_fs_1.default.rmSync(tmpDist, { recursive: true, force: true });
|
|
145
203
|
}
|
|
146
|
-
node_fs_1.default.rmSync(outDir, { recursive: true, force: true });
|
|
147
|
-
node_fs_1.default.mkdirSync(outDir, { recursive: true });
|
|
148
|
-
mergeInto(clientDist, outDir);
|
|
149
204
|
liftNestedEntry(outDir);
|
|
205
|
+
dropSourceMaps(outDir);
|
|
150
206
|
dropRoutesJson(outDir);
|
|
151
207
|
};
|
|
152
208
|
const STRATEGY_BY_STACK = new Map([
|
|
@@ -158,7 +214,9 @@ const STRATEGY_BY_STACK = new Map([
|
|
|
158
214
|
function resolveStrategy(stack) {
|
|
159
215
|
const strategy = STRATEGY_BY_STACK.get(stack);
|
|
160
216
|
if (!strategy) {
|
|
161
|
-
throw new error_1.AppError('
|
|
217
|
+
throw new error_1.AppError('PACK_STACK_UNSUPPORTED', `stack "${stack}" 不支持 standalone 导出`, {
|
|
218
|
+
next_actions: [`支持的 stack:${[...exports.PACK_SUPPORTED_STACKS].join(', ')}`],
|
|
219
|
+
});
|
|
162
220
|
}
|
|
163
221
|
return strategy;
|
|
164
222
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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.redactUrl = redactUrl;
|
|
7
|
+
exports.assertValidUploadUrl = assertValidUploadUrl;
|
|
8
|
+
exports.uploadZip = uploadZip;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const error_1 = require("../../../utils/error");
|
|
11
|
+
const logger_1 = require("../../../utils/logger");
|
|
12
|
+
/**
|
|
13
|
+
* 从响应头取排查用的 log id。字节侧网关/对象存储统一给 `x-tt-logid`;
|
|
14
|
+
* 拿不到时退到通用的 `x-request-id`,两个都没有就不报(不编造)。
|
|
15
|
+
*/
|
|
16
|
+
function pickLogId(resp) {
|
|
17
|
+
return resp.headers.get('x-tt-logid') ?? resp.headers.get('x-request-id') ?? undefined;
|
|
18
|
+
}
|
|
19
|
+
/** 有 log id 时拼成错误信息后缀,没有就什么都不加。 */
|
|
20
|
+
function logIdSuffix(logId) {
|
|
21
|
+
return logId ? `,log_id=${logId}` : '';
|
|
22
|
+
}
|
|
23
|
+
/** 上传超时。产物可达数 MB,给足余量;沙箱到对象存储通常是内网,不会真跑满。 */
|
|
24
|
+
const UPLOAD_TIMEOUT_MS = 120_000;
|
|
25
|
+
/**
|
|
26
|
+
* 抹掉 URL 的 query 再用于日志 / 错误信息。
|
|
27
|
+
*
|
|
28
|
+
* **预签名 URL 的签名等同于一份凭证** —— 谁拿到谁就能往那个位置写。原样打进日志、
|
|
29
|
+
* 错误上报或 JSON 输出,就等于把上传能力泄漏出去。所以对外可见的地方一律走这里。
|
|
30
|
+
*/
|
|
31
|
+
function redactUrl(raw) {
|
|
32
|
+
try {
|
|
33
|
+
const u = new URL(raw);
|
|
34
|
+
return u.search ? `${u.origin}${u.pathname}?<redacted>` : `${u.origin}${u.pathname}`;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// 连解析都失败的串,更不能原样回显
|
|
38
|
+
return '<unparseable url>';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 把 zip 以 **PUT 裸 body** 传到预签名 URL(TOS / S3 的标准上传形态)。
|
|
43
|
+
*
|
|
44
|
+
* 鉴权完全由 URL 自带的签名承担,不加任何额外 header —— 多带 header 反而可能破坏
|
|
45
|
+
* 某些实现的签名校验。
|
|
46
|
+
*
|
|
47
|
+
* 失败一律抛 `PACK_UPLOAD_FAILED`:调用方传了 `--upload-url` 就说明上传
|
|
48
|
+
* 是目的,传不上去等于没完成(设计决策:整个命令失败,退出码 1)。
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* 校验 `--upload-url` 合法性。**必须在动手构建之前调用** —— 否则 URL 写错要等一整轮
|
|
52
|
+
* 构建 + 图片下载 + 打包之后才被告知参数非法,白等几十秒。
|
|
53
|
+
*/
|
|
54
|
+
function assertValidUploadUrl(url) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = new URL(url);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new error_1.AppError('ARGS_INVALID', `--upload-url 不是合法 URL:${redactUrl(url)}`);
|
|
61
|
+
}
|
|
62
|
+
// 只允许 http(s):file: 会把产物写到本地路径,其它协议 fetch 也不支持
|
|
63
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
64
|
+
throw new error_1.AppError('ARGS_INVALID', `--upload-url 只支持 http/https,收到 ${parsed.protocol}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function uploadZip(zipPath, url) {
|
|
68
|
+
assertValidUploadUrl(url);
|
|
69
|
+
if (!node_fs_1.default.existsSync(zipPath)) {
|
|
70
|
+
throw new error_1.AppError('ARGS_INVALID', `待上传的 zip 不存在:${zipPath}`);
|
|
71
|
+
}
|
|
72
|
+
const body = node_fs_1.default.readFileSync(zipPath);
|
|
73
|
+
// 默认输出**不带 URL**(连脱敏版也不带):预签名 URL 属于凭证类信息,常规日志
|
|
74
|
+
// 里没有它的位置;排查靠响应头的 log_id 就够,那才是服务端能据以定位的东西。
|
|
75
|
+
// 完整地址只在显式 --verbose 时给出。
|
|
76
|
+
(0, logger_1.log)('pack', `Uploading ${String(body.length)} bytes...`);
|
|
77
|
+
(0, logger_1.debug)(`pack: PUT ${url}`);
|
|
78
|
+
let resp;
|
|
79
|
+
try {
|
|
80
|
+
resp = await fetch(url, {
|
|
81
|
+
method: 'PUT',
|
|
82
|
+
headers: {
|
|
83
|
+
'content-type': 'application/zip',
|
|
84
|
+
'content-length': String(body.length),
|
|
85
|
+
},
|
|
86
|
+
body,
|
|
87
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
throw new error_1.AppError('PACK_UPLOAD_FAILED', `上传失败:${err instanceof Error ? err.message : String(err)}`, {
|
|
92
|
+
next_actions: [
|
|
93
|
+
'本地产物已保留,可修正 --upload-url 后重跑,或手动上传',
|
|
94
|
+
'--verbose 可打印完整请求地址',
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const logId = pickLogId(resp);
|
|
99
|
+
if (!resp.ok) {
|
|
100
|
+
throw new error_1.AppError('PACK_UPLOAD_FAILED', `上传失败:HTTP ${String(resp.status)}${logIdSuffix(logId)}`, {
|
|
101
|
+
next_actions: [
|
|
102
|
+
'预签名 URL 可能已过期或签名不匹配,确认后重试',
|
|
103
|
+
'本地产物已保留,也可手动上传',
|
|
104
|
+
'--verbose 可打印完整请求地址',
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
(0, logger_1.log)('pack', `Uploaded (HTTP ${String(resp.status)}${logIdSuffix(logId)})`);
|
|
109
|
+
return { status: resp.status, ...(logId ? { logId } : {}) };
|
|
110
|
+
}
|
|
@@ -5,7 +5,7 @@ exports.EXCLUDES = void 0;
|
|
|
5
5
|
* 与模板 `scripts/build.sh` 的 rsync EXCLUDES 对齐:这些名字(任意层级 basename)不进产物/不计入路由。
|
|
6
6
|
*
|
|
7
7
|
* 放在 `utils/` 而非某个 domain 的 service 下,是因为它被 `deploy`(`source-scan.ts` 的
|
|
8
|
-
* `listSourceFiles`)与 `app`(`
|
|
8
|
+
* `listSourceFiles`)与 `app`(`pack` 的 `copyTree`)两个 domain 共用 ——
|
|
9
9
|
* service 层跨域互调被 `boundaries/dependencies` 禁止(见 docs/dependency-rules.md F4),
|
|
10
10
|
* 共享的纯数据/纯逻辑必须下沉到 `utils/`。
|
|
11
11
|
*/
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|