react-router
Version:
Declarative routing for React
405 lines (404 loc) • 14.7 kB
JavaScript
/**
* react-router v8.0.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
import { isRouteErrorResponse } from "../router/utils.js";
import { hasInvalidProtocol } from "../router/router.js";
import { RSCRouterContext } from "../context.js";
import { decodeRedirectErrorDigest, decodeRouteErrorResponseDigest } from "../errors.js";
import { escapeHtml } from "../dom/ssr/markup.js";
import { shouldHydrateRouteLoader } from "../dom/ssr/routes.js";
import { FrameworkContext } from "../dom/ssr/components.js";
import { StaticRouterProvider, createStaticRouter } from "../dom/server.js";
import { injectRSCPayload } from "./html-stream/server.js";
import { RSCRouterGlobalErrorBoundary } from "./errorBoundaries.js";
import { createRSCRouteModules } from "./route-modules.js";
import * as React$1 from "react";
//#region lib/rsc/server.ssr.tsx
const defaultManifestPath = "/__manifest";
/**
* Routes the incoming [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
* to the [RSC](https://react.dev/reference/rsc/server-components) server and
* appropriately proxies the server response for data / resource requests, or
* renders to HTML for a document request.
*
* @example
* import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
* import * as ReactDomServer from "react-dom/server.edge";
* import {
* unstable_RSCStaticRouter as RSCStaticRouter,
* unstable_routeRSCServerRequest as routeRSCServerRequest,
* } from "react-router";
*
* routeRSCServerRequest({
* request,
* serverResponse,
* createFromReadableStream,
* async renderHTML(getPayload) {
* const payload = getPayload();
*
* return await renderHTMLToReadableStream(
* <RSCStaticRouter getPayload={getPayload} />,
* {
* bootstrapScriptContent,
* formState: await payload.formState,
* }
* );
* },
* });
*
* @name unstable_routeRSCServerRequest
* @public
* @category RSC
* @mode data
* @param opts Options
* @param opts.createFromReadableStream Your `react-server-dom-xyz/client`'s
* `createFromReadableStream` function, used to decode payloads from the server.
* @param opts.serverResponse A Response or partial response generated by the [RSC](https://react.dev/reference/rsc/server-components) handler containing a serialized {@link unstable_RSCPayload}.
* @param opts.hydrate Whether to hydrate the server response with the RSC payload.
* Defaults to `true`.
* @param opts.renderHTML A function that renders the {@link unstable_RSCPayload} to
* HTML, usually using a {@link unstable_RSCStaticRouter | `<RSCStaticRouter>`}.
* @param opts.request The request to route.
* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
* that either contains the [RSC](https://react.dev/reference/rsc/server-components)
* payload for data requests, or renders the HTML for document requests.
*/
async function routeRSCServerRequest({ request, serverResponse, createFromReadableStream, renderHTML, hydrate = true }) {
const url = new URL(request.url);
if (isReactServerRequest(url) || isManifestRequest(url) || request.headers.has("rsc-action-id") || serverResponse.headers.get("React-Router-Resource") === "true") return serverResponse;
if (!serverResponse.body) throw new Error("Missing body in server response");
const detectRedirectResponse = serverResponse.clone();
let serverResponseB = null;
if (hydrate) serverResponseB = serverResponse.clone();
const body = serverResponse.body;
let buffer;
let streamControllers = [];
const createStream = () => {
if (!buffer) {
buffer = [];
return body.pipeThrough(new TransformStream({
transform(chunk, controller) {
buffer.push(chunk);
controller.enqueue(chunk);
streamControllers.forEach((c) => c.enqueue(chunk));
},
flush() {
streamControllers.forEach((c) => c.close());
streamControllers = [];
}
}));
}
return new ReadableStream({ start(controller) {
buffer.forEach((chunk) => controller.enqueue(chunk));
streamControllers.push(controller);
} });
};
let deepestRenderedBoundaryId = null;
const getPayload = () => {
const payloadPromise = Promise.resolve(createFromReadableStream(createStream()));
return Object.defineProperties(payloadPromise, {
_deepestRenderedBoundaryId: {
get() {
return deepestRenderedBoundaryId;
},
set(boundaryId) {
deepestRenderedBoundaryId = boundaryId;
}
},
formState: { get() {
return payloadPromise.then((payload) => payload.type === "render" ? payload.formState : void 0);
} }
});
};
let renderRedirect;
let renderError;
try {
if (!detectRedirectResponse.body) throw new Error("Failed to clone server response");
const payload = await createFromReadableStream(detectRedirectResponse.body);
if (serverResponse.status === 202 && payload.type === "redirect") {
if (hasInvalidProtocol(payload.location)) throw new Error("Invalid redirect location");
const headers = new Headers(serverResponse.headers);
headers.delete("Content-Encoding");
headers.delete("Content-Length");
headers.delete("Content-Type");
headers.delete("X-Remix-Response");
headers.set("Location", payload.location);
return new Response(serverResponseB?.body || "", {
headers,
status: payload.status,
statusText: serverResponse.statusText
});
}
let reactHeaders = new Headers();
let status = serverResponse.status;
let statusText = serverResponse.statusText;
let html = await renderHTML(getPayload, {
onError(error) {
if (typeof error === "object" && error && "digest" in error && typeof error.digest === "string") {
renderRedirect = decodeRedirectErrorDigest(error.digest);
if (renderRedirect) return error.digest;
let routeErrorResponse = decodeRouteErrorResponseDigest(error.digest);
if (routeErrorResponse) {
renderError = routeErrorResponse;
status = routeErrorResponse.status;
statusText = routeErrorResponse.statusText;
return error.digest;
}
}
},
onHeaders(headers) {
for (const [key, value] of headers) reactHeaders.append(key, value);
}
});
const headers = new Headers(reactHeaders);
for (const [key, value] of serverResponse.headers) headers.append(key, value);
headers.set("Content-Type", "text/html; charset=utf-8");
if (renderRedirect) {
if (hasInvalidProtocol(renderRedirect.location)) throw new Error("Invalid redirect location");
headers.set("Location", renderRedirect.location);
return new Response(html, {
status: renderRedirect.status,
headers
});
}
const redirectTransform = new TransformStream({ flush(controller) {
if (renderRedirect) {
if (hasInvalidProtocol(renderRedirect.location)) return;
controller.enqueue(new TextEncoder().encode(`<meta http-equiv="refresh" content="0;url=${escapeHtml(renderRedirect.location)}"/>`));
}
} });
if (!hydrate) return new Response(html.pipeThrough(redirectTransform), {
status,
statusText,
headers
});
if (!serverResponseB?.body) throw new Error("Failed to clone server response");
const body = html.pipeThrough(injectRSCPayload(serverResponseB.body)).pipeThrough(redirectTransform);
return new Response(body, {
status,
statusText,
headers
});
} catch (error) {
if (error instanceof Response) return error;
if (renderRedirect) {
if (hasInvalidProtocol(renderRedirect.location)) throw new Error("Invalid redirect location");
return new Response(`Redirect: ${renderRedirect.location}`, {
status: renderRedirect.status,
headers: { Location: renderRedirect.location }
});
}
try {
let normalizedError = renderError ?? error;
let [status, statusText] = isRouteErrorResponse(normalizedError) ? [normalizedError.status, normalizedError.statusText] : [500, ""];
let retryRedirect;
let reactHeaders = new Headers();
const html = await renderHTML(() => {
const payloadPromise = Promise.resolve(createFromReadableStream(createStream())).then((payload) => Object.assign(payload, {
status,
errors: deepestRenderedBoundaryId ? { [deepestRenderedBoundaryId]: normalizedError } : {}
}));
return Object.defineProperties(payloadPromise, {
_deepestRenderedBoundaryId: {
get() {
return deepestRenderedBoundaryId;
},
set(boundaryId) {
deepestRenderedBoundaryId = boundaryId;
}
},
formState: { get() {
return payloadPromise.then((payload) => payload.type === "render" ? payload.formState : void 0);
} }
});
}, {
onError(error) {
if (typeof error === "object" && error && "digest" in error && typeof error.digest === "string") {
retryRedirect = decodeRedirectErrorDigest(error.digest);
if (retryRedirect) return error.digest;
let routeErrorResponse = decodeRouteErrorResponseDigest(error.digest);
if (routeErrorResponse) {
status = routeErrorResponse.status;
statusText = routeErrorResponse.statusText;
return error.digest;
}
}
},
onHeaders(headers) {
for (const [key, value] of headers) reactHeaders.append(key, value);
}
});
const headers = new Headers(reactHeaders);
for (const [key, value] of serverResponse.headers) headers.append(key, value);
headers.set("Content-Type", "text/html; charset=utf-8");
if (retryRedirect) {
if (hasInvalidProtocol(retryRedirect.location)) throw new Error("Invalid redirect location");
headers.set("Location", retryRedirect.location);
return new Response(html, {
status: retryRedirect.status,
headers
});
}
const retryRedirectTransform = new TransformStream({ flush(controller) {
if (retryRedirect) {
if (hasInvalidProtocol(retryRedirect.location)) return;
controller.enqueue(new TextEncoder().encode(`<meta http-equiv="refresh" content="0;url=${escapeHtml(retryRedirect.location)}"/>`));
}
} });
if (!hydrate) return new Response(html.pipeThrough(retryRedirectTransform), {
status,
statusText,
headers
});
if (!serverResponseB?.body) throw new Error("Failed to clone server response");
const body = html.pipeThrough(injectRSCPayload(serverResponseB.body)).pipeThrough(retryRedirectTransform);
return new Response(body, {
status,
statusText,
headers
});
} catch (error2) {}
throw error;
}
}
/**
* Pre-renders an {@link unstable_RSCPayload} to HTML. Usually used in
* {@link unstable_routeRSCServerRequest}'s `renderHTML` callback.
*
* @example
* import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
* import * as ReactDomServer from "react-dom/server.edge";
* import {
* unstable_RSCStaticRouter as RSCStaticRouter,
* unstable_routeRSCServerRequest as routeRSCServerRequest,
* } from "react-router";
*
* routeRSCServerRequest({
* request,
* serverResponse,
* createFromReadableStream,
* async renderHTML(getPayload) {
* const payload = getPayload();
*
* return await renderHTMLToReadableStream(
* <RSCStaticRouter getPayload={getPayload} />,
* {
* bootstrapScriptContent,
* formState: await payload.formState,
* }
* );
* },
* });
*
* @name unstable_RSCStaticRouter
* @public
* @category RSC
* @mode data
* @param props Props
* @param {unstable_RSCStaticRouterProps.getPayload} props.getPayload n/a
* @returns A React component that renders the {@link unstable_RSCPayload} as HTML.
*/
function RSCStaticRouter({ getPayload }) {
const decoded = getPayload();
const payload = React$1.use(decoded);
if (payload.type === "redirect") {
if (hasInvalidProtocol(payload.location)) throw new Error("Invalid redirect location");
throw new Response(null, {
status: payload.status,
headers: { Location: payload.location }
});
}
if (payload.type !== "render") return null;
let patchedLoaderData = { ...payload.loaderData };
for (const match of payload.matches) if (shouldHydrateRouteLoader(match.id, match.clientLoader, match.hasLoader, false) && (match.hydrateFallbackElement || !match.hasLoader)) delete patchedLoaderData[match.id];
const context = {
get _deepestRenderedBoundaryId() {
return decoded._deepestRenderedBoundaryId ?? null;
},
set _deepestRenderedBoundaryId(boundaryId) {
decoded._deepestRenderedBoundaryId = boundaryId;
},
actionData: payload.actionData,
actionHeaders: {},
basename: payload.basename,
errors: payload.errors,
loaderData: patchedLoaderData,
loaderHeaders: {},
location: payload.location,
statusCode: 200,
matches: payload.matches.map((match) => ({
params: match.params,
pathname: match.pathname,
pathnameBase: match.pathnameBase,
route: {
id: match.id,
action: match.hasAction || !!match.clientAction,
handle: match.handle,
loader: match.hasLoader || !!match.clientLoader,
index: match.index,
path: match.path,
shouldRevalidate: match.shouldRevalidate
}
}))
};
const router = createStaticRouter(payload.matches.reduceRight((previous, match) => {
const route = {
id: match.id,
action: match.hasAction || !!match.clientAction,
element: match.element,
errorElement: match.errorElement,
handle: match.handle,
hydrateFallbackElement: match.hydrateFallbackElement,
index: match.index,
loader: match.hasLoader || !!match.clientLoader,
path: match.path,
shouldRevalidate: match.shouldRevalidate
};
if (previous.length > 0) route.children = previous;
return [route];
}, []), context);
const frameworkContext = {
future: {},
isSpaMode: false,
ssr: true,
criticalCss: "",
manifest: {
routes: {},
version: "1",
url: "",
entry: {
module: "",
imports: []
}
},
routeDiscovery: payload.routeDiscovery.mode === "initial" ? {
mode: "initial",
manifestPath: defaultManifestPath
} : {
mode: "lazy",
manifestPath: payload.routeDiscovery.manifestPath || defaultManifestPath
},
routeModules: createRSCRouteModules(payload)
};
return /* @__PURE__ */ React$1.createElement(RSCRouterContext.Provider, { value: true }, /* @__PURE__ */ React$1.createElement(RSCRouterGlobalErrorBoundary, { location: payload.location }, /* @__PURE__ */ React$1.createElement(FrameworkContext.Provider, { value: frameworkContext }, /* @__PURE__ */ React$1.createElement(StaticRouterProvider, {
context,
router,
hydrate: false,
nonce: payload.nonce
}))));
}
function isReactServerRequest(url) {
return url.pathname.endsWith(".rsc");
}
function isManifestRequest(url) {
return url.pathname.endsWith(".manifest");
}
//#endregion
export { RSCStaticRouter, routeRSCServerRequest };