@vmz/core 0.1.32 → 0.2.0
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/{client-nav.d.ts → browser/client-nav.d.ts} +4 -13
- package/dist/{client-nav.js → browser/client-nav.js} +16 -74
- package/dist/browser/direct-api.types.d.ts +58 -0
- package/dist/browser/direct-api.types.js +4 -0
- package/dist/browser/direct-host-box.d.ts +11 -0
- package/dist/{direct-host-box.js → browser/direct-host-box.js} +2 -18
- package/dist/{dom-core.d.ts → browser/dom-core.d.ts} +64 -220
- package/dist/browser/dom-core.js +2453 -0
- package/dist/browser/dom-core.types.d.ts +138 -0
- package/dist/browser/dom-core.types.js +4 -0
- package/dist/{position-context.js → browser/position-context.js} +1 -1
- package/dist/browser/unknown-component.d.ts +14 -0
- package/dist/{unknown-component.js → browser/unknown-component.js} +4 -28
- package/dist/{dom.browser.d.ts → faces/dom.browser.d.ts} +2 -2
- package/dist/{dom.browser.js → faces/dom.browser.js} +2 -3
- package/dist/faces/dom.client.d.ts +4 -0
- package/dist/faces/dom.client.js +4 -0
- package/dist/faces/dom.d.ts +6 -0
- package/dist/faces/dom.js +6 -0
- package/dist/{http.d.ts → faces/http.d.ts} +1 -1
- package/dist/{http.js → faces/http.js} +1 -2
- package/dist/{server.d.ts → faces/server.d.ts} +8 -25
- package/dist/{server.js → faces/server.js} +25 -155
- package/dist/{vmz-dom.d.ts → faces/vmz-dom.d.ts} +1 -1
- package/dist/{vmz-dom.js → faces/vmz-dom.js} +1 -2
- package/dist/{vmz-runtime.d.ts → faces/vmz-runtime.d.ts} +1 -1
- package/dist/{vmz-runtime.js → faces/vmz-runtime.js} +1 -2
- package/dist/host/deployment-registry.d.ts +16 -0
- package/dist/{deployment-registry.js → host/deployment-registry.js} +1 -68
- package/dist/host/list-client-components.d.ts +14 -0
- package/dist/{list-client-components.js → host/list-client-components.js} +0 -37
- package/dist/{localize-body-links.js → host/localize-body-links.js} +0 -1
- package/dist/host/native-addon.d.ts +6 -0
- package/dist/{native-addon.js → host/native-addon.js} +0 -20
- package/dist/host/render-host.d.ts +20 -0
- package/dist/{render-host.js → host/render-host.js} +0 -16
- package/dist/{serve-host.d.ts → host/serve-host.d.ts} +3 -3
- package/dist/{serve-host.mjs → host/serve-host.mjs} +45 -168
- package/dist/shared/host.types.d.ts +67 -0
- package/dist/shared/host.types.js +4 -0
- package/dist/shared/server.types.d.ts +39 -0
- package/dist/shared/server.types.js +4 -0
- package/dist/{dom-ssr.d.ts → ssr/dom-ssr.d.ts} +8 -37
- package/dist/{dom-ssr.js → ssr/dom-ssr.js} +159 -217
- package/package.json +34 -34
- package/dist/deployment-registry.d.ts +0 -70
- package/dist/direct-host-box.d.ts +0 -26
- package/dist/dom-core.js +0 -4815
- package/dist/dom.client.d.ts +0 -4
- package/dist/dom.client.js +0 -5
- package/dist/dom.d.ts +0 -6
- package/dist/dom.js +0 -7
- package/dist/list-client-components.d.ts +0 -62
- package/dist/native-addon.d.ts +0 -12
- package/dist/render-host.d.ts +0 -32
- package/dist/unknown-component.d.ts +0 -40
- /package/dist/{position-context.d.ts → browser/position-context.d.ts} +0 -0
- /package/dist/{index.d.ts → faces/index.d.ts} +0 -0
- /package/dist/{index.js → faces/index.js} +0 -0
- /package/dist/{localize-body-links.d.ts → host/localize-body-links.d.ts} +0 -0
- /package/dist/{route-layout-chain.d.ts → host/route-layout-chain.d.ts} +0 -0
- /package/dist/{route-layout-chain.js → host/route-layout-chain.js} +0 -0
|
@@ -1,63 +1,36 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
/**
|
|
3
2
|
* Minimal VMZ runtime — `#server` invoke + RPC/REST HTTP + optional static/SSR.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
4
|
* Browser-safe: no static `node:*` imports. Node builtins are loaded only inside
|
|
7
5
|
* Node/SSR request handlers so client bundles can `import { callServer }`.
|
|
8
6
|
*/
|
|
9
|
-
/** @typedef {{ moduleId: string, method: string, args?: unknown[] }} RpcRequest */
|
|
10
|
-
/** @typedef {{ verb: string, path: string, moduleId: string, method: string, className?: string }} Route */
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {{
|
|
13
|
-
* distDir?: string,
|
|
14
|
-
* renderIndex?: => Promise<string> | string,
|
|
15
|
-
* renderIndexStream?: (opts?: { signal?: AbortSignal }) => AsyncIterable<string>,
|
|
16
|
-
* renderPage?: (pathname: string) => Promise<string | null> | string | null,
|
|
17
|
-
* renderPageStream?: (pathname: string, opts?: { signal?: AbortSignal, searchParams?: URLSearchParams, cookieHeader?: string, method?: string, body?: unknown }) => Promise<AsyncIterable<string> | { status?: number, stream?: AsyncIterable<string>, redirect?: string, headers?: Record<string, string> } | null> | AsyncIterable<string> | null,
|
|
18
|
-
* req?: import('node:http').IncomingMessage,
|
|
19
|
-
* }} NodeRequestOptions
|
|
20
|
-
*/
|
|
21
7
|
const DEFAULT_RPC_PATH = '/__vmz/rpc';
|
|
22
|
-
/** @type {((id: string) => string | URL) | null} */
|
|
23
8
|
let resolveServerModule = null;
|
|
24
|
-
/** @type {Route[]} */
|
|
25
9
|
let routes = [];
|
|
26
10
|
/**
|
|
27
|
-
* Map `#server/foo`
|
|
28
|
-
* Only set this in Node/SSR hosts
|
|
11
|
+
* Map `#server/foo` �?filesystem / URL the host can `import`.
|
|
12
|
+
* Only set this in Node/SSR hosts �?browser bundles must omit it so RPC goes HTTP.
|
|
29
13
|
*/
|
|
30
14
|
export function setServerModuleResolver(fn) {
|
|
31
15
|
resolveServerModule = fn;
|
|
32
16
|
}
|
|
33
|
-
/** @param {Route[]} next */
|
|
34
17
|
export function setRoutes(next) {
|
|
35
18
|
routes = Array.isArray(next) ? next : [];
|
|
36
19
|
}
|
|
37
|
-
/**
|
|
38
|
-
* @param {string} moduleId
|
|
39
|
-
* @param {string} method
|
|
40
|
-
* @param {unknown[]} args
|
|
41
|
-
*/
|
|
42
20
|
export async function callServer(moduleId, method, args = []) {
|
|
43
21
|
// Browser (or tests forcing HTTP): never touch server modules in-process.
|
|
44
|
-
// Node SSR / smokes set a resolver and leave __VMZ_USE_HTTP_RPC unset
|
|
22
|
+
// Node SSR / smokes set a resolver and leave __VMZ_USE_HTTP_RPC unset �?local.
|
|
45
23
|
if (globalThis.__VMZ_USE_HTTP_RPC || !resolveServerModule) {
|
|
46
24
|
return callServerHttp(moduleId, method, args);
|
|
47
25
|
}
|
|
48
26
|
return callServerLocal(moduleId, method, args);
|
|
49
27
|
}
|
|
50
|
-
/**
|
|
51
|
-
* @param {string} moduleId
|
|
52
|
-
* @param {string} method
|
|
53
|
-
* @param {unknown[]} args
|
|
54
|
-
*/
|
|
55
28
|
async function callServerLocal(moduleId, method, args) {
|
|
56
29
|
if (!resolveServerModule) {
|
|
57
30
|
throw new Error(`vmz:runtime callServer(${moduleId}): setServerModuleResolver() required in Node`);
|
|
58
31
|
}
|
|
59
32
|
const spec = resolveServerModule(moduleId);
|
|
60
|
-
const mod = await import(spec);
|
|
33
|
+
const mod = await import(typeof spec === 'string' ? spec : spec.href);
|
|
61
34
|
const Ctor = mod.default ?? mod[exportGuess(moduleId)];
|
|
62
35
|
if (typeof Ctor !== 'function') {
|
|
63
36
|
throw new Error(`vmz:runtime: no class export for ${moduleId}`);
|
|
@@ -69,11 +42,6 @@ async function callServerLocal(moduleId, method, args) {
|
|
|
69
42
|
}
|
|
70
43
|
return fn.apply(instance, args);
|
|
71
44
|
}
|
|
72
|
-
/**
|
|
73
|
-
* @param {string} moduleId
|
|
74
|
-
* @param {string} method
|
|
75
|
-
* @param {unknown[]} args
|
|
76
|
-
*/
|
|
77
45
|
async function callServerHttp(moduleId, method, args) {
|
|
78
46
|
let rpcPath = (typeof globalThis !== 'undefined' && globalThis.__VMZ_RPC_PATH) || DEFAULT_RPC_PATH;
|
|
79
47
|
// Node undici `fetch` rejects relative URLs; browsers accept path-only.
|
|
@@ -104,17 +72,11 @@ function exportGuess(moduleId) {
|
|
|
104
72
|
const base = moduleId.split('/').pop() || 'Server';
|
|
105
73
|
return `${base}Server`;
|
|
106
74
|
}
|
|
107
|
-
/**
|
|
108
|
-
* @param {RpcRequest} body
|
|
109
|
-
*/
|
|
110
75
|
export async function handleRpc(body) {
|
|
111
76
|
return callServerLocal(body.moduleId, body.method, body.args ?? []);
|
|
112
77
|
}
|
|
113
78
|
/**
|
|
114
79
|
* Match REST route from `vmz-routes.json`.
|
|
115
|
-
* @param {string} verb
|
|
116
|
-
* @param {string} pathname
|
|
117
|
-
* @returns {Route | null}
|
|
118
80
|
*/
|
|
119
81
|
export function matchRoute(verb, pathname) {
|
|
120
82
|
const v = verb.toUpperCase();
|
|
@@ -122,13 +84,10 @@ export function matchRoute(verb, pathname) {
|
|
|
122
84
|
}
|
|
123
85
|
/**
|
|
124
86
|
* Build `callServerLocal` args from a Fetch Request for REST routes.
|
|
125
|
-
* GET/HEAD
|
|
126
|
-
* Multipart
|
|
87
|
+
* GET/HEAD �?`[]`. JSON POST/PUT/PATCH �?`[body]`. form-urlencoded �?`[record]`.
|
|
88
|
+
* Multipart �?`[record]` where File/Blob parts stay as File/Blob (tool-site binary upload).
|
|
127
89
|
* Octet-stream PUT also forwards Upload resumable chunk headers (upload-id / chunk-index / chunk-total).
|
|
128
90
|
* Extra args are ignored by zero-parameter server methods (JS).
|
|
129
|
-
* @param {Request} request
|
|
130
|
-
* @param {string} verb
|
|
131
|
-
* @returns {Promise<unknown[]>}
|
|
132
91
|
*/
|
|
133
92
|
async function routeArgsFromRequest(request, verb) {
|
|
134
93
|
const v = String(verb || 'GET').toUpperCase();
|
|
@@ -147,12 +106,12 @@ async function routeArgsFromRequest(request, verb) {
|
|
|
147
106
|
}
|
|
148
107
|
}
|
|
149
108
|
if (ctype.includes('multipart/form-data')) {
|
|
150
|
-
// Parse from raw bytes
|
|
151
|
-
// (0xFF/0xFE
|
|
109
|
+
// Parse from raw bytes �?undici Request.formData() can UTF-8-mangle high bytes in file parts
|
|
110
|
+
// (0xFF/0xFE �?U+FFFD), which breaks Upload binary / tool-site intakes.
|
|
152
111
|
const buf = Buffer.from(await request.arrayBuffer());
|
|
153
112
|
return [parseMultipartBuffer(buf, ctype)];
|
|
154
113
|
}
|
|
155
|
-
// Object-store PUT / resumable chunk PUT
|
|
114
|
+
// Object-store PUT / resumable chunk PUT �?keep bytes (never request.text()).
|
|
156
115
|
if (ctype.includes('application/octet-stream') ||
|
|
157
116
|
((v === 'PUT' || v === 'PATCH') && !ctype.includes('json') && !ctype.includes('x-www-form-urlencoded'))) {
|
|
158
117
|
const buf = Buffer.from(await request.arrayBuffer());
|
|
@@ -180,8 +139,6 @@ async function routeArgsFromRequest(request, verb) {
|
|
|
180
139
|
/**
|
|
181
140
|
* Web Standards Fetch entry for ServerArtifact hosts (Node adapter, worker/edge parity).
|
|
182
141
|
* Handles RPC + public ServerRoute only; static/SSR stay on Node host options.
|
|
183
|
-
* @param {Request} request
|
|
184
|
-
* @returns {Promise<Response>}
|
|
185
142
|
*/
|
|
186
143
|
export async function handleFetchRequest(request) {
|
|
187
144
|
const url = new URL(request.url);
|
|
@@ -209,9 +166,6 @@ export async function handleFetchRequest(request) {
|
|
|
209
166
|
/**
|
|
210
167
|
* Node `http.createServer` listener: RPC + REST + optional static / SSR index.
|
|
211
168
|
* RPC/REST go through {@link handleFetchRequest} so Node and Fetch hosts share one core.
|
|
212
|
-
* @param {import('node:http').IncomingMessage} req
|
|
213
|
-
* @param {import('node:http').ServerResponse} res
|
|
214
|
-
* @param {NodeRequestOptions} [opts]
|
|
215
169
|
*/
|
|
216
170
|
export async function handleNodeRequest(req, res, opts = {}) {
|
|
217
171
|
const host = req.headers.host || '127.0.0.1';
|
|
@@ -225,9 +179,8 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
225
179
|
const response = await handleFetchRequest(request);
|
|
226
180
|
return await writeFetchResponse(res, response);
|
|
227
181
|
}
|
|
228
|
-
// Static first for assets + DocumentMount (`/d
|
|
229
|
-
// static route HTML (`index.html`, `about/index.html`,
|
|
230
|
-
// when Server Host SSR is active, those files must not shadow live render (local/dev ≡ SSR truth).
|
|
182
|
+
// Static first for assets + DocumentMount (`/d/�`) so docs aren't swallowed by SSR 404 shells.
|
|
183
|
+
// static route HTML (`index.html`, `about/index.html`, �? is a CDN/deploy projection only �? // when Server Host SSR is active, those files must not shadow live render (local/dev �?SSR truth).
|
|
231
184
|
if (verb === 'GET' && opts.distDir) {
|
|
232
185
|
const nodePath = await import('node:path');
|
|
233
186
|
const { readFile, stat } = await import('node:fs/promises');
|
|
@@ -263,7 +216,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
263
216
|
/* ignore */
|
|
264
217
|
}
|
|
265
218
|
};
|
|
266
|
-
// Do not abort on IncomingMessage `close`
|
|
219
|
+
// Do not abort on IncomingMessage `close` �?that fires after a POST body is
|
|
267
220
|
// fully read (normal), which would cancel SSR before the first chunk.
|
|
268
221
|
req.on('aborted', onClientGone);
|
|
269
222
|
res.on('close', () => {
|
|
@@ -272,7 +225,6 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
272
225
|
});
|
|
273
226
|
try {
|
|
274
227
|
if (typeof opts.renderPageStream === 'function') {
|
|
275
|
-
/** @type {unknown} */
|
|
276
228
|
let body;
|
|
277
229
|
if (verb === 'POST') {
|
|
278
230
|
const ctype = String(req.headers['content-type'] || '');
|
|
@@ -329,7 +281,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
329
281
|
}
|
|
330
282
|
if (!res.headersSent) {
|
|
331
283
|
if (wantsBrowserHtml(req)) {
|
|
332
|
-
sendHtml(res, 404, browserErrorHtml(404, 'Not Found', `No page matched ${url.pathname}
|
|
284
|
+
sendHtml(res, 404, browserErrorHtml(404, 'Not Found', `No page matched ${url.pathname}`, undefined));
|
|
333
285
|
}
|
|
334
286
|
else {
|
|
335
287
|
sendJson(res, 404, { error: 'not found', path: url.pathname });
|
|
@@ -357,14 +309,8 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
357
309
|
}
|
|
358
310
|
}
|
|
359
311
|
}
|
|
360
|
-
/**
|
|
361
|
-
* @param {import('node:http').IncomingMessage} req
|
|
362
|
-
* @param {URL} url
|
|
363
|
-
* @returns {Promise<Request>}
|
|
364
|
-
*/
|
|
365
312
|
async function incomingToRequest(req, url) {
|
|
366
313
|
const method = (req.method || 'GET').toUpperCase();
|
|
367
|
-
/** @type {HeadersInit} */
|
|
368
314
|
const headers = {};
|
|
369
315
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
370
316
|
if (v == null)
|
|
@@ -378,10 +324,6 @@ async function incomingToRequest(req, url) {
|
|
|
378
324
|
// Node undici requires duplex when constructing Request with a body.
|
|
379
325
|
return new Request(url, { method, headers, body: raw, duplex: 'half' });
|
|
380
326
|
}
|
|
381
|
-
/**
|
|
382
|
-
* @param {import('node:http').ServerResponse} res
|
|
383
|
-
* @param {Response} response
|
|
384
|
-
*/
|
|
385
327
|
async function writeFetchResponse(res, response) {
|
|
386
328
|
const headers = {};
|
|
387
329
|
response.headers.forEach((value, key) => {
|
|
@@ -396,10 +338,6 @@ async function writeFetchResponse(res, response) {
|
|
|
396
338
|
}
|
|
397
339
|
/**
|
|
398
340
|
* Resolve a path under distDir; reject `..` escapes.
|
|
399
|
-
* @param {string} distDir
|
|
400
|
-
* @param {string} pathname
|
|
401
|
-
* @param {typeof import('node:path')} nodePath
|
|
402
|
-
* @returns {string | null}
|
|
403
341
|
*/
|
|
404
342
|
function safeDistFile(distDir, pathname, nodePath) {
|
|
405
343
|
let rel = decodeURIComponent(pathname.split('?')[0] || '/');
|
|
@@ -417,10 +355,7 @@ function safeDistFile(distDir, pathname, nodePath) {
|
|
|
417
355
|
}
|
|
418
356
|
/**
|
|
419
357
|
* static profile emits per-route HTML beside client assets. That HTML is for CDN / local-static
|
|
420
|
-
* delivery hosts
|
|
421
|
-
* @param {string} file
|
|
422
|
-
* @param {string} pathname
|
|
423
|
-
* @param {typeof import('node:path')} nodePath
|
|
358
|
+
* delivery hosts �?not for Server Host when SSR is available. DocumentMount stays static.
|
|
424
359
|
*/
|
|
425
360
|
function isWebStaticHtmlShadow(file, pathname, nodePath) {
|
|
426
361
|
const ext = nodePath.extname(file).toLowerCase();
|
|
@@ -429,21 +364,16 @@ function isWebStaticHtmlShadow(file, pathname, nodePath) {
|
|
|
429
364
|
let rel = decodeURIComponent(String(pathname || '').split('?')[0] || '/');
|
|
430
365
|
if (!rel.startsWith('/'))
|
|
431
366
|
rel = `/${rel}`;
|
|
432
|
-
// Integrated DocumentMount
|
|
367
|
+
// Integrated DocumentMount �?keep static-first (see resolveDistStatic comment).
|
|
433
368
|
if (rel === '/d' || rel.startsWith('/d/'))
|
|
434
369
|
return false;
|
|
435
370
|
return true;
|
|
436
371
|
}
|
|
437
372
|
/**
|
|
438
373
|
* Static resolve for app assets + integrated document mounts:
|
|
439
|
-
* `/d/`
|
|
440
|
-
* @param {string} distDir
|
|
441
|
-
* @param {string} pathname
|
|
442
|
-
* @param {typeof import('node:path')} nodePath
|
|
443
|
-
* @param {typeof import('node:fs/promises').stat} stat
|
|
444
|
-
* @returns {Promise<string | null>}
|
|
374
|
+
* `/d/` �?`d/index.html`, `/d/zh-hans/guide` �?`d/zh-hans/guide.html`.
|
|
445
375
|
*/
|
|
446
|
-
async function resolveDistStatic(distDir, pathname, nodePath, stat) {
|
|
376
|
+
async function resolveDistStatic(distDir, pathname, nodePath, stat, _opts = {}) {
|
|
447
377
|
const candidates = staticPathCandidates(pathname);
|
|
448
378
|
for (const candidate of candidates) {
|
|
449
379
|
const full = safeDistFile(distDir, candidate, nodePath);
|
|
@@ -466,15 +396,10 @@ async function resolveDistStatic(distDir, pathname, nodePath, stat) {
|
|
|
466
396
|
}
|
|
467
397
|
return null;
|
|
468
398
|
}
|
|
469
|
-
/**
|
|
470
|
-
* @param {string} pathname
|
|
471
|
-
* @returns {string[]}
|
|
472
|
-
*/
|
|
473
399
|
function staticPathCandidates(pathname) {
|
|
474
400
|
let rel = decodeURIComponent(pathname.split('?')[0] || '/');
|
|
475
401
|
if (!rel.startsWith('/'))
|
|
476
402
|
rel = `/${rel}`;
|
|
477
|
-
/** @type {string[]} */
|
|
478
403
|
const out = [];
|
|
479
404
|
const push = (p) => {
|
|
480
405
|
if (p && !out.includes(p))
|
|
@@ -498,16 +423,11 @@ function staticPathCandidates(pathname) {
|
|
|
498
423
|
}
|
|
499
424
|
return out;
|
|
500
425
|
}
|
|
501
|
-
/** @param {string} p */
|
|
502
426
|
function nodePathExt(p) {
|
|
503
427
|
const base = p.split('/').pop() || '';
|
|
504
428
|
const i = base.lastIndexOf('.');
|
|
505
429
|
return i > 0 ? base.slice(i) : '';
|
|
506
430
|
}
|
|
507
|
-
/**
|
|
508
|
-
* @param {string} filePath
|
|
509
|
-
* @param {typeof import('node:path')} nodePath
|
|
510
|
-
*/
|
|
511
431
|
function contentType(filePath, nodePath) {
|
|
512
432
|
const ext = nodePath.extname(filePath).toLowerCase();
|
|
513
433
|
switch (ext) {
|
|
@@ -528,9 +448,6 @@ function contentType(filePath, nodePath) {
|
|
|
528
448
|
return 'application/octet-stream';
|
|
529
449
|
}
|
|
530
450
|
}
|
|
531
|
-
/**
|
|
532
|
-
* @param {import('node:http').IncomingMessage} req
|
|
533
|
-
*/
|
|
534
451
|
function readJson(req) {
|
|
535
452
|
return new Promise((resolve, reject) => {
|
|
536
453
|
const chunks = [];
|
|
@@ -547,24 +464,17 @@ function readJson(req) {
|
|
|
547
464
|
req.on('error', reject);
|
|
548
465
|
});
|
|
549
466
|
}
|
|
550
|
-
/**
|
|
551
|
-
* @param {import('node:http').IncomingMessage} req
|
|
552
|
-
* @returns {Promise<Buffer>}
|
|
553
|
-
*/
|
|
554
467
|
function readRawBody(req) {
|
|
555
468
|
return new Promise((resolve, reject) => {
|
|
556
469
|
const chunks = [];
|
|
557
470
|
req.on('data', (c) => chunks.push(c));
|
|
558
|
-
// Buffer
|
|
471
|
+
// Buffer �?never utf8-string: multipart File bytes must survive (Upload binary gate).
|
|
559
472
|
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
560
473
|
req.on('error', reject);
|
|
561
474
|
});
|
|
562
475
|
}
|
|
563
476
|
/**
|
|
564
477
|
* Buffer-safe multipart/form-data parser (file parts stay binary).
|
|
565
|
-
* @param {Buffer} buf
|
|
566
|
-
* @param {string} contentType
|
|
567
|
-
* @returns {Record<string, unknown>}
|
|
568
478
|
*/
|
|
569
479
|
function parseMultipartBuffer(buf, contentType) {
|
|
570
480
|
const bm = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(String(contentType || ''));
|
|
@@ -573,7 +483,6 @@ function parseMultipartBuffer(buf, contentType) {
|
|
|
573
483
|
throw new Error('multipart: missing boundary');
|
|
574
484
|
}
|
|
575
485
|
const sep = Buffer.from(`--${boundary}`);
|
|
576
|
-
/** @type {Record<string, unknown>} */
|
|
577
486
|
const out = {};
|
|
578
487
|
let start = indexOfBuffer(buf, sep, 0);
|
|
579
488
|
if (start < 0)
|
|
@@ -604,7 +513,7 @@ function parseMultipartBuffer(buf, contentType) {
|
|
|
604
513
|
if (fileM) {
|
|
605
514
|
const filename = fileM[1] || 'upload.bin';
|
|
606
515
|
const type = typeM ? String(typeM[1]).trim() : 'application/octet-stream';
|
|
607
|
-
// Copy body
|
|
516
|
+
// Copy body �?File may outlive the request buffer.
|
|
608
517
|
const copy = Buffer.from(body);
|
|
609
518
|
const file = new File([copy], filename, { type });
|
|
610
519
|
const prev = out[key];
|
|
@@ -629,11 +538,6 @@ function parseMultipartBuffer(buf, contentType) {
|
|
|
629
538
|
}
|
|
630
539
|
return out;
|
|
631
540
|
}
|
|
632
|
-
/**
|
|
633
|
-
* @param {Buffer} hay
|
|
634
|
-
* @param {Buffer} needle
|
|
635
|
-
* @param {number} from
|
|
636
|
-
*/
|
|
637
541
|
function indexOfBuffer(hay, needle, from) {
|
|
638
542
|
if (!needle.length)
|
|
639
543
|
return from;
|
|
@@ -646,11 +550,6 @@ function indexOfBuffer(hay, needle, from) {
|
|
|
646
550
|
}
|
|
647
551
|
return -1;
|
|
648
552
|
}
|
|
649
|
-
/**
|
|
650
|
-
* @param {string} raw
|
|
651
|
-
* @param {string} contentType
|
|
652
|
-
* @returns {Record<string, string> | string}
|
|
653
|
-
*/
|
|
654
553
|
function parseFormBody(raw, contentType) {
|
|
655
554
|
if (!raw)
|
|
656
555
|
return {};
|
|
@@ -667,10 +566,6 @@ function parseFormBody(raw, contentType) {
|
|
|
667
566
|
return { raw };
|
|
668
567
|
}
|
|
669
568
|
}
|
|
670
|
-
/**
|
|
671
|
-
* @param {import('node:http').IncomingMessage} req
|
|
672
|
-
* @returns {boolean}
|
|
673
|
-
*/
|
|
674
569
|
function wantsBrowserHtml(req) {
|
|
675
570
|
const accept = String(req.headers?.accept || '');
|
|
676
571
|
const method = String(req.method || 'GET').toUpperCase();
|
|
@@ -697,11 +592,7 @@ function wantsBrowserHtml(req) {
|
|
|
697
592
|
}
|
|
698
593
|
/**
|
|
699
594
|
* Browser-facing error document (no N-API). Used when SSR returns null or throws
|
|
700
|
-
* on a document navigation
|
|
701
|
-
* @param {number} status
|
|
702
|
-
* @param {string} title
|
|
703
|
-
* @param {string} message
|
|
704
|
-
* @param {string} [stack]
|
|
595
|
+
* on a document navigation �?must not fall back to API-shaped JSON.
|
|
705
596
|
*/
|
|
706
597
|
function browserErrorHtml(status, title, message, stack) {
|
|
707
598
|
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
@@ -728,17 +619,12 @@ function browserErrorHtml(status, title, message, stack) {
|
|
|
728
619
|
<h1>${esc(title)}</h1>
|
|
729
620
|
<pre>${esc(message)}</pre>
|
|
730
621
|
${stackBlock}
|
|
731
|
-
<p class="hint">Document navigation returns HTML errors
|
|
622
|
+
<p class="hint">Document navigation returns HTML errors �?not API JSON.</p>
|
|
732
623
|
</main>
|
|
733
624
|
</body>
|
|
734
625
|
</html>
|
|
735
626
|
`;
|
|
736
627
|
}
|
|
737
|
-
/**
|
|
738
|
-
* @param {import('node:http').ServerResponse} res
|
|
739
|
-
* @param {number} status
|
|
740
|
-
* @param {unknown} body
|
|
741
|
-
*/
|
|
742
628
|
function sendJson(res, status, body) {
|
|
743
629
|
const payload = JSON.stringify(body);
|
|
744
630
|
res.writeHead(status, {
|
|
@@ -747,11 +633,6 @@ function sendJson(res, status, body) {
|
|
|
747
633
|
});
|
|
748
634
|
res.end(payload);
|
|
749
635
|
}
|
|
750
|
-
/**
|
|
751
|
-
* @param {import('node:http').ServerResponse} res
|
|
752
|
-
* @param {number} status
|
|
753
|
-
* @param {string} html
|
|
754
|
-
*/
|
|
755
636
|
function sendHtml(res, status, html) {
|
|
756
637
|
const payload = typeof html === 'string' ? html : String(html);
|
|
757
638
|
res.writeHead(status, {
|
|
@@ -763,10 +644,6 @@ function sendHtml(res, status, html) {
|
|
|
763
644
|
/**
|
|
764
645
|
* Stream HTML without buffering the full document (event-flow / stream SSR host).
|
|
765
646
|
* Honors AbortSignal + response destroy for cancel; awaits drain for backpressure.
|
|
766
|
-
* @param {import('node:http').ServerResponse} res
|
|
767
|
-
* @param {number} status
|
|
768
|
-
* @param {AsyncIterable<string> | Iterable<string> | AsyncGenerator<string, any, any>} source
|
|
769
|
-
* @param {AbortSignal} [signal]
|
|
770
647
|
*/
|
|
771
648
|
async function sendHtmlStream(res, status, source, signal) {
|
|
772
649
|
const aborted = () => Boolean(signal?.aborted || res.destroyed || res.writableEnded || !res.writable);
|
|
@@ -775,8 +652,8 @@ async function sendHtmlStream(res, status, source, signal) {
|
|
|
775
652
|
'transfer-encoding': 'chunked',
|
|
776
653
|
'cache-control': 'no-cache',
|
|
777
654
|
};
|
|
778
|
-
// Defer writeHead until the first successful chunk so SSR throw
|
|
779
|
-
// (not headersSent + destroy
|
|
655
|
+
// Defer writeHead until the first successful chunk so SSR throw �?clean 500
|
|
656
|
+
// (not headersSent + destroy �?ERR_EMPTY_RESPONSE).
|
|
780
657
|
let started = false;
|
|
781
658
|
try {
|
|
782
659
|
for await (const chunk of source) {
|
|
@@ -814,26 +691,19 @@ async function sendHtmlStream(res, status, source, signal) {
|
|
|
814
691
|
catch (err) {
|
|
815
692
|
if (aborted())
|
|
816
693
|
return;
|
|
817
|
-
// Before headers: let outer handler sendJson(500). After headers: rethrow
|
|
694
|
+
// Before headers: let outer handler sendJson(500). After headers: rethrow �?destroy.
|
|
818
695
|
throw err;
|
|
819
696
|
}
|
|
820
697
|
if (started && !res.writableEnded && !res.destroyed) {
|
|
821
698
|
res.end();
|
|
822
699
|
}
|
|
823
700
|
}
|
|
824
|
-
/**
|
|
825
|
-
* @param {import('node:http').ServerResponse} res
|
|
826
|
-
* @param {number} status
|
|
827
|
-
* @param {Buffer} body
|
|
828
|
-
* @param {string} type
|
|
829
|
-
*/
|
|
830
701
|
function sendBytes(res, status, body, type) {
|
|
831
|
-
/** @type {Record<string, string | number>} */
|
|
832
702
|
const headers = {
|
|
833
703
|
'content-type': type,
|
|
834
704
|
'content-length': body.byteLength,
|
|
835
705
|
};
|
|
836
|
-
// Dev: stylesheets are rebuilt in-place
|
|
706
|
+
// Dev: stylesheets are rebuilt in-place �?never cache @import siblings (VMZ-8).
|
|
837
707
|
if (process.env.VMZ_DEV === '1' && typeof type === 'string' && type.startsWith('text/css')) {
|
|
838
708
|
headers['cache-control'] = 'no-store';
|
|
839
709
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment graph — component registry bootstrap (shared by all SSR/DOM hosts).
|
|
3
|
+
* Parse/validate and graph queries delegate to Rust vmz-artifacts via N-API.
|
|
4
|
+
*/
|
|
5
|
+
import type { BootstrapComponentRegistryOpts, ComponentEntry, ComponentRegistryMap, DedupeComponentEntriesOpts, DeploymentDocument, ImportComponentEntriesOpts, LoadComponentEntriesOpts } from '../shared/host.types.js';
|
|
6
|
+
export declare const DEPLOYMENT_SCHEMA = "vmz.deployment.v0";
|
|
7
|
+
export declare function readDeploymentDocument(distDir: string, opts?: {
|
|
8
|
+
strict?: boolean;
|
|
9
|
+
}): DeploymentDocument | null;
|
|
10
|
+
export declare function componentEntriesFromDeployment(deployment: DeploymentDocument): ComponentEntry[];
|
|
11
|
+
export declare function collectDependsOnClosure(deployment: DeploymentDocument, rootChunkIds: string[]): Set<string>;
|
|
12
|
+
export declare function dedupeComponentEntriesByTag(entries: ComponentEntry[], opts?: DedupeComponentEntriesOpts): ComponentEntry[];
|
|
13
|
+
export declare function mergeExplicitComponentEntries(entries: ComponentEntry[], explicit?: Record<string, string>): ComponentEntry[];
|
|
14
|
+
export declare function loadComponentEntries(distDir: string, opts?: LoadComponentEntriesOpts): Promise<ComponentEntry[]>;
|
|
15
|
+
export declare function importAndRegisterComponentEntries(distDir: string, entries: ComponentEntry[], registerComponents: (map: ComponentRegistryMap) => void, opts?: ImportComponentEntriesOpts): Promise<ComponentRegistryMap>;
|
|
16
|
+
export declare function bootstrapComponentRegistry(distDir: string, registerComponents: (map: ComponentRegistryMap) => void, opts?: BootstrapComponentRegistryOpts): Promise<ComponentRegistryMap>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
/**
|
|
3
|
-
* Deployment graph
|
|
2
|
+
* Deployment graph — component registry bootstrap (shared by all SSR/DOM hosts).
|
|
4
3
|
* Parse/validate and graph queries delegate to Rust vmz-artifacts via N-API.
|
|
5
4
|
*/
|
|
6
5
|
import fs from 'node:fs';
|
|
@@ -8,19 +7,10 @@ import path from 'node:path';
|
|
|
8
7
|
import { pathToFileURL } from 'node:url';
|
|
9
8
|
import { requireNativeFn } from './native-addon.js';
|
|
10
9
|
export const DEPLOYMENT_SCHEMA = 'vmz.deployment.v0';
|
|
11
|
-
/**
|
|
12
|
-
* @param {string} jsonText
|
|
13
|
-
* @returns {any}
|
|
14
|
-
*/
|
|
15
10
|
function parseDeploymentJson(jsonText) {
|
|
16
11
|
requireNativeFn('deploymentValidate')(jsonText);
|
|
17
12
|
return JSON.parse(jsonText);
|
|
18
13
|
}
|
|
19
|
-
/**
|
|
20
|
-
* @param {string} distDir
|
|
21
|
-
* @param {{ strict?: boolean }} [opts]
|
|
22
|
-
* @returns {any | null}
|
|
23
|
-
*/
|
|
24
14
|
export function readDeploymentDocument(distDir, opts = {}) {
|
|
25
15
|
const strict = opts.strict === true;
|
|
26
16
|
const filePath = path.join(distDir, 'vmz-deployment.json');
|
|
@@ -48,33 +38,17 @@ export function readDeploymentDocument(distDir, opts = {}) {
|
|
|
48
38
|
return null;
|
|
49
39
|
}
|
|
50
40
|
}
|
|
51
|
-
/**
|
|
52
|
-
* @param {any} deployment
|
|
53
|
-
* @returns {Array<{ chunkId: string, name: string, entry: string, source: string }>}
|
|
54
|
-
*/
|
|
55
41
|
export function componentEntriesFromDeployment(deployment) {
|
|
56
42
|
const json = JSON.stringify(deployment);
|
|
57
43
|
return requireNativeFn('deploymentComponentEntries')(json);
|
|
58
44
|
}
|
|
59
|
-
/**
|
|
60
|
-
* @param {any} deployment
|
|
61
|
-
* @param {string[]} rootChunkIds
|
|
62
|
-
* @returns {Set<string>}
|
|
63
|
-
*/
|
|
64
45
|
export function collectDependsOnClosure(deployment, rootChunkIds) {
|
|
65
46
|
const json = JSON.stringify(deployment);
|
|
66
47
|
const ids = requireNativeFn('deploymentDependsOnClosure')(json, rootChunkIds);
|
|
67
48
|
return new Set(ids);
|
|
68
49
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Resolve tag conflicts; strict mode throws, dev mode warns and keeps last chunkId.
|
|
71
|
-
* @param {Array<{ chunkId: string, name: string, entry: string, source?: string }>} entries
|
|
72
|
-
* @param {{ strict?: boolean }} [opts]
|
|
73
|
-
* @returns {Array<{ chunkId: string, name: string, entry: string, source?: string }>}
|
|
74
|
-
*/
|
|
75
50
|
export function dedupeComponentEntriesByTag(entries, opts = {}) {
|
|
76
51
|
const strict = opts.strict === true;
|
|
77
|
-
/** @type {Map<string, { chunkId: string, name: string, entry: string, source?: string }>} */
|
|
78
52
|
const byTag = new Map();
|
|
79
53
|
for (const e of entries) {
|
|
80
54
|
const prev = byTag.get(e.name);
|
|
@@ -88,13 +62,7 @@ export function dedupeComponentEntriesByTag(entries, opts = {}) {
|
|
|
88
62
|
}
|
|
89
63
|
return [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
90
64
|
}
|
|
91
|
-
/**
|
|
92
|
-
* @param {Array<{ chunkId: string, name: string, entry: string, source?: string }>} entries
|
|
93
|
-
* @param {Record<string, string> | undefined} explicit name → chunkId (no .client.js)
|
|
94
|
-
* @returns {Array<{ chunkId: string, name: string, entry: string, source?: string }>}
|
|
95
|
-
*/
|
|
96
65
|
export function mergeExplicitComponentEntries(entries, explicit) {
|
|
97
|
-
/** @type {Map<string, { chunkId: string, name: string, entry: string, source?: string }>} */
|
|
98
66
|
const byTag = new Map(entries.map((e) => [e.name, e]));
|
|
99
67
|
if (explicit) {
|
|
100
68
|
for (const [name, chunk] of Object.entries(explicit)) {
|
|
@@ -109,19 +77,9 @@ export function mergeExplicitComponentEntries(entries, explicit) {
|
|
|
109
77
|
}
|
|
110
78
|
return [...byTag.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
111
79
|
}
|
|
112
|
-
/**
|
|
113
|
-
* @param {string} distDir
|
|
114
|
-
* @param {{
|
|
115
|
-
* strict?: boolean,
|
|
116
|
-
* closureRoots?: string[],
|
|
117
|
-
* explicit?: Record<string, string>,
|
|
118
|
-
* }} [opts]
|
|
119
|
-
* @returns {Promise<Array<{ chunkId: string, name: string, entry: string, source?: string }>>}
|
|
120
|
-
*/
|
|
121
80
|
export async function loadComponentEntries(distDir, opts = {}) {
|
|
122
81
|
const strict = opts.strict === true;
|
|
123
82
|
const deployment = readDeploymentDocument(distDir, { strict });
|
|
124
|
-
/** @type {Array<{ chunkId: string, name: string, entry: string, source?: string }>} */
|
|
125
83
|
let entries = [];
|
|
126
84
|
if (deployment) {
|
|
127
85
|
entries = componentEntriesFromDeployment(deployment);
|
|
@@ -133,22 +91,10 @@ export async function loadComponentEntries(distDir, opts = {}) {
|
|
|
133
91
|
else if (strict) {
|
|
134
92
|
throw new Error(`vmz: missing vmz-deployment.json under ${distDir} (plan-only host)`);
|
|
135
93
|
}
|
|
136
|
-
// No directory scan fallback — component closure comes from Deployment Plan only.
|
|
137
94
|
entries = mergeExplicitComponentEntries(entries, opts.explicit);
|
|
138
95
|
return dedupeComponentEntriesByTag(entries, { strict });
|
|
139
96
|
}
|
|
140
|
-
/**
|
|
141
|
-
* @param {string} distDir
|
|
142
|
-
* @param {Array<{ chunkId: string, name: string, entry: string }>} entries
|
|
143
|
-
* @param {(map: Record<string, unknown>) => void} registerComponents
|
|
144
|
-
* @param {{
|
|
145
|
-
* cacheBust?: string | number,
|
|
146
|
-
* loaded?: Set<string>,
|
|
147
|
-
* }} [opts]
|
|
148
|
-
* @returns {Promise<Record<string, unknown>>}
|
|
149
|
-
*/
|
|
150
97
|
export async function importAndRegisterComponentEntries(distDir, entries, registerComponents, opts = {}) {
|
|
151
|
-
/** @type {Record<string, unknown>} */
|
|
152
98
|
const map = {};
|
|
153
99
|
const loaded = opts.loaded ?? null;
|
|
154
100
|
for (const entry of entries) {
|
|
@@ -168,19 +114,6 @@ export async function importAndRegisterComponentEntries(distDir, entries, regist
|
|
|
168
114
|
registerComponents(map);
|
|
169
115
|
return map;
|
|
170
116
|
}
|
|
171
|
-
/**
|
|
172
|
-
* Bootstrap component registry from deployment (all or closure — no directory fallback).
|
|
173
|
-
* @param {string} distDir
|
|
174
|
-
* @param {(map: Record<string, unknown>) => void} registerComponents
|
|
175
|
-
* @param {{
|
|
176
|
-
* strict?: boolean,
|
|
177
|
-
* closureRoots?: string[],
|
|
178
|
-
* explicit?: Record<string, string>,
|
|
179
|
-
* cacheBust?: string | number,
|
|
180
|
-
* loaded?: Set<string>,
|
|
181
|
-
* preload?: 'all' | 'closure' | 'none',
|
|
182
|
-
* }} [opts]
|
|
183
|
-
*/
|
|
184
117
|
export async function bootstrapComponentRegistry(distDir, registerComponents, opts = {}) {
|
|
185
118
|
const preload = opts.preload ?? (opts.closureRoots?.length ? 'closure' : 'all');
|
|
186
119
|
if (preload === 'none')
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
5
|
+
import type { ClientComponentListEntry, ComponentRegistryMap, ListClientComponentsOpts, PreloadComponentRegistryOpts } from '../shared/host.types.js';
|
|
6
|
+
export { DEPLOYMENT_SCHEMA, readDeploymentDocument, componentEntriesFromDeployment, collectDependsOnClosure, dedupeComponentEntriesByTag, mergeExplicitComponentEntries, loadComponentEntries, importAndRegisterComponentEntries, bootstrapComponentRegistry, } from './deployment-registry.js';
|
|
7
|
+
export { createRenderHost } from './render-host.js';
|
|
8
|
+
export declare function listClientComponents(dir: string, opts?: ListClientComponentsOpts): Promise<ClientComponentListEntry[]>;
|
|
9
|
+
export declare function listClientComponentsSync(dir: string, opts?: ListClientComponentsOpts): ClientComponentListEntry[];
|
|
10
|
+
export declare function mergeComponentEntries(entries: ClientComponentListEntry[], explicit?: Record<string, string>): ClientComponentListEntry[];
|
|
11
|
+
export declare function resolveComponentEntries(distDir: string, explicit?: Record<string, string>, opts?: ListClientComponentsOpts & {
|
|
12
|
+
closureRoots?: string[];
|
|
13
|
+
}): Promise<ClientComponentListEntry[]>;
|
|
14
|
+
export declare function preloadComponentRegistry(distDir: string, registerComponents: (map: ComponentRegistryMap) => void, opts?: PreloadComponentRegistryOpts): Promise<ComponentRegistryMap>;
|