Skip to content

Commit 00297d4

Browse files
committed
chore: install browser with @puppeteer/browsers
Browser installation relied on a postinstall script from puppeteer package which was unreliable
1 parent e985267 commit 00297d4

File tree

6 files changed

+246
-64
lines changed

6 files changed

+246
-64
lines changed

.changeset/nasty-banks-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"miniflare": minor
3+
---
4+
5+
Browser Rendering for local development now uses @puppeteer/browsers package instead of puppeteer

fixtures/browser-rendering/test/index.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { resolve } from "path";
44
import { afterAll, beforeAll, describe, expect, it } from "vitest";
55
import { runWranglerDev } from "../../shared/src/run-wrangler-long-lived";
66

7-
describe("Local Browser", () => {
7+
describe.sequential("Local Browser", () => {
88
let ip: string,
99
port: number,
1010
stop: (() => Promise<unknown>) | undefined,

packages/miniflare/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"@cloudflare/workers-types": "^4.20250803.0",
6666
"@cloudflare/workflows-shared": "workspace:*",
6767
"@microsoft/api-extractor": "^7.52.8",
68+
"@puppeteer/browsers": "^2.10.6",
6869
"@types/debug": "^4.1.7",
6970
"@types/estree": "^1.0.0",
7071
"@types/glob-to-regexp": "^0.4.1",
@@ -93,7 +94,6 @@
9394
"http-cache-semantics": "^4.1.0",
9495
"kleur": "^4.1.5",
9596
"mime": "^3.0.0",
96-
"npx-import": "^1.1.4",
9797
"postal-mime": "^2.4.3",
9898
"pretty-bytes": "^6.0.0",
9999
"rimraf": "catalog:default",

packages/miniflare/src/index.ts

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import zlib from "zlib";
1414
import { checkMacOSVersion } from "@cloudflare/cli";
1515
import exitHook from "exit-hook";
1616
import { $ as colors$, green } from "kleur/colors";
17-
import { npxImport } from "npx-import";
1817
import stoppable from "stoppable";
1918
import {
2019
Dispatcher,
@@ -48,6 +47,7 @@ import {
4847
HELLO_WORLD_PLUGIN_NAME,
4948
HOST_CAPNP_CONNECT,
5049
KV_PLUGIN_NAME,
50+
launchBrowser,
5151
normaliseDurableObject,
5252
PLUGIN_ENTRIES,
5353
Plugins,
@@ -141,7 +141,7 @@ import type {
141141
Queue,
142142
R2Bucket,
143143
} from "@cloudflare/workers-types/experimental";
144-
import type { ChildProcess } from "child_process";
144+
import type { Process } from "@puppeteer/browsers";
145145

146146
const DEFAULT_HOST = "127.0.0.1";
147147
function getURLSafeHost(host: string) {
@@ -849,7 +849,7 @@ export class Miniflare {
849849
#log: Log;
850850

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

854854
readonly #runtime?: Runtime;
855855
readonly #removeExitHook?: () => void;
@@ -1179,27 +1179,21 @@ export class Miniflare {
11791179
this.#log.logWithLevel(logLevel, message);
11801180
response = new Response(null, { status: 204 });
11811181
} else if (url.pathname === "/browser/launch") {
1182-
// 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
1183-
const puppeteer = await npxImport(
1184-
1185-
this.#log.warn.bind(this.#log)
1186-
);
1187-
1188-
// @ts-expect-error Puppeteer is dynamically installed, and so doesn't have types available
1189-
const browser = await puppeteer.launch({
1190-
headless: "old",
1191-
// workaround for CI environments, to avoid sandboxing issues
1192-
args: process.env.CI ? ["--no-sandbox"] : [],
1182+
const { browserProcess, wsEndpoint } = await launchBrowser({
1183+
log: this.#log,
1184+
tmpPath: this.#tmpPath,
1185+
});
1186+
browserProcess.nodeProcess.on("exit", () => {
1187+
this.#browserProcesses.delete(wsEndpoint);
11931188
});
1194-
const wsEndpoint = browser.wsEndpoint();
1195-
this.#browserProcesses.set(wsEndpoint, browser.process());
1189+
this.#browserProcesses.set(wsEndpoint, browserProcess);
11961190
response = new Response(wsEndpoint);
11971191
} else if (url.pathname === "/browser/status") {
11981192
const wsEndpoint = url.searchParams.get("wsEndpoint");
11991193
assert(wsEndpoint !== null, "Missing wsEndpoint query parameter");
12001194
const process = this.#browserProcesses.get(wsEndpoint);
12011195
const status = {
1202-
stopped: !process || process.exitCode !== null,
1196+
stopped: !process,
12031197
};
12041198
response = new Response(JSON.stringify(status), {
12051199
headers: { "Content-Type": "application/json" },
@@ -1817,11 +1811,8 @@ export class Miniflare {
18171811
}
18181812

18191813
async #assembleAndUpdateConfig() {
1820-
for (const [wsEndpoint, process] of this.#browserProcesses.entries()) {
1821-
// .close() isn't enough
1822-
process.kill("SIGKILL");
1823-
this.#browserProcesses.delete(wsEndpoint);
1824-
}
1814+
await this.#closeBrowserProcesses();
1815+
18251816
// This function must be run with `#runtimeMutex` held
18261817
const initial = !this.#runtimeEntryURL;
18271818
assert(this.#runtime !== undefined);
@@ -1988,6 +1979,18 @@ export class Miniflare {
19881979
}
19891980
}
19901981

1982+
async #closeBrowserProcesses() {
1983+
await Promise.all(
1984+
Array.from(this.#browserProcesses.values()).map((process) =>
1985+
process.close()
1986+
)
1987+
);
1988+
assert(
1989+
this.#browserProcesses.size === 0,
1990+
"Not all browser processes were closed"
1991+
);
1992+
}
1993+
19911994
async #waitForReady(disposing = false) {
19921995
// If `#init()` threw, we'd like to propagate the error here, so `await` it.
19931996
// Note we can't use `async`/`await` with getters. We'd also like to wait
@@ -2528,11 +2531,7 @@ export class Miniflare {
25282531
try {
25292532
await this.#waitForReady(/* disposing */ true);
25302533
} finally {
2531-
for (const [wsEndpoint, process] of this.#browserProcesses.entries()) {
2532-
// .close() isn't enough
2533-
process.kill("SIGKILL");
2534-
this.#browserProcesses.delete(wsEndpoint);
2535-
}
2534+
await this.#closeBrowserProcesses();
25362535

25372536
// Remove exit hook, we're cleaning up what they would've cleaned up now
25382537
this.#removeExitHook?.();

packages/miniflare/src/plugins/browser-rendering/index.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1+
import fs from "fs";
2+
import os from "os";
3+
import path from "path";
4+
import { brandColor } from "@cloudflare/cli/colors";
5+
import { spinner } from "@cloudflare/cli/interactive";
6+
import {
7+
Browser,
8+
CDP_WEBSOCKET_ENDPOINT_REGEX,
9+
detectBrowserPlatform,
10+
install,
11+
launch,
12+
resolveBuildId,
13+
} from "@puppeteer/browsers";
14+
import { dim } from "kleur/colors";
115
import BROWSER_RENDERING_WORKER from "worker:browser-rendering/binding";
216
import { z } from "zod";
317
import { kVoid } from "../../runtime";
18+
import { Log } from "../../shared";
419
import {
520
getUserBindingServiceName,
621
Plugin,
@@ -99,3 +114,117 @@ export const BROWSER_RENDERING_PLUGIN: Plugin<
99114
];
100115
},
101116
};
117+
118+
export async function launchBrowser({
119+
log,
120+
tmpPath,
121+
}: {
122+
log: Log;
123+
tmpPath: string;
124+
}) {
125+
const platform = detectBrowserPlatform();
126+
if (!platform) {
127+
throw new Error("The current platform is not supported.");
128+
}
129+
const browser = Browser.CHROME;
130+
const version = "126.0.6478.182";
131+
const sessionId = crypto.randomUUID();
132+
133+
const s = spinner();
134+
let startedDownloading = false;
135+
136+
const { executablePath } = await install({
137+
browser,
138+
platform,
139+
cacheDir: path.join(os.homedir(), ".cache", "miniflare"),
140+
buildId: await resolveBuildId(browser, platform, version),
141+
downloadProgressCallback: (downloadedBytes, totalBytes) => {
142+
if (!startedDownloading) {
143+
s.start(`Downloading browser...`);
144+
startedDownloading = true;
145+
}
146+
const progress = Math.round((downloadedBytes / totalBytes) * 100);
147+
s.update(`Downloading browser... ${progress}%`);
148+
},
149+
});
150+
151+
if (startedDownloading) {
152+
s.stop(`${brandColor("downloaded")} ${dim(`browser`)}`);
153+
log.debug(`${browser} ${version} available at ${executablePath}`);
154+
}
155+
156+
const tempUserData = path.join(
157+
tmpPath,
158+
"browser-rendering",
159+
`profile-${sessionId}`
160+
);
161+
await fs.promises.mkdir(tempUserData, { recursive: true });
162+
163+
// https://github.com/puppeteer/puppeteer/blob/44516936ad4a878f9a89e835a9fa7b04360d6fb9/packages/puppeteer-core/src/node/ChromeLauncher.ts#L156
164+
const disabledFeatures = [
165+
"Translate",
166+
// AcceptCHFrame disabled because of crbug.com/1348106.
167+
"AcceptCHFrame",
168+
"MediaRouter",
169+
"OptimizationHints",
170+
"ProcessPerSiteUpToMainFrameThreshold",
171+
"IsolateSandboxedIframes",
172+
];
173+
const args = [
174+
"--allow-pre-commit-input",
175+
"--disable-background-networking",
176+
"--disable-background-timer-throttling",
177+
"--disable-backgrounding-occluded-windows",
178+
"--disable-breakpad",
179+
"--disable-client-side-phishing-detection",
180+
"--disable-component-extensions-with-background-pages",
181+
"--disable-crash-reporter", // No crash reporting in CfT.
182+
"--disable-default-apps",
183+
"--disable-dev-shm-usage",
184+
"--disable-hang-monitor",
185+
"--disable-infobars",
186+
"--disable-ipc-flooding-protection",
187+
"--disable-popup-blocking",
188+
"--disable-prompt-on-repost",
189+
"--disable-renderer-backgrounding",
190+
"--disable-search-engine-choice-screen",
191+
"--disable-sync",
192+
"--enable-automation",
193+
"--export-tagged-pdf",
194+
"--force-color-profile=srgb",
195+
"--generate-pdf-document-outline",
196+
"--metrics-recording-only",
197+
"--no-first-run",
198+
"--password-store=basic",
199+
"--use-mock-keychain",
200+
`--disable-features=${disabledFeatures.join(",")}`,
201+
"--headless=new",
202+
"--hide-scrollbars",
203+
"--mute-audio",
204+
"--disable-extensions",
205+
"about:blank",
206+
"--remote-debugging-port=0",
207+
`--user-data-dir=${tempUserData}`,
208+
];
209+
210+
const browserProcess = launch({
211+
executablePath,
212+
args: process.env.CI ? [...args, "--no-sandbox"] : args,
213+
handleSIGTERM: false,
214+
dumpio: false,
215+
pipe: false,
216+
onExit: async () => {
217+
await fs.promises
218+
.rm(tempUserData, { recursive: true, force: true })
219+
.catch((e) => {
220+
log.debug(
221+
`Unable to remove Chrome user data directory: ${String(e)}`
222+
);
223+
});
224+
},
225+
});
226+
const wsEndpoint = await browserProcess.waitForLineOutput(
227+
CDP_WEBSOCKET_ENDPOINT_REGEX
228+
);
229+
return { sessionId, browserProcess, wsEndpoint };
230+
}

0 commit comments

Comments
 (0)