npm.io
1.10.1 • Published 1 week ago

simple-cdp

Licence
MIT
Version
1.10.1
Deps
0
Size
39 kB
Vulns
0
Weekly
0
Stars
28

Introduction

simple-cdp is a JavaScript library to interact with the Chrome DevTools Protocol.

The implementation uses Proxy objects to expose APIs. This makes it very light (around 400 lines of code) and independent of protocol evolutions.

Install

You can install the library:

  • from JSR:
deno add @simple-cdp/simple-cdp
  • from NPM:
npm install simple-cdp

Start the browser

Start a Chromium-based browser with the switches --remote-debugging-port and --user-data-dir.

chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-cdp

Since Chrome 136, --remote-debugging-port is ignored when the browser runs on the default profile, so --user-data-dir must point to another directory. Without it, the browser starts but nothing listens on the port.

Usage example

Run the script below.

// import the module (replace with "simple-cdp" if using NPM)
import { createTarget, CDP } from "@simple-cdp/simple-cdp";

// navigate to https://example.com
const url = "https://example.com";
const targetInfo = await createTarget(url);

// create a CDP instance for the target
const cdp = new CDP(targetInfo);

// enable "Runtime" domain
await cdp.Runtime.enable();

// evaluate JavaScript expression
const expression = "41 + 1";
const { result } = await cdp.Runtime.evaluate({ expression });

// display result in the console (i.e. 42)
console.log(result.value);

Driving several targets

The cdp instance is connected to the browser itself. Attach to a target to get a session ID, and pass it as the last argument of the methods to address that target. One connection then drives as many targets as needed.

// import the module (replace with "simple-cdp" if using NPM)
import { cdp } from "@simple-cdp/simple-cdp";

// create a target and attach to it
const url = "https://example.com";
const { targetId } = await cdp.Target.createTarget({ url });
const { sessionId } = await cdp.Target.attachToTarget({
  targetId,
  flatten: true
});

// enable "Runtime" domain for that target
await cdp.Runtime.enable(null, sessionId);

// evaluate JavaScript expression in that target
const expression = "41 + 1";
const { result } = await cdp.Runtime.evaluate({ expression }, sessionId);

// display result in the console (i.e. 42)
console.log(result.value);

// close the target
await cdp.Target.closeTarget({ targetId });

Waiting for an event

The methods resolve when the browser answers the command, which is not the same as the browser finishing the work. Wait for the matching event instead, with the once option of the listener.

// register the listener before triggering the navigation
const loaded = new Promise((resolve) =>
  cdp.Page.addEventListener("loadEventFired", resolve, { once: true }));

await cdp.Page.enable(null, sessionId);
await cdp.Page.navigate({ url }, sessionId);

// wait for the page to be loaded
await loaded;

The listeners accept the options of EventTarget, so signal can be used to remove them.

Attaching to targets automatically

When targets are created by the page rather than by the script, for example when it opens a window, their IDs are unknown. Auto-attach reports a session for each target as it appears.

// import the module (replace with "simple-cdp" if using NPM)
import { cdp } from "@simple-cdp/simple-cdp";

// add event listener triggered when a session is attached to a target
cdp.Target.addEventListener("attachedToTarget", onAttachedToTarget);

// attach to the targets as they are created
await cdp.Target.setAutoAttach({
  autoAttach: true,
  flatten: true,
  waitForDebuggerOnStart: false
});

async function onAttachedToTarget({ params }) {
  // get session ID
  const { sessionId, targetInfo } = params;

  // check if the target is a page
  if (targetInfo.type === "page") {
    // enable "Runtime" domain
    await cdp.Runtime.enable(null, sessionId);

    // evaluate JavaScript expression
    const expression = "41 + 1";
    const { result } = await cdp.Runtime.evaluate(
      { expression }, sessionId);

    // display result in the console (i.e. 42)
    console.log(result.value);
  }
}

Set waitForDebuggerOnStart to true to pause each target until Runtime.runIfWaitingForDebugger is called, in order to set it up before it runs any script.

Options

The options object holds the settings shared by the cdp instance and by the target functions. Set them before the first call.

// import the module (replace with "simple-cdp" if using NPM)
import { options } from "@simple-cdp/simple-cdp";

// connect to a browser listening on another port
options.apiUrl = "http://localhost:9223";
Option Default Description
apiUrl "http://localhost:9222" Base URL of the browser
webSocketDebuggerUrl WebSocket URL, set it to skip the discovery request
apiPath "json/version" Path used to discover the WebSocket URL
apiPathTargets "json" Path used by getTargets()
apiPathNewTarget "json/new" Path used by createTarget()
apiPathActivateTarget "json/activate" Path used by activateTarget()
apiPathCloseTarget "json/close" Path used by closeTarget()
connectionMaxRetry 20 Number of attempts when the browser cannot be reached
connectionRetryDelay 500 Delay between attempts, in milliseconds
signal Signal aborting the requests opening the connection

A CDP instance created with an argument gets its own options, merging the shared ones with those passed to the constructor. Changing them does not affect the cdp instance.

const cdp = new CDP({ apiUrl: "http://localhost:9223" });

Connection and errors

The connection opens on the first call and stays open. If it is lost, the next call opens a new one and the event listeners are registered again. Calling reset() closes the connection and removes the event listeners.

The instance reports the lifecycle, so a connection lost and reopened on its own does not go unnoticed.

cdp.addEventListener("open", () => console.log("connected"));
cdp.addEventListener("close", ({ reason }) => console.log("disconnected", reason));

An instance is disposable, so declaring it with using closes the connection when it goes out of scope, including when the block throws.

using cdp = new CDP(targetInfo);
await cdp.Runtime.enable();
// the connection is closed here

The requests opening the connection can be aborted with a signal, which also interrupts the wait between the retries. The commands sent to the browser are not abortable, since the protocol cannot cancel them once they are sent.

// give up after 5 seconds instead of retrying `connectionMaxRetry` times
const targets = await getTargets({ signal: AbortSignal.timeout(5000) });

// or abort every attempt made by an instance
const cdp = new CDP({ signal: controller.signal });

The rejection carries the reason of the signal, which is a TimeoutError for AbortSignal.timeout() and an AbortError otherwise.

The waits on the browser itself can be bounded with connectionMaxTime, the maximum delay in ms for the WebSocket connection to open, and commandMaxTime, the maximum delay in ms for the response of a command to arrive. Both are unlimited by default. A command timeout rejects the pending call and leaves the connection open, since the protocol cannot cancel a command once it is sent.

// fail a call instead of waiting forever on an unresponsive browser
const cdp = new CDP({ connectionMaxTime: 5000, commandMaxTime: 30000 });

Rejected calls carry a code property. Protocol errors use the code returned by the browser (e.g. -32601 when the method does not exist), and connection errors use one of the exported codes.

Code Description
CONNECTION_REFUSED_ERROR_CODE The browser could not be reached after connectionMaxRetry attempts
CONNECTION_ERROR_CODE The browser returned an HTTP error, whose status is set on status
CONNECTION_CLOSED_ERROR_CODE The connection closed before the response was received
CONNECTION_TIMEOUT_ERROR_CODE The connection did not open within connectionMaxTime
COMMAND_TIMEOUT_ERROR_CODE The response of a command was not received within commandMaxTime

Keywords