]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(matcher): handle strict paths
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 17 Jul 2019 12:33:31 +0000 (14:33 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 17 Jul 2019 12:33:31 +0000 (14:33 +0200)
__tests__/matcher-ranking.spec.js
src/matcher.ts

index 7d0cb5a137c2eef87831dd704b79a6b3ee6dee82..77483e2dee7ac841548ef8d1db004283d2ff776a 100644 (file)
@@ -16,21 +16,31 @@ const component = null
 /** @typedef {import('../src/types').MatcherLocationNormalized} MatcherLocationNormalized */
 /** @typedef {import('path-to-regexp').RegExpOptions} RegExpOptions */
 
+function stringifyOptions(options) {
+  return Object.keys(options).length ? ` (${JSON.stringify(options)})` : ''
+}
+
 describe('createRouteMatcher', () => {
   /**
    *
-   * @param {string[]} paths
+   * @param {Array<string | [string, RegExpOptions]>} paths
    * @param {RegExpOptions} options
    */
   function checkPathOrder(paths, options = {}) {
-    const matchers = paths
+    const normalizedPaths = paths.map(pathOrCombined => {
+      if (Array.isArray(pathOrCombined))
+        return [pathOrCombined[0], { ...options, ...pathOrCombined[1] }]
+      return [pathOrCombined, options]
+    })
+    const matchers = normalizedPaths
       .slice()
       // Because sorting order is conserved, allows to mismatch order on
       // routes with the same ranking
       .reverse()
-      .map(path =>
+      .map(([path, options]) =>
         createRouteMatcher(
           {
+            // @ts-ignore types are correct
             path,
             components: { default: component },
           },
@@ -40,7 +50,9 @@ describe('createRouteMatcher', () => {
       )
       .sort((a, b) => b.score - a.score)
 
-    expect(matchers.map(matcher => matcher.record.path)).toEqual(paths)
+    expect(matchers.map(matcher => matcher.record.path)).toEqual(
+      normalizedPaths.map(([path]) => path)
+    )
 
     // Fail if two consecutive records have the same record
     for (let i = 1; i < matchers.length; i++) {
@@ -50,7 +62,13 @@ describe('createRouteMatcher', () => {
         expect(a.score).not.toBe(b.score)
       } catch (e) {
         throw new Error(
-          `Record "${a.record.path}" and "${b.record.path}" have the same score: ${a.score}. Avoid putting routes with the same score on the same test`
+          `Record "${a.record.path}"${stringifyOptions(
+            normalizedPaths[i - 1][1]
+          )} and "${b.record.path}"${stringifyOptions(
+            normalizedPaths[i][1]
+          )} have the same score: ${
+            a.score
+          }. Avoid putting routes with the same score on the same test`
         )
       }
     }
@@ -117,4 +135,25 @@ describe('createRouteMatcher', () => {
       '/:k/b/c/d/:j',
     ])
   })
+
+  it('prioritizes ending slashes', () => {
+    checkPathOrder([
+      // no strict
+      '/a/b/',
+      '/a/b',
+      '/a/',
+      '/a',
+    ])
+
+    checkPathOrder([
+      ['/a/b/', { strict: true }],
+      '/a/b/',
+      ['/a/b', { strict: true }],
+      '/a/b',
+      ['/a/', { strict: true }],
+      '/a/',
+      ['/a', { strict: true }],
+      '/a',
+    ])
+  })
 })
index 442649a68bd092ec1e88967e7a071c9d487ff12a..617343818d3cc662d1c3d76e42e50eadd441ef03 100644 (file)
@@ -56,6 +56,7 @@ enum PathScore {
   Wildcard = -1, // /:namedWildcard(.*)
   SubWildcard = 1, // Wildcard as a subsegment
   Repeatable = -0.5, // /:w+ or /:w*
+  Strict = 0.5, // when options strict: true is passed, as the regex omits \/?
   Optional = -4, // /:w? or /:w*
   SubOptional = -0.1, // optional inside a subsegment /a-:w? or /a-:w*
   Root = 1, // just /
@@ -77,7 +78,7 @@ export function createRouteMatcher(
   // to compute the score of routes
   const resolve = pathToRegexp.tokensToFunction([...tokens])
 
-  let score = 0
+  let score = options.strict ? PathScore.Strict : 0
 
   // console.log(tokens)
   // console.log('--- GROUPING ---')
@@ -266,7 +267,11 @@ export class RouterMatcher {
     record: Readonly<RouteRecord>,
     parent?: RouteMatcher
   ): void {
-    const options: pathToRegexp.RegExpOptions = {}
+    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,
+    }
 
     const recordCopy = normalizeRecord(record)