UNPKG

vue-router

Version:

> To see what versions are currently supported, please refer to the [Security Policy](./packages/router/SECURITY.md).

1,401 lines (1,400 loc) 98.4 kB
/*! * vue-router v5.1.0 * (c) 2026 Eduardo San Martin Morote * @license MIT */ import { computed, defineComponent, getCurrentInstance, h, inject, nextTick, onActivated, onDeactivated, onUnmounted, provide, reactive, ref, shallowReactive, shallowRef, unref, watch, watchEffect } from "vue"; import { setupDevtoolsPlugin } from "@vue/devtools-api"; //#region src/utils/env.ts const isBrowser = typeof document !== "undefined"; //#endregion //#region src/utils/index.ts /** * Allows differentiating lazy components from functional components and vue-class-component * @internal * * @param component */ function isRouteComponent(component) { return typeof component === "object" || "displayName" in component || "props" in component || "__vccOpts" in component; } function isESModule(obj) { return obj.__esModule || obj[Symbol.toStringTag] === "Module" || obj.default && isRouteComponent(obj.default); } const assign = Object.assign; function applyToParams(fn, params) { const newParams = {}; for (const key in params) { const value = params[key]; newParams[key] = isArray(value) ? value.map(fn) : fn(value); } return newParams; } const noop = () => {}; /** * Typesafe alternative to Array.isArray * https://github.com/microsoft/TypeScript/pull/48228 * * @internal */ const isArray = Array.isArray; function mergeOptions(defaults, partialOptions) { const options = {}; for (const key in defaults) options[key] = key in partialOptions ? partialOptions[key] : defaults[key]; return options; } //#endregion //#region src/warning.ts function warn(msg) { const args = Array.from(arguments).slice(1); console.warn.apply(console, ["[Vue Router warn]: " + msg].concat(args)); } //#endregion //#region src/encoding.ts /** * Encoding Rules (␣ = Space) * - Path: ␣ " < > # ? { } * - Query: ␣ " < > # & = * - Hash: ␣ " < > ` * * On top of that, the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2) * defines some extra characters to be encoded. Most browsers do not encode them * in encodeURI https://github.com/whatwg/url/issues/369, so it may be safer to * also encode `!'()*`. Leaving un-encoded only ASCII alphanumeric(`a-zA-Z0-9`) * plus `-._~`. This extra safety should be applied to query by patching the * string returned by encodeURIComponent encodeURI also encodes `[\]^`. `\` * should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a `\` * into a `/` if directly typed in. The _backtick_ (`````) should also be * encoded everywhere because some browsers like FF encode it when directly * written while others don't. Safari and IE don't encode ``"<>{}``` in hash. */ const HASH_RE = /#/g; const AMPERSAND_RE = /&/g; const SLASH_RE = /\//g; const EQUAL_RE = /=/g; const IM_RE = /\?/g; const PLUS_RE = /\+/g; /** * NOTE: It's not clear to me if we should encode the + symbol in queries, it * seems to be less flexible than not doing so and I can't find out the legacy * systems requiring this for regular requests like text/html. In the standard, * the encoding of the plus character is only mentioned for * application/x-www-form-urlencoded * (https://url.spec.whatwg.org/#urlencoded-parsing) and most browsers seems lo * leave the plus character as is in queries. To be more flexible, we allow the * plus character on the query, but it can also be manually encoded by the user. * * Resources: * - https://url.spec.whatwg.org/#urlencoded-parsing * - https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20 */ const ENC_BRACKET_OPEN_RE = /%5B/g; const ENC_BRACKET_CLOSE_RE = /%5D/g; const ENC_CARET_RE = /%5E/g; const ENC_BACKTICK_RE = /%60/g; const ENC_CURLY_OPEN_RE = /%7B/g; const ENC_PIPE_RE = /%7C/g; const ENC_CURLY_CLOSE_RE = /%7D/g; const ENC_SPACE_RE = /%20/g; /** * Encode characters that need to be encoded on the path, search and hash * sections of the URL. * * @internal * @param text - string to encode * @returns encoded string */ function commonEncode(text) { return text == null ? "" : encodeURI("" + text).replace(ENC_PIPE_RE, "|").replace(ENC_BRACKET_OPEN_RE, "[").replace(ENC_BRACKET_CLOSE_RE, "]"); } /** * Encode characters that need to be encoded on the hash section of the URL. * * @param text - string to encode * @returns encoded string */ function encodeHash(text) { return commonEncode(text).replace(ENC_CURLY_OPEN_RE, "{").replace(ENC_CURLY_CLOSE_RE, "}").replace(ENC_CARET_RE, "^"); } /** * Encode characters that need to be encoded query values on the query * section of the URL. * * @param text - string to encode * @returns encoded string */ function encodeQueryValue(text) { return commonEncode(text).replace(PLUS_RE, "%2B").replace(ENC_SPACE_RE, "+").replace(HASH_RE, "%23").replace(AMPERSAND_RE, "%26").replace(ENC_BACKTICK_RE, "`").replace(ENC_CURLY_OPEN_RE, "{").replace(ENC_CURLY_CLOSE_RE, "}").replace(ENC_CARET_RE, "^"); } /** * Like `encodeQueryValue` but also encodes the `=` character. * * @param text - string to encode */ function encodeQueryKey(text) { return encodeQueryValue(text).replace(EQUAL_RE, "%3D"); } /** * Encode characters that need to be encoded on the path section of the URL. * * @param text - string to encode * @returns encoded string */ function encodePath(text) { return commonEncode(text).replace(HASH_RE, "%23").replace(IM_RE, "%3F"); } /** * Encode characters that need to be encoded on the path section of the URL as a * param. This function encodes everything {@link encodePath} does plus the * slash (`/`) character. If `text` is `null` or `undefined`, returns an empty * string instead. * * @param text - string to encode * @returns encoded string */ function encodeParam(text) { return encodePath(text).replace(SLASH_RE, "%2F"); } function decode(text) { if (text == null) return null; try { return decodeURIComponent("" + text); } catch { warn(`Error decoding "${text}". Using original value`); } return "" + text; } //#endregion //#region src/location.ts const TRAILING_SLASH_RE = /\/$/; const removeTrailingSlash = (path) => path.replace(TRAILING_SLASH_RE, ""); /** * Transforms a URI into a normalized history location * * @param parseQuery * @param location - URI to normalize * @param currentLocation - current absolute location. Allows resolving relative * paths. Must start with `/`. Defaults to `/` * @returns a normalized history location */ function parseURL(parseQuery, location, currentLocation = "/") { let path, query = {}, searchString = "", hash = ""; const hashPos = location.indexOf("#"); let searchPos = location.indexOf("?"); searchPos = hashPos >= 0 && searchPos > hashPos ? -1 : searchPos; if (searchPos >= 0) { path = location.slice(0, searchPos); searchString = location.slice(searchPos, hashPos > 0 ? hashPos : location.length); query = parseQuery(searchString.slice(1)); } if (hashPos >= 0) { path = path || location.slice(0, hashPos); hash = location.slice(hashPos, location.length); } path = resolveRelativePath(path != null ? path : location, currentLocation); return { fullPath: path + searchString + hash, path, query, hash: decode(hash) }; } /** * Stringifies a URL object * * @param stringifyQuery * @param location */ function stringifyURL(stringifyQuery, location) { const query = location.query ? stringifyQuery(location.query) : ""; return location.path + (query && "?") + query + (location.hash || ""); } /** * Strips off the base from the beginning of a location.pathname in a non-case-sensitive way. * * @param pathname - location.pathname * @param base - base to strip off */ function stripBase(pathname, base) { if (!base || !pathname.toLowerCase().startsWith(base.toLowerCase())) return pathname; return pathname.slice(base.length) || "/"; } /** * Checks if two RouteLocation are equal. This means that both locations are * pointing towards the same {@link RouteRecord} and that all `params`, `query` * parameters and `hash` are the same * * @param stringifyQuery - A function that takes a query object of type LocationQueryRaw and returns a string representation of it. * @param a - first {@link RouteLocation} * @param b - second {@link RouteLocation} */ function isSameRouteLocation(stringifyQuery, a, b) { const aLastIndex = a.matched.length - 1; const bLastIndex = b.matched.length - 1; return aLastIndex > -1 && aLastIndex === bLastIndex && isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) && isSameRouteLocationParams(a.params, b.params) && stringifyQuery(a.query) === stringifyQuery(b.query) && a.hash === b.hash; } /** * Check if two `RouteRecords` are equal. Takes into account aliases: they are * considered equal to the `RouteRecord` they are aliasing. * * @param a - first {@link RouteRecord} * @param b - second {@link RouteRecord} */ function isSameRouteRecord(a, b) { return (a.aliasOf || a) === (b.aliasOf || b); } function isSameRouteLocationParams(a, b) { if (Object.keys(a).length !== Object.keys(b).length) return false; for (var key in a) if (!isSameRouteLocationParamsValue(a[key], b[key])) return false; return true; } function isSameRouteLocationParamsValue(a, b) { return isArray(a) ? isEquivalentArray(a, b) : isArray(b) ? isEquivalentArray(b, a) : (a && a.valueOf()) === (b && b.valueOf()); } /** * Check if two arrays are the same or if an array with one single entry is the * same as another primitive value. Used to check query and parameters * * @param a - array of values * @param b - array of values or a single value */ function isEquivalentArray(a, b) { return isArray(b) ? a.length === b.length && a.every((value, i) => value === b[i]) : a.length === 1 && a[0] === b; } /** * Resolves a relative path that starts with `.`. * * @param to - path location we are resolving * @param from - currentLocation.path, should start with `/` */ function resolveRelativePath(to, from) { if (to.startsWith("/")) return to; if (!from.startsWith("/")) { warn(`Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`); return to; } if (!to) return from; const fromSegments = from.split("/"); const toSegments = to.split("/"); const lastToSegment = toSegments[toSegments.length - 1]; if (lastToSegment === ".." || lastToSegment === ".") toSegments.push(""); let position = fromSegments.length - 1; let toPosition; let segment; for (toPosition = 0; toPosition < toSegments.length; toPosition++) { segment = toSegments[toPosition]; if (segment === ".") continue; if (segment === "..") { if (position > 1) position--; } else break; } return fromSegments.slice(0, position).join("/") + "/" + toSegments.slice(toPosition).join("/"); } /** * Initial route location where the router is. Can be used in navigation guards * to differentiate the initial navigation. * * @example * ```js * import { START_LOCATION } from 'vue-router' * * router.beforeEach((to, from) => { * if (from === START_LOCATION) { * // initial navigation * } * }) * ``` */ const START_LOCATION_NORMALIZED = { path: "/", name: void 0, params: {}, query: {}, hash: "", fullPath: "/", matched: [], meta: {}, redirectedFrom: void 0 }; //#endregion //#region src/history/common.ts /** * Normalizes a base by removing any trailing slash and reading the base tag if * present. * * @param base - base to normalize */ function normalizeBase(base) { if (!base) if (isBrowser) { const baseEl = document.querySelector("base"); base = baseEl && baseEl.getAttribute("href") || "/"; base = base.replace(/^\w+:\/\/[^/]+/, ""); } else base = "/"; if (base[0] !== "/" && base[0] !== "#") base = "/" + base; return removeTrailingSlash(base); } const BEFORE_HASH_RE = /^[^#]+#/; function createHref(base, location) { return base.replace(BEFORE_HASH_RE, "#") + location; } //#endregion //#region src/scrollBehavior.ts function getElementPosition(el, offset) { const docRect = document.documentElement.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); return { behavior: offset.behavior, left: elRect.left - docRect.left - (offset.left || 0), top: elRect.top - docRect.top - (offset.top || 0) }; } const computeScrollPosition = () => ({ left: window.scrollX, top: window.scrollY }); function scrollToPosition(position) { let scrollToOptions; if ("el" in position) { const positionEl = position.el; const isIdSelector = typeof positionEl === "string" && positionEl.startsWith("#"); /** * `id`s can accept pretty much any characters, including CSS combinators * like `>` or `~`. It's still possible to retrieve elements using * `document.getElementById('~')` but it needs to be escaped when using * `document.querySelector('#\\~')` for it to be valid. The only * requirements for `id`s are them to be unique on the page and to not be * empty (`id=""`). Because of that, when passing an id selector, it should * be properly escaped for it to work with `querySelector`. We could check * for the id selector to be simple (no CSS combinators `+ >~`) but that * would make things inconsistent since they are valid characters for an * `id` but would need to be escaped when using `querySelector`, breaking * their usage and ending up in no selector returned. Selectors need to be * escaped: * * - `#1-thing` becomes `#\31 -thing` * - `#with~symbols` becomes `#with\\~symbols` * * - More information about the topic can be found at * https://mathiasbynens.be/notes/html5-id-class. * - Practical example: https://mathiasbynens.be/demo/html5-id */ if (typeof position.el === "string") { if (!isIdSelector || !document.getElementById(position.el.slice(1))) try { const foundEl = document.querySelector(position.el); if (isIdSelector && foundEl) { warn(`The selector "${position.el}" should be passed as "el: document.querySelector('${position.el}')" because it starts with "#".`); return; } } catch { warn(`The selector "${position.el}" is invalid. If you are using an id selector, make sure to escape it. You can find more information about escaping characters in selectors at https://mathiasbynens.be/notes/css-escapes or use CSS.escape (https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape).`); return; } } const el = typeof positionEl === "string" ? isIdSelector ? document.getElementById(positionEl.slice(1)) : document.querySelector(positionEl) : positionEl; if (!el) { warn(`Couldn't find element using selector "${position.el}" returned by scrollBehavior.`); return; } scrollToOptions = getElementPosition(el, position); } else scrollToOptions = position; if ("scrollBehavior" in document.documentElement.style) window.scrollTo(scrollToOptions); else window.scrollTo(scrollToOptions.left != null ? scrollToOptions.left : window.scrollX, scrollToOptions.top != null ? scrollToOptions.top : window.scrollY); } function getScrollKey(path, delta) { return (history.state ? history.state.position - delta : -1) + path; } const scrollPositions = /* @__PURE__ */ new Map(); function saveScrollPosition(key, scrollPosition) { scrollPositions.set(key, scrollPosition); } function getSavedScrollPosition(key) { const scroll = scrollPositions.get(key); scrollPositions.delete(key); return scroll; } /** * ScrollBehavior instance used by the router to compute and restore the scroll * position when navigating. */ //#endregion //#region src/history/html5.ts let createBaseLocation = () => location.protocol + "//" + location.host; /** * Creates a normalized history location from a window.location object * @param base - The base path * @param location - The window.location object */ function createCurrentLocation(base, location) { const { pathname, search, hash } = location; const hashPos = base.indexOf("#"); if (hashPos > -1) { let slicePos = hash.includes(base.slice(hashPos)) ? base.slice(hashPos).length : 1; let pathFromHash = hash.slice(slicePos); if (pathFromHash[0] !== "/") pathFromHash = "/" + pathFromHash; return stripBase(pathFromHash, ""); } return stripBase(pathname, base) + search + hash; } function useHistoryListeners(base, historyState, currentLocation, replace) { let listeners = []; let teardowns = []; let pauseState = null; const popStateHandler = ({ state }) => { const to = createCurrentLocation(base, location); const from = currentLocation.value; const fromState = historyState.value; let delta = 0; if (state) { currentLocation.value = to; historyState.value = state; if (pauseState && pauseState === from) { pauseState = null; return; } delta = fromState ? state.position - fromState.position : 0; } else replace(to); listeners.forEach((listener) => { listener(currentLocation.value, from, { delta, type: "pop", direction: delta ? delta > 0 ? "forward" : "back" : "" }); }); }; function pauseListeners() { pauseState = currentLocation.value; } function listen(callback) { listeners.push(callback); const teardown = () => { const index = listeners.indexOf(callback); if (index > -1) listeners.splice(index, 1); }; teardowns.push(teardown); return teardown; } function beforeUnloadListener() { if (document.visibilityState === "hidden") { const { history } = window; if (!history.state) return; history.replaceState(assign({}, history.state, { scroll: computeScrollPosition() }), ""); } } function destroy() { for (const teardown of teardowns) teardown(); teardowns = []; window.removeEventListener("popstate", popStateHandler); window.removeEventListener("pagehide", beforeUnloadListener); document.removeEventListener("visibilitychange", beforeUnloadListener); } window.addEventListener("popstate", popStateHandler); window.addEventListener("pagehide", beforeUnloadListener); document.addEventListener("visibilitychange", beforeUnloadListener); return { pauseListeners, listen, destroy }; } /** * Creates a state object */ function buildState(back, current, forward, replaced = false, computeScroll = false) { return { back, current, forward, replaced, position: window.history.length, scroll: computeScroll ? computeScrollPosition() : null }; } function useHistoryStateNavigation(base) { const { history, location } = window; const currentLocation = { value: createCurrentLocation(base, location) }; const historyState = { value: history.state }; if (!historyState.value) changeLocation(currentLocation.value, { back: null, current: currentLocation.value, forward: null, position: history.length - 1, replaced: true, scroll: null }, true); function changeLocation(to, state, replace) { /** * if a base tag is provided, and we are on a normal domain, we have to * respect the provided `base` attribute because pushState() will use it and * potentially erase anything before the `#` like at * https://github.com/vuejs/router/issues/685 where a base of * `/folder/#` but a base of `/` would erase the `/folder/` section. If * there is no host, the `<base>` tag makes no sense and if there isn't a * base tag we can just use everything after the `#`. */ const hashIndex = base.indexOf("#"); const url = hashIndex > -1 ? (location.host && document.querySelector("base") ? base : base.slice(hashIndex)) + to : createBaseLocation() + base + to; try { history[replace ? "replaceState" : "pushState"](state, "", url); historyState.value = state; } catch (err) { warn("Error with push/replace State", err); location[replace ? "replace" : "assign"](url); } } function replace(to, data) { changeLocation(to, assign({}, history.state, buildState(historyState.value.back, to, historyState.value.forward, true), data, { position: historyState.value.position }), true); currentLocation.value = to; } function push(to, data) { const currentState = assign({}, historyState.value, history.state, { forward: to, scroll: computeScrollPosition() }); if (!history.state) warn("history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\nhistory.replaceState(history.state, '', url)\n\nYou can find more information at https://router.vuejs.org/guide/migration/#Usage-of-history-state"); changeLocation(currentState.current, currentState, true); changeLocation(to, assign({}, buildState(currentLocation.value, to, null), { position: currentState.position + 1 }, data), false); currentLocation.value = to; } return { location: currentLocation, state: historyState, push, replace }; } /** * Creates an HTML5 history. Most common history for single page applications. * * @param base - */ function createWebHistory(base) { base = normalizeBase(base); const historyNavigation = useHistoryStateNavigation(base); const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace); function go(delta, triggerListeners = true) { if (!triggerListeners) historyListeners.pauseListeners(); history.go(delta); } const routerHistory = assign({ location: "", base, go, createHref: createHref.bind(null, base) }, historyNavigation, historyListeners); Object.defineProperty(routerHistory, "location", { enumerable: true, get: () => historyNavigation.location.value }); Object.defineProperty(routerHistory, "state", { enumerable: true, get: () => historyNavigation.state.value }); return routerHistory; } //#endregion //#region src/history/hash.ts /** * Creates a hash history. Useful for web applications with no host (e.g. `file://`) or when configuring a server to * handle any URL is not possible. * * @param base - optional base to provide. Defaults to `location.pathname + location.search` If there is a `<base>` tag * in the `head`, its value will be ignored in favor of this parameter **but note it affects all the history.pushState() * calls**, meaning that if you use a `<base>` tag, it's `href` value **has to match this parameter** (ignoring anything * after the `#`). * * @example * ```js * // at https://example.com/folder * createWebHashHistory() // gives a url of `https://example.com/folder#` * createWebHashHistory('/folder/') // gives a url of `https://example.com/folder/#` * // if the `#` is provided in the base, it won't be added by `createWebHashHistory` * createWebHashHistory('/folder/#/app/') // gives a url of `https://example.com/folder/#/app/` * // you should avoid doing this because it changes the original url and breaks copying urls * createWebHashHistory('/other-folder/') // gives a url of `https://example.com/other-folder/#` * * // at file:///usr/etc/folder/index.html * // for locations with no `host`, the base is ignored * createWebHashHistory('/iAmIgnored') // gives a url of `file:///usr/etc/folder/index.html#` * ``` */ function createWebHashHistory(base) { base = location.host ? base || location.pathname + location.search : ""; if (!base.includes("#")) base += "#"; if (!base.endsWith("#/") && !base.endsWith("#")) warn(`A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, "#")}".`); return createWebHistory(base); } //#endregion //#region src/history/memory.ts /** * Creates an in-memory based history. The main purpose of this history is to handle SSR. It starts in a special location that is nowhere. * It's up to the user to replace that location with the starter location by either calling `router.push` or `router.replace`. * * @param base - Base applied to all urls, defaults to '/' * @returns a history object that can be passed to the router constructor */ function createMemoryHistory(base = "") { let listeners = []; let queue = [["", {}]]; let position = 0; base = normalizeBase(base); function setLocation(location, state = {}) { position++; if (position !== queue.length) queue.splice(position); queue.push([location, state]); } function triggerListeners(to, from, { direction, delta }) { const info = { direction, delta, type: "pop" }; for (const callback of listeners) callback(to, from, info); } const routerHistory = { location: "", state: {}, base, createHref: createHref.bind(null, base), replace(to, state) { queue.splice(position--, 1); setLocation(to, state); }, push(to, state) { setLocation(to, state); }, listen(callback) { listeners.push(callback); return () => { const index = listeners.indexOf(callback); if (index > -1) listeners.splice(index, 1); }; }, destroy() { listeners = []; queue = [["", {}]]; position = 0; }, go(delta, shouldTrigger = true) { const from = this.location; const direction = delta < 0 ? "back" : "forward"; position = Math.max(0, Math.min(position + delta, queue.length - 1)); if (shouldTrigger) triggerListeners(this.location, from, { direction, delta }); } }; Object.defineProperty(routerHistory, "location", { enumerable: true, get: () => queue[position][0] }); Object.defineProperty(routerHistory, "state", { enumerable: true, get: () => queue[position][1] }); return routerHistory; } //#endregion //#region src/types/typeGuards.ts function isRouteLocation(route) { return typeof route === "string" || route && typeof route === "object"; } function isRouteName(name) { return typeof name === "string" || typeof name === "symbol"; } //#endregion //#region src/errors.ts const NavigationFailureSymbol = Symbol("navigation failure"); /** * Enumeration with all possible types for navigation failures. Can be passed to * {@link isNavigationFailure} to check for specific failures. */ let NavigationFailureType = /* @__PURE__ */ function(NavigationFailureType) { /** * An aborted navigation is a navigation that failed because a navigation * guard returned `false` or called `next(false)` */ NavigationFailureType[NavigationFailureType["aborted"] = 4] = "aborted"; /** * A cancelled navigation is a navigation that failed because a more recent * navigation finished started (not necessarily finished). */ NavigationFailureType[NavigationFailureType["cancelled"] = 8] = "cancelled"; /** * A duplicated navigation is a navigation that failed because it was * initiated while already being at the exact same location. */ NavigationFailureType[NavigationFailureType["duplicated"] = 16] = "duplicated"; return NavigationFailureType; }({}); const ErrorTypeMessages = { [1]({ location, currentLocation }) { return `No match for\n ${JSON.stringify(location)}${currentLocation ? "\nwhile being at\n" + JSON.stringify(currentLocation) : ""}`; }, [2]({ from, to }) { return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`; }, [4]({ from, to }) { return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`; }, [8]({ from, to }) { return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`; }, [16]({ from, to: _to }) { return `Avoided redundant navigation to current location: "${from.fullPath}".`; } }; /** * Creates a typed NavigationFailure object. * @internal * @param type - NavigationFailureType * @param params - { from, to } */ function createRouterError(type, params) { return assign(new Error(ErrorTypeMessages[type](params)), { type, [NavigationFailureSymbol]: true }, params); } function isNavigationFailure(error, type) { return error instanceof Error && NavigationFailureSymbol in error && (type == null || !!(error.type & type)); } const propertiesToLog = [ "params", "query", "hash" ]; function stringifyRoute(to) { if (typeof to === "string") return to; if (to.path != null) return to.path; const location = {}; for (const key of propertiesToLog) if (key in to) location[key] = to[key]; return JSON.stringify(location, null, 2); } //#endregion //#region src/matcher/pathTokenizer.ts const ROOT_TOKEN = { type: 0, value: "" }; const VALID_PARAM_RE = /[a-zA-Z0-9_]/; function tokenizePath(path) { if (!path) return [[]]; if (path === "/") return [[ROOT_TOKEN]]; if (!path.startsWith("/")) throw new Error(`Route paths should start with a "/": "${path}" should be "/${path}".`); function crash(message) { throw new Error(`ERR (${state})/"${buffer}": ${message}`); } let state = 0; let previousState = state; const tokens = []; let segment; function finalizeSegment() { if (segment) tokens.push(segment); segment = []; } let i = 0; let char; let buffer = ""; let customRe = ""; function consumeBuffer() { if (!buffer) return; if (state === 0) segment.push({ type: 0, value: buffer }); else if (state === 1 || state === 2 || state === 3) { if (segment.length > 1 && (char === "*" || char === "+")) crash(`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`); segment.push({ type: 1, value: buffer, regexp: customRe, repeatable: char === "*" || char === "+", optional: char === "*" || char === "?" }); } else crash("Invalid state to consume buffer"); buffer = ""; } function addCharToBuffer() { buffer += char; } while (i < path.length) { char = path[i++]; switch (state) { case 0: if (char === "\\") { previousState = state; state = 4; } else if (char === "/") { if (buffer) consumeBuffer(); finalizeSegment(); } else if (char === ":") { consumeBuffer(); state = 1; } else addCharToBuffer(); break; case 4: addCharToBuffer(); state = previousState; break; case 1: if (char === "(") state = 2; else if (VALID_PARAM_RE.test(char)) addCharToBuffer(); else { consumeBuffer(); state = 0; if (char !== "*" && char !== "?" && char !== "+") i--; } break; case 2: if (char === ")") if (customRe[customRe.length - 1] == "\\") customRe = customRe.slice(0, -1) + char; else state = 3; else customRe += char; break; case 3: consumeBuffer(); state = 0; if (char !== "*" && char !== "?" && char !== "+") i--; customRe = ""; break; default: crash("Unknown state"); break; } } if (state === 2) crash(`Unfinished custom RegExp for param "${buffer}"`); consumeBuffer(); finalizeSegment(); return tokens; } //#endregion //#region src/matcher/pathParserRanker.ts const BASE_PARAM_PATTERN = "[^/]+?"; const BASE_PATH_PARSER_OPTIONS = { sensitive: false, strict: false, start: true, end: true }; const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g; /** * Creates a path parser from an array of Segments (a segment is an array of Tokens) * * @param segments - array of segments returned by tokenizePath * @param extraOptions - optional options for the regexp * @returns a PathParser */ function tokensToParser(segments, extraOptions) { const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions); const score = []; let pattern = options.start ? "^" : ""; const keys = []; for (const segment of segments) { const segmentScores = segment.length ? [] : [90]; if (options.strict && !segment.length) pattern += "/"; for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) { const token = segment[tokenIndex]; let subSegmentScore = 40 + (options.sensitive ? .25 : 0); if (token.type === 0) { if (!tokenIndex) pattern += "/"; pattern += token.value.replace(REGEX_CHARS_RE, "\\$&"); subSegmentScore += 40; } else if (token.type === 1) { const { value, repeatable, optional, regexp } = token; keys.push({ name: value, repeatable, optional }); const re = regexp ? regexp : BASE_PARAM_PATTERN; if (re !== BASE_PARAM_PATTERN) { subSegmentScore += 10; try { new RegExp(`(${re})`); } catch (err) { throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` + err.message); } } let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`; if (!tokenIndex) subPattern = optional && segment.length < 2 ? `(?:/${subPattern})` : "/" + subPattern; if (optional) subPattern += "?"; pattern += subPattern; subSegmentScore += 20; if (optional) subSegmentScore += -8; if (repeatable) subSegmentScore += -20; if (re === ".*") subSegmentScore += -50; } segmentScores.push(subSegmentScore); } score.push(segmentScores); } if (options.strict && options.end) { const i = score.length - 1; score[i][score[i].length - 1] += .7000000000000001; } if (!options.strict) pattern += "/?"; if (options.end) pattern += "$"; else if (options.strict && !pattern.endsWith("/")) pattern += "(?:/|$)"; const re = new RegExp(pattern, options.sensitive ? "" : "i"); function parse(path) { const match = path.match(re); const params = {}; if (!match) return null; for (let i = 1; i < match.length; i++) { const value = match[i] || ""; const key = keys[i - 1]; params[key.name] = value && key.repeatable ? value.split("/") : value; } return params; } function stringify(params) { let path = ""; let avoidDuplicatedSlash = false; for (const segment of segments) { if (!avoidDuplicatedSlash || !path.endsWith("/")) path += "/"; avoidDuplicatedSlash = false; for (const token of segment) if (token.type === 0) path += token.value; else if (token.type === 1) { const { value, repeatable, optional } = token; const param = value in params ? params[value] : ""; if (isArray(param) && !repeatable) throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`); const text = isArray(param) ? param.join("/") : param; if (!text) if (optional) { if (segment.length < 2) if (path.endsWith("/")) path = path.slice(0, -1); else avoidDuplicatedSlash = true; } else throw new Error(`Missing required param "${value}"`); path += text; } } return path || "/"; } return { re, score, keys, parse, stringify }; } /** * Compares an array of numbers as used in PathParser.score and returns a * number. This function can be used to `sort` an array * * @param a - first array of numbers * @param b - second array of numbers * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b * should be sorted first */ function compareScoreArray(a, b) { let i = 0; while (i < a.length && i < b.length) { const diff = b[i] - a[i]; if (diff) return diff; i++; } if (a.length < b.length) return a.length === 1 && a[0] === 80 ? -1 : 1; else if (a.length > b.length) return b.length === 1 && b[0] === 80 ? 1 : -1; return 0; } /** * Compare function that can be used with `sort` to sort an array of PathParser * * @param a - first PathParser * @param b - second PathParser * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b */ function comparePathParserScore(a, b) { let i = 0; const aScore = a.score; const bScore = b.score; while (i < aScore.length && i < bScore.length) { const comp = compareScoreArray(aScore[i], bScore[i]); if (comp) return comp; i++; } if (Math.abs(bScore.length - aScore.length) === 1) { if (isLastScoreNegative(aScore)) return 1; if (isLastScoreNegative(bScore)) return -1; } return bScore.length - aScore.length; } /** * This allows detecting splats at the end of a path: /home/:id(.*)* * * @param score - score to check * @returns true if the last entry is negative */ function isLastScoreNegative(score) { const last = score[score.length - 1]; return score.length > 0 && last[last.length - 1] < 0; } const PATH_PARSER_OPTIONS_DEFAULTS = { strict: false, end: true, sensitive: false }; //#endregion //#region src/matcher/pathMatcher.ts function createRouteRecordMatcher(record, parent, options) { const parser = tokensToParser(tokenizePath(record.path), options); { const existingKeys = /* @__PURE__ */ new Set(); for (const key of parser.keys) { if (existingKeys.has(key.name)) warn(`Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`); existingKeys.add(key.name); } } const matcher = assign(parser, { record, parent, children: [], alias: [] }); if (parent) { if (!matcher.record.aliasOf === !parent.record.aliasOf) parent.children.push(matcher); } return matcher; } //#endregion //#region src/matcher/index.ts /** * Creates a Router Matcher. * * @internal * @param routes - array of initial routes * @param globalOptions - global route options */ function createRouterMatcher(routes, globalOptions) { const matchers = []; const matcherMap = /* @__PURE__ */ new Map(); globalOptions = mergeOptions(PATH_PARSER_OPTIONS_DEFAULTS, globalOptions); function getRecordMatcher(name) { return matcherMap.get(name); } function addRoute(record, parent, originalRecord) { const isRootAdd = !originalRecord; const mainNormalizedRecord = normalizeRouteRecord(record); checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent); mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record; const options = mergeOptions(globalOptions, record); const normalizedRecords = [mainNormalizedRecord]; if ("alias" in record) { const aliases = typeof record.alias === "string" ? [record.alias] : record.alias; for (const alias of aliases) normalizedRecords.push(normalizeRouteRecord(assign({}, mainNormalizedRecord, { components: originalRecord ? originalRecord.record.components : mainNormalizedRecord.components, path: alias, aliasOf: originalRecord ? originalRecord.record : mainNormalizedRecord }))); } let matcher; let originalMatcher; for (const normalizedRecord of normalizedRecords) { const { path } = normalizedRecord; if (parent && path[0] !== "/") { const parentPath = parent.record.path; const connectingSlash = parentPath[parentPath.length - 1] === "/" ? "" : "/"; normalizedRecord.path = parent.record.path + (path && connectingSlash + path); } if (normalizedRecord.path === "*") throw new Error("Catch all routes (\"*\") must now be defined using a param with a custom regexp.\nSee more at https://router.vuejs.org/guide/migration/#Removed-star-or-catch-all-routes."); matcher = createRouteRecordMatcher(normalizedRecord, parent, options); if (parent && path[0] === "/") checkMissingParamsInAbsolutePath(matcher, parent); if (originalRecord) { originalRecord.alias.push(matcher); checkSameParams(originalRecord, matcher); } else { originalMatcher = originalMatcher || matcher; if (originalMatcher !== matcher) originalMatcher.alias.push(matcher); if (isRootAdd && record.name && !isAliasRecord(matcher)) { checkSameNameAsAncestor(record, parent); removeRoute(record.name); } } if (isMatchable(matcher)) insertMatcher(matcher); if (mainNormalizedRecord.children) { const children = mainNormalizedRecord.children; for (let i = 0; i < children.length; i++) addRoute(children[i], matcher, originalRecord && originalRecord.children[i]); } originalRecord = originalRecord || matcher; } return originalMatcher ? () => { removeRoute(originalMatcher); } : noop; } function removeRoute(matcherRef) { if (isRouteName(matcherRef)) { const matcher = matcherMap.get(matcherRef); if (matcher) { matcherMap.delete(matcherRef); matchers.splice(matchers.indexOf(matcher), 1); matcher.children.forEach(removeRoute); matcher.alias.forEach(removeRoute); } } else { const index = matchers.indexOf(matcherRef); if (index > -1) { matchers.splice(index, 1); if (matcherRef.record.name) matcherMap.delete(matcherRef.record.name); matcherRef.children.forEach(removeRoute); matcherRef.alias.forEach(removeRoute); } } } function getRoutes() { return matchers; } function insertMatcher(matcher) { const index = findInsertionIndex(matcher, matchers); matchers.splice(index, 0, matcher); if (matcher.record.name && !isAliasRecord(matcher)) matcherMap.set(matcher.record.name, matcher); } function resolve(location, currentLocation) { let matcher; let params = {}; let path; let name; if ("name" in location && location.name) { matcher = matcherMap.get(location.name); if (!matcher) throw createRouterError(1, { location }); { const invalidParams = Object.keys(location.params || {}).filter((paramName) => !matcher.keys.find((k) => k.name === paramName)); if (invalidParams.length) { const isInherited = !matcher.keys.length && invalidParams.some((name) => name in currentLocation.params); warn(`Discarded invalid param(s) "${invalidParams.join("\", \"")}" when navigating.` + (isInherited ? ` If you are using a catch-all route with a named redirect, pass an empty \`params\` object: \`redirect: { name: '...', params: {} }\`.` : "") + ` See https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22 for more details.`); } } name = matcher.record.name; params = assign(pickParams(currentLocation.params, matcher.keys.filter((k) => !k.optional).concat(matcher.parent ? matcher.parent.keys.filter((k) => k.optional) : []).map((k) => k.name)), location.params && pickParams(location.params, matcher.keys.map((k) => k.name))); path = matcher.stringify(params); } else if (location.path != null) { path = location.path; if (!path.startsWith("/")) warn(`The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://github.com/vuejs/router/issues/new/choose.`); matcher = matchers.find((m) => m.re.test(path)); if (matcher) { params = matcher.parse(path); name = matcher.record.name; matcher.keys.forEach((key) => { if (key.optional && !params[key.name]) delete params[key.name]; }); } } else { matcher = currentLocation.name ? matcherMap.get(currentLocation.name) : matchers.find((m) => m.re.test(currentLocation.path)); if (!matcher) throw createRouterError(1, { location, currentLocation }); name = matcher.record.name; params = assign({}, currentLocation.params, location.params); path = matcher.stringify(params); } const matched = []; let parentMatcher = matcher; while (parentMatcher) { matched.unshift(parentMatcher.record); parentMatcher = parentMatcher.parent; } return { name, path, params, matched, meta: mergeMetaFields(matched) }; } routes.forEach((route) => addRoute(route)); function clearRoutes() { matchers.length = 0; matcherMap.clear(); } return { addRoute, resolve, removeRoute, clearRoutes, getRoutes, getRecordMatcher }; } /** * Picks an object param to contain only specified keys. * * @param params - params object to pick from * @param keys - keys to pick */ function pickParams(params, keys) { const newParams = {}; for (const key of keys) if (key in params) newParams[key] = params[key]; return newParams; } /** * Normalizes a RouteRecordRaw. Creates a copy * * @param record * @returns the normalized version */ function normalizeRouteRecord(record) { const normalized = { path: record.path, redirect: record.redirect, name: record.name, meta: record.meta || {}, aliasOf: record.aliasOf, beforeEnter: record.beforeEnter, props: normalizeRecordProps(record), children: record.children || [], instances: {}, leaveGuards: /* @__PURE__ */ new Set(), updateGuards: /* @__PURE__ */ new Set(), enterCallbacks: {}, components: "components" in record ? record.components || null : record.component && { default: record.component } }; Object.defineProperty(normalized, "mods", { value: {} }); return normalized; } /** * Normalize the optional `props` in a record to always be an object similar to * components. Also accept a boolean for components. * @param record */ function normalizeRecordProps(record) { const propsObject = {}; const props = record.props || false; if ("component" in record) propsObject.default = props; else for (const name in record.components) propsObject[name] = typeof props === "object" ? props[name] : props; return propsObject; } /** * Checks if a record or any of its parent is an alias * @param record */ function isAliasRecord(record) { while (record) { if (record.record.aliasOf) return true; record = record.parent; } return false; } /** * Merge meta fields of an array of records * * @param matched - array of matched records */ function mergeMetaFields(matched) { return matched.reduce((meta, record) => assign(meta, record.meta), {}); } function isSameParam(a, b) { return a.name === b.name && a.optional === b.optional && a.repeatable === b.repeatable; } /** * Check if a path and its alias have the same required params * * @param a - original record * @param b - alias record */ function checkSameParams(a, b) { for (const key of a.keys) if (!key.optional && !b.keys.find(isSameParam.bind(null, key))) return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" must have the exact same param named "${key.name}"`); for (const key of b.keys) if (!key.optional && !a.keys.find(isSameParam.bind(null, key))) return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" must have the exact same param named "${key.name}"`); } /** * A route with a name and a child with an empty path without a name should warn when adding the route * * @param mainNormalizedRecord - RouteRecordNormalized * @param parent - RouteRecordMatcher */ function checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent) { if (parent && parent.record.name && !mainNormalizedRecord.name && !mainNormalizedRecord.path && mainNormalizedRecord.children.length === 0) warn(`The route named "${String(parent.record.name)}" has a child without a name, an empty path, and no children. This is probably a mistake: using that name won't render the empty path child so you probably want to move the name to the child instead. If this is intentional, add a name to the child route to silence the warning.`); } function checkSameNameAsAncestor(record, parent) { for (let ancestor = parent; ancestor; ancestor = ancestor.parent) if (ancestor.record.name === record.name) throw new Error(`A route named "${String(record.name)}" has been added as a ${parent === ancestor ? "child" : "descendant"} of a route with the same name. Route names must be unique and a nested route cannot use the same name as an ancestor.`); } function checkMissingParamsInAbsolutePath(record, parent) { for (const key of parent.keys) if (!record.keys.find(isSameParam.bind(null, key))) return warn(`Absolute path "${record.record.path}" must have the exact same param named "${key.name}" as its parent "${parent.record.path}".`); } /** * Performs a binary search to find the correct insertion index for a new matcher. * * Matchers are primarily sorted by their score. If scores are tied then we also consider parent/child relationships, * with descendants coming before ancestors. If there's still a tie, new routes are inserted after existing routes. * * @param matcher - new matcher to be inserted * @param matchers - existing matchers */ function findInsertionIndex(matcher, matchers) { let lower = 0; let upper = matchers.length; while (lower !== upper) { const mid = lower + upper >> 1; if (comparePathParserScore(matcher, matchers[mid]) < 0) upper = mid; else lower = mid + 1; } const insertionAncestor = getInsertionAncestor(matcher); if (insertionAncestor) { upper = matchers.lastIndexOf(insertionAncestor, upper - 1); if (upper < 0) warn(`Finding ancestor route "${insertionAncestor.record.path}" failed for "${matcher.record.path}"`); } return upper; } function getInsertionAncestor(matcher) { let ancestor = matcher; while (ancestor = ancestor.parent) if (isMatchable(ancestor) && comparePathParserScore(matcher, ancestor) === 0) return ancestor; } /** * Checks if a matcher can be reachable. This means if it's possible to reach it as a route. For example, routes without * a component, or name, or redirect, are just used to group other routes. * @param matcher * @param matcher.record record of the matcher * @returns */ function isMatchable({ record }) { return !!(record.name || record.components && Object.keys(record.components).length || record.redirect); } //#endregion //#region src/query.ts /** * Transforms a queryString into a {@link LocationQuery} object. Accept both, a * version with the leading `?` and without Should work as URLSearchParams * @internal * * @param search - search string to parse * @returns a query object */ function parseQuery(search) { const query = {}; if (search === "" || search === "?") return query; const searchParams = (search[0] === "?" ? search.slice(1) : search).split("&"); for (let i = 0; i < searchParams.length; ++i) { const searchParam = searchParams[i].replace(PLUS_RE, " "); const eqPos = searchParam.indexOf("="); const key = decode(eqPos < 0 ? searchParam : searchParam.slice(0, eqPos)); const value = eqPos < 0 ? null : decode(searchParam.slice(eqPos + 1)); if (key in query) { let currentValue = query[key]; if (!isArray(currentValue)) currentValue = query[key] = [currentValue]; currentValue.push(value); } else query[key] = value; } return query; } /** * Stringifies a {@link LocationQueryRaw} object. Like `URLSearchParams`, it * doesn't prepend a `?` * * @internal * * @param query - query object to stringify * @returns string version of the query without the leading `?` */ function stringifyQuery(query) { let search = ""; for (let key in query) { const value = query[key]; key = encodeQueryKey(key); if (value == null) { if (value !== void 0) search += (search.length ? "&" : "") + key; continue; } (isArray(value) ? value.map((v) => v && encodeQueryValue(v)) : [value && encodeQueryValue(value)]).forEach((value) => { if (value !== void 0) { search += (search.length ? "&" : "") + key; if (value != null) search += "=" + value; } }