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.
1. CLI parses and resolves
Section titled “1. CLI parses and resolves”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).
2. A URL is resolved
Section titled “2. A URL is resolved”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.
3. A browser opens
Section titled “3. A browser opens”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.
4. Collectors register
Section titled “4. Collectors register”console— subscribes topage.on('console')andpage.on('pageerror')network— subscribes topage.on('requestfinished')andpage.on('requestfailed')screenshot— captured after each navigationtiming— aperformance.timingsnapshot per routea11y(opt-in) — injects axe-core and runs it viapage.evaluateperf(opt-in) — runs Lighthouse against the CDP port, performance category onlyvisual(opt-in) — pixel-diffs the screenshot against a stored baseline withpixelmatch
5. Routes are visited
Section titled “5. Routes are visited”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.
6. Collectors tear down and normalize
Section titled “6. Collectors tear down and normalize”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.
7. A report is written
Section titled “7. A report is written”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.
8. Output and exit code
Section titled “8. Output and exit code”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.
Redaction happens inline, not after
Section titled “Redaction happens inline, not after”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.
Other commands
Section titled “Other commands”bundle and static don’t touch a browser at all — see
Bundle Analysis and
Static Analysis for their (simpler,
filesystem-only) flows.
- Architecture Overview — package structure and responsibilities
- Execution Flow — the same pipeline as a
diagram, plus
bundle/static/MCP flows