package cdp-gen
Install
dune-project
Dependency
Authors
Maintainers
Sources
sha256=0ce3fc800198774007743cf1415a2c8c069ba65c22e43accfbce35db1be3144c
sha512=e6196292d6a1bdcee68a15efe6fff2dea5217bfb5c87cfb476707426bdf69a8592f49a0d30c0bbcd9e554d240611514ba5425129d37adc04945caaae3542dd59
Description
Command-line generator behind the cdp package: fetches a devtools-protocol snapshot and emits typed OCaml modules with JSON codecs for the selected domains.
README
ocaml-cdp
OCaml client for the Chrome DevTools Protocol (CDP): typed protocol modules generated from the official protocol JSON, plus an Lwt connection over libcurl WebSockets.
Status: experimental, not released. The full stack works end to end against a real Chrome (see the demo below); the API may still change.
Packages
Package | What it is |
|---|---|
| Typed protocol domains: records, enums, commands, events, with JSON codecs |
| The connection: WebSocket transport, typed |
| The generator CLI: protocol JSON in, OCaml out |
cdp ships 10 domains: Browser, DOM, Debugger, Emulation, IO, Network, Page, Runtime, Security, and Target. The generator covers all 58 — see How generation works to build your own selection.
Build from source
The packages are not on opam yet; build from a clone:
git clone https://github.com/ahrefs/ocaml-cdp.git
cd ocaml-cdp
opam switch create . 5.4.1 --no-install
opam install . --deps-only --with-test
make build testcdp-lwt uses libcurl's WebSocket API, which ocurl has not released yet (latest release: 0.10.0) — cdp-lwt.opam therefore pins ocurl master via pin-depends, and opam install picks that up by itself.
Your system libcurl must be 7.86 or newer (check with curl-config --version) and built with WebSocket support. Chrome-launching code and examples need a Chrome: by default the executable is google-chrome from PATH; set the CDP_CHROME environment variable (or pass ~executable to Chrome.launch) to use another binary, e.g. chromium or CDP_CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" on macOS.
Demo
make demo launches a headless Chrome and runs examples/navigate.ml:
let%lwt chrome = Cdp_lwt.Chrome.launch () in
let%lwt transport = Cdp_lwt.Curl_transport.connect ~url:chrome.ws_url () in
let connection = Cdp_lwt.Connection.create transport in
let call ?session command = Cdp_lwt.Connection.call connection ?session ~timeout:10.0 command in
let%lwt created = call (Cdp.Target.Create_target.command (Cdp.Target.Create_target.make_params ~url:"about:blank" ())) in
let%lwt attached =
call (Cdp.Target.Attach_to_target.command
(Cdp.Target.Attach_to_target.make_params ~target_id:created.target_id ~flatten:true ()))
in
let session = attached.session_id in
let%lwt () = call ~session (Cdp.Page.Enable.command (Cdp.Page.Enable.make_params ())) in
let loaded = Cdp_lwt.Connection.next_event connection ~session Cdp.Page.Load_event_fired.event in
let%lwt _navigation = call ~session (Cdp.Page.Navigate.command (Cdp.Page.Navigate.make_params ~url:"data:text/html,<title>Hello from OCaml CDP</title>" ())) in
let%lwt _fired = loaded in
let%lwt evaluated =
call ~session (Cdp.Runtime.Evaluate.command (Cdp.Runtime.Evaluate.make_params ~expression:"document.title" ()))
inEvery step is a typed command; failures are typed too (Protocol_error, Call_timeout, Session_detached, Connection_closed). call waits up to 180 seconds by default — pass ~timeout to change it (the demo shortens it to 10), or Float.infinity to wait forever.
One rule to know: next_event catches events arriving after it is called — subscribe first, then trigger (as the demo does around navigate). next_event waits for one occurrence; for a persistent subscription use Connection.on_event, which fires on every occurrence until unsubscribed.
Examples
All examples live in examples/ and run against a real Chrome:
Example | Command | What it does |
|---|---|---|
| launch Chrome, open a page, read its title back | |
| report the main document's HTTP status, headers, and rendered HTML | |
| save a full-size screenshot and a small thumbnail into | |
| screenshot through a Chrome that is already running, closing only its own tab |
navigate, render, and screenshot launch their own headless Chrome. attach connects to an existing one: start Chrome with --remote-debugging-port=<port>, read webSocketDebuggerUrl from http://127.0.0.1:<port>/json/version, and pass it as WS=.
Chrome.launch keeps Chrome's sandbox on — it is the isolation layer between web pages and your machine. In environments where the sandbox cannot start (typically running as root in a container without user namespaces), pass ~no_sandbox:true and treat every page you open as untrusted.
The types
- Ids and timestamps are sealed: a
Request_id.tcannot be confused with aFrame_id.t. - Enums are variants with an
Otherfallback, so new Chrome enum values never crash a decode. - Optional fields are
option;Nonefields are omitted on the wire. - Deprecated and experimental protocol items carry compiler alerts.
- Broken characters from pages are repaired, not fatal: JavaScript strings may hold half of a two-unit character (
"😀".substring(0, 1)), and Chrome sends the lone half as-is. OCaml strings are UTF-8 and cannot represent it, and dropping the message would hang the pending call — so each unpaired half becomes U+FFFD (�) before parsing.
How generation works
protocol/*.json --(cdp-gen)--> lib/cdp_*.ml --(dune + ppx)--> the cdp libraryThe generated code is committed; users never run the generator. The vendored protocol snapshot is pinned in protocol/REVISION.
make generate # regenerate lib/ after changing gen/gen.ml
make update-protocol # fetch the latest protocol and regenerate
make update-protocol REV=1680125 # fetch a specific revision
make check # CI guard: lib/ matches the generatorTo generate types for your own protocol snapshot (any revision, or your own JSON), use the CLI directly:
cdp-gen fetch mydir 1650000
cdp-gen generate mydir/browser_protocol.json mydir/js_protocol.json out Network,PageTo compile the output as a library: copy the four hand-written glue files cdp_json.ml, cdp_command.ml, cdp_event.ml, and cdp_envelope.ml from lib/ next to the generated files, and use this dune stanza (the one make check-full uses, under whatever library name you like):
(library
(name my_cdp)
(wrapped false)
(libraries jsonkit yojson)
(preprocess
(pps jsonkit.ppx ppx_deriving.show ppx_deriving.eq ppx_deriving.make))
(flags
(:standard -w -a -alert -all)))Development
make help lists all targets. Tests:
- cram tests (
test/cram/*.t): small protocol JSON in, generated OCaml out — review diffs withdune runtest, accept withdune promote; - generator unit tests (
test/gen/) and schema-driven roundtrip tests over every generated type (test/roundtrip/, regenerated bymake generate); - envelope and connection tests over a mock transport (
test/,test/lwt/); make test-browser: opt-in smoke tests against a local headless Chrome;make check-full: generates and compiles ALL 58 protocol domains.
License
MIT, with one exception: the vendored protocol definitions protocol/browser_protocol.json and protocol/js_protocol.json come from the Chrome DevTools Protocol and are BSD-3-Clause, Copyright 2014 The Chromium Authors — see protocol/LICENSE. make update-protocol refreshes that license file together with the JSONs.