import {
RouteRecordRaw,
- Lazy,
isRouteLocation,
isRouteName,
- RouteLocationOptions,
MatcherLocationRaw,
} from './types'
import type {
- RouteLocation,
RouteLocationRaw,
RouteParams,
- RouteLocationNormalized,
RouteLocationNormalizedLoaded,
- NavigationGuardWithThis,
- NavigationHookAfter,
RouteLocationResolved,
RouteRecordNameGeneric,
} from './typed-routes'
-import { HistoryState, NavigationType } from './history/common'
-import {
- getSavedScrollPosition,
- getScrollKey,
- saveScrollPosition,
- computeScrollPosition,
- scrollToPosition,
- _ScrollPositionNormalized,
-} from './scrollBehavior'
-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'
+import { _ScrollPositionNormalized } from './scrollBehavior'
+import { _ErrorListener } from './errors'
+import { applyToParams, assign, mergeOptions } from './utils'
import { encodeParam, decode, encodeHash } from './encoding'
import {
normalizeQuery,
- parseQuery as originalParseQuery,
stringifyQuery as originalStringifyQuery,
LocationQuery,
+ parseQuery,
+ stringifyQuery,
} from './query'
-import { shallowRef, nextTick, App, unref, shallowReactive } from 'vue'
import { RouteRecordNormalized } from './matcher/types'
-import {
- parseURL,
- stringifyURL,
- isSameRouteLocation,
- START_LOCATION_NORMALIZED,
-} from './location'
-import {
- extractChangingRecords,
- extractComponentsGuards,
- guardToPromiseFn,
-} from './navigationGuards'
+import { parseURL, stringifyURL } from './location'
import { warn } from './warning'
-import { RouterLink } from './RouterLink'
-import { RouterView } from './RouterView'
-import {
- routeLocationKey,
- routerKey,
- routerViewLocationKey,
-} from './injectionSymbols'
-import { addDevtools } from './devtools'
import { _LiteralUnion } from './types/utils'
import {
+ EXPERIMENTAL_RouteRecordNormalized,
+ EXPERIMENTAL_RouteRecordRaw,
EXPERIMENTAL_RouterOptions_Base,
EXPERIMENTAL_Router_Base,
_OnReadyCallback,
+ experimental_createRouter,
} from './experimental/router'
+import { createCompiledMatcher } from './new-route-resolver'
+import {
+ NEW_RouterResolver,
+ NEW_MatcherRecordRaw,
+} from './new-route-resolver/resolver'
+import {
+ checkChildMissingNameWithEmptyPath,
+ normalizeRecordProps,
+ normalizeRouteRecord,
+ PathParserOptions,
+} from './matcher'
+import { PATH_PARSER_OPTIONS_DEFAULTS } from './matcher/pathParserRanker'
+import {
+ createRouteRecordMatcher,
+ NEW_createRouteRecordMatcher,
+} from './matcher/pathMatcher'
/**
* Options to initialize a {@link Router} instance.
readonly options: RouterOptions
}
+/*
+ * Normalizes a RouteRecordRaw. Creates a copy
+ *
+ * @param record
+ * @returns the normalized version
+ */
+export function NEW_normalizeRouteRecord(
+ record: RouteRecordRaw & { aliasOf?: RouteRecordNormalized },
+ parent?: RouteRecordNormalized
+): RouteRecordNormalized {
+ let { path } = record
+ // Build up the path for nested routes if the child isn't an absolute
+ // route. Only add the / delimiter if the child path isn't empty and if the
+ // parent path doesn't have a trailing slash
+ if (parent && path[0] !== '/') {
+ const parentPath = parent.path
+ const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/'
+ path = parentPath + (path && connectingSlash + path)
+ }
+
+ const normalized: Omit<RouteRecordNormalized, 'mods'> = {
+ path,
+ redirect: record.redirect,
+ name: record.name,
+ meta: record.meta || {},
+ aliasOf: record.aliasOf,
+ beforeEnter: record.beforeEnter,
+ props: normalizeRecordProps(record),
+ // TODO: normalize children here or outside?
+ children: record.children || [],
+ instances: {},
+ leaveGuards: new Set(),
+ updateGuards: new Set(),
+ enterCallbacks: {},
+ // must be declared afterwards
+ // mods: {},
+ components:
+ 'components' in record
+ ? record.components || null
+ : record.component && { default: record.component },
+ }
+
+ // mods contain modules and shouldn't be copied,
+ // logged or anything. It's just used for internal
+ // advanced use cases like data loaders
+ Object.defineProperty(normalized, 'mods', {
+ value: {},
+ })
+
+ return normalized as RouteRecordNormalized
+}
+
+export function compileRouteRecord(
+ record: RouteRecordRaw,
+ parent?: RouteRecordNormalized,
+ originalRecord?: EXPERIMENTAL_RouteRecordNormalized
+): EXPERIMENTAL_RouteRecordRaw {
+ // used later on to remove by name
+ const isRootAdd = !originalRecord
+ const options: PathParserOptions = mergeOptions(
+ PATH_PARSER_OPTIONS_DEFAULTS,
+ record
+ )
+ const mainNormalizedRecord = NEW_normalizeRouteRecord(record, parent)
+ const recordMatcher = NEW_createRouteRecordMatcher(
+ mainNormalizedRecord,
+ // FIXME: is this needed?
+ // @ts-expect-error: the parent is the record not the matcher
+ parent,
+ options
+ )
+
+ recordMatcher.record
+
+ if (__DEV__) {
+ // TODO:
+ // checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent)
+ }
+ // we might be the child of an alias
+ // mainNormalizedRecord.aliasOf = originalRecord
+ // generate an array of records to correctly handle aliases
+ const normalizedRecords: EXPERIMENTAL_RouteRecordNormalized[] = [
+ mainNormalizedRecord,
+ ]
+
+ if ('alias' in record) {
+ const aliases =
+ typeof record.alias === 'string' ? [record.alias] : record.alias!
+ for (const alias of aliases) {
+ normalizedRecords.push(
+ // we need to normalize again to ensure the `mods` property
+ // being non enumerable
+ NEW_normalizeRouteRecord(
+ assign({}, mainNormalizedRecord, {
+ // this allows us to hold a copy of the `components` option
+ // so that async components cache is hold on the original record
+ components: originalRecord
+ ? originalRecord.record.components
+ : mainNormalizedRecord.components,
+ path: alias,
+ // we might be the child of an alias
+ aliasOf: originalRecord
+ ? originalRecord.record
+ : mainNormalizedRecord,
+ // the aliases are always of the same kind as the original since they
+ // are defined on the same record
+ })
+ )
+ )
+ }
+ }
+
+ let matcher: RouteRecordMatcher
+ let originalMatcher: RouteRecordMatcher | undefined
+
+ for (const normalizedRecord of normalizedRecords) {
+ const { path } = normalizedRecord
+ // Build up the path for nested routes if the child isn't an absolute
+ // route. Only add the / delimiter if the child path isn't empty and if the
+ // parent path doesn't have a trailing slash
+ if (parent && path[0] !== '/') {
+ const parentPath = parent.record.path
+ const connectingSlash =
+ parentPath[parentPath.length - 1] === '/' ? '' : '/'
+ normalizedRecord.path =
+ parent.record.path + (path && connectingSlash + path)
+ }
+
+ if (__DEV__ && normalizedRecord.path === '*') {
+ throw new Error(
+ 'Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
+ 'See more at https://router.vuejs.org/guide/migration/#Removed-star-or-catch-all-routes.'
+ )
+ }
+
+ // create the object beforehand, so it can be passed to children
+ matcher = createRouteRecordMatcher(normalizedRecord, parent, options)
+
+ if (__DEV__ && parent && path[0] === '/')
+ checkMissingParamsInAbsolutePath(matcher, parent)
+
+ // if we are an alias we must tell the original record that we exist,
+ // so we can be removed
+ if (originalRecord) {
+ originalRecord.alias.push(matcher)
+ if (__DEV__) {
+ checkSameParams(originalRecord, matcher)
+ }
+ } else {
+ // otherwise, the first record is the original and others are aliases
+ originalMatcher = originalMatcher || matcher
+ if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
+
+ // remove the route if named and only for the top record (avoid in nested calls)
+ // this works because the original record is the first one
+ if (isRootAdd && record.name && !isAliasRecord(matcher)) {
+ if (__DEV__) {
+ checkSameNameAsAncestor(record, parent)
+ }
+ removeRoute(record.name)
+ }
+ }
+
+ // Avoid adding a record that doesn't display anything. This allows passing through records without a component to
+ // not be reached and pass through the catch all route
+ if (isMatchable(matcher)) {
+ insertMatcher(matcher)
+ }
+
+ if (mainNormalizedRecord.children) {
+ const children = mainNormalizedRecord.children
+ for (let i = 0; i < children.length; i++) {
+ addRoute(
+ children[i],
+ matcher,
+ originalRecord && originalRecord.children[i]
+ )
+ }
+ }
+
+ // if there was no original record, then the first one was not an alias and all
+ // other aliases (if any) need to reference this record when adding children
+ originalRecord = originalRecord || matcher
+
+ // TODO: add normalized records for more flexibility
+ // if (parent && isAliasRecord(originalRecord)) {
+ // parent.children.push(originalRecord)
+ // }
+ }
+
+ return originalMatcher
+ ? () => {
+ // since other matchers are aliases, they should be removed by the original matcher
+ removeRoute(originalMatcher!)
+ }
+ : noop
+ return {
+ name: record.name,
+ children: record.children?.map(child => compileRouteRecord(child, record)),
+ }
+}
+
/**
* Creates a Router instance that can be used by a Vue app.
*
* @param options - {@link RouterOptions}
*/
export function createRouter(options: RouterOptions): Router {
- const matcher = createRouterMatcher(options.routes, options)
- const parseQuery = options.parseQuery || originalParseQuery
- const stringifyQuery = options.stringifyQuery || originalStringifyQuery
- const routerHistory = options.history
- 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
+ const matcher = createCompiledMatcher<EXPERIMENTAL_RouteRecordNormalized>(
+ options.routes.map(record => compileRouteRecord(record))
)
- 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 router = experimental_createRouter({
+ matcher,
+ ...options,
+ // avoids adding the routes twice
+ routes: [],
+ })
+ return router
+}
+
+export function _createRouter(options: RouterOptions): Router {
const normalizeParams = applyToParams.bind(
null,
paramValue => '' + paramValue
}
}
- function getRoutes() {
- return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
- }
-
- function hasRoute(name: NonNullable<RouteRecordNameGeneric>): boolean {
- return !!matcher.getRecordMatcher(name)
- }
-
function resolve(
rawLocation: RouteLocationRaw,
currentLocation?: RouteLocationNormalizedLoaded
}
)
}
-
- 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
}