@handstage/core 0.0.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +25 -20
  2. package/dist/launch/bun.js +44 -0
  3. package/dist/launch/node.js +77 -0
  4. package/dist/launch/utils.js +88 -0
  5. package/dist/v3/connect/connection.js +20 -0
  6. package/dist/v3/connect/index.js +5 -0
  7. package/dist/v3/connect/local.js +106 -0
  8. package/dist/v3/connect/session.js +18 -0
  9. package/dist/v3/connect/shared.js +77 -0
  10. package/dist/v3/connect/transport.js +18 -0
  11. package/dist/v3/connect/ws.js +34 -0
  12. package/dist/v3/handstage.js +160 -0
  13. package/dist/v3/index.js +3 -3
  14. package/dist/v3/launch/resolveWS.js +32 -0
  15. package/dist/v3/logger.js +1 -1
  16. package/dist/v3/types/private/api.js +0 -1
  17. package/dist/v3/types/private/internal.js +0 -1
  18. package/dist/v3/types/private/snapshot.js +0 -1
  19. package/dist/v3/types/public/context.js +0 -1
  20. package/dist/v3/types/public/index.js +1 -0
  21. package/dist/v3/types/public/launchedChrome.js +0 -0
  22. package/dist/v3/types/public/locator.js +0 -1
  23. package/dist/v3/types/public/screenshotTypes.js +0 -1
  24. package/dist/v3/types/public/sdkErrors.js +6 -14
  25. package/dist/v3/understudy/a11y/snapshot/a11yTree.js +26 -12
  26. package/dist/v3/understudy/a11y/snapshot/activeElement.js +3 -1
  27. package/dist/v3/understudy/a11y/snapshot/capture.js +13 -2
  28. package/dist/v3/understudy/a11y/snapshot/coordinateResolver.js +6 -2
  29. package/dist/v3/understudy/a11y/snapshot/domTree.js +25 -2
  30. package/dist/v3/understudy/a11y/snapshot/focusSelectors.js +13 -5
  31. package/dist/v3/understudy/a11y/snapshot/treeFormatUtils.js +29 -10
  32. package/dist/v3/understudy/a11y/snapshot/xpathUtils.js +3 -2
  33. package/dist/v3/understudy/cdp.js +102 -140
  34. package/dist/v3/understudy/context.js +30 -25
  35. package/dist/v3/understudy/cookies.js +2 -2
  36. package/dist/v3/understudy/deepLocator.js +12 -7
  37. package/dist/v3/understudy/executionContextRegistry.js +1 -1
  38. package/dist/v3/understudy/frame.js +18 -5
  39. package/dist/v3/understudy/frameLocator.js +3 -1
  40. package/dist/v3/understudy/frameRegistry.js +19 -15
  41. package/dist/v3/understudy/locator.js +37 -61
  42. package/dist/v3/understudy/navigationResponseTracker.js +5 -5
  43. package/dist/v3/understudy/networkManager.js +1 -1
  44. package/dist/v3/understudy/page.js +30 -18
  45. package/dist/v3/understudy/piercer.js +4 -1
  46. package/dist/v3/understudy/response.js +9 -4
  47. package/dist/v3/understudy/screenshotUtils.js +7 -2
  48. package/dist/v3/understudy/targetRouter.js +3 -3
  49. package/package.json +36 -4
  50. package/src/launch/bun.ts +65 -0
  51. package/src/launch/node.ts +97 -0
  52. package/src/launch/utils.ts +127 -0
  53. package/src/v3/connect/connection.ts +32 -0
  54. package/src/v3/connect/index.ts +5 -0
  55. package/src/v3/connect/local.ts +119 -0
  56. package/src/v3/connect/session.ts +32 -0
  57. package/src/v3/connect/shared.ts +104 -0
  58. package/src/v3/connect/transport.ts +29 -0
  59. package/src/v3/connect/ws.ts +45 -0
  60. package/src/v3/handstage.ts +209 -0
  61. package/src/v3/index.ts +3 -3
  62. package/src/v3/launch/resolveWS.ts +39 -0
  63. package/src/v3/logger.ts +3 -3
  64. package/src/v3/types/private/internal.ts +0 -42
  65. package/src/v3/types/public/index.ts +2 -1
  66. package/src/v3/types/public/launchedChrome.ts +27 -0
  67. package/src/v3/types/public/locator.ts +2 -8
  68. package/src/v3/types/public/options.ts +0 -11
  69. package/src/v3/types/public/sdkErrors.ts +8 -19
  70. package/src/v3/understudy/a11y/snapshot/a11yTree.ts +25 -20
  71. package/src/v3/understudy/a11y/snapshot/activeElement.ts +6 -15
  72. package/src/v3/understudy/a11y/snapshot/capture.ts +19 -9
  73. package/src/v3/understudy/a11y/snapshot/coordinateResolver.ts +9 -16
  74. package/src/v3/understudy/a11y/snapshot/domTree.ts +48 -29
  75. package/src/v3/understudy/a11y/snapshot/focusSelectors.ts +28 -35
  76. package/src/v3/understudy/a11y/snapshot/treeFormatUtils.ts +34 -12
  77. package/src/v3/understudy/a11y/snapshot/xpathUtils.ts +9 -14
  78. package/src/v3/understudy/cdp.ts +304 -291
  79. package/src/v3/understudy/context.ts +104 -123
  80. package/src/v3/understudy/cookies.ts +4 -3
  81. package/src/v3/understudy/deepLocator.ts +16 -22
  82. package/src/v3/understudy/executionContextRegistry.ts +1 -1
  83. package/src/v3/understudy/frame.ts +45 -57
  84. package/src/v3/understudy/frameLocator.ts +4 -8
  85. package/src/v3/understudy/frameRegistry.ts +20 -17
  86. package/src/v3/understudy/locator.ts +166 -252
  87. package/src/v3/understudy/navigationResponseTracker.ts +16 -15
  88. package/src/v3/understudy/networkManager.ts +1 -1
  89. package/src/v3/understudy/page.ts +107 -108
  90. package/src/v3/understudy/piercer.ts +19 -23
  91. package/src/v3/understudy/response.ts +17 -12
  92. package/src/v3/understudy/screenshotUtils.ts +15 -12
  93. package/src/v3/understudy/selectorResolver.ts +39 -52
  94. package/src/v3/understudy/targetRouter.ts +3 -3
  95. package/dist/v3/cli.js +0 -9
  96. package/dist/v3/launch/local.js +0 -94
  97. package/dist/v3/shutdown/cleanupLocal.js +0 -25
  98. package/dist/v3/shutdown/supervisor.js +0 -153
  99. package/dist/v3/shutdown/supervisorClient.js +0 -86
  100. package/dist/v3/types/private/locator.js +0 -1
  101. package/dist/v3/types/private/shutdown.js +0 -4
  102. package/dist/v3/types/private/shutdownErrors.js +0 -21
  103. package/dist/v3/understudy/fileUploadUtils.js +0 -80
  104. package/dist/v3/v3.js +0 -515
  105. package/src/v3/launch/local.ts +0 -131
  106. package/src/v3/shutdown/cleanupLocal.ts +0 -31
  107. package/src/v3/shutdown/supervisor.ts +0 -173
  108. package/src/v3/shutdown/supervisorClient.ts +0 -122
  109. package/src/v3/types/private/locator.ts +0 -10
  110. package/src/v3/types/private/shutdown.ts +0 -16
  111. package/src/v3/types/private/shutdownErrors.ts +0 -24
  112. package/src/v3/understudy/fileUploadUtils.ts +0 -102
  113. package/src/v3/v3.ts +0 -610
package/README.md CHANGED
@@ -6,29 +6,34 @@ browser automation.
6
6
 
7
7
  ## Connection ownership
8
8
 
9
- `V3` (alias `Handstage`) owns the CDP connection it constructs:
10
-
11
- - `V3.connectLocal({ cdpUrl?, ... })` — opens (or attaches via WS) and owns
12
- the connection. `close()` closes the WebSocket.
13
- - `V3.connectTransport(transport)` — wraps and owns a raw `CDPTransport`.
14
- Wrapping the same `transport` again throws
15
- `HandstageTransportAlreadyOwnedError`.
16
- - `V3.connectSession(session)` — wraps and owns an `ExternalCDPSession`.
17
- Same ownership rule as transports.
18
- - `V3.connectConnection(existingConnection)` explicit sharing entrypoint.
19
- V3 does NOT close the connection on `close()`; the caller does.
20
-
21
- `V3Context.close()` only ever calls `Target.disposeBrowserContext` (for
9
+ Connection subpath factories own the CDP connection they construct:
10
+
11
+ - `connectLocal(chrome)` from `@handstage/core/connect/local` — attaches to
12
+ a launched Chrome pipe and owns the connection.
13
+ - `connectWS(ws)` from `@handstage/core/connect/ws` — wraps a browser
14
+ WebSocket and owns the connection. `close()` closes the WebSocket.
15
+ - `connectTransport(transport)` from `@handstage/core/connect/transport` —
16
+ wraps and owns a raw `CDPTransport`.
17
+ - `connectSession(session)` from `@handstage/core/connect/session` — wraps
18
+ and owns an `ExternalCDPSession`.
19
+ - `connectConnection(existingConnection)` from
20
+ `@handstage/core/connect/connection` — explicit sharing entrypoint. Handstage does
21
+ NOT close the connection on `close()`; the caller does.
22
+
23
+ Wrapping the same `transport` or `session` again throws
24
+ `HandstageTransportAlreadyOwnedError`.
25
+
26
+ `Context.close()` only ever calls `Target.disposeBrowserContext` (for
22
27
  dedicated contexts). It never tears down the underlying CDP connection —
23
- that responsibility lives with `V3` or, for shared connections, the caller.
28
+ that responsibility lives with `Handstage` or, for shared connections, the caller.
24
29
 
25
30
  ## Default-context attach
26
31
 
27
32
  Handstage creates instances containing the `defaultBrowserContext()` by default (which aligns with Puppeteer's `puppeteer.connect` and `puppeteer.launch`).
28
- Two Handstage clients on the same CDP websocket therefore share the default browser context natively without breaking. If you want isolation, call `v3.createBrowserContext()`, which returns an isolated browser context. Targets owned by other browser contexts are resumed/detached at the target router rather than left paused.
33
+ Two Handstage clients on the same CDP websocket therefore share the default browser context natively without breaking. If you want isolation, call `handstage.createBrowserContext()`, which returns an isolated browser context. Targets owned by other browser contexts are resumed/detached at the target router rather than left paused.
29
34
 
30
35
  To intentionally attach to the shared default context (and accept that
31
- other actors may race with you on a shared tab), simply use the default browser context (i.e. `v3.newPage()`).
36
+ other actors may race with you on a shared tab), simply use the default browser context (i.e. `handstage.newPage()`).
32
37
 
33
38
  ## Active page is gone
34
39
 
@@ -39,7 +44,7 @@ Contexts no longer auto-create an initial page and there is no
39
44
 
40
45
  ## Per-instance logging
41
46
 
42
- Pass `logger:` to any `V3.connect*` factory and that logger receives every
43
- log from that V3's contexts / pages / network managers / target-router
44
- delegate. Two V3 instances on a shared connection each receive router-level
45
- debug lines via broadcast.
47
+ Pass `logger:` to any connection factory and that logger receives every log
48
+ from that Handstage's contexts / pages / network managers / target-router delegate.
49
+ Two Handstage instances on a shared connection each receive router-level debug lines
50
+ via broadcast.
@@ -0,0 +1,44 @@
1
+ /// <reference types="bun" />
2
+ import Bun from "bun";
3
+ import { performBrowserProcessCleanup, prepareChromeLaunchOptions, } from "./utils";
4
+ export async function launchChromeBun(opts) {
5
+ const lbo = opts ?? {};
6
+ const { chromePath, finalFlags, userDataDir, createdTemp } = prepareChromeLaunchOptions(lbo);
7
+ const p = Bun.spawn([chromePath, ...finalFlags], {
8
+ stdio: ["ignore", "ignore", "ignore", "pipe", "pipe"],
9
+ });
10
+ const fd3 = p.stdio[3]; // Chrome's read pipe
11
+ const fd4 = p.stdio[4]; // Chrome's write pipe
12
+ if (typeof fd3 !== "number" || typeof fd4 !== "number") {
13
+ throw new Error("Failed to map Chrome pipes to Bun stdio streams");
14
+ }
15
+ const fd3Writer = Bun.file(fd3).writer();
16
+ const fd4Reader = Bun.file(fd4).stream();
17
+ const stdin = new WritableStream({
18
+ write(chunk) {
19
+ fd3Writer.write(chunk);
20
+ fd3Writer.flush();
21
+ },
22
+ close() {
23
+ fd3Writer.end();
24
+ },
25
+ abort() {
26
+ fd3Writer.end();
27
+ },
28
+ });
29
+ const close = async () => {
30
+ try {
31
+ fd3Writer.end();
32
+ await performBrowserProcessCleanup((signal) => p.kill(signal), p.exited, userDataDir, createdTemp, lbo);
33
+ }
34
+ catch { }
35
+ };
36
+ return {
37
+ stdout: fd4Reader,
38
+ stdin,
39
+ close,
40
+ pid: p.pid,
41
+ userDataDir,
42
+ createdTempProfile: createdTemp,
43
+ };
44
+ }
@@ -0,0 +1,77 @@
1
+ /// <reference types="node" />
2
+ import { spawn } from "node:child_process";
3
+ import { Readable, Writable } from "node:stream";
4
+ import { performBrowserProcessCleanup, prepareChromeLaunchOptions, } from "./utils";
5
+ export async function launchChromeNode(opts) {
6
+ const lbo = opts ?? {};
7
+ const { chromePath, finalFlags, userDataDir, createdTemp } = prepareChromeLaunchOptions(lbo);
8
+ const p = spawn(chromePath, finalFlags, {
9
+ stdio: ["ignore", "ignore", "ignore", "pipe", "pipe"],
10
+ });
11
+ const fd3 = p.stdio[3]; // Chrome's read pipe (our WritableStream)
12
+ const fd4 = p.stdio[4]; // Chrome's write pipe (our ReadableStream)
13
+ if (!(fd3 instanceof Writable) || !(fd4 instanceof Readable)) {
14
+ throw new Error("Failed to map Chrome pipes to stdio");
15
+ }
16
+ const stdout = new ReadableStream({
17
+ start(controller) {
18
+ fd4.on("data", (chunk) => {
19
+ controller.enqueue(new Uint8Array(chunk));
20
+ });
21
+ fd4.on("end", () => {
22
+ controller.close();
23
+ });
24
+ fd4.on("error", (err) => {
25
+ controller.error(err);
26
+ });
27
+ },
28
+ cancel() {
29
+ fd4.destroy();
30
+ },
31
+ });
32
+ const stdin = new WritableStream({
33
+ write(chunk, controller) {
34
+ return new Promise((resolve, reject) => {
35
+ fd3.write(chunk, (err) => {
36
+ if (err) {
37
+ controller.error(err);
38
+ reject(err);
39
+ }
40
+ else {
41
+ resolve();
42
+ }
43
+ });
44
+ });
45
+ },
46
+ close() {
47
+ return new Promise((resolve) => {
48
+ fd3.end(resolve);
49
+ });
50
+ },
51
+ abort(err) {
52
+ fd3.destroy(err instanceof Error ? err : new Error(String(err)));
53
+ },
54
+ });
55
+ const close = async () => {
56
+ try {
57
+ fd3.destroy();
58
+ fd4.destroy();
59
+ const exited = new Promise((resolve) => {
60
+ if (p.exitCode !== null || p.signalCode !== null)
61
+ resolve();
62
+ else
63
+ p.once("exit", resolve);
64
+ });
65
+ await performBrowserProcessCleanup((signal) => p.kill(signal), exited, userDataDir, createdTemp, lbo);
66
+ }
67
+ catch { }
68
+ };
69
+ return {
70
+ stdout,
71
+ stdin,
72
+ close,
73
+ pid: p.pid,
74
+ userDataDir,
75
+ createdTempProfile: createdTemp,
76
+ };
77
+ }
@@ -0,0 +1,88 @@
1
+ /// <reference types="node" />
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { getChromePath } from "chrome-launcher";
6
+ // `DEFAULT_FLAGS` is intentionally not re-exported from chrome-launcher's
7
+ // package entry, so import it from the flags module directly.
8
+ import { DEFAULT_FLAGS } from "chrome-launcher/dist/flags.js";
9
+ export const CHROME_EXIT_TIMEOUT_MS = 5000;
10
+ export async function waitForProcessExit(exited, timeoutMs) {
11
+ return Promise.race([
12
+ exited.then(() => true, () => true),
13
+ new Promise((resolve) => setTimeout(() => resolve(false), timeoutMs)),
14
+ ]);
15
+ }
16
+ export async function performBrowserProcessCleanup(kill, exited, userDataDir, createdTemp, opts) {
17
+ kill();
18
+ if (!(await waitForProcessExit(exited, CHROME_EXIT_TIMEOUT_MS))) {
19
+ kill("SIGKILL");
20
+ await waitForProcessExit(exited, CHROME_EXIT_TIMEOUT_MS);
21
+ }
22
+ cleanupUserDataDir(userDataDir, createdTemp, opts);
23
+ }
24
+ export function prepareChromeLaunchOptions(opts) {
25
+ const lbo = opts ?? {};
26
+ const chromePath = lbo.executablePath || getChromePath();
27
+ let userDataDir = lbo.userDataDir;
28
+ let createdTemp = false;
29
+ if (!userDataDir) {
30
+ const base = path.join(os.tmpdir(), "handstage-v3");
31
+ fs.mkdirSync(base, { recursive: true });
32
+ userDataDir = fs.mkdtempSync(path.join(base, "profile-"));
33
+ createdTemp = true;
34
+ }
35
+ let baseChromeFlags = [];
36
+ const ignore = lbo.ignoreDefaultArgs;
37
+ if (ignore === true) {
38
+ baseChromeFlags = [];
39
+ }
40
+ else if (Array.isArray(ignore)) {
41
+ baseChromeFlags = DEFAULT_FLAGS.filter((f) => !ignore.some((ex) => f.includes(ex)));
42
+ }
43
+ else {
44
+ baseChromeFlags = [...DEFAULT_FLAGS];
45
+ }
46
+ const chromeFlags = [
47
+ ...(lbo.headless !== false ? ["--headless=new"] : []),
48
+ ...baseChromeFlags,
49
+ "--remote-debugging-pipe",
50
+ ];
51
+ if (lbo.devtools)
52
+ chromeFlags.push("--auto-open-devtools-for-tabs");
53
+ if (lbo.locale)
54
+ chromeFlags.push(`--lang=${lbo.locale}`);
55
+ if (lbo.viewport?.width && lbo.viewport?.height) {
56
+ chromeFlags.push(`--window-size=${lbo.viewport.width},${lbo.viewport.height + 87}`);
57
+ }
58
+ if (typeof lbo.deviceScaleFactor === "number") {
59
+ chromeFlags.push(`--force-device-scale-factor=${Math.max(0.1, lbo.deviceScaleFactor)}`);
60
+ }
61
+ if (lbo.hasTouch)
62
+ chromeFlags.push("--touch-events=enabled");
63
+ if (lbo.ignoreHTTPSErrors)
64
+ chromeFlags.push("--ignore-certificate-errors");
65
+ if (lbo.proxy?.server)
66
+ chromeFlags.push(`--proxy-server=${lbo.proxy.server}`);
67
+ if (lbo.proxy?.bypass)
68
+ chromeFlags.push(`--proxy-bypass-list=${lbo.proxy.bypass}`);
69
+ if (userDataDir)
70
+ chromeFlags.push(`--user-data-dir=${userDataDir}`);
71
+ if (Array.isArray(lbo.args))
72
+ chromeFlags.push(...lbo.args);
73
+ const finalFlags = chromeFlags.filter((f) => typeof f === "string");
74
+ return {
75
+ chromePath,
76
+ finalFlags,
77
+ userDataDir,
78
+ createdTemp,
79
+ };
80
+ }
81
+ export function cleanupUserDataDir(userDataDir, createdTemp, opts) {
82
+ if (createdTemp && !opts?.preserveUserDataDir && userDataDir) {
83
+ try {
84
+ fs.rmSync(userDataDir, { recursive: true, force: true });
85
+ }
86
+ catch { }
87
+ }
88
+ }
@@ -0,0 +1,20 @@
1
+ import { LogLevel } from "../types/public/logs";
2
+ import { connectOptionsToLocalBrowserLaunchOptions, createSharedHandstage, setupConnectContext, } from "./shared";
3
+ /**
4
+ * Attach a Handstage instance to a pre-existing `CDPConnectionLike` that the caller
5
+ * manages. Handstage will not close the shared connection on `close()`.
6
+ */
7
+ export async function connectConnection(conn, opts) {
8
+ const { sharedOpts, logSink, logger } = setupConnectContext(opts);
9
+ logger({
10
+ category: "init",
11
+ message: "Attaching to shared CDP connection",
12
+ level: LogLevel.Info,
13
+ });
14
+ return await createSharedHandstage({
15
+ conn,
16
+ lbo: connectOptionsToLocalBrowserLaunchOptions(opts),
17
+ sharedOpts,
18
+ logSink,
19
+ });
20
+ }
@@ -0,0 +1,5 @@
1
+ export { connectConnection } from "./connection";
2
+ export { connectLocal } from "./local";
3
+ export { connectSession } from "./session";
4
+ export { connectTransport } from "./transport";
5
+ export { connectWS } from "./ws";
@@ -0,0 +1,106 @@
1
+ import { LogLevel } from "../types/public/logs";
2
+ import { CDPConnection } from "../understudy/cdp";
3
+ import { createOwnedHandstage, setupConnectContext } from "./shared";
4
+ const textEncoder = new TextEncoder();
5
+ function createUtf8DecoderStream() {
6
+ const decoder = new TextDecoder();
7
+ return new TransformStream({
8
+ transform(chunk, controller) {
9
+ controller.enqueue(decoder.decode(chunk, { stream: true }));
10
+ },
11
+ flush(controller) {
12
+ const trailing = decoder.decode();
13
+ if (trailing)
14
+ controller.enqueue(trailing);
15
+ },
16
+ });
17
+ }
18
+ async function* readNullDelimitedMessages(stream) {
19
+ const decodedStream = stream.pipeThrough(createUtf8DecoderStream());
20
+ const reader = decodedStream.getReader();
21
+ let pending = "";
22
+ try {
23
+ while (true) {
24
+ const { value, done } = await reader.read();
25
+ if (done)
26
+ break;
27
+ pending += value;
28
+ let frameStart = 0;
29
+ while (true) {
30
+ const frameEnd = pending.indexOf("\0", frameStart);
31
+ if (frameEnd === -1)
32
+ break;
33
+ yield pending.slice(frameStart, frameEnd);
34
+ frameStart = frameEnd + 1;
35
+ }
36
+ if (frameStart > 0) {
37
+ pending = pending.slice(frameStart);
38
+ }
39
+ }
40
+ }
41
+ finally {
42
+ reader.releaseLock();
43
+ }
44
+ }
45
+ function encodeNullDelimitedMessage(message) {
46
+ const encoded = textEncoder.encode(message);
47
+ const framed = new Uint8Array(encoded.byteLength + 1);
48
+ framed.set(encoded);
49
+ return framed;
50
+ }
51
+ export async function connectLocal(chrome, opts) {
52
+ const { sharedOpts, logSink, logger } = setupConnectContext(opts);
53
+ logger({
54
+ category: "init",
55
+ message: "Connecting via LaunchedChrome (pipe)",
56
+ level: LogLevel.Info,
57
+ });
58
+ const writer = chrome.stdin.getWriter();
59
+ let isClosed = false;
60
+ const transport = {
61
+ send: (message) => {
62
+ if (isClosed)
63
+ return;
64
+ writer.write(encodeNullDelimitedMessage(message)).catch(() => { });
65
+ },
66
+ close: async () => {
67
+ if (isClosed)
68
+ return;
69
+ isClosed = true;
70
+ await writer.close().catch(() => { });
71
+ await chrome.close().catch(() => { });
72
+ },
73
+ };
74
+ void (async () => {
75
+ try {
76
+ for await (const message of readNullDelimitedMessages(chrome.stdout)) {
77
+ if (isClosed)
78
+ break;
79
+ if (transport.onmessage)
80
+ transport.onmessage(message);
81
+ }
82
+ }
83
+ catch (err) {
84
+ if (transport.onerror) {
85
+ transport.onerror(err instanceof Error ? err : new Error(String(err)));
86
+ }
87
+ }
88
+ finally {
89
+ if (transport.onclose && !isClosed) {
90
+ transport.onclose("Pipe closed");
91
+ }
92
+ isClosed = true;
93
+ }
94
+ })();
95
+ const conn = new CDPConnection(transport);
96
+ const lbo = opts?.localBrowserLaunchOptions ?? {};
97
+ return await createOwnedHandstage({
98
+ conn,
99
+ lbo,
100
+ sharedOpts,
101
+ logSink,
102
+ onContextError: async () => {
103
+ await chrome.close().catch(() => { });
104
+ },
105
+ });
106
+ }
@@ -0,0 +1,18 @@
1
+ import { LogLevel } from "../types/public/logs";
2
+ import { ExternalConnectionAdapter, } from "../understudy/cdp";
3
+ import { connectOptionsToLocalBrowserLaunchOptions, createOwnedHandstage, setupConnectContext, } from "./shared";
4
+ export async function connectSession(session, opts) {
5
+ const { sharedOpts, logSink, logger } = setupConnectContext(opts);
6
+ logger({
7
+ category: "init",
8
+ message: "Connecting via custom connection",
9
+ level: LogLevel.Info,
10
+ });
11
+ const adapter = new ExternalConnectionAdapter(session);
12
+ return await createOwnedHandstage({
13
+ conn: adapter,
14
+ lbo: connectOptionsToLocalBrowserLaunchOptions(opts),
15
+ sharedOpts,
16
+ logSink,
17
+ });
18
+ }
@@ -0,0 +1,77 @@
1
+ import { createHandstageForConnection } from "../handstage";
2
+ import { createFilteredLogger } from "../logger";
3
+ import { Context } from "../understudy/context";
4
+ export function setupConnectContext(opts) {
5
+ const sharedOpts = opts ?? {};
6
+ const logSink = createFilteredLogger(sharedOpts.logger, sharedOpts.verbose);
7
+ const logger = (line) => logSink(line);
8
+ return { sharedOpts, logSink, logger };
9
+ }
10
+ export function connectOptionsToLocalBrowserLaunchOptions(opts) {
11
+ return opts
12
+ ? {
13
+ viewport: opts.viewport,
14
+ deviceScaleFactor: opts.deviceScaleFactor,
15
+ downloadsPath: opts.downloadsPath,
16
+ acceptDownloads: opts.acceptDownloads,
17
+ }
18
+ : {};
19
+ }
20
+ export function onceAsync(fn) {
21
+ let called = false;
22
+ return async () => {
23
+ if (called)
24
+ return;
25
+ called = true;
26
+ await fn();
27
+ };
28
+ }
29
+ export async function createOwnedHandstage(params) {
30
+ let ctx;
31
+ try {
32
+ ctx = await Context.createFromConnection(params.conn, {
33
+ localBrowserLaunchOptions: params.lbo,
34
+ logger: params.logSink,
35
+ });
36
+ }
37
+ catch (err) {
38
+ await params.conn.close().catch(() => { });
39
+ await params.onContextError?.();
40
+ throw err;
41
+ }
42
+ const cleanup = onceAsync(async () => {
43
+ await params.conn.close().catch(() => { });
44
+ });
45
+ const handstage = createHandstageForConnection({
46
+ connection: params.conn,
47
+ cleanup,
48
+ defaultContext: ctx,
49
+ opts: params.sharedOpts,
50
+ logSink: params.logSink,
51
+ });
52
+ await applyPostConnectLocalOptions(handstage, params.lbo);
53
+ return handstage;
54
+ }
55
+ export async function createSharedHandstage(params) {
56
+ const ctx = await Context.createFromConnection(params.conn, {
57
+ localBrowserLaunchOptions: params.lbo,
58
+ logger: params.logSink,
59
+ });
60
+ const handstage = createHandstageForConnection({
61
+ connection: params.conn,
62
+ defaultContext: ctx,
63
+ opts: params.sharedOpts,
64
+ logSink: params.logSink,
65
+ });
66
+ await applyPostConnectLocalOptions(handstage, params.lbo);
67
+ return handstage;
68
+ }
69
+ async function applyPostConnectLocalOptions(handstage, lbo) {
70
+ await handstage
71
+ .defaultBrowserContext()
72
+ .setDownloadBehavior({
73
+ downloadPath: lbo.downloadsPath,
74
+ acceptDownloads: lbo.acceptDownloads,
75
+ })
76
+ .catch(() => { });
77
+ }
@@ -0,0 +1,18 @@
1
+ import { LogLevel } from "../types/public/logs";
2
+ import { CDPConnection } from "../understudy/cdp";
3
+ import { connectOptionsToLocalBrowserLaunchOptions, createOwnedHandstage, setupConnectContext, } from "./shared";
4
+ export async function connectTransport(transport, opts) {
5
+ const { sharedOpts, logSink, logger } = setupConnectContext(opts);
6
+ logger({
7
+ category: "init",
8
+ message: "Connecting via custom transport",
9
+ level: LogLevel.Info,
10
+ });
11
+ const conn = new CDPConnection(transport);
12
+ return await createOwnedHandstage({
13
+ conn,
14
+ lbo: connectOptionsToLocalBrowserLaunchOptions(opts),
15
+ sharedOpts,
16
+ logSink,
17
+ });
18
+ }
@@ -0,0 +1,34 @@
1
+ import { LogLevel } from "../types/public/logs";
2
+ import { CDPConnection } from "../understudy/cdp";
3
+ import { connectOptionsToLocalBrowserLaunchOptions, createOwnedHandstage, setupConnectContext, } from "./shared";
4
+ export async function connectWS(ws, opts) {
5
+ const { sharedOpts, logSink, logger } = setupConnectContext(opts);
6
+ logger({
7
+ category: "init",
8
+ message: "Connecting via WebSocket",
9
+ level: LogLevel.Info,
10
+ });
11
+ const transport = {
12
+ send: (message) => ws.send(message),
13
+ close: () => ws.close(),
14
+ };
15
+ ws.addEventListener("message", (event) => {
16
+ if (transport.onmessage)
17
+ transport.onmessage(event.data.toString());
18
+ });
19
+ ws.addEventListener("close", (event) => {
20
+ if (transport.onclose)
21
+ transport.onclose(`code=${event.code} reason=${event.reason}`);
22
+ });
23
+ ws.addEventListener("error", () => {
24
+ if (transport.onerror)
25
+ transport.onerror(new Error("WebSocket error"));
26
+ });
27
+ const conn = new CDPConnection(transport);
28
+ return await createOwnedHandstage({
29
+ conn,
30
+ lbo: connectOptionsToLocalBrowserLaunchOptions(opts),
31
+ sharedOpts,
32
+ logSink,
33
+ });
34
+ }