// IDK why this is needed when it's in the tsconfig..........
// YAY PROJECT REFERENCES!
///
import type * as express from "express";
import type {
AppLoadContext,
ServerBuild,
UNSAFE_MiddlewareEnabled as MiddlewareEnabled,
unstable_InitialContext,
} from "react-router";
import { createRequestHandler as createRemixRequestHandler } from "react-router";
import {
createReadableStreamFromReadable,
writeReadableStreamToWritable,
} from "@react-router/node";
type MaybePromise = T | Promise;
/**
* A function that returns the value to use as `context` in route `loader` and
* `action` functions.
*
* You can think of this as an escape hatch that allows you to pass
* environment/platform-specific values through to your loader/action, such as
* values that are generated by Express middleware like `req.session`.
*/
export type GetLoadContextFunction = (
req: express.Request,
res: express.Response
) => MiddlewareEnabled extends true
? MaybePromise
: MaybePromise;
export type RequestHandler = (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => Promise;
/**
* Returns a request handler for Express that serves the response using Remix.
*/
export function createRequestHandler({
build,
getLoadContext,
mode = process.env.NODE_ENV,
}: {
build: ServerBuild | (() => Promise);
getLoadContext?: GetLoadContextFunction;
mode?: string;
}): RequestHandler {
let handleRequest = createRemixRequestHandler(build, mode);
return async (
req: express.Request,
res: express.Response,
next: express.NextFunction
) => {
try {
let request = createRemixRequest(req, res);
let loadContext = await getLoadContext?.(req, res);
let response = await handleRequest(request, loadContext);
await sendRemixResponse(res, response);
} catch (error: unknown) {
// Express doesn't support async functions, so we have to pass along the
// error manually using next().
next(error);
}
};
}
export function createRemixHeaders(
requestHeaders: express.Request["headers"]
): Headers {
let headers = new Headers();
for (let [key, values] of Object.entries(requestHeaders)) {
if (values) {
if (Array.isArray(values)) {
for (let value of values) {
headers.append(key, value);
}
} else {
headers.set(key, values);
}
}
}
return headers;
}
export function createRemixRequest(
req: express.Request,
res: express.Response
): Request {
// req.hostname doesn't include port information so grab that from
// `X-Forwarded-Host` or `Host`
let [, hostnamePortStr] = req.get("X-Forwarded-Host")?.split(":") ?? [];
let [, hostPortStr] = req.get("host")?.split(":") ?? [];
let hostnamePort = Number.parseInt(hostnamePortStr, 10);
let hostPort = Number.parseInt(hostPortStr, 10);
let port = Number.isSafeInteger(hostnamePort)
? hostnamePort
: Number.isSafeInteger(hostPort)
? hostPort
: "";
// Use req.hostname here as it respects the "trust proxy" setting
let resolvedHost = `${req.hostname}${port ? `:${port}` : ""}`;
// Use `req.originalUrl` so Remix is aware of the full path
let url = new URL(`${req.protocol}://${resolvedHost}${req.originalUrl}`);
// Abort action/loaders once we can no longer write a response
let controller: AbortController | null = new AbortController();
let init: RequestInit = {
method: req.method,
headers: createRemixHeaders(req.headers),
signal: controller.signal,
};
// Abort action/loaders once we can no longer write a response iff we have
// not yet sent a response (i.e., `close` without `finish`)
// `finish` -> done rendering the response
// `close` -> response can no longer be written to
res.on("finish", () => (controller = null));
res.on("close", () => controller?.abort());
if (req.method !== "GET" && req.method !== "HEAD") {
init.body = createReadableStreamFromReadable(req);
(init as { duplex: "half" }).duplex = "half";
}
return new Request(url.href, init);
}
export async function sendRemixResponse(
res: express.Response,
nodeResponse: Response
): Promise {
res.statusMessage = nodeResponse.statusText;
res.status(nodeResponse.status);
for (let [key, value] of nodeResponse.headers.entries()) {
res.append(key, value);
}
if (nodeResponse.headers.get("Content-Type")?.match(/text\/event-stream/i)) {
res.flushHeaders();
}
if (nodeResponse.body) {
await writeReadableStreamToWritable(nodeResponse.body, res);
} else {
res.end();
}
}