@vmz/core 0.1.8 → 0.1.10

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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Deployment graph → component registry bootstrap (shared by all SSR/DOM hosts).
3
+ * Replaces ad-hoc registerComponents calls with an explicit preload contract.
4
+ */
5
+ export declare const DEPLOYMENT_SCHEMA = "vmz.deployment.v0";
6
+ /**
7
+ * @param {string} distDir
8
+ * @param {{ strict?: boolean }} [opts]
9
+ * @returns {any | null}
10
+ */
11
+ export declare function readDeploymentDocument(distDir: any, opts?: {}): any;
12
+ /**
13
+ * @param {any} deployment
14
+ * @returns {Array<{ chunkId: string, name: string, entry: string, source: string }>}
15
+ */
16
+ export declare function componentEntriesFromDeployment(deployment: any): any[];
17
+ /**
18
+ * @param {any} deployment
19
+ * @param {string[]} rootChunkIds
20
+ * @returns {Set<string>}
21
+ */
22
+ export declare function collectDependsOnClosure(deployment: any, rootChunkIds: any): Set<unknown>;
23
+ /**
24
+ * Resolve tag conflicts; strict mode throws, dev mode warns and keeps last chunkId.
25
+ * @param {Array<{ chunkId: string, name: string, entry: string, source?: string }>} entries
26
+ * @param {{ strict?: boolean }} [opts]
27
+ * @returns {Array<{ chunkId: string, name: string, entry: string, source?: string }>}
28
+ */
29
+ export declare function dedupeComponentEntriesByTag(entries: any, opts?: {}): any[];
30
+ /**
31
+ * @param {Array<{ chunkId: string, name: string, entry: string, source?: string }>} entries
32
+ * @param {Record<string, string> | undefined} explicit name → chunkId (no .client.js)
33
+ * @returns {Array<{ chunkId: string, name: string, entry: string, source?: string }>}
34
+ */
35
+ export declare function mergeExplicitComponentEntries(entries: any, explicit: any): unknown[];
36
+ /**
37
+ * @param {string} distDir
38
+ * @param {{
39
+ * strict?: boolean,
40
+ * closureRoots?: string[],
41
+ * explicit?: Record<string, string>,
42
+ * }} [opts]
43
+ * @returns {Promise<Array<{ chunkId: string, name: string, entry: string, source?: string }>>}
44
+ */
45
+ export declare function loadComponentEntries(distDir: any, opts?: {}): Promise<any[]>;
46
+ /**
47
+ * @param {string} distDir
48
+ * @param {Array<{ chunkId: string, name: string, entry: string }>} entries
49
+ * @param {(map: Record<string, unknown>) => void} registerComponents
50
+ * @param {{
51
+ * cacheBust?: string | number,
52
+ * loaded?: Set<string>,
53
+ * }} [opts]
54
+ * @returns {Promise<Record<string, unknown>>}
55
+ */
56
+ export declare function importAndRegisterComponentEntries(distDir: any, entries: any, registerComponents: any, opts?: {}): Promise<{}>;
57
+ /**
58
+ * Bootstrap component registry from deployment (all, closure, or directory fallback).
59
+ * @param {string} distDir
60
+ * @param {(map: Record<string, unknown>) => void} registerComponents
61
+ * @param {{
62
+ * strict?: boolean,
63
+ * closureRoots?: string[],
64
+ * explicit?: Record<string, string>,
65
+ * cacheBust?: string | number,
66
+ * loaded?: Set<string>,
67
+ * preload?: 'all' | 'closure' | 'none',
68
+ * }} [opts]
69
+ */
70
+ export declare function bootstrapComponentRegistry(distDir: any, registerComponents: any, opts?: {}): Promise<{}>;
@@ -0,0 +1,256 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Deployment graph → component registry bootstrap (shared by all SSR/DOM hosts).
4
+ * Replaces ad-hoc registerComponents calls with an explicit preload contract.
5
+ */
6
+ import fs from 'node:fs';
7
+ import { readdir } from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ export const DEPLOYMENT_SCHEMA = 'vmz.deployment.v0';
11
+ /**
12
+ * @param {string} distDir
13
+ * @param {{ strict?: boolean }} [opts]
14
+ * @returns {any | null}
15
+ */
16
+ export function readDeploymentDocument(distDir, opts = {}) {
17
+ const strict = opts.strict === true;
18
+ const filePath = path.join(distDir, 'vmz-deployment.json');
19
+ if (!fs.existsSync(filePath)) {
20
+ if (strict) {
21
+ throw new Error(`vmz: missing vmz-deployment.json under ${distDir} (strict deployment mode)`);
22
+ }
23
+ return null;
24
+ }
25
+ let raw;
26
+ try {
27
+ raw = fs.readFileSync(filePath, 'utf8');
28
+ }
29
+ catch (e) {
30
+ if (strict)
31
+ throw new Error(`vmz: cannot read vmz-deployment.json: ${e instanceof Error ? e.message : e}`);
32
+ return null;
33
+ }
34
+ let doc;
35
+ try {
36
+ doc = JSON.parse(raw);
37
+ }
38
+ catch (e) {
39
+ if (strict)
40
+ throw new Error(`vmz: invalid vmz-deployment.json: ${e instanceof Error ? e.message : e}`);
41
+ return null;
42
+ }
43
+ if (doc.schema !== DEPLOYMENT_SCHEMA) {
44
+ if (strict)
45
+ throw new Error(`vmz: unsupported deployment schema ${doc.schema}`);
46
+ return null;
47
+ }
48
+ return doc;
49
+ }
50
+ /**
51
+ * @param {any} deployment
52
+ * @returns {Array<{ chunkId: string, name: string, entry: string, source: string }>}
53
+ */
54
+ export function componentEntriesFromDeployment(deployment) {
55
+ /** @type {Array<{ chunkId: string, name: string, entry: string, source: string }>} */
56
+ const out = [];
57
+ for (const unit of deployment.units || []) {
58
+ if (unit?.kind !== 'component')
59
+ continue;
60
+ const chunkId = String(unit.chunkId || '').replace(/\\/g, '/');
61
+ const name = chunkId.split('/').pop();
62
+ if (!name)
63
+ continue;
64
+ const entry = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
65
+ out.push({
66
+ chunkId,
67
+ name,
68
+ entry,
69
+ source: String(unit.source || ''),
70
+ });
71
+ }
72
+ return out.sort((a, b) => a.chunkId.localeCompare(b.chunkId));
73
+ }
74
+ /**
75
+ * @param {any} deployment
76
+ * @param {string[]} rootChunkIds
77
+ * @returns {Set<string>}
78
+ */
79
+ export function collectDependsOnClosure(deployment, rootChunkIds) {
80
+ /** @type {Map<string, any>} */
81
+ const byId = new Map();
82
+ for (const unit of deployment.units || []) {
83
+ byId.set(String(unit.chunkId || '').replace(/\\/g, '/'), unit);
84
+ }
85
+ /** @type {Set<string>} */
86
+ const out = new Set();
87
+ /** @type {string[]} */
88
+ const stack = rootChunkIds.map((id) => String(id).replace(/\\/g, '/')).filter(Boolean);
89
+ while (stack.length) {
90
+ const id = stack.pop();
91
+ if (!id || out.has(id))
92
+ continue;
93
+ out.add(id);
94
+ const unit = byId.get(id);
95
+ if (!unit)
96
+ continue;
97
+ for (const dep of unit.dependsOn || []) {
98
+ const d = String(dep).replace(/\\/g, '/');
99
+ if (!out.has(d))
100
+ stack.push(d);
101
+ }
102
+ }
103
+ return out;
104
+ }
105
+ /**
106
+ * Resolve tag conflicts; strict mode throws, dev mode warns and keeps last chunkId.
107
+ * @param {Array<{ chunkId: string, name: string, entry: string, source?: string }>} entries
108
+ * @param {{ strict?: boolean }} [opts]
109
+ * @returns {Array<{ chunkId: string, name: string, entry: string, source?: string }>}
110
+ */
111
+ export function dedupeComponentEntriesByTag(entries, opts = {}) {
112
+ const strict = opts.strict === true;
113
+ /** @type {Map<string, { chunkId: string, name: string, entry: string, source?: string }>} */
114
+ const byTag = new Map();
115
+ for (const e of entries) {
116
+ const prev = byTag.get(e.name);
117
+ if (prev && prev.chunkId !== e.chunkId) {
118
+ const msg = `vmz: component tag <${e.name}> maps to both ${prev.chunkId} and ${e.chunkId}`;
119
+ if (strict)
120
+ throw new Error(msg);
121
+ console.warn(`${msg}; using ${e.chunkId}`);
122
+ }
123
+ byTag.set(e.name, e);
124
+ }
125
+ return [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name));
126
+ }
127
+ /**
128
+ * @param {Array<{ chunkId: string, name: string, entry: string, source?: string }>} entries
129
+ * @param {Record<string, string> | undefined} explicit name → chunkId (no .client.js)
130
+ * @returns {Array<{ chunkId: string, name: string, entry: string, source?: string }>}
131
+ */
132
+ export function mergeExplicitComponentEntries(entries, explicit) {
133
+ /** @type {Map<string, { chunkId: string, name: string, entry: string, source?: string }>} */
134
+ const byTag = new Map(entries.map((e) => [e.name, e]));
135
+ if (explicit) {
136
+ for (const [name, chunk] of Object.entries(explicit)) {
137
+ const chunkId = String(chunk).replace(/\\/g, '/');
138
+ byTag.set(name, {
139
+ chunkId,
140
+ name,
141
+ entry: `${chunkId}.client.js`,
142
+ source: '',
143
+ });
144
+ }
145
+ }
146
+ return [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name));
147
+ }
148
+ /**
149
+ * @param {string} distDir
150
+ * @param {{
151
+ * strict?: boolean,
152
+ * closureRoots?: string[],
153
+ * explicit?: Record<string, string>,
154
+ * }} [opts]
155
+ * @returns {Promise<Array<{ chunkId: string, name: string, entry: string, source?: string }>>}
156
+ */
157
+ export async function loadComponentEntries(distDir, opts = {}) {
158
+ const strict = opts.strict === true;
159
+ const deployment = readDeploymentDocument(distDir, { strict });
160
+ /** @type {Array<{ chunkId: string, name: string, entry: string, source?: string }>} */
161
+ let entries = [];
162
+ if (deployment) {
163
+ entries = componentEntriesFromDeployment(deployment);
164
+ if (opts.closureRoots?.length) {
165
+ const closure = collectDependsOnClosure(deployment, opts.closureRoots);
166
+ entries = entries.filter((e) => closure.has(e.chunkId));
167
+ }
168
+ }
169
+ else if (!strict) {
170
+ entries = await listComponentEntriesFromDirectory(distDir);
171
+ }
172
+ entries = mergeExplicitComponentEntries(entries, opts.explicit);
173
+ return dedupeComponentEntriesByTag(entries, { strict });
174
+ }
175
+ /**
176
+ * @param {string} distDir
177
+ * @returns {Promise<Array<{ chunkId: string, name: string, entry: string }>>}
178
+ */
179
+ async function listComponentEntriesFromDirectory(distDir) {
180
+ const folder = path.join(distDir, 'components');
181
+ /** @type {string[]} */
182
+ let files = [];
183
+ try {
184
+ files = await readdir(folder);
185
+ }
186
+ catch {
187
+ return [];
188
+ }
189
+ return files
190
+ .filter((name) => name.endsWith('.client.js'))
191
+ .map((f) => {
192
+ const name = f.replace(/\.client\.js$/, '');
193
+ const chunkId = `components/${name}`;
194
+ return { chunkId, name, entry: `components/${name}.client.js` };
195
+ })
196
+ .sort((a, b) => a.name.localeCompare(b.name));
197
+ }
198
+ /**
199
+ * @param {string} distDir
200
+ * @param {Array<{ chunkId: string, name: string, entry: string }>} entries
201
+ * @param {(map: Record<string, unknown>) => void} registerComponents
202
+ * @param {{
203
+ * cacheBust?: string | number,
204
+ * loaded?: Set<string>,
205
+ * }} [opts]
206
+ * @returns {Promise<Record<string, unknown>>}
207
+ */
208
+ export async function importAndRegisterComponentEntries(distDir, entries, registerComponents, opts = {}) {
209
+ /** @type {Record<string, unknown>} */
210
+ const map = {};
211
+ const loaded = opts.loaded ?? null;
212
+ for (const entry of entries) {
213
+ if (loaded && loaded.has(entry.chunkId))
214
+ continue;
215
+ const abs = path.join(distDir, entry.entry);
216
+ let href = pathToFileURL(abs).href;
217
+ if (opts.cacheBust != null && opts.cacheBust !== '') {
218
+ href = `${href}?t=${encodeURIComponent(String(opts.cacheBust))}`;
219
+ }
220
+ const mod = await import(href);
221
+ map[entry.name] = mod.default;
222
+ if (loaded)
223
+ loaded.add(entry.chunkId);
224
+ }
225
+ if (Object.keys(map).length)
226
+ registerComponents(map);
227
+ return map;
228
+ }
229
+ /**
230
+ * Bootstrap component registry from deployment (all, closure, or directory fallback).
231
+ * @param {string} distDir
232
+ * @param {(map: Record<string, unknown>) => void} registerComponents
233
+ * @param {{
234
+ * strict?: boolean,
235
+ * closureRoots?: string[],
236
+ * explicit?: Record<string, string>,
237
+ * cacheBust?: string | number,
238
+ * loaded?: Set<string>,
239
+ * preload?: 'all' | 'closure' | 'none',
240
+ * }} [opts]
241
+ */
242
+ export async function bootstrapComponentRegistry(distDir, registerComponents, opts = {}) {
243
+ const preload = opts.preload ?? (opts.closureRoots?.length ? 'closure' : 'all');
244
+ if (preload === 'none')
245
+ return {};
246
+ const loadOpts = {
247
+ strict: opts.strict,
248
+ explicit: opts.explicit,
249
+ closureRoots: preload === 'closure' ? opts.closureRoots : undefined,
250
+ };
251
+ const entries = await loadComponentEntries(distDir, loadOpts);
252
+ return importAndRegisterComponentEntries(distDir, entries, registerComponents, {
253
+ cacheBust: opts.cacheBust,
254
+ loaded: opts.loaded,
255
+ });
256
+ }
@@ -39,7 +39,7 @@ export declare function __vmzPrecisionSnapshot(): {
39
39
  patchesByBinding: any;
40
40
  };
41
41
  export declare function noteDomCreate(): void;
42
- /** @param {Record<string, any>} map */
42
+ /** @param {Record<string, any>} map — Prefer createRenderHost().ensureComponents(); process-global registry. */
43
43
  export declare function registerComponents(map: any): void;
44
44
  /** Sync registry lookup (SSR serialize / client). */
45
45
  export declare function getRegisteredComponent(name: any): any;
package/dist/dom-core.js CHANGED
@@ -144,7 +144,7 @@ function noteDomMove() {
144
144
  if (precision.enabled)
145
145
  precision.domMoves++;
146
146
  }
147
- /** @param {Record<string, any>} map */
147
+ /** @param {Record<string, any>} map — Prefer createRenderHost().ensureComponents(); process-global registry. */
148
148
  export function registerComponents(map) {
149
149
  Object.assign(components, map);
150
150
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Discover compiled client component modules from dist (deployment graph or components/).
3
+ * Shared by serve-host SSR, static emit, and test hosts.
4
+ */
5
+ export { DEPLOYMENT_SCHEMA, readDeploymentDocument, componentEntriesFromDeployment, collectDependsOnClosure, dedupeComponentEntriesByTag, mergeExplicitComponentEntries, loadComponentEntries, importAndRegisterComponentEntries, bootstrapComponentRegistry, } from './deployment-registry.js';
6
+ export { createRenderHost } from './render-host.js';
7
+ /**
8
+ * @param {string} dir
9
+ * @param {{ strict?: boolean }} [opts]
10
+ * @returns {Promise<Array<{ name: string, entry: string, chunkId?: string }>>}
11
+ */
12
+ export declare function listClientComponents(dir: any, opts?: {}): Promise<{
13
+ name: any;
14
+ entry: any;
15
+ chunkId: any;
16
+ }[]>;
17
+ /**
18
+ * Sync variant for callers that already use fs sync (legacy static-emit helpers).
19
+ * @param {string} dir
20
+ * @param {{ strict?: boolean }} [opts]
21
+ * @returns {Array<{ name: string, entry: string, chunkId?: string }>}
22
+ */
23
+ export declare function listClientComponentsSync(dir: any, opts?: {}): {
24
+ name: any;
25
+ entry: any;
26
+ chunkId: any;
27
+ }[];
28
+ /**
29
+ * @param {Array<{ name: string, entry: string, chunkId?: string }>} entries
30
+ * @param {Record<string, string> | undefined} explicit
31
+ * @returns {Array<{ name: string, entry: string, chunkId?: string }>}
32
+ */
33
+ export declare function mergeComponentEntries(entries: any, explicit: any): {
34
+ name: any;
35
+ entry: any;
36
+ chunkId: any;
37
+ }[];
38
+ /**
39
+ * @param {string} distDir
40
+ * @param {Record<string, string> | undefined} [explicit]
41
+ * @param {{ strict?: boolean, closureRoots?: string[] }} [opts]
42
+ * @returns {Promise<Array<{ name: string, entry: string, chunkId?: string }>>}
43
+ */
44
+ export declare function resolveComponentEntries(distDir: any, explicit: any, opts?: {}): Promise<{
45
+ name: any;
46
+ entry: any;
47
+ chunkId: any;
48
+ }[]>;
49
+ /**
50
+ * Import all (or filtered) client components and register for SSR / static emit / test hosts.
51
+ * @param {string} distDir
52
+ * @param {(map: Record<string, unknown>) => void} registerComponents
53
+ * @param {{
54
+ * cacheBust?: string | number,
55
+ * include?: (entry: { name: string, entry: string, chunkId?: string }) => boolean,
56
+ * explicit?: Record<string, string>,
57
+ * strict?: boolean,
58
+ * closureRoots?: string[],
59
+ * preload?: 'all' | 'closure' | 'none',
60
+ * }} [opts]
61
+ */
62
+ export declare function preloadComponentRegistry(distDir: any, registerComponents: any, opts?: {}): Promise<{}>;
@@ -0,0 +1,142 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Discover compiled client component modules from dist (deployment graph or components/).
4
+ * Shared by serve-host SSR, static emit, and test hosts.
5
+ */
6
+ import fs from 'node:fs';
7
+ import { readdir } from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ import { componentEntriesFromDeployment, mergeExplicitComponentEntries, readDeploymentDocument, } from './deployment-registry.js';
11
+ export { DEPLOYMENT_SCHEMA, readDeploymentDocument, componentEntriesFromDeployment, collectDependsOnClosure, dedupeComponentEntriesByTag, mergeExplicitComponentEntries, loadComponentEntries, importAndRegisterComponentEntries, bootstrapComponentRegistry, } from './deployment-registry.js';
12
+ export { createRenderHost } from './render-host.js';
13
+ /**
14
+ * @param {string} dir
15
+ * @param {{ strict?: boolean }} [opts]
16
+ * @returns {Promise<Array<{ name: string, entry: string, chunkId?: string }>>}
17
+ */
18
+ export async function listClientComponents(dir, opts = {}) {
19
+ const deployment = readDeploymentDocument(dir, { strict: opts.strict === true });
20
+ if (deployment) {
21
+ return componentEntriesFromDeployment(deployment).map((e) => ({
22
+ name: e.name,
23
+ entry: e.entry,
24
+ chunkId: e.chunkId,
25
+ }));
26
+ }
27
+ const folder = path.join(dir, 'components');
28
+ /** @type {string[]} */
29
+ let files = [];
30
+ try {
31
+ files = await readdir(folder);
32
+ }
33
+ catch {
34
+ return [];
35
+ }
36
+ return files
37
+ .filter((name) => name.endsWith('.client.js'))
38
+ .map((f) => {
39
+ const name = f.replace(/\.client\.js$/, '');
40
+ return { name, entry: `components/${name}.client.js`, chunkId: `components/${name}` };
41
+ })
42
+ .sort((a, b) => a.name.localeCompare(b.name));
43
+ }
44
+ /**
45
+ * Sync variant for callers that already use fs sync (legacy static-emit helpers).
46
+ * @param {string} dir
47
+ * @param {{ strict?: boolean }} [opts]
48
+ * @returns {Array<{ name: string, entry: string, chunkId?: string }>}
49
+ */
50
+ export function listClientComponentsSync(dir, opts = {}) {
51
+ const deployment = readDeploymentDocument(dir, { strict: opts.strict === true });
52
+ if (deployment) {
53
+ return componentEntriesFromDeployment(deployment).map((e) => ({
54
+ name: e.name,
55
+ entry: e.entry,
56
+ chunkId: e.chunkId,
57
+ }));
58
+ }
59
+ const folder = path.join(dir, 'components');
60
+ /** @type {string[]} */
61
+ let files = [];
62
+ try {
63
+ files = fs.readdirSync(folder);
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ return files
69
+ .filter((name) => name.endsWith('.client.js'))
70
+ .map((f) => {
71
+ const name = f.replace(/\.client\.js$/, '');
72
+ return { name, entry: `components/${name}.client.js`, chunkId: `components/${name}` };
73
+ })
74
+ .sort((a, b) => a.name.localeCompare(b.name));
75
+ }
76
+ /**
77
+ * @param {Array<{ name: string, entry: string, chunkId?: string }>} entries
78
+ * @param {Record<string, string> | undefined} explicit
79
+ * @returns {Array<{ name: string, entry: string, chunkId?: string }>}
80
+ */
81
+ export function mergeComponentEntries(entries, explicit) {
82
+ const normalized = entries.map((e) => ({
83
+ chunkId: e.chunkId || `components/${e.name}`,
84
+ name: e.name,
85
+ entry: e.entry,
86
+ }));
87
+ return mergeExplicitComponentEntries(normalized, explicit).map((e) => ({
88
+ name: e.name,
89
+ entry: e.entry,
90
+ chunkId: e.chunkId,
91
+ }));
92
+ }
93
+ /**
94
+ * @param {string} distDir
95
+ * @param {Record<string, string> | undefined} [explicit]
96
+ * @param {{ strict?: boolean, closureRoots?: string[] }} [opts]
97
+ * @returns {Promise<Array<{ name: string, entry: string, chunkId?: string }>>}
98
+ */
99
+ export async function resolveComponentEntries(distDir, explicit, opts = {}) {
100
+ const { loadComponentEntries } = await import('./deployment-registry.js');
101
+ const entries = await loadComponentEntries(distDir, {
102
+ strict: opts.strict,
103
+ closureRoots: opts.closureRoots,
104
+ explicit,
105
+ });
106
+ return entries.map((e) => ({ name: e.name, entry: e.entry, chunkId: e.chunkId }));
107
+ }
108
+ /**
109
+ * Import all (or filtered) client components and register for SSR / static emit / test hosts.
110
+ * @param {string} distDir
111
+ * @param {(map: Record<string, unknown>) => void} registerComponents
112
+ * @param {{
113
+ * cacheBust?: string | number,
114
+ * include?: (entry: { name: string, entry: string, chunkId?: string }) => boolean,
115
+ * explicit?: Record<string, string>,
116
+ * strict?: boolean,
117
+ * closureRoots?: string[],
118
+ * preload?: 'all' | 'closure' | 'none',
119
+ * }} [opts]
120
+ */
121
+ export async function preloadComponentRegistry(distDir, registerComponents, opts = {}) {
122
+ let entries = await resolveComponentEntries(distDir, opts.explicit, {
123
+ strict: opts.strict,
124
+ closureRoots: opts.closureRoots,
125
+ });
126
+ if (opts.include)
127
+ entries = entries.filter((e) => opts.include(e));
128
+ /** @type {Record<string, unknown>} */
129
+ const map = {};
130
+ for (const entry of entries) {
131
+ const abs = path.join(distDir, entry.entry);
132
+ let href = pathToFileURL(abs).href;
133
+ if (opts.cacheBust != null && opts.cacheBust !== '') {
134
+ href = `${href}?t=${encodeURIComponent(String(opts.cacheBust))}`;
135
+ }
136
+ const mod = await import(href);
137
+ map[entry.name] = mod.default;
138
+ }
139
+ if (Object.keys(map).length)
140
+ registerComponents(map);
141
+ return map;
142
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Unified SSR/DOM render host — explicit deployment bootstrap before renderToString/stream/mount.
3
+ * Hosts must not call renderToString until ensureComponents() has run for the active closure.
4
+ */
5
+ /**
6
+ * @param {string} distDir
7
+ * @param {{
8
+ * strictDeployment?: boolean,
9
+ * strict?: boolean,
10
+ * explicit?: Record<string, string>,
11
+ * cacheBust?: string | number,
12
+ * preload?: 'all' | 'closure' | 'none',
13
+ * closureRoots?: string[],
14
+ * }} [opts]
15
+ */
16
+ export declare function createRenderHost(distDir: any, opts?: {}): Promise<{
17
+ distDir: any;
18
+ deployment: any;
19
+ dom: any;
20
+ loadedChunkIds: Set<unknown>;
21
+ ensureComponents: (rootChunkIds: any) => Promise<{}>;
22
+ closureChunkIds: (rootChunkIds: any) => Set<unknown>;
23
+ renderToString: any;
24
+ renderToStream: any;
25
+ mount: any;
26
+ hydrate: any;
27
+ resume: any;
28
+ destroy: any;
29
+ flushPending: any;
30
+ /** @deprecated Prefer ensureComponents via createRenderHost; low-level escape hatch. */
31
+ registerComponents: any;
32
+ }>;
@@ -0,0 +1,81 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Unified SSR/DOM render host — explicit deployment bootstrap before renderToString/stream/mount.
4
+ * Hosts must not call renderToString until ensureComponents() has run for the active closure.
5
+ */
6
+ import path from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+ import { bootstrapComponentRegistry, collectDependsOnClosure, loadComponentEntries, readDeploymentDocument, importAndRegisterComponentEntries, } from './deployment-registry.js';
9
+ /**
10
+ * @param {string} distDir
11
+ * @param {{
12
+ * strictDeployment?: boolean,
13
+ * strict?: boolean,
14
+ * explicit?: Record<string, string>,
15
+ * cacheBust?: string | number,
16
+ * preload?: 'all' | 'closure' | 'none',
17
+ * closureRoots?: string[],
18
+ * }} [opts]
19
+ */
20
+ export async function createRenderHost(distDir, opts = {}) {
21
+ const strict = opts.strictDeployment === true || opts.strict === true;
22
+ const domPath = path.join(distDir, 'vmz-dom.js');
23
+ const dom = await import(pathToFileURL(domPath).href);
24
+ const deployment = readDeploymentDocument(distDir, { strict });
25
+ /** @type {Set<string>} */
26
+ const loadedChunkIds = new Set();
27
+ const bootstrapOpts = {
28
+ strict,
29
+ explicit: opts.explicit,
30
+ cacheBust: opts.cacheBust,
31
+ loaded: loadedChunkIds,
32
+ preload: opts.preload ?? 'none',
33
+ closureRoots: opts.closureRoots,
34
+ };
35
+ if (bootstrapOpts.preload !== 'none') {
36
+ await bootstrapComponentRegistry(distDir, dom.registerComponents, bootstrapOpts);
37
+ }
38
+ /**
39
+ * Load component closure for root chunk ids (page + layouts + fixture).
40
+ * @param {string[]} rootChunkIds
41
+ */
42
+ async function ensureComponents(rootChunkIds) {
43
+ if (!rootChunkIds?.length)
44
+ return {};
45
+ const entries = await loadComponentEntries(distDir, {
46
+ strict,
47
+ closureRoots: rootChunkIds,
48
+ explicit: opts.explicit,
49
+ });
50
+ return importAndRegisterComponentEntries(distDir, entries, dom.registerComponents, {
51
+ cacheBust: opts.cacheBust,
52
+ loaded: loadedChunkIds,
53
+ });
54
+ }
55
+ /**
56
+ * Union closure chunk ids (pages + layouts) without importing yet.
57
+ * @param {string[]} rootChunkIds
58
+ */
59
+ function closureChunkIds(rootChunkIds) {
60
+ if (!deployment || !rootChunkIds?.length)
61
+ return new Set(rootChunkIds || []);
62
+ return collectDependsOnClosure(deployment, rootChunkIds);
63
+ }
64
+ return {
65
+ distDir,
66
+ deployment,
67
+ dom,
68
+ loadedChunkIds,
69
+ ensureComponents,
70
+ closureChunkIds,
71
+ renderToString: dom.renderToString.bind(dom),
72
+ renderToStream: dom.renderToStream.bind(dom),
73
+ mount: dom.mount.bind(dom),
74
+ hydrate: dom.hydrate?.bind(dom),
75
+ resume: dom.resume?.bind(dom),
76
+ destroy: dom.destroy?.bind(dom),
77
+ flushPending: dom.flushPending?.bind(dom),
78
+ /** @deprecated Prefer ensureComponents via createRenderHost; low-level escape hatch. */
79
+ registerComponents: dom.registerComponents.bind(dom),
80
+ };
81
+ }
@@ -23,6 +23,7 @@ import { createRequire, registerHooks } from 'node:module';
23
23
  import path from 'node:path';
24
24
  import { fileURLToPath, pathToFileURL } from 'node:url';
25
25
  import { registerComponents, renderToStream, renderToString } from './vmz-dom.js';
26
+ import { listClientComponents } from './list-client-components.js';
26
27
  import { handleNodeRequest, setRoutes, setServerModuleResolver } from './vmz-runtime.js';
27
28
  const require = createRequire(import.meta.url);
28
29
  const distDir = process.env.VMZ_DIST ? path.resolve(process.env.VMZ_DIST) : path.dirname(fileURLToPath(import.meta.url));
@@ -479,9 +480,7 @@ async function* emitPageHtml(Page, chunkId, eventOnlyShell, props = {}, opts = {
479
480
  `d.innerHTML="<div style='max-width:56rem;margin:0 auto'><p style='color:#f87171;font-weight:700'>Dev Error</p><pre style='white-space:pre-wrap'>"+String(e.message||e).replace(/[<>&]/g,function(c){return {"<":"&lt;",">":"&gt;","&":"&amp;"}[c]})+"</pre></div>";` +
480
481
  `document.documentElement.appendChild(d);})();</script>`
481
482
  : '';
482
- const buildIdBoot = isDev && lastDevBuildId
483
- ? `\n <script>window.__VMZ_DEV_BUILD_ID__=${JSON.stringify(lastDevBuildId)};</script>`
484
- : '';
483
+ const buildIdBoot = isDev && lastDevBuildId ? `\n <script>window.__VMZ_DEV_BUILD_ID__=${JSON.stringify(lastDevBuildId)};</script>` : '';
485
484
  if (signal?.aborted)
486
485
  return;
487
486
  const themeId = resolveThemeId(opts.searchParams, opts.cookieHeader);
@@ -1075,46 +1074,6 @@ async function loadPageCtor(chunkId) {
1075
1074
  pageCtors.set(chunkId, mod.default);
1076
1075
  return mod.default;
1077
1076
  }
1078
- /**
1079
- * @param {string} dir
1080
- * @returns {Promise<Array<{ name: string, entry: string }>>}
1081
- */
1082
- async function listClientComponents(dir) {
1083
- /** @type {Map<string, { name: string, entry: string }>} */
1084
- const byName = new Map();
1085
- try {
1086
- const raw = await readFile(path.join(dir, 'vmz-deployment.json'), 'utf8');
1087
- const dep = JSON.parse(raw);
1088
- for (const unit of dep.units || []) {
1089
- if (unit?.kind !== 'component')
1090
- continue;
1091
- const chunkId = String(unit.chunkId || '');
1092
- const name = chunkId.split('/').pop();
1093
- if (!name)
1094
- continue;
1095
- const entry = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
1096
- byName.set(name, { name, entry });
1097
- }
1098
- }
1099
- catch {
1100
- /* fall through to directory scan */
1101
- }
1102
- if (byName.size === 0) {
1103
- const folder = path.join(dir, 'components');
1104
- let files = [];
1105
- try {
1106
- files = await readdir(folder);
1107
- }
1108
- catch {
1109
- return [];
1110
- }
1111
- for (const f of files.filter((name) => name.endsWith('.client.js'))) {
1112
- const name = f.replace(/\.client\.js$/, '');
1113
- byName.set(name, { name, entry: `components/${name}.client.js` });
1114
- }
1115
- }
1116
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
1117
- }
1118
1077
  /**
1119
1078
  * Discover compiled page modules. Prefer Route Graph `pathPattern` from
1120
1079
  * `vmz-deployment.json`; fall back to walking `pages/**` (file-route only).
package/dist/server.js CHANGED
@@ -226,7 +226,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
226
226
  return await writeFetchResponse(res, response);
227
227
  }
228
228
  // Static first for assets + DocumentMount (`/d/…`) so docs aren't swallowed by SSR 404 shells.
229
- // web-static route HTML (`index.html`, `about/index.html`, …) is a CDN/deploy projection only —
229
+ // static route HTML (`index.html`, `about/index.html`, …) is a CDN/deploy projection only —
230
230
  // when Server Host SSR is active, those files must not shadow live render (local/dev ≡ SSR truth).
231
231
  if (verb === 'GET' && opts.distDir) {
232
232
  const nodePath = await import('node:path');
@@ -404,7 +404,7 @@ function safeDistFile(distDir, pathname, nodePath) {
404
404
  return full;
405
405
  }
406
406
  /**
407
- * web-static emits per-route HTML beside client assets. That HTML is for CDN / local-static
407
+ * static profile emits per-route HTML beside client assets. That HTML is for CDN / local-static
408
408
  * delivery hosts — not for Server Host when SSR is available. DocumentMount stays static.
409
409
  * @param {string} file
410
410
  * @param {string} pathname
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/core",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
6
6
  "exports": {
@@ -27,6 +27,14 @@
27
27
  "./client-nav": {
28
28
  "types": "./dist/client-nav.d.ts",
29
29
  "default": "./dist/client-nav.js"
30
+ },
31
+ "./component-registry": {
32
+ "types": "./dist/list-client-components.d.ts",
33
+ "default": "./dist/list-client-components.js"
34
+ },
35
+ "./render-host": {
36
+ "types": "./dist/render-host.d.ts",
37
+ "default": "./dist/render-host.js"
30
38
  }
31
39
  },
32
40
  "files": [