source: node_modules/@remix-run/router/dist/router.js@ d24f17c

main
Last change on this file since d24f17c was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 163.0 KB
RevLine 
[d24f17c]1/**
2 * @remix-run/router v1.15.1
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 */
11function _extends() {
12 _extends = Object.assign ? Object.assign.bind() : function (target) {
13 for (var i = 1; i < arguments.length; i++) {
14 var source = arguments[i];
15 for (var key in source) {
16 if (Object.prototype.hasOwnProperty.call(source, key)) {
17 target[key] = source[key];
18 }
19 }
20 }
21 return target;
22 };
23 return _extends.apply(this, arguments);
24}
25
26////////////////////////////////////////////////////////////////////////////////
27//#region Types and Constants
28////////////////////////////////////////////////////////////////////////////////
29/**
30 * Actions represent the type of change to a location value.
31 */
32var Action;
33(function (Action) {
34 /**
35 * A POP indicates a change to an arbitrary index in the history stack, such
36 * as a back or forward navigation. It does not describe the direction of the
37 * navigation, only that the current index changed.
38 *
39 * Note: This is the default action for newly created history objects.
40 */
41 Action["Pop"] = "POP";
42 /**
43 * A PUSH indicates a new entry being added to the history stack, such as when
44 * a link is clicked and a new page loads. When this happens, all subsequent
45 * entries in the stack are lost.
46 */
47 Action["Push"] = "PUSH";
48 /**
49 * A REPLACE indicates the entry at the current index in the history stack
50 * being replaced by a new one.
51 */
52 Action["Replace"] = "REPLACE";
53})(Action || (Action = {}));
54const PopStateEventType = "popstate";
55/**
56 * Memory history stores the current location in memory. It is designed for use
57 * in stateful non-browser environments like tests and React Native.
58 */
59function createMemoryHistory(options) {
60 if (options === void 0) {
61 options = {};
62 }
63 let {
64 initialEntries = ["/"],
65 initialIndex,
66 v5Compat = false
67 } = options;
68 let entries; // Declare so we can access from createMemoryLocation
69 entries = initialEntries.map((entry, index) => createMemoryLocation(entry, typeof entry === "string" ? null : entry.state, index === 0 ? "default" : undefined));
70 let index = clampIndex(initialIndex == null ? entries.length - 1 : initialIndex);
71 let action = Action.Pop;
72 let listener = null;
73 function clampIndex(n) {
74 return Math.min(Math.max(n, 0), entries.length - 1);
75 }
76 function getCurrentLocation() {
77 return entries[index];
78 }
79 function createMemoryLocation(to, state, key) {
80 if (state === void 0) {
81 state = null;
82 }
83 let location = createLocation(entries ? getCurrentLocation().pathname : "/", to, state, key);
84 warning(location.pathname.charAt(0) === "/", "relative pathnames are not supported in memory history: " + JSON.stringify(to));
85 return location;
86 }
87 function createHref(to) {
88 return typeof to === "string" ? to : createPath(to);
89 }
90 let history = {
91 get index() {
92 return index;
93 },
94 get action() {
95 return action;
96 },
97 get location() {
98 return getCurrentLocation();
99 },
100 createHref,
101 createURL(to) {
102 return new URL(createHref(to), "http://localhost");
103 },
104 encodeLocation(to) {
105 let path = typeof to === "string" ? parsePath(to) : to;
106 return {
107 pathname: path.pathname || "",
108 search: path.search || "",
109 hash: path.hash || ""
110 };
111 },
112 push(to, state) {
113 action = Action.Push;
114 let nextLocation = createMemoryLocation(to, state);
115 index += 1;
116 entries.splice(index, entries.length, nextLocation);
117 if (v5Compat && listener) {
118 listener({
119 action,
120 location: nextLocation,
121 delta: 1
122 });
123 }
124 },
125 replace(to, state) {
126 action = Action.Replace;
127 let nextLocation = createMemoryLocation(to, state);
128 entries[index] = nextLocation;
129 if (v5Compat && listener) {
130 listener({
131 action,
132 location: nextLocation,
133 delta: 0
134 });
135 }
136 },
137 go(delta) {
138 action = Action.Pop;
139 let nextIndex = clampIndex(index + delta);
140 let nextLocation = entries[nextIndex];
141 index = nextIndex;
142 if (listener) {
143 listener({
144 action,
145 location: nextLocation,
146 delta
147 });
148 }
149 },
150 listen(fn) {
151 listener = fn;
152 return () => {
153 listener = null;
154 };
155 }
156 };
157 return history;
158}
159/**
160 * Browser history stores the location in regular URLs. This is the standard for
161 * most web apps, but it requires some configuration on the server to ensure you
162 * serve the same app at multiple URLs.
163 *
164 * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory
165 */
166function createBrowserHistory(options) {
167 if (options === void 0) {
168 options = {};
169 }
170 function createBrowserLocation(window, globalHistory) {
171 let {
172 pathname,
173 search,
174 hash
175 } = window.location;
176 return createLocation("", {
177 pathname,
178 search,
179 hash
180 },
181 // state defaults to `null` because `window.history.state` does
182 globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default");
183 }
184 function createBrowserHref(window, to) {
185 return typeof to === "string" ? to : createPath(to);
186 }
187 return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options);
188}
189/**
190 * Hash history stores the location in window.location.hash. This makes it ideal
191 * for situations where you don't want to send the location to the server for
192 * some reason, either because you do cannot configure it or the URL space is
193 * reserved for something else.
194 *
195 * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory
196 */
197function createHashHistory(options) {
198 if (options === void 0) {
199 options = {};
200 }
201 function createHashLocation(window, globalHistory) {
202 let {
203 pathname = "/",
204 search = "",
205 hash = ""
206 } = parsePath(window.location.hash.substr(1));
207 // Hash URL should always have a leading / just like window.location.pathname
208 // does, so if an app ends up at a route like /#something then we add a
209 // leading slash so all of our path-matching behaves the same as if it would
210 // in a browser router. This is particularly important when there exists a
211 // root splat route (<Route path="*">) since that matches internally against
212 // "/*" and we'd expect /#something to 404 in a hash router app.
213 if (!pathname.startsWith("/") && !pathname.startsWith(".")) {
214 pathname = "/" + pathname;
215 }
216 return createLocation("", {
217 pathname,
218 search,
219 hash
220 },
221 // state defaults to `null` because `window.history.state` does
222 globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default");
223 }
224 function createHashHref(window, to) {
225 let base = window.document.querySelector("base");
226 let href = "";
227 if (base && base.getAttribute("href")) {
228 let url = window.location.href;
229 let hashIndex = url.indexOf("#");
230 href = hashIndex === -1 ? url : url.slice(0, hashIndex);
231 }
232 return href + "#" + (typeof to === "string" ? to : createPath(to));
233 }
234 function validateHashLocation(location, to) {
235 warning(location.pathname.charAt(0) === "/", "relative pathnames are not supported in hash history.push(" + JSON.stringify(to) + ")");
236 }
237 return getUrlBasedHistory(createHashLocation, createHashHref, validateHashLocation, options);
238}
239function invariant(value, message) {
240 if (value === false || value === null || typeof value === "undefined") {
241 throw new Error(message);
242 }
243}
244function warning(cond, message) {
245 if (!cond) {
246 // eslint-disable-next-line no-console
247 if (typeof console !== "undefined") console.warn(message);
248 try {
249 // Welcome to debugging history!
250 //
251 // This error is thrown as a convenience, so you can more easily
252 // find the source for a warning that appears in the console by
253 // enabling "pause on exceptions" in your JavaScript debugger.
254 throw new Error(message);
255 // eslint-disable-next-line no-empty
256 } catch (e) {}
257 }
258}
259function createKey() {
260 return Math.random().toString(36).substr(2, 8);
261}
262/**
263 * For browser-based histories, we combine the state and key into an object
264 */
265function getHistoryState(location, index) {
266 return {
267 usr: location.state,
268 key: location.key,
269 idx: index
270 };
271}
272/**
273 * Creates a Location object with a unique key from the given Path
274 */
275function createLocation(current, to, state, key) {
276 if (state === void 0) {
277 state = null;
278 }
279 let location = _extends({
280 pathname: typeof current === "string" ? current : current.pathname,
281 search: "",
282 hash: ""
283 }, typeof to === "string" ? parsePath(to) : to, {
284 state,
285 // TODO: This could be cleaned up. push/replace should probably just take
286 // full Locations now and avoid the need to run through this flow at all
287 // But that's a pretty big refactor to the current test suite so going to
288 // keep as is for the time being and just let any incoming keys take precedence
289 key: to && to.key || key || createKey()
290 });
291 return location;
292}
293/**
294 * Creates a string URL path from the given pathname, search, and hash components.
295 */
296function createPath(_ref) {
297 let {
298 pathname = "/",
299 search = "",
300 hash = ""
301 } = _ref;
302 if (search && search !== "?") pathname += search.charAt(0) === "?" ? search : "?" + search;
303 if (hash && hash !== "#") pathname += hash.charAt(0) === "#" ? hash : "#" + hash;
304 return pathname;
305}
306/**
307 * Parses a string URL path into its separate pathname, search, and hash components.
308 */
309function parsePath(path) {
310 let parsedPath = {};
311 if (path) {
312 let hashIndex = path.indexOf("#");
313 if (hashIndex >= 0) {
314 parsedPath.hash = path.substr(hashIndex);
315 path = path.substr(0, hashIndex);
316 }
317 let searchIndex = path.indexOf("?");
318 if (searchIndex >= 0) {
319 parsedPath.search = path.substr(searchIndex);
320 path = path.substr(0, searchIndex);
321 }
322 if (path) {
323 parsedPath.pathname = path;
324 }
325 }
326 return parsedPath;
327}
328function getUrlBasedHistory(getLocation, createHref, validateLocation, options) {
329 if (options === void 0) {
330 options = {};
331 }
332 let {
333 window = document.defaultView,
334 v5Compat = false
335 } = options;
336 let globalHistory = window.history;
337 let action = Action.Pop;
338 let listener = null;
339 let index = getIndex();
340 // Index should only be null when we initialize. If not, it's because the
341 // user called history.pushState or history.replaceState directly, in which
342 // case we should log a warning as it will result in bugs.
343 if (index == null) {
344 index = 0;
345 globalHistory.replaceState(_extends({}, globalHistory.state, {
346 idx: index
347 }), "");
348 }
349 function getIndex() {
350 let state = globalHistory.state || {
351 idx: null
352 };
353 return state.idx;
354 }
355 function handlePop() {
356 action = Action.Pop;
357 let nextIndex = getIndex();
358 let delta = nextIndex == null ? null : nextIndex - index;
359 index = nextIndex;
360 if (listener) {
361 listener({
362 action,
363 location: history.location,
364 delta
365 });
366 }
367 }
368 function push(to, state) {
369 action = Action.Push;
370 let location = createLocation(history.location, to, state);
371 if (validateLocation) validateLocation(location, to);
372 index = getIndex() + 1;
373 let historyState = getHistoryState(location, index);
374 let url = history.createHref(location);
375 // try...catch because iOS limits us to 100 pushState calls :/
376 try {
377 globalHistory.pushState(historyState, "", url);
378 } catch (error) {
379 // If the exception is because `state` can't be serialized, let that throw
380 // outwards just like a replace call would so the dev knows the cause
381 // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps
382 // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
383 if (error instanceof DOMException && error.name === "DataCloneError") {
384 throw error;
385 }
386 // They are going to lose state here, but there is no real
387 // way to warn them about it since the page will refresh...
388 window.location.assign(url);
389 }
390 if (v5Compat && listener) {
391 listener({
392 action,
393 location: history.location,
394 delta: 1
395 });
396 }
397 }
398 function replace(to, state) {
399 action = Action.Replace;
400 let location = createLocation(history.location, to, state);
401 if (validateLocation) validateLocation(location, to);
402 index = getIndex();
403 let historyState = getHistoryState(location, index);
404 let url = history.createHref(location);
405 globalHistory.replaceState(historyState, "", url);
406 if (v5Compat && listener) {
407 listener({
408 action,
409 location: history.location,
410 delta: 0
411 });
412 }
413 }
414 function createURL(to) {
415 // window.location.origin is "null" (the literal string value) in Firefox
416 // under certain conditions, notably when serving from a local HTML file
417 // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
418 let base = window.location.origin !== "null" ? window.location.origin : window.location.href;
419 let href = typeof to === "string" ? to : createPath(to);
420 // Treating this as a full URL will strip any trailing spaces so we need to
421 // pre-encode them since they might be part of a matching splat param from
422 // an ancestor route
423 href = href.replace(/ $/, "%20");
424 invariant(base, "No window.location.(origin|href) available to create URL for href: " + href);
425 return new URL(href, base);
426 }
427 let history = {
428 get action() {
429 return action;
430 },
431 get location() {
432 return getLocation(window, globalHistory);
433 },
434 listen(fn) {
435 if (listener) {
436 throw new Error("A history only accepts one active listener");
437 }
438 window.addEventListener(PopStateEventType, handlePop);
439 listener = fn;
440 return () => {
441 window.removeEventListener(PopStateEventType, handlePop);
442 listener = null;
443 };
444 },
445 createHref(to) {
446 return createHref(window, to);
447 },
448 createURL,
449 encodeLocation(to) {
450 // Encode a Location the same way window.location would
451 let url = createURL(to);
452 return {
453 pathname: url.pathname,
454 search: url.search,
455 hash: url.hash
456 };
457 },
458 push,
459 replace,
460 go(n) {
461 return globalHistory.go(n);
462 }
463 };
464 return history;
465}
466//#endregion
467
468var ResultType;
469(function (ResultType) {
470 ResultType["data"] = "data";
471 ResultType["deferred"] = "deferred";
472 ResultType["redirect"] = "redirect";
473 ResultType["error"] = "error";
474})(ResultType || (ResultType = {}));
475const immutableRouteKeys = new Set(["lazy", "caseSensitive", "path", "id", "index", "children"]);
476function isIndexRoute(route) {
477 return route.index === true;
478}
479// Walk the route tree generating unique IDs where necessary, so we are working
480// solely with AgnosticDataRouteObject's within the Router
481function convertRoutesToDataRoutes(routes, mapRouteProperties, parentPath, manifest) {
482 if (parentPath === void 0) {
483 parentPath = [];
484 }
485 if (manifest === void 0) {
486 manifest = {};
487 }
488 return routes.map((route, index) => {
489 let treePath = [...parentPath, index];
490 let id = typeof route.id === "string" ? route.id : treePath.join("-");
491 invariant(route.index !== true || !route.children, "Cannot specify children on an index route");
492 invariant(!manifest[id], "Found a route id collision on id \"" + id + "\". Route " + "id's must be globally unique within Data Router usages");
493 if (isIndexRoute(route)) {
494 let indexRoute = _extends({}, route, mapRouteProperties(route), {
495 id
496 });
497 manifest[id] = indexRoute;
498 return indexRoute;
499 } else {
500 let pathOrLayoutRoute = _extends({}, route, mapRouteProperties(route), {
501 id,
502 children: undefined
503 });
504 manifest[id] = pathOrLayoutRoute;
505 if (route.children) {
506 pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, mapRouteProperties, treePath, manifest);
507 }
508 return pathOrLayoutRoute;
509 }
510 });
511}
512/**
513 * Matches the given routes to a location and returns the match data.
514 *
515 * @see https://reactrouter.com/utils/match-routes
516 */
517function matchRoutes(routes, locationArg, basename) {
518 if (basename === void 0) {
519 basename = "/";
520 }
521 let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
522 let pathname = stripBasename(location.pathname || "/", basename);
523 if (pathname == null) {
524 return null;
525 }
526 let branches = flattenRoutes(routes);
527 rankRouteBranches(branches);
528 let matches = null;
529 for (let i = 0; matches == null && i < branches.length; ++i) {
530 // Incoming pathnames are generally encoded from either window.location
531 // or from router.navigate, but we want to match against the unencoded
532 // paths in the route definitions. Memory router locations won't be
533 // encoded here but there also shouldn't be anything to decode so this
534 // should be a safe operation. This avoids needing matchRoutes to be
535 // history-aware.
536 let decoded = decodePath(pathname);
537 matches = matchRouteBranch(branches[i], decoded);
538 }
539 return matches;
540}
541function convertRouteMatchToUiMatch(match, loaderData) {
542 let {
543 route,
544 pathname,
545 params
546 } = match;
547 return {
548 id: route.id,
549 pathname,
550 params,
551 data: loaderData[route.id],
552 handle: route.handle
553 };
554}
555function flattenRoutes(routes, branches, parentsMeta, parentPath) {
556 if (branches === void 0) {
557 branches = [];
558 }
559 if (parentsMeta === void 0) {
560 parentsMeta = [];
561 }
562 if (parentPath === void 0) {
563 parentPath = "";
564 }
565 let flattenRoute = (route, index, relativePath) => {
566 let meta = {
567 relativePath: relativePath === undefined ? route.path || "" : relativePath,
568 caseSensitive: route.caseSensitive === true,
569 childrenIndex: index,
570 route
571 };
572 if (meta.relativePath.startsWith("/")) {
573 invariant(meta.relativePath.startsWith(parentPath), "Absolute route path \"" + meta.relativePath + "\" nested under path " + ("\"" + parentPath + "\" is not valid. An absolute child route path ") + "must start with the combined path of all its parent routes.");
574 meta.relativePath = meta.relativePath.slice(parentPath.length);
575 }
576 let path = joinPaths([parentPath, meta.relativePath]);
577 let routesMeta = parentsMeta.concat(meta);
578 // Add the children before adding this route to the array, so we traverse the
579 // route tree depth-first and child routes appear before their parents in
580 // the "flattened" version.
581 if (route.children && route.children.length > 0) {
582 invariant(
583 // Our types know better, but runtime JS may not!
584 // @ts-expect-error
585 route.index !== true, "Index routes must not have child routes. Please remove " + ("all child routes from route path \"" + path + "\"."));
586 flattenRoutes(route.children, branches, routesMeta, path);
587 }
588 // Routes without a path shouldn't ever match by themselves unless they are
589 // index routes, so don't add them to the list of possible branches.
590 if (route.path == null && !route.index) {
591 return;
592 }
593 branches.push({
594 path,
595 score: computeScore(path, route.index),
596 routesMeta
597 });
598 };
599 routes.forEach((route, index) => {
600 var _route$path;
601 // coarse-grain check for optional params
602 if (route.path === "" || !((_route$path = route.path) != null && _route$path.includes("?"))) {
603 flattenRoute(route, index);
604 } else {
605 for (let exploded of explodeOptionalSegments(route.path)) {
606 flattenRoute(route, index, exploded);
607 }
608 }
609 });
610 return branches;
611}
612/**
613 * Computes all combinations of optional path segments for a given path,
614 * excluding combinations that are ambiguous and of lower priority.
615 *
616 * For example, `/one/:two?/three/:four?/:five?` explodes to:
617 * - `/one/three`
618 * - `/one/:two/three`
619 * - `/one/three/:four`
620 * - `/one/three/:five`
621 * - `/one/:two/three/:four`
622 * - `/one/:two/three/:five`
623 * - `/one/three/:four/:five`
624 * - `/one/:two/three/:four/:five`
625 */
626function explodeOptionalSegments(path) {
627 let segments = path.split("/");
628 if (segments.length === 0) return [];
629 let [first, ...rest] = segments;
630 // Optional path segments are denoted by a trailing `?`
631 let isOptional = first.endsWith("?");
632 // Compute the corresponding required segment: `foo?` -> `foo`
633 let required = first.replace(/\?$/, "");
634 if (rest.length === 0) {
635 // Intepret empty string as omitting an optional segment
636 // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
637 return isOptional ? [required, ""] : [required];
638 }
639 let restExploded = explodeOptionalSegments(rest.join("/"));
640 let result = [];
641 // All child paths with the prefix. Do this for all children before the
642 // optional version for all children, so we get consistent ordering where the
643 // parent optional aspect is preferred as required. Otherwise, we can get
644 // child sections interspersed where deeper optional segments are higher than
645 // parent optional segments, where for example, /:two would explode _earlier_
646 // then /:one. By always including the parent as required _for all children_
647 // first, we avoid this issue
648 result.push(...restExploded.map(subpath => subpath === "" ? required : [required, subpath].join("/")));
649 // Then, if this is an optional value, add all child versions without
650 if (isOptional) {
651 result.push(...restExploded);
652 }
653 // for absolute paths, ensure `/` instead of empty segment
654 return result.map(exploded => path.startsWith("/") && exploded === "" ? "/" : exploded);
655}
656function rankRouteBranches(branches) {
657 branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first
658 : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex)));
659}
660const paramRe = /^:[\w-]+$/;
661const dynamicSegmentValue = 3;
662const indexRouteValue = 2;
663const emptySegmentValue = 1;
664const staticSegmentValue = 10;
665const splatPenalty = -2;
666const isSplat = s => s === "*";
667function computeScore(path, index) {
668 let segments = path.split("/");
669 let initialScore = segments.length;
670 if (segments.some(isSplat)) {
671 initialScore += splatPenalty;
672 }
673 if (index) {
674 initialScore += indexRouteValue;
675 }
676 return segments.filter(s => !isSplat(s)).reduce((score, segment) => score + (paramRe.test(segment) ? dynamicSegmentValue : segment === "" ? emptySegmentValue : staticSegmentValue), initialScore);
677}
678function compareIndexes(a, b) {
679 let siblings = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
680 return siblings ?
681 // If two routes are siblings, we should try to match the earlier sibling
682 // first. This allows people to have fine-grained control over the matching
683 // behavior by simply putting routes with identical paths in the order they
684 // want them tried.
685 a[a.length - 1] - b[b.length - 1] :
686 // Otherwise, it doesn't really make sense to rank non-siblings by index,
687 // so they sort equally.
688 0;
689}
690function matchRouteBranch(branch, pathname) {
691 let {
692 routesMeta
693 } = branch;
694 let matchedParams = {};
695 let matchedPathname = "/";
696 let matches = [];
697 for (let i = 0; i < routesMeta.length; ++i) {
698 let meta = routesMeta[i];
699 let end = i === routesMeta.length - 1;
700 let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
701 let match = matchPath({
702 path: meta.relativePath,
703 caseSensitive: meta.caseSensitive,
704 end
705 }, remainingPathname);
706 if (!match) return null;
707 Object.assign(matchedParams, match.params);
708 let route = meta.route;
709 matches.push({
710 // TODO: Can this as be avoided?
711 params: matchedParams,
712 pathname: joinPaths([matchedPathname, match.pathname]),
713 pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])),
714 route
715 });
716 if (match.pathnameBase !== "/") {
717 matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
718 }
719 }
720 return matches;
721}
722/**
723 * Returns a path with params interpolated.
724 *
725 * @see https://reactrouter.com/utils/generate-path
726 */
727function generatePath(originalPath, params) {
728 if (params === void 0) {
729 params = {};
730 }
731 let path = originalPath;
732 if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
733 warning(false, "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
734 path = path.replace(/\*$/, "/*");
735 }
736 // ensure `/` is added at the beginning if the path is absolute
737 const prefix = path.startsWith("/") ? "/" : "";
738 const stringify = p => p == null ? "" : typeof p === "string" ? p : String(p);
739 const segments = path.split(/\/+/).map((segment, index, array) => {
740 const isLastSegment = index === array.length - 1;
741 // only apply the splat if it's the last segment
742 if (isLastSegment && segment === "*") {
743 const star = "*";
744 // Apply the splat
745 return stringify(params[star]);
746 }
747 const keyMatch = segment.match(/^:([\w-]+)(\??)$/);
748 if (keyMatch) {
749 const [, key, optional] = keyMatch;
750 let param = params[key];
751 invariant(optional === "?" || param != null, "Missing \":" + key + "\" param");
752 return stringify(param);
753 }
754 // Remove any optional markers from optional static segments
755 return segment.replace(/\?$/g, "");
756 })
757 // Remove empty segments
758 .filter(segment => !!segment);
759 return prefix + segments.join("/");
760}
761/**
762 * Performs pattern matching on a URL pathname and returns information about
763 * the match.
764 *
765 * @see https://reactrouter.com/utils/match-path
766 */
767function matchPath(pattern, pathname) {
768 if (typeof pattern === "string") {
769 pattern = {
770 path: pattern,
771 caseSensitive: false,
772 end: true
773 };
774 }
775 let [matcher, compiledParams] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
776 let match = pathname.match(matcher);
777 if (!match) return null;
778 let matchedPathname = match[0];
779 let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
780 let captureGroups = match.slice(1);
781 let params = compiledParams.reduce((memo, _ref, index) => {
782 let {
783 paramName,
784 isOptional
785 } = _ref;
786 // We need to compute the pathnameBase here using the raw splat value
787 // instead of using params["*"] later because it will be decoded then
788 if (paramName === "*") {
789 let splatValue = captureGroups[index] || "";
790 pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
791 }
792 const value = captureGroups[index];
793 if (isOptional && !value) {
794 memo[paramName] = undefined;
795 } else {
796 memo[paramName] = (value || "").replace(/%2F/g, "/");
797 }
798 return memo;
799 }, {});
800 return {
801 params,
802 pathname: matchedPathname,
803 pathnameBase,
804 pattern
805 };
806}
807function compilePath(path, caseSensitive, end) {
808 if (caseSensitive === void 0) {
809 caseSensitive = false;
810 }
811 if (end === void 0) {
812 end = true;
813 }
814 warning(path === "*" || !path.endsWith("*") || path.endsWith("/*"), "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
815 let params = [];
816 let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
817 .replace(/^\/*/, "/") // Make sure it has a leading /
818 .replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
819 .replace(/\/:([\w-]+)(\?)?/g, (_, paramName, isOptional) => {
820 params.push({
821 paramName,
822 isOptional: isOptional != null
823 });
824 return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
825 });
826 if (path.endsWith("*")) {
827 params.push({
828 paramName: "*"
829 });
830 regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest
831 : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
832 } else if (end) {
833 // When matching to the end, ignore trailing slashes
834 regexpSource += "\\/*$";
835 } else if (path !== "" && path !== "/") {
836 // If our path is non-empty and contains anything beyond an initial slash,
837 // then we have _some_ form of path in our regex, so we should expect to
838 // match only if we find the end of this path segment. Look for an optional
839 // non-captured trailing slash (to match a portion of the URL) or the end
840 // of the path (if we've matched to the end). We used to do this with a
841 // word boundary but that gives false positives on routes like
842 // /user-preferences since `-` counts as a word boundary.
843 regexpSource += "(?:(?=\\/|$))";
844 } else ;
845 let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
846 return [matcher, params];
847}
848function decodePath(value) {
849 try {
850 return value.split("/").map(v => decodeURIComponent(v).replace(/\//g, "%2F")).join("/");
851 } catch (error) {
852 warning(false, "The URL path \"" + value + "\" could not be decoded because it is is a " + "malformed URL segment. This is probably due to a bad percent " + ("encoding (" + error + ")."));
853 return value;
854 }
855}
856/**
857 * @private
858 */
859function stripBasename(pathname, basename) {
860 if (basename === "/") return pathname;
861 if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
862 return null;
863 }
864 // We want to leave trailing slash behavior in the user's control, so if they
865 // specify a basename with a trailing slash, we should support it
866 let startIndex = basename.endsWith("/") ? basename.length - 1 : basename.length;
867 let nextChar = pathname.charAt(startIndex);
868 if (nextChar && nextChar !== "/") {
869 // pathname does not start with basename/
870 return null;
871 }
872 return pathname.slice(startIndex) || "/";
873}
874/**
875 * Returns a resolved path object relative to the given pathname.
876 *
877 * @see https://reactrouter.com/utils/resolve-path
878 */
879function resolvePath(to, fromPathname) {
880 if (fromPathname === void 0) {
881 fromPathname = "/";
882 }
883 let {
884 pathname: toPathname,
885 search = "",
886 hash = ""
887 } = typeof to === "string" ? parsePath(to) : to;
888 let pathname = toPathname ? toPathname.startsWith("/") ? toPathname : resolvePathname(toPathname, fromPathname) : fromPathname;
889 return {
890 pathname,
891 search: normalizeSearch(search),
892 hash: normalizeHash(hash)
893 };
894}
895function resolvePathname(relativePath, fromPathname) {
896 let segments = fromPathname.replace(/\/+$/, "").split("/");
897 let relativeSegments = relativePath.split("/");
898 relativeSegments.forEach(segment => {
899 if (segment === "..") {
900 // Keep the root "" segment so the pathname starts at /
901 if (segments.length > 1) segments.pop();
902 } else if (segment !== ".") {
903 segments.push(segment);
904 }
905 });
906 return segments.length > 1 ? segments.join("/") : "/";
907}
908function getInvalidPathError(char, field, dest, path) {
909 return "Cannot include a '" + char + "' character in a manually specified " + ("`to." + field + "` field [" + JSON.stringify(path) + "]. Please separate it out to the ") + ("`to." + dest + "` field. Alternatively you may provide the full path as ") + "a string in <Link to=\"...\"> and the router will parse it for you.";
910}
911/**
912 * @private
913 *
914 * When processing relative navigation we want to ignore ancestor routes that
915 * do not contribute to the path, such that index/pathless layout routes don't
916 * interfere.
917 *
918 * For example, when moving a route element into an index route and/or a
919 * pathless layout route, relative link behavior contained within should stay
920 * the same. Both of the following examples should link back to the root:
921 *
922 * <Route path="/">
923 * <Route path="accounts" element={<Link to=".."}>
924 * </Route>
925 *
926 * <Route path="/">
927 * <Route path="accounts">
928 * <Route element={<AccountsLayout />}> // <-- Does not contribute
929 * <Route index element={<Link to=".."} /> // <-- Does not contribute
930 * </Route
931 * </Route>
932 * </Route>
933 */
934function getPathContributingMatches(matches) {
935 return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
936}
937// Return the array of pathnames for the current route matches - used to
938// generate the routePathnames input for resolveTo()
939function getResolveToMatches(matches, v7_relativeSplatPath) {
940 let pathMatches = getPathContributingMatches(matches);
941 // When v7_relativeSplatPath is enabled, use the full pathname for the leaf
942 // match so we include splat values for "." links. See:
943 // https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329
944 if (v7_relativeSplatPath) {
945 return pathMatches.map((match, idx) => idx === matches.length - 1 ? match.pathname : match.pathnameBase);
946 }
947 return pathMatches.map(match => match.pathnameBase);
948}
949/**
950 * @private
951 */
952function resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {
953 if (isPathRelative === void 0) {
954 isPathRelative = false;
955 }
956 let to;
957 if (typeof toArg === "string") {
958 to = parsePath(toArg);
959 } else {
960 to = _extends({}, toArg);
961 invariant(!to.pathname || !to.pathname.includes("?"), getInvalidPathError("?", "pathname", "search", to));
962 invariant(!to.pathname || !to.pathname.includes("#"), getInvalidPathError("#", "pathname", "hash", to));
963 invariant(!to.search || !to.search.includes("#"), getInvalidPathError("#", "search", "hash", to));
964 }
965 let isEmptyPath = toArg === "" || to.pathname === "";
966 let toPathname = isEmptyPath ? "/" : to.pathname;
967 let from;
968 // Routing is relative to the current pathname if explicitly requested.
969 //
970 // If a pathname is explicitly provided in `to`, it should be relative to the
971 // route context. This is explained in `Note on `<Link to>` values` in our
972 // migration guide from v5 as a means of disambiguation between `to` values
973 // that begin with `/` and those that do not. However, this is problematic for
974 // `to` values that do not provide a pathname. `to` can simply be a search or
975 // hash string, in which case we should assume that the navigation is relative
976 // to the current location's pathname and *not* the route pathname.
977 if (toPathname == null) {
978 from = locationPathname;
979 } else {
980 let routePathnameIndex = routePathnames.length - 1;
981 // With relative="route" (the default), each leading .. segment means
982 // "go up one route" instead of "go up one URL segment". This is a key
983 // difference from how <a href> works and a major reason we call this a
984 // "to" value instead of a "href".
985 if (!isPathRelative && toPathname.startsWith("..")) {
986 let toSegments = toPathname.split("/");
987 while (toSegments[0] === "..") {
988 toSegments.shift();
989 routePathnameIndex -= 1;
990 }
991 to.pathname = toSegments.join("/");
992 }
993 from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
994 }
995 let path = resolvePath(to, from);
996 // Ensure the pathname has a trailing slash if the original "to" had one
997 let hasExplicitTrailingSlash = toPathname && toPathname !== "/" && toPathname.endsWith("/");
998 // Or if this was a link to the current path which has a trailing slash
999 let hasCurrentTrailingSlash = (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");
1000 if (!path.pathname.endsWith("/") && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) {
1001 path.pathname += "/";
1002 }
1003 return path;
1004}
1005/**
1006 * @private
1007 */
1008function getToPathname(to) {
1009 // Empty strings should be treated the same as / paths
1010 return to === "" || to.pathname === "" ? "/" : typeof to === "string" ? parsePath(to).pathname : to.pathname;
1011}
1012/**
1013 * @private
1014 */
1015const joinPaths = paths => paths.join("/").replace(/\/\/+/g, "/");
1016/**
1017 * @private
1018 */
1019const normalizePathname = pathname => pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
1020/**
1021 * @private
1022 */
1023const normalizeSearch = search => !search || search === "?" ? "" : search.startsWith("?") ? search : "?" + search;
1024/**
1025 * @private
1026 */
1027const normalizeHash = hash => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;
1028/**
1029 * This is a shortcut for creating `application/json` responses. Converts `data`
1030 * to JSON and sets the `Content-Type` header.
1031 */
1032const json = function json(data, init) {
1033 if (init === void 0) {
1034 init = {};
1035 }
1036 let responseInit = typeof init === "number" ? {
1037 status: init
1038 } : init;
1039 let headers = new Headers(responseInit.headers);
1040 if (!headers.has("Content-Type")) {
1041 headers.set("Content-Type", "application/json; charset=utf-8");
1042 }
1043 return new Response(JSON.stringify(data), _extends({}, responseInit, {
1044 headers
1045 }));
1046};
1047class AbortedDeferredError extends Error {}
1048class DeferredData {
1049 constructor(data, responseInit) {
1050 this.pendingKeysSet = new Set();
1051 this.subscribers = new Set();
1052 this.deferredKeys = [];
1053 invariant(data && typeof data === "object" && !Array.isArray(data), "defer() only accepts plain objects");
1054 // Set up an AbortController + Promise we can race against to exit early
1055 // cancellation
1056 let reject;
1057 this.abortPromise = new Promise((_, r) => reject = r);
1058 this.controller = new AbortController();
1059 let onAbort = () => reject(new AbortedDeferredError("Deferred data aborted"));
1060 this.unlistenAbortSignal = () => this.controller.signal.removeEventListener("abort", onAbort);
1061 this.controller.signal.addEventListener("abort", onAbort);
1062 this.data = Object.entries(data).reduce((acc, _ref2) => {
1063 let [key, value] = _ref2;
1064 return Object.assign(acc, {
1065 [key]: this.trackPromise(key, value)
1066 });
1067 }, {});
1068 if (this.done) {
1069 // All incoming values were resolved
1070 this.unlistenAbortSignal();
1071 }
1072 this.init = responseInit;
1073 }
1074 trackPromise(key, value) {
1075 if (!(value instanceof Promise)) {
1076 return value;
1077 }
1078 this.deferredKeys.push(key);
1079 this.pendingKeysSet.add(key);
1080 // We store a little wrapper promise that will be extended with
1081 // _data/_error props upon resolve/reject
1082 let promise = Promise.race([value, this.abortPromise]).then(data => this.onSettle(promise, key, undefined, data), error => this.onSettle(promise, key, error));
1083 // Register rejection listeners to avoid uncaught promise rejections on
1084 // errors or aborted deferred values
1085 promise.catch(() => {});
1086 Object.defineProperty(promise, "_tracked", {
1087 get: () => true
1088 });
1089 return promise;
1090 }
1091 onSettle(promise, key, error, data) {
1092 if (this.controller.signal.aborted && error instanceof AbortedDeferredError) {
1093 this.unlistenAbortSignal();
1094 Object.defineProperty(promise, "_error", {
1095 get: () => error
1096 });
1097 return Promise.reject(error);
1098 }
1099 this.pendingKeysSet.delete(key);
1100 if (this.done) {
1101 // Nothing left to abort!
1102 this.unlistenAbortSignal();
1103 }
1104 // If the promise was resolved/rejected with undefined, we'll throw an error as you
1105 // should always resolve with a value or null
1106 if (error === undefined && data === undefined) {
1107 let undefinedError = new Error("Deferred data for key \"" + key + "\" resolved/rejected with `undefined`, " + "you must resolve/reject with a value or `null`.");
1108 Object.defineProperty(promise, "_error", {
1109 get: () => undefinedError
1110 });
1111 this.emit(false, key);
1112 return Promise.reject(undefinedError);
1113 }
1114 if (data === undefined) {
1115 Object.defineProperty(promise, "_error", {
1116 get: () => error
1117 });
1118 this.emit(false, key);
1119 return Promise.reject(error);
1120 }
1121 Object.defineProperty(promise, "_data", {
1122 get: () => data
1123 });
1124 this.emit(false, key);
1125 return data;
1126 }
1127 emit(aborted, settledKey) {
1128 this.subscribers.forEach(subscriber => subscriber(aborted, settledKey));
1129 }
1130 subscribe(fn) {
1131 this.subscribers.add(fn);
1132 return () => this.subscribers.delete(fn);
1133 }
1134 cancel() {
1135 this.controller.abort();
1136 this.pendingKeysSet.forEach((v, k) => this.pendingKeysSet.delete(k));
1137 this.emit(true);
1138 }
1139 async resolveData(signal) {
1140 let aborted = false;
1141 if (!this.done) {
1142 let onAbort = () => this.cancel();
1143 signal.addEventListener("abort", onAbort);
1144 aborted = await new Promise(resolve => {
1145 this.subscribe(aborted => {
1146 signal.removeEventListener("abort", onAbort);
1147 if (aborted || this.done) {
1148 resolve(aborted);
1149 }
1150 });
1151 });
1152 }
1153 return aborted;
1154 }
1155 get done() {
1156 return this.pendingKeysSet.size === 0;
1157 }
1158 get unwrappedData() {
1159 invariant(this.data !== null && this.done, "Can only unwrap data on initialized and settled deferreds");
1160 return Object.entries(this.data).reduce((acc, _ref3) => {
1161 let [key, value] = _ref3;
1162 return Object.assign(acc, {
1163 [key]: unwrapTrackedPromise(value)
1164 });
1165 }, {});
1166 }
1167 get pendingKeys() {
1168 return Array.from(this.pendingKeysSet);
1169 }
1170}
1171function isTrackedPromise(value) {
1172 return value instanceof Promise && value._tracked === true;
1173}
1174function unwrapTrackedPromise(value) {
1175 if (!isTrackedPromise(value)) {
1176 return value;
1177 }
1178 if (value._error) {
1179 throw value._error;
1180 }
1181 return value._data;
1182}
1183const defer = function defer(data, init) {
1184 if (init === void 0) {
1185 init = {};
1186 }
1187 let responseInit = typeof init === "number" ? {
1188 status: init
1189 } : init;
1190 return new DeferredData(data, responseInit);
1191};
1192/**
1193 * A redirect response. Sets the status code and the `Location` header.
1194 * Defaults to "302 Found".
1195 */
1196const redirect = function redirect(url, init) {
1197 if (init === void 0) {
1198 init = 302;
1199 }
1200 let responseInit = init;
1201 if (typeof responseInit === "number") {
1202 responseInit = {
1203 status: responseInit
1204 };
1205 } else if (typeof responseInit.status === "undefined") {
1206 responseInit.status = 302;
1207 }
1208 let headers = new Headers(responseInit.headers);
1209 headers.set("Location", url);
1210 return new Response(null, _extends({}, responseInit, {
1211 headers
1212 }));
1213};
1214/**
1215 * A redirect response that will force a document reload to the new location.
1216 * Sets the status code and the `Location` header.
1217 * Defaults to "302 Found".
1218 */
1219const redirectDocument = (url, init) => {
1220 let response = redirect(url, init);
1221 response.headers.set("X-Remix-Reload-Document", "true");
1222 return response;
1223};
1224/**
1225 * @private
1226 * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
1227 *
1228 * We don't export the class for public use since it's an implementation
1229 * detail, but we export the interface above so folks can build their own
1230 * abstractions around instances via isRouteErrorResponse()
1231 */
1232class ErrorResponseImpl {
1233 constructor(status, statusText, data, internal) {
1234 if (internal === void 0) {
1235 internal = false;
1236 }
1237 this.status = status;
1238 this.statusText = statusText || "";
1239 this.internal = internal;
1240 if (data instanceof Error) {
1241 this.data = data.toString();
1242 this.error = data;
1243 } else {
1244 this.data = data;
1245 }
1246 }
1247}
1248/**
1249 * Check if the given error is an ErrorResponse generated from a 4xx/5xx
1250 * Response thrown from an action/loader
1251 */
1252function isRouteErrorResponse(error) {
1253 return error != null && typeof error.status === "number" && typeof error.statusText === "string" && typeof error.internal === "boolean" && "data" in error;
1254}
1255
1256const validMutationMethodsArr = ["post", "put", "patch", "delete"];
1257const validMutationMethods = new Set(validMutationMethodsArr);
1258const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
1259const validRequestMethods = new Set(validRequestMethodsArr);
1260const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
1261const redirectPreserveMethodStatusCodes = new Set([307, 308]);
1262const IDLE_NAVIGATION = {
1263 state: "idle",
1264 location: undefined,
1265 formMethod: undefined,
1266 formAction: undefined,
1267 formEncType: undefined,
1268 formData: undefined,
1269 json: undefined,
1270 text: undefined
1271};
1272const IDLE_FETCHER = {
1273 state: "idle",
1274 data: undefined,
1275 formMethod: undefined,
1276 formAction: undefined,
1277 formEncType: undefined,
1278 formData: undefined,
1279 json: undefined,
1280 text: undefined
1281};
1282const IDLE_BLOCKER = {
1283 state: "unblocked",
1284 proceed: undefined,
1285 reset: undefined,
1286 location: undefined
1287};
1288const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
1289const defaultMapRouteProperties = route => ({
1290 hasErrorBoundary: Boolean(route.hasErrorBoundary)
1291});
1292const TRANSITIONS_STORAGE_KEY = "remix-router-transitions";
1293//#endregion
1294////////////////////////////////////////////////////////////////////////////////
1295//#region createRouter
1296////////////////////////////////////////////////////////////////////////////////
1297/**
1298 * Create a router and listen to history POP navigations
1299 */
1300function createRouter(init) {
1301 const routerWindow = init.window ? init.window : typeof window !== "undefined" ? window : undefined;
1302 const isBrowser = typeof routerWindow !== "undefined" && typeof routerWindow.document !== "undefined" && typeof routerWindow.document.createElement !== "undefined";
1303 const isServer = !isBrowser;
1304 invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter");
1305 let mapRouteProperties;
1306 if (init.mapRouteProperties) {
1307 mapRouteProperties = init.mapRouteProperties;
1308 } else if (init.detectErrorBoundary) {
1309 // If they are still using the deprecated version, wrap it with the new API
1310 let detectErrorBoundary = init.detectErrorBoundary;
1311 mapRouteProperties = route => ({
1312 hasErrorBoundary: detectErrorBoundary(route)
1313 });
1314 } else {
1315 mapRouteProperties = defaultMapRouteProperties;
1316 }
1317 // Routes keyed by ID
1318 let manifest = {};
1319 // Routes in tree format for matching
1320 let dataRoutes = convertRoutesToDataRoutes(init.routes, mapRouteProperties, undefined, manifest);
1321 let inFlightDataRoutes;
1322 let basename = init.basename || "/";
1323 // Config driven behavior flags
1324 let future = _extends({
1325 v7_fetcherPersist: false,
1326 v7_normalizeFormMethod: false,
1327 v7_partialHydration: false,
1328 v7_prependBasename: false,
1329 v7_relativeSplatPath: false
1330 }, init.future);
1331 // Cleanup function for history
1332 let unlistenHistory = null;
1333 // Externally-provided functions to call on all state changes
1334 let subscribers = new Set();
1335 // Externally-provided object to hold scroll restoration locations during routing
1336 let savedScrollPositions = null;
1337 // Externally-provided function to get scroll restoration keys
1338 let getScrollRestorationKey = null;
1339 // Externally-provided function to get current scroll position
1340 let getScrollPosition = null;
1341 // One-time flag to control the initial hydration scroll restoration. Because
1342 // we don't get the saved positions from <ScrollRestoration /> until _after_
1343 // the initial render, we need to manually trigger a separate updateState to
1344 // send along the restoreScrollPosition
1345 // Set to true if we have `hydrationData` since we assume we were SSR'd and that
1346 // SSR did the initial scroll restoration.
1347 let initialScrollRestored = init.hydrationData != null;
1348 let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
1349 let initialErrors = null;
1350 if (initialMatches == null) {
1351 // If we do not match a user-provided-route, fall back to the root
1352 // to allow the error boundary to take over
1353 let error = getInternalRouterError(404, {
1354 pathname: init.history.location.pathname
1355 });
1356 let {
1357 matches,
1358 route
1359 } = getShortCircuitMatches(dataRoutes);
1360 initialMatches = matches;
1361 initialErrors = {
1362 [route.id]: error
1363 };
1364 }
1365 let initialized;
1366 let hasLazyRoutes = initialMatches.some(m => m.route.lazy);
1367 let hasLoaders = initialMatches.some(m => m.route.loader);
1368 if (hasLazyRoutes) {
1369 // All initialMatches need to be loaded before we're ready. If we have lazy
1370 // functions around still then we'll need to run them in initialize()
1371 initialized = false;
1372 } else if (!hasLoaders) {
1373 // If we've got no loaders to run, then we're good to go
1374 initialized = true;
1375 } else if (future.v7_partialHydration) {
1376 // If partial hydration is enabled, we're initialized so long as we were
1377 // provided with hydrationData for every route with a loader, and no loaders
1378 // were marked for explicit hydration
1379 let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
1380 let errors = init.hydrationData ? init.hydrationData.errors : null;
1381 initialized = initialMatches.every(m => m.route.loader && m.route.loader.hydrate !== true && (loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined));
1382 } else {
1383 // Without partial hydration - we're initialized if we were provided any
1384 // hydrationData - which is expected to be complete
1385 initialized = init.hydrationData != null;
1386 }
1387 let router;
1388 let state = {
1389 historyAction: init.history.action,
1390 location: init.history.location,
1391 matches: initialMatches,
1392 initialized,
1393 navigation: IDLE_NAVIGATION,
1394 // Don't restore on initial updateState() if we were SSR'd
1395 restoreScrollPosition: init.hydrationData != null ? false : null,
1396 preventScrollReset: false,
1397 revalidation: "idle",
1398 loaderData: init.hydrationData && init.hydrationData.loaderData || {},
1399 actionData: init.hydrationData && init.hydrationData.actionData || null,
1400 errors: init.hydrationData && init.hydrationData.errors || initialErrors,
1401 fetchers: new Map(),
1402 blockers: new Map()
1403 };
1404 // -- Stateful internal variables to manage navigations --
1405 // Current navigation in progress (to be committed in completeNavigation)
1406 let pendingAction = Action.Pop;
1407 // Should the current navigation prevent the scroll reset if scroll cannot
1408 // be restored?
1409 let pendingPreventScrollReset = false;
1410 // AbortController for the active navigation
1411 let pendingNavigationController;
1412 // Should the current navigation enable document.startViewTransition?
1413 let pendingViewTransitionEnabled = false;
1414 // Store applied view transitions so we can apply them on POP
1415 let appliedViewTransitions = new Map();
1416 // Cleanup function for persisting applied transitions to sessionStorage
1417 let removePageHideEventListener = null;
1418 // We use this to avoid touching history in completeNavigation if a
1419 // revalidation is entirely uninterrupted
1420 let isUninterruptedRevalidation = false;
1421 // Use this internal flag to force revalidation of all loaders:
1422 // - submissions (completed or interrupted)
1423 // - useRevalidator()
1424 // - X-Remix-Revalidate (from redirect)
1425 let isRevalidationRequired = false;
1426 // Use this internal array to capture routes that require revalidation due
1427 // to a cancelled deferred on action submission
1428 let cancelledDeferredRoutes = [];
1429 // Use this internal array to capture fetcher loads that were cancelled by an
1430 // action navigation and require revalidation
1431 let cancelledFetcherLoads = [];
1432 // AbortControllers for any in-flight fetchers
1433 let fetchControllers = new Map();
1434 // Track loads based on the order in which they started
1435 let incrementingLoadId = 0;
1436 // Track the outstanding pending navigation data load to be compared against
1437 // the globally incrementing load when a fetcher load lands after a completed
1438 // navigation
1439 let pendingNavigationLoadId = -1;
1440 // Fetchers that triggered data reloads as a result of their actions
1441 let fetchReloadIds = new Map();
1442 // Fetchers that triggered redirect navigations
1443 let fetchRedirectIds = new Set();
1444 // Most recent href/match for fetcher.load calls for fetchers
1445 let fetchLoadMatches = new Map();
1446 // Ref-count mounted fetchers so we know when it's ok to clean them up
1447 let activeFetchers = new Map();
1448 // Fetchers that have requested a delete when using v7_fetcherPersist,
1449 // they'll be officially removed after they return to idle
1450 let deletedFetchers = new Set();
1451 // Store DeferredData instances for active route matches. When a
1452 // route loader returns defer() we stick one in here. Then, when a nested
1453 // promise resolves we update loaderData. If a new navigation starts we
1454 // cancel active deferreds for eliminated routes.
1455 let activeDeferreds = new Map();
1456 // Store blocker functions in a separate Map outside of router state since
1457 // we don't need to update UI state if they change
1458 let blockerFunctions = new Map();
1459 // Flag to ignore the next history update, so we can revert the URL change on
1460 // a POP navigation that was blocked by the user without touching router state
1461 let ignoreNextHistoryUpdate = false;
1462 // Initialize the router, all side effects should be kicked off from here.
1463 // Implemented as a Fluent API for ease of:
1464 // let router = createRouter(init).initialize();
1465 function initialize() {
1466 // If history informs us of a POP navigation, start the navigation but do not update
1467 // state. We'll update our own state once the navigation completes
1468 unlistenHistory = init.history.listen(_ref => {
1469 let {
1470 action: historyAction,
1471 location,
1472 delta
1473 } = _ref;
1474 // Ignore this event if it was just us resetting the URL from a
1475 // blocked POP navigation
1476 if (ignoreNextHistoryUpdate) {
1477 ignoreNextHistoryUpdate = false;
1478 return;
1479 }
1480 warning(blockerFunctions.size === 0 || delta != null, "You are trying to use a blocker on a POP navigation to a location " + "that was not created by @remix-run/router. This will fail silently in " + "production. This can happen if you are navigating outside the router " + "via `window.history.pushState`/`window.location.hash` instead of using " + "router navigation APIs. This can also happen if you are using " + "createHashRouter and the user manually changes the URL.");
1481 let blockerKey = shouldBlockNavigation({
1482 currentLocation: state.location,
1483 nextLocation: location,
1484 historyAction
1485 });
1486 if (blockerKey && delta != null) {
1487 // Restore the URL to match the current UI, but don't update router state
1488 ignoreNextHistoryUpdate = true;
1489 init.history.go(delta * -1);
1490 // Put the blocker into a blocked state
1491 updateBlocker(blockerKey, {
1492 state: "blocked",
1493 location,
1494 proceed() {
1495 updateBlocker(blockerKey, {
1496 state: "proceeding",
1497 proceed: undefined,
1498 reset: undefined,
1499 location
1500 });
1501 // Re-do the same POP navigation we just blocked
1502 init.history.go(delta);
1503 },
1504 reset() {
1505 let blockers = new Map(state.blockers);
1506 blockers.set(blockerKey, IDLE_BLOCKER);
1507 updateState({
1508 blockers
1509 });
1510 }
1511 });
1512 return;
1513 }
1514 return startNavigation(historyAction, location);
1515 });
1516 if (isBrowser) {
1517 // FIXME: This feels gross. How can we cleanup the lines between
1518 // scrollRestoration/appliedTransitions persistance?
1519 restoreAppliedTransitions(routerWindow, appliedViewTransitions);
1520 let _saveAppliedTransitions = () => persistAppliedTransitions(routerWindow, appliedViewTransitions);
1521 routerWindow.addEventListener("pagehide", _saveAppliedTransitions);
1522 removePageHideEventListener = () => routerWindow.removeEventListener("pagehide", _saveAppliedTransitions);
1523 }
1524 // Kick off initial data load if needed. Use Pop to avoid modifying history
1525 // Note we don't do any handling of lazy here. For SPA's it'll get handled
1526 // in the normal navigation flow. For SSR it's expected that lazy modules are
1527 // resolved prior to router creation since we can't go into a fallbackElement
1528 // UI for SSR'd apps
1529 if (!state.initialized) {
1530 startNavigation(Action.Pop, state.location, {
1531 initialHydration: true
1532 });
1533 }
1534 return router;
1535 }
1536 // Clean up a router and it's side effects
1537 function dispose() {
1538 if (unlistenHistory) {
1539 unlistenHistory();
1540 }
1541 if (removePageHideEventListener) {
1542 removePageHideEventListener();
1543 }
1544 subscribers.clear();
1545 pendingNavigationController && pendingNavigationController.abort();
1546 state.fetchers.forEach((_, key) => deleteFetcher(key));
1547 state.blockers.forEach((_, key) => deleteBlocker(key));
1548 }
1549 // Subscribe to state updates for the router
1550 function subscribe(fn) {
1551 subscribers.add(fn);
1552 return () => subscribers.delete(fn);
1553 }
1554 // Update our state and notify the calling context of the change
1555 function updateState(newState, opts) {
1556 if (opts === void 0) {
1557 opts = {};
1558 }
1559 state = _extends({}, state, newState);
1560 // Prep fetcher cleanup so we can tell the UI which fetcher data entries
1561 // can be removed
1562 let completedFetchers = [];
1563 let deletedFetchersKeys = [];
1564 if (future.v7_fetcherPersist) {
1565 state.fetchers.forEach((fetcher, key) => {
1566 if (fetcher.state === "idle") {
1567 if (deletedFetchers.has(key)) {
1568 // Unmounted from the UI and can be totally removed
1569 deletedFetchersKeys.push(key);
1570 } else {
1571 // Returned to idle but still mounted in the UI, so semi-remains for
1572 // revalidations and such
1573 completedFetchers.push(key);
1574 }
1575 }
1576 });
1577 }
1578 // Iterate over a local copy so that if flushSync is used and we end up
1579 // removing and adding a new subscriber due to the useCallback dependencies,
1580 // we don't get ourselves into a loop calling the new subscriber immediately
1581 [...subscribers].forEach(subscriber => subscriber(state, {
1582 deletedFetchers: deletedFetchersKeys,
1583 unstable_viewTransitionOpts: opts.viewTransitionOpts,
1584 unstable_flushSync: opts.flushSync === true
1585 }));
1586 // Remove idle fetchers from state since we only care about in-flight fetchers.
1587 if (future.v7_fetcherPersist) {
1588 completedFetchers.forEach(key => state.fetchers.delete(key));
1589 deletedFetchersKeys.forEach(key => deleteFetcher(key));
1590 }
1591 }
1592 // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
1593 // and setting state.[historyAction/location/matches] to the new route.
1594 // - Location is a required param
1595 // - Navigation will always be set to IDLE_NAVIGATION
1596 // - Can pass any other state in newState
1597 function completeNavigation(location, newState, _temp) {
1598 var _location$state, _location$state2;
1599 let {
1600 flushSync
1601 } = _temp === void 0 ? {} : _temp;
1602 // Deduce if we're in a loading/actionReload state:
1603 // - We have committed actionData in the store
1604 // - The current navigation was a mutation submission
1605 // - We're past the submitting state and into the loading state
1606 // - The location being loaded is not the result of a redirect
1607 let isActionReload = state.actionData != null && state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && state.navigation.state === "loading" && ((_location$state = location.state) == null ? void 0 : _location$state._isRedirect) !== true;
1608 let actionData;
1609 if (newState.actionData) {
1610 if (Object.keys(newState.actionData).length > 0) {
1611 actionData = newState.actionData;
1612 } else {
1613 // Empty actionData -> clear prior actionData due to an action error
1614 actionData = null;
1615 }
1616 } else if (isActionReload) {
1617 // Keep the current data if we're wrapping up the action reload
1618 actionData = state.actionData;
1619 } else {
1620 // Clear actionData on any other completed navigations
1621 actionData = null;
1622 }
1623 // Always preserve any existing loaderData from re-used routes
1624 let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData;
1625 // On a successful navigation we can assume we got through all blockers
1626 // so we can start fresh
1627 let blockers = state.blockers;
1628 if (blockers.size > 0) {
1629 blockers = new Map(blockers);
1630 blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER));
1631 }
1632 // Always respect the user flag. Otherwise don't reset on mutation
1633 // submission navigations unless they redirect
1634 let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && ((_location$state2 = location.state) == null ? void 0 : _location$state2._isRedirect) !== true;
1635 if (inFlightDataRoutes) {
1636 dataRoutes = inFlightDataRoutes;
1637 inFlightDataRoutes = undefined;
1638 }
1639 if (isUninterruptedRevalidation) ; else if (pendingAction === Action.Pop) ; else if (pendingAction === Action.Push) {
1640 init.history.push(location, location.state);
1641 } else if (pendingAction === Action.Replace) {
1642 init.history.replace(location, location.state);
1643 }
1644 let viewTransitionOpts;
1645 // On POP, enable transitions if they were enabled on the original navigation
1646 if (pendingAction === Action.Pop) {
1647 // Forward takes precedence so they behave like the original navigation
1648 let priorPaths = appliedViewTransitions.get(state.location.pathname);
1649 if (priorPaths && priorPaths.has(location.pathname)) {
1650 viewTransitionOpts = {
1651 currentLocation: state.location,
1652 nextLocation: location
1653 };
1654 } else if (appliedViewTransitions.has(location.pathname)) {
1655 // If we don't have a previous forward nav, assume we're popping back to
1656 // the new location and enable if that location previously enabled
1657 viewTransitionOpts = {
1658 currentLocation: location,
1659 nextLocation: state.location
1660 };
1661 }
1662 } else if (pendingViewTransitionEnabled) {
1663 // Store the applied transition on PUSH/REPLACE
1664 let toPaths = appliedViewTransitions.get(state.location.pathname);
1665 if (toPaths) {
1666 toPaths.add(location.pathname);
1667 } else {
1668 toPaths = new Set([location.pathname]);
1669 appliedViewTransitions.set(state.location.pathname, toPaths);
1670 }
1671 viewTransitionOpts = {
1672 currentLocation: state.location,
1673 nextLocation: location
1674 };
1675 }
1676 updateState(_extends({}, newState, {
1677 actionData,
1678 loaderData,
1679 historyAction: pendingAction,
1680 location,
1681 initialized: true,
1682 navigation: IDLE_NAVIGATION,
1683 revalidation: "idle",
1684 restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
1685 preventScrollReset,
1686 blockers
1687 }), {
1688 viewTransitionOpts,
1689 flushSync: flushSync === true
1690 });
1691 // Reset stateful navigation vars
1692 pendingAction = Action.Pop;
1693 pendingPreventScrollReset = false;
1694 pendingViewTransitionEnabled = false;
1695 isUninterruptedRevalidation = false;
1696 isRevalidationRequired = false;
1697 cancelledDeferredRoutes = [];
1698 cancelledFetcherLoads = [];
1699 }
1700 // Trigger a navigation event, which can either be a numerical POP or a PUSH
1701 // replace with an optional submission
1702 async function navigate(to, opts) {
1703 if (typeof to === "number") {
1704 init.history.go(to);
1705 return;
1706 }
1707 let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, future.v7_relativeSplatPath, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);
1708 let {
1709 path,
1710 submission,
1711 error
1712 } = normalizeNavigateOptions(future.v7_normalizeFormMethod, false, normalizedPath, opts);
1713 let currentLocation = state.location;
1714 let nextLocation = createLocation(state.location, path, opts && opts.state);
1715 // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1716 // URL from window.location, so we need to encode it here so the behavior
1717 // remains the same as POP and non-data-router usages. new URL() does all
1718 // the same encoding we'd get from a history.pushState/window.location read
1719 // without having to touch history
1720 nextLocation = _extends({}, nextLocation, init.history.encodeLocation(nextLocation));
1721 let userReplace = opts && opts.replace != null ? opts.replace : undefined;
1722 let historyAction = Action.Push;
1723 if (userReplace === true) {
1724 historyAction = Action.Replace;
1725 } else if (userReplace === false) ; else if (submission != null && isMutationMethod(submission.formMethod) && submission.formAction === state.location.pathname + state.location.search) {
1726 // By default on submissions to the current location we REPLACE so that
1727 // users don't have to double-click the back button to get to the prior
1728 // location. If the user redirects to a different location from the
1729 // action/loader this will be ignored and the redirect will be a PUSH
1730 historyAction = Action.Replace;
1731 }
1732 let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1733 let flushSync = (opts && opts.unstable_flushSync) === true;
1734 let blockerKey = shouldBlockNavigation({
1735 currentLocation,
1736 nextLocation,
1737 historyAction
1738 });
1739 if (blockerKey) {
1740 // Put the blocker into a blocked state
1741 updateBlocker(blockerKey, {
1742 state: "blocked",
1743 location: nextLocation,
1744 proceed() {
1745 updateBlocker(blockerKey, {
1746 state: "proceeding",
1747 proceed: undefined,
1748 reset: undefined,
1749 location: nextLocation
1750 });
1751 // Send the same navigation through
1752 navigate(to, opts);
1753 },
1754 reset() {
1755 let blockers = new Map(state.blockers);
1756 blockers.set(blockerKey, IDLE_BLOCKER);
1757 updateState({
1758 blockers
1759 });
1760 }
1761 });
1762 return;
1763 }
1764 return await startNavigation(historyAction, nextLocation, {
1765 submission,
1766 // Send through the formData serialization error if we have one so we can
1767 // render at the right error boundary after we match routes
1768 pendingError: error,
1769 preventScrollReset,
1770 replace: opts && opts.replace,
1771 enableViewTransition: opts && opts.unstable_viewTransition,
1772 flushSync
1773 });
1774 }
1775 // Revalidate all current loaders. If a navigation is in progress or if this
1776 // is interrupted by a navigation, allow this to "succeed" by calling all
1777 // loaders during the next loader round
1778 function revalidate() {
1779 interruptActiveLoads();
1780 updateState({
1781 revalidation: "loading"
1782 });
1783 // If we're currently submitting an action, we don't need to start a new
1784 // navigation, we'll just let the follow up loader execution call all loaders
1785 if (state.navigation.state === "submitting") {
1786 return;
1787 }
1788 // If we're currently in an idle state, start a new navigation for the current
1789 // action/location and mark it as uninterrupted, which will skip the history
1790 // update in completeNavigation
1791 if (state.navigation.state === "idle") {
1792 startNavigation(state.historyAction, state.location, {
1793 startUninterruptedRevalidation: true
1794 });
1795 return;
1796 }
1797 // Otherwise, if we're currently in a loading state, just start a new
1798 // navigation to the navigation.location but do not trigger an uninterrupted
1799 // revalidation so that history correctly updates once the navigation completes
1800 startNavigation(pendingAction || state.historyAction, state.navigation.location, {
1801 overrideNavigation: state.navigation
1802 });
1803 }
1804 // Start a navigation to the given action/location. Can optionally provide a
1805 // overrideNavigation which will override the normalLoad in the case of a redirect
1806 // navigation
1807 async function startNavigation(historyAction, location, opts) {
1808 // Abort any in-progress navigations and start a new one. Unset any ongoing
1809 // uninterrupted revalidations unless told otherwise, since we want this
1810 // new navigation to update history normally
1811 pendingNavigationController && pendingNavigationController.abort();
1812 pendingNavigationController = null;
1813 pendingAction = historyAction;
1814 isUninterruptedRevalidation = (opts && opts.startUninterruptedRevalidation) === true;
1815 // Save the current scroll position every time we start a new navigation,
1816 // and track whether we should reset scroll on completion
1817 saveScrollPosition(state.location, state.matches);
1818 pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1819 pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
1820 let routesToUse = inFlightDataRoutes || dataRoutes;
1821 let loadingNavigation = opts && opts.overrideNavigation;
1822 let matches = matchRoutes(routesToUse, location, basename);
1823 let flushSync = (opts && opts.flushSync) === true;
1824 // Short circuit with a 404 on the root error boundary if we match nothing
1825 if (!matches) {
1826 let error = getInternalRouterError(404, {
1827 pathname: location.pathname
1828 });
1829 let {
1830 matches: notFoundMatches,
1831 route
1832 } = getShortCircuitMatches(routesToUse);
1833 // Cancel all pending deferred on 404s since we don't keep any routes
1834 cancelActiveDeferreds();
1835 completeNavigation(location, {
1836 matches: notFoundMatches,
1837 loaderData: {},
1838 errors: {
1839 [route.id]: error
1840 }
1841 }, {
1842 flushSync
1843 });
1844 return;
1845 }
1846 // Short circuit if it's only a hash change and not a revalidation or
1847 // mutation submission.
1848 //
1849 // Ignore on initial page loads because since the initial load will always
1850 // be "same hash". For example, on /page#hash and submit a <Form method="post">
1851 // which will default to a navigation to /page
1852 if (state.initialized && !isRevalidationRequired && isHashChangeOnly(state.location, location) && !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))) {
1853 completeNavigation(location, {
1854 matches
1855 }, {
1856 flushSync
1857 });
1858 return;
1859 }
1860 // Create a controller/Request for this navigation
1861 pendingNavigationController = new AbortController();
1862 let request = createClientSideRequest(init.history, location, pendingNavigationController.signal, opts && opts.submission);
1863 let pendingActionData;
1864 let pendingError;
1865 if (opts && opts.pendingError) {
1866 // If we have a pendingError, it means the user attempted a GET submission
1867 // with binary FormData so assign here and skip to handleLoaders. That
1868 // way we handle calling loaders above the boundary etc. It's not really
1869 // different from an actionError in that sense.
1870 pendingError = {
1871 [findNearestBoundary(matches).route.id]: opts.pendingError
1872 };
1873 } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
1874 // Call action if we received an action submission
1875 let actionOutput = await handleAction(request, location, opts.submission, matches, {
1876 replace: opts.replace,
1877 flushSync
1878 });
1879 if (actionOutput.shortCircuited) {
1880 return;
1881 }
1882 pendingActionData = actionOutput.pendingActionData;
1883 pendingError = actionOutput.pendingActionError;
1884 loadingNavigation = getLoadingNavigation(location, opts.submission);
1885 flushSync = false;
1886 // Create a GET request for the loaders
1887 request = new Request(request.url, {
1888 signal: request.signal
1889 });
1890 }
1891 // Call loaders
1892 let {
1893 shortCircuited,
1894 loaderData,
1895 errors
1896 } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionData, pendingError);
1897 if (shortCircuited) {
1898 return;
1899 }
1900 // Clean up now that the action/loaders have completed. Don't clean up if
1901 // we short circuited because pendingNavigationController will have already
1902 // been assigned to a new controller for the next navigation
1903 pendingNavigationController = null;
1904 completeNavigation(location, _extends({
1905 matches
1906 }, pendingActionData ? {
1907 actionData: pendingActionData
1908 } : {}, {
1909 loaderData,
1910 errors
1911 }));
1912 }
1913 // Call the action matched by the leaf route for this navigation and handle
1914 // redirects/errors
1915 async function handleAction(request, location, submission, matches, opts) {
1916 if (opts === void 0) {
1917 opts = {};
1918 }
1919 interruptActiveLoads();
1920 // Put us in a submitting state
1921 let navigation = getSubmittingNavigation(location, submission);
1922 updateState({
1923 navigation
1924 }, {
1925 flushSync: opts.flushSync === true
1926 });
1927 // Call our action and get the result
1928 let result;
1929 let actionMatch = getTargetMatch(matches, location);
1930 if (!actionMatch.route.action && !actionMatch.route.lazy) {
1931 result = {
1932 type: ResultType.error,
1933 error: getInternalRouterError(405, {
1934 method: request.method,
1935 pathname: location.pathname,
1936 routeId: actionMatch.route.id
1937 })
1938 };
1939 } else {
1940 result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
1941 if (request.signal.aborted) {
1942 return {
1943 shortCircuited: true
1944 };
1945 }
1946 }
1947 if (isRedirectResult(result)) {
1948 let replace;
1949 if (opts && opts.replace != null) {
1950 replace = opts.replace;
1951 } else {
1952 // If the user didn't explicity indicate replace behavior, replace if
1953 // we redirected to the exact same location we're currently at to avoid
1954 // double back-buttons
1955 replace = result.location === state.location.pathname + state.location.search;
1956 }
1957 await startRedirectNavigation(state, result, {
1958 submission,
1959 replace
1960 });
1961 return {
1962 shortCircuited: true
1963 };
1964 }
1965 if (isErrorResult(result)) {
1966 // Store off the pending error - we use it to determine which loaders
1967 // to call and will commit it when we complete the navigation
1968 let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1969 // By default, all submissions are REPLACE navigations, but if the
1970 // action threw an error that'll be rendered in an errorElement, we fall
1971 // back to PUSH so that the user can use the back button to get back to
1972 // the pre-submission form location to try again
1973 if ((opts && opts.replace) !== true) {
1974 pendingAction = Action.Push;
1975 }
1976 return {
1977 // Send back an empty object we can use to clear out any prior actionData
1978 pendingActionData: {},
1979 pendingActionError: {
1980 [boundaryMatch.route.id]: result.error
1981 }
1982 };
1983 }
1984 if (isDeferredResult(result)) {
1985 throw getInternalRouterError(400, {
1986 type: "defer-action"
1987 });
1988 }
1989 return {
1990 pendingActionData: {
1991 [actionMatch.route.id]: result.data
1992 }
1993 };
1994 }
1995 // Call all applicable loaders for the given matches, handling redirects,
1996 // errors, etc.
1997 async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionData, pendingError) {
1998 // Figure out the right navigation we want to use for data loading
1999 let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
2000 // If this was a redirect from an action we don't have a "submission" but
2001 // we have it on the loading navigation so use that if available
2002 let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
2003 let routesToUse = inFlightDataRoutes || dataRoutes;
2004 let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, future.v7_partialHydration && initialHydration === true, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError);
2005 // Cancel pending deferreds for no-longer-matched routes or routes we're
2006 // about to reload. Note that if this is an action reload we would have
2007 // already cancelled all pending deferreds so this would be a no-op
2008 cancelActiveDeferreds(routeId => !(matches && matches.some(m => m.route.id === routeId)) || matchesToLoad && matchesToLoad.some(m => m.route.id === routeId));
2009 pendingNavigationLoadId = ++incrementingLoadId;
2010 // Short circuit if we have no loaders to run
2011 if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
2012 let updatedFetchers = markFetchRedirectsDone();
2013 completeNavigation(location, _extends({
2014 matches,
2015 loaderData: {},
2016 // Commit pending error if we're short circuiting
2017 errors: pendingError || null
2018 }, pendingActionData ? {
2019 actionData: pendingActionData
2020 } : {}, updatedFetchers ? {
2021 fetchers: new Map(state.fetchers)
2022 } : {}), {
2023 flushSync
2024 });
2025 return {
2026 shortCircuited: true
2027 };
2028 }
2029 // If this is an uninterrupted revalidation, we remain in our current idle
2030 // state. If not, we need to switch to our loading state and load data,
2031 // preserving any new action data or existing action data (in the case of
2032 // a revalidation interrupting an actionReload)
2033 // If we have partialHydration enabled, then don't update the state for the
2034 // initial data load since iot's not a "navigation"
2035 if (!isUninterruptedRevalidation && (!future.v7_partialHydration || !initialHydration)) {
2036 revalidatingFetchers.forEach(rf => {
2037 let fetcher = state.fetchers.get(rf.key);
2038 let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
2039 state.fetchers.set(rf.key, revalidatingFetcher);
2040 });
2041 let actionData = pendingActionData || state.actionData;
2042 updateState(_extends({
2043 navigation: loadingNavigation
2044 }, actionData ? Object.keys(actionData).length === 0 ? {
2045 actionData: null
2046 } : {
2047 actionData
2048 } : {}, revalidatingFetchers.length > 0 ? {
2049 fetchers: new Map(state.fetchers)
2050 } : {}), {
2051 flushSync
2052 });
2053 }
2054 revalidatingFetchers.forEach(rf => {
2055 if (fetchControllers.has(rf.key)) {
2056 abortFetcher(rf.key);
2057 }
2058 if (rf.controller) {
2059 // Fetchers use an independent AbortController so that aborting a fetcher
2060 // (via deleteFetcher) does not abort the triggering navigation that
2061 // triggered the revalidation
2062 fetchControllers.set(rf.key, rf.controller);
2063 }
2064 });
2065 // Proxy navigation abort through to revalidation fetchers
2066 let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(f => abortFetcher(f.key));
2067 if (pendingNavigationController) {
2068 pendingNavigationController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2069 }
2070 let {
2071 results,
2072 loaderResults,
2073 fetcherResults
2074 } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, request);
2075 if (request.signal.aborted) {
2076 return {
2077 shortCircuited: true
2078 };
2079 }
2080 // Clean up _after_ loaders have completed. Don't clean up if we short
2081 // circuited because fetchControllers would have been aborted and
2082 // reassigned to new controllers for the next navigation
2083 if (pendingNavigationController) {
2084 pendingNavigationController.signal.removeEventListener("abort", abortPendingFetchRevalidations);
2085 }
2086 revalidatingFetchers.forEach(rf => fetchControllers.delete(rf.key));
2087 // If any loaders returned a redirect Response, start a new REPLACE navigation
2088 let redirect = findRedirect(results);
2089 if (redirect) {
2090 if (redirect.idx >= matchesToLoad.length) {
2091 // If this redirect came from a fetcher make sure we mark it in
2092 // fetchRedirectIds so it doesn't get revalidated on the next set of
2093 // loader executions
2094 let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2095 fetchRedirectIds.add(fetcherKey);
2096 }
2097 await startRedirectNavigation(state, redirect.result, {
2098 replace
2099 });
2100 return {
2101 shortCircuited: true
2102 };
2103 }
2104 // Process and commit output from loaders
2105 let {
2106 loaderData,
2107 errors
2108 } = processLoaderData(state, matches, matchesToLoad, loaderResults, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds);
2109 // Wire up subscribers to update loaderData as promises settle
2110 activeDeferreds.forEach((deferredData, routeId) => {
2111 deferredData.subscribe(aborted => {
2112 // Note: No need to updateState here since the TrackedPromise on
2113 // loaderData is stable across resolve/reject
2114 // Remove this instance if we were aborted or if promises have settled
2115 if (aborted || deferredData.done) {
2116 activeDeferreds.delete(routeId);
2117 }
2118 });
2119 });
2120 let updatedFetchers = markFetchRedirectsDone();
2121 let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
2122 let shouldUpdateFetchers = updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
2123 return _extends({
2124 loaderData,
2125 errors
2126 }, shouldUpdateFetchers ? {
2127 fetchers: new Map(state.fetchers)
2128 } : {});
2129 }
2130 // Trigger a fetcher load/submit for the given fetcher key
2131 function fetch(key, routeId, href, opts) {
2132 if (isServer) {
2133 throw new Error("router.fetch() was called during the server render, but it shouldn't be. " + "You are likely calling a useFetcher() method in the body of your component. " + "Try moving it to a useEffect or a callback.");
2134 }
2135 if (fetchControllers.has(key)) abortFetcher(key);
2136 let flushSync = (opts && opts.unstable_flushSync) === true;
2137 let routesToUse = inFlightDataRoutes || dataRoutes;
2138 let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, future.v7_relativeSplatPath, routeId, opts == null ? void 0 : opts.relative);
2139 let matches = matchRoutes(routesToUse, normalizedPath, basename);
2140 if (!matches) {
2141 setFetcherError(key, routeId, getInternalRouterError(404, {
2142 pathname: normalizedPath
2143 }), {
2144 flushSync
2145 });
2146 return;
2147 }
2148 let {
2149 path,
2150 submission,
2151 error
2152 } = normalizeNavigateOptions(future.v7_normalizeFormMethod, true, normalizedPath, opts);
2153 if (error) {
2154 setFetcherError(key, routeId, error, {
2155 flushSync
2156 });
2157 return;
2158 }
2159 let match = getTargetMatch(matches, path);
2160 pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2161 if (submission && isMutationMethod(submission.formMethod)) {
2162 handleFetcherAction(key, routeId, path, match, matches, flushSync, submission);
2163 return;
2164 }
2165 // Store off the match so we can call it's shouldRevalidate on subsequent
2166 // revalidations
2167 fetchLoadMatches.set(key, {
2168 routeId,
2169 path
2170 });
2171 handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission);
2172 }
2173 // Call the action for the matched fetcher.submit(), and then handle redirects,
2174 // errors, and revalidation
2175 async function handleFetcherAction(key, routeId, path, match, requestMatches, flushSync, submission) {
2176 interruptActiveLoads();
2177 fetchLoadMatches.delete(key);
2178 if (!match.route.action && !match.route.lazy) {
2179 let error = getInternalRouterError(405, {
2180 method: submission.formMethod,
2181 pathname: path,
2182 routeId: routeId
2183 });
2184 setFetcherError(key, routeId, error, {
2185 flushSync
2186 });
2187 return;
2188 }
2189 // Put this fetcher into it's submitting state
2190 let existingFetcher = state.fetchers.get(key);
2191 updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
2192 flushSync
2193 });
2194 // Call the action for the fetcher
2195 let abortController = new AbortController();
2196 let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2197 fetchControllers.set(key, abortController);
2198 let originatingLoadId = incrementingLoadId;
2199 let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2200 if (fetchRequest.signal.aborted) {
2201 // We can delete this so long as we weren't aborted by our own fetcher
2202 // re-submit which would have put _new_ controller is in fetchControllers
2203 if (fetchControllers.get(key) === abortController) {
2204 fetchControllers.delete(key);
2205 }
2206 return;
2207 }
2208 // When using v7_fetcherPersist, we don't want errors bubbling up to the UI
2209 // or redirects processed for unmounted fetchers so we just revert them to
2210 // idle
2211 if (future.v7_fetcherPersist && deletedFetchers.has(key)) {
2212 if (isRedirectResult(actionResult) || isErrorResult(actionResult)) {
2213 updateFetcherState(key, getDoneFetcher(undefined));
2214 return;
2215 }
2216 // Let SuccessResult's fall through for revalidation
2217 } else {
2218 if (isRedirectResult(actionResult)) {
2219 fetchControllers.delete(key);
2220 if (pendingNavigationLoadId > originatingLoadId) {
2221 // A new navigation was kicked off after our action started, so that
2222 // should take precedence over this redirect navigation. We already
2223 // set isRevalidationRequired so all loaders for the new route should
2224 // fire unless opted out via shouldRevalidate
2225 updateFetcherState(key, getDoneFetcher(undefined));
2226 return;
2227 } else {
2228 fetchRedirectIds.add(key);
2229 updateFetcherState(key, getLoadingFetcher(submission));
2230 return startRedirectNavigation(state, actionResult, {
2231 fetcherSubmission: submission
2232 });
2233 }
2234 }
2235 // Process any non-redirect errors thrown
2236 if (isErrorResult(actionResult)) {
2237 setFetcherError(key, routeId, actionResult.error);
2238 return;
2239 }
2240 }
2241 if (isDeferredResult(actionResult)) {
2242 throw getInternalRouterError(400, {
2243 type: "defer-action"
2244 });
2245 }
2246 // Start the data load for current matches, or the next location if we're
2247 // in the middle of a navigation
2248 let nextLocation = state.navigation.location || state.location;
2249 let revalidationRequest = createClientSideRequest(init.history, nextLocation, abortController.signal);
2250 let routesToUse = inFlightDataRoutes || dataRoutes;
2251 let matches = state.navigation.state !== "idle" ? matchRoutes(routesToUse, state.navigation.location, basename) : state.matches;
2252 invariant(matches, "Didn't find any matches after fetcher action");
2253 let loadId = ++incrementingLoadId;
2254 fetchReloadIds.set(key, loadId);
2255 let loadFetcher = getLoadingFetcher(submission, actionResult.data);
2256 state.fetchers.set(key, loadFetcher);
2257 let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, false, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, {
2258 [match.route.id]: actionResult.data
2259 }, undefined // No need to send through errors since we short circuit above
2260 );
2261 // Put all revalidating fetchers into the loading state, except for the
2262 // current fetcher which we want to keep in it's current loading state which
2263 // contains it's action submission info + action data
2264 revalidatingFetchers.filter(rf => rf.key !== key).forEach(rf => {
2265 let staleKey = rf.key;
2266 let existingFetcher = state.fetchers.get(staleKey);
2267 let revalidatingFetcher = getLoadingFetcher(undefined, existingFetcher ? existingFetcher.data : undefined);
2268 state.fetchers.set(staleKey, revalidatingFetcher);
2269 if (fetchControllers.has(staleKey)) {
2270 abortFetcher(staleKey);
2271 }
2272 if (rf.controller) {
2273 fetchControllers.set(staleKey, rf.controller);
2274 }
2275 });
2276 updateState({
2277 fetchers: new Map(state.fetchers)
2278 });
2279 let abortPendingFetchRevalidations = () => revalidatingFetchers.forEach(rf => abortFetcher(rf.key));
2280 abortController.signal.addEventListener("abort", abortPendingFetchRevalidations);
2281 let {
2282 results,
2283 loaderResults,
2284 fetcherResults
2285 } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);
2286 if (abortController.signal.aborted) {
2287 return;
2288 }
2289 abortController.signal.removeEventListener("abort", abortPendingFetchRevalidations);
2290 fetchReloadIds.delete(key);
2291 fetchControllers.delete(key);
2292 revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
2293 let redirect = findRedirect(results);
2294 if (redirect) {
2295 if (redirect.idx >= matchesToLoad.length) {
2296 // If this redirect came from a fetcher make sure we mark it in
2297 // fetchRedirectIds so it doesn't get revalidated on the next set of
2298 // loader executions
2299 let fetcherKey = revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
2300 fetchRedirectIds.add(fetcherKey);
2301 }
2302 return startRedirectNavigation(state, redirect.result);
2303 }
2304 // Process and commit output from loaders
2305 let {
2306 loaderData,
2307 errors
2308 } = processLoaderData(state, state.matches, matchesToLoad, loaderResults, undefined, revalidatingFetchers, fetcherResults, activeDeferreds);
2309 // Since we let revalidations complete even if the submitting fetcher was
2310 // deleted, only put it back to idle if it hasn't been deleted
2311 if (state.fetchers.has(key)) {
2312 let doneFetcher = getDoneFetcher(actionResult.data);
2313 state.fetchers.set(key, doneFetcher);
2314 }
2315 abortStaleFetchLoads(loadId);
2316 // If we are currently in a navigation loading state and this fetcher is
2317 // more recent than the navigation, we want the newer data so abort the
2318 // navigation and complete it with the fetcher data
2319 if (state.navigation.state === "loading" && loadId > pendingNavigationLoadId) {
2320 invariant(pendingAction, "Expected pending action");
2321 pendingNavigationController && pendingNavigationController.abort();
2322 completeNavigation(state.navigation.location, {
2323 matches,
2324 loaderData,
2325 errors,
2326 fetchers: new Map(state.fetchers)
2327 });
2328 } else {
2329 // otherwise just update with the fetcher data, preserving any existing
2330 // loaderData for loaders that did not need to reload. We have to
2331 // manually merge here since we aren't going through completeNavigation
2332 updateState({
2333 errors,
2334 loaderData: mergeLoaderData(state.loaderData, loaderData, matches, errors),
2335 fetchers: new Map(state.fetchers)
2336 });
2337 isRevalidationRequired = false;
2338 }
2339 }
2340 // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
2341 async function handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission) {
2342 let existingFetcher = state.fetchers.get(key);
2343 updateFetcherState(key, getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined), {
2344 flushSync
2345 });
2346 // Call the loader for this fetcher route match
2347 let abortController = new AbortController();
2348 let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2349 fetchControllers.set(key, abortController);
2350 let originatingLoadId = incrementingLoadId;
2351 let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2352 // Deferred isn't supported for fetcher loads, await everything and treat it
2353 // as a normal load. resolveDeferredData will return undefined if this
2354 // fetcher gets aborted, so we just leave result untouched and short circuit
2355 // below if that happens
2356 if (isDeferredResult(result)) {
2357 result = (await resolveDeferredData(result, fetchRequest.signal, true)) || result;
2358 }
2359 // We can delete this so long as we weren't aborted by our our own fetcher
2360 // re-load which would have put _new_ controller is in fetchControllers
2361 if (fetchControllers.get(key) === abortController) {
2362 fetchControllers.delete(key);
2363 }
2364 if (fetchRequest.signal.aborted) {
2365 return;
2366 }
2367 // We don't want errors bubbling up or redirects followed for unmounted
2368 // fetchers, so short circuit here if it was removed from the UI
2369 if (deletedFetchers.has(key)) {
2370 updateFetcherState(key, getDoneFetcher(undefined));
2371 return;
2372 }
2373 // If the loader threw a redirect Response, start a new REPLACE navigation
2374 if (isRedirectResult(result)) {
2375 if (pendingNavigationLoadId > originatingLoadId) {
2376 // A new navigation was kicked off after our loader started, so that
2377 // should take precedence over this redirect navigation
2378 updateFetcherState(key, getDoneFetcher(undefined));
2379 return;
2380 } else {
2381 fetchRedirectIds.add(key);
2382 await startRedirectNavigation(state, result);
2383 return;
2384 }
2385 }
2386 // Process any non-redirect errors thrown
2387 if (isErrorResult(result)) {
2388 setFetcherError(key, routeId, result.error);
2389 return;
2390 }
2391 invariant(!isDeferredResult(result), "Unhandled fetcher deferred data");
2392 // Put the fetcher back into an idle state
2393 updateFetcherState(key, getDoneFetcher(result.data));
2394 }
2395 /**
2396 * Utility function to handle redirects returned from an action or loader.
2397 * Normally, a redirect "replaces" the navigation that triggered it. So, for
2398 * example:
2399 *
2400 * - user is on /a
2401 * - user clicks a link to /b
2402 * - loader for /b redirects to /c
2403 *
2404 * In a non-JS app the browser would track the in-flight navigation to /b and
2405 * then replace it with /c when it encountered the redirect response. In
2406 * the end it would only ever update the URL bar with /c.
2407 *
2408 * In client-side routing using pushState/replaceState, we aim to emulate
2409 * this behavior and we also do not update history until the end of the
2410 * navigation (including processed redirects). This means that we never
2411 * actually touch history until we've processed redirects, so we just use
2412 * the history action from the original navigation (PUSH or REPLACE).
2413 */
2414 async function startRedirectNavigation(state, redirect, _temp2) {
2415 let {
2416 submission,
2417 fetcherSubmission,
2418 replace
2419 } = _temp2 === void 0 ? {} : _temp2;
2420 if (redirect.revalidate) {
2421 isRevalidationRequired = true;
2422 }
2423 let redirectLocation = createLocation(state.location, redirect.location, {
2424 _isRedirect: true
2425 });
2426 invariant(redirectLocation, "Expected a location on the redirect navigation");
2427 if (isBrowser) {
2428 let isDocumentReload = false;
2429 if (redirect.reloadDocument) {
2430 // Hard reload if the response contained X-Remix-Reload-Document
2431 isDocumentReload = true;
2432 } else if (ABSOLUTE_URL_REGEX.test(redirect.location)) {
2433 const url = init.history.createURL(redirect.location);
2434 isDocumentReload =
2435 // Hard reload if it's an absolute URL to a new origin
2436 url.origin !== routerWindow.location.origin ||
2437 // Hard reload if it's an absolute URL that does not match our basename
2438 stripBasename(url.pathname, basename) == null;
2439 }
2440 if (isDocumentReload) {
2441 if (replace) {
2442 routerWindow.location.replace(redirect.location);
2443 } else {
2444 routerWindow.location.assign(redirect.location);
2445 }
2446 return;
2447 }
2448 }
2449 // There's no need to abort on redirects, since we don't detect the
2450 // redirect until the action/loaders have settled
2451 pendingNavigationController = null;
2452 let redirectHistoryAction = replace === true ? Action.Replace : Action.Push;
2453 // Use the incoming submission if provided, fallback on the active one in
2454 // state.navigation
2455 let {
2456 formMethod,
2457 formAction,
2458 formEncType
2459 } = state.navigation;
2460 if (!submission && !fetcherSubmission && formMethod && formAction && formEncType) {
2461 submission = getSubmissionFromNavigation(state.navigation);
2462 }
2463 // If this was a 307/308 submission we want to preserve the HTTP method and
2464 // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2465 // redirected location
2466 let activeSubmission = submission || fetcherSubmission;
2467 if (redirectPreserveMethodStatusCodes.has(redirect.status) && activeSubmission && isMutationMethod(activeSubmission.formMethod)) {
2468 await startNavigation(redirectHistoryAction, redirectLocation, {
2469 submission: _extends({}, activeSubmission, {
2470 formAction: redirect.location
2471 }),
2472 // Preserve this flag across redirects
2473 preventScrollReset: pendingPreventScrollReset
2474 });
2475 } else {
2476 // If we have a navigation submission, we will preserve it through the
2477 // redirect navigation
2478 let overrideNavigation = getLoadingNavigation(redirectLocation, submission);
2479 await startNavigation(redirectHistoryAction, redirectLocation, {
2480 overrideNavigation,
2481 // Send fetcher submissions through for shouldRevalidate
2482 fetcherSubmission,
2483 // Preserve this flag across redirects
2484 preventScrollReset: pendingPreventScrollReset
2485 });
2486 }
2487 }
2488 async function callLoadersAndMaybeResolveData(currentMatches, matches, matchesToLoad, fetchersToLoad, request) {
2489 // Call all navigation loaders and revalidating fetcher loaders in parallel,
2490 // then slice off the results into separate arrays so we can handle them
2491 // accordingly
2492 let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath)), ...fetchersToLoad.map(f => {
2493 if (f.matches && f.match && f.controller) {
2494 return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
2495 } else {
2496 let error = {
2497 type: ResultType.error,
2498 error: getInternalRouterError(404, {
2499 pathname: f.path
2500 })
2501 };
2502 return error;
2503 }
2504 })]);
2505 let loaderResults = results.slice(0, matchesToLoad.length);
2506 let fetcherResults = results.slice(matchesToLoad.length);
2507 await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, loaderResults.map(() => request.signal), false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(f => f.match), fetcherResults, fetchersToLoad.map(f => f.controller ? f.controller.signal : null), true)]);
2508 return {
2509 results,
2510 loaderResults,
2511 fetcherResults
2512 };
2513 }
2514 function interruptActiveLoads() {
2515 // Every interruption triggers a revalidation
2516 isRevalidationRequired = true;
2517 // Cancel pending route-level deferreds and mark cancelled routes for
2518 // revalidation
2519 cancelledDeferredRoutes.push(...cancelActiveDeferreds());
2520 // Abort in-flight fetcher loads
2521 fetchLoadMatches.forEach((_, key) => {
2522 if (fetchControllers.has(key)) {
2523 cancelledFetcherLoads.push(key);
2524 abortFetcher(key);
2525 }
2526 });
2527 }
2528 function updateFetcherState(key, fetcher, opts) {
2529 if (opts === void 0) {
2530 opts = {};
2531 }
2532 state.fetchers.set(key, fetcher);
2533 updateState({
2534 fetchers: new Map(state.fetchers)
2535 }, {
2536 flushSync: (opts && opts.flushSync) === true
2537 });
2538 }
2539 function setFetcherError(key, routeId, error, opts) {
2540 if (opts === void 0) {
2541 opts = {};
2542 }
2543 let boundaryMatch = findNearestBoundary(state.matches, routeId);
2544 deleteFetcher(key);
2545 updateState({
2546 errors: {
2547 [boundaryMatch.route.id]: error
2548 },
2549 fetchers: new Map(state.fetchers)
2550 }, {
2551 flushSync: (opts && opts.flushSync) === true
2552 });
2553 }
2554 function getFetcher(key) {
2555 if (future.v7_fetcherPersist) {
2556 activeFetchers.set(key, (activeFetchers.get(key) || 0) + 1);
2557 // If this fetcher was previously marked for deletion, unmark it since we
2558 // have a new instance
2559 if (deletedFetchers.has(key)) {
2560 deletedFetchers.delete(key);
2561 }
2562 }
2563 return state.fetchers.get(key) || IDLE_FETCHER;
2564 }
2565 function deleteFetcher(key) {
2566 let fetcher = state.fetchers.get(key);
2567 // Don't abort the controller if this is a deletion of a fetcher.submit()
2568 // in it's loading phase since - we don't want to abort the corresponding
2569 // revalidation and want them to complete and land
2570 if (fetchControllers.has(key) && !(fetcher && fetcher.state === "loading" && fetchReloadIds.has(key))) {
2571 abortFetcher(key);
2572 }
2573 fetchLoadMatches.delete(key);
2574 fetchReloadIds.delete(key);
2575 fetchRedirectIds.delete(key);
2576 deletedFetchers.delete(key);
2577 state.fetchers.delete(key);
2578 }
2579 function deleteFetcherAndUpdateState(key) {
2580 if (future.v7_fetcherPersist) {
2581 let count = (activeFetchers.get(key) || 0) - 1;
2582 if (count <= 0) {
2583 activeFetchers.delete(key);
2584 deletedFetchers.add(key);
2585 } else {
2586 activeFetchers.set(key, count);
2587 }
2588 } else {
2589 deleteFetcher(key);
2590 }
2591 updateState({
2592 fetchers: new Map(state.fetchers)
2593 });
2594 }
2595 function abortFetcher(key) {
2596 let controller = fetchControllers.get(key);
2597 invariant(controller, "Expected fetch controller: " + key);
2598 controller.abort();
2599 fetchControllers.delete(key);
2600 }
2601 function markFetchersDone(keys) {
2602 for (let key of keys) {
2603 let fetcher = getFetcher(key);
2604 let doneFetcher = getDoneFetcher(fetcher.data);
2605 state.fetchers.set(key, doneFetcher);
2606 }
2607 }
2608 function markFetchRedirectsDone() {
2609 let doneKeys = [];
2610 let updatedFetchers = false;
2611 for (let key of fetchRedirectIds) {
2612 let fetcher = state.fetchers.get(key);
2613 invariant(fetcher, "Expected fetcher: " + key);
2614 if (fetcher.state === "loading") {
2615 fetchRedirectIds.delete(key);
2616 doneKeys.push(key);
2617 updatedFetchers = true;
2618 }
2619 }
2620 markFetchersDone(doneKeys);
2621 return updatedFetchers;
2622 }
2623 function abortStaleFetchLoads(landedId) {
2624 let yeetedKeys = [];
2625 for (let [key, id] of fetchReloadIds) {
2626 if (id < landedId) {
2627 let fetcher = state.fetchers.get(key);
2628 invariant(fetcher, "Expected fetcher: " + key);
2629 if (fetcher.state === "loading") {
2630 abortFetcher(key);
2631 fetchReloadIds.delete(key);
2632 yeetedKeys.push(key);
2633 }
2634 }
2635 }
2636 markFetchersDone(yeetedKeys);
2637 return yeetedKeys.length > 0;
2638 }
2639 function getBlocker(key, fn) {
2640 let blocker = state.blockers.get(key) || IDLE_BLOCKER;
2641 if (blockerFunctions.get(key) !== fn) {
2642 blockerFunctions.set(key, fn);
2643 }
2644 return blocker;
2645 }
2646 function deleteBlocker(key) {
2647 state.blockers.delete(key);
2648 blockerFunctions.delete(key);
2649 }
2650 // Utility function to update blockers, ensuring valid state transitions
2651 function updateBlocker(key, newBlocker) {
2652 let blocker = state.blockers.get(key) || IDLE_BLOCKER;
2653 // Poor mans state machine :)
2654 // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
2655 invariant(blocker.state === "unblocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "proceeding" || blocker.state === "blocked" && newBlocker.state === "unblocked" || blocker.state === "proceeding" && newBlocker.state === "unblocked", "Invalid blocker state transition: " + blocker.state + " -> " + newBlocker.state);
2656 let blockers = new Map(state.blockers);
2657 blockers.set(key, newBlocker);
2658 updateState({
2659 blockers
2660 });
2661 }
2662 function shouldBlockNavigation(_ref2) {
2663 let {
2664 currentLocation,
2665 nextLocation,
2666 historyAction
2667 } = _ref2;
2668 if (blockerFunctions.size === 0) {
2669 return;
2670 }
2671 // We ony support a single active blocker at the moment since we don't have
2672 // any compelling use cases for multi-blocker yet
2673 if (blockerFunctions.size > 1) {
2674 warning(false, "A router only supports one blocker at a time");
2675 }
2676 let entries = Array.from(blockerFunctions.entries());
2677 let [blockerKey, blockerFunction] = entries[entries.length - 1];
2678 let blocker = state.blockers.get(blockerKey);
2679 if (blocker && blocker.state === "proceeding") {
2680 // If the blocker is currently proceeding, we don't need to re-check
2681 // it and can let this navigation continue
2682 return;
2683 }
2684 // At this point, we know we're unblocked/blocked so we need to check the
2685 // user-provided blocker function
2686 if (blockerFunction({
2687 currentLocation,
2688 nextLocation,
2689 historyAction
2690 })) {
2691 return blockerKey;
2692 }
2693 }
2694 function cancelActiveDeferreds(predicate) {
2695 let cancelledRouteIds = [];
2696 activeDeferreds.forEach((dfd, routeId) => {
2697 if (!predicate || predicate(routeId)) {
2698 // Cancel the deferred - but do not remove from activeDeferreds here -
2699 // we rely on the subscribers to do that so our tests can assert proper
2700 // cleanup via _internalActiveDeferreds
2701 dfd.cancel();
2702 cancelledRouteIds.push(routeId);
2703 activeDeferreds.delete(routeId);
2704 }
2705 });
2706 return cancelledRouteIds;
2707 }
2708 // Opt in to capturing and reporting scroll positions during navigations,
2709 // used by the <ScrollRestoration> component
2710 function enableScrollRestoration(positions, getPosition, getKey) {
2711 savedScrollPositions = positions;
2712 getScrollPosition = getPosition;
2713 getScrollRestorationKey = getKey || null;
2714 // Perform initial hydration scroll restoration, since we miss the boat on
2715 // the initial updateState() because we've not yet rendered <ScrollRestoration/>
2716 // and therefore have no savedScrollPositions available
2717 if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
2718 initialScrollRestored = true;
2719 let y = getSavedScrollPosition(state.location, state.matches);
2720 if (y != null) {
2721 updateState({
2722 restoreScrollPosition: y
2723 });
2724 }
2725 }
2726 return () => {
2727 savedScrollPositions = null;
2728 getScrollPosition = null;
2729 getScrollRestorationKey = null;
2730 };
2731 }
2732 function getScrollKey(location, matches) {
2733 if (getScrollRestorationKey) {
2734 let key = getScrollRestorationKey(location, matches.map(m => convertRouteMatchToUiMatch(m, state.loaderData)));
2735 return key || location.key;
2736 }
2737 return location.key;
2738 }
2739 function saveScrollPosition(location, matches) {
2740 if (savedScrollPositions && getScrollPosition) {
2741 let key = getScrollKey(location, matches);
2742 savedScrollPositions[key] = getScrollPosition();
2743 }
2744 }
2745 function getSavedScrollPosition(location, matches) {
2746 if (savedScrollPositions) {
2747 let key = getScrollKey(location, matches);
2748 let y = savedScrollPositions[key];
2749 if (typeof y === "number") {
2750 return y;
2751 }
2752 }
2753 return null;
2754 }
2755 function _internalSetRoutes(newRoutes) {
2756 manifest = {};
2757 inFlightDataRoutes = convertRoutesToDataRoutes(newRoutes, mapRouteProperties, undefined, manifest);
2758 }
2759 router = {
2760 get basename() {
2761 return basename;
2762 },
2763 get future() {
2764 return future;
2765 },
2766 get state() {
2767 return state;
2768 },
2769 get routes() {
2770 return dataRoutes;
2771 },
2772 get window() {
2773 return routerWindow;
2774 },
2775 initialize,
2776 subscribe,
2777 enableScrollRestoration,
2778 navigate,
2779 fetch,
2780 revalidate,
2781 // Passthrough to history-aware createHref used by useHref so we get proper
2782 // hash-aware URLs in DOM paths
2783 createHref: to => init.history.createHref(to),
2784 encodeLocation: to => init.history.encodeLocation(to),
2785 getFetcher,
2786 deleteFetcher: deleteFetcherAndUpdateState,
2787 dispose,
2788 getBlocker,
2789 deleteBlocker,
2790 _internalFetchControllers: fetchControllers,
2791 _internalActiveDeferreds: activeDeferreds,
2792 // TODO: Remove setRoutes, it's temporary to avoid dealing with
2793 // updating the tree while validating the update algorithm.
2794 _internalSetRoutes
2795 };
2796 return router;
2797}
2798//#endregion
2799////////////////////////////////////////////////////////////////////////////////
2800//#region createStaticHandler
2801////////////////////////////////////////////////////////////////////////////////
2802const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2803function createStaticHandler(routes, opts) {
2804 invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
2805 let manifest = {};
2806 let basename = (opts ? opts.basename : null) || "/";
2807 let mapRouteProperties;
2808 if (opts != null && opts.mapRouteProperties) {
2809 mapRouteProperties = opts.mapRouteProperties;
2810 } else if (opts != null && opts.detectErrorBoundary) {
2811 // If they are still using the deprecated version, wrap it with the new API
2812 let detectErrorBoundary = opts.detectErrorBoundary;
2813 mapRouteProperties = route => ({
2814 hasErrorBoundary: detectErrorBoundary(route)
2815 });
2816 } else {
2817 mapRouteProperties = defaultMapRouteProperties;
2818 }
2819 // Config driven behavior flags
2820 let future = _extends({
2821 v7_relativeSplatPath: false,
2822 v7_throwAbortReason: false
2823 }, opts ? opts.future : null);
2824 let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
2825 /**
2826 * The query() method is intended for document requests, in which we want to
2827 * call an optional action and potentially multiple loaders for all nested
2828 * routes. It returns a StaticHandlerContext object, which is very similar
2829 * to the router state (location, loaderData, actionData, errors, etc.) and
2830 * also adds SSR-specific information such as the statusCode and headers
2831 * from action/loaders Responses.
2832 *
2833 * It _should_ never throw and should report all errors through the
2834 * returned context.errors object, properly associating errors to their error
2835 * boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
2836 * used to emulate React error boundaries during SSr by performing a second
2837 * pass only down to the boundaryId.
2838 *
2839 * The one exception where we do not return a StaticHandlerContext is when a
2840 * redirect response is returned or thrown from any action/loader. We
2841 * propagate that out and return the raw Response so the HTTP server can
2842 * return it directly.
2843 */
2844 async function query(request, _temp3) {
2845 let {
2846 requestContext
2847 } = _temp3 === void 0 ? {} : _temp3;
2848 let url = new URL(request.url);
2849 let method = request.method;
2850 let location = createLocation("", createPath(url), null, "default");
2851 let matches = matchRoutes(dataRoutes, location, basename);
2852 // SSR supports HEAD requests while SPA doesn't
2853 if (!isValidMethod(method) && method !== "HEAD") {
2854 let error = getInternalRouterError(405, {
2855 method
2856 });
2857 let {
2858 matches: methodNotAllowedMatches,
2859 route
2860 } = getShortCircuitMatches(dataRoutes);
2861 return {
2862 basename,
2863 location,
2864 matches: methodNotAllowedMatches,
2865 loaderData: {},
2866 actionData: null,
2867 errors: {
2868 [route.id]: error
2869 },
2870 statusCode: error.status,
2871 loaderHeaders: {},
2872 actionHeaders: {},
2873 activeDeferreds: null
2874 };
2875 } else if (!matches) {
2876 let error = getInternalRouterError(404, {
2877 pathname: location.pathname
2878 });
2879 let {
2880 matches: notFoundMatches,
2881 route
2882 } = getShortCircuitMatches(dataRoutes);
2883 return {
2884 basename,
2885 location,
2886 matches: notFoundMatches,
2887 loaderData: {},
2888 actionData: null,
2889 errors: {
2890 [route.id]: error
2891 },
2892 statusCode: error.status,
2893 loaderHeaders: {},
2894 actionHeaders: {},
2895 activeDeferreds: null
2896 };
2897 }
2898 let result = await queryImpl(request, location, matches, requestContext);
2899 if (isResponse(result)) {
2900 return result;
2901 }
2902 // When returning StaticHandlerContext, we patch back in the location here
2903 // since we need it for React Context. But this helps keep our submit and
2904 // loadRouteData operating on a Request instead of a Location
2905 return _extends({
2906 location,
2907 basename
2908 }, result);
2909 }
2910 /**
2911 * The queryRoute() method is intended for targeted route requests, either
2912 * for fetch ?_data requests or resource route requests. In this case, we
2913 * are only ever calling a single action or loader, and we are returning the
2914 * returned value directly. In most cases, this will be a Response returned
2915 * from the action/loader, but it may be a primitive or other value as well -
2916 * and in such cases the calling context should handle that accordingly.
2917 *
2918 * We do respect the throw/return differentiation, so if an action/loader
2919 * throws, then this method will throw the value. This is important so we
2920 * can do proper boundary identification in Remix where a thrown Response
2921 * must go to the Catch Boundary but a returned Response is happy-path.
2922 *
2923 * One thing to note is that any Router-initiated Errors that make sense
2924 * to associate with a status code will be thrown as an ErrorResponse
2925 * instance which include the raw Error, such that the calling context can
2926 * serialize the error as they see fit while including the proper response
2927 * code. Examples here are 404 and 405 errors that occur prior to reaching
2928 * any user-defined loaders.
2929 */
2930 async function queryRoute(request, _temp4) {
2931 let {
2932 routeId,
2933 requestContext
2934 } = _temp4 === void 0 ? {} : _temp4;
2935 let url = new URL(request.url);
2936 let method = request.method;
2937 let location = createLocation("", createPath(url), null, "default");
2938 let matches = matchRoutes(dataRoutes, location, basename);
2939 // SSR supports HEAD requests while SPA doesn't
2940 if (!isValidMethod(method) && method !== "HEAD" && method !== "OPTIONS") {
2941 throw getInternalRouterError(405, {
2942 method
2943 });
2944 } else if (!matches) {
2945 throw getInternalRouterError(404, {
2946 pathname: location.pathname
2947 });
2948 }
2949 let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);
2950 if (routeId && !match) {
2951 throw getInternalRouterError(403, {
2952 pathname: location.pathname,
2953 routeId
2954 });
2955 } else if (!match) {
2956 // This should never hit I don't think?
2957 throw getInternalRouterError(404, {
2958 pathname: location.pathname
2959 });
2960 }
2961 let result = await queryImpl(request, location, matches, requestContext, match);
2962 if (isResponse(result)) {
2963 return result;
2964 }
2965 let error = result.errors ? Object.values(result.errors)[0] : undefined;
2966 if (error !== undefined) {
2967 // If we got back result.errors, that means the loader/action threw
2968 // _something_ that wasn't a Response, but it's not guaranteed/required
2969 // to be an `instanceof Error` either, so we have to use throw here to
2970 // preserve the "error" state outside of queryImpl.
2971 throw error;
2972 }
2973 // Pick off the right state value to return
2974 if (result.actionData) {
2975 return Object.values(result.actionData)[0];
2976 }
2977 if (result.loaderData) {
2978 var _result$activeDeferre;
2979 let data = Object.values(result.loaderData)[0];
2980 if ((_result$activeDeferre = result.activeDeferreds) != null && _result$activeDeferre[match.route.id]) {
2981 data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
2982 }
2983 return data;
2984 }
2985 return undefined;
2986 }
2987 async function queryImpl(request, location, matches, requestContext, routeMatch) {
2988 invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
2989 try {
2990 if (isMutationMethod(request.method.toLowerCase())) {
2991 let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
2992 return result;
2993 }
2994 let result = await loadRouteData(request, matches, requestContext, routeMatch);
2995 return isResponse(result) ? result : _extends({}, result, {
2996 actionData: null,
2997 actionHeaders: {}
2998 });
2999 } catch (e) {
3000 // If the user threw/returned a Response in callLoaderOrAction, we throw
3001 // it to bail out and then return or throw here based on whether the user
3002 // returned or threw
3003 if (isQueryRouteResponse(e)) {
3004 if (e.type === ResultType.error) {
3005 throw e.response;
3006 }
3007 return e.response;
3008 }
3009 // Redirects are always returned since they don't propagate to catch
3010 // boundaries
3011 if (isRedirectResponse(e)) {
3012 return e;
3013 }
3014 throw e;
3015 }
3016 }
3017 async function submit(request, matches, actionMatch, requestContext, isRouteRequest) {
3018 let result;
3019 if (!actionMatch.route.action && !actionMatch.route.lazy) {
3020 let error = getInternalRouterError(405, {
3021 method: request.method,
3022 pathname: new URL(request.url).pathname,
3023 routeId: actionMatch.route.id
3024 });
3025 if (isRouteRequest) {
3026 throw error;
3027 }
3028 result = {
3029 type: ResultType.error,
3030 error
3031 };
3032 } else {
3033 result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3034 isStaticRequest: true,
3035 isRouteRequest,
3036 requestContext
3037 });
3038 if (request.signal.aborted) {
3039 throwStaticHandlerAbortedError(request, isRouteRequest, future);
3040 }
3041 }
3042 if (isRedirectResult(result)) {
3043 // Uhhhh - this should never happen, we should always throw these from
3044 // callLoaderOrAction, but the type narrowing here keeps TS happy and we
3045 // can get back on the "throw all redirect responses" train here should
3046 // this ever happen :/
3047 throw new Response(null, {
3048 status: result.status,
3049 headers: {
3050 Location: result.location
3051 }
3052 });
3053 }
3054 if (isDeferredResult(result)) {
3055 let error = getInternalRouterError(400, {
3056 type: "defer-action"
3057 });
3058 if (isRouteRequest) {
3059 throw error;
3060 }
3061 result = {
3062 type: ResultType.error,
3063 error
3064 };
3065 }
3066 if (isRouteRequest) {
3067 // Note: This should only be non-Response values if we get here, since
3068 // isRouteRequest should throw any Response received in callLoaderOrAction
3069 if (isErrorResult(result)) {
3070 throw result.error;
3071 }
3072 return {
3073 matches: [actionMatch],
3074 loaderData: {},
3075 actionData: {
3076 [actionMatch.route.id]: result.data
3077 },
3078 errors: null,
3079 // Note: statusCode + headers are unused here since queryRoute will
3080 // return the raw Response or value
3081 statusCode: 200,
3082 loaderHeaders: {},
3083 actionHeaders: {},
3084 activeDeferreds: null
3085 };
3086 }
3087 if (isErrorResult(result)) {
3088 // Store off the pending error - we use it to determine which loaders
3089 // to call and will commit it when we complete the navigation
3090 let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
3091 let context = await loadRouteData(request, matches, requestContext, undefined, {
3092 [boundaryMatch.route.id]: result.error
3093 });
3094 // action status codes take precedence over loader status codes
3095 return _extends({}, context, {
3096 statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500,
3097 actionData: null,
3098 actionHeaders: _extends({}, result.headers ? {
3099 [actionMatch.route.id]: result.headers
3100 } : {})
3101 });
3102 }
3103 // Create a GET request for the loaders
3104 let loaderRequest = new Request(request.url, {
3105 headers: request.headers,
3106 redirect: request.redirect,
3107 signal: request.signal
3108 });
3109 let context = await loadRouteData(loaderRequest, matches, requestContext);
3110 return _extends({}, context, result.statusCode ? {
3111 statusCode: result.statusCode
3112 } : {}, {
3113 actionData: {
3114 [actionMatch.route.id]: result.data
3115 },
3116 actionHeaders: _extends({}, result.headers ? {
3117 [actionMatch.route.id]: result.headers
3118 } : {})
3119 });
3120 }
3121 async function loadRouteData(request, matches, requestContext, routeMatch, pendingActionError) {
3122 let isRouteRequest = routeMatch != null;
3123 // Short circuit if we have no loaders to run (queryRoute())
3124 if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader) && !(routeMatch != null && routeMatch.route.lazy)) {
3125 throw getInternalRouterError(400, {
3126 method: request.method,
3127 pathname: new URL(request.url).pathname,
3128 routeId: routeMatch == null ? void 0 : routeMatch.route.id
3129 });
3130 }
3131 let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
3132 let matchesToLoad = requestMatches.filter(m => m.route.loader || m.route.lazy);
3133 // Short circuit if we have no loaders to run (query())
3134 if (matchesToLoad.length === 0) {
3135 return {
3136 matches,
3137 // Add a null for all matched routes for proper revalidation on the client
3138 loaderData: matches.reduce((acc, m) => Object.assign(acc, {
3139 [m.route.id]: null
3140 }), {}),
3141 errors: pendingActionError || null,
3142 statusCode: 200,
3143 loaderHeaders: {},
3144 activeDeferreds: null
3145 };
3146 }
3147 let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
3148 isStaticRequest: true,
3149 isRouteRequest,
3150 requestContext
3151 }))]);
3152 if (request.signal.aborted) {
3153 throwStaticHandlerAbortedError(request, isRouteRequest, future);
3154 }
3155 // Process and commit output from loaders
3156 let activeDeferreds = new Map();
3157 let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError, activeDeferreds);
3158 // Add a null for any non-loader matches for proper revalidation on the client
3159 let executedLoaders = new Set(matchesToLoad.map(match => match.route.id));
3160 matches.forEach(match => {
3161 if (!executedLoaders.has(match.route.id)) {
3162 context.loaderData[match.route.id] = null;
3163 }
3164 });
3165 return _extends({}, context, {
3166 matches,
3167 activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
3168 });
3169 }
3170 return {
3171 dataRoutes,
3172 query,
3173 queryRoute
3174 };
3175}
3176//#endregion
3177////////////////////////////////////////////////////////////////////////////////
3178//#region Helpers
3179////////////////////////////////////////////////////////////////////////////////
3180/**
3181 * Given an existing StaticHandlerContext and an error thrown at render time,
3182 * provide an updated StaticHandlerContext suitable for a second SSR render
3183 */
3184function getStaticContextFromError(routes, context, error) {
3185 let newContext = _extends({}, context, {
3186 statusCode: isRouteErrorResponse(error) ? error.status : 500,
3187 errors: {
3188 [context._deepestRenderedBoundaryId || routes[0].id]: error
3189 }
3190 });
3191 return newContext;
3192}
3193function throwStaticHandlerAbortedError(request, isRouteRequest, future) {
3194 if (future.v7_throwAbortReason && request.signal.reason !== undefined) {
3195 throw request.signal.reason;
3196 }
3197 let method = isRouteRequest ? "queryRoute" : "query";
3198 throw new Error(method + "() call aborted: " + request.method + " " + request.url);
3199}
3200function isSubmissionNavigation(opts) {
3201 return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
3202}
3203function normalizeTo(location, matches, basename, prependBasename, to, v7_relativeSplatPath, fromRouteId, relative) {
3204 let contextualMatches;
3205 let activeRouteMatch;
3206 if (fromRouteId) {
3207 // Grab matches up to the calling route so our route-relative logic is
3208 // relative to the correct source route
3209 contextualMatches = [];
3210 for (let match of matches) {
3211 contextualMatches.push(match);
3212 if (match.route.id === fromRouteId) {
3213 activeRouteMatch = match;
3214 break;
3215 }
3216 }
3217 } else {
3218 contextualMatches = matches;
3219 activeRouteMatch = matches[matches.length - 1];
3220 }
3221 // Resolve the relative path
3222 let path = resolveTo(to ? to : ".", getResolveToMatches(contextualMatches, v7_relativeSplatPath), stripBasename(location.pathname, basename) || location.pathname, relative === "path");
3223 // When `to` is not specified we inherit search/hash from the current
3224 // location, unlike when to="." and we just inherit the path.
3225 // See https://github.com/remix-run/remix/issues/927
3226 if (to == null) {
3227 path.search = location.search;
3228 path.hash = location.hash;
3229 }
3230 // Add an ?index param for matched index routes if we don't already have one
3231 if ((to == null || to === "" || to === ".") && activeRouteMatch && activeRouteMatch.route.index && !hasNakedIndexQuery(path.search)) {
3232 path.search = path.search ? path.search.replace(/^\?/, "?index&") : "?index";
3233 }
3234 // If we're operating within a basename, prepend it to the pathname. If
3235 // this is a root navigation, then just use the raw basename which allows
3236 // the basename to have full control over the presence of a trailing slash
3237 // on root actions
3238 if (prependBasename && basename !== "/") {
3239 path.pathname = path.pathname === "/" ? basename : joinPaths([basename, path.pathname]);
3240 }
3241 return createPath(path);
3242}
3243// Normalize navigation options by converting formMethod=GET formData objects to
3244// URLSearchParams so they behave identically to links with query params
3245function normalizeNavigateOptions(normalizeFormMethod, isFetcher, path, opts) {
3246 // Return location verbatim on non-submission navigations
3247 if (!opts || !isSubmissionNavigation(opts)) {
3248 return {
3249 path
3250 };
3251 }
3252 if (opts.formMethod && !isValidMethod(opts.formMethod)) {
3253 return {
3254 path,
3255 error: getInternalRouterError(405, {
3256 method: opts.formMethod
3257 })
3258 };
3259 }
3260 let getInvalidBodyError = () => ({
3261 path,
3262 error: getInternalRouterError(400, {
3263 type: "invalid-body"
3264 })
3265 });
3266 // Create a Submission on non-GET navigations
3267 let rawFormMethod = opts.formMethod || "get";
3268 let formMethod = normalizeFormMethod ? rawFormMethod.toUpperCase() : rawFormMethod.toLowerCase();
3269 let formAction = stripHashFromPath(path);
3270 if (opts.body !== undefined) {
3271 if (opts.formEncType === "text/plain") {
3272 // text only support POST/PUT/PATCH/DELETE submissions
3273 if (!isMutationMethod(formMethod)) {
3274 return getInvalidBodyError();
3275 }
3276 let text = typeof opts.body === "string" ? opts.body : opts.body instanceof FormData || opts.body instanceof URLSearchParams ?
3277 // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#plain-text-form-data
3278 Array.from(opts.body.entries()).reduce((acc, _ref3) => {
3279 let [name, value] = _ref3;
3280 return "" + acc + name + "=" + value + "\n";
3281 }, "") : String(opts.body);
3282 return {
3283 path,
3284 submission: {
3285 formMethod,
3286 formAction,
3287 formEncType: opts.formEncType,
3288 formData: undefined,
3289 json: undefined,
3290 text
3291 }
3292 };
3293 } else if (opts.formEncType === "application/json") {
3294 // json only supports POST/PUT/PATCH/DELETE submissions
3295 if (!isMutationMethod(formMethod)) {
3296 return getInvalidBodyError();
3297 }
3298 try {
3299 let json = typeof opts.body === "string" ? JSON.parse(opts.body) : opts.body;
3300 return {
3301 path,
3302 submission: {
3303 formMethod,
3304 formAction,
3305 formEncType: opts.formEncType,
3306 formData: undefined,
3307 json,
3308 text: undefined
3309 }
3310 };
3311 } catch (e) {
3312 return getInvalidBodyError();
3313 }
3314 }
3315 }
3316 invariant(typeof FormData === "function", "FormData is not available in this environment");
3317 let searchParams;
3318 let formData;
3319 if (opts.formData) {
3320 searchParams = convertFormDataToSearchParams(opts.formData);
3321 formData = opts.formData;
3322 } else if (opts.body instanceof FormData) {
3323 searchParams = convertFormDataToSearchParams(opts.body);
3324 formData = opts.body;
3325 } else if (opts.body instanceof URLSearchParams) {
3326 searchParams = opts.body;
3327 formData = convertSearchParamsToFormData(searchParams);
3328 } else if (opts.body == null) {
3329 searchParams = new URLSearchParams();
3330 formData = new FormData();
3331 } else {
3332 try {
3333 searchParams = new URLSearchParams(opts.body);
3334 formData = convertSearchParamsToFormData(searchParams);
3335 } catch (e) {
3336 return getInvalidBodyError();
3337 }
3338 }
3339 let submission = {
3340 formMethod,
3341 formAction,
3342 formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
3343 formData,
3344 json: undefined,
3345 text: undefined
3346 };
3347 if (isMutationMethod(submission.formMethod)) {
3348 return {
3349 path,
3350 submission
3351 };
3352 }
3353 // Flatten submission onto URLSearchParams for GET submissions
3354 let parsedPath = parsePath(path);
3355 // On GET navigation submissions we can drop the ?index param from the
3356 // resulting location since all loaders will run. But fetcher GET submissions
3357 // only run a single loader so we need to preserve any incoming ?index params
3358 if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) {
3359 searchParams.append("index", "");
3360 }
3361 parsedPath.search = "?" + searchParams;
3362 return {
3363 path: createPath(parsedPath),
3364 submission
3365 };
3366}
3367// Filter out all routes below any caught error as they aren't going to
3368// render so we don't need to load them
3369function getLoaderMatchesUntilBoundary(matches, boundaryId) {
3370 let boundaryMatches = matches;
3371 if (boundaryId) {
3372 let index = matches.findIndex(m => m.route.id === boundaryId);
3373 if (index >= 0) {
3374 boundaryMatches = matches.slice(0, index);
3375 }
3376 }
3377 return boundaryMatches;
3378}
3379function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError) {
3380 let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
3381 let currentUrl = history.createURL(state.location);
3382 let nextUrl = history.createURL(location);
3383 // Pick navigation matches that are net-new or qualify for revalidation
3384 let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
3385 let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3386 let navigationMatches = boundaryMatches.filter((match, index) => {
3387 let {
3388 route
3389 } = match;
3390 if (route.lazy) {
3391 // We haven't loaded this route yet so we don't know if it's got a loader!
3392 return true;
3393 }
3394 if (route.loader == null) {
3395 return false;
3396 }
3397 if (isInitialLoad) {
3398 if (route.loader.hydrate) {
3399 return true;
3400 }
3401 return state.loaderData[route.id] === undefined && (
3402 // Don't re-run if the loader ran and threw an error
3403 !state.errors || state.errors[route.id] === undefined);
3404 }
3405 // Always call the loader on new route instances and pending defer cancellations
3406 if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {
3407 return true;
3408 }
3409 // This is the default implementation for when we revalidate. If the route
3410 // provides it's own implementation, then we give them full control but
3411 // provide this value so they can leverage it if needed after they check
3412 // their own specific use cases
3413 let currentRouteMatch = state.matches[index];
3414 let nextRouteMatch = match;
3415 return shouldRevalidateLoader(match, _extends({
3416 currentUrl,
3417 currentParams: currentRouteMatch.params,
3418 nextUrl,
3419 nextParams: nextRouteMatch.params
3420 }, submission, {
3421 actionResult,
3422 defaultShouldRevalidate:
3423 // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
3424 isRevalidationRequired ||
3425 // Clicked the same link, resubmitted a GET form
3426 currentUrl.pathname + currentUrl.search === nextUrl.pathname + nextUrl.search ||
3427 // Search params affect all loaders
3428 currentUrl.search !== nextUrl.search || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
3429 }));
3430 });
3431 // Pick fetcher.loads that need to be revalidated
3432 let revalidatingFetchers = [];
3433 fetchLoadMatches.forEach((f, key) => {
3434 // Don't revalidate:
3435 // - on initial load (shouldn't be any fetchers then anyway)
3436 // - if fetcher won't be present in the subsequent render
3437 // - no longer matches the URL (v7_fetcherPersist=false)
3438 // - was unmounted but persisted due to v7_fetcherPersist=true
3439 if (isInitialLoad || !matches.some(m => m.route.id === f.routeId) || deletedFetchers.has(key)) {
3440 return;
3441 }
3442 let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
3443 // If the fetcher path no longer matches, push it in with null matches so
3444 // we can trigger a 404 in callLoadersAndMaybeResolveData. Note this is
3445 // currently only a use-case for Remix HMR where the route tree can change
3446 // at runtime and remove a route previously loaded via a fetcher
3447 if (!fetcherMatches) {
3448 revalidatingFetchers.push({
3449 key,
3450 routeId: f.routeId,
3451 path: f.path,
3452 matches: null,
3453 match: null,
3454 controller: null
3455 });
3456 return;
3457 }
3458 // Revalidating fetchers are decoupled from the route matches since they
3459 // load from a static href. They revalidate based on explicit revalidation
3460 // (submission, useRevalidator, or X-Remix-Revalidate)
3461 let fetcher = state.fetchers.get(key);
3462 let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3463 let shouldRevalidate = false;
3464 if (fetchRedirectIds.has(key)) {
3465 // Never trigger a revalidation of an actively redirecting fetcher
3466 shouldRevalidate = false;
3467 } else if (cancelledFetcherLoads.includes(key)) {
3468 // Always revalidate if the fetcher was cancelled
3469 shouldRevalidate = true;
3470 } else if (fetcher && fetcher.state !== "idle" && fetcher.data === undefined) {
3471 // If the fetcher hasn't ever completed loading yet, then this isn't a
3472 // revalidation, it would just be a brand new load if an explicit
3473 // revalidation is required
3474 shouldRevalidate = isRevalidationRequired;
3475 } else {
3476 // Otherwise fall back on any user-defined shouldRevalidate, defaulting
3477 // to explicit revalidations only
3478 shouldRevalidate = shouldRevalidateLoader(fetcherMatch, _extends({
3479 currentUrl,
3480 currentParams: state.matches[state.matches.length - 1].params,
3481 nextUrl,
3482 nextParams: matches[matches.length - 1].params
3483 }, submission, {
3484 actionResult,
3485 defaultShouldRevalidate: isRevalidationRequired
3486 }));
3487 }
3488 if (shouldRevalidate) {
3489 revalidatingFetchers.push({
3490 key,
3491 routeId: f.routeId,
3492 path: f.path,
3493 matches: fetcherMatches,
3494 match: fetcherMatch,
3495 controller: new AbortController()
3496 });
3497 }
3498 });
3499 return [navigationMatches, revalidatingFetchers];
3500}
3501function isNewLoader(currentLoaderData, currentMatch, match) {
3502 let isNew =
3503 // [a] -> [a, b]
3504 !currentMatch ||
3505 // [a, b] -> [a, c]
3506 match.route.id !== currentMatch.route.id;
3507 // Handle the case that we don't have data for a re-used route, potentially
3508 // from a prior error or from a cancelled pending deferred
3509 let isMissingData = currentLoaderData[match.route.id] === undefined;
3510 // Always load if this is a net-new route or we don't yet have data
3511 return isNew || isMissingData;
3512}
3513function isNewRouteInstance(currentMatch, match) {
3514 let currentPath = currentMatch.route.path;
3515 return (
3516 // param change for this match, /users/123 -> /users/456
3517 currentMatch.pathname !== match.pathname ||
3518 // splat param changed, which is not present in match.path
3519 // e.g. /files/images/avatar.jpg -> files/finances.xls
3520 currentPath != null && currentPath.endsWith("*") && currentMatch.params["*"] !== match.params["*"]
3521 );
3522}
3523function shouldRevalidateLoader(loaderMatch, arg) {
3524 if (loaderMatch.route.shouldRevalidate) {
3525 let routeChoice = loaderMatch.route.shouldRevalidate(arg);
3526 if (typeof routeChoice === "boolean") {
3527 return routeChoice;
3528 }
3529 }
3530 return arg.defaultShouldRevalidate;
3531}
3532/**
3533 * Execute route.lazy() methods to lazily load route modules (loader, action,
3534 * shouldRevalidate) and update the routeManifest in place which shares objects
3535 * with dataRoutes so those get updated as well.
3536 */
3537async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
3538 if (!route.lazy) {
3539 return;
3540 }
3541 let lazyRoute = await route.lazy();
3542 // If the lazy route function was executed and removed by another parallel
3543 // call then we can return - first lazy() to finish wins because the return
3544 // value of lazy is expected to be static
3545 if (!route.lazy) {
3546 return;
3547 }
3548 let routeToUpdate = manifest[route.id];
3549 invariant(routeToUpdate, "No route found in manifest");
3550 // Update the route in place. This should be safe because there's no way
3551 // we could yet be sitting on this route as we can't get there without
3552 // resolving lazy() first.
3553 //
3554 // This is different than the HMR "update" use-case where we may actively be
3555 // on the route being updated. The main concern boils down to "does this
3556 // mutation affect any ongoing navigations or any current state.matches
3557 // values?". If not, it should be safe to update in place.
3558 let routeUpdates = {};
3559 for (let lazyRouteProperty in lazyRoute) {
3560 let staticRouteValue = routeToUpdate[lazyRouteProperty];
3561 let isPropertyStaticallyDefined = staticRouteValue !== undefined &&
3562 // This property isn't static since it should always be updated based
3563 // on the route updates
3564 lazyRouteProperty !== "hasErrorBoundary";
3565 warning(!isPropertyStaticallyDefined, "Route \"" + routeToUpdate.id + "\" has a static property \"" + lazyRouteProperty + "\" " + "defined but its lazy function is also returning a value for this property. " + ("The lazy route property \"" + lazyRouteProperty + "\" will be ignored."));
3566 if (!isPropertyStaticallyDefined && !immutableRouteKeys.has(lazyRouteProperty)) {
3567 routeUpdates[lazyRouteProperty] = lazyRoute[lazyRouteProperty];
3568 }
3569 }
3570 // Mutate the route with the provided updates. Do this first so we pass
3571 // the updated version to mapRouteProperties
3572 Object.assign(routeToUpdate, routeUpdates);
3573 // Mutate the `hasErrorBoundary` property on the route based on the route
3574 // updates and remove the `lazy` function so we don't resolve the lazy
3575 // route again.
3576 Object.assign(routeToUpdate, _extends({}, mapRouteProperties(routeToUpdate), {
3577 lazy: undefined
3578 }));
3579}
3580async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, v7_relativeSplatPath, opts) {
3581 if (opts === void 0) {
3582 opts = {};
3583 }
3584 let resultType;
3585 let result;
3586 let onReject;
3587 let runHandler = handler => {
3588 // Setup a promise we can race against so that abort signals short circuit
3589 let reject;
3590 let abortPromise = new Promise((_, r) => reject = r);
3591 onReject = () => reject();
3592 request.signal.addEventListener("abort", onReject);
3593 return Promise.race([handler({
3594 request,
3595 params: match.params,
3596 context: opts.requestContext
3597 }), abortPromise]);
3598 };
3599 try {
3600 let handler = match.route[type];
3601 if (match.route.lazy) {
3602 if (handler) {
3603 // Run statically defined handler in parallel with lazy()
3604 let handlerError;
3605 let values = await Promise.all([
3606 // If the handler throws, don't let it immediately bubble out,
3607 // since we need to let the lazy() execution finish so we know if this
3608 // route has a boundary that can handle the error
3609 runHandler(handler).catch(e => {
3610 handlerError = e;
3611 }), loadLazyRouteModule(match.route, mapRouteProperties, manifest)]);
3612 if (handlerError) {
3613 throw handlerError;
3614 }
3615 result = values[0];
3616 } else {
3617 // Load lazy route module, then run any returned handler
3618 await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
3619 handler = match.route[type];
3620 if (handler) {
3621 // Handler still run even if we got interrupted to maintain consistency
3622 // with un-abortable behavior of handler execution on non-lazy or
3623 // previously-lazy-loaded routes
3624 result = await runHandler(handler);
3625 } else if (type === "action") {
3626 let url = new URL(request.url);
3627 let pathname = url.pathname + url.search;
3628 throw getInternalRouterError(405, {
3629 method: request.method,
3630 pathname,
3631 routeId: match.route.id
3632 });
3633 } else {
3634 // lazy() route has no loader to run. Short circuit here so we don't
3635 // hit the invariant below that errors on returning undefined.
3636 return {
3637 type: ResultType.data,
3638 data: undefined
3639 };
3640 }
3641 }
3642 } else if (!handler) {
3643 let url = new URL(request.url);
3644 let pathname = url.pathname + url.search;
3645 throw getInternalRouterError(404, {
3646 pathname
3647 });
3648 } else {
3649 result = await runHandler(handler);
3650 }
3651 invariant(result !== undefined, "You defined " + (type === "action" ? "an action" : "a loader") + " for route " + ("\"" + match.route.id + "\" but didn't return anything from your `" + type + "` ") + "function. Please return a value or `null`.");
3652 } catch (e) {
3653 resultType = ResultType.error;
3654 result = e;
3655 } finally {
3656 if (onReject) {
3657 request.signal.removeEventListener("abort", onReject);
3658 }
3659 }
3660 if (isResponse(result)) {
3661 let status = result.status;
3662 // Process redirects
3663 if (redirectStatusCodes.has(status)) {
3664 let location = result.headers.get("Location");
3665 invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
3666 // Support relative routing in internal redirects
3667 if (!ABSOLUTE_URL_REGEX.test(location)) {
3668 location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location, v7_relativeSplatPath);
3669 } else if (!opts.isStaticRequest) {
3670 // Strip off the protocol+origin for same-origin + same-basename absolute
3671 // redirects. If this is a static request, we can let it go back to the
3672 // browser as-is
3673 let currentUrl = new URL(request.url);
3674 let url = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location);
3675 let isSameBasename = stripBasename(url.pathname, basename) != null;
3676 if (url.origin === currentUrl.origin && isSameBasename) {
3677 location = url.pathname + url.search + url.hash;
3678 }
3679 }
3680 // Don't process redirects in the router during static requests requests.
3681 // Instead, throw the Response and let the server handle it with an HTTP
3682 // redirect. We also update the Location header in place in this flow so
3683 // basename and relative routing is taken into account
3684 if (opts.isStaticRequest) {
3685 result.headers.set("Location", location);
3686 throw result;
3687 }
3688 return {
3689 type: ResultType.redirect,
3690 status,
3691 location,
3692 revalidate: result.headers.get("X-Remix-Revalidate") !== null,
3693 reloadDocument: result.headers.get("X-Remix-Reload-Document") !== null
3694 };
3695 }
3696 // For SSR single-route requests, we want to hand Responses back directly
3697 // without unwrapping. We do this with the QueryRouteResponse wrapper
3698 // interface so we can know whether it was returned or thrown
3699 if (opts.isRouteRequest) {
3700 let queryRouteResponse = {
3701 type: resultType === ResultType.error ? ResultType.error : ResultType.data,
3702 response: result
3703 };
3704 throw queryRouteResponse;
3705 }
3706 let data;
3707 try {
3708 let contentType = result.headers.get("Content-Type");
3709 // Check between word boundaries instead of startsWith() due to the last
3710 // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
3711 if (contentType && /\bapplication\/json\b/.test(contentType)) {
3712 if (result.body == null) {
3713 data = null;
3714 } else {
3715 data = await result.json();
3716 }
3717 } else {
3718 data = await result.text();
3719 }
3720 } catch (e) {
3721 return {
3722 type: ResultType.error,
3723 error: e
3724 };
3725 }
3726 if (resultType === ResultType.error) {
3727 return {
3728 type: resultType,
3729 error: new ErrorResponseImpl(status, result.statusText, data),
3730 headers: result.headers
3731 };
3732 }
3733 return {
3734 type: ResultType.data,
3735 data,
3736 statusCode: result.status,
3737 headers: result.headers
3738 };
3739 }
3740 if (resultType === ResultType.error) {
3741 return {
3742 type: resultType,
3743 error: result
3744 };
3745 }
3746 if (isDeferredData(result)) {
3747 var _result$init, _result$init2;
3748 return {
3749 type: ResultType.deferred,
3750 deferredData: result,
3751 statusCode: (_result$init = result.init) == null ? void 0 : _result$init.status,
3752 headers: ((_result$init2 = result.init) == null ? void 0 : _result$init2.headers) && new Headers(result.init.headers)
3753 };
3754 }
3755 return {
3756 type: ResultType.data,
3757 data: result
3758 };
3759}
3760// Utility method for creating the Request instances for loaders/actions during
3761// client-side navigations and fetches. During SSR we will always have a
3762// Request instance from the static handler (query/queryRoute)
3763function createClientSideRequest(history, location, signal, submission) {
3764 let url = history.createURL(stripHashFromPath(location)).toString();
3765 let init = {
3766 signal
3767 };
3768 if (submission && isMutationMethod(submission.formMethod)) {
3769 let {
3770 formMethod,
3771 formEncType
3772 } = submission;
3773 // Didn't think we needed this but it turns out unlike other methods, patch
3774 // won't be properly normalized to uppercase and results in a 405 error.
3775 // See: https://fetch.spec.whatwg.org/#concept-method
3776 init.method = formMethod.toUpperCase();
3777 if (formEncType === "application/json") {
3778 init.headers = new Headers({
3779 "Content-Type": formEncType
3780 });
3781 init.body = JSON.stringify(submission.json);
3782 } else if (formEncType === "text/plain") {
3783 // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3784 init.body = submission.text;
3785 } else if (formEncType === "application/x-www-form-urlencoded" && submission.formData) {
3786 // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3787 init.body = convertFormDataToSearchParams(submission.formData);
3788 } else {
3789 // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request)
3790 init.body = submission.formData;
3791 }
3792 }
3793 return new Request(url, init);
3794}
3795function convertFormDataToSearchParams(formData) {
3796 let searchParams = new URLSearchParams();
3797 for (let [key, value] of formData.entries()) {
3798 // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
3799 searchParams.append(key, typeof value === "string" ? value : value.name);
3800 }
3801 return searchParams;
3802}
3803function convertSearchParamsToFormData(searchParams) {
3804 let formData = new FormData();
3805 for (let [key, value] of searchParams.entries()) {
3806 formData.append(key, value);
3807 }
3808 return formData;
3809}
3810function processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) {
3811 // Fill in loaderData/errors from our loaders
3812 let loaderData = {};
3813 let errors = null;
3814 let statusCode;
3815 let foundError = false;
3816 let loaderHeaders = {};
3817 // Process loader results into state.loaderData/state.errors
3818 results.forEach((result, index) => {
3819 let id = matchesToLoad[index].route.id;
3820 invariant(!isRedirectResult(result), "Cannot handle redirect results in processLoaderData");
3821 if (isErrorResult(result)) {
3822 // Look upwards from the matched route for the closest ancestor
3823 // error boundary, defaulting to the root match
3824 let boundaryMatch = findNearestBoundary(matches, id);
3825 let error = result.error;
3826 // If we have a pending action error, we report it at the highest-route
3827 // that throws a loader error, and then clear it out to indicate that
3828 // it was consumed
3829 if (pendingError) {
3830 error = Object.values(pendingError)[0];
3831 pendingError = undefined;
3832 }
3833 errors = errors || {};
3834 // Prefer higher error values if lower errors bubble to the same boundary
3835 if (errors[boundaryMatch.route.id] == null) {
3836 errors[boundaryMatch.route.id] = error;
3837 }
3838 // Clear our any prior loaderData for the throwing route
3839 loaderData[id] = undefined;
3840 // Once we find our first (highest) error, we set the status code and
3841 // prevent deeper status codes from overriding
3842 if (!foundError) {
3843 foundError = true;
3844 statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500;
3845 }
3846 if (result.headers) {
3847 loaderHeaders[id] = result.headers;
3848 }
3849 } else {
3850 if (isDeferredResult(result)) {
3851 activeDeferreds.set(id, result.deferredData);
3852 loaderData[id] = result.deferredData.data;
3853 } else {
3854 loaderData[id] = result.data;
3855 }
3856 // Error status codes always override success status codes, but if all
3857 // loaders are successful we take the deepest status code.
3858 if (result.statusCode != null && result.statusCode !== 200 && !foundError) {
3859 statusCode = result.statusCode;
3860 }
3861 if (result.headers) {
3862 loaderHeaders[id] = result.headers;
3863 }
3864 }
3865 });
3866 // If we didn't consume the pending action error (i.e., all loaders
3867 // resolved), then consume it here. Also clear out any loaderData for the
3868 // throwing route
3869 if (pendingError) {
3870 errors = pendingError;
3871 loaderData[Object.keys(pendingError)[0]] = undefined;
3872 }
3873 return {
3874 loaderData,
3875 errors,
3876 statusCode: statusCode || 200,
3877 loaderHeaders
3878 };
3879}
3880function processLoaderData(state, matches, matchesToLoad, results, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds) {
3881 let {
3882 loaderData,
3883 errors
3884 } = processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds);
3885 // Process results from our revalidating fetchers
3886 for (let index = 0; index < revalidatingFetchers.length; index++) {
3887 let {
3888 key,
3889 match,
3890 controller
3891 } = revalidatingFetchers[index];
3892 invariant(fetcherResults !== undefined && fetcherResults[index] !== undefined, "Did not find corresponding fetcher result");
3893 let result = fetcherResults[index];
3894 // Process fetcher non-redirect errors
3895 if (controller && controller.signal.aborted) {
3896 // Nothing to do for aborted fetchers
3897 continue;
3898 } else if (isErrorResult(result)) {
3899 let boundaryMatch = findNearestBoundary(state.matches, match == null ? void 0 : match.route.id);
3900 if (!(errors && errors[boundaryMatch.route.id])) {
3901 errors = _extends({}, errors, {
3902 [boundaryMatch.route.id]: result.error
3903 });
3904 }
3905 state.fetchers.delete(key);
3906 } else if (isRedirectResult(result)) {
3907 // Should never get here, redirects should get processed above, but we
3908 // keep this to type narrow to a success result in the else
3909 invariant(false, "Unhandled fetcher revalidation redirect");
3910 } else if (isDeferredResult(result)) {
3911 // Should never get here, deferred data should be awaited for fetchers
3912 // in resolveDeferredResults
3913 invariant(false, "Unhandled fetcher deferred data");
3914 } else {
3915 let doneFetcher = getDoneFetcher(result.data);
3916 state.fetchers.set(key, doneFetcher);
3917 }
3918 }
3919 return {
3920 loaderData,
3921 errors
3922 };
3923}
3924function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
3925 let mergedLoaderData = _extends({}, newLoaderData);
3926 for (let match of matches) {
3927 let id = match.route.id;
3928 if (newLoaderData.hasOwnProperty(id)) {
3929 if (newLoaderData[id] !== undefined) {
3930 mergedLoaderData[id] = newLoaderData[id];
3931 }
3932 } else if (loaderData[id] !== undefined && match.route.loader) {
3933 // Preserve existing keys not included in newLoaderData and where a loader
3934 // wasn't removed by HMR
3935 mergedLoaderData[id] = loaderData[id];
3936 }
3937 if (errors && errors.hasOwnProperty(id)) {
3938 // Don't keep any loader data below the boundary
3939 break;
3940 }
3941 }
3942 return mergedLoaderData;
3943}
3944// Find the nearest error boundary, looking upwards from the leaf route (or the
3945// route specified by routeId) for the closest ancestor error boundary,
3946// defaulting to the root match
3947function findNearestBoundary(matches, routeId) {
3948 let eligibleMatches = routeId ? matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1) : [...matches];
3949 return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0];
3950}
3951function getShortCircuitMatches(routes) {
3952 // Prefer a root layout route if present, otherwise shim in a route object
3953 let route = routes.length === 1 ? routes[0] : routes.find(r => r.index || !r.path || r.path === "/") || {
3954 id: "__shim-error-route__"
3955 };
3956 return {
3957 matches: [{
3958 params: {},
3959 pathname: "",
3960 pathnameBase: "",
3961 route
3962 }],
3963 route
3964 };
3965}
3966function getInternalRouterError(status, _temp5) {
3967 let {
3968 pathname,
3969 routeId,
3970 method,
3971 type
3972 } = _temp5 === void 0 ? {} : _temp5;
3973 let statusText = "Unknown Server Error";
3974 let errorMessage = "Unknown @remix-run/router error";
3975 if (status === 400) {
3976 statusText = "Bad Request";
3977 if (method && pathname && routeId) {
3978 errorMessage = "You made a " + method + " request to \"" + pathname + "\" but " + ("did not provide a `loader` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
3979 } else if (type === "defer-action") {
3980 errorMessage = "defer() is not supported in actions";
3981 } else if (type === "invalid-body") {
3982 errorMessage = "Unable to encode submission body";
3983 }
3984 } else if (status === 403) {
3985 statusText = "Forbidden";
3986 errorMessage = "Route \"" + routeId + "\" does not match URL \"" + pathname + "\"";
3987 } else if (status === 404) {
3988 statusText = "Not Found";
3989 errorMessage = "No route matches URL \"" + pathname + "\"";
3990 } else if (status === 405) {
3991 statusText = "Method Not Allowed";
3992 if (method && pathname && routeId) {
3993 errorMessage = "You made a " + method.toUpperCase() + " request to \"" + pathname + "\" but " + ("did not provide an `action` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
3994 } else if (method) {
3995 errorMessage = "Invalid request method \"" + method.toUpperCase() + "\"";
3996 }
3997 }
3998 return new ErrorResponseImpl(status || 500, statusText, new Error(errorMessage), true);
3999}
4000// Find any returned redirect errors, starting from the lowest match
4001function findRedirect(results) {
4002 for (let i = results.length - 1; i >= 0; i--) {
4003 let result = results[i];
4004 if (isRedirectResult(result)) {
4005 return {
4006 result,
4007 idx: i
4008 };
4009 }
4010 }
4011}
4012function stripHashFromPath(path) {
4013 let parsedPath = typeof path === "string" ? parsePath(path) : path;
4014 return createPath(_extends({}, parsedPath, {
4015 hash: ""
4016 }));
4017}
4018function isHashChangeOnly(a, b) {
4019 if (a.pathname !== b.pathname || a.search !== b.search) {
4020 return false;
4021 }
4022 if (a.hash === "") {
4023 // /page -> /page#hash
4024 return b.hash !== "";
4025 } else if (a.hash === b.hash) {
4026 // /page#hash -> /page#hash
4027 return true;
4028 } else if (b.hash !== "") {
4029 // /page#hash -> /page#other
4030 return true;
4031 }
4032 // If the hash is removed the browser will re-perform a request to the server
4033 // /page#hash -> /page
4034 return false;
4035}
4036function isDeferredResult(result) {
4037 return result.type === ResultType.deferred;
4038}
4039function isErrorResult(result) {
4040 return result.type === ResultType.error;
4041}
4042function isRedirectResult(result) {
4043 return (result && result.type) === ResultType.redirect;
4044}
4045function isDeferredData(value) {
4046 let deferred = value;
4047 return deferred && typeof deferred === "object" && typeof deferred.data === "object" && typeof deferred.subscribe === "function" && typeof deferred.cancel === "function" && typeof deferred.resolveData === "function";
4048}
4049function isResponse(value) {
4050 return value != null && typeof value.status === "number" && typeof value.statusText === "string" && typeof value.headers === "object" && typeof value.body !== "undefined";
4051}
4052function isRedirectResponse(result) {
4053 if (!isResponse(result)) {
4054 return false;
4055 }
4056 let status = result.status;
4057 let location = result.headers.get("Location");
4058 return status >= 300 && status <= 399 && location != null;
4059}
4060function isQueryRouteResponse(obj) {
4061 return obj && isResponse(obj.response) && (obj.type === ResultType.data || obj.type === ResultType.error);
4062}
4063function isValidMethod(method) {
4064 return validRequestMethods.has(method.toLowerCase());
4065}
4066function isMutationMethod(method) {
4067 return validMutationMethods.has(method.toLowerCase());
4068}
4069async function resolveDeferredResults(currentMatches, matchesToLoad, results, signals, isFetcher, currentLoaderData) {
4070 for (let index = 0; index < results.length; index++) {
4071 let result = results[index];
4072 let match = matchesToLoad[index];
4073 // If we don't have a match, then we can have a deferred result to do
4074 // anything with. This is for revalidating fetchers where the route was
4075 // removed during HMR
4076 if (!match) {
4077 continue;
4078 }
4079 let currentMatch = currentMatches.find(m => m.route.id === match.route.id);
4080 let isRevalidatingLoader = currentMatch != null && !isNewRouteInstance(currentMatch, match) && (currentLoaderData && currentLoaderData[match.route.id]) !== undefined;
4081 if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) {
4082 // Note: we do not have to touch activeDeferreds here since we race them
4083 // against the signal in resolveDeferredData and they'll get aborted
4084 // there if needed
4085 let signal = signals[index];
4086 invariant(signal, "Expected an AbortSignal for revalidating fetcher deferred result");
4087 await resolveDeferredData(result, signal, isFetcher).then(result => {
4088 if (result) {
4089 results[index] = result || results[index];
4090 }
4091 });
4092 }
4093 }
4094}
4095async function resolveDeferredData(result, signal, unwrap) {
4096 if (unwrap === void 0) {
4097 unwrap = false;
4098 }
4099 let aborted = await result.deferredData.resolveData(signal);
4100 if (aborted) {
4101 return;
4102 }
4103 if (unwrap) {
4104 try {
4105 return {
4106 type: ResultType.data,
4107 data: result.deferredData.unwrappedData
4108 };
4109 } catch (e) {
4110 // Handle any TrackedPromise._error values encountered while unwrapping
4111 return {
4112 type: ResultType.error,
4113 error: e
4114 };
4115 }
4116 }
4117 return {
4118 type: ResultType.data,
4119 data: result.deferredData.data
4120 };
4121}
4122function hasNakedIndexQuery(search) {
4123 return new URLSearchParams(search).getAll("index").some(v => v === "");
4124}
4125function getTargetMatch(matches, location) {
4126 let search = typeof location === "string" ? parsePath(location).search : location.search;
4127 if (matches[matches.length - 1].route.index && hasNakedIndexQuery(search || "")) {
4128 // Return the leaf index route when index is present
4129 return matches[matches.length - 1];
4130 }
4131 // Otherwise grab the deepest "path contributing" match (ignoring index and
4132 // pathless layout routes)
4133 let pathMatches = getPathContributingMatches(matches);
4134 return pathMatches[pathMatches.length - 1];
4135}
4136function getSubmissionFromNavigation(navigation) {
4137 let {
4138 formMethod,
4139 formAction,
4140 formEncType,
4141 text,
4142 formData,
4143 json
4144 } = navigation;
4145 if (!formMethod || !formAction || !formEncType) {
4146 return;
4147 }
4148 if (text != null) {
4149 return {
4150 formMethod,
4151 formAction,
4152 formEncType,
4153 formData: undefined,
4154 json: undefined,
4155 text
4156 };
4157 } else if (formData != null) {
4158 return {
4159 formMethod,
4160 formAction,
4161 formEncType,
4162 formData,
4163 json: undefined,
4164 text: undefined
4165 };
4166 } else if (json !== undefined) {
4167 return {
4168 formMethod,
4169 formAction,
4170 formEncType,
4171 formData: undefined,
4172 json,
4173 text: undefined
4174 };
4175 }
4176}
4177function getLoadingNavigation(location, submission) {
4178 if (submission) {
4179 let navigation = {
4180 state: "loading",
4181 location,
4182 formMethod: submission.formMethod,
4183 formAction: submission.formAction,
4184 formEncType: submission.formEncType,
4185 formData: submission.formData,
4186 json: submission.json,
4187 text: submission.text
4188 };
4189 return navigation;
4190 } else {
4191 let navigation = {
4192 state: "loading",
4193 location,
4194 formMethod: undefined,
4195 formAction: undefined,
4196 formEncType: undefined,
4197 formData: undefined,
4198 json: undefined,
4199 text: undefined
4200 };
4201 return navigation;
4202 }
4203}
4204function getSubmittingNavigation(location, submission) {
4205 let navigation = {
4206 state: "submitting",
4207 location,
4208 formMethod: submission.formMethod,
4209 formAction: submission.formAction,
4210 formEncType: submission.formEncType,
4211 formData: submission.formData,
4212 json: submission.json,
4213 text: submission.text
4214 };
4215 return navigation;
4216}
4217function getLoadingFetcher(submission, data) {
4218 if (submission) {
4219 let fetcher = {
4220 state: "loading",
4221 formMethod: submission.formMethod,
4222 formAction: submission.formAction,
4223 formEncType: submission.formEncType,
4224 formData: submission.formData,
4225 json: submission.json,
4226 text: submission.text,
4227 data
4228 };
4229 return fetcher;
4230 } else {
4231 let fetcher = {
4232 state: "loading",
4233 formMethod: undefined,
4234 formAction: undefined,
4235 formEncType: undefined,
4236 formData: undefined,
4237 json: undefined,
4238 text: undefined,
4239 data
4240 };
4241 return fetcher;
4242 }
4243}
4244function getSubmittingFetcher(submission, existingFetcher) {
4245 let fetcher = {
4246 state: "submitting",
4247 formMethod: submission.formMethod,
4248 formAction: submission.formAction,
4249 formEncType: submission.formEncType,
4250 formData: submission.formData,
4251 json: submission.json,
4252 text: submission.text,
4253 data: existingFetcher ? existingFetcher.data : undefined
4254 };
4255 return fetcher;
4256}
4257function getDoneFetcher(data) {
4258 let fetcher = {
4259 state: "idle",
4260 formMethod: undefined,
4261 formAction: undefined,
4262 formEncType: undefined,
4263 formData: undefined,
4264 json: undefined,
4265 text: undefined,
4266 data
4267 };
4268 return fetcher;
4269}
4270function restoreAppliedTransitions(_window, transitions) {
4271 try {
4272 let sessionPositions = _window.sessionStorage.getItem(TRANSITIONS_STORAGE_KEY);
4273 if (sessionPositions) {
4274 let json = JSON.parse(sessionPositions);
4275 for (let [k, v] of Object.entries(json || {})) {
4276 if (v && Array.isArray(v)) {
4277 transitions.set(k, new Set(v || []));
4278 }
4279 }
4280 }
4281 } catch (e) {
4282 // no-op, use default empty object
4283 }
4284}
4285function persistAppliedTransitions(_window, transitions) {
4286 if (transitions.size > 0) {
4287 let json = {};
4288 for (let [k, v] of transitions) {
4289 json[k] = [...v];
4290 }
4291 try {
4292 _window.sessionStorage.setItem(TRANSITIONS_STORAGE_KEY, JSON.stringify(json));
4293 } catch (error) {
4294 warning(false, "Failed to save applied view transitions in sessionStorage (" + error + ").");
4295 }
4296 }
4297}
4298//#endregion
4299
4300export { AbortedDeferredError, Action, IDLE_BLOCKER, IDLE_FETCHER, IDLE_NAVIGATION, UNSAFE_DEFERRED_SYMBOL, DeferredData as UNSAFE_DeferredData, ErrorResponseImpl as UNSAFE_ErrorResponseImpl, convertRouteMatchToUiMatch as UNSAFE_convertRouteMatchToUiMatch, convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes, getResolveToMatches as UNSAFE_getResolveToMatches, invariant as UNSAFE_invariant, warning as UNSAFE_warning, createBrowserHistory, createHashHistory, createMemoryHistory, createPath, createRouter, createStaticHandler, defer, generatePath, getStaticContextFromError, getToPathname, isDeferredData, isRouteErrorResponse, joinPaths, json, matchPath, matchRoutes, normalizePathname, parsePath, redirect, redirectDocument, resolvePath, resolveTo, stripBasename };
4301//# sourceMappingURL=router.js.map
Note: See TracBrowser for help on using the repository browser.