as-test 1.0.16 → 1.1.1
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/CHANGELOG.md +57 -0
- package/README.md +45 -4
- package/as-test.config.schema.json +5 -0
- package/assembly/__fuzz__/math.fuzz.ts +19 -0
- package/assembly/__fuzz__/string.fuzz.ts +31 -0
- package/assembly/index.ts +5 -5
- package/assembly/src/expectation.ts +93 -42
- package/assembly/util/format.ts +104 -0
- package/assembly/util/helpers.ts +7 -13
- package/assembly/util/json.ts +2 -2
- package/assembly/util/wipc.ts +15 -5
- package/bin/commands/clean-core.js +135 -0
- package/bin/commands/clean.js +51 -0
- package/bin/commands/init-core.js +33 -225
- package/bin/commands/run-core.js +433 -289
- package/bin/commands/web-runner-source.js +14 -700
- package/bin/commands/web-session.js +1144 -0
- package/bin/index.js +391 -78
- package/bin/types.js +1 -0
- package/bin/util.js +16 -1
- package/bin/wipc.js +7 -2
- package/lib/build/index.d.ts +1 -0
- package/lib/build/index.js +1116 -0
- package/lib/build/web-runner/client.d.ts +1 -0
- package/lib/build/web-runner/client.js +167 -0
- package/lib/build/web-runner/html.d.ts +1 -0
- package/lib/build/web-runner/html.js +201 -0
- package/lib/build/web-runner/worker.d.ts +1 -0
- package/lib/build/web-runner/worker.js +271 -0
- package/lib/src/index.ts +1266 -0
- package/package.json +14 -6
- package/transform/lib/mock.js +50 -27
|
@@ -1,707 +1,21 @@
|
|
|
1
|
-
export function buildWebRunnerHooksSource() {
|
|
2
|
-
return `export function createUserImports(_ctx) {
|
|
3
|
-
return {
|
|
4
|
-
// env: {
|
|
5
|
-
// now_ms: () => performance.now(),
|
|
6
|
-
// },
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function runModule(_exports, _ctx) {
|
|
11
|
-
// The generated bindings helper already calls exports._start().
|
|
12
|
-
// Add extra startup calls here when your module exposes them.
|
|
13
|
-
//
|
|
14
|
-
// Example:
|
|
15
|
-
// _exports.run?.();
|
|
16
|
-
}
|
|
17
|
-
`;
|
|
18
|
-
}
|
|
19
1
|
export function buildWebRunnerSource() {
|
|
20
|
-
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
<meta charset="utf-8" />
|
|
24
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
25
|
-
<title>as-test web runner</title>
|
|
26
|
-
<style>
|
|
27
|
-
:root {
|
|
28
|
-
color-scheme: dark;
|
|
29
|
-
--bg: #0f172a;
|
|
30
|
-
--panel: #111827;
|
|
31
|
-
--muted: #94a3b8;
|
|
32
|
-
--text: #e5e7eb;
|
|
33
|
-
--accent: #38bdf8;
|
|
34
|
-
--error: #f87171;
|
|
35
|
-
}
|
|
36
|
-
body {
|
|
37
|
-
margin: 0;
|
|
38
|
-
min-height: 100vh;
|
|
39
|
-
display: grid;
|
|
40
|
-
place-items: center;
|
|
41
|
-
background:
|
|
42
|
-
radial-gradient(circle at top, rgba(56, 189, 248, 0.18), transparent 35%),
|
|
43
|
-
linear-gradient(180deg, #020617, var(--bg));
|
|
44
|
-
font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
45
|
-
color: var(--text);
|
|
46
|
-
}
|
|
47
|
-
main {
|
|
48
|
-
width: min(640px, calc(100vw - 32px));
|
|
49
|
-
background: rgba(17, 24, 39, 0.92);
|
|
50
|
-
border: 1px solid rgba(148, 163, 184, 0.18);
|
|
51
|
-
border-radius: 18px;
|
|
52
|
-
padding: 24px;
|
|
53
|
-
box-shadow: 0 20px 60px rgba(2, 6, 23, 0.45);
|
|
54
|
-
}
|
|
55
|
-
h1 {
|
|
56
|
-
margin: 0 0 12px;
|
|
57
|
-
font-size: 20px;
|
|
58
|
-
}
|
|
59
|
-
p {
|
|
60
|
-
margin: 0 0 10px;
|
|
61
|
-
color: var(--muted);
|
|
62
|
-
}
|
|
63
|
-
code {
|
|
64
|
-
color: var(--accent);
|
|
65
|
-
}
|
|
66
|
-
pre {
|
|
67
|
-
margin: 16px 0 0;
|
|
68
|
-
padding: 12px;
|
|
69
|
-
border-radius: 12px;
|
|
70
|
-
background: rgba(2, 6, 23, 0.8);
|
|
71
|
-
overflow: auto;
|
|
72
|
-
white-space: pre-wrap;
|
|
73
|
-
word-break: break-word;
|
|
74
|
-
}
|
|
75
|
-
.error {
|
|
76
|
-
color: var(--error);
|
|
77
|
-
}
|
|
78
|
-
</style>
|
|
79
|
-
</head>
|
|
80
|
-
<body>
|
|
81
|
-
<main>
|
|
82
|
-
<h1>as-test web runner</h1>
|
|
83
|
-
<p id="status">Connecting to the terminal runner...</p>
|
|
84
|
-
<pre id="details">Waiting for browser runtime bootstrap.</pre>
|
|
85
|
-
</main>
|
|
86
|
-
<script type="module" src="/client.js"></script>
|
|
87
|
-
</body>
|
|
88
|
-
</html>`;
|
|
89
|
-
const client = String.raw `const status = document.getElementById("status");
|
|
90
|
-
const details = document.getElementById("details");
|
|
91
|
-
const replyBuffer = new SharedArrayBuffer(8 + 4 * 1024 * 1024);
|
|
92
|
-
const replyState = new Int32Array(replyBuffer, 0, 2);
|
|
93
|
-
const replyBytes = new Uint8Array(replyBuffer, 8);
|
|
94
|
-
const worker = new Worker("/worker.js", { type: "module" });
|
|
95
|
-
const ws = new WebSocket((location.protocol == "https:" ? "wss://" : "ws://") + location.host + "/ws");
|
|
96
|
-
ws.binaryType = "arraybuffer";
|
|
97
|
-
|
|
98
|
-
function setStatus(message, error = false) {
|
|
99
|
-
status.textContent = message;
|
|
100
|
-
status.className = error ? "error" : "";
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function setDetails(message) {
|
|
104
|
-
details.textContent = message;
|
|
105
|
-
}
|
|
2
|
+
return `// Feel free to edit this file!
|
|
3
|
+
// Runner files use the name <mode>.<type>.js, where <type> is bindings, wasi, or web.
|
|
4
|
+
// To create a runner for another mode, copy this file to <new-mode>.<type>.js.
|
|
106
5
|
|
|
107
|
-
|
|
108
|
-
if (frame.byteLength > replyBytes.byteLength) {
|
|
109
|
-
throw new Error("WIPC reply exceeded shared browser buffer");
|
|
110
|
-
}
|
|
111
|
-
if (Atomics.load(replyState, 0) != 0) {
|
|
112
|
-
throw new Error("received concurrent WIPC replies in web runner");
|
|
113
|
-
}
|
|
114
|
-
replyBytes.set(new Uint8Array(frame), 0);
|
|
115
|
-
Atomics.store(replyState, 1, frame.byteLength);
|
|
116
|
-
Atomics.store(replyState, 0, 1);
|
|
117
|
-
Atomics.notify(replyState, 0, 1);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
worker.onmessage = (event) => {
|
|
121
|
-
const message = event.data ?? {};
|
|
122
|
-
if (message.kind == "wipc" && message.frame instanceof ArrayBuffer) {
|
|
123
|
-
ws.send(message.frame);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (message.kind == "ready") {
|
|
127
|
-
setStatus("Running tests in the browser...");
|
|
128
|
-
setDetails("Worker loaded the generated bindings helper and wasm artifact.");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (message.kind == "done") {
|
|
132
|
-
setStatus("Finished. Closing browser runner...");
|
|
133
|
-
setDetails("Test execution completed successfully.");
|
|
134
|
-
ws.send(JSON.stringify({ kind: "done" }));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (message.kind == "error") {
|
|
138
|
-
setStatus("Browser runtime failed.", true);
|
|
139
|
-
setDetails(String(message.message ?? "unknown browser runtime error"));
|
|
140
|
-
ws.send(
|
|
141
|
-
JSON.stringify({
|
|
142
|
-
kind: "error",
|
|
143
|
-
message: String(message.message ?? "unknown browser runtime error"),
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
ws.addEventListener("open", () => {
|
|
150
|
-
setStatus("Connected. Starting browser worker...");
|
|
151
|
-
setDetails("WebSocket tunnel established; waiting for wasm startup.");
|
|
152
|
-
ws.send(JSON.stringify({ kind: "ready" }));
|
|
153
|
-
worker.postMessage({
|
|
154
|
-
kind: "init",
|
|
155
|
-
helperUrl: "/artifact.js",
|
|
156
|
-
hooksUrl: "/runner-hooks.js",
|
|
157
|
-
wasmUrl: "/artifact.wasm",
|
|
158
|
-
replyBuffer,
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
ws.addEventListener("message", (event) => {
|
|
163
|
-
if (typeof event.data == "string") {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
try {
|
|
167
|
-
pushReply(event.data);
|
|
168
|
-
} catch (error) {
|
|
169
|
-
const message = String(error instanceof Error ? error.message : error);
|
|
170
|
-
setStatus("Bridge failure.", true);
|
|
171
|
-
setDetails(message);
|
|
172
|
-
ws.send(JSON.stringify({ kind: "error", message }));
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
ws.addEventListener("close", () => {
|
|
177
|
-
setStatus("Runner disconnected.", true);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
ws.addEventListener("error", () => {
|
|
181
|
-
setStatus("WebSocket connection failed.", true);
|
|
182
|
-
});
|
|
183
|
-
`;
|
|
184
|
-
const worker = String.raw `let replyState = null;
|
|
185
|
-
let replyBytes = null;
|
|
186
|
-
|
|
187
|
-
function createRunnerContext({ helperUrl, wasmUrl, module }) {
|
|
188
|
-
return {
|
|
189
|
-
helperUrl,
|
|
190
|
-
wasmUrl,
|
|
191
|
-
module,
|
|
192
|
-
postFrame(frame) {
|
|
193
|
-
self.postMessage({ kind: "wipc", frame }, [frame]);
|
|
194
|
-
return true;
|
|
195
|
-
},
|
|
196
|
-
readFrame(size) {
|
|
197
|
-
return readReply(Number(size ?? 0));
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function createAsTestImports(ctx) {
|
|
203
|
-
globalThis.process = {
|
|
204
|
-
stdout: {
|
|
205
|
-
write(data) {
|
|
206
|
-
const frame = data instanceof ArrayBuffer ? data : data?.buffer;
|
|
207
|
-
return ctx.postFrame(frame);
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
stdin: {
|
|
211
|
-
read(size) {
|
|
212
|
-
return ctx.readFrame(size);
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
return {};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function mergeImports(...groups) {
|
|
220
|
-
const out = {};
|
|
221
|
-
for (const group of groups) {
|
|
222
|
-
if (!group || typeof group != "object") continue;
|
|
223
|
-
for (const moduleName of Object.keys(group)) {
|
|
224
|
-
out[moduleName] = Object.assign(out[moduleName] || {}, group[moduleName]);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return out;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
self.onmessage = async (event) => {
|
|
231
|
-
const message = event.data ?? {};
|
|
232
|
-
if (message.kind != "init") {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const shared = message.replyBuffer;
|
|
237
|
-
replyState = new Int32Array(shared, 0, 2);
|
|
238
|
-
replyBytes = new Uint8Array(shared, 8);
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
const hooks = await import(message.hooksUrl);
|
|
242
|
-
const helper = await import(message.helperUrl);
|
|
243
|
-
if (typeof helper.instantiate != "function") {
|
|
244
|
-
throw new Error("bindings helper missing instantiate export");
|
|
245
|
-
}
|
|
246
|
-
const response = await fetch(message.wasmUrl);
|
|
247
|
-
if (!response.ok) {
|
|
248
|
-
throw new Error("failed to fetch wasm artifact: " + response.status);
|
|
249
|
-
}
|
|
250
|
-
const binary = await response.arrayBuffer();
|
|
251
|
-
const module = new WebAssembly.Module(binary);
|
|
252
|
-
const ctx = createRunnerContext({
|
|
253
|
-
helperUrl: message.helperUrl,
|
|
254
|
-
wasmUrl: message.wasmUrl,
|
|
255
|
-
module,
|
|
256
|
-
});
|
|
257
|
-
const imports = mergeImports(
|
|
258
|
-
createAsTestImports(ctx),
|
|
259
|
-
typeof hooks.createUserImports == "function"
|
|
260
|
-
? await hooks.createUserImports(ctx)
|
|
261
|
-
: {},
|
|
262
|
-
);
|
|
263
|
-
self.postMessage({ kind: "ready" });
|
|
264
|
-
const exports = await helper.instantiate(module, imports);
|
|
265
|
-
if (typeof hooks.runModule == "function") {
|
|
266
|
-
await hooks.runModule(exports, ctx);
|
|
267
|
-
}
|
|
268
|
-
self.postMessage({ kind: "done" });
|
|
269
|
-
} catch (error) {
|
|
270
|
-
const message =
|
|
271
|
-
error && typeof error == "object" && "stack" in error
|
|
272
|
-
? String(error.stack)
|
|
273
|
-
: String(error);
|
|
274
|
-
self.postMessage({ kind: "error", message });
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
function readReply(max) {
|
|
279
|
-
if (!replyState || !replyBytes || max <= 0) {
|
|
280
|
-
return new ArrayBuffer(0);
|
|
281
|
-
}
|
|
282
|
-
while (Atomics.load(replyState, 0) == 0) {
|
|
283
|
-
Atomics.wait(replyState, 0, 0);
|
|
284
|
-
}
|
|
285
|
-
const total = Atomics.load(replyState, 1);
|
|
286
|
-
const size = Math.min(max, total);
|
|
287
|
-
const out = new Uint8Array(size);
|
|
288
|
-
out.set(replyBytes.subarray(0, size));
|
|
289
|
-
if (size < total) {
|
|
290
|
-
replyBytes.copyWithin(0, size, total);
|
|
291
|
-
Atomics.store(replyState, 1, total - size);
|
|
292
|
-
} else {
|
|
293
|
-
Atomics.store(replyState, 1, 0);
|
|
294
|
-
Atomics.store(replyState, 0, 0);
|
|
295
|
-
Atomics.notify(replyState, 0, 1);
|
|
296
|
-
}
|
|
297
|
-
return out.buffer;
|
|
298
|
-
}
|
|
299
|
-
`;
|
|
300
|
-
const hooks = buildWebRunnerHooksSource();
|
|
301
|
-
return `import { createHash } from "crypto";
|
|
302
|
-
import { existsSync, readFileSync } from "fs";
|
|
303
|
-
import http from "http";
|
|
304
|
-
import path from "path";
|
|
305
|
-
import { spawn } from "child_process";
|
|
306
|
-
|
|
307
|
-
const INDEX_HTML = ${JSON.stringify(html)};
|
|
308
|
-
const CLIENT_JS = ${JSON.stringify(client)};
|
|
309
|
-
const WORKER_JS = ${JSON.stringify(worker)};
|
|
310
|
-
const DEFAULT_HOOKS_JS = ${JSON.stringify(hooks)};
|
|
311
|
-
const MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
312
|
-
const HEADLESS_FLAGS = [
|
|
313
|
-
"--headless=new",
|
|
314
|
-
"--disable-gpu",
|
|
315
|
-
"--no-first-run",
|
|
316
|
-
"--no-default-browser-check",
|
|
317
|
-
];
|
|
318
|
-
|
|
319
|
-
const rawArgs = process.argv.slice(2);
|
|
320
|
-
const headless = rawArgs.includes("--headless");
|
|
321
|
-
const wasmArg = rawArgs.find((value) => value != "--headless");
|
|
322
|
-
if (!wasmArg) {
|
|
323
|
-
process.stderr.write(
|
|
324
|
-
"usage: node ./.as-test/runners/default.web.js [--headless] <file.wasm>\\n",
|
|
325
|
-
);
|
|
326
|
-
process.exit(1);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const wasmPath = path.resolve(process.cwd(), wasmArg);
|
|
330
|
-
const helperPath = wasmPath.replace(/\\.wasm$/, ".js");
|
|
331
|
-
const hooksPath = path.resolve(process.cwd(), ".as-test/runners/default.web.hooks.js");
|
|
332
|
-
if (!existsSync(wasmPath)) {
|
|
333
|
-
process.stderr.write("missing wasm artifact: " + wasmPath + "\\n");
|
|
334
|
-
process.exit(1);
|
|
335
|
-
}
|
|
336
|
-
if (!existsSync(helperPath)) {
|
|
337
|
-
process.stderr.write("missing bindings helper: " + helperPath + "\\n");
|
|
338
|
-
process.exit(1);
|
|
339
|
-
}
|
|
6
|
+
import { instantiate } from "as-test/lib";
|
|
340
7
|
|
|
341
|
-
let
|
|
342
|
-
|
|
343
|
-
let stdinBuffer = Buffer.alloc(0);
|
|
344
|
-
let browserProcess = null;
|
|
345
|
-
let closed = false;
|
|
346
|
-
let exitCode = 0;
|
|
347
|
-
let ready = false;
|
|
348
|
-
const pendingFrames = [];
|
|
8
|
+
let exports = null;
|
|
9
|
+
const imports = {};
|
|
349
10
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
res.writeHead(200, {
|
|
359
|
-
...headers,
|
|
360
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
11
|
+
instantiate(imports)
|
|
12
|
+
.then((instance) => {
|
|
13
|
+
exports = instance.exports;
|
|
14
|
+
instance.exports.start?.();
|
|
15
|
+
// Add extra startup logic here when needed.
|
|
16
|
+
})
|
|
17
|
+
.catch((error) => {
|
|
18
|
+
throw new Error("Failed to run web module: " + String(error));
|
|
361
19
|
});
|
|
362
|
-
res.end(INDEX_HTML);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
if (url == "/client.js") {
|
|
366
|
-
res.writeHead(200, {
|
|
367
|
-
...headers,
|
|
368
|
-
"Content-Type": "text/javascript; charset=utf-8",
|
|
369
|
-
});
|
|
370
|
-
res.end(CLIENT_JS);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
if (url == "/worker.js") {
|
|
374
|
-
res.writeHead(200, {
|
|
375
|
-
...headers,
|
|
376
|
-
"Content-Type": "text/javascript; charset=utf-8",
|
|
377
|
-
});
|
|
378
|
-
res.end(WORKER_JS);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
if (url == "/artifact.js") {
|
|
382
|
-
res.writeHead(200, {
|
|
383
|
-
...headers,
|
|
384
|
-
"Content-Type": "text/javascript; charset=utf-8",
|
|
385
|
-
});
|
|
386
|
-
res.end(readFileSync(helperPath, "utf8"));
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (url == "/runner-hooks.js") {
|
|
390
|
-
res.writeHead(200, {
|
|
391
|
-
...headers,
|
|
392
|
-
"Content-Type": "text/javascript; charset=utf-8",
|
|
393
|
-
});
|
|
394
|
-
res.end(existsSync(hooksPath) ? readFileSync(hooksPath, "utf8") : DEFAULT_HOOKS_JS);
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
if (url == "/artifact.wasm") {
|
|
398
|
-
res.writeHead(200, {
|
|
399
|
-
...headers,
|
|
400
|
-
"Content-Type": "application/wasm",
|
|
401
|
-
});
|
|
402
|
-
res.end(readFileSync(wasmPath));
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
res.writeHead(404, headers);
|
|
406
|
-
res.end("not found");
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
server.on("upgrade", (req, socket) => {
|
|
410
|
-
if ((req.url ?? "") != "/ws") {
|
|
411
|
-
socket.destroy();
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
const key = String(req.headers["sec-websocket-key"] ?? "");
|
|
415
|
-
if (!key) {
|
|
416
|
-
socket.destroy();
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
const accept = createHash("sha1")
|
|
420
|
-
.update(key + MAGIC)
|
|
421
|
-
.digest("base64");
|
|
422
|
-
socket.write(
|
|
423
|
-
[
|
|
424
|
-
"HTTP/1.1 101 Switching Protocols",
|
|
425
|
-
"Upgrade: websocket",
|
|
426
|
-
"Connection: Upgrade",
|
|
427
|
-
"Sec-WebSocket-Accept: " + accept,
|
|
428
|
-
"",
|
|
429
|
-
"",
|
|
430
|
-
].join("\\r\\n"),
|
|
431
|
-
);
|
|
432
|
-
wsSocket = socket;
|
|
433
|
-
wsBuffer = Buffer.alloc(0);
|
|
434
|
-
socket.on("data", (chunk) => onWebSocketData(chunk));
|
|
435
|
-
socket.on("close", () => {
|
|
436
|
-
wsSocket = null;
|
|
437
|
-
if (!closed) {
|
|
438
|
-
finish(exitCode || 1);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
socket.on("error", (error) => {
|
|
442
|
-
process.stderr.write("web runner websocket error: " + String(error) + "\\n");
|
|
443
|
-
});
|
|
444
|
-
flushPendingFrames();
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
server.listen(0, "127.0.0.1", () => {
|
|
448
|
-
const address = server.address();
|
|
449
|
-
if (!address || typeof address == "string") {
|
|
450
|
-
process.stderr.write("failed to determine local web runner address\\n");
|
|
451
|
-
finish(1);
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
const url = "http://127.0.0.1:" + address.port + "/";
|
|
455
|
-
try {
|
|
456
|
-
browserProcess = launchBrowser(url, headless);
|
|
457
|
-
} catch (error) {
|
|
458
|
-
process.stderr.write(String(error) + "\\n");
|
|
459
|
-
finish(1);
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
process.stdin.on("data", (chunk) => {
|
|
464
|
-
stdinBuffer = Buffer.concat([stdinBuffer, chunk]);
|
|
465
|
-
while (stdinBuffer.length >= 9) {
|
|
466
|
-
const length = stdinBuffer.readUInt32LE(5);
|
|
467
|
-
const frameSize = 9 + length;
|
|
468
|
-
if (stdinBuffer.length < frameSize) {
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
const frame = stdinBuffer.subarray(0, frameSize);
|
|
472
|
-
stdinBuffer = stdinBuffer.subarray(frameSize);
|
|
473
|
-
if (ready && wsSocket) {
|
|
474
|
-
sendWebSocketFrame(wsSocket, 0x2, frame);
|
|
475
|
-
} else {
|
|
476
|
-
pendingFrames.push(Buffer.from(frame));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
process.stdin.on("end", () => finish(exitCode));
|
|
482
|
-
process.on("SIGINT", () => finish(130));
|
|
483
|
-
process.on("SIGTERM", () => finish(143));
|
|
484
|
-
|
|
485
|
-
function onWebSocketData(chunk) {
|
|
486
|
-
wsBuffer = Buffer.concat([wsBuffer, chunk]);
|
|
487
|
-
while (wsBuffer.length >= 2) {
|
|
488
|
-
const first = wsBuffer[0];
|
|
489
|
-
const second = wsBuffer[1];
|
|
490
|
-
const opcode = first & 0x0f;
|
|
491
|
-
const masked = (second & 0x80) !== 0;
|
|
492
|
-
let length = second & 0x7f;
|
|
493
|
-
let offset = 2;
|
|
494
|
-
if (length == 126) {
|
|
495
|
-
if (wsBuffer.length < offset + 2) return;
|
|
496
|
-
length = wsBuffer.readUInt16BE(offset);
|
|
497
|
-
offset += 2;
|
|
498
|
-
} else if (length == 127) {
|
|
499
|
-
if (wsBuffer.length < offset + 8) return;
|
|
500
|
-
length = Number(wsBuffer.readBigUInt64BE(offset));
|
|
501
|
-
offset += 8;
|
|
502
|
-
}
|
|
503
|
-
const maskLength = masked ? 4 : 0;
|
|
504
|
-
if (wsBuffer.length < offset + maskLength + length) return;
|
|
505
|
-
let payload = wsBuffer.subarray(offset + maskLength, offset + maskLength + length);
|
|
506
|
-
if (masked) {
|
|
507
|
-
const mask = wsBuffer.subarray(offset, offset + 4);
|
|
508
|
-
const unmasked = Buffer.alloc(length);
|
|
509
|
-
for (let i = 0; i < length; i++) {
|
|
510
|
-
unmasked[i] = payload[i] ^ mask[i % 4];
|
|
511
|
-
}
|
|
512
|
-
payload = unmasked;
|
|
513
|
-
} else {
|
|
514
|
-
payload = Buffer.from(payload);
|
|
515
|
-
}
|
|
516
|
-
wsBuffer = wsBuffer.subarray(offset + maskLength + length);
|
|
517
|
-
if (opcode == 0x8) {
|
|
518
|
-
finish(exitCode);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
if (opcode == 0x1) {
|
|
522
|
-
handleControl(payload.toString("utf8"));
|
|
523
|
-
continue;
|
|
524
|
-
}
|
|
525
|
-
if (opcode == 0x2) {
|
|
526
|
-
process.stdout.write(payload);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function handleControl(raw) {
|
|
532
|
-
let message = null;
|
|
533
|
-
try {
|
|
534
|
-
message = JSON.parse(raw);
|
|
535
|
-
} catch {
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
if (message?.kind == "ready") {
|
|
539
|
-
ready = true;
|
|
540
|
-
flushPendingFrames();
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (message?.kind == "done") {
|
|
544
|
-
finish(0);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
if (message?.kind == "error") {
|
|
548
|
-
process.stderr.write(String(message.message ?? "browser runtime failed") + "\\n");
|
|
549
|
-
finish(1);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function flushPendingFrames() {
|
|
554
|
-
if (!ready || !wsSocket) return;
|
|
555
|
-
while (pendingFrames.length) {
|
|
556
|
-
sendWebSocketFrame(wsSocket, 0x2, pendingFrames.shift());
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function sendWebSocketFrame(socket, opcode, payload) {
|
|
561
|
-
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload ?? []);
|
|
562
|
-
let header = null;
|
|
563
|
-
if (body.length < 126) {
|
|
564
|
-
header = Buffer.from([0x80 | opcode, body.length]);
|
|
565
|
-
} else if (body.length < 65536) {
|
|
566
|
-
header = Buffer.alloc(4);
|
|
567
|
-
header[0] = 0x80 | opcode;
|
|
568
|
-
header[1] = 126;
|
|
569
|
-
header.writeUInt16BE(body.length, 2);
|
|
570
|
-
} else {
|
|
571
|
-
header = Buffer.alloc(10);
|
|
572
|
-
header[0] = 0x80 | opcode;
|
|
573
|
-
header[1] = 127;
|
|
574
|
-
header.writeBigUInt64BE(BigInt(body.length), 2);
|
|
575
|
-
}
|
|
576
|
-
socket.write(Buffer.concat([header, body]));
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function launchBrowser(url, headlessMode) {
|
|
580
|
-
if (!headlessMode) {
|
|
581
|
-
const opener = openWithSystemBrowser(url);
|
|
582
|
-
if (opener) return opener;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const direct = openWithInstalledBrowser(url, headlessMode);
|
|
586
|
-
if (direct) return direct;
|
|
587
|
-
|
|
588
|
-
throw new Error(
|
|
589
|
-
headlessMode
|
|
590
|
-
? "could not find a headless-capable browser; set BROWSER to a Chromium/Firefox executable"
|
|
591
|
-
: "could not open a browser automatically; set BROWSER to a browser executable",
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function openWithSystemBrowser(url) {
|
|
596
|
-
if (process.env.BROWSER) {
|
|
597
|
-
return spawnBrowserCommand(process.env.BROWSER, url, false);
|
|
598
|
-
}
|
|
599
|
-
if (process.platform == "darwin") {
|
|
600
|
-
if (!hasExecutable("open")) return null;
|
|
601
|
-
return spawn("open", [url], { stdio: "ignore", detached: true });
|
|
602
|
-
}
|
|
603
|
-
if (process.platform == "win32") {
|
|
604
|
-
if (!hasExecutable("cmd")) return null;
|
|
605
|
-
return spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true });
|
|
606
|
-
}
|
|
607
|
-
if (!hasExecutable("xdg-open")) return null;
|
|
608
|
-
return spawn("xdg-open", [url], { stdio: "ignore", detached: true });
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function openWithInstalledBrowser(url, headlessMode) {
|
|
612
|
-
const browserEnv = process.env.BROWSER;
|
|
613
|
-
if (browserEnv) {
|
|
614
|
-
return spawnBrowserCommand(browserEnv, url, headlessMode);
|
|
615
|
-
}
|
|
616
|
-
const candidates = [
|
|
617
|
-
{ command: "chromium", headless: HEADLESS_FLAGS },
|
|
618
|
-
{ command: "chromium-browser", headless: HEADLESS_FLAGS },
|
|
619
|
-
{ command: "google-chrome", headless: HEADLESS_FLAGS },
|
|
620
|
-
{ command: "google-chrome-stable", headless: HEADLESS_FLAGS },
|
|
621
|
-
{ command: "chrome", headless: HEADLESS_FLAGS },
|
|
622
|
-
{ command: "msedge", headless: HEADLESS_FLAGS },
|
|
623
|
-
{ command: "firefox", headless: ["-headless"] },
|
|
624
|
-
];
|
|
625
|
-
for (const candidate of candidates) {
|
|
626
|
-
if (!hasExecutable(candidate.command)) continue;
|
|
627
|
-
return spawn(candidate.command, [...(headlessMode ? candidate.headless : []), url], {
|
|
628
|
-
stdio: "ignore",
|
|
629
|
-
detached: !headlessMode,
|
|
630
|
-
});
|
|
631
|
-
}
|
|
632
|
-
return null;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function spawnBrowserCommand(commandValue, url, headlessMode) {
|
|
636
|
-
const parts = String(commandValue)
|
|
637
|
-
.split(/\\s+/)
|
|
638
|
-
.filter((part) => part.length > 0);
|
|
639
|
-
if (!parts.length) {
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
const command = parts[0];
|
|
643
|
-
if (!hasExecutable(command)) {
|
|
644
|
-
return null;
|
|
645
|
-
}
|
|
646
|
-
const args = parts.slice(1);
|
|
647
|
-
if (headlessMode) {
|
|
648
|
-
args.push(...resolveHeadlessFlags(commandValue, command));
|
|
649
|
-
}
|
|
650
|
-
args.push(url);
|
|
651
|
-
return spawn(command, args, {
|
|
652
|
-
stdio: "ignore",
|
|
653
|
-
detached: !headlessMode,
|
|
654
|
-
});
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function resolveHeadlessFlags(commandValue, command) {
|
|
658
|
-
const lower = String(commandValue + " " + command).toLowerCase();
|
|
659
|
-
if (lower.includes("firefox")) {
|
|
660
|
-
return ["-headless"];
|
|
661
|
-
}
|
|
662
|
-
if (lower.includes("webkit") || lower.includes("minibrowser")) {
|
|
663
|
-
return ["--headless"];
|
|
664
|
-
}
|
|
665
|
-
return HEADLESS_FLAGS;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function hasExecutable(command) {
|
|
669
|
-
if (!command) return false;
|
|
670
|
-
if (command.includes("/") || command.includes("\\\\")) {
|
|
671
|
-
return existsSync(command);
|
|
672
|
-
}
|
|
673
|
-
const pathValue = process.env.PATH ?? "";
|
|
674
|
-
const suffixes = process.platform == "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
675
|
-
for (const base of pathValue.split(path.delimiter)) {
|
|
676
|
-
if (!base) continue;
|
|
677
|
-
for (const suffix of suffixes) {
|
|
678
|
-
if (existsSync(path.join(base, command + suffix))) {
|
|
679
|
-
return true;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function finish(code) {
|
|
687
|
-
if (closed) return;
|
|
688
|
-
closed = true;
|
|
689
|
-
exitCode = code;
|
|
690
|
-
try {
|
|
691
|
-
if (wsSocket) {
|
|
692
|
-
sendWebSocketFrame(wsSocket, 0x8, Buffer.alloc(0));
|
|
693
|
-
wsSocket.end();
|
|
694
|
-
}
|
|
695
|
-
} catch {}
|
|
696
|
-
try {
|
|
697
|
-
server.close();
|
|
698
|
-
} catch {}
|
|
699
|
-
if (browserProcess && !browserProcess.killed && headless) {
|
|
700
|
-
try {
|
|
701
|
-
browserProcess.kill("SIGTERM");
|
|
702
|
-
} catch {}
|
|
703
|
-
}
|
|
704
|
-
setTimeout(() => process.exit(exitCode), 25);
|
|
705
|
-
}
|
|
706
20
|
`;
|
|
707
21
|
}
|