@vmz/vmz 0.3.12 → 0.3.14
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/bin/vmz.js +4 -0
- package/delivery-runtime-files.json +2 -0
- package/dist/cli/cli.js +46 -23
- package/dist/cli/format-cmd.d.ts +9 -17
- package/dist/cli/format-cmd.js +37 -49
- package/dist/cli/format-config.d.ts +6 -9
- package/dist/cli/format-config.js +5 -29
- package/dist/delivery/delivery-profile.js +1 -1
- package/dist/dev/dev-incremental.d.ts +0 -2
- package/dist/dev/dev-incremental.js +0 -1
- package/dist/dev/dev-session.d.ts +53 -1
- package/dist/dev/dev-session.js +368 -65
- package/dist/document/document-base-css.d.ts +15 -0
- package/dist/document/document-base-css.js +578 -0
- package/dist/document/document-build.d.ts +1 -0
- package/dist/document/document-build.js +94 -78
- package/dist/document/document-client.d.ts +13 -0
- package/dist/document/document-client.js +217 -0
- package/dist/document/document-cmd.js +3 -1
- package/dist/document/document-designs.d.ts +9 -3
- package/dist/document/document-designs.js +20 -34
- package/dist/document/document-enrich.d.ts +9 -0
- package/dist/document/document-enrich.js +55 -0
- package/dist/document/document-highlight.d.ts +26 -0
- package/dist/document/document-highlight.js +54 -0
- package/dist/document/document-integrate.d.ts +5 -0
- package/dist/document/document-integrate.js +1 -0
- package/dist/document/document-interactive.js +10 -2
- package/dist/document/document-layout-render.js +6 -2
- package/dist/document/document-schema.d.ts +5 -0
- package/dist/workspace/build-assemble.js +18 -6
- package/dist/workspace/content-addressed-assets.d.ts +6 -17
- package/dist/workspace/content-addressed-assets.js +10 -95
- package/dist/workspace/delivery-runtime-imports.js +9 -0
- package/dist/workspace/native-addon.d.ts +4 -0
- package/dist/workspace/pack-native.d.ts +151 -0
- package/dist/workspace/pack-native.js +280 -0
- package/dist/workspace/pack.d.ts +131 -32
- package/dist/workspace/pack.js +381 -65
- package/dist/workspace/plugin-host.d.ts +4 -4
- package/dist/workspace/plugin-host.js +6 -6
- package/dist/workspace/production-observability.d.ts +2 -0
- package/dist/workspace/production-observability.js +6 -2
- package/dist/workspace/project-build.js +26 -4
- package/dist/workspace/public-api.d.ts +7 -3
- package/dist/workspace/public-api.js +5 -3
- package/dist/workspace/static-emit.d.ts +30 -0
- package/dist/workspace/static-emit.js +211 -18
- package/dist/workspace/user-config.d.ts +112 -0
- package/dist/workspace/user-config.js +12 -0
- package/host-runtime-files.json +1 -0
- package/locales/en-US/cli.json +5 -4
- package/locales/en-US/diag.json +2 -0
- package/locales/zh-CN/diag.json +2 -0
- package/package.json +20 -20
- package/dist/workspace/pack-client-packages.d.ts +0 -34
- package/dist/workspace/pack-client-packages.js +0 -550
package/dist/dev/dev-session.js
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* 3. Soft reload failure → **auto-respawn** serve-host (fresh ESM graph), not "keep broken host".
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
-
import {
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
14
15
|
import path from 'node:path';
|
|
15
16
|
import { buildIntegratedDocuments, projectHasDocuments } from '../document/document-integrate.js';
|
|
16
17
|
import { createWorkspace, resolveNativePath } from '../workspace/public-api.js';
|
|
18
|
+
import { loadVmzConfig } from '../workspace/plugin-host.js';
|
|
17
19
|
import { materializeWechatPackaging } from '../workspace/wechat-packaging.js';
|
|
18
20
|
import { emitLocaleRuntimeModules, localeHasErrors } from '../locale/locale-check.js';
|
|
19
21
|
import { log } from '../workspace/log.js';
|
|
@@ -21,6 +23,8 @@ import { coalesceRootBurst, collectDevWatchRoots, classifyWatchRoot, isDependenc
|
|
|
21
23
|
import { createConfigCycle } from './config-cycle.js';
|
|
22
24
|
import { buildReloadPayload, filterGenerationIgnore, parseHmrPlan, registerWrittenOutputsIgnore, shouldSoftReload, staticRevisionFromArtifact, } from './dev-incremental.js';
|
|
23
25
|
import { diffFingerprints, fileFingerprintMap } from '../workspace/watch-diff.js';
|
|
26
|
+
import { packFromDeploymentIr, deploymentUnitOrigins } from '../workspace/pack.js';
|
|
27
|
+
import { loadDeploymentIr } from '../workspace/bundler-adapter.js';
|
|
24
28
|
/** Track serve-host so a failed/killed launcher cannot leave an orphan port. */
|
|
25
29
|
const DEV_HOST_PID = '.vmz-dev-host.pid';
|
|
26
30
|
function cycleFailure(report) {
|
|
@@ -28,6 +32,218 @@ function cycleFailure(report) {
|
|
|
28
32
|
const error = new Error(detail);
|
|
29
33
|
return report.error ? Object.assign(error, { diagnostic: report.error }) : error;
|
|
30
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Pack 输入快照:dev 只在「决定产物字节的全部输入」完全不变时才复用上次产物。
|
|
37
|
+
* 判据是文件内容哈希,不使用 mtime、文件数量或后缀推导;读不到或未知的输入一律按变更处理。
|
|
38
|
+
*/
|
|
39
|
+
const PACK_INPUT_IGNORED_DIRS = new Set(['_vmz', 'node_modules', '.cache', '.git']);
|
|
40
|
+
const PACK_INPUT_IGNORED_NAMES = /^\.vmz-dev-host\.pid$/;
|
|
41
|
+
/** Rust 侧的模块路径带 `//?/`(或 `\\?\`)verbatim 前缀,落盘读取前需要剥离。 */
|
|
42
|
+
function packInputFsPath(filePath) {
|
|
43
|
+
return filePath.replace(/^(?:\\\\\?\\|\/\/\?\/)/, '').replace(/\\/g, '/');
|
|
44
|
+
}
|
|
45
|
+
function readFileDigest(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
return createHash('sha256').update(readFileSync(filePath)).digest('hex');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function findLockfile(project) {
|
|
54
|
+
let dir = path.resolve(project);
|
|
55
|
+
while (true) {
|
|
56
|
+
const candidate = path.join(dir, 'pnpm-lock.yaml');
|
|
57
|
+
if (existsSync(candidate))
|
|
58
|
+
return candidate;
|
|
59
|
+
const parent = path.dirname(dir);
|
|
60
|
+
if (parent === dir)
|
|
61
|
+
return null;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function listPackInputDir(dir) {
|
|
66
|
+
const names = [];
|
|
67
|
+
const walk = (current, prefix) => {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = readdirSync(current, { withFileTypes: true });
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (PACK_INPUT_IGNORED_DIRS.has(entry.name) || PACK_INPUT_IGNORED_NAMES.test(entry.name))
|
|
78
|
+
continue;
|
|
79
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
if (!walk(path.join(current, entry.name), rel))
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
names.push(rel);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
return walk(dir, '') ? names : null;
|
|
91
|
+
}
|
|
92
|
+
/** manifest 形状不认识时返回 null(而非空集合),避免把未知输入当成「没变化」。 */
|
|
93
|
+
function packInputFiles(manifest) {
|
|
94
|
+
if (!Array.isArray(manifest.chunks) || manifest.chunks.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
const files = new Set();
|
|
97
|
+
for (const chunk of manifest.chunks) {
|
|
98
|
+
if (!Array.isArray(chunk.modules))
|
|
99
|
+
return null;
|
|
100
|
+
for (const module of chunk.modules) {
|
|
101
|
+
if (typeof module !== 'string' || !module)
|
|
102
|
+
return null;
|
|
103
|
+
files.add(packInputFsPath(module));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(manifest.styles))
|
|
107
|
+
return null;
|
|
108
|
+
for (const style of manifest.styles) {
|
|
109
|
+
if (!Array.isArray(style.sourcePaths))
|
|
110
|
+
return null;
|
|
111
|
+
for (const source of style.sourcePaths) {
|
|
112
|
+
if (typeof source !== 'string' || !source)
|
|
113
|
+
return null;
|
|
114
|
+
files.add(packInputFsPath(source));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [...files].sort();
|
|
118
|
+
}
|
|
119
|
+
/** pack 只经 `deploymentUnitOrigins` 读取 IR;对这份 origins 取规范摘要,避免把构建期 affected/seed 噪声算成输入变更。 */
|
|
120
|
+
function deploymentOriginsDigest(outDir) {
|
|
121
|
+
try {
|
|
122
|
+
const origins = deploymentUnitOrigins(outDir, loadDeploymentIr(outDir));
|
|
123
|
+
const canonical = Object.keys(origins)
|
|
124
|
+
.sort()
|
|
125
|
+
.map((key) => `${key}\u0000${origins[key]}`);
|
|
126
|
+
return createHash('sha256').update(canonical.join('\u0001')).digest('hex');
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** 记下决定 pack 产物的全部输入;任何一项取不到就放弃快照(等同于永远重打包)。 */
|
|
133
|
+
export function capturePackInputSnapshot(manifest, outDir, project) {
|
|
134
|
+
const inputs = packInputFiles(manifest);
|
|
135
|
+
if (!inputs)
|
|
136
|
+
return null;
|
|
137
|
+
const files = [];
|
|
138
|
+
for (const file of inputs) {
|
|
139
|
+
const digest = readFileDigest(file);
|
|
140
|
+
if (!digest)
|
|
141
|
+
return null;
|
|
142
|
+
files.push({ path: file, digest });
|
|
143
|
+
}
|
|
144
|
+
const listings = [];
|
|
145
|
+
for (const dir of [...new Set(files.map((file) => path.dirname(file.path)))].sort()) {
|
|
146
|
+
if (dir.split(/[\\/]/).includes('node_modules'))
|
|
147
|
+
continue;
|
|
148
|
+
const names = listPackInputDir(dir);
|
|
149
|
+
if (!names)
|
|
150
|
+
return null;
|
|
151
|
+
listings.push({ dir, names });
|
|
152
|
+
}
|
|
153
|
+
const deploymentOrigins = deploymentOriginsDigest(outDir);
|
|
154
|
+
if (!deploymentOrigins)
|
|
155
|
+
return null;
|
|
156
|
+
const lockfilePath = findLockfile(project);
|
|
157
|
+
if (!lockfilePath)
|
|
158
|
+
return null;
|
|
159
|
+
const lockfileDigest = readFileDigest(lockfilePath);
|
|
160
|
+
if (!lockfileDigest)
|
|
161
|
+
return null;
|
|
162
|
+
const manifestPath = path.join(outDir, '_vmz', 'pack-manifest.json');
|
|
163
|
+
const manifestDigest = readFileDigest(manifestPath);
|
|
164
|
+
if (!manifestDigest)
|
|
165
|
+
return null;
|
|
166
|
+
return {
|
|
167
|
+
files,
|
|
168
|
+
listings,
|
|
169
|
+
deploymentOrigins,
|
|
170
|
+
lockfile: { path: lockfilePath, digest: lockfileDigest },
|
|
171
|
+
manifest: { path: manifestPath, digest: manifestDigest },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** 返回 null 表示全部输入字节全等;否则返回第一个变化原因。 */
|
|
175
|
+
export function packInputsChangedReason(snapshot, outDir) {
|
|
176
|
+
const lockfileDigest = readFileDigest(snapshot.lockfile.path);
|
|
177
|
+
if (!lockfileDigest)
|
|
178
|
+
return `lockfile unreadable: ${snapshot.lockfile.path}`;
|
|
179
|
+
if (lockfileDigest !== snapshot.lockfile.digest)
|
|
180
|
+
return 'lockfile changed';
|
|
181
|
+
const manifestDigest = readFileDigest(snapshot.manifest.path);
|
|
182
|
+
if (!manifestDigest)
|
|
183
|
+
return `pack manifest unreadable: ${snapshot.manifest.path}`;
|
|
184
|
+
if (manifestDigest !== snapshot.manifest.digest)
|
|
185
|
+
return 'pack manifest changed';
|
|
186
|
+
const deploymentOrigins = deploymentOriginsDigest(outDir);
|
|
187
|
+
if (!deploymentOrigins)
|
|
188
|
+
return 'deployment IR unreadable';
|
|
189
|
+
if (deploymentOrigins !== snapshot.deploymentOrigins)
|
|
190
|
+
return 'deployment IR changed';
|
|
191
|
+
for (const file of snapshot.files) {
|
|
192
|
+
const digest = readFileDigest(file.path);
|
|
193
|
+
if (!digest)
|
|
194
|
+
return `input unreadable: ${file.path}`;
|
|
195
|
+
if (digest !== file.digest)
|
|
196
|
+
return `input changed: ${file.path}`;
|
|
197
|
+
}
|
|
198
|
+
for (const listing of snapshot.listings) {
|
|
199
|
+
const names = listPackInputDir(listing.dir);
|
|
200
|
+
if (!names)
|
|
201
|
+
return `input directory unreadable: ${listing.dir}`;
|
|
202
|
+
if (names.length !== listing.names.length)
|
|
203
|
+
return `input directory changed: ${listing.dir}`;
|
|
204
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
205
|
+
if (names[i] !== listing.names[i])
|
|
206
|
+
return `input directory changed: ${listing.dir}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
/** 复用上次 manifest 前确认 entry/chunk/island 查表指向的文件仍在盘上;缺任何一个就退回真实 pack。 */
|
|
212
|
+
export function missingPackOutput(manifest, outDir) {
|
|
213
|
+
const packDir = path.join(outDir, '_vmz', 'pack');
|
|
214
|
+
if (!existsSync(path.join(outDir, '_vmz', 'pack-manifest.json')))
|
|
215
|
+
return '_vmz/pack-manifest.json';
|
|
216
|
+
const names = new Set();
|
|
217
|
+
for (const entry of manifest.entryFiles ?? [])
|
|
218
|
+
if (typeof entry.file === 'string' && entry.file)
|
|
219
|
+
names.add(entry.file);
|
|
220
|
+
for (const dynamic of manifest.dynamicFiles ?? []) {
|
|
221
|
+
if (typeof dynamic.from === 'string' && dynamic.from)
|
|
222
|
+
names.add(dynamic.from);
|
|
223
|
+
if (typeof dynamic.to === 'string' && dynamic.to)
|
|
224
|
+
names.add(dynamic.to);
|
|
225
|
+
}
|
|
226
|
+
for (const chunk of manifest.chunks ?? []) {
|
|
227
|
+
if (typeof chunk.fileName === 'string' && chunk.fileName)
|
|
228
|
+
names.add(chunk.fileName);
|
|
229
|
+
if (typeof chunk.sourceMap === 'string' && chunk.sourceMap)
|
|
230
|
+
names.add(chunk.sourceMap);
|
|
231
|
+
}
|
|
232
|
+
for (const style of manifest.styles ?? [])
|
|
233
|
+
if (typeof style.fileName === 'string' && style.fileName)
|
|
234
|
+
names.add(style.fileName);
|
|
235
|
+
for (const asset of manifest.assets ?? [])
|
|
236
|
+
if (typeof asset.fileName === 'string' && asset.fileName)
|
|
237
|
+
names.add(asset.fileName);
|
|
238
|
+
for (const unit of manifest.unitChunks ?? [])
|
|
239
|
+
if (typeof unit.fileName === 'string' && unit.fileName)
|
|
240
|
+
names.add(unit.fileName);
|
|
241
|
+
for (const name of names) {
|
|
242
|
+
if (!existsSync(path.join(packDir, ...name.split('/'))))
|
|
243
|
+
return name;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
31
247
|
export function createDevSession(options) {
|
|
32
248
|
const project = options.project;
|
|
33
249
|
const outDir = options.outDir;
|
|
@@ -38,7 +254,8 @@ export function createDevSession(options) {
|
|
|
38
254
|
let delivery = options.delivery ?? null;
|
|
39
255
|
const createWs = options.createWorkspaceFn ?? createWorkspace;
|
|
40
256
|
const spawnHost = options.spawnHostFn ?? defaultSpawnHost;
|
|
41
|
-
const softReload = options.softReloadFn ??
|
|
257
|
+
const softReload = options.softReloadFn ?? ((body) => defaultReloadRequest(host, port, body));
|
|
258
|
+
const packBrowser = options.packFn ?? packFromDeploymentIr;
|
|
42
259
|
const makeConfigCycle = options.configCycleFn ?? createConfigCycle;
|
|
43
260
|
const configCandidates = configCandidatePaths(project);
|
|
44
261
|
let configCycle = null;
|
|
@@ -47,6 +264,9 @@ export function createDevSession(options) {
|
|
|
47
264
|
let stopped = false;
|
|
48
265
|
let lastOutputRevision = null;
|
|
49
266
|
let lastStaticRevision = null;
|
|
267
|
+
let lastBuildMs = 0;
|
|
268
|
+
let lastPackManifest = null;
|
|
269
|
+
let lastPackInputs = null;
|
|
50
270
|
const generationIgnore = new Set();
|
|
51
271
|
function planAndRebuild(changes) {
|
|
52
272
|
if (changes?.length)
|
|
@@ -60,7 +280,9 @@ export function createDevSession(options) {
|
|
|
60
280
|
catch {
|
|
61
281
|
hmrPlan = null;
|
|
62
282
|
}
|
|
283
|
+
const startedAt = Date.now();
|
|
63
284
|
const report = ws.build();
|
|
285
|
+
lastBuildMs = Date.now() - startedAt;
|
|
64
286
|
return { report, hmrPlan };
|
|
65
287
|
}
|
|
66
288
|
function rebuild(changes) {
|
|
@@ -74,7 +296,6 @@ export function createDevSession(options) {
|
|
|
74
296
|
}
|
|
75
297
|
async function maybeReloadAfterBuild(report, hmrPlan, opts = {}) {
|
|
76
298
|
const rev = report.outputRevision ?? report.output_revision ?? null;
|
|
77
|
-
const skipEntry = Boolean(rev && rev === lastOutputRevision);
|
|
78
299
|
if (!opts.force && !opts.staticOnly && !shouldSoftReload(report, lastOutputRevision)) {
|
|
79
300
|
log.info('rebuild ok — artifact unchanged, skip soft reload');
|
|
80
301
|
if (rev)
|
|
@@ -84,14 +305,12 @@ export function createDevSession(options) {
|
|
|
84
305
|
}
|
|
85
306
|
const payload = buildReloadPayload(report, hmrPlan, {
|
|
86
307
|
staticRevision: lastStaticRevision ?? undefined,
|
|
87
|
-
skipEntryRewrite: skipEntry && !opts.force,
|
|
88
308
|
});
|
|
89
309
|
if (opts.staticOnly) {
|
|
90
310
|
payload.full = false;
|
|
91
311
|
payload.islandHmr = false;
|
|
92
|
-
payload.skipEntryRewrite = true;
|
|
93
312
|
}
|
|
94
|
-
const kind = await reloadAfterBuild(payload);
|
|
313
|
+
const kind = await reloadAfterBuild(payload, { staticOnly: Boolean(opts.staticOnly) });
|
|
95
314
|
if (rev)
|
|
96
315
|
lastOutputRevision = rev;
|
|
97
316
|
registerWrittenOutputsIgnore(report.writtenOutputs ?? report.written_outputs, outDir, project, generationIgnore);
|
|
@@ -116,7 +335,7 @@ export function createDevSession(options) {
|
|
|
116
335
|
return false;
|
|
117
336
|
const mode = report.full ? 'full' : 'affected';
|
|
118
337
|
const chunks = (report.affectedChunks || []).join(', ') || '(none)';
|
|
119
|
-
log.info(`${label} ok (${mode}; chunks=[${chunks}]; ${(report.emitted ?? []).length} emitted)`);
|
|
338
|
+
log.info(`${label} ok (${mode}; chunks=[${chunks}]; ${(report.emitted ?? []).length} emitted; ${lastBuildMs}ms build)`);
|
|
120
339
|
return true;
|
|
121
340
|
}
|
|
122
341
|
/**
|
|
@@ -148,11 +367,11 @@ export function createDevSession(options) {
|
|
|
148
367
|
log.info(`wechat pack ok → ${path.join(project, packRoot)} (WeChat DevTools compiles WXSS here)`);
|
|
149
368
|
return true;
|
|
150
369
|
}
|
|
151
|
-
async function
|
|
370
|
+
async function waitHostUp(timeoutMs = 8000) {
|
|
152
371
|
const start = Date.now();
|
|
153
372
|
while (Date.now() - start < timeoutMs) {
|
|
154
373
|
try {
|
|
155
|
-
const res = await fetch(`http://${host}:${port}/__vmz/
|
|
374
|
+
const res = await fetch(`http://${host}:${port}/__vmz/health`);
|
|
156
375
|
if (res.ok)
|
|
157
376
|
return;
|
|
158
377
|
}
|
|
@@ -161,7 +380,7 @@ export function createDevSession(options) {
|
|
|
161
380
|
}
|
|
162
381
|
await sleep(50);
|
|
163
382
|
}
|
|
164
|
-
throw new Error(`serve-host not
|
|
383
|
+
throw new Error(`serve-host not up on ${host}:${port}`);
|
|
165
384
|
}
|
|
166
385
|
async function waitHostGone(timeoutMs = 3000) {
|
|
167
386
|
const start = Date.now();
|
|
@@ -175,35 +394,136 @@ export function createDevSession(options) {
|
|
|
175
394
|
}
|
|
176
395
|
}
|
|
177
396
|
}
|
|
178
|
-
async function respawnHost(reason) {
|
|
179
|
-
log.warn(`respawning serve-host (${reason})…`);
|
|
180
|
-
killChild(child);
|
|
181
|
-
child = null;
|
|
182
|
-
await waitHostGone();
|
|
183
|
-
child = spawnHost({ project, outDir, host, port });
|
|
184
|
-
await waitHostReady();
|
|
185
|
-
log.info('serve-host respawned — browser SSE will reconnect and reload');
|
|
186
|
-
}
|
|
187
397
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
398
|
+
* 两阶段重建:host `prepare` 写入口 → CLI 侧 `vmz-pack` → host `commit` 发布。
|
|
399
|
+
* dev 的浏览器交付必须走 pack,否则逐文件模块保留裸包说明符。
|
|
400
|
+
* pack 失败显式 SSE error 且不回退到未打包入口。`staticOnly`(仅 public
|
|
401
|
+
* 资产)不改变客户端图,因此不重打包。
|
|
190
402
|
*/
|
|
191
|
-
async function
|
|
403
|
+
async function reloadWithPack(payload, opts = {}) {
|
|
192
404
|
try {
|
|
193
|
-
await softReload(
|
|
194
|
-
return 'soft';
|
|
405
|
+
await softReload({ phase: 'prepare', payload });
|
|
195
406
|
}
|
|
196
407
|
catch (err) {
|
|
408
|
+
if (opts.initial) {
|
|
409
|
+
log.warn(`initial serve-host load failed (${err}) — host stays up`);
|
|
410
|
+
return 'error';
|
|
411
|
+
}
|
|
197
412
|
log.warn(`soft reload failed (${err}) — auto-respawning serve-host`);
|
|
198
413
|
await respawnHost('soft-reload-failed');
|
|
199
414
|
return 'respawn';
|
|
200
415
|
}
|
|
416
|
+
if (!opts.staticOnly) {
|
|
417
|
+
const startedAt = Date.now();
|
|
418
|
+
const changedReason = lastPackInputs ? packInputsChangedReason(lastPackInputs, outDir) : 'no pack input snapshot';
|
|
419
|
+
const missing = changedReason === null && lastPackManifest ? missingPackOutput(lastPackManifest, outDir) : null;
|
|
420
|
+
if (changedReason === null && !missing && lastPackManifest) {
|
|
421
|
+
log.info(`dev pack skip (inputs unchanged, ${Date.now() - startedAt}ms)`);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
if (changedReason)
|
|
425
|
+
log.info(`dev pack (inputs changed: ${changedReason})`);
|
|
426
|
+
if (missing)
|
|
427
|
+
log.info(`dev pack (manifest reuse rejected: missing ${missing})`);
|
|
428
|
+
try {
|
|
429
|
+
const { manifest } = packBrowser(outDir, { projectRoot: project });
|
|
430
|
+
lastPackManifest = manifest;
|
|
431
|
+
lastPackInputs = capturePackInputSnapshot(lastPackManifest, outDir, project);
|
|
432
|
+
log.info(`dev pack ok (${manifest.chunkCount} chunks, ${Date.now() - startedAt}ms)`);
|
|
433
|
+
if (!lastPackInputs)
|
|
434
|
+
log.info('dev pack inputs: snapshot unavailable (unknown input) — next rebuild repacks');
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
lastPackManifest = null;
|
|
438
|
+
lastPackInputs = null;
|
|
439
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
440
|
+
log.error(`dev pack failed: ${message}`);
|
|
441
|
+
try {
|
|
442
|
+
await softReload({
|
|
443
|
+
phase: 'abort',
|
|
444
|
+
error: { message, stack: err instanceof Error ? err.stack : undefined },
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
/* host may be gone; keep the reported pack error */
|
|
449
|
+
}
|
|
450
|
+
return 'error';
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
await softReload({ phase: 'commit' });
|
|
456
|
+
return 'soft';
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
if (opts.initial) {
|
|
460
|
+
log.warn(`initial serve-host commit failed (${err}) — host stays up`);
|
|
461
|
+
return 'error';
|
|
462
|
+
}
|
|
463
|
+
log.warn(`reload commit failed (${err}) — auto-respawning serve-host`);
|
|
464
|
+
await respawnHost('commit-failed');
|
|
465
|
+
return 'respawn';
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function respawnHost(reason) {
|
|
469
|
+
log.warn(`respawning serve-host (${reason})…`);
|
|
470
|
+
killChild(child);
|
|
471
|
+
child = null;
|
|
472
|
+
await waitHostGone();
|
|
473
|
+
child = spawnHost({ project, outDir, host, port });
|
|
474
|
+
await waitHostUp();
|
|
475
|
+
const kind = await reloadWithPack({}, { initial: true });
|
|
476
|
+
if (kind === 'soft')
|
|
477
|
+
log.info('serve-host respawned — browser SSE will reconnect and reload');
|
|
478
|
+
else
|
|
479
|
+
log.warn('serve-host respawned but initial reload failed — fix the error and save');
|
|
480
|
+
}
|
|
481
|
+
/** @param {object} payload */
|
|
482
|
+
async function reloadAfterBuild(payload, opts = {}) {
|
|
483
|
+
return reloadWithPack(payload, opts);
|
|
484
|
+
}
|
|
485
|
+
let docEnginesPromise = null;
|
|
486
|
+
/** `engines.code` / `engines.markdown` for the document layer (loaded once per session). */
|
|
487
|
+
function docEngines() {
|
|
488
|
+
if (!docEnginesPromise) {
|
|
489
|
+
docEnginesPromise = loadVmzConfig(project)
|
|
490
|
+
.then((cfg) => cfg.engines)
|
|
491
|
+
.catch(() => undefined);
|
|
492
|
+
}
|
|
493
|
+
return docEnginesPromise;
|
|
201
494
|
}
|
|
202
495
|
async function start() {
|
|
203
496
|
const src = path.join(project, 'src');
|
|
204
497
|
if (!existsSync(src)) {
|
|
205
498
|
throw new Error(`vmz dev: missing src/ under ${project}`);
|
|
206
499
|
}
|
|
500
|
+
// Snapshot watch roots and their fingerprints **before** the initial build.
|
|
501
|
+
// A save that lands while `vmz dev` boots must still be detected by the first
|
|
502
|
+
// poll. Taking the baseline after the initial reload absorbs that edit into the
|
|
503
|
+
// baseline, so the host serves the pre-edit bytes until the next save.
|
|
504
|
+
const docsRoot = path.join(project, 'documents');
|
|
505
|
+
const localesRoot = path.join(project, 'locales');
|
|
506
|
+
const designsRoot = path.join(project, 'designs');
|
|
507
|
+
const watched = collectDevWatchRoots({ project, outDir });
|
|
508
|
+
const publicRoot = watched.publicRoot;
|
|
509
|
+
/** Candidate config files plus their local reachable deps; refreshed after each cycle. */
|
|
510
|
+
const refreshConfigPaths = () => {
|
|
511
|
+
const deps = configCandidates
|
|
512
|
+
.filter((p) => existsSync(p))
|
|
513
|
+
.flatMap((p) => collectConfigDependencies(p, project));
|
|
514
|
+
return [...new Set([...configCandidates, ...deps])];
|
|
515
|
+
};
|
|
516
|
+
let configPaths = refreshConfigPaths();
|
|
517
|
+
/** @type {string[]} */
|
|
518
|
+
const watchRoots = [...watched.roots];
|
|
519
|
+
/** @type {string[]} */
|
|
520
|
+
let dependencyRoots = [...watched.dependencyRoots];
|
|
521
|
+
/** @type {Map<string, Map<string, string>>} */
|
|
522
|
+
const fingerprints = new Map();
|
|
523
|
+
for (const root of watchRoots) {
|
|
524
|
+
fingerprints.set(root, fileFingerprintMap(root));
|
|
525
|
+
}
|
|
526
|
+
let configFingerprints = configFingerprintMap(configPaths);
|
|
207
527
|
// Config evaluation + plugin contributions run in a detached build process.
|
|
208
528
|
// Functions/plugin objects never cross the boundary; only the data report does.
|
|
209
529
|
if (configCandidates.some((p) => existsSync(p))) {
|
|
@@ -226,7 +546,7 @@ export function createDevSession(options) {
|
|
|
226
546
|
// Reclaim orphan serve-host from a previous crashed launcher (same outDir).
|
|
227
547
|
reclaimStaleDevHost(outDir);
|
|
228
548
|
if (projectHasDocuments(project)) {
|
|
229
|
-
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
549
|
+
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir, engines: await docEngines() });
|
|
230
550
|
if (!docs.ok) {
|
|
231
551
|
throw new Error('vmz dev: integrated document build failed');
|
|
232
552
|
}
|
|
@@ -242,27 +562,16 @@ export function createDevSession(options) {
|
|
|
242
562
|
throw new Error(`vmz dev: missing ${hostJs}`);
|
|
243
563
|
}
|
|
244
564
|
child = spawnHost({ project, outDir, host, port });
|
|
245
|
-
await
|
|
565
|
+
await waitHostUp().catch((err) => {
|
|
246
566
|
log.warn(String(err));
|
|
247
567
|
});
|
|
568
|
+
const initialKind = await reloadAfterBuild({}, { staticOnly: false }).catch((err) => {
|
|
569
|
+
log.warn(`initial reload failed: ${err}`);
|
|
570
|
+
return 'error';
|
|
571
|
+
});
|
|
572
|
+
if (initialKind !== 'soft')
|
|
573
|
+
log.warn('initial serve-host reload failed — fix the error and save');
|
|
248
574
|
}
|
|
249
|
-
const docsRoot = path.join(project, 'documents');
|
|
250
|
-
const localesRoot = path.join(project, 'locales');
|
|
251
|
-
const designsRoot = path.join(project, 'designs');
|
|
252
|
-
const watched = collectDevWatchRoots({ project, outDir });
|
|
253
|
-
const publicRoot = watched.publicRoot;
|
|
254
|
-
/** Candidate config files plus their local reachable deps; refreshed after each cycle. */
|
|
255
|
-
const refreshConfigPaths = () => {
|
|
256
|
-
const deps = configCandidates
|
|
257
|
-
.filter((p) => existsSync(p))
|
|
258
|
-
.flatMap((p) => collectConfigDependencies(p, project));
|
|
259
|
-
return [...new Set([...configCandidates, ...deps])];
|
|
260
|
-
};
|
|
261
|
-
let configPaths = refreshConfigPaths();
|
|
262
|
-
/** @type {string[]} */
|
|
263
|
-
const watchRoots = [...watched.roots];
|
|
264
|
-
/** @type {string[]} */
|
|
265
|
-
let dependencyRoots = [...watched.dependencyRoots];
|
|
266
575
|
if (wechatPreview) {
|
|
267
576
|
log.info(`dev → WeChat DevTools (watching ${watchRoots.join(', ')}; keep dist/wechat open)`);
|
|
268
577
|
}
|
|
@@ -272,12 +581,6 @@ export function createDevSession(options) {
|
|
|
272
581
|
if (dependencyRoots.length) {
|
|
273
582
|
log.info(`dev dependency watch roots (${dependencyRoots.length}): ${dependencyRoots.join(', ')}`);
|
|
274
583
|
}
|
|
275
|
-
/** @type {Map<string, Map<string, string>>} */
|
|
276
|
-
const fingerprints = new Map();
|
|
277
|
-
for (const root of watchRoots) {
|
|
278
|
-
fingerprints.set(root, fileFingerprintMap(root));
|
|
279
|
-
}
|
|
280
|
-
let configFingerprints = configFingerprintMap(configPaths);
|
|
281
584
|
const signal = options.signal;
|
|
282
585
|
const onAbort = () => {
|
|
283
586
|
void stop();
|
|
@@ -349,8 +652,7 @@ export function createDevSession(options) {
|
|
|
349
652
|
break;
|
|
350
653
|
if (!wechatPreview && child && child.exitCode != null) {
|
|
351
654
|
log.warn(`serve-host exited (${child.exitCode}) — respawning…`);
|
|
352
|
-
|
|
353
|
-
await waitHostReady().catch(() => { });
|
|
655
|
+
await respawnHost('host-exited');
|
|
354
656
|
continue;
|
|
355
657
|
}
|
|
356
658
|
let batch;
|
|
@@ -448,7 +750,7 @@ export function createDevSession(options) {
|
|
|
448
750
|
commitFingerprints();
|
|
449
751
|
if (!wechatPreview) {
|
|
450
752
|
const kind = await maybeReloadAfterBuild({ reloadRequired: true }, null, { force: true, staticOnly: true });
|
|
451
|
-
if (kind
|
|
753
|
+
if (kind === 'soft' || kind === 'respawn')
|
|
452
754
|
log.info('soft reload ok (public assets)');
|
|
453
755
|
}
|
|
454
756
|
continue;
|
|
@@ -533,7 +835,7 @@ export function createDevSession(options) {
|
|
|
533
835
|
}
|
|
534
836
|
dependencyRoots = [...refreshed.dependencyRoots];
|
|
535
837
|
if (batch.docsDirty || projectHasDocuments(project)) {
|
|
536
|
-
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
838
|
+
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir, engines: await docEngines() });
|
|
537
839
|
if (!docs.ok)
|
|
538
840
|
log.warn('document mount rebuild failed — keeping previous docs');
|
|
539
841
|
}
|
|
@@ -544,7 +846,7 @@ export function createDevSession(options) {
|
|
|
544
846
|
}
|
|
545
847
|
const kind = await maybeReloadAfterBuild(report, hmrPlan, { force: true });
|
|
546
848
|
commitFingerprints();
|
|
547
|
-
if (kind
|
|
849
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
548
850
|
log.info(kind === 'respawn' ? 'reload ok (respawned; deps)' : 'soft reload ok (full page; deps)');
|
|
549
851
|
}
|
|
550
852
|
continue;
|
|
@@ -563,7 +865,7 @@ export function createDevSession(options) {
|
|
|
563
865
|
}
|
|
564
866
|
await emitPublicAndTrack();
|
|
565
867
|
if (batch.docsDirty || projectHasDocuments(project)) {
|
|
566
|
-
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
868
|
+
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir, engines: await docEngines() });
|
|
567
869
|
if (!docs.ok) {
|
|
568
870
|
log.warn('document mount rebuild failed — keeping previous docs');
|
|
569
871
|
}
|
|
@@ -581,7 +883,7 @@ export function createDevSession(options) {
|
|
|
581
883
|
force: needFullReload,
|
|
582
884
|
});
|
|
583
885
|
commitFingerprints();
|
|
584
|
-
if (kind
|
|
886
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
585
887
|
log.info(kind === 'respawn'
|
|
586
888
|
? 'reload ok (respawned serve-host)'
|
|
587
889
|
: needFullReload
|
|
@@ -605,7 +907,7 @@ export function createDevSession(options) {
|
|
|
605
907
|
}
|
|
606
908
|
await emitPublicAndTrack();
|
|
607
909
|
if (batch.docsDirty || projectHasDocuments(project)) {
|
|
608
|
-
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
910
|
+
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir, engines: await docEngines() });
|
|
609
911
|
if (!docs.ok)
|
|
610
912
|
log.warn('document mount rebuild failed — keeping previous docs');
|
|
611
913
|
}
|
|
@@ -616,7 +918,7 @@ export function createDevSession(options) {
|
|
|
616
918
|
}
|
|
617
919
|
const kind = await maybeReloadAfterBuild(report, hmrPlan, { force: true });
|
|
618
920
|
commitFingerprints();
|
|
619
|
-
if (kind
|
|
921
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
620
922
|
log.info(kind === 'respawn' ? 'reload ok (respawned; designs)' : 'soft reload ok (full page; designs)');
|
|
621
923
|
}
|
|
622
924
|
continue;
|
|
@@ -637,7 +939,7 @@ export function createDevSession(options) {
|
|
|
637
939
|
else {
|
|
638
940
|
const kind = await maybeReloadAfterBuild({ reloadRequired: true, full: true }, null, { force: true });
|
|
639
941
|
commitFingerprints();
|
|
640
|
-
if (kind
|
|
942
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
641
943
|
log.info(kind === 'respawn' ? 'reload ok (respawned; locales)' : 'soft reload ok (full page; locales)');
|
|
642
944
|
}
|
|
643
945
|
if (!batch.docsDirty)
|
|
@@ -646,7 +948,7 @@ export function createDevSession(options) {
|
|
|
646
948
|
}
|
|
647
949
|
if (batch.docsDirty) {
|
|
648
950
|
log.info('documents change detected — rebuilding document mount…');
|
|
649
|
-
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir });
|
|
951
|
+
const docs = await buildIntegratedDocuments({ projectRoot: project, outDir, engines: await docEngines() });
|
|
650
952
|
if (!docs.ok) {
|
|
651
953
|
log.warn('document mount rebuild failed — keeping previous docs');
|
|
652
954
|
continue;
|
|
@@ -659,7 +961,7 @@ export function createDevSession(options) {
|
|
|
659
961
|
else {
|
|
660
962
|
const kind = await maybeReloadAfterBuild({ reloadRequired: true, full: true }, null, { force: true });
|
|
661
963
|
commitFingerprints();
|
|
662
|
-
if (kind
|
|
964
|
+
if (kind === 'soft' || kind === 'respawn') {
|
|
663
965
|
log.info(kind === 'respawn' ? 'reload ok (respawned; docs)' : 'soft reload ok (full page; docs)');
|
|
664
966
|
}
|
|
665
967
|
}
|
|
@@ -729,23 +1031,24 @@ function defaultSpawnHost(opts) {
|
|
|
729
1031
|
return child;
|
|
730
1032
|
}
|
|
731
1033
|
/**
|
|
1034
|
+
* Host reload request(两阶段协议:`prepare` / `commit` / `abort`)。
|
|
732
1035
|
* @param {string} host
|
|
733
1036
|
* @param {number} port
|
|
734
|
-
* @param {
|
|
1037
|
+
* @param {Record<string, unknown>} body
|
|
735
1038
|
*/
|
|
736
|
-
async function
|
|
1039
|
+
async function defaultReloadRequest(host, port, body) {
|
|
737
1040
|
const url = `http://${host}:${port}/__vmz/reload`;
|
|
738
|
-
const body = JSON.stringify(payload);
|
|
739
1041
|
const res = await fetch(url, {
|
|
740
1042
|
method: 'POST',
|
|
741
1043
|
headers: { 'content-type': 'application/json' },
|
|
742
|
-
body,
|
|
1044
|
+
body: JSON.stringify(body),
|
|
743
1045
|
});
|
|
744
1046
|
if (!res.ok)
|
|
745
1047
|
throw new Error(`HTTP ${res.status}`);
|
|
746
1048
|
const json = await res.json().catch(() => ({}));
|
|
747
1049
|
if (!json?.ok)
|
|
748
1050
|
throw new Error(JSON.stringify(json));
|
|
1051
|
+
return json;
|
|
749
1052
|
}
|
|
750
1053
|
function killChild(child) {
|
|
751
1054
|
if (!child || child.killed)
|