Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions integration/presets/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,23 @@ const appRouterAPWithClerkNextV6 = appRouterQuickstartV6
.setName('next-app-router-ap-clerk-next-v6')
.addDependency('@clerk/nextjs', '6');

const cacheComponents = applicationConfig()
.setName('next-cache-components')
.useTemplate(templates['next-cache-components'])
.setEnvFormatter('public', key => `NEXT_PUBLIC_${key}`)
.addScript('setup', constants.E2E_NPM_FORCE ? 'pnpm install --force' : 'pnpm install')
.addScript('dev', 'pnpm dev')
.addScript('build', 'pnpm build')
.addScript('serve', 'pnpm start')
.addDependency('@clerk/nextjs', constants.E2E_CLERK_JS_VERSION || linkPackage('nextjs'))
.addDependency('@clerk/shared', linkPackage('shared'));

export const next = {
appRouter,
appRouterTurbo,
appRouterQuickstart,
appRouterAPWithClerkNextLatest,
appRouterAPWithClerkNextV6,
appRouterQuickstartV6,
cacheComponents,
} as const;
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const templates = {
// If /integration becomes a module in the future, use these helpers:
// 'next-app-router': fileURLToPath(new URL('./next-app-router', import.meta.url)),
'next-app-router': resolve(__dirname, './next-app-router'),
'next-cache-components': resolve(__dirname, './next-cache-components'),
'next-app-router-quickstart': resolve(__dirname, './next-app-router-quickstart'),
'next-app-router-quickstart-v6': resolve(__dirname, './next-app-router-quickstart-v6'),
'react-cra': resolve(__dirname, './react-cra'),
Expand Down
23 changes: 23 additions & 0 deletions integration/templates/next-cache-components/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# dependencies
/node_modules

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*

# local env files
.env*.local

# typescript
*.tsbuildinfo
next-env.d.ts
65 changes: 65 additions & 0 deletions integration/templates/next-cache-components/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Next.js Cache Components Integration Test App

This app tests Clerk's integration with Next.js 16's experimental cache components feature.

## Setup

```bash
pnpm install
pnpm dev
```

## Configuration

The app enables cache components in `next.config.js`:

```js
cacheComponents: true, // Enables PPR and cache components
```

**Important**: ClerkProvider must be wrapped in `<Suspense>` for cache components to work correctly.

## Test Scenarios

### 1. auth() in Server Component (`/auth-server-component`)
Tests basic usage of `auth()` in a React Server Component.

### 2. auth() in Server Action (`/auth-server-action`)
Tests using `auth()` inside a server action triggered by a client component.

### 3. auth() in API Route (`/api/auth-check`)
Tests using `auth()` in a Next.js API route handler.

### 4. "use cache" with auth() - Error Case (`/use-cache-error`)
Tests that calling `auth()` inside a `"use cache"` function produces the expected error.
This is an **invalid pattern** because `auth()` uses dynamic APIs (cookies, headers).

### 5. "use cache" Correct Pattern (`/use-cache-correct`)
Demonstrates the correct way to use `"use cache"` with Clerk:
1. Call `auth()` **outside** the cache function
2. Pass the `userId` **into** the cache function
3. The cache function only contains cacheable operations

### 6. PPR with auth() (`/ppr-auth`)
Tests Partial Pre-Rendering with authenticated content.
Static content is pre-rendered while authenticated content streams in dynamically.

### 7. Protected Route (`/protected`)
Tests middleware-based route protection using `auth.protect()`.

## Expected Behaviors

| Scenario | Expected Result |
|----------|-----------------|
| auth() in RSC | Works normally |
| auth() in Server Action | Works normally |
| auth() in API Route | Works normally |
| auth() inside "use cache" | Should throw error |
| userId passed to "use cache" | Works correctly |
| PPR + auth() | Dynamic portion streams after static shell |
| Protected route (unauthenticated) | Redirects to sign-in |

## Related PRs

- PR #7119: Initial exploration of cacheComponents support
- PR #7530: Initial exploration of PPR + auth() issues
7 changes: 7 additions & 0 deletions integration/templates/next-cache-components/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
outputFileTracingRoot: '/',
cacheComponents: true,
};

module.exports = nextConfig;
24 changes: 24 additions & 0 deletions integration/templates/next-cache-components/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "next-cache-components",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "next lint",
"start": "next start"
},
"dependencies": {
"@clerk/nextjs": "workspace:*",
"@types/node": "^18.19.33",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"next": "^16.0.0-canary.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"typescript": "^5.7.3"
},
"engines": {
"node": ">=20.9.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { auth } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

export async function GET() {
const { userId, sessionId } = await auth();

return NextResponse.json({
userId: userId ?? null,
sessionId: sessionId ?? null,
isSignedIn: !!userId,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'use server';

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

export async function checkAuthAction() {
const { userId, sessionId } = await auth();
return { userId, sessionId };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { useState } from 'react';
import { checkAuthAction } from './actions';

export default function AuthServerActionPage() {
const [result, setResult] = useState<{ userId: string | null; sessionId: string | null } | null>(null);
const [error, setError] = useState<string | null>(null);

async function handleCheck() {
try {
const authResult = await checkAuthAction();
setResult(authResult);
setError(null);
} catch (e) {
setError(e instanceof Error ? e.message : 'Unknown error');
setResult(null);
}
}

return (
<main>
<h1>auth() in Server Action</h1>
<p>This page tests using auth() inside a server action.</p>

<button onClick={handleCheck} data-testid="check-auth-btn">
Check Auth via Server Action
</button>

{result && (
<div className="test-result success">
<h3>Auth Result:</h3>
<pre>{JSON.stringify(result, null, 2)}</pre>
<div data-testid="action-user-id">{result.userId ?? 'Not signed in'}</div>
</div>
)}

{error && (
<div className="test-result error">
<h3>Error:</h3>
<pre data-testid="action-error">{error}</pre>
</div>
)}
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Suspense } from 'react';
import { auth } from '@clerk/nextjs/server';

async function AuthContent() {
const { userId, sessionId } = await auth();

return (
<>
<div className={`test-result ${userId ? 'success' : ''}`}>
<h3>Auth Result:</h3>
<pre>
{JSON.stringify(
{
userId: userId ?? null,
sessionId: sessionId ?? null,
isSignedIn: !!userId,
},
null,
2
)}
</pre>
</div>

<div data-testid="user-id">{userId ?? 'Not signed in'}</div>
<div data-testid="session-id">{sessionId ?? 'No session'}</div>
</>
);
}

export default function AuthServerComponentPage() {
return (
<main>
<h1>auth() in Server Component</h1>
<p>This page tests using auth() in a standard React Server Component.</p>

<Suspense fallback={<div>Loading auth...</div>}>
<AuthContent />
</Suspense>
</main>
);
}
98 changes: 98 additions & 0 deletions integration/templates/next-cache-components/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
:root {
--foreground: #171717;
--background: #ffffff;
}

@media (prefers-color-scheme: dark) {
:root {
--foreground: #ededed;
--background: #0a0a0a;
}
}

* {
box-sizing: border-box;
padding: 0;
margin: 0;
}

html,
body {
max-width: 100vw;
overflow-x: hidden;
}

body {
color: var(--foreground);
background: var(--background);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

a {
color: inherit;
text-decoration: none;
}

main {
padding: 2rem;
}

h1 {
margin-bottom: 1rem;
}

.test-result {
padding: 1rem;
margin: 0.5rem 0;
border-radius: 4px;
background: #f5f5f5;
}

.test-result.success {
background: #e6ffe6;
border: 1px solid #00cc00;
}

.test-result.error {
background: #ffe6e6;
border: 1px solid #cc0000;
}

code {
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
background: #f0f0f0;
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-size: 0.9em;
}

pre {
background: #1a1a1a;
color: #fff;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
margin: 1rem 0;
}

nav {
padding: 1rem 2rem;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
}

nav ul {
list-style: none;
display: flex;
gap: 1rem;
flex-wrap: wrap;
}

nav a {
color: #0066cc;
text-decoration: underline;
}

nav a:hover {
color: #0044aa;
}
22 changes: 22 additions & 0 deletions integration/templates/next-cache-components/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import './globals.css';
import { ClerkProvider } from '@clerk/nextjs';
import { Suspense } from 'react';

export const metadata = {
title: 'Next.js Cache Components Test',
description: 'Integration tests for Next.js cache components with Clerk',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<body>
<Suspense fallback={<div>Loading...</div>}>
<ClerkProvider>
{children}
</ClerkProvider>
</Suspense>
</body>
</html>
);
}
Loading
Loading