Commit Graph

9 Commits

Author SHA1 Message Date
Aiden Bai aa61efae28 refactor: collapse @react-doctor/types and @react-doctor/project-info into core (#440)
* refactor: collapse @react-doctor/types and @react-doctor/project-info into core

Eliminates two private workspace packages whose only role was to
namespace shared TS types and project-discovery helpers — neither
was a real consumer boundary, and the indirection cost a `workspace:*`
edge per leaf package without saving any code.

**Layout**

- `packages/types/` → `packages/core/src/types/` (10 files, pure TS types)
- `packages/project-info/` → `packages/core/src/project-info/`
  (27 files, discoverProject + framework detection + monorepo helpers)
- `packages/types/src/react-native-dependency-names.ts` →
  `packages/oxlint-plugin-react-doctor/src/react-native-dependency-names.ts`
  (the canonical RN-aware-package list lives next to its heaviest
  consumer — the rule gate — and is re-exported from core to avoid
  the rule-plugin ↔ core dependency cycle the previous types package
  was specifically designed to break).

The two packages are deleted from the workspace; every consumer
(`react-doctor`, `api`, `oxlint-plugin-react-doctor`, `actions/review`,
+130 source/test files) now imports from `@react-doctor/core` directly.

**Errors cleanup**

Dropped the legacy plain-class `ReactDoctorError` and `isReactDoctorError`
from `project-info`. The four narrow errors (`ProjectNotFoundError`,
`NoReactDependencyError`, `PackageJsonNotFoundError`,
`AmbiguousProjectError`) now extend `Error` directly — the only valid
hierarchy is the tagged `Schema.TaggedErrorClass` `ReactDoctorError`
from `core/errors.ts`, which has been the canonical one since the
Effect v4 refactor. Added `isProjectDiscoveryError` as a narrow type
guard for the four leaves.

The discovery errors are thrown synchronously BEFORE the Effect
runtime takes over (at `diagnose()` / CLI entry points); inside the
runtime, the Project service's `translateProjectInfoError` converts
each one to its tagged-reason equivalent.

**Schema vs type name collisions**

`schemas.ts` exports `Diagnostic` / `JsonReport` / `JsonReportMode` /
… as Schema.Class values, while `types/` exports the same names as
TS types. The core barrel re-exports the TS-type versions
(broad-strokes API) and exposes the Schema classes via a new
`@react-doctor/core/schemas` subpath export for the two callers
that need them (`Reporter` service, smoke validator, schema tests).

**Validation**

- `pnpm typecheck` ✓
- `pnpm lint` ✓
- `pnpm format` ✓
- `pnpm test` ✓ (1449 passing, 3 skipped — no regressions)
- `pnpm smoke:json-report` ✓

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(core): keep RN-dependency constants out of types barrel to avoid pulling oxlint plugin into discovery

Bugbot caught (medium severity): re-exporting `isReactNativeDependencyName` from
`oxlint-plugin-react-doctor` via the core types barrel meant every consumer of
`@react-doctor/core` (including `discoverProject` / `discoverReactSubprojects`)
would load the entire 286-rule plugin at module-init.

Adds a tiny standalone leaf at `core/src/project-info/internal-rn-dependency-names.ts`
(zero imports, just the constants the workspace-discovery gate needs) and switches
`is-package-json-react-native-aware.ts` to use it. The types barrel no longer
re-exports the plugin-side constants — rule consumers import them directly from
`oxlint-plugin-react-doctor`.

The duplication is intentional and documented at both files; the two lists are
short enough that a future regression test in the plugin can observe the union.

Validated: typecheck + test (1449 pass) + lint + format all green.
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-23 01:32:44 -07:00
Aiden Bai bfaf9c9530 fix: avoid React 19 rule false positives on React 18 (#254) 2026-05-15 05:40:34 -07:00
Aiden Bai d23137e177 test: add fixed issue regression coverage (#258) 2026-05-15 02:23:03 -07:00
Aiden Bai f0198e2f2d refactor: extract @react-doctor/{types,project-info,core} internal packages + cleanup (#249) 2026-05-14 22:22:19 -07:00
Aiden Bai 4d622970b5 Revert "pub"
This reverts commit 4d9d1a5fcf.
2026-05-14 22:05:22 -07:00
Aiden Bai 4d9d1a5fcf pub 2026-05-14 22:03:02 -07:00
Aiden Bai 7169551251 refactor: tier 1 cleanup — drop dead fields, flatten core/, inline single-use utils (#244)
* refactor: tier 1 cleanup — drop dead fields, flatten core/, inline single-use utils

Five structural simplifications in one PR, all zero-behavior-change
(verified: byte-identical 262-line diagnostic snapshot vs `main`).

## 1. Drop `examples` field on Rule

The `examples: RuleExample[]` slot was authored across all 178 rules in
PR #234 but nothing ever consumed it. Pure storage / maintenance burden.
Removed the field from the interface, deleted `RuleExample` type, and
stripped the field from every rule via codemod.

## 2. Derive `framework` from bucket directory (no per-rule field)

The `framework` field was always 1:1 with the bucket directory:
- `nextjs/*.ts` → `framework: "nextjs"`
- `react-native/*.ts` → `framework: "react-native"`
- `tanstack-start/*.ts` → `framework: "tanstack-start"`
- `tanstack-query/*.ts` → `framework: "tanstack-query"`
- everything else → `framework: "global"`

Pure redundancy. Updated the codegen (`scripts/generate-rule-registry.mjs`)
to compute it from the bucket directory and inject it into the registry
entry. Rule files no longer carry the field at all. 178 lines removed
across rule files; codegen gained the bucket→framework table.

## 3. Default `category` from bucket, preserve overrides

Same shape as #2 but with overrides — most rules in a bucket share a
category, but a few (e.g. `tanstack-start-no-secrets-in-loader`,
`nextjs-no-side-effect-in-get-handler`) override to `"Security"`.

Codegen now provides a bucket→default-category table, and rules only
ship the `category` field when overriding. Codemod stripped ~170 of the
178 `category:` declarations (left 4 overrides intact, verified by
diagnostic snapshot).

## 4. Inline single-use plugin utils

Helpers in `plugin/utils/` and `plugin/rules/<bucket>/utils/` with only
one importer got inlined back into their consumer. Pure file-count
reduction — utilities shared by 2+ rules stay.

- `plugin/utils/`: ~50 → 40 files
- `plugin/rules/*/utils/`: ~30 → 27 files

## 5. Flatten `core/` subdirectories

The 5 subdirectories (`core/config/`, `core/detection/`,
`core/diagnostics/`, `core/runners/`, `core/scoring/`) collapsed into a
flat `core/` directory. Subdirs weren't pulling weight — consumers always
imported by specific file name, never as a group. Now everything's one
level deep.

(Exception: `core/runners/oxlint/` stays nested — those 6 files form a
cohesive oxlint-config subcluster.)

200+ import statements updated across the codebase. `git mv` used so
history is preserved.

## Verification

- `pnpm typecheck` — 0 errors
- `pnpm test` — 80 test files / 734 / 734 pass
- `pnpm lint` — 0 warnings, 0 errors
- `pnpm format` — clean
- Diagnostic-snapshot diff vs `origin/main` — byte-identical 262 lines

## Stats

- 330 files changed, **+650 / -6312 net** (5662 line reduction)
- 18 files deleted (dead utils + the now-pointless RuleExample type)
- 12 renamed (the core/ flatten)
- 54 renamed-with-modifications (core/ flatten + import updates)

After this PR, the largest source-of-truth files are the 178 rule files
themselves; everything else is supporting infrastructure that's now
smaller and flatter.

* fix(codegen): emit multi-line registry entries so prettier doesn't re-wrap them

CI's `format:check` reformatted long single-line registry entries to
multi-line, which would have caused the codegen-overwrite-on-build to
loop forever. Pre-emit the multi-line shape so the file is byte-stable
across `pnpm gen → pnpm format → pnpm gen`.

Verified: MD5 unchanged across gen → format → gen cycle.
2026-05-14 18:50:14 -07:00
Aiden Bai 8bb54c9823 refactor: split src/utils/ into src/core/{config,detection,diagnostics,scoring,runners}/ + src/cli/ (#224)
* refactor: split src/utils/ (68 flat files) into src/core/{config,detection,diagnostics,scoring,runners}/ + src/cli/

Final big organizational refactor in the series (#218 plugin/, #220 cli/,
#221 scan/, #222 drop browser-poc, #223 dead-code). Carves the 68-file
src/utils/ dumping ground into purpose-built directories.

New layout:

  src/cli/             presentation (26 files)
    + prompts.ts, highlighter.ts, spinner.ts, logger.ts
    + colorize-by-score.ts, format-error-chain.ts
    + indent-multiline-text.ts, wrap-indented-text.ts
    + build-hidden-diagnostics-summary.ts
    + should-auto-select-current-choice.ts, should-select-all-choices.ts
    + detect-agents.ts, to-display-name.ts
    + (existing cli/ files: index.ts, render-*.ts, etc.)

  src/core/            core logic (7 files at root + 5 subdirs)
    build-json-report.ts                shared JSON reporting (CLI + public API)
    build-json-report-error.ts          shared JSON error envelope
    group-by.ts, is-file.ts             generic primitives
    is-plain-object.ts, to-relative-path.ts
    read-file-lines-node.ts

    config/ (11)       config loading, root-dir resolution, ignore patterns,
                       glob matching, gitattributes parsing, validation
    detection/ (6)     project discovery, react/tailwind major-version parsing,
                       package.json reading, monorepo root detection
    diagnostics/ (12)  combine/filter/suppress/merge, jsx-opener-span
                       lookup, disable-directive walks
    scoring/ (6)       local + remote score calc, score breakdown,
                       reduced-motion check, proxy fetch
    runners/ (13)      oxlint + knip + their config builders, include-path
                       computation, Node-version compat, diff-files

  src/plugin/          (unchanged from #218)
  scan.ts, index.ts, errors.ts, types.ts, constants.ts,
  oxlint-config.ts, eslint-plugin.ts, install-skill.ts    top-level

Driven by a one-shot codemod that performed the moves via `git mv` (so
all 68 file renames preserve blame/log history) and rewrote every
relative import in the package + tests (98 importer files touched) to
the new paths.

One non-mechanical fix needed after the move:
  src/core/runners/run-oxlint.ts resolved its built plugin via
  `../../dist/react-doctor-plugin.js` -- with 1 extra level of nesting
  that needs to be `../../../dist/react-doctor-plugin.js`.

One test fixed:
  tests/regressions/scan-resilience.test.ts read a source file by
  hardcoded path (`src/utils/calculate-score-locally.ts`) to assert no
  `fetch(` reference -- updated to the new location
  (`src/core/scoring/calculate-score-locally.ts`).

Verification:
  - 128 files changed, +237 / -222 (almost entirely import-path edits)
  - typecheck/lint/format clean
  - 734/734 tests pass
  - build produces identical dist/cli.js, dist/index.js, etc.
  - No public API change
  - src/utils/ no longer exists

* refactor: move orchestrators into tiers + rename scan -> inspect (#225)

* refactor: move install-skill, scan, oxlint-config out of src/ root into their tiers

Top-level src/ was still holding three orchestrator-grade files that
logically belong inside the existing tier directories. Move them so the
src/ root only contains public-API entry points and cross-cutting types.

Moves (via git mv, history preserved):
  src/install-skill.ts   -> src/cli/install-skill.ts          CLI subcommand
                                                              (already imported
                                                              6 things from cli/)
  src/scan.ts            -> src/core/scan.ts                  Orchestrator
                                                              (lives with the
                                                              logic it calls)
  src/oxlint-config.ts   -> src/core/runners/oxlint-config.ts Sibling of
                                                              run-oxlint.ts
                                                              (config + runner)

After: top-level src/ contains only the entry points and shared types:
  src/
  ├── cli/                  presentation
  ├── core/                 logic
  ├── plugin/               rules
  ├── constants.ts
  ├── errors.ts
  ├── eslint-plugin.ts      public-API entry
  ├── index.ts              public-API entry
  ├── knip.d.ts             type declaration
  └── types.ts

Imports rewritten via a small codemod (11 files):
  - 3 moved files' internal imports recomputed for new depth
  - 4 src/ consumers of the moved files updated
  - 4 test files updated

Verification:
  - 11 files changed, +39/-39 (all import path edits)
  - typecheck/lint/format clean
  - 734/734 tests pass
  - build produces identical dist/* output
  - run-oxlint.ts's relative path to dist/react-doctor-plugin.js
    (../../../) is unchanged -- run-oxlint.ts itself didn't move

Stacks on top of #224 (utils->core reorg). Once #224 merges, this PR's
base auto-rebases to main.

* refactor: rename scan -> inspect (function, file, types, test)

"scan" was a misnomer -- the function does much more than scan files: it
runs lint via oxlint, runs dead-code via knip, calculates a score,
merges/filters diagnostics, and renders CLI output. "inspect" describes
the role accurately and matches v2's chosen name.

Renames:
  src/core/scan.ts            -> src/core/inspect.ts             (git mv)
  tests/scan.test.ts          -> tests/inspect.test.ts           (git mv)
  scan() function             -> inspect() function
  runScan()                   -> runInspect()
  mergeScanOptions()          -> mergeInspectOptions()
  ResolvedScanOptions         -> ResolvedInspectOptions
  ScanResult                  -> InspectResult                   (src/types.ts)
  ScanOptions                 -> InspectOptions                  (src/types.ts)
  describe("scan", ...)       -> describe("inspect", ...)        (test)

Importers updated:
  src/cli/index.ts                                4 sites
  src/core/build-json-report.ts                   3 sites
  tests/inspect.test.ts                           5 sites
  tests/regressions/cli-and-output.test.ts        4 sites
  tests/build-json-report.test.ts                 2 sites

Kept as English prose (not identifiers):
  - "full scan", "scan output", "scan only files", "scan banner" in
    comments, CLI option descriptions, and prompt text -- these describe
    the action of scanning the codebase, not references to the function.
  - constants.ts JSX_OPENER_SCAN_MAX_LINES (about lexical JSX scanning,
    unrelated to inspect()).

Verification:
  - 7 files changed, +54/-54 (all renames)
  - typecheck/lint/format clean
  - 734/734 tests pass
  - No public API change (scan/ScanResult/ScanOptions weren't exported
    from src/index.ts -- the public API still calls the underlying
    function diagnose() which is unchanged)

Stacks on #225 (which moved scan.ts to src/core/). Once #225 merges,
this PR's base auto-rebases to main.

* fix: restore help-text example paths corrupted by the import codemod

The utils->core import-path codemod's regex matched `from '...'`
substrings inside string literals -- specifically the EXAMPLE code
shown to users in oxlint diagnostic help text. Three messages in
HELP_TEXT_MAP had their example paths mangled into the codemod's
target form (`../../utils/...`) even though they were supposed to
illustrate paths in the USER'S code, not paths internal to this
package.

Restored to the original example paths:

  no-barrel-import:
    `import { Button } from '../../utils/components/Button.js'`
    -> `import { Button } from './components/Button'`

  no-dynamic-import-path:
    `import('../../utils/feature/heavy.js')`
    -> `import('./feature/heavy.js')`

  nextjs-no-css-link:
    `import styles from '../../utils/Button.module.css.js'`
    -> `import styles from './Button.module.css'`

Verified no other string literals were corrupted (grepped for
`["'`]../../(utils|core|cli)/` across src/ -- all remaining hits are
legitimate plugin/utils imports from the per-rule split in #218, which
is its own unrelated `utils/` directory).

Closes the Bugbot finding on PR #224.

* refactor: move format-error-chain / logger / highlighter to core/ (fix layering)

Addresses Bugbot's "Core modules depend backwards on CLI layer" finding
on PR #224. Several core/ files were importing from cli/, undermining
the layered structure the refactor was introducing:

  core/build-json-report-error.ts        -> cli/format-error-chain
  core/runners/extract-failed-plugin-name -> cli/format-error-chain
  core/config/load-config                -> cli/logger
  core/config/read-ignore-file           -> cli/logger
  core/config/resolve-config-root-dir    -> cli/logger

The 3 utilities in question are all pure abstractions (no CLI-specific
behavior beyond using console.* for output), so they belong in core/
where both core AND cli consumers can import them without inversion.

Moves (git mv, history preserved):
  cli/format-error-chain.ts -> core/format-error-chain.ts  (Error.cause walker)
  cli/logger.ts             -> core/logger.ts              (silent-mode logger)
  cli/highlighter.ts        -> core/highlighter.ts         (picocolors wrapper)

22 files changed, +29/-29 -- all relative-import path rewrites driven
by a small codemod with a strict `^import ... from "..."` regex
(line-anchored to avoid the help-text-literal corruption bug from the
previous codemod, which triggered the bigger Bugbot follow-up).

After this:
  - core/ no longer imports from cli/ for utilities -- only
    core/inspect.ts still does (legitimately: it's the CLI orchestrator
    that calls render-*.ts + resolve-oxlint-node prompt + spinner)
  - cli/ files import the moved utilities via `../core/<name>.js`

Verification:
  - typecheck/lint/format clean
  - 734/734 tests pass
  - `rg "from .+cli/" src/core` shows only inspect.ts's legitimate
    imports of render-*, spinner, resolve-oxlint-node (all of which
    are CLI presentation/interaction, not utilities)
2026-05-13 10:39:21 -07:00
Aiden Bai 8f100985fe feat(react-doctor): 4 new React 18→19 migration rules (#152)
Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
2026-05-07 00:44:17 -07:00