@seanhogg/builderforce-webcontainers-core 2026.10.7 → 2026.10.8
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/archive.d.ts +2 -0
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +6 -4
- package/dist/archive.js.map +1 -1
- package/dist/html.d.ts +2 -0
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +5 -3
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/node/http/client.d.ts +3 -1
- package/dist/node/http/client.d.ts.map +1 -1
- package/dist/node/http/client.js +74 -5
- package/dist/node/http/client.js.map +1 -1
- package/dist/node/http/index.d.ts.map +1 -1
- package/dist/node/http/index.js +2 -1
- package/dist/node/http/index.js.map +1 -1
- package/dist/node/http/messages.d.ts +87 -0
- package/dist/node/http/messages.d.ts.map +1 -0
- package/dist/node/http/messages.js +146 -0
- package/dist/node/http/messages.js.map +1 -0
- package/dist/node/http/server.d.ts +13 -98
- package/dist/node/http/server.d.ts.map +1 -1
- package/dist/node/http/server.js +34 -202
- package/dist/node/http/server.js.map +1 -1
- package/dist/node/http/serverResponse.d.ts +42 -0
- package/dist/node/http/serverResponse.d.ts.map +1 -0
- package/dist/node/http/serverResponse.js +89 -0
- package/dist/node/http/serverResponse.js.map +1 -0
- package/dist/node/http/upgradeSocket.d.ts +34 -0
- package/dist/node/http/upgradeSocket.d.ts.map +1 -0
- package/dist/node/http/upgradeSocket.js +73 -0
- package/dist/node/http/upgradeSocket.js.map +1 -0
- package/dist/node/runtime.js +2 -2
- package/dist/node/runtime.js.map +1 -1
- package/dist/previewBridge.d.ts +42 -0
- package/dist/previewBridge.d.ts.map +1 -0
- package/dist/previewBridge.js +192 -0
- package/dist/previewBridge.js.map +1 -0
- package/dist/shell/ast.d.ts +101 -0
- package/dist/shell/ast.d.ts.map +1 -0
- package/dist/shell/ast.js +7 -0
- package/dist/shell/ast.js.map +1 -0
- package/dist/shell/builtins.d.ts +14 -0
- package/dist/shell/builtins.d.ts.map +1 -1
- package/dist/shell/builtins.js +87 -7
- package/dist/shell/builtins.js.map +1 -1
- package/dist/shell/compound.d.ts +30 -0
- package/dist/shell/compound.d.ts.map +1 -0
- package/dist/shell/compound.js +73 -0
- package/dist/shell/compound.js.map +1 -0
- package/dist/shell/exec.d.ts +27 -6
- package/dist/shell/exec.d.ts.map +1 -1
- package/dist/shell/exec.js +127 -41
- package/dist/shell/exec.js.map +1 -1
- package/dist/shell/expand.d.ts +5 -1
- package/dist/shell/expand.d.ts.map +1 -1
- package/dist/shell/expand.js +7 -5
- package/dist/shell/expand.js.map +1 -1
- package/dist/shell/lineReader.d.ts +9 -0
- package/dist/shell/lineReader.d.ts.map +1 -0
- package/dist/shell/lineReader.js +42 -0
- package/dist/shell/lineReader.js.map +1 -0
- package/dist/shell/packageManagers.d.ts +20 -0
- package/dist/shell/packageManagers.d.ts.map +1 -0
- package/dist/shell/packageManagers.js +74 -0
- package/dist/shell/packageManagers.js.map +1 -0
- package/dist/shell/parse.d.ts +16 -66
- package/dist/shell/parse.d.ts.map +1 -1
- package/dist/shell/parse.js +244 -243
- package/dist/shell/parse.js.map +1 -1
- package/dist/shell/program.d.ts.map +1 -1
- package/dist/shell/program.js +2 -1
- package/dist/shell/program.js.map +1 -1
- package/dist/shell/testCommand.d.ts +9 -0
- package/dist/shell/testCommand.d.ts.map +1 -0
- package/dist/shell/testCommand.js +105 -0
- package/dist/shell/testCommand.js.map +1 -0
- package/dist/shell/tokenize.d.ts +25 -0
- package/dist/shell/tokenize.d.ts.map +1 -0
- package/dist/shell/tokenize.js +279 -0
- package/dist/shell/tokenize.js.map +1 -0
- package/dist/system/cookieJar.d.ts +59 -0
- package/dist/system/cookieJar.d.ts.map +1 -0
- package/dist/system/cookieJar.js +187 -0
- package/dist/system/cookieJar.js.map +1 -0
- package/dist/system/httpStream.d.ts +40 -0
- package/dist/system/httpStream.d.ts.map +1 -0
- package/dist/system/httpStream.js +156 -0
- package/dist/system/httpStream.js.map +1 -0
- package/dist/system/index.d.ts +9 -1
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +4 -0
- package/dist/system/index.js.map +1 -1
- package/dist/system/kernel.d.ts +10 -2
- package/dist/system/kernel.d.ts.map +1 -1
- package/dist/system/kernel.js +12 -3
- package/dist/system/kernel.js.map +1 -1
- package/dist/system/ports.d.ts +9 -4
- package/dist/system/ports.d.ts.map +1 -1
- package/dist/system/ports.js +21 -2
- package/dist/system/ports.js.map +1 -1
- package/dist/system/programs.d.ts +1 -1
- package/dist/system/programs.d.ts.map +1 -1
- package/dist/system/programs.js +2 -0
- package/dist/system/programs.js.map +1 -1
- package/dist/system/types.d.ts +36 -6
- package/dist/system/types.d.ts.map +1 -1
- package/dist/system/webSocketClient.d.ts +37 -0
- package/dist/system/webSocketClient.d.ts.map +1 -0
- package/dist/system/webSocketClient.js +152 -0
- package/dist/system/webSocketClient.js.map +1 -0
- package/dist/system/webSocketFrames.d.ts +39 -0
- package/dist/system/webSocketFrames.d.ts.map +1 -0
- package/dist/system/webSocketFrames.js +117 -0
- package/dist/system/webSocketFrames.js.map +1 -0
- package/package.json +1 -1
- package/src/archive.ts +8 -4
- package/src/html.ts +5 -3
- package/src/index.ts +2 -1
- package/src/node/http/client.ts +69 -6
- package/src/node/http/index.ts +2 -1
- package/src/node/http/messages.ts +150 -0
- package/src/node/http/server.ts +37 -202
- package/src/node/http/serverResponse.ts +98 -0
- package/src/node/http/upgradeSocket.ts +79 -0
- package/src/node/runtime.ts +2 -2
- package/src/previewBridge.ts +194 -0
- package/src/shell/ast.ts +85 -0
- package/src/shell/builtins.ts +97 -7
- package/src/shell/compound.ts +89 -0
- package/src/shell/exec.ts +128 -40
- package/src/shell/expand.ts +8 -6
- package/src/shell/lineReader.ts +46 -0
- package/src/shell/packageManagers.ts +75 -0
- package/src/shell/parse.ts +233 -236
- package/src/shell/program.ts +2 -1
- package/src/shell/testCommand.ts +105 -0
- package/src/shell/tokenize.ts +257 -0
- package/src/system/cookieJar.ts +187 -0
- package/src/system/httpStream.ts +168 -0
- package/src/system/index.ts +24 -1
- package/src/system/kernel.ts +16 -4
- package/src/system/ports.ts +23 -4
- package/src/system/programs.ts +3 -1
- package/src/system/types.ts +40 -6
- package/src/system/webSocketClient.ts +177 -0
- package/src/system/webSocketFrames.ts +118 -0
package/src/html.ts
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* • CRA's `%PUBLIC_URL%` placeholder is filled;
|
|
6
6
|
* • a page with no module script (CRA) gets its entry injected;
|
|
7
7
|
* • the error bridge is prepended, so runtime errors and console output reach the host;
|
|
8
|
+
* • the WebSocket bridge follows, so `new WebSocket('ws://localhost:3000')` reaches a virtual server;
|
|
8
9
|
* • the host's preview scripts run next, before the app;
|
|
9
10
|
* • the live-reload listener is prepended, so an edit re-renders the preview;
|
|
10
11
|
* • the attribution badge is appended unless the host turned it off.
|
|
11
12
|
*/
|
|
12
13
|
import { injectAttribution } from './attribution.js';
|
|
14
|
+
import { previewSocketScript } from './previewBridge.js';
|
|
13
15
|
|
|
14
16
|
export const ERROR_MESSAGE_TYPE = 'bfwc:error';
|
|
15
17
|
export const CONSOLE_MESSAGE_TYPE = 'bfwc:console';
|
|
@@ -81,8 +83,8 @@ export interface HtmlOptions {
|
|
|
81
83
|
scripts?: readonly string[];
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
/** `</script>` inside
|
|
85
|
-
const scriptTag = (source: string) => `<script>${source.replace(/<\/script/gi, '<\\/script')}</script>`;
|
|
86
|
+
/** A host script as a tag. `</script>` inside the body would end the tag early, so it is escaped. */
|
|
87
|
+
export const scriptTag = (source: string): string => `<script>${source.replace(/<\/script/gi, '<\\/script')}</script>`;
|
|
86
88
|
|
|
87
89
|
export function transformHtml(html: string, options: HtmlOptions): string {
|
|
88
90
|
const { base } = options;
|
|
@@ -97,7 +99,7 @@ export function transformHtml(html: string, options: HtmlOptions): string {
|
|
|
97
99
|
}
|
|
98
100
|
if (options.attribution !== false) out = injectAttribution(out);
|
|
99
101
|
|
|
100
|
-
const bridge = [errorBridgeScript(), liveReloadScript(base), ...(options.scripts ?? []).map(scriptTag)].join('\n');
|
|
102
|
+
const bridge = [errorBridgeScript(), previewSocketScript(), liveReloadScript(base), ...(options.scripts ?? []).map(scriptTag)].join('\n');
|
|
101
103
|
if (/<head[^>]*>/i.test(out)) return out.replace(/<head[^>]*>/i, (head) => `${head}\n${bridge}`);
|
|
102
104
|
return bridge + out;
|
|
103
105
|
}
|
package/src/index.ts
CHANGED
|
@@ -44,7 +44,8 @@ export { rewriteImports } from './rewriteImports.js';
|
|
|
44
44
|
export { resolveLocal, resolveAlias, isBareSpecifier, splitPackageSpecifier, resolvePackage, RESOLVE_EXTENSIONS } from './resolve.js';
|
|
45
45
|
export { compileScript, compileModule, isScriptPath, mapSpecifier, moduleUrl, buildDefine } from './compileScript.js';
|
|
46
46
|
export { compileComponent } from './compileComponent.js';
|
|
47
|
-
export { transformHtml, errorBridgeScript, liveReloadScript, reloadChannelName, ERROR_MESSAGE_TYPE, CONSOLE_MESSAGE_TYPE, CONSOLE_MESSAGE_LIMIT } from './html.js';
|
|
47
|
+
export { transformHtml, scriptTag, errorBridgeScript, liveReloadScript, reloadChannelName, ERROR_MESSAGE_TYPE, CONSOLE_MESSAGE_TYPE, CONSOLE_MESSAGE_LIMIT } from './html.js';
|
|
48
|
+
export { previewSocketScript, previewCookieScript, bridgeInsertionPoint, SOCKET_MESSAGE_TYPE, COOKIE_MESSAGE_TYPE, SOCKET_CONNECT_TIMEOUT_MS } from './previewBridge.js';
|
|
48
49
|
export { mimeFor } from './mime.js';
|
|
49
50
|
export { attributionScript, injectAttribution, ATTRIBUTION_URL, ATTRIBUTION_LABEL } from './attribution.js';
|
|
50
51
|
export type { AttributionOptions } from './attribution.js';
|
package/src/node/http/client.ts
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* the `fetch` global a program sees. Requests to localhost reach this runtime's
|
|
4
4
|
* own virtual servers (`loopback`); everything else goes through the browser's
|
|
5
5
|
* fetch, so CORS applies — a public API without CORS headers is unreachable
|
|
6
|
-
* from a browser page, and no shim can change that.
|
|
6
|
+
* from a browser page, and no shim can change that. Response bodies stream:
|
|
7
|
+
* `res.on('data')` and `response.body` see chunks as the server writes them.
|
|
7
8
|
*/
|
|
8
9
|
import { Buffer } from '../buffer.js';
|
|
9
10
|
import type { EventLoop } from '../loop.js';
|
|
10
11
|
import type { HttpRequestData, System } from '../../system/types.js';
|
|
11
|
-
import { FakeSocket, IncomingMessage, OutgoingHeaders } from './
|
|
12
|
+
import { FakeSocket, IncomingMessage, OutgoingHeaders } from './messages.js';
|
|
12
13
|
|
|
13
14
|
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', '[::1]']);
|
|
14
15
|
/** Headers a browser refuses to let script set; dropping them beats a TypeError. */
|
|
@@ -70,7 +71,8 @@ export async function sendRequest(system: System, url: URL, request: HttpRequest
|
|
|
70
71
|
const headers = new Headers();
|
|
71
72
|
for (const [k, v] of Object.entries(reply.headers)) for (const item of Array.isArray(v) ? v : [v]) headers.append(k, String(item));
|
|
72
73
|
const empty = reply.status === 204 || reply.status === 304 || request.method === 'HEAD';
|
|
73
|
-
|
|
74
|
+
if (empty) void reply.body.cancel().catch(() => undefined);
|
|
75
|
+
return new Response(empty ? null : reply.body, { status: reply.status, statusText: reply.statusText, headers });
|
|
74
76
|
}
|
|
75
77
|
const headers: Record<string, string> = {};
|
|
76
78
|
for (const [k, v] of Object.entries(request.headers)) if (!FORBIDDEN.has(k.toLowerCase())) headers[k] = v;
|
|
@@ -92,7 +94,9 @@ export class ClientRequest extends OutgoingHeaders {
|
|
|
92
94
|
private readonly options: RequestOptions;
|
|
93
95
|
|
|
94
96
|
constructor(private readonly deps: ClientDeps, input: unknown, extra: unknown, callback: ((res: IncomingMessage) => void) | undefined, defaultProtocol: string) {
|
|
95
|
-
|
|
97
|
+
// Not auto-destroyed on 'finish' (as in Node): the request body is done long before the
|
|
98
|
+
// response streams in, and destroy() aborts the request — which would cut that response off.
|
|
99
|
+
super({ decodeStrings: true, autoDestroy: false });
|
|
96
100
|
if (typeof extra === 'function') [callback, extra] = [extra as typeof callback, undefined];
|
|
97
101
|
this.options = toOptions(input, extra, defaultProtocol);
|
|
98
102
|
this.method = this.options.method!;
|
|
@@ -131,6 +135,33 @@ export class ClientRequest extends OutgoingHeaders {
|
|
|
131
135
|
setNoDelay(): void {}
|
|
132
136
|
setSocketKeepAlive(): void {}
|
|
133
137
|
|
|
138
|
+
/** Push the body into `res` chunk by chunk; aborting the request hangs up on the server. */
|
|
139
|
+
private async streamBody(response: Response, res: IncomingMessage): Promise<void> {
|
|
140
|
+
const reader = response.body?.getReader();
|
|
141
|
+
if (reader) {
|
|
142
|
+
const stop = () => void reader.cancel().catch(() => undefined);
|
|
143
|
+
this.controller.signal.addEventListener('abort', stop);
|
|
144
|
+
try {
|
|
145
|
+
for (;;) {
|
|
146
|
+
const { value, done } = await reader.read();
|
|
147
|
+
if (done || this.controller.signal.aborted) break;
|
|
148
|
+
this.deps.loop._run(() => res.push(Buffer.from(value.buffer as ArrayBuffer, value.byteOffset, value.byteLength)));
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (this.controller.signal.aborted) return;
|
|
152
|
+
this.deps.loop._run(() => {
|
|
153
|
+
res.aborted = true;
|
|
154
|
+
res.destroy(error as Error);
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
} finally {
|
|
158
|
+
this.controller.signal.removeEventListener('abort', stop);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (this.controller.signal.aborted) return;
|
|
162
|
+
this.deps.loop._run(() => res._complete());
|
|
163
|
+
}
|
|
164
|
+
|
|
134
165
|
private async perform(): Promise<void> {
|
|
135
166
|
const release = this.deps.loop.hold();
|
|
136
167
|
try {
|
|
@@ -144,10 +175,11 @@ export class ClientRequest extends OutgoingHeaders {
|
|
|
144
175
|
const res = new IncomingMessage(this.socket);
|
|
145
176
|
res.statusCode = response.status;
|
|
146
177
|
res.statusMessage = response.statusText;
|
|
147
|
-
res.
|
|
178
|
+
res._setHeaders(headersToRecord(response.headers));
|
|
148
179
|
this.deps.loop._run(() => {
|
|
149
180
|
if (!this.emit('response', res)) res.resume();
|
|
150
181
|
});
|
|
182
|
+
await this.streamBody(response, res);
|
|
151
183
|
} catch (error) {
|
|
152
184
|
if (!this.aborted) this.deps.loop._run(() => this.emit('error', error));
|
|
153
185
|
} finally {
|
|
@@ -165,9 +197,40 @@ export function createFetch(deps: ClientDeps): typeof fetch {
|
|
|
165
197
|
const url = new URL(request.url);
|
|
166
198
|
if (!isLocalHost(url.hostname)) return await deps.system.fetch(url.href, init ?? (typeof input === 'object' && !(input instanceof URL) ? { method: request.method, headers: request.headers, body: request.body, signal: request.signal } : undefined));
|
|
167
199
|
const body = request.method === 'GET' || request.method === 'HEAD' ? null : new Uint8Array(await request.arrayBuffer());
|
|
168
|
-
|
|
200
|
+
const response = await sendRequest(deps.system, url, { method: request.method, url: url.pathname + url.search, headers: headersToRecord(request.headers), body });
|
|
201
|
+
return holdWhileReading(response, () => deps.loop.hold());
|
|
169
202
|
} finally {
|
|
170
203
|
release();
|
|
171
204
|
}
|
|
172
205
|
};
|
|
173
206
|
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A loopback body streams from a server that may live in another process, so
|
|
210
|
+
* nothing else keeps this one alive while the program awaits the next chunk:
|
|
211
|
+
* hold the loop for each pending read — and only then, so a body nobody reads
|
|
212
|
+
* does not keep the process running.
|
|
213
|
+
*/
|
|
214
|
+
function holdWhileReading(response: Response, hold: () => () => void): Response {
|
|
215
|
+
if (!response.body) return response;
|
|
216
|
+
const reader = response.body.getReader();
|
|
217
|
+
const body = new ReadableStream<Uint8Array>(
|
|
218
|
+
{
|
|
219
|
+
async pull(controller) {
|
|
220
|
+
const release = hold();
|
|
221
|
+
try {
|
|
222
|
+
const { value, done } = await reader.read();
|
|
223
|
+
if (done) controller.close();
|
|
224
|
+
else controller.enqueue(value);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
controller.error(error);
|
|
227
|
+
} finally {
|
|
228
|
+
release();
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
cancel: (reason) => reader.cancel(reason),
|
|
232
|
+
},
|
|
233
|
+
{ highWaterMark: 0 },
|
|
234
|
+
);
|
|
235
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
236
|
+
}
|
package/src/node/http/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { EventEmitterBase } from '../events.js';
|
|
|
8
8
|
import { ClientRequest, createFetch, type ClientDeps } from './client.js';
|
|
9
9
|
import { FakeSocket, IncomingMessage, OutgoingHeaders, Server, ServerResponse } from './server.js';
|
|
10
10
|
import { METHODS, STATUS_CODES } from './status.js';
|
|
11
|
+
import { textResponse } from '../../system/httpStream.js';
|
|
11
12
|
|
|
12
13
|
export { createFetch };
|
|
13
14
|
|
|
@@ -74,7 +75,7 @@ export function createNetModule(deps: ClientDeps): Record<string, unknown> {
|
|
|
74
75
|
const port = typeof first === 'object' && first ? Number((first as { port?: number }).port ?? 0) : Number(first ?? 0);
|
|
75
76
|
if (callback) this.once('listening', callback);
|
|
76
77
|
try {
|
|
77
|
-
this.reservation = deps.system.listen(port, async () => (
|
|
78
|
+
this.reservation = deps.system.listen(port, async () => textResponse(502, 'Not an HTTP server'));
|
|
78
79
|
} catch (error) {
|
|
79
80
|
queueMicrotask(() => this.emit('error', error));
|
|
80
81
|
return this;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The message and socket objects both halves of `http` share: the socket
|
|
3
|
+
* stand-in frameworks read connection details from, IncomingMessage (a request
|
|
4
|
+
* a server receives, or a response a client receives) and the header store
|
|
5
|
+
* behind ServerResponse and ClientRequest.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitterBase } from '../events.js';
|
|
8
|
+
import { Buffer } from '../buffer.js';
|
|
9
|
+
import { Readable } from '../stream/readable.js';
|
|
10
|
+
import { Writable } from '../stream/writable.js';
|
|
11
|
+
|
|
12
|
+
/** Enough of net.Socket for frameworks that read connection details. */
|
|
13
|
+
export class FakeSocket extends EventEmitterBase {
|
|
14
|
+
remoteAddress = '127.0.0.1';
|
|
15
|
+
remotePort = 50000 + Math.floor(Math.random() * 10000);
|
|
16
|
+
remoteFamily = 'IPv4';
|
|
17
|
+
localAddress = '127.0.0.1';
|
|
18
|
+
localPort: number;
|
|
19
|
+
encrypted = false;
|
|
20
|
+
readable = true;
|
|
21
|
+
writable = true;
|
|
22
|
+
destroyed = false;
|
|
23
|
+
bytesRead = 0;
|
|
24
|
+
bytesWritten = 0;
|
|
25
|
+
constructor(port = 0) {
|
|
26
|
+
super();
|
|
27
|
+
this.localPort = port;
|
|
28
|
+
}
|
|
29
|
+
setTimeout(_ms: number, cb?: () => void): this { if (cb) this.once('timeout', cb); return this; }
|
|
30
|
+
setNoDelay(): this { return this; }
|
|
31
|
+
setKeepAlive(): this { return this; }
|
|
32
|
+
address() { return { address: this.localAddress, family: 'IPv4', port: this.localPort }; }
|
|
33
|
+
ref(): this { return this; }
|
|
34
|
+
unref(): this { return this; }
|
|
35
|
+
cork(): void {}
|
|
36
|
+
uncork(): void {}
|
|
37
|
+
write(): boolean { return true; }
|
|
38
|
+
end(): this { return this; }
|
|
39
|
+
destroy(): this {
|
|
40
|
+
this.destroyed = true;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function lowerHeaders(headers: Record<string, string>): { headers: Record<string, string | string[]>; raw: string[] } {
|
|
46
|
+
const out: Record<string, string | string[]> = {};
|
|
47
|
+
const raw: string[] = [];
|
|
48
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
49
|
+
const k = key.toLowerCase();
|
|
50
|
+
raw.push(key, value);
|
|
51
|
+
out[k] = k === 'set-cookie' ? [value] : value;
|
|
52
|
+
}
|
|
53
|
+
return { headers: out, raw };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class IncomingMessage extends Readable {
|
|
57
|
+
httpVersion = '1.1';
|
|
58
|
+
httpVersionMajor = 1;
|
|
59
|
+
httpVersionMinor = 1;
|
|
60
|
+
method?: string;
|
|
61
|
+
url?: string;
|
|
62
|
+
statusCode?: number;
|
|
63
|
+
statusMessage?: string;
|
|
64
|
+
headers: Record<string, string | string[]> = {};
|
|
65
|
+
rawHeaders: string[] = [];
|
|
66
|
+
trailers: Record<string, string> = {};
|
|
67
|
+
rawTrailers: string[] = [];
|
|
68
|
+
complete = false;
|
|
69
|
+
aborted = false;
|
|
70
|
+
socket: FakeSocket;
|
|
71
|
+
connection: FakeSocket;
|
|
72
|
+
|
|
73
|
+
constructor(socket: FakeSocket = new FakeSocket()) {
|
|
74
|
+
super();
|
|
75
|
+
this.socket = socket;
|
|
76
|
+
this.connection = socket;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override _read(): void {
|
|
80
|
+
// the body is pushed up front
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setTimeout(_ms: number, cb?: () => void): this {
|
|
84
|
+
if (cb) this.once('timeout', cb);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Fill from wire data; `body` null means none. */
|
|
89
|
+
_fill(headers: Record<string, string>, body: Uint8Array | null): void {
|
|
90
|
+
this._setHeaders(headers);
|
|
91
|
+
if (body?.length) this.push(Buffer.from(body));
|
|
92
|
+
this._complete();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Headers first, for a body that streams in through `push` and ends with `_complete`. */
|
|
96
|
+
_setHeaders(headers: Record<string, string>): void {
|
|
97
|
+
const { headers: lower, raw } = lowerHeaders(headers);
|
|
98
|
+
this.headers = lower;
|
|
99
|
+
this.rawHeaders = raw;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_complete(): void {
|
|
103
|
+
this.push(null);
|
|
104
|
+
this.complete = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class OutgoingHeaders extends Writable {
|
|
109
|
+
protected _headers = new Map<string, { name: string; value: string | string[] }>();
|
|
110
|
+
headersSent = false;
|
|
111
|
+
sendDate = true;
|
|
112
|
+
|
|
113
|
+
setHeader(name: string, value: string | number | readonly string[]): this {
|
|
114
|
+
if (this.headersSent) throw Object.assign(new Error('Cannot set headers after they are sent to the client'), { code: 'ERR_HTTP_HEADERS_SENT' });
|
|
115
|
+
this._headers.set(name.toLowerCase(), { name, value: Array.isArray(value) ? value.map(String) : String(value) });
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
appendHeader(name: string, value: string | readonly string[]): this {
|
|
119
|
+
const existing = this.getHeader(name);
|
|
120
|
+
const list = [...(existing === undefined ? [] : Array.isArray(existing) ? existing : [String(existing)]), ...(Array.isArray(value) ? value : [value as string])];
|
|
121
|
+
return this.setHeader(name, list);
|
|
122
|
+
}
|
|
123
|
+
getHeader(name: string): string | string[] | undefined {
|
|
124
|
+
return this._headers.get(name.toLowerCase())?.value;
|
|
125
|
+
}
|
|
126
|
+
getHeaders(): Record<string, string | string[]> {
|
|
127
|
+
const out: Record<string, string | string[]> = Object.create(null);
|
|
128
|
+
for (const [key, { value }] of this._headers) out[key] = value;
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
getHeaderNames(): string[] {
|
|
132
|
+
return [...this._headers.keys()];
|
|
133
|
+
}
|
|
134
|
+
getRawHeaderNames(): string[] {
|
|
135
|
+
return [...this._headers.values()].map((h) => h.name);
|
|
136
|
+
}
|
|
137
|
+
hasHeader(name: string): boolean {
|
|
138
|
+
return this._headers.has(name.toLowerCase());
|
|
139
|
+
}
|
|
140
|
+
removeHeader(name: string): void {
|
|
141
|
+
this._headers.delete(name.toLowerCase());
|
|
142
|
+
}
|
|
143
|
+
flushHeaders(): void {
|
|
144
|
+
this.headersSent = true;
|
|
145
|
+
}
|
|
146
|
+
setTimeout(_ms: number, cb?: () => void): this {
|
|
147
|
+
if (cb) this.once('timeout', cb);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/node/http/server.ts
CHANGED
|
@@ -1,205 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `http.createServer` on a virtual port. `listen(port)` registers a handler with
|
|
3
3
|
* the system; each request arrives as plain data (from the preview service
|
|
4
|
-
* worker, or `loopback` from inside the runtime)
|
|
5
|
-
* ServerResponse pair for the app
|
|
6
|
-
*
|
|
4
|
+
* worker, or `loopback` from inside the runtime) and becomes an IncomingMessage /
|
|
5
|
+
* ServerResponse pair for the app. The response streams back as it is written
|
|
6
|
+
* (see serverResponse.ts). Requests that ask to upgrade (WebSockets) go to the
|
|
7
|
+
* `upgrade` listeners with a socket over the raw connection (upgradeSocket.ts).
|
|
7
8
|
*/
|
|
8
9
|
import { EventEmitterBase } from '../events.js';
|
|
9
10
|
import { Buffer } from '../buffer.js';
|
|
10
|
-
import { Readable } from '../stream/readable.js';
|
|
11
|
-
import { Writable } from '../stream/writable.js';
|
|
12
11
|
import type { EventLoop } from '../loop.js';
|
|
13
|
-
import type { HttpRequestData,
|
|
14
|
-
import {
|
|
12
|
+
import type { ByteChannel, HttpRequestData, HttpStreamResponse, System } from '../../system/types.js';
|
|
13
|
+
import { pushStream } from '../../system/httpStream.js';
|
|
14
|
+
import { FakeSocket, IncomingMessage } from './messages.js';
|
|
15
|
+
import { ServerResponse } from './serverResponse.js';
|
|
16
|
+
import { UpgradeSocket } from './upgradeSocket.js';
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
remotePort = 50000 + Math.floor(Math.random() * 10000);
|
|
20
|
-
remoteFamily = 'IPv4';
|
|
21
|
-
localAddress = '127.0.0.1';
|
|
22
|
-
localPort: number;
|
|
23
|
-
encrypted = false;
|
|
24
|
-
readable = true;
|
|
25
|
-
writable = true;
|
|
26
|
-
destroyed = false;
|
|
27
|
-
bytesRead = 0;
|
|
28
|
-
bytesWritten = 0;
|
|
29
|
-
constructor(port = 0) {
|
|
30
|
-
super();
|
|
31
|
-
this.localPort = port;
|
|
32
|
-
}
|
|
33
|
-
setTimeout(_ms: number, cb?: () => void): this { if (cb) this.once('timeout', cb); return this; }
|
|
34
|
-
setNoDelay(): this { return this; }
|
|
35
|
-
setKeepAlive(): this { return this; }
|
|
36
|
-
address() { return { address: this.localAddress, family: 'IPv4', port: this.localPort }; }
|
|
37
|
-
ref(): this { return this; }
|
|
38
|
-
unref(): this { return this; }
|
|
39
|
-
cork(): void {}
|
|
40
|
-
uncork(): void {}
|
|
41
|
-
write(): boolean { return true; }
|
|
42
|
-
end(): this { return this; }
|
|
43
|
-
destroy(): this {
|
|
44
|
-
this.destroyed = true;
|
|
45
|
-
return this;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function lowerHeaders(headers: Record<string, string>): { headers: Record<string, string | string[]>; raw: string[] } {
|
|
50
|
-
const out: Record<string, string | string[]> = {};
|
|
51
|
-
const raw: string[] = [];
|
|
52
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
53
|
-
const k = key.toLowerCase();
|
|
54
|
-
raw.push(key, value);
|
|
55
|
-
out[k] = k === 'set-cookie' ? [value] : value;
|
|
56
|
-
}
|
|
57
|
-
return { headers: out, raw };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export class IncomingMessage extends Readable {
|
|
61
|
-
httpVersion = '1.1';
|
|
62
|
-
httpVersionMajor = 1;
|
|
63
|
-
httpVersionMinor = 1;
|
|
64
|
-
method?: string;
|
|
65
|
-
url?: string;
|
|
66
|
-
statusCode?: number;
|
|
67
|
-
statusMessage?: string;
|
|
68
|
-
headers: Record<string, string | string[]> = {};
|
|
69
|
-
rawHeaders: string[] = [];
|
|
70
|
-
trailers: Record<string, string> = {};
|
|
71
|
-
rawTrailers: string[] = [];
|
|
72
|
-
complete = false;
|
|
73
|
-
aborted = false;
|
|
74
|
-
socket: FakeSocket;
|
|
75
|
-
connection: FakeSocket;
|
|
76
|
-
|
|
77
|
-
constructor(socket: FakeSocket = new FakeSocket()) {
|
|
78
|
-
super();
|
|
79
|
-
this.socket = socket;
|
|
80
|
-
this.connection = socket;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
override _read(): void {
|
|
84
|
-
// the body is pushed up front
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
setTimeout(_ms: number, cb?: () => void): this {
|
|
88
|
-
if (cb) this.once('timeout', cb);
|
|
89
|
-
return this;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Fill from wire data; `body` null means none. */
|
|
93
|
-
_fill(headers: Record<string, string>, body: Uint8Array | null): void {
|
|
94
|
-
const { headers: lower, raw } = lowerHeaders(headers);
|
|
95
|
-
this.headers = lower;
|
|
96
|
-
this.rawHeaders = raw;
|
|
97
|
-
if (body?.length) this.push(Buffer.from(body));
|
|
98
|
-
this.push(null);
|
|
99
|
-
this.complete = true;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export class OutgoingHeaders extends Writable {
|
|
104
|
-
protected _headers = new Map<string, { name: string; value: string | string[] }>();
|
|
105
|
-
headersSent = false;
|
|
106
|
-
sendDate = true;
|
|
107
|
-
|
|
108
|
-
setHeader(name: string, value: string | number | readonly string[]): this {
|
|
109
|
-
if (this.headersSent) throw Object.assign(new Error('Cannot set headers after they are sent to the client'), { code: 'ERR_HTTP_HEADERS_SENT' });
|
|
110
|
-
this._headers.set(name.toLowerCase(), { name, value: Array.isArray(value) ? value.map(String) : String(value) });
|
|
111
|
-
return this;
|
|
112
|
-
}
|
|
113
|
-
appendHeader(name: string, value: string | readonly string[]): this {
|
|
114
|
-
const existing = this.getHeader(name);
|
|
115
|
-
const list = [...(existing === undefined ? [] : Array.isArray(existing) ? existing : [String(existing)]), ...(Array.isArray(value) ? value : [value as string])];
|
|
116
|
-
return this.setHeader(name, list);
|
|
117
|
-
}
|
|
118
|
-
getHeader(name: string): string | string[] | undefined {
|
|
119
|
-
return this._headers.get(name.toLowerCase())?.value;
|
|
120
|
-
}
|
|
121
|
-
getHeaders(): Record<string, string | string[]> {
|
|
122
|
-
const out: Record<string, string | string[]> = Object.create(null);
|
|
123
|
-
for (const [key, { value }] of this._headers) out[key] = value;
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
getHeaderNames(): string[] {
|
|
127
|
-
return [...this._headers.keys()];
|
|
128
|
-
}
|
|
129
|
-
getRawHeaderNames(): string[] {
|
|
130
|
-
return [...this._headers.values()].map((h) => h.name);
|
|
131
|
-
}
|
|
132
|
-
hasHeader(name: string): boolean {
|
|
133
|
-
return this._headers.has(name.toLowerCase());
|
|
134
|
-
}
|
|
135
|
-
removeHeader(name: string): void {
|
|
136
|
-
this._headers.delete(name.toLowerCase());
|
|
137
|
-
}
|
|
138
|
-
flushHeaders(): void {
|
|
139
|
-
this.headersSent = true;
|
|
140
|
-
}
|
|
141
|
-
setTimeout(_ms: number, cb?: () => void): this {
|
|
142
|
-
if (cb) this.once('timeout', cb);
|
|
143
|
-
return this;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export class ServerResponse extends OutgoingHeaders {
|
|
148
|
-
statusCode = 200;
|
|
149
|
-
statusMessage = '';
|
|
150
|
-
req: IncomingMessage;
|
|
151
|
-
socket: FakeSocket;
|
|
152
|
-
connection: FakeSocket;
|
|
153
|
-
chunkedEncoding = false;
|
|
154
|
-
shouldKeepAlive = false;
|
|
155
|
-
private readonly chunks: Uint8Array[] = [];
|
|
156
|
-
|
|
157
|
-
constructor(req: IncomingMessage, private readonly onDone: (response: HttpResponseData) => void) {
|
|
158
|
-
super({ decodeStrings: true });
|
|
159
|
-
this.req = req;
|
|
160
|
-
this.socket = req.socket;
|
|
161
|
-
this.connection = req.socket;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
get finished(): boolean {
|
|
165
|
-
return this.writableEnded;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
writeHead(status: number, message?: string | Record<string, unknown> | unknown[], headers?: Record<string, unknown> | unknown[]): this {
|
|
169
|
-
if (typeof message !== 'string') [headers, message] = [message, undefined];
|
|
170
|
-
this.statusCode = status;
|
|
171
|
-
if (message) this.statusMessage = message;
|
|
172
|
-
if (Array.isArray(headers)) {
|
|
173
|
-
for (let i = 0; i + 1 < headers.length; i += 2) this.setHeader(String(headers[i]), headers[i + 1] as string);
|
|
174
|
-
} else if (headers) {
|
|
175
|
-
for (const [k, v] of Object.entries(headers)) if (v !== undefined) this.setHeader(k, v as string);
|
|
176
|
-
}
|
|
177
|
-
this.headersSent = true;
|
|
178
|
-
return this;
|
|
179
|
-
}
|
|
180
|
-
writeContinue(): void {}
|
|
181
|
-
writeProcessing(): void {}
|
|
182
|
-
writeEarlyHints(): void {}
|
|
183
|
-
addTrailers(): void {}
|
|
184
|
-
assignSocket(): void {}
|
|
185
|
-
detachSocket(): void {}
|
|
186
|
-
|
|
187
|
-
override _write(chunk: Uint8Array, _encoding: string, callback: () => void): void {
|
|
188
|
-
this.headersSent = true;
|
|
189
|
-
this.chunks.push(chunk);
|
|
190
|
-
callback();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
override _final(callback: () => void): void {
|
|
194
|
-
this.headersSent = true;
|
|
195
|
-
const headers: Record<string, string | string[]> = {};
|
|
196
|
-
for (const { name, value } of this._headers.values()) headers[name] = value;
|
|
197
|
-
const body = Buffer.concat(this.chunks);
|
|
198
|
-
if (this.req.method === 'HEAD') this.chunks.length = 0;
|
|
199
|
-
this.onDone({ status: this.statusCode, statusText: this.statusMessage || STATUS_CODES[this.statusCode] || '', headers, body: this.req.method === 'HEAD' ? new Uint8Array() : body });
|
|
200
|
-
callback();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
18
|
+
export { FakeSocket, IncomingMessage, OutgoingHeaders } from './messages.js';
|
|
19
|
+
export { ServerResponse } from './serverResponse.js';
|
|
20
|
+
export { UpgradeSocket } from './upgradeSocket.js';
|
|
203
21
|
|
|
204
22
|
export interface ServerDeps {
|
|
205
23
|
system: System;
|
|
@@ -228,7 +46,7 @@ export class Server extends EventEmitterBase {
|
|
|
228
46
|
const port = typeof first === 'object' && first ? Number((first as { port?: number }).port ?? 0) : Number(first ?? 0);
|
|
229
47
|
if (callback) this.once('listening', callback);
|
|
230
48
|
try {
|
|
231
|
-
this.registration = this.deps.system.listen(port, (request) => this.handle(request));
|
|
49
|
+
this.registration = this.deps.system.listen(port, (request) => this.handle(request), (request, socket) => this.upgrade(request, socket));
|
|
232
50
|
} catch (error) {
|
|
233
51
|
queueMicrotask(() => this.emit('error', error));
|
|
234
52
|
return this;
|
|
@@ -278,20 +96,29 @@ export class Server extends EventEmitterBase {
|
|
|
278
96
|
cb(null, 0);
|
|
279
97
|
}
|
|
280
98
|
|
|
281
|
-
private
|
|
99
|
+
private toIncoming(request: HttpRequestData, socket: FakeSocket, headers: Record<string, string>, body: Uint8Array | null): IncomingMessage {
|
|
100
|
+
const req = new IncomingMessage(socket);
|
|
101
|
+
req.method = request.method.toUpperCase();
|
|
102
|
+
req.url = request.url;
|
|
103
|
+
req._fill(headers, body);
|
|
104
|
+
return req;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private handle(request: HttpRequestData): Promise<HttpStreamResponse> {
|
|
282
108
|
return new Promise((resolve) => {
|
|
283
|
-
const socket = new FakeSocket(this.registration?.port ?? 0);
|
|
284
|
-
const req = new IncomingMessage(socket);
|
|
285
|
-
req.method = request.method.toUpperCase();
|
|
286
|
-
req.url = request.url;
|
|
287
109
|
// Body parsers (body-parser/type-is) only read a body announced by
|
|
288
110
|
// content-length or transfer-encoding; a browser-originated request
|
|
289
111
|
// relayed by the service worker carries neither.
|
|
290
112
|
const headers = { ...request.headers };
|
|
291
113
|
const announced = Object.keys(headers).some((k) => /^(content-length|transfer-encoding)$/i.test(k));
|
|
292
114
|
if (!announced && request.body?.length) headers['content-length'] = String(request.body.length);
|
|
293
|
-
req.
|
|
294
|
-
const
|
|
115
|
+
const req = this.toIncoming(request, new FakeSocket(this.registration?.port ?? 0), headers, request.body);
|
|
116
|
+
const body = pushStream(() => this.deps.loop._run(() => res._hangUp()));
|
|
117
|
+
const res: ServerResponse = new ServerResponse(req, {
|
|
118
|
+
head: (head) => resolve({ ...head, body: body.stream }),
|
|
119
|
+
chunk: (data) => body.push(data),
|
|
120
|
+
end: () => body.end(),
|
|
121
|
+
});
|
|
295
122
|
this.deps.loop._run(() => {
|
|
296
123
|
try {
|
|
297
124
|
this.emit('request', req, res);
|
|
@@ -305,4 +132,12 @@ export class Server extends EventEmitterBase {
|
|
|
305
132
|
});
|
|
306
133
|
});
|
|
307
134
|
}
|
|
135
|
+
|
|
136
|
+
/** A connection asking to upgrade: Node destroys it when nobody listens for `upgrade`. */
|
|
137
|
+
private upgrade(request: HttpRequestData, channel: ByteChannel): void {
|
|
138
|
+
if (!this.listenerCount('upgrade')) return channel.close();
|
|
139
|
+
const socket = new UpgradeSocket(channel, this.registration?.port ?? 0, () => this.deps.loop.hold());
|
|
140
|
+
const req = this.toIncoming(request, socket as unknown as FakeSocket, request.headers, null);
|
|
141
|
+
this.deps.loop._run(() => this.emit('upgrade', req, socket, Buffer.alloc(0)));
|
|
142
|
+
}
|
|
308
143
|
}
|