@vmz/core 0.0.4 → 0.1.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 +20 -2
- package/dist/client-nav.js +556 -50
- package/dist/dom-core.d.ts +337 -0
- package/dist/dom-core.js +4545 -0
- package/dist/dom-ssr.d.ts +78 -0
- package/dist/dom-ssr.js +970 -0
- package/dist/dom.client.d.ts +4 -0
- package/dist/dom.client.js +5 -0
- package/dist/dom.d.ts +3 -219
- package/dist/dom.js +3 -4377
- package/dist/serve-host.d.ts +17 -0
- package/dist/serve-host.mjs +550 -156
- package/dist/server.d.ts +8 -0
- package/dist/server.js +233 -11
- package/dist/vmz-dom.d.ts +5 -0
- package/dist/vmz-dom.js +6 -0
- package/dist/vmz-runtime.d.ts +5 -0
- package/dist/vmz-runtime.js +6 -0
- package/package.json +8 -1
package/dist/server.d.ts
CHANGED
|
@@ -29,8 +29,16 @@ export declare function handleRpc(body: any): Promise<any>;
|
|
|
29
29
|
* @returns {Route | null}
|
|
30
30
|
*/
|
|
31
31
|
export declare function matchRoute(verb: any, pathname: any): any;
|
|
32
|
+
/**
|
|
33
|
+
* Web Standards Fetch entry for ServerArtifact hosts (Node adapter, worker/edge parity).
|
|
34
|
+
* Handles RPC + public ServerRoute only; static/SSR stay on Node host options.
|
|
35
|
+
* @param {Request} request
|
|
36
|
+
* @returns {Promise<Response>}
|
|
37
|
+
*/
|
|
38
|
+
export declare function handleFetchRequest(request: any): Promise<Response>;
|
|
32
39
|
/**
|
|
33
40
|
* Node `http.createServer` listener: RPC + REST + optional static / SSR index.
|
|
41
|
+
* RPC/REST go through {@link handleFetchRequest} so Node and Fetch hosts share one core.
|
|
34
42
|
* @param {import('node:http').IncomingMessage} req
|
|
35
43
|
* @param {import('node:http').ServerResponse} res
|
|
36
44
|
* @param {NodeRequestOptions} [opts]
|
package/dist/server.js
CHANGED
|
@@ -75,7 +75,21 @@ async function callServerLocal(moduleId, method, args) {
|
|
|
75
75
|
* @param {unknown[]} args
|
|
76
76
|
*/
|
|
77
77
|
async function callServerHttp(moduleId, method, args) {
|
|
78
|
-
|
|
78
|
+
let rpcPath = (typeof globalThis !== 'undefined' && globalThis.__VMZ_RPC_PATH) || DEFAULT_RPC_PATH;
|
|
79
|
+
// Node undici `fetch` rejects relative URLs; browsers accept path-only.
|
|
80
|
+
if (typeof rpcPath === 'string' && !/^https?:\/\//i.test(rpcPath)) {
|
|
81
|
+
const origin = (typeof globalThis !== 'undefined' && globalThis.__VMZ_RPC_ORIGIN) ||
|
|
82
|
+
(typeof window !== 'undefined' && window.location && window.location.origin) ||
|
|
83
|
+
null;
|
|
84
|
+
if (origin) {
|
|
85
|
+
rpcPath = new URL(rpcPath, origin).href;
|
|
86
|
+
}
|
|
87
|
+
else if (typeof window === 'undefined') {
|
|
88
|
+
const host = (typeof process !== 'undefined' && (process.env.VMZ_HOST || process.env.HOST)) || '127.0.0.1';
|
|
89
|
+
const port = (typeof process !== 'undefined' && (process.env.VMZ_PORT || process.env.PORT)) || '5173';
|
|
90
|
+
rpcPath = new URL(rpcPath, `http://${host}:${port}`).href;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
79
93
|
const res = await fetch(rpcPath, {
|
|
80
94
|
method: 'POST',
|
|
81
95
|
headers: { 'content-type': 'application/json' },
|
|
@@ -106,8 +120,95 @@ export function matchRoute(verb, pathname) {
|
|
|
106
120
|
const v = verb.toUpperCase();
|
|
107
121
|
return routes.find((r) => r.verb.toUpperCase() === v && r.path === pathname) ?? null;
|
|
108
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Build `callServerLocal` args from a Fetch Request for REST routes.
|
|
125
|
+
* GET/HEAD → `[]`. JSON POST/PUT/PATCH → `[body]`. form-urlencoded → `[record]`.
|
|
126
|
+
* Multipart → `[record]` where File/Blob parts stay as File/Blob (tool-site binary upload).
|
|
127
|
+
* Octet-stream PUT also forwards Upload resumable chunk headers (upload-id / chunk-index / chunk-total).
|
|
128
|
+
* Extra args are ignored by zero-parameter server methods (JS).
|
|
129
|
+
* @param {Request} request
|
|
130
|
+
* @param {string} verb
|
|
131
|
+
* @returns {Promise<unknown[]>}
|
|
132
|
+
*/
|
|
133
|
+
async function routeArgsFromRequest(request, verb) {
|
|
134
|
+
const v = String(verb || 'GET').toUpperCase();
|
|
135
|
+
if (v === 'GET' || v === 'HEAD' || v === 'OPTIONS')
|
|
136
|
+
return [];
|
|
137
|
+
const ctype = String(request.headers.get('content-type') || '');
|
|
138
|
+
if (ctype.includes('application/json')) {
|
|
139
|
+
const text = await request.text();
|
|
140
|
+
if (!text || !String(text).trim())
|
|
141
|
+
return [{}];
|
|
142
|
+
try {
|
|
143
|
+
return [JSON.parse(text)];
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
throw new Error(`invalid JSON body: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (ctype.includes('multipart/form-data')) {
|
|
150
|
+
// Parse from raw bytes — undici Request.formData() can UTF-8-mangle high bytes in file parts
|
|
151
|
+
// (0xFF/0xFE → U+FFFD), which breaks Upload binary / tool-site intakes.
|
|
152
|
+
const buf = Buffer.from(await request.arrayBuffer());
|
|
153
|
+
return [parseMultipartBuffer(buf, ctype)];
|
|
154
|
+
}
|
|
155
|
+
// Object-store PUT / resumable chunk PUT — keep bytes (never request.text()).
|
|
156
|
+
if (ctype.includes('application/octet-stream') ||
|
|
157
|
+
((v === 'PUT' || v === 'PATCH') && !ctype.includes('json') && !ctype.includes('x-www-form-urlencoded'))) {
|
|
158
|
+
const buf = Buffer.from(await request.arrayBuffer());
|
|
159
|
+
const key = String(request.headers.get('x-vmz-object-key') || '');
|
|
160
|
+
const uploadId = String(request.headers.get('x-vmz-upload-id') || '');
|
|
161
|
+
const chunkIndexRaw = request.headers.get('x-vmz-chunk-index');
|
|
162
|
+
const chunkTotalRaw = request.headers.get('x-vmz-chunk-total');
|
|
163
|
+
const chunkIndex = chunkIndexRaw != null && String(chunkIndexRaw).trim() !== '' ? Number(chunkIndexRaw) : undefined;
|
|
164
|
+
const chunkTotal = chunkTotalRaw != null && String(chunkTotalRaw).trim() !== '' ? Number(chunkTotalRaw) : undefined;
|
|
165
|
+
return [
|
|
166
|
+
{
|
|
167
|
+
bytes: buf,
|
|
168
|
+
size: buf.byteLength,
|
|
169
|
+
key,
|
|
170
|
+
uploadId,
|
|
171
|
+
chunkIndex: Number.isFinite(chunkIndex) ? chunkIndex : undefined,
|
|
172
|
+
chunkTotal: Number.isFinite(chunkTotal) ? chunkTotal : undefined,
|
|
173
|
+
contentType: ctype || 'application/octet-stream',
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
const raw = await request.text();
|
|
178
|
+
return [parseFormBody(raw, ctype)];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Web Standards Fetch entry for ServerArtifact hosts (Node adapter, worker/edge parity).
|
|
182
|
+
* Handles RPC + public ServerRoute only; static/SSR stay on Node host options.
|
|
183
|
+
* @param {Request} request
|
|
184
|
+
* @returns {Promise<Response>}
|
|
185
|
+
*/
|
|
186
|
+
export async function handleFetchRequest(request) {
|
|
187
|
+
const url = new URL(request.url);
|
|
188
|
+
const verb = (request.method || 'GET').toUpperCase();
|
|
189
|
+
try {
|
|
190
|
+
if (verb === 'POST' && url.pathname === DEFAULT_RPC_PATH) {
|
|
191
|
+
const body = await request.json();
|
|
192
|
+
const result = await handleRpc(body);
|
|
193
|
+
return Response.json(result);
|
|
194
|
+
}
|
|
195
|
+
const route = matchRoute(verb, url.pathname);
|
|
196
|
+
if (route) {
|
|
197
|
+
const args = await routeArgsFromRequest(request, verb);
|
|
198
|
+
const result = await callServerLocal(route.moduleId, route.method, args);
|
|
199
|
+
return Response.json(result);
|
|
200
|
+
}
|
|
201
|
+
return Response.json({ error: 'not found', path: url.pathname }, { status: 404 });
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
return Response.json({
|
|
205
|
+
error: err instanceof Error ? err.message : String(err),
|
|
206
|
+
}, { status: 500 });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
109
209
|
/**
|
|
110
210
|
* Node `http.createServer` listener: RPC + REST + optional static / SSR index.
|
|
211
|
+
* RPC/REST go through {@link handleFetchRequest} so Node and Fetch hosts share one core.
|
|
111
212
|
* @param {import('node:http').IncomingMessage} req
|
|
112
213
|
* @param {import('node:http').ServerResponse} res
|
|
113
214
|
* @param {NodeRequestOptions} [opts]
|
|
@@ -117,15 +218,12 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
117
218
|
const url = new URL(req.url || '/', `http://${host}`);
|
|
118
219
|
const verb = (req.method || 'GET').toUpperCase();
|
|
119
220
|
try {
|
|
120
|
-
|
|
121
|
-
const body = await readJson(req);
|
|
122
|
-
const result = await handleRpc(body);
|
|
123
|
-
return sendJson(res, 200, result);
|
|
124
|
-
}
|
|
221
|
+
const isRpc = verb === 'POST' && url.pathname === DEFAULT_RPC_PATH;
|
|
125
222
|
const route = matchRoute(verb, url.pathname);
|
|
126
|
-
if (route) {
|
|
127
|
-
const
|
|
128
|
-
|
|
223
|
+
if (isRpc || route) {
|
|
224
|
+
const request = await incomingToRequest(req, url);
|
|
225
|
+
const response = await handleFetchRequest(request);
|
|
226
|
+
return await writeFetchResponse(res, response);
|
|
129
227
|
}
|
|
130
228
|
// Static first for assets + DocumentMount (`/d/…`) so docs aren't swallowed by SSR 404 shells.
|
|
131
229
|
// web-static route HTML (`index.html`, `about/index.html`, …) is a CDN/deploy projection only —
|
|
@@ -247,6 +345,43 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
247
345
|
});
|
|
248
346
|
}
|
|
249
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* @param {import('node:http').IncomingMessage} req
|
|
350
|
+
* @param {URL} url
|
|
351
|
+
* @returns {Promise<Request>}
|
|
352
|
+
*/
|
|
353
|
+
async function incomingToRequest(req, url) {
|
|
354
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
355
|
+
/** @type {HeadersInit} */
|
|
356
|
+
const headers = {};
|
|
357
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
358
|
+
if (v == null)
|
|
359
|
+
continue;
|
|
360
|
+
headers[k] = Array.isArray(v) ? v.join(', ') : String(v);
|
|
361
|
+
}
|
|
362
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
363
|
+
return new Request(url, { method, headers });
|
|
364
|
+
}
|
|
365
|
+
const raw = await readRawBody(req);
|
|
366
|
+
// Node undici requires duplex when constructing Request with a body.
|
|
367
|
+
return new Request(url, { method, headers, body: raw, duplex: 'half' });
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* @param {import('node:http').ServerResponse} res
|
|
371
|
+
* @param {Response} response
|
|
372
|
+
*/
|
|
373
|
+
async function writeFetchResponse(res, response) {
|
|
374
|
+
const headers = {};
|
|
375
|
+
response.headers.forEach((value, key) => {
|
|
376
|
+
headers[key] = value;
|
|
377
|
+
});
|
|
378
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
379
|
+
if (!headers['content-length'] && !headers['Content-Length']) {
|
|
380
|
+
headers['content-length'] = String(buf.byteLength);
|
|
381
|
+
}
|
|
382
|
+
res.writeHead(response.status, headers);
|
|
383
|
+
res.end(buf);
|
|
384
|
+
}
|
|
250
385
|
/**
|
|
251
386
|
* Resolve a path under distDir; reject `..` escapes.
|
|
252
387
|
* @param {string} distDir
|
|
@@ -402,16 +537,103 @@ function readJson(req) {
|
|
|
402
537
|
}
|
|
403
538
|
/**
|
|
404
539
|
* @param {import('node:http').IncomingMessage} req
|
|
405
|
-
* @returns {Promise<
|
|
540
|
+
* @returns {Promise<Buffer>}
|
|
406
541
|
*/
|
|
407
542
|
function readRawBody(req) {
|
|
408
543
|
return new Promise((resolve, reject) => {
|
|
409
544
|
const chunks = [];
|
|
410
545
|
req.on('data', (c) => chunks.push(c));
|
|
411
|
-
|
|
546
|
+
// Buffer — never utf8-string: multipart File bytes must survive (Upload binary gate).
|
|
547
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
412
548
|
req.on('error', reject);
|
|
413
549
|
});
|
|
414
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Buffer-safe multipart/form-data parser (file parts stay binary).
|
|
553
|
+
* @param {Buffer} buf
|
|
554
|
+
* @param {string} contentType
|
|
555
|
+
* @returns {Record<string, unknown>}
|
|
556
|
+
*/
|
|
557
|
+
function parseMultipartBuffer(buf, contentType) {
|
|
558
|
+
const bm = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(String(contentType || ''));
|
|
559
|
+
const boundary = bm ? bm[1] || bm[2] : '';
|
|
560
|
+
if (!boundary) {
|
|
561
|
+
throw new Error('multipart: missing boundary');
|
|
562
|
+
}
|
|
563
|
+
const sep = Buffer.from(`--${boundary}`);
|
|
564
|
+
/** @type {Record<string, unknown>} */
|
|
565
|
+
const out = {};
|
|
566
|
+
let start = indexOfBuffer(buf, sep, 0);
|
|
567
|
+
if (start < 0)
|
|
568
|
+
return out;
|
|
569
|
+
start += sep.length;
|
|
570
|
+
// Optional leading CRLF after first boundary is handled per-part.
|
|
571
|
+
while (start < buf.length) {
|
|
572
|
+
if (buf[start] === 0x2d && buf[start + 1] === 0x2d)
|
|
573
|
+
break; // trailing --
|
|
574
|
+
if (buf[start] === 0x0d && buf[start + 1] === 0x0a)
|
|
575
|
+
start += 2;
|
|
576
|
+
const next = indexOfBuffer(buf, sep, start);
|
|
577
|
+
const end = next < 0 ? buf.length : next;
|
|
578
|
+
let part = buf.subarray(start, end);
|
|
579
|
+
// Trim trailing CRLF before boundary.
|
|
580
|
+
if (part.length >= 2 && part[part.length - 2] === 0x0d && part[part.length - 1] === 0x0a) {
|
|
581
|
+
part = part.subarray(0, part.length - 2);
|
|
582
|
+
}
|
|
583
|
+
const splitAt = indexOfBuffer(part, Buffer.from('\r\n\r\n'), 0);
|
|
584
|
+
if (splitAt >= 0) {
|
|
585
|
+
const headerText = part.subarray(0, splitAt).toString('utf8');
|
|
586
|
+
let body = part.subarray(splitAt + 4);
|
|
587
|
+
const nameM = /content-disposition:[^\r\n]*;\s*name="([^"]*)"/i.exec(headerText);
|
|
588
|
+
const fileM = /content-disposition:[^\r\n]*;\s*filename="([^"]*)"/i.exec(headerText);
|
|
589
|
+
const typeM = /content-type:\s*([^\r\n]+)/i.exec(headerText);
|
|
590
|
+
const key = nameM ? nameM[1] : '';
|
|
591
|
+
if (key) {
|
|
592
|
+
if (fileM) {
|
|
593
|
+
const filename = fileM[1] || 'upload.bin';
|
|
594
|
+
const type = typeM ? String(typeM[1]).trim() : 'application/octet-stream';
|
|
595
|
+
// Copy body — File may outlive the request buffer.
|
|
596
|
+
const copy = Buffer.from(body);
|
|
597
|
+
const file = new File([copy], filename, { type });
|
|
598
|
+
const prev = out[key];
|
|
599
|
+
if (prev == null) {
|
|
600
|
+
out[key] = file;
|
|
601
|
+
}
|
|
602
|
+
else if (Array.isArray(prev)) {
|
|
603
|
+
prev.push(file);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
out[key] = [prev, file];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
out[key] = body.toString('utf8');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (next < 0)
|
|
615
|
+
break;
|
|
616
|
+
start = next + sep.length;
|
|
617
|
+
}
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* @param {Buffer} hay
|
|
622
|
+
* @param {Buffer} needle
|
|
623
|
+
* @param {number} from
|
|
624
|
+
*/
|
|
625
|
+
function indexOfBuffer(hay, needle, from) {
|
|
626
|
+
if (!needle.length)
|
|
627
|
+
return from;
|
|
628
|
+
outer: for (let i = Math.max(0, from); i <= hay.length - needle.length; i++) {
|
|
629
|
+
for (let j = 0; j < needle.length; j++) {
|
|
630
|
+
if (hay[i + j] !== needle[j])
|
|
631
|
+
continue outer;
|
|
632
|
+
}
|
|
633
|
+
return i;
|
|
634
|
+
}
|
|
635
|
+
return -1;
|
|
636
|
+
}
|
|
415
637
|
/**
|
|
416
638
|
* @param {string} raw
|
|
417
639
|
* @param {string} contentType
|
package/dist/vmz-dom.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
|
|
6
6
|
"exports": {
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
"types": "./dist/dom.d.ts",
|
|
13
13
|
"default": "./dist/dom.js"
|
|
14
14
|
},
|
|
15
|
+
"./dom/client": {
|
|
16
|
+
"types": "./dist/dom.client.d.ts",
|
|
17
|
+
"default": "./dist/dom.client.js"
|
|
18
|
+
},
|
|
15
19
|
"./server": {
|
|
16
20
|
"types": "./dist/server.d.ts",
|
|
17
21
|
"default": "./dist/server.js"
|
|
@@ -38,6 +42,9 @@
|
|
|
38
42
|
"scripts": {
|
|
39
43
|
"build": "tsc -p tsconfig.json && node ../../../scripts/build/copy-serve-host-mjs.mjs"
|
|
40
44
|
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"linkedom": "^0.18.13"
|
|
47
|
+
},
|
|
41
48
|
"publishConfig": {
|
|
42
49
|
"access": "public"
|
|
43
50
|
},
|