@vmz/core 0.3.13 → 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.
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
135
136
  }
136
- function emitEntryClient(eager, lazy, token) {
137
- const q = `?t=${token}`;
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, q);
147
+ return native.generateServeEntryClient(eager, lazy, '');
143
148
  }
144
- function emitEntryEvent(eager, lazy, token) {
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, q);
154
+ return native.generateServeEntryEvent(eager, lazy, '');
151
155
  }
152
- export async function softReload(opts = {}) {
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, skipEntryRewrite ? serveState.reloadToken : nextToken), 'utf8');
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, skipEntryRewrite ? serveState.reloadToken : nextToken), 'utf8');
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
- notifySse(JSON.stringify({
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
- console.log(`vmz serve: soft reload ok (mode=${mode}; ${scope}; catalog=${serveState.pageCatalog.length};${rev}${aff})`);
319
+ logLine = `vmz serve: soft reload ok (mode=${mode}; ${scope}; catalog=${serveState.pageCatalog.length};${rev}${aff})`;
285
320
  }
286
- return {
287
- affectedChunks: affected,
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
- serveState.reloadToken = prevToken;
305
- serveState.pageCatalog = prevCatalog;
306
- serveState.pageCtors.clear();
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
+ }
@@ -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, softReload } from './hmr-dev.js';
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 payload = {};
73
+ let body = {};
74
74
  try {
75
- payload = raw ? JSON.parse(String(raw)) : {};
75
+ body = raw ? JSON.parse(String(raw)) : {};
76
76
  }
77
77
  catch {
78
- payload = {};
78
+ body = {};
79
79
  }
80
- return softReload({ payload });
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, token: serveState.reloadToken, ...info }));
101
+ res.end(JSON.stringify({ ok: true, phase, ...info }));
85
102
  })
86
103
  .catch((err) => {
87
- console.error('vmz serve: soft reload failed', err);
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 manifestPath = path.join(distDir, ...PACK_MANIFEST_REL);
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
- const base = String(manifest.publicBase || DEFAULT_PUBLIC_BASE).replace(/\/$/, '');
26
- return `${base}/${PACK_OUTPUT_DIR_REL.join('/')}/${hit.file}`;
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
- const { hydrate } = await import("/dom.browser.js?t=" + msg.token);
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
- if (root && pageChunk) {
298
- const pageMod = await import("/" + pageChunk + ".client.js?t=" + msg.token);
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
- // 生产 serve 必须注入打包条目:原始 `entry-client.js` 及其拉取的客户端模块保留
398
- // 裸包说明符,浏览器无法解析。dev 仍走带 token 的原始 bootstrap。
401
+ // 客户端入口一律经 pack manifest 解析:原始 `entry-client.js` 及其拉取的
402
+ // 客户端模块保留裸包说明符,浏览器无法解析。dev 与生产因此走同一张表。
399
403
  const entryLogical = eventOnlyShell ? 'entry-event.js' : 'entry-client.js';
400
- const entrySrc = serveState.isDev
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
+ }