@orkestrel/browser 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/core/index.cjs +422 -162
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +11 -2
- package/dist/src/core/index.d.ts +11 -2
- package/dist/src/core/index.js +422 -162
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +588 -411
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +52 -53
- package/dist/src/server/index.d.ts +52 -53
- package/dist/src/server/index.js +586 -410
- package/dist/src/server/index.js.map +1 -1
- package/package.json +9 -6
package/dist/src/server/index.js
CHANGED
|
@@ -2,11 +2,14 @@ import { BROWSER_DEFAULT_TIMEOUT_MS, BROWSER_WAIT_POLL_INTERVAL_MS, BrowserConte
|
|
|
2
2
|
import { existsSync, globSync } from "node:fs";
|
|
3
3
|
import { dirname, posix, win32 } from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
5
6
|
import { isRecord, isString } from "@orkestrel/contract";
|
|
6
7
|
import { randomBytes } from "node:crypto";
|
|
7
8
|
import { request } from "node:http";
|
|
9
|
+
import { request as request$1 } from "node:https";
|
|
8
10
|
import { Emitter } from "@orkestrel/emitter";
|
|
9
|
-
import { WEBSOCKET_READY_OPEN, WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket } from "@orkestrel/websocket";
|
|
11
|
+
import { WEBSOCKET_READY_CLOSED, WEBSOCKET_READY_OPEN, WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket } from "@orkestrel/websocket";
|
|
12
|
+
import { addAbortListener, once } from "node:events";
|
|
10
13
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
11
14
|
//#region src/server/constants.ts
|
|
12
15
|
/** Default CDP port probed for an existing browser and used for launches. */
|
|
@@ -19,13 +22,6 @@ var BROWSER_CDP_PROTOCOL = "http";
|
|
|
19
22
|
var BROWSER_CDP_VERSION_PATH = "/json/version";
|
|
20
23
|
/** Path appended to the CDP host to list open targets (pages, workers, etc). */
|
|
21
24
|
var BROWSER_CDP_LIST_PATH = "/json/list";
|
|
22
|
-
/** Sentinel result returned by discovery when no browser is reachable. */
|
|
23
|
-
var BROWSER_NOT_FOUND_RESULT = Object.freeze({
|
|
24
|
-
found: false,
|
|
25
|
-
endpoint: void 0,
|
|
26
|
-
browser: void 0,
|
|
27
|
-
connection: void 0
|
|
28
|
-
});
|
|
29
25
|
/** Flags always passed to a launched browser process, alongside the caller's own. */
|
|
30
26
|
var BROWSER_LAUNCH_ARGS = Object.freeze(["--no-first-run", "--no-default-browser-check"]);
|
|
31
27
|
/** Flag enabling headless mode on a launched browser process. */
|
|
@@ -36,6 +32,10 @@ var BROWSER_KILL_GRACE_MS = 3e3;
|
|
|
36
32
|
var BROWSER_PORT_PROBE_TIMEOUT_MS = 200;
|
|
37
33
|
/** Brief defer applied once when a transport loss is observed on an owned process, giving a near-simultaneous process-exit event (which libuv may reap slightly later than the socket close) first say over the diagnosis. */
|
|
38
34
|
var BROWSER_TRANSPORT_LOSS_DEFER_MS = 50;
|
|
35
|
+
/** Machine-readable error-context cause for an owned browser process exiting. */
|
|
36
|
+
var BROWSER_PROCESS_EXIT_CAUSE = "process-exit";
|
|
37
|
+
/** Machine-readable error-context cause for a CDP transport disconnecting while its browser remains alive. */
|
|
38
|
+
var BROWSER_TRANSPORT_LOSS_CAUSE = "transport-loss";
|
|
39
39
|
/** Environment variables checked (in order) for an explicit browser executable path override. */
|
|
40
40
|
var BROWSER_ENV_PATH_KEYS = Object.freeze(["PLAYWRIGHT_EXECUTABLE_PATH", "CHROME_PATH"]);
|
|
41
41
|
/** Well-known Chrome/Chromium/Edge executable paths with no platform-specific root, keyed by `process.platform`. */
|
|
@@ -259,8 +259,8 @@ function normalizeExecutablePath(path, platform) {
|
|
|
259
259
|
}
|
|
260
260
|
/** Classify a `/json/version` `Browser` string into a {@link BrowserEngine} (`Edg/` → edge, `Chrome/` → chrome, else chromium). */
|
|
261
261
|
function browserToEngine(browser) {
|
|
262
|
-
if (browser
|
|
263
|
-
if (browser
|
|
262
|
+
if (browser?.includes("Edg/") === true) return "edge";
|
|
263
|
+
if (browser?.includes("Chrome/") === true) return "chrome";
|
|
264
264
|
return "chromium";
|
|
265
265
|
}
|
|
266
266
|
/** Check the env-override keys (`PLAYWRIGHT_EXECUTABLE_PATH`, `CHROME_PATH`) in order for an existing file. */
|
|
@@ -346,7 +346,7 @@ function findAllInStore(base, platform) {
|
|
|
346
346
|
const glob = BROWSER_STORE_GLOBS[platform];
|
|
347
347
|
if (glob === void 0) return found;
|
|
348
348
|
const matches = globSync(`${base.replaceAll("\\", "/")}/${glob}`).filter((match) => existsSync(match));
|
|
349
|
-
matches.sort((a, b) => a
|
|
349
|
+
matches.sort((a, b) => b.localeCompare(a, void 0, { numeric: true }));
|
|
350
350
|
found.push(...matches);
|
|
351
351
|
return found;
|
|
352
352
|
}
|
|
@@ -378,23 +378,24 @@ function launchBrowserProcess(executable, port, headless, profile, extra) {
|
|
|
378
378
|
*
|
|
379
379
|
* @throws When the endpoint does not become ready before the timeout
|
|
380
380
|
*/
|
|
381
|
-
async function
|
|
381
|
+
async function waitForCDPReady(port, timeout, host = BROWSER_DEFAULT_HOST, signal) {
|
|
382
382
|
const url = `${BROWSER_CDP_PROTOCOL}://${host}:${port}${BROWSER_CDP_VERSION_PATH}`;
|
|
383
383
|
const deadline = Date.now() + timeout;
|
|
384
384
|
while (Date.now() < deadline) {
|
|
385
|
+
signal?.throwIfAborted();
|
|
385
386
|
const remaining = Math.max(0, deadline - Date.now());
|
|
386
|
-
const
|
|
387
|
-
const timer = setTimeout(() => controller.abort(), remaining);
|
|
387
|
+
const requestSignal = signal === void 0 ? AbortSignal.timeout(remaining) : AbortSignal.any([signal, AbortSignal.timeout(remaining)]);
|
|
388
388
|
try {
|
|
389
|
-
const response = await fetch(url, { signal:
|
|
389
|
+
const response = await fetch(url, { signal: requestSignal });
|
|
390
390
|
if (response.ok) {
|
|
391
391
|
const info = await response.json();
|
|
392
392
|
if (isRecord(info) && isString(info["webSocketDebuggerUrl"])) return info["webSocketDebuggerUrl"];
|
|
393
393
|
}
|
|
394
|
-
} catch
|
|
395
|
-
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (signal?.aborted === true) throw error;
|
|
396
396
|
}
|
|
397
|
-
|
|
397
|
+
const delay = Math.min(BROWSER_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()));
|
|
398
|
+
if (delay > 0) await setTimeout$1(delay, void 0, { signal });
|
|
398
399
|
}
|
|
399
400
|
throw new Error(`CDP endpoint on port ${port} did not become ready within ${timeout}ms`);
|
|
400
401
|
}
|
|
@@ -406,7 +407,7 @@ async function waitForCdpReady(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
406
407
|
* @param host - Host the browser exposes its CDP endpoint on (default `127.0.0.1`)
|
|
407
408
|
* @returns Normalized CDP targets
|
|
408
409
|
*/
|
|
409
|
-
async function
|
|
410
|
+
async function fetchCDPTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
410
411
|
const url = `${BROWSER_CDP_PROTOCOL}://${host}:${port}${BROWSER_CDP_LIST_PATH}`;
|
|
411
412
|
const controller = new AbortController();
|
|
412
413
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
@@ -418,12 +419,12 @@ async function fetchCdpTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
418
419
|
const targets = [];
|
|
419
420
|
for (const entry of list) {
|
|
420
421
|
if (!isRecord(entry)) continue;
|
|
421
|
-
if (!isString(entry["id"]) || !isString(entry["type"])) continue;
|
|
422
|
+
if (!isString(entry["id"]) || !isString(entry["type"]) || !isString(entry["title"]) || !isString(entry["url"])) continue;
|
|
422
423
|
targets.push({
|
|
423
424
|
id: entry["id"],
|
|
424
425
|
type: entry["type"],
|
|
425
|
-
title:
|
|
426
|
-
url:
|
|
426
|
+
title: entry["title"],
|
|
427
|
+
url: entry["url"]
|
|
427
428
|
});
|
|
428
429
|
}
|
|
429
430
|
return targets;
|
|
@@ -436,33 +437,32 @@ async function fetchCdpTargets(port, timeout, host = BROWSER_DEFAULT_HOST) {
|
|
|
436
437
|
//#endregion
|
|
437
438
|
//#region src/server/transports/WebSocketCDPTransport.ts
|
|
438
439
|
/**
|
|
439
|
-
* `@orkestrel/websocket`-backed
|
|
440
|
-
* pipe a {@link import('@src/core').CDPClientInterface} sends and receives
|
|
441
|
-
* JSON-RPC frames over.
|
|
440
|
+
* `@orkestrel/websocket`-backed raw CDP text transport.
|
|
442
441
|
*
|
|
443
442
|
* @remarks
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
* `
|
|
447
|
-
*
|
|
448
|
-
* — a mismatch (or a connection/request error, or a timeout) rejects `start()`
|
|
449
|
-
* with a coded {@link BrowserConnectionError} and cleans up the socket. On
|
|
450
|
-
* success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,
|
|
451
|
-
* head })` (CLIENT mode — no `key` — so outgoing frames are masked per RFC
|
|
452
|
-
* 6455 §5.3) and bridges its `message`/`close`/`error` events onto this
|
|
453
|
-
* transport's emitter. `start()` opens a fresh socket each call (safe to call
|
|
454
|
-
* again after `close()` — a new request/socket/NodeWebSocket triple is
|
|
455
|
-
* created every time, which is what `CDPClient.reconnect()` depends on). CDP
|
|
456
|
-
* frames are text-only, so `message` always carries a `string`.
|
|
443
|
+
* Performs and validates the RFC 6455 HTTP upgrade, then delegates frame
|
|
444
|
+
* handling to `@orkestrel/websocket`. Concurrent and repeated `start()` /
|
|
445
|
+
* `close()` calls share their active transition, and a later `start()` opens a
|
|
446
|
+
* fresh socket after the prior one closes.
|
|
457
447
|
*/
|
|
458
448
|
var WebSocketCDPTransport = class {
|
|
459
449
|
#emitter;
|
|
460
450
|
#url;
|
|
461
451
|
#timeout;
|
|
462
452
|
#socket;
|
|
463
|
-
#
|
|
453
|
+
#starting;
|
|
454
|
+
#closing;
|
|
455
|
+
#request;
|
|
456
|
+
#resolve;
|
|
457
|
+
#reject;
|
|
458
|
+
#timer;
|
|
459
|
+
#closeResolve;
|
|
460
|
+
#onSocketClose = () => this.#closeResolve?.();
|
|
464
461
|
constructor(options) {
|
|
465
|
-
this.#emitter = new Emitter(
|
|
462
|
+
this.#emitter = new Emitter({
|
|
463
|
+
on: options.on,
|
|
464
|
+
error: options.error
|
|
465
|
+
});
|
|
466
466
|
this.#url = options.url;
|
|
467
467
|
this.#timeout = options.timeout ?? BROWSER_DEFAULT_TIMEOUT_MS;
|
|
468
468
|
}
|
|
@@ -470,62 +470,22 @@ var WebSocketCDPTransport = class {
|
|
|
470
470
|
return this.#emitter;
|
|
471
471
|
}
|
|
472
472
|
async start() {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
port: url.port.length > 0 ? Number(url.port) : 80,
|
|
490
|
-
path: `${url.pathname}${url.search}`,
|
|
491
|
-
headers: {
|
|
492
|
-
Connection: "Upgrade",
|
|
493
|
-
Upgrade: "websocket",
|
|
494
|
-
"Sec-WebSocket-Key": key,
|
|
495
|
-
"Sec-WebSocket-Version": WEBSOCKET_VERSION
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
request$1.on("upgrade", (response, socket, head) => {
|
|
499
|
-
clearTimeout(timer);
|
|
500
|
-
const accept = response.headers["sec-websocket-accept"];
|
|
501
|
-
if (!isString(accept) || accept !== computeWebSocketAccept(key)) {
|
|
502
|
-
socket.destroy();
|
|
503
|
-
settle(() => reject(new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url })));
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
settle(() => resolve(createNodeWebSocket({
|
|
507
|
-
socket,
|
|
508
|
-
head
|
|
509
|
-
})));
|
|
510
|
-
});
|
|
511
|
-
request$1.on("response", (response) => {
|
|
512
|
-
clearTimeout(timer);
|
|
513
|
-
response.resume();
|
|
514
|
-
settle(() => reject(new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: upgrade declined with status ${response.statusCode ?? 0}`, { url: this.#url })));
|
|
515
|
-
});
|
|
516
|
-
request$1.on("error", (error) => {
|
|
517
|
-
clearTimeout(timer);
|
|
518
|
-
settle(() => reject(new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url })));
|
|
519
|
-
});
|
|
520
|
-
this.#connecting = {
|
|
521
|
-
abort: () => request$1.destroy(),
|
|
522
|
-
timer,
|
|
523
|
-
reject: (error) => settle(() => reject(error))
|
|
524
|
-
};
|
|
525
|
-
request$1.end();
|
|
526
|
-
});
|
|
527
|
-
this.#socket = ws;
|
|
528
|
-
this.#bind(ws);
|
|
473
|
+
if (this.#closing !== void 0) await this.#closing;
|
|
474
|
+
if (this.#socket?.readyState === WEBSOCKET_READY_OPEN) return;
|
|
475
|
+
const active = this.#starting;
|
|
476
|
+
if (active !== void 0) {
|
|
477
|
+
await active;
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.#socket?.destroy();
|
|
481
|
+
this.#socket = void 0;
|
|
482
|
+
const attempt = this.#open();
|
|
483
|
+
this.#starting = attempt;
|
|
484
|
+
try {
|
|
485
|
+
await attempt;
|
|
486
|
+
} finally {
|
|
487
|
+
if (this.#starting === attempt) this.#starting = void 0;
|
|
488
|
+
}
|
|
529
489
|
}
|
|
530
490
|
async send(data) {
|
|
531
491
|
const socket = this.#socket;
|
|
@@ -533,32 +493,139 @@ var WebSocketCDPTransport = class {
|
|
|
533
493
|
socket.send(data);
|
|
534
494
|
}
|
|
535
495
|
async close() {
|
|
536
|
-
const
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
-
this.#connecting = void 0;
|
|
540
|
-
connecting.abort();
|
|
541
|
-
connecting.reject(new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
|
|
496
|
+
const active = this.#closing;
|
|
497
|
+
if (active !== void 0) {
|
|
498
|
+
await active;
|
|
542
499
|
return;
|
|
543
500
|
}
|
|
501
|
+
const attempt = this.#stop();
|
|
502
|
+
this.#closing = attempt;
|
|
503
|
+
try {
|
|
504
|
+
await attempt;
|
|
505
|
+
} finally {
|
|
506
|
+
if (this.#closing === attempt) this.#closing = void 0;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async #open() {
|
|
510
|
+
const url = new URL(this.#url);
|
|
511
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new BrowserConnectionError(`WebSocket CDP connection requires a ws: or wss: URL: ${this.#url}`, { url: this.#url });
|
|
512
|
+
const key = randomBytes(16).toString("base64");
|
|
513
|
+
const deferred = Promise.withResolvers();
|
|
514
|
+
const options = {
|
|
515
|
+
hostname: url.hostname,
|
|
516
|
+
port: url.port.length > 0 ? Number(url.port) : url.protocol === "wss:" ? 443 : 80,
|
|
517
|
+
path: `${url.pathname}${url.search}`,
|
|
518
|
+
headers: {
|
|
519
|
+
Connection: "Upgrade",
|
|
520
|
+
Upgrade: "websocket",
|
|
521
|
+
"Sec-WebSocket-Key": key,
|
|
522
|
+
"Sec-WebSocket-Version": WEBSOCKET_VERSION
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
const request$2 = url.protocol === "wss:" ? request$1(options) : request(options);
|
|
526
|
+
this.#request = request$2;
|
|
527
|
+
this.#resolve = deferred.resolve;
|
|
528
|
+
this.#reject = deferred.reject;
|
|
529
|
+
this.#timer = setTimeout(() => this.#expire(request$2), this.#timeout);
|
|
530
|
+
request$2.on("upgrade", (response, socket, head) => {
|
|
531
|
+
this.#upgrade(request$2, key, response, socket, head);
|
|
532
|
+
});
|
|
533
|
+
request$2.on("response", (response) => this.#decline(request$2, response));
|
|
534
|
+
request$2.on("error", (error) => this.#fail(request$2, error));
|
|
535
|
+
request$2.end();
|
|
536
|
+
const socket = await deferred.promise;
|
|
537
|
+
this.#socket = socket;
|
|
538
|
+
this.#bind(socket);
|
|
539
|
+
}
|
|
540
|
+
async #stop() {
|
|
541
|
+
const request = this.#request;
|
|
542
|
+
if (request !== void 0) {
|
|
543
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} was closed before it finished connecting`, { url: this.#url }));
|
|
544
|
+
request.destroy();
|
|
545
|
+
}
|
|
546
|
+
await this.#starting?.catch(() => void 0);
|
|
544
547
|
const socket = this.#socket;
|
|
545
|
-
if (socket === void 0) return;
|
|
546
548
|
this.#socket = void 0;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
+
if (socket === void 0 || socket.readyState === WEBSOCKET_READY_CLOSED) return;
|
|
550
|
+
const closed = Promise.withResolvers();
|
|
551
|
+
this.#closeResolve = closed.resolve;
|
|
552
|
+
socket.emitter.on("close", this.#onSocketClose);
|
|
553
|
+
try {
|
|
549
554
|
socket.close();
|
|
550
|
-
|
|
555
|
+
if (socket.readyState !== WEBSOCKET_READY_CLOSED) await closed.promise;
|
|
556
|
+
} catch {
|
|
557
|
+
socket.destroy();
|
|
558
|
+
if (socket.readyState !== WEBSOCKET_READY_CLOSED) await closed.promise;
|
|
559
|
+
} finally {
|
|
560
|
+
socket.emitter.off("close", this.#onSocketClose);
|
|
561
|
+
this.#closeResolve = void 0;
|
|
562
|
+
}
|
|
551
563
|
}
|
|
552
|
-
#
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
564
|
+
#upgrade(request, key, response, socket, head) {
|
|
565
|
+
if (this.#request !== request) {
|
|
566
|
+
socket.destroy();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const accept = response.headers["sec-websocket-accept"];
|
|
570
|
+
if (!isString(accept) || accept !== computeWebSocketAccept(key)) {
|
|
571
|
+
socket.destroy();
|
|
572
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: Sec-WebSocket-Accept mismatch`, { url: this.#url }));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
this.#resolveRequest(request, createNodeWebSocket({
|
|
577
|
+
socket,
|
|
578
|
+
head
|
|
579
|
+
}));
|
|
580
|
+
} catch (error) {
|
|
581
|
+
socket.destroy();
|
|
582
|
+
this.#rejectRequest(request, error);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
#decline(request, response) {
|
|
586
|
+
response.resume();
|
|
587
|
+
const reason = response.statusCode === void 0 ? "upgrade was declined without a status code" : `upgrade declined with status ${response.statusCode}`;
|
|
588
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${reason}`, { url: this.#url }));
|
|
589
|
+
}
|
|
590
|
+
#fail(request, error) {
|
|
591
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} failed: ${error.message}`, { url: this.#url }));
|
|
592
|
+
}
|
|
593
|
+
#expire(request) {
|
|
594
|
+
this.#rejectRequest(request, new BrowserConnectionError(`WebSocket CDP connection to ${this.#url} timed out after ${this.#timeout}ms`, {
|
|
595
|
+
url: this.#url,
|
|
596
|
+
timeout: this.#timeout
|
|
597
|
+
}));
|
|
598
|
+
request.destroy();
|
|
599
|
+
}
|
|
600
|
+
#resolveRequest(request, socket) {
|
|
601
|
+
if (this.#request !== request) {
|
|
602
|
+
socket.destroy();
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const resolve = this.#resolve;
|
|
606
|
+
this.#clearRequest();
|
|
607
|
+
resolve?.(socket);
|
|
608
|
+
}
|
|
609
|
+
#rejectRequest(request, error) {
|
|
610
|
+
if (this.#request !== request) return;
|
|
611
|
+
const reject = this.#reject;
|
|
612
|
+
this.#clearRequest();
|
|
613
|
+
reject?.(error);
|
|
614
|
+
}
|
|
615
|
+
#clearRequest() {
|
|
616
|
+
if (this.#timer !== void 0) clearTimeout(this.#timer);
|
|
617
|
+
this.#timer = void 0;
|
|
618
|
+
this.#request = void 0;
|
|
619
|
+
this.#resolve = void 0;
|
|
620
|
+
this.#reject = void 0;
|
|
621
|
+
}
|
|
622
|
+
#bind(socket) {
|
|
623
|
+
socket.emitter.on("message", (data) => this.#emitter.emit("message", data));
|
|
624
|
+
socket.emitter.on("close", () => {
|
|
625
|
+
if (this.#socket === socket) this.#socket = void 0;
|
|
557
626
|
this.#emitter.emit("close");
|
|
558
627
|
});
|
|
559
|
-
|
|
560
|
-
this.#emitter.emit("error", error);
|
|
561
|
-
});
|
|
628
|
+
socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
562
629
|
}
|
|
563
630
|
};
|
|
564
631
|
//#endregion
|
|
@@ -605,20 +672,30 @@ function createScreenshotWriter() {
|
|
|
605
672
|
var Browser = class {
|
|
606
673
|
#emitter;
|
|
607
674
|
#options;
|
|
675
|
+
#abort = new AbortController();
|
|
676
|
+
#cdpPort;
|
|
677
|
+
#cdpHost;
|
|
608
678
|
#engine;
|
|
609
679
|
#status = "idle";
|
|
610
680
|
#connection;
|
|
681
|
+
#owned;
|
|
682
|
+
#endpoint;
|
|
611
683
|
#client;
|
|
612
684
|
#process;
|
|
613
|
-
#cdpPort;
|
|
614
|
-
#cdpHost;
|
|
615
685
|
#contexts = [];
|
|
616
686
|
#destroyed = false;
|
|
617
|
-
#
|
|
618
|
-
#
|
|
619
|
-
#
|
|
687
|
+
#connecting;
|
|
688
|
+
#disconnecting;
|
|
689
|
+
#shutdown;
|
|
690
|
+
#transport;
|
|
691
|
+
#onTransportClose = () => this.#handleTransportLoss();
|
|
692
|
+
#onTransportError = () => this.#handleTransportLoss();
|
|
693
|
+
#onProcessExit = () => this.#handleProcessExit();
|
|
620
694
|
constructor(options) {
|
|
621
|
-
this.#emitter = new Emitter({
|
|
695
|
+
this.#emitter = new Emitter({
|
|
696
|
+
on: options?.on,
|
|
697
|
+
error: options?.error
|
|
698
|
+
});
|
|
622
699
|
this.#options = options ?? {};
|
|
623
700
|
this.#engine = this.#options.engine ?? (this.#options.executable !== void 0 ? parseBrowserEngine(this.#options.executable) ?? "chromium" : "chromium");
|
|
624
701
|
this.#cdpPort = this.#options.cdp?.port ?? 9222;
|
|
@@ -637,63 +714,62 @@ var Browser = class {
|
|
|
637
714
|
get connection() {
|
|
638
715
|
return this.#connection;
|
|
639
716
|
}
|
|
717
|
+
get owned() {
|
|
718
|
+
return this.#owned;
|
|
719
|
+
}
|
|
640
720
|
get connected() {
|
|
641
721
|
return this.#status === "connected";
|
|
642
722
|
}
|
|
643
723
|
get pid() {
|
|
644
|
-
return this.#process?.pid
|
|
724
|
+
return this.#process?.pid;
|
|
645
725
|
}
|
|
646
726
|
async discover() {
|
|
647
|
-
const result = await this.#
|
|
727
|
+
const result = await this.#discoverCDP();
|
|
648
728
|
this.#emitter.emit("discover", result);
|
|
649
729
|
return result;
|
|
650
730
|
}
|
|
651
731
|
async connect() {
|
|
732
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
733
|
+
if (this.#disconnecting !== void 0) await this.#disconnecting;
|
|
652
734
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
653
735
|
if (this.#status === "connected") return;
|
|
654
|
-
this.#
|
|
655
|
-
|
|
736
|
+
const active = this.#connecting;
|
|
737
|
+
if (active !== void 0) {
|
|
738
|
+
await active;
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const attempt = this.#establish();
|
|
742
|
+
this.#connecting = attempt;
|
|
656
743
|
try {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
if (this.#options.cdp?.discover ?? true) {
|
|
663
|
-
this.#assertNotAborted();
|
|
664
|
-
const discovery = await this.#raceAbort(this.#discoverCdp());
|
|
665
|
-
if (discovery.found && discovery.endpoint !== void 0) {
|
|
666
|
-
this.#engine = browserToEngine(discovery.browser ?? "");
|
|
667
|
-
await this.#connectCdp(discovery.endpoint);
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
} else {
|
|
671
|
-
this.#assertNotAborted();
|
|
672
|
-
await this.#raceAbort(this.#assertPortFree());
|
|
673
|
-
}
|
|
674
|
-
this.#assertNotAborted();
|
|
675
|
-
await this.#launch();
|
|
676
|
-
} catch (thrown) {
|
|
677
|
-
this.#status = "error";
|
|
678
|
-
this.#emitter.emit("error", thrown);
|
|
679
|
-
if (thrown instanceof BrowserConnectionError) throw thrown;
|
|
680
|
-
throw new BrowserConnectionError(thrown instanceof Error ? thrown.message : String(thrown), { executable: this.#options.executable });
|
|
744
|
+
await attempt;
|
|
745
|
+
} finally {
|
|
746
|
+
if (this.#connecting === attempt) this.#connecting = void 0;
|
|
681
747
|
}
|
|
682
748
|
}
|
|
749
|
+
adopt() {
|
|
750
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
751
|
+
if (this.#status !== "connected" || this.#client === void 0 || this.#endpoint === void 0) throw new BrowserNotConnectedError();
|
|
752
|
+
this.#owned = true;
|
|
753
|
+
}
|
|
683
754
|
async disconnect() {
|
|
684
|
-
if (this.#destroyed
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
this.#
|
|
695
|
-
this.#
|
|
696
|
-
this.#
|
|
755
|
+
if (this.#destroyed) return;
|
|
756
|
+
const active = this.#disconnecting;
|
|
757
|
+
if (active !== void 0) {
|
|
758
|
+
await active;
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (this.#connecting !== void 0) {
|
|
762
|
+
await this.#connecting.catch(() => void 0);
|
|
763
|
+
if (this.#destroyed) return;
|
|
764
|
+
}
|
|
765
|
+
if (this.#status !== "connected" || this.#client === void 0) return;
|
|
766
|
+
const attempt = this.#detach();
|
|
767
|
+
this.#disconnecting = attempt;
|
|
768
|
+
try {
|
|
769
|
+
await attempt;
|
|
770
|
+
} finally {
|
|
771
|
+
if (this.#disconnecting === attempt) this.#disconnecting = void 0;
|
|
772
|
+
}
|
|
697
773
|
}
|
|
698
774
|
context(index) {
|
|
699
775
|
const i = index ?? 0;
|
|
@@ -705,237 +781,218 @@ var Browser = class {
|
|
|
705
781
|
}
|
|
706
782
|
async create(options) {
|
|
707
783
|
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
this.#
|
|
784
|
+
const client = this.#client;
|
|
785
|
+
if (this.#status !== "connected" || client === void 0) throw new BrowserNotConnectedError();
|
|
786
|
+
let context = this.#contexts[0];
|
|
787
|
+
if (context === void 0) {
|
|
788
|
+
context = new BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
|
|
789
|
+
this.#contexts.push(context);
|
|
713
790
|
}
|
|
714
|
-
const page = await
|
|
791
|
+
const page = await context.create(options);
|
|
715
792
|
this.#emitter.emit("page", page);
|
|
716
793
|
return page;
|
|
717
794
|
}
|
|
718
|
-
|
|
719
|
-
|
|
795
|
+
destroy() {
|
|
796
|
+
const active = this.#shutdown;
|
|
797
|
+
if (active !== void 0) return active;
|
|
798
|
+
if (this.#destroyed) return Promise.resolve();
|
|
720
799
|
this.#destroyed = true;
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
await ctx.close();
|
|
726
|
-
} catch {}
|
|
727
|
-
this.#contexts = [];
|
|
728
|
-
await this.#terminate(this.#process);
|
|
729
|
-
this.#process = void 0;
|
|
730
|
-
if (this.#client !== void 0) {
|
|
731
|
-
try {
|
|
732
|
-
await this.#client.close();
|
|
733
|
-
} catch {}
|
|
734
|
-
this.#client = void 0;
|
|
735
|
-
}
|
|
736
|
-
} finally {
|
|
737
|
-
this.#status = "disconnected";
|
|
738
|
-
this.#connection = void 0;
|
|
739
|
-
this.#lastPid = void 0;
|
|
740
|
-
this.#emitter.emit("destroy");
|
|
741
|
-
this.#emitter.destroy();
|
|
742
|
-
}
|
|
800
|
+
this.#abort.abort();
|
|
801
|
+
const shutdown = this.#destroyResources();
|
|
802
|
+
this.#shutdown = shutdown;
|
|
803
|
+
return shutdown;
|
|
743
804
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
* @remarks
|
|
749
|
-
* Sends CDP `Browser.close` best-effort; on an owned browser also awaits
|
|
750
|
-
* the process's exit (bounded by the kill-escalation grace period,
|
|
751
|
-
* escalating to a kill only if it does not exit in time). Ends in the
|
|
752
|
-
* same local-cleanup state as `destroy()`.
|
|
753
|
-
*/
|
|
754
|
-
async close() {
|
|
755
|
-
if (this.#destroyed) return;
|
|
805
|
+
close() {
|
|
806
|
+
const active = this.#shutdown;
|
|
807
|
+
if (active !== void 0) return active;
|
|
808
|
+
if (this.#destroyed) return Promise.resolve();
|
|
756
809
|
this.#destroyed = true;
|
|
810
|
+
this.#abort.abort();
|
|
811
|
+
const shutdown = this.#closeResources();
|
|
812
|
+
this.#shutdown = shutdown;
|
|
813
|
+
return shutdown;
|
|
814
|
+
}
|
|
815
|
+
async #establish() {
|
|
816
|
+
this.#assertNotAborted();
|
|
817
|
+
this.#status = "connecting";
|
|
757
818
|
try {
|
|
758
|
-
this.#
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if (client !== void 0) try {
|
|
762
|
-
await client.send("Browser.close");
|
|
763
|
-
} catch {}
|
|
764
|
-
const process = this.#process;
|
|
765
|
-
if (process !== void 0) {
|
|
766
|
-
const exited = new Promise((resolve) => {
|
|
767
|
-
process.once("exit", () => resolve());
|
|
768
|
-
});
|
|
769
|
-
let timer;
|
|
770
|
-
const timedOut = await Promise.race([exited.then(() => false), new Promise((resolve) => {
|
|
771
|
-
timer = setTimeout(() => resolve(true), BROWSER_KILL_GRACE_MS);
|
|
772
|
-
})]);
|
|
773
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
774
|
-
if (timedOut) await this.#terminate(process);
|
|
775
|
-
this.#process = void 0;
|
|
819
|
+
if (this.#owned === true && this.#endpoint !== void 0) {
|
|
820
|
+
await this.#connectCDP(this.#endpoint);
|
|
821
|
+
return;
|
|
776
822
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
if (client !== void 0) {
|
|
782
|
-
try {
|
|
783
|
-
await client.close();
|
|
784
|
-
} catch {}
|
|
785
|
-
this.#client = void 0;
|
|
823
|
+
const endpoint = this.#options.cdp?.endpoint;
|
|
824
|
+
if (endpoint !== void 0) {
|
|
825
|
+
await this.#connectCDP(endpoint);
|
|
826
|
+
return;
|
|
786
827
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
828
|
+
if (this.#options.cdp?.discover ?? true) {
|
|
829
|
+
const discovery = await this.#raceAbort(this.#discoverCDP(this.#signal()));
|
|
830
|
+
if (discovery.found && discovery.endpoint !== void 0) {
|
|
831
|
+
this.#engine = browserToEngine(discovery.browser);
|
|
832
|
+
await this.#connectCDP(discovery.endpoint);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
} else await this.#raceAbort(this.#assertPortFree());
|
|
836
|
+
this.#assertNotAborted();
|
|
837
|
+
await this.#launch();
|
|
838
|
+
} catch (error) {
|
|
839
|
+
if (this.#destroyed) throw new BrowserDestroyedError();
|
|
840
|
+
this.#status = "error";
|
|
841
|
+
this.#emitter.emit("error", error);
|
|
842
|
+
if (error instanceof BrowserConnectionError) throw error;
|
|
843
|
+
throw new BrowserConnectionError(error instanceof Error ? error.message : String(error), { executable: this.#options.executable });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
async #detach() {
|
|
847
|
+
this.#status = "disconnected";
|
|
848
|
+
this.#connection = void 0;
|
|
849
|
+
this.#unbindTransport();
|
|
850
|
+
const client = this.#client;
|
|
851
|
+
this.#client = void 0;
|
|
852
|
+
await this.#destroyContexts();
|
|
853
|
+
this.#contexts = [];
|
|
854
|
+
if (this.#owned !== true) {
|
|
855
|
+
this.#owned = void 0;
|
|
856
|
+
this.#endpoint = void 0;
|
|
793
857
|
}
|
|
858
|
+
await this.#closeClient(client);
|
|
859
|
+
if (this.#destroyed) return;
|
|
860
|
+
this.#emitter.emit("disconnect");
|
|
861
|
+
this.#emitter.emit("idle");
|
|
794
862
|
}
|
|
795
863
|
#assertNotAborted() {
|
|
796
|
-
if (this.#
|
|
864
|
+
if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted");
|
|
865
|
+
}
|
|
866
|
+
#signal() {
|
|
867
|
+
const external = this.#options.signal;
|
|
868
|
+
return external === void 0 ? this.#abort.signal : AbortSignal.any([this.#abort.signal, external]);
|
|
797
869
|
}
|
|
798
|
-
/**
|
|
799
|
-
* Race a promise against the connection's external AbortSignal (if any),
|
|
800
|
-
* rejecting promptly with a coded BrowserConnectionError when it fires.
|
|
801
|
-
*/
|
|
802
870
|
async #raceAbort(promise) {
|
|
803
|
-
const signal = this.#
|
|
804
|
-
if (signal === void 0) return promise;
|
|
871
|
+
const signal = this.#signal();
|
|
805
872
|
if (signal.aborted) throw new BrowserConnectionError("Connection aborted");
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
abortWon = true;
|
|
810
|
-
cleanup();
|
|
811
|
-
reject(new BrowserConnectionError("Connection aborted"));
|
|
812
|
-
};
|
|
813
|
-
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
814
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
815
|
-
promise.then((value) => {
|
|
816
|
-
cleanup();
|
|
817
|
-
resolve(value);
|
|
818
|
-
}, (error) => {
|
|
819
|
-
cleanup();
|
|
820
|
-
reject(error);
|
|
821
|
-
});
|
|
822
|
-
}).catch((error) => {
|
|
823
|
-
if (abortWon) promise.catch(() => void 0);
|
|
824
|
-
throw error;
|
|
873
|
+
const aborted = Promise.withResolvers();
|
|
874
|
+
const listener = addAbortListener(signal, () => {
|
|
875
|
+
aborted.reject(new BrowserConnectionError("Connection aborted"));
|
|
825
876
|
});
|
|
877
|
+
try {
|
|
878
|
+
return await Promise.race([promise, aborted.promise]);
|
|
879
|
+
} finally {
|
|
880
|
+
listener[Symbol.dispose]();
|
|
881
|
+
promise.catch(() => void 0);
|
|
882
|
+
}
|
|
826
883
|
}
|
|
827
|
-
#reset() {
|
|
828
|
-
this.#contexts = [];
|
|
829
|
-
this.#status = "disconnected";
|
|
830
|
-
this.#connection = void 0;
|
|
831
|
-
this.#client = void 0;
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Handle the owned process exiting on its own (not via destroy()/close()).
|
|
835
|
-
* The process is already gone — no kill is attempted. Cleans up orphaned
|
|
836
|
-
* state so the next connect() call can start fresh, and emits a coded
|
|
837
|
-
* `error` (cause: process exit) before `disconnect`.
|
|
838
|
-
*/
|
|
839
884
|
#handleProcessExit() {
|
|
840
885
|
if (this.#destroyed) return;
|
|
841
|
-
|
|
886
|
+
const connected = this.#status === "connected";
|
|
887
|
+
const client = this.#client;
|
|
888
|
+
const contexts = this.#contexts;
|
|
842
889
|
this.#unbindTransport();
|
|
843
890
|
this.#unbindProcess();
|
|
844
|
-
const client = this.#client;
|
|
845
891
|
this.#process = void 0;
|
|
846
|
-
this.#
|
|
847
|
-
this.#
|
|
848
|
-
|
|
849
|
-
this.#
|
|
850
|
-
this.#
|
|
851
|
-
this.#
|
|
892
|
+
this.#client = void 0;
|
|
893
|
+
this.#contexts = [];
|
|
894
|
+
this.#connection = void 0;
|
|
895
|
+
this.#owned = void 0;
|
|
896
|
+
this.#endpoint = void 0;
|
|
897
|
+
if (connected) this.#status = "disconnected";
|
|
898
|
+
this.#destroyContextList(contexts);
|
|
899
|
+
this.#closeClient(client);
|
|
900
|
+
this.#emitter.emit("error", new BrowserConnectionError("The browser process exited unexpectedly", { cause: BROWSER_PROCESS_EXIT_CAUSE }));
|
|
901
|
+
if (connected) {
|
|
902
|
+
this.#emitter.emit("disconnect");
|
|
903
|
+
this.#emitter.emit("idle");
|
|
904
|
+
}
|
|
852
905
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
* still alive, or the connection is CDP-attached. Never kills the
|
|
856
|
-
* process — only the local client is reset, so the SAME instance can
|
|
857
|
-
* reconnect (e.g. rediscover on the port and reattach). Emits a coded
|
|
858
|
-
* `error` (cause: connection loss) before `disconnect`.
|
|
859
|
-
*/
|
|
860
|
-
#handleTransportLoss(deferred = false) {
|
|
861
|
-
if (this.#destroyed) return;
|
|
862
|
-
if (this.#status !== "connected") return;
|
|
906
|
+
#handleTransportLoss() {
|
|
907
|
+
if (this.#destroyed || this.#status !== "connected") return;
|
|
863
908
|
const process = this.#process;
|
|
864
909
|
if (process !== void 0 && (process.exitCode !== null || process.signalCode !== null)) {
|
|
865
910
|
this.#handleProcessExit();
|
|
866
911
|
return;
|
|
867
912
|
}
|
|
868
|
-
if (process !== void 0
|
|
869
|
-
setTimeout(() => this.#
|
|
913
|
+
if (process !== void 0) {
|
|
914
|
+
setTimeout(() => this.#confirmTransportLoss(), 50);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
this.#confirmTransportLoss();
|
|
918
|
+
}
|
|
919
|
+
#confirmTransportLoss() {
|
|
920
|
+
if (this.#destroyed || this.#status !== "connected") return;
|
|
921
|
+
const process = this.#process;
|
|
922
|
+
if (process !== void 0 && (process.exitCode !== null || process.signalCode !== null)) {
|
|
923
|
+
this.#handleProcessExit();
|
|
870
924
|
return;
|
|
871
925
|
}
|
|
872
|
-
if (process
|
|
926
|
+
if (process?.pid !== void 0) try {
|
|
873
927
|
process.kill(0);
|
|
874
928
|
} catch {
|
|
875
929
|
this.#handleProcessExit();
|
|
876
930
|
return;
|
|
877
931
|
}
|
|
878
|
-
this.#unbindTransport();
|
|
879
932
|
const client = this.#client;
|
|
880
|
-
this.#
|
|
881
|
-
|
|
882
|
-
this.#
|
|
933
|
+
const owned = this.#owned === true;
|
|
934
|
+
const contexts = this.#contexts;
|
|
935
|
+
this.#unbindTransport();
|
|
936
|
+
this.#client = void 0;
|
|
937
|
+
this.#contexts = [];
|
|
938
|
+
this.#connection = void 0;
|
|
939
|
+
this.#status = "disconnected";
|
|
940
|
+
if (!owned) {
|
|
941
|
+
this.#owned = void 0;
|
|
942
|
+
this.#endpoint = void 0;
|
|
943
|
+
}
|
|
944
|
+
this.#destroyContextList(contexts);
|
|
945
|
+
this.#closeClient(client);
|
|
946
|
+
this.#emitter.emit("error", new BrowserConnectionError("The CDP transport connection was lost", { cause: BROWSER_TRANSPORT_LOSS_CAUSE }));
|
|
883
947
|
this.#emitter.emit("disconnect");
|
|
884
948
|
this.#emitter.emit("idle");
|
|
885
949
|
}
|
|
886
|
-
/** Subscribe to the CDP transport's close/error events for deterministic transport-loss detection. */
|
|
887
950
|
#bindTransport(transport) {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
transport.emitter.on("close",
|
|
891
|
-
transport.emitter.on("error",
|
|
892
|
-
this.#transportUnbind = () => {
|
|
893
|
-
transport.emitter.off("close", onClose);
|
|
894
|
-
transport.emitter.off("error", onError);
|
|
895
|
-
};
|
|
951
|
+
this.#unbindTransport();
|
|
952
|
+
this.#transport = transport;
|
|
953
|
+
transport.emitter.on("close", this.#onTransportClose);
|
|
954
|
+
transport.emitter.on("error", this.#onTransportError);
|
|
896
955
|
}
|
|
897
956
|
#unbindTransport() {
|
|
898
|
-
this.#
|
|
899
|
-
|
|
957
|
+
const transport = this.#transport;
|
|
958
|
+
if (transport === void 0) return;
|
|
959
|
+
transport.emitter.off("close", this.#onTransportClose);
|
|
960
|
+
transport.emitter.off("error", this.#onTransportError);
|
|
961
|
+
this.#transport = void 0;
|
|
900
962
|
}
|
|
901
|
-
/** Subscribe to a launched process's exit event for deterministic process-exit detection. */
|
|
902
963
|
#bindProcess(process) {
|
|
903
|
-
|
|
904
|
-
process.once("exit",
|
|
905
|
-
|
|
964
|
+
this.#unbindProcess();
|
|
965
|
+
process.once("exit", this.#onProcessExit);
|
|
966
|
+
if (process.exitCode !== null || process.signalCode !== null) queueMicrotask(this.#onProcessExit);
|
|
906
967
|
}
|
|
907
968
|
#unbindProcess() {
|
|
908
|
-
this.#
|
|
909
|
-
this.#processUnbind = void 0;
|
|
969
|
+
this.#process?.off("exit", this.#onProcessExit);
|
|
910
970
|
}
|
|
911
971
|
#timeout() {
|
|
912
972
|
return this.#options.timeout ?? BROWSER_DEFAULT_TIMEOUT_MS;
|
|
913
973
|
}
|
|
914
|
-
async #
|
|
915
|
-
const
|
|
916
|
-
const
|
|
917
|
-
const controller = new AbortController();
|
|
918
|
-
const timer = setTimeout(() => controller.abort(), this.#timeout());
|
|
974
|
+
async #discoverCDP(signal) {
|
|
975
|
+
const url = `${BROWSER_CDP_PROTOCOL}://${this.#cdpHost}:${this.#cdpPort}${BROWSER_CDP_VERSION_PATH}`;
|
|
976
|
+
const requestSignal = signal === void 0 ? AbortSignal.timeout(this.#timeout()) : AbortSignal.any([signal, AbortSignal.timeout(this.#timeout())]);
|
|
919
977
|
try {
|
|
920
|
-
const response = await fetch(url, { signal:
|
|
921
|
-
if (!response.ok) return this.#
|
|
978
|
+
const response = await fetch(url, { signal: requestSignal });
|
|
979
|
+
if (!response.ok) return this.#notFound();
|
|
922
980
|
const info = await response.json();
|
|
923
|
-
if (!isRecord(info)) return this.#
|
|
981
|
+
if (!isRecord(info)) return this.#notFound();
|
|
924
982
|
const endpoint = isString(info["webSocketDebuggerUrl"]) ? info["webSocketDebuggerUrl"] : void 0;
|
|
925
|
-
const
|
|
983
|
+
const browser = isString(info["Browser"]) ? info["Browser"] : void 0;
|
|
926
984
|
return {
|
|
927
985
|
found: endpoint !== void 0,
|
|
928
986
|
endpoint,
|
|
929
|
-
browser
|
|
987
|
+
browser,
|
|
930
988
|
connection: endpoint !== void 0 ? "cdp" : void 0
|
|
931
989
|
};
|
|
932
|
-
} catch {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
clearTimeout(timer);
|
|
990
|
+
} catch (error) {
|
|
991
|
+
if (signal?.aborted === true) throw error;
|
|
992
|
+
return this.#notFound();
|
|
936
993
|
}
|
|
937
994
|
}
|
|
938
|
-
#
|
|
995
|
+
#notFound() {
|
|
939
996
|
return {
|
|
940
997
|
found: false,
|
|
941
998
|
endpoint: void 0,
|
|
@@ -943,31 +1000,21 @@ var Browser = class {
|
|
|
943
1000
|
connection: void 0
|
|
944
1001
|
};
|
|
945
1002
|
}
|
|
946
|
-
/**
|
|
947
|
-
* Reject when something is already listening on the configured CDP port —
|
|
948
|
-
* used ahead of a `discover: false` launch, so a caller demanding a fresh
|
|
949
|
-
* browser never silently attaches to a stranger already on that port.
|
|
950
|
-
*/
|
|
951
1003
|
async #assertPortFree() {
|
|
952
1004
|
if (await this.#probePort()) throw new BrowserConnectionError(`Port ${this.#cdpPort} on ${this.#cdpHost} is already occupied by another CDP endpoint`, {
|
|
953
1005
|
port: this.#cdpPort,
|
|
954
1006
|
host: this.#cdpHost
|
|
955
1007
|
});
|
|
956
1008
|
}
|
|
957
|
-
/** Very short probe of the CDP version endpoint — just enough to detect an occupied port, not full discovery. */
|
|
958
1009
|
async #probePort() {
|
|
959
1010
|
const url = `${BROWSER_CDP_PROTOCOL}://${this.#cdpHost}:${this.#cdpPort}${BROWSER_CDP_VERSION_PATH}`;
|
|
960
|
-
const controller = new AbortController();
|
|
961
|
-
const timer = setTimeout(() => controller.abort(), 200);
|
|
962
1011
|
try {
|
|
963
|
-
return (await fetch(url, { signal:
|
|
1012
|
+
return (await fetch(url, { signal: AbortSignal.timeout(200) })).ok;
|
|
964
1013
|
} catch {
|
|
965
1014
|
return false;
|
|
966
|
-
} finally {
|
|
967
|
-
clearTimeout(timer);
|
|
968
1015
|
}
|
|
969
1016
|
}
|
|
970
|
-
async #
|
|
1017
|
+
async #connectCDP(endpoint) {
|
|
971
1018
|
const transport = createCDPTransport({
|
|
972
1019
|
url: endpoint,
|
|
973
1020
|
timeout: this.#timeout()
|
|
@@ -976,18 +1023,27 @@ var Browser = class {
|
|
|
976
1023
|
transport,
|
|
977
1024
|
timeout: this.#timeout()
|
|
978
1025
|
});
|
|
1026
|
+
const retained = this.#owned === true && this.#endpoint === endpoint;
|
|
979
1027
|
try {
|
|
980
1028
|
await this.#raceAbort(client.connect());
|
|
981
1029
|
this.#client = client;
|
|
982
|
-
this.#
|
|
1030
|
+
this.#endpoint = endpoint;
|
|
1031
|
+
this.#connection = retained ? this.#retainedConnection() : "cdp";
|
|
1032
|
+
this.#owned = retained;
|
|
983
1033
|
this.#status = "connected";
|
|
984
1034
|
this.#bindTransport(transport);
|
|
985
1035
|
await this.#syncContexts();
|
|
986
|
-
this.#emitter.emit("connect",
|
|
1036
|
+
this.#emitter.emit("connect", this.#connection);
|
|
987
1037
|
} catch (error) {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1038
|
+
this.#unbindTransport();
|
|
1039
|
+
if (this.#client === client) this.#client = void 0;
|
|
1040
|
+
this.#contexts = [];
|
|
1041
|
+
this.#connection = void 0;
|
|
1042
|
+
if (!retained) {
|
|
1043
|
+
this.#owned = void 0;
|
|
1044
|
+
this.#endpoint = void 0;
|
|
1045
|
+
}
|
|
1046
|
+
await this.#closeClient(client);
|
|
991
1047
|
throw error;
|
|
992
1048
|
}
|
|
993
1049
|
}
|
|
@@ -1005,18 +1061,16 @@ var Browser = class {
|
|
|
1005
1061
|
executable = found?.executable;
|
|
1006
1062
|
resolvedEngine = found?.engine;
|
|
1007
1063
|
}
|
|
1008
|
-
if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine
|
|
1064
|
+
if (executable === void 0) throw new BrowserConnectionError("No Chromium browser found. Install Chrome, Edge, or Chromium.", requestedEngine === void 0 ? void 0 : { engine: requestedEngine });
|
|
1009
1065
|
this.#engine = resolvedEngine ?? "chromium";
|
|
1010
|
-
const headless = this.#options.headless ?? true;
|
|
1011
1066
|
const profile = this.#options.profile;
|
|
1012
|
-
const
|
|
1013
|
-
const process = launchBrowserProcess(executable, this.#cdpPort, headless, profile, extra);
|
|
1067
|
+
const process = launchBrowserProcess(executable, this.#cdpPort, this.#options.headless ?? true, profile, this.#options.args);
|
|
1014
1068
|
this.#process = process;
|
|
1015
|
-
let transport;
|
|
1016
1069
|
let client;
|
|
1017
1070
|
try {
|
|
1018
|
-
|
|
1019
|
-
|
|
1071
|
+
const endpoint = await this.#waitForLaunch(process, executable, this.#options.args);
|
|
1072
|
+
const transport = createCDPTransport({
|
|
1073
|
+
url: endpoint,
|
|
1020
1074
|
timeout: this.#timeout()
|
|
1021
1075
|
});
|
|
1022
1076
|
client = new CDPClient({
|
|
@@ -1024,20 +1078,26 @@ var Browser = class {
|
|
|
1024
1078
|
timeout: this.#timeout()
|
|
1025
1079
|
});
|
|
1026
1080
|
await this.#raceAbort(client.connect());
|
|
1081
|
+
const connection = profile === void 0 ? "launch" : "persistent";
|
|
1027
1082
|
this.#client = client;
|
|
1028
|
-
this.#
|
|
1083
|
+
this.#endpoint = endpoint;
|
|
1084
|
+
this.#connection = connection;
|
|
1085
|
+
this.#owned = true;
|
|
1029
1086
|
this.#status = "connected";
|
|
1030
1087
|
this.#bindTransport(transport);
|
|
1031
1088
|
this.#bindProcess(process);
|
|
1032
1089
|
await this.#syncContexts();
|
|
1033
1090
|
this.#emitter.emit("launch", this.#engine);
|
|
1034
|
-
this.#emitter.emit("connect",
|
|
1091
|
+
this.#emitter.emit("connect", connection);
|
|
1035
1092
|
} catch (error) {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1093
|
+
this.#unbindTransport();
|
|
1094
|
+
this.#unbindProcess();
|
|
1095
|
+
await this.#closeClient(client);
|
|
1039
1096
|
await this.#terminate(process);
|
|
1040
1097
|
this.#process = void 0;
|
|
1098
|
+
this.#client = void 0;
|
|
1099
|
+
this.#endpoint = void 0;
|
|
1100
|
+
this.#owned = void 0;
|
|
1041
1101
|
throw error;
|
|
1042
1102
|
}
|
|
1043
1103
|
}
|
|
@@ -1046,75 +1106,191 @@ var Browser = class {
|
|
|
1046
1106
|
executable,
|
|
1047
1107
|
args
|
|
1048
1108
|
};
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
callback();
|
|
1057
|
-
};
|
|
1058
|
-
const onError = (error) => {
|
|
1059
|
-
finish(() => reject(new BrowserConnectionError(error.message, context)));
|
|
1060
|
-
};
|
|
1061
|
-
const onExit = (code, signal) => {
|
|
1062
|
-
finish(() => reject(new BrowserConnectionError(this.#formatLaunchExit(code, signal), context)));
|
|
1063
|
-
};
|
|
1064
|
-
process.once("error", onError);
|
|
1065
|
-
process.once("exit", onExit);
|
|
1066
|
-
waitForCdpReady(this.#cdpPort, this.#timeout(), this.#cdpHost).then((wsUrl) => {
|
|
1067
|
-
finish(() => resolve(wsUrl));
|
|
1068
|
-
}, (error) => {
|
|
1069
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1070
|
-
finish(() => reject(new BrowserConnectionError(message, context)));
|
|
1071
|
-
});
|
|
1109
|
+
const controller = new AbortController();
|
|
1110
|
+
const signal = AbortSignal.any([controller.signal, this.#signal()]);
|
|
1111
|
+
const ready = waitForCDPReady(this.#cdpPort, this.#timeout(), this.#cdpHost, signal);
|
|
1112
|
+
const exited = once(process, "exit", { signal }).then((values) => {
|
|
1113
|
+
const code = typeof values[0] === "number" ? values[0] : null;
|
|
1114
|
+
const exitSignal = typeof values[1] === "string" ? values[1] : null;
|
|
1115
|
+
throw new BrowserConnectionError(this.#formatLaunchExit(code, exitSignal), context);
|
|
1072
1116
|
});
|
|
1117
|
+
try {
|
|
1118
|
+
return await Promise.race([ready, exited]);
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
if (error instanceof BrowserConnectionError) throw error;
|
|
1121
|
+
if (this.#signal().aborted) throw new BrowserConnectionError("Connection aborted", context);
|
|
1122
|
+
throw new BrowserConnectionError(error instanceof Error ? error.message : String(error), context);
|
|
1123
|
+
} finally {
|
|
1124
|
+
controller.abort();
|
|
1125
|
+
ready.catch(() => void 0);
|
|
1126
|
+
exited.catch(() => void 0);
|
|
1127
|
+
}
|
|
1073
1128
|
}
|
|
1074
1129
|
#formatLaunchExit(code, signal) {
|
|
1075
1130
|
if (signal !== null) return `Browser process exited before CDP became ready (signal: ${signal})`;
|
|
1076
1131
|
if (code !== null) return `Browser process exited before CDP became ready (code: ${code})`;
|
|
1077
1132
|
return "Browser process exited before CDP became ready";
|
|
1078
1133
|
}
|
|
1134
|
+
#retainedConnection() {
|
|
1135
|
+
if (this.#process === void 0) return "cdp";
|
|
1136
|
+
return this.#options.profile === void 0 ? "launch" : "persistent";
|
|
1137
|
+
}
|
|
1079
1138
|
async #syncContexts() {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
await
|
|
1085
|
-
|
|
1139
|
+
const client = this.#client;
|
|
1140
|
+
if (client === void 0) return;
|
|
1141
|
+
let result;
|
|
1142
|
+
try {
|
|
1143
|
+
result = await client.send("Target.getTargets");
|
|
1144
|
+
} catch {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
if (!isRecord(result) || !Array.isArray(result["targetInfos"])) return;
|
|
1148
|
+
const pages = [];
|
|
1149
|
+
for (const target of result["targetInfos"]) {
|
|
1150
|
+
if (!isRecord(target) || !isString(target["targetId"]) || target["type"] !== "page" || !isString(target["title"]) || !isString(target["url"])) continue;
|
|
1151
|
+
pages.push({
|
|
1152
|
+
id: target["targetId"],
|
|
1153
|
+
type: target["type"],
|
|
1154
|
+
title: target["title"],
|
|
1155
|
+
url: target["url"]
|
|
1156
|
+
});
|
|
1086
1157
|
}
|
|
1158
|
+
if (pages.length === 0 || this.#contexts.length > 0) return;
|
|
1159
|
+
const context = new BrowserContext(client, void 0, this.#options.viewport, createScreenshotWriter());
|
|
1160
|
+
await context.sync(pages);
|
|
1161
|
+
this.#contexts.push(context);
|
|
1087
1162
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1163
|
+
async #destroyResources() {
|
|
1164
|
+
try {
|
|
1165
|
+
await this.#settle();
|
|
1166
|
+
this.#unbindTransport();
|
|
1167
|
+
this.#unbindProcess();
|
|
1168
|
+
const client = this.#client;
|
|
1169
|
+
if (this.#owned === true) {
|
|
1170
|
+
await this.#closeContexts();
|
|
1171
|
+
if (this.#process === void 0) await this.#closeRemote(client);
|
|
1172
|
+
} else await this.#destroyContexts();
|
|
1173
|
+
this.#contexts = [];
|
|
1174
|
+
await this.#terminate(this.#process);
|
|
1175
|
+
this.#process = void 0;
|
|
1176
|
+
await this.#closeClient(client);
|
|
1177
|
+
this.#client = void 0;
|
|
1178
|
+
} finally {
|
|
1179
|
+
this.#finish();
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async #closeResources() {
|
|
1183
|
+
try {
|
|
1184
|
+
await this.#settle();
|
|
1185
|
+
this.#unbindTransport();
|
|
1186
|
+
this.#unbindProcess();
|
|
1187
|
+
const client = this.#client;
|
|
1188
|
+
await this.#closeContexts();
|
|
1189
|
+
this.#contexts = [];
|
|
1190
|
+
await this.#closeRemote(client);
|
|
1191
|
+
const process = this.#process;
|
|
1192
|
+
if (process !== void 0) {
|
|
1193
|
+
if (!await this.#waitForProcessWithin(process, 3e3)) await this.#terminate(process);
|
|
1194
|
+
}
|
|
1195
|
+
this.#process = void 0;
|
|
1196
|
+
await this.#closeClient(client);
|
|
1197
|
+
this.#client = void 0;
|
|
1198
|
+
} finally {
|
|
1199
|
+
this.#finish();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async #settle() {
|
|
1203
|
+
await this.#connecting?.catch(() => void 0);
|
|
1204
|
+
await this.#disconnecting?.catch(() => void 0);
|
|
1205
|
+
}
|
|
1206
|
+
async #closeRemote(client) {
|
|
1207
|
+
let remote = client;
|
|
1208
|
+
let temporary = false;
|
|
1209
|
+
if (remote === void 0 && this.#owned === true && this.#endpoint !== void 0) {
|
|
1210
|
+
remote = new CDPClient({
|
|
1211
|
+
transport: createCDPTransport({
|
|
1212
|
+
url: this.#endpoint,
|
|
1213
|
+
timeout: this.#timeout()
|
|
1214
|
+
}),
|
|
1215
|
+
timeout: this.#timeout()
|
|
1216
|
+
});
|
|
1217
|
+
try {
|
|
1218
|
+
await remote.connect();
|
|
1219
|
+
temporary = true;
|
|
1220
|
+
} catch {
|
|
1221
|
+
await this.#closeClient(remote);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (remote !== void 0) try {
|
|
1226
|
+
await remote.send("Browser.close");
|
|
1227
|
+
} catch {}
|
|
1228
|
+
if (temporary) await this.#closeClient(remote);
|
|
1229
|
+
}
|
|
1230
|
+
async #closeContexts() {
|
|
1231
|
+
for (const context of this.#contexts) try {
|
|
1232
|
+
await context.close();
|
|
1233
|
+
} catch {}
|
|
1234
|
+
}
|
|
1235
|
+
async #destroyContexts() {
|
|
1236
|
+
await this.#destroyContextList(this.#contexts);
|
|
1237
|
+
}
|
|
1238
|
+
async #destroyContextList(contexts) {
|
|
1239
|
+
for (const context of contexts) try {
|
|
1240
|
+
await context.destroy();
|
|
1241
|
+
} catch {}
|
|
1242
|
+
}
|
|
1243
|
+
async #closeClient(client) {
|
|
1244
|
+
if (client === void 0) return;
|
|
1245
|
+
try {
|
|
1246
|
+
await client.close();
|
|
1247
|
+
} catch {}
|
|
1248
|
+
}
|
|
1249
|
+
async #waitForProcess(process) {
|
|
1095
1250
|
if (process.exitCode !== null || process.signalCode !== null) return;
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1251
|
+
await once(process, "exit");
|
|
1252
|
+
}
|
|
1253
|
+
async #waitForProcessWithin(process, timeout) {
|
|
1254
|
+
if (process.exitCode !== null || process.signalCode !== null) return true;
|
|
1255
|
+
const signal = AbortSignal.timeout(timeout);
|
|
1256
|
+
try {
|
|
1257
|
+
await once(process, "exit", { signal });
|
|
1258
|
+
return true;
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
if (signal.aborted) return process.exitCode !== null || process.signalCode !== null;
|
|
1261
|
+
throw error;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
async #terminate(process) {
|
|
1265
|
+
if (process === void 0 || process.exitCode !== null || process.signalCode !== null) return;
|
|
1099
1266
|
try {
|
|
1100
1267
|
process.kill("SIGTERM");
|
|
1101
1268
|
} catch {
|
|
1102
1269
|
return;
|
|
1103
1270
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
if (timedOut) {
|
|
1110
|
-
try {
|
|
1111
|
-
process.kill("SIGKILL");
|
|
1112
|
-
} catch {}
|
|
1113
|
-
await exited;
|
|
1271
|
+
if (await this.#waitForProcessWithin(process, 3e3)) return;
|
|
1272
|
+
try {
|
|
1273
|
+
process.kill("SIGKILL");
|
|
1274
|
+
} catch {
|
|
1275
|
+
return;
|
|
1114
1276
|
}
|
|
1277
|
+
await this.#waitForProcess(process);
|
|
1278
|
+
}
|
|
1279
|
+
#finish() {
|
|
1280
|
+
this.#unbindTransport();
|
|
1281
|
+
this.#unbindProcess();
|
|
1282
|
+
this.#status = "disconnected";
|
|
1283
|
+
this.#connection = void 0;
|
|
1284
|
+
this.#owned = void 0;
|
|
1285
|
+
this.#endpoint = void 0;
|
|
1286
|
+
this.#client = void 0;
|
|
1287
|
+
this.#process = void 0;
|
|
1288
|
+
this.#contexts = [];
|
|
1289
|
+
this.#emitter.emit("destroy");
|
|
1290
|
+
this.#emitter.destroy();
|
|
1115
1291
|
}
|
|
1116
1292
|
};
|
|
1117
1293
|
//#endregion
|
|
1118
|
-
export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_HEADLESS_ARG, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS,
|
|
1294
|
+
export { BROWSER_CDP_LIST_PATH, BROWSER_CDP_PROTOCOL, BROWSER_CDP_VERSION_PATH, BROWSER_DEFAULT_CDP_PORT, BROWSER_DEFAULT_HOST, BROWSER_ENGINE_HINTS, BROWSER_ENV_PATH_KEYS, BROWSER_EXECUTABLE_NAMES, BROWSER_EXECUTABLE_PATHS, BROWSER_HEADLESS_ARG, BROWSER_KILL_GRACE_MS, BROWSER_LAUNCH_ARGS, BROWSER_PORT_PROBE_TIMEOUT_MS, BROWSER_PROCESS_EXIT_CAUSE, BROWSER_STORE_CACHE_DIRS, BROWSER_STORE_DEFAULT_DIRS, BROWSER_STORE_ENV_KEY, BROWSER_STORE_GLOBS, BROWSER_STORE_LINK_NAME, BROWSER_TRANSPORT_LOSS_CAUSE, BROWSER_TRANSPORT_LOSS_DEFER_MS, BROWSER_WINDOWS_ROOT_FALLBACKS, BROWSER_WINDOWS_SUFFIXES, Browser, BrowserConnectionError, BrowserDestroyedError, BrowserNotConnectedError, WebSocketCDPTransport, browserToEngine, createBrowser, createCDPTransport, createScreenshotWriter, defaultInstallPaths, defaultStoreBases, fetchCDPTargets, findAllEnvOverrides, findAllInStore, findAllInstallPaths, findEnvOverride, findInStore, findInstallPath, findSystemBrowser, findSystemBrowsers, isBrowserConnectionError, isBrowserDestroyedError, isBrowserNotConnectedError, launchBrowserProcess, normalizeExecutablePath, parseBrowserEngine, probeAllPathNames, probePathNames, waitForCDPReady, windowsRoots };
|
|
1119
1295
|
|
|
1120
1296
|
//# sourceMappingURL=index.js.map
|