INDIE UNPKG

14.2 kBJavaScriptView Raw
1/**
2 * react-router v8.5.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, fetchImplementation = (request) => fetch(request), isApiOnly = false, serverOrigin) {
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(fetchImplementation, serverOrigin), ssr, void 0, isApiOnly);
68 return async (args) => args.runClientMiddleware(dataStrategy);
69}
70function getSingleFetchDataStrategyImpl(getRouter, getRouteInfo, fetchAndDecode, ssr, shouldAllowOptOut = () => true, isApiOnly = false) {
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, isApiOnly);
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, isApiOnly = false) {
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" && !isApiOnly || 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, serverOrigin) {
220 let url = typeof reqUrl === "string" ? new URL(reqUrl, serverOrigin ?? (typeof window === "undefined" ? "server://singlefetch/" : window.location.origin)) : new URL(reqUrl);
221 if (serverOrigin && url.origin !== serverOrigin) url = new URL(url.pathname + url.search + url.hash, serverOrigin);
222 if (url.pathname.endsWith("/")) url.pathname = `${url.pathname}_.${extension}`;
223 else url.pathname = `${url.pathname}.${extension}`;
224 return url;
225}
226function fetchAndDecodeViaTurboStream(fetchImplementation, serverOrigin) {
227 return async (args, targetRoutes) => {
228 let { request } = args;
229 let url = singleFetchUrl(request.url, "data", serverOrigin);
230 if (request.method === "GET") {
231 url = stripIndexParam(url);
232 if (targetRoutes) url.searchParams.set("_routes", targetRoutes.join(","));
233 }
234 let req = new Request(url, await createRequestInit(request));
235 invariant(args.unstable_initiator !== "static", "Static data strategy initiator used for a client data request");
236 let fetchContext;
237 if (args.unstable_initiator === "navigation") {
238 invariant(args.unstable_navigationType !== null, "Missing navigation type for a navigation data request");
239 fetchContext = {
240 type: "navigation",
241 fetcherKey: args.fetcherKey,
242 navigationType: args.unstable_navigationType
243 };
244 } else fetchContext = {
245 type: args.unstable_initiator,
246 fetcherKey: args.fetcherKey,
247 navigationType: null
248 };
249 let res = await fetchImplementation(req, fetchContext);
250 if (res.status >= 400 && !res.headers.has("X-Remix-Response")) throw new ErrorResponseImpl(res.status, res.statusText, await res.text());
251 if (res.status === 204 && res.headers.has("X-Remix-Redirect")) return {
252 status: 202,
253 data: { redirect: {
254 redirect: res.headers.get("X-Remix-Redirect"),
255 status: Number(res.headers.get("X-Remix-Status") || "302"),
256 revalidate: res.headers.get("X-Remix-Revalidate") === "true",
257 reload: res.headers.get("X-Remix-Reload-Document") === "true",
258 replace: res.headers.get("X-Remix-Replace") === "true"
259 } }
260 };
261 if (NO_BODY_STATUS_CODES.has(res.status)) {
262 let routes = {};
263 if (targetRoutes && request.method !== "GET") routes[targetRoutes[0]] = { data: void 0 };
264 return {
265 status: res.status,
266 data: { routes }
267 };
268 }
269 invariant(res.body, "No response body to decode");
270 try {
271 let decoded = await decodeViaTurboStream(res.body, window);
272 let data;
273 if (request.method === "GET") {
274 let typed = decoded.value;
275 if (SingleFetchRedirectSymbol in typed) data = { redirect: typed[SingleFetchRedirectSymbol] };
276 else data = { routes: typed };
277 } else {
278 let typed = decoded.value;
279 let routeId = targetRoutes?.[0];
280 invariant(routeId, "No routeId found for single fetch call decoding");
281 if ("redirect" in typed) data = { redirect: typed };
282 else data = { routes: { [routeId]: typed } };
283 }
284 return {
285 status: res.status,
286 data
287 };
288 } catch (cause) {
289 throw new Error("Unable to decode turbo-stream response", { cause });
290 }
291 };
292}
293function decodeViaTurboStream(body, global) {
294 return decode(body, { plugins: [(type, ...rest) => {
295 if (type === "SanitizedError") {
296 let [name, message, stack] = rest;
297 let Constructor = Error;
298 if (name && SUPPORTED_ERROR_TYPES.includes(name) && name in global && typeof global[name] === "function") Constructor = global[name];
299 let error = new Constructor(message);
300 error.stack = stack;
301 return { value: error };
302 }
303 if (type === "ErrorResponse") {
304 let [data, status, statusText] = rest;
305 return { value: new ErrorResponseImpl(status, statusText, data) };
306 }
307 if (type === "SingleFetchRedirect") return { value: { [SingleFetchRedirectSymbol]: rest[0] } };
308 if (type === "SingleFetchClassInstance") return { value: rest[0] };
309 if (type === "SingleFetchFallback") return { value: void 0 };
310 }] });
311}
312function unwrapSingleFetchResult(result, routeId) {
313 if ("redirect" in result) {
314 let { redirect: location, revalidate, reload, replace, status } = result.redirect;
315 throw redirect(location, {
316 status,
317 headers: {
318 ...revalidate ? { "X-Remix-Revalidate": "yes" } : null,
319 ...reload ? { "X-Remix-Reload-Document": "yes" } : null,
320 ...replace ? { "X-Remix-Replace": "yes" } : null
321 }
322 });
323 }
324 let routeResult = result.routes[routeId];
325 if (routeResult == null) throw new SingleFetchNoResultError(`No result found for routeId "${routeId}"`);
326 else if ("error" in routeResult) throw routeResult.error;
327 else if ("data" in routeResult) return routeResult.data;
328 else throw new Error(`Invalid response found for routeId "${routeId}"`);
329}
330function createDeferred() {
331 let resolve;
332 let reject;
333 let promise = new Promise((res, rej) => {
334 resolve = async (val) => {
335 res(val);
336 try {
337 await promise;
338 } catch {}
339 };
340 reject = async (error) => {
341 rej(error);
342 try {
343 await promise;
344 } catch {}
345 };
346 });
347 return {
348 promise,
349 resolve,
350 reject
351 };
352}
353//#endregion
354export { NO_BODY_STATUS_CODES, SingleFetchRedirectSymbol, StreamTransfer, decodeViaTurboStream, getSingleFetchDataStrategyImpl, getTurboStreamSingleFetchDataStrategy, singleFetchUrl, stripIndexParam };