Skip to content

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

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

refactor(capabilities): unify framework gating into one capability vocabulary (#976)#978
rayhanadev wants to merge 3 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 still 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: each rule had its own hardcoded list of frameworks, and at runtime it could only see the framework name. So a rule couldn't tell that this nextjs was a static export with no server. Now every rule reads one shared list of capabilities instead.

Before: every rule guesses on its own, from just the framework name.

flowchart TD
  PI["ProjectInfo: the facts about a project"]
  RULE["each rule keeps its own list of frameworks,<br/>and only sees the name 'nextjs'"]
  BAD["it can't tell this is a static export,<br/>so it suggests server fixes that don't work here"]
  PI --> RULE --> BAD
Loading

After: one shared list, and one question decides what each rule does.

flowchart TD
  PI["ProjectInfo: the facts about a project"]
  BC["buildCapabilities: one shared list of what<br/>the project can do (nextjs, server-actions,<br/>nextjs:static-export, ...)"]
  Q{"Does the rule still have<br/>something true to say here?"}
  OFF["No: turn the rule off<br/>(requires / disabledWhen)"]
  ON["Yes, but the advice changes:<br/>keep it on, pick the right message<br/>(hasCapability)"]
  SF["server-fetch-without-revalidate:<br/>no server means nothing to warn about,<br/>so it stays off"]
  NPD["no-prevent-default:<br/>the form still breaks without JS, so it warns,<br/>but doesn't suggest a Server Action"]
  PI --> BC --> Q
  Q -->|no| OFF --> SF
  Q -->|yes| ON --> NPD
Loading

What changed

  • One capability table: buildCapabilities is now a short list of rules instead of a 115-line if/else block. It's the single place that says what each framework supports.
  • Rules read capabilities at runtime: they look one up with hasCapability(...). no-prevent-default now checks for server-actions instead of keeping its own framework list.
  • The [False Positive] Next.js rules recommend server-only fixes under output: "export" (static export) #976 fix is one new capability: a static export gets nextjs:static-export and loses server-actions. So server-fetch-without-revalidate turns off, nextjs-no-client-side-redirect still warns but without the server advice, and no-prevent-default shows the plain <form> message.
  • Fewer files: project-info/ dropped from 47 files to 15. The merges are renames, so behavior and the public index.ts exports stay the same. The change is 11 lines shorter overall.

Test plan

  • pnpm typecheck && pnpm test && pnpm lint && pnpm smoke:json-report all pass. The full suite is 2,022 tests, run fresh with no cache.
  • New tests cover the detector, the capability set, and an end-to-end check that a static export softens the advice while a normal Next.js project keeps it.
  • I didn't run RDE. This change only turns warnings off or softens their wording for static exports, so it can't add new findings, and an open-source sweep wouldn't show much.

For later, out of scope here: mirror the no-prevent-default change in the ESLint plugin, and teach the static-export and React Compiler detectors to look in workspace folders (the Bugbot note).

🤖 Generated with Claude Code


Note

Medium Risk
Touches project discovery, rule enablement, and diagnostic text for all Next.js projects; wrong static-export detection could hide valid server rules or show bad advice, though defaults favor keeping server-aware behavior when config is missing.

Overview
Fixes #976 for Next.js apps with output: "export": discovery sets isStaticExport from next.config.*, and capability building adds nextjs:static-export while withholding server-actions. Rules no longer push impossible server fixes—server-fetch-without-revalidate is off, redirect advice is softened in core, and no-prevent-default uses hasCapability("server-actions") instead of a hardcoded framework list.

Capability model: buildCapabilities is refactored to a declarative FRAMEWORK_CAPABILITIES table; the sorted capability set is passed into oxlint settings for runtime checks. Rule metadata disabledBy is renamed to disabledWhen across lint and security-scan gating.

project-info cleanup: many small modules are consolidated into dependencies.ts, detectors.ts, workspaces.ts, version.ts, fs-utils.ts, and rn-metadata.ts with import path updates only (behavior preserved). Version helpers unify under isMajorMinorAtLeast with tests split accordingly.

Reviewed by Cursor Bugbot for commit 81c9bfe. 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: 81c9bfe

@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 1 potential issue.

Fix All in Cursor

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

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

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

@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 2 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>
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.

1 participant