Skip to content

Commit

Permalink
feat(remix): Support defer() usage in rootAuthLoader()
Browse files Browse the repository at this point in the history
  • Loading branch information
BRKalow committed Oct 20, 2023
1 parent 92727ee commit baabe78
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 17 deletions.
14 changes: 14 additions & 0 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<typeof defer>,
requestState,
args.context,
);
}

if (isResponse(handlerResult)) {
try {
// respect and pass-through any redirects without modifying them
Expand Down
61 changes: 51 additions & 10 deletions packages/remix/src/ssr/utils.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<typeof defer>,
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,
Expand All @@ -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.
Expand Down
19 changes: 12 additions & 7 deletions playground/remix-node/app/root.tsx
Original file line number Diff line number Diff line change
@@ -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 },
);
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -60,6 +60,11 @@ function App() {
<Links />
</head>
<body>
<Suspense fallback="Loading...">
<Await resolve={loaderData.data}>
{val => (<>Hello {val.foo}</>)}
</Await>
</Suspense>
<Outlet />
<ScrollRestoration />
<Scripts />
Expand Down

0 comments on commit baabe78

Please sign in to comment.