UNPKG

13.1 kBJavaScriptView Raw
1/**
2 * react-router v8.4.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 { ErrorResponseImpl, SUPPORTED_ERROR_TYPES, data, isRouteErrorResponse, redirect } from "../../router/utils.js";
12import { isDataWithResponseInit, isResponse } from "../../router/router.js";
13import invariant from "./invariant.js";
14import { escapeHtml } from "./markup.js";
15import { decode } from "../../../vendor/turbo-stream-v2/turbo-stream.js";
16import { createRequestInit } from "./data.js";
17import * as React$1 from "react";
18//#region lib/dom/ssr/single-fetch.tsx
19const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect");
20var SingleFetchNoResultError = class extends Error {};
21const NO_BODY_STATUS_CODES = new Set([
22 100,
23 101,
24 204,
25 205
26]);
27function StreamTransfer({ context, identifier, reader, textDecoder, nonce }) {
28 if (!context.renderMeta || !context.renderMeta.didRenderScripts) return null;
29 if (!context.renderMeta.streamCache) context.renderMeta.streamCache = {};
30 let { streamCache } = context.renderMeta;
31 let promise = streamCache[identifier];
32 if (!promise) promise = streamCache[identifier] = reader.read().then((result) => {
33 streamCache[identifier].result = {
34 done: result.done,
35 value: textDecoder.decode(result.value, { stream: true })
36 };
37 }).catch((e) => {
38 streamCache[identifier].error = e;
39 });
40 if (promise.error) throw promise.error;
41 if (promise.result === void 0) throw promise;
42 let { done, value } = promise.result;
43 let scriptTag = value ? /* @__PURE__ */ React$1.createElement("script", {
44 nonce,
45 dangerouslySetInnerHTML: { __html: `window.__reactRouterContext.streamController.enqueue(${escapeHtml(JSON.stringify(value))});` }
46 }) : null;
47 if (done) return /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, scriptTag, /* @__PURE__ */ React$1.createElement("script", {
48 nonce,
49 dangerouslySetInnerHTML: { __html: `window.__reactRouterContext.streamController.close();` }
50 }));
51 else return /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, scriptTag, /* @__PURE__ */ React$1.createElement(React$1.Suspense, null, /* @__PURE__ */ React$1.createElement(StreamTransfer, {
52 context,
53 identifier: identifier + 1,
54 reader,
55 textDecoder,
56 nonce
57 })));
58}
59function getTurboStreamSingleFetchDataStrategy(getRouter, manifest, routeModules, ssr) {
60 let dataStrategy = getSingleFetchDataStrategyImpl(getRouter, (match) => {
61 let manifestRoute = manifest.routes[match.route.id];
62 invariant(manifestRoute, "Route not found in manifest");
63 return {
64 hasLoader: manifestRoute.hasLoader,
65 hasClientLoader: manifestRoute.hasClientLoader
66 };
67 }, fetchAndDecodeViaTurboStream, ssr);
68 return async (args) => args.runClientMiddleware(dataStrategy);
69}
70function getSingleFetchDataStrategyImpl(getRouter, getRouteInfo, fetchAndDecode, ssr, shouldAllowOptOut = () => true) {
71 return async (args) => {
72 let { request, matches, fetcherKey } = args;
73 let router = getRouter();
74 if (request.method !== "GET") return singleFetchActionStrategy(args, fetchAndDecode);
75 let foundRevalidatingServerLoader = matches.some((m) => {
76 let { hasLoader, hasClientLoader } = getRouteInfo(m);
77 return m.shouldCallHandler() && hasLoader && !hasClientLoader;
78 });
79 if (!ssr && !foundRevalidatingServerLoader) return nonSsrStrategy(args, getRouteInfo, fetchAndDecode);
80 if (fetcherKey) return singleFetchLoaderFetcherStrategy(args, fetchAndDecode);
81 return singleFetchLoaderNavigationStrategy(args, router, getRouteInfo, fetchAndDecode, ssr, shouldAllowOptOut);
82 };
83}
84async function singleFetchActionStrategy(args, fetchAndDecode) {
85 let actionMatch = args.matches.find((m) => m.shouldCallHandler());
86 invariant(actionMatch, "No action match found");
87 let actionStatus = void 0;
88 let result = await actionMatch.resolve(async (handler) => {
89 return await handler(async () => {
90 let { data, status } = await fetchAndDecode(args, [actionMatch.route.id]);
91 actionStatus = status;
92 return unwrapSingleFetchResult(data, actionMatch.route.id);
93 });
94 });
95 if (isResponse(result.result) || isRouteErrorResponse(result.result) || isDataWithResponseInit(result.result)) return { [actionMatch.route.id]: result };
96 return { [actionMatch.route.id]: {
97 type: result.type,
98 result: data(result.result, actionStatus)
99 } };
100}
101async function nonSsrStrategy(args, getRouteInfo, fetchAndDecode) {
102 let matchesToLoad = args.matches.filter((m) => m.shouldCallHandler());
103 let results = {};
104 await Promise.all(matchesToLoad.map(async (m) => {
105 let routeId = m.route.id;
106 results[routeId] = await m.resolve(async (handler) => {
107 let { hasClientLoader } = getRouteInfo(m);
108 return hasClientLoader ? handler(async () => {
109 let { data } = await fetchAndDecode(args, [routeId]);
110 return unwrapSingleFetchResult(data, routeId);
111 }) : handler();
112 });
113 }));
114 return results;
115}
116async function singleFetchLoaderNavigationStrategy(args, router, getRouteInfo, fetchAndDecode, ssr, shouldAllowOptOut = () => true) {
117 let routesParams = /* @__PURE__ */ new Set();
118 let foundOptOutRoute = false;
119 let routeDfds = args.matches.map(() => createDeferred());
120 let singleFetchDfd = createDeferred();
121 let results = {};
122 let resolvePromise = Promise.all(args.matches.map(async (m, i) => m.resolve(async (handler) => {
123 routeDfds[i].resolve();
124 let routeId = m.route.id;
125 let { hasLoader, hasClientLoader } = getRouteInfo(m);
126 let defaultShouldRevalidate = !m.shouldRevalidateArgs || m.shouldRevalidateArgs.actionStatus == null || m.shouldRevalidateArgs.actionStatus < 400;
127 if (!m.shouldCallHandler(defaultShouldRevalidate)) {
128 foundOptOutRoute ||= m.shouldRevalidateArgs != null && hasLoader;
129 return;
130 }
131 if (shouldAllowOptOut(m) && hasClientLoader) {
132 if (hasLoader) foundOptOutRoute = true;
133 try {
134 results[routeId] = {
135 type: "data",
136 result: await handler(async () => {
137 let { data } = await fetchAndDecode(args, [routeId]);
138 return unwrapSingleFetchResult(data, routeId);
139 })
140 };
141 } catch (e) {
142 results[routeId] = {
143 type: "error",
144 result: e
145 };
146 }
147 return;
148 }
149 if (hasLoader) routesParams.add(routeId);
150 try {
151 results[routeId] = {
152 type: "data",
153 result: await handler(async () => {
154 return unwrapSingleFetchResult(await singleFetchDfd.promise, routeId);
155 })
156 };
157 } catch (e) {
158 results[routeId] = {
159 type: "error",
160 result: e
161 };
162 }
163 })));
164 await Promise.all(routeDfds.map((d) => d.promise));
165 if ((!router.state.initialized && router.state.navigation.state === "idle" || routesParams.size === 0) && !window.__reactRouterHdrActive) singleFetchDfd.resolve({ routes: {} });
166 else {
167 let targetRoutes = ssr && foundOptOutRoute && routesParams.size > 0 ? [...routesParams.keys()] : void 0;
168 try {
169 let data = await fetchAndDecode(args, targetRoutes);
170 singleFetchDfd.resolve(data.data);
171 } catch (e) {
172 singleFetchDfd.reject(e);
173 }
174 }
175 await resolvePromise;
176 await bubbleMiddlewareErrors(singleFetchDfd.promise, args.matches, routesParams, results);
177 return results;
178}
179async function bubbleMiddlewareErrors(singleFetchPromise, matches, routesParams, results) {
180 try {
181 let middlewareError;
182 let fetchedData = await singleFetchPromise;
183 if ("routes" in fetchedData) {
184 for (let match of matches) if (match.route.id in fetchedData.routes) {
185 let routeResult = fetchedData.routes[match.route.id];
186 if ("error" in routeResult) {
187 middlewareError = routeResult.error;
188 if (results[match.route.id]?.result == null) results[match.route.id] = {
189 type: "error",
190 result: middlewareError
191 };
192 break;
193 }
194 }
195 }
196 if (middlewareError !== void 0) Array.from(routesParams.values()).forEach((routeId) => {
197 if (results[routeId].result instanceof SingleFetchNoResultError) results[routeId].result = middlewareError;
198 });
199 } catch {}
200}
201async function singleFetchLoaderFetcherStrategy(args, fetchAndDecode) {
202 let fetcherMatch = args.matches.find((m) => m.shouldCallHandler());
203 invariant(fetcherMatch, "No fetcher match found");
204 let routeId = fetcherMatch.route.id;
205 let result = await fetcherMatch.resolve(async (handler) => handler(async () => {
206 let { data } = await fetchAndDecode(args, [routeId]);
207 return unwrapSingleFetchResult(data, routeId);
208 }));
209 return { [fetcherMatch.route.id]: result };
210}
211function stripIndexParam(url) {
212 let indexValues = url.searchParams.getAll("index");
213 url.searchParams.delete("index");
214 let indexValuesToKeep = [];
215 for (let indexValue of indexValues) if (indexValue) indexValuesToKeep.push(indexValue);
216 for (let toKeep of indexValuesToKeep) url.searchParams.append("index", toKeep);
217 return url;
218}
219function singleFetchUrl(reqUrl, extension) {
220 let url = typeof reqUrl === "string" ? new URL(reqUrl, typeof window === "undefined" ? "server://singlefetch/" : window.location.origin) : reqUrl;
221 if (url.pathname.endsWith("/")) url.pathname = `${url.pathname}_.${extension}`;
222 else url.pathname = `${url.pathname}.${extension}`;
223 return url;
224}
225async function fetchAndDecodeViaTurboStream(args, targetRoutes) {
226 let { request } = args;
227 let url = singleFetchUrl(request.url, "data");
228 if (request.method === "GET") {
229 url = stripIndexParam(url);
230 if (targetRoutes) url.searchParams.set("_routes", targetRoutes.join(","));
231 }
232 let res = await fetch(url, await createRequestInit(request));
233 if (res.status >= 400 && !res.headers.has("X-Remix-Response")) throw new ErrorResponseImpl(res.status, res.statusText, await res.text());
234 if (res.status === 204 && res.headers.has("X-Remix-Redirect")) return {
235 status: 202,
236 data: { redirect: {
237 redirect: res.headers.get("X-Remix-Redirect"),
238 status: Number(res.headers.get("X-Remix-Status") || "302"),
239 revalidate: res.headers.get("X-Remix-Revalidate") === "true",
240 reload: res.headers.get("X-Remix-Reload-Document") === "true",
241 replace: res.headers.get("X-Remix-Replace") === "true"
242 } }
243 };
244 if (NO_BODY_STATUS_CODES.has(res.status)) {
245 let routes = {};
246 if (targetRoutes && request.method !== "GET") routes[targetRoutes[0]] = { data: void 0 };
247 return {
248 status: res.status,
249 data: { routes }
250 };
251 }
252 invariant(res.body, "No response body to decode");
253 try {
254 let decoded = await decodeViaTurboStream(res.body, window);
255 let data;
256 if (request.method === "GET") {
257 let typed = decoded.value;
258 if (SingleFetchRedirectSymbol in typed) data = { redirect: typed[SingleFetchRedirectSymbol] };
259 else data = { routes: typed };
260 } else {
261 let typed = decoded.value;
262 let routeId = targetRoutes?.[0];
263 invariant(routeId, "No routeId found for single fetch call decoding");
264 if ("redirect" in typed) data = { redirect: typed };
265 else data = { routes: { [routeId]: typed } };
266 }
267 return {
268 status: res.status,
269 data
270 };
271 } catch (cause) {
272 throw new Error("Unable to decode turbo-stream response", { cause });
273 }
274}
275function decodeViaTurboStream(body, global) {
276 return decode(body, { plugins: [(type, ...rest) => {
277 if (type === "SanitizedError") {
278 let [name, message, stack] = rest;
279 let Constructor = Error;
280 if (name && SUPPORTED_ERROR_TYPES.includes(name) && name in global && typeof global[name] === "function") Constructor = global[name];
281 let error = new Constructor(message);
282 error.stack = stack;
283 return { value: error };
284 }
285 if (type === "ErrorResponse") {
286 let [data, status, statusText] = rest;
287 return { value: new ErrorResponseImpl(status, statusText, data) };
288 }
289 if (type === "SingleFetchRedirect") return { value: { [SingleFetchRedirectSymbol]: rest[0] } };
290 if (type === "SingleFetchClassInstance") return { value: rest[0] };
291 if (type === "SingleFetchFallback") return { value: void 0 };
292 }] });
293}
294function unwrapSingleFetchResult(result, routeId) {
295 if ("redirect" in result) {
296 let { redirect: location, revalidate, reload, replace, status } = result.redirect;
297 throw redirect(location, {
298 status,
299 headers: {
300 ...revalidate ? { "X-Remix-Revalidate": "yes" } : null,
301 ...reload ? { "X-Remix-Reload-Document": "yes" } : null,
302 ...replace ? { "X-Remix-Replace": "yes" } : null
303 }
304 });
305 }
306 let routeResult = result.routes[routeId];
307 if (routeResult == null) throw new SingleFetchNoResultError(`No result found for routeId "${routeId}"`);
308 else if ("error" in routeResult) throw routeResult.error;
309 else if ("data" in routeResult) return routeResult.data;
310 else throw new Error(`Invalid response found for routeId "${routeId}"`);
311}
312function createDeferred() {
313 let resolve;
314 let reject;
315 let promise = new Promise((res, rej) => {
316 resolve = async (val) => {
317 res(val);
318 try {
319 await promise;
320 } catch {}
321 };
322 reject = async (error) => {
323 rej(error);
324 try {
325 await promise;
326 } catch {}
327 };
328 });
329 return {
330 promise,
331 resolve,
332 reject
333 };
334}
335//#endregion
336export { NO_BODY_STATUS_CODES, SingleFetchRedirectSymbol, StreamTransfer, decodeViaTurboStream, getSingleFetchDataStrategyImpl, getTurboStreamSingleFetchDataStrategy, singleFetchUrl, stripIndexParam };