Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
744a241
refactor(cdp-agent): move snapDOM rendering out of the screencast int…
jerrychan7 Nov 1, 2026
58cec38
feat(cdp-agent): take screenshots with Page.captureScreenshot and rep…
jerrychan7 Nov 1, 2026
fcce1c3
docs: describe screenshots and their limits
jerrychan7 Nov 1, 2026
5333464
fix(cdp-agent): scale screenshots down only when the canvas cannot ho…
jerrychan7 Nov 1, 2026
3d57936
feat(cdp-agent): answer the emulation commands device mode sends
jerrychan7 Nov 1, 2026
0d9fd2a
feat(shell): device mode in shell mode, with DevTools laying out the …
jerrychan7 Nov 1, 2026
a7a4049
docs: describe device mode and the frontend's layout in shell mode
jerrychan7 Nov 1, 2026
b790266
ci(release): require npm 11.21.0, which npm dist-tag needs under trus…
jerrychan7 Nov 1, 2026
c514fa2
fix(shell): clear device mode with the frontend and tidy the shell la…
jerrychan7 Nov 1, 2026
07ec665
fix(cdp-agent): retry a screenshot smaller only when drawing fails
jerrychan7 Nov 1, 2026
82df152
fix(cdp-agent): report the deprecated layout metrics in device pixels…
jerrychan7 Nov 1, 2026
342f887
test(cdp-agent): cover the screenshot retry and the layout metrics on…
jerrychan7 Nov 1, 2026
6e67032
test(e2e): make the device mode tests check what they claim
jerrychan7 Nov 1, 2026
bd2ae42
docs: describe the branch model and the development and release flow
jerrychan7 Nov 1, 2026
bb00df2
docs: move the design out of the README into docs/design.md
jerrychan7 Nov 1, 2026
16523bd
feat(shell): resize docked DevTools with a grip, shared by the panel …
jerrychan7 Nov 1, 2026
5885cab
docs: a short README to start with, and a usage guide to look things …
jerrychan7 Nov 1, 2026
66a2a30
fix(runtime): preview console.table's rows so the frontend can draw t…
jerrychan7 Nov 1, 2026
752a241
feat(probe): one script tag from jsDelivr or unpkg, shell mode included
jerrychan7 Nov 1, 2026
5878c44
docs(changelog): note the console.table fix
jerrychan7 Nov 1, 2026
cfbe3a7
fix(shell): restore the page when the shell's element goes, and narro…
jerrychan7 Nov 1, 2026
37a2212
test: restore what the tests patch even when they fail, and prove the…
jerrychan7 Nov 1, 2026
ccb73eb
refactor(element): split element.ts into an element/ directory
jerrychan7 Nov 2, 2026
89ed730
test(input): ignore a mouse release Chrome lets through before touch …
jerrychan7 Nov 2, 2026
4d307d6
test(input): compare only the press in the auxclick test
jerrychan7 Nov 2, 2026
20c0fe0
test(e2e): click the Sources panel's controls once they are laid out
jerrychan7 Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ jobs:
registry-url: https://registry.npmjs.org

- name: Update npm
# Trusted publishing needs npm 11.5.1 or later; Node 22 comes with npm 10.
run: npm install -g npm@11
# Trusted publishing needs npm 11.5.1 or later, and 11.21.0 for npm dist-tag; Node 22 comes
# with npm 10.
run: npm install -g npm@^11.21.0

- name: Publish to npm
run: |
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All packages share one version and are released together (see DEV.md, "Releasing

## Unreleased

- From jsDelivr or unpkg, the probe needs no other attribute: it loads the frontend of its own version from there, and shell mode works (without the Service Worker). Shell configs from such a CDN are trusted only in the probe's package and the frontend's at that version, no longer anywhere on the CDN. `data-no-panel` goes without the panel: a page from such a CDN only debugged remotely (`data-remote`) needs it, or it now gets a panel too.
- The docked panel is resized with a grip on the middle of its inner edge, on the page's side, in place of the bar across its whole edge: it covers neither DevTools nor more of the page than it shows, and looks the same in and out of shell mode. The undocked window keeps its bar, which moves it.
- Device mode in shell mode, as in Chrome: DevTools lays out the page and itself, the toolbar's device button sizes the page as a device (scaled to fit) and its menu takes screenshots, docked or undocked. It sets the page's size only, not its pixel ratio, user agent or touch input. A grip against the middle of DevTools' edge, on the page's side, resizes them without covering DevTools, larger on touch screens. Outside shell mode the button is still the screencast's.
- `console.table` of objects shows their fields as columns, as in Chrome, instead of a single "Value" column; columns given as an array filter them.
- `Page.captureScreenshot` and `Page.getLayoutMetrics`: screenshots of the viewport, a region or the whole page, rendered by snapDOM and marked `approximate: true`, for tools and agents that drive the page over CDP.

## 0.1.0

Based on Chrome 156 (156.0.8078.80).
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

## Unreleased

- 探针来自 jsDelivr 或 unpkg 时,不用再写其他属性:它会从同一个 CDN 加载同版本的前端,外壳模式也能用(没有 Service Worker)。来自这类 CDN 时,外壳配置只信任探针所在的包和同版本的前端包,不再信任整个 CDN。新增 `data-no-panel`,不挂载面板:从这类 CDN 引入、只做远程调试(`data-remote`)的页面需要加上它,否则现在也会出现面板。
- 停靠的面板改用内侧边缘正中、靠页面一侧的抓手调整大小,取代原来贯穿整条边的横条:它不遮挡 DevTools,也只占页面上它看得见的那一小块,外壳模式内外样式一致。浮动窗口仍保留用来移动的横条。
- 外壳模式下新增设备模式,和 Chrome 一样:由 DevTools 排布页面和自己,工具栏上的设备按钮按设备尺寸设置页面(缩放到适合窗口),它的菜单可以截图,停靠和取消停靠时都能用。它只设置页面的尺寸,不改变像素比、UA 和触摸输入。DevTools 面板边缘正中、靠页面一侧有一个抓手,用来调整大小,不遮挡 DevTools,触屏上更大。非外壳模式下,这个按钮仍然是投屏。
- `console.table` 传入对象时,和 Chrome 一样按字段分列显示,不再只有一列「Value」;以数组给出列名时只显示这些列。
- 新增 `Page.captureScreenshot` 和 `Page.getLayoutMetrics`:可以截取视口、指定区域或整个页面,由 snapDOM 绘制,并标注 `approximate: true`,供通过 CDP 操作页面的工具和 agent 使用。

## 0.1.0

基于 Chrome 156(156.0.8078.80)。
Expand Down
49 changes: 36 additions & 13 deletions DEV.md

Large diffs are not rendered by default.

49 changes: 36 additions & 13 deletions DEV.zh-CN.md

Large diffs are not rendered by default.

237 changes: 62 additions & 175 deletions README.md

Large diffs are not rendered by default.

237 changes: 62 additions & 175 deletions README.zh-CN.md

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Design

English | [简体中文](design.zh-CN.md)

How devtools-anywhere works and why it is built this way. For using it, see the [usage guide](usage.md); for working on it, the [development guide](../DEV.md).

## Technology choices

| Part | Choice | Rationale |
|---|---|---|
| CDP implementation | Written in this project, using [chobitsu](https://github.com/liriliri/chobitsu) as a reference | In-page debugging can only collect data through JS APIs, so it has to be written in JS/TS. Owning it lets the probe and the CDP domains share the same hooks (no replay tricks), fixes chobitsu's known issues, and tracks the latest protocol through [`devtools-protocol`](https://github.com/ChromeDevTools/devtools-protocol) types |
| Frontend | Official devtools-frontend, built from source with a small patch set | The build pins an official revision and keeps patches as `.patch` files that are replayed on upgrade, instead of maintaining a long-lived fork branch. The patches only register an own entrypoint (`anywhere_app`); everything else is done without touching upstream code (see [Working on the frontend](../DEV.md#working-on-the-frontend)) |
| Isolation | iframe | Keeps DevTools' own DOM, logs and requests out of the inspected page, and prevents the two global environments from interfering with each other |

This project depends on neither Chii nor chobitsu. Their source is read as a reference for edge cases, but no code is copied; real Chrome's CDP output and the protocol definition are the source of truth.

## Architecture

```
┌────────── Top-level window (shell) ──────────┐
│ ┌────────────────────────────────────────┐ │
│ │ Page iframe │ │
│ │ Probe (first sync script in <head>) │ │
│ │ └─ buffer → CDP runtime (lazy) │ │
│ └───────────────────┬────────────────────┘ │
│ postMessage (origin-checked) │
│ ┌───────────────────┴────────────────────┐ │
│ │ DevTools iframe │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```

### 1. Probe

A synchronous script of about 6.2 KB (min+gzip, including shell mode, auto-mounting and remote debugging) that must be the first script executed in `<head>`.

- Immediately hooks `console`, `window.onerror`, `unhandledrejection`, `fetch`, `XHR` and `WebSocket`, and writes events with timestamps into a buffer. WebSocket frames are kept as given; binary payloads are only converted to base64 by the CDP agent. Until a CDP session mounts, each list (console messages, requests, WebSockets, frames per WebSocket) keeps its last 10,000 entries, a bound for pages that never load one. The session then sets its own limits: the last 1000 console messages, like V8; every request and WebSocket, like Chrome, dropping only the oldest response bodies past the buffer sizes given to `Network.enable` (Chrome's defaults when none are).
- Requests that cannot be hooked (the document, style sheets, scripts, images, fonts, …) are recorded from Resource Timing; a buffered `PerformanceObserver` also delivers those from before the probe ran. They have URL, type, timing, size and (where the browser exposes it) status and MIME type, but no headers; their bodies are loaded again from the HTTP cache on demand. In shell mode with the Service Worker, it adds the request and response headers of same-origin ones. Hooks stay first because they work from the very start, keep call stacks, and need no deployment; a Service Worker only controls pages after it is installed, conflicts with a site's own, and would have to proxy requests to see responses.
- The CDP runtime and the frontend are loaded lazily. The runtime's Runtime and Network domains consume the probe's buffer directly and subscribe to new events, so every API is hooked only once.

Nothing outside a page can observe its output; only a script that runs before the page's own code can capture the earliest output. The full CDP runtime is too large to load synchronously, so only the minimal capture logic goes first.

This approach has been validated: see [experiments/early-capture/RESULTS.md](../experiments/early-capture/RESULTS.md). With a ~1.4 KB probe and chobitsu loaded lazily, every early console call, exception and request reached DevTools, with expandable objects and original timestamps and stacks.

### 2. Shell mode (in-place rewrite)

No standalone `devtools.html` is hosted, and there is no navigation to a blob URL, since blob URLs break on reload and cannot be shared. Instead:

1. With the zero-config probe (`data-devtools-frontend-url`, or the Vite plugin), the top-level page enters shell mode by default on its first load: the probe stores the frontend and bundle URLs in `sessionStorage` itself, before the page's scripts run, so the page is not reloaded. DevTools starts hidden unless `data-open-immediately` is set. Otherwise, the lock button in DevTools' toolbar, "Keep DevTools open across reloads", stores them and reloads the page.
2. The probe, being the first script, sees the config in the top-level window. It writes an unclosed `<plaintext>` so the rest of the page becomes inert text (none of its scripts run), then replaces the document with the shell via `document.open()` / `document.write()`. The shell contains the page iframe (pointing to `location.href`) and `<devtools-anywhere>`, which knows it is the shell's element by the document it is in. (Chrome ignores `document.open()` after `window.stop()`, hence `<plaintext>`.)
3. Inside the iframe, the probe captures as usual and loads the bundle, which connects the page's CDP session to the shell. The shell relays messages between the page and the frontend.
4. When the page reloads or navigates, the new page says hello; the shell tells the frontend the page navigated and re-sends every `enable` command the frontend has sent, so the new page replays its own earliest output. The address bar and title follow the page, through loads, history traversals and same-document navigations (`pushState` where the Navigation API exists), so that a reload or a copied link opens the page shown.
5. DevTools fills the shell and lays out the page and itself, as in Chrome: the page iframe goes where the frontend's page area is, on top of it. Dock side and sizes are DevTools' own, set with its resizer. A grip also sits against the middle of DevTools' edge, on the page's side, like a tab of the panel; it is larger on touch screens. Undocked, DevTools is a window over the page. The toolbar's device button is device mode, as in Chrome, in place of the screencast: the page iframe is sized as the device and scaled to fit, and the toolbar's menu takes screenshots. Undocked, device mode's toolbar is drawn in a page of the frontend's (`device_mode_emulation_frame.html`, as Chrome does) shown under the window.
6. The toolbar's × hides DevTools and stays in shell mode; the page fills the shell at its own size until DevTools is shown again. The lock button again clears the config and reloads the page on its own; the choice is remembered in `localStorage`, and wins over the page's `data-default-shell-mode` from then on, unless the page sets `data-shell-mode-locked`.

The stored URLs are only trusted where the probe script says; otherwise the config is cleared and the page loads normally. That is the probe's origin, but on an npm CDN (jsDelivr, unpkg), where anyone can publish, only the probe's package and the frontend's, at the probe's version: the URLs are compared once parsed (so `..` and `%2e%2e` are resolved), and escaped slashes are refused. Shell mode therefore needs the bundle and the frontend served with the probe, from the page's origin or from such a CDN.

Inside an iframe, `viewport`, `100vh`, `position: fixed`, `window.top` and others may behave differently from a top-level page; the page's HTML is requested twice (by the top-level window, then by the iframe), and a page returned for a form POST is requested again with GET. Turn shell mode off with `data-default-shell-mode="off"` (the Vite plugin's `defaultShellMode: 'off'`), and DevTools is embedded directly in the page. A page that refuses to be framed (`X-Frame-Options`, CSP `frame-ancestors`), or that keeps navigating the shell away, is shown on its own for the rest of the session.

### 3. Injection

| Method | When to use |
|---|---|
| Add a `<script>` manually (must be the first one in `<head>`) | Any page |
| `@devtools-anywhere/vite-plugin`: injects in development only (`vite serve`) | Projects using Vite (a webpack plugin may follow) |
| `devtools-anywhere-service-worker.js`: in shell mode, injects the probe into same-origin pages loaded in the page iframe that do not include it | Secure context (HTTPS or localhost), a scope covering the pages (`Service-Worker-Allowed: /`), and no Service Worker of the site's own |

All methods share the same probe.

## Packaging

Why the packages listed in the [usage guide](usage.md#packages) are split this way:

- The in-page CDP agent and its probe are not a separate package: the probe and the domains share the same hooks and buffers, and only `devtools-anywhere` uses them. They live in `devtools-anywhere` (`src/cdp-agent/`), which rules out version mismatches; the probe is still built as a separate tiny file, `probe.iife.js`.
- The frontend is separate because it is large (about 16 MB) and follows Chrome's release cadence. Its other languages (about 42 MB) are another package, so that only those who need them install them.
- All packages share one version number and are released together, so matching versions always work together.

## References

- [chobitsu](https://github.com/liriliri/chobitsu): reference for the CDP implementation
- [Chii](https://github.com/liriliri/chii): embedded mode in `src/target/DevtoolsFrame.ts`, and its remote debugging server
- [ChromeDevTools/devtools-frontend](https://github.com/ChromeDevTools/devtools-frontend)
- [ChromeDevTools/devtools-protocol](https://github.com/ChromeDevTools/devtools-protocol)
- [Puppeteer](https://github.com/puppeteer/puppeteer): used as the source of truth in conformance tests
- [solid-playground preview](https://github.com/solidjs/solid-playground/blob/master/packages/solid-repl/src/components/preview.tsx)
Loading
Loading