Skip to content

feat(eslint-plugin-next): Add initial @clerk/eslint-plugin-next package and rule#8704

Open
Ephem wants to merge 9 commits into
mainfrom
fredrik/add-experimental-next-lint-rule
Open

feat(eslint-plugin-next): Add initial @clerk/eslint-plugin-next package and rule#8704
Ephem wants to merge 9 commits into
mainfrom
fredrik/add-experimental-next-lint-rule

Conversation

@Ephem
Copy link
Copy Markdown
Member

@Ephem Ephem commented May 29, 2026

Description

Adds @clerk/eslint-plugin-next, an ESLint plugin for the Next app router. Adds a first rule, require-auth-protection that enforces auth protections at the page/route/server action level.

The rule flags any page, layout, template, default, route, or Server Action under folders configured as protected that doesn't guard itself with await auth.protect() (or an equivalent early-exit auth() check).

What's included

  • New package packages/eslint-plugin-next (dual CJS/ESM, type-only deps, eslint >=9 peer, ships at 0.0.00.1.0)
  • require-auth-protection rule with protected (required), public, and mixedScopeLayouts options
  • Full test suite
  • CI setup

Config

import clerkNext from '@clerk/eslint-plugin-next';

export default [
  {
    plugins: { '@clerk/next': clerkNext },
    rules: {
      '@clerk/next/require-auth-protection': [
        'error',
        {
          protected: ['app/**'],
          public: ['app/sign-in/**', 'app/sign-up/**'],
        },
      ],
    },
  },
];

Also see README.

Errors

  • missingProtect:
    • 'Expected await auth.protect() at the top of {{subject}} in a folder configured as protected. Add the call to the top of the function, move the file into a public folder, or configure this folder as public.',
  • exportImported:
    • "This {{subject}} is exported from '{{source}}'. The rule cannot follow imports across files. Add a wrapper with await auth.protect(), or ensure the imported function calls it and add an eslint-disable comment with a reason.",
  • unverifiableExport:
    • 'This {{subject}} could not be verified as being protected, likely because it is assigned from a call expression (e.g. const handler = withAuth(impl)). Inline a function literal that calls await auth.protect(), or add an eslint-disable comment with a reason.',
  • unlistedMixedScopeLayout (only if an explicit mixedScopeLayouts was provided in config):
    • "This {{fileKind}} at '{{folder}}/' wraps both protected and public descendants but is not listed in mixedScopeLayouts. Either add '{{folder}}' to the list to acknowledge the mixed scope, or restructure so the {{fileKind}} wraps only public or protected descendants.",

Notes

  • Verified against our internal dashboard repo
  • Other tooling is upcoming
  • Before merging and releasing, we need to double check first time publish via OIDC will work

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced @clerk/eslint-plugin-next ESLint plugin for Next.js App Router
    • Added require-auth-protection rule to enforce authentication protection in pages, routes, layouts, and server actions
    • Plugin supports configurable folder patterns and validates proper authentication guard placement

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: fd1048d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/eslint-plugin-next Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
clerk-js-sandbox Skipped Skipped Jun 8, 2026 12:03pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 29c31cbf-0eca-4f28-bd21-a27a1821953d

📥 Commits

Reviewing files that changed from the base of the PR and between e0d497e and fd1048d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • packages/eslint-plugin-next/LICENSE
  • packages/eslint-plugin-next/package.json
  • packages/eslint-plugin-next/src/rules/require-auth-protection.ts
  • packages/eslint-plugin-next/tsdown.config.mts
✅ Files skipped from review due to trivial changes (2)
  • packages/eslint-plugin-next/tsdown.config.mts
  • packages/eslint-plugin-next/LICENSE
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/eslint-plugin-next/package.json
  • packages/eslint-plugin-next/src/rules/require-auth-protection.ts

📝 Walkthrough

Walkthrough

This PR introduces @clerk/eslint-plugin-next, a new ESLint plugin package that enforces auth protection in Next.js App Router code. It provides a require-auth-protection rule that verifies pages, routes, layouts, and server actions are protected via auth.protect() or equivalent guards, with glob-based folder classification and comprehensive AST analysis.

Changes

Auth Protection ESLint Plugin

Layer / File(s) Summary
Package Configuration and Build Setup
packages/eslint-plugin-next/package.json, .changeset/eslint-plugin-next-initial.md, .github/labeler.yml, packages/eslint-plugin-next/tsconfig.json, packages/eslint-plugin-next/tsdown.config.mts, packages/eslint-plugin-next/vitest.config.mts, packages/eslint-plugin-next/vitest.setup.mts, packages/eslint-plugin-next/src/global.d.ts, packages/eslint-plugin-next/LICENSE
npm package metadata, exports wiring (ESM/CJS with types), scripts for build/test/lint, peer dependency on ESLint >= 9, Vitest configuration with setup file injection of PACKAGE_VERSION, TypeScript globals declaration, and MIT license.
File Kind Classification and Module Directives
packages/eslint-plugin-next/src/lib/file-info.ts, packages/eslint-plugin-next/src/__tests__/file-info.test.ts
FileKind type, getRelativeFolder() normalizing paths to the first app segment, getFileKind() mapping filenames to Next.js resource kinds, and isServerActionModule()/isClientModule() detecting module directives. Tests validate path normalization, cwd relativization, and edge cases with undefined/empty inputs.
Glob Pattern Matching and Folder Classification
packages/eslint-plugin-next/src/lib/match-folders.ts, packages/eslint-plugin-next/src/__tests__/match-folders.test.ts
matchPath() supporting literal segments, * (single-segment), and ** (multi-segment) wildcards; specificity() counting non-wildcard segments; literalPrefix() extracting the prefix before wildcards; hasDescendantsMatching() checking descendant relationships; and classifyFolder() resolving protected vs public scope using highest specificity. Tests cover literal matching, wildcard combinations, specificity tie-breaking, and edge cases with fully-wildcard and nested patterns.
Export Resolution from AST
packages/eslint-plugin-next/src/lib/exports.ts
FunctionNode and ExportTarget types representing function exports or imports; unwrapFunction() extracting function nodes; resolveLocalIdentifierTarget() resolving local function declarations and variable initializers; resolveDefaultExportTarget() handling default exports; and iterateNamedExports() generator yielding named exports with skipping of type-only statements.
Auth Protection Detection at Function Entry
packages/eslint-plugin-next/src/lib/protection-checks.ts
findAuthLocalNames() extracting local names of imported auth from @clerk/nextjs/server; detection of auth.protect() calls in expression or awaited forms; captured-binding pattern matching (const { userId, isAuthenticated } = await auth() plus conditional check); exit-function recognition (redirect, permanentRedirect, notFound, unauthorized, forbidden); and hasProtectAtTop() verifying top-level protection in async functions while skipping non-runtime statements like directives and TS-only declarations.
ESLint Plugin Entry and Rule Implementation
packages/eslint-plugin-next/src/index.ts, packages/eslint-plugin-next/src/rules/require-auth-protection.ts
Plugin registration with rule name and version metadata; rule schema validation for protected folders (required), optional public folders, and mixedScopeLayouts mode; create() function classifying the file's folder and dispatching checks for pages/layouts/templates (default export), routes (named HTTP handlers), and server actions (named exports); error reporting for missing protection, unverifiable imports, and unacknowledged mixed-scope layouts.
Require Auth Protection Rule Behavior Tests
packages/eslint-plugin-next/src/__tests__/require-auth-protection.test.ts
Test matrix covering valid cases (correct auth.protect() placement, skipping for 'use client' modules, mixed-scope allowlists) and invalid cases (missing protection, non-top-level placement, unsupported guard patterns, unverifiable re-exports); schema validation for configuration errors; and edge cases with intercepting routes and HOF-wrapped exports.
User Documentation
packages/eslint-plugin-next/README.md
Plugin purpose, ESLint >= 9 installation requirements, eslint.config.mjs configuration example, glob-pattern semantics for protected/public scopes, recognized auth-check patterns, client-component skipping behavior, and license/contributing information.

Sequence Diagram(s)

sequenceDiagram
  participant ESLint
  participant RequireAuthRule
  participant FileInfo
  participant MatchFolders
  participant Exports
  participant ProtectionChecks
  ESLint->>RequireAuthRule: visit program node
  RequireAuthRule->>FileInfo: getRelativeFolder, getFileKind, isClientModule
  RequireAuthRule->>MatchFolders: classifyFolder
  alt Protected Folder
    RequireAuthRule->>Exports: resolveDefaultExportTarget or iterateNamedExports
    Exports-->>RequireAuthRule: export target (function or imported)
    RequireAuthRule->>ProtectionChecks: hasProtectAtTop, findAuthLocalNames
    ProtectionChecks-->>RequireAuthRule: boolean protection status
  end
  RequireAuthRule-->>ESLint: report violation or pass
Loading
sequenceDiagram
  participant Rule
  participant ProtectionChecks
  participant FunctionNode
  Rule->>ProtectionChecks: hasProtectAtTop(fn, authNames)
  ProtectionChecks->>FunctionNode: find first executable statement
  alt Top-level auth.protect() call
    FunctionNode-->>ProtectionChecks: returns true
  else await auth() destructuring + guard
    ProtectionChecks->>FunctionNode: extract captured auth fields
    ProtectionChecks->>FunctionNode: recognize auth-check condition
    ProtectionChecks->>FunctionNode: verify guard consequent exits
    FunctionNode-->>ProtectionChecks: returns true if exits via return/throw/redirect
  else No recognized pattern
    FunctionNode-->>ProtectionChecks: returns false
  end
  ProtectionChecks-->>Rule: boolean protection status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A plugin hops in, neat and sleek,
Auth guards page and route with a peek,
Glob patterns match, AST reads deep,
Next.js App Router secrets to keep! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: introducing the initial @clerk/eslint-plugin-next package with its first rule.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/eslint-plugin-next/README.md`:
- Line 5: The <img> tag in the README is missing an alt attribute; add an
appropriate alt attribute to the image element (e.g., alt="Clerk logo" or a more
descriptive string) so screen readers can convey the image content—if the image
is decorative, use alt="" to mark it as decorative; update the <img
src="/asset?url=https%3A%2F%2Fimages.clerk.com%2Fstatic%2Flogo-light-mode-400x400.png" height="64">
element accordingly.

In `@packages/eslint-plugin-next/src/lib/protection-checks.ts`:
- Around line 96-133: The current logic in capturedAuthBindings wrongly treats
multi-declarator statements like `const {userId} = await auth(), side =
doWork()` as safe; update the guard to require a single declarator by checking
that stmt.declarations.length === 1 and returning null if not, so only
statements with exactly one declarator (the destructuring await) are considered;
keep the existing checks (decl.id/ObjectPattern, decl.init/AwaitExpression, arg
CallExpression, callee in authNames, and the AUTH_FIELDS/property identity
checks) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 7ac258ec-bf2d-4658-bdc3-ee5333e132e6

📥 Commits

Reviewing files that changed from the base of the PR and between 1c42351 and 63c2aa4.

⛔ Files ignored due to path filters (2)
  • packages/eslint-plugin-next/src/__tests__/__snapshots__/plugin-shape.test.ts.snap is excluded by !**/*.snap
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • .changeset/eslint-plugin-next-initial.md
  • .github/labeler.yml
  • packages/eslint-plugin-next/README.md
  • packages/eslint-plugin-next/package.json
  • packages/eslint-plugin-next/src/__tests__/file-info.test.ts
  • packages/eslint-plugin-next/src/__tests__/match-folders.test.ts
  • packages/eslint-plugin-next/src/__tests__/plugin-shape.test.ts
  • packages/eslint-plugin-next/src/__tests__/require-auth-protection.test.ts
  • packages/eslint-plugin-next/src/global.d.ts
  • packages/eslint-plugin-next/src/index.ts
  • packages/eslint-plugin-next/src/lib/exports.ts
  • packages/eslint-plugin-next/src/lib/file-info.ts
  • packages/eslint-plugin-next/src/lib/match-folders.ts
  • packages/eslint-plugin-next/src/lib/protection-checks.ts
  • packages/eslint-plugin-next/src/rules/require-auth-protection.ts
  • packages/eslint-plugin-next/tsconfig.json
  • packages/eslint-plugin-next/tsup.config.ts
  • packages/eslint-plugin-next/vitest.config.mts
  • packages/eslint-plugin-next/vitest.setup.mts

Comment thread packages/eslint-plugin-next/README.md
Comment thread packages/eslint-plugin-next/src/lib/protection-checks.ts Outdated
@Ephem Ephem changed the title Add initial @clerk/eslint-plugin-next package and rule feat(eslint-plugin-next): Add initial @clerk/eslint-plugin-next package and rule May 29, 2026
@@ -0,0 +1,9 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Now that we have break-check, these snapshot tests are duplicative


This project is licensed under the **MIT license**.

See [LICENSE](https://github.com/clerk/javascript/blob/main/packages/eslint-plugin-next/LICENSE) for more information.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't see a LICENSE file in this PR

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