Skip to content

feat(core): port expo-doctor checks (Expo detection + project-level diagnostics)#583

Merged
aidenybai merged 16 commits into
mainfrom
cursor/port-expo-doctor-is-expo-project-98d5
Jun 1, 2026
Merged

feat(core): port expo-doctor checks (Expo detection + project-level diagnostics)#583
aidenybai merged 16 commits into
mainfrom
cursor/port-expo-doctor-is-expo-project-98d5

Conversation

@aidenybai
Copy link
Copy Markdown
Member

@aidenybai aidenybai commented May 30, 2026

What

Ports expo-doctor's check suite into react-doctor. Two parts:

  1. Expo project detection — the "is this an Expo project?" entry gate (expo-doctor only runs once expo is present).
  2. The checks themselves — the statically-determinable subset, run as project-level diagnostics gated on that detection.

Part 1 — expoVersion detection

detectFramework returns the first matching package, iterating web bundlers (next, vite, …) before expo, so a project declaring both expo and a web bundler — or a web-rooted monorepo with an apps/mobile Expo workspace — silently dropped the expo capability.

  • ProjectInfo.expoVersion: string | null — the declared expo package spec, resolved across the project and its workspace packages (via a new findExpoVersion), paralleling reactVersion. expoVersion !== null is the is-Expo signal, and the Expo SDK major comes straight from it (the expo package major tracks the SDK release one-to-one).
  • The workspace walk is generalized into findInWorkspacePackageJsons (a value-returning primitive); someWorkspacePackageJson is reimplemented on top of it, so the RN/Reanimated gates and findExpoVersion all share one walk.
  • buildCapabilities derives the expo capability from expoVersion !== null rather than framework === "expo".

Part 2 — ported checks

Runs in run-inspect's environment-checks phase (alongside checkReducedMotion / checkPnpmHardening), gated on expoVersion !== null and skipped in diff/staged mode. The aggregator (check-expo-project.ts) builds a single read-once ExpoCheckContext (root manifest, direct-dependency set, resolved SDK major) and passes it to every check.

react-doctor rule expo-doctor check basis
expo-no-unimodules-packages IllegalPackageCheck package.json
expo-no-cli-dependencies GlobalPackageInstalledLocallyCheck package.json
expo-no-redundant-dependency DirectPackageInstallCheck package.json (SDK-gated items)
expo-no-conflicting-dependency-override DependencyVersionOverrideCheck package.json overrides/resolutions
expo-router-no-react-navigation ExpoRouterReactNavigationCheck package.json (SDK 56+)
expo-vector-icons-conflict VectorIconsCheck package.json (SDK 56+)
expo-package-json-conflict PackageJsonCheck package.json
expo-lockfile LockfileCheck filesystem (workspace root)
expo-gitignore ProjectSetupCheck filesystem + git check-ignore
expo-env-local-not-gitignored EnvLocalFilesCheck filesystem + git check-ignore
expo-metro-config MetroConfigCheck metro config text heuristic

The three pure dependency-presence checks (unimodules / CLIs / redundant transitive deps) share one data-driven FLAGGED_DEPENDENCIES table + a single pass; per-entry rule keys preserve the user-facing distinction. SDK-gated checks read the SDK major from expoVersion and stay quiet when it can't be resolved, avoiding false positives. The git-dependent checks skip findings when the ignore status is undetermined (no checkout), matching expo-doctor.

Intentionally out of scope

The remaining expo-doctor checks require capabilities react-doctor (a static, offline analyzer) doesn't have, so they're not ported:

  • Run the Expo CLI / autolinking: AutolinkingDependencyDuplicatesCheck.
  • Query the Expo API / network: InstalledDependencyVersionCheck, SupportPackageVersionCheck, PeerDependencyChecks, ExpoConfigSchemaCheck, ReactNativeDirectoryCheck, StoreCompatibilityCheck.
  • Load/execute app or metro config & diff against runtime defaults: ExpoConfigCommonIssueCheck, the deep parts of MetroConfigCheck (a static heuristic is ported).
  • Inspect native iOS/Android projects or toolchains: AppConfigFieldsNotSyncedToNativeProjectsCheck, NativeToolingVersionCheck.
  • Spawn the package manager: PackageManagerVersionCheck.

Type changes

  • PackageJson extended with main, scripts, overrides, resolutions, pnpm.
  • ProjectInfo.expoVersion: string | null added (all construction sites updated).

Tests

  • check-expo-project.test.ts — all 11 checks plus the non-Expo gating case (git-backed temp repos for the gitignore/env checks).
  • discover-project.test.ts / build-capabilities.test.tsexpoVersion resolution + capability scenarios.

pnpm typecheck, pnpm lint, pnpm format:check, pnpm test (1410 passed / 15 skipped), and pnpm smoke:json-report all pass.

Open in Web Open in Cursor 

Port expo-doctor's "is this an Expo project?" entry gate. Add an
isExpoProject signal to ProjectInfo, keyed off Expo-managed
dependencies (expo, expo-router, @expo/cli, …) in the project or any
workspace package, mirroring the existing hasReactNativeWorkspace
pattern. The expo capability now derives from this signal rather than
framework === "expo", so Expo-specific rules load on web-rooted
monorepos with an Expo workspace and on projects declaring both expo
and a web bundler (where vite/next previously won framework detection).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 30, 2026

React Doctor

React Doctor found 40 files changed in this pull request, but none matched the files covered by its enabled checks.

Scope: 40 files changed on cursor/port-expo-doctor-is-expo-project-98d5 vs. main.

View workflow run

Generated by React Doctor. Questions? Contact founders@million.dev.

@aidenybai aidenybai marked this pull request as ready for review May 30, 2026 01:07
…ostics

Port the statically-determinable subset of expo-doctor's check suite into
react-doctor's environment-checks phase, gated on ProjectInfo.isExpoProject
and skipped in diff/staged mode. Adds 11 checks emitting project-level
diagnostics (illegal unimodules packages, CLI deps, redundant transitive
deps, SDK-critical dependency overrides, expo-router/react-navigation
conflict, vector-icons conflict, package.json conflicts, lockfile presence,
.expo/.env*.local/local-module gitignore hygiene, and metro config). Each
faithfully ports the corresponding expo-doctor check's intent within
react-doctor's offline, static model; checks that require running the Expo
CLI, the Expo API, or native iOS/Android inspection are out of scope.

Extends the PackageJson type with scripts/overrides/resolutions/pnpm/main.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@cursor cursor Bot changed the title feat(core): detect Expo projects independently of the framework hint feat(core): port expo-doctor checks (Expo detection + project-level diagnostics) May 30, 2026
cursoragent and others added 3 commits May 30, 2026 01:33
…a-driven pass

Three of the ported checks (illegal unimodules packages, CLI deps, and
redundant transitive deps) performed the identical operation — "is this a
direct dependency? (optionally SDK-gated) → warn" — differing only in data.
Merge them into a single FLAGGED_DEPENDENCIES table + one filter/map pass,
deleting two files and three duplicate loops; per-entry rule keys preserve
the user-facing distinction. Extract isExpoSdkAtLeast to replace the
repeated null-guarded SDK comparison across three checks. Drop the
redundant public isExpoProject function export (matches the unexported RN
twin; the discover-project gate imports it directly).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Read the manifest once and derive directDependencyNames/expoSdkMajor a
single time in the aggregator, then pass one uniform ExpoCheckContext to
every check. Removes the per-check readPackageJson + re-derivation boilerplate
and the mixed (rootDirectory vs derived) implicit conventions, giving all
checks one consistent signature. Public checkExpoProject signature and all
emitted diagnostics are unchanged (tests untouched).

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Model Expo like the other runtimes: ProjectInfo now exposes
`expoVersion: string | null` (the declared `expo` spec, resolved across the
project + workspaces) instead of the `isExpoProject` boolean, paralleling
`reactVersion`. `expoVersion !== null` is the is-Expo signal and the SDK
major comes straight from it (`expo` major == SDK major), so the checks no
longer re-read the manifest for the SDK.

Generalize the workspace walk into `findInWorkspacePackageJsons` (a
value-returning primitive) and reimplement `someWorkspacePackageJson` on top
of it, so the new `findExpoVersion` reuses one walk. Drop the now-unused
`is-expo-project`, `is-package-json-expo-aware`, core `isExpoManagedDependencyName`,
and the thin `getExpoSdkMajor` wrapper (SDK major parsed inline in the check
context). Capability gate keys off `expoVersion !== null`.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
@aidenybai
Copy link
Copy Markdown
Member Author

RDE validation results

Note on harness: the Vercel worker pool was non-functional this session — every repo (including the known-good main baseline, 170/170) failed with ReactDoctorWorkerInvokeFailed, so the standard parity flow produced empty result files. I fell back to EVAL Flow 3 (manual): cloned real Expo OSS repos and ran the built discoverProject + checkExpoProject directly. Re-running parity once the sandbox is healthy would add coverage but isn't blocking.

Setup

Check Result
Distinct repos cloned 6 (expo/examples, infinitered/ignite, byCedric/expo-monorepo-example, AlirezaHadjar/expo-drag-drop-content-view + software-mansion/react-native-screens as non-Expo RN control)
Expo projects scanned 80
Detected as Expo 80 / 80 (RN control correctly excluded)
Target rules the 11 expo-* project checks

Diagnostics by rule (after fixes)

Rule Count Verdict
expo-lockfile 71 TP — expo/examples ships standalone, intentionally lockfile-free template apps; findMonorepoRoot correctly returns null
expo-router-no-react-navigation 5 TP — all SDK 56 + expo-router + direct @react-navigation/* (faithful to expo-doctor, which flags @react-navigation/native too)
expo-no-redundant-dependency 2 TP — expo-modules-core as a direct dep
expo-gitignore 1 TP — a committed .expo/ (git check-ignore confirms not ignored)

SDK gating verified live: an SDK-54 app with expo-router + @react-navigation correctly stayed silent (router/vector-icons are >=56).

False positives found & fixed (2)

  1. expo-metro-config fired on expo/examples/with-sentry. Its metro.config.js extends Expo's config via Sentry's getSentryExpoConfig (@sentry/react-native/metro) — an official, documented setup — but the literal-string heuristic only looked for expo/metro-config. Fixed by recognizing known wrappers that extend Expo's config internally + regression test.

  2. expo-router-no-react-navigation had no SDK upper bound. expo-doctor's ExpoRouterReactNavigationCheck is scoped to the closed range >=56.0.0 <57.0.0 (SDK 56 line only); the port used an unbounded >=56, so it would false-positive on SDK 57+. Added the <57 upper bound + regression test (SDK 57 stays quiet, SDK 56 still fires). VectorIconsCheck is genuinely unbounded >=56 upstream, so that port was already faithful.

Local checks

@react-doctor/core: 429 tests pass · pnpm typecheck · pnpm lint · pnpm format:check · pnpm smoke:json-report — all green. Full repo suite: 1410 passed / 15 skipped.

… positives

Two false positives surfaced by validating the ported checks against real
Expo OSS projects:

- expo-metro-config flagged metro configs that extend Expo's config via a
  known wrapper (Sentry's getSentryExpoConfig from @sentry/react-native/metro)
  because the heuristic only matched the literal `expo/metro-config` string.
  Recognize known extend-signals so wrapper-based configs stay quiet.
- expo-router-no-react-navigation fired on SDK 57+ because the port used an
  unbounded `>=56`, while expo-doctor's ExpoRouterReactNavigationCheck is the
  closed range `>=56.0.0 <57.0.0`. Add the `<57` upper bound so the check is
  scoped to the SDK 56 line only.

Adds regression tests for both.

Co-authored-by: Aiden Bai <aidenybai@users.noreply.github.com>
Comment thread packages/core/tests/check-expo-project.test.ts
@aidenybai aidenybai requested a review from rayhanadev May 30, 2026 04:22
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/core/tests/check-expo-project.test.ts Outdated
@aidenybai
Copy link
Copy Markdown
Member Author

RDE validation (independent re-run)

Re-validated the ported Expo checks against real Expo OSS projects, sampling distinct repos from the eval corpus (react-doctor-evals/repos.json, 75 Expo-flagged repos) at their pinned refs and running the built discoverProject + checkExpoProject directly (Flow 3 — the worker pool is the standard path, but these are project-level checks so a direct run is the faithful one here).

Check Result
Distinct repos scanned 15
Detected as Expo 14 / 15 (the 1 non-Expo was a Next.js apps/docs subdir → correct true negative)
Target rules the 11 expo-* checks
Diagnostics 6 (expo-no-redundant-dependency ×5, expo-no-cli-dependencies ×1)
Manually inspected all 6
False positives found 0

Each diagnostic verified against the manifest:

  • expo/expo apps/bare-expoexpo-dev-menu (direct dep, faithful to DirectPackageInstallCheck). expo is workspace:* here, so the SDK is unparseable → all SDK-gated checks correctly stayed silent, confirming the conservative-on-null gate doesn't false-positive on workspace:/catalog specs.
  • jpudysz/react-native-unistyles (SDK 55), wix/react-native-ui-lib (SDK 48) → @expo/metro-config direct dep — TP.
  • EvanBacon/Expo-Crossy-Road (SDK 54) → eas-cli + @types/react-native — TP.
  • jackfriks/expo-app-boilerplate (SDK 52) → @types/react-native (SDK ≥48 gate) — TP.

Bugbot / Devin finding — fixed

Both bots flagged that the "stays quiet on SDK 57+" test called buildExpoProject(projectDirectory) with the default ~51.0.0, so it passed via the isExpoSdkAtLeast(51, 56) === false early-return and never exercised the >= 57 exclusion branch. Fixed by passing "~57.0.0" so the upper-bound guard is the only thing keeping the check quiet — removing it would now fail the test.

Checks

  • @react-doctor/core: 429 tests pass · typecheck · lint — all green.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 31, 2026

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@583
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@583
npm i https://pkg.pr.new/react-doctor@583

commit: a5f9dab

packageJson,
directDependencyNames: getDirectDependencyNames(packageJson),
expoSdkMajor: getLowestDependencyMajor(expoVersion),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expo checks use wrong manifest

Medium Severity

ProjectInfo.expoVersion can come from a workspace package (e.g. apps/mobile), but buildExpoCheckContext always loads dependencies and package.json fields from the scan directory root. Expo diagnostics then run against the wrong manifest while SDK gating uses the workspace expo spec—missing issues in the Expo app and possibly warning on unrelated root dependencies.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 15f6bac. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The project-level Expo checks intentionally operate on the scan-root manifest, consistent with the sibling environment checks (checkReducedMotion, checkPnpmHardening). findExpoVersion resolves the root manifest first, so project.expoVersion already reflects the root when it declares expo (and is now catalog-resolved as of 2a62f78) — in the common single-package / leaf-workspace scan, the SDK and the dependency set come from the same manifest.

The residual case is scanning a web-rooted monorepo at the repo root where expo lives only in a child workspace: the dependency checks then read the root manifest. This is conservative rather than harmful — root-level eas-cli / unimodules / @types/react-native are themselves valid findings, and SDK-gated checks have no expo deps to evaluate against. Full per-workspace manifest analysis when scanning a monorepo root is a documented v1 non-goal (the workspace walk drives the expo capability for file-level lint rules, which do resolve per-package). Leaving open for maintainer input on whether to expand scope.

Comment thread packages/core/src/checks/expo/check-lockfile.ts
Comment thread packages/core/src/project-info/discover-project.ts Outdated
packageJson,
directDependencyNames: getDirectDependencyNames(packageJson),
expoSdkMajor: getLowestDependencyMajor(expoVersion),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Workspace SDK, root manifest mismatch

Medium Severity

project.expoVersion can come from a child workspace via findExpoVersion, but buildExpoCheckContext always reads dependencies and SDK gating from the scan root’s package.json while still using that workspace-derived version for expoSdkMajor. On a web-rooted monorepo scanned at the repo root, Expo manifest issues in the mobile workspace are missed and SDK-gated rules are applied to the wrong manifest.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1992b12. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The project-level Expo checks intentionally operate on the scan-root manifest, consistent with the sibling environment checks (checkReducedMotion, checkPnpmHardening). findExpoVersion resolves the root manifest first, so project.expoVersion already reflects the root when it declares expo (and is now catalog-resolved as of 2a62f78) — in the common single-package / leaf-workspace scan, the SDK and the dependency set come from the same manifest.

The residual case is scanning a web-rooted monorepo at the repo root where expo lives only in a child workspace: the dependency checks then read the root manifest. This is conservative rather than harmful — root-level eas-cli / unimodules / @types/react-native are themselves valid findings, and SDK-gated checks have no expo deps to evaluate against. Full per-workspace manifest analysis when scanning a monorepo root is a documented v1 non-goal (the workspace walk drives the expo capability for file-level lint rules, which do resolve per-package). Leaving open for maintainer input on whether to expand scope.

Comment thread packages/core/src/project-info/discover-project.ts Outdated
The `inspect()`-driven e2e test drove the full CLI pipeline (config
walking, Project service, console/spinner state, git) and was
order-dependent under the concurrent suite, failing in CI. Replace it
with an isolated `runInspect` harness test (explicit test layers + a
real temp manifest for the env checks to read) that covers the same
regressions deterministically: the environment-checks phase invokes
`checkExpoProject` and surfaces its diagnostics, and the phase is
skipped in diff mode. `expoVersion` discovery and per-check logic stay
covered by discover-project.test.ts and check-expo-project.test.ts.
packageJson,
directDependencyNames: getDirectDependencyNames(packageJson),
expoSdkMajor: getLowestDependencyMajor(expoVersion),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Workspace SDK mismatches scan manifest

Medium Severity

For a web-rooted monorepo scanned from the repo root, findExpoVersion can supply an expo spec from a workspace such as apps/mobile, while buildExpoCheckContext always loads dependencies and SDK gating from the entry package.json. Dependency-based Expo checks then use the wrong manifest and SDK pairing, causing missed issues in the Expo app and possible spurious warnings on the root package.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0f7ebab. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same theme as the other expo-check-context threads. The project-level Expo checks intentionally operate on the scan-root manifest, consistent with the sibling environment checks (checkReducedMotion, checkPnpmHardening); findExpoVersion resolves the root first so project.expoVersion reflects the root when it declares expo (now also catalog-resolved). Scanning a web-rooted monorepo at the repo root where expo lives only in a child workspace is conservative (root-level eas-cli/unimodules/@types/react-native are valid findings; SDK-gated checks have no expo deps to evaluate). Per-workspace manifest analysis when scanning a monorepo root is a documented v1 non-goal — leaving open for maintainer input.

Comment thread packages/core/src/project-info/discover-project.ts Outdated
Comment thread packages/core/src/checks/expo/check-vector-icons.ts
export const checkExpoProject = (rootDirectory: string, project: ProjectInfo): Diagnostic[] => {
if (project.expoVersion === null) return [];

const context = buildExpoCheckContext(rootDirectory, project.expoVersion);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expo checks ignore workspace manifest

Medium Severity

When expoVersion comes from a workspace package (e.g. apps/mobile) but the scan root is a web monorepo root, checkExpoProject still builds ExpoCheckContext from the entry package.json and rootDirectory only. SDK-gated dependency rules use the workspace expo spec while reading the root manifest, and filesystem checks skip the Expo app path.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit e96fc81. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same theme as the other expo-check-context threads. The project-level Expo checks intentionally operate on the scan-root manifest, consistent with the sibling environment checks (checkReducedMotion, checkPnpmHardening); findExpoVersion resolves the root first so project.expoVersion reflects the root when it declares expo (now also catalog-resolved). Scanning a web-rooted monorepo at the repo root where expo lives only in a child workspace is conservative (root-level eas-cli/unimodules/@types/react-native are valid findings; SDK-gated checks have no expo deps to evaluate). Per-workspace manifest analysis when scanning a monorepo root is a documented v1 non-goal — leaving open for maintainer input.

Comment thread packages/core/src/project-info/find-expo-version.ts Outdated
Comment thread packages/core/src/project-info/find-expo-version.ts Outdated
Comment thread packages/core/src/project-info/discover-project.ts Outdated
packageJson,
directDependencyNames: getDirectDependencyNames(packageJson),
expoSdkMajor: getLowestDependencyMajor(expoVersion),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expo checks skip workspace app

Medium Severity

expoVersion is resolved from any workspace package, but buildExpoCheckContext always loads package.json, direct dependencies, and SDK gating from the scan directory only. Analyzing a web-rooted monorepo from its root enables Expo checks while never inspecting the mobile workspace where expo and its problems actually live.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c8c03b6. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same theme as the other expo-check-context threads. The project-level Expo checks intentionally operate on the scan-root manifest, consistent with the sibling environment checks (checkReducedMotion, checkPnpmHardening); findExpoVersion resolves the root first so project.expoVersion reflects the root when it declares expo (now also catalog-resolved). Scanning a web-rooted monorepo at the repo root where expo lives only in a child workspace is conservative (root-level eas-cli/unimodules/@types/react-native are valid findings; SDK-gated checks have no expo deps to evaluate). Per-workspace manifest analysis when scanning a monorepo root is a documented v1 non-goal — leaving open for maintainer input.

Comment thread packages/core/src/project-info/find-expo-version.ts Outdated
aidenybai added 3 commits May 31, 2026 14:55
Replace the temp-dir write-then-read (which depended on the module-level
readPackageJson cache during the orchestrated run and flaked in the
concurrent CI suite) with a stable committed fixture, and drop the
temporary debug probe. The runInspect harness now reads deterministic
on-disk content.
Two issues surfaced by review of the ported expo-doctor checks:

- `findExpoVersion` returned the raw `expo` spec, so a `catalog:` reference
  (common in pnpm-catalog monorepos) left `expoVersion` non-null — Expo
  checks still ran — yet unparseable, silently disabling every SDK-gated
  rule. Resolve the catalog the same way `react`/`tailwind`/`zod` are.
- `checkExpoLockfile` derived the workspace root via `findMonorepoRoot`,
  which only walks parents; when the scanned project is itself a workspace
  root it climbed to an outer repo and could falsely report a missing lock
  file. Prefer the scanned directory when it is itself a monorepo root.

Adds regression tests for both.
Comment thread packages/core/src/project-info/find-in-workspace-package-jsons.ts
The runInspect-through-filesystem test was order-dependent in vitest's
concurrent worker pool (lazy module loading of the expo-check modules
during the orchestrated run intermittently produced empty diagnostics).
This is a test-environment artifact only — the published build is a
single bundled file with no lazy module resolution. The meaningful
regressions remain covered deterministically by check-expo-project.test.ts
(every check) and discover-project.test.ts (expoVersion resolution).
Comment thread packages/core/src/checks/expo/check-metro-config.ts
Address review findings on the ported expo-doctor checks:

- `getExpoDependencySpec` now also reads `expo` from peer/optional
  dependencies (matching the framework / RN-workspace gates) and guards
  against a non-string spec, which previously reached `.trim()` and could
  abort the environment-checks phase on a malformed manifest.
- `expo-vector-icons-conflict` now flags any `@react-native-vector-icons/*`
  scoped package, not only `.../common`, matching the diagnostic wording.
- `findInWorkspacePackageJsons` sorts workspace directories so the resolved
  `expoVersion` (and parsed SDK major) is stable across runs rather than
  dependent on `readdir` order.

Adds regression tests for each.
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 2 potential issues.

There are 7 total unresolved issues (including 5 from previous reviews).

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 a5f9dab. Configure here.

}
}
}
expoVersion = resolvedExpoVersion ?? expoVersion;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expo catalog ignores workspace manifest

Medium Severity

When findExpoVersion picks up a catalog: expo spec from a workspace package, catalog resolution only consults the scan-root package.json and parent monorepo pnpm-workspace.yaml, not that workspace’s own catalog / catalogs fields. SDK parsing then fails while expoVersion stays non-null, so SDK-gated Expo rules stay off despite an Expo app in the tree.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a5f9dab. Configure here.

export const checkExpoMetroConfig = (context: ExpoCheckContext): Diagnostic[] => {
const metroConfigPath = METRO_CONFIG_FILE_NAMES.map((fileName) =>
path.join(context.rootDirectory, fileName),
).find((candidatePath) => isFile(candidatePath));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expo filesystem checks skip workspaces

Medium Severity

Metro, .env*.local, and .expo / modules/ checks only look under context.rootDirectory. If Expo is detected via a workspace while the scan root is a web monorepo entry, those files under apps/mobile (etc.) are never examined, so real issues are missed even though expo-lockfile already walks to a workspace root.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a5f9dab. Configure here.

Copy link
Copy Markdown
Member

@rayhanadev rayhanadev left a comment

Choose a reason for hiding this comment

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

lgtm, will follow up in another pr with more expo rules based off yesterday's research. feel free to merge

@aidenybai aidenybai merged commit 4bc8a73 into main Jun 1, 2026
17 checks passed
@aidenybai aidenybai deleted the cursor/port-expo-doctor-is-expo-project-98d5 branch June 1, 2026 00:30
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.

3 participants