UNPKG

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