@vmz/core 0.1.21 → 0.1.22

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.
@@ -55,7 +55,7 @@ export declare function loadComponentEntries(distDir: any, opts?: {}): Promise<a
55
55
  */
56
56
  export declare function importAndRegisterComponentEntries(distDir: any, entries: any, registerComponents: any, opts?: {}): Promise<{}>;
57
57
  /**
58
- * Bootstrap component registry from deployment (all, closure, or directory fallback).
58
+ * Bootstrap component registry from deployment (all or closure — no directory fallback).
59
59
  * @param {string} distDir
60
60
  * @param {(map: Record<string, unknown>) => void} registerComponents
61
61
  * @param {{
@@ -4,7 +4,6 @@
4
4
  * Parse/validate and graph queries delegate to Rust vmz-artifacts via N-API.
5
5
  */
6
6
  import fs from 'node:fs';
7
- import { readdir } from 'node:fs/promises';
8
7
  import path from 'node:path';
9
8
  import { pathToFileURL } from 'node:url';
10
9
  import { requireNativeFn } from './native-addon.js';
@@ -131,35 +130,13 @@ export async function loadComponentEntries(distDir, opts = {}) {
131
130
  entries = entries.filter((e) => closure.has(e.chunkId));
132
131
  }
133
132
  }
134
- else if (!strict) {
135
- entries = await listComponentEntriesFromDirectory(distDir);
133
+ else if (strict) {
134
+ throw new Error(`vmz: missing vmz-deployment.json under ${distDir} (plan-only host)`);
136
135
  }
136
+ // No directory scan fallback — component closure comes from Deployment Plan only.
137
137
  entries = mergeExplicitComponentEntries(entries, opts.explicit);
138
138
  return dedupeComponentEntriesByTag(entries, { strict });
139
139
  }
140
- /**
141
- * @param {string} distDir
142
- * @returns {Promise<Array<{ chunkId: string, name: string, entry: string }>>}
143
- */
144
- async function listComponentEntriesFromDirectory(distDir) {
145
- const folder = path.join(distDir, 'components');
146
- /** @type {string[]} */
147
- let files = [];
148
- try {
149
- files = await readdir(folder);
150
- }
151
- catch {
152
- return [];
153
- }
154
- return files
155
- .filter((name) => name.endsWith('.client.js'))
156
- .map((f) => {
157
- const name = f.replace(/\.client\.js$/, '');
158
- const chunkId = `components/${name}`;
159
- return { chunkId, name, entry: `components/${name}.client.js` };
160
- })
161
- .sort((a, b) => a.name.localeCompare(b.name));
162
- }
163
140
  /**
164
141
  * @param {string} distDir
165
142
  * @param {Array<{ chunkId: string, name: string, entry: string }>} entries
@@ -192,7 +169,7 @@ export async function importAndRegisterComponentEntries(distDir, entries, regist
192
169
  return map;
193
170
  }
194
171
  /**
195
- * Bootstrap component registry from deployment (all, closure, or directory fallback).
172
+ * Bootstrap component registry from deployment (all or closure — no directory fallback).
196
173
  * @param {string} distDir
197
174
  * @param {(map: Record<string, unknown>) => void} registerComponents
198
175
  * @param {{
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Discover compiled client component modules from dist (deployment graph or components/).
3
- * Shared by serve-host SSR, static emit, and test hosts.
2
+ * Discover compiled client component modules from Deployment Plan only.
3
+ * Shared by serve-host SSR, static emit, and test hosts (plan-only host).
4
4
  */
5
5
  export { DEPLOYMENT_SCHEMA, readDeploymentDocument, componentEntriesFromDeployment, collectDependsOnClosure, dedupeComponentEntriesByTag, mergeExplicitComponentEntries, loadComponentEntries, importAndRegisterComponentEntries, bootstrapComponentRegistry, } from './deployment-registry.js';
6
6
  export { createRenderHost } from './render-host.js';
@@ -1,10 +1,8 @@
1
1
  // @ts-nocheck
2
2
  /**
3
- * Discover compiled client component modules from dist (deployment graph or components/).
4
- * Shared by serve-host SSR, static emit, and test hosts.
3
+ * Discover compiled client component modules from Deployment Plan only.
4
+ * Shared by serve-host SSR, static emit, and test hosts (plan-only host).
5
5
  */
6
- import fs from 'node:fs';
7
- import { readdir } from 'node:fs/promises';
8
6
  import path from 'node:path';
9
7
  import { pathToFileURL } from 'node:url';
10
8
  import { componentEntriesFromDeployment, dedupeComponentEntriesByTag, mergeExplicitComponentEntries, readDeploymentDocument, } from './deployment-registry.js';
@@ -31,24 +29,9 @@ export async function listClientComponents(dir, opts = {}) {
31
29
  }));
32
30
  }
33
31
  if (strict) {
34
- throw new Error(`vmz: missing vmz-deployment.json under ${dir} (strict deployment mode)`);
32
+ throw new Error(`vmz: missing vmz-deployment.json under ${dir} (plan-only host)`);
35
33
  }
36
- const folder = path.join(dir, 'components');
37
- /** @type {string[]} */
38
- let files = [];
39
- try {
40
- files = await readdir(folder);
41
- }
42
- catch {
43
- return [];
44
- }
45
- return files
46
- .filter((name) => name.endsWith('.client.js'))
47
- .map((f) => {
48
- const name = f.replace(/\.client\.js$/, '');
49
- return { name, entry: `components/${name}.client.js`, chunkId: `components/${name}` };
50
- })
51
- .sort((a, b) => a.name.localeCompare(b.name));
34
+ return [];
52
35
  }
53
36
  /**
54
37
  * Sync variant for callers that already use fs sync (legacy static-emit helpers).
@@ -72,24 +55,9 @@ export function listClientComponentsSync(dir, opts = {}) {
72
55
  }));
73
56
  }
74
57
  if (strict) {
75
- throw new Error(`vmz: missing vmz-deployment.json under ${dir} (strict deployment mode)`);
76
- }
77
- const folder = path.join(dir, 'components');
78
- /** @type {string[]} */
79
- let files = [];
80
- try {
81
- files = fs.readdirSync(folder);
82
- }
83
- catch {
84
- return [];
58
+ throw new Error(`vmz: missing vmz-deployment.json under ${dir} (plan-only host)`);
85
59
  }
86
- return files
87
- .filter((name) => name.endsWith('.client.js'))
88
- .map((f) => {
89
- const name = f.replace(/\.client\.js$/, '');
90
- return { name, entry: `components/${name}.client.js`, chunkId: `components/${name}` };
91
- })
92
- .sort((a, b) => a.name.localeCompare(b.name));
60
+ return [];
93
61
  }
94
62
  /**
95
63
  * @param {Array<{ name: string, entry: string, chunkId?: string }>} entries
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Single HTML `<a data-vmz-route>` locale rewrite for serve-host and static-emit.
3
+ * Plan-only hosts must not keep a second copy of this semantic (0.1.22).
4
+ */
5
+ export type LocaleHrefArtifact = {
6
+ locales?: Array<{
7
+ id: string;
8
+ } | string>;
9
+ defaultLocale?: string;
10
+ routing?: {
11
+ strategy?: string;
12
+ defaultPrefix?: string;
13
+ defaultLocale?: string;
14
+ };
15
+ };
16
+ /** Rewrite a same-app href to the given LocaleId via route realization. */
17
+ export declare function localizeSameAppHref(href: string, localeId: string, artifact: LocaleHrefArtifact): string;
18
+ /** Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`. */
19
+ export declare function localizeBodyLinks(html: string, localeId: string, artifact: LocaleHrefArtifact, escapeAttr?: (s: string) => string): string;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Single HTML `<a data-vmz-route>` locale rewrite for serve-host and static-emit.
3
+ * Plan-only hosts must not keep a second copy of this semantic (0.1.22).
4
+ */
5
+ function normalizePath(pathname) {
6
+ let p = String(pathname || '/');
7
+ if (!p.startsWith('/'))
8
+ p = `/${p}`;
9
+ if (p.length > 1 && p.endsWith('/'))
10
+ p = p.slice(0, -1);
11
+ return p || '/';
12
+ }
13
+ function parseLocaleFromPath(pathname, supportedLocales) {
14
+ const parts = normalizePath(pathname).split('/').filter(Boolean);
15
+ if (!parts.length)
16
+ return { localeId: null, restPath: '/' };
17
+ if (supportedLocales.includes(parts[0])) {
18
+ const rest = parts.slice(1);
19
+ return { localeId: parts[0], restPath: rest.length ? `/${rest.join('/')}` : '/' };
20
+ }
21
+ return { localeId: null, restPath: normalizePath(pathname) };
22
+ }
23
+ function realizeRoutePath(localeId, pathPattern, routing) {
24
+ const strategy = routing.strategy || 'prefix';
25
+ const defaultPrefix = routing.defaultPrefix || 'include';
26
+ const defaultLocale = routing.defaultLocale;
27
+ const base = normalizePath(pathPattern);
28
+ if (strategy === 'none' || strategy === 'domain') {
29
+ return { path: base };
30
+ }
31
+ const omitDefault = defaultPrefix === 'omit' && localeId === defaultLocale;
32
+ if (omitDefault) {
33
+ return { path: base };
34
+ }
35
+ const pathOut = base === '/' ? `/${localeId}` : `/${localeId}${base}`;
36
+ return { path: pathOut };
37
+ }
38
+ /** Rewrite a same-app href to the given LocaleId via route realization. */
39
+ export function localizeSameAppHref(href, localeId, artifact) {
40
+ if (!href || !localeId || !artifact)
41
+ return href;
42
+ if (href.startsWith('#') || /^(mailto|tel|javascript):/i.test(href))
43
+ return href;
44
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href) && !href.startsWith('/'))
45
+ return href;
46
+ let pathname = String(href);
47
+ let search = '';
48
+ let hash = '';
49
+ const hashIdx = pathname.indexOf('#');
50
+ if (hashIdx >= 0) {
51
+ hash = pathname.slice(hashIdx);
52
+ pathname = pathname.slice(0, hashIdx);
53
+ }
54
+ const qIdx = pathname.indexOf('?');
55
+ if (qIdx >= 0) {
56
+ search = pathname.slice(qIdx);
57
+ pathname = pathname.slice(0, qIdx);
58
+ }
59
+ if (!pathname)
60
+ pathname = '/';
61
+ const supported = (artifact.locales || []).map((l) => (typeof l === 'string' ? l : l.id)).filter(Boolean);
62
+ const defaultLocale = artifact.defaultLocale || artifact.routing?.defaultLocale;
63
+ const routing = {
64
+ strategy: artifact.routing?.strategy || 'prefix',
65
+ defaultPrefix: artifact.routing?.defaultPrefix || 'include',
66
+ defaultLocale,
67
+ };
68
+ const parsed = parseLocaleFromPath(pathname, supported);
69
+ const rest = parsed.restPath || '/';
70
+ const realized = realizeRoutePath(localeId, rest, routing);
71
+ return `${realized.path}${search}${hash}`;
72
+ }
73
+ /** Rewrite `<a data-vmz-route href>` in HTML body to retain `localeId`. */
74
+ export function localizeBodyLinks(html, localeId, artifact, escapeAttr = (s) => String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;')) {
75
+ if (!html || !localeId || !artifact)
76
+ return html;
77
+ return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
78
+ if (!/\bdata-vmz-route\s*=/.test(attrs))
79
+ return full;
80
+ const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
81
+ if (!hm)
82
+ return full;
83
+ const next = localizeSameAppHref(hm[1], localeId, artifact);
84
+ if (next === hm[1])
85
+ return full;
86
+ const newAttrs = attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(next)}"`);
87
+ return `<a${newAttrs}>`;
88
+ });
89
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * PositionContext backed by compiler OffsetIndex via N-API (`offsetIndexLineCol`).
3
+ * `@vmz/diagnostic` stays free of a native dependency — hosts wire this helper.
4
+ */
5
+ export type LineCol = {
6
+ line: number;
7
+ column: number;
8
+ };
9
+ export type PositionContext = {
10
+ lineCol(offset: number): LineCol;
11
+ };
12
+ /**
13
+ * Build a diagnostic PositionContext for `sourceText` using the native OffsetIndex.
14
+ */
15
+ export declare function createPositionContext(sourceText: string): PositionContext;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * PositionContext backed by compiler OffsetIndex via N-API (`offsetIndexLineCol`).
3
+ * `@vmz/diagnostic` stays free of a native dependency — hosts wire this helper.
4
+ */
5
+ import { requireNativeFn } from './native-addon.js';
6
+ /**
7
+ * Build a diagnostic PositionContext for `sourceText` using the native OffsetIndex.
8
+ */
9
+ export function createPositionContext(sourceText) {
10
+ const source = String(sourceText ?? '');
11
+ const lineColFn = requireNativeFn('offsetIndexLineCol');
12
+ return {
13
+ lineCol(offset) {
14
+ const off = Math.max(0, Number(offset) || 0);
15
+ const row = lineColFn(source, off >>> 0);
16
+ return { line: row.line, column: row.column };
17
+ },
18
+ };
19
+ }
@@ -1,18 +1,18 @@
1
1
  /**
2
- * File-route layout chain: Application shell (outermost) + nested page Layout components.
2
+ * Layout chain from Deployment Plan only (plan-only host).
3
3
  */
4
4
  /** Chunk id for `src/Application.vmz` emit (`Application.client.js`). */
5
5
  export declare const APPLICATION_SHELL_CHUNK = "Application";
6
6
  /**
7
- * True when the compile output includes a root Application shell.
7
+ * True when Deployment Plan lists an Application shell unit.
8
8
  */
9
9
  export declare function hasApplicationShell(distDir: string): boolean;
10
10
  /**
11
- * Nearest page Layout.client.js walking up from the page chunk (outer to inner).
12
- * Does not include Application — use resolveRouteLayoutChain.
11
+ * Nested layout ids from Plan for a page (excludes Application).
12
+ * Prefer `resolveRouteLayoutChain` for SSR / hydrate.
13
13
  */
14
14
  export declare function resolveNestedLayoutChain(distDir: string, pageChunkId: string): string[];
15
15
  /**
16
- * Full SSR / hydrate layout chain: optional Application shell, then nested page layouts.
16
+ * Full SSR / hydrate layout chain from Deployment Plan `layoutChain`.
17
17
  */
18
18
  export declare function resolveRouteLayoutChain(distDir: string, pageChunkId: string): string[];
@@ -1,41 +1,36 @@
1
1
  /**
2
- * File-route layout chain: Application shell (outermost) + nested page Layout components.
2
+ * Layout chain from Deployment Plan only (plan-only host).
3
3
  */
4
- import { existsSync } from 'node:fs';
5
- import path from 'node:path';
4
+ import { readDeploymentDocument } from './deployment-registry.js';
6
5
  /** Chunk id for `src/Application.vmz` emit (`Application.client.js`). */
7
6
  export const APPLICATION_SHELL_CHUNK = 'Application';
8
7
  /**
9
- * True when the compile output includes a root Application shell.
8
+ * True when Deployment Plan lists an Application shell unit.
10
9
  */
11
10
  export function hasApplicationShell(distDir) {
12
- return existsSync(path.join(distDir, `${APPLICATION_SHELL_CHUNK}.client.js`));
11
+ const deployment = readDeploymentDocument(distDir, { strict: false });
12
+ const units = Array.isArray(deployment?.units) ? deployment.units : [];
13
+ return units.some((u) => String(u?.chunkId || '') === APPLICATION_SHELL_CHUNK);
13
14
  }
14
15
  /**
15
- * Nearest page Layout.client.js walking up from the page chunk (outer to inner).
16
- * Does not include Application — use resolveRouteLayoutChain.
16
+ * Nested layout ids from Plan for a page (excludes Application).
17
+ * Prefer `resolveRouteLayoutChain` for SSR / hydrate.
17
18
  */
18
19
  export function resolveNestedLayoutChain(distDir, pageChunkId) {
19
- const rel = pageChunkId.replace(/^pages\//, '');
20
- const parts = rel.split('/').filter(Boolean);
21
- parts.pop();
22
- const chain = [];
23
- for (let i = parts.length; i >= 0; i--) {
24
- const dirParts = parts.slice(0, i);
25
- const layoutChunk = ['pages', ...dirParts, 'Layout'].join('/');
26
- if (existsSync(path.join(distDir, `${layoutChunk}.client.js`))) {
27
- chain.unshift(layoutChunk);
28
- }
29
- }
30
- return chain;
20
+ return resolveRouteLayoutChain(distDir, pageChunkId).filter((id) => id !== APPLICATION_SHELL_CHUNK);
31
21
  }
32
22
  /**
33
- * Full SSR / hydrate layout chain: optional Application shell, then nested page layouts.
23
+ * Full SSR / hydrate layout chain from Deployment Plan `layoutChain`.
34
24
  */
35
25
  export function resolveRouteLayoutChain(distDir, pageChunkId) {
36
- const chain = resolveNestedLayoutChain(distDir, pageChunkId);
37
- if (hasApplicationShell(distDir)) {
38
- chain.unshift(APPLICATION_SHELL_CHUNK);
26
+ const deployment = readDeploymentDocument(distDir, { strict: true });
27
+ const units = Array.isArray(deployment?.units) ? deployment.units : [];
28
+ const unit = units.find((u) => String(u?.chunkId || '').replace(/\\/g, '/') === pageChunkId);
29
+ if (!unit) {
30
+ throw new Error(`resolveRouteLayoutChain: no deployment unit for ${pageChunkId} (plan-only host)`);
31
+ }
32
+ if (!Array.isArray(unit.layoutChain)) {
33
+ throw new Error(`resolveRouteLayoutChain: page unit ${pageChunkId} missing layoutChain (plan-only host)`);
39
34
  }
40
- return chain;
35
+ return unit.layoutChain.map((id) => String(id));
41
36
  }
@@ -17,11 +17,12 @@
17
17
  * dist so soft reload does not keep a stale `lib/*.js` ESM cache entry.
18
18
  */
19
19
  import { existsSync, readFileSync } from 'node:fs';
20
- import { readdir, readFile, writeFile } from 'node:fs/promises';
20
+ import { 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
+ import { localizeBodyLinks } from './localize-body-links.js';
25
26
  import { createRenderHost } from './render-host.js';
26
27
  import { listClientComponents } from './list-client-components.js';
27
28
  import { loadNativeAddon } from './native-addon.js';
@@ -141,7 +142,7 @@ let reloadToken = Date.now();
141
142
  let ssrRenderHost = null;
142
143
  /** @type {string | null} Correlatable build id from vmz dev (Living §12.8). */
143
144
  let lastDevBuildId = null;
144
- /** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parseChunkSegments> }>} */
145
+ /** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parsePathPattern> }>} */
145
146
  let pageCatalog = [];
146
147
  /** @type {Map<string, any>} */
147
148
  const pageCtors = new Map();
@@ -606,7 +607,7 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
606
607
  }
607
608
  // Locale discipline: same-app Links retain current LocaleId (realization authority).
608
609
  if (localeArtifact && localeId) {
609
- bodyHtml = localizeBodyLinksInHost(bodyHtml, localeId, localeArtifact);
610
+ bodyHtml = localizeBodyLinks(bodyHtml, localeId, localeArtifact);
610
611
  }
611
612
  }
612
613
  finally {
@@ -799,9 +800,7 @@ async function softReload(opts = {}) {
799
800
  cacheBust: nextToken,
800
801
  });
801
802
  const nextCatalog = await listPageClientFiles(distDir);
802
- if (!nextCatalog.length) {
803
- throw new Error(`vmz serve: no pages/**/*.client.js in ${distDir}`);
804
- }
803
+ // empty catalog throws inside listPageClientFiles
805
804
  /** @type {Map<string, any>} */
806
805
  const nextCtors = new Map();
807
806
  if (!islandHmr) {
@@ -1053,79 +1052,6 @@ function resolveLocalePath(pathname, cookieHeader) {
1053
1052
  redirectTo: null,
1054
1053
  };
1055
1054
  }
1056
- /**
1057
- * Realize href for current LocaleId (prefix strategy). Kept local so serve-host
1058
- * stays free of CLI package imports.
1059
- * @param {string} href
1060
- * @param {string} localeId
1061
- * @param {any} artifact
1062
- */
1063
- function localizeSameAppHrefHost(href, localeId, artifact) {
1064
- if (!href || !localeId || !artifact)
1065
- return href;
1066
- if (href.startsWith('#') || /^(mailto|tel|javascript):/i.test(href))
1067
- return href;
1068
- if (/^[a-z][a-z0-9+.-]*:/i.test(href) && !href.startsWith('/'))
1069
- return href;
1070
- let pathname = String(href);
1071
- let search = '';
1072
- let hash = '';
1073
- const hashIdx = pathname.indexOf('#');
1074
- if (hashIdx >= 0) {
1075
- hash = pathname.slice(hashIdx);
1076
- pathname = pathname.slice(0, hashIdx);
1077
- }
1078
- const qIdx = pathname.indexOf('?');
1079
- if (qIdx >= 0) {
1080
- search = pathname.slice(qIdx);
1081
- pathname = pathname.slice(0, qIdx);
1082
- }
1083
- if (!pathname)
1084
- pathname = '/';
1085
- const supported = (artifact.locales || []).map((l) => l.id).filter(Boolean);
1086
- const defaultLocale = artifact.defaultLocale || artifact.routing?.defaultLocale;
1087
- const routing = artifact.routing || {};
1088
- const strategy = routing.strategy || 'prefix';
1089
- const defaultPrefix = routing.defaultPrefix || 'include';
1090
- const parts = pathname.split('/').filter(Boolean);
1091
- let rest = pathname;
1092
- if (parts.length && supported.includes(parts[0])) {
1093
- const r = parts.slice(1);
1094
- rest = r.length ? `/${r.join('/')}` : '/';
1095
- }
1096
- if (rest.length > 1 && rest.endsWith('/'))
1097
- rest = rest.slice(0, -1);
1098
- if (!rest.startsWith('/'))
1099
- rest = `/${rest}`;
1100
- if (strategy === 'none' || strategy === 'domain')
1101
- return `${rest}${search}${hash}`;
1102
- const omitDefault = defaultPrefix === 'omit' && localeId === defaultLocale;
1103
- if (omitDefault)
1104
- return `${rest}${search}${hash}`;
1105
- const pathOut = rest === '/' ? `/${localeId}` : `/${localeId}${rest}`;
1106
- return `${pathOut}${search}${hash}`;
1107
- }
1108
- /**
1109
- * @param {string} html
1110
- * @param {string} localeId
1111
- * @param {any} artifact
1112
- */
1113
- function localizeBodyLinksInHost(html, localeId, artifact) {
1114
- if (!html || !localeId || !artifact)
1115
- return html;
1116
- return String(html).replace(/<a\b([^>]*)>/gi, (full, attrs) => {
1117
- if (!/\bdata-vmz-route\s*=/.test(attrs))
1118
- return full;
1119
- const hm = attrs.match(/\bhref\s*=\s*"([^"]*)"/i);
1120
- if (!hm)
1121
- return full;
1122
- const next = localizeSameAppHrefHost(hm[1], localeId, artifact);
1123
- if (next === hm[1])
1124
- return full;
1125
- const newAttrs = attrs.replace(/\bhref\s*=\s*"[^"]*"/i, `href="${escapeAttr(next)}"`);
1126
- return `<a${newAttrs}>`;
1127
- });
1128
- }
1129
1055
  /**
1130
1056
  * @param {string} chunkId
1131
1057
  * @param {string} localeId
@@ -1152,50 +1078,22 @@ async function loadPageCtor(chunkId) {
1152
1078
  return mod.default;
1153
1079
  }
1154
1080
  /**
1155
- * Discover compiled page modules. Prefer Route Graph `pathPattern` from
1156
- * `vmz-deployment.json`; fall back to walking `pages/**` (file-route only).
1081
+ * Discover compiled page modules from Route Graph `pathPattern` in
1082
+ * `vmz-deployment.json` only (plan-only host — no `pages/**` walk).
1157
1083
  * @param {string} dir
1158
1084
  */
1159
1085
  async function listPageClientFiles(dir) {
1160
1086
  const fromDep = await listPagesFromDeployment(dir);
1161
- if (fromDep.length)
1162
- return fromDep;
1163
- const root = path.join(dir, 'pages');
1164
- /** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parseChunkSegments> }>} */
1165
- const out = [];
1166
- async function walk(abs, relParts) {
1167
- let ents;
1168
- try {
1169
- ents = await readdir(abs, { withFileTypes: true });
1170
- }
1171
- catch {
1172
- return;
1173
- }
1174
- for (const e of ents) {
1175
- if (e.isDirectory()) {
1176
- await walk(path.join(abs, e.name), [...relParts, e.name]);
1177
- }
1178
- else if (e.isFile() && e.name.endsWith('.client.js')) {
1179
- const stem = e.name.replace(/\.client\.js$/, '');
1180
- if (isRouteBoundaryStem(stem))
1181
- continue;
1182
- const chunkId = ['pages', ...relParts, stem].join('/');
1183
- out.push({
1184
- chunkId,
1185
- pageRel: `${chunkId}.client.js`,
1186
- segs: parseChunkSegments(chunkId),
1187
- });
1188
- }
1189
- }
1087
+ if (!fromDep.length) {
1088
+ throw new Error(`vmz serve: no page units with pathPattern in ${path.join(dir, 'vmz-deployment.json')} (plan-only host)`);
1190
1089
  }
1191
- await walk(root, []);
1192
- return out;
1090
+ return fromDep;
1193
1091
  }
1194
1092
  /**
1195
1093
  * @param {string} dir
1196
1094
  */
1197
1095
  async function listPagesFromDeployment(dir) {
1198
- /** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parseChunkSegments> }>} */
1096
+ /** @type {Array<{ chunkId: string, pageRel: string, segs: ReturnType<typeof parsePathPattern> }>} */
1199
1097
  const out = [];
1200
1098
  try {
1201
1099
  const raw = await readFile(path.join(dir, 'vmz-deployment.json'), 'utf8');
@@ -1209,40 +1107,25 @@ async function listPagesFromDeployment(dir) {
1209
1107
  const stem = chunkId.split('/').pop() || '';
1210
1108
  if (isRouteBoundaryStem(stem))
1211
1109
  continue;
1212
- const pageRel = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
1213
1110
  const pattern = String(unit.pathPattern || '').trim();
1111
+ if (!pattern) {
1112
+ throw new Error(`vmz serve: page unit ${chunkId} missing pathPattern in vmz-deployment.json (plan-only host)`);
1113
+ }
1114
+ const pageRel = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
1214
1115
  out.push({
1215
1116
  chunkId,
1216
1117
  pageRel,
1217
- segs: pattern ? parsePathPattern(pattern) : parseChunkSegments(chunkId),
1118
+ segs: parsePathPattern(pattern),
1218
1119
  });
1219
1120
  }
1220
1121
  }
1221
- catch {
1122
+ catch (err) {
1123
+ if (err && typeof err.message === 'string' && err.message.includes('plan-only host'))
1124
+ throw err;
1222
1125
  return [];
1223
1126
  }
1224
1127
  return out;
1225
1128
  }
1226
- /**
1227
- * File-route segments from chunk id (`pages/Install` → `/install`).
1228
- * Skips URL-invisible `(group)` dirs; boundary stems never reach here.
1229
- * @param {string} chunkId
1230
- */
1231
- function parseChunkSegments(chunkId) {
1232
- const rel = chunkId.replace(/^pages\//, '');
1233
- const parts = rel.split('/').filter(Boolean);
1234
- /** @type {Array<{ kind: 'static' | 'param' | 'catch', value?: string, name?: string }>} */
1235
- const segs = [];
1236
- for (let i = 0; i < parts.length; i++) {
1237
- const p = parts[i];
1238
- if (isRouteGroupDir(p))
1239
- continue;
1240
- if (p === 'index' && i === parts.length - 1)
1241
- continue;
1242
- segs.push(parsePathSegment(p));
1243
- }
1244
- return segs;
1245
- }
1246
1129
  /**
1247
1130
  * Browser HTTP pattern (`/` / `/home` / `/users/:id` / `/blog/[...slug]`).
1248
1131
  * @param {string} pattern
@@ -1306,7 +1189,7 @@ function matchFileRoute(pathname, catalog) {
1306
1189
  return best;
1307
1190
  }
1308
1191
  /**
1309
- * @param {ReturnType<typeof parseChunkSegments>} segs
1192
+ * @param {ReturnType<typeof parsePathPattern>} segs
1310
1193
  * @param {string} pathname
1311
1194
  * @returns {Record<string, string>}
1312
1195
  */
@@ -1335,7 +1218,7 @@ function extractRouteParams(segs, pathname) {
1335
1218
  return params;
1336
1219
  }
1337
1220
  /**
1338
- * @param {ReturnType<typeof parseChunkSegments>} segs
1221
+ * @param {ReturnType<typeof parsePathPattern>} segs
1339
1222
  * @param {string[]} pathParts
1340
1223
  * @returns {number | null}
1341
1224
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/core",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "type": "module",
5
5
  "description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
6
6
  "exports": {
@@ -39,6 +39,14 @@
39
39
  "./route-layout-chain": {
40
40
  "types": "./dist/route-layout-chain.d.ts",
41
41
  "default": "./dist/route-layout-chain.js"
42
+ },
43
+ "./localize-body-links": {
44
+ "types": "./dist/localize-body-links.d.ts",
45
+ "default": "./dist/localize-body-links.js"
46
+ },
47
+ "./position-context": {
48
+ "types": "./dist/position-context.d.ts",
49
+ "default": "./dist/position-context.js"
42
50
  }
43
51
  },
44
52
  "files": [
@@ -58,12 +66,12 @@
58
66
  "linkedom": "^0.18.13"
59
67
  },
60
68
  "optionalDependencies": {
61
- "@vmz/vmz-win32-x64": "0.1.21",
62
- "@vmz/vmz-win32-arm64": "0.1.21",
63
- "@vmz/vmz-darwin-x64": "0.1.21",
64
- "@vmz/vmz-darwin-arm64": "0.1.21",
65
- "@vmz/vmz-linux-x64": "0.1.21",
66
- "@vmz/vmz-linux-arm64": "0.1.21"
69
+ "@vmz/vmz-win32-x64": "0.1.22",
70
+ "@vmz/vmz-win32-arm64": "0.1.22",
71
+ "@vmz/vmz-darwin-x64": "0.1.22",
72
+ "@vmz/vmz-darwin-arm64": "0.1.22",
73
+ "@vmz/vmz-linux-x64": "0.1.22",
74
+ "@vmz/vmz-linux-arm64": "0.1.22"
67
75
  },
68
76
  "publishConfig": {
69
77
  "access": "public"