@vmz/core 0.0.3 → 0.1.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 +64 -0
- package/dist/client-nav.js +696 -0
- package/dist/dom-core.d.ts +337 -0
- package/dist/dom-core.js +4565 -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 -200
- package/dist/dom.js +3 -3055
- package/dist/serve-host.d.ts +13 -0
- package/dist/serve-host.mjs +718 -94
- package/dist/server.d.ts +8 -0
- package/dist/server.js +313 -19
- 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 +12 -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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* renderIndex?: => Promise<string> | string,
|
|
15
15
|
* renderIndexStream?: (opts?: { signal?: AbortSignal }) => AsyncIterable<string>,
|
|
16
16
|
* renderPage?: (pathname: string) => Promise<string | null> | string | null,
|
|
17
|
-
* renderPageStream?: (pathname: string, opts?: { signal?: AbortSignal, searchParams?: URLSearchParams, cookieHeader?: string }) => Promise<AsyncIterable<string> | null> | AsyncIterable<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
18
|
* req?: import('node:http').IncomingMessage,
|
|
19
19
|
* }} NodeRequestOptions
|
|
20
20
|
*/
|
|
@@ -106,8 +106,95 @@ export function matchRoute(verb, pathname) {
|
|
|
106
106
|
const v = verb.toUpperCase();
|
|
107
107
|
return routes.find((r) => r.verb.toUpperCase() === v && r.path === pathname) ?? null;
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Build `callServerLocal` args from a Fetch Request for REST routes.
|
|
111
|
+
* GET/HEAD → `[]`. JSON POST/PUT/PATCH → `[body]`. form-urlencoded → `[record]`.
|
|
112
|
+
* Multipart → `[record]` where File/Blob parts stay as File/Blob (tool-site binary upload).
|
|
113
|
+
* Octet-stream PUT also forwards Upload resumable chunk headers (upload-id / chunk-index / chunk-total).
|
|
114
|
+
* Extra args are ignored by zero-parameter server methods (JS).
|
|
115
|
+
* @param {Request} request
|
|
116
|
+
* @param {string} verb
|
|
117
|
+
* @returns {Promise<unknown[]>}
|
|
118
|
+
*/
|
|
119
|
+
async function routeArgsFromRequest(request, verb) {
|
|
120
|
+
const v = String(verb || 'GET').toUpperCase();
|
|
121
|
+
if (v === 'GET' || v === 'HEAD' || v === 'OPTIONS')
|
|
122
|
+
return [];
|
|
123
|
+
const ctype = String(request.headers.get('content-type') || '');
|
|
124
|
+
if (ctype.includes('application/json')) {
|
|
125
|
+
const text = await request.text();
|
|
126
|
+
if (!text || !String(text).trim())
|
|
127
|
+
return [{}];
|
|
128
|
+
try {
|
|
129
|
+
return [JSON.parse(text)];
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw new Error(`invalid JSON body: ${err instanceof Error ? err.message : String(err)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (ctype.includes('multipart/form-data')) {
|
|
136
|
+
// Parse from raw bytes — undici Request.formData() can UTF-8-mangle high bytes in file parts
|
|
137
|
+
// (0xFF/0xFE → U+FFFD), which breaks Upload binary / tool-site intakes.
|
|
138
|
+
const buf = Buffer.from(await request.arrayBuffer());
|
|
139
|
+
return [parseMultipartBuffer(buf, ctype)];
|
|
140
|
+
}
|
|
141
|
+
// Object-store PUT / resumable chunk PUT — keep bytes (never request.text()).
|
|
142
|
+
if (ctype.includes('application/octet-stream') ||
|
|
143
|
+
((v === 'PUT' || v === 'PATCH') && !ctype.includes('json') && !ctype.includes('x-www-form-urlencoded'))) {
|
|
144
|
+
const buf = Buffer.from(await request.arrayBuffer());
|
|
145
|
+
const key = String(request.headers.get('x-vmz-object-key') || '');
|
|
146
|
+
const uploadId = String(request.headers.get('x-vmz-upload-id') || '');
|
|
147
|
+
const chunkIndexRaw = request.headers.get('x-vmz-chunk-index');
|
|
148
|
+
const chunkTotalRaw = request.headers.get('x-vmz-chunk-total');
|
|
149
|
+
const chunkIndex = chunkIndexRaw != null && String(chunkIndexRaw).trim() !== '' ? Number(chunkIndexRaw) : undefined;
|
|
150
|
+
const chunkTotal = chunkTotalRaw != null && String(chunkTotalRaw).trim() !== '' ? Number(chunkTotalRaw) : undefined;
|
|
151
|
+
return [
|
|
152
|
+
{
|
|
153
|
+
bytes: buf,
|
|
154
|
+
size: buf.byteLength,
|
|
155
|
+
key,
|
|
156
|
+
uploadId,
|
|
157
|
+
chunkIndex: Number.isFinite(chunkIndex) ? chunkIndex : undefined,
|
|
158
|
+
chunkTotal: Number.isFinite(chunkTotal) ? chunkTotal : undefined,
|
|
159
|
+
contentType: ctype || 'application/octet-stream',
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
const raw = await request.text();
|
|
164
|
+
return [parseFormBody(raw, ctype)];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Web Standards Fetch entry for ServerArtifact hosts (Node adapter, worker/edge parity).
|
|
168
|
+
* Handles RPC + public ServerRoute only; static/SSR stay on Node host options.
|
|
169
|
+
* @param {Request} request
|
|
170
|
+
* @returns {Promise<Response>}
|
|
171
|
+
*/
|
|
172
|
+
export async function handleFetchRequest(request) {
|
|
173
|
+
const url = new URL(request.url);
|
|
174
|
+
const verb = (request.method || 'GET').toUpperCase();
|
|
175
|
+
try {
|
|
176
|
+
if (verb === 'POST' && url.pathname === DEFAULT_RPC_PATH) {
|
|
177
|
+
const body = await request.json();
|
|
178
|
+
const result = await handleRpc(body);
|
|
179
|
+
return Response.json(result);
|
|
180
|
+
}
|
|
181
|
+
const route = matchRoute(verb, url.pathname);
|
|
182
|
+
if (route) {
|
|
183
|
+
const args = await routeArgsFromRequest(request, verb);
|
|
184
|
+
const result = await callServerLocal(route.moduleId, route.method, args);
|
|
185
|
+
return Response.json(result);
|
|
186
|
+
}
|
|
187
|
+
return Response.json({ error: 'not found', path: url.pathname }, { status: 404 });
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
return Response.json({
|
|
191
|
+
error: err instanceof Error ? err.message : String(err),
|
|
192
|
+
}, { status: 500 });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
109
195
|
/**
|
|
110
196
|
* Node `http.createServer` listener: RPC + REST + optional static / SSR index.
|
|
197
|
+
* RPC/REST go through {@link handleFetchRequest} so Node and Fetch hosts share one core.
|
|
111
198
|
* @param {import('node:http').IncomingMessage} req
|
|
112
199
|
* @param {import('node:http').ServerResponse} res
|
|
113
200
|
* @param {NodeRequestOptions} [opts]
|
|
@@ -117,22 +204,25 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
117
204
|
const url = new URL(req.url || '/', `http://${host}`);
|
|
118
205
|
const verb = (req.method || 'GET').toUpperCase();
|
|
119
206
|
try {
|
|
120
|
-
|
|
121
|
-
const body = await readJson(req);
|
|
122
|
-
const result = await handleRpc(body);
|
|
123
|
-
return sendJson(res, 200, result);
|
|
124
|
-
}
|
|
207
|
+
const isRpc = verb === 'POST' && url.pathname === DEFAULT_RPC_PATH;
|
|
125
208
|
const route = matchRoute(verb, url.pathname);
|
|
126
|
-
if (route) {
|
|
127
|
-
const
|
|
128
|
-
|
|
209
|
+
if (isRpc || route) {
|
|
210
|
+
const request = await incomingToRequest(req, url);
|
|
211
|
+
const response = await handleFetchRequest(request);
|
|
212
|
+
return await writeFetchResponse(res, response);
|
|
129
213
|
}
|
|
130
|
-
// Static first
|
|
214
|
+
// Static first for assets + DocumentMount (`/d/…`) so docs aren't swallowed by SSR 404 shells.
|
|
215
|
+
// web-static route HTML (`index.html`, `about/index.html`, …) is a CDN/deploy projection only —
|
|
216
|
+
// when Server Host SSR is active, those files must not shadow live render (local/dev ≡ SSR truth).
|
|
131
217
|
if (verb === 'GET' && opts.distDir) {
|
|
132
218
|
const nodePath = await import('node:path');
|
|
133
219
|
const { readFile, stat } = await import('node:fs/promises');
|
|
134
220
|
const file = await resolveDistStatic(opts.distDir, url.pathname, nodePath, stat);
|
|
135
|
-
|
|
221
|
+
const hasSsr = typeof opts.renderPageStream === 'function' ||
|
|
222
|
+
typeof opts.renderPage === 'function' ||
|
|
223
|
+
typeof opts.renderIndexStream === 'function' ||
|
|
224
|
+
typeof opts.renderIndex === 'function';
|
|
225
|
+
if (file && !(hasSsr && isWebStaticHtmlShadow(file, url.pathname, nodePath))) {
|
|
136
226
|
try {
|
|
137
227
|
const body = await readFile(file);
|
|
138
228
|
return sendBytes(res, 200, body, contentType(file, nodePath));
|
|
@@ -147,9 +237,9 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
147
237
|
}
|
|
148
238
|
}
|
|
149
239
|
}
|
|
150
|
-
if (verb === 'GET' && (opts.renderPageStream || opts.renderPage || opts.renderIndexStream || opts.renderIndex)) {
|
|
240
|
+
if ((verb === 'GET' || verb === 'POST') && (opts.renderPageStream || opts.renderPage || opts.renderIndexStream || opts.renderIndex)) {
|
|
151
241
|
const ac = new AbortController();
|
|
152
|
-
const
|
|
242
|
+
const onClientGone = () => {
|
|
153
243
|
try {
|
|
154
244
|
ac.abort();
|
|
155
245
|
}
|
|
@@ -157,28 +247,57 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
157
247
|
/* ignore */
|
|
158
248
|
}
|
|
159
249
|
};
|
|
160
|
-
|
|
161
|
-
|
|
250
|
+
// Do not abort on IncomingMessage `close` — that fires after a POST body is
|
|
251
|
+
// fully read (normal), which would cancel SSR before the first chunk.
|
|
252
|
+
req.on('aborted', onClientGone);
|
|
253
|
+
res.on('close', () => {
|
|
254
|
+
if (!res.writableEnded)
|
|
255
|
+
onClientGone();
|
|
256
|
+
});
|
|
162
257
|
try {
|
|
163
258
|
if (typeof opts.renderPageStream === 'function') {
|
|
259
|
+
/** @type {unknown} */
|
|
260
|
+
let body;
|
|
261
|
+
if (verb === 'POST') {
|
|
262
|
+
const ctype = String(req.headers['content-type'] || '');
|
|
263
|
+
if (ctype.includes('application/json')) {
|
|
264
|
+
body = await readJson(req);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const raw = await readRawBody(req);
|
|
268
|
+
body = parseFormBody(raw, ctype);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
164
271
|
const rendered = await opts.renderPageStream(url.pathname, {
|
|
165
272
|
signal: ac.signal,
|
|
166
273
|
searchParams: url.searchParams,
|
|
167
274
|
cookieHeader: String(req.headers.cookie || ''),
|
|
275
|
+
method: verb,
|
|
276
|
+
body,
|
|
168
277
|
});
|
|
169
278
|
if (rendered) {
|
|
279
|
+
if (typeof rendered === 'object' && rendered.redirect) {
|
|
280
|
+
const status = Number(rendered.status) || 302;
|
|
281
|
+
const headers = {
|
|
282
|
+
Location: String(rendered.redirect),
|
|
283
|
+
...(rendered.headers && typeof rendered.headers === 'object' ? rendered.headers : {}),
|
|
284
|
+
};
|
|
285
|
+
res.writeHead(status, headers);
|
|
286
|
+
res.end();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
170
289
|
const status = rendered && typeof rendered === 'object' && 'status' in rendered ? Number(rendered.status) || 200 : 200;
|
|
171
290
|
const stream = rendered && typeof rendered === 'object' && rendered.stream ? rendered.stream : rendered;
|
|
172
291
|
return await sendHtmlStream(res, status, stream, ac.signal);
|
|
173
292
|
}
|
|
174
293
|
}
|
|
175
|
-
else if (typeof opts.renderPage === 'function') {
|
|
294
|
+
else if (verb === 'GET' && typeof opts.renderPage === 'function') {
|
|
176
295
|
const html = await opts.renderPage(url.pathname);
|
|
177
296
|
if (html != null) {
|
|
178
297
|
return sendHtml(res, 200, html);
|
|
179
298
|
}
|
|
180
299
|
}
|
|
181
|
-
else if (url.pathname === '/' || url.pathname === '/index.html') {
|
|
300
|
+
else if (verb === 'GET' && (url.pathname === '/' || url.pathname === '/index.html')) {
|
|
182
301
|
// Legacy index-only SSR (pre multi-page file routes).
|
|
183
302
|
if (typeof opts.renderIndexStream === 'function') {
|
|
184
303
|
return await sendHtmlStream(res, 200, opts.renderIndexStream({ signal: ac.signal }), ac.signal);
|
|
@@ -189,8 +308,7 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
189
308
|
}
|
|
190
309
|
}
|
|
191
310
|
finally {
|
|
192
|
-
req.off('
|
|
193
|
-
req.off('aborted', onClose);
|
|
311
|
+
req.off('aborted', onClientGone);
|
|
194
312
|
}
|
|
195
313
|
}
|
|
196
314
|
if (!res.headersSent) {
|
|
@@ -213,6 +331,43 @@ export async function handleNodeRequest(req, res, opts = {}) {
|
|
|
213
331
|
});
|
|
214
332
|
}
|
|
215
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* @param {import('node:http').IncomingMessage} req
|
|
336
|
+
* @param {URL} url
|
|
337
|
+
* @returns {Promise<Request>}
|
|
338
|
+
*/
|
|
339
|
+
async function incomingToRequest(req, url) {
|
|
340
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
341
|
+
/** @type {HeadersInit} */
|
|
342
|
+
const headers = {};
|
|
343
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
344
|
+
if (v == null)
|
|
345
|
+
continue;
|
|
346
|
+
headers[k] = Array.isArray(v) ? v.join(', ') : String(v);
|
|
347
|
+
}
|
|
348
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
349
|
+
return new Request(url, { method, headers });
|
|
350
|
+
}
|
|
351
|
+
const raw = await readRawBody(req);
|
|
352
|
+
// Node undici requires duplex when constructing Request with a body.
|
|
353
|
+
return new Request(url, { method, headers, body: raw, duplex: 'half' });
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* @param {import('node:http').ServerResponse} res
|
|
357
|
+
* @param {Response} response
|
|
358
|
+
*/
|
|
359
|
+
async function writeFetchResponse(res, response) {
|
|
360
|
+
const headers = {};
|
|
361
|
+
response.headers.forEach((value, key) => {
|
|
362
|
+
headers[key] = value;
|
|
363
|
+
});
|
|
364
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
365
|
+
if (!headers['content-length'] && !headers['Content-Length']) {
|
|
366
|
+
headers['content-length'] = String(buf.byteLength);
|
|
367
|
+
}
|
|
368
|
+
res.writeHead(response.status, headers);
|
|
369
|
+
res.end(buf);
|
|
370
|
+
}
|
|
216
371
|
/**
|
|
217
372
|
* Resolve a path under distDir; reject `..` escapes.
|
|
218
373
|
* @param {string} distDir
|
|
@@ -234,6 +389,25 @@ function safeDistFile(distDir, pathname, nodePath) {
|
|
|
234
389
|
return null;
|
|
235
390
|
return full;
|
|
236
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* web-static emits per-route HTML beside client assets. That HTML is for CDN / local-static
|
|
394
|
+
* delivery hosts — not for Server Host when SSR is available. DocumentMount stays static.
|
|
395
|
+
* @param {string} file
|
|
396
|
+
* @param {string} pathname
|
|
397
|
+
* @param {typeof import('node:path')} nodePath
|
|
398
|
+
*/
|
|
399
|
+
function isWebStaticHtmlShadow(file, pathname, nodePath) {
|
|
400
|
+
const ext = nodePath.extname(file).toLowerCase();
|
|
401
|
+
if (ext !== '.html' && ext !== '.htm')
|
|
402
|
+
return false;
|
|
403
|
+
let rel = decodeURIComponent(String(pathname || '').split('?')[0] || '/');
|
|
404
|
+
if (!rel.startsWith('/'))
|
|
405
|
+
rel = `/${rel}`;
|
|
406
|
+
// Integrated DocumentMount — keep static-first (see resolveDistStatic comment).
|
|
407
|
+
if (rel === '/d' || rel.startsWith('/d/'))
|
|
408
|
+
return false;
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
237
411
|
/**
|
|
238
412
|
* Static resolve for app assets + integrated document mounts:
|
|
239
413
|
* `/d/` → `d/index.html`, `/d/zh-hans/guide` → `d/zh-hans/guide.html`.
|
|
@@ -347,6 +521,126 @@ function readJson(req) {
|
|
|
347
521
|
req.on('error', reject);
|
|
348
522
|
});
|
|
349
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* @param {import('node:http').IncomingMessage} req
|
|
526
|
+
* @returns {Promise<Buffer>}
|
|
527
|
+
*/
|
|
528
|
+
function readRawBody(req) {
|
|
529
|
+
return new Promise((resolve, reject) => {
|
|
530
|
+
const chunks = [];
|
|
531
|
+
req.on('data', (c) => chunks.push(c));
|
|
532
|
+
// Buffer — never utf8-string: multipart File bytes must survive (Upload binary gate).
|
|
533
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
534
|
+
req.on('error', reject);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Buffer-safe multipart/form-data parser (file parts stay binary).
|
|
539
|
+
* @param {Buffer} buf
|
|
540
|
+
* @param {string} contentType
|
|
541
|
+
* @returns {Record<string, unknown>}
|
|
542
|
+
*/
|
|
543
|
+
function parseMultipartBuffer(buf, contentType) {
|
|
544
|
+
const bm = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(String(contentType || ''));
|
|
545
|
+
const boundary = bm ? bm[1] || bm[2] : '';
|
|
546
|
+
if (!boundary) {
|
|
547
|
+
throw new Error('multipart: missing boundary');
|
|
548
|
+
}
|
|
549
|
+
const sep = Buffer.from(`--${boundary}`);
|
|
550
|
+
/** @type {Record<string, unknown>} */
|
|
551
|
+
const out = {};
|
|
552
|
+
let start = indexOfBuffer(buf, sep, 0);
|
|
553
|
+
if (start < 0)
|
|
554
|
+
return out;
|
|
555
|
+
start += sep.length;
|
|
556
|
+
// Optional leading CRLF after first boundary is handled per-part.
|
|
557
|
+
while (start < buf.length) {
|
|
558
|
+
if (buf[start] === 0x2d && buf[start + 1] === 0x2d)
|
|
559
|
+
break; // trailing --
|
|
560
|
+
if (buf[start] === 0x0d && buf[start + 1] === 0x0a)
|
|
561
|
+
start += 2;
|
|
562
|
+
const next = indexOfBuffer(buf, sep, start);
|
|
563
|
+
const end = next < 0 ? buf.length : next;
|
|
564
|
+
let part = buf.subarray(start, end);
|
|
565
|
+
// Trim trailing CRLF before boundary.
|
|
566
|
+
if (part.length >= 2 && part[part.length - 2] === 0x0d && part[part.length - 1] === 0x0a) {
|
|
567
|
+
part = part.subarray(0, part.length - 2);
|
|
568
|
+
}
|
|
569
|
+
const splitAt = indexOfBuffer(part, Buffer.from('\r\n\r\n'), 0);
|
|
570
|
+
if (splitAt >= 0) {
|
|
571
|
+
const headerText = part.subarray(0, splitAt).toString('utf8');
|
|
572
|
+
let body = part.subarray(splitAt + 4);
|
|
573
|
+
const nameM = /content-disposition:[^\r\n]*;\s*name="([^"]*)"/i.exec(headerText);
|
|
574
|
+
const fileM = /content-disposition:[^\r\n]*;\s*filename="([^"]*)"/i.exec(headerText);
|
|
575
|
+
const typeM = /content-type:\s*([^\r\n]+)/i.exec(headerText);
|
|
576
|
+
const key = nameM ? nameM[1] : '';
|
|
577
|
+
if (key) {
|
|
578
|
+
if (fileM) {
|
|
579
|
+
const filename = fileM[1] || 'upload.bin';
|
|
580
|
+
const type = typeM ? String(typeM[1]).trim() : 'application/octet-stream';
|
|
581
|
+
// Copy body — File may outlive the request buffer.
|
|
582
|
+
const copy = Buffer.from(body);
|
|
583
|
+
const file = new File([copy], filename, { type });
|
|
584
|
+
const prev = out[key];
|
|
585
|
+
if (prev == null) {
|
|
586
|
+
out[key] = file;
|
|
587
|
+
}
|
|
588
|
+
else if (Array.isArray(prev)) {
|
|
589
|
+
prev.push(file);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
out[key] = [prev, file];
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
out[key] = body.toString('utf8');
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (next < 0)
|
|
601
|
+
break;
|
|
602
|
+
start = next + sep.length;
|
|
603
|
+
}
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* @param {Buffer} hay
|
|
608
|
+
* @param {Buffer} needle
|
|
609
|
+
* @param {number} from
|
|
610
|
+
*/
|
|
611
|
+
function indexOfBuffer(hay, needle, from) {
|
|
612
|
+
if (!needle.length)
|
|
613
|
+
return from;
|
|
614
|
+
outer: for (let i = Math.max(0, from); i <= hay.length - needle.length; i++) {
|
|
615
|
+
for (let j = 0; j < needle.length; j++) {
|
|
616
|
+
if (hay[i + j] !== needle[j])
|
|
617
|
+
continue outer;
|
|
618
|
+
}
|
|
619
|
+
return i;
|
|
620
|
+
}
|
|
621
|
+
return -1;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* @param {string} raw
|
|
625
|
+
* @param {string} contentType
|
|
626
|
+
* @returns {Record<string, string> | string}
|
|
627
|
+
*/
|
|
628
|
+
function parseFormBody(raw, contentType) {
|
|
629
|
+
if (!raw)
|
|
630
|
+
return {};
|
|
631
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
632
|
+
const out = {};
|
|
633
|
+
for (const [k, v] of new URLSearchParams(raw))
|
|
634
|
+
out[k] = v;
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
return JSON.parse(raw);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return { raw };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
350
644
|
/**
|
|
351
645
|
* @param {import('node:http').ServerResponse} res
|
|
352
646
|
* @param {number} status
|
package/dist/vmz-dom.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
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"
|
|
@@ -19,6 +23,10 @@
|
|
|
19
23
|
"./http": {
|
|
20
24
|
"types": "./dist/http.d.ts",
|
|
21
25
|
"default": "./dist/http.js"
|
|
26
|
+
},
|
|
27
|
+
"./client-nav": {
|
|
28
|
+
"types": "./dist/client-nav.d.ts",
|
|
29
|
+
"default": "./dist/client-nav.js"
|
|
22
30
|
}
|
|
23
31
|
},
|
|
24
32
|
"files": [
|
|
@@ -34,6 +42,9 @@
|
|
|
34
42
|
"scripts": {
|
|
35
43
|
"build": "tsc -p tsconfig.json && node ../../../scripts/build/copy-serve-host-mjs.mjs"
|
|
36
44
|
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"linkedom": "^0.18.13"
|
|
47
|
+
},
|
|
37
48
|
"publishConfig": {
|
|
38
49
|
"access": "public"
|
|
39
50
|
},
|