UNPKG

14.7 kBJavaScriptView Raw
1/**
2 * react-router v8.0.0
3 *
4 * Copyright (c) Remix Software Inc.
5 *
6 * This source code is licensed under the MIT license found in the
7 * LICENSE.md file in the root directory of this source tree.
8 *
9 * @license MIT
10 */
11import { isRouteErrorResponse } from "../router/utils.js";
12import { hasInvalidProtocol } from "../router/router.js";
13import { RSCRouterContext } from "../context.js";
14import { decodeRedirectErrorDigest, decodeRouteErrorResponseDigest } from "../errors.js";
15import { escapeHtml } from "../dom/ssr/markup.js";
16import { shouldHydrateRouteLoader } from "../dom/ssr/routes.js";
17import { FrameworkContext } from "../dom/ssr/components.js";
18import { StaticRouterProvider, createStaticRouter } from "../dom/server.js";
19import { injectRSCPayload } from "./html-stream/server.js";
20import { RSCRouterGlobalErrorBoundary } from "./errorBoundaries.js";
21import { createRSCRouteModules } from "./route-modules.js";
22import * as React$1 from "react";
23//#region lib/rsc/server.ssr.tsx
24const defaultManifestPath = "/__manifest";
25/**
26* Routes the incoming [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
27* to the [RSC](https://react.dev/reference/rsc/server-components) server and
28* appropriately proxies the server response for data / resource requests, or
29* renders to HTML for a document request.
30*
31* @example
32* import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
33* import * as ReactDomServer from "react-dom/server.edge";
34* import {
35* unstable_RSCStaticRouter as RSCStaticRouter,
36* unstable_routeRSCServerRequest as routeRSCServerRequest,
37* } from "react-router";
38*
39* routeRSCServerRequest({
40* request,
41* serverResponse,
42* createFromReadableStream,
43* async renderHTML(getPayload) {
44* const payload = getPayload();
45*
46* return await renderHTMLToReadableStream(
47* <RSCStaticRouter getPayload={getPayload} />,
48* {
49* bootstrapScriptContent,
50* formState: await payload.formState,
51* }
52* );
53* },
54* });
55*
56* @name unstable_routeRSCServerRequest
57* @public
58* @category RSC
59* @mode data
60* @param opts Options
61* @param opts.createFromReadableStream Your `react-server-dom-xyz/client`'s
62* `createFromReadableStream` function, used to decode payloads from the server.
63* @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}.
64* @param opts.hydrate Whether to hydrate the server response with the RSC payload.
65* Defaults to `true`.
66* @param opts.renderHTML A function that renders the {@link unstable_RSCPayload} to
67* HTML, usually using a {@link unstable_RSCStaticRouter | `<RSCStaticRouter>`}.
68* @param opts.request The request to route.
69* @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
70* that either contains the [RSC](https://react.dev/reference/rsc/server-components)
71* payload for data requests, or renders the HTML for document requests.
72*/
73async function routeRSCServerRequest({ request, serverResponse, createFromReadableStream, renderHTML, hydrate = true }) {
74 const url = new URL(request.url);
75 if (isReactServerRequest(url) || isManifestRequest(url) || request.headers.has("rsc-action-id") || serverResponse.headers.get("React-Router-Resource") === "true") return serverResponse;
76 if (!serverResponse.body) throw new Error("Missing body in server response");
77 const detectRedirectResponse = serverResponse.clone();
78 let serverResponseB = null;
79 if (hydrate) serverResponseB = serverResponse.clone();
80 const body = serverResponse.body;
81 let buffer;
82 let streamControllers = [];
83 const createStream = () => {
84 if (!buffer) {
85 buffer = [];
86 return body.pipeThrough(new TransformStream({
87 transform(chunk, controller) {
88 buffer.push(chunk);
89 controller.enqueue(chunk);
90 streamControllers.forEach((c) => c.enqueue(chunk));
91 },
92 flush() {
93 streamControllers.forEach((c) => c.close());
94 streamControllers = [];
95 }
96 }));
97 }
98 return new ReadableStream({ start(controller) {
99 buffer.forEach((chunk) => controller.enqueue(chunk));
100 streamControllers.push(controller);
101 } });
102 };
103 let deepestRenderedBoundaryId = null;
104 const getPayload = () => {
105 const payloadPromise = Promise.resolve(createFromReadableStream(createStream()));
106 return Object.defineProperties(payloadPromise, {
107 _deepestRenderedBoundaryId: {
108 get() {
109 return deepestRenderedBoundaryId;
110 },
111 set(boundaryId) {
112 deepestRenderedBoundaryId = boundaryId;
113 }
114 },
115 formState: { get() {
116 return payloadPromise.then((payload) => payload.type === "render" ? payload.formState : void 0);
117 } }
118 });
119 };
120 let renderRedirect;
121 let renderError;
122 try {
123 if (!detectRedirectResponse.body) throw new Error("Failed to clone server response");
124 const payload = await createFromReadableStream(detectRedirectResponse.body);
125 if (serverResponse.status === 202 && payload.type === "redirect") {
126 if (hasInvalidProtocol(payload.location)) throw new Error("Invalid redirect location");
127 const headers = new Headers(serverResponse.headers);
128 headers.delete("Content-Encoding");
129 headers.delete("Content-Length");
130 headers.delete("Content-Type");
131 headers.delete("X-Remix-Response");
132 headers.set("Location", payload.location);
133 return new Response(serverResponseB?.body || "", {
134 headers,
135 status: payload.status,
136 statusText: serverResponse.statusText
137 });
138 }
139 let reactHeaders = new Headers();
140 let status = serverResponse.status;
141 let statusText = serverResponse.statusText;
142 let html = await renderHTML(getPayload, {
143 onError(error) {
144 if (typeof error === "object" && error && "digest" in error && typeof error.digest === "string") {
145 renderRedirect = decodeRedirectErrorDigest(error.digest);
146 if (renderRedirect) return error.digest;
147 let routeErrorResponse = decodeRouteErrorResponseDigest(error.digest);
148 if (routeErrorResponse) {
149 renderError = routeErrorResponse;
150 status = routeErrorResponse.status;
151 statusText = routeErrorResponse.statusText;
152 return error.digest;
153 }
154 }
155 },
156 onHeaders(headers) {
157 for (const [key, value] of headers) reactHeaders.append(key, value);
158 }
159 });
160 const headers = new Headers(reactHeaders);
161 for (const [key, value] of serverResponse.headers) headers.append(key, value);
162 headers.set("Content-Type", "text/html; charset=utf-8");
163 if (renderRedirect) {
164 if (hasInvalidProtocol(renderRedirect.location)) throw new Error("Invalid redirect location");
165 headers.set("Location", renderRedirect.location);
166 return new Response(html, {
167 status: renderRedirect.status,
168 headers
169 });
170 }
171 const redirectTransform = new TransformStream({ flush(controller) {
172 if (renderRedirect) {
173 if (hasInvalidProtocol(renderRedirect.location)) return;
174 controller.enqueue(new TextEncoder().encode(`<meta http-equiv="refresh" content="0;url=${escapeHtml(renderRedirect.location)}"/>`));
175 }
176 } });
177 if (!hydrate) return new Response(html.pipeThrough(redirectTransform), {
178 status,
179 statusText,
180 headers
181 });
182 if (!serverResponseB?.body) throw new Error("Failed to clone server response");
183 const body = html.pipeThrough(injectRSCPayload(serverResponseB.body)).pipeThrough(redirectTransform);
184 return new Response(body, {
185 status,
186 statusText,
187 headers
188 });
189 } catch (error) {
190 if (error instanceof Response) return error;
191 if (renderRedirect) {
192 if (hasInvalidProtocol(renderRedirect.location)) throw new Error("Invalid redirect location");
193 return new Response(`Redirect: ${renderRedirect.location}`, {
194 status: renderRedirect.status,
195 headers: { Location: renderRedirect.location }
196 });
197 }
198 try {
199 let normalizedError = renderError ?? error;
200 let [status, statusText] = isRouteErrorResponse(normalizedError) ? [normalizedError.status, normalizedError.statusText] : [500, ""];
201 let retryRedirect;
202 let reactHeaders = new Headers();
203 const html = await renderHTML(() => {
204 const payloadPromise = Promise.resolve(createFromReadableStream(createStream())).then((payload) => Object.assign(payload, {
205 status,
206 errors: deepestRenderedBoundaryId ? { [deepestRenderedBoundaryId]: normalizedError } : {}
207 }));
208 return Object.defineProperties(payloadPromise, {
209 _deepestRenderedBoundaryId: {
210 get() {
211 return deepestRenderedBoundaryId;
212 },
213 set(boundaryId) {
214 deepestRenderedBoundaryId = boundaryId;
215 }
216 },
217 formState: { get() {
218 return payloadPromise.then((payload) => payload.type === "render" ? payload.formState : void 0);
219 } }
220 });
221 }, {
222 onError(error) {
223 if (typeof error === "object" && error && "digest" in error && typeof error.digest === "string") {
224 retryRedirect = decodeRedirectErrorDigest(error.digest);
225 if (retryRedirect) return error.digest;
226 let routeErrorResponse = decodeRouteErrorResponseDigest(error.digest);
227 if (routeErrorResponse) {
228 status = routeErrorResponse.status;
229 statusText = routeErrorResponse.statusText;
230 return error.digest;
231 }
232 }
233 },
234 onHeaders(headers) {
235 for (const [key, value] of headers) reactHeaders.append(key, value);
236 }
237 });
238 const headers = new Headers(reactHeaders);
239 for (const [key, value] of serverResponse.headers) headers.append(key, value);
240 headers.set("Content-Type", "text/html; charset=utf-8");
241 if (retryRedirect) {
242 if (hasInvalidProtocol(retryRedirect.location)) throw new Error("Invalid redirect location");
243 headers.set("Location", retryRedirect.location);
244 return new Response(html, {
245 status: retryRedirect.status,
246 headers
247 });
248 }
249 const retryRedirectTransform = new TransformStream({ flush(controller) {
250 if (retryRedirect) {
251 if (hasInvalidProtocol(retryRedirect.location)) return;
252 controller.enqueue(new TextEncoder().encode(`<meta http-equiv="refresh" content="0;url=${escapeHtml(retryRedirect.location)}"/>`));
253 }
254 } });
255 if (!hydrate) return new Response(html.pipeThrough(retryRedirectTransform), {
256 status,
257 statusText,
258 headers
259 });
260 if (!serverResponseB?.body) throw new Error("Failed to clone server response");
261 const body = html.pipeThrough(injectRSCPayload(serverResponseB.body)).pipeThrough(retryRedirectTransform);
262 return new Response(body, {
263 status,
264 statusText,
265 headers
266 });
267 } catch (error2) {}
268 throw error;
269 }
270}
271/**
272* Pre-renders an {@link unstable_RSCPayload} to HTML. Usually used in
273* {@link unstable_routeRSCServerRequest}'s `renderHTML` callback.
274*
275* @example
276* import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
277* import * as ReactDomServer from "react-dom/server.edge";
278* import {
279* unstable_RSCStaticRouter as RSCStaticRouter,
280* unstable_routeRSCServerRequest as routeRSCServerRequest,
281* } from "react-router";
282*
283* routeRSCServerRequest({
284* request,
285* serverResponse,
286* createFromReadableStream,
287* async renderHTML(getPayload) {
288* const payload = getPayload();
289*
290* return await renderHTMLToReadableStream(
291* <RSCStaticRouter getPayload={getPayload} />,
292* {
293* bootstrapScriptContent,
294* formState: await payload.formState,
295* }
296* );
297* },
298* });
299*
300* @name unstable_RSCStaticRouter
301* @public
302* @category RSC
303* @mode data
304* @param props Props
305* @param {unstable_RSCStaticRouterProps.getPayload} props.getPayload n/a
306* @returns A React component that renders the {@link unstable_RSCPayload} as HTML.
307*/
308function RSCStaticRouter({ getPayload }) {
309 const decoded = getPayload();
310 const payload = React$1.use(decoded);
311 if (payload.type === "redirect") {
312 if (hasInvalidProtocol(payload.location)) throw new Error("Invalid redirect location");
313 throw new Response(null, {
314 status: payload.status,
315 headers: { Location: payload.location }
316 });
317 }
318 if (payload.type !== "render") return null;
319 let patchedLoaderData = { ...payload.loaderData };
320 for (const match of payload.matches) if (shouldHydrateRouteLoader(match.id, match.clientLoader, match.hasLoader, false) && (match.hydrateFallbackElement || !match.hasLoader)) delete patchedLoaderData[match.id];
321 const context = {
322 get _deepestRenderedBoundaryId() {
323 return decoded._deepestRenderedBoundaryId ?? null;
324 },
325 set _deepestRenderedBoundaryId(boundaryId) {
326 decoded._deepestRenderedBoundaryId = boundaryId;
327 },
328 actionData: payload.actionData,
329 actionHeaders: {},
330 basename: payload.basename,
331 errors: payload.errors,
332 loaderData: patchedLoaderData,
333 loaderHeaders: {},
334 location: payload.location,
335 statusCode: 200,
336 matches: payload.matches.map((match) => ({
337 params: match.params,
338 pathname: match.pathname,
339 pathnameBase: match.pathnameBase,
340 route: {
341 id: match.id,
342 action: match.hasAction || !!match.clientAction,
343 handle: match.handle,
344 loader: match.hasLoader || !!match.clientLoader,
345 index: match.index,
346 path: match.path,
347 shouldRevalidate: match.shouldRevalidate
348 }
349 }))
350 };
351 const router = createStaticRouter(payload.matches.reduceRight((previous, match) => {
352 const route = {
353 id: match.id,
354 action: match.hasAction || !!match.clientAction,
355 element: match.element,
356 errorElement: match.errorElement,
357 handle: match.handle,
358 hydrateFallbackElement: match.hydrateFallbackElement,
359 index: match.index,
360 loader: match.hasLoader || !!match.clientLoader,
361 path: match.path,
362 shouldRevalidate: match.shouldRevalidate
363 };
364 if (previous.length > 0) route.children = previous;
365 return [route];
366 }, []), context);
367 const frameworkContext = {
368 future: {},
369 isSpaMode: false,
370 ssr: true,
371 criticalCss: "",
372 manifest: {
373 routes: {},
374 version: "1",
375 url: "",
376 entry: {
377 module: "",
378 imports: []
379 }
380 },
381 routeDiscovery: payload.routeDiscovery.mode === "initial" ? {
382 mode: "initial",
383 manifestPath: defaultManifestPath
384 } : {
385 mode: "lazy",
386 manifestPath: payload.routeDiscovery.manifestPath || defaultManifestPath
387 },
388 routeModules: createRSCRouteModules(payload)
389 };
390 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, {
391 context,
392 router,
393 hydrate: false,
394 nonce: payload.nonce
395 }))));
396}
397function isReactServerRequest(url) {
398 return url.pathname.endsWith(".rsc");
399}
400function isManifestRequest(url) {
401 return url.pathname.endsWith(".manifest");
402}
403//#endregion
404export { RSCStaticRouter, routeRSCServerRequest };