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
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', () => {
} 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
) => 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,
+ })
}
}
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
name?: string
beforeEnter?: NavigationGuard | NavigationGuard[]
meta?: Record<string | number | symbol, any>
+ options?: RegExpOptions
}
export type RouteRecordRedirectOption =