react-doctor 0.1.6 → 0.2.0-beta.0

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,13 +9,13 @@
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 scans React projects with native codebase analysis, a curated oxlint rule set, and actionable diagnostics.
13
13
 
14
- Works with Next.js, Vite, and React Native.
14
+ Works with React, Next.js, React Native, Expo, TanStack Start, and common React ecosystem libraries.
15
15
 
16
- ### [See it in action](https://react.doctor)
16
+ ### [See it in action](https://react.doctor)
17
17
 
18
- ## Install
18
+ ## Run
19
19
 
20
20
  Run this at your project root:
21
21
 
@@ -23,104 +23,119 @@ Run this at your project root:
23
23
  npx -y react-doctor@latest .
24
24
  ```
25
25
 
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, accessibility, and dead code. Rules toggle automatically based on your framework and React version.
26
+ By default React Doctor runs:
27
+
28
+ - native project structure and codebase graph checks
29
+ - oxlint with the React Doctor custom plugin
30
+ - scoring and grouped human output
31
+
32
+ You get a 0 to 100 score and a list of issues across state and effects, performance, architecture, security, accessibility, framework usage, dependencies, and dead code. Rules toggle automatically based on your framework, React version, and detected libraries.
27
33
 
28
34
  https://github.com/user-attachments/assets/07cc88d9-9589-44c3-aa73-5d603cb1c570
29
35
 
30
- ## Install for your coding agent
36
+ ## React Doctor Skill
31
37
 
32
- Teach your coding agent React best practices so it stops writing the bad code in the first place.
38
+ React Doctor also ships as an agent Skill. The CLI catches problems after code is written; the Skill teaches your coding agent the same React, framework, and performance guidance before it writes the next patch.
33
39
 
34
40
  ```bash
35
41
  npx -y react-doctor@latest install
36
42
  ```
37
43
 
38
- You'll be prompted to pick which detected agents to install for. Pass `--yes` to skip prompts.
39
-
40
- Works with Claude Code, Cursor, Codex, OpenCode, and 50+ other agents.
44
+ Use the Skill when you want agents to:
41
45
 
42
- ## GitHub Actions
46
+ - avoid common state and effect mistakes
47
+ - choose framework-native APIs for Next.js, React Native, Expo, and TanStack Start
48
+ - keep rendering, animation, data fetching, and accessibility choices high-signal
49
+ - understand React Doctor diagnostics and fix the underlying issue instead of hiding it
43
50
 
44
- A composite action ships with this repository. Drop it into `.github/workflows/react-doctor.yml`:
51
+ The installer detects supported coding agents and prompts you to choose where to install the Skill. Pass `--yes` to accept the default detected targets.
45
52
 
46
- ```yaml
47
- name: React Doctor
53
+ ## CLI
48
54
 
49
- on:
50
- pull_request:
51
- push:
52
- branches: [main]
55
+ ```bash
56
+ react-doctor [directory]
57
+ ```
53
58
 
54
- permissions:
55
- contents: read
56
- pull-requests: write # required to post PR comments
59
+ Useful flags:
57
60
 
58
- jobs:
59
- react-doctor:
60
- runs-on: ubuntu-latest
61
- steps:
62
- - uses: actions/checkout@v5
63
- with:
64
- fetch-depth: 0 # required for `diff`
65
- - uses: millionco/react-doctor@main
66
- with:
67
- diff: main
68
- github-token: ${{ secrets.GITHUB_TOKEN }}
61
+ ```bash
62
+ react-doctor apps/web --json
63
+ react-doctor apps/web --json --json-compact
64
+ react-doctor apps/web --no-lint
65
+ react-doctor apps/web --no-dead-code
66
+ react-doctor apps/web --custom-rules-only
67
+ react-doctor apps/web --staged
68
+ react-doctor apps/web --unstaged
69
+ react-doctor apps/web --changed
70
+ react-doctor apps/web --diff main
71
+ react-doctor apps/web --offline
72
+ react-doctor apps/web --fail-on error
69
73
  ```
70
74
 
71
- When `github-token` is set on `pull_request` events, findings are posted (and updated) as a PR comment. The action also exposes a `score` output (0–100) you can use in subsequent steps.
75
+ Changed-file modes only inspect matching source files:
72
76
 
73
- **Inputs:** `directory`, `verbose`, `project`, `diff`, `github-token`, `fail-on` (`error` / `warning` / `none`), `offline`, `node-version`. See [`action.yml`](https://github.com/millionco/react-doctor/blob/main/action.yml) for full descriptions.
77
+ - `--staged` scans the git index for pre-commit flows.
78
+ - `--unstaged` scans unstaged and untracked source files.
79
+ - `--changed` scans staged, unstaged, and untracked source files since `HEAD`.
80
+ - `--diff [base]` scans files changed against a base branch, defaulting to `main`.
74
81
 
75
- Prefer not to add a marketplace action? The bare `npx` form works too:
82
+ If no changed source files are found, source checks are skipped instead of falling back to a full scan.
76
83
 
77
- ```yaml
78
- - run: npx -y react-doctor@latest --fail-on warning
79
- ```
84
+ `--fail-on` accepts `error`, `warning`, or `none`.
80
85
 
81
86
  ## Configuration
82
87
 
83
- Create a `react-doctor.config.json` in your project root:
88
+ React Doctor looks for configuration in:
89
+
90
+ - `react-doctor.config.json`
91
+ - `package.json#reactDoctor`
92
+
93
+ Config lookup starts at the requested directory and walks ancestors until a project boundary. `rootDir` is resolved relative to the config source, not the current working directory.
84
94
 
85
95
  ```json
86
96
  {
97
+ "rootDir": "apps/web",
98
+ "lint": true,
99
+ "deadCode": true,
100
+ "customRulesOnly": false,
101
+ "offline": true,
102
+ "failOn": "error",
103
+ "respectInlineDisables": true,
104
+ "adoptExistingLintConfig": false,
105
+ "includeEcosystemRules": true,
106
+ "ignoredTags": ["design"],
107
+ "textComponents": ["Trans"],
108
+ "rawTextWrapperComponents": ["Button"],
87
109
  "ignore": {
88
- "rules": ["react/no-danger", "jsx-a11y/no-autofocus"],
110
+ "rules": ["react-doctor/no-gradient-text"],
89
111
  "files": ["src/generated/**"],
90
112
  "overrides": [
91
113
  {
92
- "files": ["components/modules/diff/**"],
93
- "rules": ["react-doctor/no-array-index-as-key", "react-doctor/no-render-in-render"]
94
- },
95
- {
96
- "files": ["components/search/HighlightedSnippet.tsx"],
97
- "rules": ["react/no-danger"]
114
+ "files": ["src/legacy/**"],
115
+ "rules": ["react-doctor/no-default-props"]
98
116
  }
99
117
  ]
100
118
  }
101
119
  }
102
120
  ```
103
121
 
104
- Three nested keys, three layers of granularity — pick the narrowest one that fits:
122
+ Pick the narrowest ignore that fits:
105
123
 
106
- - **`ignore.rules`** silences a rule across the whole codebase.
107
- - **`ignore.files`** silences **every** rule on the matched files (use sparingly — it loses coverage for unrelated rules).
124
+ - **`ignore.rules`** silences a rule across the codebase.
125
+ - **`ignore.files`** silences every rule on matched files.
108
126
  - **`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.
109
127
 
110
- You can also use the `"reactDoctor"` key in `package.json`. CLI flags always override config values.
128
+ React Doctor scans only its curated rule set by default. Set `adoptExistingLintConfig` to `true` to adopt the first JSON `.oxlintrc.json` or `.eslintrc.json` found while walking ancestors.
111
129
 
112
- 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.
130
+ `ignoredTags` lets you trim noisy categories without turning the whole scanner off. For example, `["design"]` keeps structural React checks while skipping subjective visual style suggestions.
113
131
 
114
- 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.
132
+ For React Native, `textComponents` marks custom components that behave like `<Text>`, while `rawTextWrapperComponents` marks components that safely wrap string-only children in text internally.
115
133
 
116
- #### Optional companion plugins
134
+ ## Scoring
117
135
 
118
- 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. Both are listed as **optional peer dependencies** install only what you want.
136
+ Scores are a simple health signal, not a moral judgment. React Doctor starts at 100, subtracts more for error-level rule families than warning-level families, and counts a repeated rule once so one noisy pattern does not dominate the whole project.
119
137
 
120
- | Plugin | Adds | Namespace |
121
- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ |
122
- | [`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/*` |
123
- | [`eslint-plugin-react-you-might-not-need-an-effect`](https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect) (v0.10+) | Complementary effects-as-anti-pattern rules (`no-derived-state`, `no-chain-state-updates`, `no-event-handler`, `no-pass-data-to-parent`, …) that run alongside React Doctor's native State & Effects rules. | `effect/*` |
138
+ The score can move between releases as rules become more precise, new framework rules are added, or noisy checks are demoted. Treat the detailed diagnostics as the source of truth and use the score for trend tracking across repeated runs.
124
139
 
125
140
  ### Inline suppressions
126
141
 
@@ -139,7 +154,7 @@ When two rules fire on the same line, you have two equivalent options. Comma-sep
139
154
  const [localSearch, setLocalSearch] = useState(searchQuery);
140
155
  ```
141
156
 
142
- 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:
157
+ Or stack one comment per rule directly above the diagnostic:
143
158
 
144
159
  ```tsx
145
160
  // react-doctor-disable-next-line react-doctor/rerender-state-only-in-handlers
@@ -147,122 +162,142 @@ Or stack one comment per rule directly above the diagnostic. Stacked comments ar
147
162
  const [localSearch, setLocalSearch] = useState(searchQuery);
148
163
  ```
149
164
 
150
- 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.
151
-
152
165
  Block comments work inside JSX:
153
166
 
154
167
  <!-- prettier-ignore -->
155
168
  ```tsx
156
- {/* react-doctor-disable-next-line react/no-danger */}
169
+ {/* react-doctor-disable-next-line no-danger */}
157
170
  <div dangerouslySetInnerHTML={{ __html }} />
158
171
  ```
159
172
 
160
173
  For multi-line JSX, putting the comment immediately above the opening tag covers the entire attribute list (matching ESLint convention).
161
174
 
162
- ## Lint plugin (standalone)
175
+ ## Lint Integrations
163
176
 
164
177
  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.
165
178
 
166
- **oxlint** in `.oxlintrc.json`:
179
+ Oxlint (`.oxlintrc.json`):
167
180
 
168
181
  ```jsonc
169
182
  {
170
183
  "jsPlugins": [{ "name": "react-doctor", "specifier": "react-doctor/oxlint-plugin" }],
171
184
  "rules": {
172
185
  "react-doctor/no-fetch-in-effect": "warn",
173
- "react-doctor/no-derived-state-effect": "warn",
174
186
  },
175
187
  }
176
188
  ```
177
189
 
178
- **ESLint** flat config:
190
+ ESLint:
179
191
 
180
192
  ```js
181
193
  import reactDoctor from "react-doctor/eslint-plugin";
182
194
 
183
195
  export default [
184
- reactDoctor.configs.recommended,
185
- reactDoctor.configs.next,
186
- reactDoctor.configs["react-native"],
187
- reactDoctor.configs["tanstack-start"],
188
- reactDoctor.configs["tanstack-query"],
196
+ {
197
+ plugins: {
198
+ "react-doctor": reactDoctor,
199
+ },
200
+ rules: {
201
+ "react-doctor/no-fetch-in-effect": "warn",
202
+ },
203
+ },
189
204
  ];
190
205
  ```
191
206
 
192
- The full rule list lives in [`oxlint-config.ts`](https://github.com/millionco/react-doctor/blob/main/packages/react-doctor/src/oxlint-config.ts).
207
+ The ESLint wrapper reuses the same rule implementations and metadata as the oxlint plugin.
208
+
209
+ ## SDK
193
210
 
194
- ## CLI reference
211
+ ```ts
212
+ import { createReactDoctor, inspectReactProject } from "react-doctor";
195
213
 
214
+ const result = await inspectReactProject({
215
+ rootDirectory: "apps/web",
216
+ lint: true,
217
+ deadCode: true,
218
+ });
219
+
220
+ const reactDoctor = createReactDoctor({ rootDirectory: "apps/web" });
221
+ const nextResult = await reactDoctor.inspect();
196
222
  ```
197
- Usage: react-doctor [directory] [options]
198
-
199
- Options:
200
- -v, --version display the version number
201
- --no-lint skip linting
202
- --no-dead-code skip dead code detection
203
- --verbose show every rule and per-file details (default shows top 3 rules)
204
- --score output only the score
205
- --json output a single structured JSON report
206
- -y, --yes skip prompts, scan all workspace projects
207
- --full skip prompts, always run a full scan
208
- --project <name> select workspace project (comma-separated for multiple)
209
- --diff [base] scan only files changed vs base branch
210
- --staged scan only staged files (for pre-commit hooks)
211
- --offline skip telemetry
212
- --fail-on <level> exit with error on diagnostics: error, warning, none
213
- --annotations output diagnostics as GitHub Actions annotations
214
- --explain <file:line> diagnose why a rule fired or why a suppression didn't apply
215
- --why <file:line> alias for --explain
216
- -h, --help display help
223
+
224
+ The result includes project metadata, check results, normalized issues, score, and timing.
225
+
226
+ ```ts
227
+ import { buildReactDoctorJsonReport } from "react-doctor";
228
+
229
+ const report = buildReactDoctorJsonReport(result);
230
+ ```
231
+
232
+ Typed runtime errors are exported from the main SDK:
233
+
234
+ ```ts
235
+ import { ReactDoctorInvalidConfigError, isReactDoctorError } from "react-doctor";
217
236
  ```
218
237
 
219
- 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.
238
+ ## Compatibility API
220
239
 
221
- `--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.
240
+ Deprecated compatibility APIs live under `react-doctor/api` and are intentionally isolated from the main runtime.
222
241
 
223
- ### Config keys
242
+ ```ts
243
+ import { diagnose, clearCaches } from "react-doctor/api";
224
244
 
225
- | Key | Type | Default |
226
- | -------------------------- | -------------------------------- | -------- |
227
- | `ignore.rules` | `string[]` | `[]` |
228
- | `ignore.files` | `string[]` | `[]` |
229
- | `ignore.overrides` | `{ files, rules? }[]` | `[]` |
230
- | `lint` | `boolean` | `true` |
231
- | `deadCode` | `boolean` | `true` |
232
- | `verbose` | `boolean` | `false` |
233
- | `diff` | `boolean \| string` | |
234
- | `failOn` | `"error" \| "warning" \| "none"` | `"none"` |
235
- | `customRulesOnly` | `boolean` | `false` |
236
- | `share` | `boolean` | `true` |
237
- | `textComponents` | `string[]` | `[]` |
238
- | `rawTextWrapperComponents` | `string[]` | `[]` |
239
- | `respectInlineDisables` | `boolean` | `true` |
240
- | `adoptExistingLintConfig` | `boolean` | `true` |
245
+ const result = await diagnose("apps/web", {
246
+ lint: true,
247
+ deadCode: true,
248
+ });
241
249
 
242
- `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.
250
+ clearCaches();
251
+ ```
243
252
 
244
- `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.
253
+ Prefer `createReactDoctor()` or `inspectReactProject()` for new integrations.
245
254
 
246
- ## Node.js API
255
+ ## Development
247
256
 
248
- ```js
249
- import { diagnose, toJsonReport, summarizeDiagnostics } from "react-doctor/api";
257
+ Run package checks from the package directory:
258
+
259
+ ```bash
260
+ nr typecheck
261
+ nr test
262
+ nr build
263
+ ```
250
264
 
251
- const result = await diagnose("./path/to/your/react-project");
265
+ Run workspace formatting and linting from the repository root:
252
266
 
253
- console.log(result.score); // { score: 82, label: "Great" } or null
254
- console.log(result.diagnostics); // Diagnostic[]
255
- console.log(result.project); // detected framework, React version, etc.
267
+ ```bash
268
+ nr format:check packages/react-doctor/src packages/react-doctor/tests
269
+ nr lint packages/react-doctor/src packages/react-doctor/tests
256
270
  ```
257
271
 
258
- `diagnose` accepts a second argument: `{ lint?: boolean, deadCode?: boolean }`.
272
+ ## Regression testing
259
273
 
260
- ```js
261
- const report = toJsonReport(result, { version: "1.0.0" });
262
- const counts = summarizeDiagnostics(result.diagnostics);
274
+ Drive the sandbox test suite in the sibling [`react-review`](https://github.com/millionco/react-review) repo against a fleet of real React projects, using the local working copy of `react-doctor` (packed into a tarball).
275
+
276
+ ```bash
277
+ pnpm --filter react-doctor test:regression
263
278
  ```
264
279
 
265
- `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.
280
+ The script builds the package, packs it into `packages/react-doctor/.regression/react-doctor-<version>.tgz`, then shells out to `~/Developer/react-review/apps/api` and runs `pnpm test` with:
281
+
282
+ - `GITHUB_TOKEN` from `gh auth token` (required so GitHub does not 403 the tarball downloads).
283
+ - `REACT_DOCTOR_SPECIFIERS` pointing at the local tarball — `react-review`'s sandbox test detects the `.tgz`, uploads it into the Vercel Sandbox, and installs via `file:`.
284
+ - `REACT_DOCTOR_TEST_REPOS` defaulting to the first 10 repos from a curated fleet, comma-separated `owner/repo` entries.
285
+
286
+ Preconditions the script checks for and surfaces clear errors when missing:
287
+
288
+ - `gh auth token` succeeds (run `gh auth login` first if not).
289
+ - `~/Developer/react-review/apps/api` exists.
290
+ - `~/Developer/react-review/apps/api/.env.local` exists (run `vercel env pull` inside `~/Developer/react-review/apps/api` to populate `@vercel/sandbox` credentials).
291
+
292
+ Overrides:
293
+
294
+ ```bash
295
+ REACT_DOCTOR_REGRESSION_SAMPLE=25 pnpm --filter react-doctor test:regression
296
+ REACT_DOCTOR_REGRESSION_SAMPLE=all pnpm --filter react-doctor test:regression
297
+ REACT_DOCTOR_TEST_REPOS="vercel/ai-chatbot,shadcn-ui/ui" pnpm --filter react-doctor test:regression
298
+ ```
299
+
300
+ Each repo runs sequentially inside a Vercel Sandbox; expect a few minutes per repo. The default 10-repo sample is the sane batch size for a single run.
266
301
 
267
302
  ## Leaderboard
268
303
 
@@ -272,7 +307,7 @@ Top React codebases scanned by React Doctor, ranked by score. Updated automatica
272
307
  <!-- prettier-ignore -->
273
308
  | # | Repo | Score |
274
309
  | -- | ---- | ----: |
275
- | 1 | [executor](https://github.com/RhysSullivan/executor) | 96 |
310
+ | 1 | [executor](https://github.com/RhysSullivan/executor) | 94 |
276
311
  | 2 | [nodejs.org](https://github.com/nodejs/nodejs.org) | 86 |
277
312
  | 3 | [tldraw](https://github.com/tldraw/tldraw) | 70 |
278
313
  | 4 | [t3code](https://github.com/pingdotgg/t3code) | 68 |
@@ -296,13 +331,15 @@ Looking to contribute back? Clone the repo, install, build, and submit a PR.
296
331
  ```bash
297
332
  git clone https://github.com/millionco/react-doctor
298
333
  cd react-doctor
299
- pnpm install
300
- pnpm build
301
- node packages/react-doctor/bin/react-doctor.js /path/to/your/react-project
334
+ ni
335
+ nr build
336
+ node packages/react-doctor/bin/react-doctor.js apps/web
302
337
  ```
303
338
 
304
339
  Find a bug? Head to the [issue tracker](https://github.com/millionco/react-doctor/issues).
305
340
 
341
+ Release notes are published on [GitHub Releases](https://github.com/millionco/react-doctor/releases).
342
+
306
343
  ### License
307
344
 
308
345
  React Doctor is MIT-licensed open-source software.