Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
072d37e
perf(plugin): memoize closureCaptures per scope analysis and function…
rayhanadev Jul 3, 2026
a1c8ee1
perf(plugin): index line starts once per content in security-scan loc…
rayhanadev Jul 3, 2026
5fec491
perf(plugin): memoize getElementType per JSX opening element across a…
rayhanadev Jul 3, 2026
05f6399
perf(plugin): memoize functionContainsReactRenderOutput per function …
rayhanadev Jul 3, 2026
6a9a73b
perf(plugin): gate route-scoped rules at create() instead of per node…
rayhanadev Jul 3, 2026
2980d0f
perf(plugin): cache compiled glob patterns in compileGlob (#1044)
rayhanadev Jul 3, 2026
fc75a3e
perf(core): balance lint batches by file size (LPT) to kill straggler…
rayhanadev Jul 3, 2026
fa61c20
perf(plugin): early-exit sweep — cheap discriminators before expensiv…
rayhanadev Jul 3, 2026
ebeee56
perf(plugin): regex-hoist sweep — stop rebuilding RegExps per call (#…
rayhanadev Jul 3, 2026
ac71a3b
perf(plugin): fused-walk sweep — one pass where several walked the sa…
rayhanadev Jul 3, 2026
d8628d7
perf(plugin): memoization sweep — cache pure per-file analyses (#1051)
rayhanadev Jul 3, 2026
da3b19c
perf(plugin): set-membership sweep — replace linear scans with Sets/M…
rayhanadev Jul 3, 2026
59e8178
perf(core): stop lint's pre-spawn burst and the scan walk from starvi…
rayhanadev Jul 3, 2026
173cc0a
fix(rules): four false-positive fixes from corpus census + adversaria…
devin-ai-integration[bot] Jul 3, 2026
8325343
chore: version packages (#1031)
github-actions[bot] Jul 4, 2026
1571119
fix(cli): raise runGit maxBuffer so the scan cache works on large rep…
rayhanadev Jul 4, 2026
e257a5e
perf(core): cache dead-code results so unchanged rescans skip the ana…
rayhanadev Jul 4, 2026
8c004f0
perf(cli): key the scan-result cache on dirty worktree content instea…
rayhanadev Jul 4, 2026
20d81f6
fix(core): harden the dead-code cache key against version and input-l…
rayhanadev Jul 4, 2026
ce49250
perf(core): incremental dead-code analysis via a per-file summary cac…
rayhanadev Jul 4, 2026
ea9a775
perf(core): incremental cross-file sidecar lint via dependency probe …
rayhanadev Jul 4, 2026
bdf8074
feat(telemetry): classify every scan's cache temperature in the wide …
rayhanadev Jul 4, 2026
ced746f
fix(core): make the cache stack survive CI checkouts and the action c…
rayhanadev Jul 4, 2026
574cba6
chore: version packages (#1055)
github-actions[bot] Jul 4, 2026
6b21b70
feat(core): surface reactDetected so a gated-off scan can't pass for …
devin-ai-integration[bot] Jul 4, 2026
c0c3fc1
fix(core): CI-gate FP — match structural findings by occurrence in co…
devin-ai-integration[bot] Jul 4, 2026
160f84c
chore: version packages (#1063)
github-actions[bot] Jul 4, 2026
ce9dabf
fix(core): --staged now works in monorepo subdirectories (git show :.…
rayhanadev Jul 4, 2026
64452aa
fix(cli): scan cache no longer replays across projects at a reused pa…
rayhanadev Jul 5, 2026
5809083
feat(cli): add --supply-chain / --no-supply-chain to toggle the depen…
rayhanadev Jul 5, 2026
d353dad
feat(core): detect React when scanning a monorepo package subdirector…
rayhanadev Jul 5, 2026
1880b15
fix(oxlint-plugin): scope useEffectEvent effect-event rules to React …
devin-ai-integration[bot] Jul 5, 2026
bcc2e6a
test: vite-plus prerelease 0.2.3 smoke test
wan9chi Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions .changeset/jsx-no-jsx-as-prop-slot-names.md

This file was deleted.

8 changes: 8 additions & 0 deletions .changeset/monorepo-subdir-react-detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@react-doctor/core": patch
---

Detect React when scanning a package subdirectory of a monorepo, so React rules no longer gate off silently. Two additions at the `discoverProject` seam:

- **Nearest-ancestor discovery.** A scan target with no `package.json` of its own now adopts the nearest enclosing package (a leaf workspace, a plain app root, or a monorepo root — whichever is closest, bounded by the git root) instead of only workspace-configured monorepo roots. Scanning `app/src/components` in a plain React app now inherits the app's React detection rather than synthesizing an empty, React-blind project.
- **Node-resolution React version fallback.** When declarations yield no usable React version (a version-less spec like `workspace:*` / `*` / a dist-tag, or React living only in a hoisted `node_modules` the declaration walks never reach), the version is resolved the way Node itself would — `require.resolve("react/package.json")` — making "React is installed and importable" ⇒ "React is detected" an invariant. Guarded to installations physically inside the enclosing repo so a globally installed React can't leak in, and it never overrides a parseable peer range (`^18 || ^19` still floors to the lowest supported major).
10 changes: 0 additions & 10 deletions .changeset/no-secrets-author-name-fp.md

This file was deleted.

5 changes: 5 additions & 0 deletions .changeset/no-supply-chain-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-doctor": minor
---

Add `--supply-chain` / `--no-supply-chain` CLI flags to toggle the dependency supply-chain scan, mirroring `--lint`/`--no-lint` and `--dead-code`/`--no-dead-code`. Supply-chain enablement now resolves as a scan option (`InspectOptions.supplyChain`) against `supplyChain.enabled` — the flag wins — so it takes precedence over per-project config on every scan (a workspace module's config can't undo `--no-supply-chain`), and config isn't mutated so `scan.hasCustomConfig` telemetry stays accurate. The enabled state also rides the per-scan wide event as `scan.supplyChain`.
5 changes: 5 additions & 0 deletions .changeset/non-react-use-effect-event.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"oxlint-plugin-react-doctor": patch
---

Stop `rules-of-hooks` and `no-effect-event-in-deps` from firing on a `useEffectEvent` imported from a non-React package. Both rules match the hook by NAME to stay in parity with eslint-plugin-react-hooks (whose fixtures call a bare global), so a same-named custom hook — e.g. `@rocket.chat/fuselage-hooks`'s `useEffectEvent`, a stable-callback helper designed to be stored and passed as props — was flagged as if it were React's experimental effect event ("only works when called from Effects", "re-runs your effect every render"). Detection is now disambiguated by import source: a `useEffectEvent` explicitly imported from a module outside `REACT_RUNTIME_MODULE_SOURCES` (`react`, `react-dom`, `preact/compat`, `preact/hooks`) is left alone, while React's own and bare/unimported names keep their existing behavior.
12 changes: 0 additions & 12 deletions .changeset/only-export-components-nested-scope-fp.md

This file was deleted.

5 changes: 5 additions & 0 deletions .changeset/scan-cache-cross-project-replay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-doctor": patch
---

Fix whole-repo scan cache replaying another project's diagnostics when a .git-less checkout sits inside an unrelated repository (e.g. a gitignored benchmark/mining clone directory reused across projects). The cache key's git identity (HEAD sha, worktree fingerprint) resolved from the enclosing repository, which cannot see the checkout's files, so two different projects materialized at the same path keyed identically. The key now requires the fingerprinted repository to actually track files under the project directory (cache off otherwise), and every cache hit re-verifies the stored payload's directory and `package.json` content hash so any future keying bug of this class degrades to a miss instead of a cross-project replay.
5 changes: 5 additions & 0 deletions .changeset/staged-monorepo-subdir-pathspec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-doctor/core": patch
---

Fix `--staged` silently scanning nothing when the project is a subdirectory of the git repo (the standard monorepo layout, e.g. `apps/webui`). Staged paths are collected project-relative (`git diff --cached --relative`), but the staged-content read used a bare `git show :<path>` index pathspec, which git resolves against the repo root — so in a subproject every read missed, the file was silently skipped, and the scan "passed" with `scannedFileCount: 0` (particularly dangerous in a pre-commit hook). The index read now uses the cwd-relative `git show :./<path>` form, matching how baseline `<ref>:<path>` reads were already resolved.
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
shamefully-hoist=true
# vite-plus preview build registry bridge (auto-added by vp)
registry=https://registry-bridge.viteplus.dev/
43 changes: 24 additions & 19 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,30 @@ runs:
path: ${{ runner.temp }}/react-doctor-toolchain
key: react-doctor-toolchain-${{ steps.resolve-version.outputs.resolved }}-node${{ inputs.node-version }}-${{ runner.os }}-${{ runner.arch }}

# Restore react-doctor's scan caches (the per-file content-addressed lint
# cache + the supply-chain cache) from the most recent previous run. In CI
# every commit is a fresh, SHA-scoped checkout, so the project-local
# `node_modules/.cache` never survives between commits — pointing
# REACT_DOCTOR_CACHE_DIR (set on the scan step) at a stable
# `${runner.temp}` path lets actions/cache carry it across runs. Restore +
# explicit save (after the scan) instead of the combined action: the
# combined post-job save is skipped when the job fails, and a blocking PR
# scan with findings fails the job BY DESIGN — exactly the runs whose
# fix-and-push retry needs a warm cache. The key is unique per run (an
# exact hit would suppress the save), so every run persists its refreshed
# state instead of freezing the first snapshot under an immutable key;
# restore falls back by prefix to the newest same-version save, then to
# ANY version. Cross-version restore is sound because every cached
# artifact re-validates internally (content hash + ruleset hash for lint
# entries — the bucket is LRU-pruned — schema version for scan results,
# TTL for supply-chain scores): a stale entry just misses and re-computes,
# while the version-independent supply-chain cache keeps a react-doctor
# release from cold-starting every repo's network checks at once.
# Restore react-doctor's scan caches (the whole-repo scan-result cache,
# the per-file content-addressed lint cache, the cross-file sidecar cache,
# the dead-code result + incremental summary caches, and the supply-chain
# cache) from the most recent previous run. In CI every commit is a fresh,
# SHA-scoped checkout, so the project-local `node_modules/.cache` never
# survives between commits — pointing REACT_DOCTOR_CACHE_DIR (set on the
# scan step) at a stable `${runner.temp}` path lets actions/cache carry it
# across runs. Restore + explicit save (after the scan) instead of the
# combined action: the combined post-job save is skipped when the job
# fails, and a blocking PR scan with findings fails the job BY DESIGN —
# exactly the runs whose fix-and-push retry needs a warm cache. The key is
# unique per run (an exact hit would suppress the save), so every run
# persists its refreshed state instead of freezing the first snapshot
# under an immutable key; restore falls back by prefix to the newest
# same-version save, then to ANY version. Cross-version restore is sound
# because every cached artifact re-validates internally (content hash +
# ruleset hash for lint entries — the bucket is LRU-pruned — schema
# version + per-file content hashes for scan results and the dead-code
# caches, TTL for supply-chain scores): a stale entry just misses and
# re-computes, while the version-independent supply-chain cache keeps a
# react-doctor release from cold-starting every repo's network checks at
# once. The stat-fingerprinted caches carry content-hash repair witnesses,
# so the fresh checkout's bumped mtimes are repaired in place instead of
# missing every run.
- id: scan-cache
if: ${{ steps.resolve-version.outputs.cacheable == 'true' }}
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
Expand Down
17 changes: 3 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,12 @@
"ts-json-schema-generator": "^2.9.0",
"turbo": "^2.9.7",
"typescript": "^6.0.3",
"vite-plus": "^0.1.15"
"vite": "catalog:",
"vite-plus": "catalog:"
},
"engines": {
"node": "^20.19.0 || >=22.13.0",
"pnpm": ">=8"
},
"packageManager": "pnpm@10.29.1",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"@sentry/cli",
"esbuild",
"unrs-resolver"
],
"overrides": {
"oxlint": ">=1.66.0 <1.67.0",
"oxlint-tsgolint": "^0.23.0"
}
}
"packageManager": "pnpm@10.29.1"
}
21 changes: 21 additions & 0 deletions packages/api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# @react-doctor/api

## 0.7.1

### Patch Changes

- Updated dependencies [[`c0c3fc1`](https://github.com/millionco/react-doctor/commit/c0c3fc170972876c8bbc2419b32e66b9c864df85)]:
- @react-doctor/core@0.7.1

## 0.7.0

### Patch Changes

- Updated dependencies []:
- @react-doctor/core@0.7.0

## 0.6.3

### Patch Changes

- Updated dependencies []:
- @react-doctor/core@0.6.3

## 0.6.2

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@react-doctor/api",
"version": "0.6.2",
"version": "0.7.1",
"private": true,
"description": "Programmatic API for React Doctor.",
"license": "SEE LICENSE IN LICENSE",
Expand Down
11 changes: 8 additions & 3 deletions packages/api/src/diagnose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
detectAiTrainingEnvironment,
Files,
Git,
hasReactRuntime,
layerOtlp,
Linter,
LintPartialFailures,
Expand Down Expand Up @@ -125,6 +126,7 @@ const outputToDiagnoseResult = (
skippedChecks,
...(Object.keys(skippedCheckReasons).length > 0 ? { skippedCheckReasons } : {}),
project: output.project,
reactDetected: hasReactRuntime(output.project),
elapsedMilliseconds,
};
};
Expand Down Expand Up @@ -236,12 +238,15 @@ const diagnoseProjectBatch = async (
(projectDefinition) => diagnoseProject(projectDefinition, baseOptions, batchConfig),
);

const succeededProjects = projectResults.filter((projectResult) => projectResult.ok);

return {
projects: projectResults,
diagnostics: projectResults.flatMap((projectResult) =>
projectResult.ok ? projectResult.diagnostics : [],
),
diagnostics: succeededProjects.flatMap((projectResult) => projectResult.diagnostics),
score: findWorstScore(projectResults),
...(succeededProjects.length > 0
? { reactDetected: succeededProjects.some((projectResult) => projectResult.reactDetected) }
: {}),
elapsedMilliseconds: globalThis.performance.now() - startTime,
};
};
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { diagnose } from "./diagnose.js";
export { defineConfig } from "@react-doctor/core";
export { defineConfig, hasReactRuntime } from "@react-doctor/core";

export type {
DiagnoseOptions,
Expand Down
42 changes: 42 additions & 0 deletions packages/api/tests/diagnose.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,32 @@ describe("diagnose", () => {
}
});

it("sets reactDetected true on a React project and false on a non-React one", async () => {
const reactResult = await diagnose(path.join(FIXTURES_DIRECTORY, "basic-react"), {
deadCode: false,
lint: false,
});
expect(reactResult.reactDetected).toBe(true);

const nonReactDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "rdc-nonreact-"));
fs.writeFileSync(
path.join(nonReactDirectory, "package.json"),
JSON.stringify({ name: "non-react-tool", dependencies: { lodash: "^4.0.0" } }),
);
fs.mkdirSync(path.join(nonReactDirectory, "src"));
fs.writeFileSync(
path.join(nonReactDirectory, "src", "index.ts"),
"export const add = (firstNumber: number, secondNumber: number): number => firstNumber + secondNumber;\n",
);
try {
const nonReactResult = await diagnose(nonReactDirectory, { deadCode: false, lint: false });
expect(nonReactResult.reactDetected).toBe(false);
expect(nonReactResult.project.reactVersion).toBeNull();
} finally {
fs.rmSync(nonReactDirectory, { recursive: true, force: true });
}
});

it("elapsedMilliseconds is non-negative", async () => {
const result = await diagnose(path.join(FIXTURES_DIRECTORY, "basic-react"), {
deadCode: false,
Expand Down Expand Up @@ -201,9 +227,25 @@ describe("diagnose({ projects })", () => {
expect(result.projects).toHaveLength(0);
expect(result.diagnostics).toHaveLength(0);
expect(result.score).toBeNull();
expect(result.reactDetected).toBeUndefined();
expect(result.elapsedMilliseconds).toBeGreaterThanOrEqual(0);
});

it("aggregates reactDetected across succeeded projects", async () => {
const result = await diagnose({
projects: [
{ directory: path.join(FIXTURES_DIRECTORY, "basic-react") },
{ directory: noReactTempDirectory },
],
deadCode: false,
lint: false,
});

expect(result.reactDetected).toBe(true);
const succeeded = result.projects.find((projectResult) => projectResult.ok);
expect(succeeded?.ok && succeeded.reactDetected).toBe(true);
});

it("clamps concurrency: 0 to 1 without hanging", async () => {
const result = await diagnose({
projects: [{ directory: path.join(FIXTURES_DIRECTORY, "basic-react") }],
Expand Down
26 changes: 26 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# @react-doctor/core

## 0.7.1

### Patch Changes

- [#1061](https://github.com/millionco/react-doctor/pull/1061) [`c0c3fc1`](https://github.com/millionco/react-doctor/commit/c0c3fc170972876c8bbc2419b32e66b9c864df85) Thanks [@devin-ai-integration](https://github.com/apps/devin-ai-integration)! - Fix a CI-gate false positive in the baseline delta: pre-existing element-level findings (Accessibility-category rules, plus rules flagged `matchByOccurrence` like `iframe-missing-sandbox`) are now matched by `(file, rule)` occurrence count instead of the flagged line's text, so reformatting the flagged line (reindentation, prettier reflow, collapsing a multi-line JSX element) no longer reports the finding as newly introduced. The flag is resolved at diagnostic creation and carried on the diagnostic as an optional `matchByOccurrence` field (also present in the JSON report). Expression-level rules keep line-text-sensitive matching, and a genuinely new extra occurrence still surfaces.

- Updated dependencies [[`c0c3fc1`](https://github.com/millionco/react-doctor/commit/c0c3fc170972876c8bbc2419b32e66b9c864df85)]:
- oxlint-plugin-react-doctor@0.7.1
- deslop-js@0.7.1

## 0.7.0

### Patch Changes

- Updated dependencies [[`ced746f`](https://github.com/millionco/react-doctor/commit/ced746f518f11e8283d488c4ff31c44e478bb0e5), [`20d81f6`](https://github.com/millionco/react-doctor/commit/20d81f6f26dc8f0562118076f835da2468591d5f), [`ce49250`](https://github.com/millionco/react-doctor/commit/ce4925008d37d7c86a234e6b9c7c2c3afe873405)]:
- deslop-js@0.7.0
- oxlint-plugin-react-doctor@0.7.0

## 0.6.3

### Patch Changes

- Updated dependencies [[`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`b4faf74`](https://github.com/millionco/react-doctor/commit/b4faf74744c730d0836235854b0233ce59a42566), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`b4faf74`](https://github.com/millionco/react-doctor/commit/b4faf74744c730d0836235854b0233ce59a42566), [`b4faf74`](https://github.com/millionco/react-doctor/commit/b4faf74744c730d0836235854b0233ce59a42566), [`072d37e`](https://github.com/millionco/react-doctor/commit/072d37e8e4f82454d2e187114d0194f26efc1bf0), [`2980d0f`](https://github.com/millionco/react-doctor/commit/2980d0f4ed6abfee061ac02f3a0820806f942b95), [`5fec491`](https://github.com/millionco/react-doctor/commit/5fec491e6844d73f658f355ae2cbe86285068f0e), [`05f6399`](https://github.com/millionco/react-doctor/commit/05f639910abf2b3bfc0802e9ad568ecd2b7ce13d), [`a1c8ee1`](https://github.com/millionco/react-doctor/commit/a1c8ee110e137bbc8771c8a471c20287cccd2b38), [`fa61c20`](https://github.com/millionco/react-doctor/commit/fa61c2056951df2429e79d888e5f7334aaf61cfd), [`ac71a3b`](https://github.com/millionco/react-doctor/commit/ac71a3b8cfc8bdd157f0f1bcd242b61ec69f9c17), [`d8628d7`](https://github.com/millionco/react-doctor/commit/d8628d7f21e60b0e6dfd98d76c9f24e03f7afe24), [`ebeee56`](https://github.com/millionco/react-doctor/commit/ebeee568abf9a7ed37ed9fe0bba695e4f2a11c9f), [`da3b19c`](https://github.com/millionco/react-doctor/commit/da3b19c79c27945d873eb24e34431cbefa8f9938), [`6a9a73b`](https://github.com/millionco/react-doctor/commit/6a9a73b14908272535aabab6742258b61bc2ee5c), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b), [`173cc0a`](https://github.com/millionco/react-doctor/commit/173cc0a8ba5578229e3832b2167d3f7a5386c91b)]:
- oxlint-plugin-react-doctor@0.6.3
- deslop-js@0.6.3

## 0.6.2

### Patch Changes
Expand Down
Loading
Loading