]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
refactor(matcher): split in multiple files
authorEduardo San Martin Morote <posva13@gmail.com>
Sat, 26 Oct 2019 14:47:45 +0000 (15:47 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Sat, 26 Oct 2019 14:47:45 +0000 (15:47 +0100)
__tests__/matcher/ranking.spec.ts
src/matcher/index.ts [new file with mode: 0644]
src/matcher/path-ranker.ts [moved from src/matcher.ts with 56% similarity]
src/matcher/types.ts [new file with mode: 0644]

index 75126d38431874b07334eb88137071b7ec8fa1fb..8806ba29ef52bc6eb6546f758ef625f6a7258492 100644 (file)
@@ -1,4 +1,7 @@
-import { createRouteMatcher, RouteMatcher } from '../../src/matcher'
+import {
+  createRouteMatcher,
+  RouteRecordMatcher,
+} from '../../src/matcher/path-ranker'
 import { RegExpOptions } from 'path-to-regexp'
 import { RouteComponent } from '../../src/types'
 
@@ -21,7 +24,7 @@ describe('createRouteMatcher', () => {
     })
 
     const matchers: Array<
-      RouteMatcher & { _options: RegExpOptions }
+      RouteRecordMatcher & { _options: RegExpOptions }
     > = normalizedPaths
       .slice()
       // Because sorting order is conserved, allows to mismatch order on
diff --git a/src/matcher/index.ts b/src/matcher/index.ts
new file mode 100644 (file)
index 0000000..2125753
--- /dev/null
@@ -0,0 +1,220 @@
+import pathToRegexp from 'path-to-regexp'
+import {
+  RouteRecord,
+  RouteParams,
+  MatcherLocation,
+  MatcherLocationNormalized,
+  MatcherLocationRedirect,
+  // TODO: add it to matched
+  // MatchedRouteRecord,
+} from '../types'
+import { NoRouteMatchError, InvalidRouteMatch } from '../errors'
+import { createRouteMatcher, normalizeRouteRecord } from './path-ranker'
+import { RouteRecordMatcher } from './types'
+
+export class RouterMatcher {
+  private matchers: RouteRecordMatcher[] = []
+
+  constructor(routes: RouteRecord[]) {
+    for (const route of routes) {
+      this.addRouteRecord(route)
+    }
+  }
+
+  private addRouteRecord(
+    record: Readonly<RouteRecord>,
+    parent?: RouteRecordMatcher
+  ): void {
+    const options: pathToRegexp.RegExpOptions = {
+      // NOTE: should we make strict by default and redirect /users/ to /users
+      // so that it's the same from SEO perspective?
+      strict: false,
+    }
+
+    // generate an array of records to correctly handle aliases
+    const normalizedRecords = [normalizeRouteRecord(record)]
+    if ('alias' in record && record.alias) {
+      const aliases =
+        typeof record.alias === 'string' ? [record.alias] : record.alias
+      for (const alias of aliases) {
+        const copyForAlias = normalizeRouteRecord(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
+        for (const normalizedRecord of normalizedRecords) {
+          if (normalizedRecord.path) path += '/'
+          path += record.path
+          normalizedRecord.path = path
+        }
+      }
+    }
+
+    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)
+        }
+        // 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)
+    }
+  }
+
+  private insertMatcher(matcher: RouteRecordMatcher) {
+    let i = 0
+    while (i < this.matchers.length && matcher.score <= this.matchers[i].score)
+      i++
+    this.matchers.splice(i, 0, matcher)
+  }
+
+  /**
+   * Resolve a location without doing redirections so it can be used for anchors
+   */
+  resolveAsPath() {}
+
+  /**
+   * Transforms a MatcherLocation object into a normalized location
+   * @param location MatcherLocation to resolve to a url
+   * @param currentLocation MatcherLocationNormalized of the current location
+   */
+  resolve(
+    location: Readonly<MatcherLocation>,
+    currentLocation: Readonly<MatcherLocationNormalized>
+  ): MatcherLocationNormalized | MatcherLocationRedirect {
+    let matcher: RouteRecordMatcher | void
+    let params: RouteParams = {}
+    let path: MatcherLocationNormalized['path']
+    let name: MatcherLocationNormalized['name']
+
+    if ('name' in location && location.name) {
+      matcher = this.matchers.find(m => m.record.name === location.name)
+
+      if (!matcher) throw new NoRouteMatchError(currentLocation, location)
+
+      name = matcher.record.name
+      // TODO: merge params
+      params = location.params || currentLocation.params
+      // params are automatically encoded
+      // TODO: try catch to provide better error messages
+      path = matcher.resolve(params)
+      // TODO: check missing params
+
+      if ('redirect' in matcher.record) {
+        const { redirect } = matcher.record
+        return {
+          redirect,
+          normalizedLocation: {
+            name,
+            path,
+            matched: [],
+            params,
+            meta: matcher.record.meta || {},
+          },
+        }
+      }
+    } else if ('path' in location) {
+      matcher = this.matchers.find(m => m.re.test(location.path))
+
+      // TODO: if no matcher, return the location with an empty matched array
+      // to allow non existent matches
+      // TODO: warning of unused params if provided
+      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
+
+      // fill params
+      const result = matcher.re.exec(path)
+
+      if (!result) {
+        // TODO: redo message: matching path against X
+        throw new Error(`Error parsing path "${location.path}"`)
+      }
+
+      for (let i = 0; i < matcher.keys.length; i++) {
+        const key = matcher.keys[i]
+        let value: string = result[i + 1]
+        try {
+          value = decodeURIComponent(value)
+        } catch (err) {
+          if (err instanceof URIError) {
+            console.warn(
+              `[vue-router] failed decoding param "${key}" with value "${value}". When providing a string location or the "path" property, URL must be properly encoded (TODO: link). Falling back to unencoded value`
+            )
+          } else {
+            throw err
+          }
+        }
+        if (!value) {
+          // TODO: handle optional params
+          throw new Error(
+            `Error parsing path "${location.path}" when looking for param "${key}"`
+          )
+        }
+        params[key] = value
+      }
+
+      if ('redirect' in matcher.record) {
+        const { redirect } = matcher.record
+        return {
+          redirect,
+          normalizedLocation: {
+            name,
+            path,
+            matched: [],
+            params,
+            meta: matcher.record.meta || {},
+          },
+        }
+      }
+      // location is a relative path
+    } else {
+      // match by name or path of current route
+      matcher = currentLocation.name
+        ? this.matchers.find(m => m.record.name === currentLocation.name)
+        : this.matchers.find(m => m.re.test(currentLocation.path))
+      if (!matcher) throw new NoRouteMatchError(currentLocation, location)
+      name = matcher.record.name
+      params = location.params || currentLocation.params
+      path = matcher.resolve(params)
+    }
+
+    // this should never happen because it will mean that the user ended up in a route
+    // that redirects but ended up not redirecting
+    if ('redirect' in matcher.record) throw new InvalidRouteMatch(location)
+
+    const matched: MatcherLocationNormalized['matched'] = [matcher.record]
+    let parentMatcher: RouteRecordMatcher | void = matcher.parent
+    while (parentMatcher) {
+      // reversed order so parents are at the beginning
+      // TODO: should be doable by typing RouteRecordMatcher in a different way
+      if ('redirect' in parentMatcher.record) throw new Error('TODO')
+      matched.unshift(parentMatcher.record)
+      parentMatcher = parentMatcher.parent
+    }
+
+    return {
+      name,
+      path,
+      params,
+      matched,
+      meta: matcher.record.meta || {},
+    }
+  }
+}
similarity index 56%
rename from src/matcher.ts
rename to src/matcher/path-ranker.ts
index 1ed75330506a9630819c1dd79986d6b8ba0ea815..2c13f325259934a9eaa193fda6ed7fe3e7ec6259 100644 (file)
@@ -1,35 +1,14 @@
 import pathToRegexp from 'path-to-regexp'
 import {
   RouteRecord,
-  RouteParams,
-  MatcherLocation,
-  MatcherLocationNormalized,
-  MatcherLocationRedirect,
   RouteRecordRedirect,
   RouteRecordMultipleViews,
   RouteRecordSingleView,
   Mutable,
   // TODO: add it to matched
   // MatchedRouteRecord,
-} from './types/index'
-import { NoRouteMatchError, InvalidRouteMatch } from './errors'
-
-// normalize component/components into components
-type NormalizedRouteRecord =
-  | Omit<RouteRecordRedirect, 'alias'>
-  | Omit<RouteRecordMultipleViews, 'alias'>
-
-export interface RouteMatcher {
-  re: RegExp
-  resolve: (params?: RouteParams) => string
-  record: NormalizedRouteRecord
-  parent: RouteMatcher | void
-  // TODO: children so they can be removed
-  // children: RouteMatcher[]
-  // TODO: needs information like optional, repeatable
-  keys: string[]
-  score: number
-}
+} from '../types'
+import { NormalizedRouteRecord, RouteRecordMatcher } from './types'
 
 function copyObject<T extends Object, K extends keyof T>(
   a: T,
@@ -66,7 +45,7 @@ const ROUTE_RECORD_MULTIPLE_VIEWS_KEYS: (keyof (
  * @param record
  * @returns the normalized version
  */
-export function normalizeRecord(
+export function normalizeRouteRecord(
   record: Readonly<RouteRecord>
 ): NormalizedRouteRecord {
   // TODO: could be refactored to improve typings
@@ -112,9 +91,9 @@ const isDefaultPathRegExpRE = /^\[\^[^\]]+\]\+\?$/
 
 export function createRouteMatcher(
   record: Readonly<NormalizedRouteRecord>,
-  parent: RouteMatcher | void,
+  parent: RouteRecordMatcher | void,
   options: pathToRegexp.RegExpOptions
-): RouteMatcher {
+): RouteRecordMatcher {
   const keys: pathToRegexp.Key[] = []
   // options only use `delimiter`
   const tokens = pathToRegexp.parse(record.path, options)
@@ -302,210 +281,3 @@ export function createRouteMatcher(
     score,
   }
 }
-
-export class RouterMatcher {
-  private matchers: RouteMatcher[] = []
-
-  constructor(routes: RouteRecord[]) {
-    for (const route of routes) {
-      this.addRouteRecord(route)
-    }
-  }
-
-  private addRouteRecord(
-    record: Readonly<RouteRecord>,
-    parent?: RouteMatcher
-  ): void {
-    const options: pathToRegexp.RegExpOptions = {
-      // NOTE: should we make strict by default and redirect /users/ to /users
-      // so that it's the same from SEO perspective?
-      strict: false,
-    }
-
-    // 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
-        for (const normalizedRecord of normalizedRecords) {
-          if (normalizedRecord.path) path += '/'
-          path += record.path
-          normalizedRecord.path = path
-        }
-      }
-    }
-
-    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)
-        }
-        // 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)
-    }
-  }
-
-  private insertMatcher(matcher: RouteMatcher) {
-    let i = 0
-    while (i < this.matchers.length && matcher.score <= this.matchers[i].score)
-      i++
-    this.matchers.splice(i, 0, matcher)
-  }
-
-  /**
-   * Resolve a location without doing redirections so it can be used for anchors
-   */
-  resolveAsPath() {}
-
-  /**
-   * Transforms a MatcherLocation object into a normalized location
-   * @param location MatcherLocation to resolve to a url
-   * @param currentLocation MatcherLocationNormalized of the current location
-   */
-  resolve(
-    location: Readonly<MatcherLocation>,
-    currentLocation: Readonly<MatcherLocationNormalized>
-  ): MatcherLocationNormalized | MatcherLocationRedirect {
-    let matcher: RouteMatcher | void
-    let params: RouteParams = {}
-    let path: MatcherLocationNormalized['path']
-    let name: MatcherLocationNormalized['name']
-
-    if ('name' in location && location.name) {
-      matcher = this.matchers.find(m => m.record.name === location.name)
-
-      if (!matcher) throw new NoRouteMatchError(currentLocation, location)
-
-      name = matcher.record.name
-      // TODO: merge params
-      params = location.params || currentLocation.params
-      // params are automatically encoded
-      // TODO: try catch to provide better error messages
-      path = matcher.resolve(params)
-      // TODO: check missing params
-
-      if ('redirect' in matcher.record) {
-        const { redirect } = matcher.record
-        return {
-          redirect,
-          normalizedLocation: {
-            name,
-            path,
-            matched: [],
-            params,
-            meta: matcher.record.meta || {},
-          },
-        }
-      }
-    } else if ('path' in location) {
-      matcher = this.matchers.find(m => m.re.test(location.path))
-
-      // TODO: if no matcher, return the location with an empty matched array
-      // to allow non existent matches
-      // TODO: warning of unused params if provided
-      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
-
-      // fill params
-      const result = matcher.re.exec(path)
-
-      if (!result) {
-        // TODO: redo message: matching path against X
-        throw new Error(`Error parsing path "${location.path}"`)
-      }
-
-      for (let i = 0; i < matcher.keys.length; i++) {
-        const key = matcher.keys[i]
-        let value: string = result[i + 1]
-        try {
-          value = decodeURIComponent(value)
-        } catch (err) {
-          if (err instanceof URIError) {
-            console.warn(
-              `[vue-router] failed decoding param "${key}" with value "${value}". When providing a string location or the "path" property, URL must be properly encoded (TODO: link). Falling back to unencoded value`
-            )
-          } else {
-            throw err
-          }
-        }
-        if (!value) {
-          // TODO: handle optional params
-          throw new Error(
-            `Error parsing path "${location.path}" when looking for param "${key}"`
-          )
-        }
-        params[key] = value
-      }
-
-      if ('redirect' in matcher.record) {
-        const { redirect } = matcher.record
-        return {
-          redirect,
-          normalizedLocation: {
-            name,
-            path,
-            matched: [],
-            params,
-            meta: matcher.record.meta || {},
-          },
-        }
-      }
-      // location is a relative path
-    } else {
-      // match by name or path of current route
-      matcher = currentLocation.name
-        ? this.matchers.find(m => m.record.name === currentLocation.name)
-        : this.matchers.find(m => m.re.test(currentLocation.path))
-      if (!matcher) throw new NoRouteMatchError(currentLocation, location)
-      name = matcher.record.name
-      params = location.params || currentLocation.params
-      path = matcher.resolve(params)
-    }
-
-    // this should never happen because it will mean that the user ended up in a route
-    // that redirects but ended up not redirecting
-    if ('redirect' in matcher.record) throw new InvalidRouteMatch(location)
-
-    const matched: MatcherLocationNormalized['matched'] = [matcher.record]
-    let parentMatcher: RouteMatcher | void = matcher.parent
-    while (parentMatcher) {
-      // reversed order so parents are at the beginning
-      // TODO: should be doable by typing RouteMatcher in a different way
-      if ('redirect' in parentMatcher.record) throw new Error('TODO')
-      matched.unshift(parentMatcher.record)
-      parentMatcher = parentMatcher.parent
-    }
-
-    return {
-      name,
-      path,
-      params,
-      matched,
-      meta: matcher.record.meta || {},
-    }
-  }
-}
diff --git a/src/matcher/types.ts b/src/matcher/types.ts
new file mode 100644 (file)
index 0000000..851d377
--- /dev/null
@@ -0,0 +1,22 @@
+import {
+  RouteParams,
+  RouteRecordMultipleViews,
+  RouteRecordRedirect,
+} from '../types'
+
+// normalize component/components into components
+export type NormalizedRouteRecord =
+  | Omit<RouteRecordRedirect, 'alias'>
+  | Omit<RouteRecordMultipleViews, 'alias'>
+
+export interface RouteRecordMatcher {
+  re: RegExp
+  resolve: (params?: RouteParams) => string
+  record: NormalizedRouteRecord
+  parent: RouteRecordMatcher | void
+  // TODO: children so they can be removed
+  // children: RouteMatcher[]
+  // TODO: needs information like optional, repeatable
+  keys: string[]
+  score: number
+}