]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(matcher): handle strict traling slash
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 12 Nov 2019 21:09:07 +0000 (16:09 -0500)
committerEduardo San Martin Morote <posva13@gmail.com>
Tue, 12 Nov 2019 21:09:10 +0000 (16:09 -0500)
__tests__/matcher/resolve.spec.ts
src/matcher/index.ts
src/types/index.ts

index 2984dcf1657c094bf2c29e62f8b8c11a70ea6877..d3df65030af2aeb26b1e2d626a3e25d488b48806 100644 (file)
@@ -42,7 +42,9 @@ describe('Router Matcher', () => {
         if (!resolved.matched)
           // @ts-ignore
           resolved.matched = record.map(normalizeRouteRecord)
-        else resolved.matched = resolved.matched.map(normalizeRouteRecord)
+        // allow passing an expect.any(Array)
+        else if (Array.isArray(resolved.matched))
+          resolved.matched = resolved.matched.map(normalizeRouteRecord)
       }
 
       // allows not passing params
@@ -206,6 +208,50 @@ describe('Router Matcher', () => {
           assertErrorMatch({ path: '/', components }, { path: '/foo' })
         ).toMatchSnapshot()
       })
+
+      it('disallows multiple trailing slashes', () => {
+        expect(
+          assertErrorMatch({ path: '/home/', components }, { path: '/home//' })
+        ).toMatchSnapshot()
+      })
+
+      it('allows an optional trailing slash', () => {
+        assertRecordMatch(
+          { path: '/home/', name: 'Home', components },
+          { path: '/home/' },
+          { name: 'Home', path: '/home/', matched: expect.any(Array) }
+        )
+      })
+
+      it('keeps required trailing slash (strict: true)', () => {
+        const record = {
+          path: '/home/',
+          name: 'Home',
+          components,
+          options: { strict: true },
+        }
+        assertRecordMatch(
+          record,
+          { path: '/home/' },
+          { name: 'Home', path: '/home/', matched: expect.any(Array) }
+        )
+        assertErrorMatch(record, { path: '/home' })
+      })
+
+      it('rejects a trailing slash when strict', () => {
+        const record = {
+          path: '/home',
+          name: 'Home',
+          components,
+          options: { strict: true },
+        }
+        assertRecordMatch(
+          record,
+          { path: '/home' },
+          { name: 'Home', path: '/home', matched: expect.any(Array) }
+        )
+        assertErrorMatch(record, { path: '/home/' })
+      })
     })
 
     describe('LocationAsName', () => {
index 973c441627d649b5ed3ec346388ea23c655e66b3..cf59f4f0c55e24c6f7e5bbe02c2df19a811b8115 100644 (file)
@@ -10,7 +10,7 @@ import {
 } from '../types'
 import { NoRouteMatchError, InvalidRouteMatch } from '../errors'
 import { createRouteRecordMatcher, normalizeRouteRecord } from './path-matcher'
-import { RouteRecordMatcher } from './types'
+import { RouteRecordMatcher, RouteRecordNormalized } from './types'
 
 interface RouterMatcher {
   addRoute: (record: Readonly<RouteRecord>, parent?: RouteRecordMatcher) => void
@@ -20,28 +20,43 @@ interface RouterMatcher {
   ) => MatcherLocationNormalized | MatcherLocationRedirect
 }
 
-export function createRouterMatcher(routes: RouteRecord[]): RouterMatcher {
+const TRAILING_SLASH_RE = /(.)\/+$/
+function removeTrailingSlash(path: string): string {
+  return path.replace(TRAILING_SLASH_RE, '$1')
+}
+
+const DEFAULT_REGEX_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,
+}
+
+export function createRouterMatcher(
+  routes: RouteRecord[],
+  globalOptions: pathToRegexp.RegExpOptions = DEFAULT_REGEX_OPTIONS
+): RouterMatcher {
   const matchers: RouteRecordMatcher[] = []
 
   function addRoute(
     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,
-    }
-
+    const mainNormalizedRecord: RouteRecordNormalized = normalizeRouteRecord(
+      record
+    )
+    const options = { ...globalOptions, ...record.options }
+    if (!options.strict)
+      mainNormalizedRecord.path = removeTrailingSlash(mainNormalizedRecord.path)
     // generate an array of records to correctly handle aliases
-    const normalizedRecords = [normalizeRouteRecord(record)]
+    const normalizedRecords: RouteRecordNormalized[] = [mainNormalizedRecord]
     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)
+        normalizedRecords.push({
+          ...mainNormalizedRecord,
+          path: alias,
+        })
       }
     }
 
index e2294e0bb73c0248275d12393a40fc24bc554470..f0a8dc88cdf0ec462a3952b0e9b423959a7d43ca 100644 (file)
@@ -1,4 +1,5 @@
 import { HistoryQuery, RawHistoryQuery } from '../history/common'
+import { RegExpOptions } from 'path-to-regexp'
 // import Vue, { ComponentOptions, AsyncComponent } from 'vue'
 
 // type Component = ComponentOptions<Vue> | typeof Vue | AsyncComponent
@@ -111,6 +112,7 @@ export interface RouteRecordCommon {
   name?: string
   beforeEnter?: NavigationGuard | NavigationGuard[]
   meta?: Record<string | number | symbol, any>
+  options?: RegExpOptions
 }
 
 export type RouteRecordRedirectOption =