@lark-apaas/miaoda-cli 0.1.33-alpha.e318de2 → 0.1.33-beta.b418cfe
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/deploy/modern.js +6 -5
- package/dist/cli/handlers/app/migrate.js +58 -133
- 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/utils/index.js +1 -5
- package/package.json +1 -1
- package/dist/utils/preview-control.js +0 -139
|
@@ -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>}}
|
|
@@ -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',
|
|
@@ -36,8 +36,10 @@ function isHtml(rel) {
|
|
|
36
36
|
/**
|
|
37
37
|
* 把 changeset 组成 TosFileAction[]:
|
|
38
38
|
* - create/update 读本地文件、自动编码;delete 仅 FilePath。
|
|
39
|
-
* - .html 被 create / delete
|
|
40
|
-
*
|
|
39
|
+
* - 任一 .html 被 create / update / delete 时,追加重算 routes.json 的 UPDATE action。
|
|
40
|
+
* update 也算:routes.json 的 name 取自 HTML 的 `<title>`,改方案名就是一次纯 update,
|
|
41
|
+
* 只按 create/delete 判定会让改名永远进不了 routes.json。
|
|
42
|
+
* - 纯非 .html 改动(js / css / 图片)不动 routes.json。
|
|
41
43
|
*/
|
|
42
44
|
function buildTosActions(projectDir, cs) {
|
|
43
45
|
const actions = [];
|
|
@@ -48,8 +50,8 @@ function buildTosActions(projectDir, cs) {
|
|
|
48
50
|
for (const rel of cs.deletes) {
|
|
49
51
|
actions.push({ actionType: index_1.TosActionType.DELETE, filePath: normalizeRel(rel) });
|
|
50
52
|
}
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
+
const routesMayChange = [...cs.creates, ...cs.updates, ...cs.deletes].some(isHtml);
|
|
54
|
+
if (routesMayChange) {
|
|
53
55
|
actions.push({
|
|
54
56
|
actionType: index_1.TosActionType.UPDATE,
|
|
55
57
|
filePath: 'routes.json',
|
|
@@ -0,0 +1,75 @@
|
|
|
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.extractTitle = extractTitle;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
/**
|
|
9
|
+
* 读 `<title>` 时的扫描窗口上限(字节)。title 规范上在 `<head>` 里、正常紧贴文档开头,
|
|
10
|
+
* 64KB 足以覆盖 head 里塞了内联 style / script 的情况,同时给大 HTML 兜住 IO 上限
|
|
11
|
+
* (generateRoutes 会遍历工程内所有 .html,不能整文件读)。
|
|
12
|
+
*/
|
|
13
|
+
const HEAD_SCAN_BYTES = 64 * 1024;
|
|
14
|
+
const TITLE_RE = /<title[^>]*>([^<]*)<\/title>/i;
|
|
15
|
+
const HEAD_CLOSE_RE = /<\/head\s*>/i;
|
|
16
|
+
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
17
|
+
const ENTITY_RE = /&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi;
|
|
18
|
+
/** HTML 里可能出现在 title 文本中的命名实体。未列出的原样保留,不做全量实体表。 */
|
|
19
|
+
const NAMED_ENTITIES = {
|
|
20
|
+
amp: '&',
|
|
21
|
+
lt: '<',
|
|
22
|
+
gt: '>',
|
|
23
|
+
quot: '"',
|
|
24
|
+
apos: "'",
|
|
25
|
+
nbsp: ' ',
|
|
26
|
+
};
|
|
27
|
+
/** 把 title 文本里的实体还原成明文——name 是给前端直接当文本渲染的,不该带 `&`。 */
|
|
28
|
+
function decodeEntities(s) {
|
|
29
|
+
return s.replace(ENTITY_RE, (matched, body) => {
|
|
30
|
+
if (!body.startsWith('#')) {
|
|
31
|
+
return NAMED_ENTITIES[body.toLowerCase()] ?? matched;
|
|
32
|
+
}
|
|
33
|
+
const code = /^#x/i.test(body) ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
|
|
34
|
+
if (!Number.isInteger(code) || code <= 0 || code > 0x10ffff)
|
|
35
|
+
return matched;
|
|
36
|
+
return String.fromCodePoint(code);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** 只读文件头部 HEAD_SCAN_BYTES 字节。尾部可能截断多字节字符,对 title 提取无影响。 */
|
|
40
|
+
function readHead(absPath) {
|
|
41
|
+
const fd = node_fs_1.default.openSync(absPath, 'r');
|
|
42
|
+
try {
|
|
43
|
+
const buf = Buffer.alloc(HEAD_SCAN_BYTES);
|
|
44
|
+
const bytes = node_fs_1.default.readSync(fd, buf, 0, HEAD_SCAN_BYTES, 0);
|
|
45
|
+
return buf.subarray(0, bytes).toString('utf-8');
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
node_fs_1.default.closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 取 HTML 的 `<title>` 文本作为该页面的方案名。
|
|
53
|
+
*
|
|
54
|
+
* - 扫描范围截到第一个 `</head>`:body 里的 `<title>` 不算——内联 SVG 图标的 a11y
|
|
55
|
+
* `<title>` 很常见,抓到就会把图标描述当成方案名。找不到 `</head>` 时退回整个窗口。
|
|
56
|
+
* - 先剔 HTML 注释,避免抓到被注释掉的 title。
|
|
57
|
+
* - 实体还原 + 空白折叠 + trim;空 title / 无 title / 读失败一律返回 undefined,
|
|
58
|
+
* 由消费侧 fallback 到 path(旧应用没有 title 时保持显示页面地址)。
|
|
59
|
+
*/
|
|
60
|
+
function extractTitle(absPath) {
|
|
61
|
+
let head;
|
|
62
|
+
try {
|
|
63
|
+
head = readHead(absPath);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const headClose = HEAD_CLOSE_RE.exec(head)?.index ?? -1;
|
|
69
|
+
const scope = (headClose >= 0 ? head.slice(0, headClose) : head).replace(COMMENT_RE, '');
|
|
70
|
+
const raw = TITLE_RE.exec(scope)?.[1];
|
|
71
|
+
if (raw === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const text = decodeEntities(raw).replace(/\s+/g, ' ').trim();
|
|
74
|
+
return text === '' ? undefined : text;
|
|
75
|
+
}
|
|
@@ -11,7 +11,7 @@ const actions_1 = require("./actions");
|
|
|
11
11
|
var actions_2 = require("./actions");
|
|
12
12
|
Object.defineProperty(exports, "buildTosActions", { enumerable: true, get: function () { return actions_2.buildTosActions; } });
|
|
13
13
|
/**
|
|
14
|
-
* design-html 增量发布:读本地文件组 TosFileAction
|
|
14
|
+
* design-html 增量发布:读本地文件组 TosFileAction(改动含 .html 时带重算的
|
|
15
15
|
* routes.json),调后端 applyTosDiff 应用到 latest。scope 限 design_local_deploy。
|
|
16
16
|
*/
|
|
17
17
|
async function patchDesignDeploy(opts) {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.generateRoutes = generateRoutes;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const html_title_1 = require("./html-title");
|
|
4
9
|
const source_scan_1 = require("./source-scan");
|
|
5
10
|
/**
|
|
6
11
|
* .html → route path 推导(相对路径,不带 base 前缀)。rel 为 posix 相对路径。
|
|
@@ -16,8 +21,9 @@ function toRoute(rel) {
|
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
23
|
* 扫描 projectDir 下所有 .html(套 source-scan 的 EXCLUDES),生成 routes.json 文本(含尾换行)。
|
|
19
|
-
* 每项为 { path, file }:path 为相对路径(不带 CLIENT_BASE_PATH 前缀,由消费侧拼接),
|
|
20
|
-
* file 为该路由对应的原始 .html
|
|
24
|
+
* 每项为 { path, file, name? }:path 为相对路径(不带 CLIENT_BASE_PATH 前缀,由消费侧拼接),
|
|
25
|
+
* file 为该路由对应的原始 .html 源文件,name 为该文件 `<title>` 解析出的方案名(可缺省)。
|
|
26
|
+
* 按 path 排序、同 path 取字典序靠前的 file 去重。
|
|
21
27
|
* 零 html 时输出空数组——不伪造解析不到文件的根路由。
|
|
22
28
|
*/
|
|
23
29
|
function generateRoutes(projectDir) {
|
|
@@ -27,7 +33,11 @@ function generateRoutes(projectDir) {
|
|
|
27
33
|
if (!byPath.has(entry.path))
|
|
28
34
|
byPath.set(entry.path, entry);
|
|
29
35
|
}
|
|
30
|
-
|
|
36
|
+
// title 只读去重后胜出的 file:同 path 多源(foo.html / foo/index.html)时不做无用 IO。
|
|
37
|
+
const routes = [...byPath.values()].map((entry) => {
|
|
38
|
+
const name = (0, html_title_1.extractTitle)(node_path_1.default.join(projectDir, entry.file));
|
|
39
|
+
return name === undefined ? entry : { ...entry, name };
|
|
40
|
+
});
|
|
31
41
|
return JSON.stringify(routes, null, 2) + '\n';
|
|
32
42
|
}
|
|
33
43
|
function byPathThenFile(a, b) {
|
package/dist/utils/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pollUntilDone = exports.msToSec = exports.msToNs = exports.parseToSec = exports.parseToNs = exports.parseToMs = exports.parseTimeToMs = exports.
|
|
3
|
+
exports.pollUntilDone = exports.msToSec = exports.msToNs = exports.parseToSec = exports.parseToNs = exports.parseToMs = exports.parseTimeToMs = exports.isSandboxEnv = exports.getInnerApi = exports.postInnerApi = exports.setRuntimeHttpClient = exports.setHttpClient = exports.resetHttpClient = exports.getRuntimeHttpClient = exports.getHttpClient = exports.log = exports.debug = exports.fmt = exports.isJsonMode = exports.emitPaged = exports.emitOk = exports.emitError = exports.emit = exports.getLogId = exports.generateLogId = exports.initConfigFromOpts = exports.resetConfig = exports.setConfig = exports.getConfig = exports.HttpError = exports.AppError = void 0;
|
|
4
4
|
var error_1 = require("./error");
|
|
5
5
|
Object.defineProperty(exports, "AppError", { enumerable: true, get: function () { return error_1.AppError; } });
|
|
6
6
|
Object.defineProperty(exports, "HttpError", { enumerable: true, get: function () { return error_1.HttpError; } });
|
|
@@ -32,10 +32,6 @@ Object.defineProperty(exports, "postInnerApi", { enumerable: true, get: function
|
|
|
32
32
|
Object.defineProperty(exports, "getInnerApi", { enumerable: true, get: function () { return http_1.getInnerApi; } });
|
|
33
33
|
var env_1 = require("./env");
|
|
34
34
|
Object.defineProperty(exports, "isSandboxEnv", { enumerable: true, get: function () { return env_1.isSandboxEnv; } });
|
|
35
|
-
var preview_control_1 = require("./preview-control");
|
|
36
|
-
Object.defineProperty(exports, "stopPreview", { enumerable: true, get: function () { return preview_control_1.stopPreview; } });
|
|
37
|
-
Object.defineProperty(exports, "startPreview", { enumerable: true, get: function () { return preview_control_1.startPreview; } });
|
|
38
|
-
Object.defineProperty(exports, "resolveCodeServerBaseUrl", { enumerable: true, get: function () { return preview_control_1.resolveCodeServerBaseUrl; } });
|
|
39
35
|
var time_1 = require("./time");
|
|
40
36
|
Object.defineProperty(exports, "parseTimeToMs", { enumerable: true, get: function () { return time_1.parseTimeToMs; } });
|
|
41
37
|
Object.defineProperty(exports, "parseToMs", { enumerable: true, get: function () { return time_1.parseToMs; } });
|
package/package.json
CHANGED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveCodeServerBaseUrl = resolveCodeServerBaseUrl;
|
|
4
|
-
exports.stopPreview = stopPreview;
|
|
5
|
-
exports.startPreview = startPreview;
|
|
6
|
-
const logger_1 = require("../utils/logger");
|
|
7
|
-
/**
|
|
8
|
-
* 沙箱内 dev server(预览)的显式启停控制。
|
|
9
|
-
*
|
|
10
|
-
* 沙箱里 dev server 的保活由 code-server 负责:直接 `pkill dev.js` 只会被保活立刻重新拉起,
|
|
11
|
-
* 拿不到"停住"这个状态。code-server 为此暴露了两个本地接口:
|
|
12
|
-
*
|
|
13
|
-
* POST <base>/api/v1/preview/stop 停止预览(连带停掉保活)
|
|
14
|
-
* POST <base>/api/v1/preview/start 启动预览(恢复保活)
|
|
15
|
-
*
|
|
16
|
-
* base 默认 `http://localhost:8080/code-server` —— migrate / sync 这类脚本本身就在沙箱内执行,
|
|
17
|
-
* 直连 localhost 即可,无需鉴权;`MIAODA_CODE_SERVER_BASE_URL` 可覆盖(本地联调 / 端口变更)。
|
|
18
|
-
*
|
|
19
|
-
* 典型用途:migrate 要重装依赖(`rm -rf node_modules` + `npm install`)。dev server 活着时
|
|
20
|
-
* vite 会持续往 `node_modules/.vite/deps` 写文件,`fs.rmSync` 自底向上删到顶层 `rmdir` 时目录
|
|
21
|
-
* 又变非空,直接 `ENOTEMPTY` 失败。先 stop 再删才是干净的做法。
|
|
22
|
-
*
|
|
23
|
-
* 服务端语义(ide-vscode `src/vs/platform/force/node/preview/previewService.ts`):
|
|
24
|
-
* - stop 先置暂停位(`_suspended`),再等在途创建落地、shutdown 并等进程真正退出。
|
|
25
|
-
* **返回成功即代表 dev 进程已死,调用方不需要自己 pkill。**
|
|
26
|
-
* - 等退出有 10s 超时,超时按失败处理(HTTP 500)。所以本地超时必须 > 10s,否则会抢在
|
|
27
|
-
* 服务端给出结论前 abort(见 STOP_TIMEOUT_MS)。
|
|
28
|
-
* - ⚠️ 暂停位在 `_doStop()` 第一行就置上,**在抛错之前**。所以 stop 失败 ≠ 没进暂停态:
|
|
29
|
-
* 除了 404(路由不存在,必然无副作用)以外的任何失败,都必须当成"可能已暂停"去恢复,
|
|
30
|
-
* 否则保活被永久关掉。见 {@link PreviewControlResult.mayBeSuspended}。
|
|
31
|
-
* - 暂停位只存内存,不持久化:code-server 重启会丢状态并重新拉起预览(兜底自愈路径)。
|
|
32
|
-
* - start 在已运行时是 no-op;设计态沙箱(miaoda_design)本就不启动预览,start 也是 no-op。
|
|
33
|
-
*
|
|
34
|
-
* ⚠️ 调用契约:**只要 stop 可能已经生效就必须保证 start 被调到**(调用方放 `finally`,并按
|
|
35
|
-
* `mayBeSuspended` 判断)。漏掉 start 会让用户沙箱的预览再也起不来 —— 比迁移失败严重得多。
|
|
36
|
-
*
|
|
37
|
-
* 全部 API 都是软失败语义:不抛异常,失败信息放在返回值的 `error` 上,由调用方决定降级路径
|
|
38
|
-
* (老版本 code-server 没有这两个接口,会返回 404)。
|
|
39
|
-
*/
|
|
40
|
-
const DEFAULT_CODE_SERVER_BASE_URL = 'http://localhost:8080/code-server';
|
|
41
|
-
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
42
|
-
/**
|
|
43
|
-
* stop 的本地超时。必须大于服务端等进程退出的 10s(STOP_TIMEOUT)—— 否则我们会先 abort,
|
|
44
|
-
* 拿到一个"网络超时",而服务端那边照样置着暂停位并继续走完流程。
|
|
45
|
-
*/
|
|
46
|
-
const STOP_TIMEOUT_MS = 20_000;
|
|
47
|
-
/** start 是恢复动作,值得多试几次;stop 失败由调用方分类处理,不在这里重试 */
|
|
48
|
-
const START_ATTEMPTS = 3;
|
|
49
|
-
const START_RETRY_DELAY_MS = 500;
|
|
50
|
-
/** code-server 本地基址,去掉尾部斜杠。`MIAODA_CODE_SERVER_BASE_URL` > 默认 localhost:8080。 */
|
|
51
|
-
function resolveCodeServerBaseUrl() {
|
|
52
|
-
const fromEnv = process.env.MIAODA_CODE_SERVER_BASE_URL;
|
|
53
|
-
const base = fromEnv !== undefined && fromEnv !== '' ? fromEnv : DEFAULT_CODE_SERVER_BASE_URL;
|
|
54
|
-
return base.replace(/\/+$/, '');
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* 停止预览(dev 进程 + 保活)。返回 `ok` 时进程已确认退出,无需再 pkill。
|
|
58
|
-
*
|
|
59
|
-
* 软失败:不抛异常。失败时务必看 `mayBeSuspended` —— 为 true 就必须调 {@link startPreview} 恢复。
|
|
60
|
-
*/
|
|
61
|
-
async function stopPreview(opts = {}) {
|
|
62
|
-
const res = await postPreview('stop', 1, {
|
|
63
|
-
...opts,
|
|
64
|
-
timeoutMs: opts.timeoutMs ?? STOP_TIMEOUT_MS,
|
|
65
|
-
});
|
|
66
|
-
// 404 = 路由不存在(老 code-server),确定没有副作用;其余失败保守认为暂停位可能已置上
|
|
67
|
-
return { ...res, mayBeSuspended: res.ok || res.status !== 404 };
|
|
68
|
-
}
|
|
69
|
-
/** 启动预览(解除暂停 + 拉起进程)。软失败:不抛异常;内部带重试,因为这是恢复动作。 */
|
|
70
|
-
async function startPreview(opts = {}) {
|
|
71
|
-
return postPreview('start', START_ATTEMPTS, opts);
|
|
72
|
-
}
|
|
73
|
-
async function postPreview(action, attempts, opts) {
|
|
74
|
-
const url = `${resolveCodeServerBaseUrl()}/api/v1/preview/${action}`;
|
|
75
|
-
const logPrefix = opts.logPrefix ?? 'preview';
|
|
76
|
-
let last = {
|
|
77
|
-
ok: false,
|
|
78
|
-
url,
|
|
79
|
-
error: 'no attempt made',
|
|
80
|
-
mayBeSuspended: false,
|
|
81
|
-
};
|
|
82
|
-
for (let i = 1; i <= attempts; i++) {
|
|
83
|
-
last = await postOnce(url, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
84
|
-
if (last.ok)
|
|
85
|
-
return last;
|
|
86
|
-
if (i < attempts) {
|
|
87
|
-
(0, logger_1.log)(logPrefix, `⚠ preview ${action} 失败(第 ${String(i)}/${String(attempts)} 次), 重试: ${last.error ?? ''}`);
|
|
88
|
-
await sleep(START_RETRY_DELAY_MS);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return last;
|
|
92
|
-
}
|
|
93
|
-
async function postOnce(url, timeoutMs) {
|
|
94
|
-
try {
|
|
95
|
-
// 带空 JSON body —— 这两个接口不需要参数,但显式给 content-type 可以避免服务端框架对
|
|
96
|
-
// 无 body POST 的处理差异。
|
|
97
|
-
const res = await fetch(url, {
|
|
98
|
-
method: 'POST',
|
|
99
|
-
headers: { 'content-type': 'application/json' },
|
|
100
|
-
body: '{}',
|
|
101
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
102
|
-
});
|
|
103
|
-
if (!res.ok) {
|
|
104
|
-
const detail = await readBodySafe(res);
|
|
105
|
-
return {
|
|
106
|
-
ok: false,
|
|
107
|
-
url,
|
|
108
|
-
status: res.status,
|
|
109
|
-
error: `POST ${url} → ${String(res.status)} ${res.statusText}${detail}`,
|
|
110
|
-
mayBeSuspended: false,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
return { ok: true, url, status: res.status, mayBeSuspended: false };
|
|
114
|
-
}
|
|
115
|
-
catch (err) {
|
|
116
|
-
return {
|
|
117
|
-
ok: false,
|
|
118
|
-
url,
|
|
119
|
-
error: `POST ${url} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
120
|
-
mayBeSuspended: false,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
async function readBodySafe(res) {
|
|
125
|
-
try {
|
|
126
|
-
const text = await res.text();
|
|
127
|
-
if (!text)
|
|
128
|
-
return '';
|
|
129
|
-
return ` ${text.length > 300 ? `${text.slice(0, 300)}...` : text}`;
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
return '';
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
function sleep(ms) {
|
|
136
|
-
return new Promise((resolve) => {
|
|
137
|
-
setTimeout(resolve, ms);
|
|
138
|
-
});
|
|
139
|
-
}
|