Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/quiet-waves-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/nextjs': patch
---

Make `@clerk/nextjs` ESM-safe for non-Node.js runtimes like Cloudflare Workers (vinext). Replaces `require('server-only')`, `require('node:fs')`, and `require('next/navigation')` with ESM-compatible alternatives.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ jobs:
"vue",
"nuxt",
"react-router",
"vinext",
"custom",
]
test-project: ["chrome"]
Expand Down
2 changes: 2 additions & 0 deletions integration/presets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { nuxt } from './nuxt';
import { react } from './react';
import { reactRouter } from './react-router';
import { tanstack } from './tanstack';
import { vinext } from './vinext';
import { vue } from './vue';

export const appConfigs = {
Expand All @@ -22,6 +23,7 @@ export const appConfigs = {
astro,
tanstack,
nuxt,
vinext,
vue,
reactRouter,
secrets: {
Expand Down
16 changes: 16 additions & 0 deletions integration/presets/vinext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { applicationConfig } from '../models/applicationConfig';
import { templates } from '../templates';
import { linkPackage } from './utils';

const app = applicationConfig()
.setName('vinext-app')
.useTemplate(templates['vinext-app'])
.setEnvFormatter('public', key => `NEXT_PUBLIC_${key}`)
.addScript('setup', 'pnpm install')
.addScript('dev', 'pnpm dev')
.addScript('build', 'pnpm build')
.addScript('serve', 'pnpm start')
.addDependency('@clerk/nextjs', linkPackage('nextjs'))
.addDependency('@clerk/shared', linkPackage('shared'));

export const vinext = { app } as const;
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const templates = {
'react-router-node': resolve(__dirname, './react-router-node'),
'react-router-library': resolve(__dirname, './react-router-library'),
'custom-flows-react-vite': resolve(__dirname, './custom-flows-react-vite'),
'vinext-app': resolve(__dirname, './vinext-app'),
} as const;

if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Expand Down
4 changes: 4 additions & 0 deletions integration/templates/vinext-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
.next
dist
.vinext
14 changes: 14 additions & 0 deletions integration/templates/vinext-app/app/api/me/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { auth } from '@clerk/nextjs/server';

export async function GET() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the mentioned files exist and inspect their function signatures
echo "=== Checking integration/templates/vinext-app/app/api/me/route.ts ==="
if [ -f "integration/templates/vinext-app/app/api/me/route.ts" ]; then
  cat -n "integration/templates/vinext-app/app/api/me/route.ts"
else
  echo "File not found"
fi

echo ""
echo "=== Checking integration/templates/vinext-app/app/sign-in/[[...sign-in]]/page.tsx ==="
if [ -f "integration/templates/vinext-app/app/sign-in/[[...sign-in]]/page.tsx" ]; then
  cat -n "integration/templates/vinext-app/app/sign-in/[[...sign-in]]/page.tsx"
else
  echo "File not found"
fi

echo ""
echo "=== Checking integration/templates/vinext-app/app/sign-up/[[...sign-up]]/page.tsx ==="
if [ -f "integration/templates/vinext-app/app/sign-up/[[...sign-up]]/page.tsx" ]; then
  cat -n "integration/templates/vinext-app/app/sign-up/[[...sign-up]]/page.tsx"
else
  echo "File not found"
fi

Repository: clerk/javascript

Length of output: 950


Add explicit return types on exported template functions.

Exported functions in these files lack explicit return type annotations:

  • integration/templates/vinext-app/app/api/me/route.ts line 3: GET()
  • integration/templates/vinext-app/app/sign-in/[[...sign-in]]/page.tsx line 2: Page()
  • integration/templates/vinext-app/app/sign-up/[[...sign-up]]/page.tsx line 2: Page()

Per TypeScript guidelines, always define explicit return types for public APIs.

Proposed fixes
-export async function GET() {
+export async function GET(): Promise<Response> {
   const authObj = await auth();
   return new Response(
-export default function Page() {
+export default function Page(): JSX.Element {
  return <SignIn />;
}
-export default function Page() {
+export default function Page(): JSX.Element {
  return <SignUp />;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function GET() {
export async function GET(): Promise<Response> {
const authObj = await auth();
return new Response(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integration/templates/vinext-app/app/api/me/route.ts` at line 3, Exported
template functions are missing explicit return type annotations; add them to the
three functions: annotate GET in
integration/templates/vinext-app/app/api/me/route.ts as async GET():
Promise<Response> (or Promise<NextResponse> if you’re using NextResponse), and
annotate the React page components Page in
integration/templates/vinext-app/app/sign-in/[[...sign-in]]/page.tsx and
integration/templates/vinext-app/app/sign-up/[[...sign-up]]/page.tsx as function
Page(): JSX.Element (or async Page(): Promise<JSX.Element> if they are async),
updating the function signatures only to include these explicit return types.

const authObj = await auth();
return new Response(
JSON.stringify({
userId: authObj.userId,
sessionId: authObj.sessionId,
}),
{
headers: { 'content-type': 'application/json' },
},
);
}
21 changes: 21 additions & 0 deletions integration/templates/vinext-app/app/auth-display.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

import { SignInButton, UserButton, useAuth } from '@clerk/nextjs';

export function AuthDisplay() {
const { isSignedIn } = useAuth();

if (isSignedIn) {
return (
<div>
<UserButton />
</div>
);
}

return (
<div>
<SignInButton />
</div>
);
}
11 changes: 11 additions & 0 deletions integration/templates/vinext-app/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ClerkProvider } from '@clerk/nextjs';

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html lang='en'>
<body>{children}</body>
</html>
</ClerkProvider>
);
}
13 changes: 13 additions & 0 deletions integration/templates/vinext-app/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { auth } from '@clerk/nextjs/server';
import { AuthDisplay } from './auth-display';

export default async function Home() {
const { userId } = await auth();
return (
<main>
<h1>vinext + Clerk</h1>
<AuthDisplay />
<p data-clerk-user-id={userId || ''}>{userId ? `server-user-id: ${userId}` : 'server-signed-out'}</p>
</main>
);
}
11 changes: 11 additions & 0 deletions integration/templates/vinext-app/app/protected/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { auth } from '@clerk/nextjs/server';

export default async function Protected() {
const { userId } = await auth.protect();
return (
<main>
<h1>Protected Page</h1>
<p>User ID: {userId}</p>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SignIn } from '@clerk/nextjs';
export default function Page() {
return <SignIn />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SignUp } from '@clerk/nextjs';
export default function Page() {
return <SignUp />;
}
3 changes: 3 additions & 0 deletions integration/templates/vinext-app/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { clerkMiddleware } from '@clerk/nextjs/server';

export default clerkMiddleware();
26 changes: 26 additions & 0 deletions integration/templates/vinext-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "vinext-app",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite dev",
"start": "vite preview"
},
"dependencies": {
"next": "16.1.6",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.25.6",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"react-server-dom-webpack": "^19.2.4",
"typescript": "^5",
"vinext": "^0.0.15",
"vite": "^7.3.1"
}
}
22 changes: 22 additions & 0 deletions integration/templates/vinext-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
9 changes: 9 additions & 0 deletions integration/templates/vinext-app/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import vinext from 'vinext';
import { defineConfig } from 'vite';

export default defineConfig({
server: {
port: parseInt(process.env.PORT || '5173'),
},
plugins: [vinext()],
});
116 changes: 116 additions & 0 deletions integration/tests/vinext-auth-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { expect, test } from '@playwright/test';

import type { Application } from '../models/application';
import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils } from '../testUtils';

test.describe('vinext @vinext @auth-state', () => {
test.describe.configure({ mode: 'serial' });

let app: Application;
let fakeUser: FakeUser;

test.beforeAll(async () => {
test.setTimeout(120_000);
app = await appConfigs.vinext.app.clone().commit();
await app.setup();
await app.withEnv(appConfigs.envs.withEmailCodes);
await app.dev();

const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

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

test('first visit shows signed-out state', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();

await u.po.expect.toBeSignedOut();
await expect(u.page.getByText('server-signed-out')).toBeVisible();
});

test('page refresh preserves signed-in state', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();

await page.reload();
await u.page.waitForClerkJsLoaded();

await u.po.expect.toBeSignedIn();
await expect(u.page.getByText(/server-user-id:/)).toBeVisible();
});

test('new tab shares auth state', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();

await u.page.goToAppHome();
const mainUserId = await u.page.locator('p[data-clerk-user-id]').getAttribute('data-clerk-user-id');
expect(mainUserId).toBeTruthy();

await u.tabs.runInNewTab(async m => {
await m.page.goToAppHome();
await m.page.waitForClerkJsLoaded();

await m.po.expect.toBeSignedIn();

const tabUserId = await m.page.locator('p[data-clerk-user-id]').getAttribute('data-clerk-user-id');
expect(tabUserId).toBe(mainUserId);
});
});

test('sign out clears auth state', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();

await u.page.goToAppHome();
await u.po.userButton.waitForMounted();
await u.po.userButton.toggleTrigger();
await u.po.userButton.waitForPopover();
await u.po.userButton.triggerSignOut();

await u.po.expect.toBeSignedOut();

await page.reload();
await u.page.waitForClerkJsLoaded();

await u.po.expect.toBeSignedOut();
await expect(u.page.getByText('server-signed-out')).toBeVisible();
});

test('server and client auth state match', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();

await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();

const serverUserId = await u.page.locator('p[data-clerk-user-id]').getAttribute('data-clerk-user-id');
expect(serverUserId).toBeTruthy();

const clientUserId = await page.evaluate(() => window.Clerk?.user?.id);
expect(clientUserId).toBeTruthy();

expect(serverUserId).toBe(clientUserId);
});
});
Loading
Loading