Skip to content

fix(nextjs): make @clerk/nextjs ESM-safe for non-Node.js runtimes#7954

Open
nikosdouvlis wants to merge 3 commits intomainfrom
nk/fix-nextjs-esm-server-only
Open

fix(nextjs): make @clerk/nextjs ESM-safe for non-Node.js runtimes#7954
nikosdouvlis wants to merge 3 commits intomainfrom
nk/fix-nextjs-esm-server-only

Conversation

@nikosdouvlis
Copy link
Member

@nikosdouvlis nikosdouvlis commented Feb 27, 2026

Why

@clerk/nextjs crashes on import in pure ESM runtimes like Cloudflare Workers (via vinext). Three separate require() patterns fail because require is not defined in ESM-only environments.

Ref: cloudflare/vinext#73

What changed

  • auth(), auth.protect(), and currentUser() used require('server-only') which crashes in ESM runtimes. Replaced with assertServerOnly() that checks typeof require before calling it. The server-only package uses the react-server export condition, which vinext/Workers already enforce at the bundler level, so the guard is redundant there.

  • safe-node-apis.js (both node and browser variants) used require('node:fs') / module.exports, which crashes in Vite's ESM module runner. Converted to ESM import/export default.

  • usePathnameWithoutCatchAll used require('next/navigation') to lazily load hooks. Since @clerk/nextjs only supports Next.js 15.2.8+, next/navigation is always available as a static import.

Testing

Verified with a vinext smoke test app (Vite 7.3.1 + vinext 0.0.15 + Next.js 16.1.6):

  • / (home with auth()) - 200, renders auth state
  • /api/me (API route with auth()) - 200, returns userId/sessionId
  • /sign-in (<SignIn /> component) - 200, renders sign-in UI
  • /protected (auth.protect()) - correctly redirects unauthenticated users

Summary by CodeRabbit

  • Chores
    • Refactored internal module system for improved ES module compatibility across different runtime environments.
    • Added server-side assertion utilities for enhanced runtime safety.

auth(), auth.protect(), and currentUser() call require('server-only')
at runtime. Cloudflare Workers is pure ESM with no require(), so this
crashes with "require is not defined" when running @clerk/nextjs on
vinext (Cloudflare's Vite-based Next.js reimplementation).

Replace the three bare require() calls with an assertServerOnly()
helper that checks typeof require before calling. On Next.js (where
require exists) behavior is identical. On pure ESM runtimes the guard
is skipped, deferring to the bundler's own RSC/client environment
separation.
@vercel
Copy link

vercel bot commented Feb 27, 2026

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

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Feb 27, 2026 11:57pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Feb 27, 2026

🦋 Changeset detected

Latest commit: f166d40

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

This PR includes changesets to release 1 package
Name Type
@clerk/nextjs Patch

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

Adds a new assertServerOnly utility to make server-side checks ESM-safe by only invoking require('server-only') when CommonJS require exists. Replaces direct require('server-only') calls in auth.ts and currentUser.ts with assertServerOnly(). Introduces a unit test validating the assertion behavior in CommonJS-capable test environments. Converts several runtime helper modules from CommonJS to ESM default exports and replaces dynamic require usage with static ESM imports in a client-boundary hook. No public API signatures were changed.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: making @clerk/nextjs ESM-safe for non-Node.js runtimes. It reflects the core changes across multiple files (assertServerOnly helper, ESM conversions, and static imports).

✏️ 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.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 27, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7954

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7954

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7954

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7954

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7954

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7954

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@7954

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7954

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7954

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7954

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@7954

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7954

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7954

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7954

@clerk/react

npm i https://pkg.pr.new/@clerk/react@7954

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7954

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7954

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7954

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7954

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@7954

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7954

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7954

commit: f166d40

safe-node-apis.js (node + browser) used require/module.exports which
crashes in Vite's ESM module runner. usePathnameWithoutCatchAll used
require('next/navigation') to lazily load hooks, which also fails in
pure ESM runtimes. Since @clerk/nextjs only supports Next.js 15.2.8+,
next/navigation is always available as a static import.
@nikosdouvlis nikosdouvlis changed the title fix(nextjs): make server-only guard ESM-safe for vinext/Workers fix(nextjs): make @clerk/nextjs ESM-safe for non-Node.js runtimes Feb 27, 2026
Copy link
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 the current code and only fix it if needed.

Inline comments:
In `@packages/nextjs/src/client-boundary/hooks/usePathnameWithoutCatchAll.tsx`:
- Around line 27-35: The hook calls usePathname() and useParams() are currently
invoked only when pagesRouter is falsy, violating the Rules of Hooks; move both
usePathname() and useParams() to be called unconditionally at the top of
usePathnameWithoutCatchAll (so always call usePathname() and useParams() to get
pathname and params) and then keep the existing pagesRouter conditional logic to
decide which values to use or to short-circuit, using the unconditional values
rather than calling hooks inside branches; update any references to pathname,
pathParts, and catchAllParams accordingly (functions/identifiers:
usePathnameWithoutCatchAll, usePathname, useParams, pagesRouter, pathname,
params, catchAllParams).

ℹ️ Review info

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

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7b67aae and f166d40.

📒 Files selected for processing (4)
  • .changeset/quiet-waves-guard.md
  • packages/nextjs/src/client-boundary/hooks/usePathnameWithoutCatchAll.tsx
  • packages/nextjs/src/runtime/browser/safe-node-apis.js
  • packages/nextjs/src/runtime/node/safe-node-apis.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/quiet-waves-guard.md

Comment on lines +27 to +35
const pathname = usePathname() ?? '';
const pathParts = pathname.split('/').filter(Boolean);
// the useParams hook returns an object with all named and catch all params
// for named params, the key in the returned object always contains a single value
// for catch all params, the key in the returned object contains an array of values
// we find the catch all params by checking if the value is an array
// and then we remove one path part for each catch all param
const catchAllParams = Object.values(useParams() || {})
const params = useParams();
const catchAllParams = Object.values(params ?? {})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Rules of Hooks violation: usePathname() and useParams() are called conditionally.

The early return on lines 11-20 when pagesRouter exists means these hooks are only called when in App Router mode. This violates React's Rules of Hooks and is flagged by Biome.

While this may work in practice because an app is typically either Pages Router or App Router (not both), this pattern can cause React warnings in development and potential issues if the router mode detection ever changes between renders.

Consider restructuring to call hooks unconditionally at the top of the function, then conditionally use their values.

Proposed restructure
 export const usePathnameWithoutCatchAll = () => {
   const pathRef = React.useRef<string>();
-
   const { pagesRouter } = usePagesRouter();
+  const pathname = usePathname() ?? '';
+  const params = useParams();

   if (pagesRouter) {
     if (pathRef.current) {
       return pathRef.current;
     } else {
       pathRef.current = pagesRouter.pathname.replace(/\/\[\[\.\.\..*/, '');
       return pathRef.current;
     }
   }

-  const pathname = usePathname() ?? '';
   const pathParts = pathname.split('/').filter(Boolean);
-  const params = useParams();
   const catchAllParams = Object.values(params ?? {})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const pathname = usePathname() ?? '';
const pathParts = pathname.split('/').filter(Boolean);
// the useParams hook returns an object with all named and catch all params
// for named params, the key in the returned object always contains a single value
// for catch all params, the key in the returned object contains an array of values
// we find the catch all params by checking if the value is an array
// and then we remove one path part for each catch all param
const catchAllParams = Object.values(useParams() || {})
const params = useParams();
const catchAllParams = Object.values(params ?? {})
export const usePathnameWithoutCatchAll = () => {
const pathRef = React.useRef<string>();
const { pagesRouter } = usePagesRouter();
const pathname = usePathname() ?? '';
const params = useParams();
if (pagesRouter) {
if (pathRef.current) {
return pathRef.current;
} else {
pathRef.current = pagesRouter.pathname.replace(/\/\[\[\.\.\..*/, '');
return pathRef.current;
}
}
const pathParts = pathname.split('/').filter(Boolean);
const catchAllParams = Object.values(params ?? {})
🧰 Tools
🪛 Biome (2.4.4)

[error] 27-27: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

(lint/correctness/useHookAtTopLevel)


[error] 34-34: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/nextjs/src/client-boundary/hooks/usePathnameWithoutCatchAll.tsx`
around lines 27 - 35, The hook calls usePathname() and useParams() are currently
invoked only when pagesRouter is falsy, violating the Rules of Hooks; move both
usePathname() and useParams() to be called unconditionally at the top of
usePathnameWithoutCatchAll (so always call usePathname() and useParams() to get
pathname and params) and then keep the existing pagesRouter conditional logic to
decide which values to use or to short-circuit, using the unconditional values
rather than calling hooks inside branches; update any references to pathname,
pathParts, and catchAllParams accordingly (functions/identifiers:
usePathnameWithoutCatchAll, usePathname, useParams, pagesRouter, pathname,
params, catchAllParams).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant