@mefistofelix/cdp.js 0.2.0 → 0.4.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.
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
 
@@ -33,8 +37,9 @@ requires Deno. In Deno, import from `npm:@mefistofelix/cdp.js`.
33
37
  - Browser proxies and named direct-mode contexts with independent proxies/storage
34
38
  and automatic cleanup after their last target closes.
35
39
  - Incognito targets in direct and reverse modes.
40
+ - Named, colored tab groups through the reverse Chrome extension.
36
41
  - Attachment through WebSocket, HTTP discovery or a debugging port.
37
- - Optional Chrome extension reverse connections through one native Deno server,
42
+ - Optional Chrome extension reverse connections through one native Deno/Bun server,
38
43
  without browser remote-debugging arguments.
39
44
  - Generic `chrome.x.y` extension API calls on that connection, including scripting,
40
45
  tabs/windows, cookies, storage, downloads and browser settings.
@@ -116,8 +121,8 @@ import api, { util, jsrpc, browser, cdp } from './cdp.js'
116
121
  | `client.call({ method, params = {} })` | Resolve routing and return a promise for the native CDP result, or a custom handler's result. |
117
122
  | `client.evaluate_xpath(params, fn, options)` | Execute a serialized function with a normalized XPath in a target; return the native `Runtime.evaluate` result. |
118
123
  | `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`. |
124
+ | `new jsrpc(urlOrSocket, options = {})` | Open a raw WebSocket JSON-RPC connection or equip an accepted socket with the same RPC methods. |
125
+ | `client.listen_reverse(options = {})` | Start the native Deno or Bun WebSocket server; returns that server with common lifecycle fields documented below. |
121
126
  | `socket.req(request, timeout_ms)` | Assign a request ID and await its native result. |
122
127
  | `socket.notify(request)` | Serialize and send a raw message; no ID allocation or response wait. |
123
128
  | `socket.close()` | Native WebSocket close; does not terminate the browser. |
@@ -237,8 +242,8 @@ local configuration.
237
242
  | `request_timeout_ms` | number, `30000` | Default timeout for each protocol request on this browser's socket. |
238
243
  | `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
244
  | `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. |
245
+ | `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. |
246
+ | `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
247
  | `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
248
  | `http_url` | string, unset | Discover the endpoint from an HTTP(S) base URL or exact `/json/version` URL. |
244
249
  | `port` | number, unset | Discover an existing browser at `http://<host>:<port>/json/version`. |
@@ -343,7 +348,7 @@ Native proxy rules can use semicolon-separated strings; the generic CLI converte
343
348
  still comma-joins every array. Bypass lists accept both separators. Chrome gives
344
349
  `no-proxy-server` precedence, so overriding `proxy-server` alone does not cancel
345
350
  that flag; override `args['no-proxy-server']: false` too if needed.
346
- Browser proxy options affect Chrome traffic, not Node/Deno's endpoint discovery
351
+ Browser proxy options affect Chrome traffic, not the runtime's endpoint discovery
347
352
  or CDP transport. Chrome normally bypasses loopback addresses. To proxy them,
348
353
  use `args['proxy-bypass-list']: '<-loopback>'`; in reverse mode keep the extension's
349
354
  local WebSocket listener reachable. The library inserts no extra bypass rule.
@@ -491,12 +496,13 @@ browser instances on the same port; each extension announces its native ID
491
496
  in its first message. No remote-debugging argument is needed.
492
497
 
493
498
  Run the server under Deno using its native
494
- [WebSocket upgrade](https://docs.deno.com/api/deno/websockets/).
499
+ [WebSocket upgrade](https://docs.deno.com/api/deno/websockets/) or under Bun using
500
+ [Bun.serve](https://bun.sh/docs/runtime/http/websockets).
495
501
  Node's direct CDP mode remains supported; `listen_reverse()` under Node rejects
496
- with an explicit Deno requirement. There is no WebSocket framing implementation,
502
+ with an explicit Deno/Bun requirement. There is no WebSocket framing implementation,
497
503
  third-party server dependency or additional build step.
498
504
 
499
- Run `deno run -A examples/reverse.mjs` to launch Chrome with the extension, create
505
+ Run `deno run -A examples/reverse.mjs` or `bun examples/reverse.mjs` to launch Chrome with the extension, create
500
506
  a target and evaluate `21 * 2`. Ctrl+C closes the owned browser and server.
501
507
  The same mode is available per browser:
502
508
 
@@ -589,15 +595,24 @@ retry. Direct browsers do not keep this listener alive.
589
595
  `client.reverse_server` is the public shared configuration object. After startup
590
596
  it contains the effective listening port, including when `port: 0` originally
591
597
  requested an available port. Restarts reuse that port so existing extensions can
592
- still reconnect. `client.server` contains the current native Deno `HttpServer`,
598
+ still reconnect. `client.server` contains the current native Deno `HttpServer` or Bun `Server`,
593
599
  or is unset/null while stopped. A call arriving during shutdown waits for it to
594
600
  finish before starting a replacement server.
595
601
 
596
602
  `listen_reverse(options = client.reverse_server)` remains available to start the
597
- listener explicitly ahead of a browser call. It returns the native `HttpServer`;
603
+ listener explicitly ahead of a browser call. It returns the native runtime server;
598
604
  calling it while a server is running rejects. Explicit options replace the stored
599
605
  settings, with omitted fields taking the hostname/port defaults above.
600
606
 
607
+ Both runtimes expose `server.addr.hostname`, `server.addr.port`,
608
+ `server.shutdown()` and `server.finished`. On Bun, the library adds these fields:
609
+ `shutdown()` calls native `stop()` and resolves `finished` after shutdown.
610
+ Use `client.close()` to also close owned browsers and incoming sockets.
611
+ Bun's callback-based server sockets receive a small EventTarget adapter with
612
+ native send/close and live ready state. The same `jsrpc` handles both runtimes;
613
+ registration, request IDs, pending waits, events and session routing are shared.
614
+ Native Bun WebSocket limits still apply.
615
+
601
616
  Reverse calls reuse the normal target setup, custom handlers, mappings, routed
602
617
  events, request correlation, timeout and error behavior. `reverse: true` alone
603
618
  attaches externally: the manager neither launches nor updates that browser.
@@ -1259,6 +1274,7 @@ await client.call({
1259
1274
  | `targetId` | `null` | Optional existing physical target ID. Reused if discovered; otherwise create a new target. |
1260
1275
  | `context` | unset | Direct-mode context name or first-use configuration object; see named contexts above. Reverse mode rejects this option. |
1261
1276
  | `incognito` | `false` | Create/select a private target; direct mode uses the shared `'incognito'` context by default, reverse uses a native incognito window with the user's extension grant. |
1277
+ | `group` | unset | Reverse-only tab group name or first-use `{ name, color }` configuration. False skips grouping. |
1262
1278
  | `create_params` | `{}` | Native creation overrides, merged over `url: 'about:blank'` and `background: true`; start from a blank target without requesting foreground activation. |
1263
1279
  | `initialize` | `true` | Run the convenience setup sequence; false skips it. |
1264
1280
  | `runtime` | `'bootstrap'` | Enable Runtime for setup, then disable reporting. `true` leaves it enabled; `false` skips both operations. |
@@ -1269,6 +1285,78 @@ await client.call({
1269
1285
  | `binding` | `'_send_to_cdp'` | Default page-to-host JavaScript binding. String changes its name; false skips installation; true selects the default name. |
1270
1286
  | `background_service` | `true` | Clear, observe and record `pushMessaging` background-service events in Chromium. |
1271
1287
 
1288
+ ### Named tab groups
1289
+
1290
+ With `cdp_ext: true` or external `reverse: true`, set `target.group` when first
1291
+ configuring a target:
1292
+
1293
+ ```js
1294
+ await client.call({
1295
+ method: 'Page.navigate',
1296
+ params: {
1297
+ browser: {
1298
+ name: 'work',
1299
+ cdp_ext: true,
1300
+ },
1301
+ target: {
1302
+ name: 'search',
1303
+ group: {
1304
+ name: 'Research',
1305
+ color: 'blue',
1306
+ },
1307
+ },
1308
+ url: 'https://example.com/',
1309
+ },
1310
+ })
1311
+ await client.call({
1312
+ method: 'Page.navigate',
1313
+ params: {
1314
+ browser: 'work',
1315
+ target: {
1316
+ name: 'notes',
1317
+ group: 'Research',
1318
+ },
1319
+ url: 'https://example.com/notes',
1320
+ },
1321
+ })
1322
+ ```
1323
+
1324
+ | Group option | Default | Meaning |
1325
+ | --- | --- | --- |
1326
+ | `name` | required in an object | Logical name in `record.groups`; also the visible group title. A string reference creates a default group if this name is new. |
1327
+ | `color` | unset | Chrome's native color selection for new groups; otherwise retain the current color. Explicit values: `grey`, `blue`, `red`, `yellow`, `green`, `pink`, `purple`, `cyan`, `orange`. |
1328
+
1329
+ The first use of a group name records its configuration. Concurrent target
1330
+ creation shares that definition and serializes group operations. Names are scoped
1331
+ to the logical browser. Each window has its own native group for that name:
1332
+ tabs stay in their current window, including separate regular/incognito windows.
1333
+ Groups are created by the library, not adopted from unrelated groups that happen
1334
+ to have the same visible title. Incognito still requires the bridge's user grant.
1335
+
1336
+ Grouping happens before optional CDP setup, including with `initialize: false`.
1337
+ It also works with `targetId` selecting an existing tab. As with other target
1338
+ options, passing a new group on a target label that was already configured does
1339
+ not reconfigure that label. Use another label with its physical `targetId`, or
1340
+ edit the public record and reinitialize the target.
1341
+
1342
+ Reconnection reapplies the name/color and rejoins a surviving group. If its last
1343
+ tab was closed or ungrouped, Chrome removes the native group; later target setup
1344
+ creates a new one. `record.groups[name].window_groups[windowId]` stores the last
1345
+ known native group ID. These IDs are checked against Chrome during setup and can
1346
+ remain stale between operations; no extra event subscriptions or persistence are
1347
+ added. In reverse target info, `groupId` is the current native ID, or `-1` when
1348
+ ungrouped. Generic `chrome.tabs` / `chrome.tabGroups` calls remain available for
1349
+ immediate native updates. Native errors, including invalid colors, reject setup
1350
+ with the usual RPC error context and can be retried after correcting the record.
1351
+
1352
+ Direct CDP does not expose browser tab-group commands. A non-false `group` option
1353
+ therefore rejects before connecting or launching in direct mode. The bridge's
1354
+ existing `tabGroups` permission is sufficient; no extra permission is added.
1355
+ See [Chrome tab groups](https://developer.chrome.com/docs/extensions/reference/api/tabGroups)
1356
+ and [grouping tabs](https://developer.chrome.com/docs/extensions/reference/api/tabs#method-group).
1357
+
1358
+ ### Target initialization
1359
+
1272
1360
  Domain options set to false skip setup commands; they do not actively disable a
1273
1361
  domain already enabled by another caller. Likewise, `binding: false` does not
1274
1362
  remove a previously installed binding. Use native CDP commands for immediate
@@ -1530,9 +1618,9 @@ dictionary containers have a null prototype.
1530
1618
  | `client.reverse_server` | Shared listener configuration; the effective hostname/port are stored after startup. |
1531
1619
  | `client.custom_methods[name]` | Handler function. |
1532
1620
  | `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. |
1621
+ | `client.reverse_sockets[extension_id]` | Announced incoming socket, available even before the first call; native on Deno, an EventTarget adapter on Bun. |
1534
1622
  | `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. |
1623
+ | `client.server` | Current native Deno/Bun reverse server with shared lifecycle fields, or unset/null when not listening. |
1536
1624
  | `client.server_closing` | Shutdown promise while the reverse listener is stopping, otherwise unset/null. |
1537
1625
  | `record.socket` | Current `jsrpc` connection once created. |
1538
1626
  | `record.proc` | Owned child process; absent for external attachment. |
@@ -1540,6 +1628,7 @@ dictionary containers have a null prototype.
1540
1628
  | `record.targets[label]` | Target options, physical `targetId`, `sessionId`, `connecting` and `setup_errors` after setup. |
1541
1629
  | `record.contexts[name]` | Recorded context options, native `browserContextId` or null, and pending `connecting`/`closing` promises. Direct mode only. |
1542
1630
  | `record.context_checking` | Pending automatic empty-context cleanup, otherwise unset/null. |
1631
+ | `record.groups[name]` | Recorded tab-group options, per-window native IDs in `window_groups`, and a pending `connecting` operation. Reverse only. |
1543
1632
  | `record.target_info[targetId]` | Discovered native target info plus cached `sessionId` when attached. |
1544
1633
  | `record.session_targets[sessionId]` | Physical `targetId` for a session. |
1545
1634
  | `socket.pending[id]` | Response-wait promise, removed on completion. |
@@ -1599,11 +1688,13 @@ subject to the debugging-port restrictions described above.
1599
1688
  Readiness timeout or launch failure rejects and attempts to
1600
1689
  terminate the spawned process.
1601
1690
 
1602
- `jsrpc` takes a WebSocket URL or an accepted native WebSocket and an options
1691
+ `jsrpc` takes a WebSocket URL or an accepted EventTarget socket and an options
1603
1692
  object whose recognized option is
1604
1693
  `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.
1694
+ For an accepted socket, it returns that same object with RPC fields/methods.
1695
+ The object supplies `send`, `close`, live `readyState` and normal message/close/error
1696
+ events. Deno sockets remain native WebSockets; Bun uses the adapter described
1697
+ above. Accepted sockets need not be instances of the `jsrpc` subclass.
1607
1698
  The reverse server consumes the initial browser announcement before equipping
1608
1699
  the socket with RPC behavior. Empty-text heartbeats are ignored.
1609
1700
  The request is a raw envelope: `method`, optional `params`, and optional top-level
@@ -1625,6 +1716,7 @@ labels, target labels, custom methods or reconnection policy in this layer.
1625
1716
  | Failed discovery or local launch | Reject the call establishing the connection. |
1626
1717
  | Named context in reverse mode | Reject explicitly; no separate process emulation. |
1627
1718
  | Reverse incognito without the extension's user grant | Reject with an instruction to enable Allow in incognito. |
1719
+ | Tab group in direct mode | Reject before connecting; tab groups require the reverse extension APIs. |
1628
1720
  | Automatic context cleanup fails | Emit the manager's `error` event with browser label and original error. |
1629
1721
 
1630
1722
  ```js
@@ -1662,15 +1754,18 @@ node --check cdp.js
1662
1754
  node --check cdp_ext/cdp_ext.js
1663
1755
  node --test tests/cdp.test.js
1664
1756
  deno test -A tests/reverse.test.js
1757
+ bun test tests/cdp.test.js tests/reverse.test.js --timeout 60000
1665
1758
  ```
1666
1759
 
1667
1760
  Integration tests launch local headless Chromium with disposable profiles under
1668
1761
  `build/tests/`. Set `CDP_BROWSER` if discovery does not match the machine. Direct
1669
1762
  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
1763
+ The shared Deno/Bun reverse suite loads the real extension into two headless Chrome instances on one
1671
1764
  server port without remote-debugging arguments. It checks native/custom calls,
1672
1765
  Network events, webRequest notifications without debugger attachment, bindings,
1673
1766
  child sessions, reconnect, detach/recreate, incognito and isolation.
1767
+ Tab-group checks cover concurrent creation, existing tabs, title/color, per-window
1768
+ incognito separation, reconnection, recreation and native validation/retry.
1674
1769
  Node coverage also checks actual browser/context proxy routing, shared private
1675
1770
  storage, concurrent context creation, automatic disposal, unselected targets and
1676
1771
  context ownership in externally attached browsers.
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,
@@ -640,9 +684,58 @@ export class cdp extends EventTarget {
640
684
  }
641
685
  }
642
686
 
687
+ async _group(browser, target) {
688
+ const socket = browser.socket
689
+ const group = browser.groups[target.group]
690
+ while (group.connecting) await group.connecting
691
+ return group.connecting = (async () => {
692
+ const tab = await socket.req({
693
+ method: 'chrome.tabs.get',
694
+ params: { args: [Number(target.targetId)] },
695
+ })
696
+ const groups = await socket.req({
697
+ method: 'chrome.tabGroups.query',
698
+ params: { args: [{ windowId: tab.windowId }] },
699
+ })
700
+ const groupId = group.window_groups[tab.windowId]
701
+ const existing = groups.some(value => value.id === groupId)
702
+ const id = await socket.req({
703
+ method: 'chrome.tabs.group',
704
+ params: {
705
+ args: [{
706
+ tabIds: tab.id,
707
+ groupId: existing ? groupId : undefined,
708
+ createProperties: existing ? undefined : { windowId: tab.windowId },
709
+ }],
710
+ },
711
+ })
712
+ group.window_groups[tab.windowId] = id
713
+ await socket.req({
714
+ method: 'chrome.tabGroups.update',
715
+ params: {
716
+ args: [id, {
717
+ title: group.name,
718
+ color: group.color,
719
+ }],
720
+ },
721
+ })
722
+ return id
723
+ })().finally(() => {
724
+ group.connecting = null
725
+ })
726
+ }
727
+
643
728
  async _target(browser, target) {
644
729
  const socket = browser.socket
645
730
  const reverse = browser.reverse || browser.cdp_ext
731
+ if (target.group != null && target.group !== false) {
732
+ const spec = typeof target.group === 'string' ? { name: target.group } : target.group
733
+ browser.groups[spec.name] ??= {
734
+ ...spec,
735
+ window_groups: Object.create(null),
736
+ }
737
+ target.group = spec.name
738
+ }
646
739
  if (target.incognito && !reverse && target.context == null && target.create_params?.browserContextId == null) {
647
740
  target.context = 'incognito'
648
741
  }
@@ -676,6 +769,7 @@ export class cdp extends EventTarget {
676
769
  Object.assign(info, targetInfo)
677
770
  if (!info.incognito) throw new Error('Existing target is not incognito')
678
771
  }
772
+ if (target.group != null && target.group !== false) info.groupId = await this._group(browser, target)
679
773
  if (!info.sessionId) {
680
774
  info.sessionId = (await socket.req({
681
775
  method: 'Target.attachToTarget',
@@ -752,10 +846,14 @@ export class cdp extends EventTarget {
752
846
  },
753
847
  targets: Object.create(null),
754
848
  contexts: Object.create(null),
849
+ groups: Object.create(null),
755
850
  }
756
851
  if (target?.context != null && (record.reverse || record.cdp_ext)) {
757
852
  throw new Error('Named browser contexts are unavailable through the reverse Chrome extension')
758
853
  }
854
+ if (target?.group != null && target.group !== false && !record.reverse && !record.cdp_ext) {
855
+ throw new Error('Tab groups require the reverse Chrome extension (cdp_ext or reverse)')
856
+ }
759
857
  if (!record.connecting && record.socket?.readyState !== WebSocket.OPEN) {
760
858
  record.connecting = this._connect(record).catch(error => {
761
859
  record.socket?.close()
@@ -19,6 +19,7 @@ function target_info(tab) {
19
19
  url: tab.url ?? tab.pendingUrl ?? '',
20
20
  attached: !!attached[tab.id],
21
21
  incognito: tab.incognito,
22
+ groupId: tab.groupId,
22
23
  }
23
24
  }
24
25
 
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.4.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": [