@vmz/core 0.1.0 → 0.1.2
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/client-nav.js +4 -8
- package/dist/dom-core.js +37 -57
- package/dist/serve-host.d.ts +7 -3
- package/dist/serve-host.mjs +385 -159
- package/dist/server.js +15 -1
- package/package.json +1 -1
package/dist/client-nav.js
CHANGED
|
@@ -237,6 +237,9 @@ export function installClientNavigation(opts = {}) {
|
|
|
237
237
|
/** @type {Element | null} */
|
|
238
238
|
let liveRoot = root;
|
|
239
239
|
let retainedLayout = false;
|
|
240
|
+
// Apply target LocaleId before hydrate/onMount so `#locales/*` and
|
|
241
|
+
// retained shells (SiteHeader) see the committed projection.
|
|
242
|
+
applyLocaleRealization(nextApp);
|
|
240
243
|
if (retainLayouts) {
|
|
241
244
|
applyAppAttrs(root, nextApp);
|
|
242
245
|
if (chunkId) {
|
|
@@ -670,14 +673,7 @@ function canRetainLayouts(root, prevLayout, nextLayout) {
|
|
|
670
673
|
* @param {Element} nextApp
|
|
671
674
|
*/
|
|
672
675
|
function applyAppAttrs(root, nextApp) {
|
|
673
|
-
for (const name of [
|
|
674
|
-
'data-vmz-page',
|
|
675
|
-
'data-vmz-props',
|
|
676
|
-
'data-vmz-layout',
|
|
677
|
-
'data-vmz-route',
|
|
678
|
-
'data-vmz-locale',
|
|
679
|
-
'data-vmz-dir',
|
|
680
|
-
]) {
|
|
676
|
+
for (const name of ['data-vmz-page', 'data-vmz-props', 'data-vmz-layout', 'data-vmz-route', 'data-vmz-locale', 'data-vmz-dir']) {
|
|
681
677
|
const v = nextApp.getAttribute(name);
|
|
682
678
|
if (v == null)
|
|
683
679
|
root.removeAttribute(name);
|
package/dist/dom-core.js
CHANGED
|
@@ -1158,12 +1158,8 @@ export const directApi = {
|
|
|
1158
1158
|
// DOM-as-entry (Element): drop expandos.
|
|
1159
1159
|
if (entry.nodeType === 1) {
|
|
1160
1160
|
entry.__vmzBox = null;
|
|
1161
|
-
entry.
|
|
1162
|
-
entry.
|
|
1163
|
-
entry.__vmzE0 = null;
|
|
1164
|
-
entry.__vmzE1 = null;
|
|
1165
|
-
entry.__vmzE2 = null;
|
|
1166
|
-
entry.__vmzE3 = null;
|
|
1161
|
+
entry.__vmzT = null;
|
|
1162
|
+
entry.__vmzE = null;
|
|
1167
1163
|
entry.__vmzTexts = null;
|
|
1168
1164
|
entry.__vmzBp = null;
|
|
1169
1165
|
return;
|
|
@@ -2345,45 +2341,19 @@ export const directApi = {
|
|
|
2345
2341
|
return false;
|
|
2346
2342
|
const entries = entryByIndex;
|
|
2347
2343
|
const n = arr.length;
|
|
2348
|
-
// Fastest path: text-only leaf → mutate + __vmzT
|
|
2344
|
+
// Fastest path: text-only leaf → mutate + __vmzT[slot].nodeValue (no applyByField).
|
|
2349
2345
|
const slot = rkTextSlots ? rkTextSlots[leaf] : undefined;
|
|
2350
2346
|
if (op === '+' && slot != null && !rkHostFieldSet.has(leaf)) {
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
else if (slot === 1) {
|
|
2364
|
-
for (let i = s; i < n; i += st) {
|
|
2365
|
-
const item = arr[i];
|
|
2366
|
-
if (item == null || typeof item !== 'object')
|
|
2367
|
-
continue;
|
|
2368
|
-
const v = item[leaf] + rhs;
|
|
2369
|
-
item[leaf] = v;
|
|
2370
|
-
const entry = entries[i];
|
|
2371
|
-
if (entry && entry.nodeType === 1)
|
|
2372
|
-
entry.__vmzT1.nodeValue = v;
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
else {
|
|
2376
|
-
const tKey = '__vmzT' + slot;
|
|
2377
|
-
for (let i = s; i < n; i += st) {
|
|
2378
|
-
const item = arr[i];
|
|
2379
|
-
if (item == null || typeof item !== 'object')
|
|
2380
|
-
continue;
|
|
2381
|
-
const v = item[leaf] + rhs;
|
|
2382
|
-
item[leaf] = v;
|
|
2383
|
-
const entry = entries[i];
|
|
2384
|
-
if (entry && entry.nodeType === 1)
|
|
2385
|
-
entry[tKey].nodeValue = v;
|
|
2386
|
-
}
|
|
2347
|
+
for (let i = s; i < n; i += st) {
|
|
2348
|
+
const item = arr[i];
|
|
2349
|
+
if (item == null || typeof item !== 'object')
|
|
2350
|
+
continue;
|
|
2351
|
+
const v = item[leaf] + rhs;
|
|
2352
|
+
item[leaf] = v;
|
|
2353
|
+
const entry = entries[i];
|
|
2354
|
+
const texts = entry && entry.nodeType === 1 ? entry.__vmzT : null;
|
|
2355
|
+
if (texts)
|
|
2356
|
+
texts[slot].nodeValue = v;
|
|
2387
2357
|
}
|
|
2388
2358
|
return true;
|
|
2389
2359
|
}
|
|
@@ -2432,20 +2402,10 @@ export const directApi = {
|
|
|
2432
2402
|
item[leaf] = value;
|
|
2433
2403
|
if (slot != null && !needsThis) {
|
|
2434
2404
|
const entry = entries[i];
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
}
|
|
2440
|
-
if (slot === 1) {
|
|
2441
|
-
entry.__vmzT1.nodeValue = value;
|
|
2442
|
-
continue;
|
|
2443
|
-
}
|
|
2444
|
-
const tn = entry['__vmzT' + slot];
|
|
2445
|
-
if (tn) {
|
|
2446
|
-
tn.nodeValue = value;
|
|
2447
|
-
continue;
|
|
2448
|
-
}
|
|
2405
|
+
const texts = entry && entry.nodeType === 1 ? entry.__vmzT : null;
|
|
2406
|
+
if (texts) {
|
|
2407
|
+
texts[slot].nodeValue = value;
|
|
2408
|
+
continue;
|
|
2449
2409
|
}
|
|
2450
2410
|
}
|
|
2451
2411
|
const entry = entries[i];
|
|
@@ -2692,6 +2652,26 @@ export function applyDomAttr(el, name, value) {
|
|
|
2692
2652
|
}
|
|
2693
2653
|
return;
|
|
2694
2654
|
}
|
|
2655
|
+
// <textarea value="…"> as an attribute does not update visible text; INPUT/SELECT
|
|
2656
|
+
// also need the IDL `.value` property so controlled updates stay in sync after switches.
|
|
2657
|
+
// linkedom `<select>.value` is getter-only — sync via `option.selected` instead of throwing.
|
|
2658
|
+
if (key === 'value' && el && (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT' || el.tagName === 'SELECT')) {
|
|
2659
|
+
const next = value == null || value === false ? '' : String(value);
|
|
2660
|
+
if (el.tagName === 'SELECT') {
|
|
2661
|
+
const opts = el.options || el.querySelectorAll?.('option') || [];
|
|
2662
|
+
for (const opt of opts) {
|
|
2663
|
+
opt.selected = String(opt.value ?? '') === next;
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
else if (el.value !== next) {
|
|
2667
|
+
el.value = next;
|
|
2668
|
+
}
|
|
2669
|
+
if (value == null || value === false)
|
|
2670
|
+
el.removeAttribute('value');
|
|
2671
|
+
else
|
|
2672
|
+
el.setAttribute('value', next);
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2695
2675
|
if (value == null || value === false)
|
|
2696
2676
|
el.removeAttribute(key);
|
|
2697
2677
|
else
|
package/dist/serve-host.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generic VMZ Node host — SSR
|
|
2
|
+
* Generic VMZ Node host — SSR Route Graph pages + dist static + RPC/REST.
|
|
3
3
|
*
|
|
4
4
|
* Invoked by `vmz serve` / `vmz dev` (or: node dist/vmz-serve-host.mjs).
|
|
5
5
|
*
|
|
6
|
-
* Pathname
|
|
7
|
-
*
|
|
6
|
+
* Pathname matches `vmz-deployment.json` `pathPattern` (explicit `<router>.path`
|
|
7
|
+
* or file-route default). Mini page stems are a different host projection.
|
|
8
|
+
* Not an SPA shell.
|
|
8
9
|
*
|
|
9
10
|
* `VMZ_DEV=1`: POST `/__vmz/reload` soft-reloads modules (cache-bust import);
|
|
10
11
|
* GET `/__vmz/events` SSE notifies the browser:
|
|
11
12
|
* - island HMR → re-import `entry-client.js` (no full document reload)
|
|
12
13
|
* - otherwise → `location.reload`
|
|
14
|
+
*
|
|
15
|
+
* Dev resolve hook propagates `?t=` onto nested relative `file:` imports under
|
|
16
|
+
* dist so soft reload does not keep a stale `lib/*.js` ESM cache entry.
|
|
13
17
|
*/
|
|
14
18
|
export {};
|
package/dist/serve-host.mjs
CHANGED
|
@@ -1,28 +1,78 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* Generic VMZ Node host — SSR
|
|
3
|
+
* Generic VMZ Node host — SSR Route Graph pages + dist static + RPC/REST.
|
|
4
4
|
*
|
|
5
5
|
* Invoked by `vmz serve` / `vmz dev` (or: node dist/vmz-serve-host.mjs).
|
|
6
6
|
*
|
|
7
|
-
* Pathname
|
|
8
|
-
*
|
|
7
|
+
* Pathname matches `vmz-deployment.json` `pathPattern` (explicit `<router>.path`
|
|
8
|
+
* or file-route default). Mini page stems are a different host projection.
|
|
9
|
+
* Not an SPA shell.
|
|
9
10
|
*
|
|
10
11
|
* `VMZ_DEV=1`: POST `/__vmz/reload` soft-reloads modules (cache-bust import);
|
|
11
12
|
* GET `/__vmz/events` SSE notifies the browser:
|
|
12
13
|
* - island HMR → re-import `entry-client.js` (no full document reload)
|
|
13
14
|
* - otherwise → `location.reload`
|
|
15
|
+
*
|
|
16
|
+
* Dev resolve hook propagates `?t=` onto nested relative `file:` imports under
|
|
17
|
+
* dist so soft reload does not keep a stale `lib/*.js` ESM cache entry.
|
|
14
18
|
*/
|
|
15
19
|
import { existsSync } from 'node:fs';
|
|
16
20
|
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
17
21
|
import http from 'node:http';
|
|
22
|
+
import { createRequire, registerHooks } from 'node:module';
|
|
18
23
|
import path from 'node:path';
|
|
19
24
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
25
|
import { registerComponents, renderToStream, renderToString } from './vmz-dom.js';
|
|
21
26
|
import { handleNodeRequest, setRoutes, setServerModuleResolver } from './vmz-runtime.js';
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
22
28
|
const distDir = process.env.VMZ_DIST ? path.resolve(process.env.VMZ_DIST) : path.dirname(fileURLToPath(import.meta.url));
|
|
23
29
|
const host = process.env.VMZ_HOST || '127.0.0.1';
|
|
24
30
|
const port = Number(process.env.VMZ_PORT || process.env.PORT || 5173);
|
|
25
31
|
const isDev = process.env.VMZ_DEV === '1' || process.env.VMZ_DEV === 'true';
|
|
32
|
+
// Absolute origin for in-process client graphs that fall back to HTTP RPC
|
|
33
|
+
// (separate `dist/vmz-runtime.js` instance without setServerModuleResolver).
|
|
34
|
+
globalThis.__VMZ_RPC_ORIGIN = `http://${host}:${port}`;
|
|
35
|
+
/**
|
|
36
|
+
* Soft reload only busts the top-level `import(page?t=token)`. Nested relative
|
|
37
|
+
* imports (`../../lib/units.js`) keep the first-loaded ESM cache entry — so a
|
|
38
|
+
* page can demand exports that the stale dep never had (or vice versa).
|
|
39
|
+
* Propagate `t` from parentURL onto file: children under this dist.
|
|
40
|
+
*/
|
|
41
|
+
if (isDev) {
|
|
42
|
+
const distUrlPrefix = pathToFileURL(distDir.endsWith(path.sep) ? distDir : `${distDir}${path.sep}`).href;
|
|
43
|
+
registerHooks({
|
|
44
|
+
resolve(specifier, context, nextResolve) {
|
|
45
|
+
const result = nextResolve(specifier, context);
|
|
46
|
+
if (!specifier.startsWith('.') || !context.parentURL || !result?.url)
|
|
47
|
+
return result;
|
|
48
|
+
let token = '';
|
|
49
|
+
try {
|
|
50
|
+
token = new URL(context.parentURL).searchParams.get('t') || '';
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
if (!token)
|
|
56
|
+
return result;
|
|
57
|
+
if (!result.url.startsWith('file:'))
|
|
58
|
+
return result;
|
|
59
|
+
if (!result.url.startsWith(distUrlPrefix)) {
|
|
60
|
+
try {
|
|
61
|
+
if (!fileURLToPath(result.url).startsWith(distDir))
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const u = new URL(result.url);
|
|
69
|
+
if (u.searchParams.get('t') === token)
|
|
70
|
+
return result;
|
|
71
|
+
u.searchParams.set('t', token);
|
|
72
|
+
return { ...result, url: u.href, shortCircuit: true };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
26
76
|
/** @type {number} */
|
|
27
77
|
let reloadToken = Date.now();
|
|
28
78
|
/** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parseChunkSegments> }>} */
|
|
@@ -44,6 +94,12 @@ let shuttingDown = false;
|
|
|
44
94
|
let ready = false;
|
|
45
95
|
/** @type {{ message: string, stack?: string, at: number } | null} */
|
|
46
96
|
let lastDevError = null;
|
|
97
|
+
/**
|
|
98
|
+
* Native CodeGenerators handle — must be declared before top-level `await softReload()`
|
|
99
|
+
* (TDZ: requireNativeGenerator may run during that await).
|
|
100
|
+
* @type {any}
|
|
101
|
+
*/
|
|
102
|
+
let _nativeGen;
|
|
47
103
|
setServerModuleResolver((moduleId) => {
|
|
48
104
|
const rel = moduleId.replace(/^#server\//, '') + '.js';
|
|
49
105
|
return bustUrl(pathToFileURL(path.join(distDir, '#server', rel)).href);
|
|
@@ -277,11 +333,36 @@ function normalizeActionResult(acted) {
|
|
|
277
333
|
* @param {string} marker
|
|
278
334
|
*/
|
|
279
335
|
async function* emitAccessShell(marker) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
336
|
+
const native = requireNativeGenerator();
|
|
337
|
+
if (typeof native.generateHtmlShell !== 'function') {
|
|
338
|
+
throw new Error('vmz native addon missing generateHtmlShell — rebuild with `pnpm napi:build`');
|
|
339
|
+
}
|
|
340
|
+
yield native.generateHtmlShell({
|
|
341
|
+
title: 'App',
|
|
342
|
+
lang: 'en',
|
|
343
|
+
cssHrefs: [],
|
|
344
|
+
bodyHtml: `<p>${marker}</p>`,
|
|
345
|
+
bodyAttrs: [],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Prefer page `static meta()` for document title/description — never brand the framework in business HTML.
|
|
350
|
+
* @param {any} Page
|
|
351
|
+
*/
|
|
352
|
+
function resolvePageDocumentMeta(Page) {
|
|
353
|
+
try {
|
|
354
|
+
let raw = {};
|
|
355
|
+
if (typeof Page?.meta === 'function')
|
|
356
|
+
raw = Page.meta() || {};
|
|
357
|
+
else if (Page?.meta && typeof Page.meta === 'object')
|
|
358
|
+
raw = Page.meta;
|
|
359
|
+
const title = String(raw.title || '').trim();
|
|
360
|
+
const description = String(raw.description || '').trim();
|
|
361
|
+
return { title: title || 'App', description };
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return { title: 'App', description: '' };
|
|
365
|
+
}
|
|
285
366
|
}
|
|
286
367
|
/**
|
|
287
368
|
* @param {any} Page
|
|
@@ -297,6 +378,12 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
297
378
|
? `\n <script>
|
|
298
379
|
(() => {
|
|
299
380
|
const es = new EventSource("/__vmz/events");
|
|
381
|
+
let sawDisconnect = false;
|
|
382
|
+
es.onerror = () => { sawDisconnect = true; };
|
|
383
|
+
es.onopen = () => {
|
|
384
|
+
// Host respawn drops SSE — reload once the new process is up (no manual restart).
|
|
385
|
+
if (sawDisconnect) location.reload();
|
|
386
|
+
};
|
|
300
387
|
function showOverlay(err) {
|
|
301
388
|
let el = document.getElementById("vmz-dev-overlay");
|
|
302
389
|
if (!el) {
|
|
@@ -315,7 +402,7 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
315
402
|
const stack = (err && err.stack) || "";
|
|
316
403
|
const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({"&":"&","<":"<",">":">"}[c]));
|
|
317
404
|
el.innerHTML = "<div style=\\"max-width:56rem;margin:0 auto\\">"
|
|
318
|
-
+ "<p style=\\"margin:0 0 .5rem;color:#f87171;font-weight:700\\">
|
|
405
|
+
+ "<p style=\\"margin:0 0 .5rem;color:#f87171;font-weight:700\\">Dev Error</p>"
|
|
319
406
|
+ "<pre style=\\"white-space:pre-wrap;margin:0 0 1rem;font-size:13px;line-height:1.45\\">" + esc(msg) + "</pre>"
|
|
320
407
|
+ (stack ? "<pre style=\\"white-space:pre-wrap;opacity:.7;font-size:12px\\">" + esc(stack) + "</pre>" : "")
|
|
321
408
|
+ "<p style=\\"opacity:.65;font-size:12px\\">Fix the file and save — soft reload will clear this overlay.</p>"
|
|
@@ -387,70 +474,88 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
387
474
|
`/* paint immediately */` +
|
|
388
475
|
`var d=document.createElement("div");d.id="vmz-dev-overlay";d.setAttribute("role","alert");` +
|
|
389
476
|
`Object.assign(d.style,{position:"fixed",inset:"0",zIndex:"2147483646",background:"rgba(15,17,21,0.92)",color:"#f4f4f5",fontFamily:"ui-monospace,monospace",padding:"2rem",overflow:"auto"});` +
|
|
390
|
-
`d.innerHTML="<div style='max-width:56rem;margin:0 auto'><p style='color:#f87171;font-weight:700'>
|
|
477
|
+
`d.innerHTML="<div style='max-width:56rem;margin:0 auto'><p style='color:#f87171;font-weight:700'>Dev Error</p><pre style='white-space:pre-wrap'>"+String(e.message||e).replace(/[<>&]/g,function(c){return {"<":"<",">":">","&":"&"}[c]})+"</pre></div>";` +
|
|
391
478
|
`document.documentElement.appendChild(d);})();</script>`
|
|
392
479
|
: '';
|
|
393
480
|
if (signal?.aborted)
|
|
394
481
|
return;
|
|
395
482
|
const themeId = resolveThemeId(opts.searchParams, opts.cookieHeader);
|
|
396
|
-
const htmlTheme = htmlThemeAttributeForId(themeId);
|
|
397
483
|
const themeBoot = themeBootstrapScript();
|
|
398
|
-
const cssLink = cssEntry ? ` <link rel="stylesheet" href="/${String(cssEntry).replace(/^\/+/, '')}?t=${reloadToken}" />\n` : '';
|
|
399
484
|
const propsJson = JSON.stringify(props ?? {});
|
|
400
|
-
const layoutAttr = layoutChain.length ? ` data-vmz-layout="${escapeAttr(layoutChain.join(','))}"` : '';
|
|
401
485
|
const localeId = localeCtx.localeId || localeArtifact?.defaultLocale || 'en';
|
|
402
486
|
const dir = localeCtx.dir || 'ltr';
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
487
|
+
/** @type {string[]} */
|
|
488
|
+
const htmlExtraAttrs = [...htmlThemeAttrPair(themeId)];
|
|
489
|
+
if (localeArtifact?.routing) {
|
|
490
|
+
htmlExtraAttrs.push('data-vmz-locale-routing', JSON.stringify({
|
|
406
491
|
strategy: localeArtifact.routing.strategy || 'prefix',
|
|
407
492
|
defaultPrefix: localeArtifact.routing.defaultPrefix || 'include',
|
|
408
493
|
defaultLocale: localeArtifact.defaultLocale,
|
|
409
494
|
locales: (localeArtifact.locales || []).map((l) => l.id),
|
|
410
|
-
}))
|
|
411
|
-
: '';
|
|
412
|
-
const routingAttr = routingJson ? ` data-vmz-locale-routing="${routingJson}"` : '';
|
|
413
|
-
const hreflangLinks = (localeCtx.alternates || [])
|
|
414
|
-
.map((a) => ` <link rel="alternate" hreflang="${escapeAttr(a.hreflang)}" href="${escapeAttr(a.href)}" />`)
|
|
415
|
-
.join('\n');
|
|
416
|
-
const hreflangBlock = hreflangLinks ? `${hreflangLinks}\n` : '';
|
|
417
|
-
yield `<!DOCTYPE html>
|
|
418
|
-
<html lang="${escapeAttr(localeId)}" data-locale="${escapeAttr(localeId)}" dir="${escapeAttr(dir)}"${routingAttr}${htmlTheme}>
|
|
419
|
-
<head>
|
|
420
|
-
<meta charset="utf-8" />
|
|
421
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
422
|
-
<title>VMZ</title>
|
|
423
|
-
${hreflangBlock}${themeBoot}${cssLink}</head>
|
|
424
|
-
<body>
|
|
425
|
-
<div id="app" data-vmz-page="${escapeAttr(chunkId)}"${layoutAttr}${localeAttr} data-vmz-props="${escapeAttr(propsJson)}">`;
|
|
426
|
-
let bodyHtml = '';
|
|
427
|
-
for await (const chunk of renderToStream(Page, props, { signal })) {
|
|
428
|
-
if (signal?.aborted)
|
|
429
|
-
return;
|
|
430
|
-
bodyHtml += chunk;
|
|
495
|
+
}));
|
|
431
496
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
497
|
+
const pageDocMeta = resolvePageDocumentMeta(Page);
|
|
498
|
+
const prevLocaleHint = globalThis.__vmzLocaleIdHint;
|
|
499
|
+
globalThis.__vmzLocaleIdHint = localeId;
|
|
500
|
+
let bodyHtml = '';
|
|
501
|
+
try {
|
|
502
|
+
for await (const chunk of renderToStream(Page, props, { signal })) {
|
|
503
|
+
if (signal?.aborted)
|
|
504
|
+
return;
|
|
505
|
+
bodyHtml += chunk;
|
|
506
|
+
}
|
|
440
507
|
if (signal?.aborted)
|
|
441
508
|
return;
|
|
509
|
+
// Wrap page HTML in layout chain (outer → inner) via default slot injection.
|
|
510
|
+
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
511
|
+
const Layout = await loadPageCtor(layoutChain[i]);
|
|
512
|
+
if (!Layout)
|
|
513
|
+
continue;
|
|
514
|
+
bodyHtml = await renderToString(Layout, {}, { signal, slotHtml: bodyHtml });
|
|
515
|
+
if (signal?.aborted)
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// Locale discipline: same-app Links retain current LocaleId (realization authority).
|
|
519
|
+
if (localeArtifact && localeId) {
|
|
520
|
+
bodyHtml = localizeBodyLinksInHost(bodyHtml, localeId, localeArtifact);
|
|
521
|
+
}
|
|
442
522
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
523
|
+
finally {
|
|
524
|
+
if (prevLocaleHint === undefined)
|
|
525
|
+
delete globalThis.__vmzLocaleIdHint;
|
|
526
|
+
else
|
|
527
|
+
globalThis.__vmzLocaleIdHint = prevLocaleHint;
|
|
446
528
|
}
|
|
447
|
-
yield bodyHtml;
|
|
448
529
|
if (signal?.aborted)
|
|
449
530
|
return;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
531
|
+
const native = requireNativeGenerator();
|
|
532
|
+
if (typeof native.generatePageShell !== 'function') {
|
|
533
|
+
throw new Error('vmz native addon missing generatePageShell — rebuild with `pnpm napi:build`');
|
|
534
|
+
}
|
|
535
|
+
const entrySrc = `/${eventOnlyShell ? 'entry-event.js' : 'entry-client.js'}?t=${reloadToken}`;
|
|
536
|
+
const cssHref = cssEntry ? `${String(cssEntry).replace(/^\/+/, '')}?t=${reloadToken}` : undefined;
|
|
537
|
+
yield native.generatePageShell({
|
|
538
|
+
bodyHtml,
|
|
539
|
+
chunkId,
|
|
540
|
+
layoutChain,
|
|
541
|
+
propsJson,
|
|
542
|
+
meta: {
|
|
543
|
+
title: pageDocMeta.title,
|
|
544
|
+
description: pageDocMeta.description,
|
|
545
|
+
canonical: '',
|
|
546
|
+
robots: '',
|
|
547
|
+
lang: localeId,
|
|
548
|
+
dir,
|
|
549
|
+
alternates: localeCtx.alternates || [],
|
|
550
|
+
},
|
|
551
|
+
// napi Option<String>: omit/undefined = None; null is rejected as String
|
|
552
|
+
...(cssHref ? { cssEntry: cssHref } : {}),
|
|
553
|
+
isErrorDocument: false,
|
|
554
|
+
htmlExtraAttrs,
|
|
555
|
+
headExtraHtml: themeBoot,
|
|
556
|
+
moduleScriptSrc: entrySrc,
|
|
557
|
+
bodyTailHtml: `${live}${bootOverlay}`,
|
|
558
|
+
});
|
|
454
559
|
}
|
|
455
560
|
const server = http.createServer((req, res) => {
|
|
456
561
|
const url = new URL(req.url || '/', `http://${host}:${port}`);
|
|
@@ -565,17 +670,20 @@ process.on('SIGINT', () => {
|
|
|
565
670
|
* Re-import routes / pages / components with a new cache-bust token.
|
|
566
671
|
* Keeps the HTTP server process alive (no Node restart).
|
|
567
672
|
* Failed reloads keep the previous in-memory modules (Vite-like resilience).
|
|
568
|
-
* @param {{ quiet?: boolean, payload?: { affectedChunks?: string[], seedChunks?: string[], full?: boolean, islandHmr?: boolean } }} [opts]
|
|
673
|
+
* @param {{ quiet?: boolean, payload?: { affectedChunks?: string[], seedChunks?: string[], emitted?: string[], full?: boolean, islandHmr?: boolean } }} [opts]
|
|
569
674
|
*/
|
|
570
675
|
async function softReload(opts = {}) {
|
|
571
676
|
const prevToken = reloadToken;
|
|
572
677
|
const prevCatalog = pageCatalog;
|
|
678
|
+
const prevCtors = new Map(pageCtors);
|
|
573
679
|
const nextToken = Date.now();
|
|
574
680
|
reloadToken = nextToken;
|
|
575
681
|
const affected = opts.payload?.affectedChunks ?? [];
|
|
576
682
|
const seeds = opts.payload?.seedChunks ?? [];
|
|
683
|
+
const emitted = opts.payload?.emitted ?? [];
|
|
577
684
|
const full = opts.payload?.full;
|
|
578
685
|
const islandHmr = Boolean(opts.payload?.islandHmr);
|
|
686
|
+
const reloadAllPages = shouldReloadAllPages({ full, affected, emitted, islandHmr });
|
|
579
687
|
try {
|
|
580
688
|
try {
|
|
581
689
|
const routes = JSON.parse(await readFile(path.join(distDir, 'vmz-routes.json'), 'utf8'));
|
|
@@ -613,7 +721,8 @@ async function softReload(opts = {}) {
|
|
|
613
721
|
components[entry.name] = mod.default;
|
|
614
722
|
}
|
|
615
723
|
if (!islandHmr) {
|
|
616
|
-
|
|
724
|
+
const pagesToLoad = reloadAllPages ? nextCatalog : nextCatalog.filter((p) => pageNeedsReload(p.chunkId, affected));
|
|
725
|
+
for (const p of pagesToLoad) {
|
|
617
726
|
const pageRel = `${p.chunkId}.client.js`;
|
|
618
727
|
const href = bustUrl(pathToFileURL(path.join(distDir, pageRel)).href);
|
|
619
728
|
const mod = await import(href);
|
|
@@ -622,9 +731,21 @@ async function softReload(opts = {}) {
|
|
|
622
731
|
}
|
|
623
732
|
pageCatalog = nextCatalog;
|
|
624
733
|
if (!islandHmr) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
734
|
+
if (reloadAllPages) {
|
|
735
|
+
pageCtors.clear();
|
|
736
|
+
for (const [k, v] of nextCtors)
|
|
737
|
+
pageCtors.set(k, v);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
// Keep unaffected page constructors; only swap what we re-imported.
|
|
741
|
+
for (const [k, v] of nextCtors)
|
|
742
|
+
pageCtors.set(k, v);
|
|
743
|
+
// Drop ctors for pages that disappeared from catalog.
|
|
744
|
+
for (const id of [...pageCtors.keys()]) {
|
|
745
|
+
if (!nextCatalog.some((p) => p.chunkId === id))
|
|
746
|
+
pageCtors.delete(id);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
628
749
|
}
|
|
629
750
|
if (Object.keys(components).length) {
|
|
630
751
|
registerComponents(components);
|
|
@@ -656,7 +777,8 @@ async function softReload(opts = {}) {
|
|
|
656
777
|
}));
|
|
657
778
|
if (!opts.quiet) {
|
|
658
779
|
const aff = affected.length > 0 ? ` affected=[${affected.join(', ')}]` : full === false ? ' affected=[]' : '';
|
|
659
|
-
|
|
780
|
+
const scope = islandHmr ? 'island' : reloadAllPages ? 'all-pages' : `pages=${nextCtors.size}`;
|
|
781
|
+
console.log(`vmz serve: soft reload ok (mode=${mode}; ${scope}; catalog=${pageCatalog.length}; t=${reloadToken}${aff})`);
|
|
660
782
|
}
|
|
661
783
|
return {
|
|
662
784
|
affectedChunks: affected,
|
|
@@ -666,15 +788,49 @@ async function softReload(opts = {}) {
|
|
|
666
788
|
mode,
|
|
667
789
|
eventOnlyShell,
|
|
668
790
|
pageCount: pageCatalog.length,
|
|
791
|
+
reloadedPages: islandHmr ? 0 : nextCtors.size,
|
|
792
|
+
reloadAllPages,
|
|
669
793
|
};
|
|
670
794
|
}
|
|
671
795
|
catch (err) {
|
|
672
796
|
reloadToken = prevToken;
|
|
673
797
|
pageCatalog = prevCatalog;
|
|
798
|
+
pageCtors.clear();
|
|
799
|
+
for (const [k, v] of prevCtors)
|
|
800
|
+
pageCtors.set(k, v);
|
|
674
801
|
lastDevError = normalizeDevError(err);
|
|
675
802
|
throw err;
|
|
676
803
|
}
|
|
677
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* Shared lib / full rebuild / missing affected list → refresh every page ctor.
|
|
807
|
+
* Otherwise only re-import the dirty page chunks (Vite-like module graph).
|
|
808
|
+
* @param {{ full?: boolean, affected: string[], emitted: string[], islandHmr: boolean }} opts
|
|
809
|
+
*/
|
|
810
|
+
function shouldReloadAllPages(opts) {
|
|
811
|
+
if (opts.islandHmr)
|
|
812
|
+
return false;
|
|
813
|
+
if (opts.full)
|
|
814
|
+
return true;
|
|
815
|
+
if (!opts.affected.length)
|
|
816
|
+
return true;
|
|
817
|
+
for (const f of opts.emitted) {
|
|
818
|
+
const n = String(f).replace(/\\/g, '/');
|
|
819
|
+
if (n.includes('/lib/') || /\/Application\.client\.js$/.test(n) || /\/vmz-(dom|runtime|http|client-nav)\.js$/.test(n)) {
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
/** @param {string} chunkId @param {string[]} affected */
|
|
826
|
+
function pageNeedsReload(chunkId, affected) {
|
|
827
|
+
if (chunkId === 'pages/Layout' || chunkId.endsWith('/Layout'))
|
|
828
|
+
return true;
|
|
829
|
+
return affected.some((a) => {
|
|
830
|
+
const id = String(a);
|
|
831
|
+
return id === chunkId || chunkId.startsWith(`${id}/`) || id.startsWith(`${chunkId}/`);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
678
834
|
/** @param {string} event */
|
|
679
835
|
function notifySse(event) {
|
|
680
836
|
for (const client of [...sseClients]) {
|
|
@@ -702,23 +858,16 @@ function normalizeDevError(err) {
|
|
|
702
858
|
async function* emitDevErrorHtml(err) {
|
|
703
859
|
const msg = escapeHtml(err.message || 'Unknown error');
|
|
704
860
|
const stack = err.stack ? escapeHtml(err.stack) : '';
|
|
705
|
-
|
|
706
|
-
<html lang="en">
|
|
707
|
-
<head>
|
|
708
|
-
<meta charset="utf-8" />
|
|
709
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
710
|
-
<title>VMZ Dev Error</title>
|
|
711
|
-
<style>
|
|
861
|
+
const style = `<style>
|
|
712
862
|
body{margin:0;background:#0f1115;color:#f4f4f5;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
713
863
|
main{max-width:56rem;margin:0 auto;padding:2rem 1.25rem}
|
|
714
864
|
h1{margin:0 0 .75rem;color:#f87171;font-size:1.1rem}
|
|
715
865
|
pre{white-space:pre-wrap;margin:0 0 1rem}
|
|
716
866
|
.hint{opacity:.65;font-size:12px}
|
|
717
|
-
</style
|
|
718
|
-
|
|
719
|
-
<body>
|
|
867
|
+
</style>`;
|
|
868
|
+
const body = `${style}
|
|
720
869
|
<main>
|
|
721
|
-
<h1>
|
|
870
|
+
<h1>Dev Error</h1>
|
|
722
871
|
<pre>${msg}</pre>
|
|
723
872
|
${stack ? `<pre style="opacity:.7;font-size:12px">${stack}</pre>` : ''}
|
|
724
873
|
<p class="hint">Dev host stayed up. Fix the source and save — soft reload will recover.</p>
|
|
@@ -732,9 +881,18 @@ async function* emitDevErrorHtml(err) {
|
|
|
732
881
|
if (msg && msg.type === "hmr") location.reload();
|
|
733
882
|
};
|
|
734
883
|
})();
|
|
735
|
-
</script
|
|
736
|
-
|
|
737
|
-
|
|
884
|
+
</script>`;
|
|
885
|
+
const native = requireNativeGenerator();
|
|
886
|
+
if (typeof native.generateHtmlShell !== 'function') {
|
|
887
|
+
throw new Error('vmz native addon missing generateHtmlShell — rebuild with `pnpm napi:build`');
|
|
888
|
+
}
|
|
889
|
+
yield native.generateHtmlShell({
|
|
890
|
+
title: 'Dev Error',
|
|
891
|
+
lang: 'en',
|
|
892
|
+
cssHrefs: [],
|
|
893
|
+
bodyHtml: body,
|
|
894
|
+
bodyAttrs: [],
|
|
895
|
+
});
|
|
738
896
|
}
|
|
739
897
|
/** @param {string} s */
|
|
740
898
|
function escapeHtml(s) {
|
|
@@ -919,10 +1077,14 @@ async function listClientComponents(dir) {
|
|
|
919
1077
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
920
1078
|
}
|
|
921
1079
|
/**
|
|
922
|
-
* Discover compiled page modules
|
|
1080
|
+
* Discover compiled page modules. Prefer Route Graph `pathPattern` from
|
|
1081
|
+
* `vmz-deployment.json`; fall back to walking `pages/**` (file-route only).
|
|
923
1082
|
* @param {string} dir
|
|
924
1083
|
*/
|
|
925
1084
|
async function listPageClientFiles(dir) {
|
|
1085
|
+
const fromDep = await listPagesFromDeployment(dir);
|
|
1086
|
+
if (fromDep.length)
|
|
1087
|
+
return fromDep;
|
|
926
1088
|
const root = path.join(dir, 'pages');
|
|
927
1089
|
/** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parseChunkSegments> }>} */
|
|
928
1090
|
const out = [];
|
|
@@ -954,6 +1116,38 @@ async function listPageClientFiles(dir) {
|
|
|
954
1116
|
await walk(root, []);
|
|
955
1117
|
return out;
|
|
956
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* @param {string} dir
|
|
1121
|
+
*/
|
|
1122
|
+
async function listPagesFromDeployment(dir) {
|
|
1123
|
+
/** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parseChunkSegments> }>} */
|
|
1124
|
+
const out = [];
|
|
1125
|
+
try {
|
|
1126
|
+
const raw = await readFile(path.join(dir, 'vmz-deployment.json'), 'utf8');
|
|
1127
|
+
const dep = JSON.parse(raw);
|
|
1128
|
+
for (const unit of dep.units || []) {
|
|
1129
|
+
if (unit?.kind !== 'page')
|
|
1130
|
+
continue;
|
|
1131
|
+
const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
|
|
1132
|
+
if (!chunkId.startsWith('pages/'))
|
|
1133
|
+
continue;
|
|
1134
|
+
const stem = chunkId.split('/').pop() || '';
|
|
1135
|
+
if (isRouteBoundaryStem(stem))
|
|
1136
|
+
continue;
|
|
1137
|
+
const pageRel = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
|
|
1138
|
+
const pattern = String(unit.pathPattern || '').trim();
|
|
1139
|
+
out.push({
|
|
1140
|
+
chunkId,
|
|
1141
|
+
pageRel,
|
|
1142
|
+
segs: pattern ? parsePathPattern(pattern) : parseChunkSegments(chunkId),
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
catch {
|
|
1147
|
+
return [];
|
|
1148
|
+
}
|
|
1149
|
+
return out;
|
|
1150
|
+
}
|
|
957
1151
|
/**
|
|
958
1152
|
* File-route segments from chunk id (`pages/Install` → `/install`).
|
|
959
1153
|
* Skips URL-invisible `(group)` dirs; boundary stems never reach here.
|
|
@@ -970,17 +1164,43 @@ function parseChunkSegments(chunkId) {
|
|
|
970
1164
|
continue;
|
|
971
1165
|
if (p === 'index' && i === parts.length - 1)
|
|
972
1166
|
continue;
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1167
|
+
segs.push(parsePathSegment(p));
|
|
1168
|
+
}
|
|
1169
|
+
return segs;
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Browser HTTP pattern (`/` / `/home` / `/users/:id` / `/blog/[...slug]`).
|
|
1173
|
+
* @param {string} pattern
|
|
1174
|
+
*/
|
|
1175
|
+
function parsePathPattern(pattern) {
|
|
1176
|
+
const raw = String(pattern || '').trim();
|
|
1177
|
+
if (!raw || raw === '/')
|
|
1178
|
+
return [];
|
|
1179
|
+
const parts = raw.replace(/^\/+/, '').split('/').filter(Boolean);
|
|
1180
|
+
/** @type {Array<{ kind: 'static' | 'param' | 'catch', value?: string, name?: string }>} */
|
|
1181
|
+
const segs = [];
|
|
1182
|
+
for (const p of parts) {
|
|
1183
|
+
if (isRouteGroupDir(p))
|
|
1184
|
+
continue;
|
|
1185
|
+
segs.push(parsePathSegment(p));
|
|
981
1186
|
}
|
|
982
1187
|
return segs;
|
|
983
1188
|
}
|
|
1189
|
+
/**
|
|
1190
|
+
* @param {string} p
|
|
1191
|
+
*/
|
|
1192
|
+
function parsePathSegment(p) {
|
|
1193
|
+
const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(p);
|
|
1194
|
+
const param = /^\[([^\]]+)\]$/.exec(p);
|
|
1195
|
+
const colon = /^:([A-Za-z_][\w]*)$/.exec(p);
|
|
1196
|
+
if (catchAll)
|
|
1197
|
+
return { kind: 'catch', name: catchAll[1] };
|
|
1198
|
+
if (param)
|
|
1199
|
+
return { kind: 'param', name: param[1] };
|
|
1200
|
+
if (colon)
|
|
1201
|
+
return { kind: 'param', name: colon[1] };
|
|
1202
|
+
return { kind: 'static', value: p.toLowerCase() };
|
|
1203
|
+
}
|
|
984
1204
|
function isRouteGroupDir(seg) {
|
|
985
1205
|
return typeof seg === 'string' && seg.startsWith('(') && seg.endsWith(')') && seg.length > 2;
|
|
986
1206
|
}
|
|
@@ -1130,51 +1350,11 @@ async function runRouteGate(pathname, chunkId) {
|
|
|
1130
1350
|
*/
|
|
1131
1351
|
function emitEntryClient(eager, lazy, token) {
|
|
1132
1352
|
const q = `?t=${token}`;
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
globalThis.__vmzLoadComponent = async (name) => {
|
|
1139
|
-
const entry = __vmzComponentEntries[name] || ("components/" + name + ".client.js");
|
|
1140
|
-
const mod = await import("./" + entry + "${q}");
|
|
1141
|
-
return mod.default;
|
|
1142
|
-
};`
|
|
1143
|
-
: '';
|
|
1144
|
-
return `/**
|
|
1145
|
-
* Generated by vmz serve — hydrate matched file-route page (data-vmz-page) + layout chain + client Link takeover.
|
|
1146
|
-
*/
|
|
1147
|
-
import { registerComponents, hydrate, hydrateRoute, hydrateRoutePage, destroy } from ${JSON.stringify(`./vmz-dom.js${q}`)};
|
|
1148
|
-
import { installClientNavigation } from ${JSON.stringify(`./vmz-client-nav.js${q}`)};
|
|
1149
|
-
${imports}
|
|
1150
|
-
|
|
1151
|
-
${map}
|
|
1152
|
-
${loader}
|
|
1153
|
-
|
|
1154
|
-
const root = document.getElementById("app");
|
|
1155
|
-
if (!root) throw new Error("vmz: missing #app");
|
|
1156
|
-
const chunkId = root.getAttribute("data-vmz-page");
|
|
1157
|
-
if (!chunkId) throw new Error("vmz: missing data-vmz-page");
|
|
1158
|
-
let props = {};
|
|
1159
|
-
try {
|
|
1160
|
-
const raw = root.getAttribute("data-vmz-props");
|
|
1161
|
-
if (raw) props = JSON.parse(raw);
|
|
1162
|
-
} catch { /* ignore */ }
|
|
1163
|
-
const layoutChain = (root.getAttribute("data-vmz-layout") || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1164
|
-
const layoutCtors = [];
|
|
1165
|
-
for (const id of layoutChain) {
|
|
1166
|
-
layoutCtors.push((await import("./" + id + ".client.js${q}")).default);
|
|
1167
|
-
}
|
|
1168
|
-
const Page = (await import("./" + chunkId + ".client.js${q}")).default;
|
|
1169
|
-
await hydrateRoute(Page, root, props, layoutCtors);
|
|
1170
|
-
installClientNavigation({
|
|
1171
|
-
hydrate,
|
|
1172
|
-
hydrateRoute,
|
|
1173
|
-
hydrateRoutePage,
|
|
1174
|
-
destroy,
|
|
1175
|
-
importPage: async (id) => (await import("./" + id + ".client.js${q}")).default,
|
|
1176
|
-
});
|
|
1177
|
-
`;
|
|
1353
|
+
const native = requireNativeGenerator();
|
|
1354
|
+
if (typeof native.generateServeEntryClient !== 'function') {
|
|
1355
|
+
throw new Error('vmz native addon missing generateServeEntryClient — rebuild with `pnpm napi:build`');
|
|
1356
|
+
}
|
|
1357
|
+
return native.generateServeEntryClient(eager, lazy, q);
|
|
1178
1358
|
}
|
|
1179
1359
|
/**
|
|
1180
1360
|
* EventEntry zero-framework bootstrap: no static import of vmz-dom / page / islands.
|
|
@@ -1183,35 +1363,80 @@ installClientNavigation({
|
|
|
1183
1363
|
*/
|
|
1184
1364
|
function emitEntryEvent(token) {
|
|
1185
1365
|
const q = `?t=${token}`;
|
|
1186
|
-
|
|
1187
|
-
|
|
1366
|
+
const native = requireNativeGenerator();
|
|
1367
|
+
if (typeof native.generateServeEntryEvent !== 'function') {
|
|
1368
|
+
throw new Error('vmz native addon missing generateServeEntryEvent — rebuild with `pnpm napi:build`');
|
|
1369
|
+
}
|
|
1370
|
+
return native.generateServeEntryEvent(q);
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Load vmz N-API CodeGenerators (same discovery as `@vmz/vmz` native-addon).
|
|
1374
|
+
* @returns {any}
|
|
1188
1375
|
*/
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1376
|
+
function requireNativeGenerator() {
|
|
1377
|
+
if (_nativeGen !== undefined) {
|
|
1378
|
+
if (!_nativeGen) {
|
|
1379
|
+
throw new Error('vmz native addon missing — run `pnpm napi:build` (serve entry printers live in vmz-generator via N-API)');
|
|
1380
|
+
}
|
|
1381
|
+
return _nativeGen;
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
const envPath = (typeof process.env.VMZ_NATIVE_NODE === 'string' && process.env.VMZ_NATIVE_NODE.trim()) || '';
|
|
1385
|
+
if (envPath) {
|
|
1386
|
+
_nativeGen = require(path.resolve(envPath));
|
|
1387
|
+
return _nativeGen;
|
|
1388
|
+
}
|
|
1389
|
+
const { platform, arch } = process;
|
|
1390
|
+
let triple = `${platform}-${arch}`;
|
|
1391
|
+
if (platform === 'win32' && arch === 'x64')
|
|
1392
|
+
triple = 'win32-x64-msvc';
|
|
1393
|
+
else if (platform === 'win32' && arch === 'arm64')
|
|
1394
|
+
triple = 'win32-arm64-msvc';
|
|
1395
|
+
else if (platform === 'darwin' && arch === 'arm64')
|
|
1396
|
+
triple = 'darwin-arm64';
|
|
1397
|
+
else if (platform === 'darwin' && arch === 'x64')
|
|
1398
|
+
triple = 'darwin-x64';
|
|
1399
|
+
else if (platform === 'linux' && arch === 'x64')
|
|
1400
|
+
triple = 'linux-x64-gnu';
|
|
1401
|
+
else if (platform === 'linux' && arch === 'arm64')
|
|
1402
|
+
triple = 'linux-arm64-gnu';
|
|
1403
|
+
const short = triple === 'win32-x64-msvc'
|
|
1404
|
+
? 'win32-x64'
|
|
1405
|
+
: triple === 'win32-arm64-msvc'
|
|
1406
|
+
? 'win32-arm64'
|
|
1407
|
+
: triple === 'linux-x64-gnu'
|
|
1408
|
+
? 'linux-x64'
|
|
1409
|
+
: triple === 'linux-arm64-gnu'
|
|
1410
|
+
? 'linux-arm64'
|
|
1411
|
+
: triple;
|
|
1412
|
+
const name = `@vmz/vmz-${short}`;
|
|
1413
|
+
/** @type {string[]} */
|
|
1414
|
+
const candidates = [];
|
|
1415
|
+
try {
|
|
1416
|
+
const resolved = require.resolve(`${name}/package.json`);
|
|
1417
|
+
const dir = path.dirname(resolved);
|
|
1418
|
+
candidates.push(path.join(dir, `vmz.${triple}.node`), path.join(dir, 'vmz.node'));
|
|
1419
|
+
}
|
|
1420
|
+
catch {
|
|
1421
|
+
/* optional */
|
|
1422
|
+
}
|
|
1423
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
1424
|
+
candidates.push(path.join(here, 'node_modules', name, `vmz.${triple}.node`), path.join(here, 'node_modules', name, 'vmz.node'), path.join(here, '..', 'node_modules', name, `vmz.${triple}.node`), path.join(here, '..', 'node_modules', name, 'vmz.node'));
|
|
1425
|
+
for (const p of candidates) {
|
|
1426
|
+
if (existsSync(p)) {
|
|
1427
|
+
_nativeGen = require(p);
|
|
1428
|
+
return _nativeGen;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
_nativeGen = null;
|
|
1432
|
+
}
|
|
1433
|
+
catch {
|
|
1434
|
+
_nativeGen = null;
|
|
1435
|
+
}
|
|
1436
|
+
if (!_nativeGen) {
|
|
1437
|
+
throw new Error('vmz native addon missing — run `pnpm napi:build` (serve entry printers live in vmz-generator via N-API)');
|
|
1438
|
+
}
|
|
1439
|
+
return _nativeGen;
|
|
1215
1440
|
}
|
|
1216
1441
|
/**
|
|
1217
1442
|
* Style Theme cookie / localStorage key (host contract, not a second theme API).
|
|
@@ -1269,14 +1494,15 @@ function resolveThemeId(searchParams, cookieHeader) {
|
|
|
1269
1494
|
/**
|
|
1270
1495
|
* Always emit activation attr for an explicit theme id (incl. default) so it overrides OS media.
|
|
1271
1496
|
* @param {string|null} themeId
|
|
1497
|
+
* @returns {[string, string] | []} flattened attr pair for generatePageShell
|
|
1272
1498
|
*/
|
|
1273
|
-
function
|
|
1499
|
+
function htmlThemeAttrPair(themeId) {
|
|
1274
1500
|
if (!styleTheme || !themeId)
|
|
1275
|
-
return
|
|
1501
|
+
return [];
|
|
1276
1502
|
const attr = styleTheme.activationAttr || 'data-theme';
|
|
1277
1503
|
if (!(styleTheme.themeIds || []).includes(themeId))
|
|
1278
|
-
return
|
|
1279
|
-
return
|
|
1504
|
+
return [];
|
|
1505
|
+
return [attr, themeId];
|
|
1280
1506
|
}
|
|
1281
1507
|
/**
|
|
1282
1508
|
* Inline boot when SSR had no query/cookie: apply explicit `localStorage` only.
|
package/dist/server.js
CHANGED
|
@@ -75,7 +75,21 @@ async function callServerLocal(moduleId, method, args) {
|
|
|
75
75
|
* @param {unknown[]} args
|
|
76
76
|
*/
|
|
77
77
|
async function callServerHttp(moduleId, method, args) {
|
|
78
|
-
|
|
78
|
+
let rpcPath = (typeof globalThis !== 'undefined' && globalThis.__VMZ_RPC_PATH) || DEFAULT_RPC_PATH;
|
|
79
|
+
// Node undici `fetch` rejects relative URLs; browsers accept path-only.
|
|
80
|
+
if (typeof rpcPath === 'string' && !/^https?:\/\//i.test(rpcPath)) {
|
|
81
|
+
const origin = (typeof globalThis !== 'undefined' && globalThis.__VMZ_RPC_ORIGIN) ||
|
|
82
|
+
(typeof window !== 'undefined' && window.location && window.location.origin) ||
|
|
83
|
+
null;
|
|
84
|
+
if (origin) {
|
|
85
|
+
rpcPath = new URL(rpcPath, origin).href;
|
|
86
|
+
}
|
|
87
|
+
else if (typeof window === 'undefined') {
|
|
88
|
+
const host = (typeof process !== 'undefined' && (process.env.VMZ_HOST || process.env.HOST)) || '127.0.0.1';
|
|
89
|
+
const port = (typeof process !== 'undefined' && (process.env.VMZ_PORT || process.env.PORT)) || '5173';
|
|
90
|
+
rpcPath = new URL(rpcPath, `http://${host}:${port}`).href;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
79
93
|
const res = await fetch(rpcPath, {
|
|
80
94
|
method: 'POST',
|
|
81
95
|
headers: { 'content-type': 'application/json' },
|