react-doctor 0.2.0-beta.4 → 0.2.0-beta.6

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
@@ -20,10 +20,12 @@ Works with Next.js, Vite, and React Native.
20
20
  Run this at your project root:
21
21
 
22
22
  ```bash
23
- npx -y react-doctor@latest .
23
+ npx 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
+ 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.
27
29
 
28
30
  https://github.com/user-attachments/assets/07cc88d9-9589-44c3-aa73-5d603cb1c570
29
31
 
@@ -32,7 +34,7 @@ https://github.com/user-attachments/assets/07cc88d9-9589-44c3-aa73-5d603cb1c570
32
34
  Teach your coding agent React best practices so it stops writing the bad code in the first place.
33
35
 
34
36
  ```bash
35
- npx -y react-doctor@latest install
37
+ npx react-doctor@latest install
36
38
  ```
37
39
 
38
40
  You'll be prompted to pick which detected agents to install for. Pass `--yes` to skip prompts.
@@ -68,16 +70,87 @@ jobs:
68
70
  github-token: ${{ secrets.GITHUB_TOKEN }}
69
71
  ```
70
72
 
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.
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 use in subsequent steps.
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.
72
80
 
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.
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
+ ```
74
92
 
75
93
  Prefer not to add a marketplace action? The bare `npx` form works too:
76
94
 
77
95
  ```yaml
78
- - run: npx -y react-doctor@latest --fail-on warning
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
79
119
  ```
80
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
+ if [ -n "$SCORE" ] && [ "$SCORE" -lt "$FLOOR" ]; then
147
+ echo "::error::React Doctor score $SCORE is below floor $FLOOR"
148
+ exit 1
149
+ fi
150
+ ```
151
+
152
+ 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)).
153
+
81
154
  ## Configuration
82
155
 
83
156
  Create a `react-doctor.config.json` in your project root:
@@ -113,6 +186,38 @@ React Doctor respects `.gitignore`, `.eslintignore`, `.oxlintignore`, `.prettier
113
186
 
114
187
  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.
115
188
 
189
+ #### Surface controls (CLI, PR comments, score, CI failure)
190
+
191
+ 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:
192
+
193
+ ```json
194
+ {
195
+ "surfaces": {
196
+ "prComment": {
197
+ "includeTags": ["design"],
198
+ "excludeCategories": ["Performance"]
199
+ },
200
+ "score": { "includeRules": ["react-doctor/design-no-redundant-size-axes"] },
201
+ "ciFailure": { "excludeTags": ["test-noise"] }
202
+ }
203
+ }
204
+ ```
205
+
206
+ 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.
207
+
208
+ #### Rule severity (`rules`, `categories`)
209
+
210
+ 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"`, …).
211
+
212
+ ```json
213
+ {
214
+ "rules": { "react-doctor/no-array-index-as-key": "error" },
215
+ "categories": { "React Native": "warn" }
216
+ }
217
+ ```
218
+
219
+ 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.
220
+
116
221
  #### Optional companion plugins
117
222
 
118
223
  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.
@@ -210,6 +315,8 @@ Options:
210
315
  --offline skip the score API and share URL (no score shown)
211
316
  --fail-on <level> exit with error on diagnostics: error, warning, none
212
317
  --annotations output diagnostics as GitHub Actions annotations
318
+ --pr-comment tune CLI output for sticky PR comments (drops design
319
+ cleanup from the printed list and fail-on gate)
213
320
  --explain <file:line> diagnose why a rule fired or why a suppression didn't apply
214
321
  --why <file:line> alias for --explain
215
322
  -h, --help display help
@@ -235,6 +342,7 @@ When a suppression isn't working, `--explain <file:line>` (or its alias `--why <
235
342
  | `offline` | `boolean` | `false` |
236
343
  | `textComponents` | `string[]` | `[]` |
237
344
  | `rawTextWrapperComponents` | `string[]` | `[]` |
345
+ | `serverAuthFunctionNames` | `string[]` | `[]` |
238
346
  | `respectInlineDisables` | `boolean` | `true` |
239
347
  | `adoptExistingLintConfig` | `boolean` | `true` |
240
348
  | `ignore.tags` | `string[]` | `[]` |
@@ -243,10 +351,34 @@ When a suppression isn't working, `--explain <file:line>` (or its alias `--why <
243
351
 
244
352
  `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.
245
353
 
354
+ `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.
355
+
246
356
  `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"`.
247
357
 
248
358
  `offline` skips the score API entirely — no score is shown and no share URL is generated. Automatically enabled in CI environments (GitHub Actions, GitLab CI, CircleCI) so CI runs don't depend on the network.
249
359
 
360
+ ### React Native rules in mixed monorepos
361
+
362
+ `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:
363
+
364
+ - 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.
365
+ - 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.
366
+ - Packages with no clear local signal → fall back to the project-level framework detection.
367
+
368
+ 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).
369
+
370
+ 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.
371
+
372
+ `rn-no-raw-text` additionally short-circuits raw text inside platform-fork branches:
373
+
374
+ - `if (Platform.OS === "web") { … }` consequent — and the `else` branch of `if (Platform.OS !== "web")`.
375
+ - `Platform.OS === "web" ? <X /> : …` ternaries, `Platform.OS === "web" && <X />` short-circuits, and the reversed-operand form `"web" === Platform.OS`.
376
+ - `switch (Platform.OS) { case "web": … }` case bodies (other cases still report).
377
+ - `Platform.select({ web: <X />, default: <Y /> })` — only the `web` arm is exempt.
378
+ - `Platform?.OS === "web"` (optional chain) and `Platform.OS! === "web"` (TS non-null assertion) parse the same way as the bare form.
379
+
380
+ 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.
381
+
250
382
  ## Scoring
251
383
 
252
384
  The health score formula: `100 - (unique_error_rules x 1.5) - (unique_warning_rules x 0.75)`.
@@ -280,8 +412,8 @@ React Doctor detects 50+ coding agents (Claude Code, Cursor, Codex, OpenCode, Wi
280
412
  - **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.
281
413
  - **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.
282
414
  - **Score-only output**: `--score` outputs just the numeric score (0-100), useful for threshold checks in agent loops.
283
- - **GitHub Actions annotations**: `--annotations` emits `::error` / `::warning` format for inline PR annotations.
284
- - **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.
415
+ - **GitHub Actions annotations**: `--annotations` emits `::error` / `::warning` format for inline PR annotations. Annotations don't change the exit code.
416
+ - **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.
285
417
  - **Programmatic API**: `import { diagnose } from "react-doctor/api"` for direct integration in scripts and automation.
286
418
 
287
419
  In CI environments, prompts are automatically skipped and `--offline` is implied (no network round-trip; score is omitted from the output).