@vmz/core 0.3.13 → 0.3.15
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/browser/client-nav.types.d.ts +2 -5
- package/dist/browser/direct-api.types.d.ts +11 -0
- package/dist/browser/direct-host-box.d.ts +11 -0
- package/dist/browser/direct-host-box.js +13 -0
- package/dist/browser/dom/attributes.js +142 -2
- package/dist/browser/dom/direct-api.d.ts +51 -3
- package/dist/browser/dom/direct-api.js +489 -88
- package/dist/browser/dom-core.types.d.ts +19 -2
- package/dist/browser/navigation/client-navigation.js +18 -30
- package/dist/browser/reactivity/write-barrier.js +5 -1
- package/dist/browser/resume/adopt.js +284 -14
- package/dist/browser/resume/hydrate.js +34 -0
- package/dist/host/deployment/deployment-registry.js +5 -2
- package/dist/host/render/render-host.js +2 -0
- package/dist/host/serve/bootstrap.js +8 -4
- package/dist/host/serve/hmr-dev.d.ts +16 -17
- package/dist/host/serve/hmr-dev.js +96 -35
- package/dist/host/serve/http.js +25 -8
- package/dist/host/serve/pack-entry.d.ts +21 -0
- package/dist/host/serve/pack-entry.js +78 -7
- package/dist/host/serve/page-render.js +10 -8
- package/dist/shared/dom-attr-normalize.d.ts +6 -0
- package/dist/shared/dom-attr-normalize.js +1 -1
- package/dist/shared/host.types.d.ts +9 -0
- package/dist/shared/html-digest.d.ts +15 -0
- package/dist/shared/html-digest.js +25 -0
- package/dist/shared/row-key.d.ts +13 -0
- package/dist/shared/row-key.js +19 -0
- package/dist/ssr/serialize.js +357 -17
- package/package.json +7 -7
|
@@ -12,6 +12,7 @@ import { setRoutes } from '../../faces/vmz-runtime.js';
|
|
|
12
12
|
import { LOCALE_LINK_PLAN_REL } from './constants.js';
|
|
13
13
|
import { loadDeploymentStyle } from './locale-theme.js';
|
|
14
14
|
import { bustUrl, isEventOnlyShell, isEventStrategy, listPageClientFiles, loadPageResumeEntries, pageNeedsReload } from './page-catalog.js';
|
|
15
|
+
import { packIslandHrefs } from './pack-entry.js';
|
|
15
16
|
import { serveState } from './state.js';
|
|
16
17
|
export function installDevImportTokenHooks() {
|
|
17
18
|
if (!serveState.isDev)
|
|
@@ -133,34 +134,52 @@ export async function* emitDevErrorHtml(err) {
|
|
|
133
134
|
function escapeHtml(s) {
|
|
134
135
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
135
136
|
}
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
/**
|
|
138
|
+
* 客户端入口模块由 pack 消费,必须保持内容稳定:每次重建都把 `?t=` 注入
|
|
139
|
+
* 动态 import 会让打包产物每轮都变,也会让浏览器拿到两个不同 URL 的同一模块。
|
|
140
|
+
* 缓存破坏由打包文件名(内容哈希)与 SSE 侧的 reload 负责。
|
|
141
|
+
*/
|
|
142
|
+
function emitEntryClient(eager, lazy) {
|
|
138
143
|
const native = loadNativeAddon();
|
|
139
144
|
if (typeof native.generateServeEntryClient !== 'function') {
|
|
140
145
|
throw new Error('vmz native addon missing generateServeEntryClient — rebuild with `pnpm napi:build`');
|
|
141
146
|
}
|
|
142
|
-
return native.generateServeEntryClient(eager, lazy,
|
|
147
|
+
return native.generateServeEntryClient(eager, lazy, '');
|
|
143
148
|
}
|
|
144
|
-
function emitEntryEvent(eager, lazy
|
|
145
|
-
const q = `?t=${token}`;
|
|
149
|
+
function emitEntryEvent(eager, lazy) {
|
|
146
150
|
const native = loadNativeAddon();
|
|
147
151
|
if (typeof native.generateServeEntryEvent !== 'function') {
|
|
148
152
|
throw new Error('vmz native addon missing generateServeEntryEvent — rebuild with `pnpm napi:build`');
|
|
149
153
|
}
|
|
150
|
-
return native.generateServeEntryEvent(eager, lazy,
|
|
154
|
+
return native.generateServeEntryEvent(eager, lazy, '');
|
|
151
155
|
}
|
|
152
|
-
|
|
156
|
+
let pendingReload = null;
|
|
157
|
+
let pendingRollback = null;
|
|
158
|
+
/**
|
|
159
|
+
* 第一阶段:加载构建产物、写客户端入口模块,但**不**发布 SSE、不置 ready。
|
|
160
|
+
*
|
|
161
|
+
* dev 的浏览器交付必须经 `vmz-pack`,而 pack 由 CLI 侧执行(runtime 不反向
|
|
162
|
+
* 依赖 `@vmz/vmz`)。因此先把入口写到 outDir,让调用方 pack,再 `commitReload`。
|
|
163
|
+
*/
|
|
164
|
+
export async function prepareReload(opts = {}) {
|
|
153
165
|
const prevToken = serveState.reloadToken;
|
|
154
166
|
const prevCatalog = serveState.pageCatalog;
|
|
155
167
|
const prevCtors = new Map(serveState.pageCtors);
|
|
156
168
|
const nextToken = Date.now();
|
|
157
169
|
serveState.reloadToken = nextToken;
|
|
170
|
+
pendingReload = null;
|
|
171
|
+
pendingRollback = () => {
|
|
172
|
+
serveState.reloadToken = prevToken;
|
|
173
|
+
serveState.pageCatalog = prevCatalog;
|
|
174
|
+
serveState.pageCtors.clear();
|
|
175
|
+
for (const [k, v] of prevCtors)
|
|
176
|
+
serveState.pageCtors.set(k, v);
|
|
177
|
+
};
|
|
158
178
|
const affected = opts.payload?.affectedChunks ?? [];
|
|
159
179
|
const seeds = opts.payload?.seedChunks ?? [];
|
|
160
180
|
const full = opts.payload?.full;
|
|
161
181
|
const islandHmr = Boolean(opts.payload?.islandHmr);
|
|
162
182
|
const rerunLoaders = opts.payload?.rerunLoaders ?? [];
|
|
163
|
-
const skipEntryRewrite = Boolean(opts.payload?.skipEntryRewrite);
|
|
164
183
|
const buildId = opts.payload?.buildId != null ? String(opts.payload.buildId) : null;
|
|
165
184
|
const sourceRevision = opts.payload?.sourceRevision != null ? String(opts.payload.sourceRevision) : null;
|
|
166
185
|
const bundleRevision = opts.payload?.bundleRevision != null ? String(opts.payload.bundleRevision) : null;
|
|
@@ -255,13 +274,28 @@ export async function softReload(opts = {}) {
|
|
|
255
274
|
}
|
|
256
275
|
}
|
|
257
276
|
const layoutEntries = [...layoutNames].sort().map((id) => ({ name: id, entry: `${id}.client.js` }));
|
|
258
|
-
await writeFile(path.join(serveState.distDir, 'entry-client.js'), emitEntryClient([...eagerComponents, ...pageEntries, ...layoutEntries], lazyComponents
|
|
277
|
+
await writeFile(path.join(serveState.distDir, 'entry-client.js'), emitEntryClient([...eagerComponents, ...pageEntries, ...layoutEntries], lazyComponents), 'utf8');
|
|
259
278
|
const strategies = resumeEntries.map((e) => e.strategy);
|
|
260
279
|
const eventOnlyShell = isEventOnlyShell(strategies);
|
|
261
|
-
await writeFile(path.join(serveState.distDir, 'entry-event.js'), emitEntryEvent([], lazyComponents
|
|
262
|
-
serveState.lastDevError = null;
|
|
280
|
+
await writeFile(path.join(serveState.distDir, 'entry-event.js'), emitEntryEvent([], lazyComponents), 'utf8');
|
|
263
281
|
const mode = islandHmr ? 'island' : eventOnlyShell ? 'event-shell' : 'full';
|
|
264
|
-
|
|
282
|
+
const info = {
|
|
283
|
+
affectedChunks: affected,
|
|
284
|
+
seedChunks: seeds,
|
|
285
|
+
full: Boolean(full),
|
|
286
|
+
islandHmr,
|
|
287
|
+
mode,
|
|
288
|
+
eventOnlyShell,
|
|
289
|
+
pageCount: serveState.pageCatalog.length,
|
|
290
|
+
reloadedPages: islandHmr ? 0 : nextCtors.size,
|
|
291
|
+
reloadAllPages,
|
|
292
|
+
buildId: buildId || serveState.lastDevBuildId,
|
|
293
|
+
sourceRevision,
|
|
294
|
+
bundleRevision,
|
|
295
|
+
serveRevision: String(serveState.reloadToken),
|
|
296
|
+
token: serveState.reloadToken,
|
|
297
|
+
};
|
|
298
|
+
const sse = {
|
|
265
299
|
type: 'hmr',
|
|
266
300
|
mode,
|
|
267
301
|
affectedChunks: affected,
|
|
@@ -273,7 +307,8 @@ export async function softReload(opts = {}) {
|
|
|
273
307
|
serveRevision: String(serveState.reloadToken),
|
|
274
308
|
full: Boolean(full),
|
|
275
309
|
eventOnlyShell,
|
|
276
|
-
}
|
|
310
|
+
};
|
|
311
|
+
let logLine = null;
|
|
277
312
|
if (!opts.quiet) {
|
|
278
313
|
const aff = affected.length > 0 ? ` affected=[${affected.join(', ')}]` : full === false ? ' affected=[]' : '';
|
|
279
314
|
const scope = islandHmr ? 'island' : reloadAllPages ? 'all-pages' : `pages=${nextCtors.size}`;
|
|
@@ -281,32 +316,58 @@ export async function softReload(opts = {}) {
|
|
|
281
316
|
const rev = bid || sourceRevision || bundleRevision
|
|
282
317
|
? ` buildId=${bid || '-'} source=${sourceRevision || '-'} bundle=${bundleRevision || '-'} serve=${serveState.reloadToken}`
|
|
283
318
|
: ` t=${serveState.reloadToken}`;
|
|
284
|
-
|
|
319
|
+
logLine = `vmz serve: soft reload ok (mode=${mode}; ${scope}; catalog=${serveState.pageCatalog.length};${rev}${aff})`;
|
|
285
320
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
seedChunks: seeds,
|
|
289
|
-
full: Boolean(full),
|
|
290
|
-
islandHmr,
|
|
291
|
-
mode,
|
|
292
|
-
eventOnlyShell,
|
|
293
|
-
pageCount: serveState.pageCatalog.length,
|
|
294
|
-
reloadedPages: islandHmr ? 0 : nextCtors.size,
|
|
295
|
-
reloadAllPages,
|
|
296
|
-
buildId: buildId || serveState.lastDevBuildId,
|
|
297
|
-
sourceRevision,
|
|
298
|
-
bundleRevision,
|
|
299
|
-
serveRevision: String(serveState.reloadToken),
|
|
300
|
-
token: serveState.reloadToken,
|
|
301
|
-
};
|
|
321
|
+
pendingReload = { info, sse, logLine };
|
|
322
|
+
return info;
|
|
302
323
|
}
|
|
303
324
|
catch (err) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
for (const [k, v] of prevCtors)
|
|
308
|
-
serveState.pageCtors.set(k, v);
|
|
325
|
+
pendingRollback?.();
|
|
326
|
+
pendingRollback = null;
|
|
327
|
+
pendingReload = null;
|
|
309
328
|
serveState.lastDevError = normalizeDevError(err);
|
|
310
329
|
throw err;
|
|
311
330
|
}
|
|
312
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* 第二阶段:发布 SSE 并置 ready。island 热更新的地址表从 pack manifest 精确
|
|
334
|
+
* 查得(逻辑 `chunkId` → chunk 文件、含 `dom.browser` 的共享 chunk),不做后缀
|
|
335
|
+
* 猜测;查不到即整页 reload。
|
|
336
|
+
*/
|
|
337
|
+
export function commitReload() {
|
|
338
|
+
const prepared = pendingReload;
|
|
339
|
+
if (!prepared)
|
|
340
|
+
throw new Error('commitReload: no prepared reload');
|
|
341
|
+
let island = null;
|
|
342
|
+
if (serveState.isDev) {
|
|
343
|
+
try {
|
|
344
|
+
island = packIslandHrefs(serveState.distDir);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
abortReload(err);
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
pendingReload = null;
|
|
352
|
+
pendingRollback = null;
|
|
353
|
+
serveState.lastDevError = null;
|
|
354
|
+
serveState.ready = true;
|
|
355
|
+
notifySse(JSON.stringify({ ...prepared.sse, island }));
|
|
356
|
+
if (prepared.logLine)
|
|
357
|
+
console.log(prepared.logLine);
|
|
358
|
+
return { ...prepared.info, island };
|
|
359
|
+
}
|
|
360
|
+
/** pack 失败:回滚到上一轮已发布状态,并以 SSE error 显式告知浏览器。 */
|
|
361
|
+
export function abortReload(err) {
|
|
362
|
+
pendingReload = null;
|
|
363
|
+
pendingRollback?.();
|
|
364
|
+
pendingRollback = null;
|
|
365
|
+
serveState.ready = false;
|
|
366
|
+
serveState.lastDevError = normalizeDevError(err);
|
|
367
|
+
notifySse(JSON.stringify({
|
|
368
|
+
type: 'error',
|
|
369
|
+
message: serveState.lastDevError.message,
|
|
370
|
+
stack: serveState.lastDevError.stack,
|
|
371
|
+
at: serveState.lastDevError.at,
|
|
372
|
+
}));
|
|
373
|
+
}
|
package/dist/host/serve/http.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { handleNodeRequest } from '../../faces/vmz-runtime.js';
|
|
3
3
|
import { SHUTDOWN_TIMEOUT_MS } from './constants.js';
|
|
4
|
-
import { normalizeDevError, notifySse,
|
|
4
|
+
import { abortReload, commitReload, normalizeDevError, notifySse, prepareReload } from './hmr-dev.js';
|
|
5
5
|
import { renderPage, renderPageStream } from './page-render.js';
|
|
6
6
|
import { serveState } from './state.js';
|
|
7
7
|
function readRequestBody(req) {
|
|
@@ -70,21 +70,38 @@ export function startServeHttp() {
|
|
|
70
70
|
if (url.pathname === '/__vmz/reload' && req.method === 'POST') {
|
|
71
71
|
readRequestBody(req)
|
|
72
72
|
.then((raw) => {
|
|
73
|
-
let
|
|
73
|
+
let body = {};
|
|
74
74
|
try {
|
|
75
|
-
|
|
75
|
+
body = raw ? JSON.parse(String(raw)) : {};
|
|
76
76
|
}
|
|
77
77
|
catch {
|
|
78
|
-
|
|
78
|
+
body = {};
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
// dev 两阶段:prepare 写入口 → CLI 侧 `vmz-pack` → commit 发布。
|
|
81
|
+
// 未带 phase 的请求不属于 v0 合同,直接拒绝而不是猜测阶段。
|
|
82
|
+
const phase = body.phase;
|
|
83
|
+
if (phase === 'prepare') {
|
|
84
|
+
return prepareReload({ payload: body.payload }).then((info) => ({
|
|
85
|
+
phase,
|
|
86
|
+
info,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
if (phase === 'commit') {
|
|
90
|
+
return { phase, info: commitReload() };
|
|
91
|
+
}
|
|
92
|
+
if (phase === 'abort') {
|
|
93
|
+
const rawError = body.error;
|
|
94
|
+
abortReload(new Error(rawError?.message || 'pack failed'));
|
|
95
|
+
return { phase, info: { ok: true } };
|
|
96
|
+
}
|
|
97
|
+
throw Object.assign(new Error('reload: unknown phase'), { statusCode: 400 });
|
|
81
98
|
})
|
|
82
|
-
.then((info) => {
|
|
99
|
+
.then(({ phase, info }) => {
|
|
83
100
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
84
|
-
res.end(JSON.stringify({ ok: true,
|
|
101
|
+
res.end(JSON.stringify({ ok: true, phase, ...info }));
|
|
85
102
|
})
|
|
86
103
|
.catch((err) => {
|
|
87
|
-
console.error('vmz serve:
|
|
104
|
+
console.error('vmz serve: reload failed', err);
|
|
88
105
|
serveState.lastDevError = normalizeDevError(err);
|
|
89
106
|
notifySse(JSON.stringify({
|
|
90
107
|
type: 'error',
|
|
@@ -8,3 +8,24 @@
|
|
|
8
8
|
* `vmz-pack` rewrites.
|
|
9
9
|
*/
|
|
10
10
|
export declare function packEntryHref(distDir: string, logical: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* 部署单元逻辑 `chunkId` → 浏览器 chunk URL。
|
|
13
|
+
*
|
|
14
|
+
* 只读 `unitChunks[]` 的精确 `logical` 匹配:逻辑 `chunkId` 与 pack chunk id
|
|
15
|
+
* 不保证相同,禁止前缀推导或后缀猜测。单元未进客户端图(`fileName: null`)
|
|
16
|
+
* 视为错误,消费方不得回退到未打包文件。
|
|
17
|
+
*/
|
|
18
|
+
export declare function packChunkHref(distDir: string, logical: string): string;
|
|
19
|
+
/** 含指定产物模块的 chunk URL(精确路径匹配,如 `dom.browser.js`)。 */
|
|
20
|
+
export declare function packModuleChunkHref(distDir: string, moduleRel: string): string;
|
|
21
|
+
export interface PackIslandHrefs {
|
|
22
|
+
/** 含 `dom.browser` 的共享 chunk(`hydrate` 由该 chunk 导出)。 */
|
|
23
|
+
dom: string;
|
|
24
|
+
/** 部署单元逻辑 `chunkId` → 其客户端 chunk URL(未进图的不列出)。 */
|
|
25
|
+
pages: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* island 热更新地址表:一次读 manifest,给出 runtime 共享 chunk 与各页面 chunk
|
|
29
|
+
* 的 URL。调用方只按 `data-vmz-page` 逻辑名查表,缺失即走整页 reload。
|
|
30
|
+
*/
|
|
31
|
+
export declare function packIslandHrefs(distDir: string): PackIslandHrefs;
|
|
@@ -3,6 +3,24 @@ import path from 'node:path';
|
|
|
3
3
|
const PACK_MANIFEST_REL = ['_vmz', 'pack-manifest.json'];
|
|
4
4
|
const PACK_OUTPUT_DIR_REL = ['_vmz', 'pack'];
|
|
5
5
|
const DEFAULT_PUBLIC_BASE = '/';
|
|
6
|
+
function readPackManifest(distDir, caller) {
|
|
7
|
+
const manifestPath = path.join(distDir, ...PACK_MANIFEST_REL);
|
|
8
|
+
if (!fs.existsSync(manifestPath)) {
|
|
9
|
+
throw new Error(`${caller}: missing pack manifest at ${manifestPath} — run vmz build (pack) first`);
|
|
10
|
+
}
|
|
11
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
12
|
+
}
|
|
13
|
+
function chunkHref(manifest, fileName) {
|
|
14
|
+
const base = String(manifest.publicBase || DEFAULT_PUBLIC_BASE).replace(/\/$/, '');
|
|
15
|
+
return `${base}/${PACK_OUTPUT_DIR_REL.join('/')}/${fileName}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 模块路径比较键:去掉 Windows verbatim 前缀(Rust `canonicalize` 给
|
|
19
|
+
* `//?/E:/…`,Node 给 `E:/…`)并统一分隔符,避免同一文件因前缀差异查不到。
|
|
20
|
+
*/
|
|
21
|
+
function moduleKey(filePath) {
|
|
22
|
+
return filePath.replace(/^(?:\\\\\?\\|\/\/\?\/)/, '').replace(/\\/g, '/');
|
|
23
|
+
}
|
|
6
24
|
/**
|
|
7
25
|
* Resolve the browser URL of a packed bootstrap entry from
|
|
8
26
|
* `_vmz/pack-manifest.json`.
|
|
@@ -13,15 +31,68 @@ const DEFAULT_PUBLIC_BASE = '/';
|
|
|
13
31
|
* `vmz-pack` rewrites.
|
|
14
32
|
*/
|
|
15
33
|
export function packEntryHref(distDir, logical) {
|
|
16
|
-
const
|
|
17
|
-
if (!fs.existsSync(manifestPath)) {
|
|
18
|
-
throw new Error(`packEntryHref: missing ${manifestPath} — run vmz build (pack) first`);
|
|
19
|
-
}
|
|
20
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
34
|
+
const manifest = readPackManifest(distDir, 'packEntryHref');
|
|
21
35
|
const hit = (manifest.entryFiles || []).find((entry) => entry && entry.logical === logical);
|
|
22
36
|
if (!hit || typeof hit.file !== 'string' || !hit.file) {
|
|
23
37
|
throw new Error(`packEntryHref: pack manifest has no entry file for "${logical}"`);
|
|
24
38
|
}
|
|
25
|
-
|
|
26
|
-
|
|
39
|
+
return chunkHref(manifest, hit.file);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 部署单元逻辑 `chunkId` → 浏览器 chunk URL。
|
|
43
|
+
*
|
|
44
|
+
* 只读 `unitChunks[]` 的精确 `logical` 匹配:逻辑 `chunkId` 与 pack chunk id
|
|
45
|
+
* 不保证相同,禁止前缀推导或后缀猜测。单元未进客户端图(`fileName: null`)
|
|
46
|
+
* 视为错误,消费方不得回退到未打包文件。
|
|
47
|
+
*/
|
|
48
|
+
export function packChunkHref(distDir, logical) {
|
|
49
|
+
const manifest = readPackManifest(distDir, 'packChunkHref');
|
|
50
|
+
const hit = (manifest.unitChunks || []).find((entry) => entry && entry.logical === logical);
|
|
51
|
+
if (!hit) {
|
|
52
|
+
throw new Error(`packChunkHref: pack manifest has no unit chunk for "${logical}"`);
|
|
53
|
+
}
|
|
54
|
+
if (typeof hit.fileName !== 'string' || !hit.fileName) {
|
|
55
|
+
throw new Error(`packChunkHref: unit "${logical}" is not in the browser client graph`);
|
|
56
|
+
}
|
|
57
|
+
return chunkHref(manifest, hit.fileName);
|
|
58
|
+
}
|
|
59
|
+
/** 含指定产物模块的 chunk URL(精确路径匹配,如 `dom.browser.js`)。 */
|
|
60
|
+
export function packModuleChunkHref(distDir, moduleRel) {
|
|
61
|
+
const manifest = readPackManifest(distDir, 'packModuleChunkHref');
|
|
62
|
+
const target = moduleKey(path.join(distDir, ...moduleRel.split('/')));
|
|
63
|
+
for (const chunk of manifest.chunks || []) {
|
|
64
|
+
if (typeof chunk.fileName !== 'string' || !chunk.fileName)
|
|
65
|
+
continue;
|
|
66
|
+
for (const module of chunk.modules || []) {
|
|
67
|
+
if (moduleKey(String(module)) === target)
|
|
68
|
+
return chunkHref(manifest, chunk.fileName);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`packModuleChunkHref: no chunk carries "${moduleRel}"`);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* island 热更新地址表:一次读 manifest,给出 runtime 共享 chunk 与各页面 chunk
|
|
75
|
+
* 的 URL。调用方只按 `data-vmz-page` 逻辑名查表,缺失即走整页 reload。
|
|
76
|
+
*/
|
|
77
|
+
export function packIslandHrefs(distDir) {
|
|
78
|
+
const manifest = readPackManifest(distDir, 'packIslandHrefs');
|
|
79
|
+
const pages = {};
|
|
80
|
+
for (const unit of manifest.unitChunks || []) {
|
|
81
|
+
if (!unit || typeof unit.logical !== 'string' || !unit.logical)
|
|
82
|
+
continue;
|
|
83
|
+
if (typeof unit.fileName !== 'string' || !unit.fileName)
|
|
84
|
+
continue;
|
|
85
|
+
pages[unit.logical] = chunkHref(manifest, unit.fileName);
|
|
86
|
+
}
|
|
87
|
+
const target = moduleKey(path.join(distDir, 'dom.browser.js'));
|
|
88
|
+
for (const chunk of manifest.chunks || []) {
|
|
89
|
+
if (typeof chunk.fileName !== 'string' || !chunk.fileName)
|
|
90
|
+
continue;
|
|
91
|
+
for (const module of chunk.modules || []) {
|
|
92
|
+
if (moduleKey(String(module)) === target) {
|
|
93
|
+
return { dom: chunkHref(manifest, chunk.fileName), pages };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
throw new Error('packIslandHrefs: no chunk carries "dom.browser.js"');
|
|
27
98
|
}
|
|
@@ -291,11 +291,15 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
291
291
|
hideOverlay();
|
|
292
292
|
if (msg.mode === "island") {
|
|
293
293
|
try {
|
|
294
|
-
|
|
294
|
+
// 地址表来自 pack manifest(逻辑名精确匹配),缺失即整页 reload:
|
|
295
|
+
// 不再单独加载裸 dom.browser.js,避免与 packed bootstrap 形成两份 runtime。
|
|
296
|
+
const island = msg.island || {};
|
|
295
297
|
const root = document.getElementById("app");
|
|
296
298
|
const pageChunk = root && root.getAttribute("data-vmz-page");
|
|
297
|
-
|
|
298
|
-
|
|
299
|
+
const pageHref = pageChunk && island.pages ? island.pages[pageChunk] : null;
|
|
300
|
+
if (root && pageChunk && island.dom && pageHref) {
|
|
301
|
+
const { hydrate } = await import(island.dom);
|
|
302
|
+
const pageMod = await import(pageHref);
|
|
299
303
|
let hmrProps = {};
|
|
300
304
|
try {
|
|
301
305
|
const raw = root.getAttribute("data-vmz-props");
|
|
@@ -394,12 +398,10 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
394
398
|
if (typeof native.generatePageShell !== 'function') {
|
|
395
399
|
throw new Error('vmz native addon missing generatePageShell — rebuild with `pnpm napi:build`');
|
|
396
400
|
}
|
|
397
|
-
//
|
|
398
|
-
//
|
|
401
|
+
// 客户端入口一律经 pack manifest 解析:原始 `entry-client.js` 及其拉取的
|
|
402
|
+
// 客户端模块保留裸包说明符,浏览器无法解析。dev 与生产因此走同一张表。
|
|
399
403
|
const entryLogical = eventOnlyShell ? 'entry-event.js' : 'entry-client.js';
|
|
400
|
-
const entrySrc = serveState.
|
|
401
|
-
? `/${entryLogical}?t=${serveState.reloadToken}`
|
|
402
|
-
: packEntryHref(serveState.distDir, entryLogical);
|
|
404
|
+
const entrySrc = packEntryHref(serveState.distDir, entryLogical);
|
|
403
405
|
const cssHref = cssEntryWithBust(serveState.cssEntry);
|
|
404
406
|
yield native.generatePageShell({
|
|
405
407
|
bodyHtml,
|
|
@@ -12,5 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
/** Normalize one or more class fragments into a deduped class string. */
|
|
14
14
|
export declare function mergeClassParts(...parts: unknown[]): string;
|
|
15
|
+
/**
|
|
16
|
+
* Split a style fragment on top-level `;`. Semicolons inside `url(...)` /
|
|
17
|
+
* nested parens and inside quotes (e.g. `content: 'a;b'`, data URIs with
|
|
18
|
+
* `;base64,`) do not split — a naive split would truncate those values.
|
|
19
|
+
*/
|
|
20
|
+
export declare function splitStyleDeclarations(text: string): string[];
|
|
15
21
|
/** Merge static + dynamic style fragments; later parts override same keys. */
|
|
16
22
|
export declare function mergeStyleParts(...parts: unknown[]): string;
|
|
@@ -129,7 +129,7 @@ function pushStyleValues(out, key, value) {
|
|
|
129
129
|
* nested parens and inside quotes (e.g. `content: 'a;b'`, data URIs with
|
|
130
130
|
* `;base64,`) do not split — a naive split would truncate those values.
|
|
131
131
|
*/
|
|
132
|
-
function splitStyleDeclarations(text) {
|
|
132
|
+
export function splitStyleDeclarations(text) {
|
|
133
133
|
const chunks = [];
|
|
134
134
|
let depth = 0;
|
|
135
135
|
let quote = null;
|
|
@@ -66,6 +66,13 @@ export type DedupeComponentEntriesOpts = {
|
|
|
66
66
|
export type ImportComponentEntriesOpts = {
|
|
67
67
|
cacheBust?: string | number;
|
|
68
68
|
loaded?: Set<string>;
|
|
69
|
+
/**
|
|
70
|
+
* Import the unpacked client tree (`${chunkId}.client.js`) even when
|
|
71
|
+
* `_vmz/pack-node-manifest.json` exists. Client hosts (headless logic
|
|
72
|
+
* mounts) execute unpacked client bytes, so their registry must use the
|
|
73
|
+
* same tree. Node/SSR hosts keep the default packed-node lookup.
|
|
74
|
+
*/
|
|
75
|
+
unpacked?: boolean;
|
|
69
76
|
};
|
|
70
77
|
export type BootstrapComponentRegistryOpts = LoadComponentEntriesOpts & ImportComponentEntriesOpts & {
|
|
71
78
|
preload?: 'all' | 'closure' | 'none';
|
|
@@ -109,5 +116,7 @@ export type RenderHostOpts = {
|
|
|
109
116
|
preload?: 'all' | 'closure' | 'none';
|
|
110
117
|
cacheBust?: string | number;
|
|
111
118
|
rootChunkIds?: string[] | null;
|
|
119
|
+
/** Client hosts import registry entries from the unpacked client tree. */
|
|
120
|
+
unpackedEntries?: boolean;
|
|
112
121
|
[key: string]: unknown;
|
|
113
122
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content identity for `v-html` payloads shared by SSR and resume.
|
|
3
|
+
*
|
|
4
|
+
* The browser serializer expands self-closing tags (`<path/>` becomes
|
|
5
|
+
* `<path></path>`), so comparing a generated markup string with the serialized
|
|
6
|
+
* SSR DOM always misses. SSR therefore records the digest of the exact string
|
|
7
|
+
* it wrote, and resume compares that marker with the digest of the string it
|
|
8
|
+
* would write. The digest is taken over the generated string only: no DOM
|
|
9
|
+
* serialization, no normalization.
|
|
10
|
+
*
|
|
11
|
+
* Two independent 32-bit FNV-1a lanes plus the code-unit length give 64 bits of
|
|
12
|
+
* identity without pulling a crypto dependency into the browser bundle.
|
|
13
|
+
*/
|
|
14
|
+
export declare const HTML_HASH_ATTR = "data-vmz-html-hash";
|
|
15
|
+
export declare function htmlDigest(value: string): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content identity for `v-html` payloads shared by SSR and resume.
|
|
3
|
+
*
|
|
4
|
+
* The browser serializer expands self-closing tags (`<path/>` becomes
|
|
5
|
+
* `<path></path>`), so comparing a generated markup string with the serialized
|
|
6
|
+
* SSR DOM always misses. SSR therefore records the digest of the exact string
|
|
7
|
+
* it wrote, and resume compares that marker with the digest of the string it
|
|
8
|
+
* would write. The digest is taken over the generated string only: no DOM
|
|
9
|
+
* serialization, no normalization.
|
|
10
|
+
*
|
|
11
|
+
* Two independent 32-bit FNV-1a lanes plus the code-unit length give 64 bits of
|
|
12
|
+
* identity without pulling a crypto dependency into the browser bundle.
|
|
13
|
+
*/
|
|
14
|
+
export const HTML_HASH_ATTR = 'data-vmz-html-hash';
|
|
15
|
+
export function htmlDigest(value) {
|
|
16
|
+
const text = value == null ? '' : String(value);
|
|
17
|
+
let laneA = 0x811c9dc5;
|
|
18
|
+
let laneB = 0x9e3779b9;
|
|
19
|
+
for (let i = 0; i < text.length; i++) {
|
|
20
|
+
const code = text.charCodeAt(i);
|
|
21
|
+
laneA = Math.imul(laneA ^ code, 0x01000193) >>> 0;
|
|
22
|
+
laneB = Math.imul(laneB ^ code, 0x85ebca77) >>> 0;
|
|
23
|
+
}
|
|
24
|
+
return `${text.length.toString(36)}-${laneA.toString(36)}-${laneB.toString(36)}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** SSR row identity for multi-root `<template v-for>` rows. */
|
|
2
|
+
export declare const ROW_MARKER_PREFIX = "vmz-row:";
|
|
3
|
+
export declare const ROW_END_MARKER_PREFIX = "vmz-row-end:";
|
|
4
|
+
/**
|
|
5
|
+
* Comment-safe row key token.
|
|
6
|
+
*
|
|
7
|
+
* Row identity travels inside HTML comments, where `--` is illegal. Encode the
|
|
8
|
+
* key once (both SSR writer and resume reader use this same function) and strip
|
|
9
|
+
* the remaining hyphens so no key can ever close the comment early.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rowKeyToken(key: unknown): string;
|
|
12
|
+
export declare function rowMarker(key: unknown): string;
|
|
13
|
+
export declare function rowEndMarker(key: unknown): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** SSR row identity for multi-root `<template v-for>` rows. */
|
|
2
|
+
export const ROW_MARKER_PREFIX = 'vmz-row:';
|
|
3
|
+
export const ROW_END_MARKER_PREFIX = 'vmz-row-end:';
|
|
4
|
+
/**
|
|
5
|
+
* Comment-safe row key token.
|
|
6
|
+
*
|
|
7
|
+
* Row identity travels inside HTML comments, where `--` is illegal. Encode the
|
|
8
|
+
* key once (both SSR writer and resume reader use this same function) and strip
|
|
9
|
+
* the remaining hyphens so no key can ever close the comment early.
|
|
10
|
+
*/
|
|
11
|
+
export function rowKeyToken(key) {
|
|
12
|
+
return encodeURIComponent(String(key)).replace(/-/g, '%2D');
|
|
13
|
+
}
|
|
14
|
+
export function rowMarker(key) {
|
|
15
|
+
return `${ROW_MARKER_PREFIX}${rowKeyToken(key)}`;
|
|
16
|
+
}
|
|
17
|
+
export function rowEndMarker(key) {
|
|
18
|
+
return `${ROW_END_MARKER_PREFIX}${rowKeyToken(key)}`;
|
|
19
|
+
}
|