Changelog
Release notes for the React Doctor CLI, read directly from the GitHub changelog.
0.5.6Patch
Patch Changes
- Add five
security-scanrules distilled from security-researcher writeups and the deepsec scanner-matcher catalog, closing CWE shapes the bucket didn't cover:
#812
- `unsafe-json-in-html` —
JSON.stringify(...)embedded indangerouslySetInnerHTMLor inline<script>markup.JSON.stringifydoes not HTML-escape, so data containing</script>or<breaks out — the classic SSR data-hydration XSS. Suppressed when an HTML-safe serializer (serialize-javascript, devalue, superjson) or\u003cescaping is used.- `jwt-insecure-verification` — the JWT
nonealgorithm (alg: none/algorithms: ["none"]), which disables signature verification and lets any forged token through. (Detecting an unpinnedjwt.verifyprecisely needs scope-aware analysis, so that is left to a future AST rule.) - `secret-in-fallback` — a secret-shaped env var with a hardcoded string fallback (
process.env.STRIPE_SECRET_KEY ?? "<hardcoded>"): a committed secret that also makes the app fail open when the var is unset. Skips public vars (PUBLIC/PUBLISHABLE/ANON) and placeholder defaults. - `request-body-mass-assignment` — spreading or merging request input (
{ ...req.body },Object.assign(target, req.body), lodashmerge/defaultsDeep) without a field allowlist: mass assignment (client-set owner/role/price columns) or prototype pollution. - `insecure-session-cookie` — auth/session cookies exposed to JavaScript:
httpOnly: false, set viadocument.cookie, or a bareres.cookie("session", value)/cookies().set(...)with no options.
- `jwt-insecure-verification` — the JWT
All five register through defineRule with a project-level scan, carry the Security category and security-scan tag, and are silenced by react-doctor rules ignore-tag security-scan like the rest of the family.
- Show the full file total when the scan hands off to dead-code analysis, so the live counter no longer looks stuck below
N(#815).
#824
The linter already emits a final (N, N) progress tick when its last batch finishes, but ora throttles renders to its frame interval — that last frame was overwritten by the "Analyzing dead code…" text before it ever painted, so the spinner appeared to freeze at whatever value the smooth-creep timer last drew (e.g. 80/165). Every file was always scanned; only the counter looked short. The dead-code phase now reads Scanned N files, analyzing dead code…, keeping the complete count visible for the whole (longer) dead-code pass.
- Fix false positives reported in the security and TanStack rules:
#819
- `query-destructure-result` (#818): only flags
useQuery/useSuspenseQuery/… when they actually come from a TanStack Query package (@tanstack/*-query, legacyreact-query). A same-named hook imported from elsewhere — notably Convex'suseQueryfromconvex/react, which returns the data directly — is no longer flagged.- `artifact-env-leak` / `artifact-secret-leak` (#816, #817): no longer treat server-side or dev-mode Next.js output as browser artifacts.
.next/dev/server/**(dev source maps), any.next/**/server/**,.output/server/**, and the dev server's.next/dev/**output are excluded; production browser bundles (.next/static,dist/assets,public/, …) are still scanned. - `repository-secret-file` / `key-lifecycle-risk` (#813): no longer flag a credential/key file that git ignores — a local-only, gitignored
.envis not "checked into the repository". Findings are dropped only when git definitively reports the path as ignored (the finding stands when there is no repo or git is unavailable). - `webhook-signature-risk` (#814): recognizes a delegated verification helper (a call pairing a verify-ish verb with a security noun, e.g.
isValidSecret(...),verifySignature(...),checkWebhookHmac(...)) as verification evidence, so an extractedtimingSafeEqualcomparison in another module no longer trips the rule.
- `artifact-env-leak` / `artifact-secret-leak` (#816, #817): no longer treat server-side or dev-mode Next.js output as browser artifacts.
- Add a
supabase-table-missing-rlssecurity-scan rule. It flags a Supabase migration (supabase/migrations/**,supabase/schemas/**) that runscreate tablefor a public-schema table but never enables Row Level Security — the highest-impact and most common Supabase misconfiguration, because RLS is OFF by default for SQL-created tables, so every row is readable and writable with the public anon key. It targets the same misconfiguration Supabase's ownrls_disabled_in_publicdatabase linter flags, and the gap that turns the public anon key into the service key.
#812
The existing supabase-rls-policy-risk only caught an explicit disable row level security; this complements it by catching the far more common "never enabled it" case. RLS is checked per table — each create table must have an alter table <name> enable row level security for that same table, after the create (a sibling table enabling RLS, or a policy without enabling it, does not vouch). SQL comments and string literals are ignored, non-public/Supabase-managed schemas (auth., storage., a private. schema, …) are skipped, and the rule is scoped to the supabase/ directory so plain Drizzle/Prisma .sql migrations are not flagged. The scan runs per migration file, so enabling RLS in a _different_ migration than the create table is not detected — the same-file pattern (what Supabase tooling emits) is the supported case. Like the rest of the family it carries the security-scan tag and is silenced by react-doctor rules ignore-tag security-scan.
resolveConcreteVersion called semver.minVersion(spec) directly, but semver throws (TypeError: Invalid comparator: latest) on a non-range spec instead of returning null. Any full scan — or PR scan touching package.json — containing a dist-tag like "trigger.dev": "latest" (or "next") crashed before the Socket fail-open path could run (regression from #804, affecting 0.5.3–0.5.5).
The spec is now validated with semver.validRange before resolving its floor: dist-tags and other non-ranges are skipped (nothing to score), as is a wildcard-only range (*/x/X), which previously resolved to a synthetic 0.0.0 and scored a version nobody pinned. Real ranges (^1.2.3, 1.x, >=2 <3) and protocol/URL specs (workspace:, file:, npm:, git+…) are unchanged.
- Updated dependencies [`ea3b827`, `5fc0e27`, `ea3b827`]:
- oxlint-plugin-react-doctor@0.5.6
0.5.5Patch
Patch Changes
- Updated dependencies [`e90eb7a`]:
- oxlint-plugin-react-doctor@0.5.5
0.5.4Patch
Patch Changes
- Add a project-level security file scan: 36 first-class scan rules (leaked artifact secrets and env dumps, permissive Firebase/Supabase rules, raw SQL injection risk, unsafe webhook signature comparisons, committed private key material, public debug artifacts, …) ship in the oxlint plugin as ordinary
defineRulemodules that declare a project-levelscaninstead of AST visitors and run in@react-doctor/core's environment-check phase over one bounded whole-tree walk — covering shipped bundles, dotenv/config files, SQL, and Firebase rules files that per-file linting never sees.
#744
Scan rules register metadata (id, title, severity, recommendation, Security category, security-scan tag) like any other rule but carry a project-level scan instead of AST visitors, so their findings flow through the standard diagnostic pipeline: per-rule and per-category severity overrides, inline disables, and output surfaces now apply to scan-rule diagnostics, and react-doctor rules ignore-tag security-scan (config ignore.tags) silences the whole family. They never appear in generated oxlint configs or the ESLint presets — they only execute through React Doctor's scan. A plain --diff / --staged scan skips them like the other whole-project checks, and the gate is now diff mode itself rather than the presence of include paths, so projects configuring ignore.files get the security scan too.
- Remove the
--sfwdemo flag (the standalone Socket.dev supply-chain score listing that printed every direct dependency's score and exited).
#744
The Socket.dev supply-chain check is unaffected — it still runs during normal full scans (and on diff scans whose package.json changed) and its scores still appear in the JSON report. Only the standalone listing is gone, along with its demo-only internals (collectSupplyChainScores, the DependencyScore type, the monorepo-wide dependency collector, and the score-table renderer).
- Updated dependencies [`eacdcf2`, `eacdcf2`]:
- oxlint-plugin-react-doctor@0.5.4
0.5.3Patch
Patch Changes
- Clearer Socket supply-chain diagnostics (
socket/low-supply-chain-score). When Socket returns a concrete alert, the message now names it — e.g. a critical "known malware" alert, the offending file, and a one-line description — instead of only a bare score; when it doesn't (metric-driven dips like CVE-only scores), the message explains what the failing axis means. The help is now axis-aware: remove a package flagged as compromised, upgrade past known vulnerabilities (npm audit), or vet-and-raise the threshold — rather than a generic "update or replace". The headline leads with the exact failing axis and collapses the redundant "declared as X, scored at X" phrasing (a range now readspkg@floor (lowest version "^x.y.z" allows)). JSON report shape is unchanged (schemaVersion: 1).
#804
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.5.3
0.5.2Patch
Patch Changes
- The GitHub Action's
blockinginput now defaults tonone(advisory) instead oferror. Every PR still gets the full React Doctor report — the sticky summary comment, inline review comments, and a commit status with the health score — but the check no longer fails on findings, so a brand-new install can't red-X a teammate's PR on day one (trust-before-gate). To turn the gate back on, setblocking: warning(fail on any finding) orblocking: error(fail on error-severity findings) on the action. The generatedreact-doctor.ymldocuments this inline.
#767
Note: this changes behavior for existing millionco/react-doctor@v2 workflows that never set blocking — they were gating on error-severity findings and will now run advisory. Add blocking: error to the action's with: block to keep the previous behavior.
The CLI / config default is unchanged: react-doctor (and --blocking / the blocking config key) still defaults to error, so local runs, pre-commit hooks, and non-action CI keep failing on error-severity findings.
- Bump
engines.nodeto^20.19.0 || >=22.13.0so the declared support range matches transitive dependencies (eslint-scope@9,eslint-visitor-keys@5require^22.13.0), preventing EBADENGINE warnings on npm and hard install failures on Yarn 1 under Node 22.12.x. #766
- Bundle Effect into the published CLI so
npx react-doctor@latestno longer installs Effect'sini@7dependency and avoids the Node 22.19 engine warning.
#731
- Cap the
oxlintdependency to>=1.66.0 <1.67.0. oxlint 1.67.0 added an optional peer dependency onvite-plus, which in pnpm workspaces that installvite-plusat the root forces a second peer-resolution context for the Vite+ toolchain. That split installs a duplicate copy of the Vitest fork (@voidzero-dev/vite-plus-test), and test runs fail at collection withVitest failed to find the current suitebecause hooks register in one copy while suites live in the other (#699). Pinning below 1.67 keeps react-doctor's oxlint free of thevite-pluspeer edge, so pnpm dedupes the toolchain back to a single instance.
#791
- Carry the React Compiler bail-out reason in the primary diagnostic message.
react-hooks-js/*diagnostics previously all rendered the same generic "This component misses React Compiler's automatic memoization…" message, with the specific reason relegated tohelp. The message now includes the first line of the compiler's reason (e.g.useMemo() callbacks may not be async or generator functions) so contexts that only show the message explain _why_ the compiler bailed; the reason's remaining lines stay inhelp, so the rendered message + suggestion never repeat the same sentence.tododiagnostics keep the generic message — their reasons are compiler-internal work notes, not user-facing copy. Because diagnostics dedupe on their full message, two _different_ bail-out reasons anchored at the same source location now survive as two diagnostics instead of collapsing into one, so counts can rise slightly on affected projects. #793
- Load
doctor.config.tsfiles that importdefineConfigfromreact-doctor/apieven when the scanned repo has no installed node_modules (e.g. the GitHub Action runs the CLI vianpm execwithout installing the repo's dependencies). The config loader now retries the load withreact-doctor/apialiased to the running package's own copy instead of silently falling back to default config. #800
- Consolidate the scan-scope controls into one
--scopeflag (andscopeconfig option) with four values, shared verbatim by the CLI and the GitHub Action:
#769
full(default) — the whole project, every issue. Whole-project checks (dead-code, environment, supply-chain) run only here.files— only the files changed vs the base, with all issues in them (no compare-to-main). What--stagedand an uncommitted--diffdid.changed— only issues the change introduced vs the base (the baseline delta). What--diff <base>and the action'sscope: changeddid.lines— only issues on the lines the change actually touched. New: previously this scoping existed only inside the GitHub Action's inline-review-comment step; it now lives in the engine, so the CI gate, score display, summary, and inline comments all honor one scope.
--base <ref> sets the comparison base for files / changed / lines (auto-detected when omitted). Behavior is unchanged by default: the CLI --scope defaults to full and the action scope input still defaults to changed. --diff / config.diff keep working as a deprecated alias (--diff <base> → --scope changed --base <base>, --diff false → --scope full) and emit a one-time deprecation warning; --staged is retained as the source selector and composes with --scope files / --scope lines.
- Diagnostics in test, spec, fixture, and Storybook files are now labeled with their file context. The terminal report and the per-rule text dumps tag those sites as
(test file)/(story file)so a finding in a spec doesn't read as a production problem, and each diagnostic in the JSON report carries an optionalfileContextfield ("test"/"story"; omitted for production files). The classification reuses the same path heuristics that already drive test-noise auto-suppression, so the label and the suppression can never disagree. #795
- Fix a false positive in
nextjs-missing-metadata(#775): an App Router page is no longer flagged as "missing metadata for search previews" when it inheritsmetadata/generateMetadatafrom a co-located or ancestorlayout.*. Next.js merges metadata down the segment chain, so a page covered by a parent layout's title/description already has search-preview metadata. The rule now walks up the App Router directory tree (bounded, stopping atapp/) and stays quiet when an ancestor layout supplies metadata; pages with no metadata anywhere in the chain are still flagged.
#784
- CI onboarding now resolves the repository's actual default branch instead of assuming
main. The pull request opened during setup asks GitHub (gh repo view) for the default branch — falling back toorigin/HEAD, thenmain/master— and uses it as the PR base, and the installed workflow's push trigger scans that same branch (master,develop, …) so the health-score trend works on repos whose default branch isn'tmain.
#768
- Add a
--output-dir <dir>flag that writes the full diagnostics dump (diagnostics.json + one .txt per rule) to a directory of your choice instead of a random temp folder, prints the written path whenever the flag is set (previously--verbose-only), and makes the agent handoff reuse that directory instead of writing a second temp copy. Without the flag, behavior is unchanged. #783
- Title
react-hooks-js/tododiagnostics "React Compiler doesn't support this syntax" instead of the generic "React Compiler can't optimize this" headline. Thetodorule fires when the compiler bails out on syntax it doesn't handle yet, so the headline now says what actually happened. #792
- Add
rn-no-metro-babel-runtime-version— warns when a babel config usesmodule:@react-native/babel-presetwithout anenableBabelRuntimeversion. Without a version the preset can duplicate Babel runtime helpers across files instead of importing them once from@babel/runtime, increasing the JS bundle (facebook/react-native#57123). It fires as awarning(a bundle-size optimization, not a broken build, so it never blocks CI on the default React Native config), only when the preset is referenced as a real string literal (Expo'sbabel-preset-expoand comment mentions are unaffected), and treatsenableBabelRuntime: true/falseas still missing a version. #801
- Fix false positives in
rn-no-raw-text(#788) for custom components that forward their children into a<Text>: the in-file wrapper detection now recognizes components that render{children}(or{props.children}) inside a nested<Text>(the<View><Text>{children}</Text></View>shape), not just components whose returned root is a<Text>. Detection also handles parenthesizedreturn (...)bodies,memo/forwardRef-wrapped components, fragment roots, conditional and logical returns, early returns insideifbranches, renamed destructured children ({ children: content }), the<Text children={children} />prop form, wrappers that forward through another in-file wrapper, children aliased to a variable or destructured from props in the body, props spreads that carry children (<Text {...props} />,<Text {...rest} />,<Text {...this.props} />), class components, andstyled(Text)/styled.Textfactories. The rule is also taggedtest-noise, so it no longer fires in test/story files — raw text rendered through React Native Testing Library never ships to users, and cross-file wrappers (an imported<Chip>Test Chip</Chip>in a.test.tsx) were the main source of unfixable noise there. #790
- The Socket supply-chain check now gates on the security axes (supply chain, vulnerability) instead of Socket's
overallscore, and the diagnostic names the exact axis that failed. Socket'soverallis its lowest axis, so a package with perfect security scores could fail the Security gate purely on quality/maintenance —@types/bunwas reported as having a "supply-chain score of 48" while socket.dev showed Supply Chain 100 (issue #770). Known-bad packages (event-stream@3.3.6, vulnerableminimist/lodashreleases) are still flagged via their vulnerability axis, and the reported number now always matches the axis named on the socket.dev package page.
#780
- Updated dependencies [`94f9f4f`, `038aaf7`, `fee3fc4`, `c4f0e60`, `f52bd07`, `7c88165`]:
- oxlint-plugin-react-doctor@0.5.2
0.5.1Patch
Patch Changes
- Updated dependencies [`77a70ab`]:
- oxlint-plugin-react-doctor@0.5.1
0.5.0Feature
Minor Changes
- React Doctor now runs on repositories that don't depend on React. Previously a scan hard-failed with
No React project found/No React dependency, even though many checks (security, bundle size, JS performance, architecture, and the Zod rules) are framework-agnostic and apply to any TypeScript / JavaScript codebase.
#756
A project is now analyzable when it has source files, with or without React. A bare directory of TypeScript files — including a monorepo's packages/ subfolder that has no package.json of its own — is scanned by inheriting dependency/framework detection from the enclosing workspace root.
React-flavoured rules stay off without React. A new react capability (set only when React or Preact is present) gates every React-runtime rule family (hooks, JSX, accessibility, render performance, React state) plus any rule tagged react-jsx-only, so hook/component-name heuristics like rules-of-hooks, no-legacy-class-lifecycles, and no-nested-component-definition can't false-fire on ordinary TypeScript. Once React (or Preact) is detected, every rule behaves exactly as before.
- Add a
--sfwdemo flag that prints the Socket.dev supply-chain score (0–100) of every direct dependency — across every workspacepackage.jsonin a monorepo, de-duplicated byname@version— color-coded and sorted worst-first, then exits without running a scan. Scores come from Socket's free, keyless PURL endpoint (the same one the supply-chain check uses).
#747
- Add a Socket.dev supply-chain score check. Every direct dependency in
package.jsonis scored against Socket's free, keyless PURL endpoint (the same lookup Socket Firewall's free tier uses) and any dependency whose Socket score falls belowsupplyChain.minScore(default50, 0–100 scale) produces aSecuritydiagnostic anchored at the offendingpackage.jsonentry. At the defaultseverity: "error"a low score fails the scan at the standardblockinggate.
#747
The check runs by default; opt out with supplyChain: { enabled: false }. It is fail-open (per-package timeouts / network failures are skipped, never sinking the scan). A plain --diff / --staged scan skips it like the other whole-project checks, but a diff that edits a package.json (including any workspace's in a monorepo) still scores that project's dependencies — so a PR that adds or bumps a dependency is covered. next is excluded (its framework-specific risks are already covered by the Next.js / server-components rules).
Patch Changes
- CI setup: collapsed the multi-line inline comments in the generated
.github/workflows/react-doctor.ymlto a single explanatory sentence per trigger and one line for the concurrency block, and dropped the permissions comment (the four well-named keys are self-explanatory). The resulting workflow still configures the same triggers, permissions, and action ref — just with less scrolling for new users.
#739
- Fold the standalone
doctor-explainskill into thereact-doctorskill asreferences/explain.md.
#729
Rule-explanation and config-tuning guidance now ships as an on-demand reference inside the primary skill (per the agentskills.io references/ convention) instead of a separate sibling skill. react-doctor install installs a single skill, and the dead bundled-sibling-skill install machinery is removed.
- Name every unused dependency in the verbose warning tail.
#752
Unused-dependency warnings all report at the same line-less location (package.json:0), so the dim location header collapsed every finding into one line and dropped the package names — leaving only a generic deslop/unused-dependency ×N line (#690). react-doctor --verbose now lists each deslop/unused-dependency and deslop/unused-dev-dependency by name, with the shared "why" explanation shown once instead of repeated per package. Errors and code-frame rendering are unchanged.
- Updated dependencies [`b4b79ad`, `af98f83`, `93d4eec`]:
- oxlint-plugin-react-doctor@0.5.0
0.4.2Patch
Patch Changes
- Add a
defineConfighelper for authoring a typeddoctor.config.{ts,js,mjs,cjs}and readreact-doctor.config.jsonas a deprecated fallback.
#721
defineConfig is exported from react-doctor/api (and @react-doctor/api / @react-doctor/core) as an identity helper that gives editor autocomplete and type-checking without an explicit satisfies ReactDoctorConfig annotation:
```ts // doctor.config.ts import { defineConfig } from "react-doctor/api";
export default defineConfig({ lint: true, rules: { "react-doctor/no-array-index-as-key": "off" }, }); ```
The pre-migration react-doctor.config.json filename is now read as the lowest-priority fallback (after doctor.config.* and package.json#reactDoctor) instead of being ignored, so an un-migrated config keeps applying. It still emits a deprecation warning nudging a rename, and interactive runs continue to auto-migrate it to doctor.config.ts. A present-but-broken legacy file stops config resolution (it won't silently inherit an ancestor repo's config), and react-doctor rules <...> migrates a legacy file to doctor.config.json on write rather than editing it in place.
Note: a react-doctor.config.json that was previously ignored in non-interactive runs (CI, coding agents, --json/--score/--staged) is now honored again, which can change which rules fire, the score, and PR gating for projects that still have one. Rename it to doctor.config.json (or delete it) to avoid surprises.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.4.2
0.4.1Patch
Patch Changes
- Fix false positive in
require-reduced-motion: the check now searches untracked files so newly created source (e.g. aproviders.tsxwith<MotionConfig reducedMotion="user">not yet committed) is detected. #711
- CI setup now offers a one-time, per-repo prompt to upgrade an existing React Doctor GitHub Actions workflow from
@v1to@v2— accepting opens a PR with the bump, declining is remembered so it never asks again. The generated / "Add to CI" workflow now pinsmillionco/react-doctor@v2and grantsstatuses: write, so the action can publish the score as a commit status (and surface results on pushes to the default branch).
#706
- Updated dependencies [`dc35070`, `b1a22ef`, `73dcb20`, `64667da`, `ee9ab33`, `fe5f3de`, `831cf3f`]:
- oxlint-plugin-react-doctor@0.4.1
0.4.0Feature
Minor Changes
- Rework CI reporting: a renamed
blockinggate, PR-introduced-issues-only baselines, inline PR review comments, and a simpler CLI flag surface.
#663
CI gate
- `fail-on` is renamed to `blocking` (CLI
--blocking <level>, configblocking, GitHub Actionblockinginput). Sameerror | warning | nonevalues, defaulterror: a scan fails CI when anerror-severity diagnostic reaches theciFailuresurface;warningblocks on any diagnostic;nonestays advisory (always exits 0).--fail-on/failOnstill work as a deprecated, warned alias hidden from--help.--blocking warningnow wins over--no-warnings(it previously silently no-op'd the gate — you can't block on warnings you've hidden).
Baseline — report only the issues a PR introduces (Codecov-style)
- In
--diff <base>mode, react-doctor runs a second lint pass over the changed files as they existed at the base merge-base and reports only the diagnostics the change introduced; pre-existing findings that merely shifted lines are matched out by a content fingerprint (file + rule + flagged-line hash). The head project-health score is unchanged; the gate fails on newly-introduced errors only. If the baseline can't be computed (base unreachable, or a lint pass failed), the run degrades to a plain diff — all findings stay visible and CI isn't gated on findings whose new-vs-pre-existing attribution is unknown.- New core API:
computeDiagnosticDelta,Git.showRefContent/Git.mergeBase,materializeSourceTree, andInspectOptions.baseline/InspectResult.baselineDelta. - JSON report v2: baseline runs emit
schemaVersion: 2with abaselineblock (newCount,fixedCount,baseTotalCount) andmode: "baseline";summary.scorestays the head score. v1 reports are unchanged.
- New core API:
GitHub Action
- Posts inline PR review comments on the changed lines that triggered each diagnostic (with fix guidance + a docs link), plus a restyled CLI-style sticky summary with linkable findings and the new / fixed delta. The
annotationsinput was removed.- On pull requests it fetches the base commit for baselining — use
fetch-depth: 0onactions/checkout. Newfixed-issuesoutput. Defaults:project: "*",node-version: 24.
- On pull requests it fetches the base commit for baselining — use
CLI flags (fewer flags, fewer footguns)
--explain/--why→ thereact-doctor why <file>:<line>subcommand (rules explain <rule>still explains what a rule means).- Removed
--full(use--diff falseto force a full scan),--pr-comment(the Action renders its comment from--json), and the positive--respect-inline-disables(already the default; use--no-respect-inline-disablesfor audit mode). The internal--changed-files-fromis hidden from--help. - Removed flags now fail with a migration error instead of being silently dropped, and an empty
--projectfilter (e.g.--project ",") is rejected.
- Removed
Patch Changes
- Add
react-doctor experimental-lsp, an experimental language server that surfaces React Doctor diagnostics directly in your editor — VS Code, Cursor, Zed, Neovim, Sublime Text, Emacs, Helix, or any LSP client. It is gated behind theexperimental-prefix while its protocol, caching, and diagnostics stabilize. It scans the file you are editing live from the unsaved buffer, underlines the exact offending token via precise ranges, shows rich hovers (rule, category, recommendation, docs link), and offers quick fixes (disable-for-this-line with the correct comment style, suppress-all-in-file, explain, open docs, report false positive). It discovers every React project across workspace folders and monorepo packages, runs offline (no score lookup, no git), prioritizes open-buffer scans, supports push and pull diagnostics, and invalidates caches when config / package.json / lockfiles change. The background workspace scan is chunked so diagnostics stream in progressively (seconds to first results on large repos), parallelizes across all CPU cores, reserves a slot so edits stay responsive while it runs, and cancels in-flight work when config changes. Results are cached per file (by content metadata, invalidated on config change) and persisted to disk, so re-opening the editor or re-scanning surfaces diagnostics almost instantly — on an ~8,800-file repo a cold scan is ~27s and a warm scan ~2s.
#681
Start it with react-doctor experimental-lsp --stdio (or npx react-doctor@latest experimental-lsp --stdio). A scanOnType initialization option toggles live-as-you-type scanning, with first-class companion extensions for VS Code/Cursor and Zed.
Like the CLI, the language server reports anonymized usage analytics to Sentry — a per-workspace-scan wide event plus session/scan counters — sharing the CLI's IP-stripping and path/secret scrubbing. Opt out with REACT_DOCTOR_NO_TELEMETRY=1 (or by launching it with --no-telemetry).
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.4.0
0.3.0Feature
Minor Changes
- Add an "Add to CI" path to the post-scan handoff and make
installset up CI by default.
#658
The post-scan prompt now leads with an "Add to CI" choice (the default) that installs the react-doctor dev dependency + doctor script and writes a .github/workflows/react-doctor.yml GitHub Actions workflow so every pull request is scanned. When you instead hand off to an agent, the generated prompt now asks the agent to offer CI setup first. The install subcommand pre-selects the workflow and install --yes now writes it by default. The workflow's action is pinned to the @v1 floating major (never @main, per the supply-chain guidance in issue #299).
Patch Changes
react-doctor --full --yesno longer errors with "Cannot combine --yes and --full; pick one." #676
--yes (skip prompts, scan all workspace projects) and --full (force a full scan, overriding any diff value) control orthogonal concerns, so combining them is a valid request — "scan every workspace project fully, without prompting." The mutual-exclusion check that rejected the pair has been removed.
- Bump bundled
deslop-jsto^0.0.17, which stopsdeslop/unused-dev-dependencyfrom false-positiving on dependencies referenced in apackage.jsonscript as a flag argument rather than the leading command — e.g.jest --testResultsProcessor jest-sonar-reporteror--reporters=jest-junit(#653).
#674
- Update the dead-code analysis engine (
deslop-js) to0.0.16.
#668
- react-doctor no longer crashes when the
--changed-files-fromfile can't be read.
#655
--changed-files-from <file> is user input, so an unreadable file — missing, a directory, permission-denied, or a stale pipe/process-substitution descriptor (EBADF, REACT-DOCTOR-V) — is an invocation mistake, not a bug. It now exits non-zero with a clean, single-line message telling you to pass a readable text file, instead of printing the generic "Something went wrong" block and reporting the read failure to Sentry.
- react-doctor now records a single anonymized per-scan "wide event" on its Sentry run span — the full run/CI/project/outcome context (scan mode, score, diagnostics by severity and category, top rule, lint/dead-code state, and, in CI, the GitHub event, an official-action marker, the forwarded action inputs, and the pull-request gate) — so usage and CI behavior can be analyzed by querying spans instead of pre-aggregated counters.
#660
It also mints a random per-run runId attached to the Sentry run context (never as a tag or metric dimension) to correlate the spans of a single run. Telemetry stays anonymized — no repo, owner, username, branch, or path is sent to Sentry — and --no-score / --no-telemetry still opts out entirely. The official GitHub Action forwards its inputs (fail-on, non-blocking, comment, annotations, version) so action configuration is visible in telemetry.
- Polished the first-run onboarding experience — the animated welcome scene now plays on every interactive regular-mode run (not just the first) but at half the cadence for returning users (
hasCompletedOnboarding()),--verboseskips the intro entirely and goes straight to the static branded header, and the closing"Let's scan your codebase..."typewriter beat was cut so the intro ends on the tagline.
#658
Restructured the scan-report layout so the top-errors detail (code frames + fixes) leads the report and the per-category breakdown moves down as a wrap-up overview directly above the score. The breakdown now has its own bold All N issues header (mirroring Top N errors you should fix) with the total folded into the header text, categories sort in a fixed Security → Bugs → Performance → Accessibility → Maintainability order, and warnings no longer get boxed code frames in --verbose (errors still do) so a long warning tail stops drowning the report. The trailing --verbose CTA drops the redundant +N more rules and +N optional warnings stats (the breakdown above already carries those) and reads as a clean Run npx react-doctor@latest --verbose to list every error and warning.
Quieted the "Add to CI" handoff: it no longer runs the local dev-dep install (the doctor package script and the GitHub workflow both invoke npx react-doctor@latest, so a local copy adds nothing and on pnpm with a beta channel it noisily trips the supply-chain trust guard for zero user benefit). The trust-policy skip on the react-doctor install path now renders as a yellow ⚠ warning with a tightened one-liner and a dim follow-up showing the manual install command, instead of a red ✖ that read like a crash next to its own "React Doctor still works" reassurance.
Made the case for GitHub Actions before the handoff prompt instead of after it. The scan-report footer now closes with a GitHub Actions: https://react.doctor/ci entry (matching the Share / Docs / GitHub bold label + dim description shape) carrying the strongest reasons in two short lines: Scan every pull request: new PRs stay clean while you fix the backlog + Used by teams at PayPal, Rippling, and Alibaba. Sitting last in the footer makes it the final thing read before the handoff prompt that recommends the same action. The prompt's choice reads as Add to GitHub Actions (recommended) (or (already configured)) with a description of what gets set up; the state tag lives in the title so the description always describes what the option _does_, not the project's current state. The post-pick message drops the social-proof + backlog framing (now redundant — the footer already showed it) and just confirms what changed plus the docs link.
- React Compiler projects no longer report
jsx-no-constructed-context-valuesfor fresh context provider values that the compiler memoizes automatically.
#667
- react-doctor no longer crashes when a directory can't be enumerated during project discovery.
#654
The recursive subproject crawl reads directories best-effort and already skipped ones it couldn't open for permission or missing-path reasons (EACCES/EPERM/ENOENT/ENOTDIR). It now also skips directories the underlying filesystem rejects outright — EINVAL on scandir (REACT-DOCTOR-N, seen on special/virtual mounts), plus symlink loops (ELOOP) and over-long paths (ENAMETOOLONG) — instead of throwing and reporting the environment issue to Sentry. The crawl continues past the unreadable directory.
- Retires
rn-animate-layout-property. ReanimateduseAnimatedStyleruns entirely on the UI thread, so layout-affecting style animations driven by helpers likewithTimingorwithSpringare valid and should not be flagged.
#666
- Two React Native rules no longer false-positive on Expo Universal UI (
@expo/ui).
#645
@expo/ui is a native UI layer (it delegates to SwiftUI / Jetpack Compose), not React Native's core primitives, so several RN-core assumptions don't hold for its components:
- `rn-no-raw-text`: Universal UI's
<ListItem>renders its raw string children inside the native headline text area, and its compound slot markers (<ListItem.Leading>,<ListItem.Supporting>,<ListItem.Trailing>) forward strings into native text too — so raw text inside them is safe, unlike React Native's core<View>. The rule now recognizes them as text-handling.- `rn-no-scrollview-mapped-list`: Universal UI's
<ScrollView>is a native scroll container; React Native's virtualized lists (FlashList/FlatList) can't compose inside its<Host>tree, and@expo/uiships its own virtualized<List>. The rule no longer flags mapped children inside an@expo/uiScrollView.
- `rn-no-scrollview-mapped-list`: Universal UI's
Both checks are gated on the @expo/ui import (root, @expo/ui/swift-ui, or @expo/ui/jetpack-compose, including renamed and namespace imports), so same-named components from other libraries — or with no import — still report.
prefer-module-scope-static-value("Static value rebuilt every render") is now disabled when React Compiler is enabled.
#672
React Compiler already hoists and caches per-render array/object allocations, so both halves of the recommendation — avoid the re-allocation and preserve referential equality for memoized children — are handled automatically, making the warning pure noise on a compiler-enabled codebase (#669). The rule now carries disabledBy: ["react-compiler"], matching the jsx-no-new-*-as-prop rules that gate on the same capability.
- A terminal hangup during an interactive prompt no longer crashes the CLI. When the terminal/PTY backing a prompt goes away mid-read (closing the tab, a dropped SSH/tmux session, sleep/wake), Node raises
read EIOon the raw-mode stdin handle; the CLI now exits cleanly (code 129) instead of surfacing it as a fatal uncaught exception and reporting it to crash telemetry. Genuine stdin errors still funnel to the error reporter unchanged.
#650
no-wide-letter-spacingno longer false-positives on uppercase labels styled through a wrapper component prop.
#673
The rule exempts wide tracking on uppercase text, but it could only see textTransform: 'uppercase' written inline in the same style object. Design-system text components routinely apply the transform from a prop instead (<SSText uppercase style={{ letterSpacing: 2 }}>), which the rule can't see inside the component (#671). It now also treats a sibling uppercase boolean prop or a textTransform="uppercase" prop on the same element as the uppercase signal, so those short labels stay quiet.
- Updated dependencies [`eba20ae`, `5d7b36b`]:
- oxlint-plugin-react-doctor@0.3.0
0.2.18Patch
Patch Changes
- The interactive category breakdown now reveals issues one at a time — a category's errors before its warnings, top to bottom — instead of every count easing up in parallel, holds for a beat once it settles, and finally plays on monorepo (multi-project) scans too.
#640
Previously the count-up only animated on single-project scans; the multi-project aggregate report rendered the breakdown (and the score projection) statically. Both now share the same interactive reveal, gated on the same real-TTY predicate, so a monorepo's report animates like a single project's. Small and medium breakdowns step one issue per frame; very large ones grow the per-step increment so the reveal still resolves quickly.
- react-doctor no longer crashes when
gitisn't installed.
#643
During a normal scan, diff auto-detection reads the current branch first. When the git binary couldn't be spawned (e.g. a bare container with no git on PATH), that best-effort read threw instead of degrading, crashing the scan and reporting an environment issue to Sentry (REACT-DOCTOR-F). It now degrades to "unknown branch" — matching how a non-zero git exit was already handled — so the scan continues without git context.
- Expected, user-actionable failures are no longer reported to Sentry or rendered as crashes.
#642
When react-doctor exits because of the user's project or invocation — not a bug — it now prints a clean, single-line message and exits non-zero, instead of the generic "Something went wrong, open a prefilled issue" block. These cases are also no longer sent to Sentry or counted in the alertable error-rate metric. This was flooding crash reporting with non-bugs from CI, coding agents, and sandboxes.
Covered cases:
- No React / no project / missing path — every project-discovery failure (
NoReactDependencyError,ProjectNotFoundError,PackageJsonNotFoundError,NotADirectoryError,AmbiguousProjectError) is now treated as a clean user error (REACT-DOCTOR-1, -4, -6, -7). When the scan target simply doesn't exist on disk, the message now says the path doesn't exist instead of the misleading "Expected a package.json…" guidance.- CLI invocation mistakes — a malformed
<file>:<line>argument, mutually exclusive flags (e.g.--yes+--full), and an unknown--projectname now render as clean errors (REACT-DOCTOR-B, -D, -G, -H). - Read-only config directory — react-doctor no longer crashes when it can't create/read its global setup-prompt store on a locked-down or read-only filesystem; it degrades gracefully (REACT-DOCTOR-E).
- CLI invocation mistakes — a malformed
The fix is enforced centrally in reportErrorToSentry, so the CLI entry point, inspect, and install all benefit.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.18
0.2.17Patch
Patch Changes
- Redesigned the interactive scan report and added a first-run onboarding reveal.
#638
The default single-project report now reads top-to-bottom the way a human scans it: the category tally, then the score box (with the total issue count inline on the score line, e.g. 7 / 100 Critical 295 issues), the projection, the top fixes one by one, the warning roll-up, a single merged +N more errors and +N more warnings overflow line, and finally Share / Docs / Tip. The per-section +N more rules lines, the N warnings sub-header, and the Top N errors you should fix header were removed for a cleaner read. CI, coding-agent, git-hook, and verbose runs keep the classic information-dense layout (diagnostics first, then agent guidance and score).
On a user's first interactive run it plays as an onboarding sequence: a happy React Doctor "welcome" scene opens, the scan runs, the category tallies count up from zero in parallel, and then each section — and each of the top errors — reveals on an ~850ms beat (quickening to ~680ms once the score lands) instead of a wall of text. It runs only once (a marker persisted in the global config records that it was shown), and is skipped entirely in CI, under coding agents, and on any non-TTY / score-only / JSON run.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.17
0.2.16Patch
Patch Changes
- Redesign the scan output's summary and footer. The default (non-verbose) run no longer lists every warning rule — warnings are rolled into a single overflow line alongside the hidden errors (e.g.
+4 more rules and +50 optional warnings — run npx react-doctor@latest --verbose for details).--verbosenow renders warnings in the same boxed, titled, code-framed format as errors (with a "Learn more" docs link), instead of a separate compact list. The closing footer is restructured into aShare:/Docs:/GitHub:block (each with a one-line description) separated by a divider, and the share link now appears for monorepo runs too (gated the same way as single-project: shown unless CI,--no-score, orshare: false). The scan spinner's worker count now reads as a dimmed[~N workers].
#637
--diffnow accepts git commit ranges, and a bad--diffvalue is no longer treated as a crash.
#633
--diff A..B(two-dot, diff A directly against B) and--diff A...B(three-dot, diff from the merge-base of A and B to B) are now supported, matching git's own range syntax — an empty endpoint defaults toHEAD(main..⇒main..HEAD). Previously any value containing..was rejected outright, soreact-doctor --diff 7694215..c4de712failed. Each range endpoint is still individually validated against the anti-injection guard, so a range can't smuggle a--upload-pack=…-style option past it.- An invalid
--diffvalue (a malformed ref/range or a base branch that hasn't been fetched) is now rendered as a clean, single-line message and exits non-zero — it no longer prints the generic "Something went wrong, open a prefilled issue" block or reports the expected user error to Sentry.
- An invalid
- Lint in parallel by default. React Doctor now fans the lint pass across your CPU cores out of the box (previously serial) and automatically falls back to a single worker if a parallel run exhausts system resources (
EAGAIN/EMFILE/ENFILE/ENOMEM); any other failure still surfaces. Pass--no-parallel(or setREACT_DOCTOR_PARALLEL=0) to force serial linting, or setREACT_DOCTOR_PARALLEL=<n>to pin a worker count. The experimental--experimental-parallelflag is replaced by--no-parallel.
#635
- Add anonymized Sentry Application Metrics (counters + distributions) to the CLI, alongside the existing crash reporting and tracing, so we can track reliability/performance and prioritize work.
#634
- Counters & distributions: each run records
cli.invoked(per command),scan.completed,scan.duration/scan.files/scan.score,project.detected(anonymous project shape),rule.fired(a per-rule counter keyed byrule/plugin/category/severity, so we can see which rules actually catch issues, which are noisy, and which never fire),lint.failed/deadcode.failed/scan.check_skipped/score.unavailable,cli.error, plus growth/activation signals oninstall(which coding agents, git hook, CI workflow, agent hooks, dependency outcome), the agent-handoff fix loop (agent.handoff), andrulesconfig changes (rules.changed/rules.queried).- Trace-connected & enabled by default: metrics use
Sentry.metrics.*(SDK ≥ 10.25), flow independently ofSENTRY_TRACES_SAMPLE_RATE, and carry the run snapshot + project shape (rebuilt per emit, mirroring the per-event run tags). - Anonymized by default: a
beforeSendMetrichook drops theserver.addresshostname attribute and scrubs home-directory paths + known secrets from attribute values via the same redactor used for events, dropping the metric on failure. Attributes are enums/booleans/counts/rule names only — no source code or specific findings. - Opt-out unchanged:
--no-score(and its--no-telemetryalias) disables metrics along with crash reporting and tracing; metrics are skipped under test runs, and the programmatic@react-doctor/apilibrary never initializes Sentry.
- Trace-connected & enabled by default: metrics use
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.16
0.2.15Patch
Patch Changes
- Stop flagging known public client keys in
no-secrets-in-client-code. Keys that vendors design to ship in the browser bundle — RevenueCat public SDK keys (appl_/goog_/amzn_/strp_), Stripe/Clerk publishable keys (pk_live_/pk_test_), Supabase publishable keys (sb_publishable_), PostHog project keys (phc_), Stytch public tokens (public-token-), and Mapbox public access tokens (pk.) — are now allowlisted, so the variable-name heuristic no longer reports them as hardcoded secrets. Ambiguous shapes that can be either public or sensitive (Google/FirebaseAIza…browser keys, and bare Supabaseanon/service_roleJWTs) are intentionally still flagged.
#612
- Diagnostic ranking now depends solely on the score API's per-rule priority. The hand-rolled severity/category-stakes weighting (and the offline priority midpoints) is gone: when the API priority is unavailable (
--no-score, offline, or API failure) rules and categories keep their scan order, with categories falling back to alphabetical for determinism.
#596
- Treat
CI=1andCI=Trueas CI environments, not justCI=true. CI-only behavior (suppressing the share URL, marking the run as CI-originated for scoring) now triggers consistently across providers that setCIto a truthy value other than the literal string"true"; explicitCI=false/CI=0are still treated as non-CI.
#619
A present-but-unparseable react-doctor.config.json at the scanned root no longer silently falls through to a parent directory's config. The tool stops there instead of letting an ancestor repo's config govern the project; a package.json reactDoctor config in the same directory is still used as a fallback.
- Update the dead-code analysis engine (
deslop-js) to0.0.14so the published CLI's unused-file / dead-code detection runs on the latest release. The CLI previously pinned^0.0.13while the internal core engine was already on0.0.14; this aligns both on a single version and drops the duplicate from the lockfile.
#605
- Collapse diagnostic categories into five clear, outcome-based buckets: Security, Bugs, Performance, Accessibility, and Maintainability. The previous fine-grained labels (Correctness, State & Effects, React Compiler, Next.js, React Native, Server, TanStack Query/Start, Preact → Bugs; Bundle Size → Performance; Architecture/Design → Maintainability) now roll up so the scan output reads as plain issue types at a glance.
#596
This changes the category value on every diagnostic (CLI output, the per-error headline prefix like Security: Use of eval(), and JSON/programmatic output). If you key categories severity overrides off the old names, update them to the new buckets. Dead-code findings (unused files/exports/dependencies, circular imports) now report Maintainability instead of Dead Code. Bundle-size findings now sort with Performance (higher stakes) rather than near the bottom of the top-errors block.
- Align the CLI with the clig.dev and 12-factor CLI guidelines:
#623
--color/--no-colorflags force or disable colored output, with app-specificREACT_DOCTOR_NO_COLOR/REACT_DOCTOR_FORCE_COLORenv overrides. Flags win over env vars, which win over picocolors' built-inNO_COLOR/FORCE_COLOR/TERM/ TTY detection; the preference is resolved before parsing so it reaches every surface (scan report, branded header, score, prompts, errors).react-doctor --helpandreact-doctor install --helpnow lead with worked examples and link to where to report feedback.- New
react-doctor versionsubcommand prints the version with Node and platform info (e.g.react-doctor/0.2.14 darwin-arm64 node-v24.14.0);-v/-V/--versionstay terse for scripts. react-doctor helpandreact-doctor help <command>now show help instead of failing by trying to scan a directory named "help".
- Port expo-doctor's project-level checks as Expo-gated diagnostics. When an Expo project is detected (
expoVersion !== null), react-doctor now runs the statically-determinable subset of expo-doctor's check suite during the environment-checks phase (skipped in diff/staged mode):
#583
expo-no-unimodules-packages— legacy@unimodules/*/react-native-unimodulespackages (IllegalPackageCheck).expo-no-cli-dependencies—expo-cli/eas-clilisted as project dependencies (GlobalPackageInstalledLocallyCheck).expo-no-redundant-dependency— packages Expo installs transitively or that were removed/deprecated (expo-modules-core,@expo/metro-config,@types/react-native,expo-permissions, theexpo-firebase-*family, …), each SDK-version gated (DirectPackageInstallCheck).expo-no-conflicting-dependency-override—overrides/resolutions/pnpm.overridesthat pin SDK-critical packages like@expo/cliormetro*(DependencyVersionOverrideCheck).expo-router-no-react-navigation— direct@react-navigation/*alongsideexpo-routeron the SDK 56 line only (>=56 <57, matching expo-doctor's range) (ExpoRouterReactNavigationCheck).expo-vector-icons-conflict— scoped icon packages mixed with@expo/vector-icons/react-native-vector-icons(VectorIconsCheck).expo-package-json-conflict—expo/react-nativescripts shadowing node_modules bins, and a package name colliding with a dependency (PackageJsonCheck).expo-lockfile— missing or multiple lock files at the workspace root (LockfileCheck).expo-gitignore— a committed.expo/directory, or local module native dirs that are gitignored (ProjectSetupCheck).expo-env-local-not-gitignored— committed.env*.localfiles (EnvLocalFilesCheck).expo-metro-config— a metro config that doesn't extendexpo/metro-config, while tolerating known wrappers that extend it internally such as Sentry'sgetSentryExpoConfig(MetroConfigCheck).
The remaining expo-doctor checks require running the Expo CLI, querying the Expo API, or inspecting native iOS/Android projects — none of which fit react-doctor's offline, static model — so they're intentionally out of scope.
- Detect Expo projects independently of the single-valued
frameworkhint. Project discovery now surfaces anexpoVersionsignal (the declaredexpopackage spec, looked up in the project or any of its workspace packages, ornull), parallelingreactVersion. Theexpocapability is keyed offexpoVersion !== nullrather thanframework === "expo", so Expo-specific rules now load on web-rooted monorepos whoseapps/mobileworkspace targets Expo, and on projects that declare bothexpoand a web bundler (wherevite/nextpreviously won framework detection and silently dropped theexpocapability). The file-level package boundary inoxlint-plugin-react-doctorstill keeps Expo-only rules quiet on web workspaces.
#583
- Fix two dead-code / fix-recipe papercuts surfaced on alias-heavy Next.js projects.
#615
Dead-code no longer mis-flags `@/…` (and other) imports as unused. The dead-code pass resolves imports through oxc-resolver, which returns realpath'd (symlink-free) paths, but built its module graph from the scan root as-is. When the project root sat behind a symlink — e.g. a macOS iCloud-synced ~/Documents / ~/Desktop, or a symlinked checkout — the two path spaces diverged, every import edge dropped, and files reachable only through those imports (in an alias-heavy codebase, every @/… target) were reported as "unused / unreachable". The scan root is now canonicalized before analysis so the module graph and the resolver agree. This was never specific to @/* aliases; relative imports were affected the same way.
Per-rule fix-recipe URLs are only shown when a recipe exists. Findings advertised a "fetch the canonical fix recipe" URL (/prompts/rules/<plugin>/<rule>.md) for every diagnostic, but recipes are only published for react-doctor's own engine rules. Dead-code (deslop/*), the environment / supply-chain checks (require-reduced-motion, require-pnpm-hardening), and adopted third-party plugins (eslint, unicorn, react-hooks-js, …) have no recipe, so their links 404. The directive is now gated to engine rules, so agents are no longer sent to dead links.
- Fix
react-doctor --staged(and other scans) hanging after the diagnostics summary is already printed. When an adopted lint config crashed oxlint on the first attempt, the oxlint runner's per-batch progress timer was left running while the scan silently retried withextendsstripped — so the run finished and printed results, but the orphanedsetIntervalkept the Node event loop alive and the process never returned control to the shell. The batch loop now clears the timer in afinally, so it's always cleaned up even when a batch throws. See #599.
#607
jsx-keyno longer reports a missing key when a list element spreads the whole iteration item —items.map((item) => <Item {...item} />). Spreading the row object is the canonical "this row carries its own identity" shape and was the dominant source ofjsx-keynoise on real lists, while rarely catching a genuine reorder bug. Genuine keyless lists still report:items.map((item) => <Item name={item.name} />), index keys, array literals ([<Item {...item} />]), and spreads of anything other than the iteration variable.
#614
- App-only heuristics now stay quiet in published libraries, and React Compiler memoization-cleanup is demoted to a warning.
#614
react-hooks-js/static-componentsandno-render-prop-childrenno longer fire on files in a published library — a non-privatepackage.jsonthat declares the publish contract (name+exports). They still fire in applications (including private monorepo apps that live underpackages/or declare a niche internalexportsmap) and in any package without that contract, and an explicit per-rule severity in config always re-enables them.react-compiler-no-manual-memoizationnow defaults towarninstead oferrorwhen React Compiler is detected — redundantuseMemo/useCallback/memois correctness-neutral cleanup, so it's hidden from the default report. The externalreact-hooks-js/*compiler rules stayerrorbecause each marks code the compiler could not optimize (a real perf regression).- New
bucketsconfig field: set{ "buckets": { "compiler-cleanup": "error" } }to re-enable strict errors for the redundant-memoization rule. A per-rule override still wins over a bucket.
- Speed up scans of effect-heavy codebases by memoizing
getDownstreamRefsin the State & Effects rule helpers.ascend()re-descended the same large definition subtrees on every recursion step, so the seven effect rules (led byno-pass-data-to-parent) blew up superlinearly on big components with manyuseEffects — re-walking and re-scoping identical bodies across recursion, across effects, and across rules. Caching the downstream-reference lookup per Program node (aWeakMapkeyed on the per-Programanalysis singleton, GC-bound with the file) collapses that to a single descent.
#613
On an 866-file Next.js app this cut ~9s (~24%) off a full scan — the worst rule on the largest file (a 1,159-line component with 10 effects) dropped from ~9.5s to ~0.18s, and the hot lint batch from ~13.5s to ~2.5s. Diagnostics are byte-identical (verified by a SHA-256 fingerprint over every diagnostic before/after); the cache only stores arrays callers already read and never mutate.
- Add an
--experimental-parallel [workers]flag that runs the oxlint lint pass across multiple worker processes instead of one batch at a time. React Doctor's rules are oxlint JS plugins (single-threaded per process), so a serial scan only ever pins one core;--experimental-parallelfans the file batches out across the requested number of concurrent oxlint subprocesses, which scales the scan nearly linearly with CPU cores (measured ~3.5–4.6x on a 1,500-file project and ~4.6x on Sentry's 8,773 files) while producing byte-identical diagnostics.
#616
--experimental-parallel with no value auto-detects available cores; --experimental-parallel <n> caps the worker count; REACT_DOCTOR_PARALLEL=<n> seeds the default for flag-less / CI runs. The worker count is clamped to a safe range to bound peak memory, and the default remains serial so resource usage stays opt-in.
- Publish a JSON Schema for
react-doctor.config.jsonathttps://react.doctor/schema/config.json.
#601
Pointing $schema at the URL enables editor autocomplete, hover docs from the interface JSDoc, and typo warnings in any editor that understands JSON Schema. Closes #497.
jsonc { "$schema": "https://react.doctor/schema/config.json", "lint": true }
The schema is generated from packages/core/src/types/config.ts via pnpm build:schema and checked into packages/website/public/schema/config.json.
- Redact secrets and PII from diagnostic output. Every diagnostic's
message/helpis now scrubbed for API keys, tokens, private keys, JWTs, credentialed URLs, and email addresses before it reaches the terminal, the JSON report, or the score API — so react-doctor never echoes or transmits a secret embedded in your source. Provider tokens keep their non-secret, type-identifying prefix (e.g.sk_live_<redacted>,ghp_<redacted>) so you can tell which credential leaked while the secret itself stays masked.
#606
- Add 10 React Native & Expo diagnostics (researched against first-party docs/RFCs and validated against an OSS corpus). Six are oxlint AST rules; four are project-level checks gated on the React Native / Expo capability and run in the environment-checks phase (skipped in diff/staged mode).
#625
AST rules:
rn-no-deep-imports— deep imports of public symbols fromreact-native/Libraries/*(RFC 0894; breaks on upgrade). Curated to symbols re-exported from the root, with a tailored message for the relocatedNewAppScreen; skips type-only imports and the Codegen/TurboModule authoring surface.rn-no-set-native-props—ref.current(?.).setNativeProps(...), a silent no-op under the New Architecture (Fabric).rn-no-image-children— children inside react-native's<Image>(which renders none); use<ImageBackground>. Resolves the element to thereact-nativeimport soexpo-image/customImageare ignored.rn-no-panresponder—PanResponderimported fromreact-native(JS-thread gestures); usereact-native-gesture-handler.rn-detox-missing-await— un-awaited Detox actions /waitFor/expect(element(...))in*.e2e.*files.expo-no-non-inlined-env— computedprocess.env[...]andprocess.envdestructuring, whichbabel-preset-expocan't inline (value isundefinedat runtime); scoped to Expo client files.
Project-level checks:
rn-no-metro-babel-preset—module:metro-react-native-babel-presetin a babel config (renamed to@react-native/babel-preset; uninstalled on RN 0.73+).rn-library-react-in-dependencies— areact-native-builder-boblibrary listingreact/react-nativeindependenciesinstead ofpeerDependencies(duplicate-React / duplicate-native-module crashes).expo-reanimated-v4-requires-new-arch—react-native-reanimatedv4 withnewArchEnabled: falsein the app config (first-launch crash).expo-updates-no-unsafe-production-config—updates.disableAntiBrickingMeasures: truein the app config (can brick installed apps).
rn-no-raw-textnow auto-detects in-file custom text wrappers, cutting false positives on design-system<Text>forwarders. A component whose returned root is a<Text>— e.g.const Banner = ({ children }) => <Text>{children}</Text>orexport const Caption = (props) => <Text {...props} />— is treated as a string-only text forwarder, so raw text passed to it (<Banner>Hello</Banner>) no longer reports. Mixed children still report (<Banner><Icon /> hi</Banner>) because a single-<Text>forwarder can't be trusted to route a JSX child into text. Components only referenced (not defined) in the file keep the existing name-heuristic behavior, and the config-driventextComponents/rawTextWrapperComponentsoverrides are unchanged.
#614
- Configure React Doctor with
doctor.config.{ts,js,mjs,cjs,mts,cts,json}(or apackage.json#reactDoctorkey), and addreact-doctor rulescommands to list, explain, and configure rules without hand-editing config.
#617
- TS-first config. Author
doctor.config.ts(or any JS/JSON variant) — TypeScript and ESM configs load viajiti, and JSON configs allow comments and trailing commas (JSONC).- `rules` commands.
rules listshows every rule and the severity it runs at;rules explain <rule>describes why a rule matters and how to tune it; andrules set/enable/disable/category/ignore-tag/unignore-tagedit your config for you. TS/JS configs are edited in place viamagicast(formatting and comments preserved); JSON andpackage.jsonare edited as data; adoctor.config.jsonis created when no config exists. Rule references accept the full key (react-doctor/no-danger), the bare id (no-danger), or a legacy key (react/no-danger). - `doctor-explain` skill (alias
doctor-config), shipped viareact-doctor install, teaches coding agents to explain a rule before disabling it and to pick the narrowest control (rule severity vs category vs tag vssurfaces).
- `rules` commands.
Breaking: the config file is now doctor.config.* instead of react-doctor.config.json. The next time you run react-doctor interactively, an existing react-doctor.config.json is automatically migrated to a typed doctor.config.ts (settings preserved, $schema dropped) and you're told once — CI, coding-agent, --staged, JSON/score, and non-TTY runs are left untouched (a warning still nudges them). The package.json#reactDoctor key is unchanged.
- Cleaner scan output and smarter file scoping:
#596
- The post-scan summary now leads with a "Top errors you should fix" block — each error shows a plain-language explanation and an inline code frame, with the rule's human title prefixed by its category (e.g.
Security: Use of eval()) instead of its id, so it's clear at a glance what kind of problem it is.- Security rules now read as security findings:
dangerouslySetInnerHTML(XSS) is categorized under Security, and security messages use explicit vulnerability language (code injection, XSS, reverse tabnabbing, CSRF, secret exposure). - Every rule's messages were rewritten to be short, plain, and dash-free, and each rule now carries a short
title. - Generated bundler output (
*.iife.js,*.umd.js,*.global.js,*.min.js) is now excluded from scans by default. As a resultproject.sourceFileCount(and the scanned-file totals) no longer count these generated bundles. - Minified files that carry an ordinary extension (e.g. a one-line
public/inject.jsbundle) are now detected by content and skipped, so they no longer flood the report with noise. Any diagnostic that still lands on an overlong single line falls back to afile:linereference instead of rendering an unreadable code frame. - Multi-project scans now report the number of UNIQUE files scanned, so nested workspace packages (a parent whose tree contains a child package) are no longer double-counted in the "Scanned N files" total.
- Security rules now read as security findings:
- Add Sentry crash reporting to the CLI. Uncaught errors that reach the CLI's error funnels are now captured via
@sentry/nodeand flushed before the process exits, each enriched with aruncontext snapshot (version, node/platform/arch, the invocationcommand/argv,cwd, CI provider, coding agent, interactivity, and JSON mode) to make crashes triage-able. Sentry initializes as the first statement of the CLI entry so its global handlers are armed before any command runs, and it's scoped to the CLI only — the programmatic@react-doctor/apilibrary never initializes Sentry.
#621
Reporting is opt-out: pass --no-score to disable crash reporting along with the hosted score API and share URL. The SDK is also skipped under test runs (VITEST / NODE_ENV=test).
- Deepen the CLI's Sentry integration: uploaded source maps, unified tracing, and richer run context.
#628
- Source maps: the CLI bundle is now built with source maps, and the release pipeline injects Sentry Debug IDs into
dist/cli.jsand uploads the maps (scripts/sentry-sourcemaps.mjs, run frompnpm releaseand the@devpublish job) so crash stack traces are fully de-minified. Maps are uploaded to Sentry, not shipped in the npm tarball. Wired for both tagged releases and@devsnapshots; a no-op unless theSENTRY_AUTH_TOKEN/SENTRY_ORG/SENTRY_PROJECTCI secrets are configured.- Tracing / OpenTelemetry: each run is now a Sentry transaction, and the existing Effect instrumentation (
runInspectplus everyEffect.fn("Service.method")span) is bridged straight into Sentry as one unified per-run trace. If a user has their own OTLP backend configured (REACT_DOCTOR_OTLP_*), that still wins and the Effect trace is additionally parented under the Sentry trace so the two share atrace_id. Tracing is tunable viaSENTRY_TRACES_SAMPLE_RATE(set to0to disable; default samples every run). - Crash references: when an error is reported, the Sentry event id is surfaced as a reference — printed in the CLI's error output ("Reference (mention this when reporting): …") and added to the prefilled GitHub issue — so a user-reported crash can be located in Sentry by id. Errors thrown during a scan are also linked back to that run's transaction trace (same
trace_id) so the crash and its spans appear together. - Environment / run information: events now carry a Sentry
environment(production/development, overridable viaSENTRY_ENVIRONMENT), areact-doctor@<version>releasethat matches the uploaded source-map artifacts, and the full run snapshot as searchable tags on _every_ event (not just exceptions) — including which command ran (command,argv), where it ran (origin= cli/ci/agent/git-hook, plusci/ciProvider), the launching package manager (invokedVia, e.g. npm vs. pnpm dlx), and Node major version. - Project information: the anonymous project shape we already detect during a scan is attached to crashes and the run transaction as soon as it's discovered — searchable
project.*tags (framework, React major, TypeScript, React Compiler, Expo, React Native) plus aprojectcontext block (versions of React/Tailwind/Zod/Preact/Expo, TanStack Query, Reanimated, source-file count). The identifyingprojectName/rootDirectoryare deliberately excluded; no source code or diagnostic findings are sent. - Anonymized by default: every event and transaction is scrubbed before it leaves the machine —
sendDefaultPiiis off (no IP), the hostname/server_name/device name and captured local variables are stripped, the OS username is removed from all paths (home directory →~) across cwd, argv, stack frames, and span attributes (e.g. theinspect.directorypath), and known secrets/emails are masked via the same redactor used for diagnostics. If scrubbing ever fails, the event is dropped rather than sent. - Configuration:
SENTRY_DSN,SENTRY_ENVIRONMENT,SENTRY_RELEASE,SENTRY_TRACES_SAMPLE_RATE, andSENTRY_DEBUGare all honored at runtime.
- Tracing / OpenTelemetry: each run is now a Sentry transaction, and the existing Effect instrumentation (
Reporting remains opt-out and CLI-only: --no-score disables Sentry entirely (crash reporting and tracing), it's skipped under test runs, and the programmatic @react-doctor/api library never initializes Sentry.
- Show
"warning"-severity diagnostics by default again. A scan that reports only errors is too generous a bar for a health check, so warnings surface on every surface (CLI, PR comment, score,--fail-on) out of the box. Opt out with--no-warningsor"warnings": false; per-rule / per-category severity overrides still win as before.
#622
- Hide
warning-severity diagnostics by default — a clean scan now reports onlyerror-severity findings (errors always show). Opt warnings back in with the--warningsflag or"warnings": trueconfig option;--no-warnings/"warnings": falseis the explicit default-off. The toggle is the master switch and runs after per-rule / per-category severity overrides, so a rule explicitly set to"warn"viarules/categoriesstill shows even when warnings are hidden.
#596
Because dead-code analysis only emits warning-severity findings, it's now skipped entirely when warnings are hidden (its results would be filtered out anyway) — avoiding an expensive analysis pass on the default path. --warnings / "warnings": true (and --fail-on warning) re-enable it.
- Updated dependencies [`6e59f10`, `75c1f99`]:
- oxlint-plugin-react-doctor@0.2.15
0.2.14Patch
Patch Changes
- Guard the startup
process.stdinunref onprocess.stdin.isTTYso interactive prompts no longer exit by themselves. The startup unref (added so one-shot non-interactive runs like--jsonfrom an eval runner holding the stdin pipe open can exit cleanly) was applied unconditionally, including on a real terminal. On a TTYpromptsnever re-refs the unref'd stdin handle —readline.createInterface+setRawMode(true)do not re-ref it — so the multiselect ("Select projects") rendered and the CLI then drained the event loop and exited (code 0) before the user could answer. Skipping the unref when stdin is a TTY keeps the one-shot exit fix for non-interactive pipes/sockets while leaving interactive terminals untouched. Adds an in-process behavioral test and a real-PTY CI smoke (pnpm smoke:tty-prompt).
#593
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.14
0.2.13Patch
Patch Changes
- Fix the CLI hanging after the post-scan prompts. Interactive prompts re-ref stdin via
readlineand never release it on close, undoing the startupunrefStdin()and holding the one-shot CLI's event loop open. The sharedpromptswrapper now re-unrefs stdin once each prompt settles.
#586
- `d40a933` Thanks @aidenybai! - Trigger a release.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.13
0.2.12Patch
Patch Changes
- Add the
no-prop-typesarchitecture rule. React 19 removed runtimepropTypesvalidation entirely — React no longer readsComponent.propTypes, so invalid props that used to log a console warning now pass silently. The rule flagsComponent.propTypes = { ... }assignments andstatic propTypesclass fields on component-cased identifiers, and is version-gated to React 19+ (requires: ["react:19"]) so projects wherepropTypesstill runs stay quiet. It steers users toward TypeScript prop types plus explicit runtime validation. See #460.
#570
- Fix a
rn-no-raw-textfalse positive on fbtee translation tags. fbtee's<fbt>/<fbs>(and namespaced children like<fbt:param>) are compile-time translation tags that disappear at build time, so text inside<Text><fbt>…</fbt></Text>is really rendered inside<Text>and is safe on React Native. The rule now treatsfbt/fbsas transparent wrappers when every ancestor up to a text-handling component is also transparent, while still reporting raw text when an<fbt>is used outside a<Text>boundary. See #581.
#582
- Scope React subproject discovery so running
react-doctorfrom a home directory no longer reports unrelated, vendored projects as ambiguous candidates. When the scan root has nopackage.jsonor workspace manifest, the filesystem crawl now skips OS/editor app-data directories (AppData,Library, …) and stops descending past a fixed depth. Previously a home-directory scan could surface React packages bundled inside editor installs (e.g. a VS Code extension underAppData) alongside real projects, aborting withMultiple React projects found. See #545.
#557
- Unref
process.stdinat CLI startup so an inherited stdin pipe/socket can no longer keep the event loop alive after a scan completes. Previouslyreact-doctor --json(and other one-shot runs) could finish the scan and flush the full report yet never exit when launched by a parent that holds the stdin write-end open (eval runners, CI harnesses, editor integrations) — Node kept the loop alive on the idleSocket fd=0. Interactive prompts are unaffected becauseprompts'readlineinterface re-refs stdin on demand.
#576
- Updated dependencies [`d917f62`, `d0f5206`, `b2934f9`]:
- oxlint-plugin-react-doctor@0.2.12
0.2.11Patch
Patch Changes
- Updated dependencies [`6f8640f`]:
- oxlint-plugin-react-doctor@0.2.11
0.2.10Patch
Patch Changes
- Add Preact project detection so
react-doctor inspectrecognizes Preact workspaces, including Vite + Preact projects that still reportviteas their framework, and enables the bundled Preact rule family.
- Bundle new diagnostics across the rule set: Preact compatibility checks, HTML correctness and dialog accessibility rules,
hooks-no-nan-in-deps, Jotai atom diagnostics, React Native performance rules,js-async-reduce-without-awaited-acc, and React 19.2<Activity>effect-boundary checks.
- Fix CLI reliability around dead-code scans and setup prompts. Dead-code analysis now runs with a bounded worker path instead of freezing the scan, monorepo scans still show the setup prompt, and repeated setup questions collapse into one install flow.
- Inherit false-positive fixes for
control-has-associated-labelandno-giant-component.
- Dependency bump:
oxlint-plugin-react-doctor@0.2.10.
0.2.9Patch
Patch Changes
- Publish workflow now uses npm trusted publishing through GitHub OIDC, including an npm version with provenance support. Releases no longer need a long-lived npm token.
- Dependency bump:
oxlint-plugin-react-doctor@0.2.9.
0.2.8Patch
Patch Changes
- add react-doctor.config.json schema field
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.8
0.2.7Patch
Patch Changes
- Animated score progress bar. The CLI health score now renders with a smooth progress-bar animation, automatically skipped in CI and coding-agent environments.
- CI and agent detection. New
isCiOrAgentutility detects CI providers and coding agents (Cursor, Claude Code, Codex, etc.) and suppresses interactive prompts, animations, and the onboarding flow so scans run non-interactively where appropriate.
- Concurrent lint + dead-code analysis. The
inspectcommand now runs linting and dead-code detection in parallel instead of sequentially, reducing wall-clock scan time.
- Agent install hint. When running inside a coding agent, the CLI suggests
react-doctor installto set up the agent skill for in-editor diagnostics.
- Skip prefilled project question. The monorepo project-selection prompt is skipped when there is only one scannable project, removing an unnecessary interactive step.
- `/doctor` triage skill. The React Doctor agent skill now includes a
/doctorcommand that fetches the canonical playbook for full-project triage.
- Bundle
eslint-plugin-react-hooksas a direct dependency so React Compiler rules work out of the box.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.7
0.2.6Patch
Patch Changes
- Remove
design-no-bold-headingrule - the heuristic produced too many false positives in design systems where headings intentionally vary weight.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.6
0.2.5Patch
Patch Changes
- First-run onboarding. New users see a brief walkthrough on their first
react-doctorinvocation explaining what the tool does and how to read the report.
- Node 20 support. Fix runtime dependency resolution so the CLI runs correctly on Node 20 without requiring Node 22+ built-ins.
- Cover child workspace diff include paths so
--diffmode in monorepos correctly scans files changed inside nested workspace packages.
- Stop
jsx-keyfrom flagging shorthand JSX fragments (<>...</>) which cannot accept akeyprop.
- Normalize static template literal handling so rules treat `
hellothe same as"hello"`.
- Add
require-pnpm-hardeningenvironment check that warns whenpnpmis detected without strict lockfile settings.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.5
0.2.4Patch
Patch Changes
- Effect v4 runtime migration. The entire scan pipeline is rebuilt on Effect v4 - tagged errors, dependency-injected services, generator-based control flow, and
Context.Referenceambient config replace the previous imperative architecture.
- New `@react-doctor/api` package. Programmatic
diagnose()entry point backed by the samerunInspectorchestrator the CLI uses, with typedReactDoctorErrorfailures andEffect.catchReasonsdispatch.
- `inspect()` rewired through `runInspect`. The CLI
inspectcommand now delegates to the core streaming orchestrator instead of managing the scan loop directly, aligning CLI and API behavior.
- Native agent hook installer.
react-doctor installwrites post-checkout / post-merge git hooks that auto-run the scan on relevant file changes.
- Opt-in OpenTelemetry.
REACT_DOCTOR_OTLP_ENDPOINT+REACT_DOCTOR_OTLP_AUTH_HEADERship every service span to an OTLP backend.
- User-plugin extension.
config.plugins: [...]loads custom oxlint plugin packages alongside the built-in rules.
- Security hardening. Pin CI workflow permissions, add fork guards, fix four pre-existing audit findings.
- Collapse
@react-doctor/typesand@react-doctor/project-infointo@react-doctor/core.
- Adopt
Effect.Consolethroughout - drop the customLoggerservice.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.4
0.2.3Patch
Patch Changes
- Fix vite build configuration for bundling workspace dependencies so
npx react-doctorresolves internal workspace imports correctly.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.3
0.2.2Patch
Patch Changes
- Restore
eslint-plugin-react-hooksas a hard dependency so React Compiler rules resolve without requiring users to install the peer separately.
(NickvanDyke, MIT) into oxlint-plugin-react-doctor. They now ship as react-doctor/* rules and no longer require the optional peer dependency. The optional peer-dep surface (effect/* rules, resolveYouMightNotNeedEffectPlugin, YOU_MIGHT_NOT_NEED_EFFECT_NAMESPACE) is removed from @react-doctor/core.
The ports use a real eslint-scope ScopeManager (cached per Program via WeakMap) - same references / resolved.defs[].node.init / isEventualCallTo chasing the upstream plugin uses. Diagnostic messages match upstream verbatim with template variables substituted in JS.
| Rule (now react-doctor/<id>) | What it catches | | ----------------------------------- | ------------------------------------------------------------------------ | | no-derived-state | Storing derived state via a useEffect instead of computing during render | | no-chain-state-updates | Chaining state updates across effects | | no-event-handler | Using state + a guarded effect as an event handler | | no-adjust-state-on-prop-change | Adjusting state in an effect when a prop changes | | no-reset-all-state-on-prop-change | Resetting all state in an effect (use a key prop) | | no-pass-live-state-to-parent | Pushing live state to a parent via a callback in an effect | | no-pass-data-to-parent | Passing fetched data to a parent via a callback in an effect | | no-initialize-state | Initializing state inside a mount-only effect |
Parity coverage: 195 of 196 upstream test cases pass (the 1 remaining case is upstream's own todo: true, "Set derived state via identical intermediate setter").
These coexist with React Doctor's existing thematically-related rules (no-derived-state-effect, no-effect-chain, no-event-trigger-state, no-prop-callback-in-effect) - different IDs, different shapes, different messages.
- Updated dependencies [`47772b7`]:
- oxlint-plugin-react-doctor@0.2.2
0.2.1Patch
Patch Changes
- Make filesystem walks tolerate EPERM/EACCES (macOS Library)
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.1
0.2.0Feature
Minor Changes
- `5be2ead` - Add configuration-level controls for React Doctor's rule output. Users can now set top-level
rulesandcategoriesseverity overrides, tune individual output surfaces (cli,prComment,score, andciFailure) by tag/category/rule id, and rely on registered rule-family tags such asdesign,react-native,server-action,test-noise, andmigration-hintfor broad filtering.
The scan pipeline now applies those controls both when generating the oxlint config and when post-processing diagnostics, so "off" can skip rules before they run while "warn" / "error" restamp emitted diagnostics consistently across the CLI, score, PR comments, and CI failure gate. The oxlint plugin also exposes shared rule-set maps that the ESLint plugin reuses for its flat configs.
Expose the GitHub Action's annotations input so workflow users can opt into inline PR annotations without dropping down to the raw CLI.
- `809e38c` - Extract project / dependency / framework detection, the oxlint runner +
scoring engine, and the shared TypeScript type layer out of the react-doctor monolith into three new public workspace packages: @react-doctor/types, @react-doctor/project-info, and @react-doctor/core (#249). The oxlint plugin is restructured into per-rule modules under src/plugin/rules/<category>/<rule>.ts with a codegen'd rule-registry.ts (#218, #228, #230, #231, #234, #235, #236, #242). Land the user-feedback sweep (#208): scoring transparency hooks, per-rule severity + rule-set selection config options, and reduced false positives across the design / Tailwind / state-and-effects rule families. Reorganise the CLI into cli/commands/ + cli/utils/ (#250), and forward reactMajorVersion through programmatic diagnose() (#174).
Patch Changes
- `29b7229` - Add
oxlint-plugin-react-doctortodependenciesso it is installed
alongside the CLI. The bundler correctly externalises the plugin (oxlint loads it by file path at runtime) but it was missing from the published dependency list, causing ERR_MODULE_NOT_FOUND on npx react-doctor.
- `99f6a6a` - Rule-fix wave for the 0.2.0-beta.5 release:
- Scope
no-secrets-in-client-codeto client-reachable bindings -
skips server-only modules, public env-prefixed values, and locally-classified safe files (#252).
nextjs-no-side-effect-in-get-handlerstops flagging
response.headers.set(...) and locally-constructed Map / Set / Headers inside GET handlers; the same safe-bindings classifier benefits server-auth-actions and the TanStack Start get-mutation rule (#260).
async-defer-awaitno longer reports awaits inside destructured
patterns with defaults, bare-statement early-returns, or awaits guarded by an earlier if … return … (#265).
js-length-check-firstdetects length guards anywhere earlier in
an && chain, not only as the immediate left operand (#269).
async-parallelis suppressed in test files, browser-fixture /
Playwright helpers, and ordered UI flows where serial awaits are deliberate (#270).
js-combine-iterationsskips lazyIteratorhelper chains
(Iterator.from, Iterator.prototype.{map,filter,take,drop,…}) whose evaluation semantics differ from Array.prototype (#272, resolves #205).
no-prevent-defaultis framework-aware: Remix / Next.js
progressive-enhancement form handlers, synthetic event types with no documented alternative, and form onSubmit handlers that subsequently call fetch / a server action no longer trip (#274).
- New per-surface diagnostic controls in
@react-doctor/core+
react-doctor: design and Tailwind cleanup categories are demoted from the default PR-comment surface while staying visible in the CLI report and at the CI failure gate (#271).
- `10d5de8` - Fix workspace packages not being bundled into dist, causing
ERR_MODULE_NOT_FOUND: Cannot find package '@react-doctor/core' when running the published CLI.
- Replace the hand-rolled glob-to-regex compiler with `picomatch`, the proven matcher behind
chokidar,fast-glob, andmicromatch. The previous compiler turned patterns like**/**/**/**/**/foo.tsxinto nested optional(?:.+/)?groups whose backtracking is exponential in the number of**segments - a 20-deep pattern hung for over 30 seconds on a 60-character non-matching input.- Reject obviously pathological patterns early with a clear
InvalidGlobPatternErrorcarrying the offending pattern and a human-readable reason, instead of crashing the scan. Limits live in@react-doctor/core/constants(MAX_GLOB_PATTERN_LENGTH_CHARS = 1024,MAX_GLOB_PATTERN_WILDCARD_COUNT = 24) and bound worst-case work regardless of the underlying engine. Real-world ignore patterns like**/foo/**/bar/**/*.tsxsit well under the cap. - Surface invalid
ignore.filesandignore.overrides[*].filesentries as[react-doctor] …warnings on stderr and skip just the bad pattern, so a single typo no longer takes the whole scan down. - Add regression tests covering the worst-case patterns (deeply-stacked globstars and dense
a*a*a*…alternations) and the validation surface.
- Reject obviously pathological patterns early with a clear
rn-* rule fired on every file in a project whose top-level framework was detected as React Native or Expo - even on sibling workspaces that were clearly web targets. In a mixed RN + web monorepo (apps/mobile alongside apps/web and packages/storybook) the rules would noisily report issues against Next.js, Vite, Docusaurus, Storybook, and plain React DOM packages where they don't apply.
React Native rules now walk up to the file's nearest package.json before running. The rule body is skipped when the package declares a web-only framework (next, vite, react-scripts, gatsby, @remix-run/react, @docusaurus/core, @storybook/*, or plain react-dom without an RN sibling) and stays active when the package declares react-native, expo, react-native-tvos, react-native-windows, react-native-macos, anything under the @react-native/ or @react-native- community namespaces (@react-native-firebase/*, @react-native-async-storage/*, @react-native-community/*, …), or Metro's top-level "react-native" resolution field.
The detection is bidirectional: a web-rooted monorepo (root package.json declares next or vite) still loads rn-* rules when any workspace targets React Native or Expo, so the rules now fire on apps/mobile of a next-rooted repo as well as the inverse layout that the file-level boundary alone covered.
rn-no-raw-text additionally skips raw text inside Platform.OS === "web" branches: if, ?:, and && / || short-circuits, the mirror Platform.OS !== "web" else branches, switch (Platform.OS) { case "web": … } case bodies, and the web arm of Platform.select({ web: …, default: … }). Optional chaining (Platform?.OS) and the TS non-null assertion (Platform.OS!) parse the same way as the bare form. The walker stops at function and Program boundaries so JSX defined inside a callback hoisted out of a Platform.OS branch does not inherit the parent guard.
Native-only file extensions (.ios.tsx, .android.tsx, .native.tsx) keep the rule active even when the surrounding package classification is ambiguous.
- `99f6a6a` - False-positive sweep across the rule plugin and the oxlint runner:
- Gate React-19-only rules on the detected React major version so they
stay silent on React 18 projects, with hardened catalog / peer-range / workspace traversal in @react-doctor/project-info (#254).
- Treat early-return guards as render-reachable state reads so
rerender-state-only-in-handlers / no-event-trigger-state stop recommending useRef for state that gates render output (#255).
- Narrow
no-effect-event-handler- DOM imperatives, prop callbacks
invoked from effects, and side effects routed through a stable ref are no longer reclassified as handler-only (#256).
- Suppress rules-of-hooks diagnostics on locally-defined
useX
helpers that are not React hooks, and add the no-em-dash-in-jsx-text / no-three-period-ellipsis typography rules (#257).
- Collapse duplicate oxlint diagnostics and recover diagnostics from
large monorepo projects via batched runs + a new dedupe-diagnostics helper in @react-doctor/core (#262).
including pnpm and Bun catalog references) and gate Tailwind-aware rules on it. design-no-redundant-size-axes (which suggests collapsing w-N h-N → size-N) now stays silent on Tailwind v3.0 … v3.3 - those versions predate the size-N shorthand and the suggestion would generate classes that don't compile. The rule still fires on Tailwind v3.4+, v4+, and when the Tailwind version cannot be resolved.
A new tailwindVersion field is added to ProjectInfo and printed during scans so it's visible alongside the detected React version and framework.
- Updated dependencies [`99f6a6a`, `529015d`, `5be2ead`, `99f6a6a`, `809e38c`]:
- oxlint-plugin-react-doctor@0.2.0
0.2.0-beta.6Feature
Minor Changes
- Add configuration-level controls for React Doctor's rule output. Users can now set top-level
rulesandcategoriesseverity overrides, tune individual output surfaces (cli,prComment,score, andciFailure) by tag/category/rule id, and rely on registered rule-family tags such asdesign,react-native,server-action,test-noise, andmigration-hintfor broad filtering.
The scan pipeline now applies those controls both when generating the oxlint config and when post-processing diagnostics, so "off" can skip rules before they run while "warn" / "error" restamp emitted diagnostics consistently across the CLI, score, PR comments, and CI failure gate. The oxlint plugin also exposes shared rule-set maps that the ESLint plugin reuses for its flat configs.
Expose the GitHub Action's annotations input so workflow users can opt into inline PR annotations without dropping down to the raw CLI.
Patch Changes
- Replace the hand-rolled glob-to-regex compiler with `picomatch`, the proven matcher behind
chokidar,fast-glob, andmicromatch. The previous compiler turned patterns like**/**/**/**/**/foo.tsxinto nested optional(?:.+/)?groups whose backtracking is exponential in the number of**segments - a 20-deep pattern hung for over 30 seconds on a 60-character non-matching input.- Reject obviously pathological patterns early with a clear
InvalidGlobPatternErrorcarrying the offending pattern and a human-readable reason, instead of crashing the scan. Limits live in@react-doctor/core/constants(MAX_GLOB_PATTERN_LENGTH_CHARS = 1024,MAX_GLOB_PATTERN_WILDCARD_COUNT = 24) and bound worst-case work regardless of the underlying engine. Real-world ignore patterns like**/foo/**/bar/**/*.tsxsit well under the cap. - Surface invalid
ignore.filesandignore.overrides[*].filesentries as[react-doctor] …warnings on stderr and skip just the bad pattern, so a single typo no longer takes the whole scan down. - Add regression tests covering the worst-case patterns (deeply-stacked globstars and dense
a*a*a*…alternations) and the validation surface.
- Reject obviously pathological patterns early with a clear
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.0-beta.6
0.2.0-beta.5Patch
Patch Changes
cli/utils/inspect-flags.ts flag + companion cli/utils/resolve-cli-inspect-options.ts and cli/utils/validate-mode-flags.ts plumbing wire the new @react-doctor/core surface filter into cli/commands/inspect.ts and inspect.ts. Design + Tailwind cleanup categories are demoted from the default PR-comment surface so they no longer dominate code review output, while still appearing in the CLI report and at the CI failure gate. Documented in the package README and the new action.yml knobs.
- Inherits the rule-fix wave from
oxlint-plugin-react-doctor@0.2.0-beta.5 (rules are bundled into the CLI): no-secrets-in-client-code scoping (#252), nextjs-no-side-effect-in-get-handler safe local bindings (#260), async-defer-await false-positive fixes (#265), js-length-check-first &&-chain detection (#269), async-parallel test / browser-fixture suppression (#270), js-combine-iterations lazy Iterator skip (#272), and no-prevent-default framework awareness (#274).
rn-* rule fired on every file in a project whose top-level framework was detected as React Native or Expo - even on sibling workspaces that were clearly web targets. In a mixed RN + web monorepo (apps/mobile alongside apps/web and packages/storybook) the rules would noisily report issues against Next.js, Vite, Docusaurus, Storybook, and plain React DOM packages where they don't apply.
React Native rules now walk up to the file's nearest package.json before running. The rule body is skipped when the package declares a web-only framework (next, vite, react-scripts, gatsby, @remix-run/react, @docusaurus/core, @storybook/*, or plain react-dom without an RN sibling) and stays active when the package declares react-native, expo, react-native-tvos, react-native-windows, react-native-macos, anything under the @react-native/ or @react-native- community namespaces (@react-native-firebase/*, @react-native-async-storage/*, @react-native-community/*, …), or Metro's top-level "react-native" resolution field.
The detection is bidirectional: a web-rooted monorepo (root package.json declares next or vite) still loads rn-* rules when any workspace targets React Native or Expo, so the rules now fire on apps/mobile of a next-rooted repo as well as the inverse layout that the file-level boundary alone covered.
rn-no-raw-text additionally skips raw text inside Platform.OS === "web" branches: if, ?:, and && / || short-circuits, the mirror Platform.OS !== "web" else branches, switch (Platform.OS) { case "web": … } case bodies, and the web arm of Platform.select({ web: …, default: … }). Optional chaining (Platform?.OS) and the TS non-null assertion (Platform.OS!) parse the same way as the bare form. The walker stops at function and Program boundaries so JSX defined inside a callback hoisted out of a Platform.OS branch does not inherit the parent guard.
Native-only file extensions (.ios.tsx, .android.tsx, .native.tsx) keep the rule active even when the surrounding package classification is ambiguous.
- Updated dependencies [`529015d`]:
- oxlint-plugin-react-doctor@0.2.0-beta.5
0.2.0-beta.4Patch
Patch Changes
- Add
oxlint-plugin-react-doctortodependenciesso it is installed
alongside the CLI. The bundler correctly externalises the plugin (oxlint loads it by file path at runtime) but it was missing from the published dependency list, causing ERR_MODULE_NOT_FOUND on npx react-doctor.
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.0-beta.4
0.2.0-beta.3Patch
Patch Changes
- `10d5de8` - Fix workspace packages
(@react-doctor/core, @react-doctor/project-info, @react-doctor/types) not being bundled into the published dist/ output, which caused ERR_MODULE_NOT_FOUND: Cannot find package '@react-doctor/core' on npx react-doctor after the package extraction in beta.2. Vite config now treats the workspace dependencies as bundle-time inputs.
- Inherits the
#253 no-barrel-import index-resolution fix from oxlint-plugin-react-doctor@0.2.0-beta.3 (rules are bundled into the CLI).
- Updated dependencies []:
- oxlint-plugin-react-doctor@0.2.0-beta.3
0.2.0-beta.2Feature
Minor Changes
detection, the oxlint runner, scoring, or the shared type layer inline - those modules now live in @react-doctor/types, @react-doctor/project-info, and @react-doctor/core and are consumed as workspace dependencies. Bundled into dist/ on publish (see also #253 bundler follow-up in beta.3). The react-doctor, react-doctor inspect, and react-doctor install binaries are surface-compatible with 0.1.6.
utils/, mirroring the layout ported from react-grab. Each subcommand has a dedicated module (inspect, install, version / help). No user-visible change to flags or output.
Patch Changes
Tailwind / state-and-effects rule groups, surface per-rule scoring contributions in react-doctor inspect, and add --severity / --rule-set CLI options plus their react-doctor.config.json counterparts. Closes the bulk of the feedback collected on 0.1.x.
- #174 - Forward
reactMajorVersion through the programmatic diagnose() entry point so embedders running react-doctor inside their own pipeline (Vercel AI Code Review sandbox and friends) get the same React-19 rule gating the CLI gets.
including pnpm and Bun catalog references) and gate Tailwind-aware rules on it. design-no-redundant-size-axes (which suggests collapsing w-N h-N → size-N) now stays silent on Tailwind v3.0 … v3.3 - those versions predate the size-N shorthand and the suggestion would generate classes that don't compile. The rule still fires on Tailwind v3.4+, v4+, and when the Tailwind version cannot be resolved.
A new tailwindVersion field is added to ProjectInfo and printed during scans so it's visible alongside the detected React version and framework.
0.1.6Patch
Patch Changes
- `e9e4217` - Harden
discover-projectandresolve-diagnose-target: tighter
workspace-root detection constants, additional regression coverage in tests/diagnose.test.ts and tests/discover-project.test.ts for the nested-subproject fallback added in 0.1.5.
0.1.5Patch
Patch Changes
requested directory has no root package.json, instead of crashing with No package.json found in <directory>. This unblocks external review runners (e.g. the Vercel AI Code Review sandbox) that point diagnose() at the cloned repo root for projects whose package.json lives in a subfolder like apps/web. When neither the root nor any nested subdirectory contains a React project, diagnose() now throws a clearer No React project found in <directory> error.
- #200 - Typed
errors from diagnose() plus a rootDir config option so embedders can target a specific subdirectory without relying on cwd inference.
- #201 - Integrate
eslint-plugin-react-you-might-not-need-an-effect into the curated rule set so its useEffect-elimination diagnostics flow into the score alongside react-doctor's own state-and-effects rules.
- #194 - Resolve
the React version from Bun grouped catalogs (in addition to pnpm catalogs) so monorepos using Bun for dependency hoisting still get an accurate React major back from the catalog resolver.
- #196 - Match
react-doctor-disable* suppression comments that carry descriptive trailing text (e.g. // react-doctor-disable-next-line rule -- why) instead of requiring a bare comment. Resolves #159.
- #198 - Expose
--why as a documented public alias for --explain in the CLI. Resolves #161.
- #195 - The
GitHub Action's score step is output-only and never fails the job, so consumers can gate on the score themselves without losing the run. Resolves #190.
- #197 - Docs:
clarify that ignore.overrides covers per-file rule ignores.
- #199 - Docs:
full GitHub Actions workflow example and inputs reference.
0.1.4Patch
Patch Changes
- `a63d5d5` - CLI scan output reformat. Adds
utils/wrap-indented-text.tsfor
consistent wrapping of multi-line diagnostic recommendations, expands the scan-summary types to carry per-line wrap state, and threads the helper through scan.ts. Backed by the new wrap-indented-text.test.ts unit suite and a cli-and-output regression suite that snapshots the rendered CLI output.
0.1.3Patch
Patch Changes
- #184 - Add a
rawTextWrapperComponents config option so projects can teach rn-no-raw-text about their own <Text> wrappers (e.g. design-system primitives that render Text internally). Resolves #183.
- #182 - Restore
React Compiler rules to error severity. They had silently regressed to warn in 0.1.0 when the plugin-resolution gating landed, masking Compiler-blocking violations behind the warning lane.
- #181 - Website
fix: keep the diagnostic count next to the rule name on narrow widths in the leaderboard / diagnostic listings.
- `cca5808` - Promote
react-hooks-js/*diagnostics to errors so projects with
React Hooks rule violations no longer pass with a clean score.
- `9ee3a6d` - Refresh the website's terminal demo to match the new CLI output
format introduced in 0.1.1 / 0.1.4.
0.1.2Patch
Patch Changes
- `6ddb02c` - Polish follow-up to the 0.1.1 CLI redesign. Consolidates duplicated
scan-summary literals into constants.ts, simplifies scan.ts to drop a redundant branch (-9 LOC), and tightens the spinner.ts helper so its cleanup is symmetric with start. No user-visible behaviour change.
0.1.1Patch
Patch Changes
- #178 - CLI
scan-summary redesign. The final report now inlines a category breakdown (state-and-effects / design / bundle-size / …) and a compact rule list grouped under each category, replacing the previous single-line counts. Verbose mode keeps the per-diagnostic listing.
0.1.0Feature
Minor Changes
- d71a6bf: feat(react-doctor): ship rules as an ESLint plugin (
react-doctor/eslint-plugin)
The same React Doctor rule set that powers the CLI scan and the react-doctor/oxlint-plugin export is now available as a first-class ESLint plugin. Drop it into your eslint.config.js flat config and diagnostics surface inline through whichever IDE / agent / pre-commit hook already speaks ESLint - no separate react-doctor invocation needed.
```js // eslint.config.js import reactDoctor from "react-doctor/eslint-plugin";
export default [ reactDoctor.configs.recommended, reactDoctor.configs.next, // composable framework presets reactDoctor.configs["react-native"], reactDoctor.configs["tanstack-start"], reactDoctor.configs["tanstack-query"], // reactDoctor.configs.all, // every rule at react-doctor's default severity ]; ```
The exported recommended, next, react-native, tanstack-start, tanstack-query, and all configs reuse the exact severity maps the react-doctor CLI emits to oxlint, so behavior stays in lock-step between engines. You can also cherry-pick individual rules under the react-doctor/* namespace.
The visitor signatures inside each rule are already ESLint-compatible (create(context) => visitors); the new export wraps each rule with the ESLint-required meta (type, docs.url, schema) and exposes the plugin shape ESLint v9 flat configs expect. Closes #143.
- d71a6bf: feat(react-doctor): adopt the project's existing oxlint / eslint config and factor those rules into the score
When a project has a JSON-format oxlint or eslint config (.oxlintrc.json or .eslintrc.json) at the scanned directory or any ancestor up to the nearest project boundary (.git directory or monorepo root), react-doctor now folds that config into the same scan via oxlint's extends field. The user's existing rules fire alongside the curated react-doctor rule set, and the resulting diagnostics count toward the 0–100 health score - no separate oxlint / eslint invocation needed.
Behavior change on upgrade. Projects with an existing .oxlintrc.json / .eslintrc.json will see new diagnostics flow into the score on first run; the score may drop. Set "adoptExistingLintConfig": false in react-doctor.config.json (or the "reactDoctor" key in package.json) to preserve the previous behavior. customRulesOnly: true also implies opt-out, since that mode runs only the react-doctor/* plugin.
Resilience. If oxlint can't load the user's config (broken JSON, missing plugin, unknown rule name), react-doctor logs the reason on stderr and retries the scan once without extends so the score is still computed off the curated rule set instead of failing the whole lint pass.
Coverage broadened. Diagnostics on .ts and .js files are now reported (previously the parser dropped everything that wasn't .tsx / .jsx). This affects react-doctor's own JS-performance / bundle-size rules in addition to adopted user rules.
Limitations. Only JSON configs are picked up: oxlint's extends cannot evaluate JS or TS, so flat configs (eslint.config.js), .eslintrc.{js,cjs}, and oxlint.config.ts are silently skipped. Rule-level severities ("rules": {...}) flow through, but category-level enables ("categories": {...}) do not - react-doctor's local categories block always wins. Closes #143.
- d71a6bf: feat(react-doctor): add 11 new lint rules - 3 state / correctness, 8 design system
3 new state / correctness rules (all warn):
react-doctor/no-direct-state-mutation- flagsstate.foo = xand
in-place array mutators (push / pop / shift / unshift / splice / sort / reverse / fill / copyWithin) on useState values. Tracks shadowed names through nested function params and locals so a handler that re-binds the state name doesn't false-positive.
react-doctor/no-set-state-in-render- flags only unconditional
top-level setter calls so the canonical if (prev !== prop) setPrev(prop) derive-from-props pattern stays clean.
react-doctor/no-uncontrolled-input- catches<input value={…}>
without onChange / readOnly, value + defaultValue conflicts, and useState() flip-from-undefined. Bails on JSX spread props ({...register(…)}, Headless UI, Radix) where onChange may come from spread.
8 new design-system rules in `react-ui.ts` (all warn):
react-doctor/design-no-bold-heading-
font-bold / font-extrabold / font-black or inline fontWeight ≥ 700 on h1–h6.
react-doctor/design-no-redundant-padding-axes- collapse
px-N py-N → p-N.
react-doctor/design-no-redundant-size-axes- collapsew-N h-N→
size-N.
react-doctor/design-no-space-on-flex-children- usegap-*over
space-*-*.
react-doctor/design-no-em-dash-in-jsx-text- em dashes in JSX
text.
react-doctor/design-no-three-period-ellipsis-Loading...→
Loading….
react-doctor/design-no-default-tailwind-palette-
indigo-* / gray-* / slate-* reads as the Tailwind template default; reports every offending token in the className (not just the first).
react-doctor/design-no-vague-button-label-OK/Continue/
Submit etc.; recurses into <>…</> fragment children.
Each new rule has dedicated regression tests covering both the positive trigger and the false-positive cases above.
Other
- Hoists shared regex / token patterns into the appropriate
constants.ts per AGENTS.md.
- d71a6bf: remove(react-doctor): drop browser entrypoints, browser CLI, and the
react-doctor-browser workspace package
Removed package exports. react-doctor/browser and react-doctor/worker are no longer published. Imports of either subpath will fail with ERR_PACKAGE_PATH_NOT_EXPORTED. If you depended on the in-browser diagnostics pipeline (caller-supplied projectFiles map + runOxlint callback running oxlint in a Web Worker), pin react-doctor@0.0.47 or vendor the relevant modules from the archive/browser git branch.
Removed CLI subcommand. react-doctor browser … (start, stop, status, snapshot, screenshot, playwright) is gone. The long-running headless Chrome session, ARIA snapshot helpers, screenshot capture, and --eval Playwright harness are no longer available from the CLI.
Removed companion package. The react-doctor-browser npm package (headless browser automation, CDP discovery, system Chrome launcher, cross-browser cookie extraction) has been removed from the workspace. The last published version remains installable on npm but will not receive further updates.
Why. The browser surface area was unused inside the monorepo (the website does not import it) and added a heavy dependency footprint (playwright, libsql, etc.) for a public API with no known internal consumers. Removing it tightens what react-doctor is responsible for: the diagnostics CLI, the Node react-doctor/api, and the react-doctor/eslint-plugin / react-doctor/oxlint-plugin exports.
The full removed source remains available on the archive/browser branch for anyone who wants to fork or vendor the modules.
Patch Changes
- 2aebfa6: fix(react-doctor): support block comment forms of
react-doctor-disable-line/react-doctor-disable-next-line
The inline-suppression matcher previously only recognized line comments (// react-doctor-disable-…). Block comments - including the JSX form {/* react-doctor-disable-next-line … */}, which is the only suppression form legal directly inside JSX - were silently ignored, forcing users to write {/* // react-doctor-disable-line … */} as a workaround. Both forms now work, and either accepts a comma- or whitespace-separated rule list or no rule id (suppress every diagnostic on the targeted line). Closes #144.
- 2aebfa6: fix(react-doctor): stop flagging
useStateasuseRefwhen state reaches render throughuseMemo, derived values, or contextvalue
rerender-state-only-in-handlers (the rule that suggests "use useRef because this state is never read in render") only checked whether the state name appeared by name in the component's return JSX. That heuristic produced loud false positives for ordinary patterns:
- state filtered/derived through
useMemo→ JSX uses the memo result- state passed as the
valueof a React Context Provider - state combined with other variables into a rendered constant
- state passed as the
Following the bad hint and converting these to useRef silently broke apps because ref.current = … does not trigger a re-render - search results stopped updating, dialogs stayed open, and context consumers saw stale snapshots.
The rule now performs a transitive "render-reachable" analysis on top-level component bindings. A useState is only flagged when neither the value itself nor anything derived from it (recursively) appears anywhere in the rendered JSX, including attribute values like <Context value={…}>, style={…}, className={…}, etc. Truly transient state (e.g. a scroll position only stored to be ignored) still fires. Closes #146.
and 11 new lint rules: 3 state / correctness rules (no-direct-state-mutation, no-set-state-in-render, no-uncontrolled-input) and 8 design-system rules (see the dedicated bullet above).
forwardRef-deprecation / new context API / new ref-callback cleanup / use() adoption migration paths.
subscriptions that should be useSyncExternalStore (concurrent-mode safe, tearing-resistant).
written from an event handler and only read from the rendered JSX return - a frequent prop-derivation antipattern.
effect's setState triggers another effect, which is almost always a signal to collapse the chain into a derived value or event handler.
(no-mutable-in-deps, no-mirror-prop-effect, effect-needs-cleanup) plus shared dependency-tracking infrastructure.
doesn't fire on React 18 projects (where useEffectEvent is not available).
lookups so callbacks hoisted out of effects don't inherit the surrounding effect classification.
overrides, and near-miss hints for misspelled rule ids.
validation, --explain working inside monorepos, JSX generics parsing, line-comment skip semantics, and a single-pass evaluator for the suppression matcher.
default scan output stays scannable on large projects; --verbose restores the full listing.
rules-of-hooks / handler-detection / render-reachable code paths.
entry point so embedders get the same React-19 rule gating the CLI uses.
the adopt-config noise introduced in d71a6bf when the user's config contains unknown rules.
0.0.47Patch
Patch Changes
- 6a0e6d6: chore(react-doctor): bump oxlint to ^1.62.0
Pulls in oxlint v1.61.0 + v1.62.0 improvements (additional Vue rules, jest/vitest rule splits, autofix for prefer-template, no-unknown-property support for React 19's precedence prop, jsx-a11y/anchor-is-valid attribute settings, and various correctness fixes). The release-line breaking changes are internal Rust API only - oxlint's CLI and config schema are unchanged.
- dbf200d: fix(react-doctor): filter React Compiler rules to those the loaded
eslint-plugin-react-hooksactually exports
Follow-up to the #141 fix in 0.0.46. The peer range ^6 || ^7 allows v6.x of eslint-plugin-react-hooks, which doesn't expose the void-use-memo rule (added in v7). When a v6 user had React Compiler detected, oxlint failed with Rule 'void-use-memo' not found in plugin 'react-hooks-js'. The config now introspects the loaded plugin's rules map and only enables react-hooks-js/* entries that the installed version actually exports - so future rule additions or removals can no longer crash a scan.
0.0.46Patch
Patch Changes
- c13a8df: fix(react-doctor): skip React Compiler rules when
eslint-plugin-react-hooksisn't installed
When a project had React Compiler detected but the optional peer eslint-plugin-react-hooks was not installed, oxlint failed with react-hooks-js not found because the React Compiler rules were emitted into the config without the corresponding plugin entry. Gate REACT_COMPILER_RULES on successful plugin resolution so a missing optional peer silently skips them instead of crashing the scan (#141).
0.0.45Patch
Patch Changes
- 6b07924:
react-doctor installnow delegates skill installation to
`agent-install` 0.0.4, which natively models 54 supported coding agents (up from the 8 we previously hand-rolled).
Behavior changes:
- Detection is now the union of CLI binaries on
$PATH(the previous
signal) and config dirs in $HOME (~/.claude, ~/.cursor, ~/.codex, ~/.factory, ~/.pi, etc.). This catches agents the user has run at least once even if the CLI is no longer on $PATH, and vice versa.
- All 8 originally documented agents stay supported: Claude Code,
Codex, Cursor, Factory Droid, Gemini CLI, GitHub Copilot, OpenCode, Pi.
- 46 newly supported agents via upstream
agent-install@0.0.4:
Goose, Windsurf, Roo Code, Cline, Kilo Code, Warp, Replit, OpenHands, Qwen Code, Continue, Aider Desk, Augment, Cortex, Devin, Junie, Kiro CLI, Crush, Mux, Pochi, Qoder, Trae, Zencoder, and many more.
- Bug fix: malformed
SKILL.mdfrontmatter now surfaces as an error
instead of a silent "installed for ..." success with zero files written. Build-time validation in vite.config.ts also catches this before publish.
0.0.44Patch
Patch Changes
- `57467cd` - Patch follow-up to the 0.0.43 ignore-respecting refactor: misc
rough edges in the new ignore-pattern collector and inline-disable matcher.
0.0.43Patch
Patch Changes
- Respect existing eslint / oxlint / prettier ignores by default. React Doctor now honors
.gitignore,.eslintignore,.oxlintignore,.prettierignore, and.gitattributeslinguist-vendored/linguist-generatedannotations, plus inline// eslint-disable*and// oxlint-disable*comments. Previously inline disable comments were neutralized so react-doctor saw through every prior suppression - this surprised users who hadeslint-disablein place for legitimate reasons. Behavior change: existing users may see fewer findings (previously-suppressed code is now correctly suppressed). To restore the old "audit everything" behavior, set"respectInlineDisables": falseinreact-doctor.config.jsonor pass--no-respect-inline-disableson the CLI. - Internals: the ignore-pattern collector now writes a single combined
--ignore-pathfile rather than passing N--ignore-patternargs; this removes abaseArgs-length pressure point that could shrink batch sizes on large diffs. Boolean config fields (lint,deadCode,verbose,customRulesOnly,share,respectInlineDisables) are now coerced from the common"true"/"false"JSON-string typo at config-load time, with a warning. TheparseOxlintOutput"no files to lint" workaround is now locale-agnostic (it skips any noise before the first{). The non-git audit-mode fallback walks the project tree directly instead of silently no-op'ing whengit grepisn't available. New regression suite covers all of the above end-to-end.
0.0.42Patch
Patch Changes
- 79fb877: Fix
Dead code detection failed (non-fatal, skipping)(#135). The plugin-failure detector now walks the error cause chain, matches Windows-style paths, plugin configs without a leading directory, and parser errors, so knip plugin loading errors are recovered from in more environments. The retry loop also now surfaces the original knip error after exhausting attempts (previously could throw a genericUnreachableerror) and only disables knip plugin keys it actually recognizes. Dead-code and lint failures are now reported with the full cause chain instead of a single wrappedError loading …line. - 391b751: Fix knip step ignoring workspace-local config in monorepos (#136). When a workspace owns its own knip config (
knip.json,knip.jsonc,knip.ts, etc.),runKnipnow runs knip withcwd = workspaceDirectoryso the config is discovered, instead of running from the monorepo root with--workspaceand silently falling back to knip's defaults - which mass-flagged every file asUnused filefor setups like TanStack Start whose entry layout doesn't match the defaults. Behavior for monorepos with a root-levelknip.jsoncontaining aworkspacesmapping is unchanged.
0.0.41Patch
Patch Changes
- `1fdc9a0` - Patch follow-up to the 0.0.39 browser-entrypoint work: misc
bundling fixes for the now-removed react-doctor/browser and react-doctor/worker subpath exports.
0.0.40Patch
Patch Changes
- `874f7bc` - Publishing-pipeline retry of 0.0.39 (no code delta).
0.0.39Patch
Patch Changes
detection. Knip 6.x returns issues.files as an IssueRecords object instead of a Set<string>. The dead-code pass now handles both shapes (and arrays) defensively.
processBrowserDiagnostics API so the website's in-browser demo can run the same scoring + rule pipeline as the CLI without shelling out. Shared diagnose helpers and the browser scorer are extracted so bundles can omit the proxy-fetch path. (The browser surface was later removed in 0.1.0 - see that section.)
- `b5519b6` - Inject the browser scorer at build time so the bundled
react-doctor/browser output omits the proxy-fetch / Node-only score path.
0.0.38Patch
Patch Changes
- `8b0485a` - GitHub Action improvements (input validation + step output cleanups),
clickable file paths in CLI diagnostic output (terminal hyperlinks via OSC-8), and a website hydration fix.
- `bb5188f` - Document every config option supported by
react-doctor.config.json
in the README.
- `100731c` -
install-skillanddetect-agentsformatting fixes so CI's
format:check step stays green.
0.0.37Patch
Patch Changes
- `f1bd776` - Republish 0.0.36 after a botched skill payload - the
install-skill SKILL.md frontmatter was malformed and the prior publish shipped an unusable skill. No code delta vs 0.0.36 beyond the SKILL.md regeneration.
0.0.36Patch
Patch Changes
rule family - bold headings, redundant padding/size axes, vague button labels, etc.). Expanded substantially in 0.1.0.
- `074f854` - Add the
react-doctor installsubcommand that installs the
React Doctor SKILL.md into the user's configured coding agents.
0.0.35Patch
Patch Changes
- `7136aa5` - Republish 0.0.34 after a packaging hiccup; no code delta.
0.0.34Patch
Patch Changes
hygiene, useServerFn adoption hints).
detection.
0.0.33Patch
Patch Changes
- `87d4b86` - Republish 0.0.32 after a packaging hiccup; no code delta.
0.0.32Patch
Patch Changes
rule false-positive fixes, CLI option polish, and detection hardening.
/ member-expression accessors no longer count as setter invocations.
0.0.31Patch
Patch Changes
semantics, CLI ergonomics, React Native detection, Next.js detection, the --offline flag, and monorepo discovery.
before linting starts (previously they were linted then filtered).
0.0.30Patch
Patch Changes
- `c405f4a` - Resolve multiple GitHub issues (#71, #72, #76, #77, #83, #84, #86,
#87, #89, #92, #93, #94): broad rule false-positive sweep across detection, scoring, and rule output formatting.
- `97b21f1` - Replace
fs.existsSyncwith the sharedisFileutility for
consistent file checks across the codebase.
0.0.29Patch
Patch Changes
changed in the PR, plus optional PR-comment posting from the action step.
family when a project targets them.
0.0.28Patch
Patch Changes
- `bd949cc` - Bump the Node version requirement, enhance the linting process with
improved error handling, and tighten the CI matrix.
0.0.27Patch
Patch Changes
- `370ea4c` - Refactor CLI option handling and improve automated-environment
detection (CI / agent contexts).
- `051a02c` - Score-calculation refactor: extract shared constants and add the
proxy-fetch fallback used by the in-browser scorer.
- `bf21a87` - Fix
--fixdeeplink rendering issues introduced when the install
prompt was integrated into the CLI workflow.
0.0.26Patch
Patch Changes
- `7716d6c` - Integrate the SKILL.md installation prompt directly into the CLI
workflow so first-run users get the skill installed without a separate command.
0.0.25Patch
Patch Changes
longer auto-installs itself globally on first run. Resolves #43.
- `7e20da1` - Refactor the diagnostic payload structure used by the
score-estimation API and tighten its validation.
- `da83168` - Enhance the React Doctor skill installation script with detailed
usage instructions and support for multiple platforms.
0.0.24Patch
Patch Changes
0.0.23Patch
Patch Changes
0.0.22Patch
Patch Changes
- `84bb6d5` / `61406e0` / `1b07fa2` / `0299fc4` - Patch sweep - rule false-positive fixes and formatting follow-ups.
0.0.21Patch
Patch Changes
- `73da9e2` - Add the
--offlineflag so the CLI skips network calls (telemetry,
the leaderboard upload step) for users behind firewalls or in air-gapped CI.
0.0.20Patch
Patch Changes
produce diagnosable error messages instead of a silent non-zero exit. Also updates CLI option docs.
0.0.19Patch
Patch Changes
miscellaneous fixes.
0.0.18Patch
Patch Changes
requirement; previously users without TypeScript installed got a warning on every scan.
0.0.17Patch
Patch Changes
longer drops the rest of the scan output.
0.0.16Patch
Patch Changes
- `06fb14e` - Improve error handling in the linting and dead-code analysis paths
so failures log a useful message instead of a stack trace.
- `595ca55` - Remove the video package from
main(kept on a separate branch for
asset generation).
0.0.15Patch
Patch Changes
the README with usage docs for the published node API.
0.0.14Patch
Patch Changes
- `90ffa0a` - Add
llms.txtso models discovering the package via the npm
registry can find structured docs.
- `e218d63` - Format the leaderboard data files to satisfy CI
format:check.
0.0.13Patch
Patch Changes
refresh. Reverted in `28d820a` when the assets failed to render correctly on npm.
0.0.12Patch
Patch Changes
- `b200689` / `4519747` / `7f0b4d2` / `6d1ae5e` / `5688c1f` / `6dd481a` / `ce8437e` - Iteration sweep on detection / output formatting / README copy
during the pre-1.0 stabilization push.
0.0.11Patch
Patch Changes
REACT_DOCTOR_* env vars through the score-estimation API call so CI overrides actually take effect.
0.0.10Patch
Patch Changes
- `e5ef934` - "Almost ready" milestone - the rule pipeline + scoring + CLI surface
are end-to-end functional for the first time. No discrete commits between 0.0.9 and 0.0.10 beyond the version bump itself.
0.0.9Patch
Patch Changes
(Ami-style copy block formatting).
CLI prompt.
score line).
darken them.
- `b5ea69b` - Add the GitHub Action for CI integration and fix monorepo scanning
inside CI environments.
- `578e75a` - Auto-install globally in the background when run via
npx. (Later
removed in 0.0.25 / #44 because it surprised users.)
- `bde1167` - Add the video package and Ami skills for marketing-asset
generation.
0.0.8Patch
Patch Changes
- `19fa34b` - Resolve a merge conflict in
cli.tsintroduced in 0.0.7.
- `eef87e0` - Use a single deeplink for
--fixinstead of the two-step deeplink
- sleep dance.
rules table, promote install above options).
0.0.7Patch
Patch Changes
- `2ae9b87` - Fix the
--fixdeeplink to open the project with the correct cwd
and autosend the prompt.
0.0.6Patch
Patch Changes
- `f9157c7` - Add the
no-side-effect-in-get-handlerrule and export the oxlint
plugin as a standalone entrypoint.
update the README with consumer-friendly docs.
automated rule application.
- `330afc2` / `7aa7b3f` / `05d2f79` / `4dfaeab` - Add ASCII doctor face / box branding to the CLI score output and
the website terminal.
- `b1f1abc` - Add the CI workflow for e2e tests, lint, and format.
- `4b481c8` - Add the React Doctor skill and the install prompt on first run.
0.0.5Patch
Patch Changes
- `ccf404a` - Gracefully handle failures in
oxlint, the reduced-motion check,
and summary-file writing so a single subsystem can't take down the scan.
- `f1407d7` - Gracefully handle knip failures on non-React config files.
- Project scoring (the 0–100 health score) lands in this release.
0.0.4Patch
Patch Changes
- `2d9a69b` - Add actionable help text, animation rules, per-rule summary files,
and strengthen the framework / dependency detection.
- `327c076` - Move the website source into
packages/website.
0.0.3Patch
Patch Changes
- `680e7c4` - Reduce default scan noisiness - tighter default rule severity
thresholds for the first user-facing prerelease.
0.0.2Patch
Patch Changes
- `a8770b7` - Add CLI scaffolding with the initial oxlint integration: scan
command, oxlint runner, diagnostic-collection pipeline.
0.0.1Patch
Patch Changes
- `f50426b` - Initial publish - empty package scaffold to claim the npm name.