Skip to content

How It Works

This is the actual scan pipeline, not a marketing simplification — one level more detail than the homepage summary, matching docs/FLOW.md in this repository.

commander parses flags. detectProject(cwd) inspects package.json and lockfiles to determine { framework, hasVite, devScript?, packageManager }. resolveConfig() merges flags into a Zod-validated ResolvedConfig (see Configuration Reference).

If --url wasn’t given: findRunningDevServer() probes 127.0.0.1:{5173,3000,4173,8080} and reuses the first one that answers. Otherwise launchDevServer() spawns the detected dev script, polls the guessed port, and takes ownership of stopping it when the scan finishes.

Playwright launches Chromium — headless unless --headed. If --perf was passed, it launches with --remote-debugging-port so Lighthouse can attach over CDP to the same browser instance. A BrowserContext/Page is created with the configured viewport, recording a HAR if --har.

  • console — subscribes to page.on('console') and page.on('pageerror')
  • network — subscribes to page.on('requestfinished') and page.on('requestfailed')
  • screenshot — captured after each navigation
  • timing — a performance.timing snapshot per route
  • a11y (opt-in) — injects axe-core and runs it via page.evaluate
  • perf (opt-in) — runs Lighthouse against the CDP port, performance category only
  • visual (opt-in) — pixel-diffs the screenshot against a stored baseline with pixelmatch

For each route in the queue (starting from config.routes, ['/'] by default): page.goto() with the configured timeout, then waitForQuiet() — an event-driven settle that waits for no new console/network activity for 200ms, capped at 1500ms, not a fixed sleep. Each collector’s onNavigate fires. If --discover-routes is set, same-origin <a href> links found on the page are scraped and unseen ones are enqueued, capped at 20 total routes.

Each collector’s teardown() returns a typed result; toIssues() converts it to Issue[] (see Issue & Report Types). Results are deduplicated by a stable id and tagged with severity and evidence type.

The assembled Report is validated against reportSchema, then written to .local/frontend-qa/runs/<runId>/report.json, with screenshots under screenshots/ and, if --har, network.har alongside.

The CLI prints a severity-bucketed summary. If it launched the dev server itself, it stops it. Exit code is non-zero if any issue at or above --fail-on was found (default high), 2 if no URL could be resolved at all.

redactHeaders()/redactString() run at the collector boundary — before anything reaches Issue.metadata — so unredacted values never touch disk in the first place. See Security and the project’s docs/SECURITY.md.

bundle and static don’t touch a browser at all — see Bundle Analysis and Static Analysis for their (simpler, filesystem-only) flows.