diff --git a/.changeset/headful-browser-rendering.md b/.changeset/headful-browser-rendering.md new file mode 100644 index 0000000000..0e596900ce --- /dev/null +++ b/.changeset/headful-browser-rendering.md @@ -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: + +```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. diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index 8b26ae62ce..7fce3ca494 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -40,6 +40,7 @@ import { Response, } from "./http"; import { + BROWSER_RENDERING_PLUGIN_NAME, D1_PLUGIN_NAME, DURABLE_OBJECTS_PLUGIN_NAME, DurableObjectClassNames, @@ -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: @@ -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); diff --git a/packages/miniflare/src/plugins/browser-rendering/index.ts b/packages/miniflare/src/plugins/browser-rendering/index.ts index c72f770c42..5a014c806e 100644 --- a/packages/miniflare/src/plugins/browser-rendering/index.ts +++ b/packages/miniflare/src/plugins/browser-rendering/index.ts @@ -31,6 +31,7 @@ const BrowserRenderingSchema = z.object({ remoteProxyConnectionString: z .custom() .optional(), + headful: z.boolean().optional(), }); export const BrowserRenderingOptionsSchema = z.object({ @@ -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; }) { @@ -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", diff --git a/packages/vite-plugin-cloudflare/src/miniflare-options.ts b/packages/vite-plugin-cloudflare/src/miniflare-options.ts index 3dcd0a595b..85b5505b90 100644 --- a/packages/vite-plugin-cloudflare/src/miniflare-options.ts +++ b/packages/vite-plugin-cloudflare/src/miniflare-options.ts @@ -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, @@ -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}";`, diff --git a/packages/workers-utils/src/environment-variables/factory.ts b/packages/workers-utils/src/environment-variables/factory.ts index 4e6ae43b7d..a45a52506e 100644 --- a/packages/workers-utils/src/environment-variables/factory.ts +++ b/packages/workers-utils/src/environment-variables/factory.ts @@ -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) diff --git a/packages/workers-utils/src/environment-variables/misc-variables.ts b/packages/workers-utils/src/environment-variables/misc-variables.ts index 3339bcb36a..9a7322205a 100644 --- a/packages/workers-utils/src/environment-variables/misc-variables.ts +++ b/packages/workers-utils/src/environment-variables/misc-variables.ts @@ -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. diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index c67c8cc0bd..26efdda933 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -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"; @@ -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);