UNPKG

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