@vmz/core 0.2.5 → 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
@@ -0,0 +1,294 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { readFile, writeFile } from 'node:fs/promises';
3
+ import { registerHooks } from 'node:module';
4
+ import path from 'node:path';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import { listClientComponents } from '../list-client-components.js';
7
+ import { LOCALE_LINK_PLAN_SCHEMA, linkRouteAliasesFromUnits } from '../localize-body-links.js';
8
+ import { loadNativeAddon } from '../native-addon.js';
9
+ import { createRenderHost } from '../render/render-host.js';
10
+ import { setRoutes } from '../../faces/vmz-runtime.js';
11
+ import { LOCALE_LINK_PLAN_REL } from './constants.js';
12
+ import { loadDeploymentStyle } from './locale-theme.js';
13
+ import { bustUrl, isEventOnlyShell, isEventStrategy, listPageClientFiles, loadPageResumeEntries, pageNeedsReload } from './page-catalog.js';
14
+ import { serveState } from './state.js';
15
+ export function installDevImportTokenHooks() {
16
+ if (!serveState.isDev)
17
+ return;
18
+ const distUrlPrefix = pathToFileURL(serveState.distDir.endsWith(path.sep) ? serveState.distDir : `${serveState.distDir}${path.sep}`).href;
19
+ registerHooks({
20
+ resolve(specifier, context, nextResolve) {
21
+ const result = nextResolve(specifier, context);
22
+ if (!specifier.startsWith('.') || !context.parentURL || !result?.url)
23
+ return result;
24
+ let token = '';
25
+ try {
26
+ token = new URL(context.parentURL).searchParams.get('t') || '';
27
+ }
28
+ catch {
29
+ return result;
30
+ }
31
+ if (!token)
32
+ return result;
33
+ if (!result.url.startsWith('file:'))
34
+ return result;
35
+ if (!result.url.startsWith(distUrlPrefix)) {
36
+ try {
37
+ if (!fileURLToPath(result.url).startsWith(serveState.distDir))
38
+ return result;
39
+ }
40
+ catch {
41
+ return result;
42
+ }
43
+ }
44
+ const u = new URL(result.url);
45
+ if (u.searchParams.get('t') === token)
46
+ return result;
47
+ u.searchParams.set('t', token);
48
+ return { ...result, url: u.href, shortCircuit: true };
49
+ },
50
+ });
51
+ }
52
+ function attachLinkRouteAliases(artifact, dir) {
53
+ if (!artifact || typeof artifact !== 'object')
54
+ return artifact;
55
+ try {
56
+ const dep = JSON.parse(readFileSync(path.join(dir, 'vmz-deployment.json'), 'utf8'));
57
+ const aliases = linkRouteAliasesFromUnits(Array.isArray(dep.units) ? dep.units : []);
58
+ if (!aliases.length)
59
+ return artifact;
60
+ return { ...artifact, linkRouteAliases: aliases };
61
+ }
62
+ catch {
63
+ return artifact;
64
+ }
65
+ }
66
+ export function notifySse(event) {
67
+ for (const client of [...serveState.sseClients]) {
68
+ try {
69
+ client.write(`data: ${event}\n\n`);
70
+ }
71
+ catch {
72
+ serveState.sseClients.delete(client);
73
+ }
74
+ }
75
+ }
76
+ export function normalizeDevError(err) {
77
+ if (err && typeof err === 'object') {
78
+ const e = err;
79
+ return {
80
+ message: e.message ? String(e.message) : String(err),
81
+ stack: e.stack ? String(e.stack) : undefined,
82
+ at: Date.now(),
83
+ };
84
+ }
85
+ return { message: String(err), at: Date.now() };
86
+ }
87
+ export async function* emitDevErrorHtml(err) {
88
+ const msg = escapeHtml(err.message || 'Unknown error');
89
+ const stack = err.stack ? escapeHtml(err.stack) : '';
90
+ const style = `<style>
91
+ body{margin:0;background:#0f1115;color:#f4f4f5;font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
92
+ main{max-width:56rem;margin:0 auto;padding:2rem 1.25rem}
93
+ h1{margin:0 0 .75rem;color:#f87171;font-size:1.1rem}
94
+ pre{white-space:pre-wrap;margin:0 0 1rem}
95
+ .hint{opacity:.65;font-size:12px}
96
+ </style>`;
97
+ const body = `${style}
98
+ <main data-vmz-error="500">
99
+ <h1>Dev Error</h1>
100
+ <pre>${msg}</pre>
101
+ ${stack ? `<pre style="opacity:.7;font-size:12px">${stack}</pre>` : ''}
102
+ <p class="hint">Dev host stayed up. Fix the source and save — soft reload will recover.</p>
103
+ </main>
104
+ <script>
105
+ (() => {
106
+ const es = new EventSource("/__vmz/events");
107
+ es.onmessage = (ev) => {
108
+ let msg = null;
109
+ try { msg = JSON.parse(ev.data); } catch {}
110
+ if (msg && msg.type === "hmr") location.reload();
111
+ };
112
+ })();
113
+ </script>`;
114
+ try {
115
+ const native = loadNativeAddon();
116
+ if (typeof native.generateHtmlShell === 'function') {
117
+ yield native.generateHtmlShell({
118
+ title: 'Dev Error',
119
+ lang: 'en',
120
+ cssHrefs: [],
121
+ bodyHtml: body,
122
+ bodyAttrs: [],
123
+ });
124
+ return;
125
+ }
126
+ }
127
+ catch {
128
+ /* fall through */
129
+ }
130
+ yield `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8" /><title>Dev Error</title></head><body>${body}</body></html>`;
131
+ }
132
+ function escapeHtml(s) {
133
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
134
+ }
135
+ function emitEntryClient(eager, lazy, token) {
136
+ const q = `?t=${token}`;
137
+ const native = loadNativeAddon();
138
+ if (typeof native.generateServeEntryClient !== 'function') {
139
+ throw new Error('vmz native addon missing generateServeEntryClient — rebuild with `pnpm napi:build`');
140
+ }
141
+ return native.generateServeEntryClient(eager, lazy, q);
142
+ }
143
+ function emitEntryEvent(token) {
144
+ const q = `?t=${token}`;
145
+ const native = loadNativeAddon();
146
+ if (typeof native.generateServeEntryEvent !== 'function') {
147
+ throw new Error('vmz native addon missing generateServeEntryEvent — rebuild with `pnpm napi:build`');
148
+ }
149
+ return native.generateServeEntryEvent(q);
150
+ }
151
+ export async function softReload(opts = {}) {
152
+ const prevToken = serveState.reloadToken;
153
+ const prevCatalog = serveState.pageCatalog;
154
+ const prevCtors = new Map(serveState.pageCtors);
155
+ const nextToken = Date.now();
156
+ serveState.reloadToken = nextToken;
157
+ const affected = opts.payload?.affectedChunks ?? [];
158
+ const seeds = opts.payload?.seedChunks ?? [];
159
+ const full = opts.payload?.full;
160
+ const islandHmr = Boolean(opts.payload?.islandHmr);
161
+ const rerunLoaders = opts.payload?.rerunLoaders ?? [];
162
+ const skipEntryRewrite = Boolean(opts.payload?.skipEntryRewrite);
163
+ const buildId = opts.payload?.buildId != null ? String(opts.payload.buildId) : null;
164
+ const sourceRevision = opts.payload?.sourceRevision != null ? String(opts.payload.sourceRevision) : null;
165
+ const bundleRevision = opts.payload?.bundleRevision != null ? String(opts.payload.bundleRevision) : null;
166
+ if (buildId)
167
+ serveState.lastDevBuildId = buildId;
168
+ const reloadAllPages = Boolean(full) || (!islandHmr && rerunLoaders.length === 0 && affected.length === 0);
169
+ try {
170
+ try {
171
+ const routes = JSON.parse(await readFile(path.join(serveState.distDir, 'vmz-routes.json'), 'utf8'));
172
+ setRoutes(routes);
173
+ }
174
+ catch {
175
+ setRoutes([]);
176
+ }
177
+ try {
178
+ serveState.localeArtifact = JSON.parse(await readFile(path.join(serveState.distDir, '_vmz', 'locale-route-realization.json'), 'utf8'));
179
+ serveState.localeArtifact = attachLinkRouteAliases(serveState.localeArtifact, serveState.distDir);
180
+ }
181
+ catch {
182
+ serveState.localeArtifact = null;
183
+ }
184
+ try {
185
+ const rawPlan = JSON.parse(await readFile(path.join(serveState.distDir, ...LOCALE_LINK_PLAN_REL.split('/')), 'utf8'));
186
+ serveState.localeLinkPlan = rawPlan?.schema === LOCALE_LINK_PLAN_SCHEMA && Array.isArray(rawPlan.rows) ? rawPlan : null;
187
+ }
188
+ catch {
189
+ serveState.localeLinkPlan = null;
190
+ }
191
+ const componentEntries = await listClientComponents(serveState.distDir, { strict: !serveState.isDev });
192
+ serveState.ssrRenderHost = await createRenderHost(serveState.distDir, {
193
+ strictDeployment: !serveState.isDev,
194
+ preload: 'none',
195
+ cacheBust: nextToken,
196
+ });
197
+ const nextCatalog = await listPageClientFiles(serveState.distDir);
198
+ const nextCtors = new Map();
199
+ if (!islandHmr) {
200
+ const reloadSet = rerunLoaders.length > 0
201
+ ? rerunLoaders
202
+ : reloadAllPages
203
+ ? nextCatalog.map((p) => p.chunkId)
204
+ : nextCatalog.filter((p) => pageNeedsReload(p.chunkId, affected)).map((p) => p.chunkId);
205
+ const pagesToLoad = nextCatalog.filter((p) => reloadSet.includes(p.chunkId));
206
+ for (const p of pagesToLoad) {
207
+ const pageRel = `${p.chunkId}.client.js`;
208
+ const href = bustUrl(pathToFileURL(path.join(serveState.distDir, pageRel)).href);
209
+ const mod = await import(href);
210
+ nextCtors.set(p.chunkId, mod.default);
211
+ }
212
+ }
213
+ serveState.pageCatalog = nextCatalog;
214
+ if (!islandHmr) {
215
+ if (reloadAllPages) {
216
+ serveState.pageCtors.clear();
217
+ for (const [k, v] of nextCtors)
218
+ serveState.pageCtors.set(k, v);
219
+ }
220
+ else {
221
+ for (const [k, v] of nextCtors)
222
+ serveState.pageCtors.set(k, v);
223
+ for (const id of [...serveState.pageCtors.keys()]) {
224
+ if (!nextCatalog.some((p) => p.chunkId === id))
225
+ serveState.pageCtors.delete(id);
226
+ }
227
+ }
228
+ }
229
+ const indexChunk = serveState.pageCatalog.find((p) => p.chunkId === 'pages/index')?.chunkId || serveState.pageCatalog[0].chunkId;
230
+ const resumeEntries = await loadPageResumeEntries(serveState.distDir, indexChunk);
231
+ const styleMeta = await loadDeploymentStyle(serveState.distDir);
232
+ serveState.cssEntry = styleMeta.cssEntry;
233
+ serveState.styleBundleHash = styleMeta.styleBundleHash;
234
+ serveState.styleTheme = styleMeta.styleTheme;
235
+ const lazyEventNames = resumeEntries
236
+ .filter((e) => isEventStrategy(e.strategy))
237
+ .map((e) => e.component)
238
+ .filter(Boolean);
239
+ const lazySet = new Set(lazyEventNames);
240
+ await writeFile(path.join(serveState.distDir, 'entry-client.js'), emitEntryClient(componentEntries.filter((e) => !lazySet.has(e.name)), componentEntries.filter((e) => lazySet.has(e.name)), skipEntryRewrite ? serveState.reloadToken : nextToken), 'utf8');
241
+ const strategies = resumeEntries.map((e) => e.strategy);
242
+ const eventOnlyShell = isEventOnlyShell(strategies);
243
+ await writeFile(path.join(serveState.distDir, 'entry-event.js'), emitEntryEvent(skipEntryRewrite ? serveState.reloadToken : nextToken), 'utf8');
244
+ serveState.lastDevError = null;
245
+ const mode = islandHmr ? 'island' : eventOnlyShell ? 'event-shell' : 'full';
246
+ notifySse(JSON.stringify({
247
+ type: 'hmr',
248
+ mode,
249
+ affectedChunks: affected,
250
+ seedChunks: seeds,
251
+ token: serveState.reloadToken,
252
+ buildId: buildId || serveState.lastDevBuildId,
253
+ sourceRevision,
254
+ bundleRevision,
255
+ serveRevision: String(serveState.reloadToken),
256
+ full: Boolean(full),
257
+ eventOnlyShell,
258
+ }));
259
+ if (!opts.quiet) {
260
+ const aff = affected.length > 0 ? ` affected=[${affected.join(', ')}]` : full === false ? ' affected=[]' : '';
261
+ const scope = islandHmr ? 'island' : reloadAllPages ? 'all-pages' : `pages=${nextCtors.size}`;
262
+ const bid = buildId || serveState.lastDevBuildId;
263
+ const rev = bid || sourceRevision || bundleRevision
264
+ ? ` buildId=${bid || '-'} source=${sourceRevision || '-'} bundle=${bundleRevision || '-'} serve=${serveState.reloadToken}`
265
+ : ` t=${serveState.reloadToken}`;
266
+ console.log(`vmz serve: soft reload ok (mode=${mode}; ${scope}; catalog=${serveState.pageCatalog.length};${rev}${aff})`);
267
+ }
268
+ return {
269
+ affectedChunks: affected,
270
+ seedChunks: seeds,
271
+ full: Boolean(full),
272
+ islandHmr,
273
+ mode,
274
+ eventOnlyShell,
275
+ pageCount: serveState.pageCatalog.length,
276
+ reloadedPages: islandHmr ? 0 : nextCtors.size,
277
+ reloadAllPages,
278
+ buildId: buildId || serveState.lastDevBuildId,
279
+ sourceRevision,
280
+ bundleRevision,
281
+ serveRevision: String(serveState.reloadToken),
282
+ token: serveState.reloadToken,
283
+ };
284
+ }
285
+ catch (err) {
286
+ serveState.reloadToken = prevToken;
287
+ serveState.pageCatalog = prevCatalog;
288
+ serveState.pageCtors.clear();
289
+ for (const [k, v] of prevCtors)
290
+ serveState.pageCtors.set(k, v);
291
+ serveState.lastDevError = normalizeDevError(err);
292
+ throw err;
293
+ }
294
+ }
@@ -0,0 +1 @@
1
+ export declare function startServeHttp(): void;
@@ -0,0 +1,124 @@
1
+ import http from 'node:http';
2
+ import { handleNodeRequest } from '../../faces/vmz-runtime.js';
3
+ import { SHUTDOWN_TIMEOUT_MS } from './constants.js';
4
+ import { normalizeDevError, notifySse, softReload } from './hmr-dev.js';
5
+ import { renderPage, renderPageStream } from './page-render.js';
6
+ import { serveState } from './state.js';
7
+ function readRequestBody(req) {
8
+ return new Promise((resolve, reject) => {
9
+ const chunks = [];
10
+ req.on('data', (c) => chunks.push(c));
11
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
12
+ req.on('error', reject);
13
+ });
14
+ }
15
+ async function gracefulShutdown(signal) {
16
+ if (serveState.shuttingDown)
17
+ return;
18
+ serveState.shuttingDown = true;
19
+ serveState.ready = false;
20
+ console.log(`vmz serve: ${signal} — draining in-flight=${serveState.inFlight} timeout=${SHUTDOWN_TIMEOUT_MS}ms`);
21
+ serveState.server?.close();
22
+ const start = Date.now();
23
+ while (serveState.inFlight > 0 && Date.now() - start < SHUTDOWN_TIMEOUT_MS) {
24
+ await new Promise((r) => setTimeout(r, 25));
25
+ }
26
+ for (const client of serveState.sseClients) {
27
+ try {
28
+ client.end();
29
+ }
30
+ catch {
31
+ /* ignore */
32
+ }
33
+ }
34
+ serveState.sseClients.clear();
35
+ process.exit(serveState.inFlight > 0 ? 1 : 0);
36
+ }
37
+ export function startServeHttp() {
38
+ serveState.server = http.createServer((req, res) => {
39
+ const url = new URL(req.url || '/', `http://${serveState.host}:${serveState.port}`);
40
+ if (url.pathname === '/__vmz/health' && req.method === 'GET') {
41
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
42
+ res.end(JSON.stringify({ status: 'ok', shuttingDown: serveState.shuttingDown, inFlight: serveState.inFlight }));
43
+ return;
44
+ }
45
+ if (url.pathname === '/__vmz/ready' && req.method === 'GET') {
46
+ if (!serveState.ready || serveState.shuttingDown) {
47
+ res.writeHead(503, { 'content-type': 'application/json', 'cache-control': 'no-store' });
48
+ res.end(JSON.stringify({ status: 'not-ready', ready: serveState.ready, shuttingDown: serveState.shuttingDown }));
49
+ return;
50
+ }
51
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
52
+ res.end(JSON.stringify({ status: 'ready', ready: true, inFlight: serveState.inFlight }));
53
+ return;
54
+ }
55
+ if (serveState.shuttingDown) {
56
+ res.writeHead(503, { 'content-type': 'application/json', 'cache-control': 'no-store' });
57
+ res.end(JSON.stringify({ status: 'shutting-down' }));
58
+ return;
59
+ }
60
+ serveState.inFlight += 1;
61
+ let settled = false;
62
+ const done = () => {
63
+ if (settled)
64
+ return;
65
+ settled = true;
66
+ serveState.inFlight = Math.max(0, serveState.inFlight - 1);
67
+ };
68
+ res.on('finish', done);
69
+ res.on('close', done);
70
+ if (url.pathname === '/__vmz/reload' && req.method === 'POST') {
71
+ readRequestBody(req)
72
+ .then((raw) => {
73
+ let payload = {};
74
+ try {
75
+ payload = raw ? JSON.parse(String(raw)) : {};
76
+ }
77
+ catch {
78
+ payload = {};
79
+ }
80
+ return softReload({ payload });
81
+ })
82
+ .then((info) => {
83
+ res.writeHead(200, { 'content-type': 'application/json' });
84
+ res.end(JSON.stringify({ ok: true, token: serveState.reloadToken, ...info }));
85
+ })
86
+ .catch((err) => {
87
+ console.error('vmz serve: soft reload failed', err);
88
+ serveState.lastDevError = normalizeDevError(err);
89
+ notifySse(JSON.stringify({
90
+ type: 'error',
91
+ message: serveState.lastDevError.message,
92
+ stack: serveState.lastDevError.stack,
93
+ at: serveState.lastDevError.at,
94
+ }));
95
+ res.writeHead(500, { 'content-type': 'application/json' });
96
+ res.end(JSON.stringify({ ok: false, error: serveState.lastDevError.message }));
97
+ });
98
+ return;
99
+ }
100
+ if (url.pathname === '/__vmz/events' && req.method === 'GET') {
101
+ res.writeHead(200, {
102
+ 'content-type': 'text/event-stream',
103
+ 'cache-control': 'no-cache',
104
+ connection: 'keep-alive',
105
+ });
106
+ res.write(': connected\n\n');
107
+ serveState.sseClients.add(res);
108
+ req.on('close', () => {
109
+ serveState.sseClients.delete(res);
110
+ });
111
+ return;
112
+ }
113
+ handleNodeRequest(req, res, { distDir: serveState.distDir, renderPage, renderPageStream });
114
+ });
115
+ serveState.server.listen(serveState.port, serveState.host, () => {
116
+ console.log(`vmz serve http://${serveState.host}:${serveState.port} (dist=${serveState.distDir}${serveState.isDev ? ', dev' : ''})`);
117
+ });
118
+ process.on('SIGTERM', () => {
119
+ void gracefulShutdown('SIGTERM');
120
+ });
121
+ process.on('SIGINT', () => {
122
+ void gracefulShutdown('SIGINT');
123
+ });
124
+ }
@@ -0,0 +1,19 @@
1
+ export declare function resolveLocalePath(pathname: string, cookieHeader: string | undefined): {
2
+ localeId: any;
3
+ dir: any;
4
+ restPath: string;
5
+ redirectTo: string;
6
+ };
7
+ export declare function pageMetaAlternates(chunkId: string, localeId: string): any;
8
+ export declare function cssEntryWithBust(entry: string | null): string;
9
+ export declare function loadDeploymentStyle(dir: string): Promise<{
10
+ cssEntry: string;
11
+ styleTheme: any;
12
+ styleBundleHash: any;
13
+ }>;
14
+ export declare function resolveThemeId(searchParams: URLSearchParams | undefined, cookieHeader: string | undefined): string;
15
+ export declare function htmlThemeAttrPair(themeId: string | null): any[];
16
+ export declare function themeBootstrapScript(): string;
17
+ export declare function localeBootstrapScript(): string;
18
+ export declare function siteFaviconHeadHtml(): any;
19
+ export declare function readCookie(header: string | undefined, name: string): string;
@@ -0,0 +1,169 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { LOCALE_STORE_KEY, THEME_STORE_KEY } from './constants.js';
5
+ import { serveState } from './state.js';
6
+ export function resolveLocalePath(pathname, cookieHeader) {
7
+ const raw = String(pathname || '/');
8
+ const normalized = raw.length > 1 && raw.endsWith('/') ? raw.slice(0, -1) : raw || '/';
9
+ if (!serveState.localeArtifact) {
10
+ return { localeId: 'en', dir: 'ltr', restPath: normalized, redirectTo: null };
11
+ }
12
+ const supported = (serveState.localeArtifact.locales || []).map((l) => l.id);
13
+ const defaultLocale = serveState.localeArtifact.defaultLocale || supported[0] || 'en';
14
+ const directions = Object.fromEntries((serveState.localeArtifact.locales || []).map((l) => [l.id, l.direction || 'ltr']));
15
+ const routing = serveState.localeArtifact.routing || {};
16
+ const strategy = routing.strategy || 'prefix';
17
+ if (strategy === 'none') {
18
+ const preferred = readCookie(cookieHeader, LOCALE_STORE_KEY);
19
+ const localeId = preferred && supported.includes(preferred) ? preferred : defaultLocale;
20
+ return {
21
+ localeId,
22
+ dir: directions[localeId] || 'ltr',
23
+ restPath: normalized,
24
+ redirectTo: null,
25
+ };
26
+ }
27
+ const parts = normalized.split('/').filter(Boolean);
28
+ let localeId = null;
29
+ let restPath = normalized;
30
+ if (parts.length && supported.includes(parts[0])) {
31
+ localeId = parts[0];
32
+ const rest = parts.slice(1);
33
+ restPath = rest.length ? `/${rest.join('/')}` : '/';
34
+ }
35
+ if (routing.defaultPrefix === 'omit' && localeId === defaultLocale) {
36
+ return {
37
+ localeId: defaultLocale,
38
+ dir: directions[defaultLocale] || 'ltr',
39
+ restPath,
40
+ redirectTo: restPath,
41
+ };
42
+ }
43
+ const contentLocale = localeId || defaultLocale;
44
+ return {
45
+ localeId: contentLocale,
46
+ dir: directions[contentLocale] || 'ltr',
47
+ restPath,
48
+ redirectTo: null,
49
+ };
50
+ }
51
+ export function pageMetaAlternates(chunkId, localeId) {
52
+ if (!serveState.localeArtifact?.pageMetas)
53
+ return [];
54
+ const meta = serveState.localeArtifact.pageMetas.find((m) => m.routeId === chunkId && m.locale === localeId) ||
55
+ serveState.localeArtifact.pageMetas.find((m) => m.routeId === chunkId && m.locale === serveState.localeArtifact.defaultLocale);
56
+ return Array.isArray(meta?.alternates) ? meta.alternates : [];
57
+ }
58
+ export function cssEntryWithBust(entry) {
59
+ if (!entry)
60
+ return undefined;
61
+ const base = String(entry).replace(/^\/+/, '');
62
+ const params = new URLSearchParams();
63
+ params.set('t', String(serveState.reloadToken));
64
+ if (serveState.styleBundleHash)
65
+ params.set('h', serveState.styleBundleHash);
66
+ return `${base}?${params.toString()}`;
67
+ }
68
+ export async function loadDeploymentStyle(dir) {
69
+ try {
70
+ const raw = await readFile(path.join(dir, 'vmz-deployment.json'), 'utf8');
71
+ const dep = JSON.parse(raw);
72
+ const entry = dep.cssEntry;
73
+ const css = typeof entry === 'string' && entry.trim() ? entry.trim().replace(/^\/+/, '') : null;
74
+ const st = dep.styleTheme;
75
+ let theme = null;
76
+ if (st && typeof st === 'object') {
77
+ theme = {
78
+ defaultThemeId: String(st.defaultThemeId || 'default'),
79
+ themeIds: Array.isArray(st.themeIds) ? st.themeIds.map(String) : [],
80
+ activationAttr: String(st.activationAttr || 'data-theme'),
81
+ prefersColorScheme: st.prefersColorScheme && typeof st.prefersColorScheme === 'object'
82
+ ? Object.fromEntries(Object.entries(st.prefersColorScheme).map(([k, v]) => [String(k), String(v)]))
83
+ : {},
84
+ contentHash: st.contentHash ? String(st.contentHash) : null,
85
+ };
86
+ }
87
+ const bundleHash = typeof dep.styleBundleHash === 'string' && dep.styleBundleHash.trim() ? dep.styleBundleHash.trim() : null;
88
+ return { cssEntry: css, styleTheme: theme, styleBundleHash: bundleHash };
89
+ }
90
+ catch {
91
+ return { cssEntry: null, styleTheme: null, styleBundleHash: null };
92
+ }
93
+ }
94
+ export function resolveThemeId(searchParams, cookieHeader) {
95
+ if (!serveState.styleTheme)
96
+ return null;
97
+ const ids = serveState.styleTheme.themeIds || [];
98
+ const q = searchParams && typeof searchParams.get === 'function' ? searchParams.get('theme') : null;
99
+ if (q && ids.includes(q))
100
+ return q;
101
+ const fromCookie = readCookie(cookieHeader, THEME_STORE_KEY);
102
+ if (fromCookie && ids.includes(fromCookie))
103
+ return fromCookie;
104
+ return null;
105
+ }
106
+ export function htmlThemeAttrPair(themeId) {
107
+ if (!serveState.styleTheme || !themeId)
108
+ return [];
109
+ const attr = serveState.styleTheme.activationAttr || 'data-theme';
110
+ if (!(serveState.styleTheme.themeIds || []).includes(themeId))
111
+ return [];
112
+ return [attr, themeId];
113
+ }
114
+ export function themeBootstrapScript() {
115
+ if (!serveState.styleTheme)
116
+ return '';
117
+ const attr = JSON.stringify(serveState.styleTheme.activationAttr || 'data-theme');
118
+ const ids = JSON.stringify(serveState.styleTheme.themeIds || []);
119
+ const key = JSON.stringify(THEME_STORE_KEY);
120
+ 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`;
121
+ }
122
+ export function localeBootstrapScript() {
123
+ if (!serveState.localeArtifact)
124
+ return '';
125
+ const routing = serveState.localeArtifact.routing || {};
126
+ if ((routing.strategy || 'prefix') !== 'none')
127
+ return '';
128
+ const ids = (serveState.localeArtifact.locales || []).map((l) => l.id).filter(Boolean);
129
+ if (!ids.length)
130
+ return '';
131
+ const key = JSON.stringify(LOCALE_STORE_KEY);
132
+ const idList = JSON.stringify(ids);
133
+ 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`;
134
+ }
135
+ export function siteFaviconHeadHtml() {
136
+ try {
137
+ const p = path.join(serveState.distDir, '_vmz', 'site-favicon.json');
138
+ if (!existsSync(p))
139
+ return '';
140
+ const raw = readFileSync(p, 'utf8');
141
+ const j = JSON.parse(raw);
142
+ if (j?.status !== 'ready' || typeof j.headHtml !== 'string')
143
+ return '';
144
+ return j.headHtml;
145
+ }
146
+ catch {
147
+ return '';
148
+ }
149
+ }
150
+ export function readCookie(header, name) {
151
+ if (!header)
152
+ return null;
153
+ const parts = String(header).split(';');
154
+ for (const part of parts) {
155
+ const idx = part.indexOf('=');
156
+ if (idx < 0)
157
+ continue;
158
+ const k = part.slice(0, idx).trim();
159
+ if (k !== name)
160
+ continue;
161
+ try {
162
+ return decodeURIComponent(part.slice(idx + 1).trim());
163
+ }
164
+ catch {
165
+ return part.slice(idx + 1).trim();
166
+ }
167
+ }
168
+ return null;
169
+ }
@@ -0,0 +1,14 @@
1
+ export declare function bustUrl(href: string): string;
2
+ export declare function loadPageCtor(chunkId: string): Promise<any>;
3
+ export declare function listPageClientFiles(dir: string): Promise<{
4
+ chunkId: string;
5
+ pageRel: string;
6
+ routeId: string;
7
+ pathPattern?: string;
8
+ segs: string[];
9
+ }[]>;
10
+ export declare function runRouteGate(pathname: string, chunkId: string | undefined): Promise<any>;
11
+ export declare function pageNeedsReload(chunkId: string, affected: string[]): boolean;
12
+ export declare function loadPageResumeEntries(dir: string, chunkId: string): Promise<any>;
13
+ export declare function isEventStrategy(strategy: string): boolean;
14
+ export declare function isEventOnlyShell(strategies: string[]): boolean;