From: Eduardo San Martin Morote Date: Tue, 12 Nov 2019 21:09:07 +0000 (-0500) Subject: feat(matcher): handle strict traling slash X-Git-Tag: v4.0.0-alpha.0~173 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=412ce20a3d92569632e8fd2836ea1b4dad7c0250;p=thirdparty%2Fvuejs%2Frouter.git feat(matcher): handle strict traling slash --- diff --git a/__tests__/matcher/resolve.spec.ts b/__tests__/matcher/resolve.spec.ts index 2984dcf1..d3df6503 100644 --- a/__tests__/matcher/resolve.spec.ts +++ b/__tests__/matcher/resolve.spec.ts @@ -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', () => { diff --git a/src/matcher/index.ts b/src/matcher/index.ts index 973c4416..cf59f4f0 100644 --- a/src/matcher/index.ts +++ b/src/matcher/index.ts @@ -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, 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, 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, + }) } } diff --git a/src/types/index.ts b/src/types/index.ts index e2294e0b..f0a8dc88 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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 | typeof Vue | AsyncComponent @@ -111,6 +112,7 @@ export interface RouteRecordCommon { name?: string beforeEnter?: NavigationGuard | NavigationGuard[] meta?: Record + options?: RegExpOptions } export type RouteRecordRedirectOption =