Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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.
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
Copy Markdown
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);
});
});
78 changes: 78 additions & 0 deletions integration/tests/vinext-protect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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 @protect', () => {
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('unauthenticated user cannot access protected page', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await u.page.goToRelative('/protected');

// vinext returns a 404 page for auth.protect() when unauthenticated,
// unlike Next.js which redirects to sign-in
await expect(u.page.getByText(/not found/i)).toBeVisible();
});

test('authenticated user can access protected page', 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.goToRelative('/protected');

await expect(u.page.getByRole('heading', { name: /Protected Page/i })).toBeVisible();
await expect(u.page.getByText(/User ID:/i)).toBeVisible();
});

test('API route returns auth state correctly', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
const apiUrl = new URL('/api/me', app.serverUrl).toString();

// When signed out, userId should be null
const signedOutResponse = await u.page.request.get(apiUrl);
expect(signedOutResponse.ok()).toBe(true);
const signedOutData = await signedOutResponse.json();
expect(signedOutData.userId).toBeNull();

// Sign in
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();

// When signed in, userId and sessionId should be present
const signedInResponse = await u.page.request.get(apiUrl);
expect(signedInResponse.ok()).toBe(true);
const signedInData = await signedInResponse.json();
expect(signedInData.userId).not.toBeNull();
expect(signedInData.sessionId).not.toBeNull();
});
});
Loading
Loading