Skip to content

refactor(capabilities): unify framework gating into one capability vocabulary (#976)#978

Open
rayhanadev wants to merge 11 commits into
mainfrom
ray/nextjs-capability-gating
Open

refactor(capabilities): unify framework gating into one capability vocabulary (#976)#978
rayhanadev wants to merge 11 commits into
mainfrom
ray/nextjs-capability-gating

Conversation

@rayhanadev

@rayhanadev rayhanadev commented Jun 27, 2026

Copy link
Copy Markdown
Member

Why

A Next.js app built with output: "export" is fully static — no server runs when someone loads a page. But several rules suggested server-only fixes: a server redirect(), middleware, or a Server Action. A static export can't do any of those, so the advice was impossible to follow (#976).

The deeper problem was architectural: the fact "this project has no server" had to be expressed in five places (a detector, a ProjectInfo field, capability rows, a rule-name-matched message rewrite inside core, and rule metadata) — and the discovery layer feeding it walked the same workspaces up to seven times, in nondeterministic filesystem order, with three different scopes for three detectors. That scope mismatch was exactly the Bugbot finding on this PR: a workspace-level output: "export" was invisible to a monorepo-root scan.

This PR restructures the whole path into three layers, each fact expressed once:

flowchart TD
  W["collectWorkspaceFacts: ONE workspace pass<br/>(sorted, deduped; records which manifest<br/>supplied each signal)"]
  PI["ProjectInfo (wire shape unchanged)"]
  BC["buildCapabilities: one typed projection<br/>ProjectInfo → ReadonlySet&lt;Capability&gt;<br/>(memoized per project)"]
  R1["config time: requires / disabledWhen<br/>(typed — a typo fails tsc)"]
  R2["rule content: hasCapability at report time,<br/>recommendationFor at help time<br/>(core carries zero rule prose)"]
  W --> PI --> BC --> R1
  BC --> R2
Loading

What changed

  • Typed vocabulary. Capability is one union owned by the plugin; requires / disabledWhen / hasCapability all compile against it, so a misspelled token ("sever-actions") is a build error instead of a silently never-matching gate. Core's Framework aliases the plugin's FrameworkToken, so the two lists can't drift.
  • One projection, computed once. buildCapabilities moved out of the oxlint runner into project-info/ (it's a pure projection of ProjectInfo, also used by the security scan), lost its closure-table interpreter in favor of straight-line code, and is memoized per project. Capabilities are deliberately not a ProjectInfo field — the wire shape in the JSON report is unchanged.
  • Rules own their behavior. A new recommendationFor(hasCapability) hook lets a rule pick its advice from the capability set; core's getRuleRecommendation is now one generic dispatch with zero rule-name strings and zero rule prose (the static-export redirect advice and the per-framework secret-env advice both moved into their rules). no-prevent-default finished its conversion: the hardcoded CLIENT_ONLY_FRAMEWORKS set became the client-only capability.
  • Single-pass discovery. One workspace traversal collects every workspace-derived fact (react/tailwind/zod/framework, RN awareness, reanimated, expo, flash-list, next) instead of up to ~7 walks. All precedence quirks are preserved and now documented in one place; workspace order is sorted-deterministic instead of readdir order, and the cross-workspace framework merge is two-tier (web outranks mobile, matching detectFramework's own in-manifest priority — mobile coverage rides hasReactNativeWorkspace/expoVersion, so nothing is lost).
  • The [False Positive] Next.js rules recommend server-only fixes under output: "export" (static export) #976 fix, completed. The static-export probe reads next.config.* next to the manifest that supplied the next signal — so a monorepo whose apps/web sets output: "export" now drops server-actions, gains nextjs:static-export, turns server-fetch-without-revalidate off, and softens the redirect/form advice. This closes the deferred Bugbot finding.

Validated against the 863-repo OSS corpus

discoverProject output was diffed pre/post rewrite across all 863 cached RDE clones:

  • 23 repos (2.7%) differ, all multi-workspace monorepos.
  • No repo loses RN/Expo rule coverage; several web-primary monorepos (linkwarden, t4-app, better-auth, formik) regain their Next.js rules that alphabetical accident would have dropped.
  • The two isStaticExport flips (hyperdx, open-design) are real workspace static exports — dual-mode conditional configs (output: 'export' in one branch), treated as export-capable by design: advice must be possible in every configured mode.
  • A full priority rank across workspaces was measured too and rejected (it reclassified 34 repos toward docs-site workspaces).
  • The remainder is deterministic version re-attribution that previously depended on filesystem readdir order.

Compatibility

  • Public ProjectInfo / JSON-report shape: unchanged (pinned by a snapshot test).
  • One-time ruleset-cache turnover from the new client-only token (deterministic; the cache key already rotates on every plugin release). No SCAN_RESULT_CACHE_SCHEMA_VERSION bump — CachedScanPayload is untouched.
  • ESLint-plugin users who suppressed the <form> variant via settings["react-doctor"].framework should set settings["react-doctor"].capabilities: ["client-only"] (noted in the changeset). Missing settings degrade safely to the generic message.

Test plan

  • pnpm typecheck && pnpm lint && pnpm format:check && pnpm smoke:json-report && gen:check all pass; full suites green: core 1,115, react-doctor 2,023, oxlint-plugin 7,293, language-server 61, api 15. (Two pre-existing core failures on this machine are a known local global-gitignore artifact, present on the base commit too.)
  • A characterization commit lands before the rewrite: full-ProjectInfo snapshots over a fixture matrix (pnpm named catalogs, RN-in-web monorepo, nx, leaf-in-monorepo, overlapping globs, the tailwind piggyback quirk) that must hold bit-for-bit across the rewrite. The 100-case discovery spec passes with zero assertion edits.
  • New pins: client-only capability matrix, memo identity, workspace static-export discovery (root config / workspace config / two-Next-apps first-match / no-export), web-over-mobile merge, and a no-override e2e proving a monorepo-root scan softens the redirect advice.
  • The corpus sweep above stands in for RDE: this change only re-attributes discovery facts and softens/removes advice; the 23-repo diff was triaged repo-by-repo.

For later, out of scope: widening react-compiler detection to workspaces (flips 12 disabledWhen: ["react-compiler"] rules — needs its own RDE-validated PR), and parsing next.config.* with oxc instead of regex to see through comments/conditionals.

🤖 Generated with Claude Code


Note

Medium Risk
Large refactor of discovery and rule-enablement paths affects monorepo classification for ~2.7% of OSS repos; behavior is heavily tested but version/framework attribution can shift deterministically.

Overview
Fixes #976 by treating Next.js output: "export" as a first-class project fact: discovery reads next.config.* next to the manifest that supplied the next dependency (including workspace apps in a monorepo root scan), sets isStaticExport, and projects nextjs:static-export / drops server-actions. Rules then gate or soften advice—e.g. server-fetch-without-revalidate off, redirect guidance without middleware/SSR, no-prevent-default using framework-neutral form messaging instead of Server Actions.

Capability vocabulary is centralized: a typed Capability union drives requires, renamed disabledWhen (was disabledBy), memoized getCapabilities, and oxlint settings["react-doctor"].capabilities. New tokens include client-only, server-actions, and nextjs:static-export. Rules own conditional prose via recommendationFor(hasCapability); core no longer rewrites specific rule messages.

Project discovery is refactored into one sorted workspace pass (collectWorkspaceFacts) instead of many redundant walks, with deterministic framework merge (web over mobile). project-info modules are consolidated (capabilities, detectors, dependencies, version, workspaces, etc.).

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

Next.js `output: "export"` apps have no request-time server, so rules that recommend server `redirect()`, middleware, or Server Actions emitted impossible advice. Detect static export and surface it through a general framework-capability vocabulary so the relevant rules adapt: server-fetch-without-revalidate gates off, nextjs-no-client-side-redirect keeps firing but drops its server clause, and no-prevent-default falls back to the framework-neutral <form> message.

Refactors buildCapabilities into one declarative capability table (the single source of truth across all frameworks) consumed via a runtime `capabilities` channel, replacing rules' hardcoded framework Sets. Consolidates project-info/ from 47 tiny files into 15 cohesive modules. Behavior-neutral; the public export surface is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Jun 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: a3f218b

Comment thread packages/core/src/project-info/discover-project.ts Outdated

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@rayhanadev rayhanadev changed the title fix(capabilities): gate Next.js static-export server-only advice (#976) refactor(capabilities): unify framework gating into one capability vocabulary (#976) Jun 27, 2026
rayhanadev and others added 9 commits June 26, 2026 23:47
Shorter name for the runtime capability accessor. Its doc comment now defines the whole vocabulary in one place: build-time gating (`requires` / `disabledBy` via `shouldEnableRule`) versus the runtime read (`hasCapability`).

The `disabledBy` -> `disabledWhen` rename is left as a follow-up; it touches ~80 rule files plus the registry codegen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consistent vocabulary with `hasCapability`: `disabledWhen: ["react-compiler"]` reads "disabled when react-compiler is present." Pure rename of the rule-metadata field and the `shouldEnableRule` parameter across 21 files; behavior is unchanged (net 0 LOC). Historical CHANGELOG entries keep the old name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ned union

`requires` / `disabledWhen` / `hasCapability` were stringly typed, so a
misspelled token ("sever-actions") was a silently never-matching gate.
The vocabulary now lives in one typed `Capability` union in the plugin
(core imports the plugin, never the reverse), and core's `Framework`
aliases the plugin's `FrameworkToken` so the two unions cannot drift.
Registry codegen emits `new Set<Capability>(...)` so the merged
`requires` arrays keep their literal types through the spread.

Also adds the (not yet consumed) `recommendationFor` hook: a
capability-conditioned override of `recommendation`, evaluated later by
core's diagnostic pipeline — groundwork for moving rule prose out of
parse-output.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oized, straight-line

`buildCapabilities` is a pure projection over `ProjectInfo`, consumed by
the oxlint config, the security scan, and (next commit) the diagnostic
pipeline — it never belonged to the oxlint runner. It now lives in
`project-info/`, and `getCapabilities` memoizes one set per `ProjectInfo`
identity (discoverProject caches one instance per directory, so every
consumer shares a single computation).

The `CapabilityRule` closure table (three mutually-exclusive row shapes +
interpreter loop) is replaced by straight-line adds — the rows were
closures, so the table was never data; the ifs read the same, typecheck
against `Capability`, and drop ~50 lines of mechanism. One helper remains
for the react/preact major ladders.

New `client-only` trait row: the SPA / mobile framework list moves out of
`no-prevent-default` (consumed there in the next commit) into the one
projection, so rules ask a capability question instead of keeping their
own framework sets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…special cases

`parse-output.ts` carried a rule-name string match rewriting
nextjs-no-client-side-redirect's advice under static export (reading
`project.isStaticExport` directly, bypassing the capability vocabulary)
plus a second special case for no-secrets-in-client-code's per-framework
env-prefix advice. Both move into the rules themselves via the
`recommendationFor(hasCapability)` hook; `getRuleRecommendation`
collapses to one generic dispatch and core's
build-no-secrets-recommendation.ts / get-public-env-prefix.ts are
deleted. Core now carries zero rule-specific prose.

`no-prevent-default` finishes its capability conversion: the hardcoded
CLIENT_ONLY_FRAMEWORKS set (kept alongside the hasCapability check in
the original conversion) becomes the `client-only` capability, checked
at runtime so the `<a onClick preventDefault>` variant still fires on
SPA frameworks. The framework list now lives only in buildCapabilities.

All 75 regression pins (no-prevent-default framework matrix, #976
static-export e2e, no-secrets prose, rule-messages) pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full-ProjectInfo snapshots over a workspace fixture matrix (pnpm named
catalogs, yarn-workspaces web monorepo with an RN workspace, nx, leaf
scan through monorepo catalogs, lowest-react-major merge, overlapping
globs, the tailwind piggyback quirk). Every fixture is traversal-order-
independent, so these pins must hold bit-for-bit across the legacy
multi-walk discovery and the single-pass rewrite that follows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`discoverProject` ran up to ~7 separate workspace traversals per project
— react/tailwind/zod/framework, RN awareness, reanimated, expo,
flash-list, and next each re-resolved the same globs and re-visited the
same manifests, and the react walk used raw readdir order
(nondeterministic) while the others sorted. `collectWorkspaceFacts` now
enumerates the workspace directories once (pattern order, sorted within
each pattern, deduped across overlapping globs) and evaluates every
workspace-derived fact per manifest, recording the source directory that
supplied each dependency signal.

Semantics preserved exactly: the stage-D react group still fills only
when the root leaves react/framework unresolved (the tailwind/zod
piggyback quirk), settles on the legacy early-exit condition (react ≤ 17
+ tailwind + framework), and skips its catalog work entirely when the
gate is closed; expoVersion/nextjsVersion/hasReanimated keep their
semantic nulls; the leaf-vs-root fallback asymmetry in
findDependencyInfoFromMonorepoRoot is unchanged (and now documented).
The one intended change: framework/react workspace precedence is now
sorted-deterministic instead of readdir-order.

The full discovery spec (~100 cases), the new characterization pins, and
all 2,022 react-doctor e2e tests pass with zero assertion edits.
Deletes per-dependency-version.ts and the five per-fact walkers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The static-export probe read `next.config.*` at the scan root only, while
the `nextjs` framework signal can come from a workspace — so a monorepo
whose `apps/web` sets `output: "export"` kept the `server-actions`
capability and the impossible server advice #976 is about (the deferred
Bugbot finding on this PR). The probe now reads the config next to the
manifest that supplied the `next` dependency signal, riding the source
directory the single-pass collector already records: the scan root when
it declares `next` itself (unchanged), otherwise the first workspace in
walk order that does — the same first-match attribution `nextjsVersion`
uses, pinned for the multi-Next-workspace case.

Covered at both levels: discovery tests (root config, workspace config,
two-apps first-match, no-export), and a no-override e2e in the #976
regression suite proving a monorepo-root scan now softens the redirect
advice.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce merge

Validated the sorted-order determinism change against the 863-repo RDE
corpus by diffing discoverProject output pre/post rewrite. Plain
first-in-sorted-order let an `apps/mobile` Expo workspace claim the
framework slot from an `apps/web` Next.js app purely because "mobile"
sorts first — silently dropping all 23 `requires: ["nextjs"]` rules on
web-primary monorepos (linkwarden, t4-app, taro in the corpus).

The merge is now two-tier: web frameworks outrank expo/react-native,
walk order only breaks ties within a tier — the same web-over-mobile
priority detectFramework already applies within one manifest, and
strictly coverage-preserving (`rn-*` / Expo rules ride
hasReactNativeWorkspace / expoVersion, not the framework slot; mobile
and web SPA frameworks carry `client-only` alike). A full-priority rank
across all workspaces was measured too and rejected: it reclassified 34
corpus repos (vite apps flipping to their Next.js docs-site workspace).

Final corpus blast radius: 23/863 repos (2.7%) — no repo loses RN/Expo
coverage, several web-primary monorepos regain their Next.js rules, the
two isStaticExport flips are real workspace static exports (dual-mode
conditional configs, treated as export-capable by design), and the rest
is deterministic version re-attribution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 1, 2026 23:06

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 using default effort and found 2 potential issues.

Fix All in Cursor

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

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a3b51be. Configure here.

Comment thread packages/core/src/project-info/collect-project-facts.ts
Comment thread packages/core/src/project-info/detectors.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes Next.js output: "export" (static export) false-positive/unactionable advice by replacing per-rule, framework-name-only gating with a single shared, typed “capability” vocabulary produced from ProjectInfo. Rules can now (a) fully disable via requires / disabledWhen and (b) tailor their recommendation text via recommendationFor(hasCapability) when the project’s capability set is known.

Changes:

  • Added typed Capability / FrameworkToken vocabulary and plumbed a sorted capabilities array through oxlint settings for consistent gating and caching.
  • Added Next.js static export detection (next.config.*isStaticExport) and capability projection (nextjs:static-export), updating rule behavior accordingly.
  • Refactored project discovery to a single workspace traversal and consolidated many project-info/ helpers (version parsing, workspace scanning, RN metadata).

Reviewed changes

Copilot reviewed 106 out of 106 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/react-doctor/tests/regressions/scan-resilience.test.ts Updates gating field name in comments
packages/react-doctor/tests/regressions/nextjs-static-export.test.ts New E2E regression coverage for #976
packages/react-doctor/tests/regressions/_helpers.ts Adds isStaticExport test-project knob
packages/oxlint-plugin-react-doctor/src/react-native-dependency-names.ts Updates comment path after refactor
packages/oxlint-plugin-react-doctor/src/plugin/utils/rule.ts Types requires/disabledWhen; adds recommendationFor
packages/oxlint-plugin-react-doctor/src/plugin/utils/get-react-doctor-setting.ts Adds hasCapability helper
packages/oxlint-plugin-react-doctor/src/plugin/utils/capability.ts Defines typed capability vocabulary
packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/redux-useselector-returns-new-collection.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/redux-useselector-inline-derivation.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/server/server-fetch-without-revalidate.ts Disables on nextjs:static-export; renames field
packages/oxlint-plugin-react-doctor/src/plugin/rules/security/no-secrets-in-client-code.ts Adds capability-conditioned recommendation text
packages/oxlint-plugin-react-doctor/src/plugin/rules/react-native/rn-no-inline-object-in-list-item.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/react-native/rn-no-inline-flatlist-renderitem.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/react-native/rn-list-callback-per-row.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/performance/prefer-stable-empty-fallback.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/nextjs/nextjs-no-client-side-redirect.ts Softens recommendation under nextjs:static-export
packages/oxlint-plugin-react-doctor/src/plugin/rules/js-performance/js-tosorted-immutable.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/plugin/rules/correctness/no-prevent-default.ts Switches from framework lists to capabilities
packages/oxlint-plugin-react-doctor/src/plugin/rules/architecture/prefer-module-scope-static-value.ts Renames disabledBydisabledWhen
packages/oxlint-plugin-react-doctor/src/index.ts Exposes capability vocabulary from package
packages/oxlint-plugin-react-doctor/scripts/generate-rule-registry.mjs Emits typed Capability in registry output
packages/core/tests/parse-zod-major.test.ts Removes superseded parser test
packages/core/tests/parse-tailwind-major-minor.test.ts Removes superseded helper tests
packages/core/tests/parse-react-major-minor.test.ts Removes superseded helper tests
packages/core/tests/major-minor.test.ts Adds shared version-compare tests
packages/core/tests/discover-project.test.ts Adds isStaticExport detection tests
packages/core/tests/discover-project-characterization.test.ts New characterization pins for discovery rewrite
packages/core/tests/detect-pre-es2023-target.test.ts Updates imports after refactor
packages/core/tests/detect-nextjs-static-export.test.ts New detector regression coverage
packages/core/tests/build-capabilities.test.ts Expands capability projection + memoization tests
packages/core/src/utils/is-project-boundary.ts Updates monorepo-root import path
packages/core/src/utils/get-public-env-prefix.ts Removes helper (moved to rule)
packages/core/src/utils/classify-package-role.ts Updates comment path after refactor
packages/core/src/utils/build-no-secrets-recommendation.ts Removes helper (moved to rule)
packages/core/src/types/project-info.ts Adds isStaticExport; Framework type alias
packages/core/src/types/index.ts Updates comment path after refactor
packages/core/src/services/git.ts Updates fs-utils import path
packages/core/src/runners/oxlint/parse-output.ts Uses capabilities + recommendationFor for help
packages/core/src/runners/oxlint/config.ts Uses getCapabilities; serializes capabilities setting
packages/core/src/runners/oxlint/capabilities.ts Removes old capabilities implementation
packages/core/src/project-info/workspaces.ts New consolidated workspace helpers
packages/core/src/project-info/version.ts New consolidated version parsing utilities
packages/core/src/project-info/utils/is-plain-object.ts Moved into fs-utils
packages/core/src/project-info/utils/is-package-json-reanimated-aware.ts Moved into rn-metadata
packages/core/src/project-info/utils/is-package-json-react-native-aware.ts Moved into rn-metadata
packages/core/src/project-info/utils/is-file.ts Moved into fs-utils
packages/core/src/project-info/utils/is-directory.ts Moved into fs-utils
packages/core/src/project-info/utils/get-dependency-spec.ts Moved into dependencies
packages/core/src/project-info/utils/get-dependency-declaration.ts Moved into dependencies
packages/core/src/project-info/utils/dependency-version-spec.ts Moved into version
packages/core/src/project-info/some-workspace-package-json.ts Removed (covered by collect pass)
packages/core/src/project-info/rn-metadata.ts New consolidated RN/expo/reanimated metadata helpers
packages/core/src/project-info/resolve-workspace-directories.ts Removed (moved to workspaces)
packages/core/src/project-info/resolve-effective-react-major.ts Removed (moved to version)
packages/core/src/project-info/resolve-catalog-backed-dependency-version.ts Removed (moved to dependencies)
packages/core/src/project-info/parse-zod-major.ts Removed (inlined via getLowestDependencyMajor)
packages/core/src/project-info/parse-tailwind-major-minor.ts Removed (moved to version)
packages/core/src/project-info/parse-react-peer-range.ts Removed (moved to version)
packages/core/src/project-info/parse-react-major.ts Removed (moved to version)
packages/core/src/project-info/parse-react-major-minor.ts Removed (moved to version)
packages/core/src/project-info/parse-pnpm-workspace-patterns.ts Removed (moved to workspaces)
packages/core/src/project-info/package-json.ts Adds cycle-breaking comment; renamed module
packages/core/src/project-info/monorepo-root.ts Renamed module; updates imports
packages/core/src/project-info/list-workspace-packages.ts Removed (moved to workspaces)
packages/core/src/project-info/internal-rn-dependency-names.ts Removed (moved to rn-metadata)
packages/core/src/project-info/index.ts Re-exports reorganized helpers
packages/core/src/project-info/has-tanstack-query.ts Removed (moved to dependencies)
packages/core/src/project-info/has-react-native-workspace-anywhere.ts Removed (folded into collect pass)
packages/core/src/project-info/has-react-dependency.ts Removed (moved to dependencies)
packages/core/src/project-info/get-workspace-patterns.ts Removed (moved to workspaces)
packages/core/src/project-info/get-preact-version.ts Removed (moved to dependencies)
packages/core/src/project-info/get-nx-workspace-directories.ts Removed (moved to workspaces)
packages/core/src/project-info/fs-utils.ts Consolidates fs helpers and plain-object check
packages/core/src/project-info/find-shopify-flash-list-version.ts Removed (folded into collect pass)
packages/core/src/project-info/find-react-in-workspaces.ts Removed (folded into collect pass)
packages/core/src/project-info/find-nextjs-version.ts Removed (folded into collect pass)
packages/core/src/project-info/find-in-workspace-package-jsons.ts Removed (folded into collect pass)
packages/core/src/project-info/find-expo-version.ts Removed (folded into collect pass)
packages/core/src/project-info/find-dependency-info-from-monorepo-root.ts Removed (replaced by collect-project-facts)
packages/core/src/project-info/extract-dependency-info.ts Removed (moved into dependencies)
packages/core/src/project-info/errors.ts Makes options interface file-local
packages/core/src/project-info/discover-react-subprojects.ts Switches to consolidated workspace utils
packages/core/src/project-info/discover-project.ts Single-pass workspace facts + static export detection
packages/core/src/project-info/detectors.ts Consolidates framework/compiler/static-export/ES2023 detection
packages/core/src/project-info/detect-react-compiler.ts Removed (moved to detectors)
packages/core/src/project-info/detect-pre-es2023-target.ts Removed (moved to detectors)
packages/core/src/project-info/detect-framework.ts Removed (moved to detectors)
packages/core/src/project-info/dependencies.ts Consolidates dependency extraction + helpers
packages/core/src/project-info/count-source-files.ts Updates fs-utils import
packages/core/src/project-info/collect-project-facts.ts New single traversal of workspace-derived facts
packages/core/src/project-info/capabilities.ts New single source of truth for capability projection
packages/core/src/index.ts Re-exports capabilities from new location
packages/core/src/checks/security-scan/collect-security-scan-files.ts Updates fs-utils import
packages/core/src/checks/expo/utils/is-expo-sdk-at-least.ts Updates comment reference
packages/core/src/checks/expo/utils/find-local-module-native-files.ts Updates fs-utils import
packages/core/src/checks/expo/expo-check-context.ts Uses new version utility path
packages/core/src/checks/expo/check-reanimated-new-arch.ts Uses new version utility path
packages/core/src/check-security-scan.ts Uses getCapabilities + disabledWhen
packages/core/src/check-react-server-components-advisory.ts Uses consolidated workspace helpers
.changeset/nextjs-static-export-capability.md Changeset for published packages

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +84 to +98
const wildcardIndex = cleanPattern.indexOf("*");
const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));
const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);

if (!isDirectory(baseDirectory)) {
return [];
}

const resolved: string[] = [];
for (const entry of readDirectoryEntries(baseDirectory)) {
const entryPath = path.join(baseDirectory, entry.name, suffixAfterWildcard);
if (isDirectory(entryPath) && isFile(path.join(entryPath, "package.json"))) {
resolved.push(entryPath);
}
}
Comment on lines 94 to +96
const getRuleRecommendation = (ruleName: string, project: ProjectInfo): string | undefined => {
if (ruleName === "no-secrets-in-client-code") {
return buildNoSecretsRecommendation(
project,
reactDoctorPlugin.rules["no-secrets-in-client-code"]?.recommendation ??
"Move secrets to server-only code",
const rule = reactDoctorPlugin.rules[ruleName];
if (!rule) return undefined;
Comment on lines +26 to +50
export type Capability =
// The bare framework name, including "unknown" — `buildCapabilities`
// emits `project.framework` unconditionally (the token feeds the
// ruleset cache key, so even "unknown" is load-bearing).
| FrameworkToken
| "react"
| "pure-preact"
| "react-native"
| "server-actions"
| "client-only"
| "nextjs:static-export"
| "nextjs:15"
| "tailwind"
| "tailwind:3.4"
| "zod"
| "zod:4"
| "typescript"
| "react-compiler"
| "tanstack-query"
| "pre-es2023"
// Major-version ladders (`react:17`…) plus minor-versioned gates like
// `react:19.2` — both parse as numeric template members. Bounds live in
// core's constants (`EARLIEST_GATED_*` / `LATEST_KNOWN_*`).
| `react:${number}`
| `preact:${number}`;
…ne ladder

discoverProject hand-threaded react/tailwind/zod through every resolution
stage as three copies of the same code (declaration probe, root-catalog
fill, monorepo-root-catalog fill, raw-spec fallback). The ladder is now
written once over a small tracked-dependency record; the shared section-
order constants move to dependencies.ts. Corpus probe diff vs the
previous commit: zero — pure consolidation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants