]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
chore: wip refactor refactor/use-experimental-router
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 8 Jan 2025 09:49:03 +0000 (10:49 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 8 Jan 2025 09:49:03 +0000 (10:49 +0100)
The idea here was to reuse the experimental router within the actual
router. This turns out to be a lot of work without any security of
having something working and without breaking changes in the end. So I
think it's better to keep two versions of the createRouter function with
prefix `EXPERIMENTAL_`. In the end, one code base only uses one of the
function so it's fine to keep the code duplicated until v5. This branch
is here as a reminder of the failure.

packages/router/src/experimental/router.ts
packages/router/src/matcher/pathMatcher.ts
packages/router/src/router.ts

index 9cf885cc1dfc6f0d63589a325635d03ef73f7f0f..654bc3b6dff7ab6fbf92eae430c2de4c55cac798 100644 (file)
@@ -410,6 +410,7 @@ export interface EXPERIMENTAL_RouteRecordNormalized extends NEW_MatcherRecord {
    * Arbitrary data attached to the record.
    */
   meta: RouteMeta
+  parent?: EXPERIMENTAL_RouteRecordNormalized
 }
 
 function normalizeRouteRecord(
index aae2b782690d715cc806c75af1b2db86b5af321f..a4e00f7cf2df4ab6cde482cd8f94219fd9340be7 100644 (file)
@@ -16,6 +16,32 @@ export interface RouteRecordMatcher extends PathParser {
   alias: RouteRecordMatcher[]
 }
 
+export function NEW_createRouteRecordMatcher(
+  record: Readonly<RouteRecord>,
+  parent: RouteRecordMatcher | undefined,
+  options?: PathParserOptions
+): RouteRecordMatcher {
+  const parser = tokensToParser(tokenizePath(record.path), options)
+
+  const matcher: RouteRecordMatcher = assign(parser, {
+    record,
+    parent,
+    // these needs to be populated by the parent
+    children: [],
+    alias: [],
+  })
+
+  if (parent) {
+    // both are aliases or both are not aliases
+    // we don't want to mix them because the order is used when
+    // passing originalRecord in Matcher.addRoute
+    if (!matcher.record.aliasOf === !parent.record.aliasOf)
+      parent.children.push(matcher)
+  }
+
+  return matcher
+}
+
 export function createRouteRecordMatcher(
   record: Readonly<RouteRecord>,
   parent: RouteRecordMatcher | undefined,
index 059606db29fb05e9e4f02961d8402ee59e076fae..d8a121fa91266ddd245e61cb0e18a035066fc515 100644 (file)
@@ -1,77 +1,55 @@
 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.
@@ -94,35 +72,229 @@ export interface Router
   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
@@ -165,14 +337,6 @@ export function createRouter(options: RouterOptions): Router {
     }
   }
 
-  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
@@ -314,734 +478,4 @@ export function createRouter(options: RouterOptions): Router {
       }
     )
   }
-
-  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
 }