@vmz/core 0.1.9 → 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.
- package/dist/deployment-registry.d.ts +70 -0
- package/dist/deployment-registry.js +256 -0
- package/dist/dom-core.d.ts +1 -1
- package/dist/dom-core.js +1 -1
- package/dist/list-client-components.d.ts +44 -7
- package/dist/list-client-components.js +97 -75
- package/dist/render-host.d.ts +32 -0
- package/dist/render-host.js +81 -0
- package/package.json +5 -1
|
@@ -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
|
+
}
|
package/dist/dom-core.d.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -1,25 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Discover compiled client component modules from dist (deployment graph or components/).
|
|
3
|
-
* Shared by serve-host SSR
|
|
3
|
+
* Shared by serve-host SSR, static emit, and test hosts.
|
|
4
4
|
*/
|
|
5
|
+
export { DEPLOYMENT_SCHEMA, readDeploymentDocument, componentEntriesFromDeployment, collectDependsOnClosure, dedupeComponentEntriesByTag, mergeExplicitComponentEntries, loadComponentEntries, importAndRegisterComponentEntries, bootstrapComponentRegistry, } from './deployment-registry.js';
|
|
6
|
+
export { createRenderHost } from './render-host.js';
|
|
5
7
|
/**
|
|
6
8
|
* @param {string} dir
|
|
7
|
-
* @
|
|
9
|
+
* @param {{ strict?: boolean }} [opts]
|
|
10
|
+
* @returns {Promise<Array<{ name: string, entry: string, chunkId?: string }>>}
|
|
8
11
|
*/
|
|
9
|
-
export declare function listClientComponents(dir: any): Promise<
|
|
12
|
+
export declare function listClientComponents(dir: any, opts?: {}): Promise<{
|
|
13
|
+
name: any;
|
|
14
|
+
entry: any;
|
|
15
|
+
chunkId: any;
|
|
16
|
+
}[]>;
|
|
10
17
|
/**
|
|
11
18
|
* Sync variant for callers that already use fs sync (legacy static-emit helpers).
|
|
12
19
|
* @param {string} dir
|
|
13
|
-
* @
|
|
20
|
+
* @param {{ strict?: boolean }} [opts]
|
|
21
|
+
* @returns {Array<{ name: string, entry: string, chunkId?: string }>}
|
|
14
22
|
*/
|
|
15
|
-
export declare function listClientComponentsSync(dir: any):
|
|
23
|
+
export declare function listClientComponentsSync(dir: any, opts?: {}): {
|
|
24
|
+
name: any;
|
|
25
|
+
entry: any;
|
|
26
|
+
chunkId: any;
|
|
27
|
+
}[];
|
|
16
28
|
/**
|
|
17
|
-
*
|
|
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.
|
|
18
51
|
* @param {string} distDir
|
|
19
52
|
* @param {(map: Record<string, unknown>) => void} registerComponents
|
|
20
53
|
* @param {{
|
|
21
54
|
* cacheBust?: string | number,
|
|
22
|
-
* include?: (entry: { name: string, entry: string }) => boolean,
|
|
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',
|
|
23
60
|
* }} [opts]
|
|
24
61
|
*/
|
|
25
62
|
export declare function preloadComponentRegistry(distDir: any, registerComponents: any, opts?: {}): Promise<{}>;
|
|
@@ -1,111 +1,133 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
3
|
* Discover compiled client component modules from dist (deployment graph or components/).
|
|
4
|
-
* Shared by serve-host SSR
|
|
4
|
+
* Shared by serve-host SSR, static emit, and test hosts.
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'node:fs';
|
|
7
|
-
import { readdir
|
|
7
|
+
import { readdir } from 'node:fs/promises';
|
|
8
8
|
import path from 'node:path';
|
|
9
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';
|
|
10
13
|
/**
|
|
11
14
|
* @param {string} dir
|
|
12
|
-
* @
|
|
15
|
+
* @param {{ strict?: boolean }} [opts]
|
|
16
|
+
* @returns {Promise<Array<{ name: string, entry: string, chunkId?: string }>>}
|
|
13
17
|
*/
|
|
14
|
-
export async function listClientComponents(dir) {
|
|
15
|
-
|
|
16
|
-
|
|
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 = [];
|
|
17
30
|
try {
|
|
18
|
-
|
|
19
|
-
const dep = JSON.parse(raw);
|
|
20
|
-
for (const unit of dep.units || []) {
|
|
21
|
-
if (unit?.kind !== 'component')
|
|
22
|
-
continue;
|
|
23
|
-
const chunkId = String(unit.chunkId || '');
|
|
24
|
-
const name = chunkId.split('/').pop();
|
|
25
|
-
if (!name)
|
|
26
|
-
continue;
|
|
27
|
-
const entry = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
|
|
28
|
-
byName.set(name, { name, entry });
|
|
29
|
-
}
|
|
31
|
+
files = await readdir(folder);
|
|
30
32
|
}
|
|
31
33
|
catch {
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
if (byName.size === 0) {
|
|
35
|
-
const folder = path.join(dir, 'components');
|
|
36
|
-
let files = [];
|
|
37
|
-
try {
|
|
38
|
-
files = await readdir(folder);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
for (const f of files.filter((name) => name.endsWith('.client.js'))) {
|
|
44
|
-
const name = f.replace(/\.client\.js$/, '');
|
|
45
|
-
byName.set(name, { name, entry: `components/${name}.client.js` });
|
|
46
|
-
}
|
|
34
|
+
return [];
|
|
47
35
|
}
|
|
48
|
-
return
|
|
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));
|
|
49
43
|
}
|
|
50
44
|
/**
|
|
51
45
|
* Sync variant for callers that already use fs sync (legacy static-emit helpers).
|
|
52
46
|
* @param {string} dir
|
|
53
|
-
* @
|
|
47
|
+
* @param {{ strict?: boolean }} [opts]
|
|
48
|
+
* @returns {Array<{ name: string, entry: string, chunkId?: string }>}
|
|
54
49
|
*/
|
|
55
|
-
export function listClientComponentsSync(dir) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (unit?.kind !== 'component')
|
|
64
|
-
continue;
|
|
65
|
-
const chunkId = String(unit.chunkId || '');
|
|
66
|
-
const name = chunkId.split('/').pop();
|
|
67
|
-
if (!name)
|
|
68
|
-
continue;
|
|
69
|
-
const entry = String(unit.clientEntry || `${chunkId}.client.js`).replace(/\\/g, '/');
|
|
70
|
-
byName.set(name, { name, entry });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
/* fall through */
|
|
75
|
-
}
|
|
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
|
+
}));
|
|
76
58
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
for (const f of files.filter((name) => name.endsWith('.client.js'))) {
|
|
87
|
-
const name = f.replace(/\.client\.js$/, '');
|
|
88
|
-
byName.set(name, { name, entry: `components/${name}.client.js` });
|
|
89
|
-
}
|
|
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 [];
|
|
90
67
|
}
|
|
91
|
-
return
|
|
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 }));
|
|
92
107
|
}
|
|
93
108
|
/**
|
|
94
|
-
* Import all (or filtered) client components and register for SSR / static emit.
|
|
109
|
+
* Import all (or filtered) client components and register for SSR / static emit / test hosts.
|
|
95
110
|
* @param {string} distDir
|
|
96
111
|
* @param {(map: Record<string, unknown>) => void} registerComponents
|
|
97
112
|
* @param {{
|
|
98
113
|
* cacheBust?: string | number,
|
|
99
|
-
* include?: (entry: { name: string, entry: string }) => boolean,
|
|
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',
|
|
100
119
|
* }} [opts]
|
|
101
120
|
*/
|
|
102
121
|
export async function preloadComponentRegistry(distDir, registerComponents, opts = {}) {
|
|
103
|
-
|
|
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));
|
|
104
128
|
/** @type {Record<string, unknown>} */
|
|
105
129
|
const map = {};
|
|
106
130
|
for (const entry of entries) {
|
|
107
|
-
if (opts.include && !opts.include(entry))
|
|
108
|
-
continue;
|
|
109
131
|
const abs = path.join(distDir, entry.entry);
|
|
110
132
|
let href = pathToFileURL(abs).href;
|
|
111
133
|
if (opts.cacheBust != null && opts.cacheBust !== '') {
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.1.
|
|
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": {
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"./component-registry": {
|
|
32
32
|
"types": "./dist/list-client-components.d.ts",
|
|
33
33
|
"default": "./dist/list-client-components.js"
|
|
34
|
+
},
|
|
35
|
+
"./render-host": {
|
|
36
|
+
"types": "./dist/render-host.d.ts",
|
|
37
|
+
"default": "./dist/render-host.js"
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
40
|
"files": [
|