@vmz/core 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deployment-registry.d.ts +1 -1
- package/dist/deployment-registry.js +4 -27
- package/dist/list-client-components.d.ts +2 -2
- package/dist/list-client-components.js +6 -38
- package/dist/localize-body-links.d.ts +19 -0
- package/dist/localize-body-links.js +89 -0
- package/dist/position-context.d.ts +15 -0
- package/dist/position-context.js +19 -0
- package/dist/route-layout-chain.d.ts +5 -5
- package/dist/route-layout-chain.js +19 -24
- package/dist/serve-host.mjs +21 -138
- package/package.json +15 -7
|
@@ -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
|
|
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 (
|
|
135
|
-
|
|
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
|
|
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
|
|
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
|
|
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} (
|
|
32
|
+
throw new Error(`vmz: missing vmz-deployment.json under ${dir} (plan-only host)`);
|
|
35
33
|
}
|
|
36
|
-
|
|
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} (
|
|
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
|
|
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, '&').replace(/"/g, '"')) {
|
|
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
|
-
*
|
|
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
|
|
7
|
+
* True when Deployment Plan lists an Application shell unit.
|
|
8
8
|
*/
|
|
9
9
|
export declare function hasApplicationShell(distDir: string): boolean;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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
|
-
*
|
|
2
|
+
* Layout chain from Deployment Plan only (plan-only host).
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
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
|
|
8
|
+
* True when Deployment Plan lists an Application shell unit.
|
|
10
9
|
*/
|
|
11
10
|
export function hasApplicationShell(distDir) {
|
|
12
|
-
|
|
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
|
-
*
|
|
16
|
-
*
|
|
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
|
-
|
|
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
|
|
23
|
+
* Full SSR / hydrate layout chain from Deployment Plan `layoutChain`.
|
|
34
24
|
*/
|
|
35
25
|
export function resolveRouteLayoutChain(distDir, pageChunkId) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
35
|
+
return unit.layoutChain.map((id) => String(id));
|
|
41
36
|
}
|
package/dist/serve-host.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1156
|
-
* `vmz-deployment.json
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.1.23",
|
|
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.
|
|
62
|
-
"@vmz/vmz-win32-arm64": "0.1.
|
|
63
|
-
"@vmz/vmz-darwin-x64": "0.1.
|
|
64
|
-
"@vmz/vmz-darwin-arm64": "0.1.
|
|
65
|
-
"@vmz/vmz-linux-x64": "0.1.
|
|
66
|
-
"@vmz/vmz-linux-arm64": "0.1.
|
|
69
|
+
"@vmz/vmz-win32-x64": "0.1.23",
|
|
70
|
+
"@vmz/vmz-win32-arm64": "0.1.23",
|
|
71
|
+
"@vmz/vmz-darwin-x64": "0.1.23",
|
|
72
|
+
"@vmz/vmz-darwin-arm64": "0.1.23",
|
|
73
|
+
"@vmz/vmz-linux-x64": "0.1.23",
|
|
74
|
+
"@vmz/vmz-linux-arm64": "0.1.23"
|
|
67
75
|
},
|
|
68
76
|
"publishConfig": {
|
|
69
77
|
"access": "public"
|