]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(matcher): allow alias
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 17 Oct 2019 14:23:55 +0000 (16:23 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 17 Oct 2019 14:23:55 +0000 (16:23 +0200)
__tests__/matcher.spec.ts
explorations/html5.ts
src/matcher.ts
src/types/index.ts

index 734a08cba74f4b5e0e54f65898163065fe4b8163..0c45d3674bda0255b0c1c744e09560c036be8365 100644 (file)
@@ -25,14 +25,13 @@ describe('Router Matcher', () => {
     ) {
       record = Array.isArray(record) ? record : [record]
       const matcher = new RouterMatcher(record)
-      const targetLocation = {}
 
       if (!('meta' in resolved)) {
         resolved.meta = record[0].meta || {}
       }
 
       // add location if provided as it should be the same value
-      if ('path' in location) {
+      if ('path' in location && !('path' in resolved)) {
         resolved.path = location.path
       }
 
@@ -61,14 +60,7 @@ describe('Router Matcher', () => {
       // make matched non enumerable
       Object.defineProperty(startCopy, 'matched', { enumerable: false })
 
-      const result = matcher.resolve(
-        {
-          ...targetLocation,
-          // override anything provided in location
-          ...location,
-        },
-        startCopy
-      )
+      const result = matcher.resolve(location, startCopy)
       expect(result).toEqual(resolved)
     }
 
@@ -91,6 +83,35 @@ describe('Router Matcher', () => {
       }
     }
 
+    describe('alias', () => {
+      it('resolves an alias', () => {
+        assertRecordMatch(
+          {
+            path: '/',
+            alias: '/home',
+            name: 'Home',
+            components,
+            meta: { foo: true },
+          },
+          { path: '/home' },
+          {
+            name: 'Home',
+            path: '/home',
+            params: {},
+            meta: { foo: true },
+            matched: [
+              {
+                path: '/home',
+                name: 'Home',
+                components,
+                meta: { foo: true },
+              },
+            ],
+          }
+        )
+      })
+    })
+
     describe('LocationAsPath', () => {
       it('resolves a normal path', () => {
         assertRecordMatch(
index 58e70a7f12d3d19602dcfc557ea8bb91ca9bd000..086ec8aa8da8343e5720902e63cbcf85e8aae5f5 100644 (file)
@@ -113,7 +113,7 @@ const scrollWaiter = new ScrollQueue()
 const router = new Router({
   history: routerHistory,
   routes: [
-    { path: '/', component: Home, name: 'home' },
+    { path: '/', component: Home, name: 'home', alias: '/home' },
     { path: '/users/:id', name: 'user', component: User },
     { path: '/documents/:id', name: 'docs', component: User },
     { path: encodeURI('/n/€'), name: 'euro', component },
index 387bcd41ea43cc95aaa9c696974f6676252dbe78..1ed75330506a9630819c1dd79986d6b8ba0ea815 100644 (file)
@@ -5,12 +5,19 @@ import {
   MatcherLocation,
   MatcherLocationNormalized,
   MatcherLocationRedirect,
+  RouteRecordRedirect,
+  RouteRecordMultipleViews,
+  RouteRecordSingleView,
+  Mutable,
   // TODO: add it to matched
   // MatchedRouteRecord,
 } from './types/index'
 import { NoRouteMatchError, InvalidRouteMatch } from './errors'
 
-type NormalizedRouteRecord = Exclude<RouteRecord, { component: any }> // normalize component/components into components
+// normalize component/components into components
+type NormalizedRouteRecord =
+  | Omit<RouteRecordRedirect, 'alias'>
+  | Omit<RouteRecordMultipleViews, 'alias'>
 
 export interface RouteMatcher {
   re: RegExp
@@ -19,10 +26,41 @@ export interface RouteMatcher {
   parent: RouteMatcher | void
   // TODO: children so they can be removed
   // children: RouteMatcher[]
+  // TODO: needs information like optional, repeatable
   keys: string[]
   score: number
 }
 
+function copyObject<T extends Object, K extends keyof T>(
+  a: T,
+  keys: K[]
+): Mutable<Pick<T, K>> {
+  const copy: Pick<T, K> = {} as Pick<T, K>
+
+  for (const key of keys) {
+    if (!a.hasOwnProperty(key)) continue
+    if (key in a) copy[key] = a[key]
+  }
+
+  return copy
+}
+
+const ROUTE_RECORD_REDIRECT_KEYS: (keyof RouteRecordRedirect)[] = [
+  'path',
+  'name',
+  'beforeEnter',
+  'redirect',
+  'meta',
+]
+const ROUTE_RECORD_MULTIPLE_VIEWS_KEYS: (keyof (
+  | RouteRecordMultipleViews
+  | RouteRecordSingleView))[] = [
+  'path',
+  'name',
+  'beforeEnter',
+  'children',
+  'meta',
+]
 /**
  * Normalizes a RouteRecord into a MatchedRouteRecord. Creates a copy
  * @param record
@@ -31,20 +69,18 @@ export interface RouteMatcher {
 export function normalizeRecord(
   record: Readonly<RouteRecord>
 ): NormalizedRouteRecord {
-  if ('component' in record) {
-    const { component, ...rest } = record
-    // @ts-ignore I could do it type safe by copying again rest:
-    // return {
-    //   ...rest,
-    //   components: { default: component }
-    // }
-    // but it's slower
-    rest.components = { default: component }
-    return rest as NormalizedRouteRecord
+  // TODO: could be refactored to improve typings
+  if ('redirect' in record) {
+    return copyObject(record, ROUTE_RECORD_REDIRECT_KEYS)
+  } else {
+    const copy: RouteRecordMultipleViews = copyObject(
+      record,
+      ROUTE_RECORD_MULTIPLE_VIEWS_KEYS
+    ) as RouteRecordMultipleViews
+    copy.components =
+      'components' in record ? record.components : { default: record.component }
+    return copy
   }
-
-  // otherwise just create a copy
-  return { ...record }
 }
 
 const enum PathScore {
@@ -286,33 +322,47 @@ export class RouterMatcher {
       strict: false,
     }
 
-    const recordCopy = normalizeRecord(record)
+    // generate an array of records to correctly handle aliases
+    const normalizedRecords = [normalizeRecord(record)]
+    if ('alias' in record && record.alias) {
+      const aliases =
+        typeof record.alias === 'string' ? [record.alias] : record.alias
+      for (const alias of aliases) {
+        const copyForAlias = normalizeRecord(record)
+        copyForAlias.path = alias
+        normalizedRecords.push(copyForAlias)
+      }
+    }
 
     if (parent) {
       // if the child isn't an absolute route
       if (record.path[0] !== '/') {
         let path = parent.record.path
         // only add the / delimiter if the child path isn't empty
-        if (recordCopy.path) path += '/'
-        path += record.path
-        recordCopy.path = path
+        for (const normalizedRecord of normalizedRecords) {
+          if (normalizedRecord.path) path += '/'
+          path += record.path
+          normalizedRecord.path = path
+        }
       }
     }
 
-    // create the object before hand so it can be passed to children
-    const matcher = createRouteMatcher(recordCopy, parent, options)
+    for (const normalizedRecord of normalizedRecords) {
+      // create the object before hand so it can be passed to children
+      const matcher = createRouteMatcher(normalizedRecord, parent, options)
 
-    if ('children' in record && record.children) {
-      for (const childRecord of record.children) {
-        this.addRouteRecord(childRecord, matcher)
+      if ('children' in record && record.children) {
+        for (const childRecord of record.children) {
+          this.addRouteRecord(childRecord, matcher)
+        }
+        // TODO: the parent is special, we should match their children. They
+        // reference to the parent so we can render the parent
+        //
+        // matcher.score = -10
       }
-      // TODO: the parent is special, we should match their children. They
-      // reference to the parent so we can render the parent
-      //
-      // matcher.score = -10
-    }
 
-    this.insertMatcher(matcher)
+      this.insertMatcher(matcher)
+    }
   }
 
   private insertMatcher(matcher: RouteMatcher) {
@@ -376,6 +426,7 @@ export class RouterMatcher {
       if (!matcher) throw new NoRouteMatchError(currentLocation, location)
 
       // no need to resolve the path with the matcher as it was provided
+      // this also allows the user to control the encoding
       path = location.path
       name = matcher.record.name
 
index e291717e55258f196b6db344f74bf1d1eccfbddc..e2294e0bb73c0248275d12393a40fc24bc554470 100644 (file)
@@ -105,8 +105,9 @@ export type RouteComponent = Component | Lazy<Component>
 // and I don't thin it's possible to filter out the route
 // by any means
 
-interface RouteRecordCommon {
-  path: string // | RegExp
+export interface RouteRecordCommon {
+  path: string
+  alias?: string | string[]
   name?: string
   beforeEnter?: NavigationGuard | NavigationGuard[]
   meta?: Record<string | number | symbol, any>
@@ -119,12 +120,12 @@ export interface RouteRecordRedirect extends RouteRecordCommon {
   redirect: RouteRecordRedirectOption
 }
 
-interface RouteRecordSingleView extends RouteRecordCommon {
+export interface RouteRecordSingleView extends RouteRecordCommon {
   component: RouteComponent
   children?: RouteRecord[]
 }
 
-interface RouteRecordMultipleViews extends RouteRecordCommon {
+export interface RouteRecordMultipleViews extends RouteRecordCommon {
   components: Record<string, RouteComponent>
   // TODO: add tests
   children?: RouteRecord[]
@@ -201,3 +202,7 @@ export interface PostNavigationGuard {
 }
 
 export * from './type-guards'
+
+export type Mutable<T> = {
+  -readonly [P in keyof T]: T[P]
+}