react-doctor 0.2.3 → 0.2.5

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/README.md CHANGED
@@ -9,528 +9,44 @@
9
9
 
10
10
  Your agent writes bad React, this catches it.
11
11
 
12
- One command scans your codebase and outputs a **0 to 100 health score** with actionable diagnostics.
12
+ React Doctor deterministically scans your codebase and finds issues across state & effects, performance, architecture, security, and accessibility.
13
13
 
14
- Works with Next.js, Vite, and React Native.
14
+ Works for all React frameworks and libraries - Next.js, Vite, TanStack, React Native, Expo, you name it.
15
15
 
16
- ### [See it in action →](https://react.doctor)
16
+ [Website →](https://react.doctor/docs)
17
17
 
18
18
  ## Install
19
19
 
20
- Run this at your project root:
20
+ ### 1. Quick start
21
+
22
+ Run this at your project root to get an audit.
21
23
 
22
24
  ```bash
23
25
  npx react-doctor@latest
24
26
  ```
25
27
 
26
- You'll get a score (75+ Great, 50 to 74 Needs work, under 50 Critical) and a list of issues across state & effects, performance, architecture, security, and accessibility. Rules toggle automatically based on your framework and React version.
27
-
28
- > **Migration note:** React Doctor used to bundle [knip](https://knip.dev/) for dead-code detection. That integration was removed in v0.2 — if you want dead-code analysis, run `npx knip` directly as part of your own pre-commit or CI pipeline.
29
-
30
28
  https://github.com/user-attachments/assets/07cc88d9-9589-44c3-aa73-5d603cb1c570
31
29
 
32
- ## Install for your coding agent
30
+ ### 2. Install for agents
33
31
 
34
- Teach your coding agent React best practices so it stops writing the bad code in the first place.
32
+ Once you have an audit, you can install the skill for your coding agent to learn from the issues and fix them in the future.
35
33
 
36
34
  ```bash
37
35
  npx react-doctor@latest install
38
36
  ```
39
37
 
40
- You'll be prompted to pick which detected agents to install for. Pass `--yes` to skip prompts.
41
-
42
- Works with Claude Code, Cursor, Codex, OpenCode, and 50+ other agents.
43
-
44
- ## GitHub Actions
45
-
46
- A composite action ships with this repository. Drop it into `.github/workflows/react-doctor.yml`:
47
-
48
- ```yaml
49
- name: React Doctor
50
-
51
- on:
52
- pull_request:
53
- push:
54
- branches: [main]
55
-
56
- permissions:
57
- contents: read
58
- pull-requests: write # required to post PR comments
59
-
60
- jobs:
61
- react-doctor:
62
- runs-on: ubuntu-latest
63
- steps:
64
- - uses: actions/checkout@v5
65
- with:
66
- fetch-depth: 0 # required for `diff`
67
- - uses: millionco/react-doctor@main
68
- with:
69
- diff: main
70
- github-token: ${{ secrets.GITHUB_TOKEN }}
71
- ```
72
-
73
- When `github-token` is set on `pull_request` events, findings are posted (and updated) as a sticky PR comment. The action also exposes a `score` output (0–100) you can read in subsequent steps — see [PR blocking and exit codes](#pr-blocking-and-exit-codes) for a score-floor recipe.
74
-
75
- **Inputs:** `directory`, `verbose`, `project`, `diff`, `github-token`, `fail-on` (`error` / `warning` / `none`), `offline`, `annotations`, `node-version`. See [`action.yml`](https://github.com/millionco/react-doctor/blob/main/action.yml) for full descriptions.
76
-
77
- #### PR feedback modes
78
-
79
- Pick one or both; they're independent.
80
-
81
- - **Comments only** (default): set `github-token`.
82
- - **Annotations only**: set `annotations: true`.
83
- - **Both**: set `github-token` and `annotations: true`. Annotation lines are stripped from the comment body.
84
-
85
- ```yaml
86
- - uses: millionco/react-doctor@main
87
- with:
88
- diff: main
89
- github-token: ${{ secrets.GITHUB_TOKEN }}
90
- annotations: true
91
- ```
92
-
93
- Prefer not to add a marketplace action? The bare `npx` form works too:
94
-
95
- ```yaml
96
- - run: npx react-doctor@latest --fail-on warning
97
- ```
98
-
99
- ## PR blocking and exit codes
100
-
101
- Two independent gates can block a PR — pick one or both:
102
-
103
- - **`--fail-on <level>`** exits non-zero on diagnostics: `error` (default, any error-severity rule fires), `warning` (any diagnostic fires), or `none` (never). Runs against the `ciFailure` surface, so the default `design`-tag exclusion still applies.
104
- - **Score floor** — a follow-up step that reads the action's `score` output and `exit 1`s when it's below your threshold.
105
-
106
- Combine `--fail-on` with `--diff <base>` to scope the gate to the PR's changed files only — that's the built-in way to fail on **new** regressions without dragging in baseline backlog. There is no separate `--fail-on-new` flag.
107
-
108
- `--annotations` (bare `npx` only) and `github-token` (sticky PR comment) are visualization layers and never change the exit code.
109
-
110
- ### Examples
111
-
112
- **Advisory mode** — never blocks, always comments:
113
-
114
- ```yaml
115
- - uses: millionco/react-doctor@main
116
- with:
117
- github-token: ${{ secrets.GITHUB_TOKEN }}
118
- fail-on: none
119
- ```
120
-
121
- **Regression-only mode** — fail only on new diagnostics introduced by the PR:
122
-
123
- ```yaml
124
- - uses: actions/checkout@v5
125
- with:
126
- fetch-depth: 0 # required for `diff`
127
- - uses: millionco/react-doctor@main
128
- with:
129
- diff: main
130
- fail-on: warning
131
- github-token: ${{ secrets.GITHUB_TOKEN }}
132
- ```
133
-
134
- **Strict threshold mode** — fail when the baseline score drops below a floor:
135
-
136
- ```yaml
137
- - id: doctor
138
- uses: millionco/react-doctor@main
139
- with:
140
- fail-on: error
141
- github-token: ${{ secrets.GITHUB_TOKEN }}
142
- - env:
143
- SCORE: ${{ steps.doctor.outputs.score }}
144
- FLOOR: "80"
145
- run: |
146
- # `score` is best-effort and may be empty (e.g. when offline is on).
147
- # Skip the floor when it's empty so unrelated PRs aren't blocked.
148
- if [ -z "$SCORE" ]; then
149
- echo "::notice::React Doctor score unavailable — skipping floor check"
150
- exit 0
151
- fi
152
- if [ "$SCORE" -lt "$FLOOR" ]; then
153
- echo "::error::React Doctor score $SCORE is below floor $FLOOR"
154
- exit 1
155
- fi
156
- ```
157
-
158
- Pin a specific `react-doctor` version when using a score floor — new rule releases can lower the score even when your code hasn't changed (see [Scoring](#scoring)).
159
-
160
- ## Configuration
161
-
162
- Create a `react-doctor.config.json` in your project root:
163
-
164
- ```json
165
- {
166
- "ignore": {
167
- "rules": ["react-doctor/no-danger", "react-doctor/no-autofocus"],
168
- "files": ["src/generated/**"],
169
- "overrides": [
170
- {
171
- "files": ["components/modules/diff/**"],
172
- "rules": ["react-doctor/no-array-index-as-key", "react-doctor/no-render-in-render"]
173
- },
174
- {
175
- "files": ["components/search/HighlightedSnippet.tsx"],
176
- "rules": ["react-doctor/no-danger"]
177
- }
178
- ]
179
- }
180
- }
181
- ```
182
-
183
- Three nested keys, three layers of granularity — pick the narrowest one that fits:
184
-
185
- - **`ignore.rules`** silences a rule across the whole codebase.
186
- - **`ignore.files`** silences **every** rule on the matched files (use sparingly — it loses coverage for unrelated rules).
187
- - **`ignore.overrides`** silences only the listed rules on the matched files, leaving every other rule active. This is what you want when a single file (or glob) legitimately needs an exemption from one or two rules but should still be scanned for everything else.
188
-
189
- You can also use the `"reactDoctor"` key in `package.json`. CLI flags always override config values.
190
-
191
- React Doctor respects `.gitignore`, `.eslintignore`, `.oxlintignore`, `.prettierignore`, and `linguist-vendored` / `linguist-generated` annotations in `.gitattributes`. Inline `// eslint-disable*` and `// oxlint-disable*` comments are honored too.
192
-
193
- If you have a JSON oxlint or eslint config (`.oxlintrc.json` or `.eslintrc.json`), its rules get merged into the scan automatically and count toward the score. Set `adoptExistingLintConfig: false` to opt out.
194
-
195
- #### Surface controls (CLI, PR comments, score, CI failure)
196
-
197
- Diagnostics flow through four independent surfaces — `cli`, `prComment`, `score`, and `ciFailure` — and each one can be tuned per tag, category, or rule id. By default the `design` tag (Tailwind shorthand cleanup like `w-5 h-5 → size-5`, pure-black backgrounds, gradient text, …) stays visible on the local CLI but is excluded from the PR comment, the score, and the `--fail-on` gate so style cleanup can't dilute meaningful React findings:
198
-
199
- ```json
200
- {
201
- "surfaces": {
202
- "prComment": {
203
- "includeTags": ["design"],
204
- "excludeCategories": ["Performance"]
205
- },
206
- "score": { "includeRules": ["react-doctor/design-no-redundant-size-axes"] },
207
- "ciFailure": { "excludeTags": ["test-noise"] }
208
- }
209
- }
210
- ```
211
-
212
- Each surface accepts `includeTags`, `excludeTags`, `includeCategories`, `excludeCategories`, `includeRules`, and `excludeRules`. Include wins over exclude when both match. Run the CLI with `--pr-comment` (the GitHub Action passes it automatically when `github-token` is set) to apply the `prComment` surface to the printed output destined for sticky PR comments.
213
-
214
- #### Rule severity (`rules`, `categories`)
215
-
216
- Same shape as ESLint / oxlint. `rules` is ESLint's exact field; `categories` mirrors oxlint's, keyed by React Doctor display categories (`"React Native"`, `"Server"`, `"Architecture"`, …).
217
-
218
- ```json
219
- {
220
- "rules": { "react-doctor/no-array-index-as-key": "error" },
221
- "categories": { "React Native": "warn" }
222
- }
223
- ```
224
-
225
- Per-rule wins over per-category. `"off"` short-circuits before the rule runs; `"warn"` / `"error"` re-stamps the diagnostic so every channel — CLI, PR comment, score, `--fail-on` — sees the chosen severity, including for external-plugin rules. Use `surfaces` instead when you only want to hide a rule from one channel; use `ignore.tags` to silence a whole tag-defined family (`"design"`, `"test-noise"`, `"migration-hint"`) that doesn't align with a single category.
226
-
227
- #### Optional companion plugins
228
-
229
- When the following ESLint plugins are installed in the scanned project (or hoisted in your monorepo), React Doctor folds their rules into the same scan. Listed as **optional peer dependencies** — install only what you want.
230
-
231
- | Plugin | Adds | Namespace |
232
- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------ |
233
- | [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) (v6 or v7) | The React Compiler frontend's correctness rules — fired when a React Compiler is detected in the project. | `react-hooks-js/*` |
38
+ Works with Claude Code, Cursor, Codex, OpenCode, and many more.
234
39
 
235
- The 8 rules from [`eslint-plugin-react-you-might-not-need-an-effect`](https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect) (NickvanDyke, MIT) are now ported natively into React Doctor — they fire as `react-doctor/no-derived-state`, `react-doctor/no-chain-state-updates`, `react-doctor/no-event-handler`, `react-doctor/no-adjust-state-on-prop-change`, `react-doctor/no-reset-all-state-on-prop-change`, `react-doctor/no-pass-live-state-to-parent`, `react-doctor/no-pass-data-to-parent`, and `react-doctor/no-initialize-state`. No peer dependency required.
236
-
237
- ### Inline suppressions
238
-
239
- ```tsx
240
- // react-doctor-disable-next-line react-doctor/no-cascading-set-state
241
- useEffect(() => {
242
- setA(value);
243
- setB(value);
244
- }, [value]);
245
- ```
246
-
247
- When two rules fire on the same line, you have two equivalent options. Comma-separate the rule ids on a single comment:
248
-
249
- ```tsx
250
- // react-doctor-disable-next-line react-doctor/rerender-state-only-in-handlers, react-doctor/no-derived-useState
251
- const [localSearch, setLocalSearch] = useState(searchQuery);
252
- ```
253
-
254
- Or stack one comment per rule directly above the diagnostic. Stacked comments are honored as long as nothing but other `react-doctor-disable-next-line` comments sits between them and the target line:
255
-
256
- ```tsx
257
- // react-doctor-disable-next-line react-doctor/rerender-state-only-in-handlers
258
- // react-doctor-disable-next-line react-doctor/no-derived-useState
259
- const [localSearch, setLocalSearch] = useState(searchQuery);
260
- ```
261
-
262
- A code line between stacked comments breaks the chain: only the comment immediately above the diagnostic (and any contiguous `react-doctor-disable-next-line` comments stacked on top of it) is honored. If a comment looks adjacent but the rule still fires, run `react-doctor --explain <file:line>` — it reports whether a nearby suppression was found, what rules it covers, and why it didn't apply.
263
-
264
- Block comments work inside JSX:
265
-
266
- <!-- prettier-ignore -->
267
- ```tsx
268
- {/* react-doctor-disable-next-line react-doctor/no-danger */}
269
- <div dangerouslySetInnerHTML={{ __html }} />
270
- ```
271
-
272
- For multi-line JSX, putting the comment immediately above the opening tag covers the entire attribute list (matching ESLint convention).
273
-
274
- ## Lint plugin (standalone)
275
-
276
- The same rule set ships as both an oxlint plugin and an ESLint plugin, so you can wire it into whichever lint engine your project already runs. These are published as separate packages, so you can install just the lint integration without pulling in the full CLI.
277
-
278
- **oxlint** in `.oxlintrc.json` (install [`oxlint-plugin-react-doctor`](https://npmjs.com/package/oxlint-plugin-react-doctor)):
279
-
280
- ```jsonc
281
- {
282
- "jsPlugins": [{ "name": "react-doctor", "specifier": "oxlint-plugin-react-doctor" }],
283
- "rules": {
284
- "react-doctor/no-fetch-in-effect": "warn",
285
- "react-doctor/no-derived-state-effect": "warn",
286
- },
287
- }
288
- ```
40
+ ### 3. Run in CI (GitHub Actions) for your team
289
41
 
290
- **ESLint** flat config (install [`eslint-plugin-react-doctor`](https://npmjs.com/package/eslint-plugin-react-doctor)):
42
+ [![GitHub Action](https://img.shields.io/badge/GitHub%20Action-React%20Doctor-000000?style=flat&labelColor=000000&logo=githubactions&logoColor=white)](https://github.com/marketplace/actions/react-doctor)
291
43
 
292
- ```js
293
- import reactDoctor from "eslint-plugin-react-doctor";
44
+ Add the reusable GitHub Action from Marketplace to scan every pull request, show inline annotations, and leave findings where reviewers already look.
294
45
 
295
- export default [
296
- reactDoctor.configs.recommended,
297
- reactDoctor.configs.next,
298
- reactDoctor.configs["react-native"],
299
- reactDoctor.configs["tanstack-start"],
300
- reactDoctor.configs["tanstack-query"],
301
- ];
302
- ```
303
-
304
- The full rule list lives in [`packages/oxlint-plugin-react-doctor/src/plugin/rules`](https://github.com/millionco/react-doctor/tree/main/packages/oxlint-plugin-react-doctor/src/plugin/rules).
305
-
306
- ## CLI reference
307
-
308
- ```
309
- Usage: react-doctor [directory] [options]
310
-
311
- Options:
312
- -v, --version display the version number
313
- --no-lint skip linting
314
- --verbose show every rule and per-file details (default shows top 3 rules)
315
- --score output only the score
316
- --json output a single structured JSON report
317
- -y, --yes skip prompts, scan all workspace projects
318
- --full skip prompts, always run a full scan
319
- --project <name> select workspace project (comma-separated for multiple)
320
- --diff [base] scan only files changed vs base branch
321
- --staged scan only staged files (for pre-commit hooks)
322
- --offline skip the score API and share URL (no score shown)
323
- --fail-on <level> exit with error on diagnostics: error, warning, none
324
- --annotations output diagnostics as GitHub Actions annotations
325
- --pr-comment tune CLI output for sticky PR comments (drops design
326
- cleanup from the printed list and fail-on gate)
327
- --explain <file:line> diagnose why a rule fired or why a suppression didn't apply
328
- --why <file:line> alias for --explain
329
- -h, --help display help
330
- ```
331
-
332
- When a suppression isn't working, `--explain <file:line>` (or its alias `--why <file:line>`) reports what the scanner sees at that location, including why a nearby `react-doctor-disable-next-line` didn't apply. The diagnosis distinguishes the common failure modes — adjacent comment for a different rule (use the comma form), a code line between the comment and the diagnostic (the chain is broken), or no nearby suppression at all. The same hint surfaces inline with `--verbose` for every flagged site, and in `--json` output as `diagnostic.suppressionHint`, so a single scan doubles as a suppression audit without a separate flag.
333
-
334
- `--json` produces a parsable object on stdout with all human-readable output suppressed. Errors still produce a JSON object with `ok: false`, so stdout is always a valid document.
335
-
336
- ### Config keys
337
-
338
- | Key | Type | Default |
339
- | -------------------------- | -------------------------------- | -------- |
340
- | `ignore.rules` | `string[]` | `[]` |
341
- | `ignore.files` | `string[]` | `[]` |
342
- | `ignore.overrides` | `{ files, rules? }[]` | `[]` |
343
- | `lint` | `boolean` | `true` |
344
- | `verbose` | `boolean` | `false` |
345
- | `diff` | `boolean \| string` | |
346
- | `failOn` | `"error" \| "warning" \| "none"` | `"none"` |
347
- | `customRulesOnly` | `boolean` | `false` |
348
- | `share` | `boolean` | `true` |
349
- | `offline` | `boolean` | `false` |
350
- | `textComponents` | `string[]` | `[]` |
351
- | `rawTextWrapperComponents` | `string[]` | `[]` |
352
- | `serverAuthFunctionNames` | `string[]` | `[]` |
353
- | `respectInlineDisables` | `boolean` | `true` |
354
- | `adoptExistingLintConfig` | `boolean` | `true` |
355
- | `ignore.tags` | `string[]` | `[]` |
356
-
357
- `textComponents` is the broad escape hatch for `rn-no-raw-text` — list components that themselves behave like React Native's `<Text>` (custom `Typography`, `NativeTabs.Trigger.Label`, etc.) and the rule will treat them as text containers regardless of what their children look like.
358
-
359
- `rawTextWrapperComponents` is the narrower option for components that are not text elements but safely route string-only children through an internal `<Text>` (e.g. `heroui-native`'s `Button`, which stringifies its children and renders them through a `ButtonLabel`). Listed wrappers suppress `rn-no-raw-text` only when their children are entirely stringifiable. A wrapper with mixed children — e.g. `<Button>Save<Icon /></Button>` — still reports because the wrapper can't safely route raw text alongside a sibling JSX element.
360
-
361
- `serverAuthFunctionNames` teaches `server-auth-actions` about custom auth guards your codebase wraps around its auth library (e.g. `requireWorkspaceMember`, `ensureSignedIn`). Listed names are accepted as a valid top-of-action auth check whether called bare (`requireWorkspaceMember()`) or as a member (`guards.requireWorkspaceMember()`), and — unlike the built-in default list — are treated as distinctive so the receiver is not re-validated.
362
-
363
- `ignore.tags` suppresses entire categories of rules by tag. For example, `"tags": ["design"]` disables all opinionated design rules (gradient text, pure black backgrounds, side tab borders, default Tailwind palettes). Available tags: `"design"`.
364
-
365
- `offline` skips the score API entirely — no score is shown and no share URL is generated. CI runs (GitHub Actions, GitLab CI, CircleCI) are not offline by default; only the share URL is suppressed. Set `offline: true` (or `--offline`) explicitly when you want zero network.
366
-
367
- ### React Native rules in mixed monorepos
368
-
369
- `rn-*` rules respect per-package boundaries automatically. In a mixed React Native + web monorepo (`apps/mobile` alongside `apps/web` / `apps/vite-app` / `packages/storybook` / `apps/docs`), every `rn-*` rule walks up to the file's nearest `package.json` before running:
370
-
371
- - Packages that declare `react-native`, `react-native-tvos`, `expo`, `expo-router`, `@expo/*`, `react-native-windows`, `react-native-macos`, anything under the `@react-native/` or `@react-native-` namespaces (`@react-native-firebase/app`, `@react-native-async-storage/async-storage`, …), or Metro's top-level `"react-native"` resolution field → rules ON.
372
- - Packages that declare a web-only framework (`next`, `vite`, `react-scripts`, `gatsby`, `@remix-run/*`, `@docusaurus/*`, `@storybook/*`, or plain `react-dom` without an RN sibling) → rules OFF.
373
- - Packages with no clear local signal → fall back to the project-level framework detection.
374
-
375
- File extensions override the package classification when they're unambiguous: `*.web.tsx` / `*.web.jsx` are always skipped (Metro resolves these only against `react-native-web`); `*.ios.tsx` / `*.android.tsx` / `*.native.tsx` are always scanned (mobile-only).
376
-
377
- The detection is bidirectional: a web-rooted monorepo (root `package.json` declares `next` or `vite`) still loads the `rn-*` rules when any workspace targets React Native — the file-level boundary then keeps them silent on the web workspaces and active on the mobile ones.
378
-
379
- `rn-no-raw-text` additionally short-circuits raw text inside platform-fork branches:
380
-
381
- - `if (Platform.OS === "web") { … }` consequent — and the `else` branch of `if (Platform.OS !== "web")`.
382
- - `Platform.OS === "web" ? <X /> : …` ternaries, `Platform.OS === "web" && <X />` short-circuits, and the reversed-operand form `"web" === Platform.OS`.
383
- - `switch (Platform.OS) { case "web": … }` case bodies (other cases still report).
384
- - `Platform.select({ web: <X />, default: <Y /> })` — only the `web` arm is exempt.
385
- - `Platform?.OS === "web"` (optional chain) and `Platform.OS! === "web"` (TS non-null assertion) parse the same way as the bare form.
386
-
387
- The walker stops at function and `Program` boundaries — JSX defined inside a callback hoisted out of a `Platform.OS` branch does not inherit the parent guard. Negative platform checks like `Platform.OS === "ios"` are deliberately NOT treated as web exemptions; only the explicit web branch is.
388
-
389
- ## Scoring
390
-
391
- The health score formula: `100 - (unique_error_rules x 1.5) - (unique_warning_rules x 0.75)`.
392
-
393
- Scoring runs on react.doctor's API and is **network-dependent**: without a successful API round-trip (or under `--offline`) the score is omitted and the rest of the report still renders normally. Score-based automation must treat an empty value as a no-op (see the strict-threshold example above). Key details:
394
-
395
- - The score counts **unique rules triggered**, not total instances. Fixing 49 of 50 `no-barrel-import` violations does not change the score; fixing all 50 removes the 0.75 penalty for that rule.
396
- - Error-severity rules cost 1.5 points each. Warning-severity rules cost 0.75 points each.
397
- - Category breakdowns shown in the output are for display only and do not weight the score.
398
-
399
- Score labels: 75+ is **Great**, 50 to 74 is **Needs work**, under 50 is **Critical**.
400
-
401
- Scores may decrease across releases as new rules are added. Each new rule that fires in your codebase introduces an additional penalty. This is expected — it means the tool is catching more issues, not that your code got worse. Pin to a specific react-doctor version in CI if you need stable scores across upgrades.
402
-
403
- ## Diff and staged modes
404
-
405
- React Doctor can scan only changed files instead of the full project:
406
-
407
- - **`--diff [base]`** scans files changed vs a base branch. Auto-detects `main`/`master`, or pass an explicit branch: `--diff develop`. Also available as a config key: `"diff": true` or `"diff": "develop"`.
408
- - **`--staged`** scans only files in the git staging area (index). Designed for pre-commit hooks — materializes staged file contents into a temp directory so the scan reflects exactly what will be committed.
409
- - **`--full`** forces a full scan, overriding any `diff` value in config or CLI.
410
-
411
- When on a feature branch without explicit flags, you'll be prompted: "Only scan changed files?" This prompt is suppressed in CI, `--json` mode, and non-interactive environments.
412
-
413
- `--staged` and `--diff` cannot be combined.
414
-
415
- ### Pre-commit hooks with Husky + lint-staged
416
-
417
- The most common setup is [Husky](https://typicode.github.io/husky/) for the git hook and [lint-staged](https://github.com/lint-staged/lint-staged) to filter which files run through each tool. React Doctor's `--staged` mode is built for this: it reads file contents from the git **index** (not the working tree) and materializes them into a temp directory, so partially-staged files are scanned exactly as they will be committed.
418
-
419
- Install both, then wire them up:
420
-
421
- ```bash
422
- npx ni -D husky lint-staged
423
- npx husky init
424
- ```
425
-
426
- `husky init` creates `.husky/pre-commit`. Replace its contents with:
427
-
428
- ```bash
429
- npx lint-staged
430
- ```
431
-
432
- Then add a `lint-staged` block to your `package.json`. Because React Doctor already filters to the staged set via `--staged`, **do not pass the lint-staged-injected file list** — invoke it with a single command and let it discover the index itself:
433
-
434
- ```json
435
- {
436
- "lint-staged": {
437
- "*.{ts,tsx,js,jsx}": "react-doctor --staged --fail-on warning"
438
- }
439
- }
440
- ```
441
-
442
- A few notes that bite people:
443
-
444
- - **Don't append `{staged-files}`** — lint-staged would otherwise pass the matched paths as positional arguments and you'd get the union (path filter + index scan) instead of the intent.
445
- - **Use the function form when you only want the hook to run if any matching file is staged** but still want a single project-wide scan:
446
-
447
- ```js
448
- // lint-staged.config.js
449
- export default {
450
- "*.{ts,tsx,js,jsx}": () => "react-doctor --staged --fail-on warning",
451
- };
452
- ```
453
-
454
- - **`--fail-on warning`** blocks the commit on any diagnostic. Use `--fail-on error` for a softer gate, or `--fail-on none` to lint advisory-only.
455
- - **Index vs. working tree:** `--staged` reflects `git diff --cached`, not your editor buffer. If you `git add` half a file and keep typing, only the added half is scanned — the unstaged tail is ignored.
456
- - **Skip in CI:** lint-staged is a pre-commit concern. In CI, use the GitHub Action (above) or `react-doctor --diff <base>` directly; running both does duplicate work.
457
- - **Other hook managers:** the same `react-doctor --staged --fail-on warning` command works under [Lefthook](https://lefthook.dev/), [pre-commit](https://pre-commit.com/), or a hand-written `.git/hooks/pre-commit` — `--staged` is hook-manager-agnostic.
458
-
459
- To bypass the hook for a one-off commit, use `git commit --no-verify`.
460
-
461
- ## Agent and CI integration
462
-
463
- React Doctor detects 50+ coding agents (Claude Code, Cursor, Codex, OpenCode, Windsurf, and more) and adapts its behavior automatically:
464
-
465
- - **Install for agents**: `npx react-doctor@latest install` writes agent-specific rule files (SKILL.md, AGENTS.md, .cursorrules) into your project so agents learn React best practices.
466
- - **JSON output**: `--json` produces a structured `JsonReport` on stdout. Errors still produce a valid JSON document with `ok: false`. Use `--json-compact` for minimal whitespace.
467
- - **Score-only output**: `--score` outputs just the numeric score (0-100), useful for threshold checks in agent loops.
468
- - **GitHub Actions annotations**: `--annotations` emits `::error` / `::warning` format for inline PR annotations. Annotations don't change the exit code.
469
- - **Exit codes**: `--fail-on error` (default) exits non-zero when error-severity diagnostics are found. Use `--fail-on warning` or `--fail-on none` to tune CI gating. See [PR blocking and exit codes](#pr-blocking-and-exit-codes) for the full model — including how to fail only on new regressions vs. fail on the baseline score.
470
- - **Programmatic API**: `import { diagnose } from "react-doctor/api"` for direct integration in scripts and automation.
471
-
472
- In CI environments, prompts are automatically skipped. Pass `--offline` explicitly when you need zero network.
473
-
474
- ## Node.js API
475
-
476
- ```js
477
- import { diagnose, toJsonReport, summarizeDiagnostics } from "react-doctor/api";
478
-
479
- const result = await diagnose("./path/to/your/react-project");
480
-
481
- console.log(result.score); // { score: 82, label: "Great" } or null
482
- console.log(result.diagnostics); // Diagnostic[]
483
- console.log(result.project); // detected framework, React version, etc.
484
- ```
485
-
486
- `diagnose` accepts a second argument: `{ lint?: boolean }`.
487
-
488
- ```js
489
- const report = toJsonReport(result, { version: "1.0.0" });
490
- const counts = summarizeDiagnostics(result.diagnostics);
491
- ```
492
-
493
- `react-doctor/api` re-exports `JsonReport`, `JsonReportSummary`, `JsonReportProjectEntry`, `JsonReportMode`, plus the lower-level `buildJsonReport` and `buildJsonReportError` builders. See [`packages/react-doctor/src/api.ts`](https://github.com/millionco/react-doctor/blob/main/packages/react-doctor/src/api.ts) for the full types.
494
-
495
- ## Leaderboard
496
-
497
- Top React codebases scanned by React Doctor, ranked by score. Updated automatically from [millionco/react-doctor-benchmarks](https://github.com/millionco/react-doctor-benchmarks).
498
-
499
- <!-- LEADERBOARD:START -->
500
- <!-- prettier-ignore -->
501
- | # | Repo | Score |
502
- | -- | ---- | ----: |
503
- | 1 | [executor](https://github.com/RhysSullivan/executor) | 96 |
504
- | 2 | [nodejs.org](https://github.com/nodejs/nodejs.org) | 86 |
505
- | 3 | [tldraw](https://github.com/tldraw/tldraw) | 71 |
506
- | 4 | [t3code](https://github.com/pingdotgg/t3code) | 69 |
507
- | 5 | [better-auth](https://github.com/better-auth/better-auth) | 64 |
508
- | 6 | [mastra](https://github.com/mastra-ai/mastra) | 63 |
509
- | 7 | [excalidraw](https://github.com/excalidraw/excalidraw) | 62 |
510
- | 8 | [payload](https://github.com/payloadcms/payload) | 60 |
511
- | 9 | [typebot](https://github.com/baptisteArno/typebot.io) | 57 |
512
- | 10 | [medusajs/admin](https://github.com/medusajs/medusa) | 56 |
513
-
514
- <!-- LEADERBOARD:END -->
515
-
516
- See the [full leaderboard](https://www.react.doctor/leaderboard).
517
-
518
- ## Resources & Contributing Back
519
-
520
- Want to try it out? Check out [the demo](https://react.doctor).
521
-
522
- Looking to contribute back? Clone the repo, install, build, and submit a PR.
523
-
524
- ```bash
525
- git clone https://github.com/millionco/react-doctor
526
- cd react-doctor
527
- pnpm install
528
- pnpm build
529
- node packages/react-doctor/bin/react-doctor.js /path/to/your/react-project
530
- ```
46
+ [Add GitHub Action →](https://github.com/marketplace/actions/react-doctor)
531
47
 
532
- Find a bug? Head to the [issue tracker](https://github.com/millionco/react-doctor/issues).
48
+ ## Contributing
533
49
 
534
- ### License
50
+ [Issues welcome!](https://github.com/millionco/react-doctor/issues)
535
51
 
536
- React Doctor is MIT-licensed open-source software.
52
+ MIT-licensed