@vmz/core 0.1.7 → 0.1.9

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.
@@ -437,10 +437,11 @@ export function installClientNavigation(opts = {}) {
437
437
  let localeTransitionGeneration = 0;
438
438
  /**
439
439
  * Atomic LocaleTransition (browser host slice):
440
- * validate → realize path → navigate/fetch → commit locale attrs from SSR HTML.
440
+ * - prefix: validate → realize path → navigate/fetch → commit locale attrs from SSR HTML
441
+ * - none: validate → Host persist (localStorage+cookie) → commit attrs → reload (v1; no URL rewrite)
441
442
  * Failure keeps the previous locale surface (no half-page commit).
442
443
  * @param {string} toLocale
443
- * @param {{ replace?: boolean }} [opts]
444
+ * @param {{ replace?: boolean, reload?: boolean }} [opts]
444
445
  */
445
446
  async function transitionLocale(toLocale, opts = {}) {
446
447
  const fromLocale = doc.documentElement?.getAttribute('data-locale') || null;
@@ -480,6 +481,10 @@ export function installClientNavigation(opts = {}) {
480
481
  win.__vmzLastLocaleTransition = out;
481
482
  return out;
482
483
  }
484
+ const strategy = routing.strategy || 'prefix';
485
+ if (strategy === 'none') {
486
+ return transitionLocaleNone(toLocale, fromLocale, opts);
487
+ }
483
488
  const gen = ++localeTransitionGeneration;
484
489
  const targetHref = realizePathForLocale(loc.pathname + loc.search + loc.hash, toLocale, routing);
485
490
  const result = await transitionTo(targetHref, { replace: opts.replace !== false, softFail: true });
@@ -539,6 +544,64 @@ export function installClientNavigation(opts = {}) {
539
544
  win.__vmzLastLocaleTransition = out;
540
545
  return out;
541
546
  }
547
+ /**
548
+ * `routing.strategy: 'none'` — LocaleId is Host preference, not URL.
549
+ * Persist → commit document attrs + hint → full reload so `#locales/*` re-resolve (I2 v1).
550
+ * @param {string} toLocale
551
+ * @param {string | null} fromLocale
552
+ * @param {{ reload?: boolean }} [opts]
553
+ */
554
+ function transitionLocaleNone(toLocale, fromLocale, opts = {}) {
555
+ const STORE_KEY = 'vmz.locale';
556
+ try {
557
+ try {
558
+ localStorage.setItem(STORE_KEY, toLocale);
559
+ }
560
+ catch {
561
+ /* private mode */
562
+ }
563
+ try {
564
+ doc.cookie = `${STORE_KEY}=${encodeURIComponent(toLocale)}; path=/; max-age=31536000; SameSite=Lax`;
565
+ }
566
+ catch {
567
+ /* ignore */
568
+ }
569
+ if (doc.documentElement) {
570
+ doc.documentElement.setAttribute('data-locale', toLocale);
571
+ doc.documentElement.setAttribute('lang', toLocale);
572
+ }
573
+ if (win)
574
+ win.__vmzLocaleIdHint = toLocale;
575
+ }
576
+ catch (err) {
577
+ const out = {
578
+ status: 'rolled_back',
579
+ fromLocale,
580
+ toLocale,
581
+ reason: 'persist_failed',
582
+ detail: err && err.message ? String(err.message) : String(err),
583
+ };
584
+ if (win)
585
+ win.__vmzLastLocaleTransition = out;
586
+ return out;
587
+ }
588
+ const out = {
589
+ status: 'committed',
590
+ fromLocale,
591
+ toLocale,
592
+ reason: 'ok',
593
+ strategy: 'none',
594
+ href: loc.pathname + loc.search,
595
+ reload: opts.reload !== false,
596
+ };
597
+ if (win)
598
+ win.__vmzLastLocaleTransition = out;
599
+ // Reload so generated `#locales` modules re-run __vmzLocaleId() with new preference.
600
+ if (opts.reload !== false && loc && typeof loc.reload === 'function') {
601
+ loc.reload();
602
+ }
603
+ return out;
604
+ }
542
605
  /**
543
606
  * @returns {{ strategy?: string, defaultPrefix?: string, defaultLocale?: string, locales?: string[] } | null}
544
607
  */
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Discover compiled client component modules from dist (deployment graph or components/).
3
+ * Shared by serve-host SSR and static emit assemble.
4
+ */
5
+ /**
6
+ * @param {string} dir
7
+ * @returns {Promise<Array<{ name: string, entry: string }>>}
8
+ */
9
+ export declare function listClientComponents(dir: any): Promise<any[]>;
10
+ /**
11
+ * Sync variant for callers that already use fs sync (legacy static-emit helpers).
12
+ * @param {string} dir
13
+ * @returns {Array<{ name: string, entry: string }>}
14
+ */
15
+ export declare function listClientComponentsSync(dir: any): any[];
16
+ /**
17
+ * Import all (or filtered) client components and register for SSR / static emit.
18
+ * @param {string} distDir
19
+ * @param {(map: Record<string, unknown>) => void} registerComponents
20
+ * @param {{
21
+ * cacheBust?: string | number,
22
+ * include?: (entry: { name: string, entry: string }) => boolean,
23
+ * }} [opts]
24
+ */
25
+ export declare function preloadComponentRegistry(distDir: any, registerComponents: any, opts?: {}): Promise<{}>;
@@ -0,0 +1,120 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Discover compiled client component modules from dist (deployment graph or components/).
4
+ * Shared by serve-host SSR and static emit assemble.
5
+ */
6
+ import fs from 'node:fs';
7
+ import { readdir, readFile } from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ /**
11
+ * @param {string} dir
12
+ * @returns {Promise<Array<{ name: string, entry: string }>>}
13
+ */
14
+ export async function listClientComponents(dir) {
15
+ /** @type {Map<string, { name: string, entry: string }>} */
16
+ const byName = new Map();
17
+ try {
18
+ const raw = await readFile(path.join(dir, 'vmz-deployment.json'), 'utf8');
19
+ const dep = JSON.parse(raw);
20
+ for (const unit of dep.units || []) {
21
+ if (unit?.kind !== 'component')
22
+ continue;
23
+ const chunkId = String(unit.chunkId || '');
24
+ const name = chunkId.split('/').pop();
25
+ if (!name)
26
+ continue;
27
+ const entry = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
28
+ byName.set(name, { name, entry });
29
+ }
30
+ }
31
+ catch {
32
+ /* fall through to directory scan */
33
+ }
34
+ if (byName.size === 0) {
35
+ const folder = path.join(dir, 'components');
36
+ let files = [];
37
+ try {
38
+ files = await readdir(folder);
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ for (const f of files.filter((name) => name.endsWith('.client.js'))) {
44
+ const name = f.replace(/\.client\.js$/, '');
45
+ byName.set(name, { name, entry: `components/${name}.client.js` });
46
+ }
47
+ }
48
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
49
+ }
50
+ /**
51
+ * Sync variant for callers that already use fs sync (legacy static-emit helpers).
52
+ * @param {string} dir
53
+ * @returns {Array<{ name: string, entry: string }>}
54
+ */
55
+ export function listClientComponentsSync(dir) {
56
+ /** @type {Map<string, { name: string, entry: string }>} */
57
+ const byName = new Map();
58
+ const deploymentPath = path.join(dir, 'vmz-deployment.json');
59
+ if (fs.existsSync(deploymentPath)) {
60
+ try {
61
+ const dep = JSON.parse(fs.readFileSync(deploymentPath, 'utf8'));
62
+ for (const unit of dep.units || []) {
63
+ if (unit?.kind !== 'component')
64
+ continue;
65
+ const chunkId = String(unit.chunkId || '');
66
+ const name = chunkId.split('/').pop();
67
+ if (!name)
68
+ continue;
69
+ const entry = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
70
+ byName.set(name, { name, entry });
71
+ }
72
+ }
73
+ catch {
74
+ /* fall through */
75
+ }
76
+ }
77
+ if (byName.size === 0) {
78
+ const folder = path.join(dir, 'components');
79
+ let files = [];
80
+ try {
81
+ files = fs.readdirSync(folder);
82
+ }
83
+ catch {
84
+ return [];
85
+ }
86
+ for (const f of files.filter((name) => name.endsWith('.client.js'))) {
87
+ const name = f.replace(/\.client\.js$/, '');
88
+ byName.set(name, { name, entry: `components/${name}.client.js` });
89
+ }
90
+ }
91
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
92
+ }
93
+ /**
94
+ * Import all (or filtered) client components and register for SSR / static emit.
95
+ * @param {string} distDir
96
+ * @param {(map: Record<string, unknown>) => void} registerComponents
97
+ * @param {{
98
+ * cacheBust?: string | number,
99
+ * include?: (entry: { name: string, entry: string }) => boolean,
100
+ * }} [opts]
101
+ */
102
+ export async function preloadComponentRegistry(distDir, registerComponents, opts = {}) {
103
+ const entries = await listClientComponents(distDir);
104
+ /** @type {Record<string, unknown>} */
105
+ const map = {};
106
+ for (const entry of entries) {
107
+ if (opts.include && !opts.include(entry))
108
+ continue;
109
+ const abs = path.join(distDir, entry.entry);
110
+ let href = pathToFileURL(abs).href;
111
+ if (opts.cacheBust != null && opts.cacheBust !== '') {
112
+ href = `${href}?t=${encodeURIComponent(String(opts.cacheBust))}`;
113
+ }
114
+ const mod = await import(href);
115
+ map[entry.name] = mod.default;
116
+ }
117
+ if (Object.keys(map).length)
118
+ registerComponents(map);
119
+ return map;
120
+ }
@@ -16,13 +16,14 @@
16
16
  * Dev resolve hook propagates `?t=` onto nested relative `file:` imports under
17
17
  * dist so soft reload does not keep a stale `lib/*.js` ESM cache entry.
18
18
  */
19
- import { existsSync } from 'node:fs';
19
+ import { existsSync, readFileSync } from 'node:fs';
20
20
  import { readdir, readFile, writeFile } from 'node:fs/promises';
21
21
  import http from 'node:http';
22
22
  import { createRequire, registerHooks } from 'node:module';
23
23
  import path from 'node:path';
24
24
  import { fileURLToPath, pathToFileURL } from 'node:url';
25
25
  import { registerComponents, renderToStream, renderToString } from './vmz-dom.js';
26
+ import { listClientComponents } from './list-client-components.js';
26
27
  import { handleNodeRequest, setRoutes, setServerModuleResolver } from './vmz-runtime.js';
27
28
  const require = createRequire(import.meta.url);
28
29
  const distDir = process.env.VMZ_DIST ? path.resolve(process.env.VMZ_DIST) : path.dirname(fileURLToPath(import.meta.url));
@@ -75,6 +76,8 @@ if (isDev) {
75
76
  }
76
77
  /** @type {number} */
77
78
  let reloadToken = Date.now();
79
+ /** @type {string | null} Correlatable build id from vmz dev (Living §12.8). */
80
+ let lastDevBuildId = null;
78
81
  /** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parseChunkSegments> }>} */
79
82
  let pageCatalog = [];
80
83
  /** @type {Map<string, any>} */
@@ -152,7 +155,7 @@ async function renderPageStream(pathname, opts = {}) {
152
155
  if (isDev && lastDevError && pageCtors.size === 0) {
153
156
  return { status: 500, stream: emitDevErrorHtml(lastDevError) };
154
157
  }
155
- const localePlan = resolveLocalePath(pathname);
158
+ const localePlan = resolveLocalePath(pathname, opts.cookieHeader);
156
159
  if (localePlan.redirectTo) {
157
160
  return { status: 302, redirect: localePlan.redirectTo, headers: { Location: localePlan.redirectTo } };
158
161
  }
@@ -477,10 +480,13 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
477
480
  `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>";` +
478
481
  `document.documentElement.appendChild(d);})();</script>`
479
482
  : '';
483
+ const buildIdBoot = isDev && lastDevBuildId ? `\n <script>window.__VMZ_DEV_BUILD_ID__=${JSON.stringify(lastDevBuildId)};</script>` : '';
480
484
  if (signal?.aborted)
481
485
  return;
482
486
  const themeId = resolveThemeId(opts.searchParams, opts.cookieHeader);
483
487
  const themeBoot = themeBootstrapScript();
488
+ const localeBoot = localeBootstrapScript();
489
+ const faviconHead = siteFaviconHeadHtml();
484
490
  const propsJson = JSON.stringify(props ?? {});
485
491
  const localeId = localeCtx.localeId || localeArtifact?.defaultLocale || 'en';
486
492
  const dir = localeCtx.dir || 'ltr';
@@ -552,9 +558,9 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
552
558
  ...(cssHref ? { cssEntry: cssHref } : {}),
553
559
  isErrorDocument: false,
554
560
  htmlExtraAttrs,
555
- headExtraHtml: themeBoot,
561
+ headExtraHtml: `${themeBoot}${localeBoot}${faviconHead}`,
556
562
  moduleScriptSrc: entrySrc,
557
- bodyTailHtml: `${live}${bootOverlay}`,
563
+ bodyTailHtml: `${live}${bootOverlay}${buildIdBoot}`,
558
564
  });
559
565
  }
560
566
  const server = http.createServer((req, res) => {
@@ -670,7 +676,7 @@ process.on('SIGINT', () => {
670
676
  * Re-import routes / pages / components with a new cache-bust token.
671
677
  * Keeps the HTTP server process alive (no Node restart).
672
678
  * Failed reloads keep the previous in-memory modules (Vite-like resilience).
673
- * @param {{ quiet?: boolean, payload?: { affectedChunks?: string[], seedChunks?: string[], emitted?: string[], full?: boolean, islandHmr?: boolean } }} [opts]
679
+ * @param {{ quiet?: boolean, payload?: { affectedChunks?: string[], seedChunks?: string[], emitted?: string[], full?: boolean, islandHmr?: boolean, buildId?: string, sourceRevision?: string, bundleRevision?: string, changed?: string[] } }} [opts]
674
680
  */
675
681
  async function softReload(opts = {}) {
676
682
  const prevToken = reloadToken;
@@ -683,6 +689,11 @@ async function softReload(opts = {}) {
683
689
  const emitted = opts.payload?.emitted ?? [];
684
690
  const full = opts.payload?.full;
685
691
  const islandHmr = Boolean(opts.payload?.islandHmr);
692
+ const buildId = opts.payload?.buildId != null ? String(opts.payload.buildId) : null;
693
+ const sourceRevision = opts.payload?.sourceRevision != null ? String(opts.payload.sourceRevision) : null;
694
+ const bundleRevision = opts.payload?.bundleRevision != null ? String(opts.payload.bundleRevision) : null;
695
+ if (buildId)
696
+ lastDevBuildId = buildId;
686
697
  const reloadAllPages = shouldReloadAllPages({ full, affected, emitted, islandHmr });
687
698
  try {
688
699
  try {
@@ -772,13 +783,21 @@ async function softReload(opts = {}) {
772
783
  affectedChunks: affected,
773
784
  seedChunks: seeds,
774
785
  token: reloadToken,
786
+ buildId: buildId || lastDevBuildId,
787
+ sourceRevision,
788
+ bundleRevision,
789
+ serveRevision: String(reloadToken),
775
790
  full: Boolean(full),
776
791
  eventOnlyShell,
777
792
  }));
778
793
  if (!opts.quiet) {
779
794
  const aff = affected.length > 0 ? ` affected=[${affected.join(', ')}]` : full === false ? ' affected=[]' : '';
780
795
  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})`);
796
+ const bid = buildId || lastDevBuildId;
797
+ const rev = bid || sourceRevision || bundleRevision
798
+ ? ` buildId=${bid || '-'} source=${sourceRevision || '-'} bundle=${bundleRevision || '-'} serve=${reloadToken}`
799
+ : ` t=${reloadToken}`;
800
+ console.log(`vmz serve: soft reload ok (mode=${mode}; ${scope}; catalog=${pageCatalog.length};${rev}${aff})`);
782
801
  }
783
802
  return {
784
803
  affectedChunks: affected,
@@ -790,6 +809,11 @@ async function softReload(opts = {}) {
790
809
  pageCount: pageCatalog.length,
791
810
  reloadedPages: islandHmr ? 0 : nextCtors.size,
792
811
  reloadAllPages,
812
+ buildId: buildId || lastDevBuildId,
813
+ sourceRevision,
814
+ bundleRevision,
815
+ serveRevision: String(reloadToken),
816
+ token: reloadToken,
793
817
  };
794
818
  }
795
819
  catch (err) {
@@ -899,11 +923,14 @@ function escapeHtml(s) {
899
923
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
900
924
  }
901
925
  /**
902
- * Resolve LocaleId from pathname using `_vmz/locale-route-realization.json`.
903
- * LocaleId is a realization dimension matching still uses stable route path.
926
+ * Resolve LocaleId for this request.
927
+ * - `prefix`: LocaleId from URL path (existing).
928
+ * - `none`: Host preference from cookie `vmz.locale` (validated), else defaultLocale.
929
+ * URL never carries LocaleId.
904
930
  * @param {string} pathname
931
+ * @param {string | undefined} cookieHeader
905
932
  */
906
- function resolveLocalePath(pathname) {
933
+ function resolveLocalePath(pathname, cookieHeader) {
907
934
  const raw = String(pathname || '/');
908
935
  const normalized = raw.length > 1 && raw.endsWith('/') ? raw.slice(0, -1) : raw || '/';
909
936
  if (!localeArtifact) {
@@ -913,6 +940,17 @@ function resolveLocalePath(pathname) {
913
940
  const defaultLocale = localeArtifact.defaultLocale || supported[0] || 'en';
914
941
  const directions = Object.fromEntries((localeArtifact.locales || []).map((l) => [l.id, l.direction || 'ltr']));
915
942
  const routing = localeArtifact.routing || {};
943
+ const strategy = routing.strategy || 'prefix';
944
+ if (strategy === 'none') {
945
+ const preferred = readCookie(cookieHeader, LOCALE_STORE_KEY);
946
+ const localeId = preferred && supported.includes(preferred) ? preferred : defaultLocale;
947
+ return {
948
+ localeId,
949
+ dir: directions[localeId] || 'ltr',
950
+ restPath: normalized,
951
+ redirectTo: null,
952
+ };
953
+ }
916
954
  const parts = normalized.split('/').filter(Boolean);
917
955
  let localeId = null;
918
956
  let restPath = normalized;
@@ -1036,46 +1074,6 @@ async function loadPageCtor(chunkId) {
1036
1074
  pageCtors.set(chunkId, mod.default);
1037
1075
  return mod.default;
1038
1076
  }
1039
- /**
1040
- * @param {string} dir
1041
- * @returns {Promise<Array<{ name: string, entry: string }>>}
1042
- */
1043
- async function listClientComponents(dir) {
1044
- /** @type {Map<string, { name: string, entry: string }>} */
1045
- const byName = new Map();
1046
- try {
1047
- const raw = await readFile(path.join(dir, 'vmz-deployment.json'), 'utf8');
1048
- const dep = JSON.parse(raw);
1049
- for (const unit of dep.units || []) {
1050
- if (unit?.kind !== 'component')
1051
- continue;
1052
- const chunkId = String(unit.chunkId || '');
1053
- const name = chunkId.split('/').pop();
1054
- if (!name)
1055
- continue;
1056
- const entry = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
1057
- byName.set(name, { name, entry });
1058
- }
1059
- }
1060
- catch {
1061
- /* fall through to directory scan */
1062
- }
1063
- if (byName.size === 0) {
1064
- const folder = path.join(dir, 'components');
1065
- let files = [];
1066
- try {
1067
- files = await readdir(folder);
1068
- }
1069
- catch {
1070
- return [];
1071
- }
1072
- for (const f of files.filter((name) => name.endsWith('.client.js'))) {
1073
- const name = f.replace(/\.client\.js$/, '');
1074
- byName.set(name, { name, entry: `components/${name}.client.js` });
1075
- }
1076
- }
1077
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
1078
- }
1079
1077
  /**
1080
1078
  * Discover compiled page modules. Prefer Route Graph `pathPattern` from
1081
1079
  * `vmz-deployment.json`; fall back to walking `pages/**` (file-route only).
@@ -1442,6 +1440,8 @@ function requireNativeGenerator() {
1442
1440
  * Style Theme cookie / localStorage key (host contract, not a second theme API).
1443
1441
  */
1444
1442
  const THEME_STORE_KEY = 'vmz-theme';
1443
+ /** Host preference key for `routing.strategy: 'none'` (cookie + localStorage). */
1444
+ const LOCALE_STORE_KEY = 'vmz.locale';
1445
1445
  /**
1446
1446
  * @param {string} dir
1447
1447
  * @returns {Promise<{ cssEntry: string|null, styleTheme: typeof styleTheme, styleBundleHash: string|null }>}
@@ -1517,6 +1517,45 @@ function themeBootstrapScript() {
1517
1517
  const key = JSON.stringify(THEME_STORE_KEY);
1518
1518
  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`;
1519
1519
  }
1520
+ /**
1521
+ * LocaleId as client state (routing.strategy = none): apply localStorage before any
1522
+ * page/client module runs so `#locales/*` pick the right variant. Prefix strategy
1523
+ * keeps LocaleId in the URL — no boot rewrite.
1524
+ * Also mirrors into cookie so the next SSR negotiate sees Host preference.
1525
+ */
1526
+ function localeBootstrapScript() {
1527
+ if (!localeArtifact)
1528
+ return '';
1529
+ const routing = localeArtifact.routing || {};
1530
+ if ((routing.strategy || 'prefix') !== 'none')
1531
+ return '';
1532
+ const ids = (localeArtifact.locales || []).map((l) => l.id).filter(Boolean);
1533
+ if (!ids.length)
1534
+ return '';
1535
+ const key = JSON.stringify(LOCALE_STORE_KEY);
1536
+ const idList = JSON.stringify(ids);
1537
+ 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`;
1538
+ }
1539
+ /**
1540
+ * Site favicon links from build artifact `_vmz/site-favicon.json` (author SVG → PNG/ICO).
1541
+ * Empty when skipped / missing — do not invent broken <link>s.
1542
+ */
1543
+ function siteFaviconHeadHtml() {
1544
+ try {
1545
+ const p = path.join(distDir, '_vmz', 'site-favicon.json');
1546
+ if (!existsSync(p))
1547
+ return '';
1548
+ // Sync read: head is per-request; file is tiny and rebuilt with dist.
1549
+ const raw = readFileSync(p, 'utf8');
1550
+ const j = JSON.parse(raw);
1551
+ if (j?.status !== 'ready' || typeof j.headHtml !== 'string')
1552
+ return '';
1553
+ return j.headHtml;
1554
+ }
1555
+ catch {
1556
+ return '';
1557
+ }
1558
+ }
1520
1559
  /**
1521
1560
  * @param {string|undefined} header
1522
1561
  * @param {string} name
package/dist/server.js CHANGED
@@ -226,7 +226,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
226
226
  return await writeFetchResponse(res, response);
227
227
  }
228
228
  // Static first for assets + DocumentMount (`/d/…`) so docs aren't swallowed by SSR 404 shells.
229
- // web-static route HTML (`index.html`, `about/index.html`, …) is a CDN/deploy projection only —
229
+ // static route HTML (`index.html`, `about/index.html`, …) is a CDN/deploy projection only —
230
230
  // when Server Host SSR is active, those files must not shadow live render (local/dev ≡ SSR truth).
231
231
  if (verb === 'GET' && opts.distDir) {
232
232
  const nodePath = await import('node:path');
@@ -404,7 +404,7 @@ function safeDistFile(distDir, pathname, nodePath) {
404
404
  return full;
405
405
  }
406
406
  /**
407
- * web-static emits per-route HTML beside client assets. That HTML is for CDN / local-static
407
+ * static profile emits per-route HTML beside client assets. That HTML is for CDN / local-static
408
408
  * delivery hosts — not for Server Host when SSR is available. DocumentMount stays static.
409
409
  * @param {string} file
410
410
  * @param {string} pathname
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/core",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
6
6
  "exports": {
@@ -27,6 +27,10 @@
27
27
  "./client-nav": {
28
28
  "types": "./dist/client-nav.d.ts",
29
29
  "default": "./dist/client-nav.js"
30
+ },
31
+ "./component-registry": {
32
+ "types": "./dist/list-client-components.d.ts",
33
+ "default": "./dist/list-client-components.js"
30
34
  }
31
35
  },
32
36
  "files": [