as-test 1.1.6 → 1.1.7

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 (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -9
  3. package/assembly/index.ts +10 -15
  4. package/assembly/src/expectation.ts +11 -11
  5. package/assembly/src/fuzz.ts +11 -7
  6. package/assembly/src/log.ts +2 -2
  7. package/assembly/src/suite.ts +5 -5
  8. package/assembly/src/tests.ts +8 -8
  9. package/assembly/util/wipc.ts +5 -1
  10. package/bin/build-worker-pool.js +146 -142
  11. package/bin/build-worker.js +37 -34
  12. package/bin/commands/build-core.js +577 -465
  13. package/bin/commands/build.js +49 -29
  14. package/bin/commands/clean-core.js +120 -113
  15. package/bin/commands/clean.js +14 -8
  16. package/bin/commands/doctor-core.js +288 -289
  17. package/bin/commands/doctor.js +1 -1
  18. package/bin/commands/fuzz-core.js +467 -414
  19. package/bin/commands/fuzz.js +27 -10
  20. package/bin/commands/init-core.js +905 -794
  21. package/bin/commands/init.js +2 -2
  22. package/bin/commands/run-core.js +2675 -2344
  23. package/bin/commands/run.js +43 -25
  24. package/bin/commands/test.js +56 -32
  25. package/bin/commands/web-runner-source.js +1 -1
  26. package/bin/commands/web-session.js +516 -525
  27. package/bin/coverage-points.js +363 -341
  28. package/bin/crash-store.js +56 -66
  29. package/bin/index.js +4092 -3150
  30. package/bin/reporters/default.js +1090 -890
  31. package/bin/reporters/tap.js +319 -325
  32. package/bin/types.js +67 -67
  33. package/bin/util.js +1290 -1239
  34. package/bin/wipc.js +70 -73
  35. package/lib/build/index.d.ts +3 -1
  36. package/lib/build/index.js +1039 -1034
  37. package/lib/build/web-runner/client.js +1 -1
  38. package/lib/build/web-runner/html.js +1 -1
  39. package/lib/build/web-runner/worker.js +1 -1
  40. package/package.json +6 -3
  41. package/transform/lib/log.js +9 -5
  42. package/assembly/util/json.ts +0 -112
@@ -5,579 +5,570 @@ import http from "http";
5
5
  import path from "path";
6
6
  const WEB_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
7
7
  export class PersistentWebSessionHost {
8
- constructor(headless) {
9
- this.headless = headless;
10
- this.html = buildWebSessionHtml();
11
- this.client = buildWebSessionClientSource();
12
- this.worker = buildWebSessionWorkerSource();
13
- this.server = http.createServer((req, res) => this.onRequest(req, res));
14
- this.browserProcess = null;
15
- this.ownsBrowserProcess = false;
16
- this.serverSockets = new Set();
17
- this.wsSocket = null;
18
- this.wsBuffer = Buffer.alloc(0);
19
- this.ready = false;
20
- this.closed = false;
21
- this.currentJob = null;
22
- this.nextJobId = 1;
23
- this.closeError = null;
24
- this.readyResolve = null;
25
- this.readyReject = null;
26
- this.terminalHooksEnabled = Boolean(process.stdin.isTTY);
27
- this.onTerminalClosed = () => {
28
- void this.exitFromTerminalClosure();
29
- };
30
- this.onTerminalHangup = () => {
31
- void this.exitFromTerminalClosure();
32
- };
33
- this.readyPromise = new Promise((resolve, reject) => {
34
- this.readyResolve = resolve;
35
- this.readyReject = reject;
36
- });
8
+ constructor(headless) {
9
+ this.headless = headless;
10
+ this.html = buildWebSessionHtml();
11
+ this.client = buildWebSessionClientSource();
12
+ this.worker = buildWebSessionWorkerSource();
13
+ this.server = http.createServer((req, res) => this.onRequest(req, res));
14
+ this.browserProcess = null;
15
+ this.ownsBrowserProcess = false;
16
+ this.serverSockets = new Set();
17
+ this.wsSocket = null;
18
+ this.wsBuffer = Buffer.alloc(0);
19
+ this.ready = false;
20
+ this.closed = false;
21
+ this.currentJob = null;
22
+ this.nextJobId = 1;
23
+ this.closeError = null;
24
+ this.readyResolve = null;
25
+ this.readyReject = null;
26
+ this.terminalHooksEnabled = Boolean(process.stdin.isTTY);
27
+ this.onTerminalClosed = () => {
28
+ void this.exitFromTerminalClosure();
29
+ };
30
+ this.onTerminalHangup = () => {
31
+ void this.exitFromTerminalClosure();
32
+ };
33
+ this.readyPromise = new Promise((resolve, reject) => {
34
+ this.readyResolve = resolve;
35
+ this.readyReject = reject;
36
+ });
37
+ }
38
+ static async start(headless) {
39
+ const host = new PersistentWebSessionHost(headless);
40
+ await host.listen();
41
+ return host;
42
+ }
43
+ async runJob(env, label, onBinary) {
44
+ await this.readyPromise;
45
+ if (this.closed) {
46
+ throw this.closeError ?? new Error("web session host is already closed");
37
47
  }
38
- static async start(headless) {
39
- const host = new PersistentWebSessionHost(headless);
40
- await host.listen();
41
- return host;
48
+ if (this.currentJob) {
49
+ throw new Error("web session host already has an active job");
42
50
  }
43
- async runJob(env, label, onBinary) {
44
- await this.readyPromise;
45
- if (this.closed) {
46
- throw this.closeError ?? new Error("web session host is already closed");
47
- }
48
- if (this.currentJob) {
49
- throw new Error("web session host already has an active job");
50
- }
51
- const wasmPath = env.AS_TEST_WASM_PATH || "";
52
- if (!wasmPath.length) {
53
- throw new Error("AS_TEST_WASM_PATH is not set for web session job");
54
- }
55
- const helperPath = env.AS_TEST_HELPER_PATH?.length
56
- ? env.AS_TEST_HELPER_PATH
57
- : null;
58
- const jobId = String(this.nextJobId++);
59
- const browserEnv = {
60
- ...env,
61
- AS_TEST_WASM_PATH: `/job/${jobId}/${path.basename(wasmPath)}`,
62
- };
63
- if (helperPath) {
64
- browserEnv.AS_TEST_HELPER_PATH = `/job/${jobId}/${path.basename(helperPath)}`;
65
- }
66
- else {
67
- delete browserEnv.AS_TEST_HELPER_PATH;
68
- }
69
- await new Promise((resolve, reject) => {
70
- this.currentJob = {
71
- id: jobId,
72
- env: browserEnv,
73
- label,
74
- wasmPath,
75
- helperPath,
76
- onBinary,
77
- resolve: () => {
78
- this.currentJob = null;
79
- resolve();
80
- },
81
- reject: (error) => {
82
- this.currentJob = null;
83
- reject(error);
84
- },
85
- started: false,
86
- };
87
- this.sendControl({ kind: "load", env: browserEnv, label });
88
- });
51
+ const wasmPath = env.AS_TEST_WASM_PATH || "";
52
+ if (!wasmPath.length) {
53
+ throw new Error("AS_TEST_WASM_PATH is not set for web session job");
89
54
  }
90
- sendReply(frame) {
91
- if (!this.wsSocket || !frame.length)
92
- return;
93
- sendWebSocketFrame(this.wsSocket, 0x2, frame);
55
+ const helperPath = env.AS_TEST_HELPER_PATH?.length
56
+ ? env.AS_TEST_HELPER_PATH
57
+ : null;
58
+ const jobId = String(this.nextJobId++);
59
+ const browserEnv = {
60
+ ...env,
61
+ AS_TEST_WASM_PATH: `/job/${jobId}/${path.basename(wasmPath)}`,
62
+ };
63
+ if (helperPath) {
64
+ browserEnv.AS_TEST_HELPER_PATH = `/job/${jobId}/${path.basename(helperPath)}`;
65
+ } else {
66
+ delete browserEnv.AS_TEST_HELPER_PATH;
94
67
  }
95
- async close(reason) {
96
- if (this.closed)
97
- return;
98
- this.closed = true;
99
- this.removeTerminalHooks();
100
- const closeError = reason ?? new Error("web session host closed");
101
- this.closeError = closeError;
102
- if (!this.ready) {
103
- this.readyReject?.(closeError);
104
- this.readyResolve = null;
105
- this.readyReject = null;
106
- }
107
- if (this.currentJob) {
108
- this.currentJob.reject(closeError);
109
- }
110
- try {
111
- this.sendControl({
112
- kind: "shutdown",
113
- ok: reason == null,
114
- message: reason?.message ?? "",
115
- });
116
- }
117
- catch { }
118
- try {
119
- this.wsSocket?.end();
120
- }
121
- catch { }
122
- try {
123
- this.wsSocket?.destroy();
68
+ await new Promise((resolve, reject) => {
69
+ this.currentJob = {
70
+ id: jobId,
71
+ env: browserEnv,
72
+ label,
73
+ wasmPath,
74
+ helperPath,
75
+ onBinary,
76
+ resolve: () => {
77
+ this.currentJob = null;
78
+ resolve();
79
+ },
80
+ reject: (error) => {
81
+ this.currentJob = null;
82
+ reject(error);
83
+ },
84
+ started: false,
85
+ };
86
+ this.sendControl({ kind: "load", env: browserEnv, label });
87
+ });
88
+ }
89
+ sendReply(frame) {
90
+ if (!this.wsSocket || !frame.length) return;
91
+ sendWebSocketFrame(this.wsSocket, 0x2, frame);
92
+ }
93
+ async close(reason) {
94
+ if (this.closed) return;
95
+ this.closed = true;
96
+ this.removeTerminalHooks();
97
+ const closeError = reason ?? new Error("web session host closed");
98
+ this.closeError = closeError;
99
+ if (!this.ready) {
100
+ this.readyReject?.(closeError);
101
+ this.readyResolve = null;
102
+ this.readyReject = null;
103
+ }
104
+ if (this.currentJob) {
105
+ this.currentJob.reject(closeError);
106
+ }
107
+ try {
108
+ this.sendControl({
109
+ kind: "shutdown",
110
+ ok: reason == null,
111
+ message: reason?.message ?? "",
112
+ });
113
+ } catch {}
114
+ try {
115
+ this.wsSocket?.end();
116
+ } catch {}
117
+ try {
118
+ this.wsSocket?.destroy();
119
+ } catch {}
120
+ try {
121
+ this.server.closeIdleConnections?.();
122
+ this.server.closeAllConnections?.();
123
+ } catch {}
124
+ for (const socket of this.serverSockets) {
125
+ try {
126
+ socket.destroy();
127
+ } catch {}
128
+ }
129
+ await new Promise((resolve) => {
130
+ try {
131
+ this.server.close(() => resolve());
132
+ } catch {
133
+ resolve();
134
+ }
135
+ });
136
+ if (
137
+ this.browserProcess &&
138
+ this.ownsBrowserProcess &&
139
+ !this.browserProcess.killed
140
+ ) {
141
+ killOwnedBrowserProcess(this.browserProcess);
142
+ }
143
+ }
144
+ async exitFromTerminalClosure() {
145
+ await this.close(new Error("terminal side closed"));
146
+ process.exit(0);
147
+ }
148
+ async listen() {
149
+ this.installTerminalHooks();
150
+ this.server.on("connection", (socket) => {
151
+ this.serverSockets.add(socket);
152
+ socket.on("close", () => {
153
+ this.serverSockets.delete(socket);
154
+ });
155
+ });
156
+ this.server.on("upgrade", (req, socket) => {
157
+ if ((req.url ?? "") != "/ws") {
158
+ socket.destroy();
159
+ return;
160
+ }
161
+ const key = String(req.headers["sec-websocket-key"] ?? "");
162
+ if (!key) {
163
+ socket.destroy();
164
+ return;
165
+ }
166
+ const accept = createHash("sha1")
167
+ .update(key + WEB_MAGIC)
168
+ .digest("base64");
169
+ socket.write(
170
+ [
171
+ "HTTP/1.1 101 Switching Protocols",
172
+ "Upgrade: websocket",
173
+ "Connection: Upgrade",
174
+ "Sec-WebSocket-Accept: " + accept,
175
+ "",
176
+ "",
177
+ ].join("\r\n"),
178
+ );
179
+ this.wsSocket = socket;
180
+ this.wsBuffer = Buffer.alloc(0);
181
+ socket.on("data", (chunk) => this.onWebSocketData(chunk));
182
+ socket.on("end", () => {
183
+ this.wsSocket = null;
184
+ if (!this.closed) {
185
+ void this.close(new Error("web browser disconnected"));
124
186
  }
125
- catch { }
126
- try {
127
- this.server.closeIdleConnections?.();
128
- this.server.closeAllConnections?.();
187
+ });
188
+ socket.on("close", () => {
189
+ this.wsSocket = null;
190
+ if (!this.closed) {
191
+ void this.close(new Error("web browser disconnected"));
129
192
  }
130
- catch { }
131
- for (const socket of this.serverSockets) {
132
- try {
133
- socket.destroy();
134
- }
135
- catch { }
193
+ });
194
+ socket.on("error", (error) => {
195
+ const err = error instanceof Error ? error : new Error(String(error));
196
+ if (!this.closed) {
197
+ void this.close(err);
136
198
  }
137
- await new Promise((resolve) => {
138
- try {
139
- this.server.close(() => resolve());
140
- }
141
- catch {
142
- resolve();
143
- }
144
- });
145
- if (this.browserProcess &&
146
- this.ownsBrowserProcess &&
147
- !this.browserProcess.killed) {
148
- killOwnedBrowserProcess(this.browserProcess);
199
+ });
200
+ });
201
+ await new Promise((resolve, reject) => {
202
+ this.server.once("error", (error) =>
203
+ reject(error instanceof Error ? error : new Error(String(error))),
204
+ );
205
+ this.server.listen(0, "127.0.0.1", () => resolve());
206
+ });
207
+ const address = this.server.address();
208
+ if (!address || typeof address == "string") {
209
+ throw new Error("failed to determine web session host address");
210
+ }
211
+ const url = `http://127.0.0.1:${address.port}/`;
212
+ if (!this.headless && !process.env.BROWSER?.trim().length) {
213
+ process.stdout.write(`Open web session: ${url}\n`);
214
+ } else {
215
+ const launched = launchBrowser(url, this.headless);
216
+ this.browserProcess = launched.process;
217
+ this.ownsBrowserProcess = launched.ownsProcess;
218
+ this.browserProcess.on("close", (code) => {
219
+ if (this.closed) return;
220
+ const error = new Error(
221
+ `web browser process exited with code ${code ?? 0}`,
222
+ );
223
+ if (!this.ready && this.readyReject) {
224
+ this.readyReject(error);
225
+ return;
149
226
  }
227
+ void this.close(error);
228
+ });
150
229
  }
151
- async exitFromTerminalClosure() {
152
- await this.close(new Error("terminal side closed"));
153
- process.exit(0);
230
+ await this.readyPromise;
231
+ }
232
+ installTerminalHooks() {
233
+ if (!this.terminalHooksEnabled) return;
234
+ process.stdin.on("close", this.onTerminalClosed);
235
+ process.stdin.on("end", this.onTerminalClosed);
236
+ process.on("SIGHUP", this.onTerminalHangup);
237
+ }
238
+ removeTerminalHooks() {
239
+ if (!this.terminalHooksEnabled) return;
240
+ process.stdin.off("close", this.onTerminalClosed);
241
+ process.stdin.off("end", this.onTerminalClosed);
242
+ process.off("SIGHUP", this.onTerminalHangup);
243
+ }
244
+ onRequest(req, res) {
245
+ const headers = {
246
+ "Cross-Origin-Embedder-Policy": "require-corp",
247
+ "Cross-Origin-Opener-Policy": "same-origin",
248
+ "Cache-Control": "no-store",
249
+ };
250
+ const url = req.url ?? "/";
251
+ if (url == "/" || url.startsWith("/?")) {
252
+ res.writeHead(200, {
253
+ ...headers,
254
+ "Content-Type": "text/html; charset=utf-8",
255
+ });
256
+ res.end(this.html);
257
+ return;
154
258
  }
155
- async listen() {
156
- this.installTerminalHooks();
157
- this.server.on("connection", (socket) => {
158
- this.serverSockets.add(socket);
159
- socket.on("close", () => {
160
- this.serverSockets.delete(socket);
161
- });
162
- });
163
- this.server.on("upgrade", (req, socket) => {
164
- if ((req.url ?? "") != "/ws") {
165
- socket.destroy();
166
- return;
167
- }
168
- const key = String(req.headers["sec-websocket-key"] ?? "");
169
- if (!key) {
170
- socket.destroy();
171
- return;
172
- }
173
- const accept = createHash("sha1")
174
- .update(key + WEB_MAGIC)
175
- .digest("base64");
176
- socket.write([
177
- "HTTP/1.1 101 Switching Protocols",
178
- "Upgrade: websocket",
179
- "Connection: Upgrade",
180
- "Sec-WebSocket-Accept: " + accept,
181
- "",
182
- "",
183
- ].join("\r\n"));
184
- this.wsSocket = socket;
185
- this.wsBuffer = Buffer.alloc(0);
186
- socket.on("data", (chunk) => this.onWebSocketData(chunk));
187
- socket.on("end", () => {
188
- this.wsSocket = null;
189
- if (!this.closed) {
190
- void this.close(new Error("web browser disconnected"));
191
- }
192
- });
193
- socket.on("close", () => {
194
- this.wsSocket = null;
195
- if (!this.closed) {
196
- void this.close(new Error("web browser disconnected"));
197
- }
198
- });
199
- socket.on("error", (error) => {
200
- const err = error instanceof Error ? error : new Error(String(error));
201
- if (!this.closed) {
202
- void this.close(err);
203
- }
204
- });
259
+ if (url == "/client.js") {
260
+ res.writeHead(200, {
261
+ ...headers,
262
+ "Content-Type": "text/javascript; charset=utf-8",
263
+ });
264
+ res.end(this.client);
265
+ return;
266
+ }
267
+ if (url == "/worker.js") {
268
+ res.writeHead(200, {
269
+ ...headers,
270
+ "Content-Type": "text/javascript; charset=utf-8",
271
+ });
272
+ res.end(this.worker);
273
+ return;
274
+ }
275
+ if (this.currentJob) {
276
+ const wasmUrl = this.currentJob.env.AS_TEST_WASM_PATH;
277
+ const helperUrl = this.currentJob.env.AS_TEST_HELPER_PATH ?? "";
278
+ if (url == wasmUrl) {
279
+ res.writeHead(200, {
280
+ ...headers,
281
+ "Content-Type": "application/wasm",
205
282
  });
206
- await new Promise((resolve, reject) => {
207
- this.server.once("error", (error) => reject(error instanceof Error ? error : new Error(String(error))));
208
- this.server.listen(0, "127.0.0.1", () => resolve());
283
+ res.end(fs.readFileSync(this.currentJob.wasmPath));
284
+ return;
285
+ }
286
+ if (helperUrl.length && url == helperUrl && this.currentJob.helperPath) {
287
+ res.writeHead(200, {
288
+ ...headers,
289
+ "Content-Type": "text/javascript; charset=utf-8",
209
290
  });
210
- const address = this.server.address();
211
- if (!address || typeof address == "string") {
212
- throw new Error("failed to determine web session host address");
213
- }
214
- const url = `http://127.0.0.1:${address.port}/`;
215
- if (!this.headless && !process.env.BROWSER?.trim().length) {
216
- process.stdout.write(`Open web session: ${url}\n`);
217
- }
218
- else {
219
- const launched = launchBrowser(url, this.headless);
220
- this.browserProcess = launched.process;
221
- this.ownsBrowserProcess = launched.ownsProcess;
222
- this.browserProcess.on("close", (code) => {
223
- if (this.closed)
224
- return;
225
- const error = new Error(`web browser process exited with code ${code ?? 0}`);
226
- if (!this.ready && this.readyReject) {
227
- this.readyReject(error);
228
- return;
229
- }
230
- void this.close(error);
231
- });
291
+ res.end(fs.readFileSync(this.currentJob.helperPath, "utf8"));
292
+ return;
293
+ }
294
+ }
295
+ res.writeHead(404, headers);
296
+ res.end("not found");
297
+ }
298
+ onWebSocketData(chunk) {
299
+ this.wsBuffer = Buffer.concat([this.wsBuffer, chunk]);
300
+ while (this.wsBuffer.length >= 2) {
301
+ const first = this.wsBuffer[0];
302
+ const second = this.wsBuffer[1];
303
+ const opcode = first & 0x0f;
304
+ const masked = (second & 0x80) !== 0;
305
+ let length = second & 0x7f;
306
+ let offset = 2;
307
+ if (length == 126) {
308
+ if (this.wsBuffer.length < offset + 2) return;
309
+ length = this.wsBuffer.readUInt16BE(offset);
310
+ offset += 2;
311
+ } else if (length == 127) {
312
+ if (this.wsBuffer.length < offset + 8) return;
313
+ length = Number(this.wsBuffer.readBigUInt64BE(offset));
314
+ offset += 8;
315
+ }
316
+ const maskLength = masked ? 4 : 0;
317
+ if (this.wsBuffer.length < offset + maskLength + length) return;
318
+ let payload = this.wsBuffer.subarray(
319
+ offset + maskLength,
320
+ offset + maskLength + length,
321
+ );
322
+ if (masked) {
323
+ const mask = this.wsBuffer.subarray(offset, offset + 4);
324
+ const unmasked = Buffer.alloc(length);
325
+ for (let i = 0; i < length; i++) {
326
+ unmasked[i] = payload[i] ^ mask[i % 4];
232
327
  }
233
- await this.readyPromise;
328
+ payload = unmasked;
329
+ } else {
330
+ payload = Buffer.from(payload);
331
+ }
332
+ this.wsBuffer = this.wsBuffer.subarray(offset + maskLength + length);
333
+ if (opcode == 0x8) {
334
+ return;
335
+ }
336
+ if (opcode == 0x1) {
337
+ this.onControl(payload.toString("utf8"));
338
+ continue;
339
+ }
340
+ if (opcode == 0x2 && this.currentJob) {
341
+ this.currentJob.onBinary(Buffer.from(payload));
342
+ }
234
343
  }
235
- installTerminalHooks() {
236
- if (!this.terminalHooksEnabled)
237
- return;
238
- process.stdin.on("close", this.onTerminalClosed);
239
- process.stdin.on("end", this.onTerminalClosed);
240
- process.on("SIGHUP", this.onTerminalHangup);
344
+ }
345
+ onControl(raw) {
346
+ let message = null;
347
+ try {
348
+ message = JSON.parse(raw);
349
+ } catch {
350
+ return;
241
351
  }
242
- removeTerminalHooks() {
243
- if (!this.terminalHooksEnabled)
244
- return;
245
- process.stdin.off("close", this.onTerminalClosed);
246
- process.stdin.off("end", this.onTerminalClosed);
247
- process.off("SIGHUP", this.onTerminalHangup);
352
+ if (message?.kind == "ready") {
353
+ this.ready = true;
354
+ this.readyResolve?.();
355
+ this.readyResolve = null;
356
+ this.readyReject = null;
357
+ return;
248
358
  }
249
- onRequest(req, res) {
250
- const headers = {
251
- "Cross-Origin-Embedder-Policy": "require-corp",
252
- "Cross-Origin-Opener-Policy": "same-origin",
253
- "Cache-Control": "no-store",
254
- };
255
- const url = req.url ?? "/";
256
- if (url == "/" || url.startsWith("/?")) {
257
- res.writeHead(200, {
258
- ...headers,
259
- "Content-Type": "text/html; charset=utf-8",
260
- });
261
- res.end(this.html);
262
- return;
263
- }
264
- if (url == "/client.js") {
265
- res.writeHead(200, {
266
- ...headers,
267
- "Content-Type": "text/javascript; charset=utf-8",
268
- });
269
- res.end(this.client);
270
- return;
271
- }
272
- if (url == "/worker.js") {
273
- res.writeHead(200, {
274
- ...headers,
275
- "Content-Type": "text/javascript; charset=utf-8",
276
- });
277
- res.end(this.worker);
278
- return;
279
- }
280
- if (this.currentJob) {
281
- const wasmUrl = this.currentJob.env.AS_TEST_WASM_PATH;
282
- const helperUrl = this.currentJob.env.AS_TEST_HELPER_PATH ?? "";
283
- if (url == wasmUrl) {
284
- res.writeHead(200, {
285
- ...headers,
286
- "Content-Type": "application/wasm",
287
- });
288
- res.end(fs.readFileSync(this.currentJob.wasmPath));
289
- return;
290
- }
291
- if (helperUrl.length && url == helperUrl && this.currentJob.helperPath) {
292
- res.writeHead(200, {
293
- ...headers,
294
- "Content-Type": "text/javascript; charset=utf-8",
295
- });
296
- res.end(fs.readFileSync(this.currentJob.helperPath, "utf8"));
297
- return;
298
- }
299
- }
300
- res.writeHead(404, headers);
301
- res.end("not found");
359
+ if (message?.kind == "instantiated") {
360
+ if (this.currentJob && !this.currentJob.started) {
361
+ this.currentJob.started = true;
362
+ this.sendControl({ kind: "start" });
363
+ }
364
+ return;
302
365
  }
303
- onWebSocketData(chunk) {
304
- this.wsBuffer = Buffer.concat([this.wsBuffer, chunk]);
305
- while (this.wsBuffer.length >= 2) {
306
- const first = this.wsBuffer[0];
307
- const second = this.wsBuffer[1];
308
- const opcode = first & 0x0f;
309
- const masked = (second & 0x80) !== 0;
310
- let length = second & 0x7f;
311
- let offset = 2;
312
- if (length == 126) {
313
- if (this.wsBuffer.length < offset + 2)
314
- return;
315
- length = this.wsBuffer.readUInt16BE(offset);
316
- offset += 2;
317
- }
318
- else if (length == 127) {
319
- if (this.wsBuffer.length < offset + 8)
320
- return;
321
- length = Number(this.wsBuffer.readBigUInt64BE(offset));
322
- offset += 8;
323
- }
324
- const maskLength = masked ? 4 : 0;
325
- if (this.wsBuffer.length < offset + maskLength + length)
326
- return;
327
- let payload = this.wsBuffer.subarray(offset + maskLength, offset + maskLength + length);
328
- if (masked) {
329
- const mask = this.wsBuffer.subarray(offset, offset + 4);
330
- const unmasked = Buffer.alloc(length);
331
- for (let i = 0; i < length; i++) {
332
- unmasked[i] = payload[i] ^ mask[i % 4];
333
- }
334
- payload = unmasked;
335
- }
336
- else {
337
- payload = Buffer.from(payload);
338
- }
339
- this.wsBuffer = this.wsBuffer.subarray(offset + maskLength + length);
340
- if (opcode == 0x8) {
341
- return;
342
- }
343
- if (opcode == 0x1) {
344
- this.onControl(payload.toString("utf8"));
345
- continue;
346
- }
347
- if (opcode == 0x2 && this.currentJob) {
348
- this.currentJob.onBinary(Buffer.from(payload));
349
- }
350
- }
366
+ if (message?.kind == "done") {
367
+ this.currentJob?.resolve();
368
+ return;
351
369
  }
352
- onControl(raw) {
353
- let message = null;
354
- try {
355
- message = JSON.parse(raw);
356
- }
357
- catch {
358
- return;
359
- }
360
- if (message?.kind == "ready") {
361
- this.ready = true;
362
- this.readyResolve?.();
363
- this.readyResolve = null;
364
- this.readyReject = null;
365
- return;
366
- }
367
- if (message?.kind == "instantiated") {
368
- if (this.currentJob && !this.currentJob.started) {
369
- this.currentJob.started = true;
370
- this.sendControl({ kind: "start" });
371
- }
372
- return;
373
- }
374
- if (message?.kind == "done") {
375
- this.currentJob?.resolve();
376
- return;
377
- }
378
- if (message?.kind == "error") {
379
- this.currentJob?.reject(new Error(String(message.message ?? "browser runtime failed")));
380
- }
370
+ if (message?.kind == "error") {
371
+ this.currentJob?.reject(
372
+ new Error(String(message.message ?? "browser runtime failed")),
373
+ );
381
374
  }
382
- sendControl(message) {
383
- if (!this.wsSocket) {
384
- throw new Error("web session host is not connected to a browser");
385
- }
386
- sendWebSocketFrame(this.wsSocket, 0x1, Buffer.from(JSON.stringify(message), "utf8"));
375
+ }
376
+ sendControl(message) {
377
+ if (!this.wsSocket) {
378
+ throw new Error("web session host is not connected to a browser");
387
379
  }
380
+ sendWebSocketFrame(
381
+ this.wsSocket,
382
+ 0x1,
383
+ Buffer.from(JSON.stringify(message), "utf8"),
384
+ );
385
+ }
388
386
  }
389
387
  function sendWebSocketFrame(socket, opcode, payload) {
390
- let header;
391
- if (payload.length < 126) {
392
- header = Buffer.from([0x80 | opcode, payload.length]);
393
- }
394
- else if (payload.length < 65536) {
395
- header = Buffer.alloc(4);
396
- header[0] = 0x80 | opcode;
397
- header[1] = 126;
398
- header.writeUInt16BE(payload.length, 2);
399
- }
400
- else {
401
- header = Buffer.alloc(10);
402
- header[0] = 0x80 | opcode;
403
- header[1] = 127;
404
- header.writeBigUInt64BE(BigInt(payload.length), 2);
405
- }
406
- socket.write(Buffer.concat([header, payload]));
388
+ let header;
389
+ if (payload.length < 126) {
390
+ header = Buffer.from([0x80 | opcode, payload.length]);
391
+ } else if (payload.length < 65536) {
392
+ header = Buffer.alloc(4);
393
+ header[0] = 0x80 | opcode;
394
+ header[1] = 126;
395
+ header.writeUInt16BE(payload.length, 2);
396
+ } else {
397
+ header = Buffer.alloc(10);
398
+ header[0] = 0x80 | opcode;
399
+ header[1] = 127;
400
+ header.writeBigUInt64BE(BigInt(payload.length), 2);
401
+ }
402
+ socket.write(Buffer.concat([header, payload]));
407
403
  }
408
404
  function launchBrowser(url, headless) {
409
- if (process.env.BROWSER?.trim()) {
410
- const child = spawnBrowserCommand(process.env.BROWSER, url, headless);
411
- if (child)
412
- return { process: child, ownsProcess: true };
413
- }
414
- if (!headless) {
415
- const opened = openWithSystemBrowser(url);
416
- if (opened)
417
- return { process: opened, ownsProcess: false };
418
- }
419
- const direct = openWithInstalledBrowser(url, headless);
420
- if (direct)
421
- return { process: direct, ownsProcess: true };
422
- throw new Error(headless
423
- ? "could not find a headless-capable browser"
424
- : "could not open a browser automatically");
405
+ if (process.env.BROWSER?.trim()) {
406
+ const child = spawnBrowserCommand(process.env.BROWSER, url, headless);
407
+ if (child) return { process: child, ownsProcess: true };
408
+ }
409
+ if (!headless) {
410
+ const opened = openWithSystemBrowser(url);
411
+ if (opened) return { process: opened, ownsProcess: false };
412
+ }
413
+ const direct = openWithInstalledBrowser(url, headless);
414
+ if (direct) return { process: direct, ownsProcess: true };
415
+ throw new Error(
416
+ headless
417
+ ? "could not find a headless-capable browser"
418
+ : "could not open a browser automatically",
419
+ );
425
420
  }
426
421
  function openWithSystemBrowser(url) {
427
- if (process.platform == "darwin") {
428
- if (!hasExecutable("open"))
429
- return null;
430
- return spawn("open", [url], { stdio: "ignore", detached: true });
431
- }
432
- if (process.platform == "win32") {
433
- if (!hasExecutable("cmd"))
434
- return null;
435
- return spawn("cmd", ["/c", "start", "", url], {
436
- stdio: "ignore",
437
- detached: true,
438
- });
439
- }
440
- if (!hasExecutable("xdg-open"))
441
- return null;
442
- return spawn("xdg-open", [url], { stdio: "ignore", detached: true });
422
+ if (process.platform == "darwin") {
423
+ if (!hasExecutable("open")) return null;
424
+ return spawn("open", [url], { stdio: "ignore", detached: true });
425
+ }
426
+ if (process.platform == "win32") {
427
+ if (!hasExecutable("cmd")) return null;
428
+ return spawn("cmd", ["/c", "start", "", url], {
429
+ stdio: "ignore",
430
+ detached: true,
431
+ });
432
+ }
433
+ if (!hasExecutable("xdg-open")) return null;
434
+ return spawn("xdg-open", [url], { stdio: "ignore", detached: true });
443
435
  }
444
436
  function openWithInstalledBrowser(url, headless) {
445
- const candidates = [
446
- { command: "chromium", headless: ["--headless=new"] },
447
- { command: "chromium-browser", headless: ["--headless=new"] },
448
- { command: "google-chrome", headless: ["--headless=new"] },
449
- { command: "google-chrome-stable", headless: ["--headless=new"] },
450
- { command: "chrome", headless: ["--headless=new"] },
451
- { command: "msedge", headless: ["--headless=new"] },
452
- { command: "firefox", headless: ["-headless"] },
453
- ];
454
- for (const candidate of candidates) {
455
- if (!hasExecutable(candidate.command))
456
- continue;
457
- return spawn(candidate.command, [...(headless ? candidate.headless : []), url], { stdio: "ignore", detached: true });
458
- }
459
- return null;
437
+ const candidates = [
438
+ { command: "chromium", headless: ["--headless=new"] },
439
+ { command: "chromium-browser", headless: ["--headless=new"] },
440
+ { command: "google-chrome", headless: ["--headless=new"] },
441
+ { command: "google-chrome-stable", headless: ["--headless=new"] },
442
+ { command: "chrome", headless: ["--headless=new"] },
443
+ { command: "msedge", headless: ["--headless=new"] },
444
+ { command: "firefox", headless: ["-headless"] },
445
+ ];
446
+ for (const candidate of candidates) {
447
+ if (!hasExecutable(candidate.command)) continue;
448
+ return spawn(
449
+ candidate.command,
450
+ [...(headless ? candidate.headless : []), url],
451
+ { stdio: "ignore", detached: true },
452
+ );
453
+ }
454
+ return null;
460
455
  }
461
456
  function spawnBrowserCommand(commandValue, url, headless) {
462
- const direct = unwrapQuotedPath(String(commandValue).trim());
463
- if (hasExecutable(direct)) {
464
- const args = headless ? resolveHeadlessFlags(direct) : [];
465
- args.push(url);
466
- return spawn(direct, args, {
467
- stdio: "ignore",
468
- detached: true,
469
- });
470
- }
471
- const parts = splitCommand(String(commandValue));
472
- if (!parts.length)
473
- return null;
474
- const command = parts[0];
475
- if (!hasExecutable(command))
476
- return null;
477
- const args = parts.slice(1);
478
- if (headless) {
479
- args.push(...resolveHeadlessFlags(commandValue));
480
- }
457
+ const direct = unwrapQuotedPath(String(commandValue).trim());
458
+ if (hasExecutable(direct)) {
459
+ const args = headless ? resolveHeadlessFlags(direct) : [];
481
460
  args.push(url);
482
- return spawn(command, args, {
483
- stdio: "ignore",
484
- detached: true,
461
+ return spawn(direct, args, {
462
+ stdio: "ignore",
463
+ detached: true,
485
464
  });
465
+ }
466
+ const parts = splitCommand(String(commandValue));
467
+ if (!parts.length) return null;
468
+ const command = parts[0];
469
+ if (!hasExecutable(command)) return null;
470
+ const args = parts.slice(1);
471
+ if (headless) {
472
+ args.push(...resolveHeadlessFlags(commandValue));
473
+ }
474
+ args.push(url);
475
+ return spawn(command, args, {
476
+ stdio: "ignore",
477
+ detached: true,
478
+ });
486
479
  }
487
480
  function resolveHeadlessFlags(commandValue) {
488
- const lower = commandValue.toLowerCase();
489
- if (lower.includes("firefox"))
490
- return ["-headless"];
491
- return [
492
- "--headless=new",
493
- "--disable-gpu",
494
- "--no-first-run",
495
- "--no-default-browser-check",
496
- ];
481
+ const lower = commandValue.toLowerCase();
482
+ if (lower.includes("firefox")) return ["-headless"];
483
+ return [
484
+ "--headless=new",
485
+ "--disable-gpu",
486
+ "--no-first-run",
487
+ "--no-default-browser-check",
488
+ ];
497
489
  }
498
490
  function hasExecutable(command) {
499
- if (!command.length)
500
- return false;
501
- if (command.includes("/") || command.includes("\\")) {
502
- return fs.existsSync(command);
503
- }
504
- const pathValue = process.env.PATH ?? "";
505
- const suffixes = process.platform == "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
506
- for (const base of pathValue.split(path.delimiter)) {
507
- if (!base)
508
- continue;
509
- for (const suffix of suffixes) {
510
- if (fs.existsSync(path.join(base, command + suffix))) {
511
- return true;
512
- }
513
- }
491
+ if (!command.length) return false;
492
+ if (command.includes("/") || command.includes("\\")) {
493
+ return fs.existsSync(command);
494
+ }
495
+ const pathValue = process.env.PATH ?? "";
496
+ const suffixes =
497
+ process.platform == "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
498
+ for (const base of pathValue.split(path.delimiter)) {
499
+ if (!base) continue;
500
+ for (const suffix of suffixes) {
501
+ if (fs.existsSync(path.join(base, command + suffix))) {
502
+ return true;
503
+ }
514
504
  }
515
- return false;
505
+ }
506
+ return false;
516
507
  }
517
508
  function splitCommand(commandValue) {
518
- const parts = [];
519
- let current = "";
520
- let quote = "";
521
- for (let i = 0; i < commandValue.length; i++) {
522
- const char = commandValue[i];
523
- if (quote) {
524
- if (char == quote) {
525
- quote = "";
526
- }
527
- else if (char == "\\" && i + 1 < commandValue.length) {
528
- current += commandValue[++i];
529
- }
530
- else {
531
- current += char;
532
- }
533
- continue;
534
- }
535
- if (char == "'" || char == '"') {
536
- quote = char;
537
- continue;
538
- }
539
- if (/\s/.test(char)) {
540
- if (current.length) {
541
- parts.push(current);
542
- current = "";
543
- }
544
- continue;
545
- }
546
- if (char == "\\" && i + 1 < commandValue.length) {
547
- current += commandValue[++i];
548
- continue;
549
- }
509
+ const parts = [];
510
+ let current = "";
511
+ let quote = "";
512
+ for (let i = 0; i < commandValue.length; i++) {
513
+ const char = commandValue[i];
514
+ if (quote) {
515
+ if (char == quote) {
516
+ quote = "";
517
+ } else if (char == "\\" && i + 1 < commandValue.length) {
518
+ current += commandValue[++i];
519
+ } else {
550
520
  current += char;
521
+ }
522
+ continue;
551
523
  }
552
- if (current.length) {
524
+ if (char == "'" || char == '"') {
525
+ quote = char;
526
+ continue;
527
+ }
528
+ if (/\s/.test(char)) {
529
+ if (current.length) {
553
530
  parts.push(current);
531
+ current = "";
532
+ }
533
+ continue;
534
+ }
535
+ if (char == "\\" && i + 1 < commandValue.length) {
536
+ current += commandValue[++i];
537
+ continue;
554
538
  }
555
- return parts;
539
+ current += char;
540
+ }
541
+ if (current.length) {
542
+ parts.push(current);
543
+ }
544
+ return parts;
556
545
  }
557
546
  function unwrapQuotedPath(value) {
558
- if ((value.startsWith('"') && value.endsWith('"')) ||
559
- (value.startsWith("'") && value.endsWith("'"))) {
560
- return value.slice(1, -1);
561
- }
562
- return value;
547
+ if (
548
+ (value.startsWith('"') && value.endsWith('"')) ||
549
+ (value.startsWith("'") && value.endsWith("'"))
550
+ ) {
551
+ return value.slice(1, -1);
552
+ }
553
+ return value;
563
554
  }
564
555
  function killOwnedBrowserProcess(browserProcess) {
565
- try {
566
- if (process.platform != "win32" &&
567
- typeof browserProcess.pid == "number" &&
568
- browserProcess.pid > 0) {
569
- process.kill(-browserProcess.pid, "SIGTERM");
570
- return;
571
- }
572
- }
573
- catch { }
574
- try {
575
- browserProcess.kill("SIGTERM");
556
+ try {
557
+ if (
558
+ process.platform != "win32" &&
559
+ typeof browserProcess.pid == "number" &&
560
+ browserProcess.pid > 0
561
+ ) {
562
+ process.kill(-browserProcess.pid, "SIGTERM");
563
+ return;
576
564
  }
577
- catch { }
565
+ } catch {}
566
+ try {
567
+ browserProcess.kill("SIGTERM");
568
+ } catch {}
578
569
  }
579
570
  function buildWebSessionHtml() {
580
- return `<!doctype html>
571
+ return `<!doctype html>
581
572
  <html lang="en">
582
573
  <head>
583
574
  <meta charset="utf-8" />
@@ -761,7 +752,7 @@ function buildWebSessionHtml() {
761
752
  </html>`;
762
753
  }
763
754
  function buildWebSessionClientSource() {
764
- return String.raw `const runnerOrigin = location.origin;
755
+ return String.raw`const runnerOrigin = location.origin;
765
756
  const worker = new Worker(new URL("/worker.js", runnerOrigin), { type: "module" });
766
757
  const wsUrl = new URL("/ws", runnerOrigin);
767
758
  wsUrl.protocol = location.protocol == "https:" ? "wss:" : "ws:";
@@ -918,7 +909,7 @@ exitButton.addEventListener("click", () => {
918
909
  `;
919
910
  }
920
911
  function buildWebSessionWorkerSource() {
921
- return String.raw `let replyState = null;
912
+ return String.raw`let replyState = null;
922
913
  let replyBytes = null;
923
914
  const WIPC_MAGIC = [0x57, 0x49, 0x50, 0x43];
924
915
  let runtimeEnv = {};