/**
 * @remix-run/router v1.3.1
 *
 * 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
 */
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.RemixRouter = {}));
})(this, (function (exports) { 'use strict';

  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;
    }

    function createHref(to) {
      return typeof to === "string" ? to : createPath(to);
    }

    let history = {
      get index() {
        return index;
      },

      get action() {
        return action;
      },

      get location() {
        return getCurrentLocation();
      },

      createHref,

      createURL(to) {
        return new URL(createHref(to), "http://localhost");
      },

      encodeLocation(to) {
        let path = typeof to === "string" ? parsePath(to) : to;
        return {
          pathname: path.pathname || "",
          search: path.search || "",
          hash: path.hash || ""
        };
      },

      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,
            delta: 1
          });
        }
      },

      replace(to, state) {
        action = exports.Action.Replace;
        let nextLocation = createMemoryLocation(to, state);
        entries[index] = nextLocation;

        if (v5Compat && listener) {
          listener({
            action,
            location: nextLocation,
            delta: 0
          });
        }
      },

      go(delta) {
        action = exports.Action.Pop;
        let nextIndex = clampIndex(index + delta);
        let nextLocation = entries[nextIndex];
        index = nextIndex;

        if (listener) {
          listener({
            action,
            location: nextLocation,
            delta
          });
        }
      },

      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
  ////////////////////////////////////////////////////////////////////////////////

  /**
   * @private
   */

  function invariant(value, message) {
    if (value === false || value === null || typeof value === "undefined") {
      throw new Error(message);
    }
  }

  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, index) {
    return {
      usr: location.state,
      key: location.key,
      idx: index
    };
  }
  /**
   * 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;
    let index = getIndex(); // Index should only be null when we initialize. If not, it's because the
    // user called history.pushState or history.replaceState directly, in which
    // case we should log a warning as it will result in bugs.

    if (index == null) {
      index = 0;
      globalHistory.replaceState(_extends({}, globalHistory.state, {
        idx: index
      }), "");
    }

    function getIndex() {
      let state = globalHistory.state || {
        idx: null
      };
      return state.idx;
    }

    function handlePop() {
      action = exports.Action.Pop;
      let nextIndex = getIndex();
      let delta = nextIndex == null ? null : nextIndex - index;
      index = nextIndex;

      if (listener) {
        listener({
          action,
          location: history.location,
          delta
        });
      }
    }

    function push(to, state) {
      action = exports.Action.Push;
      let location = createLocation(history.location, to, state);
      if (validateLocation) validateLocation(location, to);
      index = getIndex() + 1;
      let historyState = getHistoryState(location, index);
      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: history.location,
          delta: 1
        });
      }
    }

    function replace(to, state) {
      action = exports.Action.Replace;
      let location = createLocation(history.location, to, state);
      if (validateLocation) validateLocation(location, to);
      index = getIndex();
      let historyState = getHistoryState(location, index);
      let url = history.createHref(location);
      globalHistory.replaceState(historyState, "", url);

      if (v5Compat && listener) {
        listener({
          action,
          location: history.location,
          delta: 0
        });
      }
    }

    function createURL(to) {
      // window.location.origin is "null" (the literal string value) in Firefox
      // under certain conditions, notably when serving from a local HTML file
      // See https://bugzilla.mozilla.org/show_bug.cgi?id=878297
      let base = window.location.origin !== "null" ? window.location.origin : window.location.href;
      let href = typeof to === "string" ? to : createPath(to);
      invariant(base, "No window.location.(origin|href) available to create URL for href: " + href);
      return new URL(href, base);
    }

    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);
      },

      createURL,

      encodeLocation(to) {
        // Encode a Location the same way window.location would
        let url = createURL(to);
        return {
          pathname: url.pathname,
          search: url.search,
          hash: url.hash
        };
      },

      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/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], // Incoming pathnames are generally encoded from either window.location
      // or from router.navigate, but we want to match against the unencoded
      // paths in the route definitions.  Memory router locations won't be
      // encoded here but there also shouldn't be anything to decode so this
      // should be a safe operation.  This avoids needing matchRoutes to be
      // history-aware.
      safelyDecodeURI(pathname));
    }

    return matches;
  }

  function flattenRoutes(routes, branches, parentsMeta, parentPath) {
    if (branches === void 0) {
      branches = [];
    }

    if (parentsMeta === void 0) {
      parentsMeta = [];
    }

    if (parentPath === void 0) {
      parentPath = "";
    }

    let flattenRoute = (route, index, relativePath) => {
      let meta = {
        relativePath: relativePath === undefined ? route.path || "" : relativePath,
        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
      });
    };

    routes.forEach((route, index) => {
      var _route$path;

      // coarse-grain check for optional params
      if (route.path === "" || !((_route$path = route.path) != null && _route$path.includes("?"))) {
        flattenRoute(route, index);
      } else {
        for (let exploded of explodeOptionalSegments(route.path)) {
          flattenRoute(route, index, exploded);
        }
      }
    });
    return branches;
  }
  /**
   * Computes all combinations of optional path segments for a given path,
   * excluding combinations that are ambiguous and of lower priority.
   *
   * For example, `/one/:two?/three/:four?/:five?` explodes to:
   * - `/one/three`
   * - `/one/:two/three`
   * - `/one/three/:four`
   * - `/one/three/:five`
   * - `/one/:two/three/:four`
   * - `/one/:two/three/:five`
   * - `/one/three/:four/:five`
   * - `/one/:two/three/:four/:five`
   */


  function explodeOptionalSegments(path) {
    let segments = path.split("/");
    if (segments.length === 0) return [];
    let [first, ...rest] = segments; // Optional path segments are denoted by a trailing `?`

    let isOptional = first.endsWith("?"); // Compute the corresponding required segment: `foo?` -> `foo`

    let required = first.replace(/\?$/, "");

    if (rest.length === 0) {
      // Intepret empty string as omitting an optional segment
      // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
      return isOptional ? [required, ""] : [required];
    }

    let restExploded = explodeOptionalSegments(rest.join("/"));
    let result = []; // All child paths with the prefix.  Do this for all children before the
    // optional version for all children so we get consistent ordering where the
    // parent optional aspect is preferred as required.  Otherwise, we can get
    // child sections interspersed where deeper optional segments are higher than
    // parent optional segments, where for example, /:two would explodes _earlier_
    // then /:one.  By always including the parent as required _for all children_
    // first, we avoid this issue

    result.push(...restExploded.map(subpath => subpath === "" ? required : [required, subpath].join("/"))); // Then if this is an optional value, add all child versions without

    if (isOptional) {
      result.push(...restExploded);
    } // for absolute paths, ensure `/` instead of empty segment


    return result.map(exploded => path.startsWith("/") && exploded === "" ? "/" : exploded);
  }

  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/utils/generate-path
   */


  function generatePath(originalPath, params) {
    if (params === void 0) {
      params = {};
    }

    let path = originalPath;

    if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
      warning(false, "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
      path = path.replace(/\*$/, "/*");
    }

    return path.replace(/^:(\w+)(\??)/g, (_, key, optional) => {
      let param = params[key];

      if (optional === "?") {
        return param == null ? "" : param;
      }

      if (param == null) {
        invariant(false, "Missing \":" + key + "\" param");
      }

      return param;
    }).replace(/\/:(\w+)(\??)/g, (_, key, optional) => {
      let param = params[key];

      if (optional === "?") {
        return param == null ? "" : "/" + param;
      }

      if (param == null) {
        invariant(false, "Missing \":" + key + "\" param");
      }

      return "/" + param;
    }) // Remove any optional markers from optional static segments
    .replace(/\?/g, "").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/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 safelyDecodeURI(value) {
    try {
      return decodeURI(value);
    } catch (error) {
      warning(false, "The URL path \"" + value + "\" could not be decoded because it is is a " + "malformed URL segment. This is probably due to a bad percent " + ("encoding (" + error + ")."));
      return value;
    }
  }

  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 warning(cond, message) {
    if (!cond) {
      // eslint-disable-next-line no-console
      if (typeof console !== "undefined") console.warn(message);

      try {
        // Welcome to debugging @remix-run/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/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 <Link to=\"...\"> and the router will parse it for you.";
  }
  /**
   * @private
   *
   * When processing relative navigation we want to ignore ancestor routes that
   * do not contribute to the path, such that index/pathless layout routes don't
   * interfere.
   *
   * For example, when moving a route element into an index route and/or a
   * pathless layout route, relative link behavior contained within should stay
   * the same.  Both of the following examples should link back to the root:
   *
   *   <Route path="/">
   *     <Route path="accounts" element={<Link to=".."}>
   *   </Route>
   *
   *   <Route path="/">
   *     <Route path="accounts">
   *       <Route element={<AccountsLayout />}>       // <-- Does not contribute
   *         <Route index element={<Link to=".."} />  // <-- Does not contribute
   *       </Route
   *     </Route>
   *   </Route>
   */


  function getPathContributingMatches(matches) {
    return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
  }
  /**
   * @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 `<Link to>` 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 <a href> 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, responseInit) {
      this.pendingKeysSet = new Set();
      this.subscribers = new Set();
      this.deferredKeys = [];
      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)
        });
      }, {});

      if (this.done) {
        // All incoming values were resolved
        this.unlistenAbortSignal();
      }

      this.init = responseInit;
    }

    trackPromise(key, value) {
      if (!(value instanceof Promise)) {
        return value;
      }

      this.deferredKeys.push(key);
      this.pendingKeysSet.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.pendingKeysSet.delete(key);

      if (this.done) {
        // Nothing left to abort!
        this.unlistenAbortSignal();
      }

      if (error) {
        Object.defineProperty(promise, "_error", {
          get: () => error
        });
        this.emit(false, key);
        return Promise.reject(error);
      }

      Object.defineProperty(promise, "_data", {
        get: () => data
      });
      this.emit(false, key);
      return data;
    }

    emit(aborted, settledKey) {
      this.subscribers.forEach(subscriber => subscriber(aborted, settledKey));
    }

    subscribe(fn) {
      this.subscribers.add(fn);
      return () => this.subscribers.delete(fn);
    }

    cancel() {
      this.controller.abort();
      this.pendingKeysSet.forEach((v, k) => this.pendingKeysSet.delete(k));
      this.emit(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.pendingKeysSet.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)
        });
      }, {});
    }

    get pendingKeys() {
      return Array.from(this.pendingKeysSet);
    }

  }

  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;
  }

  const defer = function defer(data, init) {
    if (init === void 0) {
      init = {};
    }

    let responseInit = typeof init === "number" ? {
      status: init
    } : init;
    return new DeferredData(data, responseInit);
  };

  /**
   * 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, internal) {
      if (internal === void 0) {
        internal = false;
      }

      this.status = status;
      this.statusText = statusText || "";
      this.internal = internal;

      if (data instanceof Error) {
        this.data = data.toString();
        this.error = data;
      } else {
        this.data = data;
      }
    }

  }
  /**
   * Check if the given error is an ErrorResponse generated from a 4xx/5xx
   * Response thrown from an action/loader
   */

  function isRouteErrorResponse(error) {
    return error != null && typeof error.status === "number" && typeof error.statusText === "string" && typeof error.internal === "boolean" && "data" in error;
  }

  //#region Types and Constants
  ////////////////////////////////////////////////////////////////////////////////

  /**
   * A Router instance manages all navigation and data loading/mutations
   */

  const validMutationMethodsArr = ["post", "put", "patch", "delete"];
  const validMutationMethods = new Set(validMutationMethodsArr);
  const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
  const validRequestMethods = new Set(validRequestMethodsArr);
  const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
  const redirectPreserveMethodStatusCodes = new Set([307, 308]);
  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
  };
  const IDLE_BLOCKER = {
    state: "unblocked",
    proceed: undefined,
    reset: undefined,
    location: undefined
  };
  const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
  const isServer = !isBrowser; //#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 <ScrollRestoration /> until _after_
    // the initial render, we need to manually trigger a separate updateState to
    // send along the restoreScrollPosition
    // Set to true if we have `hydrationData` since we assume we were SSR'd and that
    // SSR did the initial scroll restoration.

    let initialScrollRestored = init.hydrationData != null;
    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 error = getInternalRouterError(404, {
        pathname: init.history.location.pathname
      });
      let {
        matches,
        route
      } = getShortCircuitMatches(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,
      // Don't restore on initial updateState() if we were SSR'd
      restoreScrollPosition: init.hydrationData != null ? false : 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(),
      blockers: 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(); // We ony support a single active blocker at the moment since we don't have
    // any compelling use cases for multi-blocker yet

    let activeBlocker = null; // Store blocker functions in a separate Map outside of router state since
    // we don't need to update UI state if they change

    let blockerFunctions = new Map(); // Flag to ignore the next history update, so we can revert the URL change on
    // a POP navigation that was blocked by the user without touching router state

    let ignoreNextHistoryUpdate = false; // 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,
          delta
        } = _ref;

        // Ignore this event if it was just us resetting the URL from a
        // blocked POP navigation
        if (ignoreNextHistoryUpdate) {
          ignoreNextHistoryUpdate = false;
          return;
        }

        warning(activeBlocker != null && delta === null, "You are trying to use a blocker on a POP navigation to a location " + "that was not created by @remix-run/router. This will fail silently in " + "production. This can happen if you are navigating outside the router " + "via `window.history.pushState`/`window.location.hash` instead of using " + "router navigation APIs.  This can also happen if you are using " + "createHashRouter and the user manually changes the URL.");
        let blockerKey = shouldBlockNavigation({
          currentLocation: state.location,
          nextLocation: location,
          historyAction
        });

        if (blockerKey && delta != null) {
          // Restore the URL to match the current UI, but don't update router state
          ignoreNextHistoryUpdate = true;
          init.history.go(delta * -1); // Put the blocker into a blocked state

          updateBlocker(blockerKey, {
            state: "blocked",
            location,

            proceed() {
              updateBlocker(blockerKey, {
                state: "proceeding",
                proceed: undefined,
                reset: undefined,
                location
              }); // Re-do the same POP navigation we just blocked

              init.history.go(delta);
            },

            reset() {
              deleteBlocker(blockerKey);
              updateState({
                blockers: new Map(router.state.blockers)
              });
            }

          });
          return;
        }

        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));
      state.blockers.forEach((_, key) => deleteBlocker(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 _location$state, _location$state2;

      // Deduce if we're in a loading/actionReload state:
      // - We have committed actionData in the store
      // - The current navigation was a mutation submission
      // - We're past the submitting state and into the loading state
      // - The location being loaded is not the result of a redirect
      let isActionReload = state.actionData != null && state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && state.navigation.state === "loading" && ((_location$state = location.state) == null ? void 0 : _location$state._isRedirect) !== true;
      let actionData;

      if (newState.actionData) {
        if (Object.keys(newState.actionData).length > 0) {
          actionData = newState.actionData;
        } else {
          // Empty actionData -> clear prior actionData due to an action error
          actionData = null;
        }
      } else if (isActionReload) {
        // Keep the current data if we're wrapping up the action reload
        actionData = state.actionData;
      } else {
        // Clear actionData on any other completed navigations
        actionData = null;
      } // Always preserve any existing loaderData from re-used routes


      let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData; // On a successful navigation we can assume we got through all blockers
      // so we can start fresh

      for (let [key] of blockerFunctions) {
        deleteBlocker(key);
      } // Always respect the user flag.  Otherwise don't reset on mutation
      // submission navigations unless they redirect


      let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && ((_location$state2 = location.state) == null ? void 0 : _location$state2._isRedirect) !== true;
      updateState(_extends({}, newState, {
        // matches, errors, fetchers go through as-is
        actionData,
        loaderData,
        historyAction: pendingAction,
        location,
        initialized: true,
        navigation: IDLE_NAVIGATION,
        revalidation: "idle",
        restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
        preventScrollReset,
        blockers: new Map(state.blockers)
      }));

      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 currentLocation = state.location;
      let nextLocation = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
      // URL from window.location, so we need to encode it here so the behavior
      // remains the same as POP and non-data-router usages.  new URL() does all
      // the same encoding we'd get from a history.pushState/window.location read
      // without having to touch history

      nextLocation = _extends({}, nextLocation, init.history.encodeLocation(nextLocation));
      let userReplace = opts && opts.replace != null ? opts.replace : undefined;
      let historyAction = exports.Action.Push;

      if (userReplace === true) {
        historyAction = exports.Action.Replace;
      } else if (userReplace === false) ; else if (submission != null && isMutationMethod(submission.formMethod) && submission.formAction === state.location.pathname + state.location.search) {
        // By default on submissions to the current location we REPLACE so that
        // users don't have to double-click the back button to get to the prior
        // location.  If the user redirects to a different location from the
        // action/loader this will be ignored and the redirect will be a PUSH
        historyAction = exports.Action.Replace;
      }

      let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
      let blockerKey = shouldBlockNavigation({
        currentLocation,
        nextLocation,
        historyAction
      });

      if (blockerKey) {
        // Put the blocker into a blocked state
        updateBlocker(blockerKey, {
          state: "blocked",
          location: nextLocation,

          proceed() {
            updateBlocker(blockerKey, {
              state: "proceeding",
              proceed: undefined,
              reset: undefined,
              location: nextLocation
            }); // Send the same navigation through

            navigate(to, opts);
          },

          reset() {
            deleteBlocker(blockerKey);
            updateState({
              blockers: new Map(state.blockers)
            });
          }

        });
        return;
      }

      return await startNavigation(historyAction, nextLocation, {
        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 error = getInternalRouterError(404, {
          pathname: location.pathname
        });
        let {
          matches: notFoundMatches,
          route
        } = getShortCircuitMatches(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 and not a mutation submission
      // For example, on /page#hash and submit a <Form method="post"> which will
      // default to a navigation to /page


      if (isHashChangeOnly(state.location, location) && !(opts && opts.submission && isMutationMethod(opts.submission.formMethod))) {
        completeNavigation(location, {
          matches
        });
        return;
      } // Create a controller/Request for this navigation


      pendingNavigationController = new AbortController();
      let request = createClientSideRequest(init.history, 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 && isMutationMethod(opts.submission.formMethod)) {
        // 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; // Create a GET request for the loaders

        request = new Request(request.url, {
          signal: request.signal
        });
      } // 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, _extends({
        matches
      }, pendingActionData ? {
        actionData: pendingActionData
      } : {}, {
        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 = {
          type: ResultType.error,
          error: getInternalRouterError(405, {
            method: request.method,
            pathname: location.pathname,
            routeId: actionMatch.route.id
          })
        };
      } else {
        result = await callLoaderOrAction("action", request, actionMatch, matches, router.basename);

        if (request.signal.aborted) {
          return {
            shortCircuited: true
          };
        }
      }

      if (isRedirectResult(result)) {
        let replace;

        if (opts && opts.replace != null) {
          replace = opts.replace;
        } else {
          // If the user didn't explicity indicate replace behavior, replace if
          // we redirected to the exact same location we're currently at to avoid
          // double back-buttons
          replace = result.location === state.location.pathname + state.location.search;
        }

        await startRedirectNavigation(state, result, {
          submission,
          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 {
          // Send back an empty object we can use to clear out any prior actionData
          pendingActionData: {},
          pendingActionError: {
            [boundaryMatch.route.id]: result.error
          }
        };
      }

      if (isDeferredResult(result)) {
        throw getInternalRouterError(400, {
          type: "defer-action"
        });
      }

      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 = _extends({
          state: "loading",
          location,
          formMethod: undefined,
          formAction: undefined,
          formEncType: undefined,
          formData: undefined
        }, submission);

        loadingNavigation = navigation;
      } // If this was a redirect from an action we don't have a "submission" but
      // we have it on the loading navigation so use that if available


      let activeSubmission = submission ? submission : loadingNavigation.formMethod && loadingNavigation.formAction && loadingNavigation.formData && loadingNavigation.formEncType ? {
        formMethod: loadingNavigation.formMethod,
        formAction: loadingNavigation.formAction,
        formData: loadingNavigation.formData,
        formEncType: loadingNavigation.formEncType
      } : undefined;
      let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, 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, _extends({
          matches,
          loaderData: {},
          // Commit pending error if we're short circuiting
          errors: pendingError || null
        }, pendingActionData ? {
          actionData: pendingActionData
        } : {}));
        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(rf => {
          let fetcher = state.fetchers.get(rf.key);
          let revalidatingFetcher = {
            state: "loading",
            data: fetcher && fetcher.data,
            formMethod: undefined,
            formAction: undefined,
            formEncType: undefined,
            formData: undefined,
            " _hasFetcherDoneAnything ": true
          };
          state.fetchers.set(rf.key, revalidatingFetcher);
        });
        let actionData = pendingActionData || state.actionData;
        updateState(_extends({
          navigation: loadingNavigation
        }, actionData ? Object.keys(actionData).length === 0 ? {
          actionData: null
        } : {
          actionData
        } : {}, revalidatingFetchers.length > 0 ? {
          fetchers: new Map(state.fetchers)
        } : {}));
      }

      pendingNavigationLoadId = ++incrementingLoadId;
      revalidatingFetchers.forEach(rf => fetchControllers.set(rf.key, pendingNavigationController));
      let {
        results,
        loaderResults,
        fetcherResults
      } = await callLoadersAndMaybeResolveData(state.matches, 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(rf => fetchControllers.delete(rf.key)); // If any loaders returned a redirect Response, start a new REPLACE navigation

      let redirect = findRedirect(results);

      if (redirect) {
        await startRedirectNavigation(state, redirect, {
          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 (isServer) {
        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, getInternalRouterError(404, {
          pathname: href
        }));
        return;
      }

      let {
        path,
        submission
      } = normalizeNavigateOptions(href, opts, true);
      let match = getTargetMatch(matches, path);
      pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;

      if (submission && isMutationMethod(submission.formMethod)) {
        handleFetcherAction(key, routeId, path, match, matches, submission);
        return;
      } // Store off the match so we can call it's shouldRevalidate on subsequent
      // revalidations


      fetchLoadMatches.set(key, {
        routeId,
        path,
        match,
        matches
      });
      handleFetcherLoader(key, routeId, path, match, matches, submission);
    } // Call the action for the matched fetcher.submit(), and then handle redirects,
    // errors, and revalidation


    async function handleFetcherAction(key, routeId, path, match, requestMatches, submission) {
      interruptActiveLoads();
      fetchLoadMatches.delete(key);

      if (!match.route.action) {
        let error = getInternalRouterError(405, {
          method: submission.formMethod,
          pathname: path,
          routeId: routeId
        });
        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,
        " _hasFetcherDoneAnything ": true
      });

      state.fetchers.set(key, fetcher);
      updateState({
        fetchers: new Map(state.fetchers)
      }); // Call the action for the fetcher

      let abortController = new AbortController();
      let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
      fetchControllers.set(key, abortController);
      let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, router.basename);

      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,
          " _hasFetcherDoneAnything ": true
        });

        state.fetchers.set(key, loadingFetcher);
        updateState({
          fetchers: new Map(state.fetchers)
        });
        return startRedirectNavigation(state, actionResult, {
          isFetchActionRedirect: true
        });
      } // Process any non-redirect errors thrown


      if (isErrorResult(actionResult)) {
        setFetcherError(key, routeId, actionResult.error);
        return;
      }

      if (isDeferredResult(actionResult)) {
        throw getInternalRouterError(400, {
          type: "defer-action"
        });
      } // 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 = createClientSideRequest(init.history, 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, {
        " _hasFetcherDoneAnything ": true
      });

      state.fetchers.set(key, loadFetcher);
      let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, 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(rf => rf.key !== key).forEach(rf => {
        let staleKey = rf.key;
        let existingFetcher = state.fetchers.get(staleKey);
        let revalidatingFetcher = {
          state: "loading",
          data: existingFetcher && existingFetcher.data,
          formMethod: undefined,
          formAction: undefined,
          formEncType: undefined,
          formData: undefined,
          " _hasFetcherDoneAnything ": true
        };
        state.fetchers.set(staleKey, revalidatingFetcher);
        fetchControllers.set(staleKey, abortController);
      });
      updateState({
        fetchers: new Map(state.fetchers)
      });
      let {
        results,
        loaderResults,
        fetcherResults
      } = await callLoadersAndMaybeResolveData(state.matches, matches, matchesToLoad, revalidatingFetchers, revalidationRequest);

      if (abortController.signal.aborted) {
        return;
      }

      fetchReloadIds.delete(key);
      fetchControllers.delete(key);
      revalidatingFetchers.forEach(r => fetchControllers.delete(r.key));
      let redirect = findRedirect(results);

      if (redirect) {
        return startRedirectNavigation(state, redirect);
      } // 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,
        " _hasFetcherDoneAnything ": true
      };
      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, errors)
        }, 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, matches, submission) {
      let existingFetcher = state.fetchers.get(key); // Put this fetcher into it's loading state

      let loadingFetcher = _extends({
        state: "loading",
        formMethod: undefined,
        formAction: undefined,
        formEncType: undefined,
        formData: undefined
      }, submission, {
        data: existingFetcher && existingFetcher.data,
        " _hasFetcherDoneAnything ": true
      });

      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 = createClientSideRequest(init.history, path, abortController.signal);
      fetchControllers.set(key, abortController);
      let result = await callLoaderOrAction("loader", fetchRequest, match, matches, router.basename); // Deferred isn't supported for 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)) {
        await startRedirectNavigation(state, result);
        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,
        " _hasFetcherDoneAnything ": true
      };
      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(state, redirect, _temp) {
      var _window;

      let {
        submission,
        replace,
        isFetchActionRedirect
      } = _temp === void 0 ? {} : _temp;

      if (redirect.revalidate) {
        isRevalidationRequired = true;
      }

      let redirectLocation = createLocation(state.location, redirect.location, // TODO: This can be removed once we get rid of useTransition in Remix v2
      _extends({
        _isRedirect: true
      }, isFetchActionRedirect ? {
        _isFetchActionRedirect: true
      } : {}));
      invariant(redirectLocation, "Expected a location on the redirect navigation"); // Check if this an external redirect that goes to a new origin

      if (isBrowser && typeof ((_window = window) == null ? void 0 : _window.location) !== "undefined") {
        let newOrigin = init.history.createURL(redirect.location).origin;

        if (window.location.origin !== newOrigin) {
          if (replace) {
            window.location.replace(redirect.location);
          } else {
            window.location.assign(redirect.location);
          }

          return;
        }
      } // 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; // Use the incoming submission if provided, fallback on the active one in
      // state.navigation

      let {
        formMethod,
        formAction,
        formEncType,
        formData
      } = state.navigation;

      if (!submission && formMethod && formAction && formData && formEncType) {
        submission = {
          formMethod,
          formAction,
          formEncType,
          formData
        };
      } // If this was a 307/308 submission we want to preserve the HTTP method and
      // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
      // redirected location


      if (redirectPreserveMethodStatusCodes.has(redirect.status) && submission && isMutationMethod(submission.formMethod)) {
        await startNavigation(redirectHistoryAction, redirectLocation, {
          submission: _extends({}, submission, {
            formAction: redirect.location
          }),
          // Preserve this flag across redirects
          preventScrollReset: pendingPreventScrollReset
        });
      } else {
        // Otherwise, we kick off a new loading navigation, preserving the
        // submission info for the duration of this navigation
        await startNavigation(redirectHistoryAction, redirectLocation, {
          overrideNavigation: {
            state: "loading",
            location: redirectLocation,
            formMethod: submission ? submission.formMethod : undefined,
            formAction: submission ? submission.formAction : undefined,
            formEncType: submission ? submission.formEncType : undefined,
            formData: submission ? submission.formData : undefined
          },
          // Preserve this flag across redirects
          preventScrollReset: pendingPreventScrollReset
        });
      }
    }

    async function callLoadersAndMaybeResolveData(currentMatches, matches, 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(match => callLoaderOrAction("loader", request, match, matches, router.basename)), ...fetchersToLoad.map(f => callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, request.signal), f.match, f.matches, router.basename))]);
      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(f => f.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,
          " _hasFetcherDoneAnything ": true
        };
        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 getBlocker(key, fn) {
      let blocker = state.blockers.get(key) || IDLE_BLOCKER;

      if (blockerFunctions.get(key) !== fn) {
        blockerFunctions.set(key, fn);

        if (activeBlocker == null) {
          // This is now the active blocker
          activeBlocker = key;
        } else if (key !== activeBlocker) {
          warning(false, "A router only supports one blocker at a time");
        }
      }

      return blocker;
    }

    function deleteBlocker(key) {
      state.blockers.delete(key);
      blockerFunctions.delete(key);

      if (activeBlocker === key) {
        activeBlocker = null;
      }
    } // Utility function to update blockers, ensuring valid state transitions


    function updateBlocker(key, newBlocker) {
      let blocker = state.blockers.get(key) || IDLE_BLOCKER; // Poor mans state machine :)
      // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM

      invariant(blocker.state === "unblocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "proceeding" || blocker.state === "blocked" && newBlocker.state === "unblocked" || blocker.state === "proceeding" && newBlocker.state === "unblocked", "Invalid blocker state transition: " + blocker.state + " -> " + newBlocker.state);
      state.blockers.set(key, newBlocker);
      updateState({
        blockers: new Map(state.blockers)
      });
    }

    function shouldBlockNavigation(_ref2) {
      let {
        currentLocation,
        nextLocation,
        historyAction
      } = _ref2;

      if (activeBlocker == null) {
        return;
      } // We only allow a single blocker at the moment.  This will need to be
      // updated if we enhance to support multiple blockers in the future


      let blockerFunction = blockerFunctions.get(activeBlocker);
      invariant(blockerFunction, "Could not find a function for the active blocker");
      let blocker = state.blockers.get(activeBlocker);

      if (blocker && blocker.state === "proceeding") {
        // If the blocker is currently proceeding, we don't need to re-check
        // it and can let this navigation continue
        return;
      } // At this point, we know we're unblocked/blocked so we need to check the
      // user-provided blocker function


      if (blockerFunction({
        currentLocation,
        nextLocation,
        historyAction
      })) {
        return activeBlocker;
      }
    }

    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 <ScrollRestoration> 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 <ScrollRestoration/>
      // 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,
      // Passthrough to history-aware createHref used by useHref so we get proper
      // hash-aware URLs in DOM paths
      createHref: to => init.history.createHref(to),
      encodeLocation: to => init.history.encodeLocation(to),
      getFetcher,
      deleteFetcher,
      dispose,
      getBlocker,
      deleteBlocker,
      _internalFetchControllers: fetchControllers,
      _internalActiveDeferreds: activeDeferreds
    };
    return router;
  } //#endregion
  ////////////////////////////////////////////////////////////////////////////////
  //#region createStaticHandler
  ////////////////////////////////////////////////////////////////////////////////

  const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
  function createStaticHandler(routes, opts) {
    invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
    let dataRoutes = convertRoutesToDataRoutes(routes);
    let basename = (opts ? opts.basename : null) || "/";
    /**
     * The query() method is intended for document requests, in which we want to
     * call an optional action and potentially multiple loaders for all nested
     * routes.  It returns a StaticHandlerContext object, which is very similar
     * to the router state (location, loaderData, actionData, errors, etc.) and
     * also adds SSR-specific information such as the statusCode and headers
     * from action/loaders Responses.
     *
     * It _should_ never throw and should report all errors through the
     * returned context.errors object, properly associating errors to their error
     * boundary.  Additionally, it tracks _deepestRenderedBoundaryId which can be
     * used to emulate React error boundaries during SSr by performing a second
     * pass only down to the boundaryId.
     *
     * The one exception where we do not return a StaticHandlerContext is when a
     * redirect response is returned or thrown from any action/loader.  We
     * propagate that out and return the raw Response so the HTTP server can
     * return it directly.
     */

    async function query(request, _temp2) {
      let {
        requestContext
      } = _temp2 === void 0 ? {} : _temp2;
      let url = new URL(request.url);
      let method = request.method.toLowerCase();
      let location = createLocation("", createPath(url), null, "default");
      let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't

      if (!isValidMethod(method) && method !== "head") {
        let error = getInternalRouterError(405, {
          method
        });
        let {
          matches: methodNotAllowedMatches,
          route
        } = getShortCircuitMatches(dataRoutes);
        return {
          basename,
          location,
          matches: methodNotAllowedMatches,
          loaderData: {},
          actionData: null,
          errors: {
            [route.id]: error
          },
          statusCode: error.status,
          loaderHeaders: {},
          actionHeaders: {},
          activeDeferreds: null
        };
      } else if (!matches) {
        let error = getInternalRouterError(404, {
          pathname: location.pathname
        });
        let {
          matches: notFoundMatches,
          route
        } = getShortCircuitMatches(dataRoutes);
        return {
          basename,
          location,
          matches: notFoundMatches,
          loaderData: {},
          actionData: null,
          errors: {
            [route.id]: error
          },
          statusCode: error.status,
          loaderHeaders: {},
          actionHeaders: {},
          activeDeferreds: null
        };
      }

      let result = await queryImpl(request, location, matches, requestContext);

      if (isResponse(result)) {
        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,
        basename
      }, result);
    }
    /**
     * The queryRoute() method is intended for targeted route requests, either
     * for fetch ?_data requests or resource route requests.  In this case, we
     * are only ever calling a single action or loader, and we are returning the
     * returned value directly.  In most cases, this will be a Response returned
     * from the action/loader, but it may be a primitive or other value as well -
     * and in such cases the calling context should handle that accordingly.
     *
     * We do respect the throw/return differentiation, so if an action/loader
     * throws, then this method will throw the value.  This is important so we
     * can do proper boundary identification in Remix where a thrown Response
     * must go to the Catch Boundary but a returned Response is happy-path.
     *
     * One thing to note is that any Router-initiated Errors that make sense
     * to associate with a status code will be thrown as an ErrorResponse
     * instance which include the raw Error, such that the calling context can
     * serialize the error as they see fit while including the proper response
     * code.  Examples here are 404 and 405 errors that occur prior to reaching
     * any user-defined loaders.
     */


    async function queryRoute(request, _temp3) {
      let {
        routeId,
        requestContext
      } = _temp3 === void 0 ? {} : _temp3;
      let url = new URL(request.url);
      let method = request.method.toLowerCase();
      let location = createLocation("", createPath(url), null, "default");
      let matches = matchRoutes(dataRoutes, location, basename); // SSR supports HEAD requests while SPA doesn't

      if (!isValidMethod(method) && method !== "head" && method !== "options") {
        throw getInternalRouterError(405, {
          method
        });
      } else if (!matches) {
        throw getInternalRouterError(404, {
          pathname: location.pathname
        });
      }

      let match = routeId ? matches.find(m => m.route.id === routeId) : getTargetMatch(matches, location);

      if (routeId && !match) {
        throw getInternalRouterError(403, {
          pathname: location.pathname,
          routeId
        });
      } else if (!match) {
        // This should never hit I don't think?
        throw getInternalRouterError(404, {
          pathname: location.pathname
        });
      }

      let result = await queryImpl(request, location, matches, requestContext, match);

      if (isResponse(result)) {
        return result;
      }

      let error = result.errors ? Object.values(result.errors)[0] : undefined;

      if (error !== undefined) {
        // 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


      if (result.actionData) {
        return Object.values(result.actionData)[0];
      }

      if (result.loaderData) {
        var _result$activeDeferre;

        let data = Object.values(result.loaderData)[0];

        if ((_result$activeDeferre = result.activeDeferreds) != null && _result$activeDeferre[match.route.id]) {
          data[UNSAFE_DEFERRED_SYMBOL] = result.activeDeferreds[match.route.id];
        }

        return data;
      }

      return undefined;
    }

    async function queryImpl(request, location, matches, requestContext, routeMatch) {
      invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");

      try {
        if (isMutationMethod(request.method.toLowerCase())) {
          let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
          return result;
        }

        let result = await loadRouteData(request, matches, requestContext, routeMatch);
        return isResponse(result) ? result : _extends({}, result, {
          actionData: null,
          actionHeaders: {}
        });
      } catch (e) {
        // If the user threw/returned a Response in callLoaderOrAction, we throw
        // it to bail out and then return or throw here based on whether the user
        // returned or threw
        if (isQueryRouteResponse(e)) {
          if (e.type === ResultType.error && !isRedirectResponse(e.response)) {
            throw e.response;
          }

          return e.response;
        } // Redirects are always returned since they don't propagate to catch
        // boundaries


        if (isRedirectResponse(e)) {
          return e;
        }

        throw e;
      }
    }

    async function submit(request, matches, actionMatch, requestContext, isRouteRequest) {
      let result;

      if (!actionMatch.route.action) {
        let error = getInternalRouterError(405, {
          method: request.method,
          pathname: new URL(request.url).pathname,
          routeId: actionMatch.route.id
        });

        if (isRouteRequest) {
          throw error;
        }

        result = {
          type: ResultType.error,
          error
        };
      } else {
        result = await callLoaderOrAction("action", request, actionMatch, matches, basename, true, isRouteRequest, requestContext);

        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
        // callLoaderOrAction, 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)) {
        let error = getInternalRouterError(400, {
          type: "defer-action"
        });

        if (isRouteRequest) {
          throw error;
        }

        result = {
          type: ResultType.error,
          error
        };
      }

      if (isRouteRequest) {
        // Note: This should only be non-Response values if we get here, since
        // isRouteRequest should throw any Response received in callLoaderOrAction
        if (isErrorResult(result)) {
          throw result.error;
        }

        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: {},
          activeDeferreds: null
        };
      }

      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, requestContext, undefined, {
          [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
          } : {})
        });
      } // Create a GET request for the loaders


      let loaderRequest = new Request(request.url, {
        headers: request.headers,
        redirect: request.redirect,
        signal: request.signal
      });
      let context = await loadRouteData(loaderRequest, matches, requestContext);
      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, requestContext, routeMatch, pendingActionError) {
      let isRouteRequest = routeMatch != null; // Short circuit if we have no loaders to run (queryRoute())

      if (isRouteRequest && !(routeMatch != null && routeMatch.route.loader)) {
        throw getInternalRouterError(400, {
          method: request.method,
          pathname: new URL(request.url).pathname,
          routeId: routeMatch == null ? void 0 : routeMatch.route.id
        });
      }

      let requestMatches = routeMatch ? [routeMatch] : getLoaderMatchesUntilBoundary(matches, Object.keys(pendingActionError || {})[0]);
      let matchesToLoad = requestMatches.filter(m => m.route.loader); // Short circuit if we have no loaders to run (query())

      if (matchesToLoad.length === 0) {
        return {
          matches,
          // Add a null for all matched routes for proper revalidation on the client
          loaderData: matches.reduce((acc, m) => Object.assign(acc, {
            [m.route.id]: null
          }), {}),
          errors: pendingActionError || null,
          statusCode: 200,
          loaderHeaders: {},
          activeDeferreds: null
        };
      }

      let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, basename, true, isRouteRequest, requestContext))]);

      if (request.signal.aborted) {
        let method = isRouteRequest ? "queryRoute" : "query";
        throw new Error(method + "() call aborted");
      } // Process and commit output from loaders


      let activeDeferreds = new Map();
      let context = processRouteLoaderData(matches, matchesToLoad, results, pendingActionError, activeDeferreds); // Add a null for any non-loader matches for proper revalidation on the client

      let executedLoaders = new Set(matchesToLoad.map(match => match.route.id));
      matches.forEach(match => {
        if (!executedLoaders.has(match.route.id)) {
          context.loaderData[match.route.id] = null;
        }
      });
      return _extends({}, context, {
        matches,
        activeDeferreds: activeDeferreds.size > 0 ? Object.fromEntries(activeDeferreds.entries()) : null
      });
    }

    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;
  }

  function isSubmissionNavigation(opts) {
    return opts != null && "formData" in opts;
  } // 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 || !isSubmissionNavigation(opts)) {
      return {
        path
      };
    }

    if (opts.formMethod && !isValidMethod(opts.formMethod)) {
      return {
        path,
        error: getInternalRouterError(405, {
          method: opts.formMethod
        })
      };
    } // Create a Submission on non-GET navigations


    let submission;

    if (opts.formData) {
      submission = {
        formMethod: opts.formMethod || "get",
        formAction: stripHashFromPath(path),
        formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
        formData: opts.formData
      };

      if (isMutationMethod(submission.formMethod)) {
        return {
          path,
          submission
        };
      }
    } // Flatten submission onto URLSearchParams for GET submissions


    let parsedPath = parsePath(path);
    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;
    return {
      path: createPath(parsedPath),
      submission
    };
  } // 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(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, pendingActionData, pendingError, fetchLoadMatches) {
    let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
    let currentUrl = history.createURL(state.location);
    let nextUrl = history.createURL(location);
    let defaultShouldRevalidate = // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
    isRevalidationRequired || // Clicked the same link, resubmitted a GET form
    currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders
    currentUrl.search !== nextUrl.search; // 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) => {
      if (match.route.loader == null) {
        return false;
      } // Always call the loader on new route instances and pending defer cancellations


      if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {
        return true;
      } // This is the default implementation for 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


      let currentRouteMatch = state.matches[index];
      let nextRouteMatch = match;
      return shouldRevalidateLoader(match, _extends({
        currentUrl,
        currentParams: currentRouteMatch.params,
        nextUrl,
        nextParams: nextRouteMatch.params
      }, submission, {
        actionResult,
        defaultShouldRevalidate: defaultShouldRevalidate || isNewRouteInstance(currentRouteMatch, nextRouteMatch)
      }));
    }); // Pick fetcher.loads that need to be revalidated

    let revalidatingFetchers = [];
    fetchLoadMatches && fetchLoadMatches.forEach((f, key) => {
      if (!matches.some(m => m.route.id === f.routeId)) {
        // This fetcher is not going to be present in the subsequent render so
        // there's no need to revalidate it
        return;
      } else if (cancelledFetcherLoads.includes(key)) {
        // This fetcher was cancelled from a prior action submission - force reload
        revalidatingFetchers.push(_extends({
          key
        }, f));
      } else {
        // Revalidating fetchers are decoupled from the route matches since they
        // hit a static href, so they _always_ check shouldRevalidate and the
        // default is strictly if a revalidation is explicitly required (action
        // submissions, useRevalidator, X-Remix-Revalidate).
        let shouldRevalidate = shouldRevalidateLoader(f.match, _extends({
          currentUrl,
          currentParams: state.matches[state.matches.length - 1].params,
          nextUrl,
          nextParams: matches[matches.length - 1].params
        }, submission, {
          actionResult,
          defaultShouldRevalidate
        }));

        if (shouldRevalidate) {
          revalidatingFetchers.push(_extends({
            key
          }, f));
        }
      }
    });
    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 != null && currentPath.endsWith("*") && currentMatch.params["*"] !== match.params["*"]
    );
  }

  function shouldRevalidateLoader(loaderMatch, arg) {
    if (loaderMatch.route.shouldRevalidate) {
      let routeChoice = loaderMatch.route.shouldRevalidate(arg);

      if (typeof routeChoice === "boolean") {
        return routeChoice;
      }
    }

    return arg.defaultShouldRevalidate;
  }

  async function callLoaderOrAction(type, request, match, matches, basename, isStaticRequest, isRouteRequest, requestContext) {
    if (basename === void 0) {
      basename = "/";
    }

    if (isStaticRequest === void 0) {
      isStaticRequest = 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,
        context: requestContext
      }), abortPromise]);
      invariant(result !== undefined, "You defined " + (type === "action" ? "an action" : "a loader") + " for route " + ("\"" + match.route.id + "\" but didn't return anything from your `" + type + "` ") + "function. Please return a value or `null`.");
    } catch (e) {
      resultType = ResultType.error;
      result = e;
    } finally {
      request.signal.removeEventListener("abort", onReject);
    }

    if (isResponse(result)) {
      let status = result.status; // Process redirects

      if (redirectStatusCodes.has(status)) {
        let location = result.headers.get("Location");
        invariant(location, "Redirects returned/thrown from loaders/actions must have a Location header");
        let isAbsolute = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(location); // Support relative routing in internal redirects

        if (!isAbsolute) {
          let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
          let routePathnames = getPathContributingMatches(activeMatches).map(match => match.pathnameBase);
          let resolvedLocation = resolveTo(location, routePathnames, new URL(request.url).pathname);
          invariant(createPath(resolvedLocation), "Unable to resolve redirect location: " + location); // Prepend the basename to the redirect location if we have one

          if (basename) {
            let path = resolvedLocation.pathname;
            resolvedLocation.pathname = path === "/" ? basename : joinPaths([basename, path]);
          }

          location = createPath(resolvedLocation);
        } else if (!isStaticRequest) {
          // Strip off the protocol+origin for same-origin absolute redirects.
          // If this is a static reques, we can let it go back to the browser
          // as-is
          let currentUrl = new URL(request.url);
          let url = location.startsWith("//") ? new URL(currentUrl.protocol + location) : new URL(location);

          if (url.origin === currentUrl.origin) {
            location = url.pathname + url.search + url.hash;
          }
        } // Don't process redirects in the router during static requests requests.
        // Instead, throw the Response and let the server handle it with an HTTP
        // redirect.  We also update the Location header in place in this flow so
        // basename and relative routing is taken into account


        if (isStaticRequest) {
          result.headers.set("Location", location);
          throw result;
        }

        return {
          type: ResultType.redirect,
          status,
          location,
          revalidate: result.headers.get("X-Remix-Revalidate") !== null
        };
      } // For SSR single-route requests, we want to hand Responses back directly
      // without unwrapping.  We do this with the QueryRouteResponse wrapper
      // interface so we can know whether it was returned or thrown


      if (isRouteRequest) {
        // eslint-disable-next-line no-throw-literal
        throw {
          type: resultType || ResultType.data,
          response: result
        };
      }

      let data;
      let contentType = result.headers.get("Content-Type"); // Check between word boundaries instead of startsWith() due to the last
      // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type

      if (contentType && /\bapplication\/json\b/.test(contentType)) {
        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
    };
  } // Utility method for creating the Request instances for loaders/actions during
  // client-side navigations and fetches.  During SSR we will always have a
  // Request instance from the static handler (query/queryRoute)


  function createClientSideRequest(history, location, signal, submission) {
    let url = history.createURL(stripHashFromPath(location)).toString();
    let init = {
      signal
    };

    if (submission && isMutationMethod(submission.formMethod)) {
      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()) {
      // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#converting-an-entry-list-to-a-list-of-name-value-pairs
      searchParams.append(key, value instanceof File ? value.name : 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 = errors || {}; // Prefer higher error values if lower errors bubble to the same boundary

        if (errors[boundaryMatch.route.id] == null) {
          errors[boundaryMatch.route.id] = error;
        } // Clear our any prior loaderData for the throwing route


        loaderData[id] = undefined; // 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.set(id, result.deferredData);
          loaderData[id] = result.deferredData.data;
        } 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.  Also clear out any loaderData for the
    // throwing route

    if (pendingError) {
      errors = pendingError;
      loaderData[Object.keys(pendingError)[0]] = undefined;
    }

    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
        invariant(false, "Unhandled fetcher revalidation redirect");
      } else if (isDeferredResult(result)) {
        // Should never get here, deferred data should be awaited for fetchers
        // in resolveDeferredResults
        invariant(false, "Unhandled fetcher deferred data");
      } else {
        let doneFetcher = {
          state: "idle",
          data: result.data,
          formMethod: undefined,
          formAction: undefined,
          formEncType: undefined,
          formData: undefined,
          " _hasFetcherDoneAnything ": true
        };
        state.fetchers.set(key, doneFetcher);
      }
    }

    return {
      loaderData,
      errors
    };
  }

  function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
    let mergedLoaderData = _extends({}, newLoaderData);

    for (let match of matches) {
      let id = match.route.id;

      if (newLoaderData.hasOwnProperty(id)) {
        if (newLoaderData[id] !== undefined) {
          mergedLoaderData[id] = newLoaderData[id];
        }
      } else if (loaderData[id] !== undefined) {
        mergedLoaderData[id] = loaderData[id];
      }

      if (errors && errors.hasOwnProperty(id)) {
        // Don't keep any loader data below the boundary
        break;
      }
    }

    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 getShortCircuitMatches(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-error-route__"
    };
    return {
      matches: [{
        params: {},
        pathname: "",
        pathnameBase: "",
        route
      }],
      route
    };
  }

  function getInternalRouterError(status, _temp4) {
    let {
      pathname,
      routeId,
      method,
      type
    } = _temp4 === void 0 ? {} : _temp4;
    let statusText = "Unknown Server Error";
    let errorMessage = "Unknown @remix-run/router error";

    if (status === 400) {
      statusText = "Bad Request";

      if (method && pathname && routeId) {
        errorMessage = "You made a " + method + " request to \"" + pathname + "\" but " + ("did not provide a `loader` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
      } else if (type === "defer-action") {
        errorMessage = "defer() is not supported in actions";
      }
    } else if (status === 403) {
      statusText = "Forbidden";
      errorMessage = "Route \"" + routeId + "\" does not match URL \"" + pathname + "\"";
    } else if (status === 404) {
      statusText = "Not Found";
      errorMessage = "No route matches URL \"" + pathname + "\"";
    } else if (status === 405) {
      statusText = "Method Not Allowed";

      if (method && pathname && routeId) {
        errorMessage = "You made a " + method.toUpperCase() + " request to \"" + pathname + "\" but " + ("did not provide an `action` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
      } else if (method) {
        errorMessage = "Invalid request method \"" + method.toUpperCase() + "\"";
      }
    }

    return new ErrorResponse(status || 500, statusText, new Error(errorMessage), true);
  } // 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;
      }
    }
  }

  function stripHashFromPath(path) {
    let parsedPath = typeof path === "string" ? parsePath(path) : path;
    return createPath(_extends({}, parsedPath, {
      hash: ""
    }));
  }

  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;
  }

  function isResponse(value) {
    return value != null && typeof value.status === "number" && typeof value.statusText === "string" && typeof value.headers === "object" && typeof value.body !== "undefined";
  }

  function isRedirectResponse(result) {
    if (!isResponse(result)) {
      return false;
    }

    let status = result.status;
    let location = result.headers.get("Location");
    return status >= 300 && status <= 399 && location != null;
  }

  function isQueryRouteResponse(obj) {
    return obj && isResponse(obj.response) && (obj.type === ResultType.data || ResultType.error);
  }

  function isValidMethod(method) {
    return validRequestMethods.has(method);
  }

  function isMutationMethod(method) {
    return validMutationMethods.has(method);
  }

  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 the leaf index route when index is present
      return matches[matches.length - 1];
    } // Otherwise grab the deepest "path contributing" match (ignoring index and
    // pathless layout routes)


    let pathMatches = getPathContributingMatches(matches);
    return pathMatches[pathMatches.length - 1];
  } //#endregion

  exports.AbortedDeferredError = AbortedDeferredError;
  exports.ErrorResponse = ErrorResponse;
  exports.IDLE_BLOCKER = IDLE_BLOCKER;
  exports.IDLE_FETCHER = IDLE_FETCHER;
  exports.IDLE_NAVIGATION = IDLE_NAVIGATION;
  exports.UNSAFE_DEFERRED_SYMBOL = UNSAFE_DEFERRED_SYMBOL;
  exports.UNSAFE_DeferredData = DeferredData;
  exports.UNSAFE_convertRoutesToDataRoutes = convertRoutesToDataRoutes;
  exports.UNSAFE_getPathContributingMatches = getPathContributingMatches;
  exports.createBrowserHistory = createBrowserHistory;
  exports.createHashHistory = createHashHistory;
  exports.createMemoryHistory = createMemoryHistory;
  exports.createPath = createPath;
  exports.createRouter = createRouter;
  exports.createStaticHandler = createStaticHandler;
  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.warning = warning;

  Object.defineProperty(exports, '__esModule', { value: true });

}));
//# sourceMappingURL=router.umd.js.map
