@vmz/core 0.1.32 → 0.2.1
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 +141 -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} +67 -220
- package/dist/browser/dom-core.js +2484 -0
- package/dist/browser/dom-core.types.d.ts +127 -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} +5 -30
- 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/faces/http.d.ts +6 -0
- package/dist/{http.js → faces/http.js} +0 -1
- package/dist/faces/server.d.ts +30 -0
- package/dist/{server.js → faces/server.js} +27 -149
- 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} +1 -17
- 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/dom-attr-normalize.d.ts +8 -0
- package/dist/shared/dom-attr-normalize.js +71 -0
- package/dist/shared/host.types.d.ts +113 -0
- package/dist/shared/host.types.js +4 -0
- package/dist/shared/server.types.d.ts +67 -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} +189 -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/http.d.ts +0 -6
- 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/server.d.ts +0 -46
- 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,44 +1,22 @@
|
|
|
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
11
|
* Map `#server/foo` → filesystem / URL the host can `import`.
|
|
28
|
-
* Only set this in Node/SSR hosts
|
|
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
22
|
// Node SSR / smokes set a resolver and leave __VMZ_USE_HTTP_RPC unset → local.
|
|
@@ -47,21 +25,17 @@ export async function callServer(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);
|
|
61
|
-
const
|
|
62
|
-
if (typeof
|
|
33
|
+
const mod = (await import(typeof spec === 'string' ? spec : spec.href));
|
|
34
|
+
const exported = mod.default ?? mod[exportGuess(moduleId)];
|
|
35
|
+
if (typeof exported !== 'function') {
|
|
63
36
|
throw new Error(`vmz:runtime: no class export for ${moduleId}`);
|
|
64
37
|
}
|
|
38
|
+
const Ctor = exported;
|
|
65
39
|
const instance = new Ctor();
|
|
66
40
|
const fn = instance[method];
|
|
67
41
|
if (typeof fn !== 'function') {
|
|
@@ -69,11 +43,6 @@ async function callServerLocal(moduleId, method, args) {
|
|
|
69
43
|
}
|
|
70
44
|
return fn.apply(instance, args);
|
|
71
45
|
}
|
|
72
|
-
/**
|
|
73
|
-
* @param {string} moduleId
|
|
74
|
-
* @param {string} method
|
|
75
|
-
* @param {unknown[]} args
|
|
76
|
-
*/
|
|
77
46
|
async function callServerHttp(moduleId, method, args) {
|
|
78
47
|
let rpcPath = (typeof globalThis !== 'undefined' && globalThis.__VMZ_RPC_PATH) || DEFAULT_RPC_PATH;
|
|
79
48
|
// Node undici `fetch` rejects relative URLs; browsers accept path-only.
|
|
@@ -104,17 +73,11 @@ function exportGuess(moduleId) {
|
|
|
104
73
|
const base = moduleId.split('/').pop() || 'Server';
|
|
105
74
|
return `${base}Server`;
|
|
106
75
|
}
|
|
107
|
-
/**
|
|
108
|
-
* @param {RpcRequest} body
|
|
109
|
-
*/
|
|
110
76
|
export async function handleRpc(body) {
|
|
111
77
|
return callServerLocal(body.moduleId, body.method, body.args ?? []);
|
|
112
78
|
}
|
|
113
79
|
/**
|
|
114
80
|
* Match REST route from `vmz-routes.json`.
|
|
115
|
-
* @param {string} verb
|
|
116
|
-
* @param {string} pathname
|
|
117
|
-
* @returns {Route | null}
|
|
118
81
|
*/
|
|
119
82
|
export function matchRoute(verb, pathname) {
|
|
120
83
|
const v = verb.toUpperCase();
|
|
@@ -126,9 +89,6 @@ export function matchRoute(verb, pathname) {
|
|
|
126
89
|
* Multipart → `[record]` where File/Blob parts stay as File/Blob (tool-site binary upload).
|
|
127
90
|
* Octet-stream PUT also forwards Upload resumable chunk headers (upload-id / chunk-index / chunk-total).
|
|
128
91
|
* Extra args are ignored by zero-parameter server methods (JS).
|
|
129
|
-
* @param {Request} request
|
|
130
|
-
* @param {string} verb
|
|
131
|
-
* @returns {Promise<unknown[]>}
|
|
132
92
|
*/
|
|
133
93
|
async function routeArgsFromRequest(request, verb) {
|
|
134
94
|
const v = String(verb || 'GET').toUpperCase();
|
|
@@ -147,12 +107,12 @@ async function routeArgsFromRequest(request, verb) {
|
|
|
147
107
|
}
|
|
148
108
|
}
|
|
149
109
|
if (ctype.includes('multipart/form-data')) {
|
|
150
|
-
// Parse from raw bytes
|
|
110
|
+
// Parse from raw bytes → undici Request.formData() can UTF-8-mangle high bytes in file parts
|
|
151
111
|
// (0xFF/0xFE → U+FFFD), which breaks Upload binary / tool-site intakes.
|
|
152
112
|
const buf = Buffer.from(await request.arrayBuffer());
|
|
153
113
|
return [parseMultipartBuffer(buf, ctype)];
|
|
154
114
|
}
|
|
155
|
-
// Object-store PUT / resumable chunk PUT
|
|
115
|
+
// Object-store PUT / resumable chunk PUT → keep bytes (never request.text()).
|
|
156
116
|
if (ctype.includes('application/octet-stream') ||
|
|
157
117
|
((v === 'PUT' || v === 'PATCH') && !ctype.includes('json') && !ctype.includes('x-www-form-urlencoded'))) {
|
|
158
118
|
const buf = Buffer.from(await request.arrayBuffer());
|
|
@@ -180,15 +140,13 @@ async function routeArgsFromRequest(request, verb) {
|
|
|
180
140
|
/**
|
|
181
141
|
* Web Standards Fetch entry for ServerArtifact hosts (Node adapter, worker/edge parity).
|
|
182
142
|
* Handles RPC + public ServerRoute only; static/SSR stay on Node host options.
|
|
183
|
-
* @param {Request} request
|
|
184
|
-
* @returns {Promise<Response>}
|
|
185
143
|
*/
|
|
186
144
|
export async function handleFetchRequest(request) {
|
|
187
145
|
const url = new URL(request.url);
|
|
188
146
|
const verb = (request.method || 'GET').toUpperCase();
|
|
189
147
|
try {
|
|
190
148
|
if (verb === 'POST' && url.pathname === DEFAULT_RPC_PATH) {
|
|
191
|
-
const body = await request.json();
|
|
149
|
+
const body = (await request.json());
|
|
192
150
|
const result = await handleRpc(body);
|
|
193
151
|
return Response.json(result);
|
|
194
152
|
}
|
|
@@ -209,9 +167,6 @@ export async function handleFetchRequest(request) {
|
|
|
209
167
|
/**
|
|
210
168
|
* Node `http.createServer` listener: RPC + REST + optional static / SSR index.
|
|
211
169
|
* 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
170
|
*/
|
|
216
171
|
export async function handleNodeRequest(req, res, opts = {}) {
|
|
217
172
|
const host = req.headers.host || '127.0.0.1';
|
|
@@ -227,9 +182,9 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
227
182
|
}
|
|
228
183
|
// Static first for assets + DocumentMount (`/d/…`) so docs aren't swallowed by SSR 404 shells.
|
|
229
184
|
// static route HTML (`index.html`, `about/index.html`, …) is a CDN/deploy projection only —
|
|
230
|
-
// when Server Host SSR is active, those files must not shadow live render (local/dev
|
|
185
|
+
// when Server Host SSR is active, those files must not shadow live render (local/dev → SSR truth).
|
|
231
186
|
if (verb === 'GET' && opts.distDir) {
|
|
232
|
-
const nodePath = await import('node:path');
|
|
187
|
+
const nodePath = (await import('node:path'));
|
|
233
188
|
const { readFile, stat } = await import('node:fs/promises');
|
|
234
189
|
const file = await resolveDistStatic(opts.distDir, url.pathname, nodePath, stat, {
|
|
235
190
|
cookieHeader: String(req.headers.cookie || ''),
|
|
@@ -244,7 +199,8 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
244
199
|
return sendBytes(res, 200, body, contentType(file, nodePath));
|
|
245
200
|
}
|
|
246
201
|
catch (err) {
|
|
247
|
-
|
|
202
|
+
const code = err && typeof err === 'object' && 'code' in err ? err.code : undefined;
|
|
203
|
+
if (code === 'ENOENT' || code === 'EISDIR') {
|
|
248
204
|
/* fall through */
|
|
249
205
|
}
|
|
250
206
|
else {
|
|
@@ -272,7 +228,6 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
272
228
|
});
|
|
273
229
|
try {
|
|
274
230
|
if (typeof opts.renderPageStream === 'function') {
|
|
275
|
-
/** @type {unknown} */
|
|
276
231
|
let body;
|
|
277
232
|
if (verb === 'POST') {
|
|
278
233
|
const ctype = String(req.headers['content-type'] || '');
|
|
@@ -292,7 +247,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
292
247
|
body,
|
|
293
248
|
});
|
|
294
249
|
if (rendered) {
|
|
295
|
-
if (typeof rendered === 'object' && rendered.redirect) {
|
|
250
|
+
if (typeof rendered === 'object' && 'redirect' in rendered && rendered.redirect) {
|
|
296
251
|
const status = Number(rendered.status) || 302;
|
|
297
252
|
const headers = {
|
|
298
253
|
Location: String(rendered.redirect),
|
|
@@ -303,7 +258,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
303
258
|
return;
|
|
304
259
|
}
|
|
305
260
|
const status = rendered && typeof rendered === 'object' && 'status' in rendered ? Number(rendered.status) || 200 : 200;
|
|
306
|
-
const stream = rendered && typeof rendered === 'object' && rendered.stream ? rendered.stream : rendered;
|
|
261
|
+
const stream = rendered && typeof rendered === 'object' && 'stream' in rendered && rendered.stream ? rendered.stream : rendered;
|
|
307
262
|
return await sendHtmlStream(res, status, stream, ac.signal);
|
|
308
263
|
}
|
|
309
264
|
}
|
|
@@ -329,7 +284,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
329
284
|
}
|
|
330
285
|
if (!res.headersSent) {
|
|
331
286
|
if (wantsBrowserHtml(req)) {
|
|
332
|
-
sendHtml(res, 404, browserErrorHtml(404, 'Not Found', `No page matched ${url.pathname}
|
|
287
|
+
sendHtml(res, 404, browserErrorHtml(404, 'Not Found', `No page matched ${url.pathname}`, undefined));
|
|
333
288
|
}
|
|
334
289
|
else {
|
|
335
290
|
sendJson(res, 404, { error: 'not found', path: url.pathname });
|
|
@@ -357,14 +312,8 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
357
312
|
}
|
|
358
313
|
}
|
|
359
314
|
}
|
|
360
|
-
/**
|
|
361
|
-
* @param {import('node:http').IncomingMessage} req
|
|
362
|
-
* @param {URL} url
|
|
363
|
-
* @returns {Promise<Request>}
|
|
364
|
-
*/
|
|
365
315
|
async function incomingToRequest(req, url) {
|
|
366
316
|
const method = (req.method || 'GET').toUpperCase();
|
|
367
|
-
/** @type {HeadersInit} */
|
|
368
317
|
const headers = {};
|
|
369
318
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
370
319
|
if (v == null)
|
|
@@ -378,10 +327,6 @@ async function incomingToRequest(req, url) {
|
|
|
378
327
|
// Node undici requires duplex when constructing Request with a body.
|
|
379
328
|
return new Request(url, { method, headers, body: raw, duplex: 'half' });
|
|
380
329
|
}
|
|
381
|
-
/**
|
|
382
|
-
* @param {import('node:http').ServerResponse} res
|
|
383
|
-
* @param {Response} response
|
|
384
|
-
*/
|
|
385
330
|
async function writeFetchResponse(res, response) {
|
|
386
331
|
const headers = {};
|
|
387
332
|
response.headers.forEach((value, key) => {
|
|
@@ -396,10 +341,6 @@ async function writeFetchResponse(res, response) {
|
|
|
396
341
|
}
|
|
397
342
|
/**
|
|
398
343
|
* 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
344
|
*/
|
|
404
345
|
function safeDistFile(distDir, pathname, nodePath) {
|
|
405
346
|
let rel = decodeURIComponent(pathname.split('?')[0] || '/');
|
|
@@ -418,9 +359,6 @@ function safeDistFile(distDir, pathname, nodePath) {
|
|
|
418
359
|
/**
|
|
419
360
|
* static profile emits per-route HTML beside client assets. That HTML is for CDN / local-static
|
|
420
361
|
* delivery hosts — not for Server Host when SSR is available. DocumentMount stays static.
|
|
421
|
-
* @param {string} file
|
|
422
|
-
* @param {string} pathname
|
|
423
|
-
* @param {typeof import('node:path')} nodePath
|
|
424
362
|
*/
|
|
425
363
|
function isWebStaticHtmlShadow(file, pathname, nodePath) {
|
|
426
364
|
const ext = nodePath.extname(file).toLowerCase();
|
|
@@ -437,13 +375,8 @@ function isWebStaticHtmlShadow(file, pathname, nodePath) {
|
|
|
437
375
|
/**
|
|
438
376
|
* Static resolve for app assets + integrated document mounts:
|
|
439
377
|
* `/d/` → `d/index.html`, `/d/zh-hans/guide` → `d/zh-hans/guide.html`.
|
|
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>}
|
|
445
378
|
*/
|
|
446
|
-
async function resolveDistStatic(distDir, pathname, nodePath, stat) {
|
|
379
|
+
async function resolveDistStatic(distDir, pathname, nodePath, stat, _opts = {}) {
|
|
447
380
|
const candidates = staticPathCandidates(pathname);
|
|
448
381
|
for (const candidate of candidates) {
|
|
449
382
|
const full = safeDistFile(distDir, candidate, nodePath);
|
|
@@ -466,15 +399,10 @@ async function resolveDistStatic(distDir, pathname, nodePath, stat) {
|
|
|
466
399
|
}
|
|
467
400
|
return null;
|
|
468
401
|
}
|
|
469
|
-
/**
|
|
470
|
-
* @param {string} pathname
|
|
471
|
-
* @returns {string[]}
|
|
472
|
-
*/
|
|
473
402
|
function staticPathCandidates(pathname) {
|
|
474
403
|
let rel = decodeURIComponent(pathname.split('?')[0] || '/');
|
|
475
404
|
if (!rel.startsWith('/'))
|
|
476
405
|
rel = `/${rel}`;
|
|
477
|
-
/** @type {string[]} */
|
|
478
406
|
const out = [];
|
|
479
407
|
const push = (p) => {
|
|
480
408
|
if (p && !out.includes(p))
|
|
@@ -498,16 +426,11 @@ function staticPathCandidates(pathname) {
|
|
|
498
426
|
}
|
|
499
427
|
return out;
|
|
500
428
|
}
|
|
501
|
-
/** @param {string} p */
|
|
502
429
|
function nodePathExt(p) {
|
|
503
430
|
const base = p.split('/').pop() || '';
|
|
504
431
|
const i = base.lastIndexOf('.');
|
|
505
432
|
return i > 0 ? base.slice(i) : '';
|
|
506
433
|
}
|
|
507
|
-
/**
|
|
508
|
-
* @param {string} filePath
|
|
509
|
-
* @param {typeof import('node:path')} nodePath
|
|
510
|
-
*/
|
|
511
434
|
function contentType(filePath, nodePath) {
|
|
512
435
|
const ext = nodePath.extname(filePath).toLowerCase();
|
|
513
436
|
switch (ext) {
|
|
@@ -528,13 +451,12 @@ function contentType(filePath, nodePath) {
|
|
|
528
451
|
return 'application/octet-stream';
|
|
529
452
|
}
|
|
530
453
|
}
|
|
531
|
-
/**
|
|
532
|
-
* @param {import('node:http').IncomingMessage} req
|
|
533
|
-
*/
|
|
534
454
|
function readJson(req) {
|
|
535
455
|
return new Promise((resolve, reject) => {
|
|
536
456
|
const chunks = [];
|
|
537
|
-
req.on('data', (c) =>
|
|
457
|
+
req.on('data', (c) => {
|
|
458
|
+
chunks.push(typeof c === 'string' ? Buffer.from(c) : c);
|
|
459
|
+
});
|
|
538
460
|
req.on('end', () => {
|
|
539
461
|
try {
|
|
540
462
|
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
@@ -547,14 +469,12 @@ function readJson(req) {
|
|
|
547
469
|
req.on('error', reject);
|
|
548
470
|
});
|
|
549
471
|
}
|
|
550
|
-
/**
|
|
551
|
-
* @param {import('node:http').IncomingMessage} req
|
|
552
|
-
* @returns {Promise<Buffer>}
|
|
553
|
-
*/
|
|
554
472
|
function readRawBody(req) {
|
|
555
473
|
return new Promise((resolve, reject) => {
|
|
556
474
|
const chunks = [];
|
|
557
|
-
req.on('data', (c) =>
|
|
475
|
+
req.on('data', (c) => {
|
|
476
|
+
chunks.push(typeof c === 'string' ? Buffer.from(c) : c);
|
|
477
|
+
});
|
|
558
478
|
// Buffer — never utf8-string: multipart File bytes must survive (Upload binary gate).
|
|
559
479
|
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
560
480
|
req.on('error', reject);
|
|
@@ -562,9 +482,6 @@ function readRawBody(req) {
|
|
|
562
482
|
}
|
|
563
483
|
/**
|
|
564
484
|
* Buffer-safe multipart/form-data parser (file parts stay binary).
|
|
565
|
-
* @param {Buffer} buf
|
|
566
|
-
* @param {string} contentType
|
|
567
|
-
* @returns {Record<string, unknown>}
|
|
568
485
|
*/
|
|
569
486
|
function parseMultipartBuffer(buf, contentType) {
|
|
570
487
|
const bm = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(String(contentType || ''));
|
|
@@ -573,7 +490,6 @@ function parseMultipartBuffer(buf, contentType) {
|
|
|
573
490
|
throw new Error('multipart: missing boundary');
|
|
574
491
|
}
|
|
575
492
|
const sep = Buffer.from(`--${boundary}`);
|
|
576
|
-
/** @type {Record<string, unknown>} */
|
|
577
493
|
const out = {};
|
|
578
494
|
let start = indexOfBuffer(buf, sep, 0);
|
|
579
495
|
if (start < 0)
|
|
@@ -629,11 +545,6 @@ function parseMultipartBuffer(buf, contentType) {
|
|
|
629
545
|
}
|
|
630
546
|
return out;
|
|
631
547
|
}
|
|
632
|
-
/**
|
|
633
|
-
* @param {Buffer} hay
|
|
634
|
-
* @param {Buffer} needle
|
|
635
|
-
* @param {number} from
|
|
636
|
-
*/
|
|
637
548
|
function indexOfBuffer(hay, needle, from) {
|
|
638
549
|
if (!needle.length)
|
|
639
550
|
return from;
|
|
@@ -646,31 +557,23 @@ function indexOfBuffer(hay, needle, from) {
|
|
|
646
557
|
}
|
|
647
558
|
return -1;
|
|
648
559
|
}
|
|
649
|
-
/**
|
|
650
|
-
* @param {string} raw
|
|
651
|
-
* @param {string} contentType
|
|
652
|
-
* @returns {Record<string, string> | string}
|
|
653
|
-
*/
|
|
654
560
|
function parseFormBody(raw, contentType) {
|
|
655
|
-
|
|
561
|
+
const text = typeof raw === 'string' ? raw : raw.toString('utf8');
|
|
562
|
+
if (!text)
|
|
656
563
|
return {};
|
|
657
564
|
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
658
565
|
const out = {};
|
|
659
|
-
for (const [k, v] of new URLSearchParams(
|
|
566
|
+
for (const [k, v] of new URLSearchParams(text))
|
|
660
567
|
out[k] = v;
|
|
661
568
|
return out;
|
|
662
569
|
}
|
|
663
570
|
try {
|
|
664
|
-
return JSON.parse(
|
|
571
|
+
return JSON.parse(text);
|
|
665
572
|
}
|
|
666
573
|
catch {
|
|
667
|
-
return { raw };
|
|
574
|
+
return { raw: text };
|
|
668
575
|
}
|
|
669
576
|
}
|
|
670
|
-
/**
|
|
671
|
-
* @param {import('node:http').IncomingMessage} req
|
|
672
|
-
* @returns {boolean}
|
|
673
|
-
*/
|
|
674
577
|
function wantsBrowserHtml(req) {
|
|
675
578
|
const accept = String(req.headers?.accept || '');
|
|
676
579
|
const method = String(req.method || 'GET').toUpperCase();
|
|
@@ -698,10 +601,6 @@ function wantsBrowserHtml(req) {
|
|
|
698
601
|
/**
|
|
699
602
|
* Browser-facing error document (no N-API). Used when SSR returns null or throws
|
|
700
603
|
* on a document navigation — must not fall back to API-shaped JSON.
|
|
701
|
-
* @param {number} status
|
|
702
|
-
* @param {string} title
|
|
703
|
-
* @param {string} message
|
|
704
|
-
* @param {string} [stack]
|
|
705
604
|
*/
|
|
706
605
|
function browserErrorHtml(status, title, message, stack) {
|
|
707
606
|
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
@@ -734,11 +633,6 @@ function browserErrorHtml(status, title, message, stack) {
|
|
|
734
633
|
</html>
|
|
735
634
|
`;
|
|
736
635
|
}
|
|
737
|
-
/**
|
|
738
|
-
* @param {import('node:http').ServerResponse} res
|
|
739
|
-
* @param {number} status
|
|
740
|
-
* @param {unknown} body
|
|
741
|
-
*/
|
|
742
636
|
function sendJson(res, status, body) {
|
|
743
637
|
const payload = JSON.stringify(body);
|
|
744
638
|
res.writeHead(status, {
|
|
@@ -747,11 +641,6 @@ function sendJson(res, status, body) {
|
|
|
747
641
|
});
|
|
748
642
|
res.end(payload);
|
|
749
643
|
}
|
|
750
|
-
/**
|
|
751
|
-
* @param {import('node:http').ServerResponse} res
|
|
752
|
-
* @param {number} status
|
|
753
|
-
* @param {string} html
|
|
754
|
-
*/
|
|
755
644
|
function sendHtml(res, status, html) {
|
|
756
645
|
const payload = typeof html === 'string' ? html : String(html);
|
|
757
646
|
res.writeHead(status, {
|
|
@@ -763,10 +652,6 @@ function sendHtml(res, status, html) {
|
|
|
763
652
|
/**
|
|
764
653
|
* Stream HTML without buffering the full document (event-flow / stream SSR host).
|
|
765
654
|
* 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
655
|
*/
|
|
771
656
|
async function sendHtmlStream(res, status, source, signal) {
|
|
772
657
|
const aborted = () => Boolean(signal?.aborted || res.destroyed || res.writableEnded || !res.writable);
|
|
@@ -821,14 +706,7 @@ async function sendHtmlStream(res, status, source, signal) {
|
|
|
821
706
|
res.end();
|
|
822
707
|
}
|
|
823
708
|
}
|
|
824
|
-
/**
|
|
825
|
-
* @param {import('node:http').ServerResponse} res
|
|
826
|
-
* @param {number} status
|
|
827
|
-
* @param {Buffer} body
|
|
828
|
-
* @param {string} type
|
|
829
|
-
*/
|
|
830
709
|
function sendBytes(res, status, body, type) {
|
|
831
|
-
/** @type {Record<string, string | number>} */
|
|
832
710
|
const headers = {
|
|
833
711
|
'content-type': type,
|
|
834
712
|
'content-length': body.byteLength,
|
|
@@ -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>;
|