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
5 changes: 5 additions & 0 deletions .changeset/nasty-banks-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"miniflare": minor
---

Browser Rendering for local development now uses @puppeteer/browsers package instead of puppeteer
2 changes: 1 addition & 1 deletion fixtures/browser-rendering/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { resolve } from "path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived";

describe("Local Browser", () => {
describe.sequential("Local Browser", () => {
let ip: string,
port: number,
stop: (() => Promise<unknown>) | undefined,
Expand Down
2 changes: 1 addition & 1 deletion packages/miniflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@cloudflare/workers-types": "^4.20250813.0",
"@cloudflare/workflows-shared": "workspace:*",
"@microsoft/api-extractor": "^7.52.8",
"@puppeteer/browsers": "^2.10.6",
"@types/debug": "^4.1.7",
"@types/estree": "^1.0.0",
"@types/glob-to-regexp": "^0.4.1",
Expand Down Expand Up @@ -93,7 +94,6 @@
"http-cache-semantics": "^4.1.0",
"kleur": "^4.1.5",
"mime": "^3.0.0",
"npx-import": "^1.1.4",
"postal-mime": "^2.4.3",
"pretty-bytes": "^6.0.0",
"rimraf": "catalog:default",
Expand Down
60 changes: 33 additions & 27 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import zlib from "zlib";
import { checkMacOSVersion } from "@cloudflare/cli";
import exitHook from "exit-hook";
import { $ as colors$, green } from "kleur/colors";
import { npxImport } from "npx-import";
import stoppable from "stoppable";
import {
Dispatcher,
Expand Down Expand Up @@ -48,6 +47,7 @@ import {
HELLO_WORLD_PLUGIN_NAME,
HOST_CAPNP_CONNECT,
KV_PLUGIN_NAME,
launchBrowser,
normaliseDurableObject,
PLUGIN_ENTRIES,
Plugins,
Expand Down Expand Up @@ -141,7 +141,7 @@ import type {
Queue,
R2Bucket,
} from "@cloudflare/workers-types/experimental";
import type { ChildProcess } from "child_process";
import type { Process } from "@puppeteer/browsers";

const DEFAULT_HOST = "127.0.0.1";
function getURLSafeHost(host: string) {
Expand Down Expand Up @@ -849,7 +849,7 @@ export class Miniflare {
#log: Log;

// key is the browser wsEndpoint, value is the browser process
#browserProcesses: Map<string, ChildProcess> = new Map();
#browserProcesses: Map<string, Process> = new Map();

readonly #runtime?: Runtime;
readonly #removeExitHook?: () => void;
Expand Down Expand Up @@ -1179,27 +1179,28 @@ export class Miniflare {
this.#log.logWithLevel(logLevel, message);
response = new Response(null, { status: 204 });
} else if (url.pathname === "/browser/launch") {
// Version should be kept in sync with the supported version at https://github.com/cloudflare/puppeteer?tab=readme-ov-file#workers-version-of-puppeteer-core
const puppeteer = await npxImport(
"[email protected]",
this.#log.warn.bind(this.#log)
);

// @ts-expect-error Puppeteer is dynamically installed, and so doesn't have types available
const browser = await puppeteer.launch({
headless: "old",
// workaround for CI environments, to avoid sandboxing issues
args: process.env.CI ? ["--no-sandbox"] : [],
const { browserProcess, wsEndpoint } = await launchBrowser({
// Puppeteer v22.8.2 supported chrome version:
// https://pptr.dev/supported-browsers#supported-browser-version-list
//
// It should match the supported chrome version for the upstream puppeteer
// version from which @cloudflare/puppeteer branched off, which is specified in:
// https://github.com/cloudflare/puppeteer/tree/v1.0.2?tab=readme-ov-file#workers-version-of-puppeteer-core
browserVersion: "124.0.6367.207",
log: this.#log,
tmpPath: this.#tmpPath,
});
browserProcess.nodeProcess.on("exit", () => {
this.#browserProcesses.delete(wsEndpoint);
});
const wsEndpoint = browser.wsEndpoint();
this.#browserProcesses.set(wsEndpoint, browser.process());
this.#browserProcesses.set(wsEndpoint, browserProcess);
response = new Response(wsEndpoint);
} else if (url.pathname === "/browser/status") {
const wsEndpoint = url.searchParams.get("wsEndpoint");
assert(wsEndpoint !== null, "Missing wsEndpoint query parameter");
const process = this.#browserProcesses.get(wsEndpoint);
const status = {
stopped: !process || process.exitCode !== null,
stopped: !process,
};
response = new Response(JSON.stringify(status), {
headers: { "Content-Type": "application/json" },
Expand Down Expand Up @@ -1817,11 +1818,8 @@ export class Miniflare {
}

async #assembleAndUpdateConfig() {
for (const [wsEndpoint, process] of this.#browserProcesses.entries()) {
// .close() isn't enough
process.kill("SIGKILL");
this.#browserProcesses.delete(wsEndpoint);
}
await this.#closeBrowserProcesses();

// This function must be run with `#runtimeMutex` held
const initial = !this.#runtimeEntryURL;
assert(this.#runtime !== undefined);
Expand Down Expand Up @@ -1988,6 +1986,18 @@ export class Miniflare {
}
}

async #closeBrowserProcesses() {
await Promise.all(
Array.from(this.#browserProcesses.values()).map((process) =>
process.close()
)
);
assert(
this.#browserProcesses.size === 0,
"Not all browser processes were closed"
);
}

async #waitForReady(disposing = false) {
// If `#init()` threw, we'd like to propagate the error here, so `await` it.
// Note we can't use `async`/`await` with getters. We'd also like to wait
Expand Down Expand Up @@ -2528,11 +2538,7 @@ export class Miniflare {
try {
await this.#waitForReady(/* disposing */ true);
} finally {
for (const [wsEndpoint, process] of this.#browserProcesses.entries()) {
// .close() isn't enough
process.kill("SIGKILL");
this.#browserProcesses.delete(wsEndpoint);
}
await this.#closeBrowserProcesses();

// Remove exit hook, we're cleaning up what they would've cleaned up now
this.#removeExitHook?.();
Expand Down
130 changes: 130 additions & 0 deletions packages/miniflare/src/plugins/browser-rendering/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import fs from "fs";
import path from "path";
import { brandColor } from "@cloudflare/cli/colors";
import { spinner } from "@cloudflare/cli/interactive";
import {
Browser,
CDP_WEBSOCKET_ENDPOINT_REGEX,
detectBrowserPlatform,
install,
launch,
resolveBuildId,
} from "@puppeteer/browsers";
import { dim } from "kleur/colors";
import BROWSER_RENDERING_WORKER from "worker:browser-rendering/binding";
import { z } from "zod";
import { kVoid } from "../../runtime";
import { Log } from "../../shared";
import { getGlobalWranglerCachePath } from "../../shared/wrangler";
import {
getUserBindingServiceName,
Plugin,
Expand Down Expand Up @@ -99,3 +114,118 @@ export const BROWSER_RENDERING_PLUGIN: Plugin<
];
},
};

export async function launchBrowser({
browserVersion,
log,
tmpPath,
}: {
browserVersion: string;
log: Log;
tmpPath: string;
}) {
const platform = detectBrowserPlatform();
if (!platform) {
throw new Error("The current platform is not supported.");
}
const browser = Browser.CHROME;
const sessionId = crypto.randomUUID();

const s = spinner();
let startedDownloading = false;

const { executablePath } = await install({
browser,
platform,
cacheDir: getGlobalWranglerCachePath(),
buildId: await resolveBuildId(browser, platform, browserVersion),
downloadProgressCallback: (downloadedBytes, totalBytes) => {
if (!startedDownloading) {
s.start(`Downloading browser...`);
startedDownloading = true;
}
const progress = Math.round((downloadedBytes / totalBytes) * 100);
s.update(`Downloading browser... ${progress}%`);
},
});

if (startedDownloading) {
s.stop(`${brandColor("downloaded")} ${dim(`browser`)}`);
log.debug(`${browser} ${browserVersion} available at ${executablePath}`);
}

const tempUserData = path.join(
tmpPath,
"browser-rendering",
`profile-${sessionId}`
);
await fs.promises.mkdir(tempUserData, { recursive: true });

// https://github.com/puppeteer/puppeteer/blob/44516936ad4a878f9a89e835a9fa7b04360d6fb9/packages/puppeteer-core/src/node/ChromeLauncher.ts#L156
const disabledFeatures = [
"Translate",
// AcceptCHFrame disabled because of crbug.com/1348106.
"AcceptCHFrame",
"MediaRouter",
"OptimizationHints",
"ProcessPerSiteUpToMainFrameThreshold",
"IsolateSandboxedIframes",
];
const args = [
"--allow-pre-commit-input",
"--disable-background-networking",
"--disable-background-timer-throttling",
"--disable-backgrounding-occluded-windows",
"--disable-breakpad",
"--disable-client-side-phishing-detection",
"--disable-component-extensions-with-background-pages",
"--disable-crash-reporter", // No crash reporting in CfT.
"--disable-default-apps",
"--disable-dev-shm-usage",
"--disable-hang-monitor",
"--disable-infobars",
"--disable-ipc-flooding-protection",
"--disable-popup-blocking",
"--disable-prompt-on-repost",
"--disable-renderer-backgrounding",
"--disable-search-engine-choice-screen",
"--disable-sync",
"--enable-automation",
"--export-tagged-pdf",
"--force-color-profile=srgb",
"--generate-pdf-document-outline",
"--metrics-recording-only",
"--no-first-run",
"--password-store=basic",
"--use-mock-keychain",
`--disable-features=${disabledFeatures.join(",")}`,
"--headless=new",
"--hide-scrollbars",
"--mute-audio",
"--disable-extensions",
"about:blank",
"--remote-debugging-port=0",
`--user-data-dir=${tempUserData}`,
];

const browserProcess = launch({
executablePath,
args: process.env.CI ? [...args, "--no-sandbox"] : args,
handleSIGTERM: false,
dumpio: false,
pipe: false,
onExit: async () => {
await fs.promises
.rm(tempUserData, { recursive: true, force: true })
.catch((e) => {
log.debug(
`Unable to remove Chrome user data directory: ${String(e)}`
);
});
},
});
const wsEndpoint = await browserProcess.waitForLineOutput(
CDP_WEBSOCKET_ENDPOINT_REGEX
);
return { sessionId, browserProcess, wsEndpoint };
}
4 changes: 4 additions & 0 deletions packages/miniflare/src/shared/wrangler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ export function getGlobalWranglerConfigPath() {
return configDir;
}
}

export function getGlobalWranglerCachePath() {
return xdgAppPaths(".wrangler").cache();
}
Loading
Loading