From 9f0325e1937d9656c65cf96b8f589b149d0d1bff Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 23 Apr 2026 19:13:42 -0500 Subject: [PATCH 1/5] fix(nextjs): declare dist/esm as ESM via type:module sidecar Add `"type": "module"` to `package.esm.json` so `dist/esm/package.json` declares the bundle as ESM. Node 22+ and tsx consumers under `"type": "module"` can now import named exports from `@clerk/nextjs/server` without hitting SyntaxError at instantiate time. Swap `lint:attw --ignore-rules unexpected-module-syntax` (no longer needed) for `--ignore-rules false-cjs`, which fires because types are still emitted as `.d.ts`. The `.d.mts` split is tracked separately. Closes #8396 --- .changeset/nextjs-esm-type-module.md | 5 +++++ packages/nextjs/package.esm.json | 1 + packages/nextjs/package.json | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/nextjs-esm-type-module.md diff --git a/.changeset/nextjs-esm-type-module.md b/.changeset/nextjs-esm-type-module.md new file mode 100644 index 00000000000..70d81097595 --- /dev/null +++ b/.changeset/nextjs-esm-type-module.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Fix ESM resolution under consumers with `"type": "module"`. The package now emits `dist/esm/package.json` with `"type": "module"`, so Node 22+ and tsx no longer treat bundled ESM output as CJS and named imports from `@clerk/nextjs/server` resolve correctly. Closes #8396. diff --git a/packages/nextjs/package.esm.json b/packages/nextjs/package.esm.json index b94ce328572..e05cba92d11 100644 --- a/packages/nextjs/package.esm.json +++ b/packages/nextjs/package.esm.json @@ -1,4 +1,5 @@ { + "type": "module", "sideEffects": false, "imports": { "#components": { diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 872c0a566d8..a4bd1caede1 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -78,7 +78,7 @@ "format": "node ../../scripts/format-package.mjs", "format:check": "node ../../scripts/format-package.mjs --check", "lint": "eslint src", - "lint:attw": "attw --pack . --profile node16 --ignore-rules unexpected-module-syntax", + "lint:attw": "attw --pack . --profile node16 --ignore-rules false-cjs", "lint:publint": "publint", "test": "vitest run", "test:watch": "vitest watch" From 05425f5d8b6baed7c69936f398d42541317d0ac7 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 23 Apr 2026 20:06:44 -0500 Subject: [PATCH 2/5] fix(nextjs): replace export * in experimental with named re-exports Webpack's next-flight-loader rejects 'export *' in client boundaries once a package declares itself as ESM. With the type:module sidecar now in place, the existing re-export chain through '@clerk/nextjs/experimental' broke Next.js 15 app builds that imported from it in client components. Spell out the named exports to satisfy the client-boundary rule while keeping the public API intact. --- packages/nextjs/src/experimental.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/experimental.ts b/packages/nextjs/src/experimental.ts index 3edfcf8564f..a75517f6b2f 100644 --- a/packages/nextjs/src/experimental.ts +++ b/packages/nextjs/src/experimental.ts @@ -1,3 +1,23 @@ 'use client'; -export * from '@clerk/react/experimental'; +export { + CheckoutButton, + PlanDetailsButton, + SubscriptionDetailsButton, + PaymentElementProvider, + usePaymentElement, + PaymentElement, + usePaymentAttempts, + useStatements, + usePaymentMethods, + usePlans, + useSubscription, + CheckoutProvider, + useCheckout, +} from '@clerk/react/experimental'; + +export type { + CheckoutButtonProps, + SubscriptionDetailsButtonProps, + PlanDetailsButtonProps, +} from '@clerk/react/experimental'; From ca51781f1f24d7a6ab5bb4dbf3b54355c5fa3bcf Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 24 Apr 2026 11:37:49 -0500 Subject: [PATCH 3/5] fix(nextjs): split ./server by react-server and import 'server-only' statically Fixes the Next 15 prerender regression that the dist/esm sidecar introduced. - Split `./server` into a pages-safe `index.ts` and an RSC-layer `index.rsc.ts`. The RSC variant re-exports the pages-safe surface plus `auth` and `currentUser`, which are now only resolvable under the `react-server` export condition. - Replace the lazy `require('server-only')` calls in `auth.ts` and `currentUser.ts` with a top-of-file `import 'server-only'`. Safe because those modules only load under the `react-server` condition; webpack can analyze the static import in both CJS and ESM module classifications. - Update `./server` exports condition map and extend the exports snapshot test to cover both the default and react-server surfaces. --- .changeset/nextjs-rsc-server-only.md | 5 +++++ packages/nextjs/package.json | 6 +++++- packages/nextjs/src/app-router/server/auth.ts | 8 ++------ .../src/app-router/server/currentUser.ts | 5 ++--- .../__snapshots__/exports.test.ts.snap | 18 +++++++++++++++++- .../src/server/__tests__/exports.test.ts | 13 +++++++++++-- packages/nextjs/src/server/index.rsc.ts | 14 ++++++++++++++ packages/nextjs/src/server/index.ts | 7 +++++-- 8 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 .changeset/nextjs-rsc-server-only.md create mode 100644 packages/nextjs/src/server/index.rsc.ts diff --git a/.changeset/nextjs-rsc-server-only.md b/.changeset/nextjs-rsc-server-only.md new file mode 100644 index 00000000000..f4c4ddba141 --- /dev/null +++ b/.changeset/nextjs-rsc-server-only.md @@ -0,0 +1,5 @@ +--- +'@clerk/nextjs': patch +--- + +Restore Next 15 prerender compatibility for pages that call `auth()` / `currentUser()`. The `./server` subpath now splits by the `react-server` export condition: app-router-only helpers live under the RSC condition, while the pages-router-safe surface (`getAuth`, `buildClerkProps`, `clerkMiddleware`, etc.) remains in the default condition. `server-only` is now imported statically in `auth.ts` / `currentUser.ts` — cleaner than the previous lazy-`require` and analyzable by webpack in both CJS and ESM module classifications. diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index a4bd1caede1..5e0b2d08c0c 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -31,7 +31,11 @@ "require": "./dist/cjs/index.js" }, "./server": { - "types": "./dist/types/server/index.d.ts", + "types": "./dist/types/server/index.rsc.d.ts", + "react-server": { + "import": "./dist/esm/server/index.rsc.js", + "require": "./dist/cjs/server/index.rsc.js" + }, "import": "./dist/esm/server/index.js", "require": "./dist/cjs/server/index.js" }, diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index f1bae2c325e..bd151205e61 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -1,3 +1,5 @@ +import 'server-only'; + import type { SessionAuthObject } from '@clerk/backend'; import type { AuthOptions, GetAuthFnNoRequest, RedirectFun } from '@clerk/backend/internal'; import { constants, createClerkRequest, createRedirect, TokenType } from '@clerk/backend/internal'; @@ -75,9 +77,6 @@ export type AuthFn = GetAuthFnNoRequest & { * - Requires [`clerkMiddleware()`](https://clerk.com/docs/reference/nextjs/clerk-middleware) to be configured. */ export const auth: AuthFn = (async (options?: AuthOptions) => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - require('server-only'); - try { const request = await buildRequestLike(); @@ -158,9 +157,6 @@ export const auth: AuthFn = (async (options?: AuthOptions) => { }) as AuthFn; auth.protect = async (...args: any[]) => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - require('server-only'); - const request = await buildRequestLike(); const requestedToken = args?.[0]?.token || args?.[1]?.token || TokenType.SessionToken; const authObject = await auth({ acceptsToken: requestedToken }); diff --git a/packages/nextjs/src/app-router/server/currentUser.ts b/packages/nextjs/src/app-router/server/currentUser.ts index 02ac8fabfb6..821e751d7ef 100644 --- a/packages/nextjs/src/app-router/server/currentUser.ts +++ b/packages/nextjs/src/app-router/server/currentUser.ts @@ -1,3 +1,5 @@ +import 'server-only'; + import type { User } from '@clerk/backend'; import type { PendingSessionOptions } from '@clerk/shared/types'; @@ -29,9 +31,6 @@ type CurrentUserOptions = PendingSessionOptions; * ``` */ export async function currentUser(opts?: CurrentUserOptions): Promise { - // eslint-disable-next-line @typescript-eslint/no-require-imports - require('server-only'); - try { const { userId } = await auth({ treatPendingAsSignedOut: opts?.treatPendingAsSignedOut }); if (!userId) { diff --git a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap index fcbeab2a164..93497d18a22 100644 --- a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap @@ -1,6 +1,22 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`/server public exports > should not include a breaking change 1`] = ` +exports[`/server public exports > default condition (pages-safe) should not include a breaking change 1`] = ` +[ + "buildClerkProps", + "clerkClient", + "clerkFrontendApiProxy", + "clerkMiddleware", + "createClerkClient", + "createFrontendApiProxyHandlers", + "createRouteMatcher", + "getAuth", + "reverificationError", + "reverificationErrorResponse", + "verifyToken", +] +`; + +exports[`/server public exports > react-server condition should not include a breaking change 1`] = ` [ "auth", "buildClerkProps", diff --git a/packages/nextjs/src/server/__tests__/exports.test.ts b/packages/nextjs/src/server/__tests__/exports.test.ts index 6bbcf0d1ea6..38bdcb370b1 100644 --- a/packages/nextjs/src/server/__tests__/exports.test.ts +++ b/packages/nextjs/src/server/__tests__/exports.test.ts @@ -1,9 +1,18 @@ import { describe, expect, it } from 'vitest'; import * as publicExports from '../index'; +import * as rscExports from '../index.rsc'; describe('/server public exports', () => { - it('should not include a breaking change', () => { - expect(Object.keys(publicExports).sort()).toMatchSnapshot(); + describe('default condition (pages-safe)', () => { + it('should not include a breaking change', () => { + expect(Object.keys(publicExports).sort()).toMatchSnapshot(); + }); + }); + + describe('react-server condition', () => { + it('should not include a breaking change', () => { + expect(Object.keys(rscExports).sort()).toMatchSnapshot(); + }); }); }); diff --git a/packages/nextjs/src/server/index.rsc.ts b/packages/nextjs/src/server/index.rsc.ts new file mode 100644 index 00000000000..f9fa75c8b32 --- /dev/null +++ b/packages/nextjs/src/server/index.rsc.ts @@ -0,0 +1,14 @@ +/** + * RSC-layer entrypoint for `@clerk/nextjs/server`. + * + * Selected by the `react-server` export condition in `package.json`. Adds the + * app-router-only helpers (`auth`, `currentUser`) on top of the pages-safe + * surface exported from `./index`. Pulling those helpers here — rather than + * from `./index` directly — keeps pages-router consumers from transitively + * importing `server-only`, which throws under a non-RSC condition. + */ + +export * from './index'; + +export { auth } from '../app-router/server/auth'; +export { currentUser } from '../app-router/server/currentUser'; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 477ad2eac59..bdec1fa6619 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -38,11 +38,14 @@ export type { /** * NextJS-specific exports + * + * NOTE: `auth` and `currentUser` are re-exported from `./index.rsc.ts` only, + * which is selected via the `react-server` export condition. Loading them + * outside the RSC layer (e.g. from pages-router code) would transitively + * import `server-only` and crash at module load. */ export { getAuth } from './createGetAuth'; export { buildClerkProps } from './buildClerkProps'; -export { auth } from '../app-router/server/auth'; -export { currentUser } from '../app-router/server/currentUser'; export { clerkMiddleware } from './clerkMiddleware'; export type { ClerkMiddlewareAuth, ClerkMiddlewareSessionAuthObject, ClerkMiddlewareOptions } from './clerkMiddleware'; From 84fedaebb811ff33cf0e18ba9e89c9b5c4abfdbc Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 24 Apr 2026 11:45:46 -0500 Subject: [PATCH 4/5] test(nextjs): mock server-only in /server exports test vitest has no react-server resolve condition, so importing the RSC surface transitively loads auth.ts and crashes on the client-facing server-only export. Stubbing the module keeps the test focused on verifying the re-export surface. --- packages/nextjs/src/server/__tests__/exports.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/server/__tests__/exports.test.ts b/packages/nextjs/src/server/__tests__/exports.test.ts index 38bdcb370b1..2f0e9241a0c 100644 --- a/packages/nextjs/src/server/__tests__/exports.test.ts +++ b/packages/nextjs/src/server/__tests__/exports.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('server-only', () => ({})); import * as publicExports from '../index'; import * as rscExports from '../index.rsc'; From e9c5776b14a1bb581cfc2767482b2840ab28e08e Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 24 Apr 2026 11:52:47 -0500 Subject: [PATCH 5/5] test(nextjs): fix snapshot keys for nested describe blocks Vitest keys nested describes with ' > ' between every level including the final `it`. The initial snapshot file was missing the separator between the inner describe and the test name, so CI (which refuses to auto-create snapshots) reported the tests as mismatched. --- .../src/server/__tests__/__snapshots__/exports.test.ts.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap index 93497d18a22..05a96ea6ab4 100644 --- a/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/nextjs/src/server/__tests__/__snapshots__/exports.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`/server public exports > default condition (pages-safe) should not include a breaking change 1`] = ` +exports[`/server public exports > default condition (pages-safe) > should not include a breaking change 1`] = ` [ "buildClerkProps", "clerkClient", @@ -16,7 +16,7 @@ exports[`/server public exports > default condition (pages-safe) should not incl ] `; -exports[`/server public exports > react-server condition should not include a breaking change 1`] = ` +exports[`/server public exports > react-server condition > should not include a breaking change 1`] = ` [ "auth", "buildClerkProps",