]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
fix: skip initial guards with static redirect
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 30 Mar 2020 20:29:16 +0000 (22:29 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 30 Mar 2020 20:37:04 +0000 (22:37 +0200)
BREAKING CHANGE: Renamed types by removing suffix Normalized and using Raw instead
  - `RouteLocation` -> `RouteLocationRaw`
  - `RouteLocationNormalized` -> `RouteLocation`
  - `RouteLocationNormalized` is now a location that can be displayed (not a static redirect)
  - `RouteLocationNormalizedResolved` -> `RouteLocationNormalizedLoaded`
  - `RouteRecord` -> `RouteRecordRaw`
  - `RouteRecordNormalized` -> `RouteRecord`
  - `RouteRecordNormalized` is now a record that is not a static redirect

33 files changed:
__tests__/RouterLink.spec.ts
__tests__/errors.spec.ts
__tests__/extractComponentsGuards.spec.ts
__tests__/guards/component-beforeRouteEnter.spec.ts
__tests__/guards/component-beforeRouteLeave.spec.ts
__tests__/guards/component-beforeRouteUpdate.spec.ts
__tests__/guards/global-after.spec.ts
__tests__/guards/global-beforeEach.spec.ts
__tests__/guards/route-beforeEnter.spec.ts
__tests__/matcher/addingRemoving.spec.ts
__tests__/matcher/records.spec.ts
__tests__/matcher/resolve.spec.ts
__tests__/mount.ts
__tests__/router.spec.ts
__tests__/url-encoding.spec.ts
__tests__/utils.ts
circle.yml
src/components/Link.ts
src/components/View.ts
src/errors.ts
src/index.ts
src/matcher/index.ts
src/matcher/path-matcher.ts
src/matcher/types.ts
src/navigationGuards.ts
src/router.ts
src/types/index.ts
src/types/type-guards.ts
src/useApi.ts
src/utils/guardToPromiseFn.ts
src/utils/index.ts
src/utils/injectionSymbols.ts
src/utils/query.ts

index 6bd5952ee27ecb3091e73266a76b3eb198d37367..17744633dc3fbd43a9df03d246efdea3de4b8e34 100644 (file)
@@ -5,8 +5,9 @@ import { Link as RouterLink } from '../src/components/Link'
 import {
   START_LOCATION_NORMALIZED,
   RouteQueryAndHash,
-  MatcherLocation,
+  MatcherLocationRaw,
   RouteLocationNormalized,
+  RouteLocation,
 } from '../src/types'
 import { createMemoryHistory } from '../src'
 import { mount, tick } from './mount'
@@ -25,7 +26,9 @@ const records = {
 
 // fix the aliasOf
 records.homeAlias = { aliasOf: records.home } as RouteRecordNormalized
-records.parentAlias = { aliasOf: records.parent } as RouteRecordNormalized
+records.parentAlias = {
+  aliasOf: records.parent,
+} as RouteRecordNormalized
 records.childAlias = { aliasOf: records.child } as RouteRecordNormalized
 
 const locations: Record<
@@ -33,7 +36,7 @@ const locations: Record<
   {
     string: string
     normalized: RouteLocationNormalized
-    toResolve?: MatcherLocation & Required<RouteQueryAndHash>
+    toResolve?: MatcherLocationRaw & Required<RouteQueryAndHash>
   }
 > = {
   basic: {
@@ -216,7 +219,7 @@ describe('RouterLink', () => {
   function factory(
     currentLocation: RouteLocationNormalized,
     propsData: any,
-    resolvedLocation: RouteLocationNormalized,
+    resolvedLocation: RouteLocation,
     template: string = `<RouterLink :to="to">a link</RouterLink>`
   ) {
     const router = {
index 9ee94107258fa271f4eaedd48b8ff4cd1249f45b..1bfea05f325d4f3481fe24b61045af05c94e5b86 100644 (file)
@@ -1,9 +1,9 @@
 import { createRouter as newRouter, createMemoryHistory } from '../src'
 import { ErrorTypes } from '../src/errors'
 import { components, tick } from './utils'
-import { RouteRecord } from '../src/types'
+import { RouteRecordRaw } from '../src/types'
 
-const routes: RouteRecord[] = [
+const routes: RouteRecordRaw[] = [
   { path: '/', component: components.Home },
   { path: '/foo', component: components.Foo, name: 'Foo' },
   { path: '/to-foo', redirect: '/foo' },
index 824850668e2466fdf76a2bf1b35e95f60a4ff953..4089da5e7362ebbf0a47fe752018f9a3b64d1b6d 100644 (file)
@@ -1,7 +1,8 @@
 import { extractComponentsGuards } from '../src/utils'
-import { START_LOCATION_NORMALIZED, RouteRecord } from '../src/types'
+import { START_LOCATION_NORMALIZED, RouteRecordRaw } from '../src/types'
 import { components } from './utils'
 import { normalizeRouteRecord } from '../src/matcher'
+import { RouteRecordNormalized } from 'src/matcher/types'
 
 const beforeRouteEnter = jest.fn()
 
@@ -9,12 +10,12 @@ const beforeRouteEnter = jest.fn()
 const to = START_LOCATION_NORMALIZED
 const from = START_LOCATION_NORMALIZED
 
-const NoGuard: RouteRecord = { path: '/', component: components.Home }
-const SingleGuard: RouteRecord = {
+const NoGuard: RouteRecordRaw = { path: '/', component: components.Home }
+const SingleGuard: RouteRecordRaw = {
   path: '/',
   component: { ...components.Home, beforeRouteEnter },
 }
-const SingleGuardNamed: RouteRecord = {
+const SingleGuardNamed: RouteRecordRaw = {
   path: '/',
   components: {
     default: { ...components.Home, beforeRouteEnter },
@@ -30,14 +31,14 @@ beforeEach(() => {
 })
 
 async function checkGuards(
-  components: Exclude<RouteRecord, { redirect: any }>[],
+  components: Exclude<RouteRecordRaw, { redirect: any }>[],
   n: number,
   guardsLength: number = n
 ) {
   beforeRouteEnter.mockClear()
   const guards = await extractComponentsGuards(
     // type is fine as we excluded RouteRecordRedirect in components argument
-    components.map(normalizeRouteRecord),
+    components.map(normalizeRouteRecord) as RouteRecordNormalized[],
     'beforeRouteEnter',
     to,
     from
index 9dba291068437d567f9250753829f0f9ddfd079c..f9002fe60f6c2a56861ccef35c5102db2e812275 100644 (file)
@@ -1,11 +1,11 @@
 import { RouterOptions, createRouter as newRouter } from '../../src/router'
 import fakePromise from 'faked-promise'
 import { createDom, noGuard } from '../utils'
-import { RouteRecord, NavigationGuard } from '../../src/types'
+import { RouteRecordRaw, NavigationGuard } from '../../src/types'
 import { createWebHistory } from '../../src'
 
 function createRouter(
-  options: Partial<RouterOptions> & { routes: RouteRecord[] }
+  options: Partial<RouterOptions> & { routes: RouteRecordRaw[] }
 ) {
   return newRouter({
     history: createWebHistory(),
@@ -35,7 +35,7 @@ const nested = {
   nestedNestedParam: jest.fn(),
 }
 
-const routes: RouteRecord[] = [
+const routes: RouteRecordRaw[] = [
   { path: '/', component: Home },
   { path: '/foo', component: Foo },
   {
index 9a5ce6494bfca7f83e4e8a536653cfeeda09e5c5..be1e3ea24dd3b14af3c1e7334a696db6aede59cd 100644 (file)
@@ -1,11 +1,11 @@
 import { RouterOptions, createRouter as newRouter } from '../../src/router'
 import { createDom, noGuard } from '../utils'
-import { RouteRecord } from '../../src/types'
+import { RouteRecordRaw } from '../../src/types'
 import { createWebHistory } from '../../src'
 
 // TODO: refactor in utils
 function createRouter(
-  options: Partial<RouterOptions> & { routes: RouteRecord[] }
+  options: Partial<RouterOptions> & { routes: RouteRecordRaw[] }
 ) {
   return newRouter({
     history: createWebHistory(),
@@ -28,7 +28,7 @@ const nested = {
 }
 const beforeRouteLeave = jest.fn()
 
-const routes: RouteRecord[] = [
+const routes: RouteRecordRaw[] = [
   { path: '/', component: Home },
   { path: '/foo', component: Foo },
   {
index 6250b8fa2b5ecaf9679f236ae13c7f55f43377bd..517d70fe3e233580263ccae4030cb9a8684f80e8 100644 (file)
@@ -1,11 +1,11 @@
 import fakePromise from 'faked-promise'
 import { createDom, noGuard } from '../utils'
 import { createRouter as newRouter, createWebHistory } from '../../src'
-import { RouteRecord } from '../../src/types'
+import { RouteRecordRaw } from '../../src/types'
 
 function createRouter(
   options: Partial<import('../../src/router').RouterOptions> & {
-    routes: import('../../src/types').RouteRecord[]
+    routes: import('../../src/types').RouteRecordRaw[]
   }
 ) {
   return newRouter({
@@ -18,7 +18,7 @@ const Home = { template: `<div>Home</div>` }
 const Foo = { template: `<div>Foo</div>` }
 
 const beforeRouteUpdate = jest.fn()
-const routes: RouteRecord[] = [
+const routes: RouteRecordRaw[] = [
   { path: '/', component: Home },
   { path: '/foo', component: Foo },
   {
index a7e55c90ee24c07da4be7c4b48801e1989497382..f2b96db1fc1dae32db5e8e49fd6f068e13651758 100644 (file)
@@ -1,9 +1,10 @@
 import { createDom } from '../utils'
 import { createWebHistory, createRouter as newRouter } from '../../src'
+import { RouteRecordRaw } from 'src/types'
 
 function createRouter(
   options: Partial<import('../../src/router').RouterOptions> & {
-    routes: import('../../src/types').RouteRecord[]
+    routes: import('../../src/types').RouteRecordRaw[]
   }
 ) {
   return newRouter({
@@ -16,8 +17,7 @@ const Home = { template: `<div>Home</div>` }
 const Foo = { template: `<div>Foo</div>` }
 const Nested = { template: `<div>Nested<router-view/></div>` }
 
-/** @type {import('../../src/types').RouteRecord[]} */
-const routes = [
+const routes: RouteRecordRaw[] = [
   { path: '/', component: Home },
   { path: '/foo', component: Foo },
   {
index 8ab1421a7ccbdb2ce76ba098afc650f5a54b2b48..797333cbbc4f22886ec6c529c16d8e45f3b7a15b 100644 (file)
@@ -1,11 +1,11 @@
 import { RouterOptions } from '../../src/router'
 import fakePromise from 'faked-promise'
 import { createDom, tick, noGuard } from '../utils'
-import { RouteRecord, RouteLocation } from '../../src/types'
+import { RouteRecordRaw, RouteLocationRaw } from '../../src/types'
 import { createWebHistory, createRouter as newRouter } from '../../src'
 
 function createRouter(
-  options: Partial<RouterOptions> & { routes: RouteRecord[] }
+  options: Partial<RouterOptions> & { routes: RouteRecordRaw[] }
 ) {
   return newRouter({
     history: createWebHistory(),
@@ -17,7 +17,7 @@ const Home = { template: `<div>Home</div>` }
 const Foo = { template: `<div>Foo</div>` }
 const Nested = { template: `<div>Nested<router-view/></div>` }
 
-const routes: RouteRecord[] = [
+const routes: RouteRecordRaw[] = [
   { path: '/', component: Home },
   { path: '/foo', component: Foo },
   { path: '/other', component: Foo },
@@ -117,7 +117,7 @@ describe('router.beforeEach', () => {
     expect(router.currentRoute.value.fullPath).toBe('/other')
   })
 
-  async function assertRedirect(redirectFn: (i: string) => RouteLocation) {
+  async function assertRedirect(redirectFn: (i: string) => RouteLocationRaw) {
     const spy = jest.fn()
     const router = createRouter({ routes })
     await router.push('/')
index a69c8396033fbffbb9940cde3afcdf0ed21733fc..825549d012bdb18a35018cb6faf8b15a6ccfb4aa 100644 (file)
@@ -1,11 +1,11 @@
 import { RouterOptions, createRouter as newRouter } from '../../src/router'
 import fakePromise from 'faked-promise'
 import { createDom, noGuard, tick } from '../utils'
-import { RouteRecord } from '../../src/types'
+import { RouteRecordRaw } from '../../src/types'
 import { createWebHistory } from '../../src'
 
 function createRouter(
-  options: Partial<RouterOptions> & { routes: RouteRecord[] }
+  options: Partial<RouterOptions> & { routes: RouteRecordRaw[] }
 ) {
   return newRouter({
     history: createWebHistory(),
@@ -28,7 +28,7 @@ const nested = {
   nestedNestedParam: jest.fn(),
 }
 
-const routes: RouteRecord[] = [
+const routes: RouteRecordRaw[] = [
   { path: '/', component: Home },
   { path: '/home', component: Home, beforeEnter },
   { path: '/foo', component: Foo },
index de3ab8f61cad6d25e182be43664683bb876337c6..644b90149a8c75405ef35018376e17d6b0dbbbf3 100644 (file)
@@ -1,7 +1,7 @@
 import { createRouterMatcher } from '../../src/matcher'
-import { MatcherLocationNormalized } from '../../src/types'
+import { MatcherLocation } from '../../src/types'
 
-const currentLocation = { path: '/' } as MatcherLocationNormalized
+const currentLocation = { path: '/' } as MatcherLocation
 // @ts-ignore
 const component: RouteComponent = null
 
index 278452f6d8211c68a681a9edfc806c293c091a15..1446565c1b0fe29a21821595a067d273abf548dc 100644 (file)
@@ -52,16 +52,12 @@ describe('normalizeRouteRecord', () => {
     })
 
     expect(record).toEqual({
-      beforeEnter: expect.any(Function),
-      children: [],
       aliasOf: undefined,
       components: {},
-      leaveGuards: [],
-      instances: {},
       meta: { foo: true },
       name: 'name',
       path: '/redirect',
-      props: false,
+      redirect: '/home',
     })
   })
 
@@ -88,65 +84,10 @@ describe('normalizeRouteRecord', () => {
     })
   })
 
-  it('transforms a redirect record into a beforeEnter guard', () => {
-    const record = normalizeRouteRecord({
-      path: '/redirect',
-      redirect: '/home',
-    })
-    expect(record).toEqual({
-      beforeEnter: expect.any(Function),
-      children: [],
-      aliasOf: undefined,
-      components: {},
-      leaveGuards: [],
-      instances: {},
-      meta: {},
-      name: undefined,
-      path: '/redirect',
-      props: false,
-    })
-  })
-
-  it('beforeEnter is called with the string redirect', () => {
-    const record = normalizeRouteRecord({
-      path: '/redirect',
-      redirect: '/home',
-    })
+  // TODO: move to router
+  it.todo('beforeEnter is called with the string redirect')
 
-    let spy = jest.fn()
-    ;(record.beforeEnter as Function)({} as any, {} as any, spy)
-    expect(spy).toHaveBeenCalledTimes(1)
-    expect(spy).toHaveBeenCalledWith('/home')
-  })
+  it.todo('beforeEnter is called with object redirect')
 
-  it('beforeEnter is called with object redirect', () => {
-    const record = normalizeRouteRecord({
-      path: '/redirect',
-      redirect: { name: 'home' },
-    })
-
-    let spy = jest.fn()
-    ;(record.beforeEnter as Function)({} as any, {} as any, spy)
-    expect(spy).toHaveBeenCalledTimes(1)
-    expect(spy).toHaveBeenCalledWith({ name: 'home' })
-  })
-
-  it('function redirect is invoked by beforeEnter', () => {
-    const redirect = jest.fn(() => '/home')
-    const record = normalizeRouteRecord({
-      path: '/redirect',
-      redirect,
-    })
-
-    let spy = jest.fn()
-    ;(record.beforeEnter as Function)(
-      { path: '/redirect' } as any,
-      {} as any,
-      spy
-    )
-    expect(redirect).toHaveBeenCalledTimes(1)
-    expect(redirect).toHaveBeenCalledWith({ path: '/redirect' })
-    expect(spy).toHaveBeenCalledTimes(1)
-    expect(spy).toHaveBeenCalledWith('/home')
-  })
+  it.todo('function redirect is invoked by beforeEnter')
 })
index 268e4405a28d6c9db63acd73306f9cd2ee875554..fec63e269adfc3d2e924b93f01ac4ff3fdbd5c8f 100644 (file)
@@ -2,9 +2,9 @@ import { createRouterMatcher, normalizeRouteRecord } from '../../src/matcher'
 import {
   START_LOCATION_NORMALIZED,
   RouteComponent,
-  RouteRecord,
+  RouteRecordRaw,
+  MatcherLocationRaw,
   MatcherLocation,
-  MatcherLocationNormalized,
 } from '../../src/types'
 import { MatcherLocationNormalizedLoose } from '../utils'
 
@@ -17,10 +17,10 @@ const components = { default: component }
 describe('Router Matcher', () => {
   describe('resolve', () => {
     function assertRecordMatch(
-      record: RouteRecord | RouteRecord[],
-      location: MatcherLocation,
+      record: RouteRecordRaw | RouteRecordRaw[],
+      location: MatcherLocationRaw,
       resolved: Partial<MatcherLocationNormalizedLoose>,
-      start: MatcherLocationNormalized = START_LOCATION_NORMALIZED
+      start: MatcherLocation = START_LOCATION_NORMALIZED
     ) {
       record = Array.isArray(record) ? record : [record]
       const matcher = createRouterMatcher(record, {})
@@ -60,12 +60,12 @@ describe('Router Matcher', () => {
         resolved.params = resolved.params || {}
       }
 
-      const startCopy = {
+      const startCopy: MatcherLocation = {
         ...start,
         matched: start.matched.map(m => ({
           ...normalizeRouteRecord(m),
           aliasOf: m.aliasOf,
-        })),
+        })) as MatcherLocation['matched'],
       }
 
       // make matched non enumerable
@@ -83,9 +83,9 @@ describe('Router Matcher', () => {
      * @returns error
      */
     function assertErrorMatch(
-      record: RouteRecord | RouteRecord[],
-      location: MatcherLocation,
-      start: MatcherLocationNormalized = START_LOCATION_NORMALIZED
+      record: RouteRecordRaw | RouteRecordRaw[],
+      location: MatcherLocationRaw,
+      start: MatcherLocation = START_LOCATION_NORMALIZED
     ): any {
       try {
         assertRecordMatch(record, location, {}, start)
@@ -909,9 +909,7 @@ describe('Router Matcher', () => {
             name: 'nested',
             path: '/foo',
             params: {},
-            matched: [Foo, { ...Nested, path: `${Foo.path}` }].map(
-              normalizeRouteRecord
-            ),
+            matched: [Foo, { ...Nested, path: `${Foo.path}` }],
           }
         )
       })
@@ -1123,9 +1121,7 @@ describe('Router Matcher', () => {
             name: 'nested',
             path: '/nested',
             params: {},
-            matched: [Parent, { ...Nested, path: `/nested` }].map(
-              normalizeRouteRecord
-            ),
+            matched: [Parent, { ...Nested, path: `/nested` }],
           }
         )
       })
@@ -1145,9 +1141,7 @@ describe('Router Matcher', () => {
             name: 'nested',
             path: '/parent/nested',
             params: {},
-            matched: [Parent, { ...Nested, path: `/parent/nested` }].map(
-              normalizeRouteRecord
-            ),
+            matched: [Parent, { ...Nested, path: `/parent/nested` }],
           }
         )
       })
index af591679e343bb5c7f99f7cf2f88881cfe320e2b..881ad8d277a6cff74e2af3f1323f16cc1efc13cc 100644 (file)
@@ -8,7 +8,7 @@ import {
 } from 'vue'
 import * as runtimeDom from '@vue/runtime-dom'
 import { compile } from '@vue/compiler-dom'
-import { Router, RouteLocationNormalizedResolved } from '../src'
+import { Router, RouteLocationNormalizedLoaded } from '../src'
 import { routerKey, routeLocationKey } from '../src/utils/injectionSymbols'
 
 export function mount(
@@ -24,8 +24,8 @@ export function mount(
   const app = createApp(ComponentWithoutTemplate as any, rootProps)
 
   const reactiveRoute = {} as {
-    [k in keyof RouteLocationNormalizedResolved]: ComputedRef<
-      RouteLocationNormalizedResolved[k]
+    [k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
+      RouteLocationNormalizedLoaded[k]
     >
   }
   for (let key in router.currentRoute.value) {
index 6617fa6775bb0f3a866d9a95e3a3be5366bc7f8a..76593fbf44deefd5a49e0a6632af19f513c4f8b2 100644 (file)
@@ -3,12 +3,12 @@ import { createRouter, createMemoryHistory, createWebHistory } from '../src'
 import { ErrorTypes } from '../src/errors'
 import { createDom, components, tick } from './utils'
 import {
-  RouteRecord,
-  RouteLocation,
+  RouteRecordRaw,
+  RouteLocationRaw,
   START_LOCATION_NORMALIZED,
 } from '../src/types'
 
-const routes: RouteRecord[] = [
+const routes: RouteRecordRaw[] = [
   { path: '/', component: components.Home, name: 'home' },
   { path: '/home', redirect: '/' },
   {
@@ -25,6 +25,7 @@ const routes: RouteRecord[] = [
   { path: '/to-foo2', redirect: '/to-foo' },
   { path: '/p/:p', name: 'Param', component: components.Bar },
   { path: '/to-p/:p', redirect: to => `/p/${to.params.p}` },
+  { path: '/before-leave', component: components.BeforeLeave },
   {
     path: '/inc-query-hash',
     redirect: to => ({
@@ -231,7 +232,7 @@ describe('Router', () => {
 
   describe('navigation', () => {
     async function checkNavigationCancelledOnPush(
-      target?: RouteLocation | false | ((vm: any) => void)
+      target?: RouteLocationRaw | false | ((vm: any) => void)
     ) {
       const [p1, r1] = fakePromise()
       const [p2, r2] = fakePromise()
@@ -278,7 +279,7 @@ describe('Router', () => {
     })
 
     async function checkNavigationCancelledOnPopstate(
-      target?: RouteLocation | false | ((vm: any) => void)
+      target?: RouteLocationRaw | false | ((vm: any) => void)
     ) {
       const [p1, r1] = fakePromise()
       const [p2, r2] = fakePromise()
@@ -370,6 +371,20 @@ describe('Router', () => {
       })
     })
 
+    it('only triggers guards once with a redirect option', async () => {
+      const history = createMemoryHistory()
+      const router = createRouter({ history, routes })
+      const spy = jest.fn((to, from, next) => next())
+      router.beforeEach(spy)
+      await router.push('/to-foo')
+      expect(spy).toHaveBeenCalledTimes(1)
+      expect(spy).toHaveBeenCalledWith(
+        expect.objectContaining({ path: '/foo' }),
+        expect.objectContaining({ path: '/' }),
+        expect.any(Function)
+      )
+    })
+
     it('handles a double redirect from route record', async () => {
       const history = createMemoryHistory()
       const router = createRouter({ history, routes })
@@ -405,7 +420,8 @@ describe('Router', () => {
     it('can pass on query and hash when redirecting', async () => {
       const history = createMemoryHistory()
       const router = createRouter({ history, routes })
-      const loc = await router.push('/inc-query-hash?n=3#fa')
+      await router.push('/inc-query-hash?n=3#fa')
+      const loc = router.currentRoute.value
       expect(loc).toMatchObject({
         name: 'Foo',
         query: {
index c03f89ef27576dd110442e9247846f7c2a634f9e..0fe20e44b5c2708bd863263de751647b73e96b2a 100644 (file)
@@ -1,12 +1,12 @@
 import { createRouter as newRouter } from '../src/router'
 import { createDom, components } from './utils'
-import { RouteRecord } from '../src/types'
+import { RouteRecordRaw } from '../src/types'
 import { createMemoryHistory } from '../src'
 import * as encoding from '../src/utils/encoding'
 
 jest.mock('../src/utils/encoding')
 
-const routes: RouteRecord[] = [
+const routes: RouteRecordRaw[] = [
   { path: '/', name: 'home', component: components.Home },
   { path: '/%25', name: 'percent', component: components.Home },
   { path: '/to-p/:p', redirect: to => `/p/${to.params.p}` },
index 061201654b580c9afc09a59fc2dd86c56f48c1e9..04167bcfc70deea20f819f405ea149a9cde2d9c8 100644 (file)
@@ -2,9 +2,9 @@ import { JSDOM, ConstructorOptions } from 'jsdom'
 import {
   NavigationGuard,
   RouteRecordMultipleViews,
-  MatcherLocationNormalized,
+  MatcherLocation,
   RouteLocationNormalized,
-  RouteRecordCommon,
+  _RouteRecordBase,
   RouteComponent,
 } from '../src/types'
 import { h, resolveComponent, ComponentOptions } from 'vue'
@@ -28,7 +28,7 @@ export interface RouteRecordViewLoose
   > {
   leaveGuards?: any
   instances: Record<string, any>
-  props?: RouteRecordCommon['props']
+  props?: _RouteRecordBase['props']
   aliasOf: RouteRecordViewLoose | undefined
 }
 
@@ -38,7 +38,7 @@ export interface RouteLocationNormalizedLoose extends RouteLocationNormalized {
   path: string
   // record?
   params: any
-  redirectedFrom?: Partial<MatcherLocationNormalized>
+  redirectedFrom?: Partial<MatcherLocation>
   meta: any
   matched: Partial<RouteRecordViewLoose>[]
 }
@@ -48,7 +48,7 @@ export interface MatcherLocationNormalizedLoose {
   path: string
   // record?
   params: any
-  redirectedFrom?: Partial<MatcherLocationNormalized>
+  redirectedFrom?: Partial<MatcherLocation>
   meta: any
   matched: Partial<RouteRecordViewLoose>[]
   instances: Record<string, any>
@@ -125,4 +125,10 @@ export const components = {
       ])
     },
   },
+  BeforeLeave: {
+    render: () => h('div', {}, 'before leave'),
+    beforeRouteLeave(to, from, next) {
+      next()
+    },
+  } as RouteComponent,
 }
index 78404defd4d4a25e28a062dc6b48da52bfbd61f8..1be4cc8b297f8ff6599399709981c89b58d91464 100644 (file)
@@ -31,6 +31,7 @@ jobs:
       - run: yarn run test:types
       - run: yarn run test:unit --maxWorkers=2
       - run: yarn build
+      - run: yarn build:dts
 
       - run:
           name: Send code coverage
index f02c5ba5ad1e2452df90946b22b0bf17903f06ee..3aa9f5d8d1c7a23c68acac6a96073a43e92a553c 100644 (file)
@@ -7,13 +7,13 @@ import {
   reactive,
   unref,
 } from 'vue'
-import { RouteLocation, RouteLocationNormalized, VueUseOptions } from '../types'
+import { RouteLocationRaw, VueUseOptions, RouteLocation } from '../types'
 import { isSameLocationObject, isSameRouteRecord } from '../utils'
 import { routerKey } from '../utils/injectionSymbols'
-import { RouteRecordNormalized } from '../matcher/types'
+import { RouteRecord } from '../matcher/types'
 
 interface LinkProps {
-  to: RouteLocation
+  to: RouteLocationRaw
   // TODO: refactor using extra options allowed in router.push
   replace?: boolean
 }
@@ -29,7 +29,7 @@ export function useLink(props: UseLinkOptions) {
 
   const activeRecordIndex = computed<number>(() => {
     // TODO: handle children with empty path: they should relate to their parent
-    const currentMatched: RouteRecordNormalized | undefined =
+    const currentMatched: RouteRecord | undefined =
       route.value.matched[route.value.matched.length - 1]
     if (!currentMatched) return -1
     return currentRoute.value.matched.findIndex(
@@ -69,7 +69,7 @@ export const Link = defineComponent({
   name: 'RouterLink',
   props: {
     to: {
-      type: [String, Object] as PropType<RouteLocation>,
+      type: [String, Object] as PropType<RouteLocationRaw>,
       required: true,
     },
   },
@@ -119,8 +119,8 @@ function guardEvent(e: MouseEvent) {
 }
 
 function includesParams(
-  outter: RouteLocationNormalized['params'],
-  inner: RouteLocationNormalized['params']
+  outter: RouteLocation['params'],
+  inner: RouteLocation['params']
 ): boolean {
   for (let key in inner) {
     let innerValue = inner[key]
index 587fa6631ab39634af7aeb1597a07fcbdfd2bfd7..e408a32d0eb21f362bad302ffcc11c95ae010060 100644 (file)
@@ -11,11 +11,7 @@ import {
   SetupContext,
   toRefs,
 } from 'vue'
-import {
-  RouteLocationMatched,
-  VueUseOptions,
-  RouteLocationNormalizedResolved,
-} from '../types'
+import { VueUseOptions, RouteLocationNormalizedLoaded } from '../types'
 import {
   matchedRouteKey,
   viewDepthKey,
@@ -23,7 +19,7 @@ import {
 } from '../utils/injectionSymbols'
 
 interface ViewProps {
-  route: RouteLocationNormalizedResolved
+  route: RouteLocationNormalizedLoaded
   name: string
 }
 
@@ -35,7 +31,9 @@ export function useView(options: UseViewOptions) {
 
   const matchedRoute = computed(
     () =>
-      unref(options.route).matched[depth] as RouteLocationMatched | undefined
+      unref(options.route).matched[depth] as
+        | ViewProps['route']['matched'][any]
+        | undefined
   )
   const ViewComponent = computed(
     () =>
index 69d855a79347b7bd07610d71fc5b8dcdef98d3fe..f266126084987eb7729934fcc31d56bddca47798 100644 (file)
@@ -1,7 +1,7 @@
 import {
+  MatcherLocationRaw,
   MatcherLocation,
-  MatcherLocationNormalized,
-  RouteLocation,
+  RouteLocationRaw,
   RouteLocationNormalized,
 } from './types'
 
@@ -24,8 +24,8 @@ interface RouterErrorBase extends Error {
 
 export interface MatcherError extends RouterErrorBase {
   type: ErrorTypes.MATCHER_NOT_FOUND
-  location: MatcherLocation
-  currentLocation?: MatcherLocationNormalized
+  location: MatcherLocationRaw
+  currentLocation?: MatcherLocation
 }
 
 export interface NavigationError extends RouterErrorBase {
@@ -37,7 +37,7 @@ export interface NavigationError extends RouterErrorBase {
 export interface NavigationRedirectError
   extends Omit<NavigationError, 'to' | 'type'> {
   type: ErrorTypes.NAVIGATION_GUARD_REDIRECT
-  to: RouteLocation
+  to: RouteLocationRaw
 }
 
 // DEV only debug messages
@@ -87,7 +87,7 @@ export function createRouterError<E extends RouterError>(
 
 const propertiesToLog = ['params', 'query', 'hash'] as const
 
-function stringifyRoute(to: RouteLocation): string {
+function stringifyRoute(to: RouteLocationRaw): string {
   if (typeof to === 'string') return to
   if ('path' in to) return to.path
   const location = {} as Record<string, unknown>
index 34c76dfd6989ec79b989a9280e399974d0d458cb..8aefcd07e23d37746465d83db7cdac43fdd28b11 100644 (file)
@@ -12,17 +12,17 @@ export {
 
 export { RouterHistory } from './history/common'
 
-export { RouteRecordNormalized } from './matcher/types'
+export { RouteRecord, RouteRecordNormalized } from './matcher/types'
 
 export {
-  RouteLocation,
-  RouteLocationMatched,
+  RouteLocationRaw,
   RouteLocationNormalized,
-  RouteLocationNormalizedResolved,
+  RouteLocationNormalizedLoaded,
   START_LOCATION_NORMALIZED as START_LOCATION,
   RouteParams,
+  RouteLocationMatched,
   RouteLocationOptions,
-  RouteRecord,
+  RouteRecordRaw,
   NavigationGuard,
   PostNavigationGuard,
 } from './types'
index 8e3059318530a370380d3fa77d5cf44e7d324541..6fcb4ca4a9a69f71d4fc054702eb2455c97dab73 100644 (file)
@@ -1,11 +1,7 @@
-import {
-  RouteRecord,
-  MatcherLocation,
-  MatcherLocationNormalized,
-} from '../types'
+import { RouteRecordRaw, MatcherLocationRaw, MatcherLocation } from '../types'
 import { createRouterError, ErrorTypes, MatcherError } from '../errors'
 import { createRouteRecordMatcher, RouteRecordMatcher } from './path-matcher'
-import { RouteRecordNormalized } from './types'
+import { RouteRecordRedirect, RouteRecordNormalized } from './types'
 import {
   PathParams,
   comparePathParserScore,
@@ -15,23 +11,23 @@ import {
 let noop = () => {}
 
 interface RouterMatcher {
-  addRoute: (record: RouteRecord, parent?: RouteRecordMatcher) => () => void
+  addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
   removeRoute: {
     (matcher: RouteRecordMatcher): void
-    (name: Required<RouteRecord>['name']): void
+    (name: Required<RouteRecordRaw>['name']): void
   }
   getRoutes: () => RouteRecordMatcher[]
   getRecordMatcher: (
-    name: Required<RouteRecord>['name']
+    name: Required<RouteRecordRaw>['name']
   ) => RouteRecordMatcher | undefined
   resolve: (
-    location: MatcherLocation,
-    currentLocation: MatcherLocationNormalized
-  ) => MatcherLocationNormalized
+    location: MatcherLocationRaw,
+    currentLocation: MatcherLocation
+  ) => MatcherLocation
 }
 
 export function createRouterMatcher(
-  routes: RouteRecord[],
+  routes: RouteRecordRaw[],
   globalOptions: PathParserOptions
 ): RouterMatcher {
   // normalized ordered array of matchers
@@ -44,7 +40,7 @@ export function createRouterMatcher(
 
   // TODO: add routes to children of parent
   function addRoute(
-    record: Readonly<RouteRecord>,
+    record: RouteRecordRaw,
     parent?: RouteRecordMatcher,
     originalRecord?: RouteRecordMatcher
   ) {
@@ -53,7 +49,9 @@ export function createRouterMatcher(
     mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record
     const options: PathParserOptions = { ...globalOptions, ...record.options }
     // generate an array of records to correctly handle aliases
-    const normalizedRecords: RouteRecordNormalized[] = [mainNormalizedRecord]
+    const normalizedRecords: typeof mainNormalizedRecord[] = [
+      mainNormalizedRecord,
+    ]
     if ('alias' in record) {
       const aliases =
         typeof record.alias === 'string' ? [record.alias] : record.alias!
@@ -70,7 +68,9 @@ export function createRouterMatcher(
           aliasOf: originalRecord
             ? originalRecord.record
             : mainNormalizedRecord,
-        })
+          // the aliases are always of the same kind as the original since they
+          // are defined on the same record
+        } as typeof mainNormalizedRecord)
       }
     }
 
@@ -103,13 +103,16 @@ export function createRouterMatcher(
         if (originalMatcher !== matcher) originalMatcher.alias.push(matcher)
       }
 
-      let children = mainNormalizedRecord.children
-      for (let i = 0; i < children.length; i++) {
-        addRoute(
-          children[i],
-          matcher,
-          originalRecord && originalRecord.children[i]
-        )
+      // only non redirect records have children
+      if ('children' in mainNormalizedRecord) {
+        let 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
@@ -169,17 +172,18 @@ export function createRouterMatcher(
 
   /**
    * Resolves a location. Gives access to the route record that corresponds to the actual path as well as filling the corresponding params objects
-   * @param location - MatcherLocation to resolve to a url
-   * @param currentLocation - MatcherLocationNormalized of the current location
+   *
+   * @param location - MatcherLocationRaw to resolve to a url
+   * @param currentLocation - MatcherLocation of the current location
    */
   function resolve(
-    location: Readonly<MatcherLocation>,
-    currentLocation: Readonly<MatcherLocationNormalized>
-  ): MatcherLocationNormalized {
+    location: Readonly<MatcherLocationRaw>,
+    currentLocation: Readonly<MatcherLocation>
+  ): MatcherLocation {
     let matcher: RouteRecordMatcher | undefined
     let params: PathParams = {}
-    let path: MatcherLocationNormalized['path']
-    let name: MatcherLocationNormalized['name']
+    let path: MatcherLocation['path']
+    let name: MatcherLocation['name']
 
     if ('name' in location && location.name) {
       matcher = matcherMap.get(location.name)
@@ -223,11 +227,11 @@ export function createRouterMatcher(
       path = matcher.stringify(params)
     }
 
-    const matched: MatcherLocationNormalized['matched'] = []
-    let parentMatcher: RouteRecordMatcher | void = matcher
+    const matched: MatcherLocation['matched'] = []
+    let parentMatcher: RouteRecordMatcher | undefined = matcher
     while (parentMatcher) {
       // reversed order so parents are at the beginning
-      // const { record } = parentMatcher
+
       // TODO: check resolving child routes by path when parent has an alias
       matched.unshift(parentMatcher.record)
       parentMatcher = parentMatcher.parent
@@ -249,39 +253,39 @@ export function createRouterMatcher(
 }
 
 /**
- * Normalizes a RouteRecord. Transforms the `redirect` option into a `beforeEnter`
+ * Normalizes a RouteRecordRaw. Transforms the `redirect` option into a `beforeEnter`
  * @param record
  * @returns the normalized version
  */
 export function normalizeRouteRecord(
-  record: Readonly<RouteRecord>
-): RouteRecordNormalized {
-  let components: RouteRecordNormalized['components']
-  let beforeEnter: RouteRecordNormalized['beforeEnter']
-  if ('redirect' in record) {
-    components = {}
-    let { redirect } = record
-    beforeEnter = (to, from, next) => {
-      next(typeof redirect === 'function' ? redirect(to) : redirect)
-    }
-  } else {
-    components =
-      'components' in record ? record.components : { default: record.component }
-    beforeEnter = record.beforeEnter
-  }
-
-  return {
+  record: RouteRecordRaw
+): RouteRecordNormalized | RouteRecordRedirect {
+  const commonInitialValues = {
     path: record.path,
-    components,
-    // record is an object and if it has a children property, it's an array
-    children: (record as any).children || [],
     name: record.name,
-    beforeEnter,
-    props: record.props || false,
     meta: record.meta || {},
-    leaveGuards: [],
-    instances: {},
     aliasOf: undefined,
+    components: {},
+  }
+
+  if ('redirect' in record) {
+    return {
+      ...commonInitialValues,
+      redirect: record.redirect,
+    }
+  } else {
+    return {
+      ...commonInitialValues,
+      beforeEnter: record.beforeEnter,
+      props: record.props || false,
+      children: record.children || [],
+      instances: {},
+      leaveGuards: [],
+      components:
+        'components' in record
+          ? record.components
+          : { default: record.component },
+    }
   }
 }
 
index 34c5f47ac43ac28a01b4a474654dae2544207c4d..c64b2a13d3425276e201c4d28dbb98241c9c9558 100644 (file)
@@ -1,4 +1,4 @@
-import { RouteRecordNormalized } from './types'
+import { RouteRecord } from './types'
 import {
   tokensToParser,
   PathParser,
@@ -7,7 +7,7 @@ import {
 import { tokenizePath } from './path-tokenizer'
 
 export interface RouteRecordMatcher extends PathParser {
-  record: RouteRecordNormalized
+  record: RouteRecord
   parent: RouteRecordMatcher | undefined
   children: RouteRecordMatcher[]
   // aliases that must be removed when removing this record
@@ -15,12 +15,11 @@ export interface RouteRecordMatcher extends PathParser {
 }
 
 export function createRouteRecordMatcher(
-  record: Readonly<RouteRecordNormalized>,
+  record: Readonly<RouteRecord>,
   parent: RouteRecordMatcher | undefined,
   options?: PathParserOptions
 ): RouteRecordMatcher {
   const parser = tokensToParser(tokenizePath(record.path), options)
-
   const matcher: RouteRecordMatcher = {
     ...parser,
     record,
index be6f66363d3480e76267598e431443e624c0d1e1..68e1af6f1a59b77f3190466df98e2fad77c817fb 100644 (file)
@@ -1,8 +1,10 @@
 import {
   RouteRecordMultipleViews,
   NavigationGuard,
-  RouteRecordCommon,
+  _RouteRecordBase,
+  RouteRecordRedirectRaw,
 } from '../types'
+import { ComponentPublicInstance } from 'vue'
 
 // normalize component/components into components and make every property always present
 export interface RouteRecordNormalized {
@@ -11,10 +13,23 @@ export interface RouteRecordNormalized {
   components: RouteRecordMultipleViews['components']
   children: Exclude<RouteRecordMultipleViews['children'], void>
   meta: Exclude<RouteRecordMultipleViews['meta'], void>
-  props: Exclude<RouteRecordCommon['props'], void>
+  props: Exclude<_RouteRecordBase['props'], void>
   beforeEnter: RouteRecordMultipleViews['beforeEnter']
   leaveGuards: NavigationGuard<undefined>[]
-  // TODO: should be ComponentPublicInstance but breaks Immutable type
-  instances: Record<string, {} | undefined | null>
+  instances: Record<string, ComponentPublicInstance | undefined | null>
+  // can only be of of the same type as this record
   aliasOf: RouteRecordNormalized | undefined
 }
+
+export interface RouteRecordRedirect {
+  path: RouteRecordMultipleViews['path']
+  name: RouteRecordMultipleViews['name']
+  redirect: RouteRecordRedirectRaw['redirect']
+  // can only be of of the same type as this record
+  aliasOf: RouteRecordRedirect | undefined
+  meta: Exclude<RouteRecordMultipleViews['meta'], void>
+  // this object will always be empty but it simplifies things
+  components: RouteRecordMultipleViews['components']
+}
+
+export type RouteRecord = RouteRecordNormalized | RouteRecordRedirect
index 2671cb9905b180a73eed4cd7bb0f9158e4a3e86b..db7bd60a72d0d3885ee58f42c7f99e794c1c84f1 100644 (file)
@@ -1,6 +1,5 @@
 import { NavigationGuard } from './types'
 import { inject, getCurrentInstance, warn } from 'vue'
-import { RouteRecordNormalized } from './matcher/types'
 import { matchedRouteKey } from './utils/injectionSymbols'
 
 export function onBeforeRouteLeave(leaveGuard: NavigationGuard) {
@@ -11,10 +10,7 @@ export function onBeforeRouteLeave(leaveGuard: NavigationGuard) {
     return
   }
 
-  const activeRecord: RouteRecordNormalized | undefined = inject(
-    matchedRouteKey,
-    {} as any
-  ).value
+  const activeRecord = inject(matchedRouteKey, {} as any).value
 
   if (!activeRecord) {
     __DEV__ &&
index 22182c09a1a50b98028d2701db9d370ad6c1cf28..38c9865dc73fac2e83b02d7c3348edbe59f865ed 100644 (file)
@@ -1,14 +1,15 @@
 import {
   RouteLocationNormalized,
-  RouteRecord,
-  RouteLocation,
+  RouteRecordRaw,
+  RouteLocationRaw,
   NavigationGuard,
   PostNavigationGuard,
   START_LOCATION_NORMALIZED,
   Lazy,
   TODO,
-  MatcherLocationNormalized,
-  RouteLocationNormalizedResolved,
+  MatcherLocation,
+  RouteLocationNormalizedLoaded,
+  RouteLocation,
 } from './types'
 import {
   RouterHistory,
@@ -48,7 +49,7 @@ import {
   reactive,
   ComputedRef,
 } from 'vue'
-import { RouteRecordNormalized } from './matcher/types'
+import { RouteRecord, RouteRecordNormalized } from './matcher/types'
 import { Link } from './components/Link'
 import { View } from './components/View'
 import { routerKey, routeLocationKey } from './utils/injectionSymbols'
@@ -64,14 +65,14 @@ type OnReadyCallback = [() => void, (reason?: any) => void]
 interface ScrollBehavior {
   (
     to: RouteLocationNormalized,
-    from: RouteLocationNormalizedResolved,
+    from: RouteLocationNormalizedLoaded,
     savedPosition: ScrollToPosition | null
   ): ScrollPosition | Promise<ScrollPosition>
 }
 
 export interface RouterOptions {
   history: RouterHistory
-  routes: RouteRecord[]
+  routes: RouteRecordRaw[]
   scrollBehavior?: ScrollBehavior
   parseQuery?: typeof originalParseQuery
   stringifyQuery?: typeof originalStringifyQuery
@@ -80,17 +81,17 @@ export interface RouterOptions {
 
 export interface Router {
   history: RouterHistory
-  currentRoute: Ref<RouteLocationNormalizedResolved>
+  currentRoute: Ref<RouteLocationNormalizedLoaded>
 
-  addRoute(parentName: string, route: RouteRecord): () => void
-  addRoute(route: RouteRecord): () => void
+  addRoute(parentName: string, route: RouteRecordRaw): () => void
+  addRoute(route: RouteRecordRaw): () => void
   removeRoute(name: string): void
-  getRoutes(): RouteRecordNormalized[]
+  getRoutes(): RouteRecord[]
 
-  resolve(to: RouteLocation): RouteLocationNormalized
-  createHref(to: RouteLocationNormalized): string
-  push(to: RouteLocation): Promise<RouteLocationNormalizedResolved>
-  replace(to: RouteLocation): Promise<RouteLocationNormalizedResolved>
+  resolve(to: RouteLocationRaw): RouteLocation
+  createHref(to: RouteLocation): string
+  push(to: RouteLocationRaw): Promise<TODO>
+  replace(to: RouteLocationRaw): Promise<TODO>
 
   beforeEach(guard: NavigationGuard<undefined>): () => void
   afterEach(guard: PostNavigationGuard): () => void
@@ -114,25 +115,28 @@ export function createRouter({
 
   const beforeGuards = useCallbacks<NavigationGuard<undefined>>()
   const afterGuards = useCallbacks<PostNavigationGuard>()
-  const currentRoute = ref<RouteLocationNormalizedResolved>(
+  const currentRoute = ref<RouteLocationNormalizedLoaded>(
     START_LOCATION_NORMALIZED
   )
-  let pendingLocation: RouteLocationNormalized = START_LOCATION_NORMALIZED
+  let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
 
   if (isClient && 'scrollRestoration' in window.history) {
     window.history.scrollRestoration = 'manual'
   }
 
-  function createHref(to: RouteLocationNormalized): string {
+  function createHref(to: RouteLocation): string {
     return history.base + to.fullPath
   }
 
   const encodeParams = applyToParams.bind(null, encodeParam)
   const decodeParams = applyToParams.bind(null, decode)
 
-  function addRoute(parentOrRoute: string | RouteRecord, route?: RouteRecord) {
+  function addRoute(
+    parentOrRoute: string | RouteRecordRaw,
+    route?: RouteRecordRaw
+  ) {
     let parent: Parameters<typeof matcher['addRoute']>[1] | undefined
-    let record: RouteRecord
+    let record: RouteRecordRaw
     if (typeof parentOrRoute === 'string') {
       parent = matcher.getRecordMatcher(parentOrRoute)
       record = route!
@@ -153,14 +157,14 @@ export function createRouter({
     }
   }
 
-  function getRoutes(): RouteRecordNormalized[] {
+  function getRoutes() {
     return matcher.getRoutes().map(routeMatcher => routeMatcher.record)
   }
 
   function resolve(
-    location: RouteLocation,
-    currentLocation?: RouteLocationNormalizedResolved
-  ): RouteLocationNormalized {
+    location: RouteLocationRaw,
+    currentLocation?: RouteLocationNormalizedLoaded
+  ): RouteLocation {
     // const objectLocation = routerLocationAsObject(location)
     currentLocation = currentLocation || currentRoute.value
     if (typeof location === 'string') {
@@ -178,7 +182,7 @@ export function createRouter({
       }
     }
 
-    let matchedRoute: MatcherLocationNormalized = // relative or named location, path is ignored
+    let matchedRoute: MatcherLocation = // relative or named location, path is ignored
       // for same reason TS thinks location.params can be undefined
       matcher.resolve(
         'params' in location
@@ -206,23 +210,20 @@ export function createRouter({
     }
   }
 
-  function push(
-    // TODO: should not allow normalized version
-    to: RouteLocation | RouteLocationNormalized
-  ): Promise<RouteLocationNormalizedResolved> {
+  function push(to: RouteLocationRaw | RouteLocation): Promise<TODO> {
     return pushWithRedirect(to, undefined)
   }
 
-  function replace(to: RouteLocation | RouteLocationNormalized) {
+  function replace(to: RouteLocationRaw | RouteLocationNormalized) {
     const location = typeof to === 'string' ? { path: to } : to
     return push({ ...location, replace: true })
   }
 
   async function pushWithRedirect(
-    to: RouteLocation | RouteLocationNormalized,
-    redirectedFrom: RouteLocationNormalized | undefined
+    to: RouteLocationRaw | RouteLocation,
+    redirectedFrom: RouteLocation | undefined
   ): Promise<TODO> {
-    const toLocation: RouteLocationNormalized = (pendingLocation =
+    const targetLocation: RouteLocation = (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 = currentRoute.value
@@ -231,7 +232,21 @@ export function createRouter({
     const force: boolean | undefined = to.force
 
     // TODO: should we throw an error as the navigation was aborted
-    if (!force && isSameRouteLocation(from, toLocation)) return from
+    if (!force && isSameRouteLocation(from, targetLocation)) return from
+
+    const lastMatched =
+      targetLocation.matched[targetLocation.matched.length - 1]
+    if (lastMatched && 'redirect' in lastMatched) {
+      const { redirect } = lastMatched
+      return pushWithRedirect(
+        typeof redirect === 'function' ? redirect(targetLocation) : redirect,
+        // 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
 
@@ -260,11 +275,11 @@ export function createRouter({
     }
 
     finalizeNavigation(
-      toLocation as RouteLocationNormalizedResolved,
+      toLocation as RouteLocationNormalizedLoaded,
       from,
       true,
       // RouteLocationNormalized will give undefined
-      (to as RouteLocation).replace === true,
+      (to as RouteLocationRaw).replace === true,
       data
     )
 
@@ -273,7 +288,7 @@ export function createRouter({
 
   async function navigate(
     to: RouteLocationNormalized,
-    from: RouteLocationNormalizedResolved
+    from: RouteLocationNormalizedLoaded
   ): Promise<TODO> {
     let guards: Lazy<any>[]
 
@@ -355,7 +370,7 @@ export function createRouter({
     // TODO: add tests
     //  this should be done only if the navigation succeeds
     // if we redirect, it shouldn't be done because we don't know
-    for (const record of leavingRecords) {
+    for (const record of leavingRecords as RouteRecordNormalized[]) {
       // free the references
       record.instances = {}
     }
@@ -367,8 +382,8 @@ export function createRouter({
    * - Calls the scrollBehavior
    */
   function finalizeNavigation(
-    toLocation: RouteLocationNormalizedResolved,
-    from: RouteLocationNormalizedResolved,
+    toLocation: RouteLocationNormalizedLoaded,
+    from: RouteLocationNormalizedLoaded,
     isPush: boolean,
     replace?: boolean,
     data?: HistoryState
@@ -418,7 +433,8 @@ export function createRouter({
   // attach listener to history to trigger navigations
   history.listen(async (to, _from, info) => {
     // TODO: try catch to correctly log the matcher error
-    const toLocation = resolve(to.fullPath)
+    // cannot be a redirect route because it was in history
+    const toLocation = resolve(to.fullPath) as RouteLocationNormalized
     // console.log({ to, matchedRoute })
 
     pendingLocation = toLocation
@@ -428,7 +444,7 @@ export function createRouter({
       await navigate(toLocation, from)
       finalizeNavigation(
         // after navigation, all matched components are resolved
-        toLocation as RouteLocationNormalizedResolved,
+        toLocation as RouteLocationNormalizedLoaded,
         from,
         false
       )
@@ -514,8 +530,8 @@ export function createRouter({
   // Scroll behavior
 
   async function handleScroll(
-    to: RouteLocationNormalizedResolved,
-    from: RouteLocationNormalizedResolved,
+    to: RouteLocationNormalizedLoaded,
+    from: RouteLocationNormalizedLoaded,
     scrollPosition?: ScrollToPosition
   ) {
     if (!scrollBehavior) return
@@ -574,8 +590,8 @@ function applyRouterPlugin(app: App, router: Router) {
     })
 
   const reactiveRoute = {} as {
-    [k in keyof RouteLocationNormalizedResolved]: ComputedRef<
-      RouteLocationNormalizedResolved[k]
+    [k in keyof RouteLocationNormalizedLoaded]: ComputedRef<
+      RouteLocationNormalizedLoaded[k]
     >
   }
   for (let key in START_LOCATION_NORMALIZED) {
@@ -596,7 +612,7 @@ async function runGuardQueue(guards: Lazy<any>[]): Promise<void> {
 
 function extractChangingRecords(
   to: RouteLocationNormalized,
-  from: RouteLocationNormalizedResolved
+  from: RouteLocationNormalizedLoaded
 ) {
   const leavingRecords: RouteRecordNormalized[] = []
   const updatingRecords: RouteRecordNormalized[] = []
@@ -616,10 +632,7 @@ function extractChangingRecords(
   return [leavingRecords, updatingRecords, enteringRecords]
 }
 
-function isSameRouteLocation(
-  a: RouteLocationNormalized,
-  b: RouteLocationNormalized
-): boolean {
+function isSameRouteLocation(a: RouteLocation, b: RouteLocation): boolean {
   let aLastIndex = a.matched.length - 1
   let bLastIndex = b.matched.length - 1
 
index be9f9015c577340753eebbf7fc0873e323400b9d..0e0f34d39e62595b16f2f05855b4c74c1de6c659 100644 (file)
@@ -7,7 +7,7 @@ import {
   Ref,
   ComputedRef,
 } from 'vue'
-import { RouteRecordNormalized } from '../matcher/types'
+import { RouteRecord, RouteRecordNormalized } from '../matcher/types'
 import { HistoryState } from '../history/common'
 
 export type Lazy<T> = () => Promise<T>
@@ -66,43 +66,72 @@ export interface RouteLocationOptions {
   state?: HistoryState
 }
 
-// User level location
-export type RouteLocation =
+/**
+ * User-level route location
+ */
+export type RouteLocationRaw =
   | string
   | (RouteQueryAndHash & LocationAsPath & RouteLocationOptions)
   | (RouteQueryAndHash & LocationAsName & RouteLocationOptions)
   | (RouteQueryAndHash & LocationAsRelative & RouteLocationOptions)
 
 export interface RouteLocationMatched extends RouteRecordNormalized {
+  // components cannot be Lazy<RouteComponent>
   components: Record<string, RouteComponent>
 }
 
-// A matched record cannot be a redirection and must contain
-
-// matched contains resolved components
-export interface RouteLocationNormalizedResolved {
+/**
+ * Base properties for a normalized route location
+ *
+ * @internal
+ */
+export interface _RouteLocationBase {
   path: string
   fullPath: string
   query: LocationQuery
   hash: string
   name: string | null | undefined
   params: RouteParams
-  matched: RouteLocationMatched[] // non-enumerable
-  redirectedFrom: RouteLocationNormalized | undefined
+  // TODO: make it an array?
+  redirectedFrom: RouteLocation | undefined
   meta: Record<string | number | symbol, any>
 }
 
-export interface RouteLocationNormalized {
-  path: string
-  fullPath: string
-  query: LocationQuery
-  hash: string
-  name: string | null | undefined
-  params: RouteParams
+// matched contains resolved components
+/**
+ * {@link RouteLocationRaw} with
+ */
+export interface RouteLocationNormalizedLoaded extends _RouteLocationBase {
+  /**
+   * Array of {@link RouteLocationMatched} containing only plain components (any
+   * lazy-loaded components have been loaded and were replaced inside of the
+   * `components` object) so it can be directly used to display routes. It
+   * cannot contain redirect records either
+   */
+  matched: RouteLocationMatched[] // non-enumerable
+}
+
+/**
+ * {@link RouteLocationRaw} resolved using the matcher
+ */
+export interface RouteLocation extends _RouteLocationBase {
+  /**
+   * Array of {@link RouteRecord} containing components as they were
+   * passed when adding records. It can also contain redirect records. This
+   * can't be used directly
+   */
+  matched: RouteRecord[] // non-enumerable
+}
+
+/**
+ * Similar to {@link RouteLocation} but its
+ * {@link RouteLocationNormalized.matched} cannot contain redirect records
+ */
+export interface RouteLocationNormalized extends _RouteLocationBase {
+  /**
+   * Array of {@link RouteRecordNormalized}
+   */
   matched: RouteRecordNormalized[] // non-enumerable
-  // TODO: make it an array?
-  redirectedFrom: RouteLocationNormalized | undefined
-  meta: Record<string | number | symbol, any>
 }
 
 // interface PropsTransformer {
@@ -110,7 +139,7 @@ export interface RouteLocationNormalized {
 // }
 
 // export interface RouterLocation<PT extends PropsTransformer> {
-//   record: RouteRecord<PT>
+//   record: RouteRecordRaw<PT>
 //   path: string
 //   params: ReturnType<PT>
 // }
@@ -122,8 +151,8 @@ export interface RouteComponentInterface {
   /**
    * Guard called when the router is navigating away from the current route
    * that is rendering this component.
-   * @param to - RouteLocation we are navigating to
-   * @param from - RouteLocation we are navigating from
+   * @param to - RouteLocationRaw we are navigating to
+   * @param from - RouteLocationRaw we are navigating from
    * @param next - function to validate, cancel or modify (by redirectering) the navigation
    */
   beforeRouteLeave?: NavigationGuard
@@ -131,8 +160,8 @@ export interface RouteComponentInterface {
    * Guard called whenever the route that renders this component has changed but
    * it is reused for the new route. This allows you to guard for changes in params,
    * the query or the hash.
-   * @param to - RouteLocation we are navigating to
-   * @param from - RouteLocation we are navigating from
+   * @param to - RouteLocationRaw we are navigating to
+   * @param from - RouteLocationRaw we are navigating from
    * @param next - function to validate, cancel or modify (by redirectering) the navigation
    */
   beforeRouteUpdate?: NavigationGuard
@@ -143,16 +172,37 @@ export type RouteComponent = ComponentOptions & RouteComponentInterface
 export type RawRouteComponent = RouteComponent | Lazy<RouteComponent>
 
 // TODO: could this be moved to matcher?
-export interface RouteRecordCommon {
+/**
+ * Common properties among all kind of {@link RouteRecordRaw}
+ */
+export interface _RouteRecordBase {
+  /**
+   * Path of the record. Should start with `/` unless the record is the child of
+   * another record.
+   */
   path: string
+  /**
+   * Aliases for the record. Allows defining extra paths that will behave like a
+   * copy of the record. Allows having paths shorthands like `/users/:id` and
+   * `/u/:id`. All `alias` and `path` values must share the same params.
+   */
   alias?: string | string[]
+  /**
+   * Name for the route record.
+   */
   name?: string
+  /**
+   * Allow passing down params as props to the component rendered by `router-view`.
+   */
   props?:
     | boolean
     | Record<string, any>
     | ((to: RouteLocationNormalized) => Record<string, any>)
   // TODO: beforeEnter has no effect with redirect, move and test
   beforeEnter?: NavigationGuard<undefined> | NavigationGuard<undefined>[]
+  /**
+   * Arbitraty data attached to the record.
+   */
   meta?: Record<string | number | symbol, any>
   // TODO: only allow a subset?
   // TODO: RFC: remove this and only allow global options
@@ -160,31 +210,31 @@ export interface RouteRecordCommon {
 }
 
 export type RouteRecordRedirectOption =
-  | RouteLocation
-  | ((to: RouteLocationNormalized) => RouteLocation)
-export interface RouteRecordRedirect extends RouteRecordCommon {
+  | RouteLocationRaw
+  | ((to: RouteLocation) => RouteLocationRaw)
+export interface RouteRecordRedirectRaw extends _RouteRecordBase {
   redirect: RouteRecordRedirectOption
   beforeEnter?: never
   component?: never
   components?: never
 }
 
-export interface RouteRecordSingleView extends RouteRecordCommon {
+export interface RouteRecordSingleView extends _RouteRecordBase {
   component: RawRouteComponent
-  children?: RouteRecord[]
+  children?: RouteRecordRaw[]
 }
 
-export interface RouteRecordMultipleViews extends RouteRecordCommon {
+export interface RouteRecordMultipleViews extends _RouteRecordBase {
   components: Record<string, RawRouteComponent>
-  children?: RouteRecord[]
+  children?: RouteRecordRaw[]
 }
 
-export type RouteRecord =
+export type RouteRecordRaw =
   | RouteRecordSingleView
   | RouteRecordMultipleViews
-  | RouteRecordRedirect
+  | RouteRecordRedirectRaw
 
-export const START_LOCATION_NORMALIZED: RouteLocationNormalizedResolved = markNonReactive(
+export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = markNonReactive(
   {
     path: '/',
     name: undefined,
@@ -206,32 +256,23 @@ export const START_LOCATION_NORMALIZED: RouteLocationNormalizedResolved = markNo
 
 // Matcher types
 // the matcher doesn't care about query and hash
-export type MatcherLocation =
+export type MatcherLocationRaw =
   | LocationAsPath
   | LocationAsName
   | LocationAsRelative
 
-// TODO: should probably be the other way around: RouteLocationNormalized extending from MatcherLocationNormalized
-export interface MatcherLocationNormalized
+// TODO: should probably be the other way around: RouteLocationNormalized extending from MatcherLocation
+export interface MatcherLocation
   extends Pick<
-    RouteLocationNormalized,
+    RouteLocation,
     'name' | 'path' | 'params' | 'matched' | 'meta'
   > {}
 
-// used when the route records requires a redirection
-// with a function call. The matcher isn't able to do it
-// by itself, so it dispatches the information so the router
-// can pick it up
-export interface MatcherLocationRedirect {
-  redirect: RouteRecordRedirectOption
-  normalizedLocation: MatcherLocationNormalized
-}
-
 // TODO: remove any to type vm and use a generic that comes from the component
 // where the navigation guard callback is defined
 export interface NavigationGuardCallback {
   (): void
-  (location: RouteLocation): void
+  (location: RouteLocationRaw): void
   (valid: boolean): void
   (cb: (vm: any) => void): void
 }
index 964248d6b614da85b25dd7e3af3f2bbd010e89c9..a00d4dbfb910a6a481b415f0df562de37429bb2b 100644 (file)
@@ -1,5 +1,5 @@
-import { RouteLocation } from './index'
+import { RouteLocationRaw } from './index'
 
-export function isRouteLocation(route: any): route is RouteLocation {
+export function isRouteLocation(route: any): route is RouteLocationRaw {
   return typeof route === 'string' || (route && typeof route === 'object')
 }
index 6e3e541c601cb1446a9f9c0d00aa5577697dd551..8d4b90d4682edda9d6d16982a153dd5dd77d75b6 100644 (file)
@@ -1,12 +1,12 @@
 import { inject } from 'vue'
 import { routerKey, routeLocationKey } from './utils/injectionSymbols'
 import { Router } from './router'
-import { RouteLocationNormalizedResolved } from './types'
+import { RouteLocationNormalizedLoaded } from './types'
 
 export function useRouter(): Router {
   return inject(routerKey)!
 }
 
-export function useRoute(): RouteLocationNormalizedResolved {
+export function useRoute(): RouteLocationNormalizedLoaded {
   return inject(routeLocationKey)!
 }
index 0d20bb8d67595729d9c7928384075f349328c630..9e31269a6a4462c16a2beb06c65156c9b6042acf 100644 (file)
@@ -2,12 +2,12 @@ import {
   NavigationGuard,
   RouteLocationNormalized,
   NavigationGuardCallback,
-  RouteLocation,
-  RouteLocationNormalizedResolved,
+  RouteLocationRaw,
+  RouteLocationNormalizedLoaded,
   NavigationGuardNextCallback,
+  isRouteLocation,
 } from '../types'
 
-import { isRouteLocation } from '../types'
 import {
   createRouterError,
   ErrorTypes,
@@ -19,7 +19,7 @@ import { ComponentPublicInstance } from 'vue'
 export function guardToPromiseFn(
   guard: NavigationGuard<undefined>,
   to: RouteLocationNormalized,
-  from: RouteLocationNormalizedResolved,
+  from: RouteLocationNormalizedLoaded,
   instance?: undefined
 ): () => Promise<void>
 export function guardToPromiseFn<
@@ -27,13 +27,13 @@ export function guardToPromiseFn<
 >(
   guard: NavigationGuard<ThisType>,
   to: RouteLocationNormalized,
-  from: RouteLocationNormalizedResolved,
+  from: RouteLocationNormalizedLoaded,
   instance: ThisType
 ): () => Promise<void> {
   return () =>
     new Promise((resolve, reject) => {
       const next: NavigationGuardCallback = (
-        valid?: boolean | RouteLocation | NavigationGuardNextCallback
+        valid?: boolean | RouteLocationRaw | NavigationGuardNextCallback
       ) => {
         if (valid === false)
           reject(
index 30633c297aca4a92b16702cce43b042452e52e62..b34cb1a3036269d37b687be1a6e5ca6e06a8c828 100644 (file)
@@ -2,10 +2,10 @@ import {
   RouteLocationNormalized,
   RouteParams,
   RouteComponent,
-  RouteLocationNormalizedResolved,
+  RouteLocationNormalizedLoaded,
 } from '../types'
 import { guardToPromiseFn } from './guardToPromiseFn'
-import { RouteRecordNormalized } from '../matcher/types'
+import { RouteRecord, RouteRecordNormalized } from '../matcher/types'
 import { LocationQueryValue } from './query'
 import { hasSymbol } from './injectionSymbols'
 
@@ -21,7 +21,7 @@ export function extractComponentsGuards(
   matched: RouteRecordNormalized[],
   guardType: GuardType,
   to: RouteLocationNormalized,
-  from: RouteLocationNormalizedResolved
+  from: RouteLocationNormalizedLoaded
 ) {
   const guards: Array<() => Promise<void>> = []
 
@@ -71,10 +71,7 @@ export function applyToParams(
   return newParams
 }
 
-export function isSameRouteRecord(
-  a: RouteRecordNormalized,
-  b: RouteRecordNormalized
-): boolean {
+export function isSameRouteRecord(a: RouteRecord, b: RouteRecord): boolean {
   // since the original record has an undefined value for aliasOf
   // but all aliases point to the original record, this will always compare
   // the original record
index 184e87d37f806056fbf725e11ddb341f4649d359..7247804ce342f604fe7b3beadd622f4bf60452be 100644 (file)
@@ -1,6 +1,7 @@
 import { InjectionKey, ComputedRef } from 'vue'
-import { RouteLocationMatched, RouteLocationNormalizedResolved } from '../types'
+import { RouteLocationNormalizedLoaded } from '../types'
 import { Router } from '../router'
+import { RouteRecordNormalized } from '../matcher/types'
 
 export const hasSymbol =
   typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'
@@ -11,7 +12,7 @@ export const PolySymbol = (name: string) =>
 
 // rvlm = Router View Location Matched
 export const matchedRouteKey = PolySymbol('rvlm') as InjectionKey<
-  ComputedRef<RouteLocationMatched | undefined>
+  ComputedRef<RouteRecordNormalized | undefined>
 >
 // rvd = Router View Depth
 export const viewDepthKey = PolySymbol('rvd') as InjectionKey<number>
@@ -20,5 +21,5 @@ export const viewDepthKey = PolySymbol('rvd') as InjectionKey<number>
 export const routerKey = PolySymbol('r') as InjectionKey<Router>
 // rt = route location
 export const routeLocationKey = PolySymbol('rl') as InjectionKey<
-  RouteLocationNormalizedResolved
+  RouteLocationNormalizedLoaded
 >
index 81dcf5deb27bf9d46a3b7af60acb8076cc8b4b60..caee46d68571b33df1633aedb83883b491fea740 100644 (file)
@@ -24,7 +24,7 @@ export type LocationQuery = Record<
 /**
  * Loose {@link LocationQuery} object that can be passed to functions like
  * {@link Router.push} and {@link Router.replace} or anywhere when creating a
- * {@link RouteLocation}
+ * {@link RouteLocationRaw}
  *
  * @public
  */