@vmz/core 0.0.1 → 0.0.2
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/README.md +46 -1
- package/dist/dom.d.ts +206 -0
- package/dist/dom.js +3067 -0
- package/dist/http.d.ts +6 -0
- package/dist/http.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/serve-host.d.ts +1 -0
- package/dist/serve-host.js +736 -0
- package/dist/serve-host.mjs +736 -0
- package/dist/server.d.ts +39 -0
- package/dist/server.js +437 -0
- package/package.json +34 -4
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal VMZ runtime — `#server` invoke + RPC/REST HTTP + optional static/SSR.
|
|
3
|
+
*
|
|
4
|
+
* Design: 规划设计/vmz/08-虚拟server.md, 07-REST接口.md
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe: no static `node:*` imports. Node builtins are loaded only inside
|
|
7
|
+
* Node/SSR request handlers so client bundles can `import { callServer }`.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Map `#server/foo` → filesystem / URL the host can `import()`.
|
|
11
|
+
* Only set this in Node/SSR hosts — browser bundles must omit it so RPC goes HTTP.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setServerModuleResolver(fn: any): void;
|
|
14
|
+
/** @param {Route[]} next */
|
|
15
|
+
export declare function setRoutes(next: any): void;
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} moduleId
|
|
18
|
+
* @param {string} method
|
|
19
|
+
* @param {unknown[]} args
|
|
20
|
+
*/
|
|
21
|
+
export declare function callServer(moduleId: any, method: any, args?: any[]): Promise<any>;
|
|
22
|
+
/**
|
|
23
|
+
* @param {RpcRequest} body
|
|
24
|
+
*/
|
|
25
|
+
export declare function handleRpc(body: any): Promise<any>;
|
|
26
|
+
/**
|
|
27
|
+
* Match REST route from `vmz-routes.json`.
|
|
28
|
+
* @param {string} verb
|
|
29
|
+
* @param {string} pathname
|
|
30
|
+
* @returns {Route | null}
|
|
31
|
+
*/
|
|
32
|
+
export declare function matchRoute(verb: any, pathname: any): any;
|
|
33
|
+
/**
|
|
34
|
+
* Node `http.createServer` listener: RPC + REST + optional static / SSR index.
|
|
35
|
+
* @param {import('node:http').IncomingMessage} req
|
|
36
|
+
* @param {import('node:http').ServerResponse} res
|
|
37
|
+
* @param {NodeRequestOptions} [opts]
|
|
38
|
+
*/
|
|
39
|
+
export declare function handleNodeRequest(req: any, res: any, opts?: {}): Promise<void>;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Minimal VMZ runtime — `#server` invoke + RPC/REST HTTP + optional static/SSR.
|
|
4
|
+
*
|
|
5
|
+
* Design: 规划设计/vmz/08-虚拟server.md, 07-REST接口.md
|
|
6
|
+
*
|
|
7
|
+
* Browser-safe: no static `node:*` imports. Node builtins are loaded only inside
|
|
8
|
+
* Node/SSR request handlers so client bundles can `import { callServer }`.
|
|
9
|
+
*/
|
|
10
|
+
/** @typedef {{ moduleId: string, method: string, args?: unknown[] }} RpcRequest */
|
|
11
|
+
/** @typedef {{ verb: string, path: string, moduleId: string, method: string, className?: string }} Route */
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{
|
|
14
|
+
* distDir?: string,
|
|
15
|
+
* renderIndex?: () => Promise<string> | string,
|
|
16
|
+
* renderIndexStream?: (opts?: { signal?: AbortSignal }) => AsyncIterable<string>,
|
|
17
|
+
* renderPage?: (pathname: string) => Promise<string | null> | string | null,
|
|
18
|
+
* renderPageStream?: (pathname: string, opts?: { signal?: AbortSignal, searchParams?: URLSearchParams, cookieHeader?: string }) => Promise<AsyncIterable<string> | null> | AsyncIterable<string> | null,
|
|
19
|
+
* req?: import('node:http').IncomingMessage,
|
|
20
|
+
* }} NodeRequestOptions
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_RPC_PATH = '/__vmz/rpc';
|
|
23
|
+
/** @type {((id: string) => string | URL) | null} */
|
|
24
|
+
let resolveServerModule = null;
|
|
25
|
+
/** @type {Route[]} */
|
|
26
|
+
let routes = [];
|
|
27
|
+
/**
|
|
28
|
+
* Map `#server/foo` → filesystem / URL the host can `import()`.
|
|
29
|
+
* Only set this in Node/SSR hosts — browser bundles must omit it so RPC goes HTTP.
|
|
30
|
+
*/
|
|
31
|
+
export function setServerModuleResolver(fn) {
|
|
32
|
+
resolveServerModule = fn;
|
|
33
|
+
}
|
|
34
|
+
/** @param {Route[]} next */
|
|
35
|
+
export function setRoutes(next) {
|
|
36
|
+
routes = Array.isArray(next) ? next : [];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} moduleId
|
|
40
|
+
* @param {string} method
|
|
41
|
+
* @param {unknown[]} args
|
|
42
|
+
*/
|
|
43
|
+
export async function callServer(moduleId, method, args = []) {
|
|
44
|
+
// Browser (or tests forcing HTTP): never touch server modules in-process.
|
|
45
|
+
// Node SSR / smokes set a resolver and leave __VMZ_USE_HTTP_RPC unset → local.
|
|
46
|
+
if (globalThis.__VMZ_USE_HTTP_RPC || !resolveServerModule) {
|
|
47
|
+
return callServerHttp(moduleId, method, args);
|
|
48
|
+
}
|
|
49
|
+
return callServerLocal(moduleId, method, args);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} moduleId
|
|
53
|
+
* @param {string} method
|
|
54
|
+
* @param {unknown[]} args
|
|
55
|
+
*/
|
|
56
|
+
async function callServerLocal(moduleId, method, args) {
|
|
57
|
+
if (!resolveServerModule) {
|
|
58
|
+
throw new Error(`vmz:runtime callServer(${moduleId}): setServerModuleResolver() required in Node`);
|
|
59
|
+
}
|
|
60
|
+
const spec = resolveServerModule(moduleId);
|
|
61
|
+
const mod = await import(spec);
|
|
62
|
+
const Ctor = mod.default ?? mod[exportGuess(moduleId)];
|
|
63
|
+
if (typeof Ctor !== 'function') {
|
|
64
|
+
throw new Error(`vmz:runtime: no class export for ${moduleId}`);
|
|
65
|
+
}
|
|
66
|
+
const instance = new Ctor();
|
|
67
|
+
const fn = instance[method];
|
|
68
|
+
if (typeof fn !== 'function') {
|
|
69
|
+
throw new Error(`vmz:runtime: ${moduleId}.${method} is not a function`);
|
|
70
|
+
}
|
|
71
|
+
return fn.apply(instance, args);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} moduleId
|
|
75
|
+
* @param {string} method
|
|
76
|
+
* @param {unknown[]} args
|
|
77
|
+
*/
|
|
78
|
+
async function callServerHttp(moduleId, method, args) {
|
|
79
|
+
const rpcPath = (typeof globalThis !== 'undefined' && globalThis.__VMZ_RPC_PATH) || DEFAULT_RPC_PATH;
|
|
80
|
+
const res = await fetch(rpcPath, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'content-type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ moduleId, method, args }),
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
throw new Error(`vmz:runtime RPC ${res.status} for ${moduleId}.${method}`);
|
|
87
|
+
}
|
|
88
|
+
return res.json();
|
|
89
|
+
}
|
|
90
|
+
function exportGuess(moduleId) {
|
|
91
|
+
const base = moduleId.split('/').pop() || 'Server';
|
|
92
|
+
return `${base}Server`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* @param {RpcRequest} body
|
|
96
|
+
*/
|
|
97
|
+
export async function handleRpc(body) {
|
|
98
|
+
return callServerLocal(body.moduleId, body.method, body.args ?? []);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Match REST route from `vmz-routes.json`.
|
|
102
|
+
* @param {string} verb
|
|
103
|
+
* @param {string} pathname
|
|
104
|
+
* @returns {Route | null}
|
|
105
|
+
*/
|
|
106
|
+
export function matchRoute(verb, pathname) {
|
|
107
|
+
const v = verb.toUpperCase();
|
|
108
|
+
return routes.find((r) => r.verb.toUpperCase() === v && r.path === pathname) ?? null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Node `http.createServer` listener: RPC + REST + optional static / SSR index.
|
|
112
|
+
* @param {import('node:http').IncomingMessage} req
|
|
113
|
+
* @param {import('node:http').ServerResponse} res
|
|
114
|
+
* @param {NodeRequestOptions} [opts]
|
|
115
|
+
*/
|
|
116
|
+
export async function handleNodeRequest(req, res, opts = {}) {
|
|
117
|
+
const host = req.headers.host || '127.0.0.1';
|
|
118
|
+
const url = new URL(req.url || '/', `http://${host}`);
|
|
119
|
+
const verb = (req.method || 'GET').toUpperCase();
|
|
120
|
+
try {
|
|
121
|
+
if (verb === 'POST' && url.pathname === DEFAULT_RPC_PATH) {
|
|
122
|
+
const body = await readJson(req);
|
|
123
|
+
const result = await handleRpc(body);
|
|
124
|
+
return sendJson(res, 200, result);
|
|
125
|
+
}
|
|
126
|
+
const route = matchRoute(verb, url.pathname);
|
|
127
|
+
if (route) {
|
|
128
|
+
const result = await callServerLocal(route.moduleId, route.method, []);
|
|
129
|
+
return sendJson(res, 200, result);
|
|
130
|
+
}
|
|
131
|
+
// Static first (integrated DocumentMount + assets) so /d/… is not swallowed by SSR 404 shells.
|
|
132
|
+
if (verb === 'GET' && opts.distDir) {
|
|
133
|
+
const nodePath = await import('node:path');
|
|
134
|
+
const { readFile, stat } = await import('node:fs/promises');
|
|
135
|
+
const file = await resolveDistStatic(opts.distDir, url.pathname, nodePath, stat);
|
|
136
|
+
if (file) {
|
|
137
|
+
try {
|
|
138
|
+
const body = await readFile(file);
|
|
139
|
+
return sendBytes(res, 200, body, contentType(file, nodePath));
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (err && (err.code === 'ENOENT' || err.code === 'EISDIR')) {
|
|
143
|
+
/* fall through */
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (verb === 'GET' && (opts.renderPageStream || opts.renderPage || opts.renderIndexStream || opts.renderIndex)) {
|
|
152
|
+
const ac = new AbortController();
|
|
153
|
+
const onClose = () => {
|
|
154
|
+
try {
|
|
155
|
+
ac.abort();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
/* ignore */
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
req.on('close', onClose);
|
|
162
|
+
req.on('aborted', onClose);
|
|
163
|
+
try {
|
|
164
|
+
if (typeof opts.renderPageStream === 'function') {
|
|
165
|
+
const rendered = await opts.renderPageStream(url.pathname, {
|
|
166
|
+
signal: ac.signal,
|
|
167
|
+
searchParams: url.searchParams,
|
|
168
|
+
cookieHeader: String(req.headers.cookie || ''),
|
|
169
|
+
});
|
|
170
|
+
if (rendered) {
|
|
171
|
+
const status = rendered && typeof rendered === 'object' && 'status' in rendered ? Number(rendered.status) || 200 : 200;
|
|
172
|
+
const stream = rendered && typeof rendered === 'object' && rendered.stream ? rendered.stream : rendered;
|
|
173
|
+
return await sendHtmlStream(res, status, stream, ac.signal);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (typeof opts.renderPage === 'function') {
|
|
177
|
+
const html = await opts.renderPage(url.pathname);
|
|
178
|
+
if (html != null) {
|
|
179
|
+
return sendHtml(res, 200, html);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (url.pathname === '/' || url.pathname === '/index.html') {
|
|
183
|
+
// Legacy index-only SSR (pre multi-page file routes).
|
|
184
|
+
if (typeof opts.renderIndexStream === 'function') {
|
|
185
|
+
return await sendHtmlStream(res, 200, opts.renderIndexStream({ signal: ac.signal }), ac.signal);
|
|
186
|
+
}
|
|
187
|
+
if (typeof opts.renderIndex === 'function') {
|
|
188
|
+
return sendHtml(res, 200, await opts.renderIndex());
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
req.off('close', onClose);
|
|
194
|
+
req.off('aborted', onClose);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!res.headersSent) {
|
|
198
|
+
sendJson(res, 404, { error: 'not found', path: url.pathname });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (res.headersSent || res.writableEnded || res.destroyed) {
|
|
203
|
+
console.error('vmz serve: request failed after headers', err);
|
|
204
|
+
try {
|
|
205
|
+
res.destroy(err instanceof Error ? err : undefined);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
/* ignore */
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
sendJson(res, 500, {
|
|
213
|
+
error: err instanceof Error ? err.message : String(err),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Resolve a path under distDir; reject `..` escapes.
|
|
219
|
+
* @param {string} distDir
|
|
220
|
+
* @param {string} pathname
|
|
221
|
+
* @param {typeof import('node:path')} nodePath
|
|
222
|
+
* @returns {string | null}
|
|
223
|
+
*/
|
|
224
|
+
function safeDistFile(distDir, pathname, nodePath) {
|
|
225
|
+
let rel = decodeURIComponent(pathname.split('?')[0] || '/');
|
|
226
|
+
if (rel === '/' || rel === '')
|
|
227
|
+
return null;
|
|
228
|
+
if (rel.startsWith('/'))
|
|
229
|
+
rel = rel.slice(1);
|
|
230
|
+
if (!rel || rel.includes('\0'))
|
|
231
|
+
return null;
|
|
232
|
+
const root = nodePath.resolve(distDir);
|
|
233
|
+
const full = nodePath.resolve(root, rel);
|
|
234
|
+
if (full !== root && !full.startsWith(root + nodePath.sep))
|
|
235
|
+
return null;
|
|
236
|
+
return full;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Static resolve for app assets + integrated document mounts:
|
|
240
|
+
* `/d/` → `d/index.html`, `/d/zh-hans/guide` → `d/zh-hans/guide.html`.
|
|
241
|
+
* @param {string} distDir
|
|
242
|
+
* @param {string} pathname
|
|
243
|
+
* @param {typeof import('node:path')} nodePath
|
|
244
|
+
* @param {typeof import('node:fs/promises').stat} stat
|
|
245
|
+
* @returns {Promise<string | null>}
|
|
246
|
+
*/
|
|
247
|
+
async function resolveDistStatic(distDir, pathname, nodePath, stat) {
|
|
248
|
+
const candidates = staticPathCandidates(pathname);
|
|
249
|
+
for (const candidate of candidates) {
|
|
250
|
+
const full = safeDistFile(distDir, candidate, nodePath);
|
|
251
|
+
if (!full)
|
|
252
|
+
continue;
|
|
253
|
+
try {
|
|
254
|
+
const st = await stat(full);
|
|
255
|
+
if (st.isFile())
|
|
256
|
+
return full;
|
|
257
|
+
if (st.isDirectory()) {
|
|
258
|
+
const index = nodePath.join(full, 'index.html');
|
|
259
|
+
const indexSt = await stat(index).catch(() => null);
|
|
260
|
+
if (indexSt && indexSt.isFile())
|
|
261
|
+
return index;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
/* try next */
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* @param {string} pathname
|
|
272
|
+
* @returns {string[]}
|
|
273
|
+
*/
|
|
274
|
+
function staticPathCandidates(pathname) {
|
|
275
|
+
let rel = decodeURIComponent(pathname.split('?')[0] || '/');
|
|
276
|
+
if (!rel.startsWith('/'))
|
|
277
|
+
rel = `/${rel}`;
|
|
278
|
+
/** @type {string[]} */
|
|
279
|
+
const out = [];
|
|
280
|
+
const push = (p) => {
|
|
281
|
+
if (p && !out.includes(p))
|
|
282
|
+
out.push(p);
|
|
283
|
+
};
|
|
284
|
+
push(rel);
|
|
285
|
+
if (rel.endsWith('/')) {
|
|
286
|
+
push(`${rel}index.html`);
|
|
287
|
+
const trimmed = rel.replace(/\/+$/, '');
|
|
288
|
+
if (trimmed) {
|
|
289
|
+
push(`${trimmed}.html`);
|
|
290
|
+
push(trimmed);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
push(`${rel}/`);
|
|
295
|
+
push(`${rel}/index.html`);
|
|
296
|
+
if (!nodePathExt(rel)) {
|
|
297
|
+
push(`${rel}.html`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
/** @param {string} p */
|
|
303
|
+
function nodePathExt(p) {
|
|
304
|
+
const base = p.split('/').pop() || '';
|
|
305
|
+
const i = base.lastIndexOf('.');
|
|
306
|
+
return i > 0 ? base.slice(i) : '';
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* @param {string} filePath
|
|
310
|
+
* @param {typeof import('node:path')} nodePath
|
|
311
|
+
*/
|
|
312
|
+
function contentType(filePath, nodePath) {
|
|
313
|
+
const ext = nodePath.extname(filePath).toLowerCase();
|
|
314
|
+
switch (ext) {
|
|
315
|
+
case '.html':
|
|
316
|
+
return 'text/html; charset=utf-8';
|
|
317
|
+
case '.js':
|
|
318
|
+
case '.mjs':
|
|
319
|
+
return 'text/javascript; charset=utf-8';
|
|
320
|
+
case '.css':
|
|
321
|
+
return 'text/css; charset=utf-8';
|
|
322
|
+
case '.json':
|
|
323
|
+
return 'application/json; charset=utf-8';
|
|
324
|
+
case '.svg':
|
|
325
|
+
return 'image/svg+xml';
|
|
326
|
+
case '.map':
|
|
327
|
+
return 'application/json; charset=utf-8';
|
|
328
|
+
default:
|
|
329
|
+
return 'application/octet-stream';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* @param {import('node:http').IncomingMessage} req
|
|
334
|
+
*/
|
|
335
|
+
function readJson(req) {
|
|
336
|
+
return new Promise((resolve, reject) => {
|
|
337
|
+
const chunks = [];
|
|
338
|
+
req.on('data', (c) => chunks.push(c));
|
|
339
|
+
req.on('end', () => {
|
|
340
|
+
try {
|
|
341
|
+
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
342
|
+
resolve(JSON.parse(raw));
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
reject(e);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
req.on('error', reject);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* @param {import('node:http').ServerResponse} res
|
|
353
|
+
* @param {number} status
|
|
354
|
+
* @param {unknown} body
|
|
355
|
+
*/
|
|
356
|
+
function sendJson(res, status, body) {
|
|
357
|
+
const payload = JSON.stringify(body);
|
|
358
|
+
res.writeHead(status, {
|
|
359
|
+
'content-type': 'application/json; charset=utf-8',
|
|
360
|
+
'content-length': Buffer.byteLength(payload),
|
|
361
|
+
});
|
|
362
|
+
res.end(payload);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* @param {import('node:http').ServerResponse} res
|
|
366
|
+
* @param {number} status
|
|
367
|
+
* @param {string} html
|
|
368
|
+
*/
|
|
369
|
+
function sendHtml(res, status, html) {
|
|
370
|
+
const payload = typeof html === 'string' ? html : String(html);
|
|
371
|
+
res.writeHead(status, {
|
|
372
|
+
'content-type': 'text/html; charset=utf-8',
|
|
373
|
+
'content-length': Buffer.byteLength(payload),
|
|
374
|
+
});
|
|
375
|
+
res.end(payload);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Stream HTML without buffering the full document (event-flow / stream SSR host).
|
|
379
|
+
* Honors AbortSignal + response destroy for cancel; awaits drain for backpressure.
|
|
380
|
+
* @param {import('node:http').ServerResponse} res
|
|
381
|
+
* @param {number} status
|
|
382
|
+
* @param {AsyncIterable<string> | Iterable<string> | AsyncGenerator<string, any, any>} source
|
|
383
|
+
* @param {AbortSignal} [signal]
|
|
384
|
+
*/
|
|
385
|
+
async function sendHtmlStream(res, status, source, signal) {
|
|
386
|
+
res.writeHead(status, {
|
|
387
|
+
'content-type': 'text/html; charset=utf-8',
|
|
388
|
+
'transfer-encoding': 'chunked',
|
|
389
|
+
'cache-control': 'no-cache',
|
|
390
|
+
});
|
|
391
|
+
const aborted = () => Boolean(signal?.aborted || res.destroyed || res.writableEnded || !res.writable);
|
|
392
|
+
try {
|
|
393
|
+
for await (const chunk of source) {
|
|
394
|
+
if (aborted())
|
|
395
|
+
break;
|
|
396
|
+
if (chunk == null || chunk === '')
|
|
397
|
+
continue;
|
|
398
|
+
const s = typeof chunk === 'string' ? chunk : String(chunk);
|
|
399
|
+
const ok = res.write(s);
|
|
400
|
+
if (!ok) {
|
|
401
|
+
await Promise.race([
|
|
402
|
+
new Promise((resolve) => res.once('drain', resolve)),
|
|
403
|
+
new Promise((resolve) => {
|
|
404
|
+
if (!signal)
|
|
405
|
+
return;
|
|
406
|
+
if (signal.aborted)
|
|
407
|
+
return resolve();
|
|
408
|
+
signal.addEventListener('abort', () => resolve(), { once: true });
|
|
409
|
+
}),
|
|
410
|
+
new Promise((resolve) => res.once('close', resolve)),
|
|
411
|
+
]);
|
|
412
|
+
if (aborted())
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
if (!aborted())
|
|
419
|
+
throw err;
|
|
420
|
+
}
|
|
421
|
+
if (!res.writableEnded && !res.destroyed) {
|
|
422
|
+
res.end();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* @param {import('node:http').ServerResponse} res
|
|
427
|
+
* @param {number} status
|
|
428
|
+
* @param {Buffer} body
|
|
429
|
+
* @param {string} type
|
|
430
|
+
*/
|
|
431
|
+
function sendBytes(res, status, body, type) {
|
|
432
|
+
res.writeHead(status, {
|
|
433
|
+
'content-type': type,
|
|
434
|
+
'content-length': body.byteLength,
|
|
435
|
+
});
|
|
436
|
+
res.end(body);
|
|
437
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vmz/core",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "VMZ production runtime core — DOM / SSR / HTTP / WriteBarrier (no compiler)",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"default": "./dist/server.js"
|
|
10
|
+
},
|
|
11
|
+
"./dom": {
|
|
12
|
+
"types": "./dist/dom.d.ts",
|
|
13
|
+
"default": "./dist/dom.js"
|
|
14
|
+
},
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./dist/server.d.ts",
|
|
17
|
+
"default": "./dist/server.js"
|
|
18
|
+
},
|
|
19
|
+
"./http": {
|
|
20
|
+
"types": "./dist/http.d.ts",
|
|
21
|
+
"default": "./dist/http.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
7
24
|
"files": [
|
|
25
|
+
"dist",
|
|
8
26
|
"README.md"
|
|
9
27
|
],
|
|
28
|
+
"keywords": [
|
|
29
|
+
"vmz",
|
|
30
|
+
"runtime"
|
|
31
|
+
],
|
|
32
|
+
"main": "./dist/server.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json && node ../../../scripts/copy-serve-host-mjs.mjs"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
10
40
|
"repository": {
|
|
11
41
|
"type": "git",
|
|
12
42
|
"url": "git+https://github.com/doki-land/vmz-framework.git"
|