UNPKG

@remix-run/router

Version:

Nested/Data-driven/Framework-agnostic Routing

1,503 lines (1,423 loc) 207 kB
/** * @remix-run/router v1.23.0 * * Copyright (c) Remix Software Inc. * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } //////////////////////////////////////////////////////////////////////////////// //#region Types and Constants //////////////////////////////////////////////////////////////////////////////// /** * Actions represent the type of change to a location value. */ let Action = /*#__PURE__*/function (Action) { Action["Pop"] = "POP"; Action["Push"] = "PUSH"; Action["Replace"] = "REPLACE"; return Action; }({}); /** * The pathname, search, and hash values of a URL. */ // TODO: (v7) Change the Location generic default from `any` to `unknown` and // remove Remix `useLocation` wrapper. /** * An entry in a history stack. A location contains information about the * URL path, as well as possibly some arbitrary state and a key. */ /** * A change to the current location. */ /** * A function that receives notifications about location changes. */ /** * Describes a location that is the destination of some navigation, either via * `history.push` or `history.replace`. This may be either a URL or the pieces * of a URL path. */ /** * A history is an interface to the navigation stack. The history serves as the * source of truth for the current location, as well as provides a set of * methods that may be used to change it. * * It is similar to the DOM's `window.history` object, but with a smaller, more * focused API. */ 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. */ /** * A memory history stores locations in memory. This is useful in stateful * environments where there is no web browser, such as node tests or React * Native. */ /** * 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 = 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(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 = 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 = Action.Replace; let nextLocation = createMemoryLocation(to, state); entries[index] = nextLocation; if (v5Compat && listener) { listener({ action, location: nextLocation, delta: 0 }); } }, go(delta) { action = 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)); // Hash URL should always have a leading / just like window.location.pathname // does, so if an app ends up at a route like /#something then we add a // leading slash so all of our path-matching behaves the same as if it would // in a browser router. This is particularly important when there exists a // root splat route (<Route path="*">) since that matches internally against // "/*" and we'd expect /#something to 404 in a hash router app. if (!pathname.startsWith("/") && !pathname.startsWith(".")) { pathname = "/" + pathname; } 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(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(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 = 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 = 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 = 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) { // If the exception is because `state` can't be serialized, let that throw // outwards just like a replace call would so the dev knows the cause // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps // https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal if (error instanceof DOMException && error.name === "DataCloneError") { throw 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 = 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); // Treating this as a full URL will strip any trailing spaces so we need to // pre-encode them since they might be part of a matching splat param from // an ancestor route href = href.replace(/ $/, "%20"); 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 = /*#__PURE__*/function (ResultType) { ResultType["data"] = "data"; ResultType["deferred"] = "deferred"; ResultType["redirect"] = "redirect"; ResultType["error"] = "error"; return ResultType; }({}); /** * Successful result from a loader or action */ /** * Successful defer() result from a loader or action */ /** * Redirect result from a loader or action */ /** * Unsuccessful result from a loader or action */ /** * Result from a loader or action - potentially successful or unsuccessful */ /** * Users can specify either lowercase or uppercase form methods on `<Form>`, * useSubmit(), `<fetcher.Form>`, etc. */ /** * Active navigation/fetcher form methods are exposed in lowercase on the * RouterState */ /** * In v7, active navigation/fetcher form methods are exposed in uppercase on the * RouterState. This is to align with the normalization done via fetch(). */ // Thanks https://github.com/sindresorhus/type-fest! /** * @private * Internal interface to pass around for action submissions, not intended for * external consumption */ /** * @private * Arguments passed to route loader/action functions. Same for now but we keep * this as a private implementation detail in case they diverge in the future. */ // TODO: (v7) Change the defaults from any to unknown in and remove Remix wrappers: // ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs // Also, make them a type alias instead of an interface /** * Arguments passed to loader functions */ /** * Arguments passed to action functions */ /** * Loaders and actions can return anything except `undefined` (`null` is a * valid return value if there is no data to return). Responses are preferred * and will ease any future migration to Remix */ /** * Route loader function signature */ /** * Route action function signature */ /** * Arguments passed to shouldRevalidate function */ /** * Route shouldRevalidate function signature. This runs after any submission * (navigation or fetcher), so we flatten the navigation/fetcher submission * onto the arguments. It shouldn't matter whether it came from a navigation * or a fetcher, what really matters is the URLs and the formData since loaders * have to re-run based on the data models that were potentially mutated. */ /** * Function provided by the framework-aware layers to set `hasErrorBoundary` * from the framework-aware `errorElement` prop * * @deprecated Use `mapRouteProperties` instead */ /** * Result from a loader or action called via dataStrategy */ /** * Function provided by the framework-aware layers to set any framework-specific * properties from framework-agnostic properties */ /** * Keys we cannot change from within a lazy() function. We spread all other keys * onto the route. Either they're meaningful to the router, or they'll get * ignored. */ const immutableRouteKeys = new Set(["lazy", "caseSensitive", "path", "id", "index", "children"]); /** * lazy() function to load a route definition, which can add non-matching * related properties to a route */ /** * Base RouteObject with common props shared by all types of routes */ /** * Index routes must not have children */ /** * Non-index routes may have children, but cannot have index */ /** * A route object represents a logical route, with (optionally) its child * routes organized in a tree-like structure. */ /** * A data route object, which is just a RouteObject with a required unique ID */ // Recursive helper for finding path parameters in the absence of wildcards /** * Examples: * "/a/b/*" -> "*" * ":a" -> "a" * "/a/:b" -> "b" * "/a/blahblahblah:b" -> "b" * "/:a/:b" -> "a" | "b" * "/:a/b/:c/*" -> "a" | "c" | "*" */ // Attempt to parse the given string segment. If it fails, then just return the // plain string type as a default fallback. Otherwise, return the union of the // parsed string literals that were referenced as dynamic segments in the route. /** * The parameters that were parsed from the URL path. */ /** * A RouteMatch contains info about how a route matched a URL. */ 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, mapRouteProperties, parentPath, manifest) { if (parentPath === void 0) { parentPath = []; } if (manifest === void 0) { manifest = {}; } return routes.map((route, index) => { let treePath = [...parentPath, String(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(!manifest[id], "Found a route id collision on id \"" + id + "\". Route " + "id's must be globally unique within Data Router usages"); if (isIndexRoute(route)) { let indexRoute = _extends({}, route, mapRouteProperties(route), { id }); manifest[id] = indexRoute; return indexRoute; } else { let pathOrLayoutRoute = _extends({}, route, mapRouteProperties(route), { id, children: undefined }); manifest[id] = pathOrLayoutRoute; if (route.children) { pathOrLayoutRoute.children = convertRoutesToDataRoutes(route.children, mapRouteProperties, treePath, manifest); } return pathOrLayoutRoute; } }); } /** * Matches the given routes to a location and returns the match data. * * @see https://reactrouter.com/v6/utils/match-routes */ function matchRoutes(routes, locationArg, basename) { if (basename === void 0) { basename = "/"; } return matchRoutesImpl(routes, locationArg, basename, false); } function matchRoutesImpl(routes, locationArg, basename, allowPartial) { 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) { // 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. let decoded = decodePath(pathname); matches = matchRouteBranch(branches[i], decoded, allowPartial); } return matches; } function convertRouteMatchToUiMatch(match, loaderData) { let { route, pathname, params } = match; return { id: route.id, pathname, params, data: loaderData[route.id], handle: route.handle }; } 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 explode _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, allowPartial) { if (allowPartial === void 0) { allowPartial = false; } 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); let route = meta.route; if (!match && end && allowPartial && !routesMeta[routesMeta.length - 1].route.index) { match = matchPath({ path: meta.relativePath, caseSensitive: meta.caseSensitive, end: false }, remainingPathname); } if (!match) { return null; } Object.assign(matchedParams, match.params); 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/v6/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(/\*$/, "/*"); } // ensure `/` is added at the beginning if the path is absolute const prefix = path.startsWith("/") ? "/" : ""; const stringify = p => p == null ? "" : typeof p === "string" ? p : String(p); const segments = path.split(/\/+/).map((segment, index, array) => { const isLastSegment = index === array.length - 1; // only apply the splat if it's the last segment if (isLastSegment && segment === "*") { const star = "*"; // Apply the splat return stringify(params[star]); } const keyMatch = segment.match(/^:([\w-]+)(\??)$/); if (keyMatch) { const [, key, optional] = keyMatch; let param = params[key]; invariant(optional === "?" || param != null, "Missing \":" + key + "\" param"); return stringify(param); } // Remove any optional markers from optional static segments return segment.replace(/\?$/g, ""); }) // Remove empty segments .filter(segment => !!segment); return prefix + segments.join("/"); } /** * A PathPattern is used to match on some portion of a URL pathname. */ /** * A PathMatch contains info about how a PathPattern matched on a URL pathname. */ /** * Performs pattern matching on a URL pathname and returns information about * the match. * * @see https://reactrouter.com/v6/utils/match-path */ function matchPath(pattern, pathname) { if (typeof pattern === "string") { pattern = { path: pattern, caseSensitive: false, end: true }; } let [matcher, compiledParams] = 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 = compiledParams.reduce((memo, _ref, index) => { let { paramName, isOptional } = _ref; // 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"); } const value = captureGroups[index]; if (isOptional && !value) { memo[paramName] = undefined; } else { memo[paramName] = (value || "").replace(/%2F/g, "/"); } 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 params = []; 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, isOptional) => { params.push({ paramName, isOptional: isOptional != null }); return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)"; }); if (path.endsWith("*")) { params.push({ paramName: "*" }); 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, params]; } function decodePath(value) { try { return value.split("/").map(v => decodeURIComponent(v).replace(/\//g, "%2F")).join("/"); } 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; } } /** * @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) || "/"; } /** * Returns a resolved path object relative to the given pathname. * * @see https://reactrouter.com/v6/utils/resolve-path */ function resolvePath(to, fromPathname) { if (fromPathname === void 0) { fromPathname = "/"; } let { pathname: toPathname, search = "", hash = "" } = typeof to === "string" ? parsePath(to) : to; let pathname = toPathname ? toPathname.startsWith("/") ? toPathname : resolvePathname(toPathname, fromPathname) : fromPathname; return { pathname, search: normalizeSearch(search), hash: normalizeHash(hash) }; } function resolvePathname(relativePath, fromPathname) { let segments = fromPathname.replace(/\/+$/, "").split("/"); let relativeSegments = relativePath.split("/"); relativeSegments.forEach(segment => { if (segment === "..") { // Keep the root "" segment so the pathname starts at / if (segments.length > 1) segments.pop(); } else if (segment !== ".") { segments.push(segment); } }); return segments.length > 1 ? segments.join("/") : "/"; } function getInvalidPathError(char, field, dest, path) { return "Cannot include a '" + char + "' character in a manually specified " + ("`to." + field + "` field [" + JSON.stringify(path) + "]. Please separate it out to the ") + ("`to." + dest + "` field. Alternatively you may provide the full path as ") + "a string in <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); } // Return the array of pathnames for the current route matches - used to // generate the routePathnames input for resolveTo() function getResolveToMatches(matches, v7_relativeSplatPath) { let pathMatches = getPathContributingMatches(matches); // When v7_relativeSplatPath is enabled, use the full pathname for the leaf // match so we include splat values for "." links. See: // https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329 if (v7_relativeSplatPath) { return pathMatches.map((match, idx) => idx === pathMatches.length - 1 ? match.pathname : match.pathnameBase); } return pathMatches.map(match => match.pathnameBase); } /** * @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 (toPathname == null) { from = locationPathname; } else { let routePathnameIndex = routePathnames.length - 1; // With relative="route" (the default), 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". if (!isPathRelative && toPathname.startsWith("..")) { let toSegments = toPathname.split("/"); while (toSegments[0] === "..") { toSegments.shift(); routePathnameIndex -= 1; } to.pathname = toSegments.join("/"); } 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. * * @deprecated The `json` method is deprecated in favor of returning raw objects. * This method will be removed in v7. */ 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 DataWithResponseInit { constructor(data, init) { this.type = "DataWithResponseInit"; this.data = data; this.init = init || null; } } /** * Create "responses" that contain `status`/`headers` without forcing * serialization into an actual `Response` - used by Remix single fetch */ function data(data, init) { return new DataWithResponseInit(data, typeof init === "number" ? { status: init } : init); } 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, _ref2) => { let [key, value] = _ref2; 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, undefined, 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 the promise was resolved/rejected with undefined, we'll throw an error as you // should always resolve with a value or null if (error === undefined && data === undefined) { let undefinedError = new Error("Deferred data for key \"" + key + "\" resolved/rejected with `undefined`, " + "you must resolve/reject with a value or `null`."); Object.defineProperty(promise, "_error", { get: () => undefinedError }); this.emit(false, key); return Promise.reject(undefinedError); } if (data === undefined) { 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, _ref3) => { let [key, value] = _ref3; 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; } /** * @deprecated The `defer` method is deprecated in favor of returning raw * objects. This method will be removed in v7. */ 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