@seanhogg/builderforce-webcontainers-core 2026.10.6 → 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 +13 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +53 -0
- package/dist/archive.js.map +1 -0
- package/dist/devServer.d.ts +5 -0
- package/dist/devServer.d.ts.map +1 -1
- package/dist/devServer.js +11 -2
- package/dist/devServer.js.map +1 -1
- package/dist/html.d.ts +12 -3
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +31 -5
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/installer/index.d.ts +2 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +1 -0
- package/dist/installer/index.js.map +1 -1
- package/dist/installer/install.d.ts +3 -0
- package/dist/installer/install.d.ts.map +1 -1
- package/dist/installer/install.js +1 -1
- package/dist/installer/install.js.map +1 -1
- package/dist/installer/npmrc.d.ts +26 -0
- package/dist/installer/npmrc.d.ts.map +1 -0
- package/dist/installer/npmrc.js +55 -0
- package/dist/installer/npmrc.js.map +1 -0
- package/dist/installer/registry.d.ts +3 -0
- package/dist/installer/registry.d.ts.map +1 -1
- package/dist/installer/registry.js +10 -5
- package/dist/installer/registry.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/npm.d.ts.map +1 -1
- package/dist/shell/npm.js +5 -1
- package/dist/shell/npm.js.map +1 -1
- 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/dist/vfs.d.ts +16 -1
- package/dist/vfs.d.ts.map +1 -1
- package/dist/vfs.js +45 -3
- package/dist/vfs.js.map +1 -1
- package/package.json +1 -1
- package/src/archive.ts +63 -0
- package/src/devServer.ts +12 -2
- package/src/html.ts +34 -5
- package/src/index.ts +5 -1
- package/src/installer/index.ts +2 -0
- package/src/installer/install.ts +4 -1
- package/src/installer/npmrc.ts +69 -0
- package/src/installer/registry.ts +12 -5
- 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/npm.ts +5 -1
- 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/vfs.ts +46 -3
|
@@ -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
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServerResponse, streamed: the head goes out on `writeHead`/`flushHeaders` or
|
|
3
|
+
* the first write, each `write` is a body chunk, `end` ends the body. So
|
|
4
|
+
* Server-Sent Events and chunked progress reach the client as they happen. A
|
|
5
|
+
* client that hangs up mid-response is seen the way Node reports it.
|
|
6
|
+
*/
|
|
7
|
+
import type { HttpResponseHead } from '../../system/types.js';
|
|
8
|
+
import { STATUS_CODES } from './status.js';
|
|
9
|
+
import { FakeSocket, IncomingMessage, OutgoingHeaders } from './messages.js';
|
|
10
|
+
|
|
11
|
+
/** Where a response goes as it is written. */
|
|
12
|
+
export interface ResponseSink {
|
|
13
|
+
head(head: HttpResponseHead): void;
|
|
14
|
+
chunk(data: Uint8Array): void;
|
|
15
|
+
end(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Statuses that never carry a body. */
|
|
19
|
+
const NO_BODY = new Set([101, 204, 304]);
|
|
20
|
+
|
|
21
|
+
export class ServerResponse extends OutgoingHeaders {
|
|
22
|
+
statusCode = 200;
|
|
23
|
+
statusMessage = '';
|
|
24
|
+
req: IncomingMessage;
|
|
25
|
+
socket: FakeSocket;
|
|
26
|
+
connection: FakeSocket;
|
|
27
|
+
chunkedEncoding = false;
|
|
28
|
+
shouldKeepAlive = false;
|
|
29
|
+
private headWritten = false;
|
|
30
|
+
private hungUp = false;
|
|
31
|
+
|
|
32
|
+
constructor(req: IncomingMessage, private readonly sink: ResponseSink) {
|
|
33
|
+
super({ decodeStrings: true });
|
|
34
|
+
this.req = req;
|
|
35
|
+
this.socket = req.socket;
|
|
36
|
+
this.connection = req.socket;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get finished(): boolean {
|
|
40
|
+
return this.writableEnded;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
writeHead(status: number, message?: string | Record<string, unknown> | unknown[], headers?: Record<string, unknown> | unknown[]): this {
|
|
44
|
+
if (typeof message !== 'string') [headers, message] = [message, undefined];
|
|
45
|
+
this.statusCode = status;
|
|
46
|
+
if (message) this.statusMessage = message;
|
|
47
|
+
if (Array.isArray(headers)) {
|
|
48
|
+
for (let i = 0; i + 1 < headers.length; i += 2) this.setHeader(String(headers[i]), headers[i + 1] as string);
|
|
49
|
+
} else if (headers) {
|
|
50
|
+
for (const [k, v] of Object.entries(headers)) if (v !== undefined) this.setHeader(k, v as string);
|
|
51
|
+
}
|
|
52
|
+
this.sendHead();
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
override flushHeaders(): void {
|
|
56
|
+
this.sendHead();
|
|
57
|
+
}
|
|
58
|
+
writeContinue(): void {}
|
|
59
|
+
writeProcessing(): void {}
|
|
60
|
+
writeEarlyHints(): void {}
|
|
61
|
+
addTrailers(): void {}
|
|
62
|
+
assignSocket(): void {}
|
|
63
|
+
detachSocket(): void {}
|
|
64
|
+
|
|
65
|
+
override _write(chunk: Uint8Array, _encoding: string, callback: () => void): void {
|
|
66
|
+
this.sendHead();
|
|
67
|
+
if (!this.hungUp && this.req.method !== 'HEAD' && !NO_BODY.has(this.statusCode)) this.sink.chunk(new Uint8Array(chunk));
|
|
68
|
+
callback();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override _final(callback: () => void): void {
|
|
72
|
+
this.sendHead();
|
|
73
|
+
if (!this.hungUp) this.sink.end();
|
|
74
|
+
callback();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The client went away (closed its EventSource, navigated off): `aborted`/`close` on the request, `close` here; later writes are dropped. */
|
|
78
|
+
_hangUp(): void {
|
|
79
|
+
if (this.hungUp || this.writableFinished) return;
|
|
80
|
+
this.hungUp = true;
|
|
81
|
+
this._writableState.emitClose = false;
|
|
82
|
+
this.req.aborted = true;
|
|
83
|
+
this.req.emit('aborted');
|
|
84
|
+
this.req.emit('close');
|
|
85
|
+
this.socket.destroyed = true;
|
|
86
|
+
this.socket.emit('close', false);
|
|
87
|
+
this.emit('close');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private sendHead(): void {
|
|
91
|
+
this.headersSent = true;
|
|
92
|
+
if (this.headWritten) return;
|
|
93
|
+
this.headWritten = true;
|
|
94
|
+
const headers: Record<string, string | string[]> = {};
|
|
95
|
+
for (const { name, value } of this._headers.values()) headers[name] = value;
|
|
96
|
+
this.sink.head({ status: this.statusCode, statusText: this.statusMessage || STATUS_CODES[this.statusCode] || '', headers });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The socket an `http.Server` hands its `upgrade` listeners: a real Duplex
|
|
3
|
+
* over the upgraded connection's byte pipe, with the slice of net.Socket that
|
|
4
|
+
* WebSocket servers touch (`ws` writes its `101` reply and frames with
|
|
5
|
+
* `write(data, cb)`, corks, pauses, reads `_readableState`, and calls
|
|
6
|
+
* `setNoDelay`/`setTimeout`). Raw bytes only — framing is the package's job.
|
|
7
|
+
*/
|
|
8
|
+
import { Buffer } from '../buffer.js';
|
|
9
|
+
import { Duplex } from '../stream/index.js';
|
|
10
|
+
import type { ByteChannel } from '../../system/types.js';
|
|
11
|
+
|
|
12
|
+
type Callback = (error?: Error | null) => void;
|
|
13
|
+
|
|
14
|
+
export class UpgradeSocket extends Duplex {
|
|
15
|
+
remoteAddress = '127.0.0.1';
|
|
16
|
+
remotePort = 50000 + Math.floor(Math.random() * 10000);
|
|
17
|
+
remoteFamily = 'IPv4';
|
|
18
|
+
localAddress = '127.0.0.1';
|
|
19
|
+
localPort: number;
|
|
20
|
+
encrypted = false;
|
|
21
|
+
connecting = false;
|
|
22
|
+
bytesRead = 0;
|
|
23
|
+
bytesWritten = 0;
|
|
24
|
+
private release?: () => void;
|
|
25
|
+
|
|
26
|
+
/** `hold` keeps the process alive while the connection is open, as a TCP socket would. */
|
|
27
|
+
constructor(private readonly channel: ByteChannel, port: number, hold?: () => () => void) {
|
|
28
|
+
super({ allowHalfOpen: false });
|
|
29
|
+
this.localPort = port;
|
|
30
|
+
this.release = hold?.();
|
|
31
|
+
channel.onData((data) => {
|
|
32
|
+
this.bytesRead += data.byteLength;
|
|
33
|
+
this.push(Buffer.from(data.buffer as ArrayBuffer, data.byteOffset, data.byteLength));
|
|
34
|
+
});
|
|
35
|
+
channel.onClose(() => {
|
|
36
|
+
this.push(null);
|
|
37
|
+
this.end();
|
|
38
|
+
});
|
|
39
|
+
this.once('close', () => {
|
|
40
|
+
this.release?.();
|
|
41
|
+
this.release = undefined;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override _read(): void {
|
|
46
|
+
// bytes are pushed as they arrive
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_write(chunk: Uint8Array, _encoding: string, callback: Callback): void {
|
|
50
|
+
this.bytesWritten += chunk.byteLength;
|
|
51
|
+
this.channel.send(chunk);
|
|
52
|
+
callback();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_final(callback: Callback): void {
|
|
56
|
+
this.channel.close();
|
|
57
|
+
this.push(null);
|
|
58
|
+
callback();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override _destroy(error: Error | null, callback: Callback): void {
|
|
62
|
+
this.channel.close();
|
|
63
|
+
callback(error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setTimeout(_ms: number, cb?: () => void): this {
|
|
67
|
+
if (cb) this.once('timeout', cb);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
setNoDelay(): this { return this; }
|
|
71
|
+
setKeepAlive(): this { return this; }
|
|
72
|
+
address() { return { address: this.localAddress, family: 'IPv4', port: this.localPort }; }
|
|
73
|
+
ref(): this { return this; }
|
|
74
|
+
unref(): this {
|
|
75
|
+
this.release?.();
|
|
76
|
+
this.release = undefined;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/node/runtime.ts
CHANGED
|
@@ -57,8 +57,8 @@ export async function startNode(options: NodeOptions): Promise<NodeInstance> {
|
|
|
57
57
|
// Servers die with the process: wrap listen so exit can close what is left open.
|
|
58
58
|
const processSystem: System = {
|
|
59
59
|
...system,
|
|
60
|
-
listen(port, handler) {
|
|
61
|
-
const registration = system.listen(port, handler);
|
|
60
|
+
listen(port, handler, upgrade) {
|
|
61
|
+
const registration = system.listen(port, handler, upgrade);
|
|
62
62
|
const tracked = {
|
|
63
63
|
port: registration.port,
|
|
64
64
|
close() {
|