as-test 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +4 -9
- package/assembly/index.ts +10 -15
- package/assembly/src/expectation.ts +11 -11
- package/assembly/src/fuzz.ts +11 -7
- package/assembly/src/log.ts +2 -2
- package/assembly/src/suite.ts +5 -5
- package/assembly/src/tests.ts +8 -8
- package/assembly/util/wipc.ts +5 -1
- package/bin/build-worker-pool.js +146 -142
- package/bin/build-worker.js +37 -34
- package/bin/commands/build-core.js +577 -465
- package/bin/commands/build.js +49 -29
- package/bin/commands/clean-core.js +120 -113
- package/bin/commands/clean.js +14 -8
- package/bin/commands/doctor-core.js +288 -289
- package/bin/commands/doctor.js +1 -1
- package/bin/commands/fuzz-core.js +467 -414
- package/bin/commands/fuzz.js +27 -10
- package/bin/commands/init-core.js +908 -794
- package/bin/commands/init.js +2 -2
- package/bin/commands/run-core.js +2675 -2344
- package/bin/commands/run.js +43 -25
- package/bin/commands/test.js +56 -32
- package/bin/commands/web-runner-source.js +1 -1
- package/bin/commands/web-session.js +516 -525
- package/bin/coverage-points.js +363 -341
- package/bin/crash-store.js +56 -66
- package/bin/index.js +4092 -3150
- package/bin/reporters/default.js +1090 -890
- package/bin/reporters/tap.js +319 -325
- package/bin/types.js +67 -67
- package/bin/util.js +1290 -1239
- package/bin/wipc.js +70 -73
- package/lib/build/index.d.ts +3 -1
- package/lib/build/index.js +1039 -1034
- package/lib/build/web-runner/client.js +1 -1
- package/lib/build/web-runner/html.js +1 -1
- package/lib/build/web-runner/worker.js +1 -1
- package/package.json +6 -3
- package/transform/lib/log.js +9 -5
- package/assembly/util/json.ts +0 -112
package/lib/build/index.js
CHANGED
|
@@ -12,1155 +12,1160 @@ let patchedNodeIo = false;
|
|
|
12
12
|
const wasiInstances = new WeakMap();
|
|
13
13
|
const WEB_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
14
14
|
const WEB_HEADLESS_FLAGS = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
"--headless=new",
|
|
16
|
+
"--disable-gpu",
|
|
17
|
+
"--no-first-run",
|
|
18
|
+
"--no-default-browser-check",
|
|
19
19
|
];
|
|
20
20
|
function withNodeIo(imports) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
validateImports(imports, "withNodeIo");
|
|
22
|
+
patchNodeIo();
|
|
23
|
+
return imports;
|
|
24
24
|
}
|
|
25
25
|
export async function instantiate(imports) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
26
|
+
validateImports(imports, "instantiate");
|
|
27
|
+
const wasmPath = resolveWasmPath();
|
|
28
|
+
const target = resolveRuntimeTarget();
|
|
29
|
+
if (target == "wasi") {
|
|
30
|
+
return instantiateWasiInstance(wasmPath, imports);
|
|
31
|
+
}
|
|
32
|
+
if (target == "web") {
|
|
33
|
+
return instantiateWebInstance(wasmPath, imports);
|
|
34
|
+
}
|
|
35
|
+
const helperPath = resolveBindingsHelperPath(wasmPath);
|
|
36
|
+
const kind = resolveBindingsKind(helperPath);
|
|
37
|
+
if (kind == "raw") {
|
|
38
|
+
return instantiateRawInstance(wasmPath, helperPath, imports);
|
|
39
|
+
}
|
|
40
|
+
if (kind == "esm") {
|
|
41
|
+
return instantiateEsmInstance(wasmPath, helperPath, imports);
|
|
42
|
+
}
|
|
43
|
+
if (kind == "none") {
|
|
44
|
+
return instantiateNoBindingsInstance(wasmPath, imports);
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`unsupported bindings kind "${kind}"`);
|
|
47
47
|
}
|
|
48
48
|
function resolveRuntimeTarget() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return "web";
|
|
58
|
-
return "bindings";
|
|
49
|
+
const envTarget = process.env.AS_TEST_RUNTIME_TARGET?.trim();
|
|
50
|
+
if (envTarget == "bindings" || envTarget == "wasi" || envTarget == "web") {
|
|
51
|
+
return envTarget;
|
|
52
|
+
}
|
|
53
|
+
const runnerPath = String(process.argv[1] ?? "");
|
|
54
|
+
if (runnerPath.includes(".wasi.")) return "wasi";
|
|
55
|
+
if (runnerPath.includes(".web.")) return "web";
|
|
56
|
+
return "bindings";
|
|
59
57
|
}
|
|
60
58
|
function resolveWasmPath() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
59
|
+
const envWasmPath = process.env.AS_TEST_WASM_PATH?.trim();
|
|
60
|
+
if (envWasmPath?.length) {
|
|
61
|
+
return path.resolve(envWasmPath);
|
|
62
|
+
}
|
|
63
|
+
const argWasmPath = process.argv[2]?.trim();
|
|
64
|
+
if (argWasmPath?.length) {
|
|
65
|
+
return path.resolve(argWasmPath);
|
|
66
|
+
}
|
|
67
|
+
const runnerPath = String(process.argv[1] ?? "");
|
|
68
|
+
const runnerName = path.basename(runnerPath || "runner.js");
|
|
69
|
+
throw new Error(
|
|
70
|
+
[
|
|
71
|
+
"No wasm artifact was provided for this runner.",
|
|
72
|
+
"",
|
|
73
|
+
`Direct usage: node .as-test/runners/${runnerName} .as-test/build/<artifact>.wasm`,
|
|
74
|
+
"Managed usage: bunx ast test --mode <mode>",
|
|
75
|
+
"",
|
|
76
|
+
"as-test normally sets AS_TEST_WASM_PATH automatically when it launches the runner.",
|
|
77
|
+
].join("\n"),
|
|
78
|
+
);
|
|
79
79
|
}
|
|
80
80
|
function resolveBindingsHelperPath(wasmPath) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
const envHelperPath = process.env.AS_TEST_HELPER_PATH?.trim();
|
|
82
|
+
if (envHelperPath?.length) {
|
|
83
|
+
return path.resolve(envHelperPath);
|
|
84
|
+
}
|
|
85
|
+
const candidate = wasmPath.replace(/\.wasm$/i, ".js");
|
|
86
|
+
return fs.existsSync(candidate) ? candidate : "";
|
|
87
87
|
}
|
|
88
88
|
function resolveBindingsKind(helperPath) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
const envKind = process.env.AS_TEST_BINDINGS_KIND?.trim();
|
|
90
|
+
if (envKind == "raw" || envKind == "esm" || envKind == "none") {
|
|
91
|
+
return envKind;
|
|
92
|
+
}
|
|
93
|
+
if (!helperPath.length) {
|
|
94
|
+
return "none";
|
|
95
|
+
}
|
|
96
|
+
const source = fs.readFileSync(helperPath, "utf8");
|
|
97
|
+
if (/\bexport\s+(?:async\s+)?function\s+instantiate\b/.test(source)) {
|
|
98
|
+
return "raw";
|
|
99
|
+
}
|
|
100
|
+
return "esm";
|
|
101
101
|
}
|
|
102
102
|
function validateImports(imports, fnName) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
if (arguments.length < 1) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`${fnName}(imports) requires an imports object; pass {} when unused`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (!imports || typeof imports != "object" || Array.isArray(imports)) {
|
|
109
|
+
throw new Error(`${fnName}(imports) requires a non-null imports object`);
|
|
110
|
+
}
|
|
109
111
|
}
|
|
110
112
|
function patchNodeIo() {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
process.stdin.read = ((size) => readExact(Number(size ?? 0)));
|
|
113
|
+
if (patchedNodeIo) return;
|
|
114
|
+
patchedNodeIo = true;
|
|
115
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
116
|
+
process.stdout.write = (chunk, ...args) => {
|
|
117
|
+
if (chunk instanceof ArrayBuffer) {
|
|
118
|
+
writeRaw(chunk);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return originalWrite(chunk, ...args);
|
|
122
|
+
};
|
|
123
|
+
process.stdin.read = (size) => readExact(Number(size ?? 0));
|
|
123
124
|
}
|
|
124
125
|
function readExact(length) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (!read)
|
|
142
|
-
break;
|
|
143
|
-
offset += read;
|
|
126
|
+
const out = Buffer.alloc(length);
|
|
127
|
+
let offset = 0;
|
|
128
|
+
while (offset < length) {
|
|
129
|
+
let read = 0;
|
|
130
|
+
try {
|
|
131
|
+
read = fs.readSync(0, out, offset, length - offset, null);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (
|
|
134
|
+
error &&
|
|
135
|
+
typeof error == "object" &&
|
|
136
|
+
"code" in error &&
|
|
137
|
+
error.code == "EAGAIN"
|
|
138
|
+
) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
144
142
|
}
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
if (!read) break;
|
|
144
|
+
offset += read;
|
|
145
|
+
}
|
|
146
|
+
const view = out.subarray(0, offset);
|
|
147
|
+
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
|
|
147
148
|
}
|
|
148
149
|
function writeRaw(data) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!written)
|
|
166
|
-
continue;
|
|
167
|
-
offset += written;
|
|
150
|
+
const view = Buffer.from(data);
|
|
151
|
+
let offset = 0;
|
|
152
|
+
while (offset < view.byteLength) {
|
|
153
|
+
let written = 0;
|
|
154
|
+
try {
|
|
155
|
+
written = fs.writeSync(1, view, offset, view.byteLength - offset);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (
|
|
158
|
+
error &&
|
|
159
|
+
typeof error == "object" &&
|
|
160
|
+
"code" in error &&
|
|
161
|
+
error.code == "EAGAIN"
|
|
162
|
+
) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
168
166
|
}
|
|
167
|
+
if (!written) continue;
|
|
168
|
+
offset += written;
|
|
169
|
+
}
|
|
169
170
|
}
|
|
170
171
|
function mergeImports(...groups) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
172
|
+
const out = {};
|
|
173
|
+
for (const group of groups) {
|
|
174
|
+
if (!group || typeof group != "object" || Array.isArray(group)) continue;
|
|
175
|
+
for (const [key, value] of Object.entries(group)) {
|
|
176
|
+
if (
|
|
177
|
+
value &&
|
|
178
|
+
typeof value == "object" &&
|
|
179
|
+
!Array.isArray(value) &&
|
|
180
|
+
typeof value != "function"
|
|
181
|
+
) {
|
|
182
|
+
out[key] = mergeImports(out[key], value);
|
|
183
|
+
} else {
|
|
184
|
+
out[key] = value;
|
|
185
|
+
}
|
|
186
186
|
}
|
|
187
|
-
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
188
189
|
}
|
|
189
190
|
async function instantiateRawInstance(wasmPath, helperPath, imports) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
191
|
+
validateImports(imports, "instantiateRawInstance");
|
|
192
|
+
if (!helperPath.length) {
|
|
193
|
+
throw new Error("bindings kind is raw but AS_TEST_HELPER_PATH is not set");
|
|
194
|
+
}
|
|
195
|
+
const binary = fs.readFileSync(wasmPath);
|
|
196
|
+
const module = new WebAssembly.Module(binary);
|
|
197
|
+
const helper = await import(
|
|
198
|
+
`${pathToFileURL(helperPath).href}?t=${Date.now()}`
|
|
199
|
+
);
|
|
200
|
+
if (typeof helper.instantiate != "function") {
|
|
201
|
+
throw new Error("bindings helper missing instantiate export");
|
|
202
|
+
}
|
|
203
|
+
const mergedImports = mergeImports(withNodeIo({}), imports);
|
|
204
|
+
const instance = await captureHelperInstance(async () => {
|
|
205
|
+
await helper.instantiate(module, mergedImports);
|
|
206
|
+
});
|
|
207
|
+
return decorateInstance(instance, "bindings");
|
|
205
208
|
}
|
|
206
209
|
async function instantiateEsmInstance(wasmPath, helperPath, imports) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
210
|
+
validateImports(imports, "instantiateEsmInstance");
|
|
211
|
+
if (!helperPath.length) {
|
|
212
|
+
throw new Error("bindings kind is esm but AS_TEST_HELPER_PATH is not set");
|
|
213
|
+
}
|
|
214
|
+
if (hasUserImports(imports)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
"esm bindings do not support custom imports in as-test/lib; pass {} or switch to raw bindings",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const instance = await captureHelperInstance(async () => {
|
|
220
|
+
await import(`${pathToFileURL(helperPath).href}?t=${Date.now()}`);
|
|
221
|
+
});
|
|
222
|
+
return decorateInstance(instance, "bindings");
|
|
218
223
|
}
|
|
219
224
|
async function instantiateNoBindingsInstance(wasmPath, imports) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
validateImports(imports, "instantiateNoBindingsInstance");
|
|
226
|
+
const instance = await instantiateModuleInstance(wasmPath, imports);
|
|
227
|
+
return decorateInstance(instance, "bindings");
|
|
223
228
|
}
|
|
224
229
|
async function instantiateWasiInstance(wasmPath, imports) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
230
|
+
validateImports(imports, "instantiateWasiInstance");
|
|
231
|
+
suppressExperimentalWasiWarning();
|
|
232
|
+
const { WASI } = await import("wasi");
|
|
233
|
+
const binary = fs.readFileSync(wasmPath);
|
|
234
|
+
const module = new WebAssembly.Module(binary);
|
|
235
|
+
const wasi = new WASI({
|
|
236
|
+
version: "preview1",
|
|
237
|
+
args: [wasmPath],
|
|
238
|
+
env: process.env,
|
|
239
|
+
preopens: {},
|
|
240
|
+
});
|
|
241
|
+
const mergedImports = createWasmImports(module, imports);
|
|
242
|
+
mergedImports.wasi_snapshot_preview1 = wasi.wasiImport;
|
|
243
|
+
const instance = new WebAssembly.Instance(module, mergedImports);
|
|
244
|
+
wasiInstances.set(instance, wasi);
|
|
245
|
+
return decorateInstance(instance, "wasi");
|
|
241
246
|
}
|
|
242
247
|
async function instantiateWebInstance(wasmPath, imports) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
248
|
+
validateImports(imports, "instantiateWebInstance");
|
|
249
|
+
if (hasUserImports(imports)) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
"web runtime does not support custom imports in the default runner; pass {} or write a custom web runner",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const bindingsKind = process.env.AS_TEST_BINDINGS_KIND || "raw";
|
|
255
|
+
const helperPath = process.env.AS_TEST_HELPER_PATH
|
|
256
|
+
? path.resolve(process.cwd(), process.env.AS_TEST_HELPER_PATH)
|
|
257
|
+
: wasmPath.replace(/\.wasm$/, ".js");
|
|
258
|
+
const wasmUrlPath = "/" + path.basename(wasmPath);
|
|
259
|
+
const helperUrlPath = "/" + path.basename(helperPath);
|
|
260
|
+
if (!fs.existsSync(wasmPath)) {
|
|
261
|
+
throw new Error(`missing wasm artifact: ${wasmPath}`);
|
|
262
|
+
}
|
|
263
|
+
if (bindingsKind != "none" && !fs.existsSync(helperPath)) {
|
|
264
|
+
throw new Error(`missing bindings helper: ${helperPath}`);
|
|
265
|
+
}
|
|
266
|
+
const html = buildWebRunnerHtml();
|
|
267
|
+
const client = buildWebRunnerClientSource();
|
|
268
|
+
const worker = buildWebRunnerWorkerSource();
|
|
269
|
+
const headless = process.argv.includes("--headless");
|
|
270
|
+
const webRuntimeEnv = {
|
|
271
|
+
AS_TEST_RUNTIME_TARGET: "web",
|
|
272
|
+
AS_TEST_WASM_PATH: wasmUrlPath,
|
|
273
|
+
AS_TEST_BINDINGS_KIND: bindingsKind,
|
|
274
|
+
...(bindingsKind != "none" ? { AS_TEST_HELPER_PATH: helperUrlPath } : {}),
|
|
275
|
+
};
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
let resolved = false;
|
|
278
|
+
let finished = false;
|
|
279
|
+
let ready = false;
|
|
280
|
+
let wsSocket = null;
|
|
281
|
+
let wsBuffer = Buffer.alloc(0);
|
|
282
|
+
let stdinBuffer = Buffer.alloc(0);
|
|
283
|
+
let browserProcess = null;
|
|
284
|
+
let browserStderr = "";
|
|
285
|
+
let browserRetryTimer = null;
|
|
286
|
+
let browserStartupTimer = null;
|
|
287
|
+
let browserTempProfileDir = null;
|
|
288
|
+
let ownsBrowserProcess = false;
|
|
289
|
+
const pendingFrames = [];
|
|
290
|
+
const rejectOnce = (error) => {
|
|
291
|
+
if (resolved || finished) return;
|
|
292
|
+
finished = true;
|
|
293
|
+
reject(error);
|
|
294
|
+
cleanup();
|
|
269
295
|
};
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
stdinBuffer = Buffer.concat([stdinBuffer, chunk]);
|
|
442
|
-
while (stdinBuffer.length >= 9) {
|
|
443
|
-
const length = stdinBuffer.readUInt32LE(5);
|
|
444
|
-
const frameSize = 9 + length;
|
|
445
|
-
if (stdinBuffer.length < frameSize)
|
|
446
|
-
return;
|
|
447
|
-
const frame = stdinBuffer.subarray(0, frameSize);
|
|
448
|
-
stdinBuffer = stdinBuffer.subarray(frameSize);
|
|
449
|
-
if (ready && wsSocket) {
|
|
450
|
-
sendWebSocketFrame(wsSocket, 0x2, frame);
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
pendingFrames.push(Buffer.from(frame));
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
};
|
|
457
|
-
const onStdinEnd = () => {
|
|
458
|
-
stdinBuffer = Buffer.alloc(0);
|
|
459
|
-
};
|
|
460
|
-
const onSigint = () => finish(130);
|
|
461
|
-
const onSigterm = () => finish(143);
|
|
462
|
-
const server = http.createServer((req, res) => {
|
|
463
|
-
const headers = {
|
|
464
|
-
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
465
|
-
"Cross-Origin-Opener-Policy": "same-origin",
|
|
466
|
-
"Cache-Control": "no-store",
|
|
467
|
-
};
|
|
468
|
-
const url = req.url ?? "/";
|
|
469
|
-
if (url == "/" || url.startsWith("/?")) {
|
|
470
|
-
res.writeHead(200, {
|
|
471
|
-
...headers,
|
|
472
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
473
|
-
});
|
|
474
|
-
res.end(html.replace("</body>", " <script>window.__AS_TEST_ENV__ = " +
|
|
475
|
-
JSON.stringify(webRuntimeEnv) +
|
|
476
|
-
";</script>\n </body>"));
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
if (url == "/client.js") {
|
|
480
|
-
res.writeHead(200, {
|
|
481
|
-
...headers,
|
|
482
|
-
"Content-Type": "text/javascript; charset=utf-8",
|
|
483
|
-
});
|
|
484
|
-
res.end(client);
|
|
485
|
-
return;
|
|
486
|
-
}
|
|
487
|
-
if (url == "/worker.js") {
|
|
488
|
-
res.writeHead(200, {
|
|
489
|
-
...headers,
|
|
490
|
-
"Content-Type": "text/javascript; charset=utf-8",
|
|
491
|
-
});
|
|
492
|
-
res.end(worker);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
if (url == helperUrlPath) {
|
|
496
|
-
if (bindingsKind == "none") {
|
|
497
|
-
res.writeHead(404, headers);
|
|
498
|
-
res.end("not found");
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
res.writeHead(200, {
|
|
502
|
-
...headers,
|
|
503
|
-
"Content-Type": "text/javascript; charset=utf-8",
|
|
504
|
-
});
|
|
505
|
-
res.end(fs.readFileSync(helperPath, "utf8"));
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
if (url == wasmUrlPath) {
|
|
509
|
-
res.writeHead(200, {
|
|
510
|
-
...headers,
|
|
511
|
-
"Content-Type": "application/wasm",
|
|
512
|
-
});
|
|
513
|
-
res.end(fs.readFileSync(wasmPath));
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
res.writeHead(404, headers);
|
|
517
|
-
res.end("not found");
|
|
296
|
+
const finish = (code) => {
|
|
297
|
+
if (finished) return;
|
|
298
|
+
finished = true;
|
|
299
|
+
cleanup();
|
|
300
|
+
if (!resolved && code != 0) {
|
|
301
|
+
reject(new Error(`web runtime exited with code ${code}`));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (!resolved) {
|
|
305
|
+
reject(new Error("web runtime exited before instantiation completed"));
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const cleanup = () => {
|
|
309
|
+
process.stdin.off("data", onStdinData);
|
|
310
|
+
process.stdin.off("end", onStdinEnd);
|
|
311
|
+
try {
|
|
312
|
+
process.stdin.pause();
|
|
313
|
+
} catch {}
|
|
314
|
+
process.off("SIGINT", onSigint);
|
|
315
|
+
process.off("SIGTERM", onSigterm);
|
|
316
|
+
try {
|
|
317
|
+
wsSocket?.end();
|
|
318
|
+
} catch {}
|
|
319
|
+
try {
|
|
320
|
+
wsSocket?.destroy();
|
|
321
|
+
} catch {}
|
|
322
|
+
try {
|
|
323
|
+
server.close();
|
|
324
|
+
} catch {}
|
|
325
|
+
try {
|
|
326
|
+
server.unref();
|
|
327
|
+
} catch {}
|
|
328
|
+
if (browserRetryTimer) {
|
|
329
|
+
clearInterval(browserRetryTimer);
|
|
330
|
+
browserRetryTimer = null;
|
|
331
|
+
}
|
|
332
|
+
if (browserStartupTimer) {
|
|
333
|
+
clearTimeout(browserStartupTimer);
|
|
334
|
+
browserStartupTimer = null;
|
|
335
|
+
}
|
|
336
|
+
if (browserTempProfileDir) {
|
|
337
|
+
try {
|
|
338
|
+
fs.rmSync(browserTempProfileDir, { recursive: true, force: true });
|
|
339
|
+
} catch {}
|
|
340
|
+
browserTempProfileDir = null;
|
|
341
|
+
}
|
|
342
|
+
if (browserProcess && ownsBrowserProcess && !browserProcess.killed) {
|
|
343
|
+
killOwnedBrowserProcess(browserProcess);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const sendControl = (message) => {
|
|
347
|
+
if (!wsSocket) return;
|
|
348
|
+
sendWebSocketFrame(wsSocket, 0x1, Buffer.from(JSON.stringify(message)));
|
|
349
|
+
};
|
|
350
|
+
const flushPendingFrames = () => {
|
|
351
|
+
if (!ready || !wsSocket) return;
|
|
352
|
+
while (pendingFrames.length) {
|
|
353
|
+
sendWebSocketFrame(wsSocket, 0x2, pendingFrames.shift());
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const onControl = (raw) => {
|
|
357
|
+
let message = null;
|
|
358
|
+
try {
|
|
359
|
+
message = JSON.parse(raw);
|
|
360
|
+
} catch {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (message?.kind == "ready") {
|
|
364
|
+
ready = true;
|
|
365
|
+
flushPendingFrames();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (message?.kind == "instantiated") {
|
|
369
|
+
if (resolved) return;
|
|
370
|
+
resolved = true;
|
|
371
|
+
resolve(
|
|
372
|
+
createWebInstanceController(() => {
|
|
373
|
+
sendControl({ kind: "start" });
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (message?.kind == "done") {
|
|
379
|
+
finish(0);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (message?.kind == "error") {
|
|
383
|
+
rejectOnce(
|
|
384
|
+
new Error(String(message.message ?? "browser runtime failed")),
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const onWebSocketData = (chunk) => {
|
|
389
|
+
wsBuffer = Buffer.concat([wsBuffer, chunk]);
|
|
390
|
+
while (wsBuffer.length >= 2) {
|
|
391
|
+
const first = wsBuffer[0];
|
|
392
|
+
const second = wsBuffer[1];
|
|
393
|
+
const opcode = first & 0x0f;
|
|
394
|
+
const masked = (second & 0x80) !== 0;
|
|
395
|
+
let length = second & 0x7f;
|
|
396
|
+
let offset = 2;
|
|
397
|
+
if (length == 126) {
|
|
398
|
+
if (wsBuffer.length < offset + 2) return;
|
|
399
|
+
length = wsBuffer.readUInt16BE(offset);
|
|
400
|
+
offset += 2;
|
|
401
|
+
} else if (length == 127) {
|
|
402
|
+
if (wsBuffer.length < offset + 8) return;
|
|
403
|
+
length = Number(wsBuffer.readBigUInt64BE(offset));
|
|
404
|
+
offset += 8;
|
|
405
|
+
}
|
|
406
|
+
const maskLength = masked ? 4 : 0;
|
|
407
|
+
if (wsBuffer.length < offset + maskLength + length) return;
|
|
408
|
+
let payload = wsBuffer.subarray(
|
|
409
|
+
offset + maskLength,
|
|
410
|
+
offset + maskLength + length,
|
|
411
|
+
);
|
|
412
|
+
if (masked) {
|
|
413
|
+
const mask = wsBuffer.subarray(offset, offset + 4);
|
|
414
|
+
const unmasked = Buffer.alloc(length);
|
|
415
|
+
for (let i = 0; i < length; i++) {
|
|
416
|
+
unmasked[i] = payload[i] ^ mask[i % 4];
|
|
417
|
+
}
|
|
418
|
+
payload = unmasked;
|
|
419
|
+
} else {
|
|
420
|
+
payload = Buffer.from(payload);
|
|
421
|
+
}
|
|
422
|
+
wsBuffer = wsBuffer.subarray(offset + maskLength + length);
|
|
423
|
+
if (opcode == 0x8) {
|
|
424
|
+
finish(0);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (opcode == 0x1) {
|
|
428
|
+
onControl(payload.toString("utf8"));
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (opcode == 0x2) {
|
|
432
|
+
process.stdout.write(payload);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const onStdinData = (chunk) => {
|
|
437
|
+
stdinBuffer = Buffer.concat([stdinBuffer, chunk]);
|
|
438
|
+
while (stdinBuffer.length >= 9) {
|
|
439
|
+
const length = stdinBuffer.readUInt32LE(5);
|
|
440
|
+
const frameSize = 9 + length;
|
|
441
|
+
if (stdinBuffer.length < frameSize) return;
|
|
442
|
+
const frame = stdinBuffer.subarray(0, frameSize);
|
|
443
|
+
stdinBuffer = stdinBuffer.subarray(frameSize);
|
|
444
|
+
if (ready && wsSocket) {
|
|
445
|
+
sendWebSocketFrame(wsSocket, 0x2, frame);
|
|
446
|
+
} else {
|
|
447
|
+
pendingFrames.push(Buffer.from(frame));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
const onStdinEnd = () => {
|
|
452
|
+
stdinBuffer = Buffer.alloc(0);
|
|
453
|
+
};
|
|
454
|
+
const onSigint = () => finish(130);
|
|
455
|
+
const onSigterm = () => finish(143);
|
|
456
|
+
const server = http.createServer((req, res) => {
|
|
457
|
+
const headers = {
|
|
458
|
+
"Cross-Origin-Embedder-Policy": "require-corp",
|
|
459
|
+
"Cross-Origin-Opener-Policy": "same-origin",
|
|
460
|
+
"Cache-Control": "no-store",
|
|
461
|
+
};
|
|
462
|
+
const url = req.url ?? "/";
|
|
463
|
+
if (url == "/" || url.startsWith("/?")) {
|
|
464
|
+
res.writeHead(200, {
|
|
465
|
+
...headers,
|
|
466
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
518
467
|
});
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
"HTTP/1.1 101 Switching Protocols",
|
|
534
|
-
"Upgrade: websocket",
|
|
535
|
-
"Connection: Upgrade",
|
|
536
|
-
"Sec-WebSocket-Accept: " + accept,
|
|
537
|
-
"",
|
|
538
|
-
"",
|
|
539
|
-
].join("\r\n"));
|
|
540
|
-
wsSocket = socket;
|
|
541
|
-
wsBuffer = Buffer.alloc(0);
|
|
542
|
-
if (browserStartupTimer) {
|
|
543
|
-
clearTimeout(browserStartupTimer);
|
|
544
|
-
browserStartupTimer = null;
|
|
545
|
-
}
|
|
546
|
-
socket.on("data", (chunk) => onWebSocketData(chunk));
|
|
547
|
-
socket.on("close", () => {
|
|
548
|
-
wsSocket = null;
|
|
549
|
-
if (!finished)
|
|
550
|
-
finish(1);
|
|
551
|
-
});
|
|
552
|
-
socket.on("error", (error) => {
|
|
553
|
-
if (!finished) {
|
|
554
|
-
rejectOnce(error instanceof Error ? error : new Error(String(error)));
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
flushPendingFrames();
|
|
468
|
+
res.end(
|
|
469
|
+
html.replace(
|
|
470
|
+
"</body>",
|
|
471
|
+
" <script>window.__AS_TEST_ENV__ = " +
|
|
472
|
+
JSON.stringify(webRuntimeEnv) +
|
|
473
|
+
";</script>\n </body>",
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (url == "/client.js") {
|
|
479
|
+
res.writeHead(200, {
|
|
480
|
+
...headers,
|
|
481
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
558
482
|
});
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
483
|
+
res.end(client);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (url == "/worker.js") {
|
|
487
|
+
res.writeHead(200, {
|
|
488
|
+
...headers,
|
|
489
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
490
|
+
});
|
|
491
|
+
res.end(worker);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (url == helperUrlPath) {
|
|
495
|
+
if (bindingsKind == "none") {
|
|
496
|
+
res.writeHead(404, headers);
|
|
497
|
+
res.end("not found");
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
res.writeHead(200, {
|
|
501
|
+
...headers,
|
|
502
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
503
|
+
});
|
|
504
|
+
res.end(fs.readFileSync(helperPath, "utf8"));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (url == wasmUrlPath) {
|
|
508
|
+
res.writeHead(200, {
|
|
509
|
+
...headers,
|
|
510
|
+
"Content-Type": "application/wasm",
|
|
511
|
+
});
|
|
512
|
+
res.end(fs.readFileSync(wasmPath));
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
res.writeHead(404, headers);
|
|
516
|
+
res.end("not found");
|
|
517
|
+
});
|
|
518
|
+
server.on("upgrade", (req, socket) => {
|
|
519
|
+
if ((req.url ?? "") != "/ws") {
|
|
520
|
+
socket.destroy();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const key = String(req.headers["sec-websocket-key"] ?? "");
|
|
524
|
+
if (!key) {
|
|
525
|
+
socket.destroy();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const accept = createHash("sha1")
|
|
529
|
+
.update(key + WEB_MAGIC)
|
|
530
|
+
.digest("base64");
|
|
531
|
+
socket.write(
|
|
532
|
+
[
|
|
533
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
534
|
+
"Upgrade: websocket",
|
|
535
|
+
"Connection: Upgrade",
|
|
536
|
+
"Sec-WebSocket-Accept: " + accept,
|
|
537
|
+
"",
|
|
538
|
+
"",
|
|
539
|
+
].join("\r\n"),
|
|
540
|
+
);
|
|
541
|
+
wsSocket = socket;
|
|
542
|
+
wsBuffer = Buffer.alloc(0);
|
|
543
|
+
if (browserStartupTimer) {
|
|
544
|
+
clearTimeout(browserStartupTimer);
|
|
545
|
+
browserStartupTimer = null;
|
|
546
|
+
}
|
|
547
|
+
socket.on("data", (chunk) => onWebSocketData(chunk));
|
|
548
|
+
socket.on("close", () => {
|
|
549
|
+
wsSocket = null;
|
|
550
|
+
if (!finished) finish(1);
|
|
551
|
+
});
|
|
552
|
+
socket.on("error", (error) => {
|
|
553
|
+
if (!finished) {
|
|
554
|
+
rejectOnce(error instanceof Error ? error : new Error(String(error)));
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
flushPendingFrames();
|
|
558
|
+
});
|
|
559
|
+
process.stdin.on("data", onStdinData);
|
|
560
|
+
process.stdin.on("end", onStdinEnd);
|
|
561
|
+
process.on("SIGINT", onSigint);
|
|
562
|
+
process.on("SIGTERM", onSigterm);
|
|
563
|
+
server.listen(0, "127.0.0.1", () => {
|
|
564
|
+
const address = server.address();
|
|
565
|
+
if (!address || typeof address == "string") {
|
|
566
|
+
rejectOnce(new Error("failed to determine local web runner address"));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const url = "http://127.0.0.1:" + address.port + "/";
|
|
570
|
+
try {
|
|
571
|
+
const launched = launchWebBrowser(url, headless);
|
|
572
|
+
browserProcess = launched.process;
|
|
573
|
+
browserTempProfileDir = launched.tempProfileDir;
|
|
574
|
+
ownsBrowserProcess = launched.ownsProcess;
|
|
575
|
+
if (browserProcess.stderr) {
|
|
576
|
+
browserProcess.stderr.on("data", (chunk) => {
|
|
577
|
+
browserStderr = appendBrowserOutput(
|
|
578
|
+
browserStderr,
|
|
579
|
+
typeof chunk == "string" ? chunk : chunk.toString("utf8"),
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (!headless) {
|
|
584
|
+
browserRetryTimer = setInterval(() => {
|
|
585
|
+
if (finished || resolved || ready || wsSocket) return;
|
|
570
586
|
try {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (headless) {
|
|
592
|
-
browserStartupTimer = setTimeout(() => {
|
|
593
|
-
if (finished || resolved || ready || wsSocket)
|
|
594
|
-
return;
|
|
595
|
-
rejectOnce(new Error("headless web browser did not connect to the local runner"));
|
|
596
|
-
}, 10000);
|
|
597
|
-
browserStartupTimer.unref?.();
|
|
598
|
-
browserProcess.on("close", (code) => {
|
|
599
|
-
if (finished)
|
|
600
|
-
return;
|
|
601
|
-
if (resolved) {
|
|
602
|
-
finish(code ?? 0);
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
if (code && code != 0) {
|
|
606
|
-
rejectOnce(new Error(formatBrowserExitError(code, browserStderr)));
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
if (ready || wsSocket) {
|
|
610
|
-
finish(code ?? 0);
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
}
|
|
587
|
+
openWithReusableBrowserWindow(url);
|
|
588
|
+
} catch {}
|
|
589
|
+
}, 750);
|
|
590
|
+
browserRetryTimer.unref?.();
|
|
591
|
+
}
|
|
592
|
+
if (headless) {
|
|
593
|
+
browserStartupTimer = setTimeout(() => {
|
|
594
|
+
if (finished || resolved || ready || wsSocket) return;
|
|
595
|
+
rejectOnce(
|
|
596
|
+
new Error(
|
|
597
|
+
"headless web browser did not connect to the local runner",
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
}, 10000);
|
|
601
|
+
browserStartupTimer.unref?.();
|
|
602
|
+
browserProcess.on("close", (code) => {
|
|
603
|
+
if (finished) return;
|
|
604
|
+
if (resolved) {
|
|
605
|
+
finish(code ?? 0);
|
|
606
|
+
return;
|
|
614
607
|
}
|
|
615
|
-
|
|
616
|
-
|
|
608
|
+
if (code && code != 0) {
|
|
609
|
+
rejectOnce(
|
|
610
|
+
new Error(formatBrowserExitError(code, browserStderr)),
|
|
611
|
+
);
|
|
612
|
+
return;
|
|
617
613
|
}
|
|
618
|
-
|
|
614
|
+
if (ready || wsSocket) {
|
|
615
|
+
finish(code ?? 0);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
} catch (error) {
|
|
620
|
+
rejectOnce(error instanceof Error ? error : new Error(String(error)));
|
|
621
|
+
}
|
|
619
622
|
});
|
|
623
|
+
});
|
|
620
624
|
}
|
|
621
625
|
function hasUserImports(imports) {
|
|
622
|
-
|
|
626
|
+
return Object.keys(imports).length > 0;
|
|
623
627
|
}
|
|
624
628
|
function createWasmImports(module, imports) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
629
|
+
const mergedImports = mergeImports(withNodeIo({}), imports);
|
|
630
|
+
if (!mergedImports.env || typeof mergedImports.env != "object") {
|
|
631
|
+
mergedImports.env = {};
|
|
632
|
+
}
|
|
633
|
+
for (const entry of WebAssembly.Module.imports(module)) {
|
|
634
|
+
if (
|
|
635
|
+
entry.module == "env" &&
|
|
636
|
+
entry.kind == "function" &&
|
|
637
|
+
!(entry.name in mergedImports.env)
|
|
638
|
+
) {
|
|
639
|
+
mergedImports.env[entry.name] = () => 0;
|
|
635
640
|
}
|
|
636
|
-
|
|
641
|
+
}
|
|
642
|
+
return mergedImports;
|
|
637
643
|
}
|
|
638
644
|
let patchedWasiWarning = false;
|
|
639
645
|
function suppressExperimentalWasiWarning() {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
646
|
+
if (patchedWasiWarning) return;
|
|
647
|
+
patchedWasiWarning = true;
|
|
648
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
649
|
+
process.emitWarning = (warning, ...args) => {
|
|
650
|
+
const type = typeof args[0] == "string" ? args[0] : "";
|
|
651
|
+
const name =
|
|
652
|
+
warning && typeof warning == "object" && "name" in warning
|
|
653
|
+
? String(warning.name ?? type)
|
|
654
|
+
: type;
|
|
655
|
+
const message =
|
|
656
|
+
typeof warning == "string"
|
|
657
|
+
? warning
|
|
658
|
+
: String(
|
|
659
|
+
warning && typeof warning == "object" && "message" in warning
|
|
660
|
+
? (warning.message ?? "")
|
|
661
|
+
: "",
|
|
662
|
+
);
|
|
663
|
+
if (
|
|
664
|
+
name == "ExperimentalWarning" &&
|
|
665
|
+
message.includes("WASI is an experimental feature")
|
|
666
|
+
) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
return originalEmitWarning(warning, ...args);
|
|
670
|
+
};
|
|
660
671
|
}
|
|
661
672
|
async function instantiateModuleInstance(wasmPath, imports) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
673
|
+
const binary = fs.readFileSync(wasmPath);
|
|
674
|
+
const module = new WebAssembly.Module(binary);
|
|
675
|
+
return new WebAssembly.Instance(module, createWasmImports(module, imports));
|
|
665
676
|
}
|
|
666
677
|
function decorateInstance(instance, target) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
return exportsProxy;
|
|
687
|
-
return Reflect.get(targetInstance, prop, receiver);
|
|
688
|
-
},
|
|
689
|
-
});
|
|
678
|
+
const exports = instance.exports;
|
|
679
|
+
const start = createStartFunction(instance, target, exports);
|
|
680
|
+
if (!start) return instance;
|
|
681
|
+
const exportsProxy = new Proxy(exports, {
|
|
682
|
+
get(targetExports, prop, receiver) {
|
|
683
|
+
if (prop == "start") return start;
|
|
684
|
+
return Reflect.get(targetExports, prop, receiver);
|
|
685
|
+
},
|
|
686
|
+
has(targetExports, prop) {
|
|
687
|
+
if (prop == "start") return true;
|
|
688
|
+
return Reflect.has(targetExports, prop);
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
return new Proxy(instance, {
|
|
692
|
+
get(targetInstance, prop, receiver) {
|
|
693
|
+
if (prop == "exports") return exportsProxy;
|
|
694
|
+
return Reflect.get(targetInstance, prop, receiver);
|
|
695
|
+
},
|
|
696
|
+
});
|
|
690
697
|
}
|
|
691
698
|
function createStartFunction(instance, target, exports) {
|
|
692
|
-
|
|
693
|
-
return () => {
|
|
694
|
-
const wasi = wasiInstances.get(instance);
|
|
695
|
-
if (!wasi) {
|
|
696
|
-
throw new Error("WASI runtime state missing for instance");
|
|
697
|
-
}
|
|
698
|
-
wasi.start(instance);
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
const startFn = exports._start;
|
|
702
|
-
if (typeof startFn != "function") {
|
|
703
|
-
return null;
|
|
704
|
-
}
|
|
699
|
+
if (target == "wasi") {
|
|
705
700
|
return () => {
|
|
706
|
-
|
|
701
|
+
const wasi = wasiInstances.get(instance);
|
|
702
|
+
if (!wasi) {
|
|
703
|
+
throw new Error("WASI runtime state missing for instance");
|
|
704
|
+
}
|
|
705
|
+
wasi.start(instance);
|
|
707
706
|
};
|
|
707
|
+
}
|
|
708
|
+
const startFn = exports._start;
|
|
709
|
+
if (typeof startFn != "function") {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return () => {
|
|
713
|
+
startFn();
|
|
714
|
+
};
|
|
708
715
|
}
|
|
709
716
|
function createWebInstanceController(start) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
717
|
+
const exportsProxy = new Proxy(
|
|
718
|
+
{},
|
|
719
|
+
{
|
|
720
|
+
get(_target, prop) {
|
|
721
|
+
if (prop == "start") return start;
|
|
722
|
+
return undefined;
|
|
723
|
+
},
|
|
724
|
+
has(_target, prop) {
|
|
725
|
+
return prop == "start";
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
);
|
|
729
|
+
return new Proxy(
|
|
730
|
+
{},
|
|
731
|
+
{
|
|
732
|
+
get(_target, prop) {
|
|
733
|
+
if (prop == "exports") return exportsProxy;
|
|
734
|
+
return undefined;
|
|
735
|
+
},
|
|
736
|
+
has(_target, prop) {
|
|
737
|
+
return prop == "exports";
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
);
|
|
730
741
|
}
|
|
731
742
|
function sendWebSocketFrame(socket, opcode, payload) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
socket.write(Buffer.concat([header, payload]));
|
|
743
|
+
let header;
|
|
744
|
+
if (payload.length < 126) {
|
|
745
|
+
header = Buffer.from([0x80 | opcode, payload.length]);
|
|
746
|
+
} else if (payload.length < 65536) {
|
|
747
|
+
header = Buffer.alloc(4);
|
|
748
|
+
header[0] = 0x80 | opcode;
|
|
749
|
+
header[1] = 126;
|
|
750
|
+
header.writeUInt16BE(payload.length, 2);
|
|
751
|
+
} else {
|
|
752
|
+
header = Buffer.alloc(10);
|
|
753
|
+
header[0] = 0x80 | opcode;
|
|
754
|
+
header[1] = 127;
|
|
755
|
+
header.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
756
|
+
}
|
|
757
|
+
socket.write(Buffer.concat([header, payload]));
|
|
749
758
|
}
|
|
750
759
|
function launchWebBrowser(url, headless) {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
+
if (!headless) {
|
|
761
|
+
const reused = openWithReusableBrowserWindow(url);
|
|
762
|
+
if (reused) {
|
|
763
|
+
return { process: reused, tempProfileDir: null, ownsProcess: false };
|
|
764
|
+
}
|
|
765
|
+
const opener = openWithSystemBrowser(url);
|
|
766
|
+
if (opener) {
|
|
767
|
+
return { process: opener, tempProfileDir: null, ownsProcess: false };
|
|
760
768
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
769
|
+
}
|
|
770
|
+
const direct = openWithInstalledBrowser(url, headless);
|
|
771
|
+
if (direct) return direct;
|
|
772
|
+
throw new Error(
|
|
773
|
+
headless
|
|
774
|
+
? "could not find a headless-capable browser; set BROWSER to a Chromium/Firefox executable"
|
|
775
|
+
: "could not open a browser automatically; set BROWSER to a browser executable",
|
|
776
|
+
);
|
|
767
777
|
}
|
|
768
778
|
function openWithReusableBrowserWindow(url) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
779
|
+
if (process.platform != "darwin") return null;
|
|
780
|
+
if (!hasExecutable("osascript")) return null;
|
|
781
|
+
const browserApp = resolveMacBrowserAppName(
|
|
782
|
+
process.env.BROWSER?.trim() ?? "",
|
|
783
|
+
);
|
|
784
|
+
if (!browserApp) return null;
|
|
785
|
+
const script = buildMacBrowserOpenScript(browserApp, url);
|
|
786
|
+
if (!script.length) return null;
|
|
787
|
+
return spawn(
|
|
788
|
+
"osascript",
|
|
789
|
+
script.flatMap((line) => ["-e", line]),
|
|
790
|
+
{ stdio: "ignore", detached: true },
|
|
791
|
+
);
|
|
780
792
|
}
|
|
781
793
|
function openWithSystemBrowser(url) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
return spawn("xdg-open", [url], { stdio: "ignore", detached: true });
|
|
794
|
+
if (process.env.BROWSER) {
|
|
795
|
+
return (
|
|
796
|
+
spawnBrowserCommand(process.env.BROWSER, url, false)?.process ?? null
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
if (process.platform == "darwin") {
|
|
800
|
+
if (!hasExecutable("open")) return null;
|
|
801
|
+
return spawn("open", [url], { stdio: "ignore", detached: true });
|
|
802
|
+
}
|
|
803
|
+
if (process.platform == "win32") {
|
|
804
|
+
if (!hasExecutable("cmd")) return null;
|
|
805
|
+
return spawn("cmd", ["/c", "start", "", url], {
|
|
806
|
+
stdio: "ignore",
|
|
807
|
+
detached: true,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
if (!hasExecutable("xdg-open")) return null;
|
|
811
|
+
return spawn("xdg-open", [url], { stdio: "ignore", detached: true });
|
|
801
812
|
}
|
|
802
813
|
function resolveMacBrowserAppName(browser) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
safari: "Safari",
|
|
821
|
-
};
|
|
822
|
-
return aliases[command] ?? null;
|
|
814
|
+
const trimmed = browser.trim();
|
|
815
|
+
if (!trimmed.length) return null;
|
|
816
|
+
const extracted = extractMacAppNameFromExecutable(trimmed);
|
|
817
|
+
if (extracted) return extracted;
|
|
818
|
+
const command = splitCommand(trimmed)[0]?.toLowerCase() ?? "";
|
|
819
|
+
if (!command.length) return null;
|
|
820
|
+
const aliases = {
|
|
821
|
+
chrome: "Google Chrome",
|
|
822
|
+
"google-chrome": "Google Chrome",
|
|
823
|
+
"google-chrome-stable": "Google Chrome",
|
|
824
|
+
chromium: "Chromium",
|
|
825
|
+
"chromium-browser": "Chromium",
|
|
826
|
+
msedge: "Microsoft Edge",
|
|
827
|
+
firefox: "Firefox",
|
|
828
|
+
safari: "Safari",
|
|
829
|
+
};
|
|
830
|
+
return aliases[command] ?? null;
|
|
823
831
|
}
|
|
824
832
|
function extractMacAppNameFromExecutable(browser) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
return appMatch[1];
|
|
833
|
+
const appMatch = browser.match(/\/([^/]+)\.app\/Contents\/MacOS\//);
|
|
834
|
+
if (!appMatch?.[1]) return null;
|
|
835
|
+
return appMatch[1];
|
|
829
836
|
}
|
|
830
837
|
function buildMacBrowserOpenScript(appName, url) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
838
|
+
const escapedApp = escapeAppleScriptString(appName);
|
|
839
|
+
const escapedUrl = escapeAppleScriptString(url);
|
|
840
|
+
const lower = appName.toLowerCase();
|
|
841
|
+
if (
|
|
842
|
+
lower.includes("chrome") ||
|
|
843
|
+
lower.includes("chromium") ||
|
|
844
|
+
lower.includes("edge")
|
|
845
|
+
) {
|
|
846
|
+
return [
|
|
847
|
+
`tell application "${escapedApp}"`,
|
|
848
|
+
"activate",
|
|
849
|
+
"if (count of windows) = 0 then make new window",
|
|
850
|
+
`set URL of active tab of front window to "${escapedUrl}"`,
|
|
851
|
+
"end tell",
|
|
852
|
+
];
|
|
853
|
+
}
|
|
854
|
+
if (lower.includes("safari")) {
|
|
855
|
+
return [
|
|
856
|
+
`tell application "${escapedApp}"`,
|
|
857
|
+
"activate",
|
|
858
|
+
"if (count of windows) = 0 then make new document",
|
|
859
|
+
`set URL of front document to "${escapedUrl}"`,
|
|
860
|
+
"end tell",
|
|
861
|
+
];
|
|
862
|
+
}
|
|
863
|
+
if (lower.includes("firefox")) {
|
|
864
|
+
return [
|
|
865
|
+
`tell application "${escapedApp}"`,
|
|
866
|
+
"activate",
|
|
867
|
+
`open location "${escapedUrl}"`,
|
|
868
|
+
"end tell",
|
|
869
|
+
];
|
|
870
|
+
}
|
|
871
|
+
return [];
|
|
863
872
|
}
|
|
864
873
|
function escapeAppleScriptString(value) {
|
|
865
|
-
|
|
874
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
866
875
|
}
|
|
867
876
|
function openWithInstalledBrowser(url, headless) {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
877
|
+
const browserEnv = process.env.BROWSER;
|
|
878
|
+
if (browserEnv) {
|
|
879
|
+
return spawnBrowserCommand(browserEnv, url, headless);
|
|
880
|
+
}
|
|
881
|
+
const candidates = [
|
|
882
|
+
{ command: "chromium", headless: [...WEB_HEADLESS_FLAGS] },
|
|
883
|
+
{ command: "chromium-browser", headless: [...WEB_HEADLESS_FLAGS] },
|
|
884
|
+
{ command: "google-chrome", headless: [...WEB_HEADLESS_FLAGS] },
|
|
885
|
+
{ command: "google-chrome-stable", headless: [...WEB_HEADLESS_FLAGS] },
|
|
886
|
+
{ command: "chrome", headless: [...WEB_HEADLESS_FLAGS] },
|
|
887
|
+
{ command: "msedge", headless: [...WEB_HEADLESS_FLAGS] },
|
|
888
|
+
{ command: "firefox", headless: ["-headless"] },
|
|
889
|
+
];
|
|
890
|
+
for (const candidate of candidates) {
|
|
891
|
+
if (!hasExecutable(candidate.command)) continue;
|
|
892
|
+
return {
|
|
893
|
+
process: spawn(
|
|
894
|
+
candidate.command,
|
|
895
|
+
[...(headless ? candidate.headless : []), url],
|
|
896
|
+
{
|
|
897
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
898
|
+
detached: true,
|
|
899
|
+
},
|
|
900
|
+
),
|
|
901
|
+
tempProfileDir: null,
|
|
902
|
+
ownsProcess: true,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
const playwrightFallback =
|
|
906
|
+
resolvePlaywrightBrowserExecutable("chromium") ??
|
|
907
|
+
resolvePlaywrightBrowserExecutable("firefox") ??
|
|
908
|
+
resolvePlaywrightBrowserExecutable("webkit");
|
|
909
|
+
if (playwrightFallback) {
|
|
910
|
+
return spawnBrowserCommand(playwrightFallback, url, headless);
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
900
913
|
}
|
|
901
914
|
function spawnBrowserCommand(commandValue, url, headless) {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
args.push(url);
|
|
909
|
-
return {
|
|
910
|
-
process: spawn(directCommand, args, {
|
|
911
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
912
|
-
detached: true,
|
|
913
|
-
}),
|
|
914
|
-
tempProfileDir: resolvedHeadless.tempProfileDir,
|
|
915
|
-
ownsProcess: true,
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
const parts = splitCommand(String(commandValue));
|
|
919
|
-
if (!parts.length)
|
|
920
|
-
return null;
|
|
921
|
-
const command = parts[0];
|
|
922
|
-
if (!hasExecutable(command))
|
|
923
|
-
return null;
|
|
924
|
-
const args = parts.slice(1);
|
|
925
|
-
let tempProfileDir = null;
|
|
926
|
-
if (headless) {
|
|
927
|
-
const resolvedHeadless = resolveHeadlessLaunch(commandValue, command);
|
|
928
|
-
args.push(...resolvedHeadless.flags);
|
|
929
|
-
tempProfileDir = resolvedHeadless.tempProfileDir;
|
|
930
|
-
}
|
|
915
|
+
const directCommand = unwrapQuotedPath(String(commandValue).trim());
|
|
916
|
+
if (hasExecutable(directCommand)) {
|
|
917
|
+
const resolvedHeadless = headless
|
|
918
|
+
? resolveHeadlessLaunch(directCommand, directCommand)
|
|
919
|
+
: { flags: [], tempProfileDir: null };
|
|
920
|
+
const args = [...resolvedHeadless.flags];
|
|
931
921
|
args.push(url);
|
|
932
922
|
return {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
923
|
+
process: spawn(directCommand, args, {
|
|
924
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
925
|
+
detached: true,
|
|
926
|
+
}),
|
|
927
|
+
tempProfileDir: resolvedHeadless.tempProfileDir,
|
|
928
|
+
ownsProcess: true,
|
|
939
929
|
};
|
|
930
|
+
}
|
|
931
|
+
const parts = splitCommand(String(commandValue));
|
|
932
|
+
if (!parts.length) return null;
|
|
933
|
+
const command = parts[0];
|
|
934
|
+
if (!hasExecutable(command)) return null;
|
|
935
|
+
const args = parts.slice(1);
|
|
936
|
+
let tempProfileDir = null;
|
|
937
|
+
if (headless) {
|
|
938
|
+
const resolvedHeadless = resolveHeadlessLaunch(commandValue, command);
|
|
939
|
+
args.push(...resolvedHeadless.flags);
|
|
940
|
+
tempProfileDir = resolvedHeadless.tempProfileDir;
|
|
941
|
+
}
|
|
942
|
+
args.push(url);
|
|
943
|
+
return {
|
|
944
|
+
process: spawn(command, args, {
|
|
945
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
946
|
+
detached: true,
|
|
947
|
+
}),
|
|
948
|
+
tempProfileDir,
|
|
949
|
+
ownsProcess: true,
|
|
950
|
+
};
|
|
940
951
|
}
|
|
941
952
|
function splitCommand(commandValue) {
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}
|
|
954
|
-
else {
|
|
955
|
-
current += char;
|
|
956
|
-
}
|
|
957
|
-
continue;
|
|
958
|
-
}
|
|
959
|
-
if (char == "'" || char == '"') {
|
|
960
|
-
quote = char;
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
if (/\s/.test(char)) {
|
|
964
|
-
if (current.length) {
|
|
965
|
-
parts.push(current);
|
|
966
|
-
current = "";
|
|
967
|
-
}
|
|
968
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
if (char == "\\" && i + 1 < commandValue.length) {
|
|
971
|
-
current += commandValue[++i];
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
953
|
+
const parts = [];
|
|
954
|
+
let current = "";
|
|
955
|
+
let quote = "";
|
|
956
|
+
for (let i = 0; i < commandValue.length; i++) {
|
|
957
|
+
const char = commandValue[i];
|
|
958
|
+
if (quote) {
|
|
959
|
+
if (char == quote) {
|
|
960
|
+
quote = "";
|
|
961
|
+
} else if (char == "\\" && i + 1 < commandValue.length) {
|
|
962
|
+
current += commandValue[++i];
|
|
963
|
+
} else {
|
|
974
964
|
current += char;
|
|
965
|
+
}
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (char == "'" || char == '"') {
|
|
969
|
+
quote = char;
|
|
970
|
+
continue;
|
|
975
971
|
}
|
|
976
|
-
if (
|
|
972
|
+
if (/\s/.test(char)) {
|
|
973
|
+
if (current.length) {
|
|
977
974
|
parts.push(current);
|
|
975
|
+
current = "";
|
|
976
|
+
}
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
if (char == "\\" && i + 1 < commandValue.length) {
|
|
980
|
+
current += commandValue[++i];
|
|
981
|
+
continue;
|
|
978
982
|
}
|
|
979
|
-
|
|
983
|
+
current += char;
|
|
984
|
+
}
|
|
985
|
+
if (current.length) {
|
|
986
|
+
parts.push(current);
|
|
987
|
+
}
|
|
988
|
+
return parts;
|
|
980
989
|
}
|
|
981
990
|
function appendBrowserOutput(current, next) {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
return combined.slice(combined.length - 16384);
|
|
991
|
+
const combined = current + next;
|
|
992
|
+
if (combined.length <= 16384) return combined;
|
|
993
|
+
return combined.slice(combined.length - 16384);
|
|
986
994
|
}
|
|
987
995
|
function formatBrowserExitError(code, stderr) {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
996
|
+
const trimmed = stderr.trim();
|
|
997
|
+
if (!trimmed.length) {
|
|
998
|
+
return `web browser process exited with code ${code}`;
|
|
999
|
+
}
|
|
1000
|
+
return `web browser process exited with code ${code}\nstderr:\n${trimmed}`;
|
|
993
1001
|
}
|
|
994
1002
|
function killOwnedBrowserProcess(browserProcess) {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1003
|
+
try {
|
|
1004
|
+
if (
|
|
1005
|
+
process.platform != "win32" &&
|
|
1006
|
+
typeof browserProcess.pid == "number" &&
|
|
1007
|
+
browserProcess.pid > 0
|
|
1008
|
+
) {
|
|
1009
|
+
process.kill(-browserProcess.pid, "SIGTERM");
|
|
1010
|
+
return;
|
|
1002
1011
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
catch { }
|
|
1012
|
+
} catch {}
|
|
1013
|
+
try {
|
|
1014
|
+
browserProcess.kill("SIGTERM");
|
|
1015
|
+
} catch {}
|
|
1008
1016
|
}
|
|
1009
1017
|
function unwrapQuotedPath(value) {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
return value;
|
|
1018
|
+
if (
|
|
1019
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
1020
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
1021
|
+
) {
|
|
1022
|
+
return value.slice(1, -1);
|
|
1023
|
+
}
|
|
1024
|
+
return value;
|
|
1015
1025
|
}
|
|
1016
1026
|
function resolveHeadlessLaunch(commandValue, command) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
return { flags: [
|
|
1027
|
+
const lower = `${commandValue} ${command}`.toLowerCase();
|
|
1028
|
+
if (lower.includes("firefox")) {
|
|
1029
|
+
const tempProfileDir = fs.mkdtempSync(
|
|
1030
|
+
path.join(os.tmpdir(), "as-test-firefox-profile-"),
|
|
1031
|
+
);
|
|
1032
|
+
return {
|
|
1033
|
+
flags: ["-headless", "-no-remote", "-profile", tempProfileDir],
|
|
1034
|
+
tempProfileDir,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
if (lower.includes("webkit") || lower.includes("minibrowser")) {
|
|
1038
|
+
return { flags: ["--headless"], tempProfileDir: null };
|
|
1039
|
+
}
|
|
1040
|
+
return { flags: [...WEB_HEADLESS_FLAGS], tempProfileDir: null };
|
|
1029
1041
|
}
|
|
1030
1042
|
function hasExecutable(command) {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1043
|
+
if (!command.length) return false;
|
|
1044
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
1045
|
+
return fs.existsSync(command);
|
|
1046
|
+
}
|
|
1047
|
+
const pathValue = process.env.PATH ?? "";
|
|
1048
|
+
const suffixes =
|
|
1049
|
+
process.platform == "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
1050
|
+
for (const base of pathValue.split(path.delimiter)) {
|
|
1051
|
+
if (!base) continue;
|
|
1052
|
+
for (const suffix of suffixes) {
|
|
1053
|
+
if (fs.existsSync(path.join(base, command + suffix))) {
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1035
1056
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
for (const base of pathValue.split(path.delimiter)) {
|
|
1039
|
-
if (!base)
|
|
1040
|
-
continue;
|
|
1041
|
-
for (const suffix of suffixes) {
|
|
1042
|
-
if (fs.existsSync(path.join(base, command + suffix))) {
|
|
1043
|
-
return true;
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
return false;
|
|
1057
|
+
}
|
|
1058
|
+
return false;
|
|
1048
1059
|
}
|
|
1049
1060
|
function resolvePlaywrightBrowserExecutable(browser) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
return matches[matches.length - 1];
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1061
|
+
const patterns = getPlaywrightBrowserPatterns(browser);
|
|
1062
|
+
if (!patterns.length) return null;
|
|
1063
|
+
for (const cacheRoot of getPlaywrightCacheRoots()) {
|
|
1064
|
+
if (!fs.existsSync(cacheRoot)) continue;
|
|
1065
|
+
for (const pattern of patterns) {
|
|
1066
|
+
const matches = fs.globSync(path.join(cacheRoot, pattern)).sort();
|
|
1067
|
+
if (matches.length) {
|
|
1068
|
+
return matches[matches.length - 1];
|
|
1069
|
+
}
|
|
1062
1070
|
}
|
|
1063
|
-
|
|
1071
|
+
}
|
|
1072
|
+
return null;
|
|
1064
1073
|
}
|
|
1065
1074
|
function getPlaywrightCacheRoots() {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1075
|
+
const roots = new Set();
|
|
1076
|
+
const configured = process.env.PLAYWRIGHT_BROWSERS_PATH?.trim() ?? "";
|
|
1077
|
+
if (configured.length && configured != "0") {
|
|
1078
|
+
roots.add(path.resolve(configured));
|
|
1079
|
+
}
|
|
1080
|
+
const home = process.env.HOME ?? "";
|
|
1081
|
+
if (process.platform == "darwin" && home.length) {
|
|
1082
|
+
roots.add(path.join(home, "Library", "Caches", "ms-playwright"));
|
|
1083
|
+
} else if (process.platform == "win32") {
|
|
1084
|
+
const localAppData = process.env.LOCALAPPDATA?.trim() ?? "";
|
|
1085
|
+
if (localAppData.length) {
|
|
1086
|
+
roots.add(path.join(localAppData, "ms-playwright"));
|
|
1070
1087
|
}
|
|
1071
|
-
const
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1088
|
+
const userProfile = process.env.USERPROFILE?.trim() ?? "";
|
|
1089
|
+
if (userProfile.length) {
|
|
1090
|
+
roots.add(path.join(userProfile, "AppData", "Local", "ms-playwright"));
|
|
1074
1091
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
}
|
|
1080
|
-
const userProfile = process.env.USERPROFILE?.trim() ?? "";
|
|
1081
|
-
if (userProfile.length) {
|
|
1082
|
-
roots.add(path.join(userProfile, "AppData", "Local", "ms-playwright"));
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
else if (home.length) {
|
|
1086
|
-
roots.add(path.join(home, ".cache", "ms-playwright"));
|
|
1087
|
-
}
|
|
1088
|
-
return [...roots];
|
|
1092
|
+
} else if (home.length) {
|
|
1093
|
+
roots.add(path.join(home, ".cache", "ms-playwright"));
|
|
1094
|
+
}
|
|
1095
|
+
return [...roots];
|
|
1089
1096
|
}
|
|
1090
1097
|
function getPlaywrightBrowserPatterns(browser) {
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
};
|
|
1110
|
-
return macMap[browser] ?? [];
|
|
1111
|
-
}
|
|
1112
|
-
if (process.platform == "win32") {
|
|
1113
|
-
const winMap = {
|
|
1114
|
-
chromium: [
|
|
1115
|
-
"chromium-*/chrome-win/chrome.exe",
|
|
1116
|
-
"chromium-*/chrome-win64/chrome.exe",
|
|
1117
|
-
"chromium_headless_shell-*/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
1118
|
-
],
|
|
1119
|
-
chrome: [
|
|
1120
|
-
"chromium-*/chrome-win/chrome.exe",
|
|
1121
|
-
"chromium-*/chrome-win64/chrome.exe",
|
|
1122
|
-
"chromium_headless_shell-*/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
1123
|
-
],
|
|
1124
|
-
firefox: ["firefox-*/firefox/firefox.exe"],
|
|
1125
|
-
webkit: ["webkit-*/Playwright.exe"],
|
|
1126
|
-
};
|
|
1127
|
-
return winMap[browser] ?? [];
|
|
1128
|
-
}
|
|
1129
|
-
const linuxMap = {
|
|
1130
|
-
chromium: [
|
|
1131
|
-
"chromium-*/chrome-linux/chrome",
|
|
1132
|
-
"chromium-*/chrome-linux64/chrome",
|
|
1133
|
-
"chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell",
|
|
1134
|
-
],
|
|
1135
|
-
chrome: [
|
|
1136
|
-
"chromium-*/chrome-linux/chrome",
|
|
1137
|
-
"chromium-*/chrome-linux64/chrome",
|
|
1138
|
-
"chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell",
|
|
1139
|
-
],
|
|
1140
|
-
firefox: ["firefox-*/firefox/firefox"],
|
|
1141
|
-
webkit: ["webkit-*/pw_run.sh"],
|
|
1098
|
+
if (process.platform == "darwin") {
|
|
1099
|
+
const macMap = {
|
|
1100
|
+
chromium: [
|
|
1101
|
+
"chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
|
1102
|
+
"chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium",
|
|
1103
|
+
"chromium_headless_shell-*/chrome-headless-shell-mac*/chrome-headless-shell",
|
|
1104
|
+
],
|
|
1105
|
+
chrome: [
|
|
1106
|
+
"chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing",
|
|
1107
|
+
"chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium",
|
|
1108
|
+
"chromium_headless_shell-*/chrome-headless-shell-mac*/chrome-headless-shell",
|
|
1109
|
+
],
|
|
1110
|
+
firefox: [
|
|
1111
|
+
"firefox-*/firefox/*.app/Contents/MacOS/firefox",
|
|
1112
|
+
"firefox-*/*.app/Contents/MacOS/firefox",
|
|
1113
|
+
"firefox-*/firefox/firefox",
|
|
1114
|
+
],
|
|
1115
|
+
webkit: ["webkit-*/pw_run.sh"],
|
|
1142
1116
|
};
|
|
1143
|
-
return
|
|
1117
|
+
return macMap[browser] ?? [];
|
|
1118
|
+
}
|
|
1119
|
+
if (process.platform == "win32") {
|
|
1120
|
+
const winMap = {
|
|
1121
|
+
chromium: [
|
|
1122
|
+
"chromium-*/chrome-win/chrome.exe",
|
|
1123
|
+
"chromium-*/chrome-win64/chrome.exe",
|
|
1124
|
+
"chromium_headless_shell-*/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
1125
|
+
],
|
|
1126
|
+
chrome: [
|
|
1127
|
+
"chromium-*/chrome-win/chrome.exe",
|
|
1128
|
+
"chromium-*/chrome-win64/chrome.exe",
|
|
1129
|
+
"chromium_headless_shell-*/chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
1130
|
+
],
|
|
1131
|
+
firefox: ["firefox-*/firefox/firefox.exe"],
|
|
1132
|
+
webkit: ["webkit-*/Playwright.exe"],
|
|
1133
|
+
};
|
|
1134
|
+
return winMap[browser] ?? [];
|
|
1135
|
+
}
|
|
1136
|
+
const linuxMap = {
|
|
1137
|
+
chromium: [
|
|
1138
|
+
"chromium-*/chrome-linux/chrome",
|
|
1139
|
+
"chromium-*/chrome-linux64/chrome",
|
|
1140
|
+
"chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell",
|
|
1141
|
+
],
|
|
1142
|
+
chrome: [
|
|
1143
|
+
"chromium-*/chrome-linux/chrome",
|
|
1144
|
+
"chromium-*/chrome-linux64/chrome",
|
|
1145
|
+
"chromium_headless_shell-*/chrome-headless-shell-linux64/chrome-headless-shell",
|
|
1146
|
+
],
|
|
1147
|
+
firefox: ["firefox-*/firefox/firefox"],
|
|
1148
|
+
webkit: ["webkit-*/pw_run.sh"],
|
|
1149
|
+
};
|
|
1150
|
+
return linuxMap[browser] ?? [];
|
|
1144
1151
|
}
|
|
1145
1152
|
async function captureHelperInstance(runHelper) {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
}
|
|
1153
|
-
return result;
|
|
1154
|
-
});
|
|
1155
|
-
try {
|
|
1156
|
-
await runHelper();
|
|
1157
|
-
}
|
|
1158
|
-
finally {
|
|
1159
|
-
WebAssembly.instantiate =
|
|
1160
|
-
originalInstantiate;
|
|
1161
|
-
}
|
|
1162
|
-
if (!instance) {
|
|
1163
|
-
throw new Error("bindings helper did not produce a WebAssembly.Instance");
|
|
1153
|
+
const originalInstantiate = WebAssembly.instantiate.bind(WebAssembly);
|
|
1154
|
+
let instance = null;
|
|
1155
|
+
WebAssembly.instantiate = async (source, importObject) => {
|
|
1156
|
+
const result = await originalInstantiate(source, importObject);
|
|
1157
|
+
if (result instanceof WebAssembly.Instance) {
|
|
1158
|
+
instance = result;
|
|
1164
1159
|
}
|
|
1165
|
-
return
|
|
1160
|
+
return result;
|
|
1161
|
+
};
|
|
1162
|
+
try {
|
|
1163
|
+
await runHelper();
|
|
1164
|
+
} finally {
|
|
1165
|
+
WebAssembly.instantiate = originalInstantiate;
|
|
1166
|
+
}
|
|
1167
|
+
if (!instance) {
|
|
1168
|
+
throw new Error("bindings helper did not produce a WebAssembly.Instance");
|
|
1169
|
+
}
|
|
1170
|
+
return instance;
|
|
1166
1171
|
}
|