Skip to content

Commit

Permalink
Align server code with other loathers projects
Browse files Browse the repository at this point in the history
  • Loading branch information
gausie committed Sep 30, 2024
1 parent 334786f commit 305a845
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 70 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
"engines": {
"node": ">= 18.20"
},
"packageManager": "yarn@4.5.0"
"packageManager": "yarn@4.5.0",
"resolutions": {
"@emotion/utils": "1.4.0"
}
}
46 changes: 21 additions & 25 deletions packages/excavator-web/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
import createEmotionCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { RemixBrowser } from "@remix-run/react";
import React, { useState } from "react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

import { ClientStyleContext } from "./context.js";
import createEmotionCache, { defaultCache } from "./createEmotionCache.js";
const hydrate = () => {
const emotionCache = createEmotionCache({ key: "css" });

interface ClientCacheProviderProps {
children: React.ReactNode;
}

function ClientCacheProvider({ children }: ClientCacheProviderProps) {
const [cache, setCache] = useState(defaultCache);
startTransition(() => {
hydrateRoot(
document,
<StrictMode>
<CacheProvider value={emotionCache}>
<RemixBrowser />
</CacheProvider>
</StrictMode>,
);
});
};

function reset() {
setCache(createEmotionCache());
}

return (
<ClientStyleContext.Provider value={{ reset }}>
<CacheProvider value={cache}>{children}</CacheProvider>
</ClientStyleContext.Provider>
);
if (typeof requestIdleCallback === "function") {
requestIdleCallback(hydrate);
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
setTimeout(hydrate, 1);
}

hydrateRoot(
document,
<ClientCacheProvider>
<RemixBrowser />
</ClientCacheProvider>,
);
155 changes: 126 additions & 29 deletions packages/excavator-web/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,142 @@
import { CacheProvider } from "@emotion/react";
import createEmotionCache from "@emotion/cache";
import { CacheProvider as EmotionCacheProvider } from "@emotion/react";
import createEmotionServer from "@emotion/server/create-instance";
import type { EntryContext } from "@remix-run/node";
import type { AppLoadContext, EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import React from "react";
import { renderToString } from "react-dom/server";
import { Response } from "@remix-run/web-fetch";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import { PassThrough } from "stream";

// Depends on the runtime you choose
import { ServerStyleContext } from "./context.js";
import createEmotionCache from "./createEmotionCache.js";
const ABORT_DELAY = 5000;

export default function handleRequest(
const handleRequest = (
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
const cache = createEmotionCache();
const { extractCriticalToChunks } = createEmotionServer(cache);
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadContext: AppLoadContext,
) =>
isbot(request.headers.get("user-agent"))
? handleBotRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
)
: handleBrowserRequest(
request,
responseStatusCode,
responseHeaders,
remixContext,
);
export default handleRequest;

const html = renderToString(
<ServerStyleContext.Provider value={null}>
<CacheProvider value={cache}>
const handleBotRequest = (
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) =>
new Promise((resolve, reject) => {
let shellRendered = false;
const emotionCache = createEmotionCache({ key: "css" });

const { pipe, abort } = renderToPipeableStream(
<EmotionCacheProvider value={emotionCache}>
<RemixServer context={remixContext} url={request.url} />
</CacheProvider>
</ServerStyleContext.Provider>,
);
</EmotionCacheProvider>,
{
onAllReady: () => {
shellRendered = true;
const reactBody = new PassThrough();
const emotionServer = createEmotionServer(emotionCache);

const bodyWithStyles = emotionServer.renderStylesToNodeStream();
reactBody.pipe(bodyWithStyles);

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

resolve(
// @ts-expect-error: Stream is not compatible with ReadWriteStream
new Response(bodyWithStyles, {
headers: responseHeaders,
status: responseStatusCode,
}),
);

pipe(reactBody);
},
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);
}
},
},
);

const chunks = extractCriticalToChunks(html);
setTimeout(abort, ABORT_DELAY);
});

const markup = renderToString(
<ServerStyleContext.Provider value={chunks.styles}>
<CacheProvider value={cache}>
const handleBrowserRequest = (
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) =>
new Promise((resolve, reject) => {
const emotionCache = createEmotionCache({ key: "css" });

let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<EmotionCacheProvider value={emotionCache}>
<RemixServer context={remixContext} url={request.url} />
</CacheProvider>
</ServerStyleContext.Provider>,
);
</EmotionCacheProvider>,
{
onShellReady: () => {
shellRendered = true;
const reactBody = new PassThrough();
const emotionServer = createEmotionServer(emotionCache);

const bodyWithStyles = emotionServer.renderStylesToNodeStream();
reactBody.pipe(bodyWithStyles);

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

resolve(
// @ts-expect-error: Stream is not compatible with ReadWriteStream
new Response(bodyWithStyles, {
headers: responseHeaders,
status: responseStatusCode,
}),
);

responseHeaders.set("Content-Type", "text/html");
pipe(reactBody);
},
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);
}
},
},
);

return new Response(`<!DOCTYPE html>${markup}`, {
status: responseStatusCode,
headers: responseHeaders,
setTimeout(abort, ABORT_DELAY);
});
}
16 changes: 1 addition & 15 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3555,27 +3555,13 @@ __metadata:
languageName: node
linkType: hard

"@emotion/utils@npm:^1.2.1":
version: 1.2.1
resolution: "@emotion/utils@npm:1.2.1"
checksum: 10c0/db43ca803361740c14dfb1cca1464d10d27f4c8b40d3e8864e6932ccf375d1450778ff4e4eadee03fb97f2aeb18de9fae98294905596a12ff7d4cd1910414d8d
languageName: node
linkType: hard

"@emotion/utils@npm:^1.4.0":
"@emotion/utils@npm:1.4.0":
version: 1.4.0
resolution: "@emotion/utils@npm:1.4.0"
checksum: 10c0/b2ae698d6e935f4961a8349286b5b0a6117a16e179459cbf9c8d97d5daa7d96c99876b950f09b1a793d6b295713b2c8f89544bd8c3f26b8e4db60a218a0d4c42
languageName: node
linkType: hard

"@emotion/utils@npm:^1.4.1":
version: 1.4.1
resolution: "@emotion/utils@npm:1.4.1"
checksum: 10c0/f4704e0bdf48062fd6eb9c64771c88f521aab1e108a48cb23d65b6438597c63a6945301cef4c43611e79e0e76a304ec5481c31025ea8f573d7ad5423d747602c
languageName: node
linkType: hard

"@emotion/weak-memoize@npm:^0.4.0":
version: 0.4.0
resolution: "@emotion/weak-memoize@npm:0.4.0"
Expand Down

0 comments on commit 305a845

Please sign in to comment.