/*! * vue-router v4.4.5 * (c) 2024 Eduardo San Martin Morote * @license MIT */ 'use strict'; var vue = require('vue'); const isBrowser = typeof document !== 'undefined'; /** * 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' || // support CF with dynamic imports that do not // add the Module string tag (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 */ const isArray = Array.isArray; /** * 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 EXTRA_RESERVED_RE = /[!'()*]/g // const encodeReservedReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16) const HASH_RE = /#/g; // %23 const AMPERSAND_RE = /&/g; // %26 const SLASH_RE = /\//g; // %2F const EQUAL_RE = /=/g; // %3D const IM_RE = /\?/g; // %3F const PLUS_RE = /\+/g; // %2B /** * 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 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) // Encode the space as +, encode the + to differentiate it from the space .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 text == null ? '' : encodePath(text).replace(SLASH_RE, '%2F'); } /** * Decode text using `decodeURIComponent`. Returns the original text if it * fails. * * @param text - string to decode * @returns decoded string */ function decode(text) { try { return decodeURIComponent('' + text); } catch (err) { } return '' + text; } 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 = ''; // Could use URL and URLSearchParams but IE 11 doesn't support it // TODO: move to new URL() const hashPos = location.indexOf('#'); let searchPos = location.indexOf('?'); // the hash appears before the search, so it's not part of the search string if (hashPos < searchPos && hashPos >= 0) { searchPos = -1; } if (searchPos > -1) { path = location.slice(0, searchPos); searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length); query = parseQuery(searchString); } if (hashPos > -1) { path = path || location.slice(0, hashPos); // keep the # character hash = location.slice(hashPos, location.length); } // no search and no query path = resolveRelativePath(path != null ? path : location, currentLocation); // empty path means a relative query or hash `?foo=f`, `#thing` return { fullPath: path + (searchString && '?') + 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) { // no base or base is not found at the beginning 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) { // since the original record has an undefined value for aliasOf // but all aliases point to the original record, this will always compare // the original record return (a.aliasOf || a) === (b.aliasOf || b); } function isSameRouteLocationParams(a, b) { if (Object.keys(a).length !== Object.keys(b).length) return false; for (const 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 === b; } /** * 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 (!to) return from; const fromSegments = from.split('/'); const toSegments = to.split('/'); const lastToSegment = toSegments[toSegments.length - 1]; // make . and ./ the same (../ === .., ../../ === ../..) // this is the same behavior as new URL() if (lastToSegment === '..' || lastToSegment === '.') { toSegments.push(''); } let position = fromSegments.length - 1; let toPosition; let segment; for (toPosition = 0; toPosition < toSegments.length; toPosition++) { segment = toSegments[toPosition]; // we stay on the same position if (segment === '.') continue; // go up in the from array if (segment === '..') { // we can't go below zero, but we still need to increment toPosition if (position > 1) position--; // continue } // we reached a non-relative path, we stop here 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: '/', // TODO: could we use a symbol in the future? name: undefined, params: {}, query: {}, hash: '', fullPath: '/', matched: [], meta: {}, redirectedFrom: undefined, }; var NavigationType; (function (NavigationType) { NavigationType["pop"] = "pop"; NavigationType["push"] = "push"; })(NavigationType || (NavigationType = {})); var NavigationDirection; (function (NavigationDirection) { NavigationDirection["back"] = "back"; NavigationDirection["forward"] = "forward"; NavigationDirection["unknown"] = ""; })(NavigationDirection || (NavigationDirection = {})); /** * Starting location for Histories */ const START = ''; // Generic utils /** * 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) { // respect tag const baseEl = document.querySelector('base'); base = (baseEl && baseEl.getAttribute('href')) || '/'; // strip full URL origin base = base.replace(/^\w+:\/\/[^\/]+/, ''); } else { base = '/'; } } // ensure leading slash when it was removed by the regex above avoid leading // slash with hash because the file could be read from the disk like file:// // and the leading slash would cause problems if (base[0] !== '/' && base[0] !== '#') base = '/' + base; // remove the trailing slash so all other method can just do `base + fullPath` // to build an href return removeTrailingSlash(base); } // remove any character before the hash const BEFORE_HASH_RE = /^[^#]+#/; function createHref(base, location) { return base.replace(BEFORE_HASH_RE, '#') + location; } 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('#'); const el = typeof positionEl === 'string' ? isIdSelector ? document.getElementById(positionEl.slice(1)) : document.querySelector(positionEl) : positionEl; if (!el) { 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) { const position = history.state ? history.state.position - delta : -1; return position + path; } const scrollPositions = new Map(); function saveScrollPosition(key, scrollPosition) { scrollPositions.set(key, scrollPosition); } function getSavedScrollPosition(key) { const scroll = scrollPositions.get(key); // consume it so it's not used again scrollPositions.delete(key); return scroll; } // TODO: RFC about how to save scroll position /** * ScrollBehavior instance used by the router to compute and restore the scroll * position when navigating. */ // export interface ScrollHandler { // // returns a scroll position that can be saved in history // compute(): ScrollPositionEntry // // can take an extended ScrollPositionEntry // scroll(position: ScrollPosition): void // } // export const scrollHandler: ScrollHandler = { // compute: computeScroll, // scroll: scrollToPosition, // } 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; // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end const hashPos = base.indexOf('#'); if (hashPos > -1) { let slicePos = hash.includes(base.slice(hashPos)) ? base.slice(hashPos).length : 1; let pathFromHash = hash.slice(slicePos); // prepend the starting slash to hash so the url starts with /# if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash; return stripBase(pathFromHash, ''); } const path = stripBase(pathname, base); return path + search + hash; } function useHistoryListeners(base, historyState, currentLocation, replace) { let listeners = []; let teardowns = []; // TODO: should it be a stack? a Dict. Check if the popstate listener // can trigger twice 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; // ignore the popstate and reset the pauseState if (pauseState && pauseState === from) { pauseState = null; return; } delta = fromState ? state.position - fromState.position : 0; } else { replace(to); } // Here we could also revert the navigation by calling history.go(-delta) // this listener will have to be adapted to not trigger again and to wait for the url // to be updated before triggering the listeners. Some kind of validation function would also // need to be passed to the listeners so the navigation can be accepted // call all listeners listeners.forEach(listener => { listener(currentLocation.value, from, { delta, type: NavigationType.pop, direction: delta ? delta > 0 ? NavigationDirection.forward : NavigationDirection.back : NavigationDirection.unknown, }); }); }; function pauseListeners() { pauseState = currentLocation.value; } function listen(callback) { // set up the listener and prepare teardown callbacks listeners.push(callback); const teardown = () => { const index = listeners.indexOf(callback); if (index > -1) listeners.splice(index, 1); }; teardowns.push(teardown); return teardown; } function beforeUnloadListener() { 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('beforeunload', beforeUnloadListener); } // set up the listeners and prepare teardown callbacks window.addEventListener('popstate', popStateHandler); // TODO: could we use 'pagehide' or 'visibilitychange' instead? // https://developer.chrome.com/blog/page-lifecycle-api/ window.addEventListener('beforeunload', beforeUnloadListener, { passive: true, }); 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; // private variables const currentLocation = { value: createCurrentLocation(base, location), }; const historyState = { value: history.state }; // build current history entry as this is a fresh navigation if (!historyState.value) { changeLocation(currentLocation.value, { back: null, current: currentLocation.value, forward: null, // the length is off by one, we need to decrease it position: history.length - 1, replaced: true, // don't add a scroll as the user may have an anchor, and we want // scrollBehavior to be triggered without a saved position 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 `` 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 { // BROWSER QUIRK // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds history[replace ? 'replaceState' : 'pushState'](state, '', url); historyState.value = state; } catch (err) { { console.error(err); } // Force the navigation, this also resets the call count location[replace ? 'replace' : 'assign'](url); } } function replace(to, data) { const state = assign({}, history.state, buildState(historyState.value.back, // keep back and forward entries but override current position to, historyState.value.forward, true), data, { position: historyState.value.position }); changeLocation(to, state, true); currentLocation.value = to; } function push(to, data) { // Add to current entry the information of where we are going // as well as saving the current position const currentState = assign({}, // use current history state to gracefully handle a wrong call to // history.replaceState // https://github.com/vuejs/router/issues/366 historyState.value, history.state, { forward: to, scroll: computeScrollPosition(), }); changeLocation(currentState.current, currentState, true); const state = assign({}, buildState(currentLocation.value, to, null), { position: currentState.position + 1 }, data); changeLocation(to, state, 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({ // it's overridden right after 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; } /** * 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 = [START]; let position = 0; base = normalizeBase(base); function setLocation(location) { position++; if (position !== queue.length) { // we are in the middle, we remove everything from here in the queue queue.splice(position); } queue.push(location); } function triggerListeners(to, from, { direction, delta }) { const info = { direction, delta, type: NavigationType.pop, }; for (const callback of listeners) { callback(to, from, info); } } const routerHistory = { // rewritten by Object.defineProperty location: START, // TODO: should be kept in queue state: {}, base, createHref: createHref.bind(null, base), replace(to) { // remove current entry and decrement position queue.splice(position--, 1); setLocation(to); }, push(to, data) { setLocation(to); }, listen(callback) { listeners.push(callback); return () => { const index = listeners.indexOf(callback); if (index > -1) listeners.splice(index, 1); }; }, destroy() { listeners = []; queue = [START]; position = 0; }, go(delta, shouldTrigger = true) { const from = this.location; const direction = // we are considering delta === 0 going forward, but in abstract mode // using 0 for the delta doesn't make sense like it does in html5 where // it reloads the page delta < 0 ? NavigationDirection.back : NavigationDirection.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], }); return routerHistory; } /** * 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 `` 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 `` 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) { // Make sure this implementation is fine in terms of encoding, specially for IE11 // for `file://`, directly use the pathname and ignore the base // location.pathname contains an initial `/` even at the root: `https://example.com` base = location.host ? base || location.pathname + location.search : ''; // allow the user to provide a `#` in the middle: `/base/#/app` if (!base.includes('#')) base += '#'; return createWebHistory(base); } function isRouteLocation(route) { return typeof route === 'string' || (route && typeof route === 'object'); } function isRouteName(name) { return typeof name === 'string' || typeof name === 'symbol'; } const NavigationFailureSymbol = Symbol(''); /** * Enumeration with all possible types for navigation failures. Can be passed to * {@link isNavigationFailure} to check for specific failures. */ exports.NavigationFailureType = void 0; (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"; })(exports.NavigationFailureType || (exports.NavigationFailureType = {})); // DEV only debug messages const ErrorTypeMessages = { [1 /* ErrorTypes.MATCHER_NOT_FOUND */]({ location, currentLocation }) { return `No match for\n ${JSON.stringify(location)}${currentLocation ? '\nwhile being at\n' + JSON.stringify(currentLocation) : ''}`; }, [2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */]({ from, to, }) { return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`; }, [4 /* ErrorTypes.NAVIGATION_ABORTED */]({ from, to }) { return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`; }, [8 /* ErrorTypes.NAVIGATION_CANCELLED */]({ from, to }) { return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`; }, [16 /* ErrorTypes.NAVIGATION_DUPLICATED */]({ from, 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) { // keep full error messages in cjs versions { 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); } // default pattern for a param: non-greedy everything but / const BASE_PARAM_PATTERN = '[^/]+?'; const BASE_PATH_PARSER_OPTIONS = { sensitive: false, strict: false, start: true, end: true, }; // Special Regex characters that must be escaped in static tokens 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); // the amount of scores is the same as the length of segments except for the root segment "/" const score = []; // the regexp as a string let pattern = options.start ? '^' : ''; // extracted keys const keys = []; for (const segment of segments) { // the root segment needs special treatment const segmentScores = segment.length ? [] : [90 /* PathScore.Root */]; // allow trailing slash if (options.strict && !segment.length) pattern += '/'; for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) { const token = segment[tokenIndex]; // resets the score if we are inside a sub-segment /:a-other-:b let subSegmentScore = 40 /* PathScore.Segment */ + (options.sensitive ? 0.25 /* PathScore.BonusCaseSensitive */ : 0); if (token.type === 0 /* TokenType.Static */) { // prepend the slash if we are starting a new segment if (!tokenIndex) pattern += '/'; pattern += token.value.replace(REGEX_CHARS_RE, '\\$&'); subSegmentScore += 40 /* PathScore.Static */; } else if (token.type === 1 /* TokenType.Param */) { const { value, repeatable, optional, regexp } = token; keys.push({ name: value, repeatable, optional, }); const re = regexp ? regexp : BASE_PARAM_PATTERN; // the user provided a custom regexp /:id(\\d+) if (re !== BASE_PARAM_PATTERN) { subSegmentScore += 10 /* PathScore.BonusCustomRegExp */; // make sure the regexp is valid before using it try { new RegExp(`(${re})`); } catch (err) { throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` + err.message); } } // when we repeat we must take care of the repeating leading slash let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`; // prepend the slash if we are starting a new segment if (!tokenIndex) subPattern = // avoid an optional / if there are more segments e.g. /:p?-static // or /:p?-:p2 optional && segment.length < 2 ? `(?:/${subPattern})` : '/' + subPattern; if (optional) subPattern += '?'; pattern += subPattern; subSegmentScore += 20 /* PathScore.Dynamic */; if (optional) subSegmentScore += -8 /* PathScore.BonusOptional */; if (repeatable) subSegmentScore += -20 /* PathScore.BonusRepeatable */; if (re === '.*') subSegmentScore += -50 /* PathScore.BonusWildcard */; } segmentScores.push(subSegmentScore); } // an empty array like /home/ -> [[{home}], []] // if (!segment.length) pattern += '/' score.push(segmentScores); } // only apply the strict bonus to the last score if (options.strict && options.end) { const i = score.length - 1; score[i][score[i].length - 1] += 0.7000000000000001 /* PathScore.BonusStrict */; } // TODO: dev only warn double trailing slash if (!options.strict) pattern += '/?'; if (options.end) pattern += '$'; // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else else if (options.strict) 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 = ''; // for optional parameters to allow to be empty let avoidDuplicatedSlash = false; for (const segment of segments) { if (!avoidDuplicatedSlash || !path.endsWith('/')) path += '/'; avoidDuplicatedSlash = false; for (const token of segment) { if (token.type === 0 /* TokenType.Static */) { path += token.value; } else if (token.type === 1 /* TokenType.Param */) { 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 we have more than one optional param like /:a?-static we don't need to care about the optional param if (segment.length < 2) { // remove the last slash as we could be at the end if (path.endsWith('/')) path = path.slice(0, -1); // do not append a slash on the next iteration else avoidDuplicatedSlash = true; } } else throw new Error(`Missing required param "${value}"`); } path += text; } } } // avoid empty path when we have multiple optional params 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]; // only keep going if diff === 0 if (diff) return diff; i++; } // if the last subsegment was Static, the shorter segments should be sorted first // otherwise sort the longest segment first if (a.length < b.length) { return a.length === 1 && a[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */ ? -1 : 1; } else if (a.length > b.length) { return b.length === 1 && b[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */ ? 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]); // do not return if both are equal if (comp) return comp; i++; } if (Math.abs(bScore.length - aScore.length) === 1) { if (isLastScoreNegative(aScore)) return 1; if (isLastScoreNegative(bScore)) return -1; } // if a and b share the same score entries but b has more, sort b first return bScore.length - aScore.length; // this is the ternary version // return aScore.length < bScore.length // ? 1 // : aScore.length > bScore.length // ? -1 // : 0 } /** * 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 ROOT_TOKEN = { type: 0 /* TokenType.Static */, value: '', }; const VALID_PARAM_RE = /[a-zA-Z0-9_]/; // After some profiling, the cache seems to be unnecessary because tokenizePath // (the slowest part of adding a route) is very fast // const tokenCache = new Map() function tokenizePath(path) { if (!path) return [[]]; if (path === '/') return [[ROOT_TOKEN]]; if (!path.startsWith('/')) { throw new Error(`Invalid path "${path}"`); } // if (tokenCache.has(path)) return tokenCache.get(path)! function crash(message) { throw new Error(`ERR (${state})/"${buffer}": ${message}`); } let state = 0 /* TokenizerState.Static */; let previousState = state; const tokens = []; // the segment will always be valid because we get into the initial state // with the leading / let segment; function finalizeSegment() { if (segment) tokens.push(segment); segment = []; } // index on the path let i = 0; // char at index let char; // buffer of the value read let buffer = ''; // custom regexp for a param let customRe = ''; function consumeBuffer() { if (!buffer) return; if (state === 0 /* TokenizerState.Static */) { segment.push({ type: 0 /* TokenType.Static */, value: buffer, }); } else if (state === 1 /* TokenizerState.Param */ || state === 2 /* TokenizerState.ParamRegExp */ || state === 3 /* TokenizerState.ParamRegExpEnd */) { if (segment.length > 1 && (char === '*' || char === '+')) crash(`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`); segment.push({ type: 1 /* TokenType.Param */, 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++]; if (char === '\\' && state !== 2 /* TokenizerState.ParamRegExp */) { previousState = state; state = 4 /* TokenizerState.EscapeNext */; continue; } switch (state) { case 0 /* TokenizerState.Static */: if (char === '/') { if (buffer) { consumeBuffer(); } finalizeSegment(); } else if (char === ':') { consumeBuffer(); state = 1 /* TokenizerState.Param */; } else { addCharToBuffer(); } break; case 4 /* TokenizerState.EscapeNext */: addCharToBuffer(); state = previousState; break; case 1 /* TokenizerState.Param */: if (char === '(') { state = 2 /* TokenizerState.ParamRegExp */; } else if (VALID_PARAM_RE.test(char)) { addCharToBuffer(); } else { consumeBuffer(); state = 0 /* TokenizerState.Static */; // go back one character if we were not modifying if (char !== '*' && char !== '?' && char !== '+') i--; } break; case 2 /* TokenizerState.ParamRegExp */: // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix) // it already works by escaping the closing ) // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB# // is this really something people need since you can also write // /prefix_:p()_suffix if (char === ')') { // handle the escaped ) if (customRe[customRe.length - 1] == '\\') customRe = customRe.slice(0, -1) + char; else state = 3 /* TokenizerState.ParamRegExpEnd */; } else { customRe += char; } break; case 3 /* TokenizerState.ParamRegExpEnd */: // same as finalizing a param consumeBuffer(); state = 0 /* TokenizerState.Static */; // go back one character if we were not modifying if (char !== '*' && char !== '?' && char !== '+') i--; customRe = ''; break; default: crash('Unknown state'); break; } } if (state === 2 /* TokenizerState.ParamRegExp */) crash(`Unfinished custom RegExp for param "${buffer}"`); consumeBuffer(); finalizeSegment(); // tokenCache.set(path, tokens) return tokens; } function createRouteRecordMatcher(record, parent, options) { const parser = tokensToParser(tokenizePath(record.path), options); const matcher = assign(parser, { record, parent, // these needs to be populated by the parent children: [], alias: [], }); if (parent) { // both are aliases or both are not aliases // we don't want to mix them because the order is used when // passing originalRecord in Matcher.addRoute if (!matcher.record.aliasOf === !parent.record.aliasOf) parent.children.push(matcher); } return matcher; } /** * Creates a Router Matcher. * * @internal * @param routes - array of initial routes * @param globalOptions - global route options */ function createRouterMatcher(routes, globalOptions) { // normalized ordered array of matchers const matchers = []; const matcherMap = new Map(); globalOptions = mergeOptions({ strict: false, end: true, sensitive: false }, globalOptions); function getRecordMatcher(name) { return matcherMap.get(name); } function addRoute(record, parent, originalRecord) { // used later on to remove by name const isRootAdd = !originalRecord; const mainNormalizedRecord = normalizeRouteRecord(record); // we might be the child of an alias mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record; const options = mergeOptions(globalOptions, record); // generate an array of records to correctly handle aliases const normalizedRecords = [mainNormalizedRecord]; if ('alias' in record) { const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias; for (const alias of aliases) { normalizedRecords.push( // we need to normalize again to ensure the `mods` property // being non enumerable normalizeRouteRecord(assign({}, mainNormalizedRecord, { // this allows us to hold a copy of the `components` option // so that async components cache is hold on the original record components: originalRecord ? originalRecord.record.components : mainNormalizedRecord.components, path: alias, // we might be the child of an alias aliasOf: originalRecord ? originalRecord.record : mainNormalizedRecord, // the aliases are always of the same kind as the original since they // are defined on the same record }))); } } let matcher; let originalMatcher; for (const normalizedRecord of normalizedRecords) { const { path } = normalizedRecord; // Build up the path for nested routes if the child isn't an absolute // route. Only add the / delimiter if the child path isn't empty and if the // parent path doesn't have a trailing slash if (parent && path[0] !== '/') { const parentPath = parent.record.path; const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/'; normalizedRecord.path = parent.record.path + (path && connectingSlash + path); } // create the object beforehand, so it can be passed to children matcher = createRouteRecordMatcher(normalizedRecord, parent, options); // if we are an alias we must tell the original record that we exist, // so we can be removed if (originalRecord) { originalRecord.alias.push(matcher); } else { // otherwise, the first record is the original and others are aliases originalMatcher = originalMatcher || matcher; if (originalMatcher !== matcher) originalMatcher.alias.push(matcher); // remove the route if named and only for the top record (avoid in nested calls) // this works because the original record is the first one if (isRootAdd && record.name && !isAliasRecord(matcher)) removeRoute(record.name); } // Avoid adding a record that doesn't display anything. This allows passing through records without a component to // not be reached and pass through the catch all route 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]); } } // if there was no original record, then the first one was not an alias and all // other aliases (if any) need to reference this record when adding children originalRecord = originalRecord || matcher; // TODO: add normalized records for more flexibility // if (parent && isAliasRecord(originalRecord)) { // parent.children.push(originalRecord) // } } return originalMatcher ? () => { // since other matchers are aliases, they should be removed by the original matcher 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); // only add the original record to the name map 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 /* ErrorTypes.MATCHER_NOT_FOUND */, { location, }); name = matcher.record.name; params = assign( // paramsFromLocation is a new object paramsFromLocation(currentLocation.params, // only keep params that exist in the resolved location // only keep optional params coming from a parent record matcher.keys .filter(k => !k.optional) .concat(matcher.parent ? matcher.parent.keys.filter(k => k.optional) : []) .map(k => k.name)), // discard any existing params in the current location that do not exist here // #1497 this ensures better active/exact matching location.params && paramsFromLocation(location.params, matcher.keys.map(k => k.name))); // throws if cannot be stringified path = matcher.stringify(params); } else if (location.path != null) { // no need to resolve the path with the matcher as it was provided // this also allows the user to control the encoding path = location.path; matcher = matchers.find(m => m.re.test(path)); // matcher should have a value after the loop if (matcher) { // we know the matcher works because we tested the regexp params = matcher.parse(path); name = matcher.record.name; } // location is a relative path } else { // match by name or path of current route matcher = currentLocation.name ? matcherMap.get(currentLocation.name) : matchers.find(m => m.re.test(currentLocation.path)); if (!matcher) throw createRouterError(1 /* ErrorTypes.MATCHER_NOT_FOUND */, { location, currentLocation, }); name = matcher.record.name; // since we are navigating to the same location, we don't need to pick the // params like when `name` is provided params = assign({}, currentLocation.params, location.params); path = matcher.stringify(params); } const matched = []; let parentMatcher = matcher; while (parentMatcher) { // reversed order so parents are at the beginning matched.unshift(parentMatcher.record); parentMatcher = parentMatcher.parent; } return { name, path, params, matched, meta: mergeMetaFields(matched), }; } // add initial routes routes.forEach(route => addRoute(route)); function clearRoutes() { matchers.length = 0; matcherMap.clear(); } return { addRoute, resolve, removeRoute, clearRoutes, getRoutes, getRecordMatcher, }; } function paramsFromLocation(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: new Set(), updateGuards: new Set(), enterCallbacks: {}, // must be declared afterwards // mods: {}, components: 'components' in record ? record.components || null : record.component && { default: record.component }, }; // mods contain modules and shouldn't be copied, // logged or anything. It's just used for internal // advanced use cases like data loaders 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 = {}; // props does not exist on redirect records, but we can set false directly const props = record.props || false; if ('component' in record) { propsObject.default = props; } else { // NOTE: we could also allow a function to be applied to every component. // Would need user feedback for use cases 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 mergeOptions(defaults, partialOptions) { const options = {}; for (const key in defaults) { options[key] = key in partialOptions ? partialOptions[key] : defaults[key]; } return options; } /** * 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) { // First phase: binary search based on score let lower = 0; let upper = matchers.length; while (lower !== upper) { const mid = (lower + upper) >> 1; const sortOrder = comparePathParserScore(matcher, matchers[mid]); if (sortOrder < 0) { upper = mid; } else { lower = mid + 1; } } // Second phase: check for an ancestor with the same score const insertionAncestor = getInsertionAncestor(matcher); if (insertionAncestor) { upper = matchers.lastIndexOf(insertionAncestor, upper - 1); } return upper; } function getInsertionAncestor(matcher) { let ancestor = matcher; while ((ancestor = ancestor.parent)) { if (isMatchable(ancestor) && comparePathParserScore(matcher, ancestor) === 0) { return ancestor; } } return; } /** * 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); } /** * 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 = {}; // avoid creating an object with an empty key and empty value // because of split('&') if (search === '' || search === '?') return query; const hasLeadingIM = search[0] === '?'; const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&'); for (let i = 0; i < searchParams.length; ++i) { // pre decode the + into space const searchParam = searchParams[i].replace(PLUS_RE, ' '); // allow the = character 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) { // an extra variable for ts types 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) { // only null adds the value if (value !== undefined) { search += (search.length ? '&' : '') + key; } continue; } // keep null values const values = isArray(value) ? value.map(v => v && encodeQueryValue(v)) : [value && encodeQueryValue(value)]; values.forEach(value => { // skip undefined values in arrays as if they were not present // smaller code than using filter if (value !== undefined) { // only append & with non-empty search search += (search.length ? '&' : '') + key; if (value != null) search += '=' + value; } }); } return search; } /** * Transforms a {@link LocationQueryRaw} into a {@link LocationQuery} by casting * numbers into strings, removing keys with an undefined value and replacing * undefined with null in arrays * * @param query - query object to normalize * @returns a normalized query object */ function normalizeQuery(query) { const normalizedQuery = {}; for (const key in query) { const value = query[key]; if (value !== undefined) { normalizedQuery[key] = isArray(value) ? value.map(v => (v == null ? null : '' + v)) : value == null ? value : '' + value; } } return normalizedQuery; } /** * RouteRecord being rendered by the closest ancestor Router View. Used for * `onBeforeRouteUpdate` and `onBeforeRouteLeave`. rvlm stands for Router View * Location Matched * * @internal */ const matchedRouteKey = Symbol(''); /** * Allows overriding the router view depth to control which component in * `matched` is rendered. rvd stands for Router View Depth * * @internal */ const viewDepthKey = Symbol(''); /** * Allows overriding the router instance returned by `useRouter` in tests. r * stands for router * * @internal */ const routerKey = Symbol(''); /** * Allows overriding the current route returned by `useRoute` in tests. rl * stands for route location * * @internal */ const routeLocationKey = Symbol(''); /** * Allows overriding the current route used by router-view. Internally this is * used when the `route` prop is passed. * * @internal */ const routerViewLocationKey = Symbol(''); /** * Create a list of callbacks that can be reset. Used to create before and after navigation guards list */ function useCallbacks() { let handlers = []; function add(handler) { handlers.push(handler); return () => { const i = handlers.indexOf(handler); if (i > -1) handlers.splice(i, 1); }; } function reset() { handlers = []; } return { add, list: () => handlers.slice(), reset, }; } function registerGuard(record, name, guard) { const removeFromList = () => { record[name].delete(guard); }; vue.onUnmounted(removeFromList); vue.onDeactivated(removeFromList); vue.onActivated(() => { record[name].add(guard); }); record[name].add(guard); } /** * Add a navigation guard that triggers whenever the component for the current * location is about to be left. Similar to {@link beforeRouteLeave} but can be * used in any component. The guard is removed when the component is unmounted. * * @param leaveGuard - {@link NavigationGuard} */ function onBeforeRouteLeave(leaveGuard) { const activeRecord = vue.inject(matchedRouteKey, // to avoid warning {}).value; if (!activeRecord) { return; } registerGuard(activeRecord, 'leaveGuards', leaveGuard); } /** * Add a navigation guard that triggers whenever the current location is about * to be updated. Similar to {@link beforeRouteUpdate} but can be used in any * component. The guard is removed when the component is unmounted. * * @param updateGuard - {@link NavigationGuard} */ function onBeforeRouteUpdate(updateGuard) { const activeRecord = vue.inject(matchedRouteKey, // to avoid warning {}).value; if (!activeRecord) { return; } registerGuard(activeRecord, 'updateGuards', updateGuard); } function guardToPromiseFn(guard, to, from, record, name, runWithContext = fn => fn()) { // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place const enterCallbackArray = record && // name is defined if record is because of the function overload (record.enterCallbacks[name] = record.enterCallbacks[name] || []); return () => new Promise((resolve, reject) => { const next = (valid) => { if (valid === false) { reject(createRouterError(4 /* ErrorTypes.NAVIGATION_ABORTED */, { from, to, })); } else if (valid instanceof Error) { reject(valid); } else if (isRouteLocation(valid)) { reject(createRouterError(2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */, { from: to, to: valid, })); } else { if (enterCallbackArray && // since enterCallbackArray is truthy, both record and name also are record.enterCallbacks[name] === enterCallbackArray && typeof valid === 'function') { enterCallbackArray.push(valid); } resolve(); } }; // wrapping with Promise.resolve allows it to work with both async and sync guards const guardReturn = runWithContext(() => guard.call(record && record.instances[name], to, from, next)); let guardCall = Promise.resolve(guardReturn); if (guard.length < 3) guardCall = guardCall.then(next); guardCall.catch(err => reject(err)); }); } function extractComponentsGuards(matched, guardType, to, from, runWithContext = fn => fn()) { const guards = []; for (const record of matched) { for (const name in record.components) { let rawComponent = record.components[name]; // skip update and leave guards if the route component is not mounted if (guardType !== 'beforeRouteEnter' && !record.instances[name]) continue; if (isRouteComponent(rawComponent)) { // __vccOpts is added by vue-class-component and contain the regular options const options = rawComponent.__vccOpts || rawComponent; const guard = options[guardType]; guard && guards.push(guardToPromiseFn(guard, to, from, record, name, runWithContext)); } else { // start requesting the chunk already let componentPromise = rawComponent(); guards.push(() => componentPromise.then(resolved => { if (!resolved) throw new Error(`Couldn't resolve component "${name}" at "${record.path}"`); const resolvedComponent = isESModule(resolved) ? resolved.default : resolved; // keep the resolved module for plugins like data loaders record.mods[name] = resolved; // replace the function with the resolved component // cannot be null or undefined because we went into the for loop record.components[name] = resolvedComponent; // __vccOpts is added by vue-class-component and contain the regular options const options = resolvedComponent.__vccOpts || resolvedComponent; const guard = options[guardType]; return (guard && guardToPromiseFn(guard, to, from, record, name, runWithContext)()); })); } } } return guards; } /** * Ensures a route is loaded, so it can be passed as o prop to ``. * * @param route - resolved route to load */ function loadRouteLocation(route) { return route.matched.every(record => record.redirect) ? Promise.reject(new Error('Cannot load a route that redirects.')) : Promise.all(route.matched.map(record => record.components && Promise.all(Object.keys(record.components).reduce((promises, name) => { const rawComponent = record.components[name]; if (typeof rawComponent === 'function' && !('displayName' in rawComponent)) { promises.push(rawComponent().then(resolved => { if (!resolved) return Promise.reject(new Error(`Couldn't resolve component "${name}" at "${record.path}". Ensure you passed a function that returns a promise.`)); const resolvedComponent = isESModule(resolved) ? resolved.default : resolved; // keep the resolved module for plugins like data loaders record.mods[name] = resolved; // replace the function with the resolved component // cannot be null or undefined because we went into the for loop record.components[name] = resolvedComponent; return; })); } return promises; }, [])))).then(() => route); } // TODO: we could allow currentRoute as a prop to expose `isActive` and // `isExactActive` behavior should go through an RFC /** * Returns the internal behavior of a {@link RouterLink} without the rendering part. * * @param props - a `to` location and an optional `replace` flag */ function useLink(props) { const router = vue.inject(routerKey); const currentRoute = vue.inject(routeLocationKey); const route = vue.computed(() => { const to = vue.unref(props.to); return router.resolve(to); }); const activeRecordIndex = vue.computed(() => { const { matched } = route.value; const { length } = matched; const routeMatched = matched[length - 1]; const currentMatched = currentRoute.matched; if (!routeMatched || !currentMatched.length) return -1; const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched)); if (index > -1) return index; // possible parent record const parentRecordPath = getOriginalPath(matched[length - 2]); return ( // we are dealing with nested routes length > 1 && // if the parent and matched route have the same path, this link is // referring to the empty child. Or we currently are on a different // child of the same parent getOriginalPath(routeMatched) === parentRecordPath && // avoid comparing the child with its parent currentMatched[currentMatched.length - 1].path !== parentRecordPath ? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2])) : index); }); const isActive = vue.computed(() => activeRecordIndex.value > -1 && includesParams(currentRoute.params, route.value.params)); const isExactActive = vue.computed(() => activeRecordIndex.value > -1 && activeRecordIndex.value === currentRoute.matched.length - 1 && isSameRouteLocationParams(currentRoute.params, route.value.params)); function navigate(e = {}) { if (guardEvent(e)) { return router[vue.unref(props.replace) ? 'replace' : 'push'](vue.unref(props.to) // avoid uncaught errors are they are logged anyway ).catch(noop); } return Promise.resolve(); } /** * NOTE: update {@link _RouterLinkI}'s `$slots` type when updating this */ return { route, href: vue.computed(() => route.value.href), isActive, isExactActive, navigate, }; } const RouterLinkImpl = /*#__PURE__*/ vue.defineComponent({ name: 'RouterLink', compatConfig: { MODE: 3 }, props: { to: { type: [String, Object], required: true, }, replace: Boolean, activeClass: String, // inactiveClass: String, exactActiveClass: String, custom: Boolean, ariaCurrentValue: { type: String, default: 'page', }, }, useLink, setup(props, { slots }) { const link = vue.reactive(useLink(props)); const { options } = vue.inject(routerKey); const elClass = vue.computed(() => ({ [getLinkClass(props.activeClass, options.linkActiveClass, 'router-link-active')]: link.isActive, // [getLinkClass( // props.inactiveClass, // options.linkInactiveClass, // 'router-link-inactive' // )]: !link.isExactActive, [getLinkClass(props.exactActiveClass, options.linkExactActiveClass, 'router-link-exact-active')]: link.isExactActive, })); return () => { const children = slots.default && slots.default(link); return props.custom ? children : vue.h('a', { 'aria-current': link.isExactActive ? props.ariaCurrentValue : null, href: link.href, // this would override user added attrs but Vue will still add // the listener, so we end up triggering both onClick: link.navigate, class: elClass.value, }, children); }; }, }); // export the public type for h/tsx inference // also to avoid inline import() in generated d.ts files /** * Component to render a link that triggers a navigation on click. */ const RouterLink = RouterLinkImpl; function guardEvent(e) { // don't redirect with control keys if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return; // don't redirect when preventDefault called if (e.defaultPrevented) return; // don't redirect on right click if (e.button !== undefined && e.button !== 0) return; // don't redirect if `target="_blank"` // @ts-expect-error getAttribute does exist if (e.currentTarget && e.currentTarget.getAttribute) { // @ts-expect-error getAttribute exists const target = e.currentTarget.getAttribute('target'); if (/\b_blank\b/i.test(target)) return; } // this may be a Weex event which doesn't have this method if (e.preventDefault) e.preventDefault(); return true; } function includesParams(outer, inner) { for (const key in inner) { const innerValue = inner[key]; const outerValue = outer[key]; if (typeof innerValue === 'string') { if (innerValue !== outerValue) return false; } else { if (!isArray(outerValue) || outerValue.length !== innerValue.length || innerValue.some((value, i) => value !== outerValue[i])) return false; } } return true; } /** * Get the original path value of a record by following its aliasOf * @param record */ function getOriginalPath(record) { return record ? (record.aliasOf ? record.aliasOf.path : record.path) : ''; } /** * Utility class to get the active class based on defaults. * @param propClass * @param globalClass * @param defaultClass */ const getLinkClass = (propClass, globalClass, defaultClass) => propClass != null ? propClass : globalClass != null ? globalClass : defaultClass; const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({ name: 'RouterView', // #674 we manually inherit them inheritAttrs: false, props: { name: { type: String, default: 'default', }, route: Object, }, // Better compat for @vue/compat users // https://github.com/vuejs/router/issues/1315 compatConfig: { MODE: 3 }, setup(props, { attrs, slots }) { const injectedRoute = vue.inject(routerViewLocationKey); const routeToDisplay = vue.computed(() => props.route || injectedRoute.value); const injectedDepth = vue.inject(viewDepthKey, 0); // The depth changes based on empty components option, which allows passthrough routes e.g. routes with children // that are used to reuse the `path` property const depth = vue.computed(() => { let initialDepth = vue.unref(injectedDepth); const { matched } = routeToDisplay.value; let matchedRoute; while ((matchedRoute = matched[initialDepth]) && !matchedRoute.components) { initialDepth++; } return initialDepth; }); const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]); vue.provide(viewDepthKey, vue.computed(() => depth.value + 1)); vue.provide(matchedRouteKey, matchedRouteRef); vue.provide(routerViewLocationKey, routeToDisplay); const viewRef = vue.ref(); // watch at the same time the component instance, the route record we are // rendering, and the name vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => { // copy reused instances if (to) { // this will update the instance for new instances as well as reused // instances when navigating to a new route to.instances[name] = instance; // the component instance is reused for a different route or name, so // we copy any saved update or leave guards. With async setup, the // mounting component will mount before the matchedRoute changes, // making instance === oldInstance, so we check if guards have been // added before. This works because we remove guards when // unmounting/deactivating components if (from && from !== to && instance && instance === oldInstance) { if (!to.leaveGuards.size) { to.leaveGuards = from.leaveGuards; } if (!to.updateGuards.size) { to.updateGuards = from.updateGuards; } } } // trigger beforeRouteEnter next callbacks if (instance && to && // if there is no instance but to and from are the same this might be // the first visit (!from || !isSameRouteRecord(to, from) || !oldInstance)) { (to.enterCallbacks[name] || []).forEach(callback => callback(instance)); } }, { flush: 'post' }); return () => { const route = routeToDisplay.value; // we need the value at the time we render because when we unmount, we // navigated to a different location so the value is different const currentName = props.name; const matchedRoute = matchedRouteRef.value; const ViewComponent = matchedRoute && matchedRoute.components[currentName]; if (!ViewComponent) { return normalizeSlot(slots.default, { Component: ViewComponent, route }); } // props from route configuration const routePropsOption = matchedRoute.props[currentName]; const routeProps = routePropsOption ? routePropsOption === true ? route.params : typeof routePropsOption === 'function' ? routePropsOption(route) : routePropsOption : null; const onVnodeUnmounted = vnode => { // remove the instance reference to prevent leak if (vnode.component.isUnmounted) { matchedRoute.instances[currentName] = null; } }; const component = vue.h(ViewComponent, assign({}, routeProps, attrs, { onVnodeUnmounted, ref: viewRef, })); return ( // pass the vnode to the slot as a prop. // h and both accept vnodes normalizeSlot(slots.default, { Component: component, route }) || component); }; }, }); function normalizeSlot(slot, data) { if (!slot) return null; const slotContent = slot(data); return slotContent.length === 1 ? slotContent[0] : slotContent; } // export the public type for h/tsx inference // also to avoid inline import() in generated d.ts files /** * Component to display the current route the user is at. */ const RouterView = RouterViewImpl; /** * Creates a Router instance that can be used by a Vue app. * * @param options - {@link RouterOptions} */ function createRouter(options) { const matcher = createRouterMatcher(options.routes, options); const parseQuery$1 = options.parseQuery || parseQuery; const stringifyQuery$1 = options.stringifyQuery || stringifyQuery; const routerHistory = options.history; const beforeGuards = useCallbacks(); const beforeResolveGuards = useCallbacks(); const afterGuards = useCallbacks(); const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED); let pendingLocation = START_LOCATION_NORMALIZED; // leave the scrollRestoration if no scrollBehavior is provided if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) { history.scrollRestoration = 'manual'; } const normalizeParams = applyToParams.bind(null, paramValue => '' + paramValue); const encodeParams = applyToParams.bind(null, encodeParam); const decodeParams = // @ts-expect-error: intentionally avoid the type check applyToParams.bind(null, decode); function addRoute(parentOrRoute, route) { let parent; let record; if (isRouteName(parentOrRoute)) { parent = matcher.getRecordMatcher(parentOrRoute); record = route; } else { record = parentOrRoute; } return matcher.addRoute(record, parent); } function removeRoute(name) { const recordMatcher = matcher.getRecordMatcher(name); if (recordMatcher) { matcher.removeRoute(recordMatcher); } } function getRoutes() { return matcher.getRoutes().map(routeMatcher => routeMatcher.record); } function hasRoute(name) { return !!matcher.getRecordMatcher(name); } function resolve(rawLocation, currentLocation) { // const resolve: Router['resolve'] = (rawLocation: RouteLocationRaw, currentLocation) => { // const objectLocation = routerLocationAsObject(rawLocation) // we create a copy to modify it later currentLocation = assign({}, currentLocation || currentRoute.value); if (typeof rawLocation === 'string') { const locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path); const matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation); const href = routerHistory.createHref(locationNormalized.fullPath); // locationNormalized is always a new object return assign(locationNormalized, matchedRoute, { params: decodeParams(matchedRoute.params), hash: decode(locationNormalized.hash), redirectedFrom: undefined, href, }); } let matcherLocation; // path could be relative in object as well if (rawLocation.path != null) { matcherLocation = assign({}, rawLocation, { path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path, }); } else { // remove any nullish param const targetParams = assign({}, rawLocation.params); for (const key in targetParams) { if (targetParams[key] == null) { delete targetParams[key]; } } // pass encoded values to the matcher, so it can produce encoded path and fullPath matcherLocation = assign({}, rawLocation, { params: encodeParams(targetParams), }); // current location params are decoded, we need to encode them in case the // matcher merges the params currentLocation.params = encodeParams(currentLocation.params); } const matchedRoute = matcher.resolve(matcherLocation, currentLocation); const hash = rawLocation.hash || ''; // the matcher might have merged current location params, so // we need to run the decoding again matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params)); const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, { hash: encodeHash(hash), path: matchedRoute.path, })); const href = routerHistory.createHref(fullPath); return assign({ fullPath, // keep the hash encoded so fullPath is effectively path + encodedQuery + // hash hash, query: // if the user is using a custom query lib like qs, we might have // nested objects, so we keep the query as is, meaning it can contain // numbers at `$route.query`, but at the point, the user will have to // use their own type anyway. // https://github.com/vuejs/router/issues/328#issuecomment-649481567 stringifyQuery$1 === stringifyQuery ? normalizeQuery(rawLocation.query) : (rawLocation.query || {}), }, matchedRoute, { redirectedFrom: undefined, href, }); } function locationAsObject(to) { return typeof to === 'string' ? parseURL(parseQuery$1, to, currentRoute.value.path) : assign({}, to); } function checkCanceledNavigation(to, from) { if (pendingLocation !== to) { return createRouterError(8 /* ErrorTypes.NAVIGATION_CANCELLED */, { from, to, }); } } function push(to) { return pushWithRedirect(to); } function replace(to) { return push(assign(locationAsObject(to), { replace: true })); } function handleRedirectRecord(to) { const lastMatched = to.matched[to.matched.length - 1]; if (lastMatched && lastMatched.redirect) { const { redirect } = lastMatched; let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect; if (typeof newTargetLocation === 'string') { newTargetLocation = newTargetLocation.includes('?') || newTargetLocation.includes('#') ? (newTargetLocation = locationAsObject(newTargetLocation)) : // force empty params { path: newTargetLocation }; // @ts-expect-error: force empty params when a string is passed to let // the router parse them again newTargetLocation.params = {}; } return assign({ query: to.query, hash: to.hash, // avoid transferring params if the redirect has a path params: newTargetLocation.path != null ? {} : to.params, }, newTargetLocation); } } function pushWithRedirect(to, redirectedFrom) { const targetLocation = (pendingLocation = resolve(to)); const from = currentRoute.value; const data = to.state; const force = to.force; // to could be a string where `replace` is a function const replace = to.replace === true; const shouldRedirect = handleRedirectRecord(targetLocation); if (shouldRedirect) return pushWithRedirect(assign(locationAsObject(shouldRedirect), { state: typeof shouldRedirect === 'object' ? assign({}, data, shouldRedirect.state) : data, force, replace, }), // keep original redirectedFrom if it exists redirectedFrom || targetLocation); // if it was a redirect we already called `pushWithRedirect` above const toLocation = targetLocation; toLocation.redirectedFrom = redirectedFrom; let failure; if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) { failure = createRouterError(16 /* ErrorTypes.NAVIGATION_DUPLICATED */, { to: toLocation, from }); // trigger scroll to allow scrolling to the same anchor handleScroll(from, from, // this is a push, the only way for it to be triggered from a // history.listen is with a redirect, which makes it become a push true, // This cannot be the first navigation because the initial location // cannot be manually navigated to false); } return (failure ? Promise.resolve(failure) : navigate(toLocation, from)) .catch((error) => isNavigationFailure(error) ? // navigation redirects still mark the router as ready isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */) ? error : markAsReady(error) // also returns the error : // reject any unknown error triggerError(error, toLocation, from)) .then((failure) => { if (failure) { if (isNavigationFailure(failure, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) { return pushWithRedirect( // keep options assign({ // preserve an existing replacement but allow the redirect to override it replace, }, locationAsObject(failure.to), { state: typeof failure.to === 'object' ? assign({}, data, failure.to.state) : data, force, }), // preserve the original redirectedFrom if any redirectedFrom || toLocation); } } else { // if we fail we don't finalize the navigation failure = finalizeNavigation(toLocation, from, true, replace, data); } triggerAfterEach(toLocation, from, failure); return failure; }); } /** * Helper to reject and skip all navigation guards if a new navigation happened * @param to * @param from */ function checkCanceledNavigationAndReject(to, from) { const error = checkCanceledNavigation(to, from); return error ? Promise.reject(error) : Promise.resolve(); } function runWithContext(fn) { const app = installedApps.values().next().value; // support Vue < 3.3 return app && typeof app.runWithContext === 'function' ? app.runWithContext(fn) : fn(); } // TODO: refactor the whole before guards by internally using router.beforeEach function navigate(to, from) { let guards; const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from); // all components here have been resolved once because we are leaving guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from); // leavingRecords is already reversed for (const record of leavingRecords) { record.leaveGuards.forEach(guard => { guards.push(guardToPromiseFn(guard, to, from)); }); } const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from); guards.push(canceledNavigationCheck); // run the queue of per route beforeRouteLeave guards return (runGuardQueue(guards) .then(() => { // check global guards beforeEach guards = []; for (const guard of beforeGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)); } guards.push(canceledNavigationCheck); return runGuardQueue(guards); }) .then(() => { // check in components beforeRouteUpdate guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from); for (const record of updatingRecords) { record.updateGuards.forEach(guard => { guards.push(guardToPromiseFn(guard, to, from)); }); } guards.push(canceledNavigationCheck); // run the queue of per route beforeEnter guards return runGuardQueue(guards); }) .then(() => { // check the route beforeEnter guards = []; for (const record of enteringRecords) { // do not trigger beforeEnter on reused views if (record.beforeEnter) { if (isArray(record.beforeEnter)) { for (const beforeEnter of record.beforeEnter) guards.push(guardToPromiseFn(beforeEnter, to, from)); } else { guards.push(guardToPromiseFn(record.beforeEnter, to, from)); } } } guards.push(canceledNavigationCheck); // run the queue of per route beforeEnter guards return runGuardQueue(guards); }) .then(() => { // NOTE: at this point to.matched is normalized and does not contain any () => Promise // clear existing enterCallbacks, these are added by extractComponentsGuards to.matched.forEach(record => (record.enterCallbacks = {})); // check in-component beforeRouteEnter guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from, runWithContext); guards.push(canceledNavigationCheck); // run the queue of per route beforeEnter guards return runGuardQueue(guards); }) .then(() => { // check global guards beforeResolve guards = []; for (const guard of beforeResolveGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)); } guards.push(canceledNavigationCheck); return runGuardQueue(guards); }) // catch any navigation canceled .catch(err => isNavigationFailure(err, 8 /* ErrorTypes.NAVIGATION_CANCELLED */) ? err : Promise.reject(err))); } function triggerAfterEach(to, from, failure) { // navigation is confirmed, call afterGuards // TODO: wrap with error handlers afterGuards .list() .forEach(guard => runWithContext(() => guard(to, from, failure))); } /** * - Cleans up any navigation guards * - Changes the url if necessary * - Calls the scrollBehavior */ function finalizeNavigation(toLocation, from, isPush, replace, data) { // a more recent navigation took place const error = checkCanceledNavigation(toLocation, from); if (error) return error; // only consider as push if it's not the first navigation const isFirstNavigation = from === START_LOCATION_NORMALIZED; const state = !isBrowser ? {} : history.state; // change URL only if the user did a push/replace and if it's not the initial navigation because // it's just reflecting the url if (isPush) { // on the initial navigation, we want to reuse the scroll position from // history state if it exists if (replace || isFirstNavigation) routerHistory.replace(toLocation.fullPath, assign({ scroll: isFirstNavigation && state && state.scroll, }, data)); else routerHistory.push(toLocation.fullPath, data); } // accept current navigation currentRoute.value = toLocation; handleScroll(toLocation, from, isPush, isFirstNavigation); markAsReady(); } let removeHistoryListener; // attach listener to history to trigger navigations function setupListeners() { // avoid setting up listeners twice due to an invalid first navigation if (removeHistoryListener) return; removeHistoryListener = routerHistory.listen((to, _from, info) => { if (!router.listening) return; // cannot be a redirect route because it was in history const toLocation = resolve(to); // due to dynamic routing, and to hash history with manual navigation // (manually changing the url or calling history.hash = '#/somewhere'), // there could be a redirect record in history const shouldRedirect = handleRedirectRecord(toLocation); if (shouldRedirect) { pushWithRedirect(assign(shouldRedirect, { replace: true }), toLocation).catch(noop); return; } pendingLocation = toLocation; const from = currentRoute.value; // TODO: should be moved to web history? if (isBrowser) { saveScrollPosition(getScrollKey(from.fullPath, info.delta), computeScrollPosition()); } navigate(toLocation, from) .catch((error) => { if (isNavigationFailure(error, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) { return error; } if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) { // Here we could call if (info.delta) routerHistory.go(-info.delta, // false) but this is bug prone as we have no way to wait the // navigation to be finished before calling pushWithRedirect. Using // a setTimeout of 16ms seems to work but there is no guarantee for // it to work on every browser. So instead we do not restore the // history entry and trigger a new navigation as requested by the // navigation guard. // the error is already handled by router.push we just want to avoid // logging the error pushWithRedirect(error.to, toLocation // avoid an uncaught rejection, let push call triggerError ) .then(failure => { // manual change in hash history #916 ending up in the URL not // changing, but it was changed by the manual url change, so we // need to manually change it ourselves if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* ErrorTypes.NAVIGATION_DUPLICATED */) && !info.delta && info.type === NavigationType.pop) { routerHistory.go(-1, false); } }) .catch(noop); // avoid the then branch return Promise.reject(); } // do not restore history on unknown direction if (info.delta) { routerHistory.go(-info.delta, false); } // unrecognized error, transfer to the global handler return triggerError(error, toLocation, from); }) .then((failure) => { failure = failure || finalizeNavigation( // after navigation, all matched components are resolved toLocation, from, false); // revert the navigation if (failure) { if (info.delta && // a new navigation has been triggered, so we do not want to revert, that will change the current history // entry while a different route is displayed !isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) { routerHistory.go(-info.delta, false); } else if (info.type === NavigationType.pop && isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) { // manual change in hash history #916 // it's like a push but lacks the information of the direction routerHistory.go(-1, false); } } triggerAfterEach(toLocation, from, failure); }) // avoid warnings in the console about uncaught rejections, they are logged by triggerErrors .catch(noop); }); } // Initialization and Errors let readyHandlers = useCallbacks(); let errorListeners = useCallbacks(); let ready; /** * Trigger errorListeners added via onError and throws the error as well * * @param error - error to throw * @param to - location we were navigating to when the error happened * @param from - location we were navigating from when the error happened * @returns the error as a rejected promise */ function triggerError(error, to, from) { markAsReady(error); const list = errorListeners.list(); if (list.length) { list.forEach(handler => handler(error, to, from)); } else { console.error(error); } // reject the error no matter there were error listeners or not return Promise.reject(error); } function isReady() { if (ready && currentRoute.value !== START_LOCATION_NORMALIZED) return Promise.resolve(); return new Promise((resolve, reject) => { readyHandlers.add([resolve, reject]); }); } function markAsReady(err) { if (!ready) { // still not ready if an error happened ready = !err; setupListeners(); readyHandlers .list() .forEach(([resolve, reject]) => (err ? reject(err) : resolve())); readyHandlers.reset(); } return err; } // Scroll behavior function handleScroll(to, from, isPush, isFirstNavigation) { const { scrollBehavior } = options; if (!isBrowser || !scrollBehavior) return Promise.resolve(); const scrollPosition = (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) || ((isFirstNavigation || !isPush) && history.state && history.state.scroll) || null; return vue.nextTick() .then(() => scrollBehavior(to, from, scrollPosition)) .then(position => position && scrollToPosition(position)) .catch(err => triggerError(err, to, from)); } const go = (delta) => routerHistory.go(delta); let started; const installedApps = new Set(); const router = { currentRoute, listening: true, addRoute, removeRoute, clearRoutes: matcher.clearRoutes, hasRoute, getRoutes, resolve, options, push, replace, go, back: () => go(-1), forward: () => go(1), beforeEach: beforeGuards.add, beforeResolve: beforeResolveGuards.add, afterEach: afterGuards.add, onError: errorListeners.add, isReady, install(app) { const router = this; app.component('RouterLink', RouterLink); app.component('RouterView', RouterView); app.config.globalProperties.$router = router; Object.defineProperty(app.config.globalProperties, '$route', { enumerable: true, get: () => vue.unref(currentRoute), }); // this initial navigation is only necessary on client, on server it doesn't // make sense because it will create an extra unnecessary navigation and could // lead to problems if (isBrowser && // used for the initial navigation client side to avoid pushing // multiple times when the router is used in multiple apps !started && currentRoute.value === START_LOCATION_NORMALIZED) { // see above started = true; push(routerHistory.location).catch(err => { }); } const reactiveRoute = {}; for (const key in START_LOCATION_NORMALIZED) { Object.defineProperty(reactiveRoute, key, { get: () => currentRoute.value[key], enumerable: true, }); } app.provide(routerKey, router); app.provide(routeLocationKey, vue.shallowReactive(reactiveRoute)); app.provide(routerViewLocationKey, currentRoute); const unmountApp = app.unmount; installedApps.add(app); app.unmount = function () { installedApps.delete(app); // the router is not attached to an app anymore if (installedApps.size < 1) { // invalidate the current navigation pendingLocation = START_LOCATION_NORMALIZED; removeHistoryListener && removeHistoryListener(); removeHistoryListener = null; currentRoute.value = START_LOCATION_NORMALIZED; started = false; ready = false; } unmountApp(); }; }, }; // TODO: type this as NavigationGuardReturn or similar instead of any function runGuardQueue(guards) { return guards.reduce((promise, guard) => promise.then(() => runWithContext(guard)), Promise.resolve()); } return router; } function extractChangingRecords(to, from) { const leavingRecords = []; const updatingRecords = []; const enteringRecords = []; const len = Math.max(from.matched.length, to.matched.length); for (let i = 0; i < len; i++) { const recordFrom = from.matched[i]; if (recordFrom) { if (to.matched.find(record => isSameRouteRecord(record, recordFrom))) updatingRecords.push(recordFrom); else leavingRecords.push(recordFrom); } const recordTo = to.matched[i]; if (recordTo) { // the type doesn't matter because we are comparing per reference if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) { enteringRecords.push(recordTo); } } } return [leavingRecords, updatingRecords, enteringRecords]; } /** * Returns the router instance. Equivalent to using `$router` inside * templates. */ function useRouter() { return vue.inject(routerKey); } /** * Returns the current route location. Equivalent to using `$route` inside * templates. */ function useRoute(_name) { return vue.inject(routeLocationKey); } exports.RouterLink = RouterLink; exports.RouterView = RouterView; exports.START_LOCATION = START_LOCATION_NORMALIZED; exports.createMemoryHistory = createMemoryHistory; exports.createRouter = createRouter; exports.createRouterMatcher = createRouterMatcher; exports.createWebHashHistory = createWebHashHistory; exports.createWebHistory = createWebHistory; exports.isNavigationFailure = isNavigationFailure; exports.loadRouteLocation = loadRouteLocation; exports.matchedRouteKey = matchedRouteKey; exports.onBeforeRouteLeave = onBeforeRouteLeave; exports.onBeforeRouteUpdate = onBeforeRouteUpdate; exports.parseQuery = parseQuery; exports.routeLocationKey = routeLocationKey; exports.routerKey = routerKey; exports.routerViewLocationKey = routerViewLocationKey; exports.stringifyQuery = stringifyQuery; exports.useLink = useLink; exports.useRoute = useRoute; exports.useRouter = useRouter; exports.viewDepthKey = viewDepthKey;