Skip to content

feat(react-doctor): adopt user lint config, ship as ESLint plugin, remove browser surface#151

Merged
aidenybai merged 10 commits intomainfrom
feat/adopt-existing-lint-config
May 7, 2026
Merged

feat(react-doctor): adopt user lint config, ship as ESLint plugin, remove browser surface#151
aidenybai merged 10 commits intomainfrom
feat/adopt-existing-lint-config

Conversation

@aidenybai
Copy link
Copy Markdown
Member

@aidenybai aidenybai commented May 5, 2026

Closes #143 from both sides.

Issue #143 asked for react-doctor to integrate with existing ESLint setups so diagnostics flow through "classic hooks / analysis." This PR addresses that two ways:

  1. Fold the user's config INTO a react-doctor scanadoptExistingLintConfig (default-on). 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, react-doctor merges it into the same scan via oxlint's extends field. Diagnostics from those rules count toward the 0–100 score.
  2. Fold react-doctor's rules INTO the user's eslint run — new react-doctor/eslint-plugin export. Drop into a flat config, get the same curated rule set with recommended / next / react-native / tanstack-start / tanstack-query / all presets, and diagnostics surface inline through whichever ESLint integration the user already runs.

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 to preserve the previous behavior. customRulesOnly: true also implies opt-out.

The PR also broadens the diagnostic file-extension filter from .tsx/.jsx to the full source-file pattern so adopted rules and react-doctor's own JS-performance rules now report on plain .ts/.js files.

Resilience

If oxlint can't load the user's config (broken JSON, missing plugin, unknown rule name), react-doctor logs the reason on stderr ([react-doctor] could not adopt existing lint config (...); retrying without extends. Set "adoptExistingLintConfig": false to silence.) and retries the scan once without extends so a real score is still produced off the curated rule set.

Limitations (documented)

  • 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. Category-level enables ("categories": {...}) do not — react-doctor's local categories block always wins.
  • Plugins from the user's config are unioned in (verified empirically against oxlint 1.63), so "plugins": ["unicorn"] + "rules": { "unicorn/no-array-for-each": "error" } works.
  • Detection walks up only to a project boundary (.git directory or monorepo root), never to the user's home directory.

Implementation highlights

Adopt-existing-lint-config

  • New detect-user-lint-config.ts that walks up to a project boundary and returns the first matching config (.oxlintrc.json preferred over .eslintrc.json). Filename list lives in constants.ts (ADOPTABLE_LINT_CONFIG_FILENAMES) next to KNIP_CONFIG_LOCATIONS.
  • createOxlintConfig accepts extendsPaths; emits extends only when non-empty.
  • runOxlint writes the config via a small writeOxlintConfig helper (allows in-place rewrite for retry), runs the spawn loop through spawnLintBatches. On failure with non-empty extends, we rewrite without extends, log to stderr, and retry once.
  • New adoptExistingLintConfig?: boolean field on ReactDoctorConfig (default true), wired through validate-config-types.ts, scan.ts, and index.ts.
  • PLUGIN_CATEGORY_MAP gains category defaults for eslint, oxc, typescript, unicorn, import, promise, n, node, vitest, jest, nextjs so adopted-rule diagnostics group meaningfully.
  • Diagnostic file filter JSX_FILE_PATTERNSOURCE_FILE_PATTERN so .ts/.js files aren't dropped post-parse.

ESLint plugin

  • New src/eslint-plugin.ts reuses every rule from the existing oxlint plugin (each rule's create(context) => visitors signature is already ESLint-compatible), wraps each in the ESLint v9-required { meta, create } shape, and ships flat configs reusing the exact severity maps the CLI emits to oxlint so the engines stay in lock-step.
  • RuleSeverity, NEXTJS_RULES, REACT_NATIVE_RULES, TANSTACK_START_RULES, TANSTACK_QUERY_RULES, and GLOBAL_REACT_DOCTOR_RULES are now exported from oxlint-config.ts so the eslint-plugin module can read the canonical rule sets without re-declaring them.
  • New ./eslint-plugin entry in package.json exports + a matching vite.config.ts build entry; dist/eslint-plugin.{js,d.ts} ships alongside react-doctor-plugin.{js,d.ts}.

Commits

  • 87e5cc6 — adopt-existing-lint-config (core feature, retry-on-failure, detection walk-up, fixtures, tests)
  • 605cc5echore: export rule severity / category constants from oxlint-config
  • ea988a3chore: bump oxlint to ^1.63.0 and pin oxlint-tsgolint via override
  • 2b59680 — eslint-plugin source
  • 6acdddcbuild: wire react-doctor/eslint-plugin entry + README docs
  • 8eed892 — eslint-plugin changeset

Test plan

  • pnpm typecheck clean
  • pnpm lint 0 errors (333 pre-existing warnings unchanged)
  • pnpm test — 461 tests pass (was 456); new tests cover:
    • detect-user-lint-config.test.ts — empty dir, single oxlint/eslint config, first-match priority, dotless / JS variants ignored, walk-up to root, project-boundary stop.
    • run-oxlint.test.tsadoptExistingLintConfig: default-on merges user rules, .ts files reported (not just .tsx), opt-out skips, customRulesOnly skips, broken-config fallback emits stderr warning and still resolves.
    • validate-config-types.test.ts — passthrough + stringy coercion for the new boolean.
  • pnpm --filter react-doctor build produces dist/eslint-plugin.{js,d.ts} (~257 kB / ~55 kB gzipped).
  • End-to-end CLI verification:
    • react-doctor packages/react-doctor/tests/fixtures/user-oxlint-config reports (2) debugger violations across app.tsx + util.ts (was (1), .tsx only) and folds them into the score (97/100, vs 99/100 with adoptExistingLintConfig: false).
    • react-doctor packages/react-doctor/tests/fixtures/user-oxlint-config-broken (intentionally references unknown plugin) emits the retry warning on stderr and produces a real score (100/100 on the clean fixture) instead of failing the lint pass.

Note

Medium Risk
Medium risk because it changes how lint diagnostics are sourced and scored (now optionally extends user configs and includes .ts/.js), and it removes previously published browser-related entrypoints/packages which may break downstream consumers.

Overview
React Doctor now (by default) adopts an existing JSON .oxlintrc.json / .eslintrc.json found at or above the scan root (up to a project boundary) by wiring it into oxlint via extends, so user rules run alongside the curated rule set and count toward the health score; failures to load the adopted config are handled by logging and retrying once without extends.

It also adds a first-class ESLint v9 flat-config plugin export at react-doctor/eslint-plugin, exposing recommended/framework presets and all configs that mirror the CLI’s severity maps.

Separately, the PR drops the browser surface area (the react-doctor-browser/headless-browser workspace package and related CLI/exports) and adds a pnpm override for oxlint-tsgolint.

Reviewed by Cursor Bugbot for commit 51e8448. Bugbot is set up for automated code reviews on this repo. Configure here.

…nd factor those rules into the score (#143)

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 it into the same scan via oxlint's `extends`
field. The user's existing rules fire alongside the curated react-doctor
rule set and count toward the 0–100 score — no separate oxlint / eslint
invocation needed. Default-on; opt out via `"adoptExistingLintConfig": false`.

If oxlint can't load the user's config (broken JSON, missing plugin,
unknown rule), react-doctor logs the reason on stderr and retries once
without `extends` so the scan still produces a useful score off the
curated rule set instead of failing the entire lint pass.

Also broadens the diagnostic file-extension filter from `.tsx`/`.jsx`
to the full source-file pattern so adopted rules and react-doctor's own
JS-performance rules now report on plain `.ts`/`.js` files.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-doctor-website Ready Ready Preview, Comment May 7, 2026 5:39am

aidenybai added 3 commits May 5, 2026 15:34
…xlint-config

Marks `RuleSeverity`, `NEXTJS_RULES`, `REACT_NATIVE_RULES`,
`TANSTACK_START_RULES`, `TANSTACK_QUERY_RULES`, and
`GLOBAL_REACT_DOCTOR_RULES` as `export` so downstream consumers (the
website's rule-list page, tooling that introspects react-doctor's
curated surface) can read the canonical rule sets without re-declaring
them. No behavior change for the lint pipeline.
Tracks the 1.62.0 → 1.63.0 oxlint release so we pick up upstream rule
fixes and `extends` improvements. Adds a workspace-level
`pnpm.overrides` entry pinning `oxlint-tsgolint` to `^0.22.1` to keep
its peer-dep range from drifting on lockfile regenerations.
…g plugin (#143)

Adds `react-doctor/eslint-plugin` so users on a flat-config ESLint
setup can register the same curated rules they'd get from a CLI scan.
Wraps each rule from the existing oxlint plugin in an ESLint
`{ meta, create }` shape and ships ready-made flat configs:

  import reactDoctor from "react-doctor/eslint-plugin";
  export default [reactDoctor.configs.recommended];

Available configs:
  - `recommended`  — `GLOBAL_REACT_DOCTOR_RULES`
  - `next`         — Next.js rules
  - `react-native` — React Native rules
  - `tanstack-start` / `tanstack-query`
  - `all`          — every rule above at its recommended severity

Pairs with the same-PR adopt-existing-lint-config feature: that one
folds the user's eslint config into a react-doctor scan; this one
folds react-doctor's rules into the user's eslint run.
aidenybai added 2 commits May 5, 2026 15:35
…nt usage

Adds the `./eslint-plugin` subpath to `package.json` exports and a
matching build entry in `vite.config.ts` so the plugin emits as
`dist/eslint-plugin.{js,d.ts}` alongside the existing oxlint plugin.
The `VERSION` env var is threaded into the build env so
`eslintPlugin.meta.version` resolves to the package version at compile
time instead of the `"0.0.0"` fallback.

README split: the existing "Use the oxlint plugin standalone" section
becomes a more general "Use the lint plugin standalone" with two
subsections (oxlint, ESLint flat config), example presets, and a
cherry-pick example for users who only want a few rules.
Documents the new ESLint flat-config plugin alongside the existing
adopt-existing-lint-config changeset so the changelog tells both
sides of the issue-#143 story:

  - adopt-existing-lint-config — fold the user's eslint config INTO a
    react-doctor scan
  - eslint-plugin — fold react-doctor's rules INTO the user's eslint run

Both ship together under a single minor bump.
@aidenybai aidenybai changed the title feat: adopt project's existing oxlint / eslint config and factor those rules into the score (#143) feat: address #143 — adopt user's lint config + ship react-doctor as an ESLint plugin May 5, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d50c93c. Configure here.

Comment thread packages/react-doctor/src/utils/run-oxlint.ts
…r-browser package

Removes the entire browser surface that landed in #148:

- `react-doctor/browser` and `react-doctor/worker` subpath exports
  (in-browser diagnostics merge / filter / score pipeline)
- `react-doctor browser …` CLI subcommand (`start` / `stop` / `status` /
  `snapshot` / `screenshot` / `playwright`)
- `react-doctor-browser` workspace package (Playwright + CDP + system
  Chrome launcher + cross-browser cookie extraction)

The website never imported them and no other internal package consumed
them, so this trims ~9.2k LOC and drops `playwright` + `libsql` and
their transitive deps from the install graph.

The full removed source is preserved on the `archive/browser` branch
for future revival or vendoring.

Collateral changes:

- Collapse `utils/calculate-score-node.ts` into `utils/calculate-score.ts`
  now that there is no `-browser` counterpart to disambiguate from. The
  Node-only `proxyFetch` flow is unchanged.
- Drop the constants the browser CLI owned (`CDP_CONNECT_TIMEOUT_MS`,
  `SNAPSHOT_TIMEOUT_DEFAULT_MS`, viewport defaults, SIGTERM grace,
  session paths) from `constants.ts`.
- Generalize the EPIPE handler comment in `cli.ts` away from the
  `react-doctor browser snapshot | head` example.
- Drop the "Browser API" section from the README.

Documentation:

- Replace the deleted `.changeset/browser-cli-and-design-rules.md`
  (which doubled up the browser changelog with 11 still-shipping lint
  rules) with two focused changesets:
  - `remove-browser-entrypoints-and-cli.md` — the breaking change above
  - `new-state-correctness-and-design-rules.md` — preserves the
    previously-bundled changelog for the 3 state/correctness rules and
    8 design-system rules in `react-ui.ts`

Tests + tooling:

- Add `tests/calculate-score.test.ts` covering `tryScoreFromApi`
  (network failure, missing fetch, non-2xx, success with `filePath`
  stripping, malformed payload) and the `calculateScore` orchestration
  with a stubbed global `fetch`. Restores the API-failure-fallback
  coverage that was previously only in the deleted `browser.test.ts`.
- Drop `tests/fixtures/**` from workspace lint and add
  `tests/fixtures/.oxlintignore` so direct `vp lint --fix` /
  `oxlint --fix` invocations against fixture paths can't silently
  strip the `debugger;` statements and empty blocks the
  `adoptExistingLintConfig` tests assert on.
- Drop dead code uncovered while making the lint pass cleanly: unused
  `isMemberProperty` import in `nextjs.ts`, unused `fileContainsPattern`
  helper in `discover-project.ts`, unused locals in two test files.
Copy link
Copy Markdown

@vercel vercel Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

In diff mode and with file ignores, .ts/.js files are filtered out by computeJsxIncludePaths and resolveLintIncludePaths, contradicting the PR's stated goal of broadening coverage to include those files.

Fix on Vercel

…lobPattern wrapper

The `diagnoseCore` engine in `src/core/` was designed so the deleted
browser adapters could share orchestration with the Node `diagnose()`
API. With the browser surface gone, the only caller is
`src/index.ts::diagnose`, so the dependency-injection shape
(`loadUserConfig`, `discoverProjectInfo`, `calculateDiagnosticsScore`,
`getExtraDiagnostics`, `createRunners`) is pure indirection.

- Inline `diagnoseCore` and `buildDiagnoseTimedResult` directly into
  `index.ts::diagnose`. Lint / dead-code parallelism, error-isolating
  `Promise.allSettled`, and reduced-motion environment checks all
  preserved verbatim.
- Move `core/calculate-score-locally.ts` and `core/try-score-from-api.ts`
  to `utils/` (they were never browser-shared — `proxyFetch` is
  Node-only) and drop the now-empty `src/core/` directory.
- Remove the unused `matchGlobPattern` wrapper from
  `utils/match-glob-pattern.ts` (production code uses
  `compileGlobPattern` directly via `is-ignored-file.ts`); rewrite
  `tests/match-glob-pattern.test.ts` to exercise `compileGlobPattern`
  with a local helper.

Net –116 LOC. `scan.ts` was already independent of `diagnoseCore` and
is unchanged. All 443 tests pass; built CLI smoke unaffected.
…int-config

# Conflicts:
#	packages/react-doctor/src/commands/browser/playwright.ts
@aidenybai aidenybai changed the title feat: address #143 — adopt user's lint config + ship react-doctor as an ESLint plugin feat(react-doctor): adopt user lint config, ship as ESLint plugin, remove browser surface May 7, 2026
@aidenybai aidenybai merged commit d71a6bf into main May 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

eslint plugin

1 participant