Skip to content
Draft
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
19 changes: 18 additions & 1 deletion templates/ords-remix-jwt-sample/app/entry.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,30 @@ import {
import { hydrateRoot } from 'react-dom/client';
import { MuiProvider } from './mui/MuiProvider';

const SERVER_HEAD_PATTERN = /<!--start head-->[\s\S]*?<!--end head-->/;

/**
* Removes the static server-rendered head so the client portal can own it.
*/
function removeServerHead() : void {
document.head.innerHTML = document.head.innerHTML.replace(SERVER_HEAD_PATTERN, '');
}

startTransition(() => {
const rootElement = document.getElementById('root');

if (rootElement === null) {
throw new Error('Could not find the root element to hydrate.');
}

hydrateRoot(
document,
rootElement,
<StrictMode>
<MuiProvider>
<RemixBrowser />
</MuiProvider>
</StrictMode>,
);

removeServerHead();
});
164 changes: 39 additions & 125 deletions templates/ords-remix-jwt-sample/app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,126 +11,24 @@
/* eslint-disable jsdoc/require-param-description */
/* eslint-disable jsdoc/require-returns */
/* eslint-disable jsdoc/require-jsdoc */
/* eslint-disable no-magic-numbers */
/* eslint-disable no-console */
/* eslint-disable no-param-reassign */
import { PassThrough } from 'node:stream';

import type {
AppLoadContext, EntryContext,
} from '@remix-run/node';
import { createReadableStreamFromReadable } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import { isbot } from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
import { renderToString } from 'react-dom/server';
import { MuiProvider } from './mui/MuiProvider';
import createEmotionCache from './mui/createEmotionCache';
import { Head } from './root';

const ABORT_DELAY = 5_000;

function handleBotRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const {
pipe, abort,
} = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onAllReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set('Content-Type', 'text/html');
const HEAD_START = '<!--start head-->';
const HEAD_END = '<!--end head-->';

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);

setTimeout(abort, ABORT_DELAY);
});
}

function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const {
pipe, abort,
} = renderToPipeableStream(
<MuiProvider>
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>
</MuiProvider>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set('Content-Type', 'text/html');

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);

setTimeout(abort, ABORT_DELAY);
});
function renderDocument(
head: string,
body: string,
): string {
return `<!DOCTYPE html><html lang="en"><head>${HEAD_START}${head}${HEAD_END}</head><body><div id="root">${body}</div></body></html>`;
}

/**
Expand All @@ -154,17 +52,33 @@ export default function handleRequest(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext,
) {
return isbot(request.headers.get('user-agent') || '')
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
);
const rootRouteModule = remixContext.routeModules.root;
const headContext: EntryContext = {
...remixContext,
routeModules: {
...remixContext.routeModules,
root: {
...rootRouteModule,
default: Head,
ErrorBoundary: Head,
},
},
};

const head = renderToString(
<RemixServer context={headContext} url={request.url} />,
);

const body = renderToString(
<MuiProvider emotionCache={createEmotionCache()}>
<RemixServer context={remixContext} url={request.url} />
</MuiProvider>,
);

responseHeaders.set('Content-Type', 'text/html');

return new Response(renderDocument(head, body), {
headers: responseHeaders,
status: responseStatusCode,
});
}
21 changes: 11 additions & 10 deletions templates/ords-remix-jwt-sample/app/mui/MuiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,29 @@
import { CacheProvider } from '@emotion/react';
import React from 'react';
import { ThemeProvider } from '@mui/material';
import createCache from '@emotion/cache';
import type { EmotionCache } from '@emotion/react';
import createEmotionCache from './createEmotionCache';
import theme from './theme';

/**
* Create an instance of the emotion cache on every request to make the style
* configuration available to all components in the component tree.
* @returns an instance of the emotion cache.
*/
function createEmotionCache(): EmotionCache {
return createCache({ key: 'css' });
interface MuiProviderProps {
children: React.ReactNode;
emotionCache?: EmotionCache;
}

/**
* Provider used to wrap the App root component to make all of the
* MUI style configurations available.
* @param root0 the Page React Nodes.
* @param root0.children the page React Node children's.
* @param root0.emotionCache the Emotion cache used for SSR and hydration.
* @see {@link https://mui.com/material-ui/guides/server-rendering/}
* @returns the react root component.
*/
export function MuiProvider({ children }: { children: React.ReactNode }): React.ReactNode {
const cache = createEmotionCache();
export function MuiProvider({
children,
emotionCache,
}: MuiProviderProps): React.ReactNode {
const [cache] = React.useState(() => emotionCache ?? createEmotionCache());

return (
<CacheProvider value={cache}>
Expand Down
18 changes: 18 additions & 0 deletions templates/ords-remix-jwt-sample/app/mui/createEmotionCache.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
**
** Copyright (c) 2024, Oracle and/or its affiliates.
** All rights reserved
** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
*/
import createCache from '@emotion/cache';
import type { EmotionCache } from '@emotion/react';

/**
* Creates the Emotion cache used by both the server and the client.
* @returns the Emotion cache instance.
*/
export default function createEmotionCache(): EmotionCache {
return createCache({
key: 'css',
});
}
Loading