UNPKG

140 kBJavaScriptView Raw
1/*!
2 * vue-router v4.0.15
3 * (c) 2022 Eduardo San Martin Morote
4 * @license MIT
5 */
6'use strict';
7
8Object.defineProperty(exports, '__esModule', { value: true });
9
10var vue = require('vue');
11var devtoolsApi = require('@vue/devtools-api');
12
13const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol';
14const PolySymbol = (name) =>
15// vr = vue router
16hasSymbol
17 ? Symbol('[vue-router]: ' + name )
18 : ('[vue-router]: ' ) + name;
19// rvlm = Router View Location Matched
20/**
21 * RouteRecord being rendered by the closest ancestor Router View. Used for
22 * `onBeforeRouteUpdate` and `onBeforeRouteLeave`. rvlm stands for Router View
23 * Location Matched
24 *
25 * @internal
26 */
27const matchedRouteKey = /*#__PURE__*/ PolySymbol('router view location matched' );
28/**
29 * Allows overriding the router view depth to control which component in
30 * `matched` is rendered. rvd stands for Router View Depth
31 *
32 * @internal
33 */
34const viewDepthKey = /*#__PURE__*/ PolySymbol('router view depth' );
35/**
36 * Allows overriding the router instance returned by `useRouter` in tests. r
37 * stands for router
38 *
39 * @internal
40 */
41const routerKey = /*#__PURE__*/ PolySymbol('router' );
42/**
43 * Allows overriding the current route returned by `useRoute` in tests. rl
44 * stands for route location
45 *
46 * @internal
47 */
48const routeLocationKey = /*#__PURE__*/ PolySymbol('route location' );
49/**
50 * Allows overriding the current route used by router-view. Internally this is
51 * used when the `route` prop is passed.
52 *
53 * @internal
54 */
55const routerViewLocationKey = /*#__PURE__*/ PolySymbol('router view location' );
56
57const isBrowser = typeof window !== 'undefined';
58
59function isESModule(obj) {
60 return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module');
61}
62const assign = Object.assign;
63function applyToParams(fn, params) {
64 const newParams = {};
65 for (const key in params) {
66 const value = params[key];
67 newParams[key] = Array.isArray(value) ? value.map(fn) : fn(value);
68 }
69 return newParams;
70}
71const noop = () => { };
72
73function warn(msg) {
74 // avoid using ...args as it breaks in older Edge builds
75 const args = Array.from(arguments).slice(1);
76 console.warn.apply(console, ['[Vue Router warn]: ' + msg].concat(args));
77}
78
79const TRAILING_SLASH_RE = /\/$/;
80const removeTrailingSlash = (path) => path.replace(TRAILING_SLASH_RE, '');
81/**
82 * Transforms an URI into a normalized history location
83 *
84 * @param parseQuery
85 * @param location - URI to normalize
86 * @param currentLocation - current absolute location. Allows resolving relative
87 * paths. Must start with `/`. Defaults to `/`
88 * @returns a normalized history location
89 */
90function parseURL(parseQuery, location, currentLocation = '/') {
91 let path, query = {}, searchString = '', hash = '';
92 // Could use URL and URLSearchParams but IE 11 doesn't support it
93 const searchPos = location.indexOf('?');
94 const hashPos = location.indexOf('#', searchPos > -1 ? searchPos : 0);
95 if (searchPos > -1) {
96 path = location.slice(0, searchPos);
97 searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
98 query = parseQuery(searchString);
99 }
100 if (hashPos > -1) {
101 path = path || location.slice(0, hashPos);
102 // keep the # character
103 hash = location.slice(hashPos, location.length);
104 }
105 // no search and no query
106 path = resolveRelativePath(path != null ? path : location, currentLocation);
107 // empty path means a relative query or hash `?foo=f`, `#thing`
108 return {
109 fullPath: path + (searchString && '?') + searchString + hash,
110 path,
111 query,
112 hash,
113 };
114}
115/**
116 * Stringifies a URL object
117 *
118 * @param stringifyQuery
119 * @param location
120 */
121function stringifyURL(stringifyQuery, location) {
122 const query = location.query ? stringifyQuery(location.query) : '';
123 return location.path + (query && '?') + query + (location.hash || '');
124}
125/**
126 * Strips off the base from the beginning of a location.pathname in a non
127 * case-sensitive way.
128 *
129 * @param pathname - location.pathname
130 * @param base - base to strip off
131 */
132function stripBase(pathname, base) {
133 // no base or base is not found at the beginning
134 if (!base || !pathname.toLowerCase().startsWith(base.toLowerCase()))
135 return pathname;
136 return pathname.slice(base.length) || '/';
137}
138/**
139 * Checks if two RouteLocation are equal. This means that both locations are
140 * pointing towards the same {@link RouteRecord} and that all `params`, `query`
141 * parameters and `hash` are the same
142 *
143 * @param a - first {@link RouteLocation}
144 * @param b - second {@link RouteLocation}
145 */
146function isSameRouteLocation(stringifyQuery, a, b) {
147 const aLastIndex = a.matched.length - 1;
148 const bLastIndex = b.matched.length - 1;
149 return (aLastIndex > -1 &&
150 aLastIndex === bLastIndex &&
151 isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&
152 isSameRouteLocationParams(a.params, b.params) &&
153 stringifyQuery(a.query) === stringifyQuery(b.query) &&
154 a.hash === b.hash);
155}
156/**
157 * Check if two `RouteRecords` are equal. Takes into account aliases: they are
158 * considered equal to the `RouteRecord` they are aliasing.
159 *
160 * @param a - first {@link RouteRecord}
161 * @param b - second {@link RouteRecord}
162 */
163function isSameRouteRecord(a, b) {
164 // since the original record has an undefined value for aliasOf
165 // but all aliases point to the original record, this will always compare
166 // the original record
167 return (a.aliasOf || a) === (b.aliasOf || b);
168}
169function isSameRouteLocationParams(a, b) {
170 if (Object.keys(a).length !== Object.keys(b).length)
171 return false;
172 for (const key in a) {
173 if (!isSameRouteLocationParamsValue(a[key], b[key]))
174 return false;
175 }
176 return true;
177}
178function isSameRouteLocationParamsValue(a, b) {
179 return Array.isArray(a)
180 ? isEquivalentArray(a, b)
181 : Array.isArray(b)
182 ? isEquivalentArray(b, a)
183 : a === b;
184}
185/**
186 * Check if two arrays are the same or if an array with one single entry is the
187 * same as another primitive value. Used to check query and parameters
188 *
189 * @param a - array of values
190 * @param b - array of values or a single value
191 */
192function isEquivalentArray(a, b) {
193 return Array.isArray(b)
194 ? a.length === b.length && a.every((value, i) => value === b[i])
195 : a.length === 1 && a[0] === b;
196}
197/**
198 * Resolves a relative path that starts with `.`.
199 *
200 * @param to - path location we are resolving
201 * @param from - currentLocation.path, should start with `/`
202 */
203function resolveRelativePath(to, from) {
204 if (to.startsWith('/'))
205 return to;
206 if (!from.startsWith('/')) {
207 warn(`Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`);
208 return to;
209 }
210 if (!to)
211 return from;
212 const fromSegments = from.split('/');
213 const toSegments = to.split('/');
214 let position = fromSegments.length - 1;
215 let toPosition;
216 let segment;
217 for (toPosition = 0; toPosition < toSegments.length; toPosition++) {
218 segment = toSegments[toPosition];
219 // can't go below zero
220 if (position === 1 || segment === '.')
221 continue;
222 if (segment === '..')
223 position--;
224 // found something that is not relative path
225 else
226 break;
227 }
228 return (fromSegments.slice(0, position).join('/') +
229 '/' +
230 toSegments
231 .slice(toPosition - (toPosition === toSegments.length ? 1 : 0))
232 .join('/'));
233}
234
235var NavigationType;
236(function (NavigationType) {
237 NavigationType["pop"] = "pop";
238 NavigationType["push"] = "push";
239})(NavigationType || (NavigationType = {}));
240var NavigationDirection;
241(function (NavigationDirection) {
242 NavigationDirection["back"] = "back";
243 NavigationDirection["forward"] = "forward";
244 NavigationDirection["unknown"] = "";
245})(NavigationDirection || (NavigationDirection = {}));
246/**
247 * Starting location for Histories
248 */
249const START = '';
250// Generic utils
251/**
252 * Normalizes a base by removing any trailing slash and reading the base tag if
253 * present.
254 *
255 * @param base - base to normalize
256 */
257function normalizeBase(base) {
258 if (!base) {
259 if (isBrowser) {
260 // respect <base> tag
261 const baseEl = document.querySelector('base');
262 base = (baseEl && baseEl.getAttribute('href')) || '/';
263 // strip full URL origin
264 base = base.replace(/^\w+:\/\/[^\/]+/, '');
265 }
266 else {
267 base = '/';
268 }
269 }
270 // ensure leading slash when it was removed by the regex above avoid leading
271 // slash with hash because the file could be read from the disk like file://
272 // and the leading slash would cause problems
273 if (base[0] !== '/' && base[0] !== '#')
274 base = '/' + base;
275 // remove the trailing slash so all other method can just do `base + fullPath`
276 // to build an href
277 return removeTrailingSlash(base);
278}
279// remove any character before the hash
280const BEFORE_HASH_RE = /^[^#]+#/;
281function createHref(base, location) {
282 return base.replace(BEFORE_HASH_RE, '#') + location;
283}
284
285function getElementPosition(el, offset) {
286 const docRect = document.documentElement.getBoundingClientRect();
287 const elRect = el.getBoundingClientRect();
288 return {
289 behavior: offset.behavior,
290 left: elRect.left - docRect.left - (offset.left || 0),
291 top: elRect.top - docRect.top - (offset.top || 0),
292 };
293}
294const computeScrollPosition = () => ({
295 left: window.pageXOffset,
296 top: window.pageYOffset,
297});
298function scrollToPosition(position) {
299 let scrollToOptions;
300 if ('el' in position) {
301 const positionEl = position.el;
302 const isIdSelector = typeof positionEl === 'string' && positionEl.startsWith('#');
303 /**
304 * `id`s can accept pretty much any characters, including CSS combinators
305 * like `>` or `~`. It's still possible to retrieve elements using
306 * `document.getElementById('~')` but it needs to be escaped when using
307 * `document.querySelector('#\\~')` for it to be valid. The only
308 * requirements for `id`s are them to be unique on the page and to not be
309 * empty (`id=""`). Because of that, when passing an id selector, it should
310 * be properly escaped for it to work with `querySelector`. We could check
311 * for the id selector to be simple (no CSS combinators `+ >~`) but that
312 * would make things inconsistent since they are valid characters for an
313 * `id` but would need to be escaped when using `querySelector`, breaking
314 * their usage and ending up in no selector returned. Selectors need to be
315 * escaped:
316 *
317 * - `#1-thing` becomes `#\31 -thing`
318 * - `#with~symbols` becomes `#with\\~symbols`
319 *
320 * - More information about the topic can be found at
321 * https://mathiasbynens.be/notes/html5-id-class.
322 * - Practical example: https://mathiasbynens.be/demo/html5-id
323 */
324 if (typeof position.el === 'string') {
325 if (!isIdSelector || !document.getElementById(position.el.slice(1))) {
326 try {
327 const foundEl = document.querySelector(position.el);
328 if (isIdSelector && foundEl) {
329 warn(`The selector "${position.el}" should be passed as "el: document.querySelector('${position.el}')" because it starts with "#".`);
330 // return to avoid other warnings
331 return;
332 }
333 }
334 catch (err) {
335 warn(`The selector "${position.el}" is invalid. If you are using an id selector, make sure to escape it. You can find more information about escaping characters in selectors at https://mathiasbynens.be/notes/css-escapes or use CSS.escape (https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape).`);
336 // return to avoid other warnings
337 return;
338 }
339 }
340 }
341 const el = typeof positionEl === 'string'
342 ? isIdSelector
343 ? document.getElementById(positionEl.slice(1))
344 : document.querySelector(positionEl)
345 : positionEl;
346 if (!el) {
347 warn(`Couldn't find element using selector "${position.el}" returned by scrollBehavior.`);
348 return;
349 }
350 scrollToOptions = getElementPosition(el, position);
351 }
352 else {
353 scrollToOptions = position;
354 }
355 if ('scrollBehavior' in document.documentElement.style)
356 window.scrollTo(scrollToOptions);
357 else {
358 window.scrollTo(scrollToOptions.left != null ? scrollToOptions.left : window.pageXOffset, scrollToOptions.top != null ? scrollToOptions.top : window.pageYOffset);
359 }
360}
361function getScrollKey(path, delta) {
362 const position = history.state ? history.state.position - delta : -1;
363 return position + path;
364}
365const scrollPositions = new Map();
366function saveScrollPosition(key, scrollPosition) {
367 scrollPositions.set(key, scrollPosition);
368}
369function getSavedScrollPosition(key) {
370 const scroll = scrollPositions.get(key);
371 // consume it so it's not used again
372 scrollPositions.delete(key);
373 return scroll;
374}
375// TODO: RFC about how to save scroll position
376/**
377 * ScrollBehavior instance used by the router to compute and restore the scroll
378 * position when navigating.
379 */
380// export interface ScrollHandler<ScrollPositionEntry extends HistoryStateValue, ScrollPosition extends ScrollPositionEntry> {
381// // returns a scroll position that can be saved in history
382// compute(): ScrollPositionEntry
383// // can take an extended ScrollPositionEntry
384// scroll(position: ScrollPosition): void
385// }
386// export const scrollHandler: ScrollHandler<ScrollPosition> = {
387// compute: computeScroll,
388// scroll: scrollToPosition,
389// }
390
391let createBaseLocation = () => location.protocol + '//' + location.host;
392/**
393 * Creates a normalized history location from a window.location object
394 * @param location -
395 */
396function createCurrentLocation(base, location) {
397 const { pathname, search, hash } = location;
398 // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
399 const hashPos = base.indexOf('#');
400 if (hashPos > -1) {
401 let slicePos = hash.includes(base.slice(hashPos))
402 ? base.slice(hashPos).length
403 : 1;
404 let pathFromHash = hash.slice(slicePos);
405 // prepend the starting slash to hash so the url starts with /#
406 if (pathFromHash[0] !== '/')
407 pathFromHash = '/' + pathFromHash;
408 return stripBase(pathFromHash, '');
409 }
410 const path = stripBase(pathname, base);
411 return path + search + hash;
412}
413function useHistoryListeners(base, historyState, currentLocation, replace) {
414 let listeners = [];
415 let teardowns = [];
416 // TODO: should it be a stack? a Dict. Check if the popstate listener
417 // can trigger twice
418 let pauseState = null;
419 const popStateHandler = ({ state, }) => {
420 const to = createCurrentLocation(base, location);
421 const from = currentLocation.value;
422 const fromState = historyState.value;
423 let delta = 0;
424 if (state) {
425 currentLocation.value = to;
426 historyState.value = state;
427 // ignore the popstate and reset the pauseState
428 if (pauseState && pauseState === from) {
429 pauseState = null;
430 return;
431 }
432 delta = fromState ? state.position - fromState.position : 0;
433 }
434 else {
435 replace(to);
436 }
437 // console.log({ deltaFromCurrent })
438 // Here we could also revert the navigation by calling history.go(-delta)
439 // this listener will have to be adapted to not trigger again and to wait for the url
440 // to be updated before triggering the listeners. Some kind of validation function would also
441 // need to be passed to the listeners so the navigation can be accepted
442 // call all listeners
443 listeners.forEach(listener => {
444 listener(currentLocation.value, from, {
445 delta,
446 type: NavigationType.pop,
447 direction: delta
448 ? delta > 0
449 ? NavigationDirection.forward
450 : NavigationDirection.back
451 : NavigationDirection.unknown,
452 });
453 });
454 };
455 function pauseListeners() {
456 pauseState = currentLocation.value;
457 }
458 function listen(callback) {
459 // setup the listener and prepare teardown callbacks
460 listeners.push(callback);
461 const teardown = () => {
462 const index = listeners.indexOf(callback);
463 if (index > -1)
464 listeners.splice(index, 1);
465 };
466 teardowns.push(teardown);
467 return teardown;
468 }
469 function beforeUnloadListener() {
470 const { history } = window;
471 if (!history.state)
472 return;
473 history.replaceState(assign({}, history.state, { scroll: computeScrollPosition() }), '');
474 }
475 function destroy() {
476 for (const teardown of teardowns)
477 teardown();
478 teardowns = [];
479 window.removeEventListener('popstate', popStateHandler);
480 window.removeEventListener('beforeunload', beforeUnloadListener);
481 }
482 // setup the listeners and prepare teardown callbacks
483 window.addEventListener('popstate', popStateHandler);
484 window.addEventListener('beforeunload', beforeUnloadListener);
485 return {
486 pauseListeners,
487 listen,
488 destroy,
489 };
490}
491/**
492 * Creates a state object
493 */
494function buildState(back, current, forward, replaced = false, computeScroll = false) {
495 return {
496 back,
497 current,
498 forward,
499 replaced,
500 position: window.history.length,
501 scroll: computeScroll ? computeScrollPosition() : null,
502 };
503}
504function useHistoryStateNavigation(base) {
505 const { history, location } = window;
506 // private variables
507 const currentLocation = {
508 value: createCurrentLocation(base, location),
509 };
510 const historyState = { value: history.state };
511 // build current history entry as this is a fresh navigation
512 if (!historyState.value) {
513 changeLocation(currentLocation.value, {
514 back: null,
515 current: currentLocation.value,
516 forward: null,
517 // the length is off by one, we need to decrease it
518 position: history.length - 1,
519 replaced: true,
520 // don't add a scroll as the user may have an anchor and we want
521 // scrollBehavior to be triggered without a saved position
522 scroll: null,
523 }, true);
524 }
525 function changeLocation(to, state, replace) {
526 /**
527 * if a base tag is provided and we are on a normal domain, we have to
528 * respect the provided `base` attribute because pushState() will use it and
529 * potentially erase anything before the `#` like at
530 * https://github.com/vuejs/router/issues/685 where a base of
531 * `/folder/#` but a base of `/` would erase the `/folder/` section. If
532 * there is no host, the `<base>` tag makes no sense and if there isn't a
533 * base tag we can just use everything after the `#`.
534 */
535 const hashIndex = base.indexOf('#');
536 const url = hashIndex > -1
537 ? (location.host && document.querySelector('base')
538 ? base
539 : base.slice(hashIndex)) + to
540 : createBaseLocation() + base + to;
541 try {
542 // BROWSER QUIRK
543 // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
544 history[replace ? 'replaceState' : 'pushState'](state, '', url);
545 historyState.value = state;
546 }
547 catch (err) {
548 {
549 warn('Error with push/replace State', err);
550 }
551 // Force the navigation, this also resets the call count
552 location[replace ? 'replace' : 'assign'](url);
553 }
554 }
555 function replace(to, data) {
556 const state = assign({}, history.state, buildState(historyState.value.back,
557 // keep back and forward entries but override current position
558 to, historyState.value.forward, true), data, { position: historyState.value.position });
559 changeLocation(to, state, true);
560 currentLocation.value = to;
561 }
562 function push(to, data) {
563 // Add to current entry the information of where we are going
564 // as well as saving the current position
565 const currentState = assign({},
566 // use current history state to gracefully handle a wrong call to
567 // history.replaceState
568 // https://github.com/vuejs/router/issues/366
569 historyState.value, history.state, {
570 forward: to,
571 scroll: computeScrollPosition(),
572 });
573 if (!history.state) {
574 warn(`history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\n` +
575 `history.replaceState(history.state, '', url)\n\n` +
576 `You can find more information at https://next.router.vuejs.org/guide/migration/#usage-of-history-state.`);
577 }
578 changeLocation(currentState.current, currentState, true);
579 const state = assign({}, buildState(currentLocation.value, to, null), { position: currentState.position + 1 }, data);
580 changeLocation(to, state, false);
581 currentLocation.value = to;
582 }
583 return {
584 location: currentLocation,
585 state: historyState,
586 push,
587 replace,
588 };
589}
590/**
591 * Creates an HTML5 history. Most common history for single page applications.
592 *
593 * @param base -
594 */
595function createWebHistory(base) {
596 base = normalizeBase(base);
597 const historyNavigation = useHistoryStateNavigation(base);
598 const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace);
599 function go(delta, triggerListeners = true) {
600 if (!triggerListeners)
601 historyListeners.pauseListeners();
602 history.go(delta);
603 }
604 const routerHistory = assign({
605 // it's overridden right after
606 location: '',
607 base,
608 go,
609 createHref: createHref.bind(null, base),
610 }, historyNavigation, historyListeners);
611 Object.defineProperty(routerHistory, 'location', {
612 enumerable: true,
613 get: () => historyNavigation.location.value,
614 });
615 Object.defineProperty(routerHistory, 'state', {
616 enumerable: true,
617 get: () => historyNavigation.state.value,
618 });
619 return routerHistory;
620}
621
622/**
623 * Creates a in-memory based history. The main purpose of this history is to handle SSR. It starts in a special location that is nowhere.
624 * It's up to the user to replace that location with the starter location by either calling `router.push` or `router.replace`.
625 *
626 * @param base - Base applied to all urls, defaults to '/'
627 * @returns a history object that can be passed to the router constructor
628 */
629function createMemoryHistory(base = '') {
630 let listeners = [];
631 let queue = [START];
632 let position = 0;
633 base = normalizeBase(base);
634 function setLocation(location) {
635 position++;
636 if (position === queue.length) {
637 // we are at the end, we can simply append a new entry
638 queue.push(location);
639 }
640 else {
641 // we are in the middle, we remove everything from here in the queue
642 queue.splice(position);
643 queue.push(location);
644 }
645 }
646 function triggerListeners(to, from, { direction, delta }) {
647 const info = {
648 direction,
649 delta,
650 type: NavigationType.pop,
651 };
652 for (const callback of listeners) {
653 callback(to, from, info);
654 }
655 }
656 const routerHistory = {
657 // rewritten by Object.defineProperty
658 location: START,
659 // TODO: should be kept in queue
660 state: {},
661 base,
662 createHref: createHref.bind(null, base),
663 replace(to) {
664 // remove current entry and decrement position
665 queue.splice(position--, 1);
666 setLocation(to);
667 },
668 push(to, data) {
669 setLocation(to);
670 },
671 listen(callback) {
672 listeners.push(callback);
673 return () => {
674 const index = listeners.indexOf(callback);
675 if (index > -1)
676 listeners.splice(index, 1);
677 };
678 },
679 destroy() {
680 listeners = [];
681 queue = [START];
682 position = 0;
683 },
684 go(delta, shouldTrigger = true) {
685 const from = this.location;
686 const direction =
687 // we are considering delta === 0 going forward, but in abstract mode
688 // using 0 for the delta doesn't make sense like it does in html5 where
689 // it reloads the page
690 delta < 0 ? NavigationDirection.back : NavigationDirection.forward;
691 position = Math.max(0, Math.min(position + delta, queue.length - 1));
692 if (shouldTrigger) {
693 triggerListeners(this.location, from, {
694 direction,
695 delta,
696 });
697 }
698 },
699 };
700 Object.defineProperty(routerHistory, 'location', {
701 enumerable: true,
702 get: () => queue[position],
703 });
704 return routerHistory;
705}
706
707/**
708 * Creates a hash history. Useful for web applications with no host (e.g.
709 * `file://`) or when configuring a server to handle any URL is not possible.
710 *
711 * @param base - optional base to provide. Defaults to `location.pathname +
712 * location.search` If there is a `<base>` tag in the `head`, its value will be
713 * ignored in favor of this parameter **but note it affects all the
714 * history.pushState() calls**, meaning that if you use a `<base>` tag, it's
715 * `href` value **has to match this parameter** (ignoring anything after the
716 * `#`).
717 *
718 * @example
719 * ```js
720 * // at https://example.com/folder
721 * createWebHashHistory() // gives a url of `https://example.com/folder#`
722 * createWebHashHistory('/folder/') // gives a url of `https://example.com/folder/#`
723 * // if the `#` is provided in the base, it won't be added by `createWebHashHistory`
724 * createWebHashHistory('/folder/#/app/') // gives a url of `https://example.com/folder/#/app/`
725 * // you should avoid doing this because it changes the original url and breaks copying urls
726 * createWebHashHistory('/other-folder/') // gives a url of `https://example.com/other-folder/#`
727 *
728 * // at file:///usr/etc/folder/index.html
729 * // for locations with no `host`, the base is ignored
730 * createWebHashHistory('/iAmIgnored') // gives a url of `file:///usr/etc/folder/index.html#`
731 * ```
732 */
733function createWebHashHistory(base) {
734 // Make sure this implementation is fine in terms of encoding, specially for IE11
735 // for `file://`, directly use the pathname and ignore the base
736 // location.pathname contains an initial `/` even at the root: `https://example.com`
737 base = location.host ? base || location.pathname + location.search : '';
738 // allow the user to provide a `#` in the middle: `/base/#/app`
739 if (!base.includes('#'))
740 base += '#';
741 if (!base.endsWith('#/') && !base.endsWith('#')) {
742 warn(`A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, '#')}".`);
743 }
744 return createWebHistory(base);
745}
746
747function isRouteLocation(route) {
748 return typeof route === 'string' || (route && typeof route === 'object');
749}
750function isRouteName(name) {
751 return typeof name === 'string' || typeof name === 'symbol';
752}
753
754/**
755 * Initial route location where the router is. Can be used in navigation guards
756 * to differentiate the initial navigation.
757 *
758 * @example
759 * ```js
760 * import { START_LOCATION } from 'vue-router'
761 *
762 * router.beforeEach((to, from) => {
763 * if (from === START_LOCATION) {
764 * // initial navigation
765 * }
766 * })
767 * ```
768 */
769const START_LOCATION_NORMALIZED = {
770 path: '/',
771 name: undefined,
772 params: {},
773 query: {},
774 hash: '',
775 fullPath: '/',
776 matched: [],
777 meta: {},
778 redirectedFrom: undefined,
779};
780
781const NavigationFailureSymbol = /*#__PURE__*/ PolySymbol('navigation failure' );
782/**
783 * Enumeration with all possible types for navigation failures. Can be passed to
784 * {@link isNavigationFailure} to check for specific failures.
785 */
786exports.NavigationFailureType = void 0;
787(function (NavigationFailureType) {
788 /**
789 * An aborted navigation is a navigation that failed because a navigation
790 * guard returned `false` or called `next(false)`
791 */
792 NavigationFailureType[NavigationFailureType["aborted"] = 4] = "aborted";
793 /**
794 * A cancelled navigation is a navigation that failed because a more recent
795 * navigation finished started (not necessarily finished).
796 */
797 NavigationFailureType[NavigationFailureType["cancelled"] = 8] = "cancelled";
798 /**
799 * A duplicated navigation is a navigation that failed because it was
800 * initiated while already being at the exact same location.
801 */
802 NavigationFailureType[NavigationFailureType["duplicated"] = 16] = "duplicated";
803})(exports.NavigationFailureType || (exports.NavigationFailureType = {}));
804// DEV only debug messages
805const ErrorTypeMessages = {
806 [1 /* MATCHER_NOT_FOUND */]({ location, currentLocation }) {
807 return `No match for\n ${JSON.stringify(location)}${currentLocation
808 ? '\nwhile being at\n' + JSON.stringify(currentLocation)
809 : ''}`;
810 },
811 [2 /* NAVIGATION_GUARD_REDIRECT */]({ from, to, }) {
812 return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`;
813 },
814 [4 /* NAVIGATION_ABORTED */]({ from, to }) {
815 return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`;
816 },
817 [8 /* NAVIGATION_CANCELLED */]({ from, to }) {
818 return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`;
819 },
820 [16 /* NAVIGATION_DUPLICATED */]({ from, to }) {
821 return `Avoided redundant navigation to current location: "${from.fullPath}".`;
822 },
823};
824function createRouterError(type, params) {
825 // keep full error messages in cjs versions
826 {
827 return assign(new Error(ErrorTypeMessages[type](params)), {
828 type,
829 [NavigationFailureSymbol]: true,
830 }, params);
831 }
832}
833function isNavigationFailure(error, type) {
834 return (error instanceof Error &&
835 NavigationFailureSymbol in error &&
836 (type == null || !!(error.type & type)));
837}
838const propertiesToLog = ['params', 'query', 'hash'];
839function stringifyRoute(to) {
840 if (typeof to === 'string')
841 return to;
842 if ('path' in to)
843 return to.path;
844 const location = {};
845 for (const key of propertiesToLog) {
846 if (key in to)
847 location[key] = to[key];
848 }
849 return JSON.stringify(location, null, 2);
850}
851
852// default pattern for a param: non greedy everything but /
853const BASE_PARAM_PATTERN = '[^/]+?';
854const BASE_PATH_PARSER_OPTIONS = {
855 sensitive: false,
856 strict: false,
857 start: true,
858 end: true,
859};
860// Special Regex characters that must be escaped in static tokens
861const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
862/**
863 * Creates a path parser from an array of Segments (a segment is an array of Tokens)
864 *
865 * @param segments - array of segments returned by tokenizePath
866 * @param extraOptions - optional options for the regexp
867 * @returns a PathParser
868 */
869function tokensToParser(segments, extraOptions) {
870 const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions);
871 // the amount of scores is the same as the length of segments except for the root segment "/"
872 const score = [];
873 // the regexp as a string
874 let pattern = options.start ? '^' : '';
875 // extracted keys
876 const keys = [];
877 for (const segment of segments) {
878 // the root segment needs special treatment
879 const segmentScores = segment.length ? [] : [90 /* Root */];
880 // allow trailing slash
881 if (options.strict && !segment.length)
882 pattern += '/';
883 for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
884 const token = segment[tokenIndex];
885 // resets the score if we are inside a sub segment /:a-other-:b
886 let subSegmentScore = 40 /* Segment */ +
887 (options.sensitive ? 0.25 /* BonusCaseSensitive */ : 0);
888 if (token.type === 0 /* Static */) {
889 // prepend the slash if we are starting a new segment
890 if (!tokenIndex)
891 pattern += '/';
892 pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
893 subSegmentScore += 40 /* Static */;
894 }
895 else if (token.type === 1 /* Param */) {
896 const { value, repeatable, optional, regexp } = token;
897 keys.push({
898 name: value,
899 repeatable,
900 optional,
901 });
902 const re = regexp ? regexp : BASE_PARAM_PATTERN;
903 // the user provided a custom regexp /:id(\\d+)
904 if (re !== BASE_PARAM_PATTERN) {
905 subSegmentScore += 10 /* BonusCustomRegExp */;
906 // make sure the regexp is valid before using it
907 try {
908 new RegExp(`(${re})`);
909 }
910 catch (err) {
911 throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` +
912 err.message);
913 }
914 }
915 // when we repeat we must take care of the repeating leading slash
916 let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
917 // prepend the slash if we are starting a new segment
918 if (!tokenIndex)
919 subPattern =
920 // avoid an optional / if there are more segments e.g. /:p?-static
921 // or /:p?-:p2
922 optional && segment.length < 2
923 ? `(?:/${subPattern})`
924 : '/' + subPattern;
925 if (optional)
926 subPattern += '?';
927 pattern += subPattern;
928 subSegmentScore += 20 /* Dynamic */;
929 if (optional)
930 subSegmentScore += -8 /* BonusOptional */;
931 if (repeatable)
932 subSegmentScore += -20 /* BonusRepeatable */;
933 if (re === '.*')
934 subSegmentScore += -50 /* BonusWildcard */;
935 }
936 segmentScores.push(subSegmentScore);
937 }
938 // an empty array like /home/ -> [[{home}], []]
939 // if (!segment.length) pattern += '/'
940 score.push(segmentScores);
941 }
942 // only apply the strict bonus to the last score
943 if (options.strict && options.end) {
944 const i = score.length - 1;
945 score[i][score[i].length - 1] += 0.7000000000000001 /* BonusStrict */;
946 }
947 // TODO: dev only warn double trailing slash
948 if (!options.strict)
949 pattern += '/?';
950 if (options.end)
951 pattern += '$';
952 // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
953 else if (options.strict)
954 pattern += '(?:/|$)';
955 const re = new RegExp(pattern, options.sensitive ? '' : 'i');
956 function parse(path) {
957 const match = path.match(re);
958 const params = {};
959 if (!match)
960 return null;
961 for (let i = 1; i < match.length; i++) {
962 const value = match[i] || '';
963 const key = keys[i - 1];
964 params[key.name] = value && key.repeatable ? value.split('/') : value;
965 }
966 return params;
967 }
968 function stringify(params) {
969 let path = '';
970 // for optional parameters to allow to be empty
971 let avoidDuplicatedSlash = false;
972 for (const segment of segments) {
973 if (!avoidDuplicatedSlash || !path.endsWith('/'))
974 path += '/';
975 avoidDuplicatedSlash = false;
976 for (const token of segment) {
977 if (token.type === 0 /* Static */) {
978 path += token.value;
979 }
980 else if (token.type === 1 /* Param */) {
981 const { value, repeatable, optional } = token;
982 const param = value in params ? params[value] : '';
983 if (Array.isArray(param) && !repeatable)
984 throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`);
985 const text = Array.isArray(param) ? param.join('/') : param;
986 if (!text) {
987 if (optional) {
988 // if we have more than one optional param like /:a?-static and there are more segments, we don't need to
989 // care about the optional param
990 if (segment.length < 2 && segments.length > 1) {
991 // remove the last slash as we could be at the end
992 if (path.endsWith('/'))
993 path = path.slice(0, -1);
994 // do not append a slash on the next iteration
995 else
996 avoidDuplicatedSlash = true;
997 }
998 }
999 else
1000 throw new Error(`Missing required param "${value}"`);
1001 }
1002 path += text;
1003 }
1004 }
1005 }
1006 return path;
1007 }
1008 return {
1009 re,
1010 score,
1011 keys,
1012 parse,
1013 stringify,
1014 };
1015}
1016/**
1017 * Compares an array of numbers as used in PathParser.score and returns a
1018 * number. This function can be used to `sort` an array
1019 *
1020 * @param a - first array of numbers
1021 * @param b - second array of numbers
1022 * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
1023 * should be sorted first
1024 */
1025function compareScoreArray(a, b) {
1026 let i = 0;
1027 while (i < a.length && i < b.length) {
1028 const diff = b[i] - a[i];
1029 // only keep going if diff === 0
1030 if (diff)
1031 return diff;
1032 i++;
1033 }
1034 // if the last subsegment was Static, the shorter segments should be sorted first
1035 // otherwise sort the longest segment first
1036 if (a.length < b.length) {
1037 return a.length === 1 && a[0] === 40 /* Static */ + 40 /* Segment */
1038 ? -1
1039 : 1;
1040 }
1041 else if (a.length > b.length) {
1042 return b.length === 1 && b[0] === 40 /* Static */ + 40 /* Segment */
1043 ? 1
1044 : -1;
1045 }
1046 return 0;
1047}
1048/**
1049 * Compare function that can be used with `sort` to sort an array of PathParser
1050 *
1051 * @param a - first PathParser
1052 * @param b - second PathParser
1053 * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
1054 */
1055function comparePathParserScore(a, b) {
1056 let i = 0;
1057 const aScore = a.score;
1058 const bScore = b.score;
1059 while (i < aScore.length && i < bScore.length) {
1060 const comp = compareScoreArray(aScore[i], bScore[i]);
1061 // do not return if both are equal
1062 if (comp)
1063 return comp;
1064 i++;
1065 }
1066 // if a and b share the same score entries but b has more, sort b first
1067 return bScore.length - aScore.length;
1068 // this is the ternary version
1069 // return aScore.length < bScore.length
1070 // ? 1
1071 // : aScore.length > bScore.length
1072 // ? -1
1073 // : 0
1074}
1075
1076const ROOT_TOKEN = {
1077 type: 0 /* Static */,
1078 value: '',
1079};
1080const VALID_PARAM_RE = /[a-zA-Z0-9_]/;
1081// After some profiling, the cache seems to be unnecessary because tokenizePath
1082// (the slowest part of adding a route) is very fast
1083// const tokenCache = new Map<string, Token[][]>()
1084function tokenizePath(path) {
1085 if (!path)
1086 return [[]];
1087 if (path === '/')
1088 return [[ROOT_TOKEN]];
1089 if (!path.startsWith('/')) {
1090 throw new Error(`Route paths should start with a "/": "${path}" should be "/${path}".`
1091 );
1092 }
1093 // if (tokenCache.has(path)) return tokenCache.get(path)!
1094 function crash(message) {
1095 throw new Error(`ERR (${state})/"${buffer}": ${message}`);
1096 }
1097 let state = 0 /* Static */;
1098 let previousState = state;
1099 const tokens = [];
1100 // the segment will always be valid because we get into the initial state
1101 // with the leading /
1102 let segment;
1103 function finalizeSegment() {
1104 if (segment)
1105 tokens.push(segment);
1106 segment = [];
1107 }
1108 // index on the path
1109 let i = 0;
1110 // char at index
1111 let char;
1112 // buffer of the value read
1113 let buffer = '';
1114 // custom regexp for a param
1115 let customRe = '';
1116 function consumeBuffer() {
1117 if (!buffer)
1118 return;
1119 if (state === 0 /* Static */) {
1120 segment.push({
1121 type: 0 /* Static */,
1122 value: buffer,
1123 });
1124 }
1125 else if (state === 1 /* Param */ ||
1126 state === 2 /* ParamRegExp */ ||
1127 state === 3 /* ParamRegExpEnd */) {
1128 if (segment.length > 1 && (char === '*' || char === '+'))
1129 crash(`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`);
1130 segment.push({
1131 type: 1 /* Param */,
1132 value: buffer,
1133 regexp: customRe,
1134 repeatable: char === '*' || char === '+',
1135 optional: char === '*' || char === '?',
1136 });
1137 }
1138 else {
1139 crash('Invalid state to consume buffer');
1140 }
1141 buffer = '';
1142 }
1143 function addCharToBuffer() {
1144 buffer += char;
1145 }
1146 while (i < path.length) {
1147 char = path[i++];
1148 if (char === '\\' && state !== 2 /* ParamRegExp */) {
1149 previousState = state;
1150 state = 4 /* EscapeNext */;
1151 continue;
1152 }
1153 switch (state) {
1154 case 0 /* Static */:
1155 if (char === '/') {
1156 if (buffer) {
1157 consumeBuffer();
1158 }
1159 finalizeSegment();
1160 }
1161 else if (char === ':') {
1162 consumeBuffer();
1163 state = 1 /* Param */;
1164 }
1165 else {
1166 addCharToBuffer();
1167 }
1168 break;
1169 case 4 /* EscapeNext */:
1170 addCharToBuffer();
1171 state = previousState;
1172 break;
1173 case 1 /* Param */:
1174 if (char === '(') {
1175 state = 2 /* ParamRegExp */;
1176 }
1177 else if (VALID_PARAM_RE.test(char)) {
1178 addCharToBuffer();
1179 }
1180 else {
1181 consumeBuffer();
1182 state = 0 /* Static */;
1183 // go back one character if we were not modifying
1184 if (char !== '*' && char !== '?' && char !== '+')
1185 i--;
1186 }
1187 break;
1188 case 2 /* ParamRegExp */:
1189 // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
1190 // it already works by escaping the closing )
1191 // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
1192 // is this really something people need since you can also write
1193 // /prefix_:p()_suffix
1194 if (char === ')') {
1195 // handle the escaped )
1196 if (customRe[customRe.length - 1] == '\\')
1197 customRe = customRe.slice(0, -1) + char;
1198 else
1199 state = 3 /* ParamRegExpEnd */;
1200 }
1201 else {
1202 customRe += char;
1203 }
1204 break;
1205 case 3 /* ParamRegExpEnd */:
1206 // same as finalizing a param
1207 consumeBuffer();
1208 state = 0 /* Static */;
1209 // go back one character if we were not modifying
1210 if (char !== '*' && char !== '?' && char !== '+')
1211 i--;
1212 customRe = '';
1213 break;
1214 default:
1215 crash('Unknown state');
1216 break;
1217 }
1218 }
1219 if (state === 2 /* ParamRegExp */)
1220 crash(`Unfinished custom RegExp for param "${buffer}"`);
1221 consumeBuffer();
1222 finalizeSegment();
1223 // tokenCache.set(path, tokens)
1224 return tokens;
1225}
1226
1227function createRouteRecordMatcher(record, parent, options) {
1228 const parser = tokensToParser(tokenizePath(record.path), options);
1229 // warn against params with the same name
1230 {
1231 const existingKeys = new Set();
1232 for (const key of parser.keys) {
1233 if (existingKeys.has(key.name))
1234 warn(`Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`);
1235 existingKeys.add(key.name);
1236 }
1237 }
1238 const matcher = assign(parser, {
1239 record,
1240 parent,
1241 // these needs to be populated by the parent
1242 children: [],
1243 alias: [],
1244 });
1245 if (parent) {
1246 // both are aliases or both are not aliases
1247 // we don't want to mix them because the order is used when
1248 // passing originalRecord in Matcher.addRoute
1249 if (!matcher.record.aliasOf === !parent.record.aliasOf)
1250 parent.children.push(matcher);
1251 }
1252 return matcher;
1253}
1254
1255/**
1256 * Creates a Router Matcher.
1257 *
1258 * @internal
1259 * @param routes - array of initial routes
1260 * @param globalOptions - global route options
1261 */
1262function createRouterMatcher(routes, globalOptions) {
1263 // normalized ordered array of matchers
1264 const matchers = [];
1265 const matcherMap = new Map();
1266 globalOptions = mergeOptions({ strict: false, end: true, sensitive: false }, globalOptions);
1267 function getRecordMatcher(name) {
1268 return matcherMap.get(name);
1269 }
1270 function addRoute(record, parent, originalRecord) {
1271 // used later on to remove by name
1272 const isRootAdd = !originalRecord;
1273 const mainNormalizedRecord = normalizeRouteRecord(record);
1274 // we might be the child of an alias
1275 mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record;
1276 const options = mergeOptions(globalOptions, record);
1277 // generate an array of records to correctly handle aliases
1278 const normalizedRecords = [
1279 mainNormalizedRecord,
1280 ];
1281 if ('alias' in record) {
1282 const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias;
1283 for (const alias of aliases) {
1284 normalizedRecords.push(assign({}, mainNormalizedRecord, {
1285 // this allows us to hold a copy of the `components` option
1286 // so that async components cache is hold on the original record
1287 components: originalRecord
1288 ? originalRecord.record.components
1289 : mainNormalizedRecord.components,
1290 path: alias,
1291 // we might be the child of an alias
1292 aliasOf: originalRecord
1293 ? originalRecord.record
1294 : mainNormalizedRecord,
1295 // the aliases are always of the same kind as the original since they
1296 // are defined on the same record
1297 }));
1298 }
1299 }
1300 let matcher;
1301 let originalMatcher;
1302 for (const normalizedRecord of normalizedRecords) {
1303 const { path } = normalizedRecord;
1304 // Build up the path for nested routes if the child isn't an absolute
1305 // route. Only add the / delimiter if the child path isn't empty and if the
1306 // parent path doesn't have a trailing slash
1307 if (parent && path[0] !== '/') {
1308 const parentPath = parent.record.path;
1309 const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/';
1310 normalizedRecord.path =
1311 parent.record.path + (path && connectingSlash + path);
1312 }
1313 if (normalizedRecord.path === '*') {
1314 throw new Error('Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
1315 'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.');
1316 }
1317 // create the object before hand so it can be passed to children
1318 matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
1319 if (parent && path[0] === '/')
1320 checkMissingParamsInAbsolutePath(matcher, parent);
1321 // if we are an alias we must tell the original record that we exist
1322 // so we can be removed
1323 if (originalRecord) {
1324 originalRecord.alias.push(matcher);
1325 {
1326 checkSameParams(originalRecord, matcher);
1327 }
1328 }
1329 else {
1330 // otherwise, the first record is the original and others are aliases
1331 originalMatcher = originalMatcher || matcher;
1332 if (originalMatcher !== matcher)
1333 originalMatcher.alias.push(matcher);
1334 // remove the route if named and only for the top record (avoid in nested calls)
1335 // this works because the original record is the first one
1336 if (isRootAdd && record.name && !isAliasRecord(matcher))
1337 removeRoute(record.name);
1338 }
1339 if ('children' in mainNormalizedRecord) {
1340 const children = mainNormalizedRecord.children;
1341 for (let i = 0; i < children.length; i++) {
1342 addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
1343 }
1344 }
1345 // if there was no original record, then the first one was not an alias and all
1346 // other alias (if any) need to reference this record when adding children
1347 originalRecord = originalRecord || matcher;
1348 // TODO: add normalized records for more flexibility
1349 // if (parent && isAliasRecord(originalRecord)) {
1350 // parent.children.push(originalRecord)
1351 // }
1352 insertMatcher(matcher);
1353 }
1354 return originalMatcher
1355 ? () => {
1356 // since other matchers are aliases, they should be removed by the original matcher
1357 removeRoute(originalMatcher);
1358 }
1359 : noop;
1360 }
1361 function removeRoute(matcherRef) {
1362 if (isRouteName(matcherRef)) {
1363 const matcher = matcherMap.get(matcherRef);
1364 if (matcher) {
1365 matcherMap.delete(matcherRef);
1366 matchers.splice(matchers.indexOf(matcher), 1);
1367 matcher.children.forEach(removeRoute);
1368 matcher.alias.forEach(removeRoute);
1369 }
1370 }
1371 else {
1372 const index = matchers.indexOf(matcherRef);
1373 if (index > -1) {
1374 matchers.splice(index, 1);
1375 if (matcherRef.record.name)
1376 matcherMap.delete(matcherRef.record.name);
1377 matcherRef.children.forEach(removeRoute);
1378 matcherRef.alias.forEach(removeRoute);
1379 }
1380 }
1381 }
1382 function getRoutes() {
1383 return matchers;
1384 }
1385 function insertMatcher(matcher) {
1386 let i = 0;
1387 while (i < matchers.length &&
1388 comparePathParserScore(matcher, matchers[i]) >= 0 &&
1389 // Adding children with empty path should still appear before the parent
1390 // https://github.com/vuejs/router/issues/1124
1391 (matcher.record.path !== matchers[i].record.path ||
1392 !isRecordChildOf(matcher, matchers[i])))
1393 i++;
1394 matchers.splice(i, 0, matcher);
1395 // only add the original record to the name map
1396 if (matcher.record.name && !isAliasRecord(matcher))
1397 matcherMap.set(matcher.record.name, matcher);
1398 }
1399 function resolve(location, currentLocation) {
1400 let matcher;
1401 let params = {};
1402 let path;
1403 let name;
1404 if ('name' in location && location.name) {
1405 matcher = matcherMap.get(location.name);
1406 if (!matcher)
1407 throw createRouterError(1 /* MATCHER_NOT_FOUND */, {
1408 location,
1409 });
1410 name = matcher.record.name;
1411 params = assign(
1412 // paramsFromLocation is a new object
1413 paramsFromLocation(currentLocation.params,
1414 // only keep params that exist in the resolved location
1415 // TODO: only keep optional params coming from a parent record
1416 matcher.keys.filter(k => !k.optional).map(k => k.name)), location.params);
1417 // throws if cannot be stringified
1418 path = matcher.stringify(params);
1419 }
1420 else if ('path' in location) {
1421 // no need to resolve the path with the matcher as it was provided
1422 // this also allows the user to control the encoding
1423 path = location.path;
1424 if (!path.startsWith('/')) {
1425 warn(`The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/router.`);
1426 }
1427 matcher = matchers.find(m => m.re.test(path));
1428 // matcher should have a value after the loop
1429 if (matcher) {
1430 // TODO: dev warning of unused params if provided
1431 // we know the matcher works because we tested the regexp
1432 params = matcher.parse(path);
1433 name = matcher.record.name;
1434 }
1435 // location is a relative path
1436 }
1437 else {
1438 // match by name or path of current route
1439 matcher = currentLocation.name
1440 ? matcherMap.get(currentLocation.name)
1441 : matchers.find(m => m.re.test(currentLocation.path));
1442 if (!matcher)
1443 throw createRouterError(1 /* MATCHER_NOT_FOUND */, {
1444 location,
1445 currentLocation,
1446 });
1447 name = matcher.record.name;
1448 // since we are navigating to the same location, we don't need to pick the
1449 // params like when `name` is provided
1450 params = assign({}, currentLocation.params, location.params);
1451 path = matcher.stringify(params);
1452 }
1453 const matched = [];
1454 let parentMatcher = matcher;
1455 while (parentMatcher) {
1456 // reversed order so parents are at the beginning
1457 matched.unshift(parentMatcher.record);
1458 parentMatcher = parentMatcher.parent;
1459 }
1460 return {
1461 name,
1462 path,
1463 params,
1464 matched,
1465 meta: mergeMetaFields(matched),
1466 };
1467 }
1468 // add initial routes
1469 routes.forEach(route => addRoute(route));
1470 return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher };
1471}
1472function paramsFromLocation(params, keys) {
1473 const newParams = {};
1474 for (const key of keys) {
1475 if (key in params)
1476 newParams[key] = params[key];
1477 }
1478 return newParams;
1479}
1480/**
1481 * Normalizes a RouteRecordRaw. Creates a copy
1482 *
1483 * @param record
1484 * @returns the normalized version
1485 */
1486function normalizeRouteRecord(record) {
1487 return {
1488 path: record.path,
1489 redirect: record.redirect,
1490 name: record.name,
1491 meta: record.meta || {},
1492 aliasOf: undefined,
1493 beforeEnter: record.beforeEnter,
1494 props: normalizeRecordProps(record),
1495 children: record.children || [],
1496 instances: {},
1497 leaveGuards: new Set(),
1498 updateGuards: new Set(),
1499 enterCallbacks: {},
1500 components: 'components' in record
1501 ? record.components || {}
1502 : { default: record.component },
1503 };
1504}
1505/**
1506 * Normalize the optional `props` in a record to always be an object similar to
1507 * components. Also accept a boolean for components.
1508 * @param record
1509 */
1510function normalizeRecordProps(record) {
1511 const propsObject = {};
1512 // props does not exist on redirect records but we can set false directly
1513 const props = record.props || false;
1514 if ('component' in record) {
1515 propsObject.default = props;
1516 }
1517 else {
1518 // NOTE: we could also allow a function to be applied to every component.
1519 // Would need user feedback for use cases
1520 for (const name in record.components)
1521 propsObject[name] = typeof props === 'boolean' ? props : props[name];
1522 }
1523 return propsObject;
1524}
1525/**
1526 * Checks if a record or any of its parent is an alias
1527 * @param record
1528 */
1529function isAliasRecord(record) {
1530 while (record) {
1531 if (record.record.aliasOf)
1532 return true;
1533 record = record.parent;
1534 }
1535 return false;
1536}
1537/**
1538 * Merge meta fields of an array of records
1539 *
1540 * @param matched - array of matched records
1541 */
1542function mergeMetaFields(matched) {
1543 return matched.reduce((meta, record) => assign(meta, record.meta), {});
1544}
1545function mergeOptions(defaults, partialOptions) {
1546 const options = {};
1547 for (const key in defaults) {
1548 options[key] = key in partialOptions ? partialOptions[key] : defaults[key];
1549 }
1550 return options;
1551}
1552function isSameParam(a, b) {
1553 return (a.name === b.name &&
1554 a.optional === b.optional &&
1555 a.repeatable === b.repeatable);
1556}
1557/**
1558 * Check if a path and its alias have the same required params
1559 *
1560 * @param a - original record
1561 * @param b - alias record
1562 */
1563function checkSameParams(a, b) {
1564 for (const key of a.keys) {
1565 if (!key.optional && !b.keys.find(isSameParam.bind(null, key)))
1566 return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" should have the exact same param named "${key.name}"`);
1567 }
1568 for (const key of b.keys) {
1569 if (!key.optional && !a.keys.find(isSameParam.bind(null, key)))
1570 return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" should have the exact same param named "${key.name}"`);
1571 }
1572}
1573function checkMissingParamsInAbsolutePath(record, parent) {
1574 for (const key of parent.keys) {
1575 if (!record.keys.find(isSameParam.bind(null, key)))
1576 return warn(`Absolute path "${record.record.path}" should have the exact same param named "${key.name}" as its parent "${parent.record.path}".`);
1577 }
1578}
1579function isRecordChildOf(record, parent) {
1580 return parent.children.some(child => child === record || isRecordChildOf(record, child));
1581}
1582
1583/**
1584 * Encoding Rules ␣ = Space Path: ␣ " < > # ? { } Query: ␣ " < > # & = Hash: ␣ "
1585 * < > `
1586 *
1587 * On top of that, the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2)
1588 * defines some extra characters to be encoded. Most browsers do not encode them
1589 * in encodeURI https://github.com/whatwg/url/issues/369, so it may be safer to
1590 * also encode `!'()*`. Leaving unencoded only ASCII alphanumeric(`a-zA-Z0-9`)
1591 * plus `-._~`. This extra safety should be applied to query by patching the
1592 * string returned by encodeURIComponent encodeURI also encodes `[\]^`. `\`
1593 * should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a `\`
1594 * into a `/` if directly typed in. The _backtick_ (`````) should also be
1595 * encoded everywhere because some browsers like FF encode it when directly
1596 * written while others don't. Safari and IE don't encode ``"<>{}``` in hash.
1597 */
1598// const EXTRA_RESERVED_RE = /[!'()*]/g
1599// const encodeReservedReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16)
1600const HASH_RE = /#/g; // %23
1601const AMPERSAND_RE = /&/g; // %26
1602const SLASH_RE = /\//g; // %2F
1603const EQUAL_RE = /=/g; // %3D
1604const IM_RE = /\?/g; // %3F
1605const PLUS_RE = /\+/g; // %2B
1606/**
1607 * NOTE: It's not clear to me if we should encode the + symbol in queries, it
1608 * seems to be less flexible than not doing so and I can't find out the legacy
1609 * systems requiring this for regular requests like text/html. In the standard,
1610 * the encoding of the plus character is only mentioned for
1611 * application/x-www-form-urlencoded
1612 * (https://url.spec.whatwg.org/#urlencoded-parsing) and most browsers seems lo
1613 * leave the plus character as is in queries. To be more flexible, we allow the
1614 * plus character on the query but it can also be manually encoded by the user.
1615 *
1616 * Resources:
1617 * - https://url.spec.whatwg.org/#urlencoded-parsing
1618 * - https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20
1619 */
1620const ENC_BRACKET_OPEN_RE = /%5B/g; // [
1621const ENC_BRACKET_CLOSE_RE = /%5D/g; // ]
1622const ENC_CARET_RE = /%5E/g; // ^
1623const ENC_BACKTICK_RE = /%60/g; // `
1624const ENC_CURLY_OPEN_RE = /%7B/g; // {
1625const ENC_PIPE_RE = /%7C/g; // |
1626const ENC_CURLY_CLOSE_RE = /%7D/g; // }
1627const ENC_SPACE_RE = /%20/g; // }
1628/**
1629 * Encode characters that need to be encoded on the path, search and hash
1630 * sections of the URL.
1631 *
1632 * @internal
1633 * @param text - string to encode
1634 * @returns encoded string
1635 */
1636function commonEncode(text) {
1637 return encodeURI('' + text)
1638 .replace(ENC_PIPE_RE, '|')
1639 .replace(ENC_BRACKET_OPEN_RE, '[')
1640 .replace(ENC_BRACKET_CLOSE_RE, ']');
1641}
1642/**
1643 * Encode characters that need to be encoded on the hash section of the URL.
1644 *
1645 * @param text - string to encode
1646 * @returns encoded string
1647 */
1648function encodeHash(text) {
1649 return commonEncode(text)
1650 .replace(ENC_CURLY_OPEN_RE, '{')
1651 .replace(ENC_CURLY_CLOSE_RE, '}')
1652 .replace(ENC_CARET_RE, '^');
1653}
1654/**
1655 * Encode characters that need to be encoded query values on the query
1656 * section of the URL.
1657 *
1658 * @param text - string to encode
1659 * @returns encoded string
1660 */
1661function encodeQueryValue(text) {
1662 return (commonEncode(text)
1663 // Encode the space as +, encode the + to differentiate it from the space
1664 .replace(PLUS_RE, '%2B')
1665 .replace(ENC_SPACE_RE, '+')
1666 .replace(HASH_RE, '%23')
1667 .replace(AMPERSAND_RE, '%26')
1668 .replace(ENC_BACKTICK_RE, '`')
1669 .replace(ENC_CURLY_OPEN_RE, '{')
1670 .replace(ENC_CURLY_CLOSE_RE, '}')
1671 .replace(ENC_CARET_RE, '^'));
1672}
1673/**
1674 * Like `encodeQueryValue` but also encodes the `=` character.
1675 *
1676 * @param text - string to encode
1677 */
1678function encodeQueryKey(text) {
1679 return encodeQueryValue(text).replace(EQUAL_RE, '%3D');
1680}
1681/**
1682 * Encode characters that need to be encoded on the path section of the URL.
1683 *
1684 * @param text - string to encode
1685 * @returns encoded string
1686 */
1687function encodePath(text) {
1688 return commonEncode(text).replace(HASH_RE, '%23').replace(IM_RE, '%3F');
1689}
1690/**
1691 * Encode characters that need to be encoded on the path section of the URL as a
1692 * param. This function encodes everything {@link encodePath} does plus the
1693 * slash (`/`) character. If `text` is `null` or `undefined`, returns an empty
1694 * string instead.
1695 *
1696 * @param text - string to encode
1697 * @returns encoded string
1698 */
1699function encodeParam(text) {
1700 return text == null ? '' : encodePath(text).replace(SLASH_RE, '%2F');
1701}
1702/**
1703 * Decode text using `decodeURIComponent`. Returns the original text if it
1704 * fails.
1705 *
1706 * @param text - string to decode
1707 * @returns decoded string
1708 */
1709function decode(text) {
1710 try {
1711 return decodeURIComponent('' + text);
1712 }
1713 catch (err) {
1714 warn(`Error decoding "${text}". Using original value`);
1715 }
1716 return '' + text;
1717}
1718
1719/**
1720 * Transforms a queryString into a {@link LocationQuery} object. Accept both, a
1721 * version with the leading `?` and without Should work as URLSearchParams
1722
1723 * @internal
1724 *
1725 * @param search - search string to parse
1726 * @returns a query object
1727 */
1728function parseQuery(search) {
1729 const query = {};
1730 // avoid creating an object with an empty key and empty value
1731 // because of split('&')
1732 if (search === '' || search === '?')
1733 return query;
1734 const hasLeadingIM = search[0] === '?';
1735 const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&');
1736 for (let i = 0; i < searchParams.length; ++i) {
1737 // pre decode the + into space
1738 const searchParam = searchParams[i].replace(PLUS_RE, ' ');
1739 // allow the = character
1740 const eqPos = searchParam.indexOf('=');
1741 const key = decode(eqPos < 0 ? searchParam : searchParam.slice(0, eqPos));
1742 const value = eqPos < 0 ? null : decode(searchParam.slice(eqPos + 1));
1743 if (key in query) {
1744 // an extra variable for ts types
1745 let currentValue = query[key];
1746 if (!Array.isArray(currentValue)) {
1747 currentValue = query[key] = [currentValue];
1748 }
1749 currentValue.push(value);
1750 }
1751 else {
1752 query[key] = value;
1753 }
1754 }
1755 return query;
1756}
1757/**
1758 * Stringifies a {@link LocationQueryRaw} object. Like `URLSearchParams`, it
1759 * doesn't prepend a `?`
1760 *
1761 * @internal
1762 *
1763 * @param query - query object to stringify
1764 * @returns string version of the query without the leading `?`
1765 */
1766function stringifyQuery(query) {
1767 let search = '';
1768 for (let key in query) {
1769 const value = query[key];
1770 key = encodeQueryKey(key);
1771 if (value == null) {
1772 // only null adds the value
1773 if (value !== undefined) {
1774 search += (search.length ? '&' : '') + key;
1775 }
1776 continue;
1777 }
1778 // keep null values
1779 const values = Array.isArray(value)
1780 ? value.map(v => v && encodeQueryValue(v))
1781 : [value && encodeQueryValue(value)];
1782 values.forEach(value => {
1783 // skip undefined values in arrays as if they were not present
1784 // smaller code than using filter
1785 if (value !== undefined) {
1786 // only append & with non-empty search
1787 search += (search.length ? '&' : '') + key;
1788 if (value != null)
1789 search += '=' + value;
1790 }
1791 });
1792 }
1793 return search;
1794}
1795/**
1796 * Transforms a {@link LocationQueryRaw} into a {@link LocationQuery} by casting
1797 * numbers into strings, removing keys with an undefined value and replacing
1798 * undefined with null in arrays
1799 *
1800 * @param query - query object to normalize
1801 * @returns a normalized query object
1802 */
1803function normalizeQuery(query) {
1804 const normalizedQuery = {};
1805 for (const key in query) {
1806 const value = query[key];
1807 if (value !== undefined) {
1808 normalizedQuery[key] = Array.isArray(value)
1809 ? value.map(v => (v == null ? null : '' + v))
1810 : value == null
1811 ? value
1812 : '' + value;
1813 }
1814 }
1815 return normalizedQuery;
1816}
1817
1818/**
1819 * Create a list of callbacks that can be reset. Used to create before and after navigation guards list
1820 */
1821function useCallbacks() {
1822 let handlers = [];
1823 function add(handler) {
1824 handlers.push(handler);
1825 return () => {
1826 const i = handlers.indexOf(handler);
1827 if (i > -1)
1828 handlers.splice(i, 1);
1829 };
1830 }
1831 function reset() {
1832 handlers = [];
1833 }
1834 return {
1835 add,
1836 list: () => handlers,
1837 reset,
1838 };
1839}
1840
1841function registerGuard(record, name, guard) {
1842 const removeFromList = () => {
1843 record[name].delete(guard);
1844 };
1845 vue.onUnmounted(removeFromList);
1846 vue.onDeactivated(removeFromList);
1847 vue.onActivated(() => {
1848 record[name].add(guard);
1849 });
1850 record[name].add(guard);
1851}
1852/**
1853 * Add a navigation guard that triggers whenever the component for the current
1854 * location is about to be left. Similar to {@link beforeRouteLeave} but can be
1855 * used in any component. The guard is removed when the component is unmounted.
1856 *
1857 * @param leaveGuard - {@link NavigationGuard}
1858 */
1859function onBeforeRouteLeave(leaveGuard) {
1860 if (!vue.getCurrentInstance()) {
1861 warn('getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function');
1862 return;
1863 }
1864 const activeRecord = vue.inject(matchedRouteKey,
1865 // to avoid warning
1866 {}).value;
1867 if (!activeRecord) {
1868 warn('No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside of a component child of <router-view>. Maybe you called it inside of App.vue?');
1869 return;
1870 }
1871 registerGuard(activeRecord, 'leaveGuards', leaveGuard);
1872}
1873/**
1874 * Add a navigation guard that triggers whenever the current location is about
1875 * to be updated. Similar to {@link beforeRouteUpdate} but can be used in any
1876 * component. The guard is removed when the component is unmounted.
1877 *
1878 * @param updateGuard - {@link NavigationGuard}
1879 */
1880function onBeforeRouteUpdate(updateGuard) {
1881 if (!vue.getCurrentInstance()) {
1882 warn('getCurrentInstance() returned null. onBeforeRouteUpdate() must be called at the top of a setup function');
1883 return;
1884 }
1885 const activeRecord = vue.inject(matchedRouteKey,
1886 // to avoid warning
1887 {}).value;
1888 if (!activeRecord) {
1889 warn('No active route record was found when calling `onBeforeRouteUpdate()`. Make sure you call this function inside of a component child of <router-view>. Maybe you called it inside of App.vue?');
1890 return;
1891 }
1892 registerGuard(activeRecord, 'updateGuards', updateGuard);
1893}
1894function guardToPromiseFn(guard, to, from, record, name) {
1895 // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place
1896 const enterCallbackArray = record &&
1897 // name is defined if record is because of the function overload
1898 (record.enterCallbacks[name] = record.enterCallbacks[name] || []);
1899 return () => new Promise((resolve, reject) => {
1900 const next = (valid) => {
1901 if (valid === false)
1902 reject(createRouterError(4 /* NAVIGATION_ABORTED */, {
1903 from,
1904 to,
1905 }));
1906 else if (valid instanceof Error) {
1907 reject(valid);
1908 }
1909 else if (isRouteLocation(valid)) {
1910 reject(createRouterError(2 /* NAVIGATION_GUARD_REDIRECT */, {
1911 from: to,
1912 to: valid,
1913 }));
1914 }
1915 else {
1916 if (enterCallbackArray &&
1917 // since enterCallbackArray is truthy, both record and name also are
1918 record.enterCallbacks[name] === enterCallbackArray &&
1919 typeof valid === 'function')
1920 enterCallbackArray.push(valid);
1921 resolve();
1922 }
1923 };
1924 // wrapping with Promise.resolve allows it to work with both async and sync guards
1925 const guardReturn = guard.call(record && record.instances[name], to, from, canOnlyBeCalledOnce(next, to, from) );
1926 let guardCall = Promise.resolve(guardReturn);
1927 if (guard.length < 3)
1928 guardCall = guardCall.then(next);
1929 if (guard.length > 2) {
1930 const message = `The "next" callback was never called inside of ${guard.name ? '"' + guard.name + '"' : ''}:\n${guard.toString()}\n. If you are returning a value instead of calling "next", make sure to remove the "next" parameter from your function.`;
1931 if (typeof guardReturn === 'object' && 'then' in guardReturn) {
1932 guardCall = guardCall.then(resolvedValue => {
1933 // @ts-expect-error: _called is added at canOnlyBeCalledOnce
1934 if (!next._called) {
1935 warn(message);
1936 return Promise.reject(new Error('Invalid navigation guard'));
1937 }
1938 return resolvedValue;
1939 });
1940 // TODO: test me!
1941 }
1942 else if (guardReturn !== undefined) {
1943 // @ts-expect-error: _called is added at canOnlyBeCalledOnce
1944 if (!next._called) {
1945 warn(message);
1946 reject(new Error('Invalid navigation guard'));
1947 return;
1948 }
1949 }
1950 }
1951 guardCall.catch(err => reject(err));
1952 });
1953}
1954function canOnlyBeCalledOnce(next, to, from) {
1955 let called = 0;
1956 return function () {
1957 if (called++ === 1)
1958 warn(`The "next" callback was called more than once in one navigation guard when going from "${from.fullPath}" to "${to.fullPath}". It should be called exactly one time in each navigation guard. This will fail in production.`);
1959 // @ts-expect-error: we put it in the original one because it's easier to check
1960 next._called = true;
1961 if (called === 1)
1962 next.apply(null, arguments);
1963 };
1964}
1965function extractComponentsGuards(matched, guardType, to, from) {
1966 const guards = [];
1967 for (const record of matched) {
1968 for (const name in record.components) {
1969 let rawComponent = record.components[name];
1970 {
1971 if (!rawComponent ||
1972 (typeof rawComponent !== 'object' &&
1973 typeof rawComponent !== 'function')) {
1974 warn(`Component "${name}" in record with path "${record.path}" is not` +
1975 ` a valid component. Received "${String(rawComponent)}".`);
1976 // throw to ensure we stop here but warn to ensure the message isn't
1977 // missed by the user
1978 throw new Error('Invalid route component');
1979 }
1980 else if ('then' in rawComponent) {
1981 // warn if user wrote import('/component.vue') instead of () =>
1982 // import('./component.vue')
1983 warn(`Component "${name}" in record with path "${record.path}" is a ` +
1984 `Promise instead of a function that returns a Promise. Did you ` +
1985 `write "import('./MyPage.vue')" instead of ` +
1986 `"() => import('./MyPage.vue')" ? This will break in ` +
1987 `production if not fixed.`);
1988 const promise = rawComponent;
1989 rawComponent = () => promise;
1990 }
1991 else if (rawComponent.__asyncLoader &&
1992 // warn only once per component
1993 !rawComponent.__warnedDefineAsync) {
1994 rawComponent.__warnedDefineAsync = true;
1995 warn(`Component "${name}" in record with path "${record.path}" is defined ` +
1996 `using "defineAsyncComponent()". ` +
1997 `Write "() => import('./MyPage.vue')" instead of ` +
1998 `"defineAsyncComponent(() => import('./MyPage.vue'))".`);
1999 }
2000 }
2001 // skip update and leave guards if the route component is not mounted
2002 if (guardType !== 'beforeRouteEnter' && !record.instances[name])
2003 continue;
2004 if (isRouteComponent(rawComponent)) {
2005 // __vccOpts is added by vue-class-component and contain the regular options
2006 const options = rawComponent.__vccOpts || rawComponent;
2007 const guard = options[guardType];
2008 guard && guards.push(guardToPromiseFn(guard, to, from, record, name));
2009 }
2010 else {
2011 // start requesting the chunk already
2012 let componentPromise = rawComponent();
2013 if (!('catch' in componentPromise)) {
2014 warn(`Component "${name}" in record with path "${record.path}" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.`);
2015 componentPromise = Promise.resolve(componentPromise);
2016 }
2017 guards.push(() => componentPromise.then(resolved => {
2018 if (!resolved)
2019 return Promise.reject(new Error(`Couldn't resolve component "${name}" at "${record.path}"`));
2020 const resolvedComponent = isESModule(resolved)
2021 ? resolved.default
2022 : resolved;
2023 // replace the function with the resolved component
2024 record.components[name] = resolvedComponent;
2025 // __vccOpts is added by vue-class-component and contain the regular options
2026 const options = resolvedComponent.__vccOpts || resolvedComponent;
2027 const guard = options[guardType];
2028 return guard && guardToPromiseFn(guard, to, from, record, name)();
2029 }));
2030 }
2031 }
2032 }
2033 return guards;
2034}
2035/**
2036 * Allows differentiating lazy components from functional components and vue-class-component
2037 *
2038 * @param component
2039 */
2040function isRouteComponent(component) {
2041 return (typeof component === 'object' ||
2042 'displayName' in component ||
2043 'props' in component ||
2044 '__vccOpts' in component);
2045}
2046
2047// TODO: we could allow currentRoute as a prop to expose `isActive` and
2048// `isExactActive` behavior should go through an RFC
2049function useLink(props) {
2050 const router = vue.inject(routerKey);
2051 const currentRoute = vue.inject(routeLocationKey);
2052 const route = vue.computed(() => router.resolve(vue.unref(props.to)));
2053 const activeRecordIndex = vue.computed(() => {
2054 const { matched } = route.value;
2055 const { length } = matched;
2056 const routeMatched = matched[length - 1];
2057 const currentMatched = currentRoute.matched;
2058 if (!routeMatched || !currentMatched.length)
2059 return -1;
2060 const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));
2061 if (index > -1)
2062 return index;
2063 // possible parent record
2064 const parentRecordPath = getOriginalPath(matched[length - 2]);
2065 return (
2066 // we are dealing with nested routes
2067 length > 1 &&
2068 // if the parent and matched route have the same path, this link is
2069 // referring to the empty child. Or we currently are on a different
2070 // child of the same parent
2071 getOriginalPath(routeMatched) === parentRecordPath &&
2072 // avoid comparing the child with its parent
2073 currentMatched[currentMatched.length - 1].path !== parentRecordPath
2074 ? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))
2075 : index);
2076 });
2077 const isActive = vue.computed(() => activeRecordIndex.value > -1 &&
2078 includesParams(currentRoute.params, route.value.params));
2079 const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&
2080 activeRecordIndex.value === currentRoute.matched.length - 1 &&
2081 isSameRouteLocationParams(currentRoute.params, route.value.params));
2082 function navigate(e = {}) {
2083 if (guardEvent(e)) {
2084 return router[vue.unref(props.replace) ? 'replace' : 'push'](vue.unref(props.to)
2085 // avoid uncaught errors are they are logged anyway
2086 ).catch(noop);
2087 }
2088 return Promise.resolve();
2089 }
2090 // devtools only
2091 if (isBrowser) {
2092 const instance = vue.getCurrentInstance();
2093 if (instance) {
2094 const linkContextDevtools = {
2095 route: route.value,
2096 isActive: isActive.value,
2097 isExactActive: isExactActive.value,
2098 };
2099 // @ts-expect-error: this is internal
2100 instance.__vrl_devtools = instance.__vrl_devtools || [];
2101 // @ts-expect-error: this is internal
2102 instance.__vrl_devtools.push(linkContextDevtools);
2103 vue.watchEffect(() => {
2104 linkContextDevtools.route = route.value;
2105 linkContextDevtools.isActive = isActive.value;
2106 linkContextDevtools.isExactActive = isExactActive.value;
2107 }, { flush: 'post' });
2108 }
2109 }
2110 return {
2111 route,
2112 href: vue.computed(() => route.value.href),
2113 isActive,
2114 isExactActive,
2115 navigate,
2116 };
2117}
2118const RouterLinkImpl = /*#__PURE__*/ vue.defineComponent({
2119 name: 'RouterLink',
2120 props: {
2121 to: {
2122 type: [String, Object],
2123 required: true,
2124 },
2125 replace: Boolean,
2126 activeClass: String,
2127 // inactiveClass: String,
2128 exactActiveClass: String,
2129 custom: Boolean,
2130 ariaCurrentValue: {
2131 type: String,
2132 default: 'page',
2133 },
2134 },
2135 useLink,
2136 setup(props, { slots }) {
2137 const link = vue.reactive(useLink(props));
2138 const { options } = vue.inject(routerKey);
2139 const elClass = vue.computed(() => ({
2140 [getLinkClass(props.activeClass, options.linkActiveClass, 'router-link-active')]: link.isActive,
2141 // [getLinkClass(
2142 // props.inactiveClass,
2143 // options.linkInactiveClass,
2144 // 'router-link-inactive'
2145 // )]: !link.isExactActive,
2146 [getLinkClass(props.exactActiveClass, options.linkExactActiveClass, 'router-link-exact-active')]: link.isExactActive,
2147 }));
2148 return () => {
2149 const children = slots.default && slots.default(link);
2150 return props.custom
2151 ? children
2152 : vue.h('a', {
2153 'aria-current': link.isExactActive
2154 ? props.ariaCurrentValue
2155 : null,
2156 href: link.href,
2157 // this would override user added attrs but Vue will still add
2158 // the listener so we end up triggering both
2159 onClick: link.navigate,
2160 class: elClass.value,
2161 }, children);
2162 };
2163 },
2164});
2165// export the public type for h/tsx inference
2166// also to avoid inline import() in generated d.ts files
2167/**
2168 * Component to render a link that triggers a navigation on click.
2169 */
2170const RouterLink = RouterLinkImpl;
2171function guardEvent(e) {
2172 // don't redirect with control keys
2173 if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
2174 return;
2175 // don't redirect when preventDefault called
2176 if (e.defaultPrevented)
2177 return;
2178 // don't redirect on right click
2179 if (e.button !== undefined && e.button !== 0)
2180 return;
2181 // don't redirect if `target="_blank"`
2182 // @ts-expect-error getAttribute does exist
2183 if (e.currentTarget && e.currentTarget.getAttribute) {
2184 // @ts-expect-error getAttribute exists
2185 const target = e.currentTarget.getAttribute('target');
2186 if (/\b_blank\b/i.test(target))
2187 return;
2188 }
2189 // this may be a Weex event which doesn't have this method
2190 if (e.preventDefault)
2191 e.preventDefault();
2192 return true;
2193}
2194function includesParams(outer, inner) {
2195 for (const key in inner) {
2196 const innerValue = inner[key];
2197 const outerValue = outer[key];
2198 if (typeof innerValue === 'string') {
2199 if (innerValue !== outerValue)
2200 return false;
2201 }
2202 else {
2203 if (!Array.isArray(outerValue) ||
2204 outerValue.length !== innerValue.length ||
2205 innerValue.some((value, i) => value !== outerValue[i]))
2206 return false;
2207 }
2208 }
2209 return true;
2210}
2211/**
2212 * Get the original path value of a record by following its aliasOf
2213 * @param record
2214 */
2215function getOriginalPath(record) {
2216 return record ? (record.aliasOf ? record.aliasOf.path : record.path) : '';
2217}
2218/**
2219 * Utility class to get the active class based on defaults.
2220 * @param propClass
2221 * @param globalClass
2222 * @param defaultClass
2223 */
2224const getLinkClass = (propClass, globalClass, defaultClass) => propClass != null
2225 ? propClass
2226 : globalClass != null
2227 ? globalClass
2228 : defaultClass;
2229
2230const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({
2231 name: 'RouterView',
2232 // #674 we manually inherit them
2233 inheritAttrs: false,
2234 props: {
2235 name: {
2236 type: String,
2237 default: 'default',
2238 },
2239 route: Object,
2240 },
2241 // Better compat for @vue/compat users
2242 // https://github.com/vuejs/router/issues/1315
2243 compatConfig: { MODE: 3 },
2244 setup(props, { attrs, slots }) {
2245 warnDeprecatedUsage();
2246 const injectedRoute = vue.inject(routerViewLocationKey);
2247 const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
2248 const depth = vue.inject(viewDepthKey, 0);
2249 const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth]);
2250 vue.provide(viewDepthKey, depth + 1);
2251 vue.provide(matchedRouteKey, matchedRouteRef);
2252 vue.provide(routerViewLocationKey, routeToDisplay);
2253 const viewRef = vue.ref();
2254 // watch at the same time the component instance, the route record we are
2255 // rendering, and the name
2256 vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {
2257 // copy reused instances
2258 if (to) {
2259 // this will update the instance for new instances as well as reused
2260 // instances when navigating to a new route
2261 to.instances[name] = instance;
2262 // the component instance is reused for a different route or name so
2263 // we copy any saved update or leave guards. With async setup, the
2264 // mounting component will mount before the matchedRoute changes,
2265 // making instance === oldInstance, so we check if guards have been
2266 // added before. This works because we remove guards when
2267 // unmounting/deactivating components
2268 if (from && from !== to && instance && instance === oldInstance) {
2269 if (!to.leaveGuards.size) {
2270 to.leaveGuards = from.leaveGuards;
2271 }
2272 if (!to.updateGuards.size) {
2273 to.updateGuards = from.updateGuards;
2274 }
2275 }
2276 }
2277 // trigger beforeRouteEnter next callbacks
2278 if (instance &&
2279 to &&
2280 // if there is no instance but to and from are the same this might be
2281 // the first visit
2282 (!from || !isSameRouteRecord(to, from) || !oldInstance)) {
2283 (to.enterCallbacks[name] || []).forEach(callback => callback(instance));
2284 }
2285 }, { flush: 'post' });
2286 return () => {
2287 const route = routeToDisplay.value;
2288 const matchedRoute = matchedRouteRef.value;
2289 const ViewComponent = matchedRoute && matchedRoute.components[props.name];
2290 // we need the value at the time we render because when we unmount, we
2291 // navigated to a different location so the value is different
2292 const currentName = props.name;
2293 if (!ViewComponent) {
2294 return normalizeSlot(slots.default, { Component: ViewComponent, route });
2295 }
2296 // props from route configuration
2297 const routePropsOption = matchedRoute.props[props.name];
2298 const routeProps = routePropsOption
2299 ? routePropsOption === true
2300 ? route.params
2301 : typeof routePropsOption === 'function'
2302 ? routePropsOption(route)
2303 : routePropsOption
2304 : null;
2305 const onVnodeUnmounted = vnode => {
2306 // remove the instance reference to prevent leak
2307 if (vnode.component.isUnmounted) {
2308 matchedRoute.instances[currentName] = null;
2309 }
2310 };
2311 const component = vue.h(ViewComponent, assign({}, routeProps, attrs, {
2312 onVnodeUnmounted,
2313 ref: viewRef,
2314 }));
2315 if (isBrowser &&
2316 component.ref) {
2317 // TODO: can display if it's an alias, its props
2318 const info = {
2319 depth,
2320 name: matchedRoute.name,
2321 path: matchedRoute.path,
2322 meta: matchedRoute.meta,
2323 };
2324 const internalInstances = Array.isArray(component.ref)
2325 ? component.ref.map(r => r.i)
2326 : [component.ref.i];
2327 internalInstances.forEach(instance => {
2328 // @ts-expect-error
2329 instance.__vrv_devtools = info;
2330 });
2331 }
2332 return (
2333 // pass the vnode to the slot as a prop.
2334 // h and <component :is="..."> both accept vnodes
2335 normalizeSlot(slots.default, { Component: component, route }) ||
2336 component);
2337 };
2338 },
2339});
2340function normalizeSlot(slot, data) {
2341 if (!slot)
2342 return null;
2343 const slotContent = slot(data);
2344 return slotContent.length === 1 ? slotContent[0] : slotContent;
2345}
2346// export the public type for h/tsx inference
2347// also to avoid inline import() in generated d.ts files
2348/**
2349 * Component to display the current route the user is at.
2350 */
2351const RouterView = RouterViewImpl;
2352// warn against deprecated usage with <transition> & <keep-alive>
2353// due to functional component being no longer eager in Vue 3
2354function warnDeprecatedUsage() {
2355 const instance = vue.getCurrentInstance();
2356 const parentName = instance.parent && instance.parent.type.name;
2357 if (parentName &&
2358 (parentName === 'KeepAlive' || parentName.includes('Transition'))) {
2359 const comp = parentName === 'KeepAlive' ? 'keep-alive' : 'transition';
2360 warn(`<router-view> can no longer be used directly inside <transition> or <keep-alive>.\n` +
2361 `Use slot props instead:\n\n` +
2362 `<router-view v-slot="{ Component }">\n` +
2363 ` <${comp}>\n` +
2364 ` <component :is="Component" />\n` +
2365 ` </${comp}>\n` +
2366 `</router-view>`);
2367 }
2368}
2369
2370function formatRouteLocation(routeLocation, tooltip) {
2371 const copy = assign({}, routeLocation, {
2372 // remove variables that can contain vue instances
2373 matched: routeLocation.matched.map(matched => omit(matched, ['instances', 'children', 'aliasOf'])),
2374 });
2375 return {
2376 _custom: {
2377 type: null,
2378 readOnly: true,
2379 display: routeLocation.fullPath,
2380 tooltip,
2381 value: copy,
2382 },
2383 };
2384}
2385function formatDisplay(display) {
2386 return {
2387 _custom: {
2388 display,
2389 },
2390 };
2391}
2392// to support multiple router instances
2393let routerId = 0;
2394function addDevtools(app, router, matcher) {
2395 // Take over router.beforeEach and afterEach
2396 // make sure we are not registering the devtool twice
2397 if (router.__hasDevtools)
2398 return;
2399 router.__hasDevtools = true;
2400 // increment to support multiple router instances
2401 const id = routerId++;
2402 devtoolsApi.setupDevtoolsPlugin({
2403 id: 'org.vuejs.router' + (id ? '.' + id : ''),
2404 label: 'Vue Router',
2405 packageName: 'vue-router',
2406 homepage: 'https://router.vuejs.org',
2407 logo: 'https://router.vuejs.org/logo.png',
2408 componentStateTypes: ['Routing'],
2409 app,
2410 }, api => {
2411 // display state added by the router
2412 api.on.inspectComponent((payload, ctx) => {
2413 if (payload.instanceData) {
2414 payload.instanceData.state.push({
2415 type: 'Routing',
2416 key: '$route',
2417 editable: false,
2418 value: formatRouteLocation(router.currentRoute.value, 'Current Route'),
2419 });
2420 }
2421 });
2422 // mark router-link as active and display tags on router views
2423 api.on.visitComponentTree(({ treeNode: node, componentInstance }) => {
2424 if (componentInstance.__vrv_devtools) {
2425 const info = componentInstance.__vrv_devtools;
2426 node.tags.push({
2427 label: (info.name ? `${info.name.toString()}: ` : '') + info.path,
2428 textColor: 0,
2429 tooltip: 'This component is rendered by &lt;router-view&gt;',
2430 backgroundColor: PINK_500,
2431 });
2432 }
2433 // if multiple useLink are used
2434 if (Array.isArray(componentInstance.__vrl_devtools)) {
2435 componentInstance.__devtoolsApi = api;
2436 componentInstance.__vrl_devtools.forEach(devtoolsData => {
2437 let backgroundColor = ORANGE_400;
2438 let tooltip = '';
2439 if (devtoolsData.isExactActive) {
2440 backgroundColor = LIME_500;
2441 tooltip = 'This is exactly active';
2442 }
2443 else if (devtoolsData.isActive) {
2444 backgroundColor = BLUE_600;
2445 tooltip = 'This link is active';
2446 }
2447 node.tags.push({
2448 label: devtoolsData.route.path,
2449 textColor: 0,
2450 tooltip,
2451 backgroundColor,
2452 });
2453 });
2454 }
2455 });
2456 vue.watch(router.currentRoute, () => {
2457 // refresh active state
2458 refreshRoutesView();
2459 api.notifyComponentUpdate();
2460 api.sendInspectorTree(routerInspectorId);
2461 api.sendInspectorState(routerInspectorId);
2462 });
2463 const navigationsLayerId = 'router:navigations:' + id;
2464 api.addTimelineLayer({
2465 id: navigationsLayerId,
2466 label: `Router${id ? ' ' + id : ''} Navigations`,
2467 color: 0x40a8c4,
2468 });
2469 // const errorsLayerId = 'router:errors'
2470 // api.addTimelineLayer({
2471 // id: errorsLayerId,
2472 // label: 'Router Errors',
2473 // color: 0xea5455,
2474 // })
2475 router.onError((error, to) => {
2476 api.addTimelineEvent({
2477 layerId: navigationsLayerId,
2478 event: {
2479 title: 'Error during Navigation',
2480 subtitle: to.fullPath,
2481 logType: 'error',
2482 time: api.now(),
2483 data: { error },
2484 groupId: to.meta.__navigationId,
2485 },
2486 });
2487 });
2488 // attached to `meta` and used to group events
2489 let navigationId = 0;
2490 router.beforeEach((to, from) => {
2491 const data = {
2492 guard: formatDisplay('beforeEach'),
2493 from: formatRouteLocation(from, 'Current Location during this navigation'),
2494 to: formatRouteLocation(to, 'Target location'),
2495 };
2496 // Used to group navigations together, hide from devtools
2497 Object.defineProperty(to.meta, '__navigationId', {
2498 value: navigationId++,
2499 });
2500 api.addTimelineEvent({
2501 layerId: navigationsLayerId,
2502 event: {
2503 time: api.now(),
2504 title: 'Start of navigation',
2505 subtitle: to.fullPath,
2506 data,
2507 groupId: to.meta.__navigationId,
2508 },
2509 });
2510 });
2511 router.afterEach((to, from, failure) => {
2512 const data = {
2513 guard: formatDisplay('afterEach'),
2514 };
2515 if (failure) {
2516 data.failure = {
2517 _custom: {
2518 type: Error,
2519 readOnly: true,
2520 display: failure ? failure.message : '',
2521 tooltip: 'Navigation Failure',
2522 value: failure,
2523 },
2524 };
2525 data.status = formatDisplay('❌');
2526 }
2527 else {
2528 data.status = formatDisplay('✅');
2529 }
2530 // we set here to have the right order
2531 data.from = formatRouteLocation(from, 'Current Location during this navigation');
2532 data.to = formatRouteLocation(to, 'Target location');
2533 api.addTimelineEvent({
2534 layerId: navigationsLayerId,
2535 event: {
2536 title: 'End of navigation',
2537 subtitle: to.fullPath,
2538 time: api.now(),
2539 data,
2540 logType: failure ? 'warning' : 'default',
2541 groupId: to.meta.__navigationId,
2542 },
2543 });
2544 });
2545 /**
2546 * Inspector of Existing routes
2547 */
2548 const routerInspectorId = 'router-inspector:' + id;
2549 api.addInspector({
2550 id: routerInspectorId,
2551 label: 'Routes' + (id ? ' ' + id : ''),
2552 icon: 'book',
2553 treeFilterPlaceholder: 'Search routes',
2554 });
2555 function refreshRoutesView() {
2556 // the routes view isn't active
2557 if (!activeRoutesPayload)
2558 return;
2559 const payload = activeRoutesPayload;
2560 // children routes will appear as nested
2561 let routes = matcher.getRoutes().filter(route => !route.parent);
2562 // reset match state to false
2563 routes.forEach(resetMatchStateOnRouteRecord);
2564 // apply a match state if there is a payload
2565 if (payload.filter) {
2566 routes = routes.filter(route =>
2567 // save matches state based on the payload
2568 isRouteMatching(route, payload.filter.toLowerCase()));
2569 }
2570 // mark active routes
2571 routes.forEach(route => markRouteRecordActive(route, router.currentRoute.value));
2572 payload.rootNodes = routes.map(formatRouteRecordForInspector);
2573 }
2574 let activeRoutesPayload;
2575 api.on.getInspectorTree(payload => {
2576 activeRoutesPayload = payload;
2577 if (payload.app === app && payload.inspectorId === routerInspectorId) {
2578 refreshRoutesView();
2579 }
2580 });
2581 /**
2582 * Display information about the currently selected route record
2583 */
2584 api.on.getInspectorState(payload => {
2585 if (payload.app === app && payload.inspectorId === routerInspectorId) {
2586 const routes = matcher.getRoutes();
2587 const route = routes.find(route => route.record.__vd_id === payload.nodeId);
2588 if (route) {
2589 payload.state = {
2590 options: formatRouteRecordMatcherForStateInspector(route),
2591 };
2592 }
2593 }
2594 });
2595 api.sendInspectorTree(routerInspectorId);
2596 api.sendInspectorState(routerInspectorId);
2597 });
2598}
2599function modifierForKey(key) {
2600 if (key.optional) {
2601 return key.repeatable ? '*' : '?';
2602 }
2603 else {
2604 return key.repeatable ? '+' : '';
2605 }
2606}
2607function formatRouteRecordMatcherForStateInspector(route) {
2608 const { record } = route;
2609 const fields = [
2610 { editable: false, key: 'path', value: record.path },
2611 ];
2612 if (record.name != null) {
2613 fields.push({
2614 editable: false,
2615 key: 'name',
2616 value: record.name,
2617 });
2618 }
2619 fields.push({ editable: false, key: 'regexp', value: route.re });
2620 if (route.keys.length) {
2621 fields.push({
2622 editable: false,
2623 key: 'keys',
2624 value: {
2625 _custom: {
2626 type: null,
2627 readOnly: true,
2628 display: route.keys
2629 .map(key => `${key.name}${modifierForKey(key)}`)
2630 .join(' '),
2631 tooltip: 'Param keys',
2632 value: route.keys,
2633 },
2634 },
2635 });
2636 }
2637 if (record.redirect != null) {
2638 fields.push({
2639 editable: false,
2640 key: 'redirect',
2641 value: record.redirect,
2642 });
2643 }
2644 if (route.alias.length) {
2645 fields.push({
2646 editable: false,
2647 key: 'aliases',
2648 value: route.alias.map(alias => alias.record.path),
2649 });
2650 }
2651 fields.push({
2652 key: 'score',
2653 editable: false,
2654 value: {
2655 _custom: {
2656 type: null,
2657 readOnly: true,
2658 display: route.score.map(score => score.join(', ')).join(' | '),
2659 tooltip: 'Score used to sort routes',
2660 value: route.score,
2661 },
2662 },
2663 });
2664 return fields;
2665}
2666/**
2667 * Extracted from tailwind palette
2668 */
2669const PINK_500 = 0xec4899;
2670const BLUE_600 = 0x2563eb;
2671const LIME_500 = 0x84cc16;
2672const CYAN_400 = 0x22d3ee;
2673const ORANGE_400 = 0xfb923c;
2674// const GRAY_100 = 0xf4f4f5
2675const DARK = 0x666666;
2676function formatRouteRecordForInspector(route) {
2677 const tags = [];
2678 const { record } = route;
2679 if (record.name != null) {
2680 tags.push({
2681 label: String(record.name),
2682 textColor: 0,
2683 backgroundColor: CYAN_400,
2684 });
2685 }
2686 if (record.aliasOf) {
2687 tags.push({
2688 label: 'alias',
2689 textColor: 0,
2690 backgroundColor: ORANGE_400,
2691 });
2692 }
2693 if (route.__vd_match) {
2694 tags.push({
2695 label: 'matches',
2696 textColor: 0,
2697 backgroundColor: PINK_500,
2698 });
2699 }
2700 if (route.__vd_exactActive) {
2701 tags.push({
2702 label: 'exact',
2703 textColor: 0,
2704 backgroundColor: LIME_500,
2705 });
2706 }
2707 if (route.__vd_active) {
2708 tags.push({
2709 label: 'active',
2710 textColor: 0,
2711 backgroundColor: BLUE_600,
2712 });
2713 }
2714 if (record.redirect) {
2715 tags.push({
2716 label: 'redirect: ' +
2717 (typeof record.redirect === 'string' ? record.redirect : 'Object'),
2718 textColor: 0xffffff,
2719 backgroundColor: DARK,
2720 });
2721 }
2722 // add an id to be able to select it. Using the `path` is not possible because
2723 // empty path children would collide with their parents
2724 let id = record.__vd_id;
2725 if (id == null) {
2726 id = String(routeRecordId++);
2727 record.__vd_id = id;
2728 }
2729 return {
2730 id,
2731 label: record.path,
2732 tags,
2733 children: route.children.map(formatRouteRecordForInspector),
2734 };
2735}
2736// incremental id for route records and inspector state
2737let routeRecordId = 0;
2738const EXTRACT_REGEXP_RE = /^\/(.*)\/([a-z]*)$/;
2739function markRouteRecordActive(route, currentRoute) {
2740 // no route will be active if matched is empty
2741 // reset the matching state
2742 const isExactActive = currentRoute.matched.length &&
2743 isSameRouteRecord(currentRoute.matched[currentRoute.matched.length - 1], route.record);
2744 route.__vd_exactActive = route.__vd_active = isExactActive;
2745 if (!isExactActive) {
2746 route.__vd_active = currentRoute.matched.some(match => isSameRouteRecord(match, route.record));
2747 }
2748 route.children.forEach(childRoute => markRouteRecordActive(childRoute, currentRoute));
2749}
2750function resetMatchStateOnRouteRecord(route) {
2751 route.__vd_match = false;
2752 route.children.forEach(resetMatchStateOnRouteRecord);
2753}
2754function isRouteMatching(route, filter) {
2755 const found = String(route.re).match(EXTRACT_REGEXP_RE);
2756 route.__vd_match = false;
2757 if (!found || found.length < 3) {
2758 return false;
2759 }
2760 // use a regexp without $ at the end to match nested routes better
2761 const nonEndingRE = new RegExp(found[1].replace(/\$$/, ''), found[2]);
2762 if (nonEndingRE.test(filter)) {
2763 // mark children as matches
2764 route.children.forEach(child => isRouteMatching(child, filter));
2765 // exception case: `/`
2766 if (route.record.path !== '/' || filter === '/') {
2767 route.__vd_match = route.re.test(filter);
2768 return true;
2769 }
2770 // hide the / route
2771 return false;
2772 }
2773 const path = route.record.path.toLowerCase();
2774 const decodedPath = decode(path);
2775 // also allow partial matching on the path
2776 if (!filter.startsWith('/') &&
2777 (decodedPath.includes(filter) || path.includes(filter)))
2778 return true;
2779 if (decodedPath.startsWith(filter) || path.startsWith(filter))
2780 return true;
2781 if (route.record.name && String(route.record.name).includes(filter))
2782 return true;
2783 return route.children.some(child => isRouteMatching(child, filter));
2784}
2785function omit(obj, keys) {
2786 const ret = {};
2787 for (const key in obj) {
2788 if (!keys.includes(key)) {
2789 // @ts-expect-error
2790 ret[key] = obj[key];
2791 }
2792 }
2793 return ret;
2794}
2795
2796/**
2797 * Creates a Router instance that can be used by a Vue app.
2798 *
2799 * @param options - {@link RouterOptions}
2800 */
2801function createRouter(options) {
2802 const matcher = createRouterMatcher(options.routes, options);
2803 const parseQuery$1 = options.parseQuery || parseQuery;
2804 const stringifyQuery$1 = options.stringifyQuery || stringifyQuery;
2805 const routerHistory = options.history;
2806 if (!routerHistory)
2807 throw new Error('Provide the "history" option when calling "createRouter()":' +
2808 ' https://next.router.vuejs.org/api/#history.');
2809 const beforeGuards = useCallbacks();
2810 const beforeResolveGuards = useCallbacks();
2811 const afterGuards = useCallbacks();
2812 const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);
2813 let pendingLocation = START_LOCATION_NORMALIZED;
2814 // leave the scrollRestoration if no scrollBehavior is provided
2815 if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
2816 history.scrollRestoration = 'manual';
2817 }
2818 const normalizeParams = applyToParams.bind(null, paramValue => '' + paramValue);
2819 const encodeParams = applyToParams.bind(null, encodeParam);
2820 const decodeParams =
2821 // @ts-expect-error: intentionally avoid the type check
2822 applyToParams.bind(null, decode);
2823 function addRoute(parentOrRoute, route) {
2824 let parent;
2825 let record;
2826 if (isRouteName(parentOrRoute)) {
2827 parent = matcher.getRecordMatcher(parentOrRoute);
2828 record = route;
2829 }
2830 else {
2831 record = parentOrRoute;
2832 }
2833 return matcher.addRoute(record, parent);
2834 }
2835 function removeRoute(name) {
2836 const recordMatcher = matcher.getRecordMatcher(name);
2837 if (recordMatcher) {
2838 matcher.removeRoute(recordMatcher);
2839 }
2840 else {
2841 warn(`Cannot remove non-existent route "${String(name)}"`);
2842 }
2843 }
2844 function getRoutes() {
2845 return matcher.getRoutes().map(routeMatcher => routeMatcher.record);
2846 }
2847 function hasRoute(name) {
2848 return !!matcher.getRecordMatcher(name);
2849 }
2850 function resolve(rawLocation, currentLocation) {
2851 // const objectLocation = routerLocationAsObject(rawLocation)
2852 // we create a copy to modify it later
2853 currentLocation = assign({}, currentLocation || currentRoute.value);
2854 if (typeof rawLocation === 'string') {
2855 const locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path);
2856 const matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
2857 const href = routerHistory.createHref(locationNormalized.fullPath);
2858 {
2859 if (href.startsWith('//'))
2860 warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`);
2861 else if (!matchedRoute.matched.length) {
2862 warn(`No match found for location with path "${rawLocation}"`);
2863 }
2864 }
2865 // locationNormalized is always a new object
2866 return assign(locationNormalized, matchedRoute, {
2867 params: decodeParams(matchedRoute.params),
2868 hash: decode(locationNormalized.hash),
2869 redirectedFrom: undefined,
2870 href,
2871 });
2872 }
2873 let matcherLocation;
2874 // path could be relative in object as well
2875 if ('path' in rawLocation) {
2876 if ('params' in rawLocation &&
2877 !('name' in rawLocation) &&
2878 // @ts-expect-error: the type is never
2879 Object.keys(rawLocation.params).length) {
2880 warn(`Path "${
2881 // @ts-expect-error: the type is never
2882 rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`);
2883 }
2884 matcherLocation = assign({}, rawLocation, {
2885 path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path,
2886 });
2887 }
2888 else {
2889 // remove any nullish param
2890 const targetParams = assign({}, rawLocation.params);
2891 for (const key in targetParams) {
2892 if (targetParams[key] == null) {
2893 delete targetParams[key];
2894 }
2895 }
2896 // pass encoded values to the matcher so it can produce encoded path and fullPath
2897 matcherLocation = assign({}, rawLocation, {
2898 params: encodeParams(rawLocation.params),
2899 });
2900 // current location params are decoded, we need to encode them in case the
2901 // matcher merges the params
2902 currentLocation.params = encodeParams(currentLocation.params);
2903 }
2904 const matchedRoute = matcher.resolve(matcherLocation, currentLocation);
2905 const hash = rawLocation.hash || '';
2906 if (hash && !hash.startsWith('#')) {
2907 warn(`A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".`);
2908 }
2909 // decoding them) the matcher might have merged current location params so
2910 // we need to run the decoding again
2911 matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params));
2912 const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, {
2913 hash: encodeHash(hash),
2914 path: matchedRoute.path,
2915 }));
2916 const href = routerHistory.createHref(fullPath);
2917 {
2918 if (href.startsWith('//')) {
2919 warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`);
2920 }
2921 else if (!matchedRoute.matched.length) {
2922 warn(`No match found for location with path "${'path' in rawLocation ? rawLocation.path : rawLocation}"`);
2923 }
2924 }
2925 return assign({
2926 fullPath,
2927 // keep the hash encoded so fullPath is effectively path + encodedQuery +
2928 // hash
2929 hash,
2930 query:
2931 // if the user is using a custom query lib like qs, we might have
2932 // nested objects, so we keep the query as is, meaning it can contain
2933 // numbers at `$route.query`, but at the point, the user will have to
2934 // use their own type anyway.
2935 // https://github.com/vuejs/router/issues/328#issuecomment-649481567
2936 stringifyQuery$1 === stringifyQuery
2937 ? normalizeQuery(rawLocation.query)
2938 : (rawLocation.query || {}),
2939 }, matchedRoute, {
2940 redirectedFrom: undefined,
2941 href,
2942 });
2943 }
2944 function locationAsObject(to) {
2945 return typeof to === 'string'
2946 ? parseURL(parseQuery$1, to, currentRoute.value.path)
2947 : assign({}, to);
2948 }
2949 function checkCanceledNavigation(to, from) {
2950 if (pendingLocation !== to) {
2951 return createRouterError(8 /* NAVIGATION_CANCELLED */, {
2952 from,
2953 to,
2954 });
2955 }
2956 }
2957 function push(to) {
2958 return pushWithRedirect(to);
2959 }
2960 function replace(to) {
2961 return push(assign(locationAsObject(to), { replace: true }));
2962 }
2963 function handleRedirectRecord(to) {
2964 const lastMatched = to.matched[to.matched.length - 1];
2965 if (lastMatched && lastMatched.redirect) {
2966 const { redirect } = lastMatched;
2967 let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect;
2968 if (typeof newTargetLocation === 'string') {
2969 newTargetLocation =
2970 newTargetLocation.includes('?') || newTargetLocation.includes('#')
2971 ? (newTargetLocation = locationAsObject(newTargetLocation))
2972 : // force empty params
2973 { path: newTargetLocation };
2974 // @ts-expect-error: force empty params when a string is passed to let
2975 // the router parse them again
2976 newTargetLocation.params = {};
2977 }
2978 if (!('path' in newTargetLocation) &&
2979 !('name' in newTargetLocation)) {
2980 warn(`Invalid redirect found:\n${JSON.stringify(newTargetLocation, null, 2)}\n when navigating to "${to.fullPath}". A redirect must contain a name or path. This will break in production.`);
2981 throw new Error('Invalid redirect');
2982 }
2983 return assign({
2984 query: to.query,
2985 hash: to.hash,
2986 params: to.params,
2987 }, newTargetLocation);
2988 }
2989 }
2990 function pushWithRedirect(to, redirectedFrom) {
2991 const targetLocation = (pendingLocation = resolve(to));
2992 const from = currentRoute.value;
2993 const data = to.state;
2994 const force = to.force;
2995 // to could be a string where `replace` is a function
2996 const replace = to.replace === true;
2997 const shouldRedirect = handleRedirectRecord(targetLocation);
2998 if (shouldRedirect)
2999 return pushWithRedirect(assign(locationAsObject(shouldRedirect), {
3000 state: data,
3001 force,
3002 replace,
3003 }),
3004 // keep original redirectedFrom if it exists
3005 redirectedFrom || targetLocation);
3006 // if it was a redirect we already called `pushWithRedirect` above
3007 const toLocation = targetLocation;
3008 toLocation.redirectedFrom = redirectedFrom;
3009 let failure;
3010 if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) {
3011 failure = createRouterError(16 /* NAVIGATION_DUPLICATED */, { to: toLocation, from });
3012 // trigger scroll to allow scrolling to the same anchor
3013 handleScroll(from, from,
3014 // this is a push, the only way for it to be triggered from a
3015 // history.listen is with a redirect, which makes it become a push
3016 true,
3017 // This cannot be the first navigation because the initial location
3018 // cannot be manually navigated to
3019 false);
3020 }
3021 return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
3022 .catch((error) => isNavigationFailure(error)
3023 ? // navigation redirects still mark the router as ready
3024 isNavigationFailure(error, 2 /* NAVIGATION_GUARD_REDIRECT */)
3025 ? error
3026 : markAsReady(error) // also returns the error
3027 : // reject any unknown error
3028 triggerError(error, toLocation, from))
3029 .then((failure) => {
3030 if (failure) {
3031 if (isNavigationFailure(failure, 2 /* NAVIGATION_GUARD_REDIRECT */)) {
3032 if (// we are redirecting to the same location we were already at
3033 isSameRouteLocation(stringifyQuery$1, resolve(failure.to), toLocation) &&
3034 // and we have done it a couple of times
3035 redirectedFrom &&
3036 // @ts-expect-error: added only in dev
3037 (redirectedFrom._count = redirectedFrom._count
3038 ? // @ts-expect-error
3039 redirectedFrom._count + 1
3040 : 1) > 10) {
3041 warn(`Detected an infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow. This will break in production if not fixed.`);
3042 return Promise.reject(new Error('Infinite redirect in navigation guard'));
3043 }
3044 return pushWithRedirect(
3045 // keep options
3046 assign(locationAsObject(failure.to), {
3047 state: data,
3048 force,
3049 replace,
3050 }),
3051 // preserve the original redirectedFrom if any
3052 redirectedFrom || toLocation);
3053 }
3054 }
3055 else {
3056 // if we fail we don't finalize the navigation
3057 failure = finalizeNavigation(toLocation, from, true, replace, data);
3058 }
3059 triggerAfterEach(toLocation, from, failure);
3060 return failure;
3061 });
3062 }
3063 /**
3064 * Helper to reject and skip all navigation guards if a new navigation happened
3065 * @param to
3066 * @param from
3067 */
3068 function checkCanceledNavigationAndReject(to, from) {
3069 const error = checkCanceledNavigation(to, from);
3070 return error ? Promise.reject(error) : Promise.resolve();
3071 }
3072 // TODO: refactor the whole before guards by internally using router.beforeEach
3073 function navigate(to, from) {
3074 let guards;
3075 const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from);
3076 // all components here have been resolved once because we are leaving
3077 guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from);
3078 // leavingRecords is already reversed
3079 for (const record of leavingRecords) {
3080 record.leaveGuards.forEach(guard => {
3081 guards.push(guardToPromiseFn(guard, to, from));
3082 });
3083 }
3084 const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from);
3085 guards.push(canceledNavigationCheck);
3086 // run the queue of per route beforeRouteLeave guards
3087 return (runGuardQueue(guards)
3088 .then(() => {
3089 // check global guards beforeEach
3090 guards = [];
3091 for (const guard of beforeGuards.list()) {
3092 guards.push(guardToPromiseFn(guard, to, from));
3093 }
3094 guards.push(canceledNavigationCheck);
3095 return runGuardQueue(guards);
3096 })
3097 .then(() => {
3098 // check in components beforeRouteUpdate
3099 guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from);
3100 for (const record of updatingRecords) {
3101 record.updateGuards.forEach(guard => {
3102 guards.push(guardToPromiseFn(guard, to, from));
3103 });
3104 }
3105 guards.push(canceledNavigationCheck);
3106 // run the queue of per route beforeEnter guards
3107 return runGuardQueue(guards);
3108 })
3109 .then(() => {
3110 // check the route beforeEnter
3111 guards = [];
3112 for (const record of to.matched) {
3113 // do not trigger beforeEnter on reused views
3114 if (record.beforeEnter && !from.matched.includes(record)) {
3115 if (Array.isArray(record.beforeEnter)) {
3116 for (const beforeEnter of record.beforeEnter)
3117 guards.push(guardToPromiseFn(beforeEnter, to, from));
3118 }
3119 else {
3120 guards.push(guardToPromiseFn(record.beforeEnter, to, from));
3121 }
3122 }
3123 }
3124 guards.push(canceledNavigationCheck);
3125 // run the queue of per route beforeEnter guards
3126 return runGuardQueue(guards);
3127 })
3128 .then(() => {
3129 // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
3130 // clear existing enterCallbacks, these are added by extractComponentsGuards
3131 to.matched.forEach(record => (record.enterCallbacks = {}));
3132 // check in-component beforeRouteEnter
3133 guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from);
3134 guards.push(canceledNavigationCheck);
3135 // run the queue of per route beforeEnter guards
3136 return runGuardQueue(guards);
3137 })
3138 .then(() => {
3139 // check global guards beforeResolve
3140 guards = [];
3141 for (const guard of beforeResolveGuards.list()) {
3142 guards.push(guardToPromiseFn(guard, to, from));
3143 }
3144 guards.push(canceledNavigationCheck);
3145 return runGuardQueue(guards);
3146 })
3147 // catch any navigation canceled
3148 .catch(err => isNavigationFailure(err, 8 /* NAVIGATION_CANCELLED */)
3149 ? err
3150 : Promise.reject(err)));
3151 }
3152 function triggerAfterEach(to, from, failure) {
3153 // navigation is confirmed, call afterGuards
3154 // TODO: wrap with error handlers
3155 for (const guard of afterGuards.list())
3156 guard(to, from, failure);
3157 }
3158 /**
3159 * - Cleans up any navigation guards
3160 * - Changes the url if necessary
3161 * - Calls the scrollBehavior
3162 */
3163 function finalizeNavigation(toLocation, from, isPush, replace, data) {
3164 // a more recent navigation took place
3165 const error = checkCanceledNavigation(toLocation, from);
3166 if (error)
3167 return error;
3168 // only consider as push if it's not the first navigation
3169 const isFirstNavigation = from === START_LOCATION_NORMALIZED;
3170 const state = !isBrowser ? {} : history.state;
3171 // change URL only if the user did a push/replace and if it's not the initial navigation because
3172 // it's just reflecting the url
3173 if (isPush) {
3174 // on the initial navigation, we want to reuse the scroll position from
3175 // history state if it exists
3176 if (replace || isFirstNavigation)
3177 routerHistory.replace(toLocation.fullPath, assign({
3178 scroll: isFirstNavigation && state && state.scroll,
3179 }, data));
3180 else
3181 routerHistory.push(toLocation.fullPath, data);
3182 }
3183 // accept current navigation
3184 currentRoute.value = toLocation;
3185 handleScroll(toLocation, from, isPush, isFirstNavigation);
3186 markAsReady();
3187 }
3188 let removeHistoryListener;
3189 // attach listener to history to trigger navigations
3190 function setupListeners() {
3191 // avoid setting up listeners twice due to an invalid first navigation
3192 if (removeHistoryListener)
3193 return;
3194 removeHistoryListener = routerHistory.listen((to, _from, info) => {
3195 // cannot be a redirect route because it was in history
3196 const toLocation = resolve(to);
3197 // due to dynamic routing, and to hash history with manual navigation
3198 // (manually changing the url or calling history.hash = '#/somewhere'),
3199 // there could be a redirect record in history
3200 const shouldRedirect = handleRedirectRecord(toLocation);
3201 if (shouldRedirect) {
3202 pushWithRedirect(assign(shouldRedirect, { replace: true }), toLocation).catch(noop);
3203 return;
3204 }
3205 pendingLocation = toLocation;
3206 const from = currentRoute.value;
3207 // TODO: should be moved to web history?
3208 if (isBrowser) {
3209 saveScrollPosition(getScrollKey(from.fullPath, info.delta), computeScrollPosition());
3210 }
3211 navigate(toLocation, from)
3212 .catch((error) => {
3213 if (isNavigationFailure(error, 4 /* NAVIGATION_ABORTED */ | 8 /* NAVIGATION_CANCELLED */)) {
3214 return error;
3215 }
3216 if (isNavigationFailure(error, 2 /* NAVIGATION_GUARD_REDIRECT */)) {
3217 // Here we could call if (info.delta) routerHistory.go(-info.delta,
3218 // false) but this is bug prone as we have no way to wait the
3219 // navigation to be finished before calling pushWithRedirect. Using
3220 // a setTimeout of 16ms seems to work but there is not guarantee for
3221 // it to work on every browser. So Instead we do not restore the
3222 // history entry and trigger a new navigation as requested by the
3223 // navigation guard.
3224 // the error is already handled by router.push we just want to avoid
3225 // logging the error
3226 pushWithRedirect(error.to, toLocation
3227 // avoid an uncaught rejection, let push call triggerError
3228 )
3229 .then(failure => {
3230 // manual change in hash history #916 ending up in the URL not
3231 // changing but it was changed by the manual url change, so we
3232 // need to manually change it ourselves
3233 if (isNavigationFailure(failure, 4 /* NAVIGATION_ABORTED */ |
3234 16 /* NAVIGATION_DUPLICATED */) &&
3235 !info.delta &&
3236 info.type === NavigationType.pop) {
3237 routerHistory.go(-1, false);
3238 }
3239 })
3240 .catch(noop);
3241 // avoid the then branch
3242 return Promise.reject();
3243 }
3244 // do not restore history on unknown direction
3245 if (info.delta)
3246 routerHistory.go(-info.delta, false);
3247 // unrecognized error, transfer to the global handler
3248 return triggerError(error, toLocation, from);
3249 })
3250 .then((failure) => {
3251 failure =
3252 failure ||
3253 finalizeNavigation(
3254 // after navigation, all matched components are resolved
3255 toLocation, from, false);
3256 // revert the navigation
3257 if (failure) {
3258 if (info.delta) {
3259 routerHistory.go(-info.delta, false);
3260 }
3261 else if (info.type === NavigationType.pop &&
3262 isNavigationFailure(failure, 4 /* NAVIGATION_ABORTED */ | 16 /* NAVIGATION_DUPLICATED */)) {
3263 // manual change in hash history #916
3264 // it's like a push but lacks the information of the direction
3265 routerHistory.go(-1, false);
3266 }
3267 }
3268 triggerAfterEach(toLocation, from, failure);
3269 })
3270 .catch(noop);
3271 });
3272 }
3273 // Initialization and Errors
3274 let readyHandlers = useCallbacks();
3275 let errorHandlers = useCallbacks();
3276 let ready;
3277 /**
3278 * Trigger errorHandlers added via onError and throws the error as well
3279 *
3280 * @param error - error to throw
3281 * @param to - location we were navigating to when the error happened
3282 * @param from - location we were navigating from when the error happened
3283 * @returns the error as a rejected promise
3284 */
3285 function triggerError(error, to, from) {
3286 markAsReady(error);
3287 const list = errorHandlers.list();
3288 if (list.length) {
3289 list.forEach(handler => handler(error, to, from));
3290 }
3291 else {
3292 {
3293 warn('uncaught error during route navigation:');
3294 }
3295 console.error(error);
3296 }
3297 return Promise.reject(error);
3298 }
3299 function isReady() {
3300 if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
3301 return Promise.resolve();
3302 return new Promise((resolve, reject) => {
3303 readyHandlers.add([resolve, reject]);
3304 });
3305 }
3306 function markAsReady(err) {
3307 if (!ready) {
3308 // still not ready if an error happened
3309 ready = !err;
3310 setupListeners();
3311 readyHandlers
3312 .list()
3313 .forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
3314 readyHandlers.reset();
3315 }
3316 return err;
3317 }
3318 // Scroll behavior
3319 function handleScroll(to, from, isPush, isFirstNavigation) {
3320 const { scrollBehavior } = options;
3321 if (!isBrowser || !scrollBehavior)
3322 return Promise.resolve();
3323 const scrollPosition = (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
3324 ((isFirstNavigation || !isPush) &&
3325 history.state &&
3326 history.state.scroll) ||
3327 null;
3328 return vue.nextTick()
3329 .then(() => scrollBehavior(to, from, scrollPosition))
3330 .then(position => position && scrollToPosition(position))
3331 .catch(err => triggerError(err, to, from));
3332 }
3333 const go = (delta) => routerHistory.go(delta);
3334 let started;
3335 const installedApps = new Set();
3336 const router = {
3337 currentRoute,
3338 addRoute,
3339 removeRoute,
3340 hasRoute,
3341 getRoutes,
3342 resolve,
3343 options,
3344 push,
3345 replace,
3346 go,
3347 back: () => go(-1),
3348 forward: () => go(1),
3349 beforeEach: beforeGuards.add,
3350 beforeResolve: beforeResolveGuards.add,
3351 afterEach: afterGuards.add,
3352 onError: errorHandlers.add,
3353 isReady,
3354 install(app) {
3355 const router = this;
3356 app.component('RouterLink', RouterLink);
3357 app.component('RouterView', RouterView);
3358 app.config.globalProperties.$router = router;
3359 Object.defineProperty(app.config.globalProperties, '$route', {
3360 enumerable: true,
3361 get: () => vue.unref(currentRoute),
3362 });
3363 // this initial navigation is only necessary on client, on server it doesn't
3364 // make sense because it will create an extra unnecessary navigation and could
3365 // lead to problems
3366 if (isBrowser &&
3367 // used for the initial navigation client side to avoid pushing
3368 // multiple times when the router is used in multiple apps
3369 !started &&
3370 currentRoute.value === START_LOCATION_NORMALIZED) {
3371 // see above
3372 started = true;
3373 push(routerHistory.location).catch(err => {
3374 warn('Unexpected error when starting the router:', err);
3375 });
3376 }
3377 const reactiveRoute = {};
3378 for (const key in START_LOCATION_NORMALIZED) {
3379 // @ts-expect-error: the key matches
3380 reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);
3381 }
3382 app.provide(routerKey, router);
3383 app.provide(routeLocationKey, vue.reactive(reactiveRoute));
3384 app.provide(routerViewLocationKey, currentRoute);
3385 const unmountApp = app.unmount;
3386 installedApps.add(app);
3387 app.unmount = function () {
3388 installedApps.delete(app);
3389 // the router is not attached to an app anymore
3390 if (installedApps.size < 1) {
3391 // invalidate the current navigation
3392 pendingLocation = START_LOCATION_NORMALIZED;
3393 removeHistoryListener && removeHistoryListener();
3394 removeHistoryListener = null;
3395 currentRoute.value = START_LOCATION_NORMALIZED;
3396 started = false;
3397 ready = false;
3398 }
3399 unmountApp();
3400 };
3401 if (isBrowser) {
3402 addDevtools(app, router, matcher);
3403 }
3404 },
3405 };
3406 return router;
3407}
3408function runGuardQueue(guards) {
3409 return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
3410}
3411function extractChangingRecords(to, from) {
3412 const leavingRecords = [];
3413 const updatingRecords = [];
3414 const enteringRecords = [];
3415 const len = Math.max(from.matched.length, to.matched.length);
3416 for (let i = 0; i < len; i++) {
3417 const recordFrom = from.matched[i];
3418 if (recordFrom) {
3419 if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))
3420 updatingRecords.push(recordFrom);
3421 else
3422 leavingRecords.push(recordFrom);
3423 }
3424 const recordTo = to.matched[i];
3425 if (recordTo) {
3426 // the type doesn't matter because we are comparing per reference
3427 if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {
3428 enteringRecords.push(recordTo);
3429 }
3430 }
3431 }
3432 return [leavingRecords, updatingRecords, enteringRecords];
3433}
3434
3435/**
3436 * Returns the router instance. Equivalent to using `$router` inside
3437 * templates.
3438 */
3439function useRouter() {
3440 return vue.inject(routerKey);
3441}
3442/**
3443 * Returns the current route location. Equivalent to using `$route` inside
3444 * templates.
3445 */
3446function useRoute() {
3447 return vue.inject(routeLocationKey);
3448}
3449
3450exports.RouterLink = RouterLink;
3451exports.RouterView = RouterView;
3452exports.START_LOCATION = START_LOCATION_NORMALIZED;
3453exports.createMemoryHistory = createMemoryHistory;
3454exports.createRouter = createRouter;
3455exports.createRouterMatcher = createRouterMatcher;
3456exports.createWebHashHistory = createWebHashHistory;
3457exports.createWebHistory = createWebHistory;
3458exports.isNavigationFailure = isNavigationFailure;
3459exports.matchedRouteKey = matchedRouteKey;
3460exports.onBeforeRouteLeave = onBeforeRouteLeave;
3461exports.onBeforeRouteUpdate = onBeforeRouteUpdate;
3462exports.parseQuery = parseQuery;
3463exports.routeLocationKey = routeLocationKey;
3464exports.routerKey = routerKey;
3465exports.routerViewLocationKey = routerViewLocationKey;
3466exports.stringifyQuery = stringifyQuery;
3467exports.useLink = useLink;
3468exports.useRoute = useRoute;
3469exports.useRouter = useRouter;
3470exports.viewDepthKey = viewDepthKey;