* feat(browser-poc): add AST normalizers for production React analysis
Add 12 AST normalizers that transform minified production code into
patterns the react-doctor plugin rules can analyze. Enables 30+ rules
to fire on production bundles with 0 parse errors and 0 rule failures
across 6 tested sites (vercel, notion, linear, discord, shopify, ami).
Normalizers: SequenceExpression callee unwrap, OXC literal type
normalization, boolean/void recovery, return/expression sequence
splitting, JSX reconstruction (jsx/jsxs/createElement → JSXElement
tree with Fragment, ExpressionContainer, key extraction), setter
binding + reference rename, and component name uppercase recovery.
Also adds parent reference tracking in visitAst, "use client"
directive injection, WASM failure caching for CSP-blocked sites,
truncated source skip, score calculation, and a null-safety fix
for prefer-useReducer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: address user feedback — reduce false positives, improve scoring transparency, and add config options
- Add `offline`, `designRules`, and `entryFiles` config options
- Suppress React 19 deprecation rules on React 18 (migration-hint gate)
- Skip `rn-no-raw-text` for `.web.*` files (RN platform convention)
- Add sleep/delay and paginated-fetch heuristics to `asyncAwaitInLoop`
- Remove `noEmDashInJsxText` rule (em dashes are standard punctuation)
- Add `designRules` toggle to disable opinionated design rules
- Thread `entryFiles` to knip for dead-code false positive reduction
- Export `calculateScoreBreakdown` and show formula in `--verbose`
- Document scoring formula, diff/staged modes, and agent integration
- Switch to `@changesets/changelog-github` for richer changelogs
- Add GitHub Releases workflow
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor: capabilities-based rule gating system
Replace bespoke flags and filter functions with a unified
capabilities + tags system:
- Add buildCapabilities(project) that derives a flat Set<string>
from ProjectInfo (react:19, nextjs, tanstack-query, etc.)
- Add RULE_METADATA map with requires[] (capability gates) and
tags (static classification like "design", "test-noise")
- Replace filterRulesByReactMajor, filterDesignRules, VERSION_GATED_RULE_IDS,
VersionGateMode, and conditional spreads with one shouldEnableRule predicate
- Replace 9 individual fields on RunOxlintOptions/OxlintConfigOptions
with project: ProjectInfo
- Add ignore.tags to user config (replaces designRules boolean)
- Cherry-pick from cursor/library-aware-deprecation-rules-ec3b:
peerRangeSupportsLegacyReact, isTestFilePath, isLikelyBuildEntry,
parseTailwindMajorMinor, isLikelyStringReceiver (js-set-map-lookups fix)
- Compute effective React version from min(installed, peerRangeFloor)
so library-targeting-legacy is handled by version gating alone
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: library peer-range detection + README designRules cleanup
- Fix Bugbot: peerRangeMinMajor computes the floor major from the peer
range so effective version is min(installed, peerFloor) instead of null
- Fix Bugbot: replace designRules config key with ignore.tags in README
- Add peerRangeMinMajor tests
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: wire isLikelyBuildEntry + isTestFilePath into post-scan suppression
- Add auto-suppression in mergeAndFilterDiagnostics: suppress knip/files
diagnostics when a matching build artifact exists, suppress test-noise
tagged rules in test/fixture files
- Tag deprecation and design rules with "test-noise" in RULE_METADATA
- Fixes Bugbot: isLikelyBuildEntry is no longer dead code
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: remove dead DESIGN_TAGS constant, clear auto-suppression caches
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: remove dead peerRangeSupportsLegacyReact, unused ruleKey param, new Promise false negative
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: forward ignoredTags and entryFiles in diagnose() programmatic API
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: handle destructuring in loop-carried dependency detection
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: apply sleep/dependency heuristics to callback-based iteration
The loopBodyHasOnlySleepLikeAwaits and hasLoopCarriedDependency checks
were only applied in inspectLoopBody (for/while/do-while) but skipped
for callback-based iteration (.forEach, .map, etc.), causing false
positives on patterns like arr.forEach(async item => { await sleep(500) }).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: guard framework-specific rules against missing RULE_METADATA
Rules from framework-specific maps (NEXTJS_RULES, REACT_NATIVE_RULES,
etc.) without a RULE_METADATA entry were unconditionally enabled for
all projects. Now they are skipped at runtime, and
validateRuleRegistration warns about the gap at dev time.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: scope RULE_METADATA validation to framework-specific rules only
Global rules intentionally omit RULE_METADATA entries since they're
unconditionally enabled. Extracted FRAMEWORK_SPECIFIC_RULE_KEYS to
share the set between the runtime guard and the validation check.
---------
Squashes work that closes#143 plus follow-on cleanup of the browser
surface and dead-code.
**`adoptExistingLintConfig` (default-on)** — when a project has a
JSON-format `.oxlintrc.json` or `.eslintrc.json` at the scanned
directory or any ancestor up to the nearest project boundary, that
config is folded into the same scan via oxlint's `extends` field.
Diagnostics from the user's existing rules count toward the 0–100
health score alongside the curated react-doctor rule set; if oxlint
can't load the user config, react-doctor logs the reason on stderr
and retries the scan once without `extends`. JS / TS configs are
silently skipped (oxlint's `extends` can't evaluate them). Coverage
broadened to `.ts` and `.js` files (previously the parser dropped
non-JSX). Closes#143.
**`react-doctor/eslint-plugin` flat-config export** — the same
react-doctor rule set is shipped as an ESLint flat-config plugin so
projects that already run ESLint can wire it up without depending on
the CLI. Includes presets (`recommended`, `next`, `react-native`,
`tanstack-start`, `tanstack-query`, `all`) plus cherry-picking.
**Removed browser entrypoints, browser CLI, and `react-doctor-browser`
package** — the in-browser diagnostics pipeline (`react-doctor/browser`
+ `/worker`), the `react-doctor browser …` CLI subcommand
(`start` / `stop` / `status` / `snapshot` / `screenshot` /
`playwright`), and the standalone `react-doctor-browser` workspace
package (Playwright + CDP + cookie extraction) are gone. Nothing in
the monorepo consumed them and they pulled in a heavy dep footprint
(`playwright`, `libsql`). Source is preserved on the
`archive/browser` branch.
**`diagnose-core` engine inlined into `index.ts::diagnose`** —
the dependency-injection shape was designed for browser sharing.
Only one caller now, so collapsed into a flat orchestrator. Score
helpers moved from `src/core/` to `src/utils/`; `src/core/` directory
removed.
**Smaller cleanups** — drop unused `matchGlobPattern` wrapper,
`isMemberProperty` import in nextjs rule, `fileContainsPattern`
helper, and various dead test locals; ignore `tests/fixtures/**`
from workspace lint and add `.oxlintignore` so `lint --fix` against
fixture paths can't strip the `debugger;` statements that
`adoptExistingLintConfig` tests assert on.
Tests: 452 / 452 pass on Node 22 + 24. Build smoke green.
- Added `.turbo` to `.gitignore`.
- Removed obsolete `.oxlintrc.json` configuration file.
- Updated `package.json` to use `turbo` for script commands and added new `typecheck` and `check` scripts.
- Updated `devDependencies` to include `@voidzero-dev/vite-plus-core` and `turbo`, while removing `oxlint` and `tsdown`.
- Adjusted `pnpm-lock.yaml` to reflect new dependencies and overrides for `vite` and `vitest`.
- Removed `tsdown.config.ts` and `vitest.config.ts` files from `react-doctor` package.
- Refactored interfaces in `src/types.ts`, `src/core`, and `src/plugin` files for consistency.
- Cleaned up unused imports and code in various utility files.
- Deleted unused `react-doctor-icon.tsx` component from the website package.
* fix: detect namespace hook calls (React.useState, React.useEffect) in all rules
Fixes#117: isHookCall now handles MemberExpression callee types
(e.g. React.useEffect, React.useState) in addition to plain Identifier
callee types. This fixes silent misses in no-derived-state-effect,
no-fetch-in-effect, no-cascading-set-state, no-effect-event-handler,
rerender-functional-setstate, rendering-hydration-no-flicker, and
the countSetStateCalls helper.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: remove unsupported jsx-a11y/no-noninteractive-element-interactions rule
Fixes#113, #119: This rule does not exist in the bundled oxlint
binary, causing 'Failed to parse oxlint configuration file' errors
and score suppression on all scans.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: add --no flag to skip prompts and always run full scan
Fixes#106: Adds -n/--no flag that skips all interactive prompts
(like --yes) but specifically declines the diff-only scan prompt,
ensuring a full project scan runs. Useful for CI agents that cannot
interact with TTY prompts.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* test: add namespace hook detection tests (React.useEffect, React.useState, etc.)
Adds 13 tests verifying that all rules correctly detect namespace-style
hook calls (import * as React; React.useEffect, React.useState, etc.).
Covers: no-derived-state-effect, no-fetch-in-effect, no-cascading-set-state,
no-effect-event-handler, no-derived-useState, rerender-lazy-state-init,
rerender-functional-setstate, rerender-dependencies, rendering-hydration-no-flicker,
no-usememo-simple-expression, prefer-useReducer, plus a regression guard for
direct-import fixtures.
Relates to #117.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* test: add edge case tests for project discovery and catalog resolution
Adds tests for:
- peerDependencies-only with catalog reference resolution
- Catalog reference to nonexistent catalog name (fallback to default)
- Empty pnpm-workspace.yaml handling
- Malformed package.json in workspace subdirectories
- Nonexistent root directory for discoverReactSubprojects
- File entries mixed with directory entries in project root
Relates to #101, #87, #105, #71.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: handle malformed JSON in workspace package.json files gracefully
readPackageJson now catches SyntaxError from malformed JSON and returns
an empty object instead of crashing. This prevents workspace discovery
from aborting when a subdirectory has an invalid package.json.
Previously, a broken package.json in any workspace subdirectory would
crash the entire scan with an unhandled SyntaxError.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: add customRulesOnly config to disable builtin react/jsx-a11y rules
Fixes#109: Teams with existing ESLint setups can now set
customRulesOnly: true in react-doctor.config.json to only run
react-doctor/* custom rules, skipping the bundled react/, jsx-a11y/,
react-perf, and react-compiler rules.
This avoids rule duplication for projects that already lint with
eslint-plugin-react, eslint-plugin-jsx-a11y, etc.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: inherit config from ancestor directories for monorepo support
Fixes#73: loadConfig now walks up the directory tree to find
react-doctor.config.json or package.json reactDoctor key from
ancestor directories. This means a single config at the monorepo
root applies to all workspace packages without needing to duplicate
the config in each package.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: add --annotations flag for GitHub Actions annotation output
Fixes#81: The --annotations flag outputs diagnostics in GitHub
Actions annotation format (::error / ::warning) so they appear
as inline annotations on PR diffs. Example:
::error file=src/App.tsx,line=42,title=react-doctor/no-fetch-in-effect::fetch() inside useEffect
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* refactor: remove Ami integration
Removes all Ami-related code:
- --ami flag, --fix flag, fix subcommand, install-ami subcommand
- Ami detection, installation, deeplinks, and open-URL helpers
- Fix prompt (maybePromptFix) with Ami banners
- Skill installation prompt (maybePromptSkillInstall)
- fetchEstimatedScore and EstimatedScoreResult
- AMI_WEBSITE_URL, AMI_INSTALL_URL, AMI_RELEASES_URL, OPEN_BASE_URL,
ESTIMATE_SCORE_API_URL, ERROR_ESTIMATED_FIX_RATE, WARNING_ESTIMATED_FIX_RATE
- skill-prompt.ts utility
CLI bundle reduced by ~39KB (324KB → 285KB).
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* chore: remove .ami directory and all remaining Ami references
- Delete .ami/ directory (42 skill/config files)
- Delete packages/website/src/app/open/ (Ami redirect page)
- Remove Ami references from README.md, share page, install-skill
route, llms.txt, and install-skill.sh
- Zero Ami references remain in the codebase
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* chore: move .ami/skills to .agents/skills
Relocates all skill files from the deleted .ami/ directory into
.agents/skills/ and removes .agents from .gitignore so they are
tracked in version control.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: recognize custom text components in rn-no-raw-text rule
Fixes#93: The rn-no-raw-text rule now recognizes a much wider set of
text-handling components:
1. Expanded built-in names: Typography, Paragraph, Span, H1-H6
2. Keyword matching instead of suffix-only: any component whose name
contains Text, Title, Label, Heading, Caption, Subtitle, Typography,
Paragraph, Description, or Body is treated as text-handling
(e.g. StyledText, BodyText, CustomLabel, MyTypography)
3. New textComponents config option: users can specify arbitrary
component names in react-doctor.config.json that should be treated
as text-handling:
{
"textComponents": ["MyCustomComp", "NativeWindText"]
}
Diagnostics inside these components are filtered out post-lint.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* chore: upgrade dependencies
- oxlint: 1.47.0 → 1.59.0 (fixes SIGABRT crashes on large projects #84/#77)
- oxfmt: 0.32.0 → 0.44.0
- typescript: 5.9.3 → 6.0.2
- vitest: 4.0.18 → 4.1.4
- tsdown: 0.20.3 → 0.21.7
- knip: 5.83.1 → 6.3.1
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: add file count limit per oxlint batch to prevent OOM crashes
Mitigates #84/#77: Adds OXLINT_MAX_FILES_PER_BATCH (500) to cap the
number of files per oxlint invocation. Combined with the oxlint upgrade
from 1.47 to 1.59, this prevents SIGABRT/OOM crashes on large projects
with 100+ source files.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* feat: add --staged flag for pre-commit hook scanning
Fixes#115: The --staged flag scans only files in the git index
(staged with git add). It materializes staged content to a temp
directory so oxlint reads the exact index version, not the working
tree version. This is critical for pre-commit hooks where the working
tree may differ from what's being committed.
Usage:
npx react-doctor . --staged --fail-on error
For lint-staged:
"*.{tsx,jsx}": "npx react-doctor . --staged --fail-on error"
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* refactor: code review fixes per AGENTS.md rules
- DRY: Extract shared createFileLinesCache in filter-diagnostics.ts
- DRY: Extract resolveFailOnLevel in cli.ts (was duplicated in
staged and normal paths)
- DRY: Reuse exported getCalleeName from helpers.ts instead of
duplicating in state-and-effects.ts and performance.ts
- Magic number: Move maxBuffer 10MB to GIT_SHOW_MAX_BUFFER_BYTES constant
- Variable naming: prevLine → previousLine, tagName → fullTagName,
leafName → leafTagName, configFiles → projectConfigFilenames
- Remove .pop()! non-null assertion in favor of .at(-1) with fallback
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: prevent shell injection in staged file content reading
Replace execSync with string interpolation with spawnSync using an
args array. File paths with shell metacharacters (quotes, semicolons,
etc.) in their names are now passed safely without shell interpretation.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
* fix: pass resolved config to staged scan to preserve ancestor inheritance
The staged scan materializes files to /tmp, where loadConfig's ancestor
traversal would never find a monorepo root's config. Now the CLI resolves
config from the real project directory first and passes it via the new
configOverride ScanOption, so customRulesOnly, share, ignore rules,
and textComponents all apply correctly during --staged scans.
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
- Added homepage and bugs fields to package.json for better project visibility.
- Introduced new constants for React Native deprecated modules and legacy Expo package replacements in constants.ts.
- Updated index.ts to include new rules for React Native diagnostics.
Scaffolds the react-doctor CLI with project discovery, oxlint spawning,
and categorized diagnostic output. Bundles 36 rules across 5 plugins
(react, jsx-a11y, react-perf, import, typescript).
Generated with [Ami](https://ami.dev)
Co-Authored-By: Ami <noreply@ami.dev>