--- /dev/null
+import {
+ createRouterError,
+ ErrorTypes,
+ isNavigationFailure,
+ NavigationRedirectError,
+ type _ErrorListener,
+ type NavigationFailure,
+} from '../errors'
+import {
+ nextTick,
+ shallowReactive,
+ shallowRef,
+ unref,
+ warn,
+ type App,
+ type Ref,
+} from 'vue'
+import { RouterLink } from '../RouterLink'
+import { RouterView } from '../RouterView'
+import {
+ NavigationType,
+ type HistoryState,
+ type RouterHistory,
+} from '../history/common'
+import type { PathParserOptions } from '../matcher'
+import type { RouteResolver } from '../new-route-resolver/matcher'
+import {
+ LocationQuery,
+ normalizeQuery,
+ parseQuery as originalParseQuery,
+ stringifyQuery as originalStringifyQuery,
+} from '../query'
+import type { Router } from '../router'
+import {
+ _ScrollPositionNormalized,
+ computeScrollPosition,
+ getSavedScrollPosition,
+ getScrollKey,
+ saveScrollPosition,
+ scrollToPosition,
+ type RouterScrollBehavior,
+} from '../scrollBehavior'
+import type {
+ NavigationGuardWithThis,
+ NavigationHookAfter,
+ RouteLocation,
+ RouteLocationAsPath,
+ RouteLocationAsRelative,
+ RouteLocationAsRelativeTyped,
+ RouteLocationAsString,
+ RouteLocationNormalized,
+ RouteLocationNormalizedLoaded,
+ RouteLocationRaw,
+ RouteLocationResolved,
+ RouteMap,
+ RouteParams,
+ RouteRecordNameGeneric,
+} from '../typed-routes'
+import {
+ isRouteLocation,
+ isRouteName,
+ Lazy,
+ MatcherLocationRaw,
+ RouteLocationOptions,
+ type RouteRecordRaw,
+} from '../types'
+import { useCallbacks } from '../utils/callbacks'
+import {
+ isSameRouteLocation,
+ parseURL,
+ START_LOCATION_NORMALIZED,
+ stringifyURL,
+} from '../location'
+import { applyToParams, assign, isArray, isBrowser, noop } from '../utils'
+import { decode, encodeHash, encodeParam } from '../encoding'
+import {
+ extractChangingRecords,
+ extractComponentsGuards,
+ guardToPromiseFn,
+} from '../navigationGuards'
+import { addDevtools } from '../devtools'
+import {
+ routeLocationKey,
+ routerKey,
+ routerViewLocationKey,
+} from '../injectionSymbols'
+
+/**
+ * resolve, reject arguments of Promise constructor
+ * @internal
+ */
+export type _OnReadyCallback = [() => void, (reason?: any) => void]
+
+/**
+ * Options to initialize a {@link Router} instance.
+ */
+export interface EXPERIMENTAL_RouterOptions_Base extends PathParserOptions {
+ /**
+ * History implementation used by the router. Most web applications should use
+ * `createWebHistory` but it requires the server to be properly configured.
+ * You can also use a _hash_ based history with `createWebHashHistory` that
+ * does not require any configuration on the server but isn't handled at all
+ * by search engines and does poorly on SEO.
+ *
+ * @example
+ * ```js
+ * createRouter({
+ * history: createWebHistory(),
+ * // other options...
+ * })
+ * ```
+ */
+ history: RouterHistory
+
+ /**
+ * Function to control scrolling when navigating between pages. Can return a
+ * Promise to delay scrolling.
+ *
+ * @see {@link RouterScrollBehavior}.
+ *
+ * @example
+ * ```js
+ * function scrollBehavior(to, from, savedPosition) {
+ * // `to` and `from` are both route locations
+ * // `savedPosition` can be null if there isn't one
+ * }
+ * ```
+ */
+
+ scrollBehavior?: RouterScrollBehavior
+ /**
+ * Custom implementation to parse a query. See its counterpart,
+ * {@link EXPERIMENTAL_RouterOptions_Base.stringifyQuery}.
+ *
+ * @example
+ * Let's say you want to use the [qs package](https://github.com/ljharb/qs)
+ * to parse queries, you can provide both `parseQuery` and `stringifyQuery`:
+ * ```js
+ * import qs from 'qs'
+ *
+ * createRouter({
+ * // other options...
+ * parseQuery: qs.parse,
+ * stringifyQuery: qs.stringify,
+ * })
+ * ```
+ */
+
+ parseQuery?: typeof originalParseQuery
+ /**
+ * Custom implementation to stringify a query object. Should not prepend a leading `?`.
+ * {@link EXPERIMENTAL_RouterOptions_Base.parseQuery | parseQuery} counterpart to handle query parsing.
+ */
+
+ stringifyQuery?: typeof originalStringifyQuery
+ /**
+ * Default class applied to active {@link RouterLink}. If none is provided,
+ * `router-link-active` will be applied.
+ */
+
+ linkActiveClass?: string
+ /**
+ * Default class applied to exact active {@link RouterLink}. If none is provided,
+ * `router-link-exact-active` will be applied.
+ */
+
+ linkExactActiveClass?: string
+ /**
+ * Default class applied to non-active {@link RouterLink}. If none is provided,
+ * `router-link-inactive` will be applied.
+ */
+ // linkInactiveClass?: string
+}
+
+/**
+ * Options to initialize an experimental {@link EXPERIMENTAL_Router} instance.
+ * @experimental
+ */
+export interface EXPERIMENTAL_RouterOptions<TRouteRecordRaw, TRouteRecord>
+ extends EXPERIMENTAL_RouterOptions_Base {
+ /**
+ * Initial list of routes that should be added to the router.
+ */
+ routes?: Readonly<RouteRecordRaw[]>
+
+ /**
+ * Matcher to use to resolve routes.
+ * @experimental
+ */
+ matcher: RouteResolver<TRouteRecordRaw, TRouteRecord>
+}
+
+/**
+ * Router instance.
+ * @experimental This version is not stable, it's meant to replace {@link Router} in the future.
+ */
+export interface EXPERIMENTAL_Router_Base<TRouteRecordRaw, TRouteRecord> {
+ /**
+ * Current {@link RouteLocationNormalized}
+ */
+ readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
+
+ /**
+ * Allows turning off the listening of history events. This is a low level api for micro-frontend.
+ */
+ listening: boolean
+
+ /**
+ * Add a new {@link RouteRecordRaw | route record} as the child of an existing route.
+ *
+ * @param parentName - Parent Route Record where `route` should be appended at
+ * @param route - Route Record to add
+ */
+ addRoute(
+ // NOTE: it could be `keyof RouteMap` but the point of dynamic routes is not knowing the routes at build
+ parentName: NonNullable<RouteRecordNameGeneric>,
+ route: RouteRecordRaw
+ ): () => void
+ /**
+ * Add a new {@link RouteRecordRaw | route record} to the router.
+ *
+ * @param route - Route Record to add
+ */
+ addRoute(route: TRouteRecordRaw): () => void
+
+ /**
+ * Remove an existing route by its name.
+ *
+ * @param name - Name of the route to remove
+ */
+ removeRoute(name: NonNullable<RouteRecordNameGeneric>): void
+
+ /**
+ * Checks if a route with a given name exists
+ *
+ * @param name - Name of the route to check
+ */
+ hasRoute(name: NonNullable<RouteRecordNameGeneric>): boolean
+
+ /**
+ * Get a full list of all the {@link RouteRecord | route records}.
+ */
+ getRoutes(): TRouteRecord[]
+
+ /**
+ * Delete all routes from the router matcher.
+ */
+ clearRoutes(): void
+
+ /**
+ * Returns the {@link RouteLocation | normalized version} of a
+ * {@link RouteLocationRaw | route location}. Also includes an `href` property
+ * that includes any existing `base`. By default, the `currentLocation` used is
+ * `router.currentRoute` and should only be overridden in advanced use cases.
+ *
+ * @param to - Raw route location to resolve
+ * @param currentLocation - Optional current location to resolve against
+ */
+ resolve<Name extends keyof RouteMap = keyof RouteMap>(
+ to: RouteLocationAsRelativeTyped<RouteMap, Name>,
+ // NOTE: This version doesn't work probably because it infers the type too early
+ // | RouteLocationAsRelative<Name>
+ currentLocation?: RouteLocationNormalizedLoaded
+ ): RouteLocationResolved<Name>
+ resolve(
+ // not having the overload produces errors in RouterLink calls to router.resolve()
+ to: RouteLocationAsString | RouteLocationAsRelative | RouteLocationAsPath,
+ currentLocation?: RouteLocationNormalizedLoaded
+ ): RouteLocationResolved
+
+ /**
+ * Programmatically navigate to a new URL by pushing an entry in the history
+ * stack.
+ *
+ * @param to - Route location to navigate to
+ */
+ push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
+
+ /**
+ * Programmatically navigate to a new URL by replacing the current entry in
+ * the history stack.
+ *
+ * @param to - Route location to navigate to
+ */
+ replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
+
+ /**
+ * Go back in history if possible by calling `history.back()`. Equivalent to
+ * `router.go(-1)`.
+ */
+ back(): void
+
+ /**
+ * Go forward in history if possible by calling `history.forward()`.
+ * Equivalent to `router.go(1)`.
+ */
+ forward(): void
+
+ /**
+ * Allows you to move forward or backward through the history. Calls
+ * `history.go()`.
+ *
+ * @param delta - The position in the history to which you want to move,
+ * relative to the current page
+ */
+ go(delta: number): void
+
+ /**
+ * Add a navigation guard that executes before any navigation. Returns a
+ * function that removes the registered guard.
+ *
+ * @param guard - navigation guard to add
+ */
+ beforeEach(guard: NavigationGuardWithThis<undefined>): () => void
+
+ /**
+ * Add a navigation guard that executes before navigation is about to be
+ * resolved. At this state all component have been fetched and other
+ * navigation guards have been successful. Returns a function that removes the
+ * registered guard.
+ *
+ * @param guard - navigation guard to add
+ * @returns a function that removes the registered guard
+ *
+ * @example
+ * ```js
+ * router.beforeResolve(to => {
+ * if (to.meta.requiresAuth && !isAuthenticated) return false
+ * })
+ * ```
+ *
+ */
+ beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void
+
+ /**
+ * Add a navigation hook that is executed after every navigation. Returns a
+ * function that removes the registered hook.
+ *
+ * @param guard - navigation hook to add
+ * @returns a function that removes the registered hook
+ *
+ * @example
+ * ```js
+ * router.afterEach((to, from, failure) => {
+ * if (isNavigationFailure(failure)) {
+ * console.log('failed navigation', failure)
+ * }
+ * })
+ * ```
+ */
+ afterEach(guard: NavigationHookAfter): () => void
+
+ /**
+ * Adds an error handler that is called every time a non caught error happens
+ * during navigation. This includes errors thrown synchronously and
+ * asynchronously, errors returned or passed to `next` in any navigation
+ * guard, and errors occurred when trying to resolve an async component that
+ * is required to render a route.
+ *
+ * @param handler - error handler to register
+ */
+ onError(handler: _ErrorListener): () => void
+
+ /**
+ * Returns a Promise that resolves when the router has completed the initial
+ * navigation, which means it has resolved all async enter hooks and async
+ * components that are associated with the initial route. If the initial
+ * navigation already happened, the promise resolves immediately.
+ *
+ * This is useful in server-side rendering to ensure consistent output on both
+ * the server and the client. Note that on server side, you need to manually
+ * push the initial location while on client side, the router automatically
+ * picks it up from the URL.
+ */
+ isReady(): Promise<void>
+
+ /**
+ * Called automatically by `app.use(router)`. Should not be called manually by
+ * the user. This will trigger the initial navigation when on client side.
+ *
+ * @internal
+ * @param app - Application that uses the router
+ */
+ install(app: App): void
+}
+
+export interface EXPERIMENTAL_Router<TRouteRecordRaw, TRouteRecord>
+ extends EXPERIMENTAL_Router_Base<TRouteRecordRaw, TRouteRecord> {
+ /**
+ * Original options object passed to create the Router
+ */
+ readonly options: EXPERIMENTAL_RouterOptions<TRouteRecordRaw, TRouteRecord>
+}
+
+interface EXPERIMENTAL_RouteRecordRaw {}
+interface EXPERIMENTAL_RouteRecord {}
+
+export function experimental_createRouter(
+ options: EXPERIMENTAL_RouterOptions<
+ EXPERIMENTAL_RouteRecordRaw,
+ EXPERIMENTAL_RouteRecord
+ >
+): EXPERIMENTAL_Router<EXPERIMENTAL_RouteRecordRaw, EXPERIMENTAL_RouteRecord> {
+ const {
+ matcher,
+ parseQuery = originalParseQuery,
+ stringifyQuery = originalStringifyQuery,
+ history: routerHistory,
+ } = options
+
+ if (__DEV__ && !routerHistory)
+ throw new Error(
+ 'Provide the "history" option when calling "createRouter()":' +
+ ' https://router.vuejs.org/api/interfaces/RouterOptions.html#history'
+ )
+
+ const beforeGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
+ const beforeResolveGuards = useCallbacks<NavigationGuardWithThis<undefined>>()
+ const afterGuards = useCallbacks<NavigationHookAfter>()
+ const currentRoute = shallowRef<RouteLocationNormalizedLoaded>(
+ START_LOCATION_NORMALIZED
+ )
+ let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
+
+ // leave the scrollRestoration if no scrollBehavior is provided
+ if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
+ history.scrollRestoration = 'manual'
+ }
+
+ const normalizeParams = applyToParams.bind(
+ null,
+ paramValue => '' + paramValue
+ )
+ const encodeParams = applyToParams.bind(null, encodeParam)
+ const decodeParams: (params: RouteParams | undefined) => RouteParams =
+ // @ts-expect-error: intentionally avoid the type check
+ applyToParams.bind(null, decode)
+
+ function addRoute(
+ parentOrRoute: NonNullable<RouteRecordNameGeneric> | RouteRecordRaw,
+ route?: RouteRecordRaw
+ ) {
+ let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined
+ let record: RouteRecordRaw
+ if (isRouteName(parentOrRoute)) {
+ parent = matcher.getMatcher(parentOrRoute)
+ if (__DEV__ && !parent) {
+ warn(
+ `Parent route "${String(
+ parentOrRoute
+ )}" not found when adding child route`,
+ route
+ )
+ }
+ record = route!
+ } else {
+ record = parentOrRoute
+ }
+
+ return matcher.addRoute(record, parent)
+ }
+
+ function removeRoute(name: NonNullable<RouteRecordNameGeneric>) {
+ const recordMatcher = matcher.getMatcher(name)
+ if (recordMatcher) {
+ matcher.removeRoute(recordMatcher)
+ } else if (__DEV__) {
+ warn(`Cannot remove non-existent route "${String(name)}"`)
+ }
+ }
+
+ function getRoutes() {
+ return matcher.getMatchers().map(routeMatcher => routeMatcher.record)
+ }
+
+ function hasRoute(name: NonNullable<RouteRecordNameGeneric>): boolean {
+ return !!matcher.getMatcher(name)
+ }
+
+ function resolve(
+ rawLocation: RouteLocationRaw,
+ currentLocation?: RouteLocationNormalizedLoaded
+ ): RouteLocationResolved {
+ // const resolve: Router['resolve'] = (rawLocation: RouteLocationRaw, currentLocation) => {
+ // const objectLocation = routerLocationAsObject(rawLocation)
+ // we create a copy to modify it later
+ currentLocation = assign({}, currentLocation || currentRoute.value)
+ if (typeof rawLocation === 'string') {
+ const locationNormalized = parseURL(
+ parseQuery,
+ rawLocation,
+ currentLocation.path
+ )
+ const matchedRoute = matcher.resolve(
+ { path: locationNormalized.path },
+ currentLocation
+ )
+
+ const href = routerHistory.createHref(locationNormalized.fullPath)
+ if (__DEV__) {
+ if (href.startsWith('//'))
+ warn(
+ `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
+ )
+ else if (!matchedRoute.matched.length) {
+ warn(`No match found for location with path "${rawLocation}"`)
+ }
+ }
+
+ // locationNormalized is always a new object
+ return assign(locationNormalized, matchedRoute, {
+ params: decodeParams(matchedRoute.params),
+ hash: decode(locationNormalized.hash),
+ redirectedFrom: undefined,
+ href,
+ })
+ }
+
+ if (__DEV__ && !isRouteLocation(rawLocation)) {
+ warn(
+ `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`,
+ rawLocation
+ )
+ return resolve({})
+ }
+
+ let matcherLocation: MatcherLocationRaw
+
+ // path could be relative in object as well
+ if (rawLocation.path != null) {
+ if (
+ __DEV__ &&
+ 'params' in rawLocation &&
+ !('name' in rawLocation) &&
+ // @ts-expect-error: the type is never
+ Object.keys(rawLocation.params).length
+ ) {
+ warn(
+ `Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`
+ )
+ }
+ matcherLocation = assign({}, rawLocation, {
+ path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path,
+ })
+ } else {
+ // remove any nullish param
+ const targetParams = assign({}, rawLocation.params)
+ for (const key in targetParams) {
+ if (targetParams[key] == null) {
+ delete targetParams[key]
+ }
+ }
+ // pass encoded values to the matcher, so it can produce encoded path and fullPath
+ matcherLocation = assign({}, rawLocation, {
+ params: encodeParams(targetParams),
+ })
+ // current location params are decoded, we need to encode them in case the
+ // matcher merges the params
+ currentLocation.params = encodeParams(currentLocation.params)
+ }
+
+ const matchedRoute = matcher.resolve(matcherLocation, currentLocation)
+ const hash = rawLocation.hash || ''
+
+ if (__DEV__ && hash && !hash.startsWith('#')) {
+ warn(
+ `A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".`
+ )
+ }
+
+ // the matcher might have merged current location params, so
+ // we need to run the decoding again
+ matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params))
+
+ const fullPath = stringifyURL(
+ stringifyQuery,
+ assign({}, rawLocation, {
+ hash: encodeHash(hash),
+ path: matchedRoute.path,
+ })
+ )
+
+ const href = routerHistory.createHref(fullPath)
+ if (__DEV__) {
+ if (href.startsWith('//')) {
+ warn(
+ `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
+ )
+ } else if (!matchedRoute.matched.length) {
+ warn(
+ `No match found for location with path "${
+ rawLocation.path != null ? rawLocation.path : rawLocation
+ }"`
+ )
+ }
+ }
+
+ return assign(
+ {
+ fullPath,
+ // keep the hash encoded so fullPath is effectively path + encodedQuery +
+ // hash
+ hash,
+ query:
+ // if the user is using a custom query lib like qs, we might have
+ // nested objects, so we keep the query as is, meaning it can contain
+ // numbers at `$route.query`, but at the point, the user will have to
+ // use their own type anyway.
+ // https://github.com/vuejs/router/issues/328#issuecomment-649481567
+ stringifyQuery === originalStringifyQuery
+ ? normalizeQuery(rawLocation.query)
+ : ((rawLocation.query || {}) as LocationQuery),
+ },
+ matchedRoute,
+ {
+ redirectedFrom: undefined,
+ href,
+ }
+ )
+ }
+
+ function locationAsObject(
+ to: RouteLocationRaw | RouteLocationNormalized
+ ): Exclude<RouteLocationRaw, string> | RouteLocationNormalized {
+ return typeof to === 'string'
+ ? parseURL(parseQuery, to, currentRoute.value.path)
+ : assign({}, to)
+ }
+
+ function checkCanceledNavigation(
+ to: RouteLocationNormalized,
+ from: RouteLocationNormalized
+ ): NavigationFailure | void {
+ if (pendingLocation !== to) {
+ return createRouterError<NavigationFailure>(
+ ErrorTypes.NAVIGATION_CANCELLED,
+ {
+ from,
+ to,
+ }
+ )
+ }
+ }
+
+ function push(to: RouteLocationRaw) {
+ return pushWithRedirect(to)
+ }
+
+ function replace(to: RouteLocationRaw) {
+ return push(assign(locationAsObject(to), { replace: true }))
+ }
+
+ function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void {
+ const lastMatched = to.matched[to.matched.length - 1]
+ if (lastMatched && lastMatched.redirect) {
+ const { redirect } = lastMatched
+ let newTargetLocation =
+ typeof redirect === 'function' ? redirect(to) : redirect
+
+ if (typeof newTargetLocation === 'string') {
+ newTargetLocation =
+ newTargetLocation.includes('?') || newTargetLocation.includes('#')
+ ? (newTargetLocation = locationAsObject(newTargetLocation))
+ : // force empty params
+ { path: newTargetLocation }
+ // @ts-expect-error: force empty params when a string is passed to let
+ // the router parse them again
+ newTargetLocation.params = {}
+ }
+
+ if (
+ __DEV__ &&
+ newTargetLocation.path == null &&
+ !('name' in newTargetLocation)
+ ) {
+ warn(
+ `Invalid redirect found:\n${JSON.stringify(
+ newTargetLocation,
+ null,
+ 2
+ )}\n when navigating to "${
+ to.fullPath
+ }". A redirect must contain a name or path. This will break in production.`
+ )
+ throw new Error('Invalid redirect')
+ }
+
+ return assign(
+ {
+ query: to.query,
+ hash: to.hash,
+ // avoid transferring params if the redirect has a path
+ params: newTargetLocation.path != null ? {} : to.params,
+ },
+ newTargetLocation
+ )
+ }
+ }
+
+ function pushWithRedirect(
+ to: RouteLocationRaw | RouteLocation,
+ redirectedFrom?: RouteLocation
+ ): Promise<NavigationFailure | void | undefined> {
+ const targetLocation: RouteLocation = (pendingLocation = resolve(to))
+ const from = currentRoute.value
+ const data: HistoryState | undefined = (to as RouteLocationOptions).state
+ const force: boolean | undefined = (to as RouteLocationOptions).force
+ // to could be a string where `replace` is a function
+ const replace = (to as RouteLocationOptions).replace === true
+
+ const shouldRedirect = handleRedirectRecord(targetLocation)
+
+ if (shouldRedirect)
+ return pushWithRedirect(
+ assign(locationAsObject(shouldRedirect), {
+ state:
+ typeof shouldRedirect === 'object'
+ ? assign({}, data, shouldRedirect.state)
+ : data,
+ force,
+ replace,
+ }),
+ // keep original redirectedFrom if it exists
+ redirectedFrom || targetLocation
+ )
+
+ // if it was a redirect we already called `pushWithRedirect` above
+ const toLocation = targetLocation as RouteLocationNormalized
+
+ toLocation.redirectedFrom = redirectedFrom
+ let failure: NavigationFailure | void | undefined
+
+ if (!force && isSameRouteLocation(stringifyQuery, from, targetLocation)) {
+ failure = createRouterError<NavigationFailure>(
+ ErrorTypes.NAVIGATION_DUPLICATED,
+ { to: toLocation, from }
+ )
+ // trigger scroll to allow scrolling to the same anchor
+ handleScroll(
+ from,
+ from,
+ // this is a push, the only way for it to be triggered from a
+ // history.listen is with a redirect, which makes it become a push
+ true,
+ // This cannot be the first navigation because the initial location
+ // cannot be manually navigated to
+ false
+ )
+ }
+
+ return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
+ .catch((error: NavigationFailure | NavigationRedirectError) =>
+ isNavigationFailure(error)
+ ? // navigation redirects still mark the router as ready
+ isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
+ ? error
+ : markAsReady(error) // also returns the error
+ : // reject any unknown error
+ triggerError(error, toLocation, from)
+ )
+ .then((failure: NavigationFailure | NavigationRedirectError | void) => {
+ if (failure) {
+ if (
+ isNavigationFailure(failure, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
+ ) {
+ if (
+ __DEV__ &&
+ // we are redirecting to the same location we were already at
+ isSameRouteLocation(
+ stringifyQuery,
+ resolve(failure.to),
+ toLocation
+ ) &&
+ // and we have done it a couple of times
+ redirectedFrom &&
+ // @ts-expect-error: added only in dev
+ (redirectedFrom._count = redirectedFrom._count
+ ? // @ts-expect-error
+ redirectedFrom._count + 1
+ : 1) > 30
+ ) {
+ warn(
+ `Detected a possibly infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow.\n Are you always returning a new location within a navigation guard? That would lead to this error. Only return when redirecting or aborting, that should fix this. This might break in production if not fixed.`
+ )
+ return Promise.reject(
+ new Error('Infinite redirect in navigation guard')
+ )
+ }
+
+ return pushWithRedirect(
+ // keep options
+ assign(
+ {
+ // preserve an existing replacement but allow the redirect to override it
+ replace,
+ },
+ locationAsObject(failure.to),
+ {
+ state:
+ typeof failure.to === 'object'
+ ? assign({}, data, failure.to.state)
+ : data,
+ force,
+ }
+ ),
+ // preserve the original redirectedFrom if any
+ redirectedFrom || toLocation
+ )
+ }
+ } else {
+ // if we fail we don't finalize the navigation
+ failure = finalizeNavigation(
+ toLocation as RouteLocationNormalizedLoaded,
+ from,
+ true,
+ replace,
+ data
+ )
+ }
+ triggerAfterEach(
+ toLocation as RouteLocationNormalizedLoaded,
+ from,
+ failure
+ )
+ return failure
+ })
+ }
+
+ /**
+ * Helper to reject and skip all navigation guards if a new navigation happened
+ * @param to
+ * @param from
+ */
+ function checkCanceledNavigationAndReject(
+ to: RouteLocationNormalized,
+ from: RouteLocationNormalized
+ ): Promise<void> {
+ const error = checkCanceledNavigation(to, from)
+ return error ? Promise.reject(error) : Promise.resolve()
+ }
+
+ function runWithContext<T>(fn: () => T): T {
+ const app: App | undefined = installedApps.values().next().value
+ // support Vue < 3.3
+ return app && typeof app.runWithContext === 'function'
+ ? app.runWithContext(fn)
+ : fn()
+ }
+
+ // TODO: refactor the whole before guards by internally using router.beforeEach
+
+ function navigate(
+ to: RouteLocationNormalized,
+ from: RouteLocationNormalizedLoaded
+ ): Promise<any> {
+ let guards: Lazy<any>[]
+
+ const [leavingRecords, updatingRecords, enteringRecords] =
+ extractChangingRecords(to, from)
+
+ // all components here have been resolved once because we are leaving
+ guards = extractComponentsGuards(
+ leavingRecords.reverse(),
+ 'beforeRouteLeave',
+ to,
+ from
+ )
+
+ // leavingRecords is already reversed
+ for (const record of leavingRecords) {
+ record.leaveGuards.forEach(guard => {
+ guards.push(guardToPromiseFn(guard, to, from))
+ })
+ }
+
+ const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(
+ null,
+ to,
+ from
+ )
+
+ guards.push(canceledNavigationCheck)
+
+ // run the queue of per route beforeRouteLeave guards
+ return (
+ runGuardQueue(guards)
+ .then(() => {
+ // check global guards beforeEach
+ guards = []
+ for (const guard of beforeGuards.list()) {
+ guards.push(guardToPromiseFn(guard, to, from))
+ }
+ guards.push(canceledNavigationCheck)
+
+ return runGuardQueue(guards)
+ })
+ .then(() => {
+ // check in components beforeRouteUpdate
+ guards = extractComponentsGuards(
+ updatingRecords,
+ 'beforeRouteUpdate',
+ to,
+ from
+ )
+
+ for (const record of updatingRecords) {
+ record.updateGuards.forEach(guard => {
+ guards.push(guardToPromiseFn(guard, to, from))
+ })
+ }
+ guards.push(canceledNavigationCheck)
+
+ // run the queue of per route beforeEnter guards
+ return runGuardQueue(guards)
+ })
+ .then(() => {
+ // check the route beforeEnter
+ guards = []
+ for (const record of enteringRecords) {
+ // do not trigger beforeEnter on reused views
+ if (record.beforeEnter) {
+ if (isArray(record.beforeEnter)) {
+ for (const beforeEnter of record.beforeEnter)
+ guards.push(guardToPromiseFn(beforeEnter, to, from))
+ } else {
+ guards.push(guardToPromiseFn(record.beforeEnter, to, from))
+ }
+ }
+ }
+ guards.push(canceledNavigationCheck)
+
+ // run the queue of per route beforeEnter guards
+ return runGuardQueue(guards)
+ })
+ .then(() => {
+ // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
+
+ // clear existing enterCallbacks, these are added by extractComponentsGuards
+ to.matched.forEach(record => (record.enterCallbacks = {}))
+
+ // check in-component beforeRouteEnter
+ guards = extractComponentsGuards(
+ enteringRecords,
+ 'beforeRouteEnter',
+ to,
+ from,
+ runWithContext
+ )
+ guards.push(canceledNavigationCheck)
+
+ // run the queue of per route beforeEnter guards
+ return runGuardQueue(guards)
+ })
+ .then(() => {
+ // check global guards beforeResolve
+ guards = []
+ for (const guard of beforeResolveGuards.list()) {
+ guards.push(guardToPromiseFn(guard, to, from))
+ }
+ guards.push(canceledNavigationCheck)
+
+ return runGuardQueue(guards)
+ })
+ // catch any navigation canceled
+ .catch(err =>
+ isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
+ ? err
+ : Promise.reject(err)
+ )
+ )
+ }
+
+ function triggerAfterEach(
+ to: RouteLocationNormalizedLoaded,
+ from: RouteLocationNormalizedLoaded,
+ failure?: NavigationFailure | void
+ ): void {
+ // navigation is confirmed, call afterGuards
+ // TODO: wrap with error handlers
+ afterGuards
+ .list()
+ .forEach(guard => runWithContext(() => guard(to, from, failure)))
+ }
+
+ /**
+ * - Cleans up any navigation guards
+ * - Changes the url if necessary
+ * - Calls the scrollBehavior
+ */
+ function finalizeNavigation(
+ toLocation: RouteLocationNormalizedLoaded,
+ from: RouteLocationNormalizedLoaded,
+ isPush: boolean,
+ replace?: boolean,
+ data?: HistoryState
+ ): NavigationFailure | void {
+ // a more recent navigation took place
+ const error = checkCanceledNavigation(toLocation, from)
+ if (error) return error
+
+ // only consider as push if it's not the first navigation
+ const isFirstNavigation = from === START_LOCATION_NORMALIZED
+ const state: Partial<HistoryState> | null = !isBrowser ? {} : history.state
+
+ // change URL only if the user did a push/replace and if it's not the initial navigation because
+ // it's just reflecting the url
+ if (isPush) {
+ // on the initial navigation, we want to reuse the scroll position from
+ // history state if it exists
+ if (replace || isFirstNavigation)
+ routerHistory.replace(
+ toLocation.fullPath,
+ assign(
+ {
+ scroll: isFirstNavigation && state && state.scroll,
+ },
+ data
+ )
+ )
+ else routerHistory.push(toLocation.fullPath, data)
+ }
+
+ // accept current navigation
+ currentRoute.value = toLocation
+ handleScroll(toLocation, from, isPush, isFirstNavigation)
+
+ markAsReady()
+ }
+
+ let removeHistoryListener: undefined | null | (() => void)
+ // attach listener to history to trigger navigations
+ function setupListeners() {
+ // avoid setting up listeners twice due to an invalid first navigation
+ if (removeHistoryListener) return
+ removeHistoryListener = routerHistory.listen((to, _from, info) => {
+ if (!router.listening) return
+ // cannot be a redirect route because it was in history
+ const toLocation = resolve(to) as RouteLocationNormalized
+
+ // due to dynamic routing, and to hash history with manual navigation
+ // (manually changing the url or calling history.hash = '#/somewhere'),
+ // there could be a redirect record in history
+ const shouldRedirect = handleRedirectRecord(toLocation)
+ if (shouldRedirect) {
+ pushWithRedirect(
+ assign(shouldRedirect, { replace: true, force: true }),
+ toLocation
+ ).catch(noop)
+ return
+ }
+
+ pendingLocation = toLocation
+ const from = currentRoute.value
+
+ // TODO: should be moved to web history?
+ if (isBrowser) {
+ saveScrollPosition(
+ getScrollKey(from.fullPath, info.delta),
+ computeScrollPosition()
+ )
+ }
+
+ navigate(toLocation, from)
+ .catch((error: NavigationFailure | NavigationRedirectError) => {
+ if (
+ isNavigationFailure(
+ error,
+ ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED
+ )
+ ) {
+ return error
+ }
+ if (
+ isNavigationFailure(error, ErrorTypes.NAVIGATION_GUARD_REDIRECT)
+ ) {
+ // Here we could call if (info.delta) routerHistory.go(-info.delta,
+ // false) but this is bug prone as we have no way to wait the
+ // navigation to be finished before calling pushWithRedirect. Using
+ // a setTimeout of 16ms seems to work but there is no guarantee for
+ // it to work on every browser. So instead we do not restore the
+ // history entry and trigger a new navigation as requested by the
+ // navigation guard.
+
+ // the error is already handled by router.push we just want to avoid
+ // logging the error
+ pushWithRedirect(
+ assign(locationAsObject((error as NavigationRedirectError).to), {
+ force: true,
+ }),
+ toLocation
+ // avoid an uncaught rejection, let push call triggerError
+ )
+ .then(failure => {
+ // manual change in hash history #916 ending up in the URL not
+ // changing, but it was changed by the manual url change, so we
+ // need to manually change it ourselves
+ if (
+ isNavigationFailure(
+ failure,
+ ErrorTypes.NAVIGATION_ABORTED |
+ ErrorTypes.NAVIGATION_DUPLICATED
+ ) &&
+ !info.delta &&
+ info.type === NavigationType.pop
+ ) {
+ routerHistory.go(-1, false)
+ }
+ })
+ .catch(noop)
+ // avoid the then branch
+ return Promise.reject()
+ }
+ // do not restore history on unknown direction
+ if (info.delta) {
+ routerHistory.go(-info.delta, false)
+ }
+ // unrecognized error, transfer to the global handler
+ return triggerError(error, toLocation, from)
+ })
+ .then((failure: NavigationFailure | void) => {
+ failure =
+ failure ||
+ finalizeNavigation(
+ // after navigation, all matched components are resolved
+ toLocation as RouteLocationNormalizedLoaded,
+ from,
+ false
+ )
+
+ // revert the navigation
+ if (failure) {
+ if (
+ info.delta &&
+ // a new navigation has been triggered, so we do not want to revert, that will change the current history
+ // entry while a different route is displayed
+ !isNavigationFailure(failure, ErrorTypes.NAVIGATION_CANCELLED)
+ ) {
+ routerHistory.go(-info.delta, false)
+ } else if (
+ info.type === NavigationType.pop &&
+ isNavigationFailure(
+ failure,
+ ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_DUPLICATED
+ )
+ ) {
+ // manual change in hash history #916
+ // it's like a push but lacks the information of the direction
+ routerHistory.go(-1, false)
+ }
+ }
+
+ triggerAfterEach(
+ toLocation as RouteLocationNormalizedLoaded,
+ from,
+ failure
+ )
+ })
+ // avoid warnings in the console about uncaught rejections, they are logged by triggerErrors
+ .catch(noop)
+ })
+ }
+
+ // Initialization and Errors
+
+ let readyHandlers = useCallbacks<_OnReadyCallback>()
+ let errorListeners = useCallbacks<_ErrorListener>()
+ let ready: boolean
+
+ /**
+ * Trigger errorListeners added via onError and throws the error as well
+ *
+ * @param error - error to throw
+ * @param to - location we were navigating to when the error happened
+ * @param from - location we were navigating from when the error happened
+ * @returns the error as a rejected promise
+ */
+ function triggerError(
+ error: any,
+ to: RouteLocationNormalized,
+ from: RouteLocationNormalizedLoaded
+ ): Promise<unknown> {
+ markAsReady(error)
+ const list = errorListeners.list()
+ if (list.length) {
+ list.forEach(handler => handler(error, to, from))
+ } else {
+ if (__DEV__) {
+ warn('uncaught error during route navigation:')
+ }
+ console.error(error)
+ }
+ // reject the error no matter there were error listeners or not
+ return Promise.reject(error)
+ }
+
+ function isReady(): Promise<void> {
+ if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
+ return Promise.resolve()
+ return new Promise((resolve, reject) => {
+ readyHandlers.add([resolve, reject])
+ })
+ }
+
+ /**
+ * Mark the router as ready, resolving the promised returned by isReady(). Can
+ * only be called once, otherwise does nothing.
+ * @param err - optional error
+ */
+ function markAsReady<E = any>(err: E): E
+ function markAsReady<E = any>(): void
+ function markAsReady<E = any>(err?: E): E | void {
+ if (!ready) {
+ // still not ready if an error happened
+ ready = !err
+ setupListeners()
+ readyHandlers
+ .list()
+ .forEach(([resolve, reject]) => (err ? reject(err) : resolve()))
+ readyHandlers.reset()
+ }
+ return err
+ }
+
+ // Scroll behavior
+ function handleScroll(
+ to: RouteLocationNormalizedLoaded,
+ from: RouteLocationNormalizedLoaded,
+ isPush: boolean,
+ isFirstNavigation: boolean
+ ): // the return is not meant to be used
+ Promise<unknown> {
+ const { scrollBehavior } = options
+ if (!isBrowser || !scrollBehavior) return Promise.resolve()
+
+ const scrollPosition: _ScrollPositionNormalized | null =
+ (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
+ ((isFirstNavigation || !isPush) &&
+ (history.state as HistoryState) &&
+ history.state.scroll) ||
+ null
+
+ return nextTick()
+ .then(() => scrollBehavior(to, from, scrollPosition))
+ .then(position => position && scrollToPosition(position))
+ .catch(err => triggerError(err, to, from))
+ }
+
+ const go = (delta: number) => routerHistory.go(delta)
+
+ let started: boolean | undefined
+ const installedApps = new Set<App>()
+
+ const router: Router = {
+ currentRoute,
+ listening: true,
+
+ addRoute,
+ removeRoute,
+ clearRoutes: matcher.clearRoutes,
+ hasRoute,
+ getRoutes,
+ resolve,
+ options,
+
+ push,
+ replace,
+ go,
+ back: () => go(-1),
+ forward: () => go(1),
+
+ beforeEach: beforeGuards.add,
+ beforeResolve: beforeResolveGuards.add,
+ afterEach: afterGuards.add,
+
+ onError: errorListeners.add,
+ isReady,
+
+ install(app: App) {
+ const router = this
+ app.component('RouterLink', RouterLink)
+ app.component('RouterView', RouterView)
+
+ app.config.globalProperties.$router = router
+ Object.defineProperty(app.config.globalProperties, '$route', {
+ enumerable: true,
+ get: () => unref(currentRoute),
+ })
+
+ // this initial navigation is only necessary on client, on server it doesn't
+ // make sense because it will create an extra unnecessary navigation and could
+ // lead to problems
+ if (
+ isBrowser &&
+ // used for the initial navigation client side to avoid pushing
+ // multiple times when the router is used in multiple apps
+ !started &&
+ currentRoute.value === START_LOCATION_NORMALIZED
+ ) {
+ // see above
+ started = true
+ push(routerHistory.location).catch(err => {
+ if (__DEV__) warn('Unexpected error when starting the router:', err)
+ })
+ }
+
+ const reactiveRoute = {} as RouteLocationNormalizedLoaded
+ for (const key in START_LOCATION_NORMALIZED) {
+ Object.defineProperty(reactiveRoute, key, {
+ get: () => currentRoute.value[key as keyof RouteLocationNormalized],
+ enumerable: true,
+ })
+ }
+
+ app.provide(routerKey, router)
+ app.provide(routeLocationKey, shallowReactive(reactiveRoute))
+ app.provide(routerViewLocationKey, currentRoute)
+
+ const unmountApp = app.unmount
+ installedApps.add(app)
+ app.unmount = function () {
+ installedApps.delete(app)
+ // the router is not attached to an app anymore
+ if (installedApps.size < 1) {
+ // invalidate the current navigation
+ pendingLocation = START_LOCATION_NORMALIZED
+ removeHistoryListener && removeHistoryListener()
+ removeHistoryListener = null
+ currentRoute.value = START_LOCATION_NORMALIZED
+ started = false
+ ready = false
+ }
+ unmountApp()
+ }
+
+ // TODO: this probably needs to be updated so it can be used by vue-termui
+ if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
+ addDevtools(app, router, matcher)
+ }
+ },
+ }
+
+ // TODO: type this as NavigationGuardReturn or similar instead of any
+ function runGuardQueue(guards: Lazy<any>[]): Promise<any> {
+ return guards.reduce(
+ (promise, guard) => promise.then(() => runWithContext(guard)),
+ Promise.resolve()
+ )
+ }
+
+ return router
+}
NavigationGuardWithThis,
NavigationHookAfter,
RouteLocationResolved,
- RouteLocationAsRelative,
- RouteLocationAsPath,
- RouteLocationAsString,
RouteRecordNameGeneric,
} from './typed-routes'
-import { RouterHistory, HistoryState, NavigationType } from './history/common'
+import { HistoryState, NavigationType } from './history/common'
import {
- ScrollPosition,
getSavedScrollPosition,
getScrollKey,
saveScrollPosition,
scrollToPosition,
_ScrollPositionNormalized,
} from './scrollBehavior'
-import { createRouterMatcher, PathParserOptions } from './matcher'
+import { createRouterMatcher } from './matcher'
import {
createRouterError,
ErrorTypes,
NavigationFailure,
NavigationRedirectError,
isNavigationFailure,
+ _ErrorListener,
} from './errors'
import { applyToParams, isBrowser, assign, noop, isArray } from './utils'
import { useCallbacks } from './utils/callbacks'
stringifyQuery as originalStringifyQuery,
LocationQuery,
} from './query'
-import { shallowRef, Ref, nextTick, App, unref, shallowReactive } from 'vue'
-import { RouteRecord, RouteRecordNormalized } from './matcher/types'
+import { shallowRef, nextTick, App, unref, shallowReactive } from 'vue'
+import { RouteRecordNormalized } from './matcher/types'
import {
parseURL,
stringifyURL,
isSameRouteLocation,
- isSameRouteRecord,
START_LOCATION_NORMALIZED,
} from './location'
-import { extractComponentsGuards, guardToPromiseFn } from './navigationGuards'
+import {
+ extractChangingRecords,
+ extractComponentsGuards,
+ guardToPromiseFn,
+} from './navigationGuards'
import { warn } from './warning'
import { RouterLink } from './RouterLink'
import { RouterView } from './RouterView'
} from './injectionSymbols'
import { addDevtools } from './devtools'
import { _LiteralUnion } from './types/utils'
-import { RouteLocationAsRelativeTyped } from './typed-routes/route-location'
-import { RouteMap } from './typed-routes/route-map'
-
-/**
- * Internal type to define an ErrorHandler
- *
- * @param error - error thrown
- * @param to - location we were navigating to when the error happened
- * @param from - location we were navigating from when the error happened
- * @internal
- */
-export interface _ErrorListener {
- (
- error: any,
- to: RouteLocationNormalized,
- from: RouteLocationNormalizedLoaded
- ): any
-}
-// resolve, reject arguments of Promise constructor
-type OnReadyCallback = [() => void, (reason?: any) => void]
-
-type Awaitable<T> = T | Promise<T>
-
-/**
- * Type of the `scrollBehavior` option that can be passed to `createRouter`.
- */
-export interface RouterScrollBehavior {
- /**
- * @param to - Route location where we are navigating to
- * @param from - Route location where we are navigating from
- * @param savedPosition - saved position if it exists, `null` otherwise
- */
- (
- to: RouteLocationNormalized,
- from: RouteLocationNormalizedLoaded,
- savedPosition: _ScrollPositionNormalized | null
- ): Awaitable<ScrollPosition | false | void>
-}
+import {
+ EXPERIMENTAL_RouterOptions_Base,
+ EXPERIMENTAL_Router_Base,
+ _OnReadyCallback,
+} from './experimental/router'
/**
* Options to initialize a {@link Router} instance.
*/
-export interface RouterOptions extends PathParserOptions {
- /**
- * History implementation used by the router. Most web applications should use
- * `createWebHistory` but it requires the server to be properly configured.
- * You can also use a _hash_ based history with `createWebHashHistory` that
- * does not require any configuration on the server but isn't handled at all
- * by search engines and does poorly on SEO.
- *
- * @example
- * ```js
- * createRouter({
- * history: createWebHistory(),
- * // other options...
- * })
- * ```
- */
- history: RouterHistory
+export interface RouterOptions extends EXPERIMENTAL_RouterOptions_Base {
/**
* Initial list of routes that should be added to the router.
*/
routes: Readonly<RouteRecordRaw[]>
- /**
- * Function to control scrolling when navigating between pages. Can return a
- * Promise to delay scrolling. Check {@link ScrollBehavior}.
- *
- * @example
- * ```js
- * function scrollBehavior(to, from, savedPosition) {
- * // `to` and `from` are both route locations
- * // `savedPosition` can be null if there isn't one
- * }
- * ```
- */
- scrollBehavior?: RouterScrollBehavior
- /**
- * Custom implementation to parse a query. See its counterpart,
- * {@link RouterOptions.stringifyQuery}.
- *
- * @example
- * Let's say you want to use the [qs package](https://github.com/ljharb/qs)
- * to parse queries, you can provide both `parseQuery` and `stringifyQuery`:
- * ```js
- * import qs from 'qs'
- *
- * createRouter({
- * // other options...
- * parseQuery: qs.parse,
- * stringifyQuery: qs.stringify,
- * })
- * ```
- */
- parseQuery?: typeof originalParseQuery
- /**
- * Custom implementation to stringify a query object. Should not prepend a leading `?`.
- * {@link RouterOptions.parseQuery | parseQuery} counterpart to handle query parsing.
- */
- stringifyQuery?: typeof originalStringifyQuery
- /**
- * Default class applied to active {@link RouterLink}. If none is provided,
- * `router-link-active` will be applied.
- */
- linkActiveClass?: string
- /**
- * Default class applied to exact active {@link RouterLink}. If none is provided,
- * `router-link-exact-active` will be applied.
- */
- linkExactActiveClass?: string
- /**
- * Default class applied to non-active {@link RouterLink}. If none is provided,
- * `router-link-inactive` will be applied.
- */
- // linkInactiveClass?: string
}
/**
* Router instance.
*/
-export interface Router {
- /**
- * @internal
- */
- // readonly history: RouterHistory
- /**
- * Current {@link RouteLocationNormalized}
- */
- readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
+export interface Router
+ extends EXPERIMENTAL_Router_Base<RouteRecordRaw, RouteRecordNormalized> {
/**
* Original options object passed to create the Router
*/
readonly options: RouterOptions
-
- /**
- * Allows turning off the listening of history events. This is a low level api for micro-frontend.
- */
- listening: boolean
-
- /**
- * Add a new {@link RouteRecordRaw | route record} as the child of an existing route.
- *
- * @param parentName - Parent Route Record where `route` should be appended at
- * @param route - Route Record to add
- */
- addRoute(
- // NOTE: it could be `keyof RouteMap` but the point of dynamic routes is not knowing the routes at build
- parentName: NonNullable<RouteRecordNameGeneric>,
- route: RouteRecordRaw
- ): () => void
- /**
- * Add a new {@link RouteRecordRaw | route record} to the router.
- *
- * @param route - Route Record to add
- */
- addRoute(route: RouteRecordRaw): () => void
- /**
- * Remove an existing route by its name.
- *
- * @param name - Name of the route to remove
- */
- removeRoute(name: NonNullable<RouteRecordNameGeneric>): void
- /**
- * Checks if a route with a given name exists
- *
- * @param name - Name of the route to check
- */
- hasRoute(name: NonNullable<RouteRecordNameGeneric>): boolean
- /**
- * Get a full list of all the {@link RouteRecord | route records}.
- */
- getRoutes(): RouteRecord[]
-
- /**
- * Delete all routes from the router matcher.
- */
- clearRoutes(): void
-
- /**
- * Returns the {@link RouteLocation | normalized version} of a
- * {@link RouteLocationRaw | route location}. Also includes an `href` property
- * that includes any existing `base`. By default, the `currentLocation` used is
- * `router.currentRoute` and should only be overridden in advanced use cases.
- *
- * @param to - Raw route location to resolve
- * @param currentLocation - Optional current location to resolve against
- */
- resolve<Name extends keyof RouteMap = keyof RouteMap>(
- to: RouteLocationAsRelativeTyped<RouteMap, Name>,
- // NOTE: This version doesn't work probably because it infers the type too early
- // | RouteLocationAsRelative<Name>
- currentLocation?: RouteLocationNormalizedLoaded
- ): RouteLocationResolved<Name>
- resolve(
- // not having the overload produces errors in RouterLink calls to router.resolve()
- to: RouteLocationAsString | RouteLocationAsRelative | RouteLocationAsPath,
- currentLocation?: RouteLocationNormalizedLoaded
- ): RouteLocationResolved
-
- /**
- * Programmatically navigate to a new URL by pushing an entry in the history
- * stack.
- *
- * @param to - Route location to navigate to
- */
- push(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
-
- /**
- * Programmatically navigate to a new URL by replacing the current entry in
- * the history stack.
- *
- * @param to - Route location to navigate to
- */
- replace(to: RouteLocationRaw): Promise<NavigationFailure | void | undefined>
-
- /**
- * Go back in history if possible by calling `history.back()`. Equivalent to
- * `router.go(-1)`.
- */
- back(): ReturnType<Router['go']>
- /**
- * Go forward in history if possible by calling `history.forward()`.
- * Equivalent to `router.go(1)`.
- */
- forward(): ReturnType<Router['go']>
- /**
- * Allows you to move forward or backward through the history. Calls
- * `history.go()`.
- *
- * @param delta - The position in the history to which you want to move,
- * relative to the current page
- */
- go(delta: number): void
-
- /**
- * Add a navigation guard that executes before any navigation. Returns a
- * function that removes the registered guard.
- *
- * @param guard - navigation guard to add
- */
- beforeEach(guard: NavigationGuardWithThis<undefined>): () => void
- /**
- * Add a navigation guard that executes before navigation is about to be
- * resolved. At this state all component have been fetched and other
- * navigation guards have been successful. Returns a function that removes the
- * registered guard.
- *
- * @param guard - navigation guard to add
- * @returns a function that removes the registered guard
- *
- * @example
- * ```js
- * router.beforeResolve(to => {
- * if (to.meta.requiresAuth && !isAuthenticated) return false
- * })
- * ```
- *
- */
- beforeResolve(guard: NavigationGuardWithThis<undefined>): () => void
-
- /**
- * Add a navigation hook that is executed after every navigation. Returns a
- * function that removes the registered hook.
- *
- * @param guard - navigation hook to add
- * @returns a function that removes the registered hook
- *
- * @example
- * ```js
- * router.afterEach((to, from, failure) => {
- * if (isNavigationFailure(failure)) {
- * console.log('failed navigation', failure)
- * }
- * })
- * ```
- */
- afterEach(guard: NavigationHookAfter): () => void
-
- /**
- * Adds an error handler that is called every time a non caught error happens
- * during navigation. This includes errors thrown synchronously and
- * asynchronously, errors returned or passed to `next` in any navigation
- * guard, and errors occurred when trying to resolve an async component that
- * is required to render a route.
- *
- * @param handler - error handler to register
- */
- onError(handler: _ErrorListener): () => void
- /**
- * Returns a Promise that resolves when the router has completed the initial
- * navigation, which means it has resolved all async enter hooks and async
- * components that are associated with the initial route. If the initial
- * navigation already happened, the promise resolves immediately.
- *
- * This is useful in server-side rendering to ensure consistent output on both
- * the server and the client. Note that on server side, you need to manually
- * push the initial location while on client side, the router automatically
- * picks it up from the URL.
- */
- isReady(): Promise<void>
-
- /**
- * Called automatically by `app.use(router)`. Should not be called manually by
- * the user. This will trigger the initial navigation when on client side.
- *
- * @internal
- * @param app - Application that uses the router
- */
- install(app: App): void
}
/**
// Initialization and Errors
- let readyHandlers = useCallbacks<OnReadyCallback>()
+ let readyHandlers = useCallbacks<_OnReadyCallback>()
let errorListeners = useCallbacks<_ErrorListener>()
let ready: boolean
return router
}
-
-function extractChangingRecords(
- to: RouteLocationNormalized,
- from: RouteLocationNormalizedLoaded
-) {
- const leavingRecords: RouteRecordNormalized[] = []
- const updatingRecords: RouteRecordNormalized[] = []
- const enteringRecords: RouteRecordNormalized[] = []
-
- const len = Math.max(from.matched.length, to.matched.length)
- for (let i = 0; i < len; i++) {
- const recordFrom = from.matched[i]
- if (recordFrom) {
- if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))
- updatingRecords.push(recordFrom)
- else leavingRecords.push(recordFrom)
- }
- const recordTo = to.matched[i]
- if (recordTo) {
- // the type doesn't matter because we are comparing per reference
- if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {
- enteringRecords.push(recordTo)
- }
- }
- }
-
- return [leavingRecords, updatingRecords, enteringRecords]
-}