initial commit
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
/* ============================================================================
|
||||
SERVER ENTRY
|
||||
----------------------------------------------------------------------------
|
||||
The stock React Router streaming entry plus two SHARP-ER additions:
|
||||
|
||||
- `Content-Language` is stamped from the URL's locale prefix, so the
|
||||
header agrees with <html lang> that root.tsx renders. `Vary` is NOT set
|
||||
globally: every page URL carries its own locale, so only the bare "/"
|
||||
redirect varies on Accept-Language and it sets the header itself.
|
||||
- `handleError` logs server errors without the noise of client aborts.
|
||||
========================================================================= */
|
||||
|
||||
import { PassThrough } from "node:stream";
|
||||
|
||||
import type { EntryContext, RouterContextProvider } from "react-router";
|
||||
import { createReadableStreamFromReadable } from "@react-router/node";
|
||||
import { ServerRouter } from "react-router";
|
||||
import { isbot } from "isbot";
|
||||
import type { RenderToPipeableStreamOptions } from "react-dom/server";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
|
||||
import { DEFAULT_LOCALE, getLocaleFromPathname } from "~/i18n/config";
|
||||
|
||||
export const streamTimeout = 5_000;
|
||||
|
||||
/** The locale a URL addresses, for the `Content-Language` response header. */
|
||||
function contentLanguage(request: Request): string {
|
||||
try {
|
||||
return getLocaleFromPathname(new URL(request.url).pathname) ?? DEFAULT_LOCALE;
|
||||
} catch {
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
}
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
routerContext: EntryContext,
|
||||
loadContext: RouterContextProvider,
|
||||
) {
|
||||
// https://httpwg.org/specs/rfc9110.html#HEAD
|
||||
if (request.method.toUpperCase() === "HEAD") {
|
||||
return new Response(null, {
|
||||
status: responseStatusCode,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
let userAgent = request.headers.get("user-agent");
|
||||
|
||||
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
|
||||
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
|
||||
let readyOption: keyof RenderToPipeableStreamOptions =
|
||||
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
|
||||
? "onAllReady"
|
||||
: "onShellReady";
|
||||
|
||||
// Abort the rendering stream after the `streamTimeout` so it has time to
|
||||
// flush down the rejected boundaries
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(
|
||||
() => abort(),
|
||||
streamTimeout + 1000,
|
||||
);
|
||||
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<ServerRouter context={routerContext} url={request.url} />,
|
||||
{
|
||||
[readyOption]() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough({
|
||||
final(callback) {
|
||||
// Clear the timeout to prevent retaining the closure and memory leak
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = undefined;
|
||||
callback();
|
||||
},
|
||||
});
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
responseHeaders.set("Content-Language", contentLanguage(request));
|
||||
|
||||
pipe(body);
|
||||
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
}),
|
||||
);
|
||||
},
|
||||
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);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Central server-side error logging. A request the client walked away from is
|
||||
* not an application error, so it is not logged as one.
|
||||
*/
|
||||
export function handleError(
|
||||
error: unknown,
|
||||
{ request }: { request: Request },
|
||||
): void {
|
||||
if (request.signal.aborted) return;
|
||||
console.error(`[sharp-er] ${request.method} ${request.url}`, error);
|
||||
}
|
||||
Reference in New Issue
Block a user