simple-cdp
v1.10.1
Published
JavaScript library to interact with the Chrome DevTools Protocol
Maintainers
Readme
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-cdpStart 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-cdpSince 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 hereThe 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 |
