Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions .changeset/browser-rendering-devtools-endpoints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"miniflare": minor
---

Add local simulation for browser rendering devtools endpoints

The local browser rendering binding now implements the full set of devtools endpoints, matching the remote Browser Rendering API:

- `GET /v1/limits` — returns local concurrency defaults
- `GET /v1/history` — returns empty array (no persistence in local dev)
- `GET /v1/devtools/session` and `GET /v1/devtools/session/:id` — list and inspect active sessions
- `GET /v1/devtools/browser/:id/json/version`, `/json/list`, `/json`, `/json/protocol` — proxy Chrome's DevTools JSON endpoints
- `PUT /v1/devtools/browser/:id/json/new` — open a new tab
- `GET /v1/devtools/browser/:id/json/activate/:target` and `/json/close/:target` — activate or close a tab
- `GET /v1/devtools/browser/:id/page/:target` — WebSocket connection to a page target
- `GET /v1/devtools/browser/:id` — WebSocket connection to an existing browser session with `cf-browser-session-id` header
- `DELETE /v1/devtools/browser/:id` — close a browser session
- `POST /v1/devtools/browser` — acquire a new session, returning `{ sessionId }`
- `GET /v1/devtools/browser` — acquire a new session and connect via WebSocket in one step, returning `cf-browser-session-id` header
36 changes: 36 additions & 0 deletions .changeset/headful-browser-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
"wrangler": minor
"miniflare": minor
"@cloudflare/vite-plugin": minor
"@cloudflare/workers-utils": minor
---

Add headful browser rendering support for local development

When developing locally with the Browser Rendering API, you can now enable headful (visible) mode to see the browser while debugging.

**`wrangler dev`:**

```
wrangler dev --headful
```

**`@cloudflare/vite-plugin`** — via plugin config:

```ts
// vite.config.ts
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
plugins: [cloudflare({ headful: true })],
});
```

Or via environment variable (no config change needed):

```sh
BROWSER_RENDERING_HEADFUL=true vite dev
```

**Note:** when using `@cloudflare/playwright`, two Chrome windows may appear — the initial blank page and the one created by `browser.newPage()`.
This is expected behavior due to how Playwright handles browser contexts via CDP.
15 changes: 15 additions & 0 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
Response,
} from "./http";
import {
BROWSER_RENDERING_PLUGIN_NAME,
D1_PLUGIN_NAME,
DURABLE_OBJECTS_PLUGIN_NAME,
DurableObjectClassNames,
Expand Down Expand Up @@ -1310,6 +1311,9 @@ export class Miniflare {
this.#log.logWithLevel(logLevel, message);
response = new Response(null, { status: 204 });
} else if (url.pathname === "/browser/launch") {
const headful = this.#workerOpts.some(
(w) => w[BROWSER_RENDERING_PLUGIN_NAME].browserRendering?.headful
);
const { sessionId, browserProcess, startTime, wsEndpoint } =
await launchBrowser({
// Puppeteer v22.13.1 supported chrome version:
Expand All @@ -1321,6 +1325,7 @@ export class Miniflare {
browserVersion: "126.0.6478.182",
log: this.#log,
tmpPath: this.#tmpPath,
headful,
});
browserProcess.nodeProcess.on("exit", () => {
this.#browserProcesses.delete(sessionId);
Expand All @@ -1332,6 +1337,16 @@ export class Miniflare {
assert(sessionId !== null, "Missing sessionId query parameter");
const process = this.#browserProcesses.get(sessionId);
response = new Response(null, { status: process ? 200 : 410 });
} else if (url.pathname === "/browser/close") {
const sessionId = url.searchParams.get("sessionId");
assert(sessionId !== null, "Missing sessionId query parameter");
const process = this.#browserProcesses.get(sessionId);
if (!process) {
response = new Response("Session not found", { status: 404 });
} else {
await process.close();
response = new Response(null, { status: 200 });
}
} else if (url.pathname === "/browser/sessionIds") {
const sessionIds = this.#browserProcesses.keys();
response = Response.json(Array.from(sessionIds));
Expand Down
7 changes: 4 additions & 3 deletions packages/miniflare/src/plugins/browser-rendering/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const BrowserRenderingSchema = z.object({
remoteProxyConnectionString: z
.custom<RemoteProxyConnectionString>()
.optional(),
headful: z.boolean().optional(),
});

export const BrowserRenderingOptionsSchema = z.object({
Expand Down Expand Up @@ -118,10 +119,12 @@ export const BROWSER_RENDERING_PLUGIN: Plugin<

export async function launchBrowser({
browserVersion,
headful,
log,
tmpPath,
}: {
browserVersion: string;
headful?: boolean;
log: Log;
tmpPath: string;
}) {
Expand Down Expand Up @@ -200,9 +203,7 @@ export async function launchBrowser({
"--password-store=basic",
"--use-mock-keychain",
`--disable-features=${disabledFeatures.join(",")}`,
"--headless=new",
"--hide-scrollbars",
"--mute-audio",
...(headful ? [] : ["--headless=new", "--hide-scrollbars", "--mute-audio"]),
"--disable-extensions",
"about:blank",
"--remote-debugging-port=0",
Expand Down
Loading
Loading