@vmz/core 0.1.20 → 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.
- package/dist/deployment-registry.d.ts +1 -1
- package/dist/deployment-registry.js +4 -27
- package/dist/direct-host-box.d.ts +26 -0
- package/dist/direct-host-box.js +41 -0
- package/dist/dom-core.d.ts +2 -0
- package/dist/dom-core.js +15 -16
- package/dist/dom-ssr.js +15 -9
- 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/dist/unknown-component.d.ts +40 -0
- package/dist/unknown-component.js +112 -0
- 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 {{
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct host box model (`ui-direct-host-box`).
|
|
3
|
+
* Chip hosts opt into `display: contents`; block surfaces keep a real layout box.
|
|
4
|
+
*/
|
|
5
|
+
/** @type {ReadonlySet<string>} */
|
|
6
|
+
export declare const INLINE_HOST_CONTENTS: Set<string>;
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} name
|
|
9
|
+
* @param {{ __vmzHostBox?: string } | null | undefined} Ctor
|
|
10
|
+
* @returns {'contents' | 'block'}
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveDirectHostBox(name: any, Ctor: any): any;
|
|
13
|
+
/**
|
|
14
|
+
* Apply host box to a live DOM element (create / hydrate / resume).
|
|
15
|
+
* @param {HTMLElement} host
|
|
16
|
+
* @param {string} name
|
|
17
|
+
* @param {{ __vmzHostBox?: string } | null | undefined} Ctor
|
|
18
|
+
*/
|
|
19
|
+
export declare function applyDirectHostBox(host: any, name: any, Ctor: any): void;
|
|
20
|
+
/**
|
|
21
|
+
* SSR attrs fragment for host box (only when contents).
|
|
22
|
+
* @param {string} name
|
|
23
|
+
* @param {{ __vmzHostBox?: string } | null | undefined} Ctor
|
|
24
|
+
* @returns {string | null}
|
|
25
|
+
*/
|
|
26
|
+
export declare function directHostBoxStyleAttr(name: any, Ctor: any): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Direct host box model (`ui-direct-host-box`).
|
|
3
|
+
* Chip hosts opt into `display: contents`; block surfaces keep a real layout box.
|
|
4
|
+
*/
|
|
5
|
+
/** @type {ReadonlySet<string>} */
|
|
6
|
+
export const INLINE_HOST_CONTENTS = new Set(['Button', 'Badge', 'Link', 'Tag', 'Icon']);
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} name
|
|
9
|
+
* @param {{ __vmzHostBox?: string } | null | undefined} Ctor
|
|
10
|
+
* @returns {'contents' | 'block'}
|
|
11
|
+
*/
|
|
12
|
+
export function resolveDirectHostBox(name, Ctor) {
|
|
13
|
+
const explicit = Ctor && typeof Ctor.__vmzHostBox === 'string' ? Ctor.__vmzHostBox : null;
|
|
14
|
+
if (explicit === 'contents' || explicit === 'block')
|
|
15
|
+
return explicit;
|
|
16
|
+
if (INLINE_HOST_CONTENTS.has(String(name || '')))
|
|
17
|
+
return 'contents';
|
|
18
|
+
return 'block';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Apply host box to a live DOM element (create / hydrate / resume).
|
|
22
|
+
* @param {HTMLElement} host
|
|
23
|
+
* @param {string} name
|
|
24
|
+
* @param {{ __vmzHostBox?: string } | null | undefined} Ctor
|
|
25
|
+
*/
|
|
26
|
+
export function applyDirectHostBox(host, name, Ctor) {
|
|
27
|
+
if (!host || !host.style)
|
|
28
|
+
return;
|
|
29
|
+
if (resolveDirectHostBox(name, Ctor) === 'contents') {
|
|
30
|
+
host.style.display = 'contents';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* SSR attrs fragment for host box (only when contents).
|
|
35
|
+
* @param {string} name
|
|
36
|
+
* @param {{ __vmzHostBox?: string } | null | undefined} Ctor
|
|
37
|
+
* @returns {string | null}
|
|
38
|
+
*/
|
|
39
|
+
export function directHostBoxStyleAttr(name, Ctor) {
|
|
40
|
+
return resolveDirectHostBox(name, Ctor) === 'contents' ? 'display:contents' : null;
|
|
41
|
+
}
|
package/dist/dom-core.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* Mount and client patches run that same schedule (SSR/hydrate/resume in dom-ssr).
|
|
7
7
|
* Field writes only run registered dep patches — never re-create structure.
|
|
8
8
|
*/
|
|
9
|
+
export { applyDirectHostBox, INLINE_HOST_CONTENTS, resolveDirectHostBox } from './direct-host-box.js';
|
|
10
|
+
export { createUnknownComponentElement, markUnknownComponentHost, serializeUnknownComponentNode, UNKNOWN_COMPONENT_ERROR, } from './unknown-component.js';
|
|
9
11
|
/** @param {boolean} [on] */
|
|
10
12
|
export declare function __vmzPrecisionEnable(on?: boolean): void;
|
|
11
13
|
/** @param {boolean} [on] */
|
package/dist/dom-core.js
CHANGED
|
@@ -7,14 +7,12 @@
|
|
|
7
7
|
* Mount and client patches run that same schedule (SSR/hydrate/resume in dom-ssr).
|
|
8
8
|
* Field writes only run registered dep patches — never re-create structure.
|
|
9
9
|
*/
|
|
10
|
+
import { applyDirectHostBox } from './direct-host-box.js';
|
|
11
|
+
import { createUnknownComponentElement } from './unknown-component.js';
|
|
12
|
+
export { applyDirectHostBox, INLINE_HOST_CONTENTS, resolveDirectHostBox } from './direct-host-box.js';
|
|
13
|
+
export { createUnknownComponentElement, markUnknownComponentHost, serializeUnknownComponentNode, UNKNOWN_COMPONENT_ERROR, } from './unknown-component.js';
|
|
10
14
|
/** @type {Record<string, new (props?: object) => any>} */
|
|
11
15
|
const components = Object.create(null);
|
|
12
|
-
/**
|
|
13
|
-
* Inline chip hosts that opt into `display: contents` (`ui-direct-host-box`).
|
|
14
|
-
* Block surfaces (DataTable / shells / overlays) must keep a real layout box.
|
|
15
|
-
* @type {Set<string>}
|
|
16
|
-
*/
|
|
17
|
-
const INLINE_HOST_CONTENTS = new Set(['Button', 'Badge', 'Link', 'Tag', 'Icon']);
|
|
18
16
|
/**
|
|
19
17
|
* Precision lab counters (test / MCP / benchmarks — not a user API).
|
|
20
18
|
* Primary keys: BindingId (IR). `*ByDep` is transitional stable-string adapter.
|
|
@@ -452,22 +450,23 @@ export const directApi = {
|
|
|
452
450
|
// resume: resume on schedule; EventEntry may lazy-load chunk via __vmzLoadComponent.
|
|
453
451
|
scheduleClientOn(host, String(client), async () => {
|
|
454
452
|
const Ctor = await resolveComponent(name);
|
|
455
|
-
if (!Ctor)
|
|
456
|
-
|
|
453
|
+
if (!Ctor) {
|
|
454
|
+
// Replace placeholder island with leaf error node (do not throw page).
|
|
455
|
+
const err = createUnknownComponentElement(name, 'island');
|
|
456
|
+
host.replaceWith(err);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
457
459
|
await resume(Ctor, host, { props: resolved, state: {} });
|
|
458
460
|
});
|
|
459
461
|
return host;
|
|
460
462
|
}
|
|
461
463
|
const Ctor = components[name];
|
|
462
|
-
if (!Ctor)
|
|
463
|
-
|
|
464
|
-
// Inline chips opt into no-box host (`ui-direct-host-box`). Block surfaces
|
|
465
|
-
// (tables/shells/overlays) keep a real box — default `contents` breaks
|
|
466
|
-
// hit-testing / slot hosts (DataTable select timed out in ui-automation).
|
|
467
|
-
const hostBox = Ctor.__vmzHostBox;
|
|
468
|
-
if (hostBox === 'contents' || (hostBox == null && INLINE_HOST_CONTENTS.has(String(name)))) {
|
|
469
|
-
host.style.display = 'contents';
|
|
464
|
+
if (!Ctor) {
|
|
465
|
+
return createUnknownComponentElement(name, 'client');
|
|
470
466
|
}
|
|
467
|
+
// Inline chips → `display: contents` (`ui-direct-host-box`). Block surfaces
|
|
468
|
+
// keep a real box (DataTable select timed out when defaulting everything).
|
|
469
|
+
applyDirectHostBox(host, name, Ctor);
|
|
471
470
|
const child = createInstance(Ctor, resolved);
|
|
472
471
|
if (!(Ctor.__vmzDirect && typeof Ctor.__vmzCreate === 'function')) {
|
|
473
472
|
throw new Error(`vmz:dom direct component <${name}> requires __vmzCreate (rebuild child with Direct)`);
|
package/dist/dom-ssr.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* VMZ DOM SSR / hydrate / resume — precise patches, no VDOM diff.
|
|
4
4
|
* Imports client DOM primitives from ./dom-core.js for tree-shakeable browser entry.
|
|
5
5
|
*/
|
|
6
|
+
import { applyDirectHostBox, directHostBoxStyleAttr } from './direct-host-box.js';
|
|
6
7
|
import { applyDomAttr, applyPreservedState, BOOLEAN_HTML_ATTRS, createInstance, destroy, directApi, eventPropHandlerName, getRegisteredComponent, hasMeaningfulChild, isEventEntryStrategy, mount, noteDomCreate, resolveComponent, runDirectCreate, scheduleClientOn, settlePendingChildMounts, snapshotInstanceState, stripFns, } from './dom-core.js';
|
|
8
|
+
import { markUnknownComponentHost, serializeUnknownComponentNode } from './unknown-component.js';
|
|
7
9
|
/** @type {Error | null} last linkedom resolve failure (for clear SSR errors) */
|
|
8
10
|
let _ssrDocumentLastError = null;
|
|
9
11
|
/**
|
|
@@ -742,8 +744,9 @@ const serializeApi = {
|
|
|
742
744
|
},
|
|
743
745
|
component(hostInst, name, props, client) {
|
|
744
746
|
const Ctor = getRegisteredComponent(name);
|
|
745
|
-
if (!Ctor)
|
|
746
|
-
|
|
747
|
+
if (!Ctor) {
|
|
748
|
+
return serializeUnknownComponentNode(name);
|
|
749
|
+
}
|
|
747
750
|
/** @type {Record<string, any>} */
|
|
748
751
|
const resolved = {};
|
|
749
752
|
for (const [k, v] of Object.entries(props || {})) {
|
|
@@ -788,6 +791,9 @@ const serializeApi = {
|
|
|
788
791
|
'data-vmz-props': JSON.stringify(stripFns(resolved)),
|
|
789
792
|
'data-vmz-resume': JSON.stringify(resume),
|
|
790
793
|
};
|
|
794
|
+
const boxStyle = directHostBoxStyleAttr(name, Ctor);
|
|
795
|
+
if (boxStyle)
|
|
796
|
+
attrs.style = boxStyle;
|
|
791
797
|
if (isEventEntryStrategy(String(client))) {
|
|
792
798
|
attrs['data-vmz-entry'] = 'event';
|
|
793
799
|
}
|
|
@@ -810,11 +816,9 @@ const serializeApi = {
|
|
|
810
816
|
const node = Ctor.__vmzCreate.call(child, serializeApi);
|
|
811
817
|
/** @type {Record<string, string>} */
|
|
812
818
|
const attrs = { 'data-vmz': name };
|
|
813
|
-
const
|
|
814
|
-
if (
|
|
815
|
-
|
|
816
|
-
attrs.style = 'display:contents';
|
|
817
|
-
}
|
|
819
|
+
const boxStyle = directHostBoxStyleAttr(name, Ctor);
|
|
820
|
+
if (boxStyle)
|
|
821
|
+
attrs.style = boxStyle;
|
|
818
822
|
return {
|
|
819
823
|
__kind: 'el',
|
|
820
824
|
tag: 'div',
|
|
@@ -892,6 +896,8 @@ export async function resume(Component, container, slice = null) {
|
|
|
892
896
|
destroy(container.__vmzInst);
|
|
893
897
|
container.__vmzInst = null;
|
|
894
898
|
}
|
|
899
|
+
const hostName = container.getAttribute('data-vmz') || container.getAttribute('data-vmz-island') || Component.name || '';
|
|
900
|
+
applyDirectHostBox(container, hostName, Component);
|
|
895
901
|
const props = parsed.props || {};
|
|
896
902
|
const inst = createInstance(Component, props);
|
|
897
903
|
if (parsed.state)
|
|
@@ -935,7 +941,7 @@ export function resumeIslands(root = globalThis.document) {
|
|
|
935
941
|
scheduleClientOn(el, strategy, async () => {
|
|
936
942
|
const Ctor = await resolveComponent(name);
|
|
937
943
|
if (!Ctor) {
|
|
938
|
-
|
|
944
|
+
markUnknownComponentHost(el, name, 'resume');
|
|
939
945
|
return;
|
|
940
946
|
}
|
|
941
947
|
await resume(Ctor, el);
|
|
@@ -961,7 +967,7 @@ export function attachEventEntries(root = globalThis.document) {
|
|
|
961
967
|
scheduleClientOn(el, strategy, async () => {
|
|
962
968
|
const Ctor = await resolveComponent(name);
|
|
963
969
|
if (!Ctor) {
|
|
964
|
-
|
|
970
|
+
markUnknownComponentHost(el, name, 'event-entry');
|
|
965
971
|
return;
|
|
966
972
|
}
|
|
967
973
|
await resume(Ctor, el);
|
|
@@ -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
|
*/
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unknown Direct component resilience (`ssr-unknown-component-error-node`).
|
|
3
|
+
* Leaf failure → subtree error node; do not throw out of document SSR.
|
|
4
|
+
*/
|
|
5
|
+
/** @type {string} */
|
|
6
|
+
export declare const UNKNOWN_COMPONENT_ERROR = "unknown-component";
|
|
7
|
+
/**
|
|
8
|
+
* Append a leaf diagnostic for tooling (does not replace the error node).
|
|
9
|
+
* @param {string} name
|
|
10
|
+
* @param {string} [via]
|
|
11
|
+
*/
|
|
12
|
+
export declare function noteUnknownComponent(name: any, via?: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* SSR serialize-tree error node (keeps document HTTP 200).
|
|
15
|
+
* @param {string} name
|
|
16
|
+
*/
|
|
17
|
+
export declare function serializeUnknownComponentNode(name: any): {
|
|
18
|
+
__kind: string;
|
|
19
|
+
tag: string;
|
|
20
|
+
attrs: {
|
|
21
|
+
'data-vmz-error': string;
|
|
22
|
+
'data-vmz-component': string;
|
|
23
|
+
};
|
|
24
|
+
children: any[];
|
|
25
|
+
appendChild(c: any): void;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Live DOM error host (client create / schedule).
|
|
29
|
+
* @param {string} name
|
|
30
|
+
* @param {string} [via]
|
|
31
|
+
* @returns {HTMLElement}
|
|
32
|
+
*/
|
|
33
|
+
export declare function createUnknownComponentElement(name: any, via?: string): HTMLDivElement;
|
|
34
|
+
/**
|
|
35
|
+
* Mark an existing island host as unknown (resume / EventEntry).
|
|
36
|
+
* @param {HTMLElement} el
|
|
37
|
+
* @param {string} name
|
|
38
|
+
* @param {string} [via]
|
|
39
|
+
*/
|
|
40
|
+
export declare function markUnknownComponentHost(el: any, name: any, via?: string): void;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unknown Direct component resilience (`ssr-unknown-component-error-node`).
|
|
3
|
+
* Leaf failure → subtree error node; do not throw out of document SSR.
|
|
4
|
+
*/
|
|
5
|
+
/** @type {string} */
|
|
6
|
+
export const UNKNOWN_COMPONENT_ERROR = 'unknown-component';
|
|
7
|
+
/**
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
function isDevSurface() {
|
|
11
|
+
try {
|
|
12
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
13
|
+
if (process.env.VMZ_DEV === '1' || process.env.VMZ_DEV === 'true')
|
|
14
|
+
return true;
|
|
15
|
+
if (process.env.NODE_ENV === 'development')
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
/* ignore */
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Append a leaf diagnostic for tooling (does not replace the error node).
|
|
26
|
+
* @param {string} name
|
|
27
|
+
* @param {string} [via]
|
|
28
|
+
*/
|
|
29
|
+
export function noteUnknownComponent(name, via = 'ssr') {
|
|
30
|
+
const detail = { kind: UNKNOWN_COMPONENT_ERROR, component: String(name || ''), via };
|
|
31
|
+
try {
|
|
32
|
+
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
|
33
|
+
console.error(`vmz:dom unknown component <${name} /> (${via})`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* ignore */
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const g = typeof globalThis !== 'undefined' ? globalThis : null;
|
|
41
|
+
if (!g)
|
|
42
|
+
return;
|
|
43
|
+
if (!Array.isArray(g.__VMZ_COMPONENT_ERRORS__)) {
|
|
44
|
+
g.__VMZ_COMPONENT_ERRORS__ = [];
|
|
45
|
+
}
|
|
46
|
+
/** @type {unknown[]} */
|
|
47
|
+
const bag = g.__VMZ_COMPONENT_ERRORS__;
|
|
48
|
+
bag.push(detail);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* ignore */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* SSR serialize-tree error node (keeps document HTTP 200).
|
|
56
|
+
* @param {string} name
|
|
57
|
+
*/
|
|
58
|
+
export function serializeUnknownComponentNode(name) {
|
|
59
|
+
noteUnknownComponent(name, 'ssr');
|
|
60
|
+
/** @type {Record<string, string>} */
|
|
61
|
+
const attrs = {
|
|
62
|
+
'data-vmz-error': UNKNOWN_COMPONENT_ERROR,
|
|
63
|
+
'data-vmz-component': String(name || ''),
|
|
64
|
+
};
|
|
65
|
+
/** @type {any[]} */
|
|
66
|
+
const children = [];
|
|
67
|
+
if (isDevSurface()) {
|
|
68
|
+
children.push({
|
|
69
|
+
__kind: 'text',
|
|
70
|
+
value: `Unknown component <${name} />`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
__kind: 'el',
|
|
75
|
+
tag: 'div',
|
|
76
|
+
attrs,
|
|
77
|
+
children,
|
|
78
|
+
appendChild(c) {
|
|
79
|
+
if (c != null)
|
|
80
|
+
this.children.push(c);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Live DOM error host (client create / schedule).
|
|
86
|
+
* @param {string} name
|
|
87
|
+
* @param {string} [via]
|
|
88
|
+
* @returns {HTMLElement}
|
|
89
|
+
*/
|
|
90
|
+
export function createUnknownComponentElement(name, via = 'client') {
|
|
91
|
+
noteUnknownComponent(name, via);
|
|
92
|
+
const host = document.createElement('div');
|
|
93
|
+
host.setAttribute('data-vmz-error', UNKNOWN_COMPONENT_ERROR);
|
|
94
|
+
host.setAttribute('data-vmz-component', String(name || ''));
|
|
95
|
+
if (isDevSurface()) {
|
|
96
|
+
host.textContent = `Unknown component <${name} />`;
|
|
97
|
+
}
|
|
98
|
+
return host;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Mark an existing island host as unknown (resume / EventEntry).
|
|
102
|
+
* @param {HTMLElement} el
|
|
103
|
+
* @param {string} name
|
|
104
|
+
* @param {string} [via]
|
|
105
|
+
*/
|
|
106
|
+
export function markUnknownComponentHost(el, name, via = 'resume') {
|
|
107
|
+
if (!el)
|
|
108
|
+
return;
|
|
109
|
+
noteUnknownComponent(name, via);
|
|
110
|
+
el.setAttribute('data-vmz-error', UNKNOWN_COMPONENT_ERROR);
|
|
111
|
+
el.setAttribute('data-vmz-component', String(name || ''));
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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.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"
|