CLI reference
Every flag flawless-react accepts, the seven task ids you can select, and what each task writes
Usage
flawless-react <entry-or-glob> [options]
flawless-react (interactive menu)
The positional is a .tsx file path or a glob such as "src/**/*.tsx". Each matched file is analysed as its own entry component and runs against every selected task.
Resolution is deliberately simple: if the positional resolves to a path that exists, that single file is the entry. Otherwise it is treated as a glob relative to the current directory — and a value with no glob characters that does not exist on disk matches nothing, so the run stops with no files matched.
Run with no positional in a TTY and you get the interactive menu instead: a prompt for the path or glob, a prompt for the output directory, and a checkbox list of the seven tasks with perf-harness and perf-static preselected. --no-interactive disables that path entirely, in which case a positional and --tasks are required.
Options
Every flag that takes a value accepts both --flag value and --flag=value. An unrecognised --flag is an error rather than a silently ignored argument.
| Flag | What it does |
|---|---|
--out <dir> | Output directory for everything the run writes. Defaults to ./.flawless. In --nextjs mode it defaults to <nextRoot>/flawless/ instead. |
--tasks <csv> | Comma-separated task ids to run, skipping the interactive menu. An unknown id is a hard error that prints the list of valid ids. |
--roots <mode> | Which components in a matched file become entry trees. primary (default) takes each file’s main component; exported emits one tree per exported component, which is what a whole-app sweep needs — a shadcn card.tsx exports seven of them. Any other value is a hard error. |
--exclude <glob> | Skip matching files. Repeatable, and appended to the defaults rather than replacing them: *.flawless.test.*, *.test.*, *.spec.*, *.stories.*, *.d.ts, node_modules, .next, dist, build, coverage and flawless/ are already skipped. A non-default --out is not excluded automatically — add it, or a second sweep analyses the suite the first one wrote. |
--no-default-excludes | Drop those defaults and honour only what --exclude names. Then a glob can match generated specs, which is occasionally what you want and usually not. |
--no-interactive | Never prompt. Requires a positional and --tasks. |
--run-tests | After generating, run the produced test suites. If nothing selected can produce one, the run says so rather than exiting quietly — see below. |
--no-inline-summary | Do not drop a <Component>.flawless.json next to each source file. |
--json | Print a machine-readable JSON summary — { exitCode, outDir, results, keptScripts, testRun?, errors? } — to stdout, and nothing else there. The human summary table is suppressed; what suite runs say goes to stderr. Diagnostics stay on stderr too — see below. |
--ci | Shorthand for --no-interactive --json. The flag to reach for in a pipeline: no prompt can block the run, and stdout carries the JSON blob and nothing else. |
--expo [<path>] | Treat <path>, or the current directory when the value is omitted, as an Expo project: read the SDK major, the expo-router root, whether the React Compiler and NativeWind are on, and the port expo start serves web on; default the entry glob to the router root; and place everything under <expoRoot>/flawless/. Mutually exclusive with --nextjs. |
--nextjs [<path>] | Treat <path>, or the current directory when the value is omitted, as a Next.js project: detect the router style, emit MSW handlers and swagger from app/api or pages/api, auto-set the app resolver from app/layout.tsx or pages/_app.tsx, merge the generated deps into the project’s package.json as devDependencies, and place everything under <nextRoot>/flawless/. |
--app-resolver <file> | Override the auto-detected Next.js app resolver. It is an alias for --provider in Next.js mode, and the file must default-export a wrapper component. |
--swagger-url <url> | Fetch an OpenAPI spec from a live URL, for example http://localhost:3000/api/docs from next-swagger-doc, and prefer it over static AST inference. In --nextjs mode that default URL is tried automatically with a short timeout; pass --swagger-url to force a specific URL, or --swagger-url=off to skip the auto-try. |
--depth N | Levels of locally-imported child components to follow. Default 3. 0 analyses the entry component only. Must be >= 0. |
--variants N | Variant rows generated per component. Default 5. Must be >= 0. |
--changed [<ref>] | After generating, run only the specs an edit affects: the changed components’ unit, browser and profile specs, plus any e2e route whose page renders one of them. Defaults to comparing the worktree against HEAD. Analysis and generation stay complete — only the run narrows, because the profiling host is a single bundle of every component and a partial regeneration would no longer match its specs. |
--heaps <mode> | How much of the V8 heap the profile suite keeps: off, stress (default), all. A snapshot of a real app’s heap is tens of megabytes and one is taken either side of every scenario, so off is not only about disk — it takes an observer out of the measurement it was bracketing. |
--include-children | Walk transitive child components. On by default; pass --include-children=false (=0 and =no also work) to skip. |
--props '<json>' | A JSON object merged over the synthesised default props of the first entry. Use it when a prop’s type could not be resolved. |
--provider <file> | Path to a .tsx file that default-exports a wrapper component. The host renders every mounted component inside it — this is how Redux, React Query, theming, or any other context-based store gets threaded through. |
--api <spec> | Path to an OpenAPI / Swagger JSON or YAML file. Scaffolds a standalone Bun mock server (mock/server.ts) with static route handlers generated from the spec’s first example responses; the host server transparently proxies all non-asset requests to it. |
--colocate | Write unit specs beside their source as <name>.flawless.test.tsx — the ones you hand-harden. Each opens with a // @flawless-generated sha256:… provenance stamp so --force can tell an untouched spec from one you edited. |
--force | Regenerate the files a plain re-run keeps: colocated specs, e2e route specs, the derived provider wrapper. A colocated spec is only rewritten while its provenance stamp still matches — hand-edited (or pre-stamp) specs are kept and listed; delete one to regenerate it. A re-run already refreshes the out-dir harness; write-once files are never touched under any flag. |
--report | Emit scripts/report.mjs, so bun run report aggregates run output into a markdown summary. |
--flags | Print the flag inventory — every flag name, one per line — and exit. What --help renders from. |
--check '<line>' | Parse documented invocations without running them; exit nonzero if any fails to parse. Opens no file and spawns nothing. A maintenance flag: the tool dogfoods its own documentation with it, and the docs’ example table is gated by it. |
-h, --help | Print the help text and exit. |
-V, --version | Print the version and exit. |
Tasks
The seven task ids are exactly these. Anything else — including plausible-looking transpositions of these very words — is rejected by --tasks.
| Task id | What it emits or reports |
|---|---|
perf-harness | Scaffolds the standalone performance suite into <outDir>/: host/ (Bun server, bundle, window.__profile driver), playwright.config.ts, profile/<Component>.profile.spec.ts for every mountable component, budgets.json, and heaps/ for the V8 snapshots written at test time. Measurement is React’s built-in <Profiler> for renderCount / renderTime and react-scan for unnecessary rerenders. Produces files, not findings. |
unit-browser | Scaffolds Vitest units at unit/<Component>.test.tsx (jsdom + @testing-library/react) and Playwright functional specs at browser/<Component>.spec.ts, along with vitest.config.mts, setup.ts, and browser/playwright.config.ts. Outside Next.js mode it also merges test:unit plus the matching dev deps into the suite’s package.json, and test:browser when perf-harness is in the same run — the browser specs drive its host. test:perf belongs to perf-harness, which declares it. Produces files, not findings. |
perf-static | Reads the parsed tree — no browser, no build — and writes reports/<Component>.perf-static.json. Two rules fire: perf/no-memoisation-with-children, when a component renders three or more child components and uses none of useMemo / useCallback / memo / useRef, so every parent rerender rerenders all of them; and perf/unresolved-prop, when no default could be synthesised for a prop and the generated tests will pass undefined. |
a11y | Runs biome’s accessibility rules over the entry file and every walked child, then writes a single reports/a11y.json for the whole run — this is a sweep, not a per-component report. Findings are filtered to the lint/a11y category, so correctness rules do not leak into an accessibility report, and each one carries file, line, column, rule, and severity. |
react-rules | Runs a curated set of bug-catching React and hooks rules through biome — useExhaustiveDependencies, useHookAtTopLevel, useJsxKeyInIterable, noArrayIndexKey, noChildrenProp, noVoidElementsWithChildren and friends — and writes a single reports/react-rules.json for the whole run. The config is supplied by flawless-react, so the project’s own biome.json is bypassed and the rule set is the same on every machine. |
e2e | Emits e2e/ — a Playwright config pointed at the target’s real dev server (overridable with FLAWLESS_E2E_BASEURL), a global-setup.ts stub for login, and one spec per app-router page asserting the route loads on the route it asked for, with no console or page errors, plus up to three interactions harvested from that page’s component tree. Needs --nextjs or --expo (expo-router). |
load | Emits load/ — one routes.pipes with a pipe per route and per API operation, plus a write-once routes.setup.ts that crust awaits before any pipe runs — it seeds the ids from e2e/params.json and proves LOAD_AUTH still authenticates, because crust follows redirects and an expired session makes every route a login page answering 200. Run with bun run flawless:load. Every run writes last-<name>.json; nothing writes a baseline for you. Copy a good last-<name>.json to baseline-<name>.json and later runs fail when p95 exceeds 2x it. |
Selecting nothing leaves the run with perf-harness alone. Task order on the command line is the order they run in, and each task’s findings are printed as they complete.
The e2e suite is not green before it is real
A generated route spec used to claim three things: a response came back, its
status was under 500, and the console stayed clean. All three are true of a
login screen. An unauthenticated request to a protected page redirects to
/login, Playwright follows the redirect, and the login page answers 200 — so
thirty route specs could pass having tested one form nobody asked them to test.
global-setup.ts ships as a hand-fill stub, so that was the default state of
a freshly generated suite, not an edge case.
Two things close it, and neither is a flag you can forget:
- Every spec asserts where it landed. After
networkidleit compares the final pathname against the route it navigated to, trailing slashes normalised. The failure names both ways out — authenticate inglobal-setup.ts, or change the expectation if the redirect is what the route is supposed to do. Reading afternetworkidlemeans a client-siderouter.pushcounts too, not only a server 302. - REPORT.md says the auth hook is still a stub, until it isn’t. The check is byte-identity against what the tool emits, so it clears the moment you edit the file — and filling the hook in while leaving the TODO comment does not keep nagging you.
The fix lives in the route specs, not in the setup stub, and that is
deliberate: global-setup.ts is yours the moment it exists and is never
overwritten — not even by --force — so a guard added there would never reach
a single already-generated suite. Route specs are rewritten under --force,
which is how the fix reaches suites that already exist.
This one is verified by running the emitted spec in a real browser against a server that redirects, red before the hook is filled and green after. Reading the spec is exactly what let the old behaviour survive: every assertion in it looked perfectly reasonable.
Task availability and biome
perf-harness, unit-browser, perf-static, e2e and load are self-contained and work anywhere. a11y and react-rules shell out to biome, which the standalone binary cannot carry inside itself, so it is looked for in three places in order: this package’s own node_modules/.bin/biome, then node_modules/.bin/biome in the project being scanned, then biome on PATH. If none of the three hits, both tasks fail with a message naming the paths that were tried.
Any React codebase that already lints with biome satisfies this for free. Otherwise, add it to the project you are scanning:
bun add -d @biomejs/biome Or install flawless-react from the source package, which carries @biomejs/biome as a real dependency so all seven tasks work with no further setup. That channel needs Bun >= 1.1 on the machine running it.
bun add -g @lariocpt/flawless-react Output and reporting
Beyond whatever the selected tasks write, every run produces:
<outDir>/reports/summary.json— a timestamp plus, per task, its id, ok flag, one-line summary, output count, finding count, and error if it failed.<outDir>/REPORT.md— the human account of the same run: how many components can mount, what was excluded and why, and a table of the hand-work still outstanding.- A
<Component>.flawless.jsonnext to each analysed source file, unless--no-inline-summary. It holds per-task finding counts, the three highest-severity findings, and paths to the fulla11y,react-rules, andperf-staticreports. This is what makes issues surface where the code lives rather than only in a terminal that has scrolled away.
Findings are printed grouped by file, one line each as severity line:column rule message, followed by a summary table with a status, file count, and finding count per task.
Each entry in results carries what the task did
Alongside ok, a task reports a status of "generated", "refreshed" or "kept". The
distinction is the whole point of a re-run: a fresh output directory is generated, a
re-run over an existing one refreshes the derived files while write-once files hold, and
a run that deliberately wrote nothing — --changed over an untouched harness — is kept.
All three are ok: true. Reading only the ok flag turns a healthy re-run and a first
generation into the same event, which is exactly the reading that used to report a working
suite as “task threw”. See Regenerate without losing work.
The two streams are decided separately
--json (and --ci, which implies it) hands stdout to the caller: exactly one JSON blob, nothing else, so piping the whole stream into a parser always works.
That is a statement about stdout, not a vow of silence. Diagnostics keep going to stderr — a file the sweep could not parse and skipped, an OpenAPI spec it ignored, a test-script collision, a Next.js emit that failed. Those lines are the only account of work a run quietly dropped, so an unattended caller should capture stderr as well as parse stdout. Both streams used to share one flag, which meant the mode built for machines was the one that lost them.
Embedding the tool with runFlawless? log controls the stdout narration and is off by default. quiet controls the stderr diagnostics and is off by default too — meaning the warnings speak, unless you pass quiet: true to silence them.
Running the generated tests
--run-tests runs the suites after generation. It only fires when perf-harness, unit-browser or e2e was selected and no task failed, and in an interactive run you are asked to confirm instead. It runs bun install in the output directory first if node_modules is missing, then test:unit, test:perf, and test:browser as the selected tasks warrant, and prints which suites failed.
Exit 0 does not mean the suites passed
Ask for a run that cannot happen — --run-tests with only a11y selected, say
— and there is nothing to run: no task in that selection generates a suite. The
run used to end there, exit 0, nothing on stderr, and a --json blob
byte-for-byte the shape of a green suite run. Two very different outcomes, one
indistinguishable result.
So the blob carries testRun, and it has three states:
testRun | What happened |
|---|---|
{ mode: "generated" | "affected", green } | Suites actually ran. green is what folded into the exit code. |
{ mode: "none", reason } | Zero suites ran, and reason says which of three causes it was: nothing selected generates a suite --run-tests executes, nothing was affected, or the selected tasks plan no run for the work that was. |
| absent | The run never reached the test-run phase — which includes a request blocked by a failed task. Read absent as “look at results”, never as “no run was asked for”. |
The invariant worth relying on: mode: "none" if and only if nothing
executed, so a green field never appears on a phase that ran nothing. Both
runners used to return a bare boolean, and true meant either “every suite
passed” or “no suite ran” — one of them printed “all affected suites passed”
having spawned nothing at all.
The same sentence goes to stderr, so an interactive run is told too. And
errors accounts for a failure that belongs to no task — a Next.js emit that
threw exits 1 with every entry in results still ok: true, and before this
the blob had nowhere to say why. Between the two, every red run is explicable
from the blob alone.
Exit codes
| Code | Meaning |
|---|---|
0 | Every selected task succeeded. |
1 | At least one task failed or threw, or a --run-tests / --changed suite run went red, or something outside any task failed — a broken Next.js emit, say. The code says whether, never why: read results, testRun and errors. |
2 | Usage error: a bad flag or flag value, no positional and no interactive TTY, no files matched the glob, or nothing analysable was found. |