@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.
Files changed (184) hide show
  1. package/dist/archive.d.ts +13 -0
  2. package/dist/archive.d.ts.map +1 -0
  3. package/dist/archive.js +53 -0
  4. package/dist/archive.js.map +1 -0
  5. package/dist/devServer.d.ts +5 -0
  6. package/dist/devServer.d.ts.map +1 -1
  7. package/dist/devServer.js +11 -2
  8. package/dist/devServer.js.map +1 -1
  9. package/dist/html.d.ts +12 -3
  10. package/dist/html.d.ts.map +1 -1
  11. package/dist/html.js +31 -5
  12. package/dist/html.js.map +1 -1
  13. package/dist/index.d.ts +5 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/installer/index.d.ts +2 -0
  18. package/dist/installer/index.d.ts.map +1 -1
  19. package/dist/installer/index.js +1 -0
  20. package/dist/installer/index.js.map +1 -1
  21. package/dist/installer/install.d.ts +3 -0
  22. package/dist/installer/install.d.ts.map +1 -1
  23. package/dist/installer/install.js +1 -1
  24. package/dist/installer/install.js.map +1 -1
  25. package/dist/installer/npmrc.d.ts +26 -0
  26. package/dist/installer/npmrc.d.ts.map +1 -0
  27. package/dist/installer/npmrc.js +55 -0
  28. package/dist/installer/npmrc.js.map +1 -0
  29. package/dist/installer/registry.d.ts +3 -0
  30. package/dist/installer/registry.d.ts.map +1 -1
  31. package/dist/installer/registry.js +10 -5
  32. package/dist/installer/registry.js.map +1 -1
  33. package/dist/node/http/client.d.ts +3 -1
  34. package/dist/node/http/client.d.ts.map +1 -1
  35. package/dist/node/http/client.js +74 -5
  36. package/dist/node/http/client.js.map +1 -1
  37. package/dist/node/http/index.d.ts.map +1 -1
  38. package/dist/node/http/index.js +2 -1
  39. package/dist/node/http/index.js.map +1 -1
  40. package/dist/node/http/messages.d.ts +87 -0
  41. package/dist/node/http/messages.d.ts.map +1 -0
  42. package/dist/node/http/messages.js +146 -0
  43. package/dist/node/http/messages.js.map +1 -0
  44. package/dist/node/http/server.d.ts +13 -98
  45. package/dist/node/http/server.d.ts.map +1 -1
  46. package/dist/node/http/server.js +34 -202
  47. package/dist/node/http/server.js.map +1 -1
  48. package/dist/node/http/serverResponse.d.ts +42 -0
  49. package/dist/node/http/serverResponse.d.ts.map +1 -0
  50. package/dist/node/http/serverResponse.js +89 -0
  51. package/dist/node/http/serverResponse.js.map +1 -0
  52. package/dist/node/http/upgradeSocket.d.ts +34 -0
  53. package/dist/node/http/upgradeSocket.d.ts.map +1 -0
  54. package/dist/node/http/upgradeSocket.js +73 -0
  55. package/dist/node/http/upgradeSocket.js.map +1 -0
  56. package/dist/node/runtime.js +2 -2
  57. package/dist/node/runtime.js.map +1 -1
  58. package/dist/previewBridge.d.ts +42 -0
  59. package/dist/previewBridge.d.ts.map +1 -0
  60. package/dist/previewBridge.js +192 -0
  61. package/dist/previewBridge.js.map +1 -0
  62. package/dist/shell/ast.d.ts +101 -0
  63. package/dist/shell/ast.d.ts.map +1 -0
  64. package/dist/shell/ast.js +7 -0
  65. package/dist/shell/ast.js.map +1 -0
  66. package/dist/shell/builtins.d.ts +14 -0
  67. package/dist/shell/builtins.d.ts.map +1 -1
  68. package/dist/shell/builtins.js +87 -7
  69. package/dist/shell/builtins.js.map +1 -1
  70. package/dist/shell/compound.d.ts +30 -0
  71. package/dist/shell/compound.d.ts.map +1 -0
  72. package/dist/shell/compound.js +73 -0
  73. package/dist/shell/compound.js.map +1 -0
  74. package/dist/shell/exec.d.ts +27 -6
  75. package/dist/shell/exec.d.ts.map +1 -1
  76. package/dist/shell/exec.js +127 -41
  77. package/dist/shell/exec.js.map +1 -1
  78. package/dist/shell/expand.d.ts +5 -1
  79. package/dist/shell/expand.d.ts.map +1 -1
  80. package/dist/shell/expand.js +7 -5
  81. package/dist/shell/expand.js.map +1 -1
  82. package/dist/shell/lineReader.d.ts +9 -0
  83. package/dist/shell/lineReader.d.ts.map +1 -0
  84. package/dist/shell/lineReader.js +42 -0
  85. package/dist/shell/lineReader.js.map +1 -0
  86. package/dist/shell/npm.d.ts.map +1 -1
  87. package/dist/shell/npm.js +5 -1
  88. package/dist/shell/npm.js.map +1 -1
  89. package/dist/shell/packageManagers.d.ts +20 -0
  90. package/dist/shell/packageManagers.d.ts.map +1 -0
  91. package/dist/shell/packageManagers.js +74 -0
  92. package/dist/shell/packageManagers.js.map +1 -0
  93. package/dist/shell/parse.d.ts +16 -66
  94. package/dist/shell/parse.d.ts.map +1 -1
  95. package/dist/shell/parse.js +244 -243
  96. package/dist/shell/parse.js.map +1 -1
  97. package/dist/shell/program.d.ts.map +1 -1
  98. package/dist/shell/program.js +2 -1
  99. package/dist/shell/program.js.map +1 -1
  100. package/dist/shell/testCommand.d.ts +9 -0
  101. package/dist/shell/testCommand.d.ts.map +1 -0
  102. package/dist/shell/testCommand.js +105 -0
  103. package/dist/shell/testCommand.js.map +1 -0
  104. package/dist/shell/tokenize.d.ts +25 -0
  105. package/dist/shell/tokenize.d.ts.map +1 -0
  106. package/dist/shell/tokenize.js +279 -0
  107. package/dist/shell/tokenize.js.map +1 -0
  108. package/dist/system/cookieJar.d.ts +59 -0
  109. package/dist/system/cookieJar.d.ts.map +1 -0
  110. package/dist/system/cookieJar.js +187 -0
  111. package/dist/system/cookieJar.js.map +1 -0
  112. package/dist/system/httpStream.d.ts +40 -0
  113. package/dist/system/httpStream.d.ts.map +1 -0
  114. package/dist/system/httpStream.js +156 -0
  115. package/dist/system/httpStream.js.map +1 -0
  116. package/dist/system/index.d.ts +9 -1
  117. package/dist/system/index.d.ts.map +1 -1
  118. package/dist/system/index.js +4 -0
  119. package/dist/system/index.js.map +1 -1
  120. package/dist/system/kernel.d.ts +10 -2
  121. package/dist/system/kernel.d.ts.map +1 -1
  122. package/dist/system/kernel.js +12 -3
  123. package/dist/system/kernel.js.map +1 -1
  124. package/dist/system/ports.d.ts +9 -4
  125. package/dist/system/ports.d.ts.map +1 -1
  126. package/dist/system/ports.js +21 -2
  127. package/dist/system/ports.js.map +1 -1
  128. package/dist/system/programs.d.ts +1 -1
  129. package/dist/system/programs.d.ts.map +1 -1
  130. package/dist/system/programs.js +2 -0
  131. package/dist/system/programs.js.map +1 -1
  132. package/dist/system/types.d.ts +36 -6
  133. package/dist/system/types.d.ts.map +1 -1
  134. package/dist/system/webSocketClient.d.ts +37 -0
  135. package/dist/system/webSocketClient.d.ts.map +1 -0
  136. package/dist/system/webSocketClient.js +152 -0
  137. package/dist/system/webSocketClient.js.map +1 -0
  138. package/dist/system/webSocketFrames.d.ts +39 -0
  139. package/dist/system/webSocketFrames.d.ts.map +1 -0
  140. package/dist/system/webSocketFrames.js +117 -0
  141. package/dist/system/webSocketFrames.js.map +1 -0
  142. package/dist/vfs.d.ts +16 -1
  143. package/dist/vfs.d.ts.map +1 -1
  144. package/dist/vfs.js +45 -3
  145. package/dist/vfs.js.map +1 -1
  146. package/package.json +1 -1
  147. package/src/archive.ts +63 -0
  148. package/src/devServer.ts +12 -2
  149. package/src/html.ts +34 -5
  150. package/src/index.ts +5 -1
  151. package/src/installer/index.ts +2 -0
  152. package/src/installer/install.ts +4 -1
  153. package/src/installer/npmrc.ts +69 -0
  154. package/src/installer/registry.ts +12 -5
  155. package/src/node/http/client.ts +69 -6
  156. package/src/node/http/index.ts +2 -1
  157. package/src/node/http/messages.ts +150 -0
  158. package/src/node/http/server.ts +37 -202
  159. package/src/node/http/serverResponse.ts +98 -0
  160. package/src/node/http/upgradeSocket.ts +79 -0
  161. package/src/node/runtime.ts +2 -2
  162. package/src/previewBridge.ts +194 -0
  163. package/src/shell/ast.ts +85 -0
  164. package/src/shell/builtins.ts +97 -7
  165. package/src/shell/compound.ts +89 -0
  166. package/src/shell/exec.ts +128 -40
  167. package/src/shell/expand.ts +8 -6
  168. package/src/shell/lineReader.ts +46 -0
  169. package/src/shell/npm.ts +5 -1
  170. package/src/shell/packageManagers.ts +75 -0
  171. package/src/shell/parse.ts +233 -236
  172. package/src/shell/program.ts +2 -1
  173. package/src/shell/testCommand.ts +105 -0
  174. package/src/shell/tokenize.ts +257 -0
  175. package/src/system/cookieJar.ts +187 -0
  176. package/src/system/httpStream.ts +168 -0
  177. package/src/system/index.ts +24 -1
  178. package/src/system/kernel.ts +16 -4
  179. package/src/system/ports.ts +23 -4
  180. package/src/system/programs.ts +3 -1
  181. package/src/system/types.ts +40 -6
  182. package/src/system/webSocketClient.ts +177 -0
  183. package/src/system/webSocketFrames.ts +118 -0
  184. 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
+ }
@@ -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), becomes an IncomingMessage /
5
- * ServerResponse pair for the app, and the finished response goes back as data.
6
- * Responses are buffered: streaming (SSE, long-polling) arrives in one piece.
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, HttpResponseData, System } from '../../system/types.js';
14
- import { STATUS_CODES } from './status.js';
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
- /** Enough of net.Socket for frameworks that read connection details. */
17
- export class FakeSocket extends EventEmitterBase {
18
- remoteAddress = '127.0.0.1';
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 handle(request: HttpRequestData): Promise<HttpResponseData> {
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._fill(headers, request.body);
294
- const res = new ServerResponse(req, resolve);
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
+ }
@@ -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() {