Skip to content
Merged
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/headful-browser-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"wrangler": minor
"miniflare": minor
"@cloudflare/vite-plugin": minor
"@cloudflare/workers-utils": minor
---

Add experimental headful browser rendering support for local development

> **Experimental:** This feature may be removed or changed without notice.

When developing locally with the Browser Rendering API, you can enable headful (visible) mode via the `X_BROWSER_HEADFUL` environment variable to see the browser while debugging:
Comment thread
petebacondarwin marked this conversation as resolved.

```sh
X_BROWSER_HEADFUL=true wrangler dev
X_BROWSER_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.
5 changes: 5 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 @@ -1473,6 +1474,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 @@ -1484,6 +1488,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 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
12 changes: 11 additions & 1 deletion packages/vite-plugin-cloudflare/src/miniflare-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import {
generateContainerBuildId,
resolveDockerHost,
} from "@cloudflare/containers-shared";
import { getLocalExplorerEnabledFromEnv } from "@cloudflare/workers-utils";
import {
getBrowserRenderingHeadfulFromEnv,
getLocalExplorerEnabledFromEnv,
} from "@cloudflare/workers-utils";
import {
getDefaultDevRegistryPath,
kUnsafeEphemeralUniqueKey,
Expand Down Expand Up @@ -314,6 +317,13 @@ export async function getDevMiniflareOptions(

const { externalWorkers, workerOptions } = miniflareWorkerOptions;

if (
workerOptions.browserRendering &&
getBrowserRenderingHeadfulFromEnv()
) {
workerOptions.browserRendering.headful = true;
}

const wrappers = [
`import { createWorkerEntrypointWrapper, createDurableObjectWrapper, createWorkflowEntrypointWrapper } from "${RUNNER_PATH}";`,
`export { __VITE_RUNNER_OBJECT__ } from "${RUNNER_PATH}";`,
Expand Down
2 changes: 2 additions & 0 deletions packages/workers-utils/src/environment-variables/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ type VariableNames =

/** Enable the local explorer UI at /cdn-cgi/explorer (experimental, default: false). */
| "X_LOCAL_EXPLORER"
/** Open the browser in headful (visible) mode when using the Browser Rendering API in local dev (default: false). */
| "X_BROWSER_HEADFUL"

// ## CI-specific Variables (Internal Use)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,26 @@ export const getLocalExplorerEnabledFromEnv =
defaultValue: true,
});

/**
* `X_BROWSER_HEADFUL` opens the browser in headful (visible) mode when using the
* Browser Rendering API in local development.
*
* Set to "true" to enable:
*
* ```sh
* X_BROWSER_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 due to how Playwright handles
* browser contexts via CDP.
*/
export const getBrowserRenderingHeadfulFromEnv =
getBooleanEnvironmentVariableFactory({
variableName: "X_BROWSER_HEADFUL",
defaultValue: false,
});

/**
* `CLOUDFLARE_CF_FETCH_ENABLED` controls whether Miniflare fetches the `cf.json` file
* containing request.cf properties from workers.cloudflare.com.
Expand Down
4 changes: 4 additions & 0 deletions packages/wrangler/src/dev/miniflare/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from "node:assert";
import path from "node:path";
import { getDevContainerImageName } from "@cloudflare/containers-shared";
import {
getBrowserRenderingHeadfulFromEnv,
getLocalExplorerEnabledFromEnv,
UserError,
} from "@cloudflare/workers-utils";
Expand Down Expand Up @@ -982,6 +983,9 @@ export async function buildMiniflareOptions(
config,
remoteProxyConnectionString
);
if (bindingOptions.browserRendering && getBrowserRenderingHeadfulFromEnv()) {
bindingOptions.browserRendering.headful = true;
}
const sitesOptions = buildSitesOptions(config);
const defaultPersistRoot = getDefaultPersistRoot(config.localPersistencePath);
const assetOptions = buildAssetOptions(config);
Expand Down
Loading