UNPKG

105 kBJavaScriptView Raw
1/*!
2 * vue-router v4.0.0-beta.5
3 * (c) 2020 Eduardo San Martin Morote
4 * @license MIT
5 */
6import { getCurrentInstance, warn as warn$1, inject, computed, unref, defineComponent, reactive, h, provide, ref, shallowRef, nextTick } from 'vue';
7
8const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol';
9const PolySymbol = (name) =>
10// vr = vue router
11hasSymbol
12 ? Symbol( '[vue-router]: ' + name )
13 : ( '[vue-router]: ' ) + name;
14// rvlm = Router View Location Matched
15const matchedRouteKey = PolySymbol( 'router view location matched' );
16// rvd = Router View Depth
17const viewDepthKey = PolySymbol( 'router view depth' );
18// r = router
19const routerKey = PolySymbol( 'router' );
20// rt = route location
21const routeLocationKey = PolySymbol( 'route location' );
22
23const isBrowser = typeof window !== 'undefined';
24
25function isESModule(obj) {
26 return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module');
27}
28const assign = Object.assign;
29function applyToParams(fn, params) {
30 const newParams = {};
31 for (const key in params) {
32 const value = params[key];
33 newParams[key] = Array.isArray(value) ? value.map(fn) : fn(value);
34 }
35 return newParams;
36}
37let noop = () => { };
38
39function warn(msg, ...args) {
40 console.warn('[Vue Router warn]: ' + msg, ...args);
41}
42
43const TRAILING_SLASH_RE = /\/$/;
44const removeTrailingSlash = (path) => path.replace(TRAILING_SLASH_RE, '');
45/**
46 * Transforms an URI into a normalized history location
47 *
48 * @param parseQuery
49 * @param location - URI to normalize
50 * @param currentLocation - current absolute location. Allows resolving relative
51 * paths. Must start with `/`. Defaults to `/`
52 * @returns a normalized history location
53 */
54function parseURL(parseQuery, location, currentLocation = '/') {
55 let path, query = {}, searchString = '', hash = '';
56 // Could use URL and URLSearchParams but IE 11 doesn't support it
57 const searchPos = location.indexOf('?');
58 const hashPos = location.indexOf('#', searchPos > -1 ? searchPos : 0);
59 if (searchPos > -1) {
60 path = location.slice(0, searchPos);
61 searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
62 query = parseQuery(searchString);
63 }
64 if (hashPos > -1) {
65 path = path || location.slice(0, hashPos);
66 // keep the # character
67 hash = location.slice(hashPos, location.length);
68 }
69 // no search and no query
70 path = resolveRelativePath(path != null ? path : location, currentLocation);
71 // empty path means a relative query or hash `?foo=f`, `#thing`
72 return {
73 fullPath: path + (searchString && '?') + searchString + hash,
74 path,
75 query,
76 hash,
77 };
78}
79/**
80 * Stringifies a URL object
81 *
82 * @param stringifyQuery
83 * @param location
84 */
85function stringifyURL(stringifyQuery, location) {
86 let query = location.query ? stringifyQuery(location.query) : '';
87 return location.path + (query && '?') + query + (location.hash || '');
88}
89/**
90 * Strips off the base from the beginning of a location.pathname in a non
91 * case-sensitive way.
92 *
93 * @param pathname - location.pathname
94 * @param base - base to strip off
95 */
96function stripBase(pathname, base) {
97 // no base or base is not found at the beginning
98 if (!base || pathname.toLowerCase().indexOf(base.toLowerCase()))
99 return pathname;
100 return pathname.slice(base.length) || '/';
101}
102/**
103 * Checks if two RouteLocation are equal. This means that both locations are
104 * pointing towards the same {@link RouteRecord} and that all `params`, `query`
105 * parameters and `hash` are the same
106 *
107 * @param a first {@link RouteLocation}
108 * @param b second {@link RouteLocation}
109 */
110function isSameRouteLocation(stringifyQuery, a, b) {
111 let aLastIndex = a.matched.length - 1;
112 let bLastIndex = b.matched.length - 1;
113 return (aLastIndex > -1 &&
114 aLastIndex === bLastIndex &&
115 isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&
116 isSameRouteLocationParams(a.params, b.params) &&
117 stringifyQuery(a.query) === stringifyQuery(b.query) &&
118 a.hash === b.hash);
119}
120/**
121 * Check if two `RouteRecords` are equal. Takes into account aliases: they are
122 * considered equal to the `RouteRecord` they are aliasing.
123 *
124 * @param a first {@link RouteRecord}
125 * @param b second {@link RouteRecord}
126 */
127function isSameRouteRecord(a, b) {
128 // since the original record has an undefined value for aliasOf
129 // but all aliases point to the original record, this will always compare
130 // the original record
131 return (a.aliasOf || a) === (b.aliasOf || b);
132}
133function isSameRouteLocationParams(a, b) {
134 if (Object.keys(a).length !== Object.keys(b).length)
135 return false;
136 for (let key in a) {
137 if (!isSameRouteLocationParamsValue(a[key], b[key]))
138 return false;
139 }
140 return true;
141}
142function isSameRouteLocationParamsValue(a, b) {
143 return Array.isArray(a)
144 ? isEquivalentArray(a, b)
145 : Array.isArray(b)
146 ? isEquivalentArray(b, a)
147 : a === b;
148}
149/**
150 * Check if two arrays are the same or if an array with one single entry is the
151 * same as another primitive value. Used to check query and parameters
152 *
153 * @param a - array of values
154 * @param b - array of values or a single value
155 */
156function isEquivalentArray(a, b) {
157 return Array.isArray(b)
158 ? a.length === b.length && a.every((value, i) => value === b[i])
159 : a.length === 1 && a[0] === b;
160}
161/**
162 * Resolves a relative path that starts with `.`.
163 *
164 * @param to - path location we are resolving
165 * @param from - currentLocation.path, should start with `/`
166 */
167function resolveRelativePath(to, from) {
168 if (to.startsWith('/'))
169 return to;
170 if ( !from.startsWith('/')) {
171 warn(`Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`);
172 return to;
173 }
174 if (!to)
175 return from;
176 const fromSegments = from.split('/');
177 const toSegments = to.split('/');
178 let position = fromSegments.length - 1;
179 let toPosition;
180 let segment;
181 for (toPosition = 0; toPosition < toSegments.length; toPosition++) {
182 segment = toSegments[toPosition];
183 // can't go below zero
184 if (position === 1 || segment === '.')
185 continue;
186 if (segment === '..')
187 position--;
188 // found something that is not relative path
189 else
190 break;
191 }
192 return (fromSegments.slice(0, position).join('/') +
193 '/' +
194 toSegments
195 .slice(toPosition - (toPosition === toSegments.length ? 1 : 0))
196 .join('/'));
197}
198
199var NavigationType;
200(function (NavigationType) {
201 NavigationType["pop"] = "pop";
202 NavigationType["push"] = "push";
203})(NavigationType || (NavigationType = {}));
204var NavigationDirection;
205(function (NavigationDirection) {
206 NavigationDirection["back"] = "back";
207 NavigationDirection["forward"] = "forward";
208 NavigationDirection["unknown"] = "";
209})(NavigationDirection || (NavigationDirection = {}));
210/**
211 * Starting location for Histories
212 */
213const START = '';
214// Generic utils
215/**
216 * Normalizes a base by removing any trailing slash and reading the base tag if
217 * present.
218 *
219 * @param base - base to normalize
220 */
221function normalizeBase(base) {
222 if (!base) {
223 if (isBrowser) {
224 // respect <base> tag
225 const baseEl = document.querySelector('base');
226 base = (baseEl && baseEl.getAttribute('href')) || '/';
227 // strip full URL origin
228 base = base.replace(/^\w+:\/\/[^\/]+/, '');
229 }
230 else {
231 base = '/';
232 }
233 }
234 // ensure leading slash when it was removed by the regex above avoid leading
235 // slash with hash because the file could be read from the disk like file://
236 // and the leading slash would cause problems
237 if (base[0] !== '/' && base[0] !== '#')
238 base = '/' + base;
239 // remove the trailing slash so all other method can just do `base + fullPath`
240 // to build an href
241 return removeTrailingSlash(base);
242}
243// remove any character before the hash
244const BEFORE_HASH_RE = /^[^#]+#/;
245function createHref(base, location) {
246 return base.replace(BEFORE_HASH_RE, '#') + location;
247}
248
249function getElementPosition(el, offset) {
250 const docRect = document.documentElement.getBoundingClientRect();
251 const elRect = el.getBoundingClientRect();
252 return {
253 behavior: offset.behavior,
254 left: elRect.left - docRect.left - (offset.left || 0),
255 top: elRect.top - docRect.top - (offset.top || 0),
256 };
257}
258const computeScrollPosition = () => ({
259 left: window.pageXOffset,
260 top: window.pageYOffset,
261});
262function scrollToPosition(position) {
263 let scrollToOptions;
264 if ('el' in position) {
265 let positionEl = position.el;
266 const isIdSelector = typeof positionEl === 'string' && positionEl.startsWith('#');
267 /**
268 * `id`s can accept pretty much any characters, including CSS combinators
269 * like `>` or `~`. It's still possible to retrieve elements using
270 * `document.getElementById('~')` but it needs to be escaped when using
271 * `document.querySelector('#\\~')` for it to be valid. The only
272 * requirements for `id`s are them to be unique on the page and to not be
273 * empty (`id=""`). Because of that, when passing an id selector, it should
274 * be properly escaped for it to work with `querySelector`. We could check
275 * for the id selector to be simple (no CSS combinators `+ >~`) but that
276 * would make things inconsistent since they are valid characters for an
277 * `id` but would need to be escaped when using `querySelector`, breaking
278 * their usage and ending up in no selector returned. Selectors need to be
279 * escaped:
280 *
281 * - `#1-thing` becomes `#\31 -thing`
282 * - `#with~symbols` becomes `#with\\~symbols`
283 *
284 * - More information about the topic can be found at
285 * https://mathiasbynens.be/notes/html5-id-class.
286 * - Practical example: https://mathiasbynens.be/demo/html5-id
287 */
288 if ( typeof position.el === 'string') {
289 if (!isIdSelector || !document.getElementById(position.el.slice(1))) {
290 try {
291 let foundEl = document.querySelector(position.el);
292 if (isIdSelector && foundEl) {
293 warn(`The selector "${position.el}" should be passed as "el: document.querySelector('${position.el}')" because it starts with "#".`);
294 // return to avoid other warnings
295 return;
296 }
297 }
298 catch (err) {
299 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).`);
300 // return to avoid other warnings
301 return;
302 }
303 }
304 }
305 const el = typeof positionEl === 'string'
306 ? isIdSelector
307 ? document.getElementById(positionEl.slice(1))
308 : document.querySelector(positionEl)
309 : positionEl;
310 if (!el) {
311 warn(`Couldn't find element using selector "${position.el}"`);
312 return;
313 }
314 scrollToOptions = getElementPosition(el, position);
315 }
316 else {
317 scrollToOptions = position;
318 }
319 if ('scrollBehavior' in document.documentElement.style)
320 window.scrollTo(scrollToOptions);
321 else {
322 window.scrollTo(scrollToOptions.left != null ? scrollToOptions.left : window.pageXOffset, scrollToOptions.top != null ? scrollToOptions.top : window.pageYOffset);
323 }
324}
325function getScrollKey(path, delta) {
326 const position = history.state ? history.state.position - delta : -1;
327 return position + path;
328}
329const scrollPositions = new Map();
330function saveScrollPosition(key, scrollPosition) {
331 scrollPositions.set(key, scrollPosition);
332}
333function getSavedScrollPosition(key) {
334 const scroll = scrollPositions.get(key);
335 // consume it so it's not used again
336 scrollPositions.delete(key);
337 return scroll;
338}
339// TODO: RFC about how to save scroll position
340/**
341 * ScrollBehavior instance used by the router to compute and restore the scroll
342 * position when navigating.
343 */
344// export interface ScrollHandler<ScrollPositionEntry extends HistoryStateValue, ScrollPosition extends ScrollPositionEntry> {
345// // returns a scroll position that can be saved in history
346// compute(): ScrollPositionEntry
347// // can take an extended ScrollPositionEntry
348// scroll(position: ScrollPosition): void
349// }
350// export const scrollHandler: ScrollHandler<ScrollPosition> = {
351// compute: computeScroll,
352// scroll: scrollToPosition,
353// }
354
355let createBaseLocation = () => location.protocol + '//' + location.host;
356/**
357 * Creates a normalized history location from a window.location object
358 * @param location
359 */
360function createCurrentLocation(base, location) {
361 const { pathname, search, hash } = location;
362 // allows hash based url
363 const hashPos = base.indexOf('#');
364 if (hashPos > -1) {
365 // prepend the starting slash to hash so the url starts with /#
366 let pathFromHash = hash.slice(1);
367 if (pathFromHash[0] !== '/')
368 pathFromHash = '/' + pathFromHash;
369 return stripBase(pathFromHash, '');
370 }
371 const path = stripBase(pathname, base);
372 return path + search + hash;
373}
374function useHistoryListeners(base, historyState, currentLocation, replace) {
375 let listeners = [];
376 let teardowns = [];
377 // TODO: should it be a stack? a Dict. Check if the popstate listener
378 // can trigger twice
379 let pauseState = null;
380 const popStateHandler = ({ state, }) => {
381 const to = createCurrentLocation(base, location);
382 const from = currentLocation.value;
383 const fromState = historyState.value;
384 let delta = 0;
385 if (state) {
386 currentLocation.value = to;
387 historyState.value = state;
388 // ignore the popstate and reset the pauseState
389 if (pauseState && pauseState === from) {
390 pauseState = null;
391 return;
392 }
393 delta = fromState ? state.position - fromState.position : 0;
394 }
395 else {
396 replace(to);
397 }
398 // console.log({ deltaFromCurrent })
399 // Here we could also revert the navigation by calling history.go(-delta)
400 // this listener will have to be adapted to not trigger again and to wait for the url
401 // to be updated before triggering the listeners. Some kind of validation function would also
402 // need to be passed to the listeners so the navigation can be accepted
403 // call all listeners
404 listeners.forEach(listener => {
405 listener(currentLocation.value, from, {
406 delta,
407 type: NavigationType.pop,
408 direction: delta
409 ? delta > 0
410 ? NavigationDirection.forward
411 : NavigationDirection.back
412 : NavigationDirection.unknown,
413 });
414 });
415 };
416 function pauseListeners() {
417 pauseState = currentLocation.value;
418 }
419 function listen(callback) {
420 // setup the listener and prepare teardown callbacks
421 listeners.push(callback);
422 const teardown = () => {
423 const index = listeners.indexOf(callback);
424 if (index > -1)
425 listeners.splice(index, 1);
426 };
427 teardowns.push(teardown);
428 return teardown;
429 }
430 function beforeUnloadListener() {
431 const { history } = window;
432 if (!history.state)
433 return;
434 history.replaceState(assign({}, history.state, { scroll: computeScrollPosition() }), '');
435 }
436 function destroy() {
437 for (const teardown of teardowns)
438 teardown();
439 teardowns = [];
440 window.removeEventListener('popstate', popStateHandler);
441 window.removeEventListener('beforeunload', beforeUnloadListener);
442 }
443 // setup the listeners and prepare teardown callbacks
444 window.addEventListener('popstate', popStateHandler);
445 window.addEventListener('beforeunload', beforeUnloadListener);
446 return {
447 pauseListeners,
448 listen,
449 destroy,
450 };
451}
452/**
453 * Creates a state object
454 */
455function buildState(back, current, forward, replaced = false, computeScroll = false) {
456 return {
457 back,
458 current,
459 forward,
460 replaced,
461 position: window.history.length,
462 scroll: computeScroll ? computeScrollPosition() : null,
463 };
464}
465function useHistoryStateNavigation(base) {
466 const { history, location } = window;
467 // private variables
468 let currentLocation = {
469 value: createCurrentLocation(base, location),
470 };
471 let historyState = { value: history.state };
472 // build current history entry as this is a fresh navigation
473 if (!historyState.value) {
474 changeLocation(currentLocation.value, {
475 back: null,
476 current: currentLocation.value,
477 forward: null,
478 // the length is off by one, we need to decrease it
479 position: history.length - 1,
480 replaced: true,
481 // don't add a scroll as the user may have an anchor and we want
482 // scrollBehavior to be triggered without a saved position
483 scroll: null,
484 }, true);
485 }
486 function changeLocation(to, state, replace) {
487 const url = createBaseLocation() +
488 // preserve any existing query when base has a hash
489 (base.indexOf('#') > -1 && location.search
490 ? location.pathname + location.search + '#'
491 : base) +
492 to;
493 try {
494 // BROWSER QUIRK
495 // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
496 history[replace ? 'replaceState' : 'pushState'](state, '', url);
497 historyState.value = state;
498 }
499 catch (err) {
500 warn('Error with push/replace State', err);
501 // Force the navigation, this also resets the call count
502 location[replace ? 'replace' : 'assign'](url);
503 }
504 }
505 function replace(to, data) {
506 const state = assign({}, history.state, buildState(historyState.value.back,
507 // keep back and forward entries but override current position
508 to, historyState.value.forward, true), data, { position: historyState.value.position });
509 changeLocation(to, state, true);
510 currentLocation.value = to;
511 }
512 function push(to, data) {
513 // Add to current entry the information of where we are going
514 // as well as saving the current position
515 const currentState = assign({}, history.state, {
516 forward: to,
517 scroll: computeScrollPosition(),
518 });
519 changeLocation(currentState.current, currentState, true);
520 const state = assign({}, buildState(currentLocation.value, to, null), {
521 position: currentState.position + 1,
522 }, data);
523 changeLocation(to, state, false);
524 currentLocation.value = to;
525 }
526 return {
527 location: currentLocation,
528 state: historyState,
529 push,
530 replace,
531 };
532}
533function createWebHistory(base) {
534 base = normalizeBase(base);
535 const historyNavigation = useHistoryStateNavigation(base);
536 const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace);
537 function go(delta, triggerListeners = true) {
538 if (!triggerListeners)
539 historyListeners.pauseListeners();
540 history.go(delta);
541 }
542 const routerHistory = assign({
543 // it's overridden right after
544 location: '',
545 base,
546 go,
547 createHref: createHref.bind(null, base),
548 }, historyNavigation, historyListeners);
549 Object.defineProperty(routerHistory, 'location', {
550 get: () => historyNavigation.location.value,
551 });
552 Object.defineProperty(routerHistory, 'state', {
553 get: () => historyNavigation.state.value,
554 });
555 return routerHistory;
556}
557
558// TODO: verify base is working for SSR
559/**
560 * Creates a in-memory based history. The main purpose of this history is to handle SSR. It starts in a special location that is nowhere.
561 * It's up to the user to replace that location with the starter location.
562 * @param base - Base applied to all urls, defaults to '/'
563 * @returns a history object that can be passed to the router constructor
564 */
565function createMemoryHistory(base = '') {
566 let listeners = [];
567 let queue = [START];
568 let position = 0;
569 function setLocation(location) {
570 position++;
571 if (position === queue.length) {
572 // we are at the end, we can simply append a new entry
573 queue.push(location);
574 }
575 else {
576 // we are in the middle, we remove everything from here in the queue
577 queue.splice(position);
578 queue.push(location);
579 }
580 }
581 function triggerListeners(to, from, { direction, delta }) {
582 const info = {
583 direction,
584 delta,
585 type: NavigationType.pop,
586 };
587 for (let callback of listeners) {
588 callback(to, from, info);
589 }
590 }
591 const routerHistory = {
592 // rewritten by Object.defineProperty
593 location: START,
594 state: {},
595 base,
596 createHref: createHref.bind(null, base),
597 replace(to) {
598 // remove current entry and decrement position
599 queue.splice(position--, 1);
600 setLocation(to);
601 },
602 push(to, data) {
603 setLocation(to);
604 },
605 listen(callback) {
606 listeners.push(callback);
607 return () => {
608 const index = listeners.indexOf(callback);
609 if (index > -1)
610 listeners.splice(index, 1);
611 };
612 },
613 destroy() {
614 listeners = [];
615 },
616 go(delta, shouldTrigger = true) {
617 const from = this.location;
618 const direction =
619 // we are considering delta === 0 going forward, but in abstract mode
620 // using 0 for the delta doesn't make sense like it does in html5 where
621 // it reloads the page
622 delta < 0 ? NavigationDirection.back : NavigationDirection.forward;
623 position = Math.max(0, Math.min(position + delta, queue.length - 1));
624 if (shouldTrigger) {
625 triggerListeners(this.location, from, {
626 direction,
627 delta,
628 });
629 }
630 },
631 };
632 Object.defineProperty(routerHistory, 'location', {
633 get: () => queue[position],
634 });
635 return routerHistory;
636}
637
638/**
639 * Creates a hash history.
640 *
641 * @param base - optional base to provide. Defaults to `location.pathname` or
642 * `/` if at root. If there is a `base` tag in the `head`, its value will be
643 * **ignored**.
644 *
645 * @example
646 * ```js
647 * // at https://example.com/folder
648 * createWebHashHistory() // gives a url of `https://example.com/folder#`
649 * createWebHashHistory('/folder/') // gives a url of `https://example.com/folder/#`
650 * // if the `#` is provided in the base, it won't be added by `createWebHashHistory`
651 * createWebHashHistory('/folder/#/app/') // gives a url of `https://example.com/folder/#/app/`
652 * // you should avoid doing this because it changes the original url and breaks copying urls
653 * createWebHashHistory('/other-folder/') // gives a url of `https://example.com/other-folder/#`
654 *
655 * // at file:///usr/etc/folder/index.html
656 * // for locations with no `host`, the base is ignored
657 * createWebHashHistory('/iAmIgnored') // gives a url of `file:///usr/etc/folder/index.html#`
658 * ```
659 */
660function createWebHashHistory(base) {
661 // Make sure this implementation is fine in terms of encoding, specially for IE11
662 // for `file://`, directly use the pathname and ignore the base
663 // location.pathname contains an initial `/` even at the root: `https://example.com`
664 base = location.host ? base || location.pathname : location.pathname;
665 // allow the user to provide a `#` in the middle: `/base/#/app`
666 if (base.indexOf('#') < 0)
667 base += '#';
668 if ( !base.endsWith('#/') && !base.endsWith('#')) {
669 warn(`A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, '#')}".`);
670 }
671 return createWebHistory(base);
672}
673
674function isRouteLocation(route) {
675 return typeof route === 'string' || (route && typeof route === 'object');
676}
677function isRouteName(name) {
678 return typeof name === 'string' || typeof name === 'symbol';
679}
680
681const START_LOCATION_NORMALIZED = {
682 path: '/',
683 name: undefined,
684 params: {},
685 query: {},
686 hash: '',
687 fullPath: '/',
688 matched: [],
689 meta: {},
690 redirectedFrom: undefined,
691};
692
693const NavigationFailureSymbol = PolySymbol( 'navigation failure' );
694var NavigationFailureType;
695(function (NavigationFailureType) {
696 NavigationFailureType[NavigationFailureType["aborted"] = 4] = "aborted";
697 NavigationFailureType[NavigationFailureType["cancelled"] = 8] = "cancelled";
698 NavigationFailureType[NavigationFailureType["duplicated"] = 16] = "duplicated";
699})(NavigationFailureType || (NavigationFailureType = {}));
700// DEV only debug messages
701const ErrorTypeMessages = {
702 [1 /* MATCHER_NOT_FOUND */]({ location, currentLocation }) {
703 return `No match for\n ${JSON.stringify(location)}${currentLocation
704 ? '\nwhile being at\n' + JSON.stringify(currentLocation)
705 : ''}`;
706 },
707 [2 /* NAVIGATION_GUARD_REDIRECT */]({ from, to, }) {
708 return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`;
709 },
710 [4 /* NAVIGATION_ABORTED */]({ from, to }) {
711 return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`;
712 },
713 [8 /* NAVIGATION_CANCELLED */]({ from, to }) {
714 return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`;
715 },
716 [16 /* NAVIGATION_DUPLICATED */]({ from, to }) {
717 return `Avoided redundant navigation to current location: "${from.fullPath}".`;
718 },
719};
720function createRouterError(type, params) {
721 {
722 return assign(new Error(ErrorTypeMessages[type](params)), {
723 type,
724 [NavigationFailureSymbol]: true,
725 }, params);
726 }
727}
728function isNavigationFailure(error, type) {
729 return (error instanceof Error &&
730 NavigationFailureSymbol in error &&
731 (type == null || !!(error.type & type)));
732}
733const propertiesToLog = ['params', 'query', 'hash'];
734function stringifyRoute(to) {
735 if (typeof to === 'string')
736 return to;
737 if ('path' in to)
738 return to.path;
739 const location = {};
740 for (const key of propertiesToLog) {
741 if (key in to)
742 location[key] = to[key];
743 }
744 return JSON.stringify(location, null, 2);
745}
746
747// default pattern for a param: non greedy everything but /
748const BASE_PARAM_PATTERN = '[^/]+?';
749const BASE_PATH_PARSER_OPTIONS = {
750 sensitive: false,
751 strict: false,
752 start: true,
753 end: true,
754};
755// Special Regex characters that must be escaped in static tokens
756const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
757/**
758 * Creates a path parser from an array of Segments (a segment is an array of Tokens)
759 *
760 * @param segments - array of segments returned by tokenizePath
761 * @param extraOptions - optional options for the regexp
762 * @returns a PathParser
763 */
764function tokensToParser(segments, extraOptions) {
765 const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions);
766 // the amount of scores is the same as the length of segments except for the root segment "/"
767 let score = [];
768 // the regexp as a string
769 let pattern = options.start ? '^' : '';
770 // extracted keys
771 const keys = [];
772 for (const segment of segments) {
773 // the root segment needs special treatment
774 const segmentScores = segment.length ? [] : [90 /* Root */];
775 // allow trailing slash
776 if (options.strict && !segment.length)
777 pattern += '/';
778 for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
779 const token = segment[tokenIndex];
780 // resets the score if we are inside a sub segment /:a-other-:b
781 let subSegmentScore = 40 /* Segment */ +
782 (options.sensitive ? 0.25 /* BonusCaseSensitive */ : 0);
783 if (token.type === 0 /* Static */) {
784 // prepend the slash if we are starting a new segment
785 if (!tokenIndex)
786 pattern += '/';
787 pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
788 subSegmentScore += 40 /* Static */;
789 }
790 else if (token.type === 1 /* Param */) {
791 const { value, repeatable, optional, regexp } = token;
792 keys.push({
793 name: value,
794 repeatable,
795 optional,
796 });
797 const re = regexp ? regexp : BASE_PARAM_PATTERN;
798 // the user provided a custom regexp /:id(\\d+)
799 if (re !== BASE_PARAM_PATTERN) {
800 subSegmentScore += 10 /* BonusCustomRegExp */;
801 // make sure the regexp is valid before using it
802 try {
803 new RegExp(`(${re})`);
804 }
805 catch (err) {
806 throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` +
807 err.message);
808 }
809 }
810 // when we repeat we must take care of the repeating leading slash
811 let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
812 // prepend the slash if we are starting a new segment
813 if (!tokenIndex)
814 subPattern = optional ? `(?:/${subPattern})` : '/' + subPattern;
815 if (optional)
816 subPattern += '?';
817 pattern += subPattern;
818 subSegmentScore += 20 /* Dynamic */;
819 if (optional)
820 subSegmentScore += -8 /* BonusOptional */;
821 if (repeatable)
822 subSegmentScore += -20 /* BonusRepeatable */;
823 if (re === '.*')
824 subSegmentScore += -50 /* BonusWildcard */;
825 }
826 segmentScores.push(subSegmentScore);
827 }
828 // an empty array like /home/ -> [[{home}], []]
829 // if (!segment.length) pattern += '/'
830 score.push(segmentScores);
831 }
832 // only apply the strict bonus to the last score
833 if (options.strict && options.end) {
834 const i = score.length - 1;
835 score[i][score[i].length - 1] += 0.7000000000000001 /* BonusStrict */;
836 }
837 // TODO: dev only warn double trailing slash
838 if (!options.strict)
839 pattern += '/?';
840 if (options.end)
841 pattern += '$';
842 // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
843 else if (options.strict)
844 pattern += '(?:/|$)';
845 const re = new RegExp(pattern, options.sensitive ? '' : 'i');
846 function parse(path) {
847 const match = path.match(re);
848 const params = {};
849 if (!match)
850 return null;
851 for (let i = 1; i < match.length; i++) {
852 const value = match[i] || '';
853 const key = keys[i - 1];
854 params[key.name] = value && key.repeatable ? value.split('/') : value;
855 }
856 return params;
857 }
858 function stringify(params) {
859 let path = '';
860 // for optional parameters to allow to be empty
861 let avoidDuplicatedSlash = false;
862 for (const segment of segments) {
863 if (!avoidDuplicatedSlash || path[path.length - 1] !== '/')
864 path += '/';
865 avoidDuplicatedSlash = false;
866 for (const token of segment) {
867 if (token.type === 0 /* Static */) {
868 path += token.value;
869 }
870 else if (token.type === 1 /* Param */) {
871 const { value, repeatable, optional } = token;
872 const param = value in params ? params[value] : '';
873 if (Array.isArray(param) && !repeatable)
874 throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`);
875 const text = Array.isArray(param) ? param.join('/') : param;
876 if (!text) {
877 // do not append a slash on the next iteration
878 if (optional)
879 avoidDuplicatedSlash = true;
880 else
881 throw new Error(`Missing required param "${value}"`);
882 }
883 path += text;
884 }
885 }
886 }
887 return path;
888 }
889 return {
890 re,
891 score,
892 keys,
893 parse,
894 stringify,
895 };
896}
897/**
898 * Compares an array of numbers as used in PathParser.score and returns a
899 * number. This function can be used to `sort` an array
900 * @param a - first array of numbers
901 * @param b - second array of numbers
902 * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
903 * should be sorted first
904 */
905function compareScoreArray(a, b) {
906 let i = 0;
907 while (i < a.length && i < b.length) {
908 const diff = b[i] - a[i];
909 // only keep going if diff === 0
910 if (diff)
911 return diff;
912 i++;
913 }
914 // if the last subsegment was Static, the shorter segments should be sorted first
915 // otherwise sort the longest segment first
916 if (a.length < b.length) {
917 return a.length === 1 && a[0] === 40 /* Static */ + 40 /* Segment */
918 ? -1
919 : 1;
920 }
921 else if (a.length > b.length) {
922 return b.length === 1 && b[0] === 40 /* Static */ + 40 /* Segment */
923 ? 1
924 : -1;
925 }
926 return 0;
927}
928/**
929 * Compare function that can be used with `sort` to sort an array of PathParser
930 * @param a - first PathParser
931 * @param b - second PathParser
932 * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
933 */
934function comparePathParserScore(a, b) {
935 let i = 0;
936 const aScore = a.score;
937 const bScore = b.score;
938 while (i < aScore.length && i < bScore.length) {
939 const comp = compareScoreArray(aScore[i], bScore[i]);
940 // do not return if both are equal
941 if (comp)
942 return comp;
943 i++;
944 }
945 // if a and b share the same score entries but b has more, sort b first
946 return bScore.length - aScore.length;
947 // this is the ternary version
948 // return aScore.length < bScore.length
949 // ? 1
950 // : aScore.length > bScore.length
951 // ? -1
952 // : 0
953}
954
955const ROOT_TOKEN = {
956 type: 0 /* Static */,
957 value: '',
958};
959const VALID_PARAM_RE = /[a-zA-Z0-9_]/;
960// After some profiling, the cache seems to be unnecessary because tokenizePath
961// (the slowest part of adding a route) is very fast
962// const tokenCache = new Map<string, Token[][]>()
963function tokenizePath(path) {
964 if (!path)
965 return [[]];
966 if (path === '/')
967 return [[ROOT_TOKEN]];
968 // remove the leading slash
969 if (path[0] !== '/')
970 throw new Error('A non-empty path must start with "/"');
971 // if (tokenCache.has(path)) return tokenCache.get(path)!
972 function crash(message) {
973 throw new Error(`ERR (${state})/"${buffer}": ${message}`);
974 }
975 let state = 0 /* Static */;
976 let previousState = state;
977 const tokens = [];
978 // the segment will always be valid because we get into the initial state
979 // with the leading /
980 let segment;
981 function finalizeSegment() {
982 if (segment)
983 tokens.push(segment);
984 segment = [];
985 }
986 // index on the path
987 let i = 0;
988 // char at index
989 let char;
990 // buffer of the value read
991 let buffer = '';
992 // custom regexp for a param
993 let customRe = '';
994 function consumeBuffer() {
995 if (!buffer)
996 return;
997 if (state === 0 /* Static */) {
998 segment.push({
999 type: 0 /* Static */,
1000 value: buffer,
1001 });
1002 }
1003 else if (state === 1 /* Param */ ||
1004 state === 2 /* ParamRegExp */ ||
1005 state === 3 /* ParamRegExpEnd */) {
1006 if (segment.length > 1 && (char === '*' || char === '+'))
1007 crash(`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`);
1008 segment.push({
1009 type: 1 /* Param */,
1010 value: buffer,
1011 regexp: customRe,
1012 repeatable: char === '*' || char === '+',
1013 optional: char === '*' || char === '?',
1014 });
1015 }
1016 else {
1017 crash('Invalid state to consume buffer');
1018 }
1019 buffer = '';
1020 }
1021 function addCharToBuffer() {
1022 buffer += char;
1023 }
1024 while (i < path.length) {
1025 char = path[i++];
1026 if (char === '\\' && state !== 2 /* ParamRegExp */) {
1027 previousState = state;
1028 state = 4 /* EscapeNext */;
1029 continue;
1030 }
1031 switch (state) {
1032 case 0 /* Static */:
1033 if (char === '/') {
1034 if (buffer) {
1035 consumeBuffer();
1036 }
1037 finalizeSegment();
1038 }
1039 else if (char === ':') {
1040 consumeBuffer();
1041 state = 1 /* Param */;
1042 }
1043 else {
1044 addCharToBuffer();
1045 }
1046 break;
1047 case 4 /* EscapeNext */:
1048 addCharToBuffer();
1049 state = previousState;
1050 break;
1051 case 1 /* Param */:
1052 if (char === '(') {
1053 state = 2 /* ParamRegExp */;
1054 customRe = '';
1055 }
1056 else if (VALID_PARAM_RE.test(char)) {
1057 addCharToBuffer();
1058 }
1059 else {
1060 consumeBuffer();
1061 state = 0 /* Static */;
1062 // go back one character if we were not modifying
1063 if (char !== '*' && char !== '?' && char !== '+')
1064 i--;
1065 }
1066 break;
1067 case 2 /* ParamRegExp */:
1068 if (char === ')') {
1069 // handle the escaped )
1070 if (customRe[customRe.length - 1] == '\\')
1071 customRe = customRe.slice(0, -1) + char;
1072 else
1073 state = 3 /* ParamRegExpEnd */;
1074 }
1075 else {
1076 customRe += char;
1077 }
1078 break;
1079 case 3 /* ParamRegExpEnd */:
1080 // same as finalizing a param
1081 consumeBuffer();
1082 state = 0 /* Static */;
1083 // go back one character if we were not modifying
1084 if (char !== '*' && char !== '?' && char !== '+')
1085 i--;
1086 break;
1087 default:
1088 crash('Unknown state');
1089 break;
1090 }
1091 }
1092 if (state === 2 /* ParamRegExp */)
1093 crash(`Unfinished custom RegExp for param "${buffer}"`);
1094 consumeBuffer();
1095 finalizeSegment();
1096 // tokenCache.set(path, tokens)
1097 return tokens;
1098}
1099
1100function createRouteRecordMatcher(record, parent, options) {
1101 const parser = tokensToParser(tokenizePath(record.path), options);
1102 // warn against params with the same name
1103 {
1104 const existingKeys = new Set();
1105 for (const key of parser.keys) {
1106 if (existingKeys.has(key.name))
1107 warn(`Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`);
1108 existingKeys.add(key.name);
1109 }
1110 }
1111 const matcher = assign(parser, {
1112 record,
1113 parent,
1114 // these needs to be populated by the parent
1115 children: [],
1116 alias: [],
1117 });
1118 if (parent) {
1119 // both are aliases or both are not aliases
1120 // we don't want to mix them because the order is used when
1121 // passing originalRecord in Matcher.addRoute
1122 if (!matcher.record.aliasOf === !parent.record.aliasOf)
1123 parent.children.push(matcher);
1124 }
1125 return matcher;
1126}
1127
1128/**
1129 * Creates a Router Matcher.
1130 *
1131 * @internal
1132 * @param routes - array of initial routes
1133 * @param globalOptions - global route options
1134 */
1135function createRouterMatcher(routes, globalOptions) {
1136 // normalized ordered array of matchers
1137 const matchers = [];
1138 const matcherMap = new Map();
1139 globalOptions = mergeOptions({ strict: false, end: true, sensitive: false }, globalOptions);
1140 function getRecordMatcher(name) {
1141 return matcherMap.get(name);
1142 }
1143 function addRoute(record, parent, originalRecord) {
1144 // used later on to remove by name
1145 let isRootAdd = !originalRecord;
1146 let mainNormalizedRecord = normalizeRouteRecord(record);
1147 // we might be the child of an alias
1148 mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record;
1149 const options = mergeOptions(globalOptions, record);
1150 // generate an array of records to correctly handle aliases
1151 const normalizedRecords = [
1152 mainNormalizedRecord,
1153 ];
1154 if ('alias' in record) {
1155 const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias;
1156 for (const alias of aliases) {
1157 normalizedRecords.push(assign({}, mainNormalizedRecord, {
1158 // this allows us to hold a copy of the `components` option
1159 // so that async components cache is hold on the original record
1160 components: originalRecord
1161 ? originalRecord.record.components
1162 : mainNormalizedRecord.components,
1163 path: alias,
1164 // we might be the child of an alias
1165 aliasOf: originalRecord
1166 ? originalRecord.record
1167 : mainNormalizedRecord,
1168 }));
1169 }
1170 }
1171 let matcher;
1172 let originalMatcher;
1173 for (const normalizedRecord of normalizedRecords) {
1174 let { path } = normalizedRecord;
1175 // Build up the path for nested routes if the child isn't an absolute
1176 // route. Only add the / delimiter if the child path isn't empty and if the
1177 // parent path doesn't have a trailing slash
1178 if (parent && path[0] !== '/') {
1179 let parentPath = parent.record.path;
1180 let connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/';
1181 normalizedRecord.path =
1182 parent.record.path + (path && connectingSlash + path);
1183 }
1184 // create the object before hand so it can be passed to children
1185 matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
1186 if ( parent && path[0] === '/')
1187 checkMissingParamsInAbsolutePath(matcher, parent);
1188 // if we are an alias we must tell the original record that we exist
1189 // so we can be removed
1190 if (originalRecord) {
1191 originalRecord.alias.push(matcher);
1192 {
1193 checkSameParams(originalRecord, matcher);
1194 }
1195 }
1196 else {
1197 // otherwise, the first record is the original and others are aliases
1198 originalMatcher = originalMatcher || matcher;
1199 if (originalMatcher !== matcher)
1200 originalMatcher.alias.push(matcher);
1201 // remove the route if named and only for the top record (avoid in nested calls)
1202 // this works because the original record is the first one
1203 if (isRootAdd && record.name && !isAliasRecord(matcher))
1204 removeRoute(record.name);
1205 }
1206 if ('children' in mainNormalizedRecord) {
1207 let children = mainNormalizedRecord.children;
1208 for (let i = 0; i < children.length; i++) {
1209 addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
1210 }
1211 }
1212 // if there was no original record, then the first one was not an alias and all
1213 // other alias (if any) need to reference this record when adding children
1214 originalRecord = originalRecord || matcher;
1215 insertMatcher(matcher);
1216 }
1217 return originalMatcher
1218 ? () => {
1219 // since other matchers are aliases, they should be removed by the original matcher
1220 removeRoute(originalMatcher);
1221 }
1222 : noop;
1223 }
1224 function removeRoute(matcherRef) {
1225 if (isRouteName(matcherRef)) {
1226 const matcher = matcherMap.get(matcherRef);
1227 if (matcher) {
1228 matcherMap.delete(matcherRef);
1229 matchers.splice(matchers.indexOf(matcher), 1);
1230 matcher.children.forEach(removeRoute);
1231 matcher.alias.forEach(removeRoute);
1232 }
1233 }
1234 else {
1235 let index = matchers.indexOf(matcherRef);
1236 if (index > -1) {
1237 matchers.splice(index, 1);
1238 if (matcherRef.record.name)
1239 matcherMap.delete(matcherRef.record.name);
1240 matcherRef.children.forEach(removeRoute);
1241 matcherRef.alias.forEach(removeRoute);
1242 }
1243 }
1244 }
1245 function getRoutes() {
1246 return matchers;
1247 }
1248 function insertMatcher(matcher) {
1249 let i = 0;
1250 // console.log('i is', { i })
1251 while (i < matchers.length &&
1252 comparePathParserScore(matcher, matchers[i]) >= 0)
1253 i++;
1254 // console.log('END i is', { i })
1255 // while (i < matchers.length && matcher.score <= matchers[i].score) i++
1256 matchers.splice(i, 0, matcher);
1257 // only add the original record to the name map
1258 if (matcher.record.name && !isAliasRecord(matcher))
1259 matcherMap.set(matcher.record.name, matcher);
1260 }
1261 function resolve(location, currentLocation) {
1262 let matcher;
1263 let params = {};
1264 let path;
1265 let name;
1266 if ('name' in location && location.name) {
1267 matcher = matcherMap.get(location.name);
1268 if (!matcher)
1269 throw createRouterError(1 /* MATCHER_NOT_FOUND */, {
1270 location,
1271 });
1272 name = matcher.record.name;
1273 params = assign(
1274 // paramsFromLocation is a new object
1275 paramsFromLocation(currentLocation.params,
1276 // only keep params that exist in the resolved location
1277 // TODO: only keep optional params coming from a parent record
1278 matcher.keys.map(k => k.name)), location.params);
1279 // throws if cannot be stringified
1280 path = matcher.stringify(params);
1281 }
1282 else if ('path' in location) {
1283 // no need to resolve the path with the matcher as it was provided
1284 // this also allows the user to control the encoding
1285 path = location.path;
1286 if ( path[0] !== '/') {
1287 warn(`The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-router-next.`);
1288 }
1289 matcher = matchers.find(m => m.re.test(path));
1290 // matcher should have a value after the loop
1291 if (matcher) {
1292 // TODO: dev warning of unused params if provided
1293 params = matcher.parse(path);
1294 name = matcher.record.name;
1295 }
1296 // location is a relative path
1297 }
1298 else {
1299 // match by name or path of current route
1300 matcher = currentLocation.name
1301 ? matcherMap.get(currentLocation.name)
1302 : matchers.find(m => m.re.test(currentLocation.path));
1303 if (!matcher)
1304 throw createRouterError(1 /* MATCHER_NOT_FOUND */, {
1305 location,
1306 currentLocation,
1307 });
1308 name = matcher.record.name;
1309 // since we are navigating to the same location, we don't need to pick the
1310 // params like when `name` is provided
1311 params = assign({}, currentLocation.params, location.params);
1312 path = matcher.stringify(params);
1313 }
1314 const matched = [];
1315 let parentMatcher = matcher;
1316 while (parentMatcher) {
1317 // reversed order so parents are at the beginning
1318 matched.unshift(parentMatcher.record);
1319 parentMatcher = parentMatcher.parent;
1320 }
1321 return {
1322 name,
1323 path,
1324 params,
1325 matched,
1326 meta: mergeMetaFields(matched),
1327 };
1328 }
1329 // add initial routes
1330 routes.forEach(route => addRoute(route));
1331 return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher };
1332}
1333function paramsFromLocation(params, keys) {
1334 let newParams = {};
1335 for (let key of keys) {
1336 if (key in params)
1337 newParams[key] = params[key];
1338 }
1339 return newParams;
1340}
1341/**
1342 * Normalizes a RouteRecordRaw. Creates a copy
1343 *
1344 * @param record
1345 * @returns the normalized version
1346 */
1347function normalizeRouteRecord(record) {
1348 return {
1349 path: record.path,
1350 redirect: record.redirect,
1351 name: record.name,
1352 meta: record.meta || {},
1353 aliasOf: undefined,
1354 beforeEnter: record.beforeEnter,
1355 props: normalizeRecordProps(record),
1356 children: record.children || [],
1357 instances: {},
1358 leaveGuards: [],
1359 updateGuards: [],
1360 enterCallbacks: {},
1361 components: 'components' in record
1362 ? record.components || {}
1363 : { default: record.component },
1364 };
1365}
1366/**
1367 * Normalize the optional `props` in a record to always be an object similar to
1368 * components. Also accept a boolean for components.
1369 * @param record
1370 */
1371function normalizeRecordProps(record) {
1372 const propsObject = {};
1373 // props does not exist on redirect records but we can set false directly
1374 const props = record.props || false;
1375 if ('component' in record) {
1376 propsObject.default = props;
1377 }
1378 else {
1379 // NOTE: we could also allow a function to be applied to every component.
1380 // Would need user feedback for use cases
1381 for (let name in record.components)
1382 propsObject[name] = typeof props === 'boolean' ? props : props[name];
1383 }
1384 return propsObject;
1385}
1386/**
1387 * Checks if a record or any of its parent is an alias
1388 * @param record
1389 */
1390function isAliasRecord(record) {
1391 while (record) {
1392 if (record.record.aliasOf)
1393 return true;
1394 record = record.parent;
1395 }
1396 return false;
1397}
1398/**
1399 * Merge meta fields of an array of records
1400 *
1401 * @param matched array of matched records
1402 */
1403function mergeMetaFields(matched) {
1404 return matched.reduce((meta, record) => assign(meta, record.meta), {});
1405}
1406function mergeOptions(defaults, partialOptions) {
1407 let options = {};
1408 for (let key in defaults) {
1409 options[key] =
1410 key in partialOptions ? partialOptions[key] : defaults[key];
1411 }
1412 return options;
1413}
1414function isSameParam(a, b) {
1415 return (a.name === b.name &&
1416 a.optional === b.optional &&
1417 a.repeatable === b.repeatable);
1418}
1419function checkSameParams(a, b) {
1420 for (let key of a.keys) {
1421 if (!b.keys.find(isSameParam.bind(null, key)))
1422 return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" should have the exact same param named "${key.name}"`);
1423 }
1424 for (let key of b.keys) {
1425 if (!a.keys.find(isSameParam.bind(null, key)))
1426 return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" should have the exact same param named "${key.name}"`);
1427 }
1428}
1429function checkMissingParamsInAbsolutePath(record, parent) {
1430 for (let key of parent.keys) {
1431 if (!record.keys.find(isSameParam.bind(null, key)))
1432 return warn(`Absolute path "${record.record.path}" should have the exact same param named "${key.name}" as its parent "${parent.record.path}".`);
1433 }
1434}
1435
1436/**
1437 * Encoding Rules ␣ = Space Path: ␣ " < > # ? { } Query: ␣ " < > # & = Hash: ␣ "
1438 * < > `
1439 *
1440 * On top of that, the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2)
1441 * defines some extra characters to be encoded. Most browsers do not encode them
1442 * in encodeURI https://github.com/whatwg/url/issues/369, so it may be safer to
1443 * also encode `!'()*`. Leaving unencoded only ASCII alphanumeric(`a-zA-Z0-9`)
1444 * plus `-._~`. This extra safety should be applied to query by patching the
1445 * string returned by encodeURIComponent encodeURI also encodes `[\]^`. `\`
1446 * should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a `\`
1447 * into a `/` if directly typed in. The _backtick_ (`````) should also be
1448 * encoded everywhere because some browsers like FF encode it when directly
1449 * written while others don't. Safari and IE don't encode ``"<>{}``` in hash.
1450 */
1451// const EXTRA_RESERVED_RE = /[!'()*]/g
1452// const encodeReservedReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16)
1453const HASH_RE = /#/g; // %23
1454const AMPERSAND_RE = /&/g; // %26
1455const SLASH_RE = /\//g; // %2F
1456const EQUAL_RE = /=/g; // %3D
1457const IM_RE = /\?/g; // %3F
1458const ENC_BRACKET_OPEN_RE = /%5B/g; // [
1459const ENC_BRACKET_CLOSE_RE = /%5D/g; // ]
1460const ENC_CARET_RE = /%5E/g; // ^
1461const ENC_BACKTICK_RE = /%60/g; // `
1462const ENC_CURLY_OPEN_RE = /%7B/g; // {
1463const ENC_PIPE_RE = /%7C/g; // |
1464const ENC_CURLY_CLOSE_RE = /%7D/g; // }
1465/**
1466 * Encode characters that need to be encoded on the path, search and hash
1467 * sections of the URL.
1468 *
1469 * @internal
1470 * @param text - string to encode
1471 * @returns encoded string
1472 */
1473function commonEncode(text) {
1474 return encodeURI('' + text)
1475 .replace(ENC_PIPE_RE, '|')
1476 .replace(ENC_BRACKET_OPEN_RE, '[')
1477 .replace(ENC_BRACKET_CLOSE_RE, ']');
1478}
1479/**
1480 * Encode characters that need to be encoded on the hash section of the URL.
1481 *
1482 * @param text - string to encode
1483 * @returns encoded string
1484 */
1485function encodeHash(text) {
1486 return commonEncode(text)
1487 .replace(ENC_CURLY_OPEN_RE, '{')
1488 .replace(ENC_CURLY_CLOSE_RE, '}')
1489 .replace(ENC_CARET_RE, '^');
1490}
1491/**
1492 * Encode characters that need to be encoded query keys and values on the query
1493 * section of the URL.
1494 *
1495 * @param text - string to encode
1496 * @returns encoded string
1497 */
1498function encodeQueryProperty(text) {
1499 return commonEncode(text)
1500 .replace(HASH_RE, '%23')
1501 .replace(AMPERSAND_RE, '%26')
1502 .replace(EQUAL_RE, '%3D')
1503 .replace(ENC_BACKTICK_RE, '`')
1504 .replace(ENC_CURLY_OPEN_RE, '{')
1505 .replace(ENC_CURLY_CLOSE_RE, '}')
1506 .replace(ENC_CARET_RE, '^');
1507}
1508/**
1509 * Encode characters that need to be encoded on the path section of the URL.
1510 *
1511 * @param text - string to encode
1512 * @returns encoded string
1513 */
1514function encodePath(text) {
1515 return commonEncode(text).replace(HASH_RE, '%23').replace(IM_RE, '%3F');
1516}
1517/**
1518 * Encode characters that need to be encoded on the path section of the URL as a
1519 * param. This function encodes everything {@link encodePath} does plus the
1520 * slash (`/`) character.
1521 *
1522 * @param text - string to encode
1523 * @returns encoded string
1524 */
1525function encodeParam(text) {
1526 return encodePath(text).replace(SLASH_RE, '%2F');
1527}
1528/**
1529 * Decode text using `decodeURIComponent`. Returns the original text if it
1530 * fails.
1531 *
1532 * @param text - string to decode
1533 * @returns decoded string
1534 */
1535function decode(text) {
1536 try {
1537 return decodeURIComponent('' + text);
1538 }
1539 catch (err) {
1540 warn(`Error decoding "${text}". Using original value`);
1541 }
1542 return '' + text;
1543}
1544
1545/**
1546 * Transforms a queryString into a {@link LocationQuery} object. Accept both, a
1547 * version with the leading `?` and without Should work as URLSearchParams
1548 *
1549 * @param search - search string to parse
1550 * @returns a query object
1551 */
1552function parseQuery(search) {
1553 const query = {};
1554 // avoid creating an object with an empty key and empty value
1555 // because of split('&')
1556 if (search === '' || search === '?')
1557 return query;
1558 const hasLeadingIM = search[0] === '?';
1559 const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&');
1560 for (let i = 0; i < searchParams.length; ++i) {
1561 let [key, rawValue] = searchParams[i].split('=');
1562 key = decode(key);
1563 // avoid decoding null
1564 let value = rawValue == null ? null : decode(rawValue);
1565 if (key in query) {
1566 // an extra variable for ts types
1567 let currentValue = query[key];
1568 if (!Array.isArray(currentValue)) {
1569 currentValue = query[key] = [currentValue];
1570 }
1571 currentValue.push(value);
1572 }
1573 else {
1574 query[key] = value;
1575 }
1576 }
1577 return query;
1578}
1579/**
1580 * Stringifies a {@link LocationQueryRaw} object. Like `URLSearchParams`, it
1581 * doesn't prepend a `?`
1582 *
1583 * @param query - query object to stringify
1584 * @returns string version of the query without the leading `?`
1585 */
1586function stringifyQuery(query) {
1587 let search = '';
1588 for (let key in query) {
1589 if (search.length)
1590 search += '&';
1591 const value = query[key];
1592 key = encodeQueryProperty(key);
1593 if (value == null) {
1594 // only null adds the value
1595 if (value !== undefined)
1596 search += key;
1597 continue;
1598 }
1599 // keep null values
1600 let values = Array.isArray(value)
1601 ? value.map(v => v && encodeQueryProperty(v))
1602 : [value && encodeQueryProperty(value)];
1603 for (let i = 0; i < values.length; i++) {
1604 // only append & with i > 0
1605 search += (i ? '&' : '') + key;
1606 if (values[i] != null)
1607 search += ('=' + values[i]);
1608 }
1609 }
1610 return search;
1611}
1612/**
1613 * Transforms a {@link LocationQueryRaw} into a {@link LocationQuery} by casting
1614 * numbers into strings, removing keys with an undefined value and replacing
1615 * undefined with null in arrays
1616 *
1617 * @param query - query object to normalize
1618 * @returns a normalized query object
1619 */
1620function normalizeQuery(query) {
1621 const normalizedQuery = {};
1622 for (let key in query) {
1623 let value = query[key];
1624 if (value !== undefined) {
1625 normalizedQuery[key] = Array.isArray(value)
1626 ? value.map(v => (v == null ? null : '' + v))
1627 : value == null
1628 ? value
1629 : '' + value;
1630 }
1631 }
1632 return normalizedQuery;
1633}
1634
1635/**
1636 * Create a list of callbacks that can be reset. Used to create before and after navigation guards list
1637 */
1638function useCallbacks() {
1639 let handlers = [];
1640 function add(handler) {
1641 handlers.push(handler);
1642 return () => {
1643 const i = handlers.indexOf(handler);
1644 if (i > -1)
1645 handlers.splice(i, 1);
1646 };
1647 }
1648 function reset() {
1649 handlers = [];
1650 }
1651 return {
1652 add,
1653 list: () => handlers,
1654 reset,
1655 };
1656}
1657
1658/**
1659 * Add a navigation guard that triggers whenever the current location is
1660 * left. Similarly to {@link beforeRouteLeave}, it has access to the
1661 * component instance as `this`.
1662 *
1663 * @param leaveGuard - {@link NavigationGuard}
1664 */
1665function onBeforeRouteLeave(leaveGuard) {
1666 const instance = getCurrentInstance();
1667 if (!instance) {
1668
1669 warn$1('onBeforeRouteLeave must be called at the top of a setup function');
1670 return;
1671 }
1672 const activeRecord = inject(matchedRouteKey, {}).value;
1673 if (!activeRecord) {
1674
1675 warn$1('onBeforeRouteLeave must be called at the top of a setup function');
1676 return;
1677 }
1678 activeRecord.leaveGuards.push(
1679 // @ts-ignore do we even want to allow that? Passing the context in a composition api hook doesn't make sense
1680 leaveGuard.bind(instance.proxy));
1681}
1682/**
1683 * Add a navigation guard that triggers whenever the current location is
1684 * updated. Similarly to {@link beforeRouteUpdate}, it has access to the
1685 * component instance as `this`.
1686 *
1687 * @param updateGuard - {@link NavigationGuard}
1688 */
1689function onBeforeRouteUpdate(updateGuard) {
1690 const instance = getCurrentInstance();
1691 if (!instance) {
1692
1693 warn$1('onBeforeRouteUpdate must be called at the top of a setup function');
1694 return;
1695 }
1696 const activeRecord = inject(matchedRouteKey, {}).value;
1697 if (!activeRecord) {
1698
1699 warn$1('onBeforeRouteUpdate must be called at the top of a setup function');
1700 return;
1701 }
1702 activeRecord.updateGuards.push(
1703 // @ts-ignore do we even want to allow that? Passing the context in a composition api hook doesn't make sense
1704 updateGuard.bind(instance.proxy));
1705}
1706function guardToPromiseFn(guard, to, from, record, name) {
1707 // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place
1708 const enterCallbackArray = record &&
1709 // name is defined if record is because of the function overload
1710 (record.enterCallbacks[name] = record.enterCallbacks[name] || []);
1711 return () => new Promise((resolve, reject) => {
1712 const next = (valid) => {
1713 if (valid === false)
1714 reject(createRouterError(4 /* NAVIGATION_ABORTED */, {
1715 from,
1716 to,
1717 }));
1718 else if (valid instanceof Error) {
1719 reject(valid);
1720 }
1721 else if (isRouteLocation(valid)) {
1722 reject(createRouterError(2 /* NAVIGATION_GUARD_REDIRECT */, {
1723 from: to,
1724 to: valid,
1725 }));
1726 }
1727 else {
1728 if (enterCallbackArray &&
1729 // since enterCallbackArray is truthy, both record and name also are
1730 record.enterCallbacks[name] === enterCallbackArray &&
1731 typeof valid === 'function')
1732 enterCallbackArray.push(valid);
1733 resolve();
1734 }
1735 };
1736 // wrapping with Promise.resolve allows it to work with both async and sync guards
1737 let guardCall = Promise.resolve(guard.call(record && record.instances[name], to, from,
1738 // TODO: could wrap in dev to check if the guard returns before
1739 // calling next with 3 or more arguments. This would help people
1740 // forgetting to remove the `next` argument
1741 canOnlyBeCalledOnce(next, to, from) ));
1742 if (guard.length < 3)
1743 guardCall = guardCall.then(next);
1744 if ( guard.length > 2)
1745 guardCall = guardCall.then(() => {
1746 // @ts-ignore: _called is added at canOnlyBeCalledOnce
1747 if (!next._called)
1748 warn$1(`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.`);
1749 return Promise.reject(new Error('Invalid navigation guard'));
1750 });
1751 guardCall.catch(err => reject(err));
1752 });
1753}
1754function canOnlyBeCalledOnce(next, to, from) {
1755 let called = 0;
1756 return function () {
1757 if (called++ === 1)
1758 warn$1(`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.`);
1759 // @ts-ignore: we put it in the original one because it's easier to check
1760 next._called = true;
1761 if (called === 1)
1762 next.apply(null, arguments);
1763 };
1764}
1765function extractComponentsGuards(matched, guardType, to, from) {
1766 const guards = [];
1767 for (const record of matched) {
1768 for (const name in record.components) {
1769 let rawComponent = record.components[name];
1770 // warn if user wrote import('/component.vue') instead of () => import('./component.vue')
1771 if ( 'then' in rawComponent) {
1772 warn$1(`Component "${name}" in record with path "${record.path}" is a Promise instead of a function that returns a Promise. Did you write "import('./MyPage.vue')" instead of "() => import('./MyPage.vue')"? This will break in production if not fixed.`);
1773 let promise = rawComponent;
1774 rawComponent = () => promise;
1775 }
1776 // skip update and leave guards if the route component is not mounted
1777 if (guardType !== 'beforeRouteEnter' && !record.instances[name])
1778 continue;
1779 if (isRouteComponent(rawComponent)) {
1780 // __vccOpts is added by vue-class-component and contain the regular options
1781 let options = rawComponent.__vccOpts || rawComponent;
1782 const guard = options[guardType];
1783 guard && guards.push(guardToPromiseFn(guard, to, from, record, name));
1784 }
1785 else {
1786 // start requesting the chunk already
1787 let componentPromise = rawComponent();
1788 if ( !('catch' in componentPromise)) {
1789 warn$1(`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.`);
1790 componentPromise = Promise.resolve(componentPromise);
1791 }
1792 else {
1793 componentPromise = componentPromise.catch(() => null);
1794 }
1795 guards.push(() => componentPromise.then(resolved => {
1796 if (!resolved)
1797 return Promise.reject(new Error(`Couldn't resolve component "${name}" for the following record with path "${record.path}"`));
1798 const resolvedComponent = isESModule(resolved)
1799 ? resolved.default
1800 : resolved;
1801 // replace the function with the resolved component
1802 record.components[name] = resolvedComponent;
1803 // @ts-ignore: the options types are not propagated to Component
1804 const guard = resolvedComponent[guardType];
1805 return guard && guardToPromiseFn(guard, to, from, record, name)();
1806 }));
1807 }
1808 }
1809 }
1810 return guards;
1811}
1812/**
1813 * Allows differentiating lazy components from functional components and vue-class-component
1814 * @param component
1815 */
1816function isRouteComponent(component) {
1817 return (typeof component === 'object' ||
1818 'displayName' in component ||
1819 'props' in component ||
1820 '__vccOpts' in component);
1821}
1822
1823// TODO: we could allow currentRoute as a prop to expose `isActive` and
1824// `isExactActive` behavior should go through an RFC
1825function useLink(props) {
1826 const router = inject(routerKey);
1827 const currentRoute = inject(routeLocationKey);
1828 const route = computed(() => router.resolve(unref(props.to)));
1829 const activeRecordIndex = computed(() => {
1830 let { matched } = route.value;
1831 let { length } = matched;
1832 const routeMatched = matched[length - 1];
1833 let currentMatched = currentRoute.matched;
1834 if (!routeMatched || !currentMatched.length)
1835 return -1;
1836 let index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));
1837 if (index > -1)
1838 return index;
1839 // possible parent record
1840 let parentRecordPath = getOriginalPath(matched[length - 2]);
1841 return (
1842 // we are dealing with nested routes
1843 length > 1 &&
1844 // if the have the same path, this link is referring to the empty child
1845 // are we currently are on a different child of the same parent
1846 getOriginalPath(routeMatched) === parentRecordPath &&
1847 // avoid comparing the child with its parent
1848 currentMatched[currentMatched.length - 1].path !== parentRecordPath
1849 ? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))
1850 : index);
1851 });
1852 const isActive = computed(() => activeRecordIndex.value > -1 &&
1853 includesParams(currentRoute.params, route.value.params));
1854 const isExactActive = computed(() => activeRecordIndex.value > -1 &&
1855 activeRecordIndex.value === currentRoute.matched.length - 1 &&
1856 isSameRouteLocationParams(currentRoute.params, route.value.params));
1857 function navigate(e = {}) {
1858 if (guardEvent(e))
1859 return router[unref(props.replace) ? 'replace' : 'push'](unref(props.to));
1860 return Promise.resolve();
1861 }
1862 return {
1863 route,
1864 href: computed(() => route.value.href),
1865 isActive,
1866 isExactActive,
1867 navigate,
1868 };
1869}
1870const RouterLinkImpl = defineComponent({
1871 name: 'RouterLink',
1872 props: {
1873 to: {
1874 type: [String, Object],
1875 required: true,
1876 },
1877 activeClass: String,
1878 // inactiveClass: String,
1879 exactActiveClass: String,
1880 custom: Boolean,
1881 ariaCurrentValue: {
1882 type: String,
1883 default: 'page',
1884 },
1885 },
1886 setup(props, { slots, attrs }) {
1887 const link = reactive(useLink(props));
1888 const { options } = inject(routerKey);
1889 const elClass = computed(() => ({
1890 [getLinkClass(props.activeClass, options.linkActiveClass, 'router-link-active')]: link.isActive,
1891 // [getLinkClass(
1892 // props.inactiveClass,
1893 // options.linkInactiveClass,
1894 // 'router-link-inactive'
1895 // )]: !link.isExactActive,
1896 [getLinkClass(props.exactActiveClass, options.linkExactActiveClass, 'router-link-exact-active')]: link.isExactActive,
1897 }));
1898 return () => {
1899 const children = slots.default && slots.default(link);
1900 return props.custom
1901 ? children
1902 : h('a', assign({
1903 'aria-current': link.isExactActive
1904 ? props.ariaCurrentValue
1905 : null,
1906 onClick: link.navigate,
1907 href: link.href,
1908 }, attrs, {
1909 class: elClass.value,
1910 }), children);
1911 };
1912 },
1913});
1914// export the public type for h/tsx inference
1915// also to avoid inline import() in generated d.ts files
1916const RouterLink = RouterLinkImpl;
1917function guardEvent(e) {
1918 // don't redirect with control keys
1919 if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1920 return;
1921 // don't redirect when preventDefault called
1922 if (e.defaultPrevented)
1923 return;
1924 // don't redirect on right click
1925 if (e.button !== undefined && e.button !== 0)
1926 return;
1927 // don't redirect if `target="_blank"`
1928 // @ts-ignore getAttribute does exist
1929 if (e.currentTarget && e.currentTarget.getAttribute) {
1930 // @ts-ignore getAttribute exists
1931 const target = e.currentTarget.getAttribute('target');
1932 if (/\b_blank\b/i.test(target))
1933 return;
1934 }
1935 // this may be a Weex event which doesn't have this method
1936 if (e.preventDefault)
1937 e.preventDefault();
1938 return true;
1939}
1940function includesParams(outer, inner) {
1941 for (let key in inner) {
1942 let innerValue = inner[key];
1943 let outerValue = outer[key];
1944 if (typeof innerValue === 'string') {
1945 if (innerValue !== outerValue)
1946 return false;
1947 }
1948 else {
1949 if (!Array.isArray(outerValue) ||
1950 outerValue.length !== innerValue.length ||
1951 innerValue.some((value, i) => value !== outerValue[i]))
1952 return false;
1953 }
1954 }
1955 return true;
1956}
1957/**
1958 * Get the original path value of a record by following its aliasOf
1959 * @param record
1960 */
1961function getOriginalPath(record) {
1962 return record ? (record.aliasOf ? record.aliasOf.path : record.path) : '';
1963}
1964/**
1965 * Utility class to get the active class based on defaults.
1966 * @param propClass
1967 * @param globalClass
1968 * @param defaultClass
1969 */
1970let getLinkClass = (propClass, globalClass, defaultClass) => propClass != null
1971 ? propClass
1972 : globalClass != null
1973 ? globalClass
1974 : defaultClass;
1975
1976const RouterViewImpl = defineComponent({
1977 name: 'RouterView',
1978 props: {
1979 name: {
1980 type: String,
1981 default: 'default',
1982 },
1983 route: Object,
1984 },
1985 setup(props, { attrs, slots }) {
1986 warnDeprecatedUsage();
1987 const injectedRoute = inject(routeLocationKey);
1988 const depth = inject(viewDepthKey, 0);
1989 const matchedRouteRef = computed(() => (props.route || injectedRoute).matched[depth]);
1990 provide(viewDepthKey, depth + 1);
1991 provide(matchedRouteKey, matchedRouteRef);
1992 const viewRef = ref();
1993 return () => {
1994 const route = props.route || injectedRoute;
1995 const matchedRoute = matchedRouteRef.value;
1996 const ViewComponent = matchedRoute && matchedRoute.components[props.name];
1997 if (!ViewComponent) {
1998 return slots.default
1999 ? slots.default({ Component: ViewComponent, route })
2000 : null;
2001 }
2002 // props from route configration
2003 const routePropsOption = matchedRoute.props[props.name];
2004 const routeProps = routePropsOption
2005 ? routePropsOption === true
2006 ? route.params
2007 : typeof routePropsOption === 'function'
2008 ? routePropsOption(route)
2009 : routePropsOption
2010 : null;
2011 // we need the value at the time we render because when we unmount, we
2012 // navigated to a different location so the value is different
2013 const currentName = props.name;
2014 const onVnodeMounted = () => {
2015 matchedRoute.instances[currentName] = viewRef.value;
2016 (matchedRoute.enterCallbacks[currentName] || []).forEach(callback => callback(viewRef.value));
2017 };
2018 const onVnodeUnmounted = () => {
2019 // remove the instance reference to prevent leak
2020 matchedRoute.instances[currentName] = null;
2021 };
2022 const component = h(ViewComponent, assign({}, routeProps, attrs, {
2023 onVnodeMounted,
2024 onVnodeUnmounted,
2025 ref: viewRef,
2026 }));
2027 return (
2028 // pass the vnode to the slot as a prop.
2029 // h and <component :is="..."> both accept vnodes
2030 slots.default
2031 ? slots.default({ Component: component, route })
2032 : component);
2033 };
2034 },
2035});
2036// export the public type for h/tsx inference
2037// also to avoid inline import() in generated d.ts files
2038const RouterView = RouterViewImpl;
2039// warn against deprecated usage with <transition> & <keep-alive>
2040// due to functional component being no longer eager in Vue 3
2041function warnDeprecatedUsage() {
2042 const instance = getCurrentInstance();
2043 const parentName = instance.parent && instance.parent.type.name;
2044 if (parentName &&
2045 (parentName === 'KeepAlive' || parentName.includes('Transition'))) {
2046 const comp = parentName === 'KeepAlive' ? 'keep-alive' : 'transition';
2047 warn(`<router-view> can no longer be used directly inside <transition> or <keep-alive>.\n` +
2048 `Use slot props instead:\n\n` +
2049 `<router-view v-slot="{ Component }">\n` +
2050 ` <${comp}>\n` +
2051 ` <component :is="Component" />\n` +
2052 ` </${comp}>\n` +
2053 `</router-view>`);
2054 }
2055}
2056
2057/**
2058 * Create a Router instance that can be used on a Vue app.
2059 *
2060 * @param options - {@link RouterOptions}
2061 */
2062function createRouter(options) {
2063 const matcher = createRouterMatcher(options.routes, options);
2064 let parseQuery$1 = options.parseQuery || parseQuery;
2065 let stringifyQuery$1 = options.stringifyQuery || stringifyQuery;
2066 let { scrollBehavior } = options;
2067 let routerHistory = options.history;
2068 const beforeGuards = useCallbacks();
2069 const beforeResolveGuards = useCallbacks();
2070 const afterGuards = useCallbacks();
2071 const currentRoute = shallowRef(START_LOCATION_NORMALIZED);
2072 let pendingLocation = START_LOCATION_NORMALIZED;
2073 // leave the scrollRestoration if no scrollBehavior is provided
2074 if (isBrowser && scrollBehavior && 'scrollRestoration' in history) {
2075 history.scrollRestoration = 'manual';
2076 }
2077 const normalizeParams = applyToParams.bind(null, paramValue => '' + paramValue);
2078 const encodeParams = applyToParams.bind(null, encodeParam);
2079 const decodeParams = applyToParams.bind(null, decode);
2080 function addRoute(parentOrRoute, route) {
2081 let parent;
2082 let record;
2083 if (isRouteName(parentOrRoute)) {
2084 parent = matcher.getRecordMatcher(parentOrRoute);
2085 record = route;
2086 }
2087 else {
2088 record = parentOrRoute;
2089 }
2090 return matcher.addRoute(record, parent);
2091 }
2092 function removeRoute(name) {
2093 let recordMatcher = matcher.getRecordMatcher(name);
2094 if (recordMatcher) {
2095 matcher.removeRoute(recordMatcher);
2096 }
2097 else {
2098 warn(`Cannot remove non-existent route "${String(name)}"`);
2099 }
2100 }
2101 function getRoutes() {
2102 return matcher.getRoutes().map(routeMatcher => routeMatcher.record);
2103 }
2104 function hasRoute(name) {
2105 return !!matcher.getRecordMatcher(name);
2106 }
2107 function resolve(rawLocation, currentLocation) {
2108 // const objectLocation = routerLocationAsObject(rawLocation)
2109 currentLocation = currentLocation || currentRoute.value;
2110 if (typeof rawLocation === 'string') {
2111 let locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path);
2112 let matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
2113 let href = routerHistory.createHref(locationNormalized.fullPath);
2114 {
2115 if (href.startsWith('//'))
2116 warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`);
2117 else if (!matchedRoute.matched.length) {
2118 warn(`No match found for location with path "${rawLocation}"`);
2119 }
2120 }
2121 // locationNormalized is always a new object
2122 return assign(locationNormalized, matchedRoute, {
2123 params: decodeParams(matchedRoute.params),
2124 redirectedFrom: undefined,
2125 href,
2126 });
2127 }
2128 let matcherLocation;
2129 // path could be relative in object as well
2130 if ('path' in rawLocation) {
2131 if (
2132 'params' in rawLocation &&
2133 !('name' in rawLocation) &&
2134 Object.keys(rawLocation.params).length) {
2135 warn(`Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`);
2136 }
2137 matcherLocation = assign({}, rawLocation, {
2138 path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path,
2139 });
2140 }
2141 else {
2142 matcherLocation = assign({}, rawLocation, {
2143 params: encodeParams(rawLocation.params),
2144 });
2145 }
2146 let matchedRoute = matcher.resolve(matcherLocation, currentLocation);
2147 const hash = encodeHash(rawLocation.hash || '');
2148 if ( hash && !hash.startsWith('#')) {
2149 warn(`A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".`);
2150 }
2151 // put back the unencoded params as given by the user (avoid the cost of decoding them)
2152 matchedRoute.params =
2153 'params' in rawLocation
2154 ? normalizeParams(rawLocation.params)
2155 : decodeParams(matchedRoute.params);
2156 const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, {
2157 hash,
2158 path: matchedRoute.path,
2159 }));
2160 let href = routerHistory.createHref(fullPath);
2161 {
2162 if (href.startsWith('//'))
2163 warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`);
2164 else if (!matchedRoute.matched.length) {
2165 warn(`No match found for location with path "${'path' in rawLocation ? rawLocation.path : rawLocation}"`);
2166 }
2167 }
2168 return assign({
2169 fullPath,
2170 // keep the hash encoded so fullPath is effectively path + encodedQuery +
2171 // hash
2172 hash,
2173 query:
2174 // if the user is using a custom query lib like qs, we might have
2175 // nested objects, so we keep the query as is, meaning it can contain
2176 // numbers at `$route.query`, but at the point, the user will have to
2177 // use their own type anyway.
2178 // https://github.com/vuejs/vue-router-next/issues/328#issuecomment-649481567
2179 stringifyQuery$1 === stringifyQuery
2180 ? normalizeQuery(rawLocation.query)
2181 : rawLocation.query,
2182 }, matchedRoute, {
2183 redirectedFrom: undefined,
2184 href,
2185 });
2186 }
2187 function locationAsObject(to) {
2188 return typeof to === 'string' ? { path: to } : assign({}, to);
2189 }
2190 function checkCanceledNavigation(to, from) {
2191 if (pendingLocation !== to) {
2192 return createRouterError(8 /* NAVIGATION_CANCELLED */, {
2193 from,
2194 to,
2195 });
2196 }
2197 }
2198 function push(to) {
2199 return pushWithRedirect(to);
2200 }
2201 function replace(to) {
2202 return push(assign(locationAsObject(to), { replace: true }));
2203 }
2204 function pushWithRedirect(to, redirectedFrom) {
2205 const targetLocation = (pendingLocation = resolve(to));
2206 const from = currentRoute.value;
2207 const data = to.state;
2208 const force = to.force;
2209 // to could be a string where `replace` is a function
2210 const replace = to.replace === true;
2211 const lastMatched = targetLocation.matched[targetLocation.matched.length - 1];
2212 if (lastMatched && lastMatched.redirect) {
2213 const { redirect } = lastMatched;
2214 // transform it into an object to pass the original RouteLocaleOptions
2215 let newTargetLocation = locationAsObject(typeof redirect === 'function' ? redirect(targetLocation) : redirect);
2216 if (
2217 !('path' in newTargetLocation) &&
2218 !('name' in newTargetLocation)) {
2219 warn(`Invalid redirect found:\n${JSON.stringify(newTargetLocation, null, 2)}\n when navigating to "${targetLocation.fullPath}". A redirect must contain a name or path. This will break in production.`);
2220 return Promise.reject(new Error('Invalid redirect'));
2221 }
2222 return pushWithRedirect(assign({
2223 query: targetLocation.query,
2224 hash: targetLocation.hash,
2225 params: targetLocation.params,
2226 }, newTargetLocation, {
2227 state: data,
2228 force,
2229 replace,
2230 }),
2231 // keep original redirectedFrom if it exists
2232 redirectedFrom || targetLocation);
2233 }
2234 // if it was a redirect we already called `pushWithRedirect` above
2235 const toLocation = targetLocation;
2236 toLocation.redirectedFrom = redirectedFrom;
2237 let failure;
2238 if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) {
2239 failure = createRouterError(16 /* NAVIGATION_DUPLICATED */, { to: toLocation, from });
2240 // trigger scroll to allow scrolling to the same anchor
2241 handleScroll(from, from,
2242 // this is a push, the only way for it to be triggered from a
2243 // history.listen is with a redirect, which makes it become a pus
2244 true,
2245 // This cannot be the first navigation because the initial location
2246 // cannot be manually navigated to
2247 false);
2248 }
2249 return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
2250 .catch((error) => {
2251 if (isNavigationFailure(error, 4 /* NAVIGATION_ABORTED */ |
2252 8 /* NAVIGATION_CANCELLED */ |
2253 2 /* NAVIGATION_GUARD_REDIRECT */)) {
2254 return error;
2255 }
2256 // unknown error, rejects
2257 return triggerError(error);
2258 })
2259 .then((failure) => {
2260 if (failure) {
2261 if (isNavigationFailure(failure, 2 /* NAVIGATION_GUARD_REDIRECT */))
2262 // preserve the original redirectedFrom if any
2263 return pushWithRedirect(
2264 // keep options
2265 assign(locationAsObject(failure.to), {
2266 state: data,
2267 force,
2268 replace,
2269 }), redirectedFrom || toLocation);
2270 }
2271 else {
2272 // if we fail we don't finalize the navigation
2273 failure = finalizeNavigation(toLocation, from, true, replace, data);
2274 }
2275 triggerAfterEach(toLocation, from, failure);
2276 return failure;
2277 });
2278 }
2279 /**
2280 * Helper to reject and skip all navigation guards if a new navigation happened
2281 * @param to
2282 * @param from
2283 */
2284 function checkCanceledNavigationAndReject(to, from) {
2285 const error = checkCanceledNavigation(to, from);
2286 return error ? Promise.reject(error) : Promise.resolve();
2287 }
2288 // TODO: refactor the whole before guards by internally using router.beforeEach
2289 function navigate(to, from) {
2290 let guards;
2291 // all components here have been resolved once because we are leaving
2292 guards = extractComponentsGuards(from.matched.filter(record => to.matched.indexOf(record) < 0).reverse(), 'beforeRouteLeave', to, from);
2293 const [leavingRecords, updatingRecords,] = extractChangingRecords(to, from);
2294 for (const record of leavingRecords) {
2295 for (const guard of record.leaveGuards) {
2296 guards.push(guardToPromiseFn(guard, to, from));
2297 }
2298 }
2299 const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from);
2300 guards.push(canceledNavigationCheck);
2301 // run the queue of per route beforeRouteLeave guards
2302 return (runGuardQueue(guards)
2303 .then(() => {
2304 // check global guards beforeEach
2305 guards = [];
2306 for (const guard of beforeGuards.list()) {
2307 guards.push(guardToPromiseFn(guard, to, from));
2308 }
2309 guards.push(canceledNavigationCheck);
2310 return runGuardQueue(guards);
2311 })
2312 .then(() => {
2313 // check in components beforeRouteUpdate
2314 guards = extractComponentsGuards(to.matched.filter(record => from.matched.indexOf(record) > -1), 'beforeRouteUpdate', to, from);
2315 for (const record of updatingRecords) {
2316 for (const guard of record.updateGuards) {
2317 guards.push(guardToPromiseFn(guard, to, from));
2318 }
2319 }
2320 guards.push(canceledNavigationCheck);
2321 // run the queue of per route beforeEnter guards
2322 return runGuardQueue(guards);
2323 })
2324 .then(() => {
2325 // check the route beforeEnter
2326 guards = [];
2327 for (const record of to.matched) {
2328 // do not trigger beforeEnter on reused views
2329 if (record.beforeEnter && from.matched.indexOf(record) < 0) {
2330 if (Array.isArray(record.beforeEnter)) {
2331 for (const beforeEnter of record.beforeEnter)
2332 guards.push(guardToPromiseFn(beforeEnter, to, from));
2333 }
2334 else {
2335 guards.push(guardToPromiseFn(record.beforeEnter, to, from));
2336 }
2337 }
2338 }
2339 guards.push(canceledNavigationCheck);
2340 // run the queue of per route beforeEnter guards
2341 return runGuardQueue(guards);
2342 })
2343 .then(() => {
2344 // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
2345 // clear existing enterCallbacks, these are added by extractComponentsGuards
2346 to.matched.forEach(record => (record.enterCallbacks = {}));
2347 // check in-component beforeRouteEnter
2348 guards = extractComponentsGuards(
2349 // the type doesn't matter as we are comparing an object per reference
2350 to.matched.filter(record => from.matched.indexOf(record) < 0), 'beforeRouteEnter', to, from);
2351 guards.push(canceledNavigationCheck);
2352 // run the queue of per route beforeEnter guards
2353 return runGuardQueue(guards);
2354 })
2355 .then(() => {
2356 // check global guards beforeResolve
2357 guards = [];
2358 for (const guard of beforeResolveGuards.list()) {
2359 guards.push(guardToPromiseFn(guard, to, from));
2360 }
2361 guards.push(canceledNavigationCheck);
2362 return runGuardQueue(guards);
2363 })
2364 // catch any navigation canceled
2365 .catch(err => isNavigationFailure(err, 8 /* NAVIGATION_CANCELLED */)
2366 ? err
2367 : Promise.reject(err)));
2368 }
2369 function triggerAfterEach(to, from, failure) {
2370 // navigation is confirmed, call afterGuards
2371 // TODO: wrap with error handlers
2372 for (const guard of afterGuards.list())
2373 guard(to, from, failure);
2374 }
2375 /**
2376 * - Cleans up any navigation guards
2377 * - Changes the url if necessary
2378 * - Calls the scrollBehavior
2379 */
2380 function finalizeNavigation(toLocation, from, isPush, replace, data) {
2381 // a more recent navigation took place
2382 const error = checkCanceledNavigation(toLocation, from);
2383 if (error)
2384 return error;
2385 const [leavingRecords] = extractChangingRecords(toLocation, from);
2386 for (const record of leavingRecords) {
2387 // remove registered guards from removed matched records
2388 record.leaveGuards = [];
2389 record.updateGuards = [];
2390 // free the references
2391 record.instances = {};
2392 record.enterCallbacks = {};
2393 }
2394 // only consider as push if it's not the first navigation
2395 const isFirstNavigation = from === START_LOCATION_NORMALIZED;
2396 const state = !isBrowser ? {} : history.state;
2397 // change URL only if the user did a push/replace and if it's not the initial navigation because
2398 // it's just reflecting the url
2399 if (isPush) {
2400 // on the initial navigation, we want to reuse the scroll position from
2401 // history state if it exists
2402 if (replace || isFirstNavigation)
2403 routerHistory.replace(toLocation.fullPath, assign({
2404 scroll: isFirstNavigation && state && state.scroll,
2405 }, data));
2406 else
2407 routerHistory.push(toLocation.fullPath, data);
2408 }
2409 // accept current navigation
2410 currentRoute.value = toLocation;
2411 handleScroll(toLocation, from, isPush, isFirstNavigation);
2412 markAsReady();
2413 }
2414 let removeHistoryListener;
2415 // attach listener to history to trigger navigations
2416 function setupListeners() {
2417 removeHistoryListener = routerHistory.listen((to, _from, info) => {
2418 // cannot be a redirect route because it was in history
2419 const toLocation = resolve(to);
2420 pendingLocation = toLocation;
2421 const from = currentRoute.value;
2422 // TODO: should be moved to web history?
2423 if (isBrowser) {
2424 saveScrollPosition(getScrollKey(from.fullPath, info.delta), computeScrollPosition());
2425 }
2426 navigate(toLocation, from)
2427 .catch((error) => {
2428 if (isNavigationFailure(error, 4 /* NAVIGATION_ABORTED */ | 8 /* NAVIGATION_CANCELLED */)) {
2429 return error;
2430 }
2431 if (isNavigationFailure(error, 2 /* NAVIGATION_GUARD_REDIRECT */)) {
2432 // do not restore history on unknown direction
2433 if (info.delta)
2434 routerHistory.go(-info.delta, false);
2435 // the error is already handled by router.push we just want to avoid
2436 // logging the error
2437 pushWithRedirect(error.to, toLocation
2438 // avoid an uncaught rejection
2439 ).catch(noop);
2440 // avoid the then branch
2441 return Promise.reject();
2442 }
2443 // do not restore history on unknown direction
2444 if (info.delta)
2445 routerHistory.go(-info.delta, false);
2446 // unrecognized error, transfer to the global handler
2447 return triggerError(error);
2448 })
2449 .then((failure) => {
2450 failure =
2451 failure ||
2452 finalizeNavigation(
2453 // after navigation, all matched components are resolved
2454 toLocation, from, false);
2455 // revert the navigation
2456 if (failure && info.delta)
2457 routerHistory.go(-info.delta, false);
2458 triggerAfterEach(toLocation, from, failure);
2459 })
2460 .catch(noop);
2461 });
2462 }
2463 // Initialization and Errors
2464 let readyHandlers = useCallbacks();
2465 let errorHandlers = useCallbacks();
2466 let ready;
2467 /**
2468 * Trigger errorHandlers added via onError and throws the error as well
2469 * @param error - error to throw
2470 * @returns the error as a rejected promise
2471 */
2472 function triggerError(error) {
2473 markAsReady(error);
2474 errorHandlers.list().forEach(handler => handler(error));
2475 return Promise.reject(error);
2476 }
2477 /**
2478 * Returns a Promise that resolves or reject when the router has finished its
2479 * initial navigation. This will be automatic on client but requires an
2480 * explicit `router.push` call on the server. This behavior can change
2481 * depending on the history implementation used e.g. the defaults history
2482 * implementation (client only) triggers this automatically but the memory one
2483 * (should be used on server) doesn't
2484 */
2485 function isReady() {
2486 if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
2487 return Promise.resolve();
2488 return new Promise((resolve, reject) => {
2489 readyHandlers.add([resolve, reject]);
2490 });
2491 }
2492 /**
2493 * Mark the router as ready, resolving the promised returned by isReady(). Can
2494 * only be called once, otherwise does nothing.
2495 * @param err - optional error
2496 */
2497 function markAsReady(err) {
2498 if (ready)
2499 return;
2500 ready = true;
2501 setupListeners();
2502 readyHandlers
2503 .list()
2504 .forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
2505 readyHandlers.reset();
2506 }
2507 // Scroll behavior
2508 function handleScroll(to, from, isPush, isFirstNavigation) {
2509 if (!isBrowser || !scrollBehavior)
2510 return Promise.resolve();
2511 let scrollPosition = (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
2512 ((isFirstNavigation || !isPush) &&
2513 history.state &&
2514 history.state.scroll) ||
2515 null;
2516 return nextTick()
2517 .then(() => scrollBehavior(to, from, scrollPosition))
2518 .then(position => position && scrollToPosition(position))
2519 .catch(triggerError);
2520 }
2521 function go(delta) {
2522 return new Promise((resolve, reject) => {
2523 let removeError = errorHandlers.add(err => {
2524 removeError();
2525 removeAfterEach();
2526 reject(err);
2527 });
2528 let removeAfterEach = afterGuards.add((_to, _from, failure) => {
2529 removeError();
2530 removeAfterEach();
2531 resolve(failure);
2532 });
2533 routerHistory.go(delta);
2534 });
2535 }
2536 let started;
2537 const installedApps = new Set();
2538 const router = {
2539 currentRoute,
2540 addRoute,
2541 removeRoute,
2542 hasRoute,
2543 getRoutes,
2544 resolve,
2545 options,
2546 push,
2547 replace,
2548 go,
2549 back: () => go(-1),
2550 forward: () => go(1),
2551 beforeEach: beforeGuards.add,
2552 beforeResolve: beforeResolveGuards.add,
2553 afterEach: afterGuards.add,
2554 onError: errorHandlers.add,
2555 isReady,
2556 install(app) {
2557 const router = this;
2558 app.component('RouterLink', RouterLink);
2559 app.component('RouterView', RouterView);
2560 app.config.globalProperties.$router = router;
2561 Object.defineProperty(app.config.globalProperties, '$route', {
2562 get: () => unref(currentRoute),
2563 });
2564 // this initial navigation is only necessary on client, on server it doesn't
2565 // make sense because it will create an extra unnecessary navigation and could
2566 // lead to problems
2567 if (isBrowser &&
2568 // used for the initial navigation client side to avoid pushing
2569 // multiple times when the router is used in multiple apps
2570 !started &&
2571 currentRoute.value === START_LOCATION_NORMALIZED) {
2572 // see above
2573 started = true;
2574 push(routerHistory.location).catch(err => {
2575 warn('Unexpected error when starting the router:', err);
2576 });
2577 }
2578 const reactiveRoute = {};
2579 for (let key in START_LOCATION_NORMALIZED) {
2580 // @ts-ignore: the key matches
2581 reactiveRoute[key] = computed(() => currentRoute.value[key]);
2582 }
2583 app.provide(routerKey, router);
2584 app.provide(routeLocationKey, reactive(reactiveRoute));
2585 let unmountApp = app.unmount;
2586 installedApps.add(app);
2587 app.unmount = function () {
2588 installedApps.delete(app);
2589 if (installedApps.size < 1) {
2590 removeHistoryListener();
2591 currentRoute.value = START_LOCATION_NORMALIZED;
2592 started = false;
2593 ready = false;
2594 }
2595 unmountApp.call(this, arguments);
2596 };
2597 },
2598 };
2599 return router;
2600}
2601function runGuardQueue(guards) {
2602 return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
2603}
2604function extractChangingRecords(to, from) {
2605 const leavingRecords = [];
2606 const updatingRecords = [];
2607 const enteringRecords = [];
2608 const len = Math.max(from.matched.length, to.matched.length);
2609 for (let i = 0; i < len; i++) {
2610 const recordFrom = from.matched[i];
2611 if (recordFrom) {
2612 if (to.matched.indexOf(recordFrom) < 0)
2613 leavingRecords.push(recordFrom);
2614 else
2615 updatingRecords.push(recordFrom);
2616 }
2617 const recordTo = to.matched[i];
2618 if (recordTo) {
2619 // the type doesn't matter because we are comparing per reference
2620 if (from.matched.indexOf(recordTo) < 0)
2621 enteringRecords.push(recordTo);
2622 }
2623 }
2624 return [leavingRecords, updatingRecords, enteringRecords];
2625}
2626
2627function useRouter() {
2628 return inject(routerKey);
2629}
2630function useRoute() {
2631 return inject(routeLocationKey);
2632}
2633
2634export { NavigationFailureType, RouterLink, RouterView, START_LOCATION_NORMALIZED as START_LOCATION, createMemoryHistory, createRouter, createRouterMatcher, createWebHashHistory, createWebHistory, isNavigationFailure, onBeforeRouteLeave, onBeforeRouteUpdate, parseQuery, stringifyQuery, useLink, useRoute, useRouter };