]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
refactor: reorganize types and add initial experimental router
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 17 Dec 2024 14:32:50 +0000 (15:32 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Tue, 17 Dec 2024 14:32:50 +0000 (15:32 +0100)
packages/router/src/errors.ts
packages/router/src/experimental/router.ts [new file with mode: 0644]
packages/router/src/index.ts
packages/router/src/navigationGuards.ts
packages/router/src/new-route-resolver/matcher.spec.ts
packages/router/src/router.ts
packages/router/src/scrollBehavior.ts
packages/router/src/types/utils.ts

index 877a0de2169a9723a9b912d35be2d31387026a27..63abf5f8ac77b2990880a333821ee11b90ad0624 100644 (file)
@@ -1,5 +1,9 @@
 import type { MatcherLocationRaw, MatcherLocation } from './types'
-import type { RouteLocationRaw, RouteLocationNormalized } from './typed-routes'
+import type {
+  RouteLocationRaw,
+  RouteLocationNormalized,
+  RouteLocationNormalizedLoaded,
+} from './typed-routes'
 import { assign } from './utils'
 
 /**
@@ -199,3 +203,19 @@ function stringifyRoute(to: RouteLocationRaw): string {
   }
   return JSON.stringify(location, null, 2)
 }
+/**
+ * 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
+}
diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts
new file mode 100644 (file)
index 0000000..ac44da1
--- /dev/null
@@ -0,0 +1,1351 @@
+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
+}
index 2a62ad15670c4d4f21d863a81b4cf2cace137422..88e9ce73219027d40a94603996413d2f1e8cc9bc 100644 (file)
@@ -137,7 +137,8 @@ export type {
 } from './typed-routes'
 
 export { createRouter } from './router'
-export type { Router, RouterOptions, RouterScrollBehavior } from './router'
+export type { Router, RouterOptions } from './router'
+export type { RouterScrollBehavior } from './scrollBehavior'
 
 export { NavigationFailureType, isNavigationFailure } from './errors'
 export type {
index 90c079f702e10202859ddf2b1eceb313953cebab..2f314ba677d3c3885af66d9bfef828f925e88d19 100644 (file)
@@ -22,6 +22,7 @@ import { matchedRouteKey } from './injectionSymbols'
 import { RouteRecordNormalized } from './matcher/types'
 import { isESModule, isRouteComponent } from './utils'
 import { warn } from './warning'
+import { isSameRouteRecord } from './location'
 
 function registerGuard(
   record: RouteRecordNormalized,
@@ -393,3 +394,42 @@ export function loadRouteLocation(
         )
       ).then(() => route as RouteLocationNormalizedLoaded)
 }
+
+/**
+ * Split the leaving, updating, and entering records.
+ * @internal
+ *
+ * @param  to - Location we are navigating to
+ * @param from - Location we are navigating from
+ */
+export function extractChangingRecords(
+  to: RouteLocationNormalized,
+  from: RouteLocationNormalizedLoaded
+): [
+  leavingRecords: RouteRecordNormalized[],
+  updatingRecords: RouteRecordNormalized[],
+  enteringRecords: RouteRecordNormalized[]
+] {
+  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]
+}
index f508fe113877fd6ce98e8fc03f8bf3e36e440e69..22fb3e51163f783383906d54e2dc440cefb56080 100644 (file)
@@ -11,7 +11,6 @@ import {
   MatcherPatternQuery,
   MatcherPatternPathStatic,
   MatcherPatternPathDynamic,
-  defineParamParser,
 } from './matcher-pattern'
 import { miss } from './matchers/errors'
 import { EmptyParams } from './matcher-location'
index 748a06a32a2adbd558ddde432cd3823e650c0df6..059606db29fb05e9e4f02961d8402ee59e076fae 100644 (file)
@@ -15,14 +15,10 @@ import type {
   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,
@@ -30,13 +26,14 @@ import {
   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'
@@ -47,16 +44,19 @@ import {
   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'
@@ -67,314 +67,31 @@ import {
 } 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
 }
 
 /**
@@ -1141,7 +858,7 @@ export function createRouter(options: RouterOptions): Router {
 
   // Initialization and Errors
 
-  let readyHandlers = useCallbacks<OnReadyCallback>()
+  let readyHandlers = useCallbacks<_OnReadyCallback>()
   let errorListeners = useCallbacks<_ErrorListener>()
   let ready: boolean
 
@@ -1328,31 +1045,3 @@ export function createRouter(options: RouterOptions): Router {
 
   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]
-}
index 642556452b99e07bceda3b96f13780ec3ab9a0a1..8124a9fb06c6124245409826df56c34bcfd3c0f3 100644 (file)
@@ -29,6 +29,22 @@ export type _ScrollPositionNormalized = {
   top: number
 }
 
+/**
+ * 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>
+}
+
 export interface ScrollPositionElement extends ScrollToOptions {
   /**
    * A valid CSS selector. Note some characters must be escaped in id selectors (https://mathiasbynens.be/notes/css-escapes).
index 2d443f69e58d849ecdaf1071057b499e1f34efd4..34881aca563b612706edc4ad70781508f90560f0 100644 (file)
@@ -94,3 +94,5 @@ export type _AlphaNumeric =
   | '8'
   | '9'
   | '_'
+
+export type Awaitable<T> = T | Promise<T>