@mefistofelix/cdp.js 0.2.0 → 0.3.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 (3) hide show
  1. package/README.md +37 -20
  2. package/cdp.js +57 -13
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # cdp.js
2
2
 
3
- A low-level Chromium DevTools Protocol library for Node.js, with an optional
4
- native Deno server for Chrome extension reverse connections. One ES module,
3
+ A low-level Chromium DevTools Protocol library for Node.js, Deno and Bun, with an
4
+ optional native Deno/Bun server for Chrome extension reverse connections. One ES module,
5
5
  standard-library imports, no runtime dependencies or root `package.json`.
6
6
 
7
7
  Use Node.js 22.19+ and an installed Chrome, Chromium or Edge. Import `cdp.js`
8
8
  directly from an ES module, for example `example.mjs`, and run `node example.mjs`.
9
9
  Browser discovery covers common Windows, macOS and Linux installation paths;
10
10
  use `executable_path` or the `CDP_BROWSER` environment variable for other paths.
11
+ The same module also runs with `bun example.mjs` or `deno run -A example.mjs`.
12
+ Direct and reverse modes are tested with Bun 1.4.3; no Bun-specific package,
13
+ configuration file or installation step is required by the library.
11
14
 
12
15
  For a published npm release:
13
16
 
@@ -24,7 +27,8 @@ below remain supported. Publication metadata is isolated in `npm/`.
24
27
  The package includes both `cdp_ext/` files. With `cdp_ext: true`, the manager
25
28
  writes a configured extension copy into the browser's profile directory and loads
26
29
  it automatically; no manual extraction or build is needed. Reverse mode still
27
- requires Deno. In Deno, import from `npm:@mefistofelix/cdp.js`.
30
+ requires Deno or Bun. In Deno, import from `npm:@mefistofelix/cdp.js`.
31
+ In Bun, use `bun add @mefistofelix/cdp.js` and the ordinary package import above.
28
32
 
29
33
  ## Features and scope
30
34
 
@@ -34,7 +38,7 @@ requires Deno. In Deno, import from `npm:@mefistofelix/cdp.js`.
34
38
  and automatic cleanup after their last target closes.
35
39
  - Incognito targets in direct and reverse modes.
36
40
  - Attachment through WebSocket, HTTP discovery or a debugging port.
37
- - Optional Chrome extension reverse connections through one native Deno server,
41
+ - Optional Chrome extension reverse connections through one native Deno/Bun server,
38
42
  without browser remote-debugging arguments.
39
43
  - Generic `chrome.x.y` extension API calls on that connection, including scripting,
40
44
  tabs/windows, cookies, storage, downloads and browser settings.
@@ -116,8 +120,8 @@ import api, { util, jsrpc, browser, cdp } from './cdp.js'
116
120
  | `client.call({ method, params = {} })` | Resolve routing and return a promise for the native CDP result, or a custom handler's result. |
117
121
  | `client.evaluate_xpath(params, fn, options)` | Execute a serialized function with a normalized XPath in a target; return the native `Runtime.evaluate` result. |
118
122
  | `client.close()` | Close owned processes, disconnect attached/reverse browsers, stop the reverse server and clear browser/socket mappings. Profiles and external browsers remain. |
119
- | `new jsrpc(urlOrSocket, options = {})` | Open a raw WebSocket JSON-RPC connection or equip an accepted native WebSocket with the same RPC methods. |
120
- | `client.listen_reverse(options = {})` | Start the native Deno WebSocket server; returns its `HttpServer`. |
123
+ | `new jsrpc(urlOrSocket, options = {})` | Open a raw WebSocket JSON-RPC connection or equip an accepted socket with the same RPC methods. |
124
+ | `client.listen_reverse(options = {})` | Start the native Deno or Bun WebSocket server; returns that server with common lifecycle fields documented below. |
121
125
  | `socket.req(request, timeout_ms)` | Assign a request ID and await its native result. |
122
126
  | `socket.notify(request)` | Serialize and send a raw message; no ID allocation or response wait. |
123
127
  | `socket.close()` | Native WebSocket close; does not terminate the browser. |
@@ -237,8 +241,8 @@ local configuration.
237
241
  | `request_timeout_ms` | number, `30000` | Default timeout for each protocol request on this browser's socket. |
238
242
  | `waitForDebuggerOnStart` | boolean, `false` | Ask Chromium to pause newly auto-attached targets. Applied to `Target.setAutoAttach` when connecting; not a CLI argument or target option. |
239
243
  | `websocket_url` | string, unset | Attach directly to a browser WebSocket endpoint. |
240
- | `cdp_ext` | boolean, `false` | Launch with the reverse extension under Deno. Start one shared server automatically if needed, prepare a profile-specific extension copy and wait for its connection. |
241
- | `reverse` | boolean, `false` | Attach to an externally launched extension instead of launching or discovering a browser. Starts the shared listener on first use under Deno. |
244
+ | `cdp_ext` | boolean, `false` | Launch with the reverse extension under Deno or Bun. Start one shared server automatically if needed, prepare a profile-specific extension copy and wait for its connection. |
245
+ | `reverse` | boolean, `false` | Attach to an externally launched extension instead of launching or discovering a browser. Starts the shared listener on first use under Deno or Bun. |
242
246
  | `extension_id` | string, browser name for external reverse attachment | Incoming extension ID to associate with this logical browser. Set automatically for managed `cdp_ext` launches. |
243
247
  | `http_url` | string, unset | Discover the endpoint from an HTTP(S) base URL or exact `/json/version` URL. |
244
248
  | `port` | number, unset | Discover an existing browser at `http://<host>:<port>/json/version`. |
@@ -343,7 +347,7 @@ Native proxy rules can use semicolon-separated strings; the generic CLI converte
343
347
  still comma-joins every array. Bypass lists accept both separators. Chrome gives
344
348
  `no-proxy-server` precedence, so overriding `proxy-server` alone does not cancel
345
349
  that flag; override `args['no-proxy-server']: false` too if needed.
346
- Browser proxy options affect Chrome traffic, not Node/Deno's endpoint discovery
350
+ Browser proxy options affect Chrome traffic, not the runtime's endpoint discovery
347
351
  or CDP transport. Chrome normally bypasses loopback addresses. To proxy them,
348
352
  use `args['proxy-bypass-list']: '<-loopback>'`; in reverse mode keep the extension's
349
353
  local WebSocket listener reachable. The library inserts no extra bypass rule.
@@ -491,12 +495,13 @@ browser instances on the same port; each extension announces its native ID
491
495
  in its first message. No remote-debugging argument is needed.
492
496
 
493
497
  Run the server under Deno using its native
494
- [WebSocket upgrade](https://docs.deno.com/api/deno/websockets/).
498
+ [WebSocket upgrade](https://docs.deno.com/api/deno/websockets/) or under Bun using
499
+ [Bun.serve](https://bun.sh/docs/runtime/http/websockets).
495
500
  Node's direct CDP mode remains supported; `listen_reverse()` under Node rejects
496
- with an explicit Deno requirement. There is no WebSocket framing implementation,
501
+ with an explicit Deno/Bun requirement. There is no WebSocket framing implementation,
497
502
  third-party server dependency or additional build step.
498
503
 
499
- Run `deno run -A examples/reverse.mjs` to launch Chrome with the extension, create
504
+ Run `deno run -A examples/reverse.mjs` or `bun examples/reverse.mjs` to launch Chrome with the extension, create
500
505
  a target and evaluate `21 * 2`. Ctrl+C closes the owned browser and server.
501
506
  The same mode is available per browser:
502
507
 
@@ -589,15 +594,24 @@ retry. Direct browsers do not keep this listener alive.
589
594
  `client.reverse_server` is the public shared configuration object. After startup
590
595
  it contains the effective listening port, including when `port: 0` originally
591
596
  requested an available port. Restarts reuse that port so existing extensions can
592
- still reconnect. `client.server` contains the current native Deno `HttpServer`,
597
+ still reconnect. `client.server` contains the current native Deno `HttpServer` or Bun `Server`,
593
598
  or is unset/null while stopped. A call arriving during shutdown waits for it to
594
599
  finish before starting a replacement server.
595
600
 
596
601
  `listen_reverse(options = client.reverse_server)` remains available to start the
597
- listener explicitly ahead of a browser call. It returns the native `HttpServer`;
602
+ listener explicitly ahead of a browser call. It returns the native runtime server;
598
603
  calling it while a server is running rejects. Explicit options replace the stored
599
604
  settings, with omitted fields taking the hostname/port defaults above.
600
605
 
606
+ Both runtimes expose `server.addr.hostname`, `server.addr.port`,
607
+ `server.shutdown()` and `server.finished`. On Bun, the library adds these fields:
608
+ `shutdown()` calls native `stop()` and resolves `finished` after shutdown.
609
+ Use `client.close()` to also close owned browsers and incoming sockets.
610
+ Bun's callback-based server sockets receive a small EventTarget adapter with
611
+ native send/close and live ready state. The same `jsrpc` handles both runtimes;
612
+ registration, request IDs, pending waits, events and session routing are shared.
613
+ Native Bun WebSocket limits still apply.
614
+
601
615
  Reverse calls reuse the normal target setup, custom handlers, mappings, routed
602
616
  events, request correlation, timeout and error behavior. `reverse: true` alone
603
617
  attaches externally: the manager neither launches nor updates that browser.
@@ -1530,9 +1544,9 @@ dictionary containers have a null prototype.
1530
1544
  | `client.reverse_server` | Shared listener configuration; the effective hostname/port are stored after startup. |
1531
1545
  | `client.custom_methods[name]` | Handler function. |
1532
1546
  | `client.browsers[name]` | Browser configuration fields plus live state. |
1533
- | `client.reverse_sockets[extension_id]` | Announced incoming native WebSocket, available even before the first call. |
1547
+ | `client.reverse_sockets[extension_id]` | Announced incoming socket, available even before the first call; native on Deno, an EventTarget adapter on Bun. |
1534
1548
  | `record.extension_id`, `record.extension_path` | Expected extension identity and the managed copy's canonical directory. |
1535
- | `client.server` | Current native Deno reverse `HttpServer`, or unset/null when not listening. |
1549
+ | `client.server` | Current native Deno/Bun reverse server with shared lifecycle fields, or unset/null when not listening. |
1536
1550
  | `client.server_closing` | Shutdown promise while the reverse listener is stopping, otherwise unset/null. |
1537
1551
  | `record.socket` | Current `jsrpc` connection once created. |
1538
1552
  | `record.proc` | Owned child process; absent for external attachment. |
@@ -1599,11 +1613,13 @@ subject to the debugging-port restrictions described above.
1599
1613
  Readiness timeout or launch failure rejects and attempts to
1600
1614
  terminate the spawned process.
1601
1615
 
1602
- `jsrpc` takes a WebSocket URL or an accepted native WebSocket and an options
1616
+ `jsrpc` takes a WebSocket URL or an accepted EventTarget socket and an options
1603
1617
  object whose recognized option is
1604
1618
  `request_timeout_ms` (default `30000`). Wait for the socket to open before `req`.
1605
- For an accepted socket, it returns that same WebSocket with RPC fields/methods;
1606
- it remains a native WebSocket, but need not be an instance of the `jsrpc` subclass.
1619
+ For an accepted socket, it returns that same object with RPC fields/methods.
1620
+ The object supplies `send`, `close`, live `readyState` and normal message/close/error
1621
+ events. Deno sockets remain native WebSockets; Bun uses the adapter described
1622
+ above. Accepted sockets need not be instances of the `jsrpc` subclass.
1607
1623
  The reverse server consumes the initial browser announcement before equipping
1608
1624
  the socket with RPC behavior. Empty-text heartbeats are ignored.
1609
1625
  The request is a raw envelope: `method`, optional `params`, and optional top-level
@@ -1662,12 +1678,13 @@ node --check cdp.js
1662
1678
  node --check cdp_ext/cdp_ext.js
1663
1679
  node --test tests/cdp.test.js
1664
1680
  deno test -A tests/reverse.test.js
1681
+ bun test tests/cdp.test.js tests/reverse.test.js --timeout 60000
1665
1682
  ```
1666
1683
 
1667
1684
  Integration tests launch local headless Chromium with disposable profiles under
1668
1685
  `build/tests/`. Set `CDP_BROWSER` if discovery does not match the machine. Direct
1669
1686
  library use needs no build or package installation; npm packaging is optional.
1670
- The Deno test loads the real extension into two headless Chrome instances on one
1687
+ The shared Deno/Bun reverse suite loads the real extension into two headless Chrome instances on one
1671
1688
  server port without remote-debugging arguments. It checks native/custom calls,
1672
1689
  Network events, webRequest notifications without debugger attachment, bindings,
1673
1690
  child sessions, reconnect, detach/recreate, incognito and isolation.
package/cdp.js CHANGED
@@ -46,7 +46,7 @@ export class util {
46
46
 
47
47
  export class jsrpc extends WebSocket {
48
48
  constructor(url, options = {}) {
49
- const socket = url instanceof WebSocket ? url : super(url)
49
+ const socket = typeof url === 'string' || url instanceof URL ? super(url) : url
50
50
  setMaxListeners(0, socket)
51
51
  socket.id = 0
52
52
  socket.pending = {}
@@ -355,18 +355,9 @@ export class cdp extends EventTarget {
355
355
  hostname = '127.0.0.1',
356
356
  port = 9223,
357
357
  } = this.reverse_server) {
358
- if (!globalThis.Deno) throw new Error('The reverse WebSocket server requires Deno')
358
+ if (!globalThis.Deno && !globalThis.Bun) throw new Error('The reverse WebSocket server requires Deno or Bun')
359
359
  if (this.server) throw new Error('Reverse server is already listening')
360
- this.server = Deno.serve({
361
- hostname,
362
- port,
363
- }, request => {
364
- const origin = request.headers.get('origin')
365
- if (request.headers.get('upgrade') !== 'websocket' ||
366
- !origin?.startsWith('chrome-extension://')) {
367
- return new Response(null, { status: 403 })
368
- }
369
- const { socket, response } = Deno.upgradeWebSocket(request)
360
+ const accept = (socket, origin) => {
370
361
  socket.addEventListener('message', event => {
371
362
  const id = JSON.parse(event.data).browser
372
363
  if (id !== origin.slice('chrome-extension://'.length) ||
@@ -380,8 +371,61 @@ export class cdp extends EventTarget {
380
371
  })
381
372
  util.emit(this, 'reverse_' + encodeURIComponent(id), socket)
382
373
  }, { once: true })
374
+ }
375
+ const fetch = (request, server) => {
376
+ const origin = request.headers.get('origin')
377
+ if (request.headers.get('upgrade') !== 'websocket' ||
378
+ !origin?.startsWith('chrome-extension://')) {
379
+ return new Response(null, { status: 403 })
380
+ }
381
+ if (globalThis.Bun) {
382
+ if (server.upgrade(request, {
383
+ data: {
384
+ origin,
385
+ socket: new EventTarget(),
386
+ },
387
+ })) return
388
+ return new Response(null, { status: 400 })
389
+ }
390
+ const { socket, response } = Deno.upgradeWebSocket(request)
391
+ accept(socket, origin)
383
392
  return response
384
- })
393
+ }
394
+ if (globalThis.Bun) {
395
+ const server = this.server = Bun.serve({
396
+ hostname,
397
+ port,
398
+ fetch,
399
+ websocket: {
400
+ open(socket) {
401
+ const events = socket.data.socket
402
+ events.send = socket.send.bind(socket)
403
+ events.close = socket.close.bind(socket)
404
+ Object.defineProperty(events, 'readyState', { get: () => socket.readyState })
405
+ accept(events, socket.data.origin)
406
+ },
407
+ message(socket, data) {
408
+ socket.data.socket.dispatchEvent(new MessageEvent('message', { data }))
409
+ },
410
+ close(socket, code, reason) {
411
+ socket.data.socket.dispatchEvent(Object.assign(new Event('close'), {
412
+ code,
413
+ reason,
414
+ }))
415
+ },
416
+ },
417
+ })
418
+ server.addr = {
419
+ hostname,
420
+ port: server.port,
421
+ }
422
+ const finished = Promise.withResolvers()
423
+ server.finished = finished.promise
424
+ server.shutdown = () => server.stop().then(finished.resolve)
425
+ } else this.server = Deno.serve({
426
+ hostname,
427
+ port,
428
+ }, fetch)
385
429
  this.reverse_server = {
386
430
  hostname,
387
431
  port: this.server.addr.port,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mefistofelix/cdp.js",
3
- "version": "0.2.0",
4
- "description": "Low-level Chromium DevTools Protocol client for Node.js, with no runtime dependencies.",
3
+ "version": "0.3.0",
4
+ "description": "Low-level Chromium DevTools Protocol client for Node.js, Deno and Bun, with no runtime dependencies.",
5
5
  "type": "module",
6
6
  "exports": "./cdp.js",
7
7
  "files": [