From baabe78a212be1013e6641322f699dae27ebf163 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Fri, 20 Oct 2023 12:32:53 -0500 Subject: [PATCH 1/4] feat(remix): Support defer() usage in rootAuthLoader() --- packages/remix/src/ssr/rootAuthLoader.ts | 14 ++++++ packages/remix/src/ssr/utils.ts | 61 ++++++++++++++++++++---- playground/remix-node/app/root.tsx | 19 +++++--- 3 files changed, 77 insertions(+), 17 deletions(-) diff --git a/packages/remix/src/ssr/rootAuthLoader.ts b/packages/remix/src/ssr/rootAuthLoader.ts index 5158528a012..09526280090 100644 --- a/packages/remix/src/ssr/rootAuthLoader.ts +++ b/packages/remix/src/ssr/rootAuthLoader.ts @@ -1,11 +1,15 @@ import { sanitizeAuthObject } from '@clerk/backend'; +import type { defer } from '@remix-run/server-runtime'; +import { isDeferredData } from '@remix-run/server-runtime/dist/responses'; import { invalidRootLoaderCallbackReturn } from '../errors'; import { authenticateRequest } from './authenticateRequest'; import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types'; import { assertValidHandlerResult, + getResponseClerkState, injectAuthIntoRequest, + injectRequestStateIntoDeferredData, injectRequestStateIntoResponse, interstitialJsonResponse, isRedirect, @@ -64,6 +68,16 @@ export const rootAuthLoader: RootAuthLoader = async ( const handlerResult = await handler(injectAuthIntoRequest(args, sanitizeAuthObject(requestState.toAuth()))); assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn); + // When using defer(), we need to inject the clerk auth state into its internal data object. + if (isDeferredData(handlerResult)) { + return injectRequestStateIntoDeferredData( + // This is necessary because the DeferredData type is not exported from remix. + handlerResult as unknown as ReturnType, + requestState, + args.context, + ); + } + if (isResponse(handlerResult)) { try { // respect and pass-through any redirects without modifying them diff --git a/packages/remix/src/ssr/utils.ts b/packages/remix/src/ssr/utils.ts index d8607a32823..f6fd8aa99ed 100644 --- a/packages/remix/src/ssr/utils.ts +++ b/packages/remix/src/ssr/utils.ts @@ -1,6 +1,6 @@ import type { AuthObject, RequestState } from '@clerk/backend'; import { constants, debugRequestState, loadInterstitialFromLocal } from '@clerk/backend'; -import type { AppLoadContext } from '@remix-run/server-runtime'; +import type { AppLoadContext, defer } from '@remix-run/server-runtime'; import { json } from '@remix-run/server-runtime'; import cookie from 'cookie'; @@ -108,9 +108,51 @@ export const injectRequestStateIntoResponse = async ( context: AppLoadContext, ) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { reason, message, isSignedIn, isInterstitial, ...rest } = requestState; const clone = response.clone(); const data = await clone.json(); + + const { clerkState, headers } = getResponseClerkState(requestState, context); + + // set the correct content-type header in case the user returned a `Response` directly + // without setting the header, instead of using the `json()` helper + clone.headers.set(constants.Headers.ContentType, constants.ContentTypes.Json); + headers.forEach((value, key) => { + clone.headers.set(key, value); + }); + + return json({ ...(data || {}), ...clerkState }, clone); +}; + +export function injectRequestStateIntoDeferredData( + data: ReturnType, + requestState: RequestState, + context: AppLoadContext, +) { + const { clerkState, headers } = getResponseClerkState(requestState, context); + + // Avoid creating a new object here to retain referential equality. + data.data.clerkState = clerkState.clerkState; + + if (typeof data.init !== 'undefined') { + data.init.headers = new Headers(data.init.headers); + + headers.forEach((value, key) => { + // @ts-expect-error -- We are ensuring headers is defined above + data.init.headers.set(key, value); + }); + } + + return data; +} + +/** + * Returns the clerk state object and observability headers to be injected into a loader response. + * + * @internal + */ +export function getResponseClerkState(requestState: RequestState, context: AppLoadContext) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { reason, message, isSignedIn, isInterstitial, ...rest } = requestState; const clerkState = wrapWithClerkState({ __clerk_ssr_state: rest.toAuth(), __frontendApi: requestState.frontendApi, @@ -126,15 +168,14 @@ export const injectRequestStateIntoResponse = async ( __clerkJSUrl: getEnvVariable('CLERK_JS', context), __clerkJSVersion: getEnvVariable('CLERK_JS_VERSION', context), }); - // set the correct content-type header in case the user returned a `Response` directly - // without setting the header, instead of using the `json()` helper - clone.headers.set(constants.Headers.ContentType, constants.ContentTypes.Json); - observabilityHeadersFromRequestState(requestState).forEach((value, key) => { - clone.headers.set(key, value); - }); - return json({ ...(data || {}), ...clerkState }, clone); -}; + const headers = observabilityHeadersFromRequestState(requestState); + + return { + clerkState, + headers, + }; +} /** * Wraps obscured clerk internals with a readable `clerkState` key. diff --git a/playground/remix-node/app/root.tsx b/playground/remix-node/app/root.tsx index 94e960886f8..a869a30de21 100644 --- a/playground/remix-node/app/root.tsx +++ b/playground/remix-node/app/root.tsx @@ -1,18 +1,20 @@ -import type { DataFunctionArgs, Headers } from '@remix-run/node'; +import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node'; import type { MetaFunction } from '@remix-run/react'; -import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react'; +import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react'; import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server'; import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix'; +import { Suspense } from 'react'; export const loader = (args: DataFunctionArgs) => { return rootAuthLoader( args, ({ request }) => { const { user } = request; + const data: Promise<{ foo: string }> = new Promise(r => r({ foo: 'bar' })) console.log('root User:', user); - return { user }; + return defer({ user, data }, { headers: { 'x-bryce': 'cool guy' } }) }, { loadUser: true }, ); @@ -27,10 +29,8 @@ export function headers({ loaderHeaders: Headers; parentHeaders: Headers; }) { - return { - 'x-parent-header': parentHeaders.get('x-parent-header'), - ...getClerkDebugHeaders(loaderHeaders), - }; + console.log(loaderHeaders) + return loaderHeaders } export const meta: MetaFunction = () => { @@ -60,6 +60,11 @@ function App() { + + + {val => (<>Hello {val.foo})} + + From 15bd1c7b3b4f42a928a104e7dc2600a27d004b2d Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Fri, 20 Oct 2023 12:34:06 -0500 Subject: [PATCH 2/4] chore(repo): Add changeset --- .changeset/loud-peaches-travel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/loud-peaches-travel.md diff --git a/.changeset/loud-peaches-travel.md b/.changeset/loud-peaches-travel.md new file mode 100644 index 00000000000..889072cf143 --- /dev/null +++ b/.changeset/loud-peaches-travel.md @@ -0,0 +1,5 @@ +--- +'@clerk/remix': minor +--- + +Support usage of Remix's `defer()` method in the loader passed to `rootAuthLoader()`. From 5e874e4eb4661aa9cd948a4ff35a98aa47ec0cc0 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Fri, 20 Oct 2023 12:38:42 -0500 Subject: [PATCH 3/4] fix(remix): Remove unused import --- packages/remix/src/ssr/rootAuthLoader.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/remix/src/ssr/rootAuthLoader.ts b/packages/remix/src/ssr/rootAuthLoader.ts index 09526280090..23a8ccb83fc 100644 --- a/packages/remix/src/ssr/rootAuthLoader.ts +++ b/packages/remix/src/ssr/rootAuthLoader.ts @@ -7,7 +7,6 @@ import { authenticateRequest } from './authenticateRequest'; import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types'; import { assertValidHandlerResult, - getResponseClerkState, injectAuthIntoRequest, injectRequestStateIntoDeferredData, injectRequestStateIntoResponse, From 62b8c247ec181fe6ef1eda0e0435606e37096505 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Fri, 20 Oct 2023 17:25:18 -0500 Subject: [PATCH 4/4] Update playground/remix-node/app/root.tsx --- playground/remix-node/app/root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/remix-node/app/root.tsx b/playground/remix-node/app/root.tsx index a869a30de21..ee3a803aba4 100644 --- a/playground/remix-node/app/root.tsx +++ b/playground/remix-node/app/root.tsx @@ -14,7 +14,7 @@ export const loader = (args: DataFunctionArgs) => { console.log('root User:', user); - return defer({ user, data }, { headers: { 'x-bryce': 'cool guy' } }) + return defer({ user, data }, { headers: { 'x-clerk': '1' } }) }, { loadUser: true }, );