UNPKG

13.2 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 { 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((m) => m.resolve(async (handler) => {
105 try {
106 let { hasClientLoader } = getRouteInfo(m);
107 let routeId = m.route.id;
108 let result = hasClientLoader ? await handler(async () => {
109 let { data } = await fetchAndDecode(args, [routeId]);
110 return unwrapSingleFetchResult(data, routeId);
111 }) : await handler();
112 results[m.route.id] = {
113 type: "data",
114 result
115 };
116 } catch (e) {
117 results[m.route.id] = {
118 type: "error",
119 result: e
120 };
121 }
122 })));
123 return results;
124}
125async function singleFetchLoaderNavigationStrategy(args, router, getRouteInfo, fetchAndDecode, ssr, shouldAllowOptOut = () => true) {
126 let routesParams = /* @__PURE__ */ new Set();
127 let foundOptOutRoute = false;
128 let routeDfds = args.matches.map(() => createDeferred());
129 let singleFetchDfd = createDeferred();
130 let results = {};
131 let resolvePromise = Promise.all(args.matches.map(async (m, i) => m.resolve(async (handler) => {
132 routeDfds[i].resolve();
133 let routeId = m.route.id;
134 let { hasLoader, hasClientLoader } = getRouteInfo(m);
135 let defaultShouldRevalidate = !m.shouldRevalidateArgs || m.shouldRevalidateArgs.actionStatus == null || m.shouldRevalidateArgs.actionStatus < 400;
136 if (!m.shouldCallHandler(defaultShouldRevalidate)) {
137 foundOptOutRoute ||= m.shouldRevalidateArgs != null && hasLoader;
138 return;
139 }
140 if (shouldAllowOptOut(m) && hasClientLoader) {
141 if (hasLoader) foundOptOutRoute = true;
142 try {
143 results[routeId] = {
144 type: "data",
145 result: await handler(async () => {
146 let { data } = await fetchAndDecode(args, [routeId]);
147 return unwrapSingleFetchResult(data, routeId);
148 })
149 };
150 } catch (e) {
151 results[routeId] = {
152 type: "error",
153 result: e
154 };
155 }
156 return;
157 }
158 if (hasLoader) routesParams.add(routeId);
159 try {
160 results[routeId] = {
161 type: "data",
162 result: await handler(async () => {
163 return unwrapSingleFetchResult(await singleFetchDfd.promise, routeId);
164 })
165 };
166 } catch (e) {
167 results[routeId] = {
168 type: "error",
169 result: e
170 };
171 }
172 })));
173 await Promise.all(routeDfds.map((d) => d.promise));
174 if ((!router.state.initialized && router.state.navigation.state === "idle" || routesParams.size === 0) && !window.__reactRouterHdrActive) singleFetchDfd.resolve({ routes: {} });
175 else {
176 let targetRoutes = ssr && foundOptOutRoute && routesParams.size > 0 ? [...routesParams.keys()] : void 0;
177 try {
178 let data = await fetchAndDecode(args, targetRoutes);
179 singleFetchDfd.resolve(data.data);
180 } catch (e) {
181 singleFetchDfd.reject(e);
182 }
183 }
184 await resolvePromise;
185 await bubbleMiddlewareErrors(singleFetchDfd.promise, args.matches, routesParams, results);
186 return results;
187}
188async function bubbleMiddlewareErrors(singleFetchPromise, matches, routesParams, results) {
189 try {
190 let middlewareError;
191 let fetchedData = await singleFetchPromise;
192 if ("routes" in fetchedData) {
193 for (let match of matches) if (match.route.id in fetchedData.routes) {
194 let routeResult = fetchedData.routes[match.route.id];
195 if ("error" in routeResult) {
196 middlewareError = routeResult.error;
197 if (results[match.route.id]?.result == null) results[match.route.id] = {
198 type: "error",
199 result: middlewareError
200 };
201 break;
202 }
203 }
204 }
205 if (middlewareError !== void 0) Array.from(routesParams.values()).forEach((routeId) => {
206 if (results[routeId].result instanceof SingleFetchNoResultError) results[routeId].result = middlewareError;
207 });
208 } catch (e) {}
209}
210async function singleFetchLoaderFetcherStrategy(args, fetchAndDecode) {
211 let fetcherMatch = args.matches.find((m) => m.shouldCallHandler());
212 invariant(fetcherMatch, "No fetcher match found");
213 let routeId = fetcherMatch.route.id;
214 let result = await fetcherMatch.resolve(async (handler) => handler(async () => {
215 let { data } = await fetchAndDecode(args, [routeId]);
216 return unwrapSingleFetchResult(data, routeId);
217 }));
218 return { [fetcherMatch.route.id]: result };
219}
220function stripIndexParam(url) {
221 let indexValues = url.searchParams.getAll("index");
222 url.searchParams.delete("index");
223 let indexValuesToKeep = [];
224 for (let indexValue of indexValues) if (indexValue) indexValuesToKeep.push(indexValue);
225 for (let toKeep of indexValuesToKeep) url.searchParams.append("index", toKeep);
226 return url;
227}
228function singleFetchUrl(reqUrl, extension) {
229 let url = typeof reqUrl === "string" ? new URL(reqUrl, typeof window === "undefined" ? "server://singlefetch/" : window.location.origin) : reqUrl;
230 if (url.pathname.endsWith("/")) url.pathname = `${url.pathname}_.${extension}`;
231 else url.pathname = `${url.pathname}.${extension}`;
232 return url;
233}
234async function fetchAndDecodeViaTurboStream(args, targetRoutes) {
235 let { request } = args;
236 let url = singleFetchUrl(request.url, "data");
237 if (request.method === "GET") {
238 url = stripIndexParam(url);
239 if (targetRoutes) url.searchParams.set("_routes", targetRoutes.join(","));
240 }
241 let res = await fetch(url, await createRequestInit(request));
242 if (res.status >= 400 && !res.headers.has("X-Remix-Response")) throw new ErrorResponseImpl(res.status, res.statusText, await res.text());
243 if (res.status === 204 && res.headers.has("X-Remix-Redirect")) return {
244 status: 202,
245 data: { redirect: {
246 redirect: res.headers.get("X-Remix-Redirect"),
247 status: Number(res.headers.get("X-Remix-Status") || "302"),
248 revalidate: res.headers.get("X-Remix-Revalidate") === "true",
249 reload: res.headers.get("X-Remix-Reload-Document") === "true",
250 replace: res.headers.get("X-Remix-Replace") === "true"
251 } }
252 };
253 if (NO_BODY_STATUS_CODES.has(res.status)) {
254 let routes = {};
255 if (targetRoutes && request.method !== "GET") routes[targetRoutes[0]] = { data: void 0 };
256 return {
257 status: res.status,
258 data: { routes }
259 };
260 }
261 invariant(res.body, "No response body to decode");
262 try {
263 let decoded = await decodeViaTurboStream(res.body, window);
264 let data;
265 if (request.method === "GET") {
266 let typed = decoded.value;
267 if (SingleFetchRedirectSymbol in typed) data = { redirect: typed[SingleFetchRedirectSymbol] };
268 else data = { routes: typed };
269 } else {
270 let typed = decoded.value;
271 let routeId = targetRoutes?.[0];
272 invariant(routeId, "No routeId found for single fetch call decoding");
273 if ("redirect" in typed) data = { redirect: typed };
274 else data = { routes: { [routeId]: typed } };
275 }
276 return {
277 status: res.status,
278 data
279 };
280 } catch (e) {
281 throw new Error("Unable to decode turbo-stream response");
282 }
283}
284function decodeViaTurboStream(body, global) {
285 return decode(body, { plugins: [(type, ...rest) => {
286 if (type === "SanitizedError") {
287 let [name, message, stack] = rest;
288 let Constructor = Error;
289 if (name && SUPPORTED_ERROR_TYPES.includes(name) && name in global && typeof global[name] === "function") Constructor = global[name];
290 let error = new Constructor(message);
291 error.stack = stack;
292 return { value: error };
293 }
294 if (type === "ErrorResponse") {
295 let [data, status, statusText] = rest;
296 return { value: new ErrorResponseImpl(status, statusText, data) };
297 }
298 if (type === "SingleFetchRedirect") return { value: { [SingleFetchRedirectSymbol]: rest[0] } };
299 if (type === "SingleFetchClassInstance") return { value: rest[0] };
300 if (type === "SingleFetchFallback") return { value: void 0 };
301 }] });
302}
303function unwrapSingleFetchResult(result, routeId) {
304 if ("redirect" in result) {
305 let { redirect: location, revalidate, reload, replace, status } = result.redirect;
306 throw redirect(location, {
307 status,
308 headers: {
309 ...revalidate ? { "X-Remix-Revalidate": "yes" } : null,
310 ...reload ? { "X-Remix-Reload-Document": "yes" } : null,
311 ...replace ? { "X-Remix-Replace": "yes" } : null
312 }
313 });
314 }
315 let routeResult = result.routes[routeId];
316 if (routeResult == null) throw new SingleFetchNoResultError(`No result found for routeId "${routeId}"`);
317 else if ("error" in routeResult) throw routeResult.error;
318 else if ("data" in routeResult) return routeResult.data;
319 else throw new Error(`Invalid response found for routeId "${routeId}"`);
320}
321function createDeferred() {
322 let resolve;
323 let reject;
324 let promise = new Promise((res, rej) => {
325 resolve = async (val) => {
326 res(val);
327 try {
328 await promise;
329 } catch (e) {}
330 };
331 reject = async (error) => {
332 rej(error);
333 try {
334 await promise;
335 } catch (e) {}
336 };
337 });
338 return {
339 promise,
340 resolve,
341 reject
342 };
343}
344//#endregion
345export { NO_BODY_STATUS_CODES, SingleFetchRedirectSymbol, StreamTransfer, decodeViaTurboStream, getSingleFetchDataStrategyImpl, getTurboStreamSingleFetchDataStrategy, singleFetchUrl, stripIndexParam };