Skip to content

Commit

Permalink
feat(nextjs): Improve top-level API surface and drop unsupported vers…
Browse files Browse the repository at this point in the history
…ions (#2347)

* feat(nextjs): Drop support for next@12, next<13.0.4, next<14.0.3

Drop support for NextJS v12: v12 was released on 26 Oct 2021  (~2 years ago). Security support ended on 21 Nov 2022 (~11 months ago)

Drop support for NextJS <13.0.4: Many header-related bugs and breaking changes were introduced with the 12.1.0, 12.2.0, 13.0.1, 13.0.2, 13.0.3 releases. Vercel fixed all known related-bugs with version 13.0.4. In order to support every release in the range of 12 to 13.0.4 we had to resort to various workaround/compatibility layers (example https://github.com/clerk/javascript/blob/92727eec39566278263ffa118a085493f964eb94/packages/nextjs/src/server/utils.ts#L76). If we drop support for v12 and the other problematic versions, we can also remove all the extra logic from the @clerk/nextjs package

Drop support for NextJS < 14.0.3 for the issues described here: #1436

* feat(nextjs): Deprecate `authMiddleware` in favor of `clerkMiddleware`

* feat(nextjs): Move errors to /errors

* feat(nextjs): Move server-related APIs to /server

chore(repo): Add changesets

chore(repo): Update changesets

* chore(repo): Update playground apps
  • Loading branch information
nikosdouvlis committed Dec 14, 2023
1 parent 9a637e4 commit e602d6c
Show file tree
Hide file tree
Showing 53 changed files with 74 additions and 1,080 deletions.
13 changes: 13 additions & 0 deletions .changeset/grumpy-drinks-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@clerk/nextjs': major
---

Drop support for NextJS v12: v12 was released on 26 Oct 2021. Support for security updates stopped on 21 Nov 2022.

Drop support for NextJS <13.0.4: Various header-related bugs were introduced with the 12.1.0, 12.2.0, 13.0.1, 13.0.2, 13.0.3 NextJS releases which are now fixed since next@^13.0.4. We will be dropping support for these problematic versions in order to greatly reduce complexity in our codebase.

Drop support for NextJS < 14.0.3 because of the issues described here: https://github.com/clerk/javascript/issues/1436.

Deprecate `authMiddleware` in favor of `clerkMiddleware`. For more information, see https://clerk.com/docs/upgrade-guides/v5-introduction

Move the server-side APIs from `@clerk/nextjs` to the `@clerk/nextjs/server` module: `WebhookEventType`, `WebhookEvent`, `verifyToken`, `redirectToSignIn`, `auth`, `buildClerkProps`, `clerkClient`, `currentUser`, `getAuth`, `redirectToSignUp` and `authMiddleware`. For more information, see https://clerk.com/docs/upgrade-guides/v5-introduction
5 changes: 5 additions & 0 deletions .changeset/tricky-bikes-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': major
---

Drop unused SearchParams.AuthStatus constant
5 changes: 0 additions & 5 deletions packages/backend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ const Headers = {
SecFetchDest: 'sec-fetch-dest',
} as const;

const SearchParams = {
AuthStatus: Headers.AuthStatus,
} as const;

const ContentTypes = {
Json: 'application/json',
} as const;
Expand All @@ -55,7 +51,6 @@ export const constants = {
Attributes,
Cookies,
Headers,
SearchParams,
ContentTypes,
QueryParameters,
} as const;
5 changes: 5 additions & 0 deletions packages/nextjs/errors/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"main": "../dist/cjs/errors.js",
"module": "../dist/esm/errors.js",
"types": "../dist/types/errors.d.ts"
}
8 changes: 0 additions & 8 deletions packages/nextjs/package.cjs.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,6 @@
"#components": {
"react-server": "./components.server.js",
"default": "./components.client.js"
},
"#server": {
"react-server": "./server-helpers.server.js",
"edge-light": "./server-helpers.server.js",
"worker": "./server-helpers.server.js",
"node": "./server-helpers.server.js",
"browser": "./server-helpers.client.js",
"default": "./server-helpers.client.js"
}
}
}
8 changes: 0 additions & 8 deletions packages/nextjs/package.esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,6 @@
"#components": {
"react-server": "./components.server.js",
"default": "./components.client.js"
},
"#server": {
"react-server": "./server-helpers.server.js",
"edge-light": "./server-helpers.server.js",
"worker": "./server-helpers.server.js",
"node": "./server-helpers.server.js",
"browser": "./server-helpers.client.js",
"default": "./server-helpers.client.js"
}
}
}
12 changes: 8 additions & 4 deletions packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,18 @@
"types": "./dist/types/server/index.d.ts",
"import": "./dist/esm/server/index.js",
"require": "./dist/cjs/server/index.js"
},
"./errors": {
"types": "./dist/types/errors.d.ts",
"import": "./dist/esm/errors.js",
"require": "./dist/cjs/errors.js"
}
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"files": [
"dist",
"server"
"server",
"errors"
],
"scripts": {
"build": "npm run clean && tsup",
Expand Down Expand Up @@ -74,7 +78,7 @@
"typescript": "*"
},
"peerDependencies": {
"next": ">=10",
"next": "^13.0.4 || ^14.0.3",
"react": ">=18",
"react-dom": ">=18"
},
Expand Down
6 changes: 1 addition & 5 deletions packages/nextjs/src/app-router/server/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
// import { headers } from 'next/headers';
import { headers } from 'next/headers';
import { NextRequest } from 'next/server';

export const buildRequestLike = () => {
try {
// Dynamically import next/headers, otherwise Next12 apps will break
// because next/headers was introduced in next@13
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { headers } = require('next/headers');
return new NextRequest('https://placeholder.com', { headers: headers() });
} catch (e: any) {
if (
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { isClerkAPIResponseError, isEmailLinkError, isKnownError, isMetamaskError } from './client-boundary/hooks';
34 changes: 5 additions & 29 deletions packages/nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ export {
* If you do, app router will break.
*/
export {
isClerkAPIResponseError,
isEmailLinkError,
isKnownError,
isMetamaskError,
useAuth,
useClerk,
useEmailLink,
Expand All @@ -62,31 +58,11 @@ export {
*/
// @ts-ignore
import * as ComponentsModule from '#components';
/**
* Conditionally export server-side helpers.
* This allows to import server-side helpers from the top-level path.
* We defined the runtime and the type values explicitly,
* because TS will not recognize the subpath import unless the HOST
* application sets moduleResolution to 'NodeNext'.
*/
// @ts-ignore
import * as ServerHelperModule from '#server';

import type { ServerComponentsServerModuleTypes } from './components.server';
import type { ServerHelpersServerModuleTypes } from './server-helpers.server';

export const ClerkProvider = ComponentsModule.ClerkProvider as ServerComponentsServerModuleTypes['ClerkProvider'];
export const SignedIn = ComponentsModule.SignedIn as ServerComponentsServerModuleTypes['SignedIn'];
export const SignedOut = ComponentsModule.SignedOut as ServerComponentsServerModuleTypes['SignedOut'];

export const Protect = ComponentsModule.Protect;

export const auth = ServerHelperModule.auth as ServerHelpersServerModuleTypes['auth'];
export const currentUser = ServerHelperModule.currentUser as ServerHelpersServerModuleTypes['currentUser'];
// export const getAuth = ServerHelperModule.getAuth as ServerHelpersServerModuleTypes['getAuth'];
export const clerkClient = ServerHelperModule.clerkClient as ServerHelpersServerModuleTypes['clerkClient'];
export const authMiddleware = ServerHelperModule.authMiddleware as ServerHelpersServerModuleTypes['authMiddleware'];
export const redirectToSignIn =
ServerHelperModule.redirectToSignIn as ServerHelpersServerModuleTypes['redirectToSignIn'];
export const redirectToSignUp =
ServerHelperModule.redirectToSignUp as ServerHelpersServerModuleTypes['redirectToSignUp'];
export const ClerkProvider =
ComponentsModule.ClerkProvider as unknown as ServerComponentsServerModuleTypes['ClerkProvider'];
export const SignedIn = ComponentsModule.SignedIn as unknown as ServerComponentsServerModuleTypes['SignedIn'];
export const SignedOut = ComponentsModule.SignedOut as unknown as ServerComponentsServerModuleTypes['SignedOut'];
export const Protect = ComponentsModule.Protect as unknown as ServerComponentsServerModuleTypes['Protect'];
11 changes: 0 additions & 11 deletions packages/nextjs/src/server-helpers.client.ts

This file was deleted.

18 changes: 0 additions & 18 deletions packages/nextjs/src/server-helpers.server.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,15 @@

exports[`/server public exports should not include a breaking change 1`] = `
[
"AllowlistIdentifier",
"AuthStatus",
"Client",
"DeletedObject",
"Email",
"EmailAddress",
"ExternalAccount",
"IdentificationLink",
"Invitation",
"OauthAccessToken",
"ObjectType",
"Organization",
"OrganizationInvitation",
"OrganizationMembership",
"OrganizationMembershipPublicUserData",
"PhoneNumber",
"RedirectUrl",
"SMSMessage",
"Session",
"SignInToken",
"Token",
"TokenVerificationError",
"TokenVerificationErrorReason",
"User",
"Verification",
"auth",
"authMiddleware",
"buildClerkProps",
"buildRequestUrl",
"clerkClient",
"constants",
"createAuthenticateRequest",
"createClerkClient",
"createIsomorphicRequest",
"currentUser",
"debugRequestState",
"decodeJwt",
"deserialize",
"getAuth",
"hasValidSignature",
"makeAuthObjectSerializable",
"prunePrivateMetadata",
"redirect",
"redirectToSignIn",
"redirectToSignUp",
"sanitizeAuthObject",
"signJwt",
"signedInAuthObject",
"signedOutAuthObject",
"verifyJwt",
"verifyToken",
]
`;
4 changes: 4 additions & 0 deletions packages/nextjs/src/server/authMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ export interface AuthMiddleware {
(params?: AuthMiddlewareParams): NextMiddleware;
}

/**
* @deprecated Use `clerkMiddleware` instead.
* Migration guide: https://clerk.com/docs/upgrade-guides/v5-introduction
*/
const authMiddleware: AuthMiddleware = (...args: unknown[]) => {
const [params = {}] = args as [AuthMiddlewareParams?];
const { beforeAuth, afterAuth, publicRoutes, ignoredRoutes, apiRoutes, ...options } = params;
Expand Down
1 change: 0 additions & 1 deletion packages/nextjs/src/server/clerkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,3 @@ const clerkClient = createClerkClient({
});

export { clerkClient };
export * from '@clerk/backend';
12 changes: 10 additions & 2 deletions packages/nextjs/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
export * from './clerkClient';
/**
* Generic exports
*/
export { verifyToken, createClerkClient } from '@clerk/backend';
export type { WebhookEvent, WebhookEventType } from '@clerk/backend';
export { clerkClient } from './clerkClient';

/**
* NextJS-specific exports
*/
export { buildClerkProps, getAuth } from './getAuth';
export { redirectToSignIn, redirectToSignUp } from './redirect';

export { auth } from '../app-router/server/auth';
export { currentUser } from '../app-router/server/currentUser';
export { authMiddleware } from './authMiddleware';
69 changes: 7 additions & 62 deletions packages/nextjs/src/server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,16 @@ export function getCustomAttributeFromRequest(req: RequestLike, key: string): st
}

export function getAuthKeyFromRequest(req: RequestLike, key: AuthKey): string | null | undefined {
return (
getCustomAttributeFromRequest(req, constants.Attributes[key]) ||
getHeader(req, constants.Headers[key]) ||
(key === 'AuthStatus' ? getQueryParam(req, constants.SearchParams.AuthStatus) : undefined)
);
return getCustomAttributeFromRequest(req, constants.Attributes[key]) || getHeader(req, constants.Headers[key]);
}

// Tries to extract auth status from the request using several strategies
// TODO: Rename Auth status and align the naming across media
export function getAuthStatusFromRequest(req: RequestLike): string | null | undefined {
return (
getCustomAttributeFromRequest(req, constants.Attributes.AuthStatus) ||
getHeader(req, constants.Headers.AuthStatus) ||
getQueryParam(req, constants.SearchParams.AuthStatus)
getCustomAttributeFromRequest(req, constants.Attributes.AuthStatus) || getHeader(req, constants.Headers.AuthStatus)
);
}

function getQueryParam(req: RequestLike, name: string): string | null | undefined {
if (isNextRequest(req)) {
return req.nextUrl.searchParams.get(name);
}

// Check if the request contains a parsed query object
// NextApiRequest does, but the IncomingMessage in the GetServerSidePropsContext case does not
let queryParam: string | null | undefined;
if ('query' in req) {
queryParam = req.query[name] as string | undefined;
}

// Fall back to query string
if (!queryParam) {
const qs = (req.url || '').split('?')[1];
queryParam = new URLSearchParams(qs).get(name);
}
return queryParam;
}

export function getHeader(req: RequestLike, name: string): string | null | undefined {
if (isNextRequest(req)) {
return req.headers.get(name);
Expand Down Expand Up @@ -124,22 +97,6 @@ export const setRequestHeadersOnNextResponse = (
});
};

/**
* Test whether the currently installed nextjs version supports overriding the request headers.
* This feature was added in nextjs v13.0.1
* https://github.com/vercel/next.js/pull/41380
*/
export const nextJsVersionCanOverrideRequestHeaders = () => {
try {
const headerKey = 'clerkTest';
const headerKeyInRes = `${MIDDLEWARE_HEADER_PREFIX}-${headerKey}`;
const res = NextResponse.next({ request: { headers: new Headers({ [headerKey]: 'true' }) } });
return res.headers.has(headerKeyInRes);
} catch (e) {
return false;
}
};

export const injectSSRStateIntoObject = <O, T>(obj: O, authObject: T) => {
// Serializing the state on dev env is a temp workaround for the following issue:
// https://github.com/vercel/next.js/discussions/11209|Next.js
Expand Down Expand Up @@ -188,23 +145,11 @@ export function decorateRequest(
}

if (rewriteURL) {
if (nextJsVersionCanOverrideRequestHeaders()) {
// If we detect that the host app is using a nextjs installation that reliably sets the
// request headers, we don't need to fall back to the searchParams strategy.
// In this case, we won't set them at all in order to avoid having them visible in the req.url
setRequestHeadersOnNextResponse(res, req, {
[constants.Headers.AuthStatus]: status,
[constants.Headers.AuthMessage]: message || '',
[constants.Headers.AuthReason]: reason || '',
});
} else {
res.headers.set(constants.Headers.AuthStatus, status);
res.headers.set(constants.Headers.AuthMessage, message || '');
res.headers.set(constants.Headers.AuthReason, reason || '');
rewriteURL.searchParams.set(constants.SearchParams.AuthStatus, status);
rewriteURL.searchParams.set(constants.Headers.AuthMessage, message || '');
rewriteURL.searchParams.set(constants.Headers.AuthReason, reason || '');
}
setRequestHeadersOnNextResponse(res, req, {
[constants.Headers.AuthStatus]: status,
[constants.Headers.AuthMessage]: message || '',
[constants.Headers.AuthReason]: reason || '',
});
res.headers.set(nextConstants.Headers.NextRewrite, rewriteURL.href);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/nextjs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"skipLibCheck": true,
"sourceMap": false,
"strict": true,
"target": "ES2020"
"target": "ES2020",
"rootDir": "src"
},
"include": ["src"]
}

0 comments on commit e602d6c

Please sign in to comment.