/** * @remix-run/router v1.0.2 * * Copyright (c) Remix Software Inc. * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } //////////////////////////////////////////////////////////////////////////////// //#region Types and Constants //////////////////////////////////////////////////////////////////////////////// /** * Actions represent the type of change to a location value. */ exports.Action = void 0; /** * The pathname, search, and hash values of a URL. */ (function (Action) { Action["Pop"] = "POP"; Action["Push"] = "PUSH"; Action["Replace"] = "REPLACE"; })(exports.Action || (exports.Action = {})); const PopStateEventType = "popstate"; //#endregion //////////////////////////////////////////////////////////////////////////////// //#region Memory History //////////////////////////////////////////////////////////////////////////////// /** * A user-supplied object that describes a location. Used when providing * entries to `createMemoryHistory` via its `initialEntries` option. */ /** * Memory history stores the current location in memory. It is designed for use * in stateful non-browser environments like tests and React Native. */ function createMemoryHistory(options) { if (options === void 0) { options = {}; } let { initialEntries = ["/"], initialIndex, v5Compat = false } = options; let entries; // Declare so we can access from createMemoryLocation entries = initialEntries.map((entry, index) => createMemoryLocation(entry, typeof entry === "string" ? null : entry.state, index === 0 ? "default" : undefined)); let index = clampIndex(initialIndex == null ? entries.length - 1 : initialIndex); let action = exports.Action.Pop; let listener = null; function clampIndex(n) { return Math.min(Math.max(n, 0), entries.length - 1); } function getCurrentLocation() { return entries[index]; } function createMemoryLocation(to, state, key) { if (state === void 0) { state = null; } let location = createLocation(entries ? getCurrentLocation().pathname : "/", to, state, key); warning$1(location.pathname.charAt(0) === "/", "relative pathnames are not supported in memory history: " + JSON.stringify(to)); return location; } let history = { get index() { return index; }, get action() { return action; }, get location() { return getCurrentLocation(); }, createHref(to) { return typeof to === "string" ? to : createPath(to); }, push(to, state) { action = exports.Action.Push; let nextLocation = createMemoryLocation(to, state); index += 1; entries.splice(index, entries.length, nextLocation); if (v5Compat && listener) { listener({ action, location: nextLocation }); } }, replace(to, state) { action = exports.Action.Replace; let nextLocation = createMemoryLocation(to, state); entries[index] = nextLocation; if (v5Compat && listener) { listener({ action, location: nextLocation }); } }, go(delta) { action = exports.Action.Pop; index = clampIndex(index + delta); if (listener) { listener({ action, location: getCurrentLocation() }); } }, listen(fn) { listener = fn; return () => { listener = null; }; } }; return history; } //#endregion //////////////////////////////////////////////////////////////////////////////// //#region Browser History //////////////////////////////////////////////////////////////////////////////// /** * A browser history stores the current location in regular URLs in a web * browser environment. This is the standard for most web apps and provides the * cleanest URLs the browser's address bar. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#browserhistory */ /** * Browser history stores the location in regular URLs. This is the standard for * most web apps, but it requires some configuration on the server to ensure you * serve the same app at multiple URLs. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory */ function createBrowserHistory(options) { if (options === void 0) { options = {}; } function createBrowserLocation(window, globalHistory) { let { pathname, search, hash } = window.location; return createLocation("", { pathname, search, hash }, // state defaults to `null` because `window.history.state` does globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default"); } function createBrowserHref(window, to) { return typeof to === "string" ? to : createPath(to); } return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options); } //#endregion //////////////////////////////////////////////////////////////////////////////// //#region Hash History //////////////////////////////////////////////////////////////////////////////// /** * A hash history stores the current location in the fragment identifier portion * of the URL in a web browser environment. * * This is ideal for apps that do not control the server for some reason * (because the fragment identifier is never sent to the server), including some * shared hosting environments that do not provide fine-grained controls over * which pages are served at which URLs. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#hashhistory */ /** * Hash history stores the location in window.location.hash. This makes it ideal * for situations where you don't want to send the location to the server for * some reason, either because you do cannot configure it or the URL space is * reserved for something else. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory */ function createHashHistory(options) { if (options === void 0) { options = {}; } function createHashLocation(window, globalHistory) { let { pathname = "/", search = "", hash = "" } = parsePath(window.location.hash.substr(1)); return createLocation("", { pathname, search, hash }, // state defaults to `null` because `window.history.state` does globalHistory.state && globalHistory.state.usr || null, globalHistory.state && globalHistory.state.key || "default"); } function createHashHref(window, to) { let base = window.document.querySelector("base"); let href = ""; if (base && base.getAttribute("href")) { let url = window.location.href; let hashIndex = url.indexOf("#"); href = hashIndex === -1 ? url : url.slice(0, hashIndex); } return href + "#" + (typeof to === "string" ? to : createPath(to)); } function validateHashLocation(location, to) { warning$1(location.pathname.charAt(0) === "/", "relative pathnames are not supported in hash history.push(" + JSON.stringify(to) + ")"); } return getUrlBasedHistory(createHashLocation, createHashHref, validateHashLocation, options); } //#endregion //////////////////////////////////////////////////////////////////////////////// //#region UTILS //////////////////////////////////////////////////////////////////////////////// function warning$1(cond, message) { if (!cond) { // eslint-disable-next-line no-console if (typeof console !== "undefined") console.warn(message); try { // Welcome to debugging history! // // This error is thrown as a convenience so you can more easily // find the source for a warning that appears in the console by // enabling "pause on exceptions" in your JavaScript debugger. throw new Error(message); // eslint-disable-next-line no-empty } catch (e) {} } } function createKey() { return Math.random().toString(36).substr(2, 8); } /** * For browser-based histories, we combine the state and key into an object */ function getHistoryState(location) { return { usr: location.state, key: location.key }; } /** * Creates a Location object with a unique key from the given Path */ function createLocation(current, to, state, key) { if (state === void 0) { state = null; } let location = _extends({ pathname: typeof current === "string" ? current : current.pathname, search: "", hash: "" }, typeof to === "string" ? parsePath(to) : to, { state, // TODO: This could be cleaned up. push/replace should probably just take // full Locations now and avoid the need to run through this flow at all // But that's a pretty big refactor to the current test suite so going to // keep as is for the time being and just let any incoming keys take precedence key: to && to.key || key || createKey() }); return location; } /** * Creates a string URL path from the given pathname, search, and hash components. */ function createPath(_ref) { let { pathname = "/", search = "", hash = "" } = _ref; if (search && search !== "?") pathname += search.charAt(0) === "?" ? search : "?" + search; if (hash && hash !== "#") pathname += hash.charAt(0) === "#" ? hash : "#" + hash; return pathname; } /** * Parses a string URL path into its separate pathname, search, and hash components. */ function parsePath(path) { let parsedPath = {}; if (path) { let hashIndex = path.indexOf("#"); if (hashIndex >= 0) { parsedPath.hash = path.substr(hashIndex); path = path.substr(0, hashIndex); } let searchIndex = path.indexOf("?"); if (searchIndex >= 0) { parsedPath.search = path.substr(searchIndex); path = path.substr(0, searchIndex); } if (path) { parsedPath.pathname = path; } } return parsedPath; } function getUrlBasedHistory(getLocation, createHref, validateLocation, options) { if (options === void 0) { options = {}; } let { window = document.defaultView, v5Compat = false } = options; let globalHistory = window.history; let action = exports.Action.Pop; let listener = null; function handlePop() { action = exports.Action.Pop; if (listener) { listener({ action, location: history.location }); } } function push(to, state) { action = exports.Action.Push; let location = createLocation(history.location, to, state); if (validateLocation) validateLocation(location, to); let historyState = getHistoryState(location); let url = history.createHref(location); // try...catch because iOS limits us to 100 pushState calls :/ try { globalHistory.pushState(historyState, "", url); } catch (error) { // They are going to lose state here, but there is no real // way to warn them about it since the page will refresh... window.location.assign(url); } if (v5Compat && listener) { listener({ action, location }); } } function replace(to, state) { action = exports.Action.Replace; let location = createLocation(history.location, to, state); if (validateLocation) validateLocation(location, to); let historyState = getHistoryState(location); let url = history.createHref(location); globalHistory.replaceState(historyState, "", url); if (v5Compat && listener) { listener({ action, location: location }); } } let history = { get action() { return action; }, get location() { return getLocation(window, globalHistory); }, listen(fn) { if (listener) { throw new Error("A history only accepts one active listener"); } window.addEventListener(PopStateEventType, handlePop); listener = fn; return () => { window.removeEventListener(PopStateEventType, handlePop); listener = null; }; }, createHref(to) { return createHref(window, to); }, push, replace, go(n) { return globalHistory.go(n); } }; return history; } //#endregion /** * Map of routeId -> data returned from a loader/action/error */ let ResultType; /** * Successful result from a loader or action */ (function (ResultType) { ResultType["data"] = "data"; ResultType["deferred"] = "deferred"; ResultType["redirect"] = "redirect"; ResultType["error"] = "error"; })(ResultType || (ResultType = {})); function isIndexRoute(route) { return route.index === true; } // Walk the route tree generating unique IDs where necessary so we are working // solely with AgnosticDataRouteObject's within the Router function convertRoutesToDataRoutes(routes, parentPath, allIds) { if (parentPath === void 0) { parentPath = []; } if (allIds === void 0) { allIds = new Set(); } return routes.map((route, index) => { let treePath = [...parentPath, index]; let id = typeof route.id === "string" ? route.id : treePath.join("-"); invariant(route.index !== true || !route.children, "Cannot specify children on an index route"); invariant(!allIds.has(id), "Found a route id collision on id \"" + id + "\". Route " + "id's must be globally unique within Data Router usages"); allIds.add(id); if (isIndexRoute(route)) { let indexRoute = _extends({}, route, { id }); return indexRoute; } else { let pathOrLayoutRoute = _extends({}, route, { id, children: route.children ? convertRoutesToDataRoutes(route.children, treePath, allIds) : undefined }); return pathOrLayoutRoute; } }); } /** * Matches the given routes to a location and returns the match data. * * @see https://reactrouter.com/docs/en/v6/utils/match-routes */ function matchRoutes(routes, locationArg, basename) { if (basename === void 0) { basename = "/"; } let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg; let pathname = stripBasename(location.pathname || "/", basename); if (pathname == null) { return null; } let branches = flattenRoutes(routes); rankRouteBranches(branches); let matches = null; for (let i = 0; matches == null && i < branches.length; ++i) { matches = matchRouteBranch(branches[i], pathname); } return matches; } function flattenRoutes(routes, branches, parentsMeta, parentPath) { if (branches === void 0) { branches = []; } if (parentsMeta === void 0) { parentsMeta = []; } if (parentPath === void 0) { parentPath = ""; } routes.forEach((route, index) => { let meta = { relativePath: route.path || "", caseSensitive: route.caseSensitive === true, childrenIndex: index, route }; if (meta.relativePath.startsWith("/")) { 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."); meta.relativePath = meta.relativePath.slice(parentPath.length); } let path = joinPaths([parentPath, meta.relativePath]); let routesMeta = parentsMeta.concat(meta); // Add the children before adding this route to the array so we traverse the // route tree depth-first and child routes appear before their parents in // the "flattened" version. if (route.children && route.children.length > 0) { invariant( // Our types know better, but runtime JS may not! // @ts-expect-error route.index !== true, "Index routes must not have child routes. Please remove " + ("all child routes from route path \"" + path + "\".")); flattenRoutes(route.children, branches, routesMeta, path); } // Routes without a path shouldn't ever match by themselves unless they are // index routes, so don't add them to the list of possible branches. if (route.path == null && !route.index) { return; } branches.push({ path, score: computeScore(path, route.index), routesMeta }); }); return branches; } function rankRouteBranches(branches) { branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex))); } const paramRe = /^:\w+$/; const dynamicSegmentValue = 3; const indexRouteValue = 2; const emptySegmentValue = 1; const staticSegmentValue = 10; const splatPenalty = -2; const isSplat = s => s === "*"; function computeScore(path, index) { let segments = path.split("/"); let initialScore = segments.length; if (segments.some(isSplat)) { initialScore += splatPenalty; } if (index) { initialScore += indexRouteValue; } return segments.filter(s => !isSplat(s)).reduce((score, segment) => score + (paramRe.test(segment) ? dynamicSegmentValue : segment === "" ? emptySegmentValue : staticSegmentValue), initialScore); } function compareIndexes(a, b) { let siblings = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]); return siblings ? // If two routes are siblings, we should try to match the earlier sibling // first. This allows people to have fine-grained control over the matching // behavior by simply putting routes with identical paths in the order they // want them tried. a[a.length - 1] - b[b.length - 1] : // Otherwise, it doesn't really make sense to rank non-siblings by index, // so they sort equally. 0; } function matchRouteBranch(branch, pathname) { let { routesMeta } = branch; let matchedParams = {}; let matchedPathname = "/"; let matches = []; for (let i = 0; i < routesMeta.length; ++i) { let meta = routesMeta[i]; let end = i === routesMeta.length - 1; let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/"; let match = matchPath({ path: meta.relativePath, caseSensitive: meta.caseSensitive, end }, remainingPathname); if (!match) return null; Object.assign(matchedParams, match.params); let route = meta.route; matches.push({ // TODO: Can this as be avoided? params: matchedParams, pathname: joinPaths([matchedPathname, match.pathname]), pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])), route }); if (match.pathnameBase !== "/") { matchedPathname = joinPaths([matchedPathname, match.pathnameBase]); } } return matches; } /** * Returns a path with params interpolated. * * @see https://reactrouter.com/docs/en/v6/utils/generate-path */ function generatePath(path, params) { if (params === void 0) { params = {}; } return path.replace(/:(\w+)/g, (_, key) => { invariant(params[key] != null, "Missing \":" + key + "\" param"); return params[key]; }).replace(/(\/?)\*/, (_, prefix, __, str) => { const star = "*"; if (params[star] == null) { // If no splat was provided, trim the trailing slash _unless_ it's // the entire path return str === "/*" ? "/" : ""; } // Apply the splat return "" + prefix + params[star]; }); } /** * A PathPattern is used to match on some portion of a URL pathname. */ /** * Performs pattern matching on a URL pathname and returns information about * the match. * * @see https://reactrouter.com/docs/en/v6/utils/match-path */ function matchPath(pattern, pathname) { if (typeof pattern === "string") { pattern = { path: pattern, caseSensitive: false, end: true }; } let [matcher, paramNames] = compilePath(pattern.path, pattern.caseSensitive, pattern.end); let match = pathname.match(matcher); if (!match) return null; let matchedPathname = match[0]; let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1"); let captureGroups = match.slice(1); let params = paramNames.reduce((memo, paramName, index) => { // We need to compute the pathnameBase here using the raw splat value // instead of using params["*"] later because it will be decoded then if (paramName === "*") { let splatValue = captureGroups[index] || ""; pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1"); } memo[paramName] = safelyDecodeURIComponent(captureGroups[index] || "", paramName); return memo; }, {}); return { params, pathname: matchedPathname, pathnameBase, pattern }; } function compilePath(path, caseSensitive, end) { if (caseSensitive === void 0) { caseSensitive = false; } if (end === void 0) { end = true; } 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(/\*$/, "/*") + "\".")); let paramNames = []; let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below .replace(/^\/*/, "/") // Make sure it has a leading / .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars .replace(/:(\w+)/g, (_, paramName) => { paramNames.push(paramName); return "([^\\/]+)"; }); if (path.endsWith("*")) { paramNames.push("*"); regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"] } else if (end) { // When matching to the end, ignore trailing slashes regexpSource += "\\/*$"; } else if (path !== "" && path !== "/") { // If our path is non-empty and contains anything beyond an initial slash, // then we have _some_ form of path in our regex so we should expect to // match only if we find the end of this path segment. Look for an optional // non-captured trailing slash (to match a portion of the URL) or the end // of the path (if we've matched to the end). We used to do this with a // word boundary but that gives false positives on routes like // /user-preferences since `-` counts as a word boundary. regexpSource += "(?:(?=\\/|$))"; } else ; let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i"); return [matcher, paramNames]; } function safelyDecodeURIComponent(value, paramName) { try { return decodeURIComponent(value); } catch (error) { warning(false, "The value for the URL param \"" + paramName + "\" will not be decoded because" + (" the string \"" + value + "\" is a malformed URL segment. This is probably") + (" due to a bad percent encoding (" + error + ").")); return value; } } /** * @private */ function stripBasename(pathname, basename) { if (basename === "/") return pathname; if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) { return null; } // We want to leave trailing slash behavior in the user's control, so if they // specify a basename with a trailing slash, we should support it let startIndex = basename.endsWith("/") ? basename.length - 1 : basename.length; let nextChar = pathname.charAt(startIndex); if (nextChar && nextChar !== "/") { // pathname does not start with basename/ return null; } return pathname.slice(startIndex) || "/"; } /** * @private */ function invariant(value, message) { if (value === false || value === null || typeof value === "undefined") { throw new Error(message); } } /** * @private */ function warning(cond, message) { if (!cond) { // eslint-disable-next-line no-console if (typeof console !== "undefined") console.warn(message); try { // Welcome to debugging React Router! // // This error is thrown as a convenience so you can more easily // find the source for a warning that appears in the console by // enabling "pause on exceptions" in your JavaScript debugger. throw new Error(message); // eslint-disable-next-line no-empty } catch (e) {} } } /** * Returns a resolved path object relative to the given pathname. * * @see https://reactrouter.com/docs/en/v6/utils/resolve-path */ function resolvePath(to, fromPathname) { if (fromPathname === void 0) { fromPathname = "/"; } let { pathname: toPathname, search = "", hash = "" } = typeof to === "string" ? parsePath(to) : to; let pathname = toPathname ? toPathname.startsWith("/") ? toPathname : resolvePathname(toPathname, fromPathname) : fromPathname; return { pathname, search: normalizeSearch(search), hash: normalizeHash(hash) }; } function resolvePathname(relativePath, fromPathname) { let segments = fromPathname.replace(/\/+$/, "").split("/"); let relativeSegments = relativePath.split("/"); relativeSegments.forEach(segment => { if (segment === "..") { // Keep the root "" segment so the pathname starts at / if (segments.length > 1) segments.pop(); } else if (segment !== ".") { segments.push(segment); } }); return segments.length > 1 ? segments.join("/") : "/"; } function getInvalidPathError(char, field, dest, path) { 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 and the router will parse it for you."; } /** * @private */ function resolveTo(toArg, routePathnames, locationPathname, isPathRelative) { if (isPathRelative === void 0) { isPathRelative = false; } let to; if (typeof toArg === "string") { to = parsePath(toArg); } else { to = _extends({}, toArg); invariant(!to.pathname || !to.pathname.includes("?"), getInvalidPathError("?", "pathname", "search", to)); invariant(!to.pathname || !to.pathname.includes("#"), getInvalidPathError("#", "pathname", "hash", to)); invariant(!to.search || !to.search.includes("#"), getInvalidPathError("#", "search", "hash", to)); } let isEmptyPath = toArg === "" || to.pathname === ""; let toPathname = isEmptyPath ? "/" : to.pathname; let from; // Routing is relative to the current pathname if explicitly requested. // // If a pathname is explicitly provided in `to`, it should be relative to the // route context. This is explained in `Note on `` values` in our // migration guide from v5 as a means of disambiguation between `to` values // that begin with `/` and those that do not. However, this is problematic for // `to` values that do not provide a pathname. `to` can simply be a search or // hash string, in which case we should assume that the navigation is relative // to the current location's pathname and *not* the route pathname. if (isPathRelative || toPathname == null) { from = locationPathname; } else { let routePathnameIndex = routePathnames.length - 1; if (toPathname.startsWith("..")) { let toSegments = toPathname.split("/"); // Each leading .. segment means "go up one route" instead of "go up one // URL segment". This is a key difference from how works and a // major reason we call this a "to" value instead of a "href". while (toSegments[0] === "..") { toSegments.shift(); routePathnameIndex -= 1; } to.pathname = toSegments.join("/"); } // If there are more ".." segments than parent routes, resolve relative to // the root / URL. from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/"; } let path = resolvePath(to, from); // Ensure the pathname has a trailing slash if the original "to" had one let hasExplicitTrailingSlash = toPathname && toPathname !== "/" && toPathname.endsWith("/"); // Or if this was a link to the current path which has a trailing slash let hasCurrentTrailingSlash = (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/"); if (!path.pathname.endsWith("/") && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) { path.pathname += "/"; } return path; } /** * @private */ function getToPathname(to) { // Empty strings should be treated the same as / paths return to === "" || to.pathname === "" ? "/" : typeof to === "string" ? parsePath(to).pathname : to.pathname; } /** * @private */ const joinPaths = paths => paths.join("/").replace(/\/\/+/g, "/"); /** * @private */ const normalizePathname = pathname => pathname.replace(/\/+$/, "").replace(/^\/*/, "/"); /** * @private */ const normalizeSearch = search => !search || search === "?" ? "" : search.startsWith("?") ? search : "?" + search; /** * @private */ const normalizeHash = hash => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash; /** * This is a shortcut for creating `application/json` responses. Converts `data` * to JSON and sets the `Content-Type` header. */ const json = function json(data, init) { if (init === void 0) { init = {}; } let responseInit = typeof init === "number" ? { status: init } : init; let headers = new Headers(responseInit.headers); if (!headers.has("Content-Type")) { headers.set("Content-Type", "application/json; charset=utf-8"); } return new Response(JSON.stringify(data), _extends({}, responseInit, { headers })); }; class AbortedDeferredError extends Error {} class DeferredData { constructor(data) { this.pendingKeys = new Set(); this.subscriber = undefined; invariant(data && typeof data === "object" && !Array.isArray(data), "defer() only accepts plain objects"); // Set up an AbortController + Promise we can race against to exit early // cancellation let reject; this.abortPromise = new Promise((_, r) => reject = r); this.controller = new AbortController(); let onAbort = () => reject(new AbortedDeferredError("Deferred data aborted")); this.unlistenAbortSignal = () => this.controller.signal.removeEventListener("abort", onAbort); this.controller.signal.addEventListener("abort", onAbort); this.data = Object.entries(data).reduce((acc, _ref) => { let [key, value] = _ref; return Object.assign(acc, { [key]: this.trackPromise(key, value) }); }, {}); } trackPromise(key, value) { if (!(value instanceof Promise)) { return value; } this.pendingKeys.add(key); // We store a little wrapper promise that will be extended with // _data/_error props upon resolve/reject let promise = Promise.race([value, this.abortPromise]).then(data => this.onSettle(promise, key, null, data), error => this.onSettle(promise, key, error)); // Register rejection listeners to avoid uncaught promise rejections on // errors or aborted deferred values promise.catch(() => {}); Object.defineProperty(promise, "_tracked", { get: () => true }); return promise; } onSettle(promise, key, error, data) { if (this.controller.signal.aborted && error instanceof AbortedDeferredError) { this.unlistenAbortSignal(); Object.defineProperty(promise, "_error", { get: () => error }); return Promise.reject(error); } this.pendingKeys.delete(key); if (this.done) { // Nothing left to abort! this.unlistenAbortSignal(); } const subscriber = this.subscriber; if (error) { Object.defineProperty(promise, "_error", { get: () => error }); subscriber && subscriber(false); return Promise.reject(error); } Object.defineProperty(promise, "_data", { get: () => data }); subscriber && subscriber(false); return data; } subscribe(fn) { this.subscriber = fn; } cancel() { this.controller.abort(); this.pendingKeys.forEach((v, k) => this.pendingKeys.delete(k)); let subscriber = this.subscriber; subscriber && subscriber(true); } async resolveData(signal) { let aborted = false; if (!this.done) { let onAbort = () => this.cancel(); signal.addEventListener("abort", onAbort); aborted = await new Promise(resolve => { this.subscribe(aborted => { signal.removeEventListener("abort", onAbort); if (aborted || this.done) { resolve(aborted); } }); }); } return aborted; } get done() { return this.pendingKeys.size === 0; } get unwrappedData() { invariant(this.data !== null && this.done, "Can only unwrap data on initialized and settled deferreds"); return Object.entries(this.data).reduce((acc, _ref2) => { let [key, value] = _ref2; return Object.assign(acc, { [key]: unwrapTrackedPromise(value) }); }, {}); } } function isTrackedPromise(value) { return value instanceof Promise && value._tracked === true; } function unwrapTrackedPromise(value) { if (!isTrackedPromise(value)) { return value; } if (value._error) { throw value._error; } return value._data; } function defer(data) { return new DeferredData(data); } /** * A redirect response. Sets the status code and the `Location` header. * Defaults to "302 Found". */ const redirect = function redirect(url, init) { if (init === void 0) { init = 302; } let responseInit = init; if (typeof responseInit === "number") { responseInit = { status: responseInit }; } else if (typeof responseInit.status === "undefined") { responseInit.status = 302; } let headers = new Headers(responseInit.headers); headers.set("Location", url); return new Response(null, _extends({}, responseInit, { headers })); }; /** * @private * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies */ class ErrorResponse { constructor(status, statusText, data) { this.status = status; this.statusText = statusText || ""; this.data = data; } } /** * Check if the given error is an ErrorResponse generated from a 4xx/5xx * Response throw from an action/loader */ function isRouteErrorResponse(e) { return e instanceof ErrorResponse; } //#region Types and Constants //////////////////////////////////////////////////////////////////////////////// /** * A Router instance manages all navigation and data loading/mutations */ const IDLE_NAVIGATION = { state: "idle", location: undefined, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined }; const IDLE_FETCHER = { state: "idle", data: undefined, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined }; //#endregion //////////////////////////////////////////////////////////////////////////////// //#region createRouter //////////////////////////////////////////////////////////////////////////////// /** * Create a router and listen to history POP navigations */ function createRouter(init) { invariant(init.routes.length > 0, "You must provide a non-empty routes array to createRouter"); let dataRoutes = convertRoutesToDataRoutes(init.routes); // Cleanup function for history let unlistenHistory = null; // Externally-provided functions to call on all state changes let subscribers = new Set(); // Externally-provided object to hold scroll restoration locations during routing let savedScrollPositions = null; // Externally-provided function to get scroll restoration keys let getScrollRestorationKey = null; // Externally-provided function to get current scroll position let getScrollPosition = null; // One-time flag to control the initial hydration scroll restoration. Because // we don't get the saved positions from until _after_ // the initial render, we need to manually trigger a separate updateState to // send along the restoreScrollPosition let initialScrollRestored = false; let initialMatches = matchRoutes(dataRoutes, init.history.location, init.basename); let initialErrors = null; if (initialMatches == null) { // If we do not match a user-provided-route, fall back to the root // to allow the error boundary to take over let { matches, route, error } = getNotFoundMatches(dataRoutes); initialMatches = matches; initialErrors = { [route.id]: error }; } let initialized = !initialMatches.some(m => m.route.loader) || init.hydrationData != null; let router; let state = { historyAction: init.history.action, location: init.history.location, matches: initialMatches, initialized, navigation: IDLE_NAVIGATION, restoreScrollPosition: null, preventScrollReset: false, revalidation: "idle", loaderData: init.hydrationData && init.hydrationData.loaderData || {}, actionData: init.hydrationData && init.hydrationData.actionData || null, errors: init.hydrationData && init.hydrationData.errors || initialErrors, fetchers: new Map() }; // -- Stateful internal variables to manage navigations -- // Current navigation in progress (to be committed in completeNavigation) let pendingAction = exports.Action.Pop; // Should the current navigation prevent the scroll reset if scroll cannot // be restored? let pendingPreventScrollReset = false; // AbortController for the active navigation let pendingNavigationController; // We use this to avoid touching history in completeNavigation if a // revalidation is entirely uninterrupted let isUninterruptedRevalidation = false; // Use this internal flag to force revalidation of all loaders: // - submissions (completed or interrupted) // - useRevalidate() // - X-Remix-Revalidate (from redirect) let isRevalidationRequired = false; // Use this internal array to capture routes that require revalidation due // to a cancelled deferred on action submission let cancelledDeferredRoutes = []; // Use this internal array to capture fetcher loads that were cancelled by an // action navigation and require revalidation let cancelledFetcherLoads = []; // AbortControllers for any in-flight fetchers let fetchControllers = new Map(); // Track loads based on the order in which they started let incrementingLoadId = 0; // Track the outstanding pending navigation data load to be compared against // the globally incrementing load when a fetcher load lands after a completed // navigation let pendingNavigationLoadId = -1; // Fetchers that triggered data reloads as a result of their actions let fetchReloadIds = new Map(); // Fetchers that triggered redirect navigations from their actions let fetchRedirectIds = new Set(); // Most recent href/match for fetcher.load calls for fetchers let fetchLoadMatches = new Map(); // Store DeferredData instances for active route matches. When a // route loader returns defer() we stick one in here. Then, when a nested // promise resolves we update loaderData. If a new navigation starts we // cancel active deferreds for eliminated routes. let activeDeferreds = new Map(); // Initialize the router, all side effects should be kicked off from here. // Implemented as a Fluent API for ease of: // let router = createRouter(init).initialize(); function initialize() { // If history informs us of a POP navigation, start the navigation but do not update // state. We'll update our own state once the navigation completes unlistenHistory = init.history.listen(_ref => { let { action: historyAction, location } = _ref; return startNavigation(historyAction, location); }); // Kick off initial data load if needed. Use Pop to avoid modifying history if (!state.initialized) { startNavigation(exports.Action.Pop, state.location); } return router; } // Clean up a router and it's side effects function dispose() { if (unlistenHistory) { unlistenHistory(); } subscribers.clear(); pendingNavigationController && pendingNavigationController.abort(); state.fetchers.forEach((_, key) => deleteFetcher(key)); } // Subscribe to state updates for the router function subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); } // Update our state and notify the calling context of the change function updateState(newState) { state = _extends({}, state, newState); subscribers.forEach(subscriber => subscriber(state)); } // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION // and setting state.[historyAction/location/matches] to the new route. // - Location is a required param // - Navigation will always be set to IDLE_NAVIGATION // - Can pass any other state in newState function completeNavigation(location, newState) { var _state$navigation$for; // Deduce if we're in a loading/actionReload state: // - We have committed actionData in the store // - The current navigation was a submission // - We're past the submitting state and into the loading state // - The location we've finished loading is different from the submission // location, indicating we redirected from the action (avoids false // positives for loading/submissionRedirect when actionData returned // on a prior submission) let isActionReload = state.actionData != null && state.navigation.formMethod != null && state.navigation.state === "loading" && ((_state$navigation$for = state.navigation.formAction) == null ? void 0 : _state$navigation$for.split("?")[0]) === location.pathname; // Always preserve any existing loaderData from re-used routes let newLoaderData = newState.loaderData ? { loaderData: mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || []) } : {}; updateState(_extends({}, isActionReload ? {} : { actionData: null }, newState, newLoaderData, { historyAction: pendingAction, location, initialized: true, navigation: IDLE_NAVIGATION, revalidation: "idle", // Don't restore on submission navigations restoreScrollPosition: state.navigation.formData ? false : getSavedScrollPosition(location, newState.matches || state.matches), preventScrollReset: pendingPreventScrollReset })); if (isUninterruptedRevalidation) ; else if (pendingAction === exports.Action.Pop) ; else if (pendingAction === exports.Action.Push) { init.history.push(location, location.state); } else if (pendingAction === exports.Action.Replace) { init.history.replace(location, location.state); } // Reset stateful navigation vars pendingAction = exports.Action.Pop; pendingPreventScrollReset = false; isUninterruptedRevalidation = false; isRevalidationRequired = false; cancelledDeferredRoutes = []; cancelledFetcherLoads = []; } // Trigger a navigation event, which can either be a numerical POP or a PUSH // replace with an optional submission async function navigate(to, opts) { if (typeof to === "number") { init.history.go(to); return; } let { path, submission, error } = normalizeNavigateOptions(to, opts); let location = createLocation(state.location, path, opts && opts.state); let historyAction = (opts && opts.replace) === true || submission != null ? exports.Action.Replace : exports.Action.Push; let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined; return await startNavigation(historyAction, location, { submission, // Send through the formData serialization error if we have one so we can // render at the right error boundary after we match routes pendingError: error, preventScrollReset, replace: opts && opts.replace }); } // Revalidate all current loaders. If a navigation is in progress or if this // is interrupted by a navigation, allow this to "succeed" by calling all // loaders during the next loader round function revalidate() { interruptActiveLoads(); updateState({ revalidation: "loading" }); // If we're currently submitting an action, we don't need to start a new // navigation, we'll just let the follow up loader execution call all loaders if (state.navigation.state === "submitting") { return; } // If we're currently in an idle state, start a new navigation for the current // action/location and mark it as uninterrupted, which will skip the history // update in completeNavigation if (state.navigation.state === "idle") { startNavigation(state.historyAction, state.location, { startUninterruptedRevalidation: true }); return; } // Otherwise, if we're currently in a loading state, just start a new // navigation to the navigation.location but do not trigger an uninterrupted // revalidation so that history correctly updates once the navigation completes startNavigation(pendingAction || state.historyAction, state.navigation.location, { overrideNavigation: state.navigation }); } // Start a navigation to the given action/location. Can optionally provide a // overrideNavigation which will override the normalLoad in the case of a redirect // navigation async function startNavigation(historyAction, location, opts) { // Abort any in-progress navigations and start a new one. Unset any ongoing // uninterrupted revalidations unless told otherwise, since we want this // new navigation to update history normally pendingNavigationController && pendingNavigationController.abort(); pendingNavigationController = null; pendingAction = historyAction; isUninterruptedRevalidation = (opts && opts.startUninterruptedRevalidation) === true; // Save the current scroll position every time we start a new navigation, // and track whether we should reset scroll on completion saveScrollPosition(state.location, state.matches); pendingPreventScrollReset = (opts && opts.preventScrollReset) === true; let loadingNavigation = opts && opts.overrideNavigation; let matches = matchRoutes(dataRoutes, location, init.basename); // Short circuit with a 404 on the root error boundary if we match nothing if (!matches) { let { matches: notFoundMatches, route, error } = getNotFoundMatches(dataRoutes); // Cancel all pending deferred on 404s since we don't keep any routes cancelActiveDeferreds(); completeNavigation(location, { matches: notFoundMatches, loaderData: {}, errors: { [route.id]: error } }); return; } // Short circuit if it's only a hash change if (isHashChangeOnly(state.location, location)) { completeNavigation(location, { matches }); return; } // Create a controller/Request for this navigation pendingNavigationController = new AbortController(); let request = createRequest(location, pendingNavigationController.signal, opts && opts.submission); let pendingActionData; let pendingError; if (opts && opts.pendingError) { // If we have a pendingError, it means the user attempted a GET submission // with binary FormData so assign here and skip to handleLoaders. That // way we handle calling loaders above the boundary etc. It's not really // different from an actionError in that sense. pendingError = { [findNearestBoundary(matches).route.id]: opts.pendingError }; } else if (opts && opts.submission) { // Call action if we received an action submission let actionOutput = await handleAction(request, location, opts.submission, matches, { replace: opts.replace }); if (actionOutput.shortCircuited) { return; } pendingActionData = actionOutput.pendingActionData; pendingError = actionOutput.pendingActionError; let navigation = _extends({ state: "loading", location }, opts.submission); loadingNavigation = navigation; } // Call loaders let { shortCircuited, loaderData, errors } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.replace, pendingActionData, pendingError); if (shortCircuited) { return; } // Clean up now that the action/loaders have completed. Don't clean up if // we short circuited because pendingNavigationController will have already // been assigned to a new controller for the next navigation pendingNavigationController = null; completeNavigation(location, { matches, loaderData, errors }); } // Call the action matched by the leaf route for this navigation and handle // redirects/errors async function handleAction(request, location, submission, matches, opts) { interruptActiveLoads(); // Put us in a submitting state let navigation = _extends({ state: "submitting", location }, submission); updateState({ navigation }); // Call our action and get the result let result; let actionMatch = getTargetMatch(matches, location); if (!actionMatch.route.action) { result = getMethodNotAllowedResult(location); } else { result = await callLoaderOrAction("action", request, actionMatch); if (request.signal.aborted) { return { shortCircuited: true }; } } if (isRedirectResult(result)) { let redirectNavigation = _extends({ state: "loading", location: createLocation(state.location, result.location) }, submission); await startRedirectNavigation(result, redirectNavigation, opts && opts.replace); return { shortCircuited: true }; } if (isErrorResult(result)) { // Store off the pending error - we use it to determine which loaders // to call and will commit it when we complete the navigation let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id); // By default, all submissions are REPLACE navigations, but if the // action threw an error that'll be rendered in an errorElement, we fall // back to PUSH so that the user can use the back button to get back to // the pre-submission form location to try again if ((opts && opts.replace) !== true) { pendingAction = exports.Action.Push; } return { pendingActionError: { [boundaryMatch.route.id]: result.error } }; } if (isDeferredResult(result)) { throw new Error("defer() is not supported in actions"); } return { pendingActionData: { [actionMatch.route.id]: result.data } }; } // Call all applicable loaders for the given matches, handling redirects, // errors, etc. async function handleLoaders(request, location, matches, overrideNavigation, submission, replace, pendingActionData, pendingError) { // Figure out the right navigation we want to use for data loading let loadingNavigation = overrideNavigation; if (!loadingNavigation) { let navigation = { state: "loading", location, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined }; loadingNavigation = navigation; } let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, pendingActionData, pendingError, fetchLoadMatches); // Cancel pending deferreds for no-longer-matched routes or routes we're // about to reload. Note that if this is an action reload we would have // already cancelled all pending deferreds so this would be a no-op cancelActiveDeferreds(routeId => !(matches && matches.some(m => m.route.id === routeId)) || matchesToLoad && matchesToLoad.some(m => m.route.id === routeId)); // Short circuit if we have no loaders to run if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) { completeNavigation(location, { matches, loaderData: mergeLoaderData(state.loaderData, {}, matches), // Commit pending error if we're short circuiting errors: pendingError || null, actionData: pendingActionData || null }); return { shortCircuited: true }; } // If this is an uninterrupted revalidation, we remain in our current idle // state. If not, we need to switch to our loading state and load data, // preserving any new action data or existing action data (in the case of // a revalidation interrupting an actionReload) if (!isUninterruptedRevalidation) { revalidatingFetchers.forEach(_ref2 => { let [key] = _ref2; const fetcher = state.fetchers.get(key); let revalidatingFetcher = { state: "loading", data: fetcher && fetcher.data, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined }; state.fetchers.set(key, revalidatingFetcher); }); updateState(_extends({ navigation: loadingNavigation, actionData: pendingActionData || state.actionData || null }, revalidatingFetchers.length > 0 ? { fetchers: new Map(state.fetchers) } : {})); } pendingNavigationLoadId = ++incrementingLoadId; revalidatingFetchers.forEach(_ref3 => { let [key] = _ref3; return fetchControllers.set(key, pendingNavigationController); }); let { results, loaderResults, fetcherResults } = await callLoadersAndMaybeResolveData(state.matches, matchesToLoad, revalidatingFetchers, request); if (request.signal.aborted) { return { shortCircuited: true }; } // Clean up _after_ loaders have completed. Don't clean up if we short // circuited because fetchControllers would have been aborted and // reassigned to new controllers for the next navigation revalidatingFetchers.forEach(_ref4 => { let [key] = _ref4; return fetchControllers.delete(key); }); // If any loaders returned a redirect Response, start a new REPLACE navigation let redirect = findRedirect(results); if (redirect) { let redirectNavigation = getLoaderRedirect(state, redirect); await startRedirectNavigation(redirect, redirectNavigation, replace); return { shortCircuited: true }; } // Process and commit output from loaders let { loaderData, errors } = processLoaderData(state, matches, matchesToLoad, loaderResults, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds); // Wire up subscribers to update loaderData as promises settle activeDeferreds.forEach((deferredData, routeId) => { deferredData.subscribe(aborted => { // Note: No need to updateState here since the TrackedPromise on // loaderData is stable across resolve/reject // Remove this instance if we were aborted or if promises have settled if (aborted || deferredData.done) { activeDeferreds.delete(routeId); } }); }); markFetchRedirectsDone(); let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId); return _extends({ loaderData, errors }, didAbortFetchLoads || revalidatingFetchers.length > 0 ? { fetchers: new Map(state.fetchers) } : {}); } function getFetcher(key) { return state.fetchers.get(key) || IDLE_FETCHER; } // Trigger a fetcher load/submit for the given fetcher key function fetch(key, routeId, href, opts) { if (typeof AbortController === "undefined") { 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."); } if (fetchControllers.has(key)) abortFetcher(key); let matches = matchRoutes(dataRoutes, href, init.basename); if (!matches) { setFetcherError(key, routeId, new ErrorResponse(404, "Not Found", null)); return; } let { path, submission } = normalizeNavigateOptions(href, opts, true); let match = getTargetMatch(matches, path); if (submission) { handleFetcherAction(key, routeId, path, match, submission); return; } // Store off the match so we can call it's shouldRevalidate on subsequent // revalidations fetchLoadMatches.set(key, [path, match]); handleFetcherLoader(key, routeId, path, match); } // Call the action for the matched fetcher.submit(), and then handle redirects, // errors, and revalidation async function handleFetcherAction(key, routeId, path, match, submission) { interruptActiveLoads(); fetchLoadMatches.delete(key); if (!match.route.action) { let { error } = getMethodNotAllowedResult(path); setFetcherError(key, routeId, error); return; } // Put this fetcher into it's submitting state let existingFetcher = state.fetchers.get(key); let fetcher = _extends({ state: "submitting" }, submission, { data: existingFetcher && existingFetcher.data }); state.fetchers.set(key, fetcher); updateState({ fetchers: new Map(state.fetchers) }); // Call the action for the fetcher let abortController = new AbortController(); let fetchRequest = createRequest(path, abortController.signal, submission); fetchControllers.set(key, abortController); let actionResult = await callLoaderOrAction("action", fetchRequest, match); if (fetchRequest.signal.aborted) { // We can delete this so long as we weren't aborted by ou our own fetcher // re-submit which would have put _new_ controller is in fetchControllers if (fetchControllers.get(key) === abortController) { fetchControllers.delete(key); } return; } if (isRedirectResult(actionResult)) { fetchControllers.delete(key); fetchRedirectIds.add(key); let loadingFetcher = _extends({ state: "loading" }, submission, { data: undefined }); state.fetchers.set(key, loadingFetcher); updateState({ fetchers: new Map(state.fetchers) }); let redirectNavigation = _extends({ state: "loading", location: createLocation(state.location, actionResult.location) }, submission); await startRedirectNavigation(actionResult, redirectNavigation); return; } // Process any non-redirect errors thrown if (isErrorResult(actionResult)) { setFetcherError(key, routeId, actionResult.error); return; } if (isDeferredResult(actionResult)) { invariant(false, "defer() is not supported in actions"); } // Start the data load for current matches, or the next location if we're // in the middle of a navigation let nextLocation = state.navigation.location || state.location; let revalidationRequest = createRequest(nextLocation, abortController.signal); let matches = state.navigation.state !== "idle" ? matchRoutes(dataRoutes, state.navigation.location, init.basename) : state.matches; invariant(matches, "Didn't find any matches after fetcher action"); let loadId = ++incrementingLoadId; fetchReloadIds.set(key, loadId); let loadFetcher = _extends({ state: "loading", data: actionResult.data }, submission); state.fetchers.set(key, loadFetcher); let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, { [match.route.id]: actionResult.data }, undefined, // No need to send through errors since we short circuit above fetchLoadMatches); // Put all revalidating fetchers into the loading state, except for the // current fetcher which we want to keep in it's current loading state which // contains it's action submission info + action data revalidatingFetchers.filter(_ref5 => { let [staleKey] = _ref5; return staleKey !== key; }).forEach(_ref6 => { let [staleKey] = _ref6; let existingFetcher = state.fetchers.get(staleKey); let revalidatingFetcher = { state: "loading", data: existingFetcher && existingFetcher.data, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined }; state.fetchers.set(staleKey, revalidatingFetcher); fetchControllers.set(staleKey, abortController); }); updateState({ fetchers: new Map(state.fetchers) }); let { results, loaderResults, fetcherResults } = await callLoadersAndMaybeResolveData(state.matches, matchesToLoad, revalidatingFetchers, revalidationRequest); if (abortController.signal.aborted) { return; } fetchReloadIds.delete(key); fetchControllers.delete(key); revalidatingFetchers.forEach(_ref7 => { let [staleKey] = _ref7; return fetchControllers.delete(staleKey); }); let redirect = findRedirect(results); if (redirect) { let redirectNavigation = getLoaderRedirect(state, redirect); await startRedirectNavigation(redirect, redirectNavigation); return; } // Process and commit output from loaders let { loaderData, errors } = processLoaderData(state, state.matches, matchesToLoad, loaderResults, undefined, revalidatingFetchers, fetcherResults, activeDeferreds); let doneFetcher = { state: "idle", data: actionResult.data, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined }; state.fetchers.set(key, doneFetcher); let didAbortFetchLoads = abortStaleFetchLoads(loadId); // If we are currently in a navigation loading state and this fetcher is // more recent than the navigation, we want the newer data so abort the // navigation and complete it with the fetcher data if (state.navigation.state === "loading" && loadId > pendingNavigationLoadId) { invariant(pendingAction, "Expected pending action"); pendingNavigationController && pendingNavigationController.abort(); completeNavigation(state.navigation.location, { matches, loaderData, errors, fetchers: new Map(state.fetchers) }); } else { // otherwise just update with the fetcher data, preserving any existing // loaderData for loaders that did not need to reload. We have to // manually merge here since we aren't going through completeNavigation updateState(_extends({ errors, loaderData: mergeLoaderData(state.loaderData, loaderData, matches) }, didAbortFetchLoads ? { fetchers: new Map(state.fetchers) } : {})); isRevalidationRequired = false; } } // Call the matched loader for fetcher.load(), handling redirects, errors, etc. async function handleFetcherLoader(key, routeId, path, match) { let existingFetcher = state.fetchers.get(key); // Put this fetcher into it's loading state let loadingFetcher = { state: "loading", formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined, data: existingFetcher && existingFetcher.data }; state.fetchers.set(key, loadingFetcher); updateState({ fetchers: new Map(state.fetchers) }); // Call the loader for this fetcher route match let abortController = new AbortController(); let fetchRequest = createRequest(path, abortController.signal); fetchControllers.set(key, abortController); let result = await callLoaderOrAction("loader", fetchRequest, match); // Deferred isn't supported or fetcher loads, await everything and treat it // as a normal load. resolveDeferredData will return undefined if this // fetcher gets aborted, so we just leave result untouched and short circuit // below if that happens if (isDeferredResult(result)) { result = (await resolveDeferredData(result, fetchRequest.signal, true)) || result; } // We can delete this so long as we weren't aborted by ou our own fetcher // re-load which would have put _new_ controller is in fetchControllers if (fetchControllers.get(key) === abortController) { fetchControllers.delete(key); } if (fetchRequest.signal.aborted) { return; } // If the loader threw a redirect Response, start a new REPLACE navigation if (isRedirectResult(result)) { let redirectNavigation = getLoaderRedirect(state, result); await startRedirectNavigation(result, redirectNavigation); return; } // Process any non-redirect errors thrown if (isErrorResult(result)) { let boundaryMatch = findNearestBoundary(state.matches, routeId); state.fetchers.delete(key); // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch - // do we need to behave any differently with our non-redirect errors? // What if it was a non-redirect Response? updateState({ fetchers: new Map(state.fetchers), errors: { [boundaryMatch.route.id]: result.error } }); return; } invariant(!isDeferredResult(result), "Unhandled fetcher deferred data"); // Put the fetcher back into an idle state let doneFetcher = { state: "idle", data: result.data, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined }; state.fetchers.set(key, doneFetcher); updateState({ fetchers: new Map(state.fetchers) }); } /** * Utility function to handle redirects returned from an action or loader. * Normally, a redirect "replaces" the navigation that triggered it. So, for * example: * * - user is on /a * - user clicks a link to /b * - loader for /b redirects to /c * * In a non-JS app the browser would track the in-flight navigation to /b and * then replace it with /c when it encountered the redirect response. In * the end it would only ever update the URL bar with /c. * * In client-side routing using pushState/replaceState, we aim to emulate * this behavior and we also do not update history until the end of the * navigation (including processed redirects). This means that we never * actually touch history until we've processed redirects, so we just use * the history action from the original navigation (PUSH or REPLACE). */ async function startRedirectNavigation(redirect, navigation, replace) { if (redirect.revalidate) { isRevalidationRequired = true; } invariant(navigation.location, "Expected a location on the redirect navigation"); // There's no need to abort on redirects, since we don't detect the // redirect until the action/loaders have settled pendingNavigationController = null; let redirectHistoryAction = replace === true ? exports.Action.Replace : exports.Action.Push; await startNavigation(redirectHistoryAction, navigation.location, { overrideNavigation: navigation }); } async function callLoadersAndMaybeResolveData(currentMatches, matchesToLoad, fetchersToLoad, request) { // Call all navigation loaders and revalidating fetcher loaders in parallel, // then slice off the results into separate arrays so we can handle them // accordingly let results = await Promise.all([...matchesToLoad.map(m => callLoaderOrAction("loader", request, m)), ...fetchersToLoad.map(_ref8 => { let [, href, match] = _ref8; return callLoaderOrAction("loader", createRequest(href, request.signal), match); })]); let loaderResults = results.slice(0, matchesToLoad.length); let fetcherResults = results.slice(matchesToLoad.length); await Promise.all([resolveDeferredResults(currentMatches, matchesToLoad, loaderResults, request.signal, false, state.loaderData), resolveDeferredResults(currentMatches, fetchersToLoad.map(_ref9 => { let [,, match] = _ref9; return match; }), fetcherResults, request.signal, true)]); return { results, loaderResults, fetcherResults }; } function interruptActiveLoads() { // Every interruption triggers a revalidation isRevalidationRequired = true; // Cancel pending route-level deferreds and mark cancelled routes for // revalidation cancelledDeferredRoutes.push(...cancelActiveDeferreds()); // Abort in-flight fetcher loads fetchLoadMatches.forEach((_, key) => { if (fetchControllers.has(key)) { cancelledFetcherLoads.push(key); abortFetcher(key); } }); } function setFetcherError(key, routeId, error) { let boundaryMatch = findNearestBoundary(state.matches, routeId); deleteFetcher(key); updateState({ errors: { [boundaryMatch.route.id]: error }, fetchers: new Map(state.fetchers) }); } function deleteFetcher(key) { if (fetchControllers.has(key)) abortFetcher(key); fetchLoadMatches.delete(key); fetchReloadIds.delete(key); fetchRedirectIds.delete(key); state.fetchers.delete(key); } function abortFetcher(key) { let controller = fetchControllers.get(key); invariant(controller, "Expected fetch controller: " + key); controller.abort(); fetchControllers.delete(key); } function markFetchersDone(keys) { for (let key of keys) { let fetcher = getFetcher(key); let doneFetcher = { state: "idle", data: fetcher.data, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined }; state.fetchers.set(key, doneFetcher); } } function markFetchRedirectsDone() { let doneKeys = []; for (let key of fetchRedirectIds) { let fetcher = state.fetchers.get(key); invariant(fetcher, "Expected fetcher: " + key); if (fetcher.state === "loading") { fetchRedirectIds.delete(key); doneKeys.push(key); } } markFetchersDone(doneKeys); } function abortStaleFetchLoads(landedId) { let yeetedKeys = []; for (let [key, id] of fetchReloadIds) { if (id < landedId) { let fetcher = state.fetchers.get(key); invariant(fetcher, "Expected fetcher: " + key); if (fetcher.state === "loading") { abortFetcher(key); fetchReloadIds.delete(key); yeetedKeys.push(key); } } } markFetchersDone(yeetedKeys); return yeetedKeys.length > 0; } function cancelActiveDeferreds(predicate) { let cancelledRouteIds = []; activeDeferreds.forEach((dfd, routeId) => { if (!predicate || predicate(routeId)) { // Cancel the deferred - but do not remove from activeDeferreds here - // we rely on the subscribers to do that so our tests can assert proper // cleanup via _internalActiveDeferreds dfd.cancel(); cancelledRouteIds.push(routeId); activeDeferreds.delete(routeId); } }); return cancelledRouteIds; } // Opt in to capturing and reporting scroll positions during navigations, // used by the component function enableScrollRestoration(positions, getPosition, getKey) { savedScrollPositions = positions; getScrollPosition = getPosition; getScrollRestorationKey = getKey || (location => location.key); // Perform initial hydration scroll restoration, since we miss the boat on // the initial updateState() because we've not yet rendered // and therefore have no savedScrollPositions available if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) { initialScrollRestored = true; let y = getSavedScrollPosition(state.location, state.matches); if (y != null) { updateState({ restoreScrollPosition: y }); } } return () => { savedScrollPositions = null; getScrollPosition = null; getScrollRestorationKey = null; }; } function saveScrollPosition(location, matches) { if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) { let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData)); let key = getScrollRestorationKey(location, userMatches) || location.key; savedScrollPositions[key] = getScrollPosition(); } } function getSavedScrollPosition(location, matches) { if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) { let userMatches = matches.map(m => createUseMatchesMatch(m, state.loaderData)); let key = getScrollRestorationKey(location, userMatches) || location.key; let y = savedScrollPositions[key]; if (typeof y === "number") { return y; } } return null; } router = { get basename() { return init.basename; }, get state() { return state; }, get routes() { return dataRoutes; }, initialize, subscribe, enableScrollRestoration, navigate, fetch, revalidate, createHref, getFetcher, deleteFetcher, dispose, _internalFetchControllers: fetchControllers, _internalActiveDeferreds: activeDeferreds }; return router; } //#endregion //////////////////////////////////////////////////////////////////////////////// //#region createStaticHandler //////////////////////////////////////////////////////////////////////////////// function unstable_createStaticHandler(routes) { invariant(routes.length > 0, "You must provide a non-empty routes array to unstable_createStaticHandler"); let dataRoutes = convertRoutesToDataRoutes(routes); async function query(request) { let { location, result } = await queryImpl(request); if (result instanceof Response) { return result; } // When returning StaticHandlerContext, we patch back in the location here // since we need it for React Context. But this helps keep our submit and // loadRouteData operating on a Request instead of a Location return _extends({ location }, result); } async function queryRoute(request, routeId) { let { result } = await queryImpl(request, routeId); if (result instanceof Response) { return result; } let error = result.errors ? Object.values(result.errors)[0] : undefined; if (error !== undefined) { // While we always re-throw Responses returned from loaders/actions // directly for route requests and prevent the unwrapping into an // ErrorResponse, we still need this for error cases _prior_ the // execution of the loader/action, such as a 404/405 error. if (isRouteErrorResponse(error)) { return new Response(error.data, { status: error.status, statusText: error.statusText }); } // If we got back result.errors, that means the loader/action threw // _something_ that wasn't a Response, but it's not guaranteed/required // to be an `instanceof Error` either, so we have to use throw here to // preserve the "error" state outside of queryImpl. throw error; } // Pick off the right state value to return let routeData = [result.actionData, result.loaderData].find(v => v); let value = Object.values(routeData || {})[0]; if (isRouteErrorResponse(value)) { return new Response(value.data, { status: value.status, statusText: value.statusText }); } return value; } async function queryImpl(request, routeId) { invariant(request.method !== "HEAD", "query()/queryRoute() do not support HEAD requests"); invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal"); let { location, matches, shortCircuitState } = matchRequest(request, routeId); try { if (shortCircuitState) { return { location, result: shortCircuitState }; } if (request.method !== "GET") { let result = await submit(request, matches, getTargetMatch(matches, location), routeId != null); return { location, result }; } let result = await loadRouteData(request, matches, routeId != null); return { location, result: _extends({}, result, { actionData: null, actionHeaders: {} }) }; } catch (e) { if (e instanceof Response) { return { location, result: e }; } throw e; } } async function submit(request, matches, actionMatch, isRouteRequest) { let result; if (!actionMatch.route.action) { let href = createHref(new URL(request.url)); result = getMethodNotAllowedResult(href); } else { result = await callLoaderOrAction("action", request, actionMatch, true, isRouteRequest); if (request.signal.aborted) { let method = isRouteRequest ? "queryRoute" : "query"; throw new Error(method + "() call aborted"); } } if (isRedirectResult(result)) { // Uhhhh - this should never happen, we should always throw these from // calLoaderOrAction, but the type narrowing here keeps TS happy and we // can get back on the "throw all redirect responses" train here should // this ever happen :/ throw new Response(null, { status: result.status, headers: { Location: result.location } }); } if (isDeferredResult(result)) { throw new Error("defer() is not supported in actions"); } if (isRouteRequest) { if (isErrorResult(result)) { let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id); return { matches: [actionMatch], loaderData: {}, actionData: null, errors: { [boundaryMatch.route.id]: result.error }, // Note: statusCode + headers are unused here since queryRoute will // return the raw Response or value statusCode: 500, loaderHeaders: {}, actionHeaders: {} }; } return { matches: [actionMatch], loaderData: {}, actionData: { [actionMatch.route.id]: result.data }, errors: null, // Note: statusCode + headers are unused here since queryRoute will // return the raw Response or value statusCode: 200, loaderHeaders: {}, actionHeaders: {} }; } if (isErrorResult(result)) { // Store off the pending error - we use it to determine which loaders // to call and will commit it when we complete the navigation let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id); let context = await loadRouteData(request, matches, isRouteRequest, { [boundaryMatch.route.id]: result.error }); // action status codes take precedence over loader status codes return _extends({}, context, { statusCode: isRouteErrorResponse(result.error) ? result.error.status : 500, actionData: null, actionHeaders: _extends({}, result.headers ? { [actionMatch.route.id]: result.headers } : {}) }); } let context = await loadRouteData(request, matches, isRouteRequest); return _extends({}, context, result.statusCode ? { statusCode: result.statusCode } : {}, { actionData: { [actionMatch.route.id]: result.data }, actionHeaders: _extends({}, result.headers ? { [actionMatch.route.id]: result.headers } : {}) }); } async function loadRouteData(request, matches, isRouteRequest, pendingActionError) { let matchesToLoad = getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]).filter(m => m.route.loader); // Short circuit if we have no loaders to run if (matchesToLoad.length === 0) { return { matches, loaderData: {}, errors: pendingActionError || null, statusCode: 200, loaderHeaders: {} }; } let results = await Promise.all([...matchesToLoad.map(m => callLoaderOrAction("loader", request, m, true, isRouteRequest))]); if (request.signal.aborted) { let method = isRouteRequest ? "queryRoute" : "query"; throw new Error(method + "() call aborted"); } // Can't do anything with these without the Remix side of things, so just // cancel them for now results.forEach(result => { if (isDeferredResult(result)) { result.deferredData.cancel(); } }); // Process and commit output from loaders let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError); return _extends({}, context, { matches }); } function matchRequest(req, routeId) { let url = new URL(req.url); let location = createLocation("", createPath(url), null, "default"); let matches = matchRoutes(dataRoutes, location); if (matches && routeId) { matches = matches.filter(m => m.route.id === routeId); } // Short circuit with a 404 if we match nothing if (!matches) { let { matches: notFoundMatches, route, error } = getNotFoundMatches(dataRoutes); return { location, matches: notFoundMatches, shortCircuitState: { matches: notFoundMatches, loaderData: {}, actionData: null, errors: { [route.id]: error }, statusCode: 404, loaderHeaders: {}, actionHeaders: {} } }; } return { location, matches }; } return { dataRoutes, query, queryRoute }; } //#endregion //////////////////////////////////////////////////////////////////////////////// //#region Helpers //////////////////////////////////////////////////////////////////////////////// /** * Given an existing StaticHandlerContext and an error thrown at render time, * provide an updated StaticHandlerContext suitable for a second SSR render */ function getStaticContextFromError(routes, context, error) { let newContext = _extends({}, context, { statusCode: 500, errors: { [context._deepestRenderedBoundaryId || routes[0].id]: error } }); return newContext; } // Normalize navigation options by converting formMethod=GET formData objects to // URLSearchParams so they behave identically to links with query params function normalizeNavigateOptions(to, opts, isFetcher) { if (isFetcher === void 0) { isFetcher = false; } let path = typeof to === "string" ? to : createPath(to); // Return location verbatim on non-submission navigations if (!opts || !("formMethod" in opts) && !("formData" in opts)) { return { path }; } // Create a Submission on non-GET navigations if (opts.formMethod != null && opts.formMethod !== "get") { return { path, submission: { formMethod: opts.formMethod, formAction: createHref(parsePath(path)), formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded", formData: opts.formData } }; } // No formData to flatten for GET submission if (!opts.formData) { return { path }; } // Flatten submission onto URLSearchParams for GET submissions let parsedPath = parsePath(path); try { let searchParams = convertFormDataToSearchParams(opts.formData); // Since fetcher GET submissions only run a single loader (as opposed to // navigation GET submissions which run all loaders), we need to preserve // any incoming ?index params if (isFetcher && parsedPath.search && hasNakedIndexQuery(parsedPath.search)) { searchParams.append("index", ""); } parsedPath.search = "?" + searchParams; } catch (e) { return { path, error: new ErrorResponse(400, "Bad Request", "Cannot submit binary form data using GET") }; } return { path: createPath(parsedPath) }; } function getLoaderRedirect(state, redirect) { let { formMethod, formAction, formEncType, formData } = state.navigation; let navigation = { state: "loading", location: createLocation(state.location, redirect.location), formMethod: formMethod || undefined, formAction: formAction || undefined, formEncType: formEncType || undefined, formData: formData || undefined }; return navigation; } // Filter out all routes below any caught error as they aren't going to // render so we don't need to load them function getLoaderMatchesUntilBoundary(matches, boundaryId) { let boundaryMatches = matches; if (boundaryId) { let index = matches.findIndex(m => m.route.id === boundaryId); if (index >= 0) { boundaryMatches = matches.slice(0, index); } } return boundaryMatches; } function getMatchesToLoad(state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, pendingActionData, pendingError, fetchLoadMatches) { let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : null; // Pick navigation matches that are net-new or qualify for revalidation let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined; let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId); let navigationMatches = boundaryMatches.filter((match, index) => match.route.loader != null && (isNewLoader(state.loaderData, state.matches[index], match) || // If this route had a pending deferred cancelled it must be revalidated cancelledDeferredRoutes.some(id => id === match.route.id) || shouldRevalidateLoader(state.location, state.matches[index], submission, location, match, isRevalidationRequired, actionResult))); // Pick fetcher.loads that need to be revalidated let revalidatingFetchers = []; fetchLoadMatches && fetchLoadMatches.forEach((_ref10, key) => { let [href, match] = _ref10; // This fetcher was cancelled from a prior action submission - force reload if (cancelledFetcherLoads.includes(key)) { revalidatingFetchers.push([key, href, match]); } else if (isRevalidationRequired) { let shouldRevalidate = shouldRevalidateLoader(href, match, submission, href, match, isRevalidationRequired, actionResult); if (shouldRevalidate) { revalidatingFetchers.push([key, href, match]); } } }); return [navigationMatches, revalidatingFetchers]; } function isNewLoader(currentLoaderData, currentMatch, match) { let isNew = // [a] -> [a, b] !currentMatch || // [a, b] -> [a, c] match.route.id !== currentMatch.route.id; // Handle the case that we don't have data for a re-used route, potentially // from a prior error or from a cancelled pending deferred let isMissingData = currentLoaderData[match.route.id] === undefined; // Always load if this is a net-new route or we don't yet have data return isNew || isMissingData; } function isNewRouteInstance(currentMatch, match) { let currentPath = currentMatch.route.path; return (// param change for this match, /users/123 -> /users/456 currentMatch.pathname !== match.pathname || // splat param changed, which is not present in match.path // e.g. /files/images/avatar.jpg -> files/finances.xls currentPath && currentPath.endsWith("*") && currentMatch.params["*"] !== match.params["*"] ); } function shouldRevalidateLoader(currentLocation, currentMatch, submission, location, match, isRevalidationRequired, actionResult) { let currentUrl = createURL(currentLocation); let currentParams = currentMatch.params; let nextUrl = createURL(location); let nextParams = match.params; // This is the default implementation as to when we revalidate. If the route // provides it's own implementation, then we give them full control but // provide this value so they can leverage it if needed after they check // their own specific use cases // Note that fetchers always provide the same current/next locations so the // URL-based checks here don't apply to fetcher shouldRevalidate calls let defaultShouldRevalidate = isNewRouteInstance(currentMatch, match) || // Clicked the same link, resubmitted a GET form currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders currentUrl.search !== nextUrl.search || // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate isRevalidationRequired; if (match.route.shouldRevalidate) { let routeChoice = match.route.shouldRevalidate(_extends({ currentUrl, currentParams, nextUrl, nextParams }, submission, { actionResult, defaultShouldRevalidate })); if (typeof routeChoice === "boolean") { return routeChoice; } } return defaultShouldRevalidate; } async function callLoaderOrAction(type, request, match, skipRedirects, isRouteRequest) { if (skipRedirects === void 0) { skipRedirects = false; } if (isRouteRequest === void 0) { isRouteRequest = false; } let resultType; let result; // Setup a promise we can race against so that abort signals short circuit let reject; let abortPromise = new Promise((_, r) => reject = r); let onReject = () => reject(); request.signal.addEventListener("abort", onReject); try { let handler = match.route[type]; invariant(handler, "Could not find the " + type + " to run on the \"" + match.route.id + "\" route"); result = await Promise.race([handler({ request, params: match.params }), abortPromise]); } catch (e) { resultType = ResultType.error; result = e; } finally { request.signal.removeEventListener("abort", onReject); } if (result instanceof Response) { // Process redirects let status = result.status; let location = result.headers.get("Location"); // For SSR single-route requests, we want to hand Responses back directly // without unwrapping if (isRouteRequest) { throw result; } if (status >= 300 && status <= 399 && location != null) { // Don't process redirects in the router during SSR document requests. // Instead, throw the Response and let the server handle it with an HTTP // redirect if (skipRedirects) { throw result; } return { type: ResultType.redirect, status, location, revalidate: result.headers.get("X-Remix-Revalidate") !== null }; } let data; let contentType = result.headers.get("Content-Type"); if (contentType && contentType.startsWith("application/json")) { data = await result.json(); } else { data = await result.text(); } if (resultType === ResultType.error) { return { type: resultType, error: new ErrorResponse(status, result.statusText, data), headers: result.headers }; } return { type: ResultType.data, data, statusCode: result.status, headers: result.headers }; } if (resultType === ResultType.error) { return { type: resultType, error: result }; } if (result instanceof DeferredData) { return { type: ResultType.deferred, deferredData: result }; } return { type: ResultType.data, data: result }; } function createRequest(location, signal, submission) { let url = createURL(location).toString(); let init = { signal }; if (submission) { let { formMethod, formEncType, formData } = submission; init.method = formMethod.toUpperCase(); init.body = formEncType === "application/x-www-form-urlencoded" ? convertFormDataToSearchParams(formData) : formData; } // Content-Type is inferred (https://fetch.spec.whatwg.org/#dom-request) return new Request(url, init); } function convertFormDataToSearchParams(formData) { let searchParams = new URLSearchParams(); for (let [key, value] of formData.entries()) { invariant(typeof value === "string", 'File inputs are not supported with encType "application/x-www-form-urlencoded", ' + 'please use "multipart/form-data" instead.'); searchParams.append(key, value); } return searchParams; } function processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds) { // Fill in loaderData/errors from our loaders let loaderData = {}; let errors = null; let statusCode; let foundError = false; let loaderHeaders = {}; // Process loader results into state.loaderData/state.errors results.forEach((result, index) => { let id = matchesToLoad[index].route.id; invariant(!isRedirectResult(result), "Cannot handle redirect results in processLoaderData"); if (isErrorResult(result)) { // Look upwards from the matched route for the closest ancestor // error boundary, defaulting to the root match let boundaryMatch = findNearestBoundary(matches, id); let error = result.error; // If we have a pending action error, we report it at the highest-route // that throws a loader error, and then clear it out to indicate that // it was consumed if (pendingError) { error = Object.values(pendingError)[0]; pendingError = undefined; } errors = Object.assign(errors || {}, { [boundaryMatch.route.id]: error }); // Once we find our first (highest) error, we set the status code and // prevent deeper status codes from overriding if (!foundError) { foundError = true; statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500; } if (result.headers) { loaderHeaders[id] = result.headers; } } else if (isDeferredResult(result)) { activeDeferreds && activeDeferreds.set(id, result.deferredData); loaderData[id] = result.deferredData.data; // TODO: Add statusCode/headers once we wire up streaming in Remix } else { loaderData[id] = result.data; // Error status codes always override success status codes, but if all // loaders are successful we take the deepest status code. if (result.statusCode != null && result.statusCode !== 200 && !foundError) { statusCode = result.statusCode; } if (result.headers) { loaderHeaders[id] = result.headers; } } }); // If we didn't consume the pending action error (i.e., all loaders // resolved), then consume it here if (pendingError) { errors = pendingError; } return { loaderData, errors, statusCode: statusCode || 200, loaderHeaders }; } function processLoaderData(state, matches, matchesToLoad, results, pendingError, revalidatingFetchers, fetcherResults, activeDeferreds) { let { loaderData, errors } = processRouteLoaderData(matches, matchesToLoad, results, pendingError, activeDeferreds); // Process results from our revalidating fetchers for (let index = 0; index < revalidatingFetchers.length; index++) { let [key,, match] = revalidatingFetchers[index]; invariant(fetcherResults !== undefined && fetcherResults[index] !== undefined, "Did not find corresponding fetcher result"); let result = fetcherResults[index]; // Process fetcher non-redirect errors if (isErrorResult(result)) { let boundaryMatch = findNearestBoundary(state.matches, match.route.id); if (!(errors && errors[boundaryMatch.route.id])) { errors = _extends({}, errors, { [boundaryMatch.route.id]: result.error }); } state.fetchers.delete(key); } else if (isRedirectResult(result)) { // Should never get here, redirects should get processed above, but we // keep this to type narrow to a success result in the else throw new Error("Unhandled fetcher revalidation redirect"); } else if (isDeferredResult(result)) { // Should never get here, deferred data should be awaited for fetchers // in resolveDeferredResults throw new Error("Unhandled fetcher deferred data"); } else { let doneFetcher = { state: "idle", data: result.data, formMethod: undefined, formAction: undefined, formEncType: undefined, formData: undefined }; state.fetchers.set(key, doneFetcher); } } return { loaderData, errors }; } function mergeLoaderData(loaderData, newLoaderData, matches) { let mergedLoaderData = _extends({}, newLoaderData); matches.forEach(match => { let id = match.route.id; if (newLoaderData[id] === undefined && loaderData[id] !== undefined) { mergedLoaderData[id] = loaderData[id]; } }); return mergedLoaderData; } // Find the nearest error boundary, looking upwards from the leaf route (or the // route specified by routeId) for the closest ancestor error boundary, // defaulting to the root match function findNearestBoundary(matches, routeId) { let eligibleMatches = routeId ? matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1) : [...matches]; return eligibleMatches.reverse().find(m => m.route.hasErrorBoundary === true) || matches[0]; } function getNotFoundMatches(routes) { // Prefer a root layout route if present, otherwise shim in a route object let route = routes.find(r => r.index || r.path === "" || r.path === "/") || { id: "__shim-404-route__" }; return { matches: [{ params: {}, pathname: "", pathnameBase: "", route }], route, error: new ErrorResponse(404, "Not Found", null) }; } function getMethodNotAllowedResult(path) { let href = typeof path === "string" ? path : createHref(path); console.warn("You're trying to submit to a route that does not have an action. To " + "fix this, please add an `action` function to the route for " + ("[" + href + "]")); return { type: ResultType.error, error: new ErrorResponse(405, "Method Not Allowed", "No action found for [" + href + "]") }; } // Find any returned redirect errors, starting from the lowest match function findRedirect(results) { for (let i = results.length - 1; i >= 0; i--) { let result = results[i]; if (isRedirectResult(result)) { return result; } } } // Create an href to represent a "server" URL without the hash function createHref(location) { return (location.pathname || "") + (location.search || ""); } function isHashChangeOnly(a, b) { return a.pathname === b.pathname && a.search === b.search && a.hash !== b.hash; } function isDeferredResult(result) { return result.type === ResultType.deferred; } function isErrorResult(result) { return result.type === ResultType.error; } function isRedirectResult(result) { return (result && result.type) === ResultType.redirect; } async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) { for (let index = 0; index < results.length; index++) { let result = results[index]; let match = matchesToLoad[index]; let currentMatch = currentMatches.find(m => m.route.id === match.route.id); let isRevalidatingLoader = currentMatch != null && !isNewRouteInstance(currentMatch, match) && (currentLoaderData && currentLoaderData[match.route.id]) !== undefined; if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) { // Note: we do not have to touch activeDeferreds here since we race them // against the signal in resolveDeferredData and they'll get aborted // there if needed await resolveDeferredData(result, signal, isFetcher).then(result => { if (result) { results[index] = result || results[index]; } }); } } } async function resolveDeferredData(result, signal, unwrap) { if (unwrap === void 0) { unwrap = false; } let aborted = await result.deferredData.resolveData(signal); if (aborted) { return; } if (unwrap) { try { return { type: ResultType.data, data: result.deferredData.unwrappedData }; } catch (e) { // Handle any TrackedPromise._error values encountered while unwrapping return { type: ResultType.error, error: e }; } } return { type: ResultType.data, data: result.deferredData.data }; } function hasNakedIndexQuery(search) { return new URLSearchParams(search).getAll("index").some(v => v === ""); } // Note: This should match the format exported by useMatches, so if you change // this please also change that :) Eventually we'll DRY this up function createUseMatchesMatch(match, loaderData) { let { route, pathname, params } = match; return { id: route.id, pathname, params, data: loaderData[route.id], handle: route.handle }; } function getTargetMatch(matches, location) { let search = typeof location === "string" ? parsePath(location).search : location.search; if (matches[matches.length - 1].route.index && !hasNakedIndexQuery(search || "")) { return matches.slice(-2)[0]; } return matches.slice(-1)[0]; } function createURL(location) { let base = typeof window !== "undefined" && typeof window.location !== "undefined" ? window.location.origin : "unknown://unknown"; let href = typeof location === "string" ? location : createHref(location); return new URL(href, base); } //#endregion exports.AbortedDeferredError = AbortedDeferredError; exports.ErrorResponse = ErrorResponse; exports.IDLE_FETCHER = IDLE_FETCHER; exports.IDLE_NAVIGATION = IDLE_NAVIGATION; exports.UNSAFE_convertRoutesToDataRoutes = convertRoutesToDataRoutes; exports.createBrowserHistory = createBrowserHistory; exports.createHashHistory = createHashHistory; exports.createMemoryHistory = createMemoryHistory; exports.createPath = createPath; exports.createRouter = createRouter; exports.defer = defer; exports.generatePath = generatePath; exports.getStaticContextFromError = getStaticContextFromError; exports.getToPathname = getToPathname; exports.invariant = invariant; exports.isRouteErrorResponse = isRouteErrorResponse; exports.joinPaths = joinPaths; exports.json = json; exports.matchPath = matchPath; exports.matchRoutes = matchRoutes; exports.normalizePathname = normalizePathname; exports.parsePath = parsePath; exports.redirect = redirect; exports.resolvePath = resolvePath; exports.resolveTo = resolveTo; exports.stripBasename = stripBasename; exports.unstable_createStaticHandler = unstable_createStaticHandler; exports.warning = warning; //# sourceMappingURL=router.cjs.js.map