From 7fe2f0fb49003c01c46804f5ac5b4d29b9b586d8 Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Tue, 16 Jun 2026 23:54:13 -0300 Subject: [PATCH] fix(install): auto-install Playwright Chromium + friendlier missing-browser error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: a fresh `npm install -g @decocms/parity` followed by `parity run` failed with `browserType.launch: Executable doesn't exist at .../chrome-headless-shell` plus Playwright's ASCII banner. Two fixes: 1. **postinstall script** runs `npx playwright install chromium` automatically after install (one-time, ~140 MB). Skippable via `PARITY_SKIP_PLAYWRIGHT_INSTALL=1` for CI / Docker / monorepos that manage browsers separately. Failures (offline, corp proxy) downgrade to a warning so the global install itself still completes. 2. **launchBrowser catches the missing-browser error** and throws a new Error with a single clear instruction (`npx playwright install chromium`) instead of letting Playwright's stack trace + banner reach the user. Original error preserved on `.cause`. Also documented the known cosmetic peer-dep warning for zod (SDK wants zod 4, parity uses zod 3 — they coexist via npm nesting, harmless). Bumps 0.11.7. --- CHANGELOG.md | 12 +++++++++++ README.md | 6 +++++- package.json | 7 +++--- scripts/postinstall.js | 48 ++++++++++++++++++++++++++++++++++++++++++ src/engine/browser.ts | 36 ++++++++++++++++++++++++++----- 5 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 scripts/postinstall.js diff --git a/CHANGELOG.md b/CHANGELOG.md index fb430ca..01ce6ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [0.11.7](https://github.com/decocms/parity/compare/v0.11.6...v0.11.7) (2026-06-17) + +### Fixed + +* **First-run UX after `npm install -g`.** Two papercuts: + 1. **Playwright Chromium wasn't auto-installed.** A fresh global install left users with `browserType.launch: Executable doesn't exist at ...` plus a stack trace on the first `parity run`. Now there's a `postinstall` script that runs `npx playwright install chromium` automatically (one-time, ~140 MB). Skippable via `PARITY_SKIP_PLAYWRIGHT_INSTALL=1` for CI / Docker / monorepos that manage browsers separately. Failures during postinstall (offline, corp proxy) are downgraded to a warning so the global install itself still completes. + 2. **`launchBrowser` now catches the missing-browser error** and prints a single clear instruction (`npx playwright install chromium`) instead of Playwright's ASCII banner + stack trace. Original error is preserved on `.cause` for debugging. + +### Known cosmetic warning + +The `@anthropic-ai/claude-agent-sdk` peer-dep on `zod@^4.0.0` clashes with parity's `zod@^3.x`. The warning is harmless — npm nests the two versions and everything works. A migration to zod 4 is tracked separately (lots of schema API differences). + ## [0.11.6](https://github.com/decocms/parity/compare/v0.11.5...v0.11.6) (2026-06-17) ### Added diff --git a/README.md b/README.md index 25acd04..863e69e 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,15 @@ Three use cases drive the design: ## Quickstart ```bash -# Install +# Install — auto-installs Playwright Chromium on postinstall (~140 MB, one-time) npm install -g @decocms/parity # or run without install npx @decocms/parity run --prod ... --cand ... +# In CI / behind a corp proxy? Skip the auto-install and do it later: +# PARITY_SKIP_PLAYWRIGHT_INSTALL=1 npm install -g @decocms/parity +# npx playwright install chromium + # First-time smoke run (~30s, no LLM needed) parity run --prod https://oldsite.com --cand https://newsite.example.dev --preset smoke --open diff --git a/package.json b/package.json index 899f770..163e4f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@decocms/parity", - "version": "0.11.6", + "version": "0.11.7", "description": "E2E parity validator for site migrations. Compares prod vs cand and reports UI, functional, SEO, visual, and Web Vitals deltas with an LLM-ranked HTML report.", "type": "module", "license": "MIT", @@ -32,7 +32,7 @@ "bin": { "parity": "dist/cli.js" }, - "files": ["dist", "README.md", "AGENTS.md", "CHANGELOG.md", "LICENSE"], + "files": ["dist", "scripts/postinstall.js", "README.md", "AGENTS.md", "CHANGELOG.md", "LICENSE"], "scripts": { "dev": "bun run --hot src/cli.ts", "build": "bun build src/cli.ts --target=node --outfile=dist/cli.js --packages=external && bun run scripts/postbuild.ts", @@ -44,7 +44,8 @@ "deadcode": "knip", "lint": "biome lint .", "fmt": "biome format --write .", - "prepublishOnly": "bun run check && bun run build" + "prepublishOnly": "bun run check && bun run build", + "postinstall": "node scripts/postinstall.js" }, "engines": { "node": ">=20", diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 0000000..04e2408 --- /dev/null +++ b/scripts/postinstall.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node +/** + * Best-effort Playwright Chromium install on first `npm install` of + * `@decocms/parity`. Skipped when: + * - `PARITY_SKIP_PLAYWRIGHT_INSTALL=1` is set (CI / Docker / monorepos + * that manage the browser separately). + * - The binary is already present. + * + * Failures are downgraded to a warning so a `npm install -g` that + * happens to be offline / behind a corp proxy doesn't block the whole + * install. The runtime path in `engine/browser.ts:launchBrowser` catches + * the missing-browser case and tells the user exactly what to run. + */ + +if (process.env.PARITY_SKIP_PLAYWRIGHT_INSTALL === "1") { + console.log("[parity] PARITY_SKIP_PLAYWRIGHT_INSTALL=1 set — skipping Chromium install"); + process.exit(0); +} + +const { spawnSync } = require("node:child_process"); + +// Probe Playwright to find out if Chromium is already extracted. The cheap +// way: try to resolve the binary via `npx playwright install --dry-run`. +// `--dry-run` exits 0 when everything is already installed and non-zero +// when downloads are needed. We don't actually rely on the exit code; we +// always attempt `install chromium` (idempotent) and let Playwright print +// "already up to date" when there's nothing to do. +try { + console.log("[parity] Installing Playwright Chromium (one-time, ~140 MB)…"); + const result = spawnSync("npx", ["--yes", "playwright", "install", "chromium"], { + stdio: "inherit", + env: process.env, + }); + if (result.status === 0) { + console.log("[parity] Chromium ready."); + } else { + console.log( + "[parity] Chromium install returned a non-zero exit. " + + "If `parity run` later fails with 'Executable doesn't exist', " + + "run `npx playwright install chromium` manually.", + ); + } +} catch (err) { + console.log( + `[parity] Playwright install skipped (${err && err.message ? err.message : "unknown error"}). ` + + "Run `npx playwright install chromium` before your first `parity run`.", + ); +} diff --git a/src/engine/browser.ts b/src/engine/browser.ts index 086d0fc..ee82f2e 100644 --- a/src/engine/browser.ts +++ b/src/engine/browser.ts @@ -79,11 +79,37 @@ export interface LaunchOptions { } export async function launchBrowser(opts: LaunchOptions = {}): Promise { - return await chromium.launch({ - headless: opts.headless ?? true, - slowMo: opts.slowMo ?? 0, - args: ["--disable-blink-features=AutomationControlled"], - }); + try { + return await chromium.launch({ + headless: opts.headless ?? true, + slowMo: opts.slowMo ?? 0, + args: ["--disable-blink-features=AutomationControlled"], + }); + } catch (err) { + const msg = (err as Error).message ?? ""; + // Playwright's own message starts with "Executable doesn't exist at ..." + // when the browser binary wasn't installed yet (e.g. fresh `npm install + // -g @decocms/parity` with no postinstall trigger). Replace it with a + // single clear instruction so the user doesn't have to parse a stack + // trace + Playwright's ASCII banner. + if (msg.includes("Executable doesn't exist")) { + const friendly = new Error( + [ + "Playwright's Chromium binary is not installed yet. Run:", + "", + " npx playwright install chromium", + "", + "(one-time, ~140 MB download). Then re-run parity. This usually happens", + "after a fresh `npm install -g @decocms/parity`.", + ].join("\n"), + ); + // Preserve original error in `cause` so logs still have the full + // launcher path for debugging if needed. + (friendly as Error & { cause?: unknown }).cause = err; + throw friendly; + } + throw err; + } } export interface ContextOptions {