@handstage/core 0.0.7 → 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 (115) hide show
  1. package/README.md +48 -1
  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 +22 -31
  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 +2 -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 +22 -149
  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 +14 -4
  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 +151 -135
  34. package/dist/v3/understudy/context.js +186 -442
  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 +28 -6
  39. package/dist/v3/understudy/frameLocator.js +3 -1
  40. package/dist/v3/understudy/frameRegistry.js +19 -15
  41. package/dist/v3/understudy/initScripts.js +1 -1
  42. package/dist/v3/understudy/locator.js +37 -61
  43. package/dist/v3/understudy/navigationResponseTracker.js +5 -5
  44. package/dist/v3/understudy/networkManager.js +1 -1
  45. package/dist/v3/understudy/page.js +70 -41
  46. package/dist/v3/understudy/piercer.js +8 -4
  47. package/dist/v3/understudy/response.js +9 -4
  48. package/dist/v3/understudy/screenshotUtils.js +7 -2
  49. package/dist/v3/understudy/selectorResolver.js +2 -3
  50. package/dist/v3/understudy/targetRouter.js +189 -0
  51. package/package.json +36 -4
  52. package/src/launch/bun.ts +65 -0
  53. package/src/launch/node.ts +97 -0
  54. package/src/launch/utils.ts +127 -0
  55. package/src/v3/connect/connection.ts +32 -0
  56. package/src/v3/connect/index.ts +5 -0
  57. package/src/v3/connect/local.ts +119 -0
  58. package/src/v3/connect/session.ts +32 -0
  59. package/src/v3/connect/shared.ts +104 -0
  60. package/src/v3/connect/transport.ts +29 -0
  61. package/src/v3/connect/ws.ts +45 -0
  62. package/src/v3/handstage.ts +209 -0
  63. package/src/v3/index.ts +3 -3
  64. package/src/v3/launch/resolveWS.ts +39 -0
  65. package/src/v3/logger.ts +35 -38
  66. package/src/v3/types/private/internal.ts +0 -25
  67. package/src/v3/types/public/index.ts +8 -5
  68. package/src/v3/types/public/launchedChrome.ts +27 -0
  69. package/src/v3/types/public/locator.ts +2 -8
  70. package/src/v3/types/public/options.ts +0 -11
  71. package/src/v3/types/public/sdkErrors.ts +27 -13
  72. package/src/v3/understudy/a11y/snapshot/a11yTree.ts +25 -20
  73. package/src/v3/understudy/a11y/snapshot/activeElement.ts +6 -15
  74. package/src/v3/understudy/a11y/snapshot/capture.ts +20 -11
  75. package/src/v3/understudy/a11y/snapshot/coordinateResolver.ts +9 -16
  76. package/src/v3/understudy/a11y/snapshot/domTree.ts +48 -29
  77. package/src/v3/understudy/a11y/snapshot/focusSelectors.ts +28 -35
  78. package/src/v3/understudy/a11y/snapshot/treeFormatUtils.ts +34 -12
  79. package/src/v3/understudy/a11y/snapshot/xpathUtils.ts +9 -14
  80. package/src/v3/understudy/cdp.ts +357 -278
  81. package/src/v3/understudy/context.ts +304 -594
  82. package/src/v3/understudy/cookies.ts +4 -3
  83. package/src/v3/understudy/deepLocator.ts +16 -22
  84. package/src/v3/understudy/executionContextRegistry.ts +1 -1
  85. package/src/v3/understudy/frame.ts +56 -56
  86. package/src/v3/understudy/frameLocator.ts +4 -8
  87. package/src/v3/understudy/frameRegistry.ts +20 -17
  88. package/src/v3/understudy/locator.ts +166 -252
  89. package/src/v3/understudy/navigationResponseTracker.ts +16 -15
  90. package/src/v3/understudy/networkManager.ts +1 -1
  91. package/src/v3/understudy/page.ts +149 -141
  92. package/src/v3/understudy/piercer.ts +22 -24
  93. package/src/v3/understudy/response.ts +17 -12
  94. package/src/v3/understudy/screenshotUtils.ts +15 -12
  95. package/src/v3/understudy/selectorResolver.ts +41 -55
  96. package/src/v3/understudy/targetRouter.ts +236 -0
  97. package/dist/v3/cli.js +0 -9
  98. package/dist/v3/launch/local.js +0 -94
  99. package/dist/v3/shutdown/cleanupLocal.js +0 -25
  100. package/dist/v3/shutdown/supervisor.js +0 -153
  101. package/dist/v3/shutdown/supervisorClient.js +0 -86
  102. package/dist/v3/types/private/locator.js +0 -1
  103. package/dist/v3/types/private/shutdown.js +0 -4
  104. package/dist/v3/types/private/shutdownErrors.js +0 -21
  105. package/dist/v3/understudy/fileUploadUtils.js +0 -80
  106. package/dist/v3/v3.js +0 -433
  107. package/src/v3/launch/local.ts +0 -131
  108. package/src/v3/shutdown/cleanupLocal.ts +0 -31
  109. package/src/v3/shutdown/supervisor.ts +0 -173
  110. package/src/v3/shutdown/supervisorClient.ts +0 -122
  111. package/src/v3/types/private/locator.ts +0 -10
  112. package/src/v3/types/private/shutdown.ts +0 -16
  113. package/src/v3/types/private/shutdownErrors.ts +0 -24
  114. package/src/v3/understudy/fileUploadUtils.ts +0 -102
  115. package/src/v3/v3.ts +0 -494
package/README.md CHANGED
@@ -1,3 +1,50 @@
1
1
  # @handstage/core
2
2
 
3
- Core browser automation engine for Handstage. Manages browser connections, Chrome DevTools Protocol (CDP) communication, page context, frame locators, and script injection for reliable browser automation.
3
+ Core browser automation engine for Handstage. Manages CDP connections,
4
+ target routing, page/frame lifecycle, and script injection for reliable
5
+ browser automation.
6
+
7
+ ## Connection ownership
8
+
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
27
+ dedicated contexts). It never tears down the underlying CDP connection —
28
+ that responsibility lives with `Handstage` or, for shared connections, the caller.
29
+
30
+ ## Default-context attach
31
+
32
+ Handstage creates instances containing the `defaultBrowserContext()` by default (which aligns with Puppeteer's `puppeteer.connect` and `puppeteer.launch`).
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.
34
+
35
+ To intentionally attach to the shared default context (and accept that
36
+ other actors may race with you on a shared tab), simply use the default browser context (i.e. `handstage.newPage()`).
37
+
38
+ ## Active page is gone
39
+
40
+ Contexts no longer auto-create an initial page and there is no
41
+ `context.activePage()` / `setActivePage()` / `awaitActivePage()`. Track
42
+ `Page` references yourself (from `newPage()` / `pages()`) and call
43
+ `page.bringToFront()` if you want to foreground a tab.
44
+
45
+ ## Per-instance logging
46
+
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
+ }