@lariocpt/flawless-react 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/flawless-a11y/SKILL.md +113 -0
- package/.agents/skills/flawless-api/SKILL.md +159 -0
- package/.agents/skills/flawless-api-fixtures/SKILL.md +144 -0
- package/.agents/skills/flawless-e2e/SKILL.md +155 -0
- package/.agents/skills/flawless-expo/SKILL.md +283 -0
- package/.agents/skills/flawless-findings/SKILL.md +116 -0
- package/.agents/skills/flawless-load/SKILL.md +112 -0
- package/.agents/skills/flawless-nextjs/SKILL.md +198 -0
- package/.agents/skills/flawless-perf-harness/SKILL.md +158 -0
- package/.agents/skills/flawless-perf-static/SKILL.md +131 -0
- package/.agents/skills/flawless-react/SKILL.md +403 -0
- package/.agents/skills/flawless-react-rules/SKILL.md +96 -0
- package/.agents/skills/flawless-regen/SKILL.md +135 -0
- package/.agents/skills/flawless-unit-browser/SKILL.md +178 -0
- package/LICENSE +21 -0
- package/README.md +611 -2
- package/bin/flawless-react +4 -0
- package/package.json +67 -4
- package/scripts/postinstall.d.mts +46 -0
- package/scripts/postinstall.mjs +127 -0
- package/src/analyse/authShims.ts +220 -0
- package/src/analyse/bootability.ts +257 -0
- package/src/analyse/childBridge.ts +302 -0
- package/src/analyse/childrenIntoSlot.ts +66 -0
- package/src/analyse/compoundFamily.ts +100 -0
- package/src/analyse/dataExports.ts +200 -0
- package/src/analyse/extractChildAttrs.ts +112 -0
- package/src/analyse/extractDataDeps.ts +857 -0
- package/src/analyse/extractDisclosures.ts +186 -0
- package/src/analyse/extractFetchCalls.ts +81 -0
- package/src/analyse/extractFlows.ts +239 -0
- package/src/analyse/extractGuards.ts +270 -0
- package/src/analyse/extractHooks.ts +237 -0
- package/src/analyse/extractInteractions.ts +1421 -0
- package/src/analyse/extractLogging.ts +45 -0
- package/src/analyse/extractNativeA11y.ts +138 -0
- package/src/analyse/extractPredicates.ts +784 -0
- package/src/analyse/extractPropUsage.ts +162 -0
- package/src/analyse/extractProps.ts +1212 -0
- package/src/analyse/extractRenderMode.ts +165 -0
- package/src/analyse/extractRootTag.ts +117 -0
- package/src/analyse/extractState.ts +361 -0
- package/src/analyse/extractStaticText.ts +212 -0
- package/src/analyse/extractVariants.ts +186 -0
- package/src/analyse/harvestApiEndpoints.ts +284 -0
- package/src/analyse/hostProps.ts +185 -0
- package/src/analyse/hostTag.ts +72 -0
- package/src/analyse/htmlContentModel.ts +94 -0
- package/src/analyse/index.ts +744 -0
- package/src/analyse/mountPathCalls.ts +442 -0
- package/src/analyse/nativeContentModel.ts +129 -0
- package/src/analyse/nativeDialect.ts +83 -0
- package/src/analyse/nativeProps.ts +339 -0
- package/src/analyse/ownReturns.ts +31 -0
- package/src/analyse/parseFile.ts +919 -0
- package/src/analyse/pos.ts +8 -0
- package/src/analyse/providerWraps.ts +1977 -0
- package/src/analyse/pureModule.ts +88 -0
- package/src/analyse/reexportTable.ts +148 -0
- package/src/analyse/refs.ts +195 -0
- package/src/analyse/renderPath.ts +441 -0
- package/src/analyse/resolveDepReturns.ts +370 -0
- package/src/analyse/resolveIndexKeys.ts +98 -0
- package/src/analyse/resolveModule.ts +431 -0
- package/src/analyse/resolveTagRootTags.ts +103 -0
- package/src/analyse/serverActionReturns.ts +213 -0
- package/src/analyse/sourceCache.ts +263 -0
- package/src/analyse/spreadSiblings.ts +98 -0
- package/src/analyse/walkChildren.ts +98 -0
- package/src/analyse/zodSchema.ts +424 -0
- package/src/bin.ts +23 -0
- package/src/changed.ts +221 -0
- package/src/cli.ts +560 -0
- package/src/cliFlags.ts +281 -0
- package/src/core.ts +1229 -0
- package/src/docLine.ts +122 -0
- package/src/emitPaths.ts +51 -0
- package/src/expo/detect.ts +184 -0
- package/src/expo/refusal.ts +62 -0
- package/src/expo/routes.ts +145 -0
- package/src/index.ts +95 -0
- package/src/mcp/analysisTools.ts +720 -0
- package/src/mcp/infoTools.ts +135 -0
- package/src/mcp/protocol.ts +130 -0
- package/src/mcp/run.ts +269 -0
- package/src/mcp/runTools.ts +362 -0
- package/src/mcp/server.ts +279 -0
- package/src/mcp/spec.ts +86 -0
- package/src/mcp/support.ts +268 -0
- package/src/mcp/tools.ts +75 -0
- package/src/nextjs/apiRoutes.ts +116 -0
- package/src/nextjs/detect.ts +397 -0
- package/src/nextjs/emit.ts +106 -0
- package/src/nextjs/liveSpec.ts +79 -0
- package/src/nextjs/mswHandlers.ts +55 -0
- package/src/nextjs/providerWrapper.ts +603 -0
- package/src/nextjs/routes.ts +86 -0
- package/src/nextjs/swagger.ts +32 -0
- package/src/packageJson.ts +414 -0
- package/src/platform.ts +18 -0
- package/src/prompt.ts +132 -0
- package/src/reactCompiler.ts +165 -0
- package/src/reporters/findings.ts +156 -0
- package/src/reporters/report.ts +463 -0
- package/src/runner/runTests.ts +402 -0
- package/src/scaffold/adoptStyle.ts +181 -0
- package/src/scaffold/index.ts +253 -0
- package/src/scaffold/openapi.ts +183 -0
- package/src/scaffold/templates.ts +2379 -0
- package/src/scaffold-tests/index.ts +463 -0
- package/src/scaffold-tests/mockValues.ts +208 -0
- package/src/scaffold-tests/mocks.ts +794 -0
- package/src/scaffold-tests/nativeIndex.ts +52 -0
- package/src/scaffold-tests/nativeTemplates.ts +1528 -0
- package/src/scaffold-tests/postDispatch.ts +152 -0
- package/src/scaffold-tests/registry.ts +443 -0
- package/src/scaffold-tests/selectors.ts +493 -0
- package/src/scaffold-tests/selectorsNative.ts +273 -0
- package/src/scaffold-tests/stamp.ts +79 -0
- package/src/scaffold-tests/templates.ts +3578 -0
- package/src/skills/cli.ts +248 -0
- package/src/skills/compat.ts +166 -0
- package/src/skills/data.ts +192 -0
- package/src/skills/md.d.ts +7 -0
- package/src/skills/menu.ts +55 -0
- package/src/skills/spec.ts +227 -0
- package/src/suiteScripts.ts +110 -0
- package/src/tasks/a11yNative.ts +281 -0
- package/src/tasks/a11yScan.ts +67 -0
- package/src/tasks/biomeRunner.ts +301 -0
- package/src/tasks/biomeSweep.ts +166 -0
- package/src/tasks/e2eExpo.ts +436 -0
- package/src/tasks/e2eRoutes.ts +756 -0
- package/src/tasks/eslintReactRules.ts +67 -0
- package/src/tasks/index.ts +24 -0
- package/src/tasks/loadExpo.ts +273 -0
- package/src/tasks/loadPipes.ts +750 -0
- package/src/tasks/perfHarness.ts +145 -0
- package/src/tasks/staticPerfAnalysis.ts +510 -0
- package/src/tasks/types.ts +1 -0
- package/src/tasks/unitAndBrowser.ts +462 -0
- package/src/types.ts +1419 -0
- package/src/version.ts +12 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flawless-react
|
|
3
|
+
description: Generate and maintain React/Next.js/Expo test suites with flawless-react — task selection, Next.js and Expo modes, provider wrappers, MSW fixtures from an OpenAPI spec, colocated specs, route e2e, crust load pipes, the programmatic API, and how to read its findings. Use when adding component/e2e/load tests to a React codebase, when a generated flawless suite is red, or for "scaffold tests for this component" work. Deeper guidance lives in the sibling skills flawless-nextjs, flawless-unit-browser, flawless-perf-harness, flawless-perf-static, flawless-a11y, flawless-react-rules, flawless-findings, flawless-e2e, flawless-load, flawless-api-fixtures, flawless-regen, flawless-api, and flawless-expo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# flawless-react
|
|
7
|
+
|
|
8
|
+
Point it at a `.tsx` file or glob and it reads the component's props, state,
|
|
9
|
+
hooks, children and fetches, then writes a runnable suite. Everything it emits
|
|
10
|
+
is yours to edit, but most of it is REGENERATED — the suite improves, and a
|
|
11
|
+
frozen file never receives the improvement. Only what the tool cannot rebuild is
|
|
12
|
+
protected: `budgets.json`, `host/main.tsx`, `provider.fixtures.ts`,
|
|
13
|
+
`e2e/global-setup.ts`, `e2e/params.json`, `mocks/handlers.ts`, and colocated
|
|
14
|
+
specs. To keep a hand-hardened component spec, use `--colocate`; specs written
|
|
15
|
+
inside the out dir are rewritten.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# one component, unit specs only
|
|
19
|
+
bunx @lariocpt/flawless-react src/components/Widget.tsx --tasks unit-browser --no-interactive
|
|
20
|
+
|
|
21
|
+
# a Next.js app: real fixtures, colocated specs, route e2e and load pipes
|
|
22
|
+
cd apps/web
|
|
23
|
+
bunx @lariocpt/flawless-react "src/app/dashboard/page.tsx" \
|
|
24
|
+
--nextjs . --api openapi.json --swagger-url=off \
|
|
25
|
+
--tasks unit-browser,perf-static,a11y,react-rules,e2e,load \
|
|
26
|
+
--colocate --no-interactive
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Which skill to load
|
|
30
|
+
|
|
31
|
+
This skill is the map. Each plane's traps live in its own skill.
|
|
32
|
+
|
|
33
|
+
| situation | skill |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `--nextjs`: provider derivation and drops, module stubs, React pins, `package.json` merge, `.env` order | flawless-nextjs |
|
|
36
|
+
| unit + browser spec shapes, provider wrapper, `TODO(flawless)`, registered mocks | flawless-unit-browser |
|
|
37
|
+
| the runtime perf plane: `host/`, `profile/`, budgets, `--heaps`, port 5173 | flawless-perf-harness |
|
|
38
|
+
| the static analyser's nine `perf/*` rules, sidecars, what a clean report means | flawless-perf-static |
|
|
39
|
+
| accessibility findings (biome a11y on web, the six native rules) | flawless-a11y |
|
|
40
|
+
| the curated biome correctness/suspicious sweep | flawless-react-rules |
|
|
41
|
+
| triaging a findings list, REPORT.md accounting, suppressions, the counts ledger | flawless-findings |
|
|
42
|
+
| route e2e against a real app: login, `params.json`, the auth wall | flawless-e2e |
|
|
43
|
+
| crust load pipes, `LOAD_BASE`/`LOAD_AUTH`, baselines, anti-vacuous probes | flawless-load |
|
|
44
|
+
| `--api` / `--swagger-url`, MSW handlers, synthesised fixture bodies | flawless-api-fixtures |
|
|
45
|
+
| re-running over an existing suite: `--force`, `--changed`, kept vs refreshed | flawless-regen |
|
|
46
|
+
| `runFlawless()` / `analyse()` from a script | flawless-api |
|
|
47
|
+
| `--expo`: what runs, what refuses, native specs and selectors | flawless-expo |
|
|
48
|
+
|
|
49
|
+
## Tasks
|
|
50
|
+
|
|
51
|
+
| id | what it writes | needs |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `unit-browser` | Vitest + Testing Library specs (+ Playwright browser specs when `perf-harness` is also selected) | — |
|
|
54
|
+
| `perf-harness` | `host/` bundle + Profiler/react-scan Playwright suite + heap snapshots | — |
|
|
55
|
+
| `perf-static` | `reports/<C>.perf-static.json` findings, no files to run | — |
|
|
56
|
+
| `a11y` | web: biome's a11y rule group; `--expo`: the tool's six native `a11y/*` rules | biome in the target project (web path only) |
|
|
57
|
+
| `react-rules` | biome `correctness` + `suspicious`, curated; scratch config, local `biome.json` bypassed | biome |
|
|
58
|
+
| `e2e` | `flawless/e2e/` route specs against a real dev server | running app + seeded data |
|
|
59
|
+
| `load` | `flawless/load/routes.pipes` crust load pipelines | crust to run them |
|
|
60
|
+
|
|
61
|
+
Pick tasks explicitly with `--tasks a,b`; with no args in a TTY you get a
|
|
62
|
+
checkbox menu.
|
|
63
|
+
|
|
64
|
+
Over an EXISTING suite, a re-run exits 0: `perf-harness` refreshes its derived
|
|
65
|
+
files exactly as a fresh scaffold would (write-once files untouched; TaskResult
|
|
66
|
+
and REPORT.md carry `status: "refreshed"`), and under `--changed` it keeps the
|
|
67
|
+
harness entirely — nothing written, `status: "kept"` — so the affected-test run
|
|
68
|
+
happens with any task selection. `--force` is only for regenerating what a
|
|
69
|
+
plain re-run keeps (colocated specs, e2e route specs, the provider wrapper).
|
|
70
|
+
See the flawless-regen skill for the full regeneration contract.
|
|
71
|
+
|
|
72
|
+
## Next.js mode
|
|
73
|
+
|
|
74
|
+
`--nextjs [path]` detects the router, auto-selects a provider, writes
|
|
75
|
+
everything under `<root>/flawless/`, merges devDeps + `flawless:*` scripts into
|
|
76
|
+
the project's `package.json`, and appends gitignore entries.
|
|
77
|
+
|
|
78
|
+
Ignore entries are appended in **every** mode, not only under `--nextjs`. Outside
|
|
79
|
+
Next.js mode they go to the git root (then the nearest `package.json`, then the
|
|
80
|
+
out dir's parent). Until that was fixed a plain run ignored nothing it
|
|
81
|
+
generated, including `<out>/e2e/.auth.json` — the file the emitted global-setup
|
|
82
|
+
tells you to persist a logged-in storageState into.
|
|
83
|
+
|
|
84
|
+
**Providers.** App-router `layout.tsx` is usually an async server component
|
|
85
|
+
importing `next/font` or `next/headers` — unusable under jsdom. flawless
|
|
86
|
+
detects that and derives `flawless/provider.tsx` from the JSX chain that
|
|
87
|
+
encloses `{children}`, keeping only `"use client"` components and literal
|
|
88
|
+
attributes. What it leaves out is named where you will read it: a dropped
|
|
89
|
+
attribute as a `TODO(flawless)` comment beside the wrapper, a dropped provider
|
|
90
|
+
layer in a block at the top of the file with the reason and the fix. A provider
|
|
91
|
+
layer whose module directly imports an auth library (`@clerk/*`, `next-auth`)
|
|
92
|
+
is skipped rather than mounted: the unit and browser planes replace those
|
|
93
|
+
libraries with a signed-out stub, so a provider gating its children on a
|
|
94
|
+
session renders *nothing* and every spec inherits the blank screen — while the
|
|
95
|
+
stub already satisfies the context reads auth-consuming components make. Expect
|
|
96
|
+
the components under it to fail on "Could not find Convex client" or "could not
|
|
97
|
+
find … in the context" — that is the drop, not the component. `--force`
|
|
98
|
+
regenerates it, so a change to the layout's provider chain reaches an existing
|
|
99
|
+
suite — put the values you supply (a seeded auth state, a theme) in
|
|
100
|
+
`provider.fixtures.ts` beside it, which is never regenerated under any flag.
|
|
101
|
+
Pass `--provider <file>` to override — that is also the way to supply a context
|
|
102
|
+
the derived wrapper dropped, since editing `provider.tsx` does not survive
|
|
103
|
+
`--force`. In plain (non-Next) mode there is no derived `provider.tsx` at all:
|
|
104
|
+
every render is wrapped in a per-component nest derived from hook-call,
|
|
105
|
+
namespace-compound, transitive-requirement and dynamic-render evidence (a
|
|
106
|
+
dynamic `<Tag />` bound from a project hook's return owes what that hook's
|
|
107
|
+
values can hand out) — the app's own providers,
|
|
108
|
+
convention matches for bare-module hooks, and generated shims for library
|
|
109
|
+
contexts — and `--provider` sits outside that nest.
|
|
110
|
+
|
|
111
|
+
**API fixtures.** Static route walking only sees `app/api/**/route.ts`, which
|
|
112
|
+
is useless for catch-all APIs (Hono, tRPC). Pass `--api openapi.json` (or
|
|
113
|
+
`--swagger-url http://localhost:3000/api/docs`) and real example bodies flow
|
|
114
|
+
into `flawless/mocks/handlers.ts` and into each generated fetch spec. Use
|
|
115
|
+
`--swagger-url=off` to skip the live-spec probe.
|
|
116
|
+
|
|
117
|
+
Everything below that summary is in flawless-nextjs — module stubs and the rule
|
|
118
|
+
that a named one outranks a pattern one, the Testing Library pins and how a stale
|
|
119
|
+
range is repaired, the `package.json` merge with `pnpm.ignoredBuiltDependencies`,
|
|
120
|
+
the two `.env` load orders, and where gitignore entries land.
|
|
121
|
+
|
|
122
|
+
## React 17 / 18 / 19
|
|
123
|
+
|
|
124
|
+
The app's React major (`detectAppReact` in the root `package.json`) is the
|
|
125
|
+
single switch. At or above 18 the host mounts through `createRoot`; below 18
|
|
126
|
+
it falls back to legacy `ReactDOM.render`, document roots render with
|
|
127
|
+
`renderToStaticMarkup`, and promise-valued props are not passed. The suite's
|
|
128
|
+
`react`/`react-dom` are pinned to the version the app has INSTALLED, exactly:
|
|
129
|
+
`detectAppReact` reads `node_modules/react` before the declared range, because a
|
|
130
|
+
caret is a floor and `^19.2.0` installs whatever is newest. Where the analysed
|
|
131
|
+
sources resolve a different copy than that walk finds — a workspace whose root
|
|
132
|
+
declares no react while a sibling package holds one — `appReactPin` reads the
|
|
133
|
+
pin off the copy the emitted `react` aliases anchor on instead, because the pin
|
|
134
|
+
and the alias have to name the same physical copy or one plane ends up with two
|
|
135
|
+
React copies. Where no manifest declares a version at all — a library whose
|
|
136
|
+
react is a peer, `"^18.0.0 || ^19.0.0"`, declares no version — the nearest
|
|
137
|
+
installed pair is used instead,
|
|
138
|
+
scoped so an ancestor's tooling react never out-votes a real declaration. Two
|
|
139
|
+
React copies is the classic red-suite cause, and a pair that merely drifts is
|
|
140
|
+
just as fatal — one patch off dies at load on react-dom's guard
|
|
141
|
+
(`Incompatible React versions`), one major off on `Cannot read properties of
|
|
142
|
+
undefined (reading 'ReactCurrentOwner')`. A stale or loose pin left in an
|
|
143
|
+
existing suite manifest is rewritten by the next run (`repairReactPin`).
|
|
144
|
+
Testing Library follows the app: React ≤17 gets
|
|
145
|
+
`@testing-library/react@^12.1.5` + `@testing-library/dom@^8.20.1` — 13/14
|
|
146
|
+
peer-require react ^18 and 14 hard-requires the `react-dom/client` entry that
|
|
147
|
+
does not exist in 17. A stale 13+ range left by an older run is repaired by
|
|
148
|
+
the next one (`repairRtlPin`).
|
|
149
|
+
|
|
150
|
+
Two footprints complete the merge: for pnpm apps the `package.json` merge
|
|
151
|
+
records `pnpm.ignoredBuiltDependencies` for the deps it introduces
|
|
152
|
+
(`@playwright/test`, `msw`) — pnpm ≥10 errors on an unrecorded build-script
|
|
153
|
+
decision, and `ERR_PNPM_IGNORED_BUILDS` killed the app's own `pnpm run dev`
|
|
154
|
+
before Next could boot; and the suite's Vitest config reads the app's
|
|
155
|
+
`.env.test.local` → `.env.example` (first value wins, env vars beat them) —
|
|
156
|
+
`.env.example` last because template repos ship only the example, and its
|
|
157
|
+
placeholders exist to satisfy module-scope construction.
|
|
158
|
+
|
|
159
|
+
## Expo / React Native mode
|
|
160
|
+
|
|
161
|
+
`--expo [<path>]`, mutually exclusive with `--nextjs`. Reads the project's SDK major, expo-router root, React Compiler and NativeWind settings off disk, defaults the entry glob to the router root, and writes to `<expoRoot>/flawless/`.
|
|
162
|
+
|
|
163
|
+
**Every task runs under `--expo` except one, which refuses by a measured decision — and the tool says which.**
|
|
164
|
+
|
|
165
|
+
| task | under `--expo` |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `unit-browser` | **runs** — jest-expo + @testing-library/react-native |
|
|
168
|
+
| `perf-static` | **runs** |
|
|
169
|
+
| `react-rules` | **runs** |
|
|
170
|
+
| `a11y` | **runs** — the native analyser: six `a11y/*` rules, no biome involved |
|
|
171
|
+
| `e2e` | **runs** — route specs against `expo start --web`; cookie auth via storageState, copy-witness anti-vacuity |
|
|
172
|
+
| `load` | **runs** — API-only crust pipes, harvested one hop through the app's fetch wrapper (screen → api fn → apiFetch); a 401 auth probe, not a redirect check |
|
|
173
|
+
| `perf-harness` | **refuses** — measured-and-declined (~0 host-mountable components on an all-route-screens app); the refusal names the recorded evidence |
|
|
174
|
+
|
|
175
|
+
A refusal is a failed `TaskResult`, not a throw: it lands in `results[]`, the `--ci` blob and `REPORT.md`. A mixed selection still runs what it can.
|
|
176
|
+
|
|
177
|
+
**Do not work around a refusal by dropping `--expo`.** The DOM suite pointed at React Native emits specs that run, pass, and touch nothing — `container.querySelector("Pressable")` matches no element, and `getByRole("button")` finds nothing because React Native has no implicit roles.
|
|
178
|
+
|
|
179
|
+
Two things worth knowing before reading a native run's output, both measured (`docs/archive/2026-08-expo-probes.md`):
|
|
180
|
+
|
|
181
|
+
- **`a11y` on native is the tool's own analyser, and its zero carries a denominator.** Biome's 29 rules are HTML/ARIA and returned zero over 31 RN files with 82 unlabelled `<Pressable>`s (3/3 on the DOM offender fixture), so the task refused until wave 5 shipped the native engine. Its summary always says how many press targets / text inputs / icons were examined — a native zero WITHOUT that denominator is a bug, not a clean bill.
|
|
182
|
+
- **The React Compiler changes what `perf-static` means, and the task honours it.** On a project with `experiments.reactCompiler: true`, 136 of 168 findings were the three memoisation rules — work the compiler already does. `perf-static` reads that state from the project's configs in every mode (`app.json`, `next.config.*`, a babel or recognised bundler config) and suppresses those three, naming the evidence in REPORT.md; the hook-deps, effect and `set-state-in-render` rules run either way. See the `flawless-perf-static` skill for the three states and what each one prints.
|
|
183
|
+
|
|
184
|
+
See the `flawless-expo` skill for the native spec shapes, the selector ladder, and what a skip means.
|
|
185
|
+
|
|
186
|
+
## What the generated unit specs contain
|
|
187
|
+
|
|
188
|
+
Driven by static analysis, so they only assert what can be derived:
|
|
189
|
+
|
|
190
|
+
- the rendered root tag, when the analyser could pin it down;
|
|
191
|
+
- `children` and `className` passthrough, but only when the value provably
|
|
192
|
+
reaches the DOM — accepting a prop is not rendering it;
|
|
193
|
+
- for a cva/`tv` component, that every option of every key renders a **distinct**
|
|
194
|
+
class list. Never a specific Tailwind token: `cn()` is usually tailwind-merge,
|
|
195
|
+
which drops base classes a later variant conflicts with;
|
|
196
|
+
- literal copy the component renders unconditionally;
|
|
197
|
+
- one test per detected interaction, with negative assertions on every other
|
|
198
|
+
handler prop (a click that fires two callbacks fails);
|
|
199
|
+
- disclosures opened first: a control behind a closed `Dialog`/`Menu`/`Popover`
|
|
200
|
+
is not in the DOM, so the spec clicks its trigger — by prop label, literal
|
|
201
|
+
text, or its fixture child — clears the spies, then queries with `findBy*`;
|
|
202
|
+
static-copy assertions skip closed content outright. A root written `open`,
|
|
203
|
+
`defaultOpen` or `open={true}` is never gated;
|
|
204
|
+
- a named local handler read through: `onClick={handleConfirm}` whose
|
|
205
|
+
declaration calls exactly ONE prop handler directly is asserted as that prop.
|
|
206
|
+
Two props means the handler claims neither — canary;
|
|
207
|
+
- `children` handed to an `asChild`/`Slot`/`Slottable` site mounted as a
|
|
208
|
+
`<span>`, never bare text — Slot clones elements and a text child throws;
|
|
209
|
+
- id-typed props never default to `""` (`flawless-<prop>` instead) — providers
|
|
210
|
+
validate ids, they do not render them;
|
|
211
|
+
- state-derived tests: initial-state witnesses and handler→setter tests;
|
|
212
|
+
- one `describe` per `useEffect` fetch with **success / loading / error /
|
|
213
|
+
empty** cases via `server.use(...)` overrides;
|
|
214
|
+
- async server components get `await Component(props)` plus `vi.mock` stubs
|
|
215
|
+
for their non-component imports (no browser spec — they cannot mount).
|
|
216
|
+
|
|
217
|
+
Everything above describes the render the component produces **at the props the
|
|
218
|
+
spec passes**, not the last `return` in its body. A guard the analyser can decide
|
|
219
|
+
is followed; one it cannot that returns nothing falls through to the tail; one it
|
|
220
|
+
cannot that returns content produces no description at all.
|
|
221
|
+
|
|
222
|
+
Client specs mock an **allowlist only** — `next/navigation`, `next/headers`,
|
|
223
|
+
`server-only`, `next/link`, `next/font/*` and any `"use server"` module. Mocking
|
|
224
|
+
a `cn` util would strip every className off the markup, and mocking a UI
|
|
225
|
+
component would replace the element the interaction tests click.
|
|
226
|
+
|
|
227
|
+
When nothing is derivable the spec emits `test.skip` naming which preconditions
|
|
228
|
+
failed. **A skip with a reason is the intended output there — do not "fix" it by
|
|
229
|
+
deleting the assertion or by hand-writing one.** Fix the generator, or give the
|
|
230
|
+
component the `data-testid` / fixture the skip asks for.
|
|
231
|
+
|
|
232
|
+
Anything the analyser could not derive is a `TODO(flawless)` marker, never a
|
|
233
|
+
silently passing assertion. **The hardening loop is: run the suite, fill the
|
|
234
|
+
TODOs with real fixtures, rerun.** Regenerating later will not clobber your
|
|
235
|
+
work if the specs are colocated (see below).
|
|
236
|
+
|
|
237
|
+
## Spec placement
|
|
238
|
+
|
|
239
|
+
Default is `flawless/unit/<Name>.test.tsx`. Same-named components from
|
|
240
|
+
different files get `<Parent>__<Name>`.
|
|
241
|
+
|
|
242
|
+
`--colocate` writes `<source-dir>/<file>.flawless.test.tsx` beside the
|
|
243
|
+
component instead (multi-component files get `<file>.<Component>.flawless...`).
|
|
244
|
+
Colocated specs are **never overwritten without `--force`** — they are the ones
|
|
245
|
+
you hand-harden. Run them with `bun run flawless:unit`.
|
|
246
|
+
|
|
247
|
+
If the project's own test script is a bare `bun test`, it will also collect
|
|
248
|
+
`*.flawless.test.tsx` and fail on vitest globals. Scope it: `"test": "bun test tests"`.
|
|
249
|
+
|
|
250
|
+
## e2e and load
|
|
251
|
+
|
|
252
|
+
Route specs, the global-setup login and the auth wall are flawless-e2e; crust
|
|
253
|
+
pipes, `LOAD_BASE`/`LOAD_AUTH` and their baselines are flawless-load. The short
|
|
254
|
+
version:
|
|
255
|
+
|
|
256
|
+
e2e runs against the app's **real dev server** — MSW cannot intercept a server
|
|
257
|
+
component's direct database access, so mocking it would be a lie. Seed the
|
|
258
|
+
database first, fill in `flawless/e2e/global-setup.ts` (it ships a worked login
|
|
259
|
+
example that persists `storageState`), and pass ids for dynamic segments:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
bun run db:reset
|
|
263
|
+
FLAWLESS_E2E_ID=<seeded-id> bun run flawless:e2e
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
For contract-checked runs, put crust's validation proxy in front and assert no
|
|
267
|
+
response violated the spec:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
crust -c 'mock-server --swagger openapi.json --port 4747 --proxy http://localhost:3001' &
|
|
271
|
+
FLAWLESS_E2E_BASEURL=http://localhost:4747 bun run flawless:e2e
|
|
272
|
+
crust -c 'GET :4747/__crust/violations | (v => v.violations.filter(x => x.direction === "response")) | assert (a => a.length === 0)'
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Load pipes target the analysed page routes plus the GET endpoints its
|
|
276
|
+
components actually fetch (mutations are excluded on purpose). Each line gates
|
|
277
|
+
p95 against a baseline: copy a good `last-<name>.json` to `baseline-<name>.json`
|
|
278
|
+
and later runs fail when p95 doubles.
|
|
279
|
+
|
|
280
|
+
Two things decide whether the numbers mean anything, and both are easy to get
|
|
281
|
+
wrong:
|
|
282
|
+
|
|
283
|
+
- **`LOAD_BASE` must be a production build** (`bun run build && bun run start`),
|
|
284
|
+
not `next dev` — the dev server compiles routes on demand, so the first
|
|
285
|
+
request to each measures the compiler.
|
|
286
|
+
- **`LOAD_AUTH` is a header line**, and for a cookie-session app that is
|
|
287
|
+
`cookie: session=<value>`, not a Bearer token. Prove it authenticates first:
|
|
288
|
+
an unauthenticated request to a dashboard route redirects to the login page,
|
|
289
|
+
which answers 200, so every pipe passes while measuring the login screen.
|
|
290
|
+
|
|
291
|
+
A pipe whose ids are unfilled is emitted **commented out**, above a line naming
|
|
292
|
+
which key in `flawless/e2e/params.json` to fill — a blank segment measures the
|
|
293
|
+
app's 404 handler, not the route. Fill the id and regenerate with `--force`,
|
|
294
|
+
which merges rather than overwrites: edited rates, hand-added flows and pipes
|
|
295
|
+
you commented out yourself all survive. The task summary reports live versus
|
|
296
|
+
commented counts, so REPORT.md does not call a gated pipe covered.
|
|
297
|
+
|
|
298
|
+
## Reading findings
|
|
299
|
+
|
|
300
|
+
`perf-static` and the biome tasks print `file:line:col rule message` and write
|
|
301
|
+
`flawless/reports/*.json`; a `<Component>.flawless.json` sidecar lands next to
|
|
302
|
+
each analysed source (`--no-inline-summary` to disable). Its paths are relative
|
|
303
|
+
to the sidecar — resolve against its own directory — and its `a11y` and
|
|
304
|
+
`reactRules` entries name the run's single aggregate, not a per-component file.
|
|
305
|
+
Rules worth knowing:
|
|
306
|
+
|
|
307
|
+
- `perf/hook-missing-dependency` / `hook-extraneous-dependency` — stale-closure
|
|
308
|
+
and over-firing effects, deliberately conservative (skipped when deps contain
|
|
309
|
+
member access or spreads, so a clean report is not proof).
|
|
310
|
+
- `perf/unstable-child-prop` — inline object/array/arrow passed to a component
|
|
311
|
+
child; flagged harder when that child is `React.memo`-wrapped.
|
|
312
|
+
- `perf/set-state-in-render` (error) — unguarded setState during render.
|
|
313
|
+
- `perf/unresolved-prop` — the analyser could not synthesise a default, so the
|
|
314
|
+
generated spec passes `undefined`. Reported apart from the findings, bucketed per
|
|
315
|
+
component under REPORT.md's `## Spec quality`; fix with `--props '{"x":1}'` or by
|
|
316
|
+
editing the spec's `DEFAULTS`.
|
|
317
|
+
|
|
318
|
+
Triage order, what a suppression costs, and which rule belongs to which task are
|
|
319
|
+
the three skills flawless-perf-static, flawless-a11y and flawless-react-rules;
|
|
320
|
+
flawless-findings keeps the REPORT.md accounting and the counts ledger.
|
|
321
|
+
|
|
322
|
+
## Programmatic use
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import { runFlawless } from "flawless-react";
|
|
326
|
+
|
|
327
|
+
const { results, trees, outDir } = await runFlawless({
|
|
328
|
+
entry: "src/components/Widget.tsx",
|
|
329
|
+
nextjs: ".",
|
|
330
|
+
tasks: ["unit-browser", "perf-static"],
|
|
331
|
+
colocate: true,
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
It never calls `process.exit`; bad input throws `UsageError`. `analyse()`,
|
|
336
|
+
`analyseTree()` (the perf rules) and the scaffolders are exported too, so you
|
|
337
|
+
can build your own reporting on `ComponentTree` without the CLI. `--ci` is
|
|
338
|
+
shorthand for `--no-interactive --json`.
|
|
339
|
+
|
|
340
|
+
## Style
|
|
341
|
+
|
|
342
|
+
Generated files are reformatted with the **host project's** biome or prettier
|
|
343
|
+
before the run finishes, and JSON merges preserve the file's existing
|
|
344
|
+
indentation — generation should produce a clean `git diff`, not a reformat of
|
|
345
|
+
your `package.json`.
|
|
346
|
+
|
|
347
|
+
## Troubleshooting
|
|
348
|
+
|
|
349
|
+
- **`Cannot read properties of null (reading 'useState')`** — two React copies.
|
|
350
|
+
The generated vitest config pins react/react-dom to the `node_modules` that
|
|
351
|
+
also holds `@testing-library/react`; if you moved things, fix the alias block.
|
|
352
|
+
- **`window.matchMedia is not a function`** — `flawless/setup.ts` ships jsdom
|
|
353
|
+
polyfills; make sure your config still lists it in `setupFiles`.
|
|
354
|
+
- **Browser specs missing** — they need the `perf-harness` host; add that task.
|
|
355
|
+
- **RSC spec renders nothing** — its `vi.mock` stubs return `vi.fn()`; give
|
|
356
|
+
them real return values (that is the TODO).
|
|
357
|
+
- **Playwright has no browser** — `bunx playwright install chromium`. The
|
|
358
|
+
runner attempts this automatically; `FLAWLESS_SKIP_BROWSER_INSTALL=1` opts out.
|
|
359
|
+
- **Spec fails `ReferenceError: <name> is not defined`, host build printed
|
|
360
|
+
nothing** — a host resolve-plugin fall-through dropped a bare specifier's
|
|
361
|
+
module onto a variant without that export (`msw` named imports were the
|
|
362
|
+
casualty). Run `bun host/server.ts` from the suite dir to see the build;
|
|
363
|
+
check the plugin's `args.path` gates, not the spec.
|
|
364
|
+
- **Every e2e route 404s while `/` loads in a browser** — the app's own
|
|
365
|
+
middleware is an auth wall (`auth.protect()`); with placeholder keys every
|
|
366
|
+
route behind it 404s. Real credentials in `e2e/global-setup.ts`, or do not
|
|
367
|
+
claim those routes.
|
|
368
|
+
- **e2e dies on "Timed out waiting …ms from config.webServer", no spec run** —
|
|
369
|
+
the same wall one stage earlier. Playwright calls the server up only on a
|
|
370
|
+
2xx/3xx from the readiness url, which is the app root, and retries a 404
|
|
371
|
+
forever (`[WebServer] GET / 404` repeating in the log). Fill the credentials,
|
|
372
|
+
or set `FLAWLESS_E2E_BASEURL` to a server you started yourself — the emitted
|
|
373
|
+
config drops its own `webServer` block for that.
|
|
374
|
+
- **Both Playwright configs collide** (`test.describe() called here` /
|
|
375
|
+
`No tests found`) — two `@playwright/test` copies (app root + suite). Run the
|
|
376
|
+
plane from the suite dir with its own config:
|
|
377
|
+
`node node_modules/@playwright/test/cli.js test --config e2e/playwright.config.ts`.
|
|
378
|
+
Same for vitest: `node ../node_modules/vitest/vitest.mjs run --config vitest.config.mts`.
|
|
379
|
+
|
|
380
|
+
## Limits worth stating up front
|
|
381
|
+
|
|
382
|
+
Static analysis only, no type checker. Imported prop types resolve two hops —
|
|
383
|
+
including types from the repo's own **workspace packages** (`@scope/shared`,
|
|
384
|
+
resolved through the workspace map into the package's source, barrel
|
|
385
|
+
re-exports chased to the declaring file); intersections, host-element props
|
|
386
|
+
(`ComponentProps<"div">`), `ComponentProps<typeof LocalX>` and cva tables all
|
|
387
|
+
resolve. `z.infer`/`z.input` aliases resolve too, by reading the zod schema
|
|
388
|
+
syntactically (object/enum/primitive/array/coerce, `.optional`/`.nullable`/
|
|
389
|
+
`.default`/`.partial`/`.extend`; `.transform`, unions, records and dates
|
|
390
|
+
bail). Non-literal unions, `Omit`/`Pick`/`Partial` and anything in
|
|
391
|
+
`node_modules` do not — the prop lands in `unresolvedProps` and the type that
|
|
392
|
+
defeated it in `unresolvedTypes`.
|
|
393
|
+
|
|
394
|
+
Mocks come from a component's **own** imports, so one that renders `<Link>` only
|
|
395
|
+
through a child gets no `next/link` mock in its own spec.
|
|
396
|
+
|
|
397
|
+
Every run writes `flawless/REPORT.md`, which accounts for every component found:
|
|
398
|
+
how many can mount in the browser profiling host, how many cannot and why, and
|
|
399
|
+
which files yielded no component at all. Read it before quoting a coverage
|
|
400
|
+
number — a file count is not coverage.
|
|
401
|
+
|
|
402
|
+
A green generated suite means "nothing obviously broken", not "covered" — the
|
|
403
|
+
value is in the scaffolding you harden, and in the findings.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flawless-react-rules
|
|
3
|
+
description: Run and act on flawless-react's react-rules task — a curated set of Biome correctness and suspicious rules (render-return, children-prop, hook order, exhaustive deps, iterable keys, duplicate JSX props, `==`, `any`) swept with a scratch config that bypasses the project's biome.json. Use when triaging `reports/react-rules.json`, when deciding whether an index key is a real bug, when the same effect is reported by both biome and `perf-static`, or when a file produces a `parse` diagnostic instead of findings. For a11y rules see flawless-a11y; for the suppression rules shared by both sweeps see flawless-findings.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# flawless react-rules
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bunx @lariocpt/flawless-react "src/**/*.tsx" --tasks react-rules
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`react-rules` is one biome sweep with a hand-picked rule set and nothing else:
|
|
13
|
+
`recommended: false`, then `correctness` and `suspicious` blocks enumerated rule by
|
|
14
|
+
rule. The report's own note says it — *"Curated React correctness rules via Biome.
|
|
15
|
+
Local biome.json is bypassed."* — because the point is to answer "does this React
|
|
16
|
+
code have a known defect", not "does this repo agree with its own lint config".
|
|
17
|
+
|
|
18
|
+
Findings print as `file:line:col lint/<group>/<rule> message` and land in the single
|
|
19
|
+
aggregate `flawless/reports/react-rules.json`. biome must be resolvable (the target
|
|
20
|
+
package's `node_modules`, any ancestor, or `PATH`); when it is not, the task fails
|
|
21
|
+
naming every place it looked.
|
|
22
|
+
|
|
23
|
+
## What is enabled, and what to do about it
|
|
24
|
+
|
|
25
|
+
Errors unless marked otherwise.
|
|
26
|
+
|
|
27
|
+
**`correctness`** — `noChildrenProp`, `noConstAssign`, `noConstantCondition`,
|
|
28
|
+
`noEmptyPattern`, `noInvalidUseBeforeDeclaration`, `noPrecisionLoss`,
|
|
29
|
+
`noRenderReturnValue`, `noVoidElementsWithChildren`, `noVoidTypeReturn`,
|
|
30
|
+
`useExhaustiveDependencies`, `useHookAtTopLevel`, `useIsNan`, `useJsxKeyInIterable`,
|
|
31
|
+
`useValidForDirection`.
|
|
32
|
+
|
|
33
|
+
**`suspicious`** — `noAssignInExpressions`, `noAsyncPromiseExecutor`,
|
|
34
|
+
`noCompareNegZero`, `noControlCharactersInRegex`, `noDuplicateCase`,
|
|
35
|
+
`noDuplicateClassMembers`, `noDuplicateJsxProps`, `noDuplicateObjectKeys`,
|
|
36
|
+
`noDuplicateParameters`, `noFallthroughSwitchClause`, `noGlobalIsFinite`,
|
|
37
|
+
`noGlobalIsNan`, `noLabelVar`, `noMisleadingCharacterClass`,
|
|
38
|
+
`noMisleadingInstantiator`; and as **warnings**: `noArrayIndexKey`,
|
|
39
|
+
`noConfusingVoidType`, `noEmptyBlockStatements`, `noExplicitAny`,
|
|
40
|
+
`noExtraNonNullAssertion`, `noDoubleEquals`, `noImplicitAnyLet`.
|
|
41
|
+
|
|
42
|
+
The ones that recur on a real codebase, in the order worth spending time on:
|
|
43
|
+
|
|
44
|
+
- **`useHookAtTopLevel`** — a hook behind an early return is a genuine bug: the hook
|
|
45
|
+
order changes between renders and React throws on the state update after. Fix by
|
|
46
|
+
moving the guard below the hooks or returning the placeholder from the same render
|
|
47
|
+
path.
|
|
48
|
+
- **`useExhaustiveDependencies`** — biome's resolution of the same question
|
|
49
|
+
`perf-static` asks as `perf/hook-missing-dependency`. The two engines do not share
|
|
50
|
+
an implementation, so a fix that silences one should be checked against the other
|
|
51
|
+
rather than assumed to cover it.
|
|
52
|
+
- **`noArrayIndexKey`** — real for an editable list: add a client-side id at
|
|
53
|
+
creation, key on it, strip the id at submit. A reasoned suppression is right for a
|
|
54
|
+
server-static list, and for anything where the index *is* the domain identity.
|
|
55
|
+
"It's only a list of labels" is not that reason.
|
|
56
|
+
- **`noRenderReturnValue` / `noChildrenProp` / `noVoidElementsWithChildren`** —
|
|
57
|
+
defects that break at runtime, not style.
|
|
58
|
+
- **`noDuplicateJsxProps`** — the later literal silently wins; the fix is usually
|
|
59
|
+
deleting the first, and reading why both were there.
|
|
60
|
+
- **`noExplicitAny` / `noDoubleEquals` / `noEmptyBlockStatements`** (warnings) — the
|
|
61
|
+
honest options are fix, or suppress with a reason, or leave it and let the ledger
|
|
62
|
+
count stay flat. Quietly adding `// biome-ignore` to reach zero is the one option
|
|
63
|
+
that makes the codebase worse.
|
|
64
|
+
|
|
65
|
+
## A parse failure is not a clean file
|
|
66
|
+
|
|
67
|
+
Biome reports `parse/*` diagnostics for a file it cannot read, and a category filter
|
|
68
|
+
would drop them — so every sweep would call an unreadable file clean. The runner
|
|
69
|
+
lets a parse diagnostic through the filter on purpose: **a file the analyser could
|
|
70
|
+
not read is never a pass.** Triage one like a finding: usually syntax biome's version
|
|
71
|
+
does not know, sometimes a file that should not have been in the sweep at all.
|
|
72
|
+
|
|
73
|
+
Diagnostics for files outside the swept set are dropped, so a sweep never claims
|
|
74
|
+
findings in code it was not asked about.
|
|
75
|
+
|
|
76
|
+
## Suppression hygiene
|
|
77
|
+
|
|
78
|
+
`suppressions/*` diagnostics — biome's own "this suppression is unused" complaints —
|
|
79
|
+
are **dropped from both sweeps**, because the sweep runs a scratch config with only
|
|
80
|
+
its own rules and would otherwise punish you for answering a finding belonging to
|
|
81
|
+
your project's config. Your project's `biome ci` does not drop them. How to make the
|
|
82
|
+
two agree is in flawless-findings.
|
|
83
|
+
|
|
84
|
+
## Under `--expo`
|
|
85
|
+
|
|
86
|
+
`react-rules` runs on React Native — same rules, same aggregate file. Rules that only
|
|
87
|
+
exist for DOM elements (`noVoidElementsWithChildren`) cannot match native JSX, so a
|
|
88
|
+
lower count there is expected and is not a coverage loss. See flawless-expo.
|
|
89
|
+
|
|
90
|
+
## Never
|
|
91
|
+
|
|
92
|
+
- Never treat "not enabled in my project's biome.json" as a reason to dismiss a
|
|
93
|
+
finding: this sweep bypasses that config by design.
|
|
94
|
+
- Never bulk-suppress `noExplicitAny` to make the number move.
|
|
95
|
+
- Never count findings from the reports directory — use the task summary or
|
|
96
|
+
REPORT.md (flawless-findings).
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flawless-regen
|
|
3
|
+
description: Regenerate a flawless-react suite safely — the generated/refreshed/kept lifecycle, what a re-run rewrites vs keeps, --force's merge semantics and the colocated-spec provenance stamp, --changed's affected-test narrowing, and proving a run happened via result JSON + REPORT.md rather than stdout. Use before any regeneration over an adopted suite, after upgrading the tool, or when a --changed run appears to have done nothing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Regenerating a flawless suite
|
|
7
|
+
|
|
8
|
+
> **Expo mode adds one write-once file.** `<outDir>/jest.setup.ts` is where
|
|
9
|
+
> hand-written module mocks live and nothing can rebuild them from the analysis, so a
|
|
10
|
+
> re-run never overwrites it — delete it to regenerate. `jest.config.cjs` and every
|
|
11
|
+
> native spec under `unit/` ARE regenerated on each run, like the rest of the derived
|
|
12
|
+
> suite.
|
|
13
|
+
|
|
14
|
+
The model is regenerate-by-default: derived files track the tool and your
|
|
15
|
+
sources, and only what the tool cannot rebuild is protected. Know the table
|
|
16
|
+
before touching an adopted suite:
|
|
17
|
+
|
|
18
|
+
| Category | Files | On re-run | Under `--force` |
|
|
19
|
+
| --- | --- | --- | --- |
|
|
20
|
+
| Write-once | `budgets.json`, `host/main.tsx` (wrapper only — the `host/registry.ts` beside it regenerates), `provider.fixtures.ts`, `e2e/global-setup.ts`, `mocks/handlers.ts`, `mock/routes.ts` | kept | **kept** |
|
|
21
|
+
| Merge-kept | `e2e/params.json`, `load/routes.pipes` | kept | **merged** — your edits, rates, commented pipes survive |
|
|
22
|
+
| Colocated specs | `src/**/*.flawless.test.tsx` | kept | **stamp-checked** — untouched refreshed; edited or pre-stamp kept and listed |
|
|
23
|
+
| Everything else | out-dir specs, configs, generated prose, REPORT.md, sidecars | rewritten | rewritten |
|
|
24
|
+
|
|
25
|
+
## The no-force run over an existing suite
|
|
26
|
+
|
|
27
|
+
**Exits 0.** Every task — `perf-harness` included — refreshes its derived
|
|
28
|
+
files, rewriting them as a fresh scaffold would (the standalone suite's
|
|
29
|
+
package.json is MERGED — your fields win, and in Next.js mode a
|
|
30
|
+
`flawless:*` script you customised is kept unless its value is one of the
|
|
31
|
+
tool's own spellings — kept keys are listed in REPORT.md as
|
|
32
|
+
`> Kept script(s): ...`, and deleting the key takes the tool's current
|
|
33
|
+
command); write-once files are
|
|
34
|
+
untouched (delete one to get a fresh copy). The perf-harness TaskResult and
|
|
35
|
+
REPORT.md's task row carry a status word: `generated` (fresh out dir),
|
|
36
|
+
`refreshed` (this case), or `kept` (`--changed`, below). Emission is
|
|
37
|
+
deterministic, so a same-version refresh usually diffs nothing; after a tool
|
|
38
|
+
upgrade expect the derived files — and the version banner on the suite's
|
|
39
|
+
`playwright.config.ts` first line — to move. An exit 1 means a real
|
|
40
|
+
failure, never the old harness refusal (that was findings FR-028, fixed).
|
|
41
|
+
|
|
42
|
+
A regeneration with a wave-3 tool also BURNS DOWN hand-work: the three
|
|
43
|
+
biggest `TODO(flawless)` clusters are now auto-filled where the source
|
|
44
|
+
supports inference — server-action stubs from the action's own literal
|
|
45
|
+
returns, data-dep fixtures from `Promise<T>` annotations or OpenAPI
|
|
46
|
+
examples, and compound parts mounted through their own file's family.
|
|
47
|
+
REPORT.md's hand-work table shows the drop; the exact count depends on how
|
|
48
|
+
much of the app's surface is inferable, so never promise a number —
|
|
49
|
+
markers that remain are exactly the cases inference honestly refused.
|
|
50
|
+
|
|
51
|
+
## `--force` and the colocated stamp
|
|
52
|
+
|
|
53
|
+
Every colocated spec opens with a provenance stamp —
|
|
54
|
+
`// @flawless-generated sha256:<hex16> flawless-react@<version>` — hashing
|
|
55
|
+
everything below the line (recomputed after the run's style pass, so a
|
|
56
|
+
formatted spec still reads as untouched). `--force` uses it as the verdict:
|
|
57
|
+
|
|
58
|
+
- stamp matches body → **rewritten** (this is the refresh you asked for);
|
|
59
|
+
- stamp mismatch → you edited or reformatted it: **kept**, listed in the
|
|
60
|
+
summary as hand-edited;
|
|
61
|
+
- no stamp (generated pre-stamp, or the line was deleted) → **kept**, listed
|
|
62
|
+
as pre-stamp.
|
|
63
|
+
|
|
64
|
+
The escape hatch for both kept cases is the same: delete the file and re-run.
|
|
65
|
+
Kept files also drop out of `handWork` tallies — their discarded render's
|
|
66
|
+
TODOs are not your debt. Still commit (or stash) before forcing: the stamp
|
|
67
|
+
protects hand-edited specs, but the rewrite of untouched ones is exactly what
|
|
68
|
+
you're asking for, and a diff you can read beats trusting any tool.
|
|
69
|
+
|
|
70
|
+
Expect new spec files for components the analyser newly detected — their
|
|
71
|
+
browser specs mount through `host/registry.ts`, which the same run refreshed;
|
|
72
|
+
hand registrations live in `main.tsx`'s `ALL` spread and survive (see
|
|
73
|
+
flawless-unit-browser). A pre-split `main.tsx` (no `./registry` import)
|
|
74
|
+
triggers the run's migration warning instead.
|
|
75
|
+
|
|
76
|
+
## `--changed`
|
|
77
|
+
|
|
78
|
+
`--changed` narrows only the RUN: vitest `related` walks the module graph for
|
|
79
|
+
unit specs, playwright gets an anchored `--grep`, e2e runs when an affected
|
|
80
|
+
component renders on a route. Over an existing suite the perf harness is
|
|
81
|
+
**kept** — the scaffold writes nothing (`status: "kept"`, summary `harness
|
|
82
|
+
kept (generated by flawless-react <X>, tool is <Y>)`, with X read from the
|
|
83
|
+
suite's own banner; pre-banner suites report "unknown version"). The affected
|
|
84
|
+
run is still gated on no task having failed, but a kept harness is a success,
|
|
85
|
+
so any task selection — the full seven included — reaches the affected tests:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
flawless-react "src/**/*.tsx" --nextjs . --tasks perf-harness,unit-browser,perf-static,a11y,react-rules,e2e,load --changed --no-interactive
|
|
89
|
+
# --tasks matters: bare --no-interactive defaults to perf-harness ALONE, and
|
|
90
|
+
# a harness-only selection has no unit/e2e suites for the affected run to run.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`--changed --force` regenerates: an explicit force outranks the keep. Suite
|
|
94
|
+
failures reach the exit code (a failed affected suite exits 1), so
|
|
95
|
+
`--ci --changed` in a hook is honest.
|
|
96
|
+
|
|
97
|
+
## Proving a run happened
|
|
98
|
+
|
|
99
|
+
Never take stdout as evidence — write evidence to files and check the disk:
|
|
100
|
+
|
|
101
|
+
- **Programmatic runs** (sandboxes, scripts): call `runFlawless`/`runCore` and
|
|
102
|
+
write `{ exitCode, outDir, results }` to a result file. Pass `nextjs`
|
|
103
|
+
relative to a cwd you set explicitly, or pass an explicit out dir — in
|
|
104
|
+
Next.js mode the default resolves against the target root, and a scratch
|
|
105
|
+
run from the wrong cwd rewrites the target's `package.json` scripts.
|
|
106
|
+
- REPORT.md is rewritten every run — a missing REPORT.md diff means the run
|
|
107
|
+
did not happen or ran somewhere else.
|
|
108
|
+
- `git status --porcelain` in the target must match the expected touch surface
|
|
109
|
+
for the run kind (tiny for no-force; `--force` touches the untouched
|
|
110
|
+
colocated specs, while the ones you hardened stay out of the diff).
|
|
111
|
+
- Another session may be working the same repos: check `git log -1` against
|
|
112
|
+
what you last saw before attributing a surprise diff to the tool.
|
|
113
|
+
|
|
114
|
+
## Style adoption
|
|
115
|
+
|
|
116
|
+
Generated files are reformatted with the host project's biome or prettier
|
|
117
|
+
(including `.mts`/`.cts` configs), and JSON merges preserve the file's
|
|
118
|
+
indentation — a regeneration should produce a semantic diff, never a reformat.
|
|
119
|
+
If `biome ci` errors on a generated file after a regen, that is a tool bug;
|
|
120
|
+
report it rather than hand-formatting generated output.
|
|
121
|
+
|
|
122
|
+
## Never
|
|
123
|
+
|
|
124
|
+
- Never `--force` an adopted suite with uncommitted work — the stamp keeps
|
|
125
|
+
hand-edited colocated specs, but e2e route specs and the provider wrapper
|
|
126
|
+
have no such guard, and a committed baseline is what makes any rewrite
|
|
127
|
+
reviewable.
|
|
128
|
+
- Never hand-delete a spec's stamp line to "claim" it — that demotes the file
|
|
129
|
+
to pre-stamp (kept but nagged about); the file is already yours the moment
|
|
130
|
+
you edit the body.
|
|
131
|
+
- Never read a mid-session change to the target app as proof your run
|
|
132
|
+
executed — only your own result file plus the REPORT.md diff counts.
|
|
133
|
+
- Never reach for `--force` on a plain refresh — a no-force re-run already
|
|
134
|
+
rewrites every derived file, and `--force` adds only the untouched-colocated
|
|
135
|
+
/e2e-spec rewrite. Decide whether you want that before asking for it.
|