@vmz/core 0.0.4 → 0.1.1
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.d.ts +20 -2
- package/dist/client-nav.js +556 -50
- package/dist/dom-core.d.ts +337 -0
- package/dist/dom-core.js +4545 -0
- package/dist/dom-ssr.d.ts +78 -0
- package/dist/dom-ssr.js +970 -0
- package/dist/dom.client.d.ts +4 -0
- package/dist/dom.client.js +5 -0
- package/dist/dom.d.ts +3 -219
- package/dist/dom.js +3 -4377
- package/dist/serve-host.d.ts +17 -0
- package/dist/serve-host.mjs +550 -156
- package/dist/server.d.ts +8 -0
- package/dist/server.js +233 -11
- package/dist/vmz-dom.d.ts +5 -0
- package/dist/vmz-dom.js +6 -0
- package/dist/vmz-runtime.d.ts +5 -0
- package/dist/vmz-runtime.js +6 -0
- package/package.json +8 -1
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
|
*/
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
15
21
|
import http from 'node:http';
|
|
22
|
+
import { createRequire, registerHooks } from 'node:module';
|
|
16
23
|
import path from 'node:path';
|
|
17
|
-
import { readdir, writeFile, readFile } from 'node:fs/promises';
|
|
18
|
-
import { existsSync } from 'node:fs';
|
|
19
24
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
|
-
import { setServerModuleResolver, setRoutes, handleNodeRequest } from './vmz-runtime.js';
|
|
21
25
|
import { registerComponents, renderToStream, renderToString } from './vmz-dom.js';
|
|
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> }>} */
|
|
@@ -33,6 +83,8 @@ const pageCtors = new Map();
|
|
|
33
83
|
let cssEntry = null;
|
|
34
84
|
/** @type {{ defaultThemeId: string, themeIds: string[], activationAttr: string, contentHash: string|null } | null} */
|
|
35
85
|
let styleTheme = null;
|
|
86
|
+
/** Locale route realization artifact from `_vmz/locale-route-realization.json` (optional). */
|
|
87
|
+
let localeArtifact = null;
|
|
36
88
|
/** @type {Set<import('node:http').ServerResponse>} */
|
|
37
89
|
const sseClients = new Set();
|
|
38
90
|
/** In-flight HTTP requests (graceful shutdown drain). */
|
|
@@ -42,6 +94,12 @@ let shuttingDown = false;
|
|
|
42
94
|
let ready = false;
|
|
43
95
|
/** @type {{ message: string, stack?: string, at: number } | null} */
|
|
44
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;
|
|
45
103
|
setServerModuleResolver((moduleId) => {
|
|
46
104
|
const rel = moduleId.replace(/^#server\//, '') + '.js';
|
|
47
105
|
return bustUrl(pathToFileURL(path.join(distDir, '#server', rel)).href);
|
|
@@ -94,9 +152,14 @@ async function renderPageStream(pathname, opts = {}) {
|
|
|
94
152
|
if (isDev && lastDevError && pageCtors.size === 0) {
|
|
95
153
|
return { status: 500, stream: emitDevErrorHtml(lastDevError) };
|
|
96
154
|
}
|
|
97
|
-
|
|
155
|
+
const localePlan = resolveLocalePath(pathname);
|
|
156
|
+
if (localePlan.redirectTo) {
|
|
157
|
+
return { status: 302, redirect: localePlan.redirectTo, headers: { Location: localePlan.redirectTo } };
|
|
158
|
+
}
|
|
159
|
+
const routePath = localePlan.restPath || pathname;
|
|
160
|
+
let match = matchFileRoute(routePath, pageCatalog);
|
|
98
161
|
let status = 200;
|
|
99
|
-
const gated = await runRouteGate(
|
|
162
|
+
const gated = await runRouteGate(routePath, match?.chunkId);
|
|
100
163
|
if (gated === 'not_found') {
|
|
101
164
|
match = findRootCatchAll(pageCatalog);
|
|
102
165
|
status = 404;
|
|
@@ -121,16 +184,24 @@ async function renderPageStream(pathname, opts = {}) {
|
|
|
121
184
|
}
|
|
122
185
|
return null;
|
|
123
186
|
}
|
|
124
|
-
const params = extractRouteParams(match.segs,
|
|
187
|
+
const params = extractRouteParams(match.segs, routePath);
|
|
125
188
|
const method = String(opts.method || 'GET').toUpperCase();
|
|
189
|
+
const localeCtx = {
|
|
190
|
+
localeId: localePlan.localeId,
|
|
191
|
+
dir: localePlan.dir,
|
|
192
|
+
pathname,
|
|
193
|
+
routePath,
|
|
194
|
+
alternates: pageMetaAlternates(match.chunkId, localePlan.localeId),
|
|
195
|
+
};
|
|
126
196
|
if (typeof Page.access === 'function') {
|
|
127
197
|
const access = await Page.access({
|
|
128
198
|
params,
|
|
129
|
-
pathname,
|
|
199
|
+
pathname: routePath,
|
|
130
200
|
chunkId: match.chunkId,
|
|
131
201
|
signal: opts.signal,
|
|
132
202
|
searchParams: opts.searchParams,
|
|
133
203
|
method,
|
|
204
|
+
localeId: localeCtx.localeId,
|
|
134
205
|
});
|
|
135
206
|
const closed = normalizeAccessResult(access);
|
|
136
207
|
if (closed.kind === 'redirect') {
|
|
@@ -148,7 +219,7 @@ async function renderPageStream(pathname, opts = {}) {
|
|
|
148
219
|
const eventOnlyShell = isEventOnlyShell(resumeEntries.map((e) => e.strategy));
|
|
149
220
|
return {
|
|
150
221
|
status: 404,
|
|
151
|
-
stream: emitPageHtml(NotFound, catchAll.chunkId, eventOnlyShell, { ...params }, opts),
|
|
222
|
+
stream: emitPageHtml(NotFound, catchAll.chunkId, eventOnlyShell, { ...params }, opts, [], localeCtx),
|
|
152
223
|
};
|
|
153
224
|
}
|
|
154
225
|
}
|
|
@@ -159,12 +230,13 @@ async function renderPageStream(pathname, opts = {}) {
|
|
|
159
230
|
if (method === 'POST' && typeof Page.action === 'function') {
|
|
160
231
|
const acted = await Page.action({
|
|
161
232
|
params,
|
|
162
|
-
pathname,
|
|
233
|
+
pathname: routePath,
|
|
163
234
|
chunkId: match.chunkId,
|
|
164
235
|
signal: opts.signal,
|
|
165
236
|
searchParams: opts.searchParams,
|
|
166
237
|
body: opts.body,
|
|
167
238
|
method,
|
|
239
|
+
localeId: localeCtx.localeId,
|
|
168
240
|
});
|
|
169
241
|
const actionClosed = normalizeActionResult(acted);
|
|
170
242
|
if (actionClosed.kind === 'redirect') {
|
|
@@ -183,10 +255,11 @@ async function renderPageStream(pathname, opts = {}) {
|
|
|
183
255
|
if (typeof Page.load === 'function') {
|
|
184
256
|
const loaded = await Page.load({
|
|
185
257
|
params,
|
|
186
|
-
pathname,
|
|
258
|
+
pathname: routePath,
|
|
187
259
|
chunkId: match.chunkId,
|
|
188
260
|
signal: opts.signal,
|
|
189
261
|
searchParams: opts.searchParams,
|
|
262
|
+
localeId: localeCtx.localeId,
|
|
190
263
|
});
|
|
191
264
|
if (opts.signal?.aborted) {
|
|
192
265
|
return { status: 499, stream: emitAccessShell('route-nav-cancelled') };
|
|
@@ -204,7 +277,7 @@ async function renderPageStream(pathname, opts = {}) {
|
|
|
204
277
|
const layoutChain = resolveLayoutChain(match.chunkId);
|
|
205
278
|
return {
|
|
206
279
|
status,
|
|
207
|
-
stream: emitPageHtml(Page, match.chunkId, eventOnlyShell, props, opts, layoutChain),
|
|
280
|
+
stream: emitPageHtml(Page, match.chunkId, eventOnlyShell, props, opts, layoutChain, localeCtx),
|
|
208
281
|
};
|
|
209
282
|
}
|
|
210
283
|
/**
|
|
@@ -260,11 +333,36 @@ function normalizeActionResult(acted) {
|
|
|
260
333
|
* @param {string} marker
|
|
261
334
|
*/
|
|
262
335
|
async function* emitAccessShell(marker) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
}
|
|
268
366
|
}
|
|
269
367
|
/**
|
|
270
368
|
* @param {any} Page
|
|
@@ -274,12 +372,18 @@ async function* emitAccessShell(marker) {
|
|
|
274
372
|
* @param {{ signal?: AbortSignal, searchParams?: URLSearchParams, cookieHeader?: string }} [opts]
|
|
275
373
|
* @param {string[]} [layoutChain] layout chunk ids outer→inner
|
|
276
374
|
*/
|
|
277
|
-
async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {}, layoutChain = []) {
|
|
375
|
+
async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {}, layoutChain = [], localeCtx = {}) {
|
|
278
376
|
const signal = opts.signal;
|
|
279
377
|
const live = isDev
|
|
280
378
|
? `\n <script>
|
|
281
379
|
(() => {
|
|
282
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
|
+
};
|
|
283
387
|
function showOverlay(err) {
|
|
284
388
|
let el = document.getElementById("vmz-dev-overlay");
|
|
285
389
|
if (!el) {
|
|
@@ -298,7 +402,7 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
298
402
|
const stack = (err && err.stack) || "";
|
|
299
403
|
const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({"&":"&","<":"<",">":">"}[c]));
|
|
300
404
|
el.innerHTML = "<div style=\\"max-width:56rem;margin:0 auto\\">"
|
|
301
|
-
+ "<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>"
|
|
302
406
|
+ "<pre style=\\"white-space:pre-wrap;margin:0 0 1rem;font-size:13px;line-height:1.45\\">" + esc(msg) + "</pre>"
|
|
303
407
|
+ (stack ? "<pre style=\\"white-space:pre-wrap;opacity:.7;font-size:12px\\">" + esc(stack) + "</pre>" : "")
|
|
304
408
|
+ "<p style=\\"opacity:.65;font-size:12px\\">Fix the file and save — soft reload will clear this overlay.</p>"
|
|
@@ -370,50 +474,88 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
|
|
|
370
474
|
`/* paint immediately */` +
|
|
371
475
|
`var d=document.createElement("div");d.id="vmz-dev-overlay";d.setAttribute("role","alert");` +
|
|
372
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"});` +
|
|
373
|
-
`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>";` +
|
|
374
478
|
`document.documentElement.appendChild(d);})();</script>`
|
|
375
479
|
: '';
|
|
376
480
|
if (signal?.aborted)
|
|
377
481
|
return;
|
|
378
482
|
const themeId = resolveThemeId(opts.searchParams, opts.cookieHeader);
|
|
379
|
-
const htmlTheme = htmlThemeAttributeForId(themeId);
|
|
380
483
|
const themeBoot = themeBootstrapScript();
|
|
381
|
-
const cssLink = cssEntry ? ` <link rel="stylesheet" href="/${String(cssEntry).replace(/^\/+/, '')}?t=${reloadToken}" />\n` : '';
|
|
382
484
|
const propsJson = JSON.stringify(props ?? {});
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
485
|
+
const localeId = localeCtx.localeId || localeArtifact?.defaultLocale || 'en';
|
|
486
|
+
const dir = localeCtx.dir || 'ltr';
|
|
487
|
+
/** @type {string[]} */
|
|
488
|
+
const htmlExtraAttrs = [...htmlThemeAttrPair(themeId)];
|
|
489
|
+
if (localeArtifact?.routing) {
|
|
490
|
+
htmlExtraAttrs.push('data-vmz-locale-routing', JSON.stringify({
|
|
491
|
+
strategy: localeArtifact.routing.strategy || 'prefix',
|
|
492
|
+
defaultPrefix: localeArtifact.routing.defaultPrefix || 'include',
|
|
493
|
+
defaultLocale: localeArtifact.defaultLocale,
|
|
494
|
+
locales: (localeArtifact.locales || []).map((l) => l.id),
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
const pageDocMeta = resolvePageDocumentMeta(Page);
|
|
498
|
+
const prevLocaleHint = globalThis.__vmzLocaleIdHint;
|
|
499
|
+
globalThis.__vmzLocaleIdHint = localeId;
|
|
393
500
|
let bodyHtml = '';
|
|
394
|
-
|
|
501
|
+
try {
|
|
502
|
+
for await (const chunk of renderToStream(Page, props, { signal })) {
|
|
503
|
+
if (signal?.aborted)
|
|
504
|
+
return;
|
|
505
|
+
bodyHtml += chunk;
|
|
506
|
+
}
|
|
395
507
|
if (signal?.aborted)
|
|
396
508
|
return;
|
|
397
|
-
|
|
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
|
+
}
|
|
398
522
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (!Layout)
|
|
405
|
-
continue;
|
|
406
|
-
bodyHtml = await renderToString(Layout, {}, { signal, slotHtml: bodyHtml });
|
|
407
|
-
if (signal?.aborted)
|
|
408
|
-
return;
|
|
523
|
+
finally {
|
|
524
|
+
if (prevLocaleHint === undefined)
|
|
525
|
+
delete globalThis.__vmzLocaleIdHint;
|
|
526
|
+
else
|
|
527
|
+
globalThis.__vmzLocaleIdHint = prevLocaleHint;
|
|
409
528
|
}
|
|
410
|
-
yield bodyHtml;
|
|
411
529
|
if (signal?.aborted)
|
|
412
530
|
return;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
+
});
|
|
417
559
|
}
|
|
418
560
|
const server = http.createServer((req, res) => {
|
|
419
561
|
const url = new URL(req.url || '/', `http://${host}:${port}`);
|
|
@@ -528,17 +670,20 @@ process.on('SIGINT', () => {
|
|
|
528
670
|
* Re-import routes / pages / components with a new cache-bust token.
|
|
529
671
|
* Keeps the HTTP server process alive (no Node restart).
|
|
530
672
|
* Failed reloads keep the previous in-memory modules (Vite-like resilience).
|
|
531
|
-
* @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]
|
|
532
674
|
*/
|
|
533
675
|
async function softReload(opts = {}) {
|
|
534
676
|
const prevToken = reloadToken;
|
|
535
677
|
const prevCatalog = pageCatalog;
|
|
678
|
+
const prevCtors = new Map(pageCtors);
|
|
536
679
|
const nextToken = Date.now();
|
|
537
680
|
reloadToken = nextToken;
|
|
538
681
|
const affected = opts.payload?.affectedChunks ?? [];
|
|
539
682
|
const seeds = opts.payload?.seedChunks ?? [];
|
|
683
|
+
const emitted = opts.payload?.emitted ?? [];
|
|
540
684
|
const full = opts.payload?.full;
|
|
541
685
|
const islandHmr = Boolean(opts.payload?.islandHmr);
|
|
686
|
+
const reloadAllPages = shouldReloadAllPages({ full, affected, emitted, islandHmr });
|
|
542
687
|
try {
|
|
543
688
|
try {
|
|
544
689
|
const routes = JSON.parse(await readFile(path.join(distDir, 'vmz-routes.json'), 'utf8'));
|
|
@@ -547,6 +692,12 @@ async function softReload(opts = {}) {
|
|
|
547
692
|
catch {
|
|
548
693
|
setRoutes([]);
|
|
549
694
|
}
|
|
695
|
+
try {
|
|
696
|
+
localeArtifact = JSON.parse(await readFile(path.join(distDir, '_vmz', 'locale-route-realization.json'), 'utf8'));
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
localeArtifact = null;
|
|
700
|
+
}
|
|
550
701
|
const componentEntries = await listClientComponents(distDir);
|
|
551
702
|
const nextCatalog = await listPageClientFiles(distDir);
|
|
552
703
|
if (!nextCatalog.length) {
|
|
@@ -570,7 +721,8 @@ async function softReload(opts = {}) {
|
|
|
570
721
|
components[entry.name] = mod.default;
|
|
571
722
|
}
|
|
572
723
|
if (!islandHmr) {
|
|
573
|
-
|
|
724
|
+
const pagesToLoad = reloadAllPages ? nextCatalog : nextCatalog.filter((p) => pageNeedsReload(p.chunkId, affected));
|
|
725
|
+
for (const p of pagesToLoad) {
|
|
574
726
|
const pageRel = `${p.chunkId}.client.js`;
|
|
575
727
|
const href = bustUrl(pathToFileURL(path.join(distDir, pageRel)).href);
|
|
576
728
|
const mod = await import(href);
|
|
@@ -579,9 +731,21 @@ async function softReload(opts = {}) {
|
|
|
579
731
|
}
|
|
580
732
|
pageCatalog = nextCatalog;
|
|
581
733
|
if (!islandHmr) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
+
}
|
|
585
749
|
}
|
|
586
750
|
if (Object.keys(components).length) {
|
|
587
751
|
registerComponents(components);
|
|
@@ -613,7 +777,8 @@ async function softReload(opts = {}) {
|
|
|
613
777
|
}));
|
|
614
778
|
if (!opts.quiet) {
|
|
615
779
|
const aff = affected.length > 0 ? ` affected=[${affected.join(', ')}]` : full === false ? ' affected=[]' : '';
|
|
616
|
-
|
|
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})`);
|
|
617
782
|
}
|
|
618
783
|
return {
|
|
619
784
|
affectedChunks: affected,
|
|
@@ -623,15 +788,49 @@ async function softReload(opts = {}) {
|
|
|
623
788
|
mode,
|
|
624
789
|
eventOnlyShell,
|
|
625
790
|
pageCount: pageCatalog.length,
|
|
791
|
+
reloadedPages: islandHmr ? 0 : nextCtors.size,
|
|
792
|
+
reloadAllPages,
|
|
626
793
|
};
|
|
627
794
|
}
|
|
628
795
|
catch (err) {
|
|
629
796
|
reloadToken = prevToken;
|
|
630
797
|
pageCatalog = prevCatalog;
|
|
798
|
+
pageCtors.clear();
|
|
799
|
+
for (const [k, v] of prevCtors)
|
|
800
|
+
pageCtors.set(k, v);
|
|
631
801
|
lastDevError = normalizeDevError(err);
|
|
632
802
|
throw err;
|
|
633
803
|
}
|
|
634
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
|
+
}
|
|
635
834
|
/** @param {string} event */
|
|
636
835
|
function notifySse(event) {
|
|
637
836
|
for (const client of [...sseClients]) {
|
|
@@ -659,23 +858,16 @@ function normalizeDevError(err) {
|
|
|
659
858
|
async function* emitDevErrorHtml(err) {
|
|
660
859
|
const msg = escapeHtml(err.message || 'Unknown error');
|
|
661
860
|
const stack = err.stack ? escapeHtml(err.stack) : '';
|
|
662
|
-
|
|
663
|
-
<html lang="en">
|
|
664
|
-
<head>
|
|
665
|
-
<meta charset="utf-8" />
|
|
666
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
667
|
-
<title>VMZ Dev Error</title>
|
|
668
|
-
<style>
|
|
861
|
+
const style = `<style>
|
|
669
862
|
body{margin:0;background:#0f1115;color:#f4f4f5;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
670
863
|
main{max-width:56rem;margin:0 auto;padding:2rem 1.25rem}
|
|
671
864
|
h1{margin:0 0 .75rem;color:#f87171;font-size:1.1rem}
|
|
672
865
|
pre{white-space:pre-wrap;margin:0 0 1rem}
|
|
673
866
|
.hint{opacity:.65;font-size:12px}
|
|
674
|
-
</style
|
|
675
|
-
|
|
676
|
-
<body>
|
|
867
|
+
</style>`;
|
|
868
|
+
const body = `${style}
|
|
677
869
|
<main>
|
|
678
|
-
<h1>
|
|
870
|
+
<h1>Dev Error</h1>
|
|
679
871
|
<pre>${msg}</pre>
|
|
680
872
|
${stack ? `<pre style="opacity:.7;font-size:12px">${stack}</pre>` : ''}
|
|
681
873
|
<p class="hint">Dev host stayed up. Fix the source and save — soft reload will recover.</p>
|
|
@@ -689,14 +881,147 @@ async function* emitDevErrorHtml(err) {
|
|
|
689
881
|
if (msg && msg.type === "hmr") location.reload();
|
|
690
882
|
};
|
|
691
883
|
})();
|
|
692
|
-
</script
|
|
693
|
-
|
|
694
|
-
|
|
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
|
+
});
|
|
695
896
|
}
|
|
696
897
|
/** @param {string} s */
|
|
697
898
|
function escapeHtml(s) {
|
|
698
899
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
699
900
|
}
|
|
901
|
+
/**
|
|
902
|
+
* Resolve LocaleId from pathname using `_vmz/locale-route-realization.json`.
|
|
903
|
+
* LocaleId is a realization dimension — matching still uses stable route path.
|
|
904
|
+
* @param {string} pathname
|
|
905
|
+
*/
|
|
906
|
+
function resolveLocalePath(pathname) {
|
|
907
|
+
const raw = String(pathname || '/');
|
|
908
|
+
const normalized = raw.length > 1 && raw.endsWith('/') ? raw.slice(0, -1) : raw || '/';
|
|
909
|
+
if (!localeArtifact) {
|
|
910
|
+
return { localeId: 'en', dir: 'ltr', restPath: normalized, redirectTo: null };
|
|
911
|
+
}
|
|
912
|
+
const supported = (localeArtifact.locales || []).map((l) => l.id);
|
|
913
|
+
const defaultLocale = localeArtifact.defaultLocale || supported[0] || 'en';
|
|
914
|
+
const directions = Object.fromEntries((localeArtifact.locales || []).map((l) => [l.id, l.direction || 'ltr']));
|
|
915
|
+
const routing = localeArtifact.routing || {};
|
|
916
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
917
|
+
let localeId = null;
|
|
918
|
+
let restPath = normalized;
|
|
919
|
+
if (parts.length && supported.includes(parts[0])) {
|
|
920
|
+
localeId = parts[0];
|
|
921
|
+
const rest = parts.slice(1);
|
|
922
|
+
restPath = rest.length ? `/${rest.join('/')}` : '/';
|
|
923
|
+
}
|
|
924
|
+
// omit defaultPrefix: prefixed defaultLocale URL redirects to unprefixed canonical.
|
|
925
|
+
if (routing.defaultPrefix === 'omit' && localeId === defaultLocale) {
|
|
926
|
+
return {
|
|
927
|
+
localeId: defaultLocale,
|
|
928
|
+
dir: directions[defaultLocale] || 'ltr',
|
|
929
|
+
restPath,
|
|
930
|
+
redirectTo: restPath,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
const contentLocale = localeId || defaultLocale;
|
|
934
|
+
return {
|
|
935
|
+
localeId: contentLocale,
|
|
936
|
+
dir: directions[contentLocale] || 'ltr',
|
|
937
|
+
restPath,
|
|
938
|
+
redirectTo: null,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Realize href for current LocaleId (prefix strategy). Kept local so serve-host
|
|
943
|
+
* stays free of CLI package imports.
|
|
944
|
+
* @param {string} href
|
|
945
|
+
* @param {string} localeId
|
|
946
|
+
* @param {any} artifact
|
|
947
|
+
*/
|
|
948
|
+
function localizeSameAppHrefHost(href, localeId, artifact) {
|
|
949
|
+
if (!href || !localeId || !artifact)
|
|
950
|
+
return href;
|
|
951
|
+
if (href.startsWith('#') || /^(mailto|tel|javascript):/i.test(href))
|
|
952
|
+
return href;
|
|
953
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href) && !href.startsWith('/'))
|
|
954
|
+
return href;
|
|
955
|
+
let pathname = String(href);
|
|
956
|
+
let search = '';
|
|
957
|
+
let hash = '';
|
|
958
|
+
const hashIdx = pathname.indexOf('#');
|
|
959
|
+
if (hashIdx >= 0) {
|
|
960
|
+
hash = pathname.slice(hashIdx);
|
|
961
|
+
pathname = pathname.slice(0, hashIdx);
|
|
962
|
+
}
|
|
963
|
+
const qIdx = pathname.indexOf('?');
|
|
964
|
+
if (qIdx >= 0) {
|
|
965
|
+
search = pathname.slice(qIdx);
|
|
966
|
+
pathname = pathname.slice(0, qIdx);
|
|
967
|
+
}
|
|
968
|
+
if (!pathname)
|
|
969
|
+
pathname = '/';
|
|
970
|
+
const supported = (artifact.locales || []).map((l) => l.id).filter(Boolean);
|
|
971
|
+
const defaultLocale = artifact.defaultLocale || artifact.routing?.defaultLocale;
|
|
972
|
+
const routing = artifact.routing || {};
|
|
973
|
+
const strategy = routing.strategy || 'prefix';
|
|
974
|
+
const defaultPrefix = routing.defaultPrefix || 'include';
|
|
975
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
976
|
+
let rest = pathname;
|
|
977
|
+
if (parts.length && supported.includes(parts[0])) {
|
|
978
|
+
const r = parts.slice(1);
|
|
979
|
+
rest = r.length ? `/${r.join('/')}` : '/';
|
|
980
|
+
}
|
|
981
|
+
if (rest.length > 1 && rest.endsWith('/'))
|
|
982
|
+
rest = rest.slice(0, -1);
|
|
983
|
+
if (!rest.startsWith('/'))
|
|
984
|
+
rest = `/${rest}`;
|
|
985
|
+
if (strategy === 'none' || strategy === 'domain')
|
|
986
|
+
return `${rest}${search}${hash}`;
|
|
987
|
+
const omitDefault = defaultPrefix === 'omit' && localeId === defaultLocale;
|
|
988
|
+
if (omitDefault)
|
|
989
|
+
return `${rest}${search}${hash}`;
|
|
990
|
+
const pathOut = rest === '/' ? `/${localeId}` : `/${localeId}${rest}`;
|
|
991
|
+
return `${pathOut}${search}${hash}`;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* @param {string} html
|
|
995
|
+
* @param {string} localeId
|
|
996
|
+
* @param {any} artifact
|
|
997
|
+
*/
|
|
998
|
+
function localizeBodyLinksInHost(html, localeId, artifact) {
|
|
999
|
+
if (!html || !localeId || !artifact)
|
|
1000
|
+
return html;
|
|
1001
|
+
return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
|
|
1002
|
+
if (!/\bdata-vmz-route\s*=/.test(attrs))
|
|
1003
|
+
return full;
|
|
1004
|
+
const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
|
|
1005
|
+
if (!hm)
|
|
1006
|
+
return full;
|
|
1007
|
+
const next = localizeSameAppHrefHost(hm[1], localeId, artifact);
|
|
1008
|
+
if (next === hm[1])
|
|
1009
|
+
return full;
|
|
1010
|
+
const newAttrs = attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(next)}"`);
|
|
1011
|
+
return `<a${newAttrs}>`;
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* @param {string} chunkId
|
|
1016
|
+
* @param {string} localeId
|
|
1017
|
+
*/
|
|
1018
|
+
function pageMetaAlternates(chunkId, localeId) {
|
|
1019
|
+
if (!localeArtifact?.pageMetas)
|
|
1020
|
+
return [];
|
|
1021
|
+
const meta = localeArtifact.pageMetas.find((m) => m.routeId === chunkId && m.locale === localeId) ||
|
|
1022
|
+
localeArtifact.pageMetas.find((m) => m.routeId === chunkId && m.locale === localeArtifact.defaultLocale);
|
|
1023
|
+
return Array.isArray(meta?.alternates) ? meta.alternates : [];
|
|
1024
|
+
}
|
|
700
1025
|
/** @param {string} href */
|
|
701
1026
|
function bustUrl(href) {
|
|
702
1027
|
const u = new URL(href);
|
|
@@ -752,10 +1077,14 @@ async function listClientComponents(dir) {
|
|
|
752
1077
|
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
753
1078
|
}
|
|
754
1079
|
/**
|
|
755
|
-
* 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).
|
|
756
1082
|
* @param {string} dir
|
|
757
1083
|
*/
|
|
758
1084
|
async function listPageClientFiles(dir) {
|
|
1085
|
+
const fromDep = await listPagesFromDeployment(dir);
|
|
1086
|
+
if (fromDep.length)
|
|
1087
|
+
return fromDep;
|
|
759
1088
|
const root = path.join(dir, 'pages');
|
|
760
1089
|
/** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parseChunkSegments> }>} */
|
|
761
1090
|
const out = [];
|
|
@@ -787,6 +1116,38 @@ async function listPageClientFiles(dir) {
|
|
|
787
1116
|
await walk(root, []);
|
|
788
1117
|
return out;
|
|
789
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
|
+
}
|
|
790
1151
|
/**
|
|
791
1152
|
* File-route segments from chunk id (`pages/Install` → `/install`).
|
|
792
1153
|
* Skips URL-invisible `(group)` dirs; boundary stems never reach here.
|
|
@@ -803,17 +1164,43 @@ function parseChunkSegments(chunkId) {
|
|
|
803
1164
|
continue;
|
|
804
1165
|
if (p === 'index' && i === parts.length - 1)
|
|
805
1166
|
continue;
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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));
|
|
814
1186
|
}
|
|
815
1187
|
return segs;
|
|
816
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
|
+
}
|
|
817
1204
|
function isRouteGroupDir(seg) {
|
|
818
1205
|
return typeof seg === 'string' && seg.startsWith('(') && seg.endsWith(')') && seg.length > 2;
|
|
819
1206
|
}
|
|
@@ -963,50 +1350,11 @@ async function runRouteGate(pathname, chunkId) {
|
|
|
963
1350
|
*/
|
|
964
1351
|
function emitEntryClient(eager, lazy, token) {
|
|
965
1352
|
const q = `?t=${token}`;
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
globalThis.__vmzLoadComponent = async (name) => {
|
|
972
|
-
const entry = __vmzComponentEntries[name] || ("components/" + name + ".client.js");
|
|
973
|
-
const mod = await import("./" + entry + "${q}");
|
|
974
|
-
return mod.default;
|
|
975
|
-
};`
|
|
976
|
-
: '';
|
|
977
|
-
return `/**
|
|
978
|
-
* Generated by vmz serve — hydrate matched file-route page (data-vmz-page) + layout chain + client Link takeover.
|
|
979
|
-
*/
|
|
980
|
-
import { registerComponents, hydrate, hydrateRoute, destroy } from ${JSON.stringify(`./vmz-dom.js${q}`)};
|
|
981
|
-
import { installClientNavigation } from ${JSON.stringify(`./vmz-client-nav.js${q}`)};
|
|
982
|
-
${imports}
|
|
983
|
-
|
|
984
|
-
${map}
|
|
985
|
-
${loader}
|
|
986
|
-
|
|
987
|
-
const root = document.getElementById("app");
|
|
988
|
-
if (!root) throw new Error("vmz: missing #app");
|
|
989
|
-
const chunkId = root.getAttribute("data-vmz-page");
|
|
990
|
-
if (!chunkId) throw new Error("vmz: missing data-vmz-page");
|
|
991
|
-
let props = {};
|
|
992
|
-
try {
|
|
993
|
-
const raw = root.getAttribute("data-vmz-props");
|
|
994
|
-
if (raw) props = JSON.parse(raw);
|
|
995
|
-
} catch { /* ignore */ }
|
|
996
|
-
const layoutChain = (root.getAttribute("data-vmz-layout") || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
997
|
-
const layoutCtors = [];
|
|
998
|
-
for (const id of layoutChain) {
|
|
999
|
-
layoutCtors.push((await import("./" + id + ".client.js${q}")).default);
|
|
1000
|
-
}
|
|
1001
|
-
const Page = (await import("./" + chunkId + ".client.js${q}")).default;
|
|
1002
|
-
await hydrateRoute(Page, root, props, layoutCtors);
|
|
1003
|
-
installClientNavigation({
|
|
1004
|
-
hydrate,
|
|
1005
|
-
hydrateRoute,
|
|
1006
|
-
destroy,
|
|
1007
|
-
importPage: async (id) => (await import("./" + id + ".client.js${q}")).default,
|
|
1008
|
-
});
|
|
1009
|
-
`;
|
|
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);
|
|
1010
1358
|
}
|
|
1011
1359
|
/**
|
|
1012
1360
|
* EventEntry zero-framework bootstrap: no static import of vmz-dom / page / islands.
|
|
@@ -1015,35 +1363,80 @@ installClientNavigation({
|
|
|
1015
1363
|
*/
|
|
1016
1364
|
function emitEntryEvent(token) {
|
|
1017
1365
|
const q = `?t=${token}`;
|
|
1018
|
-
|
|
1019
|
-
|
|
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}
|
|
1020
1375
|
*/
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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;
|
|
1047
1440
|
}
|
|
1048
1441
|
/**
|
|
1049
1442
|
* Style Theme cookie / localStorage key (host contract, not a second theme API).
|
|
@@ -1101,14 +1494,15 @@ function resolveThemeId(searchParams, cookieHeader) {
|
|
|
1101
1494
|
/**
|
|
1102
1495
|
* Always emit activation attr for an explicit theme id (incl. default) so it overrides OS media.
|
|
1103
1496
|
* @param {string|null} themeId
|
|
1497
|
+
* @returns {[string, string] | []} flattened attr pair for generatePageShell
|
|
1104
1498
|
*/
|
|
1105
|
-
function
|
|
1499
|
+
function htmlThemeAttrPair(themeId) {
|
|
1106
1500
|
if (!styleTheme || !themeId)
|
|
1107
|
-
return
|
|
1501
|
+
return [];
|
|
1108
1502
|
const attr = styleTheme.activationAttr || 'data-theme';
|
|
1109
1503
|
if (!(styleTheme.themeIds || []).includes(themeId))
|
|
1110
|
-
return
|
|
1111
|
-
return
|
|
1504
|
+
return [];
|
|
1505
|
+
return [attr, themeId];
|
|
1112
1506
|
}
|
|
1113
1507
|
/**
|
|
1114
1508
|
* Inline boot when SSR had no query/cookie: apply explicit `localStorage` only.
|