{ path: '/n/:n', name: 'increment', component },
{ path: '/multiple/:a/:b', name: 'multiple', component },
{ path: '/long-:n', name: 'long', component: LongView },
+ {
+ path: '/lazy',
+ component: async () => {
+ await delay(500)
+ return component
+ },
+ },
{
path: '/with-guard/:n',
name: 'guarded',
declare module '*.vue' {
- import { Component } from 'vue'
- var component: Component
+ import { ComponentOptions } from 'vue'
+ var component: ComponentOptions
export default component
}
defineComponent,
PropType,
computed,
- Component,
InjectionKey,
Ref,
} from 'vue'
import { RouteRecordNormalized } from '../matcher/types'
import { routeKey } from '../injectKeys'
+import { RouteComponent } from '../types'
// TODO: make it work with no symbols too for IE
export const matchedRouteKey = Symbol() as InjectionKey<
provide('routerViewDepth', depth + 1)
const matchedRoute = computed(() => route.value.matched[depth])
- const ViewComponent = computed<Component | undefined>(
+ const ViewComponent = computed<RouteComponent | undefined>(
() => matchedRoute.value && matchedRoute.value.components[props.name]
)
import { InjectionKey, Ref, inject } from 'vue'
import { Router, RouteLocationNormalized } from '.'
+import { RouteLocationNormalizedResolved } from './types'
export const routerKey = ('router' as unknown) as InjectionKey<Router>
export const routeKey = ('route' as unknown) as InjectionKey<
- Ref<RouteLocationNormalized>
+ Ref<RouteLocationNormalizedResolved>
>
export function useRouter(): Router {
for (const alias of aliases) {
normalizedRecords.push({
...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
TODO,
Immutable,
MatcherLocationNormalized,
+ RouteLocationNormalizedResolved,
} from './types'
import { RouterHistory, parseURL, stringifyURL } from './history/common'
import {
interface ScrollBehavior {
(
to: RouteLocationNormalized,
- from: RouteLocationNormalized,
+ from: RouteLocationNormalizedResolved,
savedPosition: ScrollToPosition | null
): ScrollPosition | Promise<ScrollPosition>
}
export interface Router {
history: RouterHistory
- currentRoute: Ref<Immutable<RouteLocationNormalized>>
+ currentRoute: Ref<Immutable<RouteLocationNormalizedResolved>>
addRoute(parentName: string, route: RouteRecord): () => void
addRoute(route: RouteRecord): () => void
resolve(to: RouteLocation): RouteLocationNormalized
createHref(to: RouteLocationNormalized): string
- push(to: RouteLocation): Promise<RouteLocationNormalized>
- replace(to: RouteLocation): Promise<RouteLocationNormalized>
+ push(to: RouteLocation): Promise<RouteLocationNormalizedResolved>
+ replace(to: RouteLocation): Promise<RouteLocationNormalizedResolved>
beforeEach(guard: NavigationGuard): ListenerRemover
afterEach(guard: PostNavigationGuard): ListenerRemover
const beforeGuards = useCallbacks<NavigationGuard>()
const afterGuards = useCallbacks<PostNavigationGuard>()
- const currentRoute = ref<RouteLocationNormalized>(START_LOCATION_NORMALIZED)
+ const currentRoute = ref<RouteLocationNormalizedResolved>(
+ START_LOCATION_NORMALIZED
+ )
let pendingLocation: Immutable<RouteLocationNormalized> = START_LOCATION_NORMALIZED
if (isClient && 'scrollRestoration' in window.history) {
function resolve(
location: RouteLocation,
- currentLocation?: RouteLocationNormalized
+ currentLocation?: RouteLocationNormalizedResolved
): RouteLocationNormalized {
// const objectLocation = routerLocationAsObject(location)
currentLocation = currentLocation || currentRoute.value
function push(
to: RouteLocation | RouteLocationNormalized
- ): Promise<RouteLocationNormalized> {
+ ): Promise<RouteLocationNormalizedResolved> {
return pushWithRedirect(to, undefined)
}
async function pushWithRedirect(
to: RouteLocation | RouteLocationNormalized,
redirectedFrom: RouteLocationNormalized | undefined
- ): Promise<RouteLocationNormalized> {
+ ): Promise<RouteLocationNormalizedResolved> {
const toLocation: RouteLocationNormalized = (pendingLocation =
// Some functions will pass a normalized location and we don't need to resolve it again
typeof to === 'object' && 'matched' in to ? to : resolve(to))
- const from: RouteLocationNormalized = currentRoute.value
+ const from: RouteLocationNormalizedResolved = currentRoute.value
// @ts-ignore: no need to check the string as force do not exist on a string
const force: boolean | undefined = to.force
}
finalizeNavigation(
- toLocation,
+ toLocation as RouteLocationNormalizedResolved,
from,
true,
// RouteLocationNormalized will give undefined
async function navigate(
to: RouteLocationNormalized,
- from: RouteLocationNormalized
+ from: RouteLocationNormalizedResolved
): Promise<TODO> {
let guards: Lazy<any>[]
// check in components beforeRouteUpdate
guards = await extractComponentsGuards(
- to.matched.filter(record => from.matched.indexOf(record) > -1),
+ to.matched.filter(record => from.matched.indexOf(record as any) > -1),
'beforeRouteUpdate',
to,
from
guards = []
for (const record of to.matched) {
// do not trigger beforeEnter on reused views
- if (record.beforeEnter && from.matched.indexOf(record) < 0) {
+ if (record.beforeEnter && from.matched.indexOf(record as any) < 0) {
if (Array.isArray(record.beforeEnter)) {
for (const beforeEnter of record.beforeEnter)
guards.push(guardToPromiseFn(beforeEnter, to, from))
// run the queue of per route beforeEnter guards
await runGuardQueue(guards)
+ // TODO: at this point to.matched is normalized and does not contain any () => Promise<Component>
+
// check in-component beforeRouteEnter
- // TODO: is it okay to resolve all matched component or should we do it in order
guards = await extractComponentsGuards(
- to.matched.filter(record => from.matched.indexOf(record) < 0),
+ // the type does'nt matter as we are comparing an object per reference
+ to.matched.filter(record => from.matched.indexOf(record as any) < 0),
'beforeRouteEnter',
to,
from
* - Calls the scrollBehavior
*/
function finalizeNavigation(
- toLocation: RouteLocationNormalized,
- from: RouteLocationNormalized,
+ toLocation: RouteLocationNormalizedResolved,
+ from: RouteLocationNormalizedResolved,
isPush: boolean,
replace?: boolean
) {
try {
await navigate(toLocation, from)
- finalizeNavigation(toLocation, from, false)
+ finalizeNavigation(
+ // after navigation, all matched components are resolved
+ toLocation as RouteLocationNormalizedResolved,
+ from,
+ false
+ )
} catch (error) {
if (NavigationGuardRedirect.is(error)) {
// TODO: refactor the duplication of new NavigationCancelled by
// Scroll behavior
async function handleScroll(
- to: RouteLocationNormalized,
- from: RouteLocationNormalized,
+ to: RouteLocationNormalizedResolved,
+ from: RouteLocationNormalizedResolved,
scrollPosition?: ScrollToPosition
) {
if (!scrollBehavior) return
function extractChangingRecords(
to: RouteLocationNormalized,
- from: RouteLocationNormalized
+ from: RouteLocationNormalizedResolved
) {
const leavingRecords: RouteRecordNormalized[] = []
const updatingRecords: RouteRecordNormalized[] = []
}
for (const record of to.matched) {
- if (from.matched.indexOf(record) < 0) enteringRecords.push(record)
+ // the type doesn't matter because we are comparing per reference
+ if (from.matched.indexOf(record as any) < 0) enteringRecords.push(record)
}
return [leavingRecords, updatingRecords, enteringRecords]
import { LocationQuery, LocationQueryRaw } from '../utils/query'
import { PathParserOptions } from '../matcher/path-parser-ranker'
-import { markNonReactive } from 'vue'
+import { markNonReactive, ComponentOptions } from 'vue'
import { RouteRecordNormalized } from '../matcher/types'
-// type Component = ComponentOptions<Vue> | typeof Vue | AsyncComponent
-
export type Lazy<T> = () => Promise<T>
export type Override<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U
| (RouteQueryAndHash & LocationAsName & RouteLocationOptions)
| (RouteQueryAndHash & LocationAsRelative & RouteLocationOptions)
+export interface RouteLocationMatched extends RouteRecordNormalized {
+ components: Record<string, RouteComponent>
+}
+
// A matched record cannot be a redirection and must contain
+// matched contains resolved components
+export interface RouteLocationNormalizedResolved {
+ path: string
+ fullPath: string
+ query: LocationQuery
+ hash: string
+ name: string | null | undefined
+ params: RouteParams
+ matched: RouteLocationMatched[] // non-enumerable
+ redirectedFrom: RouteLocationNormalized | undefined
+ meta: Record<string | number | symbol, any>
+}
+
export interface RouteLocationNormalized {
path: string
fullPath: string
beforeRouteUpdate?: NavigationGuard<void>
}
-// TODO: have a real type with augmented properties
-// add async component
-// export type RouteComponent = (Component | ReturnType<typeof defineComponent>) & RouteComponentInterface
-export type RouteComponent = TODO
+// TODO: allow defineComponent export type RouteComponent = (Component | ReturnType<typeof defineComponent>) &
+export type RouteComponent = ComponentOptions & RouteComponentInterface
+export type RawRouteComponent = RouteComponent | Lazy<RouteComponent>
// TODO: could this be moved to matcher?
export interface RouteRecordCommon {
}
export interface RouteRecordSingleView extends RouteRecordCommon {
- component: RouteComponent
+ component: RawRouteComponent
children?: RouteRecord[]
}
export interface RouteRecordMultipleViews extends RouteRecordCommon {
- components: Record<string, RouteComponent>
+ components: Record<string, RawRouteComponent>
children?: RouteRecord[]
}
| RouteRecordMultipleViews
| RouteRecordRedirect
-export const START_LOCATION_NORMALIZED: RouteLocationNormalized = markNonReactive(
+export const START_LOCATION_NORMALIZED: RouteLocationNormalizedResolved = markNonReactive(
{
path: '/',
name: undefined,
-import { RouteLocationNormalized, RouteParams, Immutable } from '../types'
+import {
+ RouteLocationNormalized,
+ RouteParams,
+ Immutable,
+ RouteComponent,
+} from '../types'
import { guardToPromiseFn } from './guardToPromiseFn'
import { RouteRecordNormalized } from '../matcher/types'
import { LocationQueryValue } from './query'
export * from './guardToPromiseFn'
+const hasSymbol =
+ typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'
+
+function isESModule(obj: any): obj is { default: RouteComponent } {
+ return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module')
+}
+
type GuardType = 'beforeRouteEnter' | 'beforeRouteUpdate' | 'beforeRouteLeave'
+// TODO: remove async
export async function extractComponentsGuards(
matched: RouteRecordNormalized[],
guardType: GuardType,
to: RouteLocationNormalized,
from: RouteLocationNormalized
) {
+ // TODO: test to avoid redundant requests for aliases. It should work because we are holding a copy of the `components` option when we create aliases
const guards: Array<() => Promise<void>> = []
- await Promise.all(
- matched.map(async record => {
- // TODO: cache async routes per record
- for (const name in record.components) {
- const component = record.components[name]
- // TODO: handle Vue.extend views
- // if ('options' in component) throw new Error('TODO')
- const resolvedComponent = component
- // TODO: handle async component
- // const resolvedComponent = await (typeof component === 'function'
- // ? component()
- // : component)
- const guard = resolvedComponent[guardType]
- if (guard) {
- guards.push(guardToPromiseFn(guard, to, from))
- }
+ for (const record of matched) {
+ for (const name in record.components) {
+ const rawComponent = record.components[name]
+ if (typeof rawComponent === 'function') {
+ // start requesting the chunk already
+ const componentPromise = rawComponent()
+ guards.push(async () => {
+ const resolved = await componentPromise
+ const resolvedComponent = isESModule(resolved)
+ ? resolved.default
+ : resolved
+ // replace the function with the resolved component
+ record.components[name] = resolvedComponent
+ const guard = resolvedComponent[guardType]
+ return guard && guardToPromiseFn(guard, to, from)()
+ })
+ } else {
+ const guard = rawComponent[guardType]
+ guard && guards.push(guardToPromiseFn(guard, to, from))
}
- })
- )
+ }
+ }
return guards
}