Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nextjs): Various DX improvements #2347

Merged
merged 5 commits into from
Dec 14, 2023
Merged
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
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
*/
dimkl marked this conversation as resolved.
Show resolved Hide resolved
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';
dimkl marked this conversation as resolved.
Show resolved Hide resolved
export { clerkClient } from './clerkClient';
Copy link
Member

Choose a reason for hiding this comment

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

After this PR only the following exported types will be available from the @clerk/backend : Organization, Session, User, WebhookEvent, WebhookEventType
We should export all those.

Copy link
Member Author

Choose a reason for hiding this comment

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

What about the API types?

Copy link
Member Author

Choose a reason for hiding this comment

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

Adding a TODO, I will update the types in a 2nd PR right after


/**
* 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"
dimkl marked this conversation as resolved.
Show resolved Hide resolved
},
"include": ["src"]
}
Loading
Loading