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/tangy-sides-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/nextjs': patch
---

Updates middleware location check to account for proxy.ts in next 16+ applications.
38 changes: 29 additions & 9 deletions integration/tests/middleware-placement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,16 @@ test.describe('next start - missing middleware @quickstart', () => {
});

test('Display error for missing middleware', async ({ page, context }) => {
const { version } = await detectNext(app);
const major = parseSemverMajor(version) ?? 0;
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();

expect(app.serveOutput).toContain('Your Middleware exists at ./src/middleware.(ts|js)');
const expectedMessage =
major >= 16
? 'Your Middleware exists at ./src/middleware.(ts|js) or proxy.(ts|js)'
: 'Your Middleware exists at ./src/middleware.(ts|js)';
expect(app.serveOutput).toContain(expectedMessage);
});
});

Expand Down Expand Up @@ -105,10 +111,16 @@ test.describe('next start - invalid middleware at root on src/ @quickstart', ()
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();

expect(app.serveOutput).not.toContain('Your Middleware exists at ./src/middleware.(ts|js)');
expect(app.serveOutput).toContain(
'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./middleware.ts',
);
const expectedMessage =
major >= 16
? 'Your Middleware exists at ./src/middleware.(ts|js) or proxy.(ts|js)'
: 'Your Middleware exists at ./src/middleware.(ts|js)';
expect(app.serveOutput).not.toContain(expectedMessage);
const expectedError =
major >= 16
? 'Clerk: clerkMiddleware() was not run, your middleware or proxy file might be misplaced. Move your middleware or proxy file to ./src/middleware.ts. Currently located at ./middleware.ts'
: 'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./middleware.ts';
expect(app.serveOutput).toContain(expectedError);
});

test('Does not display misplaced middleware error on Next 16+', async ({ page, context }) => {
Expand Down Expand Up @@ -142,11 +154,19 @@ test.describe('next start - invalid middleware inside app on src/ @quickstart',
page,
context,
}) => {
const { version } = await detectNext(app);
const major = parseSemverMajor(version) ?? 0;
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
expect(app.serveOutput).not.toContain('Your Middleware exists at ./src/middleware.(ts|js)');
expect(app.serveOutput).toContain(
'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./src/app/middleware.ts',
);
const expectedMessage =
major >= 16
? 'Your Middleware exists at ./src/middleware.(ts|js) or proxy.(ts|js)'
: 'Your Middleware exists at ./src/middleware.(ts|js)';
expect(app.serveOutput).not.toContain(expectedMessage);
const expectedError =
major >= 16
? 'Clerk: clerkMiddleware() was not run, your middleware or proxy file might be misplaced. Move your middleware or proxy file to ./src/middleware.ts. Currently located at ./src/app/middleware.ts'
: 'Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./src/middleware.ts. Currently located at ./src/app/middleware.ts';
expect(app.serveOutput).toContain(expectedError);
});
});
5 changes: 3 additions & 2 deletions packages/nextjs/src/app-router/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { unauthorized } from '../../server/nextErrors';
import type { AuthProtect } from '../../server/protect';
import { createProtect } from '../../server/protect';
import { decryptClerkRequestData } from '../../server/utils';
import { isNextWithUnstableServerActions } from '../../utils/sdk-versions';
import { isNext16OrHigher, isNextWithUnstableServerActions } from '../../utils/sdk-versions';
import { buildRequestLike } from './utils';

/**
Expand Down Expand Up @@ -81,7 +81,8 @@ export const auth: AuthFn = (async (options?: AuthOptions) => {

try {
const isSrcAppDir = await import('../../server/fs/middleware-location.js').then(m => m.hasSrcAppDir());
return [`Your Middleware exists at ./${isSrcAppDir ? 'src/' : ''}middleware.(ts|js)`];
const fileName = isNext16OrHigher ? 'middleware.(ts|js) or proxy.(ts|js)' : 'middleware.(ts|js)';
return [`Your Middleware exists at ./${isSrcAppDir ? 'src/' : ''}${fileName}`];
} catch {
return [];
}
Expand Down
16 changes: 12 additions & 4 deletions packages/nextjs/src/server/fs/middleware-location.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isNext16OrHigher } from '../../utils/sdk-versions';
import { nodeCwdOrThrow, nodeFsOrThrow, nodePathOrThrow } from './utils';

function hasSrcAppDir() {
Expand All @@ -12,12 +13,17 @@ function hasSrcAppDir() {

function suggestMiddlewareLocation() {
const fileExtensions = ['ts', 'js'] as const;
// Next.js 16+ supports both middleware.ts (Edge runtime) and proxy.ts (Node.js runtime)
const fileNames = isNext16OrHigher ? ['middleware', 'proxy'] : ['middleware'];
const fileNameDisplay = isNext16OrHigher ? 'middleware or proxy' : 'middleware';

const suggestionMessage = (
fileName: string,
extension: (typeof fileExtensions)[number],
to: 'src/' | '',
from: 'src/app/' | 'app/' | '',
) =>
`Clerk: clerkMiddleware() was not run, your middleware file might be misplaced. Move your middleware file to ./${to}middleware.${extension}. Currently located at ./${from}middleware.${extension}`;
`Clerk: clerkMiddleware() was not run, your ${fileNameDisplay} file might be misplaced. Move your ${fileNameDisplay} file to ./${to}${fileName}.${extension}. Currently located at ./${from}${fileName}.${extension}`;

const { existsSync } = nodeFsOrThrow();
const path = nodePathOrThrow();
Expand All @@ -31,9 +37,11 @@ function suggestMiddlewareLocation() {
to: 'src/' | '',
from: 'src/app/' | 'app/' | '',
): string | undefined => {
for (const fileExtension of fileExtensions) {
if (existsSync(path.join(basePath, `middleware.${fileExtension}`))) {
return suggestionMessage(fileExtension, to, from);
for (const fileName of fileNames) {
for (const fileExtension of fileExtensions) {
if (existsSync(path.join(basePath, `${fileName}.${fileExtension}`))) {
return suggestionMessage(fileName, fileExtension, to, from);
}
}
}
return undefined;
Expand Down
165 changes: 165 additions & 0 deletions packages/nextjs/src/utils/__tests__/sdk-versions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

describe('sdk-versions', () => {
beforeEach(() => {
// Clear module cache to allow re-importing with different mocks
vi.resetModules();
});

describe('meetsNextMinimumVersion', () => {
it('should return true when version meets minimum major version', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '16.0.0' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(true);
});

it('should return true when version exceeds minimum major version', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '17.0.0' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(true);
});

it('should return false when version is below minimum major version', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '15.9.9' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(false);
});

it('should return false when version is exactly one below minimum', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '15.0.0' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(false);
});

it('should handle patch versions correctly', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '16.5.3' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(true);
});

it('should handle beta/prerelease versions correctly', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '16.0.0-beta.1' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(true);
});

it('should return false when version is missing', async () => {
vi.doMock('next/package.json', () => ({
default: {},
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(false);
});

it('should return false when version is null', async () => {
vi.doMock('next/package.json', () => ({
default: { version: null },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(false);
});

it('should return false when version is undefined', async () => {
vi.doMock('next/package.json', () => ({
default: { version: undefined },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(false);
});

it('should return false when version is an empty string', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(false);
});

it('should return false when version cannot be parsed as a number', async () => {
vi.doMock('next/package.json', () => ({
default: { version: 'invalid-version' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(false);
});

it('should handle single-digit major versions', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '9.0.0' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(false);
});

it('should handle double-digit major versions', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '20.0.0' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(true);
});

it('should handle version strings with leading zeros', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '016.0.0' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(true);
});
});

describe('isNext16OrHigher', () => {
it('should be a boolean value', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '16.0.0' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(typeof isNext16OrHigher).toBe('boolean');
});

it('should correctly identify Next.js 16', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '16.0.0' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(true);
});

it('should correctly identify Next.js 15 as not 16+', async () => {
vi.doMock('next/package.json', () => ({
default: { version: '15.2.3' },
}));

const { isNext16OrHigher } = await import('../sdk-versions.js');
expect(isNext16OrHigher).toBe(false);
});
});
});
20 changes: 17 additions & 3 deletions packages/nextjs/src/utils/sdk-versions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import nextPkg from 'next/package.json';

const isNext13 = nextPkg.version.startsWith('13.');
function meetsNextMinimumVersion(minimumMajorVersion: number) {
if (!nextPkg?.version) {
return false;
}

const majorVersion = parseInt(nextPkg.version.split('.')[0], 10);
return !isNaN(majorVersion) && majorVersion >= minimumMajorVersion;
}

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

/**
* Those versions are affected by a bundling issue that will break the application if `node:fs` is used inside a server function.
* The affected versions are >=next@13.5.4 and <=next@14.0.4
*/
const isNextWithUnstableServerActions = isNext13 || nextPkg.version.startsWith('14.0');
const isNextWithUnstableServerActions = isNext13 || (nextPkg?.version?.startsWith('14.0') ?? false);

/**
* Next.js 16+ renamed middleware.ts to proxy.ts
*/
const isNext16OrHigher = meetsNextMinimumVersion(16);

export { isNext13, isNextWithUnstableServerActions };
export { isNext13, isNextWithUnstableServerActions, isNext16OrHigher };
Loading