You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2954 lines
112 KiB

1 month ago
/*!
* 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 <base> 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<ScrollPositionEntry extends HistoryStateValue, ScrollPosition extends ScrollPositionEntry> {
// // 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<ScrollPosition> = {
// 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 `<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 {
// 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 `<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) {
// 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<string, Token[][]>()
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 `<RouterView>`.
*
* @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 <component :is="..."> 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<Component>
// 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;