@vmz/core 0.2.4 → 0.2.6

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.
Files changed (77) hide show
  1. package/dist/browser/client-nav.d.ts +5 -55
  2. package/dist/browser/client-nav.js +5 -769
  3. package/dist/browser/diagnostics/precision-trace.d.ts +47 -0
  4. package/dist/browser/diagnostics/precision-trace.js +86 -0
  5. package/dist/browser/dom/attributes.d.ts +5 -0
  6. package/dist/browser/dom/attributes.js +102 -0
  7. package/dist/browser/dom/component.d.ts +11 -0
  8. package/dist/browser/dom/component.js +255 -0
  9. package/dist/browser/dom/direct-api.d.ts +66 -0
  10. package/dist/browser/dom/direct-api.js +446 -0
  11. package/dist/browser/dom/lifecycle.d.ts +9 -0
  12. package/dist/browser/dom/lifecycle.js +161 -0
  13. package/dist/browser/dom/registry.d.ts +5 -0
  14. package/dist/browser/dom/registry.js +16 -0
  15. package/dist/browser/dom-core.d.ts +51 -249
  16. package/dist/browser/dom-core.js +51 -2521
  17. package/dist/browser/navigation/client-navigation.d.ts +44 -0
  18. package/dist/browser/navigation/client-navigation.js +277 -0
  19. package/dist/browser/navigation/extract-html.d.ts +1 -0
  20. package/dist/browser/navigation/extract-html.js +5 -0
  21. package/dist/browser/navigation/layout-retain.d.ts +3 -0
  22. package/dist/browser/navigation/layout-retain.js +34 -0
  23. package/dist/browser/navigation/locale-transition.d.ts +24 -0
  24. package/dist/browser/navigation/locale-transition.js +323 -0
  25. package/dist/browser/navigation/scroll-focus.d.ts +14 -0
  26. package/dist/browser/navigation/scroll-focus.js +87 -0
  27. package/dist/browser/reactivity/patch-scheduler.d.ts +12 -0
  28. package/dist/browser/reactivity/patch-scheduler.js +720 -0
  29. package/dist/browser/reactivity/write-barrier.d.ts +16 -0
  30. package/dist/browser/reactivity/write-barrier.js +546 -0
  31. package/dist/browser/resume/adopt.d.ts +1 -0
  32. package/dist/browser/resume/adopt.js +168 -0
  33. package/dist/browser/resume/hydrate.d.ts +4 -0
  34. package/dist/browser/resume/hydrate.js +107 -0
  35. package/dist/browser/resume/resume.d.ts +3 -0
  36. package/dist/browser/resume/resume.js +102 -0
  37. package/dist/faces/dom.browser.d.ts +2 -1
  38. package/dist/faces/dom.browser.js +2 -1
  39. package/dist/host/{deployment-registry.d.ts → deployment/deployment-registry.d.ts} +1 -1
  40. package/dist/host/{deployment-registry.js → deployment/deployment-registry.js} +1 -1
  41. package/dist/host/list-client-components.d.ts +2 -2
  42. package/dist/host/list-client-components.js +4 -4
  43. package/dist/host/{render-host.d.ts → render/render-host.d.ts} +2 -2
  44. package/dist/host/{render-host.js → render/render-host.js} +1 -1
  45. package/dist/host/{route-layout-chain.js → render/route-layout-chain.js} +1 -1
  46. package/dist/host/serve/bootstrap.d.ts +1 -0
  47. package/dist/host/serve/bootstrap.js +25 -0
  48. package/dist/host/serve/constants.d.ts +6 -0
  49. package/dist/host/serve/constants.js +6 -0
  50. package/dist/host/serve/hmr-dev.d.ts +34 -0
  51. package/dist/host/serve/hmr-dev.js +294 -0
  52. package/dist/host/serve/http.d.ts +1 -0
  53. package/dist/host/serve/http.js +124 -0
  54. package/dist/host/serve/locale-theme.d.ts +19 -0
  55. package/dist/host/serve/locale-theme.js +169 -0
  56. package/dist/host/serve/page-catalog.d.ts +14 -0
  57. package/dist/host/serve/page-catalog.js +105 -0
  58. package/dist/host/serve/page-render.d.ts +17 -0
  59. package/dist/host/serve/page-render.js +419 -0
  60. package/dist/host/serve/paths.d.ts +3 -0
  61. package/dist/host/serve/paths.js +74 -0
  62. package/dist/host/serve/route-catalog.d.ts +6 -0
  63. package/dist/host/serve/route-catalog.js +108 -0
  64. package/dist/host/serve/state.d.ts +30 -0
  65. package/dist/host/serve/state.js +36 -0
  66. package/dist/host/serve-host.d.ts +0 -17
  67. package/dist/host/serve-host.mjs +4 -1420
  68. package/dist/ssr/document.d.ts +2 -0
  69. package/dist/ssr/document.js +71 -0
  70. package/dist/ssr/dom-ssr.d.ts +10 -47
  71. package/dist/ssr/dom-ssr.js +10 -1284
  72. package/dist/ssr/render.d.ts +2 -0
  73. package/dist/ssr/render.js +78 -0
  74. package/dist/ssr/serialize.d.ts +3 -0
  75. package/dist/ssr/serialize.js +678 -0
  76. package/package.json +11 -11
  77. /package/dist/host/{route-layout-chain.d.ts → render/route-layout-chain.d.ts} +0 -0
@@ -1,1420 +1,4 @@
1
- /**
2
- * Generic VMZ Node host �?SSR Route Graph pages + dist static + RPC/REST.
3
- *
4
- * Invoked by `vmz serve` / `vmz dev` (or: node dist/vmz-serve-host.mjs).
5
- *
6
- * Pathname matches compiled `_vmz/route-catalog.json` (frozen segs / pathPattern).
7
- * Hosts must not re-parse deployment pathPattern into a live catalog.
8
- * Not an SPA shell.
9
- *
10
- * `VMZ_DEV=1`: POST `/__vmz/reload` soft-reloads modules (cache-bust import);
11
- * GET `/__vmz/events` SSE notifies the browser:
12
- * - island HMR �?re-import `entry-client.js` (no full document reload)
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.
17
- */
18
- import { existsSync, readFileSync } from 'node:fs';
19
- import { readFile, writeFile } from 'node:fs/promises';
20
- import http from 'node:http';
21
- import { createRequire, registerHooks } from 'node:module';
22
- import path from 'node:path';
23
- import { fileURLToPath, pathToFileURL } from 'node:url';
24
- import { listClientComponents } from './list-client-components.js';
25
- import { LOCALE_LINK_PLAN_SCHEMA, linkRouteAliasesFromUnits, localeHrefTableFromPlan, localizeBodyLinks } from './localize-body-links.js';
26
- import { loadNativeAddon } from './native-addon.js';
27
- import { createRenderHost } from './render-host.js';
28
- import { resolveRouteLayoutChain } from './route-layout-chain.js';
29
- import { handleNodeRequest, setRoutes, setServerModuleResolver } from '../faces/vmz-runtime.js';
30
- const ROUTE_CATALOG_SCHEMA = 'vmz.route.catalog.v0';
31
- const ROUTE_CATALOG_REL = '_vmz/route-catalog.json';
32
- const LOCALE_LINK_PLAN_REL = '_vmz/locale-link-plan.json';
33
- const require = createRequire(import.meta.url);
34
- function resolveServeDistDir() {
35
- if (process.env.VMZ_DIST)
36
- return path.resolve(process.env.VMZ_DIST);
37
- const here = path.dirname(fileURLToPath(import.meta.url));
38
- const norm = here.replace(/\\/g, '/');
39
- const marker = '/_vmz/host';
40
- const idx = norm.toLowerCase().lastIndexOf(marker);
41
- if (idx >= 0)
42
- return path.resolve(here.slice(0, idx));
43
- return here;
44
- }
45
- const distDir = resolveServeDistDir();
46
- function projectRootForResolve() {
47
- const fromEnv = typeof process.env.VMZ_PROJECT_ROOT === 'string' ? process.env.VMZ_PROJECT_ROOT.trim() : '';
48
- if (fromEnv)
49
- return path.resolve(fromEnv);
50
- return process.cwd();
51
- }
52
- let appPackageRequire = null;
53
- function appPackageRequireResolve() {
54
- if (!appPackageRequire) {
55
- const root = projectRootForResolve();
56
- const pkg = path.join(root, 'package.json');
57
- appPackageRequire = existsSync(pkg) ? createRequire(pkg) : require;
58
- }
59
- return appPackageRequire;
60
- }
61
- /**
62
- * Dev/prod serve-host: resolve workspace peers + JSON imports from the app package root.
63
- * Dist-relative ESM cannot see app `node_modules` without this hook.
64
- */
65
- function installAppModuleResolveHooks() {
66
- registerHooks({
67
- resolve(specifier, context, nextResolve) {
68
- if (!specifier ||
69
- specifier.startsWith('.') ||
70
- specifier.startsWith('node:') ||
71
- specifier.startsWith('file:') ||
72
- specifier.startsWith('#')) {
73
- return nextResolve(specifier, context);
74
- }
75
- if (context.parentURL?.startsWith('file:')) {
76
- try {
77
- const parentPath = fileURLToPath(context.parentURL);
78
- if (!parentPath.startsWith(distDir + path.sep))
79
- return nextResolve(specifier, context);
80
- }
81
- catch {
82
- return nextResolve(specifier, context);
83
- }
84
- }
85
- try {
86
- const appParent = pathToFileURL(path.join(projectRootForResolve(), 'package.json')).href;
87
- return nextResolve(specifier, { ...context, parentURL: appParent });
88
- }
89
- catch {
90
- // Some legacy packages expose only a CommonJS entry.
91
- }
92
- try {
93
- const resolved = appPackageRequireResolve().resolve(specifier);
94
- return { url: pathToFileURL(resolved).href, shortCircuit: true };
95
- }
96
- catch {
97
- return nextResolve(specifier, context);
98
- }
99
- },
100
- load(url, context, nextLoad) {
101
- const pathOnly = url.split('?')[0].split('#')[0];
102
- if (!pathOnly.endsWith('.json'))
103
- return nextLoad(url, context);
104
- try {
105
- const filePath = fileURLToPath(pathOnly);
106
- const raw = readFileSync(filePath, 'utf8');
107
- return {
108
- format: 'module',
109
- shortCircuit: true,
110
- source: `export default ${raw}`,
111
- };
112
- }
113
- catch {
114
- return nextLoad(url, context);
115
- }
116
- },
117
- });
118
- }
119
- installAppModuleResolveHooks();
120
- const host = process.env.VMZ_HOST || '127.0.0.1';
121
- const port = Number(process.env.VMZ_PORT || process.env.PORT || 5173);
122
- const isDev = process.env.VMZ_DEV === '1' || process.env.VMZ_DEV === 'true';
123
- // Absolute origin for in-process client graphs that fall back to HTTP RPC
124
- // (separate `dist/vmz-runtime.js` instance without setServerModuleResolver).
125
- globalThis.__VMZ_RPC_ORIGIN = `http://${host}:${port}`;
126
- /**
127
- * Soft reload only busts the top-level `import(page?t=token)`. Nested relative
128
- * imports (`../../lib/units.js`) keep the first-loaded ESM cache entry �?so a
129
- * page can demand exports that the stale dep never had (or vice versa).
130
- * Propagate `t` from parentURL onto file: children under this dist.
131
- */
132
- if (isDev) {
133
- const distUrlPrefix = pathToFileURL(distDir.endsWith(path.sep) ? distDir : `${distDir}${path.sep}`).href;
134
- registerHooks({
135
- resolve(specifier, context, nextResolve) {
136
- const result = nextResolve(specifier, context);
137
- if (!specifier.startsWith('.') || !context.parentURL || !result?.url)
138
- return result;
139
- let token = '';
140
- try {
141
- token = new URL(context.parentURL).searchParams.get('t') || '';
142
- }
143
- catch {
144
- return result;
145
- }
146
- if (!token)
147
- return result;
148
- if (!result.url.startsWith('file:'))
149
- return result;
150
- if (!result.url.startsWith(distUrlPrefix)) {
151
- try {
152
- if (!fileURLToPath(result.url).startsWith(distDir))
153
- return result;
154
- }
155
- catch {
156
- return result;
157
- }
158
- }
159
- const u = new URL(result.url);
160
- if (u.searchParams.get('t') === token)
161
- return result;
162
- u.searchParams.set('t', token);
163
- return { ...result, url: u.href, shortCircuit: true };
164
- },
165
- });
166
- }
167
- let reloadToken = Date.now();
168
- let ssrRenderHost = null;
169
- let lastDevBuildId = null;
170
- let pageCatalog = [];
171
- const pageCtors = new Map();
172
- let cssEntry = null;
173
- let styleBundleHash = null;
174
- let styleTheme = null;
175
- let localeArtifact = null;
176
- let localeLinkPlan = null;
177
- const sseClients = new Set();
178
- let inFlight = 0;
179
- let shuttingDown = false;
180
- let ready = false;
181
- let lastDevError = null;
182
- setServerModuleResolver((moduleId) => {
183
- const rel = moduleId.replace(/^#server\//, '') + '.js';
184
- return bustUrl(pathToFileURL(path.join(distDir, '#server', rel)).href);
185
- });
186
- try {
187
- await softReload({ quiet: true });
188
- ready = true;
189
- }
190
- catch (err) {
191
- lastDevError = normalizeDevError(err);
192
- ready = true; // still accept HTTP �?serve error page / recover on next reload
193
- console.error('vmz serve: initial load failed (dev host stays up)', lastDevError.message);
194
- }
195
- function readRequestBody(req) {
196
- return new Promise((resolve, reject) => {
197
- const chunks = [];
198
- req.on('data', (c) => chunks.push(c));
199
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
200
- req.on('error', reject);
201
- });
202
- }
203
- async function renderPage(pathname, opts = {}) {
204
- const rendered = await renderPageStream(pathname, opts);
205
- if (!rendered)
206
- return null;
207
- const raw = rendered.stream ?? rendered;
208
- if (!raw || typeof raw !== 'object' || typeof raw[Symbol.asyncIterator] !== 'function') {
209
- return null;
210
- }
211
- let html = '';
212
- for await (const chunk of raw) {
213
- html += chunk;
214
- }
215
- return html;
216
- }
217
- /**
218
- * Stream shell + Direct serialize body for the matched file-route page.
219
- * Runs Page.access (closed allow/redirect/not-found/deny) before load;
220
- * POST may run Page.action before re-render.
221
- */
222
- async function renderPageStream(pathname, opts = {}) {
223
- try {
224
- return await renderPageStreamInner(pathname, opts);
225
- }
226
- catch (err) {
227
- const normalized = normalizeDevError(err);
228
- lastDevError = normalized;
229
- console.error('vmz serve: renderPageStream failed', normalized.message);
230
- return { status: 500, stream: emitDevErrorHtml(normalized) };
231
- }
232
- }
233
- async function renderPageStreamInner(pathname, opts = {}) {
234
- if (isDev && lastDevError && pageCtors.size === 0) {
235
- return { status: 500, stream: emitDevErrorHtml(lastDevError) };
236
- }
237
- const localePlan = resolveLocalePath(pathname, opts.cookieHeader);
238
- if (localePlan.redirectTo) {
239
- return { status: 302, redirect: localePlan.redirectTo, headers: { Location: localePlan.redirectTo } };
240
- }
241
- const routePath = localePlan.restPath || pathname;
242
- let match = matchFileRoute(routePath, pageCatalog);
243
- let status = 200;
244
- const gated = await runRouteGate(routePath, match?.chunkId);
245
- if (gated === 'not_found') {
246
- match = findRootCatchAll(pageCatalog);
247
- status = 404;
248
- }
249
- else if (!match) {
250
- match = findRootCatchAll(pageCatalog);
251
- status = 404;
252
- }
253
- else if (isRootCatchAll(match)) {
254
- status = 404;
255
- }
256
- if (!match) {
257
- if (isDev && lastDevError) {
258
- return { status: 500, stream: emitDevErrorHtml(lastDevError) };
259
- }
260
- return null;
261
- }
262
- const Page = await loadPageCtor(match.chunkId);
263
- if (!Page) {
264
- if (isDev && lastDevError) {
265
- return { status: 500, stream: emitDevErrorHtml(lastDevError) };
266
- }
267
- return null;
268
- }
269
- const params = extractRouteParams(match.segs, routePath);
270
- const method = String(opts.method || 'GET').toUpperCase();
271
- const localeCtx = {
272
- localeId: localePlan.localeId,
273
- dir: localePlan.dir,
274
- pathname,
275
- routePath,
276
- alternates: pageMetaAlternates(match.chunkId, localePlan.localeId),
277
- };
278
- if (typeof Page.access === 'function') {
279
- const access = await Page.access({
280
- params,
281
- pathname: routePath,
282
- chunkId: match.chunkId,
283
- signal: opts.signal,
284
- searchParams: opts.searchParams,
285
- method,
286
- localeId: localeCtx.localeId,
287
- });
288
- const closed = normalizeAccessResult(access);
289
- if (closed.kind === 'redirect') {
290
- return { status: 302, redirect: closed.location, headers: { Location: closed.location } };
291
- }
292
- if (closed.kind === 'deny') {
293
- return { status: 403, stream: emitAccessShell('route-access-deny') };
294
- }
295
- if (closed.kind === 'not-found') {
296
- const catchAll = findRootCatchAll(pageCatalog);
297
- if (catchAll) {
298
- const NotFound = await loadPageCtor(catchAll.chunkId);
299
- if (NotFound) {
300
- const resumeEntries = await loadPageResumeEntries(distDir, catchAll.chunkId);
301
- const eventOnlyShell = isEventOnlyShell(resumeEntries.map((e) => e.strategy));
302
- return {
303
- status: 404,
304
- stream: emitPageHtml(NotFound, catchAll.chunkId, eventOnlyShell, { ...params }, opts, [], localeCtx),
305
- };
306
- }
307
- }
308
- return { status: 404, stream: emitAccessShell('route-access-not-found') };
309
- }
310
- }
311
- let props = { ...params };
312
- if (method === 'POST' && typeof Page.action === 'function') {
313
- const acted = await Page.action({
314
- params,
315
- pathname: routePath,
316
- chunkId: match.chunkId,
317
- signal: opts.signal,
318
- searchParams: opts.searchParams,
319
- body: opts.body,
320
- method,
321
- localeId: localeCtx.localeId,
322
- });
323
- const actionClosed = normalizeActionResult(acted);
324
- if (actionClosed.kind === 'redirect') {
325
- return { status: 302, redirect: actionClosed.location, headers: { Location: actionClosed.location } };
326
- }
327
- if (actionClosed.kind === 'deny') {
328
- return { status: 403, stream: emitAccessShell('route-action-deny') };
329
- }
330
- if (actionClosed.kind === 'not-found') {
331
- return { status: 404, stream: emitAccessShell('route-action-not-found') };
332
- }
333
- if (actionClosed.props) {
334
- props = { ...props, ...actionClosed.props };
335
- }
336
- }
337
- if (typeof Page.load === 'function') {
338
- const loaded = await Page.load({
339
- params,
340
- pathname: routePath,
341
- chunkId: match.chunkId,
342
- signal: opts.signal,
343
- searchParams: opts.searchParams,
344
- localeId: localeCtx.localeId,
345
- });
346
- if (opts.signal?.aborted) {
347
- return { status: 499, stream: emitAccessShell('route-nav-cancelled') };
348
- }
349
- if (loaded && typeof loaded === 'object' && !Array.isArray(loaded)) {
350
- props = { ...props, ...loaded };
351
- }
352
- }
353
- if (opts.signal?.aborted) {
354
- return { status: 499, stream: emitAccessShell('route-nav-cancelled') };
355
- }
356
- const resumeEntries = await loadPageResumeEntries(distDir, match.chunkId);
357
- const strategies = resumeEntries.map((e) => e.strategy);
358
- const eventOnlyShell = isEventOnlyShell(strategies);
359
- const layoutChain = resolveRouteLayoutChain(distDir, match.chunkId);
360
- return {
361
- status,
362
- stream: emitPageHtml(Page, match.chunkId, eventOnlyShell, props, opts, layoutChain, localeCtx),
363
- };
364
- }
365
- function normalizeAccessResult(access) {
366
- if (access == null || access === true)
367
- return { kind: 'allow' };
368
- if (typeof access === 'string') {
369
- const k = access.toLowerCase();
370
- if (k === 'allow')
371
- return { kind: 'allow' };
372
- if (k === 'deny')
373
- return { kind: 'deny' };
374
- if (k === 'not-found' || k === 'notfound')
375
- return { kind: 'not-found' };
376
- }
377
- if (typeof access === 'object' && access) {
378
- const row = access;
379
- const kind = String(row.kind || row.type || 'allow').toLowerCase();
380
- if (kind === 'redirect') {
381
- const location = String(row.location || row.to || row.href || '');
382
- if (!location)
383
- return { kind: 'deny' };
384
- return { kind: 'redirect', location };
385
- }
386
- if (kind === 'deny')
387
- return { kind: 'deny' };
388
- if (kind === 'not-found' || kind === 'notfound')
389
- return { kind: 'not-found' };
390
- return { kind: 'allow' };
391
- }
392
- return { kind: 'allow' };
393
- }
394
- function normalizeActionResult(acted) {
395
- const base = normalizeAccessResult(acted);
396
- if (base.kind !== 'allow')
397
- return base;
398
- if (acted && typeof acted === 'object' && !Array.isArray(acted)) {
399
- const row = acted;
400
- if (row.props && typeof row.props === 'object') {
401
- return { kind: 'allow', props: row.props };
402
- }
403
- if (!('kind' in row) && !('type' in row)) {
404
- return { kind: 'allow', props: row };
405
- }
406
- }
407
- return { kind: 'allow' };
408
- }
409
- /**
410
- * Minimal HTML for closed access/action results when no NotFound page exists.
411
- */
412
- async function* emitAccessShell(marker) {
413
- const native = loadNativeAddon();
414
- if (typeof native.generateHtmlShell !== 'function') {
415
- throw new Error('vmz native addon missing generateHtmlShell �?rebuild with `pnpm napi:build`');
416
- }
417
- yield native.generateHtmlShell({
418
- title: 'App',
419
- lang: 'en',
420
- cssHrefs: [],
421
- bodyHtml: `<p>${marker}</p>`,
422
- bodyAttrs: [],
423
- });
424
- }
425
- /**
426
- * Prefer page `static meta()` for document title/description �?never brand the framework in business HTML.
427
- */
428
- function resolvePageDocumentMeta(Page) {
429
- try {
430
- let raw = {};
431
- if (typeof Page?.meta === 'function')
432
- raw = Page.meta() || {};
433
- else if (Page?.meta && typeof Page.meta === 'object')
434
- raw = Page.meta;
435
- const title = String(raw.title || '').trim();
436
- const description = String(raw.description || '').trim();
437
- return { title: title || 'App', description };
438
- }
439
- catch {
440
- return { title: 'App', description: '' };
441
- }
442
- }
443
- async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {}, layoutChain = [], localeCtx = {}) {
444
- const signal = opts.signal;
445
- const live = isDev
446
- ? `\n <script>
447
- (() => {
448
- const es = new EventSource("/__vmz/events");
449
- let sawDisconnect = false;
450
- es.onerror = () => { sawDisconnect = true; };
451
- es.onopen = () => {
452
- // Host respawn drops SSE �?reload once the new process is up (no manual restart).
453
- if (sawDisconnect) location.reload();
454
- };
455
- function showOverlay(err) {
456
- let el = document.getElementById("vmz-dev-overlay");
457
- if (!el) {
458
- el = document.createElement("div");
459
- el.id = "vmz-dev-overlay";
460
- el.setAttribute("role", "alert");
461
- Object.assign(el.style, {
462
- position: "fixed", inset: "0", zIndex: "2147483646",
463
- background: "rgba(15,17,21,0.92)", color: "#f4f4f5",
464
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
465
- padding: "2rem", overflow: "auto",
466
- });
467
- document.documentElement.appendChild(el);
468
- }
469
- const msg = (err && err.message) || String(err || "Unknown error");
470
- const stack = (err && err.stack) || "";
471
- const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;"}[c]));
472
- el.innerHTML = "<div style=\\"max-width:56rem;margin:0 auto\\">"
473
- + "<p style=\\"margin:0 0 .5rem;color:#f87171;font-weight:700\\">Dev Error</p>"
474
- + "<pre style=\\"white-space:pre-wrap;margin:0 0 1rem;font-size:13px;line-height:1.45\\">" + esc(msg) + "</pre>"
475
- + (stack ? "<pre style=\\"white-space:pre-wrap;opacity:.7;font-size:12px\\">" + esc(stack) + "</pre>" : "")
476
- + "<p style=\\"opacity:.65;font-size:12px\\">Fix the file and save �?soft reload will clear this overlay.</p>"
477
- + "</div>";
478
- }
479
- function hideOverlay() {
480
- const el = document.getElementById("vmz-dev-overlay");
481
- if (el) el.remove();
482
- }
483
- es.onmessage = async (ev) => {
484
- let msg = null;
485
- try { msg = JSON.parse(ev.data); } catch { /* plain string */ }
486
- if (msg && msg.type === "error") {
487
- showOverlay(msg);
488
- return;
489
- }
490
- if (!msg || msg.type !== "hmr") {
491
- if (ev.data === "reload") location.reload();
492
- return;
493
- }
494
- hideOverlay();
495
- if (msg.mode === "island") {
496
- try {
497
- const { hydrate } = await import("/dom.browser.js?t=" + msg.token);
498
- const root = document.getElementById("app");
499
- const pageChunk = root && root.getAttribute("data-vmz-page");
500
- if (root && pageChunk) {
501
- // Child Ctors come from page static imports after reload with cache token.
502
- const pageMod = await import("/" + pageChunk + ".client.js?t=" + msg.token);
503
- let hmrProps = {};
504
- try {
505
- const raw = root.getAttribute("data-vmz-props");
506
- if (raw) hmrProps = JSON.parse(raw);
507
- } catch { /* ignore */ }
508
- await hydrate(pageMod.default, root, hmrProps, { preserveState: true, skipOnMount: true });
509
- } else {
510
- location.reload();
511
- }
512
- } catch (err) {
513
- console.error("vmz island HMR failed", err);
514
- showOverlay({ message: String(err && err.message || err), stack: err && err.stack });
515
- }
516
- return;
517
- }
518
- location.reload();
519
- };
520
- })();
521
- </script>`
522
- : '';
523
- const bootOverlay = isDev && lastDevError
524
- ? `\n <script>window.__VMZ_DEV_ERROR__=${JSON.stringify(lastDevError)};` +
525
- `(function(){var e=window.__VMZ_DEV_ERROR__;if(!e)return;` +
526
- `var ev=new Event("message");ev.data=JSON.stringify({type:"error",message:e.message,stack:e.stack});` +
527
- `/* paint immediately */` +
528
- `var d=document.createElement("div");d.id="vmz-dev-overlay";d.setAttribute("role","alert");` +
529
- `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"});` +
530
- `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 {"<":"&lt;",">":"&gt;","&":"&amp;"}[c]})+"</pre></div>";` +
531
- `document.documentElement.appendChild(d);})();</script>`
532
- : '';
533
- const buildIdBoot = isDev && lastDevBuildId ? `\n <script>window.__VMZ_DEV_BUILD_ID__=${JSON.stringify(lastDevBuildId)};</script>` : '';
534
- if (signal?.aborted)
535
- return;
536
- const themeId = resolveThemeId(opts.searchParams, opts.cookieHeader);
537
- const themeBoot = themeBootstrapScript();
538
- const localeBoot = localeBootstrapScript();
539
- const faviconHead = siteFaviconHeadHtml();
540
- const propsJson = JSON.stringify(props ?? {});
541
- const localeId = localeCtx.localeId || localeArtifact?.defaultLocale || 'en';
542
- const dir = localeCtx.dir || 'ltr';
543
- const htmlExtraAttrs = [...htmlThemeAttrPair(themeId)];
544
- if (localeArtifact?.routing) {
545
- htmlExtraAttrs.push('data-vmz-locale-routing', JSON.stringify({
546
- strategy: localeArtifact.routing.strategy || 'prefix',
547
- defaultPrefix: localeArtifact.routing.defaultPrefix || 'include',
548
- defaultLocale: localeArtifact.defaultLocale,
549
- locales: (localeArtifact.locales || []).map((l) => l.id),
550
- }));
551
- const hrefTable = localeHrefTableFromPlan(localeLinkPlan);
552
- if (hrefTable && Object.keys(hrefTable).length) {
553
- htmlExtraAttrs.push('data-vmz-locale-hrefs', JSON.stringify(hrefTable));
554
- }
555
- }
556
- const pageDocMeta = resolvePageDocumentMeta(Page);
557
- const prevLocaleHint = globalThis.__vmzLocaleIdHint;
558
- globalThis.__vmzLocaleIdHint = localeId;
559
- if (!ssrRenderHost) {
560
- ssrRenderHost = await createRenderHost(distDir, {
561
- strictDeployment: !isDev,
562
- preload: 'none',
563
- cacheBust: reloadToken,
564
- });
565
- }
566
- await ssrRenderHost.ensureComponents([chunkId, ...layoutChain]);
567
- let bodyHtml = '';
568
- try {
569
- for await (const chunk of ssrRenderHost.renderToStream(Page, props, { signal })) {
570
- if (signal?.aborted)
571
- return;
572
- bodyHtml += chunk;
573
- }
574
- if (signal?.aborted)
575
- return;
576
- // Wrap page HTML in layout chain (outer �?inner) via default slot injection.
577
- for (let i = layoutChain.length - 1; i >= 0; i--) {
578
- const Layout = await loadPageCtor(layoutChain[i]);
579
- if (!Layout)
580
- continue;
581
- bodyHtml = await ssrRenderHost.renderToString(Layout, {}, { signal, slotHtml: bodyHtml });
582
- if (signal?.aborted)
583
- return;
584
- }
585
- // Locale discipline: apply frozen link plan rows (0.1.30) �?no path algebra.
586
- if (localeArtifact && localeId) {
587
- bodyHtml = localizeBodyLinks(bodyHtml, localeId, localeArtifact, undefined, localeLinkPlan);
588
- }
589
- }
590
- finally {
591
- if (prevLocaleHint === undefined)
592
- delete globalThis.__vmzLocaleIdHint;
593
- else
594
- globalThis.__vmzLocaleIdHint = prevLocaleHint;
595
- }
596
- if (signal?.aborted)
597
- return;
598
- const native = loadNativeAddon();
599
- if (typeof native.generatePageShell !== 'function') {
600
- throw new Error('vmz native addon missing generatePageShell �?rebuild with `pnpm napi:build`');
601
- }
602
- const entrySrc = `/${eventOnlyShell ? 'entry-event.js' : 'entry-client.js'}?t=${reloadToken}`;
603
- const cssHref = cssEntryWithBust(cssEntry);
604
- yield native.generatePageShell({
605
- bodyHtml,
606
- chunkId,
607
- layoutChain,
608
- propsJson,
609
- meta: {
610
- title: pageDocMeta.title,
611
- description: pageDocMeta.description,
612
- canonical: '',
613
- robots: '',
614
- lang: localeId,
615
- dir,
616
- alternates: localeCtx.alternates || [],
617
- },
618
- // napi Option<String>: omit/undefined = None; null is rejected as String
619
- ...(cssHref ? { cssEntry: cssHref } : {}),
620
- isErrorDocument: false,
621
- htmlExtraAttrs,
622
- headExtraHtml: `${themeBoot}${localeBoot}${faviconHead}`,
623
- moduleScriptSrc: entrySrc,
624
- bodyTailHtml: `${live}${bootOverlay}${buildIdBoot}`,
625
- });
626
- }
627
- const server = http.createServer((req, res) => {
628
- const url = new URL(req.url || '/', `http://${host}:${port}`);
629
- if (url.pathname === '/__vmz/health' && req.method === 'GET') {
630
- res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
631
- res.end(JSON.stringify({ status: 'ok', shuttingDown, inFlight }));
632
- return;
633
- }
634
- if (url.pathname === '/__vmz/ready' && req.method === 'GET') {
635
- if (!ready || shuttingDown) {
636
- res.writeHead(503, { 'content-type': 'application/json', 'cache-control': 'no-store' });
637
- res.end(JSON.stringify({ status: 'not-ready', ready, shuttingDown }));
638
- return;
639
- }
640
- res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
641
- res.end(JSON.stringify({ status: 'ready', ready: true, inFlight }));
642
- return;
643
- }
644
- if (shuttingDown) {
645
- res.writeHead(503, { 'content-type': 'application/json', 'cache-control': 'no-store' });
646
- res.end(JSON.stringify({ status: 'shutting-down' }));
647
- return;
648
- }
649
- inFlight += 1;
650
- let settled = false;
651
- const done = () => {
652
- if (settled)
653
- return;
654
- settled = true;
655
- inFlight = Math.max(0, inFlight - 1);
656
- };
657
- res.on('finish', done);
658
- res.on('close', done);
659
- if (url.pathname === '/__vmz/reload' && req.method === 'POST') {
660
- readRequestBody(req)
661
- .then((raw) => {
662
- let payload = {};
663
- try {
664
- payload = raw ? JSON.parse(String(raw)) : {};
665
- }
666
- catch {
667
- payload = {};
668
- }
669
- return softReload({ payload });
670
- })
671
- .then((info) => {
672
- res.writeHead(200, { 'content-type': 'application/json' });
673
- res.end(JSON.stringify({ ok: true, token: reloadToken, ...info }));
674
- })
675
- .catch((err) => {
676
- console.error('vmz serve: soft reload failed', err);
677
- lastDevError = normalizeDevError(err);
678
- notifySse(JSON.stringify({
679
- type: 'error',
680
- message: lastDevError.message,
681
- stack: lastDevError.stack,
682
- at: lastDevError.at,
683
- }));
684
- res.writeHead(500, { 'content-type': 'application/json' });
685
- res.end(JSON.stringify({ ok: false, error: lastDevError.message }));
686
- });
687
- return;
688
- }
689
- if (url.pathname === '/__vmz/events' && req.method === 'GET') {
690
- res.writeHead(200, {
691
- 'content-type': 'text/event-stream',
692
- 'cache-control': 'no-cache',
693
- connection: 'keep-alive',
694
- });
695
- res.write(': connected\n\n');
696
- sseClients.add(res);
697
- req.on('close', () => {
698
- sseClients.delete(res);
699
- });
700
- return;
701
- }
702
- handleNodeRequest(req, res, { distDir, renderPage, renderPageStream });
703
- });
704
- server.listen(port, host, () => {
705
- console.log(`vmz serve http://${host}:${port} (dist=${distDir}${isDev ? ', dev' : ''})`);
706
- });
707
- const SHUTDOWN_TIMEOUT_MS = Number(process.env.VMZ_SHUTDOWN_TIMEOUT_MS || 10000);
708
- async function gracefulShutdown(signal) {
709
- if (shuttingDown)
710
- return;
711
- shuttingDown = true;
712
- ready = false;
713
- console.log(`vmz serve: ${signal} �?draining in-flight=${inFlight} timeout=${SHUTDOWN_TIMEOUT_MS}ms`);
714
- server.close();
715
- const start = Date.now();
716
- while (inFlight > 0 && Date.now() - start < SHUTDOWN_TIMEOUT_MS) {
717
- await new Promise((r) => setTimeout(r, 25));
718
- }
719
- for (const client of sseClients) {
720
- try {
721
- client.end();
722
- }
723
- catch {
724
- /* ignore */
725
- }
726
- }
727
- sseClients.clear();
728
- process.exit(inFlight > 0 ? 1 : 0);
729
- }
730
- process.on('SIGTERM', () => {
731
- void gracefulShutdown('SIGTERM');
732
- });
733
- process.on('SIGINT', () => {
734
- void gracefulShutdown('SIGINT');
735
- });
736
- /**
737
- * Attach author Link RouteId aliases so localizeBodyLinks can match `data-vmz-route="IndexPage"`
738
- * against realization rows keyed by chunk path (`pages/index`).
739
- */
740
- function attachLinkRouteAliases(artifact, dir) {
741
- if (!artifact || typeof artifact !== 'object')
742
- return artifact;
743
- try {
744
- const dep = JSON.parse(readFileSync(path.join(dir, 'vmz-deployment.json'), 'utf8'));
745
- const aliases = linkRouteAliasesFromUnits(Array.isArray(dep.units) ? dep.units : []);
746
- if (!aliases.length)
747
- return artifact;
748
- return { ...artifact, linkRouteAliases: aliases };
749
- }
750
- catch {
751
- return artifact;
752
- }
753
- }
754
- /**
755
- * Re-import routes / pages / components with a new cache-bust token.
756
- * Keeps the HTTP server process alive (no Node restart).
757
- * Failed reloads keep the previous in-memory modules (Vite-like resilience).
758
- */
759
- async function softReload(opts = {}) {
760
- const prevToken = reloadToken;
761
- const prevCatalog = pageCatalog;
762
- const prevCtors = new Map(pageCtors);
763
- const nextToken = Date.now();
764
- reloadToken = nextToken;
765
- const affected = opts.payload?.affectedChunks ?? [];
766
- const seeds = opts.payload?.seedChunks ?? [];
767
- const full = opts.payload?.full;
768
- const islandHmr = Boolean(opts.payload?.islandHmr);
769
- const rerunLoaders = opts.payload?.rerunLoaders ?? [];
770
- const skipEntryRewrite = Boolean(opts.payload?.skipEntryRewrite);
771
- const buildId = opts.payload?.buildId != null ? String(opts.payload.buildId) : null;
772
- const sourceRevision = opts.payload?.sourceRevision != null ? String(opts.payload.sourceRevision) : null;
773
- const bundleRevision = opts.payload?.bundleRevision != null ? String(opts.payload.bundleRevision) : null;
774
- if (buildId)
775
- lastDevBuildId = buildId;
776
- // 0.1.31: payload-only scope. `reloadToken` is opaque cache-bust �?never invent full/affected here.
777
- const reloadAllPages = Boolean(full) || (!islandHmr && rerunLoaders.length === 0 && affected.length === 0);
778
- try {
779
- try {
780
- const routes = JSON.parse(await readFile(path.join(distDir, 'vmz-routes.json'), 'utf8'));
781
- setRoutes(routes);
782
- }
783
- catch {
784
- setRoutes([]);
785
- }
786
- try {
787
- localeArtifact = JSON.parse(await readFile(path.join(distDir, '_vmz', 'locale-route-realization.json'), 'utf8'));
788
- localeArtifact = attachLinkRouteAliases(localeArtifact, distDir);
789
- }
790
- catch {
791
- localeArtifact = null;
792
- }
793
- try {
794
- const rawPlan = JSON.parse(await readFile(path.join(distDir, ...LOCALE_LINK_PLAN_REL.split('/')), 'utf8'));
795
- localeLinkPlan = rawPlan?.schema === LOCALE_LINK_PLAN_SCHEMA && Array.isArray(rawPlan.rows) ? rawPlan : null;
796
- }
797
- catch {
798
- localeLinkPlan = null;
799
- }
800
- const componentEntries = await listClientComponents(distDir, { strict: !isDev });
801
- ssrRenderHost = await createRenderHost(distDir, {
802
- strictDeployment: !isDev,
803
- preload: 'none',
804
- cacheBust: nextToken,
805
- });
806
- const nextCatalog = await listPageClientFiles(distDir);
807
- // empty catalog throws inside listPageClientFiles
808
- const nextCtors = new Map();
809
- if (!islandHmr) {
810
- const reloadSet = rerunLoaders.length > 0
811
- ? rerunLoaders
812
- : reloadAllPages
813
- ? nextCatalog.map((p) => p.chunkId)
814
- : nextCatalog.filter((p) => pageNeedsReload(p.chunkId, affected)).map((p) => p.chunkId);
815
- const pagesToLoad = nextCatalog.filter((p) => reloadSet.includes(p.chunkId));
816
- for (const p of pagesToLoad) {
817
- const pageRel = `${p.chunkId}.client.js`;
818
- const href = bustUrl(pathToFileURL(path.join(distDir, pageRel)).href);
819
- const mod = await import(href);
820
- nextCtors.set(p.chunkId, mod.default);
821
- }
822
- }
823
- pageCatalog = nextCatalog;
824
- if (!islandHmr) {
825
- if (reloadAllPages) {
826
- pageCtors.clear();
827
- for (const [k, v] of nextCtors)
828
- pageCtors.set(k, v);
829
- }
830
- else {
831
- // Keep unaffected page constructors; only swap what we re-imported.
832
- for (const [k, v] of nextCtors)
833
- pageCtors.set(k, v);
834
- // Drop ctors for pages that disappeared from catalog.
835
- for (const id of [...pageCtors.keys()]) {
836
- if (!nextCatalog.some((p) => p.chunkId === id))
837
- pageCtors.delete(id);
838
- }
839
- }
840
- }
841
- const indexChunk = pageCatalog.find((p) => p.chunkId === 'pages/index')?.chunkId || pageCatalog[0].chunkId;
842
- const resumeEntries = await loadPageResumeEntries(distDir, indexChunk);
843
- const styleMeta = await loadDeploymentStyle(distDir);
844
- cssEntry = styleMeta.cssEntry;
845
- styleBundleHash = styleMeta.styleBundleHash;
846
- styleTheme = styleMeta.styleTheme;
847
- const lazyEventNames = resumeEntries
848
- .filter((e) => isEventStrategy(e.strategy))
849
- .map((e) => e.component)
850
- .filter(Boolean);
851
- const lazySet = new Set(lazyEventNames);
852
- await writeFile(path.join(distDir, 'entry-client.js'), emitEntryClient(componentEntries.filter((e) => !lazySet.has(e.name)), componentEntries.filter((e) => lazySet.has(e.name)), skipEntryRewrite ? reloadToken : nextToken), 'utf8');
853
- const strategies = resumeEntries.map((e) => e.strategy);
854
- const eventOnlyShell = isEventOnlyShell(strategies);
855
- await writeFile(path.join(distDir, 'entry-event.js'), emitEntryEvent(skipEntryRewrite ? reloadToken : nextToken), 'utf8');
856
- lastDevError = null;
857
- const mode = islandHmr ? 'island' : eventOnlyShell ? 'event-shell' : 'full';
858
- notifySse(JSON.stringify({
859
- type: 'hmr',
860
- mode,
861
- affectedChunks: affected,
862
- seedChunks: seeds,
863
- token: reloadToken,
864
- buildId: buildId || lastDevBuildId,
865
- sourceRevision,
866
- bundleRevision,
867
- serveRevision: String(reloadToken),
868
- full: Boolean(full),
869
- eventOnlyShell,
870
- }));
871
- if (!opts.quiet) {
872
- const aff = affected.length > 0 ? ` affected=[${affected.join(', ')}]` : full === false ? ' affected=[]' : '';
873
- const scope = islandHmr ? 'island' : reloadAllPages ? 'all-pages' : `pages=${nextCtors.size}`;
874
- const bid = buildId || lastDevBuildId;
875
- const rev = bid || sourceRevision || bundleRevision
876
- ? ` buildId=${bid || '-'} source=${sourceRevision || '-'} bundle=${bundleRevision || '-'} serve=${reloadToken}`
877
- : ` t=${reloadToken}`;
878
- console.log(`vmz serve: soft reload ok (mode=${mode}; ${scope}; catalog=${pageCatalog.length};${rev}${aff})`);
879
- }
880
- return {
881
- affectedChunks: affected,
882
- seedChunks: seeds,
883
- full: Boolean(full),
884
- islandHmr,
885
- mode,
886
- eventOnlyShell,
887
- pageCount: pageCatalog.length,
888
- reloadedPages: islandHmr ? 0 : nextCtors.size,
889
- reloadAllPages,
890
- buildId: buildId || lastDevBuildId,
891
- sourceRevision,
892
- bundleRevision,
893
- serveRevision: String(reloadToken),
894
- token: reloadToken,
895
- };
896
- }
897
- catch (err) {
898
- reloadToken = prevToken;
899
- pageCatalog = prevCatalog;
900
- pageCtors.clear();
901
- for (const [k, v] of prevCtors)
902
- pageCtors.set(k, v);
903
- lastDevError = normalizeDevError(err);
904
- throw err;
905
- }
906
- }
907
- function pageNeedsReload(chunkId, affected) {
908
- if (chunkId === 'pages/Layout' || chunkId.endsWith('/Layout'))
909
- return true;
910
- return affected.some((a) => {
911
- const id = String(a);
912
- return id === chunkId || chunkId.startsWith(`${id}/`) || id.startsWith(`${chunkId}/`);
913
- });
914
- }
915
- function notifySse(event) {
916
- for (const client of [...sseClients]) {
917
- try {
918
- client.write(`data: ${event}\n\n`);
919
- }
920
- catch {
921
- sseClients.delete(client);
922
- }
923
- }
924
- }
925
- function normalizeDevError(err) {
926
- if (err && typeof err === 'object') {
927
- const e = err;
928
- return {
929
- message: e.message ? String(e.message) : String(err),
930
- stack: e.stack ? String(e.stack) : undefined,
931
- at: Date.now(),
932
- };
933
- }
934
- return { message: String(err), at: Date.now() };
935
- }
936
- async function* emitDevErrorHtml(err) {
937
- const msg = escapeHtml(err.message || 'Unknown error');
938
- const stack = err.stack ? escapeHtml(err.stack) : '';
939
- const style = `<style>
940
- body{margin:0;background:#0f1115;color:#f4f4f5;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
941
- main{max-width:56rem;margin:0 auto;padding:2rem 1.25rem}
942
- h1{margin:0 0 .75rem;color:#f87171;font-size:1.1rem}
943
- pre{white-space:pre-wrap;margin:0 0 1rem}
944
- .hint{opacity:.65;font-size:12px}
945
- </style>`;
946
- const body = `${style}
947
- <main data-vmz-error="500">
948
- <h1>Dev Error</h1>
949
- <pre>${msg}</pre>
950
- ${stack ? `<pre style="opacity:.7;font-size:12px">${stack}</pre>` : ''}
951
- <p class="hint">Dev host stayed up. Fix the source and save �?soft reload will recover.</p>
952
- </main>
953
- <script>
954
- (() => {
955
- const es = new EventSource("/__vmz/events");
956
- es.onmessage = (ev) => {
957
- let msg = null;
958
- try { msg = JSON.parse(ev.data); } catch {}
959
- if (msg && msg.type === "hmr") location.reload();
960
- };
961
- })();
962
- </script>`;
963
- try {
964
- const native = loadNativeAddon();
965
- if (typeof native.generateHtmlShell === 'function') {
966
- yield native.generateHtmlShell({
967
- title: 'Dev Error',
968
- lang: 'en',
969
- cssHrefs: [],
970
- bodyHtml: body,
971
- bodyAttrs: [],
972
- });
973
- return;
974
- }
975
- }
976
- catch {
977
- /* fall through to plain HTML �?never throw from the error page itself */
978
- }
979
- yield `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8" /><title>Dev Error</title></head><body>${body}</body></html>`;
980
- }
981
- function escapeHtml(s) {
982
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
983
- }
984
- /**
985
- * Resolve LocaleId for this request.
986
- * - `prefix`: LocaleId from URL path (existing).
987
- * - `none`: Host preference from cookie `vmz.locale` (validated), else defaultLocale.
988
- * URL never carries LocaleId.
989
- */
990
- function resolveLocalePath(pathname, cookieHeader) {
991
- const raw = String(pathname || '/');
992
- const normalized = raw.length > 1 && raw.endsWith('/') ? raw.slice(0, -1) : raw || '/';
993
- if (!localeArtifact) {
994
- return { localeId: 'en', dir: 'ltr', restPath: normalized, redirectTo: null };
995
- }
996
- const supported = (localeArtifact.locales || []).map((l) => l.id);
997
- const defaultLocale = localeArtifact.defaultLocale || supported[0] || 'en';
998
- const directions = Object.fromEntries((localeArtifact.locales || []).map((l) => [l.id, l.direction || 'ltr']));
999
- const routing = localeArtifact.routing || {};
1000
- const strategy = routing.strategy || 'prefix';
1001
- if (strategy === 'none') {
1002
- const preferred = readCookie(cookieHeader, LOCALE_STORE_KEY);
1003
- const localeId = preferred && supported.includes(preferred) ? preferred : defaultLocale;
1004
- return {
1005
- localeId,
1006
- dir: directions[localeId] || 'ltr',
1007
- restPath: normalized,
1008
- redirectTo: null,
1009
- };
1010
- }
1011
- const parts = normalized.split('/').filter(Boolean);
1012
- let localeId = null;
1013
- let restPath = normalized;
1014
- if (parts.length && supported.includes(parts[0])) {
1015
- localeId = parts[0];
1016
- const rest = parts.slice(1);
1017
- restPath = rest.length ? `/${rest.join('/')}` : '/';
1018
- }
1019
- // omit defaultPrefix: prefixed defaultLocale URL redirects to unprefixed canonical.
1020
- if (routing.defaultPrefix === 'omit' && localeId === defaultLocale) {
1021
- return {
1022
- localeId: defaultLocale,
1023
- dir: directions[defaultLocale] || 'ltr',
1024
- restPath,
1025
- redirectTo: restPath,
1026
- };
1027
- }
1028
- const contentLocale = localeId || defaultLocale;
1029
- return {
1030
- localeId: contentLocale,
1031
- dir: directions[contentLocale] || 'ltr',
1032
- restPath,
1033
- redirectTo: null,
1034
- };
1035
- }
1036
- function pageMetaAlternates(chunkId, localeId) {
1037
- if (!localeArtifact?.pageMetas)
1038
- return [];
1039
- const meta = localeArtifact.pageMetas.find((m) => m.routeId === chunkId && m.locale === localeId) ||
1040
- localeArtifact.pageMetas.find((m) => m.routeId === chunkId && m.locale === localeArtifact.defaultLocale);
1041
- return Array.isArray(meta?.alternates) ? meta.alternates : [];
1042
- }
1043
- function bustUrl(href) {
1044
- const u = new URL(href);
1045
- u.searchParams.set('t', String(reloadToken));
1046
- return u.href;
1047
- }
1048
- async function loadPageCtor(chunkId) {
1049
- const pageRel = `${chunkId}.client.js`;
1050
- const href = bustUrl(pathToFileURL(path.join(distDir, pageRel)).href);
1051
- const mod = await import(href);
1052
- pageCtors.set(chunkId, mod.default);
1053
- return mod.default;
1054
- }
1055
- /**
1056
- * Load compiled page catalog from `_vmz/route-catalog.json` (0.1.30 authority).
1057
- * Hosts must not re-parse deployment pathPattern into a live catalog.
1058
- */
1059
- async function listPageClientFiles(dir) {
1060
- const fromCatalog = await listPagesFromRouteCatalog(dir);
1061
- if (!fromCatalog.length) {
1062
- if (isDev) {
1063
- console.warn(`vmz dev: missing compiled ${ROUTE_CATALOG_SCHEMA} at ${path.join(dir, ...ROUTE_CATALOG_REL.split('/'))}`);
1064
- return [];
1065
- }
1066
- throw new Error(`vmz serve: missing compiled ${ROUTE_CATALOG_SCHEMA} at ${path.join(dir, ...ROUTE_CATALOG_REL.split('/'))}`);
1067
- }
1068
- return fromCatalog;
1069
- }
1070
- async function listPagesFromRouteCatalog(dir) {
1071
- const out = [];
1072
- try {
1073
- const raw = await readFile(path.join(dir, ...ROUTE_CATALOG_REL.split('/')), 'utf8');
1074
- const catalog = JSON.parse(raw);
1075
- if (catalog?.schema !== ROUTE_CATALOG_SCHEMA || !Array.isArray(catalog.pages))
1076
- return [];
1077
- for (const page of catalog.pages) {
1078
- const chunkId = String(page?.chunkId || '').replace(/\\/g, '/');
1079
- if (!chunkId.startsWith('pages/'))
1080
- continue;
1081
- const stem = chunkId.split('/').pop() || '';
1082
- if (isRouteBoundaryStem(stem))
1083
- continue;
1084
- const segs = Array.isArray(page?.segs) && page.segs.length ? page.segs : parsePathPattern(String(page?.pathPattern || ''));
1085
- const pageRel = String(page?.pageRel || `${chunkId}.client.js`).replace(/\\/g, '/');
1086
- out.push({
1087
- chunkId,
1088
- pageRel,
1089
- routeId: typeof page?.routeId === 'string' ? page.routeId : chunkId,
1090
- pathPattern: typeof page?.pathPattern === 'string' ? page.pathPattern : undefined,
1091
- segs,
1092
- });
1093
- }
1094
- }
1095
- catch {
1096
- return [];
1097
- }
1098
- return out;
1099
- }
1100
- /**
1101
- * Browser HTTP pattern (`/` / `/home` / `/users/:id` / `/blog/[...slug]`).
1102
- */
1103
- function parsePathPattern(pattern) {
1104
- const raw = String(pattern || '').trim();
1105
- if (!raw || raw === '/')
1106
- return [];
1107
- const parts = raw.replace(/^\/+/, '').split('/').filter(Boolean);
1108
- const segs = [];
1109
- for (const p of parts) {
1110
- if (isRouteGroupDir(p))
1111
- continue;
1112
- segs.push(parsePathSegment(p));
1113
- }
1114
- return segs;
1115
- }
1116
- function parsePathSegment(p) {
1117
- const catchAll = /^\[\.\.\.([^\]]+)\]$/.exec(p);
1118
- const param = /^\[([^\]]+)\]$/.exec(p);
1119
- const colon = /^:([A-Za-z_][\w]*)$/.exec(p);
1120
- if (catchAll)
1121
- return { kind: 'catch', name: catchAll[1] };
1122
- if (param)
1123
- return { kind: 'param', name: param[1] };
1124
- if (colon)
1125
- return { kind: 'param', name: colon[1] };
1126
- return { kind: 'static', value: p.toLowerCase() };
1127
- }
1128
- function isRouteGroupDir(seg) {
1129
- return typeof seg === 'string' && seg.startsWith('(') && seg.endsWith(')') && seg.length > 2;
1130
- }
1131
- function isRouteBoundaryStem(stem) {
1132
- return stem === 'Layout' || stem === 'Loading' || stem === 'Error' || stem === 'NotFound';
1133
- }
1134
- function matchFileRoute(pathname, catalog) {
1135
- const pathParts = decodeURIComponent(pathname.split('?')[0] || '/')
1136
- .replace(/\/+$/, '')
1137
- .split('/')
1138
- .filter(Boolean)
1139
- .map((p) => p.toLowerCase());
1140
- let best = null;
1141
- let bestScore = -1;
1142
- for (const page of catalog) {
1143
- const score = scoreRoute(page.segs, pathParts);
1144
- if (score == null)
1145
- continue;
1146
- if (score > bestScore) {
1147
- bestScore = score;
1148
- best = page;
1149
- }
1150
- }
1151
- return best;
1152
- }
1153
- function extractRouteParams(segs, pathname) {
1154
- const pathParts = decodeURIComponent(pathname.split('?')[0] || '/')
1155
- .replace(/\/+$/, '')
1156
- .split('/')
1157
- .filter(Boolean);
1158
- const params = {};
1159
- let j = 0;
1160
- for (let i = 0; i < segs.length; i++) {
1161
- const s = segs[i];
1162
- if (s.kind === 'catch') {
1163
- if (s.name)
1164
- params[s.name] = pathParts.slice(j).join('/');
1165
- return params;
1166
- }
1167
- if (j >= pathParts.length)
1168
- break;
1169
- if (s.kind === 'param' && s.name) {
1170
- params[s.name] = pathParts[j];
1171
- }
1172
- j++;
1173
- }
1174
- return params;
1175
- }
1176
- function scoreRoute(segs, pathParts) {
1177
- let i = 0;
1178
- let j = 0;
1179
- let score = 0;
1180
- while (i < segs.length) {
1181
- const s = segs[i];
1182
- if (s.kind === 'catch') {
1183
- // Required catch-all `[...slug]` needs �? remaining segment (not `/`).
1184
- if (j >= pathParts.length)
1185
- return null;
1186
- score += 1;
1187
- return score;
1188
- }
1189
- if (j >= pathParts.length)
1190
- return null;
1191
- if (s.kind === 'static') {
1192
- if (s.value !== pathParts[j])
1193
- return null;
1194
- score += 1000;
1195
- }
1196
- else if (s.kind === 'param') {
1197
- score += 100;
1198
- }
1199
- i++;
1200
- j++;
1201
- }
1202
- if (j !== pathParts.length)
1203
- return null;
1204
- return score + segs.length;
1205
- }
1206
- function isRootCatchAll(page) {
1207
- return page?.segs?.length === 1 && page.segs[0].kind === 'catch';
1208
- }
1209
- function findRootCatchAll(catalog) {
1210
- return catalog.find((p) => isRootCatchAll(p)) || null;
1211
- }
1212
- /**
1213
- * Optional app gate: `dist/vmz-route-gate.mjs` �?`{ check(pathname, chunkId) => 'not_found' | null }`.
1214
- */
1215
- async function runRouteGate(pathname, chunkId) {
1216
- try {
1217
- const href = bustUrl(pathToFileURL(path.join(distDir, 'vmz-route-gate.mjs')).href);
1218
- const mod = await import(href);
1219
- if (typeof mod.check !== 'function')
1220
- return null;
1221
- return await mod.check(pathname, chunkId ?? null);
1222
- }
1223
- catch {
1224
- return null;
1225
- }
1226
- }
1227
- function emitEntryClient(eager, lazy, token) {
1228
- const q = `?t=${token}`;
1229
- const native = loadNativeAddon();
1230
- if (typeof native.generateServeEntryClient !== 'function') {
1231
- throw new Error('vmz native addon missing generateServeEntryClient �?rebuild with `pnpm napi:build`');
1232
- }
1233
- return native.generateServeEntryClient(eager, lazy, q);
1234
- }
1235
- /**
1236
- * EventEntry zero-framework bootstrap: no static import of vmz-dom / page / islands.
1237
- * Framework bytes load only inside the first matching DOM event handler.
1238
- */
1239
- function emitEntryEvent(token) {
1240
- const q = `?t=${token}`;
1241
- const native = loadNativeAddon();
1242
- if (typeof native.generateServeEntryEvent !== 'function') {
1243
- throw new Error('vmz native addon missing generateServeEntryEvent �?rebuild with `pnpm napi:build`');
1244
- }
1245
- return native.generateServeEntryEvent(q);
1246
- }
1247
- /**
1248
- * Style Theme cookie / localStorage key (host contract, not a second theme API).
1249
- */
1250
- const THEME_STORE_KEY = 'vmz-theme';
1251
- const LOCALE_STORE_KEY = 'vmz.locale';
1252
- /**
1253
- * Cache-bust stylesheet entry for dev reload (token + serve revision).
1254
- */
1255
- function cssEntryWithBust(entry) {
1256
- if (!entry)
1257
- return undefined;
1258
- const base = String(entry).replace(/^\/+/, '');
1259
- const params = new URLSearchParams();
1260
- params.set('t', String(reloadToken));
1261
- if (styleBundleHash)
1262
- params.set('h', styleBundleHash);
1263
- return `${base}?${params.toString()}`;
1264
- }
1265
- async function loadDeploymentStyle(dir) {
1266
- try {
1267
- const raw = await readFile(path.join(dir, 'vmz-deployment.json'), 'utf8');
1268
- const dep = JSON.parse(raw);
1269
- const entry = dep.cssEntry;
1270
- const css = typeof entry === 'string' && entry.trim() ? entry.trim().replace(/^\/+/, '') : null;
1271
- const st = dep.styleTheme;
1272
- let theme = null;
1273
- if (st && typeof st === 'object') {
1274
- theme = {
1275
- defaultThemeId: String(st.defaultThemeId || 'default'),
1276
- themeIds: Array.isArray(st.themeIds) ? st.themeIds.map(String) : [],
1277
- activationAttr: String(st.activationAttr || 'data-theme'),
1278
- prefersColorScheme: st.prefersColorScheme && typeof st.prefersColorScheme === 'object'
1279
- ? Object.fromEntries(Object.entries(st.prefersColorScheme).map(([k, v]) => [String(k), String(v)]))
1280
- : {},
1281
- contentHash: st.contentHash ? String(st.contentHash) : null,
1282
- };
1283
- }
1284
- const bundleHash = typeof dep.styleBundleHash === 'string' && dep.styleBundleHash.trim() ? dep.styleBundleHash.trim() : null;
1285
- return { cssEntry: css, styleTheme: theme, styleBundleHash: bundleHash };
1286
- }
1287
- catch {
1288
- return { cssEntry: null, styleTheme: null, styleBundleHash: null };
1289
- }
1290
- }
1291
- /**
1292
- * Priority: `?theme=` �?cookie �?none (CSS `:root` + prefers-color-scheme media).
1293
- * Explicit ids (including default) always win over OS preference via activation attr.
1294
- */
1295
- function resolveThemeId(searchParams, cookieHeader) {
1296
- if (!styleTheme)
1297
- return null;
1298
- const ids = styleTheme.themeIds || [];
1299
- const q = searchParams && typeof searchParams.get === 'function' ? searchParams.get('theme') : null;
1300
- if (q && ids.includes(q))
1301
- return q;
1302
- const fromCookie = readCookie(cookieHeader, THEME_STORE_KEY);
1303
- if (fromCookie && ids.includes(fromCookie))
1304
- return fromCookie;
1305
- return null;
1306
- }
1307
- /**
1308
- * Always emit activation attr for an explicit theme id (incl. default) so it overrides OS media.
1309
- */
1310
- function htmlThemeAttrPair(themeId) {
1311
- if (!styleTheme || !themeId)
1312
- return [];
1313
- const attr = styleTheme.activationAttr || 'data-theme';
1314
- if (!(styleTheme.themeIds || []).includes(themeId))
1315
- return [];
1316
- return [attr, themeId];
1317
- }
1318
- /**
1319
- * Inline boot when SSR had no query/cookie: apply explicit `localStorage` only.
1320
- * No stored choice �?leave bare `<html>` so CSS `@media (prefers-color-scheme)` follows OS live.
1321
- * Explicit ids (incl. default) always set the activation attr so they override OS media.
1322
- */
1323
- function themeBootstrapScript() {
1324
- if (!styleTheme)
1325
- return '';
1326
- const attr = JSON.stringify(styleTheme.activationAttr || 'data-theme');
1327
- const ids = JSON.stringify(styleTheme.themeIds || []);
1328
- const key = JSON.stringify(THEME_STORE_KEY);
1329
- return ` <script>(function(){try{var k=${key},attr=${attr},ids=${ids};var id=localStorage.getItem(k);if(!id||ids.indexOf(id)<0)return;document.documentElement.setAttribute(attr,id);}catch(e){}})();</script>\n`;
1330
- }
1331
- /**
1332
- * LocaleId as client state (routing.strategy = none): apply localStorage before any
1333
- * page/client module runs so `#locales/*` pick the right variant. Prefix strategy
1334
- * keeps LocaleId in the URL �?no boot rewrite.
1335
- * Also mirrors into cookie so the next SSR negotiate sees Host preference.
1336
- */
1337
- function localeBootstrapScript() {
1338
- if (!localeArtifact)
1339
- return '';
1340
- const routing = localeArtifact.routing || {};
1341
- if ((routing.strategy || 'prefix') !== 'none')
1342
- return '';
1343
- const ids = (localeArtifact.locales || []).map((l) => l.id).filter(Boolean);
1344
- if (!ids.length)
1345
- return '';
1346
- const key = JSON.stringify(LOCALE_STORE_KEY);
1347
- const idList = JSON.stringify(ids);
1348
- return ` <script>(function(){try{var k=${key},ids=${idList};var id=localStorage.getItem(k);if(!id||ids.indexOf(id)<0)return;document.documentElement.setAttribute("data-locale",id);document.documentElement.setAttribute("lang",id);window.__vmzLocaleIdHint=id;document.cookie=k+"="+encodeURIComponent(id)+"; path=/; max-age=31536000; SameSite=Lax";}catch(e){}})();</script>\n`;
1349
- }
1350
- /**
1351
- * Site favicon links from build artifact `_vmz/site-favicon.json` (author SVG �?PNG/ICO).
1352
- * Empty when skipped / missing �?do not invent broken <link>s.
1353
- */
1354
- function siteFaviconHeadHtml() {
1355
- try {
1356
- const p = path.join(distDir, '_vmz', 'site-favicon.json');
1357
- if (!existsSync(p))
1358
- return '';
1359
- // Sync read: head is per-request; file is tiny and rebuilt with dist.
1360
- const raw = readFileSync(p, 'utf8');
1361
- const j = JSON.parse(raw);
1362
- if (j?.status !== 'ready' || typeof j.headHtml !== 'string')
1363
- return '';
1364
- return j.headHtml;
1365
- }
1366
- catch {
1367
- return '';
1368
- }
1369
- }
1370
- function readCookie(header, name) {
1371
- if (!header)
1372
- return null;
1373
- const parts = String(header).split(';');
1374
- for (const part of parts) {
1375
- const idx = part.indexOf('=');
1376
- if (idx < 0)
1377
- continue;
1378
- const k = part.slice(0, idx).trim();
1379
- if (k !== name)
1380
- continue;
1381
- try {
1382
- return decodeURIComponent(part.slice(idx + 1).trim());
1383
- }
1384
- catch {
1385
- return part.slice(idx + 1).trim();
1386
- }
1387
- }
1388
- return null;
1389
- }
1390
- async function loadCssEntry(dir) {
1391
- const meta = await loadDeploymentStyle(dir);
1392
- return meta.cssEntry;
1393
- }
1394
- async function loadPageResumeEntries(dir, chunkId) {
1395
- try {
1396
- const raw = await readFile(path.join(dir, 'vmz-deployment.json'), 'utf8');
1397
- const dep = JSON.parse(raw);
1398
- const units = Array.isArray(dep.units) ? dep.units : [];
1399
- const page = units.find((u) => u.chunkId === chunkId) || units.find((u) => u.chunkId === 'pages/index') || units.find((u) => u.kind === 'page');
1400
- const entries = Array.isArray(page?.resumeEntries) ? page.resumeEntries : [];
1401
- return entries.map((e) => ({
1402
- component: String(e.component || ''),
1403
- strategy: String(e.strategy || ''),
1404
- }));
1405
- }
1406
- catch {
1407
- return [];
1408
- }
1409
- }
1410
- function isEventStrategy(strategy) {
1411
- return strategy === 'event' || strategy === 'click' || strategy.startsWith('event:');
1412
- }
1413
- function isEventOnlyShell(strategies) {
1414
- if (!strategies.length)
1415
- return false;
1416
- return strategies.every((s) => isEventStrategy(s));
1417
- }
1418
- function escapeAttr(s) {
1419
- return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
1420
- }
1
+ import { bootstrapServeHost } from './serve/bootstrap.js';
2
+ import { startServeHttp } from './serve/http.js';
3
+ await bootstrapServeHost();
4
+ startServeHttp();