lib/chromium-launch-error.mjs
Rama main · réplica de lectura del repositorio original.
// lib/chromium-launch-error.mjs — what a failed chromium.launch() says went wrong.
//
// doctor validates Chromium by launching it. It used to discard the launch
// error and report every failure as "not installed → npx playwright install
// chromium". That is right for one cause only: the binary is absent. When the
// binary is there but cannot start (a slim Linux image without the system
// libraries Chromium links against, a sandbox or container that blocks the
// process, a launch timeout), the install command finds the browser already
// downloaded and does nothing, and the user is sent back to rerun it.
//
// This reads the message Playwright throws, so doctor can name the cause and
// prescribe only what fixes it. Pure string work: no Playwright import, so it
// is testable without a browser.
const LAUNCH_PREFIX = /^browserType\.launch:\s*/;
// Linux shared objects (libnss3.so, libgbm.so.1) and Windows DLLs, as Playwright
// lists them in its host-dependency box and as the dynamic loader names them.
const SHARED_LIB = /\b[\w+-]+(?:\.[\w+-]+)*\.(?:so(?:\.\d+)*|dll)\b/gi;
const MAX_DETAIL = 200;
// ANSI escape sequences (colors, dim, bold, etc.) that Playwright includes in
// error messages starting from some version around 1.64.
// eslint-disable-next-line no-control-regex
const ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
function truncate(s) {
return s.length > MAX_DETAIL ? `${s.slice(0, MAX_DETAIL - 1)}…` : s;
}
// The browser's own stderr, as Playwright echoes it under "Browser logs:" as
// `[pid=N][err] ...`. It is the line that says why the process died; the rest
// of the message is the launch command line and Playwright's call log.
function firstBrowserStderrLine(message) {
for (const line of message.split('\n')) {
// Strip ANSI escape codes before matching — Playwright 1.64+ wraps log
// lines in dim codes ([2m...[22m).
const clean = line.replace(ANSI_ESCAPE, '');
const m = clean.match(/^\s*(?:-\s*)?\[pid=\d+\]\[err\]\s*(.*\S)/);
if (m) return m[1];
}
return null;
}
/**
* Classify the error chromium.launch() threw.
*
* @param {unknown} err - The thrown value (an Error, or anything else).
* @returns {{ kind: 'missing-executable', path: string|null }
* | { kind: 'missing-libraries', libraries: string[] }
* | { kind: 'launch-failed', detail: string }}
*/
export function classifyChromiumLaunchError(err) {
const message = String(err?.message ?? err ?? '');
const exe = message.match(/Executable doesn't exist at (\S+)/);
if (exe) return { kind: 'missing-executable', path: exe[1] };
// Linux says "...missing dependencies to run browsers.", Windows says
// "...missing dependencies!" and lists DLLs.
const hostDeps = /Host system is missing dependencies/.test(message);
const loaderLine = message.split('\n').find((line) => /error while loading shared libraries/.test(line));
if (hostDeps || loaderLine) {
// The loader line names one library; the host-dependency box lists them all.
// Neither the launch command line nor the browser path holds a .so name, so
// only those two sources are scanned.
const source = hostDeps ? message.slice(message.indexOf('Host system is missing')) : loaderLine;
const libraries = [...new Set(source.match(SHARED_LIB) || [])];
return { kind: 'missing-libraries', libraries };
}
const stderr = firstBrowserStderrLine(message);
const firstLine = message.replace(LAUNCH_PREFIX, '').split('\n').find((line) => line.trim()) || 'unknown error';
return { kind: 'launch-failed', detail: truncate((stderr || firstLine).trim()) };
}