Skip to content

Commit

Permalink
chore(repo): Update tests for next 15 (#4421)
Browse files Browse the repository at this point in the history
  • Loading branch information
BRKalow authored Oct 29, 2024
1 parent 341fcd6 commit 41f2ede
Show file tree
Hide file tree
Showing 15 changed files with 221 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-pears-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/nextjs": patch
---

Fixes a bug where `<ClerkProvider dynamic>` would error when rendered in a Next.js 13 application using the App Router.
5 changes: 5 additions & 0 deletions .changeset/famous-baboons-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/clerk-react": patch
---

Updates `useDerivedAuth()` to correctly derive `has()` from the available auth data. Fixes an issue when `useAuth()` is called during server-side rendering.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ jobs:
- test-name: 'nextjs'
test-project: 'chrome'
next-version: '14'
- test-name: 'nextjs'
test-project: 'chrome'
next-version: '15'

steps:
- name: Checkout Repo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@types/node": "^20.12.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"next": "14.2.10",
"next": "^15.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.6.2"
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/next-app-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@types/node": "^18.19.33",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"next": "^14.2.13",
"next": "^15.0.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"typescript": "^5.6.2"
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/next-app-router/src/app/csp/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { headers } from 'next/headers';
import { ClerkLoaded } from '@clerk/nextjs';

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

return (
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { auth } from '@clerk/nextjs/server';

export default async function Home({ params }: { params: { id: string } }) {
export default async function Home({ params }: { params: Promise<{ id: string }> }) {
const { orgId } = await auth();
const paramsId = (await params).id;

if (params.id != orgId) {
console.log('Mismatch - returning nothing for now...', params.id, orgId);
if (paramsId != orgId) {
console.log('Mismatch - returning nothing for now...', paramsId, orgId);
}

console.log("I'm the server and I got this id: ", orgId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { auth } from '@clerk/nextjs/server';

export default async function Home({ params }: { params: { id: string } }) {
export default async function Home({ params }: { params: Promise<{ id: string }> }) {
const { orgId } = await auth();
const paramsId = (await params).id;

if (params.id != orgId) {
console.log('Mismatch - returning nothing for now...', params.id, orgId);
if (paramsId != orgId) {
console.log('Mismatch - returning nothing for now...', paramsId, orgId);
}

console.log("I'm the server and I got this id: ", orgId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { auth } from '@clerk/nextjs/server';

export default async function Home({ params }: { params: { slug: string } }) {
export default async function Home({ params }: { params: Promise<{ slug: string }> }) {
const { orgSlug } = await auth();
const paramsSlug = (await params).slug;

if (params.slug != orgSlug) {
console.log('Mismatch - returning nothing for now...', params.slug, orgSlug);
if (paramsSlug != orgSlug) {
console.log('Mismatch - returning nothing for now...', paramsSlug, orgSlug);
}

console.log("I'm the server and I got this slug: ", orgSlug);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { auth } from '@clerk/nextjs/server';

export default async function Home({ params }: { params: { slug: string } }) {
export default async function Home({ params }: { params: Promise<{ slug: string }> }) {
const { orgSlug } = await auth();
const paramsSlug = (await params).slug;

if (params.slug != orgSlug) {
console.log('Mismatch - returning nothing for now...', params.slug, orgSlug);
if (paramsSlug != orgSlug) {
console.log('Mismatch - returning nothing for now...', paramsSlug, orgSlug);
}

console.log("I'm the server and I got this slug: ", orgSlug);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ClerkProvider } from '@clerk/nextjs';

export default function Layout({ children }: { children: React.ReactNode }) {
return <ClerkProvider dynamic>{children}</ClerkProvider>;
}
136 changes: 133 additions & 3 deletions integration/tests/next-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,27 @@ import { expect, test } from '@playwright/test';
import type { Application } from '../models/application';
import { appConfigs } from '../presets';

test.describe('next build @nextjs', () => {
type RenderingModeTestCase = {
name: string;
type: 'Static' | 'Dynamic';
page: string;
};

function getIndicator(buildOutput: string, type: 'Static' | 'Dynamic') {
return buildOutput
.split('\n')
.find(msg => {
const isTypeFound = msg.includes(`(${type})`);

if (type === 'Dynamic') {
return isTypeFound || msg.includes(`(Server)`);
}
return isTypeFound;
})
.split(' ')[0];
}

test.describe('next build - provider as client component @nextjs', () => {
test.describe.configure({ mode: 'parallel' });
let app: Application;

Expand Down Expand Up @@ -58,14 +78,124 @@ export default function RootLayout({ children }: { children: React.ReactNode })
});

test('When <ClerkProvider /> is used as a client component, builds successfully and does not force dynamic rendering', () => {
const dynamicIndicator = 'λ';
// Get the static indicator from the build output
const staticIndicator = getIndicator(app.buildOutput, 'Static');

/**
* Using /_not-found as it is an internal page that should statically render by default.
* This is a good indicator of whether or not the entire app has been opted-in to dynamic rendering.
*/
const notFoundPageLine = app.buildOutput.split('\n').find(msg => msg.includes('/_not-found'));

expect(notFoundPageLine).not.toContain(dynamicIndicator);
expect(notFoundPageLine).toContain(staticIndicator);
});
});

test.describe('next build - dynamic options @nextjs', () => {
test.describe.configure({ mode: 'parallel' });
let app: Application;

test.beforeAll(async () => {
app = await appConfigs.next.appRouter
.clone()
.addFile(
'src/app/(dynamic)/layout.tsx',
() => `import '../globals.css';
import { Inter } from 'next/font/google';
import { ClerkProvider } from '@clerk/nextjs';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider dynamic>
<html lang='en'>
<body className={inter.className}>{children}</body>
</html>
</ClerkProvider>
);
}
`,
)
.addFile(
'src/app/(dynamic)/dynamic/page.tsx',
() => `export default function DynamicPage() {
return(<h1>This page is dynamic</h1>);
}`,
)
.addFile(
'src/app/nested-provider/page.tsx',
() => `import { ClerkProvider } from '@clerk/nextjs';
import { ClientComponent } from './client';
export default function Page() {
return (
<ClerkProvider dynamic>
<ClientComponent />
</ClerkProvider>
);
}
`,
)
.addFile(
'src/app/nested-provider/client.tsx',
() => `'use client';
import { useAuth } from '@clerk/nextjs';
export function ClientComponent() {
useAuth();
return <p>I am dynamically rendered</p>;
}
`,
)
.commit();
await app.setup();
await app.withEnv(appConfigs.envs.withEmailCodes);
await app.build();
});

test.afterAll(async () => {
// await app.teardown();
});

(
[
{
name: '<ClerkProvider> supports static rendering by default',
type: 'Static',
page: '/_not-found',
},
{
name: '<ClerkProvider dynamic> opts-in to dynamic rendering',
type: 'Dynamic',
page: '/dynamic',
},
{
name: 'auth() opts in to dynamic rendering',
type: 'Dynamic',
page: '/page-protected',
},
{
name: '<ClerkProvider dynamic> can be nested in the root provider',
type: 'Dynamic',
page: '/nested-provider',
},
] satisfies RenderingModeTestCase[]
).forEach(({ name, type, page }) => {
test(`ClerkProvider rendering modes - ${name}`, () => {
// Get the indicator from the build output
const indicator = getIndicator(app.buildOutput, type);

const pageLine = app.buildOutput.split('\n').find(msg => msg.includes(page));

expect(pageLine).toContain(indicator);
});
});
});
16 changes: 14 additions & 2 deletions packages/nextjs/src/app-router/server/ClerkProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AuthObject } from '@clerk/backend';
import type { InitialState, Without } from '@clerk/types';
import { headers } from 'next/headers';
import nextPkg from 'next/package.json';
import React from 'react';

import { PromisifiedAuthProvider } from '../../client-boundary/PromisifiedAuthProvider';
Expand All @@ -10,6 +11,8 @@ import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithE
import { ClientClerkProvider } from '../client/ClerkProvider';
import { buildRequestLike, getScriptNonceFromHeader } from './utils';

const isNext13 = nextPkg.version.startsWith('13.');

const getDynamicClerkState = React.cache(async function getDynamicClerkState() {
const request = await buildRequestLike();
const data = getDynamicAuthData(request);
Expand All @@ -29,8 +32,17 @@ export async function ClerkProvider(
let nonce = Promise.resolve('');

if (dynamic) {
statePromise = getDynamicClerkState();
nonce = getNonceFromCSPHeader();
if (isNext13) {
/**
* For some reason, Next 13 requires that functions which call `headers()` are awaited where they are invoked.
* Without the await here, Next will throw a DynamicServerError during build.
*/
statePromise = Promise.resolve(await getDynamicClerkState());
nonce = Promise.resolve(await getNonceFromCSPHeader());
} else {
statePromise = getDynamicClerkState();
nonce = getNonceFromCSPHeader();
}
}

const output = (
Expand Down
44 changes: 35 additions & 9 deletions packages/react/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,40 @@ export const useAuth: UseAuth = (initialAuthState = {}) => {
const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]);
const signOut: SignOut = useCallback(createSignOut(isomorphicClerk), [isomorphicClerk]);

const has = useCallback(
return useDerivedAuth({
sessionId,
userId,
actor,
orgId,
orgSlug,
orgRole,
getToken,
signOut,
orgPermissions,
__experimental_factorVerificationAge,
});
};

export function useDerivedAuth(authObject: any): UseAuthReturn {
const {
sessionId,
userId,
actor,
orgId,
orgSlug,
orgRole,
has,
signOut,
getToken,
orgPermissions,
__experimental_factorVerificationAge,
} = authObject ?? {};

const derivedHas = useCallback(
(params: Parameters<CheckAuthorizationWithCustomPermissions>[0]) => {
if (has) {
return has(params);
}
return createCheckAuthorization({
userId,
orgId,
Expand All @@ -150,12 +182,6 @@ export const useAuth: UseAuth = (initialAuthState = {}) => {
[userId, __experimental_factorVerificationAge, orgId, orgRole, orgPermissions],
);

return useDerivedAuth({ sessionId, userId, actor, orgId, orgSlug, orgRole, getToken, signOut, has });
};

export function useDerivedAuth(authObject: any): UseAuthReturn {
const { sessionId, userId, actor, orgId, orgSlug, orgRole, has, signOut, getToken } = authObject ?? {};

if (sessionId === undefined && userId === undefined) {
return {
isLoaded: false,
Expand Down Expand Up @@ -198,7 +224,7 @@ export function useDerivedAuth(authObject: any): UseAuthReturn {
orgId,
orgRole,
orgSlug: orgSlug || null,
has,
has: derivedHas,
signOut,
getToken,
};
Expand All @@ -214,7 +240,7 @@ export function useDerivedAuth(authObject: any): UseAuthReturn {
orgId: null,
orgRole: null,
orgSlug: null,
has,
has: derivedHas,
signOut,
getToken,
};
Expand Down
3 changes: 2 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"VITE_CLERK_*",
"EXPO_PUBLIC_CLERK_*",
"REACT_APP_CLERK_*",
"NEXT_PHASE"
"NEXT_PHASE",
"E2E_NEXTJS_VERSION"
],
"globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN", "ACTIONS_RUNNER_DEBUG", "ACTIONS_STEP_DEBUG"],
"tasks": {
Expand Down

0 comments on commit 41f2ede

Please sign in to comment.