Skip to content

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

Open
Ephem wants to merge 18 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 18 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

  • New Features

    • Added @clerk/eslint-plugin-next with an experimental require-auth-protection rule for Next.js App Router to enforce auth guards on pages, routes, layouts, and server functions.
  • Documentation

    • Added README describing installation, configuration options, recognized auth checks, and usage examples.
  • Tests

    • Comprehensive test suites covering folder classification, pattern matching, protection detection, rule behavior, and schema validation.
  • Chores

    • Package metadata, build/test configs, license, and CI labeler updates for publishing.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 29, 2026

🦋 Changeset detected

Latest commit: 9e6737a

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 3:42pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new package @clerk/eslint-plugin-next implementing an ESLint plugin with a single rule require-auth-protection that classifies App Router folders (protected/public) via globs, resolves exported handlers, and enforces top-of-function auth guards with comprehensive tests and packaging/build configs.

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 manifest and exports wiring (ESM/CJS + types), changelog changeset, GitHub labeler entry, TypeScript config, tsdown build config, Vitest setup and config, a test-time PACKAGE_VERSION global, 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
Utilities to normalize paths to the first app segment, derive Next.js resource kinds (page/layout/template/default/route), and detect use server/use client directives. Tests validate path/cwd/edge-case behaviors.
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
Glob matcher supporting literal segments, * (single segment) and ** (multi-segment), specificity scoring, literal-prefix extraction, descendant detection, and classification into protected/public/unmatched, with tests exercising wildcard combos and tie-breaking.
Export Resolution from AST
packages/eslint-plugin-next/src/lib/exports.ts
AST helpers and types to unwrap function nodes, resolve local identifiers to function/import targets, resolve default exports, and iterate named/export-all declarations while skipping type-only exports.
Auth Protection Detection at Function Entry
packages/eslint-plugin-next/src/lib/protection-checks.ts
Detection of local auth import names, recognition of auth.protect() (direct or awaited) and captured-destructure + guard patterns, exit-action recognition (redirect, notFound, etc.), and hasProtectAtTop() for async functions with non-runtime statement skipping.
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 exporting a typed ESLint plugin, rule option schema (required protected globs), folder classification, export-target verification for default/named/export * handlers, inline server-function scanning, and message reporting for missing/unverifiable/imported exports and mixed-scope layouts.
Require Auth Protection Rule Behavior Tests
packages/eslint-plugin-next/src/__tests__/require-auth-protection.test.ts
Extensive RuleTester-based valid and invalid matrices covering correct/incorrect protection patterns, export-resolution edge cases, mixed-scope layout behavior, inline server functions, intercepting routes, and schema validation for rule options.
User Documentation
packages/eslint-plugin-next/README.md
README describing plugin purpose, installation (ESLint >= 9 flat config), configuration example, rule options and glob semantics, recognized auth-check patterns, client skipping behavior, and contributing/security/license notes.

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

🐰 I hopped through folders, globs in paw,
AST nibbles caught what guards might miss,
Pages, routes, and server calls I saw,
A tidy rule to keep auth bliss. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.41% 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 accurately and concisely describes the main change: introducing a new ESLint plugin package for Next.js with an initial auth protection 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.


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="https://images.clerk.com/static/logo-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
Comment thread packages/eslint-plugin-next/src/__tests__/__snapshots__/plugin-shape.test.ts.snap Outdated
Comment thread packages/eslint-plugin-next/README.md
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: 1

🤖 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/src/rules/require-auth-protection.ts`:
- Around line 241-253: checkServerFunctions currently only checks
iterateNamedExports and export * and misses default-exported Server Actions; add
a loop to also iterate default export declarations (e.g.,
iterateDefaultExportDeclarations or similar helper) over programNode and call
checkMissingProtect(context, reportNode, target, `Server Function 'default'` or
`Default Server Function`, authNames, checkedFunctions) for each default export
so module-level 'use server' default async functions are validated the same as
named exports; ensure the new loop uses the same target shape
(kind/imported/source) and passes checkedFunctions where applicable.
🪄 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), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 231469a0-687b-4588-8e4f-1a55e1780c47

📥 Commits

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

📒 Files selected for processing (7)
  • packages/eslint-plugin-next/README.md
  • packages/eslint-plugin-next/src/__tests__/require-auth-protection.test.ts
  • packages/eslint-plugin-next/src/lib/exports.ts
  • packages/eslint-plugin-next/src/lib/file-info.ts
  • packages/eslint-plugin-next/src/lib/protection-checks.ts
  • 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 (1)
  • packages/eslint-plugin-next/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/eslint-plugin-next/tsdown.config.mts
  • packages/eslint-plugin-next/src/lib/protection-checks.ts
  • packages/eslint-plugin-next/src/tests/require-auth-protection.test.ts

Comment thread packages/eslint-plugin-next/src/rules/require-auth-protection.ts
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