Skip to content

Architecture Overview

Five packages, one dependency direction. Matches docs/ARCHITECTURE.md in the repository — update that file, not just this page, if the boundary ever changes.

@frontend-qa/shared (types, Zod schemas, redaction — depends on nothing)
@frontend-qa/core (orchestrator, config, detect, normalize, report writer)
@frontend-qa/browser (Playwright collectors — the only package that imports playwright)
├──▶ @frontend-qa/cli (bin: frontend-qa)
└──▶ @frontend-qa/mcp-server (bin: frontend-qa-mcp)

Pure types and Zod schemas, redaction utilities. No I/O, no side effects. Depended on by every other package.

  • Issue, Report, EvidenceType types
  • issueSchema, reportSchema, configSchema (Zod)
  • redactHeaders(headers), redactString(s)
  • hashIssueId(parts) — stable id for dedup across runs

Framework-agnostic engine — no Playwright dependency.

  • QACollector<T> interface
  • runScan(config): Promise<Report> — orchestrator
  • detectProject(cwd): ProjectInfo
  • resolveConfig(cli, cwd): ResolvedConfig
  • writeReport(report, outDir): Promise<string>
  • normalize.* — collector result → Issue[]
  • analyzeBundle(buildDir) — filesystem-only bundle-size analysis
  • analyzeStatic(srcDir) — ts-morph static source analysis

Playwright-driven collectors and the run orchestrator. The only package that imports playwright.

  • runBrowserScan(input): Promise<{ report, reportPath, outDir }>
  • consoleCollector, networkCollector, screenshotCollector, timingCollector
  • a11yCollector, perfCollector, visualCollector — opt-in
  • discoverRoutes(page, baseUrl)
  • waitForQuiet(page, quietMs?, maxMs?)
  • findRunningDevServer(), launchDevServer(cwd, project), resolveDevServerUrl(cwd, project, config, onLog?) — shared by the CLI and the MCP server
  • slug(route) — filesystem-safe route naming for screenshots
  • getFreePort() — for Lighthouse’s CDP connection

The frontend-qa bin. Wires arg parsing → config → runScan → console summary + exit code. No business logic lives here — everything reusable is in core/browser.

The frontend-qa-mcp bin (currently unpublished — see MCP Server guide). Exposes five MCP tools over stdio as thin wrappers over core/browser — no analysis logic of its own.

Every runtime collector implements the same shape, so adding one never requires touching the orchestrator:

interface QACollector<TResult = unknown> {
name: string; // e.g. 'browser.console'
category: string; // e.g. 'runtime'
setup(ctx: RunContext): Promise<void>;
onNavigate?(route: string): Promise<void>;
teardown(): Promise<TResult>;
toIssues(result: TResult, ctx: RunContext): Issue[];
}
  • shared depends on nothing.
  • core depends only on shared.
  • browser depends on shared + core (+ Playwright).
  • cli depends on core, shared, browser.
  • mcp-server depends on core, shared, browser (+ the MCP SDK) — same shape as cli, no new coupling.

Adding a new collector means a new module implementing QACollector and registering with core’s orchestrator — no edits to core itself required. Adding a new CLI command or MCP tool is additive in the same way: both are thin callers into core/browser, so neither package needs to know the other exists.

  • No LLM/API integration anywhere in the runtime.
  • No cloud upload, no telemetry.
  • The MCP server is stdio-only — no HTTP/SSE transport.