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/.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.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..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" }, @@ -78,7 +82,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" 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/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'; 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..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,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..2f0e9241a0c 100644 --- a/packages/nextjs/src/server/__tests__/exports.test.ts +++ b/packages/nextjs/src/server/__tests__/exports.test.ts @@ -1,9 +1,20 @@ -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'; 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';