Skip to content

Commit

Permalink
feat(nextjs): Next.js@15 compatibility (#4366)
Browse files Browse the repository at this point in the history
Co-authored-by: Bryce Kalow <[email protected]>
Co-authored-by: panteliselef <[email protected]>
  • Loading branch information
3 people authored Oct 22, 2024
1 parent 8ddb3ea commit a0204a8
Show file tree
Hide file tree
Showing 80 changed files with 3,122 additions and 1,987 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-suits-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/nextjs": major
---

Stop `<ClerkProvider>` from opting applications into dynamic rendering. A new prop, `<ClerkProvider dynamic>` can be used to opt-in to dynamic rendering and make auth data available during server-side rendering. The RSC `auth()` helper should be preferred for accessing auth data during dynamic rendering.
90 changes: 90 additions & 0 deletions .changeset/grumpy-hairs-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
"@clerk/nextjs": major
"@clerk/upgrade": minor
---

@clerk/nextjs: Converting auth() and clerkClient() interfaces to be async
@clerk/upgrade: Adding required codemod for @clerk/nextjs breaking changes

# Migration guide

## `auth()` is now async

Previously the `auth()` method from `@clerk/nextjs/server` was synchronous.

```typescript
import { auth } from '@clerk/nextjs/server';

export function GET() {
const { userId } = auth();
return new Response(JSON.stringify({ userId }));
}
```

The `auth` method now becomes asynchronous. You will need to make the following changes to the snippet above to make it compatible.

```diff
- export function GET() {
+ export async function GET() {
- const { userId } = auth();
+ const { userId } = await auth();
return new Response(JSON.stringify({ userId }));
}
```

## Clerk middleware auth is now async

```typescript
import { clerkClient, clerkMiddleware } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export default clerkMiddleware(async (auth, request) => {
const resolvedAuth = await auth();

const count = await resolvedAuth.users.getCount();

if (count) {
return NextResponse.redirect(new URL('/new-url', request.url));
}
});

export const config = {
matcher: [...],
};
```

## clerkClient() is now async

Previously the `clerkClient()` method from `@clerk/nextjs/server` was synchronous.

```typescript
import { clerkClient, clerkMiddleware } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export default clerkMiddleware((auth, request) => {
const client = clerkClient();

const count = await client.users?.getCount();

if (count) {
return NextResponse.redirect(new URL('/new-url', request.url));
}
});

export const config = {
matcher: [...],
};
```

The method now becomes async. You will need to make the following changes to the snippet above to make it compatible.

```diff
- export default clerkMiddleware((auth, request) => {
- const client = clerkClient();
+ export default clerkMiddleware(async (auth, request) => {
+ const client = await clerkClient();
const count = await client.users?.getCount();

if (count) {
}
```
6 changes: 6 additions & 0 deletions .changeset/shiny-numbers-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clerk/nextjs": major
---

Support `unstable_rethrow` inside `clerkMiddleware`.
We changed the errors thrown by `protect()` inside `clerkMiddleware` in order for `unstable_rethrow` to recognise them and rethrow them.
5 changes: 5 additions & 0 deletions .changeset/ten-worms-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/nextjs": major
---

Removes deprecated APIs: `authMiddleware()`, `redirectToSignIn()`, and `redirectToSignUp()`. See the migration guide to learn how to update your usage.
5 changes: 5 additions & 0 deletions .changeset/two-bottles-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/clerk-react": minor
---

Internal changes to support `<ClerkProvider dynamic>`
2 changes: 2 additions & 0 deletions integration/presets/longRunningApps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ export const createLongRunningApps = () => {
{ id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks },
{ id: 'remix.node.withEmailCodes', config: remix.remixNode, env: envs.withEmailCodes },
{ id: 'next.appRouter.withEmailCodes', config: next.appRouter, env: envs.withEmailCodes },
{ id: 'next.appRouter.15RCwithEmailCodes', config: next.appRouter15Rc, env: envs.withEmailCodes },
{
id: 'next.appRouter.withEmailCodes_persist_client',
config: next.appRouter,
env: envs.withEmailCodes_destroy_client,
},
{ id: 'next.appRouter.withCustomRoles', config: next.appRouter, env: envs.withCustomRoles },
{ id: 'next.appRouter.15RCwithCustomRoles', config: next.appRouter15Rc, env: envs.withCustomRoles },
{ id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart },
{ id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes },
{ id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles },
Expand Down
55 changes: 55 additions & 0 deletions integration/presets/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,60 @@ const appRouter = applicationConfig()
.addDependency('react-dom', constants.E2E_REACT_DOM_VERSION)
.addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal);

const appRouter15Rc = applicationConfig()
.setName('next-app-router')
.useTemplate(templates['next-app-router'])
.setEnvFormatter('public', key => `NEXT_PUBLIC_${key}`)
.addScript('setup', constants.E2E_NPM_FORCE ? 'npm i --force' : 'npm i')
.addScript('dev', 'npm run dev')
.addScript('build', 'npm run build')
.addScript('serve', 'npm run start')
.addDependency('next', 'rc')
.addDependency('react', 'rc')
.addDependency('react-dom', 'rc')
.addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal)
.addFile(
'src/middleware.ts',
() => `import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
import { unstable_rethrow } from 'next/navigation';
const csp = \`default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' 'nonce-deadbeef';
img-src 'self' https://img.clerk.com;
worker-src 'self' blob:;
style-src 'self' 'unsafe-inline';
frame-src 'self' https://challenges.cloudflare.com;
\`;
const isProtectedRoute = createRouteMatcher(['/protected(.*)', '/user(.*)', '/switcher(.*)']);
const isAdminRoute = createRouteMatcher(['/only-admin(.*)']);
const isCSPRoute = createRouteMatcher(['/csp']);
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
try { await auth.protect() }
catch (e) { unstable_rethrow(e) }
}
if (isAdminRoute(req)) {
try { await auth.protect({role: 'admin'}) }
catch (e) { unstable_rethrow(e) }
}
if (isCSPRoute(req)) {
req.headers.set('Content-Security-Policy', csp.replace(/\\n/g, ''));
}
});
export const config = {
matcher: [
'/((?!.*\\\\..*|_next).*)', // Don't run middleware on static files
'/', // Run middleware on index page
'/(api|trpc)(.*)',
], // Run middleware on API routes
};`,
);

const appRouterTurbo = appRouter
.clone()
.setName('next-app-router-turbopack')
Expand Down Expand Up @@ -48,6 +102,7 @@ const appRouterAPWithClerkNextV4 = appRouterQuickstart

export const next = {
appRouter,
appRouter15Rc,
appRouterTurbo,
appRouterQuickstart,
appRouterAPWithClerkNextLatest,
Expand Down
4 changes: 2 additions & 2 deletions integration/templates/next-app-router/src/app/api/me/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { auth } from '@clerk/nextjs/server';

export function GET() {
const { userId } = auth();
export async function GET() {
const { userId } = await auth();
return new Response(JSON.stringify({ userId }));
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { auth } from '@clerk/nextjs/server';

export function GET() {
const { userId } = auth().protect(has => has({ role: 'admin' }) || has({ role: 'org:editor' }));
export async function GET() {
const { userId } = await auth.protect((has: any) => has({ role: 'admin' }) || has({ role: 'org:editor' }));
return new Response(JSON.stringify({ userId }));
}
6 changes: 4 additions & 2 deletions integration/templates/next-app-router/src/app/csp/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { headers } from 'next/headers';
import { ClerkLoaded } from '@clerk/nextjs';

export default function CSPPage() {
export default async function CSPPage() {
const cspHeader = await headers().get('Content-Security-Policy');

return (
<div>
CSP: <pre>{headers().get('Content-Security-Policy')}</pre>
CSP: <pre>{cspHeader}</pre>
<ClerkLoaded>
<p>clerk loaded</p>
</ClerkLoaded>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div>User is admin</div>;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { auth } from '@clerk/nextjs/server';

export default function Home({ params }: { params: { id: string } }) {
const { orgId } = auth();
export default async function Home({ params }: { params: { id: string } }) {
const { orgId } = await auth();

if (params.id != orgId) {
console.log('Mismatch - returning nothing for now...', params.id, orgId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { auth } from '@clerk/nextjs/server';

export default function Home({ params }: { params: { id: string } }) {
const { orgId } = auth();
export default async function Home({ params }: { params: { id: string } }) {
const { orgId } = await auth();

if (params.id != orgId) {
console.log('Mismatch - returning nothing for now...', params.id, orgId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { auth } from '@clerk/nextjs/server';

export default function Home({ params }: { params: { slug: string } }) {
const { orgSlug } = auth();
export default async function Home({ params }: { params: { slug: string } }) {
const { orgSlug } = await auth();

if (params.slug != orgSlug) {
console.log('Mismatch - returning nothing for now...', params.slug, orgSlug);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { auth } from '@clerk/nextjs/server';

export default function Home({ params }: { params: { slug: string } }) {
const { orgSlug } = auth();
export default async function Home({ params }: { params: { slug: string } }) {
const { orgSlug } = await auth();

if (params.slug != orgSlug) {
console.log('Mismatch - returning nothing for now...', params.slug, orgSlug);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { auth } from '@clerk/nextjs/server';

export default function Page() {
auth().protect();
export default async function Page() {
await auth.protect();

return <div>Protected Page</div>;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { auth } from '@clerk/nextjs/server';

export default function Home(): {} {
const { orgId } = auth();
export default async function Home() {
const { orgId } = await auth();

if (orgId != null) {
console.log('Oh no, this page should only activate on the personal account!');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { auth } from '@clerk/nextjs/server';

export default function Page() {
const { userId, has } = auth();
export default async function Page() {
const { userId, has } = await auth();
if (!userId || !has({ permission: 'org:posts:manage' })) {
return <p>User is missing permissions</p>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { auth } from '@clerk/nextjs/server';

export default function Page() {
auth().protect({ role: 'admin' });
export default async function Page() {
await auth.protect({ role: 'admin' });
return <p>User has access</p>;
}
9 changes: 7 additions & 2 deletions integration/templates/next-app-router/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ const csp = `default-src 'self';
`;

const isProtectedRoute = createRouteMatcher(['/protected(.*)', '/user(.*)', '/switcher(.*)']);
const isAdminRoute = createRouteMatcher(['/only-admin(.*)']);
const isCSPRoute = createRouteMatcher(['/csp']);

export default clerkMiddleware((auth, req) => {
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
auth().protect();
await auth.protect();
}

if (isAdminRoute(req)) {
await auth.protect({ role: 'admin' });
}

if (isCSPRoute(req)) {
Expand Down
18 changes: 10 additions & 8 deletions integration/tests/dynamic-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,24 @@ test.describe('dynamic keys @nextjs', () => {
.clone()
.addFile(
'src/middleware.ts',
() => `import { clerkClient, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
() => `import { clerkClient, clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';
const isProtectedRoute = createRouteMatcher(['/protected']);
const shouldFetchBapi = createRouteMatcher(['/fetch-bapi-from-middleware']);
export default clerkMiddleware(async (auth, request) => {
if (isProtectedRoute(request)) {
auth().protect();
await auth.protect();
}
if (shouldFetchBapi(request)){
const count = await clerkClient().users.getCount();
const client = await clerkClient();
const count = await client.users?.getCount();
if (count){
return NextResponse.redirect(new URL('/users-count', request.url))
return NextResponse.redirect(new URL('/users-count', request.url));
}
}
}, {
Expand All @@ -45,7 +47,7 @@ test.describe('dynamic keys @nextjs', () => {
() => `import { clerkClient } from '@clerk/nextjs/server'
export default async function Page(){
const count = await clerkClient().users.getCount()
const count = await clerkClient().users?.getCount() ?? 0;
return <p>Users count: {count}</p>
}
Expand All @@ -62,7 +64,7 @@ test.describe('dynamic keys @nextjs', () => {
await app.teardown();
});

test('redirects to `signInUrl` on `auth().protect()`', async ({ page, context }) => {
test('redirects to `signInUrl` on `await auth.protect()`', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await u.page.goToAppHome();
Expand All @@ -74,7 +76,7 @@ test.describe('dynamic keys @nextjs', () => {
await u.page.waitForURL(/foobar/);
});

test('resolves auth signature with `secretKey` on `auth().protect()`', async ({ page, context }) => {
test('resolves auth signature with `secretKey` on `await auth.protect()`', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/page-protected');
await u.page.waitForURL(/foobar/);
Expand Down
Loading

0 comments on commit a0204a8

Please sign in to comment.