]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(matcher): handle redirect in rout records
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 3 May 2019 19:43:14 +0000 (21:43 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Fri, 3 May 2019 19:43:14 +0000 (21:43 +0200)
__tests__/matcher.spec.js
__tests__/router.spec.js
src/matcher.ts
src/router.ts
src/types/index.ts

index 8e8521f74c4040849ea8c9a62318c1872e381552..4a21698ec33ff6ad9516e24403b44a9bc6de2f53 100644 (file)
@@ -6,14 +6,19 @@ const { START_LOCATION_NORMALIZED } = require('../src/types')
 
 const component = null
 
+/** @typedef {import('../src/types').RouteRecord} RouteRecord */
+/** @typedef {import('../src/types').MatcherLocation} MatcherLocation */
+/** @typedef {import('../src/types').MatcherLocationRedirect} MatcherLocationRedirect */
+/** @typedef {import('../src/types').MatcherLocationNormalized} MatcherLocationNormalized */
+
 describe('Router Matcher', () => {
   describe('resolve', () => {
     /**
      *
-     * @param {import('../src/types').RouteRecord} record
-     * @param {import('../src/types').MatcherLocation} location
-     * @param {Partial<import('../src/types').MatcherLocationNormalized>} resolved
-     * @param {import('../src/types').MatcherLocationNormalized} start
+     * @param {RouteRecord | RouteRecord[]} record Record or records we are testing the matcher against
+     * @param {MatcherLocation} location location we want to reolve against
+     * @param {Partial<MatcherLocationNormalized>} resolved Expected resolved location given by the matcher
+     * @param {MatcherLocationNormalized} [start] Optional currentLocation used when resolving
      */
     function assertRecordMatch(
       record,
@@ -21,7 +26,8 @@ describe('Router Matcher', () => {
       resolved,
       start = START_LOCATION_NORMALIZED
     ) {
-      const matcher = new RouterMatcher([record])
+      record = Array.isArray(record) ? record : [record]
+      const matcher = new RouterMatcher(record)
       const targetLocation = {}
 
       // add location if provided as it should be the same value
@@ -29,8 +35,12 @@ describe('Router Matcher', () => {
         resolved.path = location.path
       }
 
-      // use one single record
-      if (!('matched' in resolved)) resolved.matched = [record]
+      if ('redirect' in record) {
+      } else {
+        // use one single record
+        // @ts-ignore
+        if (!('matched' in resolved)) resolved.matched = record
+      }
 
       // allows not passing params
       if ('params' in location) {
@@ -52,9 +62,9 @@ describe('Router Matcher', () => {
 
     /**
      *
-     * @param {import('../src/types').RouteRecord} record
-     * @param {import('../src/types').MatcherLocation} location
-     * @param {import('../src/types').MatcherLocationNormalized} start
+     * @param {RouteRecord | RouteRecord[]} record Record or records we are testing the matcher against
+     * @param {MatcherLocation} location location we want to reolve against
+     * @param {MatcherLocationNormalized} [start] Optional currentLocation used when resolving
      * @returns {any} error
      */
     function assertErrorMatch(
@@ -232,9 +242,164 @@ describe('Router Matcher', () => {
         )
       })
 
+      describe('redirects', () => {
+        /**
+         *
+         * @param {RouteRecord[]} records Record or records we are testing the matcher against
+         * @param {MatcherLocation} location location we want to reolve against
+         * @param {MatcherLocationNormalized | MatcherLocationRedirect} expected Expected resolved location given by the matcher
+         * @param {MatcherLocationNormalized} [currentLocation] Optional currentLocation used when resolving
+         */
+        function assertRedirect(
+          records,
+          location,
+          expected,
+          currentLocation = START_LOCATION_NORMALIZED
+        ) {
+          const matcher = new RouterMatcher(records)
+          const resolved = matcher.resolve(location, currentLocation)
+          expect(resolved).toEqual(expected)
+          return resolved
+        }
+
+        // FIXME: refactor the tests into the function, probably use a common set of routes
+        // tests named routes and relatives routes
+        // move to different folder
+
+        it('resolves a redirect string', () => {
+          const records = [
+            { path: '/home', component },
+            { path: '/redirect', redirect: '/home' },
+          ]
+          assertRedirect(
+            records,
+            {
+              name: undefined,
+              path: '/redirect',
+            },
+            {
+              redirect: '/home',
+              normalizedLocation: {
+                path: '/redirect',
+                params: {},
+                name: undefined,
+                matched: [],
+              },
+            }
+          )
+        })
+
+        it('resolves a redirect function that returns a string', () => {
+          const redirect = () => '/home'
+          const records = [
+            { path: '/home', component },
+            { path: '/redirect', redirect },
+          ]
+          assertRedirect(
+            records,
+            {
+              name: undefined,
+              path: '/redirect',
+            },
+            {
+              redirect,
+              normalizedLocation: {
+                path: '/redirect',
+                params: {},
+                name: undefined,
+                matched: [],
+              },
+            }
+          )
+        })
+
+        it('resolves a redirect function that returns an object route', () => {
+          const redirect = () => {
+            path: '/home'
+          }
+          const records = [
+            { path: '/home', component },
+            { path: '/redirect', redirect },
+          ]
+          assertRedirect(
+            records,
+            {
+              name: undefined,
+              path: '/redirect',
+            },
+            {
+              redirect,
+              normalizedLocation: {
+                path: '/redirect',
+                params: {},
+                name: undefined,
+                matched: [],
+              },
+            }
+          )
+        })
+
+        it('resolves a redirect as an object', () => {
+          const records = [
+            { path: '/home', component },
+            { path: '/redirect', redirect: { path: 'home' } },
+          ]
+          assertRedirect(
+            records,
+            {
+              name: undefined,
+              path: '/redirect',
+            },
+            {
+              redirect: { path: 'home' },
+              normalizedLocation: {
+                path: '/redirect',
+                params: {},
+                name: undefined,
+                matched: [],
+              },
+            }
+          )
+        })
+
+        it('normalize a location when redirecting', () => {
+          const redirect = to => ({ name: 'b', params: to.params })
+          const records = [
+            { path: '/home', component },
+            {
+              path: '/a/:a',
+              name: 'a',
+              redirect,
+            },
+            { path: '/b/:a', name: 'b', component },
+          ]
+          assertRedirect(
+            records,
+            {
+              name: undefined,
+              path: '/a/foo',
+            },
+            {
+              redirect,
+              normalizedLocation: {
+                path: '/a/foo',
+                params: { a: 'foo' },
+                name: 'a',
+                matched: [],
+              },
+            }
+          )
+        })
+      })
+
       it('throws if the current named route does not exists', () => {
         const record = { path: '/', component }
-        const start = { name: 'home', params: {}, path: '/', matched: [record] }
+        const start = {
+          name: 'home',
+          params: {},
+          path: '/',
+          matched: [record],
+        }
         // the property should be non enumerable
         Object.defineProperty(start, 'matched', { enumerable: false })
         expect(assertErrorMatch(record, {}, start)).toMatchInlineSnapshot(
index b43682daef47846d38d4efd81d4560ff6ea83c78..888c28720931d3b0577928c28fbe30cace7ef02d 100644 (file)
@@ -10,9 +10,11 @@ function mockHistory() {
   return new HTML5History()
 }
 
+/** @type {import('../src/types').RouteRecord[]} */
 const routes = [
   { path: '/', component: components.Home },
   { path: '/foo', component: components.Foo },
+  { path: '/to-foo', redirect: '/foo' },
 ]
 
 describe('Router', () => {
@@ -59,4 +61,6 @@ describe('Router', () => {
       hash: '',
     })
   })
+
+  // it('redirects with route record redirect')
 })
index 1ff6ed464b1ce5a29884aa1e58f4076b1f3c9eb8..3385f53a5526d34393a7c10fd3f82749bc2c48cc 100644 (file)
@@ -4,10 +4,10 @@ import {
   RouteParams,
   MatcherLocation,
   MatcherLocationNormalized,
+  MatcherLocationRedirect,
 } from './types/index'
 import { NoRouteMatchError } from './errors'
 
-// TODO: rename
 interface RouteMatcher {
   re: RegExp
   resolve: (params?: RouteParams) => string
@@ -46,8 +46,7 @@ export class RouterMatcher {
   resolve(
     location: Readonly<MatcherLocation>,
     currentLocation: Readonly<MatcherLocationNormalized>
-    // TODO: return type is wrong, should contain fullPath and record/matched
-  ): MatcherLocationNormalized {
+  ): MatcherLocationNormalized | MatcherLocationRedirect {
     let matcher: RouteMatcher | void
     // TODO: refactor with type guards
 
@@ -56,11 +55,6 @@ export class RouterMatcher {
       matcher = this.matchers.find(m => m.re.test(location.path))
 
       if (!matcher) throw new NoRouteMatchError(currentLocation, location)
-      // TODO: build up the array with children based on current location
-
-      if ('redirect' in matcher.record) throw new Error('TODO')
-
-      const matched = [matcher.record]
 
       const params: RouteParams = {}
       const result = matcher.re.exec(location.path)
@@ -81,6 +75,28 @@ export class RouterMatcher {
         params[key] = value
       }
 
+      if ('redirect' in matcher.record) {
+        const { redirect } = matcher.record
+        return {
+          redirect,
+          normalizedLocation: {
+            name: matcher.record.name,
+            path: location.path,
+            matched: [],
+            params,
+          },
+        }
+        // if redirect is a function we do not have enough information, so we throw
+        // TODO: not use a throw
+        // throw new RedirectInRecord(typeof redirect === 'function' ? {
+        //   redirect,
+        //   route: { name: matcher.record.name, path: location.path, params, matched: [] }
+        // } : redirect)
+      }
+
+      // TODO: build up the array with children based on current location
+      const matched = [matcher.record]
+
       return {
         name: matcher.record.name,
         /// no need to resolve the path with the matcher as it was provided
index ae3e1c0dd501cf544bd82278d3f6438ace11a9d8..91e8e15d31b95da10025a8b329a3b5781e9fcdc6 100644 (file)
@@ -11,6 +11,7 @@ import {
   TODO,
   PostNavigationGuard,
   Lazy,
+  MatcherLocation,
 } from './types/index'
 
 import { guardToPromiseFn, extractComponentsGuards } from './utils'
@@ -36,7 +37,7 @@ export class Router {
 
     this.history.listen((to, from, info) => {
       // TODO: check navigation guards
-      const matchedRoute = this.matcher.resolve(to, this.currentRoute)
+      const matchedRoute = this.matchLocation(to, this.currentRoute)
       // console.log({ to, matchedRoute })
       // TODO: navigate
 
@@ -47,6 +48,43 @@ export class Router {
     })
   }
 
+  private matchLocation(
+    location: MatcherLocation,
+    currentLocation: MatcherLocationNormalized
+  ): MatcherLocationNormalized {
+    const matchedRoute = this.matcher.resolve(location, currentLocation)
+    if ('redirect' in matchedRoute) {
+      const { redirect, normalizedLocation } = matchedRoute
+      // TODO: add from to a redirect stack?
+      if (typeof redirect === 'string') {
+        // match the redirect instead
+        return this.matchLocation(
+          this.history.utils.normalizeLocation(redirect),
+          currentLocation
+        )
+      } else if (typeof redirect === 'function') {
+        const url = this.history.utils.normalizeLocation(normalizedLocation)
+        const newLocation = redirect({
+          ...normalizedLocation,
+          ...url,
+        })
+
+        if (typeof newLocation === 'string') {
+          return this.matchLocation(
+            this.history.utils.normalizeLocation(newLocation),
+            currentLocation
+          )
+        }
+
+        return this.matchLocation(newLocation, currentLocation)
+      } else {
+        return this.matchLocation(redirect, currentLocation)
+      }
+    } else {
+      return matchedRoute
+    }
+  }
+
   /**
    * Trigger a navigation, adding an entry to the history stack. Also apply all navigation
    * guards first
@@ -58,11 +96,11 @@ export class Router {
     if (typeof to === 'string' || 'path' in to) {
       url = this.history.utils.normalizeLocation(to)
       // TODO: should allow a non matching url to allow dynamic routing to work
-      location = this.matcher.resolve(url, this.currentRoute)
+      location = this.matchLocation(url, this.currentRoute)
     } else {
       // named or relative route
       // we need to resolve first
-      location = this.matcher.resolve(to, this.currentRoute)
+      location = this.matchLocation(to, this.currentRoute)
       // intentionally drop current query and hash
       url = this.history.utils.normalizeLocation({
         query: to.query ? this.history.utils.normalizeQuery(to.query) : {},
index 2c09ecfaa50d70ca0b7e45c36c9d7614fe371100..593530ebcbfa9afbbd0c930f433a399f6000a8b8 100644 (file)
@@ -100,9 +100,11 @@ interface RouteRecordCommon {
   beforeEnter?: NavigationGuard
 }
 
-type DynamicRedirect = (to: RouteLocationNormalized) => RouteLocation
+export type RouteRecordRedirectOption =
+  | RouteLocation
+  | ((to: RouteLocationNormalized) => RouteLocation)
 interface RouteRecordRedirect extends RouteRecordCommon {
-  redirect: RouteLocation | DynamicRedirect
+  redirect: RouteRecordRedirectOption
 }
 
 interface RouteRecordSingleView extends RouteRecordCommon {
@@ -155,6 +157,15 @@ export interface MatcherLocationNormalized {
   matched: MatchedRouteRecord[]
 }
 
+// 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
+}
+
 export interface NavigationGuardCallback {
   (): void
   (location: RouteLocation): void