UNPKG

react-router

Version:
405 lines (404 loc) • 14.7 kB
/** * 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 };