fix(cli): lazy-load @puppeteer/browsers to prevent debug package crash (#1185)

* fix(cli): lazy-load @puppeteer/browsers to prevent debug package crash

Convert the static `import { ... } from "@puppeteer/browsers"` in
browser/manager.ts to dynamic imports inside the async functions that
use them. This eliminates a module-load-time crash when the transitive
`debug` dependency is missing or corrupted.

Previously, every CLI command (including init, lint, docs, help) would
crash with "Cannot find package debug" if the debug package was absent —
even though only browser-related commands need @puppeteer/browsers.

Also add `debug` as a direct dependency so npm/bun always installs it
explicitly rather than relying on transitive resolution.

PostHog data: ~3,955 total-CLI-crash occurrences since May 29.

* fix(cli): simplify isLinuxArm to sync inline check and surface real load error

isLinuxArm() was async only to call detectBrowserPlatform() from
@puppeteer/browsers, but that function just checks process.platform +
process.arch under the hood. Replace with a direct inline check and make
the function sync — no behavioral change, removes an unnecessary async
boundary and an eager load of the package we're trying to lazy-load.

Also surface the real error from loadPuppeteerBrowsers() catch block instead
of hard-coding 'likely missing transitive dependency "debug"' — the actual
cause could be anything (missing package, corrupt install, wrong Node ABI).
This commit is contained in:
Miguel Ángel
2026-06-03 22:53:16 -04:00
committed by GitHub
parent cabd0616ea
commit 8c6faa45b5
4 changed files with 32 additions and 10 deletions
+1
View File
@@ -29,6 +29,7 @@
"adm-zip": "^0.5.16",
"citty": "^0.2.1",
"compare-versions": "^6.1.1",
"debug": "^4.4.0",
"esbuild": "^0.25.12",
"fontkit": "^2.0.4",
"giget": "^3.2.0",
+18 -2
View File
@@ -3,7 +3,20 @@ import { existsSync, readdirSync, rmSync } from "node:fs";
import { basename } from "node:path";
import { homedir } from "node:os";
import { join } from "node:path";
import { Browser, detectBrowserPlatform, getInstalledBrowsers, install } from "@puppeteer/browsers";
type PuppeteerBrowsers = typeof import("@puppeteer/browsers");
async function loadPuppeteerBrowsers(): Promise<PuppeteerBrowsers> {
try {
return await import("@puppeteer/browsers");
} catch (err) {
const cause = err instanceof Error ? err.message : String(err);
throw new Error(
`Failed to load @puppeteer/browsers: ${cause}\n` +
`Fix: run \`npm install\` or \`bun install\` to restore missing packages, then retry.`,
);
}
}
const CHROME_VERSION = "131.0.6778.85";
const CACHE_DIR = join(homedir(), ".cache", "hyperframes", "chrome");
@@ -84,6 +97,7 @@ async function findFromCache(): Promise<BrowserResult | undefined> {
// download-of-last-resort). This is the fallback path: only reached when
// no puppeteer-cache binary exists.
if (existsSync(CACHE_DIR)) {
const { Browser, getInstalledBrowsers } = await loadPuppeteerBrowsers();
const installed = await getInstalledBrowsers({ cacheDir: CACHE_DIR });
const match = installed.find((b) => b.browser === Browser.CHROMEHEADLESSSHELL);
if (match) {
@@ -303,6 +317,8 @@ export async function ensureBrowser(options?: EnsureBrowserOptions): Promise<Bro
const existing = await findBrowser();
if (existing) return existing;
const { Browser, detectBrowserPlatform, install } = await loadPuppeteerBrowsers();
const platform = detectBrowserPlatform();
if (!platform) {
throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`);
@@ -338,7 +354,7 @@ export function clearBrowser(): boolean {
}
export function isLinuxArm(): boolean {
return detectBrowserPlatform() === "linux_arm";
return process.platform === "linux" && process.arch === "arm64";
}
export { CHROME_VERSION, CACHE_DIR };