+++ /dev/null
-import { createRouteRecordMatcher } from '../../src/matcher/path-matcher'
-import { RegExpOptions } from 'path-to-regexp'
-import { RouteComponent } from '../../src/types'
-import { RouteRecordMatcher } from '../../src/matcher/types'
-
-// @ts-ignore
-const component: RouteComponent = null
-
-function stringifyOptions(options: any) {
- return Object.keys(options).length ? ` (${JSON.stringify(options)})` : ''
-}
-
-describe('createRouteRecordMatcher', () => {
- function checkPathOrder(
- paths: Array<string | [string, RegExpOptions]>,
- options: RegExpOptions = {}
- ) {
- const normalizedPaths = paths.map(pathOrCombined => {
- if (Array.isArray(pathOrCombined))
- return [pathOrCombined[0], { ...options, ...pathOrCombined[1] }]
- return [pathOrCombined, options]
- })
-
- const matchers: Array<RouteRecordMatcher & {
- _options: RegExpOptions
- }> = normalizedPaths
- .slice()
- // Because sorting order is conserved, allows to mismatch order on
- // routes with the same ranking
- .reverse()
- .map(([path, options]) => ({
- ...createRouteRecordMatcher(
- {
- // @ts-ignore types are correct
- path,
- components: { default: component },
- },
- null,
- options
- ),
- // add original options
- _options: options,
- }))
- .sort((a, b) => b.score - a.score)
-
- 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++) {
- const a = matchers[i - 1]
- const b = matchers[i]
- try {
- expect(a.score).not.toBe(b.score)
- } catch (e) {
- throw new Error(
- `Record "${a.record.path}"${stringifyOptions(
- matchers[i - 1]._options
- )} and "${b.record.path}"${stringifyOptions(
- matchers[i]._options
- )} have the same score: ${
- a.score
- }. Avoid putting routes with the same score on the same test`
- )
- }
- }
- }
-
- it('orders a rest param with root', () => {
- checkPathOrder(['/a/', '/a/:w(.*)', '/a'])
- })
-
- it('orders sub segments with params', () => {
- checkPathOrder(['/a-b-c', '/a-:b-c', '/a-:b-:c', '/a-:b'])
- })
-
- it('works', () => {
- checkPathOrder([
- '/a/b/c',
- '/a/:b/c',
- '/a/b',
- '/a/:b',
- '/:a/-:b',
- '/:a/:b',
- '/a',
- '/a-:b',
- '/a-:w(.*)',
- '/:a-b',
- '/:a-:b-:c',
- '/:a-:b',
- '/:a-:b(.*)',
- '/:w',
- '/:w+',
- '/',
- ])
- })
-
- it('puts the wildcard at the end', () => {
- checkPathOrder(['/', '/:rest(.*)'])
- })
-
- it('prioritises custom regex', () => {
- checkPathOrder(['/:a(\\d+)', '/:a', '/:a(.*)'])
- checkPathOrder(['/b-:a(\\d+)', '/b-:a', '/b-:a(.*)'])
- })
-
- it('handles sub segments optional params', () => {
- // TODO: /a/c should be be bigger than /a/c/:b?
- checkPathOrder(['/a/d/c', '/a/b/c:b', '/a/c/:b', '/a/c/:b?', '/a/c'])
- })
-
- it('handles optional in sub segments', () => {
- checkPathOrder([
- '/a/_2_',
- // something like /a/_23_
- '/a/_:b(\\d)?_',
- '/a/_:b\\_', // the _ is escaped but b can be also letters
- '/a/a_:b',
- ])
- })
-
- it('works with long paths', () => {
- checkPathOrder([
- '/a/b/c/d/e',
- '/:k-foo/b/c/d/e',
- '/:k/b/c/d/e',
- '/:k/b/c/d/:j',
- ])
- })
-
- it('ending slashes less than params', () => {
- checkPathOrder([
- ['/a/:b/', { strict: true }],
- ['/a/b', { strict: false }],
- ['/a/:b', { strict: true }],
- ])
- })
-
- 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',
- ])
- })
-
- it('prioritizes case sensitive', () => {
- checkPathOrder([
- ['/a/', { sensitive: true }],
- '/a/', // explicit ending slash
- ['/a', { sensitive: true }],
- '/a', // also matches /A
- ])
- })
-
- it('ranks repeated params properly', () => {
- checkPathOrder([
- '/:a',
- '/:a+',
- '/:a?',
- '/:a*',
- // FIXME: this one should appear here but it appears before /:a*
- // '/:a(.*)'
- ])
- })
-})
-import pathToRegexp from 'path-to-regexp'
-import { RouteRecordNormalized, RouteRecordMatcher } from './types'
-
-const enum PathScore {
- _multiplier = 10,
- Segment = 4 * _multiplier, // /a-segment
- SubSegment = 2 * _multiplier, // /multiple-:things-in-one-:segment
- Static = 3 * _multiplier, // /static
- Dynamic = 2 * _multiplier, // /:someId
- DynamicCustomRegexp = 2.5 * _multiplier, // /:someId(\\d+)
- Wildcard = -1 * _multiplier, // /:namedWildcard(.*)
- SubWildcard = 1 * _multiplier, // Wildcard as a subsegment
- Repeatable = -0.5 * _multiplier, // /:w+ or /:w*
- // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
- Strict = 0.07 * _multiplier, // when options strict: true is passed, as the regex omits \/?
- CaseSensitive = 0.025 * _multiplier, // when options strict: true is passed, as the regex omits \/?
- Optional = -4 * _multiplier, // /:w? or /:w*
- SubOptional = -0.1 * _multiplier, // optional inside a subsegment /a-:w? or /a-:w*
- Root = 1 * _multiplier, // just /
+import { RouteRecordNormalized } from './types'
+import {
+ tokensToParser,
+ PathParser,
+ PathParserOptions,
+} from './path-parser-ranker'
+import { tokenizePath } from './path-tokenizer'
+
+export interface RouteRecordMatcher extends PathParser {
+ record: RouteRecordNormalized
+ parent: RouteRecordMatcher | undefined
+ // TODO: children so they can be removed
+ // children: RouteRecordMatcher[]
}
-/**
- * Non Working Rankings:
- * - ?p=AAOsIPQgYAEL9lNgQAGKBeACgQMODLpiY5C0gAQYhGyZkaNyaRFZOwOOkPODO_HsENkAAA..
- * the case sensitive and the strict option on the optional parameter `/:a-:w?` or `/:a?-:w` will come before `/:a-b`
- */
-
-// allows to check if the user provided a custom regexp
-const isDefaultPathRegExpRE = /^\[\^[^\]]+\]\+\?$/
-
export function createRouteRecordMatcher(
record: Readonly<RouteRecordNormalized>,
parent: RouteRecordMatcher | undefined,
- options: pathToRegexp.RegExpOptions
+ options?: PathParserOptions
): RouteRecordMatcher {
- const keys: pathToRegexp.Key[] = []
- // options only use `delimiter`
- const tokens = pathToRegexp.parse(record.path, options)
- const re = pathToRegexp.tokensToRegExp(tokens, keys, options)
- // we pass a copy because later on we are modifying the original token array
- // to compute the score of routes
- const resolve = pathToRegexp.tokensToFunction([...tokens])
-
- let score =
- (options.strict ? PathScore.Strict : 0) +
- (options.sensitive ? PathScore.CaseSensitive : 0)
-
- // console.log(tokens)
- // console.log('--- GROUPING ---')
-
- // special case for root path
- if (tokens.length === 1 && tokens[0] === '/') {
- score = PathScore.Segment + PathScore.Root
- } else {
- // allows us to group tokens into one single segment
- // it will point to the first token of the current group
- let currentSegment = 0
- // we group them in arrays to process them later
- const groups: Array<pathToRegexp.Token[]> = []
- // we skip the first element as it must be part of the first group
- const token = tokens[0]
- if (typeof token === 'string') {
- // TODO: refactor code in loop (right now it is duplicated)
- // we still need to check for / inside the string
- // remove the empty string because of the leading slash
- const sections = token.split('/').slice(1)
- if (sections.length > 1) {
- // the last one is going to replace currentSegment
- const last = sections.pop() as string // ts complains but length >= 2
- // we need to finalize previous group but we cannot use current entry
- // here we are sure that currentSegment < i because the token doesn't start with /
- // assert(currentSegment < i)
- const first = sections.shift() as string // ts complains but length >= 2
- // so we cut until the current segment and add the first section of current token as well
- groups.push(tokens.slice(currentSegment, 0).concat('/' + first))
- // equivalent to
- // groups.push(['/' + first])
-
- // we add the remaining sections as static groups
- for (const section of sections) {
- groups.push(['/' + section])
- }
-
- // we replace current entry with the last section and reset current segment
- tokens[0] = '/' + last
- // currentSegment = 0
- }
- }
-
- for (let i = 1; i < tokens.length; i++) {
- const token = tokens[i]
- if (typeof token === 'string') {
- if (token.charAt(0) === '/') {
- // finalize previous group and start a new one
- groups.push(tokens.slice(currentSegment, i))
- currentSegment = i
- } else {
- // we still need to check for / inside the string
- const sections = token.split('/')
- if (sections.length > 1) {
- // the last one is going to replace currentSegment
- const last = sections.pop() as string // ts complains but length >= 2
- // we need to finalize previous group but we cannot use current entry
- // here we are sure that currentSegment < i because the token doesn't start with /
- // assert(currentSegment < i)
- const first = sections.shift() as string // ts complains but length >= 2
- // so we cut until the current segment and add the first section of current token as well
- groups.push(tokens.slice(currentSegment, i).concat(first))
-
- // we add the remaining sections as static groups
- for (const section of sections) {
- groups.push(['/' + section])
- }
+ const parser = tokensToParser(tokenizePath(record.path), options)
- // we replace current entry with the last section and reset current segment
- tokens[i] = '/' + last
- currentSegment = i
- }
- }
- } else if (token.prefix.charAt(0) === '/') {
- // finalize previous group and start a new one
- groups.push(tokens.slice(currentSegment, i))
- currentSegment = i
- }
- }
-
- // add the remaining segment as one group
- // TODO: refactor the handling of ending with static like /:a/b/c
- if (currentSegment < tokens.length) {
- let token: pathToRegexp.Token
- if (
- tokens.length - currentSegment === 1 &&
- typeof (token = tokens[tokens.length - 1]) === 'string'
- ) {
- // the remaining group is a single string, so it must start with a leading /
- const sections = token.split('/').slice(1)
- // we add the remaining sections as static groups
- for (const section of sections) {
- groups.push(['/' + section])
- }
- } else {
- groups.push(tokens.slice(currentSegment))
- }
- }
-
- const scoreForSegment = function scoreForSegment(
- group: pathToRegexp.Token
- ): number {
- let score = PathScore.Segment
- if (typeof group === 'string') {
- score += group === '/' ? PathScore.Root : PathScore.Static
- } else {
- score +=
- group.pattern === '.*'
- ? PathScore.Wildcard
- : isDefaultPathRegExpRE.test(group.pattern)
- ? PathScore.Dynamic
- : PathScore.DynamicCustomRegexp
- score +=
- +group.optional * PathScore.Optional +
- +group.repeat * PathScore.Repeatable
- // if (typeof group.name === 'number') {
- // throw new TypeError('Name your param')
- // }
- // keys.push(group.name)
- }
- return score
- }
-
- const scoreForSubSegment = function scoreForSubSegment(
- group: pathToRegexp.Token
- ): number {
- let score = 0
- if (typeof group === 'string') {
- // in a sub segment, it doesn't matter if it's root or not
- score += PathScore.Static
- } else {
- score +=
- group.pattern === '.*'
- ? PathScore.SubWildcard
- : isDefaultPathRegExpRE.test(group.pattern)
- ? PathScore.Dynamic
- : PathScore.DynamicCustomRegexp
- score += +group.optional * PathScore.SubOptional
- if (typeof group.name === 'number') {
- throw new TypeError('Name your param')
- }
- // keys.push(group.name)
- }
- return score
- }
-
- for (const group of groups) {
- // console.log(group)
- if (group.length === 1) {
- score += scoreForSegment(group[0])
- } else {
- score += PathScore.Segment + PathScore.SubSegment
- let multiplier = 1 / 10
- for (let i = 0; i < group.length; i++) {
- score += scoreForSubSegment(group[i]) * multiplier
- multiplier /= 10
- }
- }
- // segments.push('/' + section)
- }
-
- // console.log(record.path, { score })
- // console.log('____'.repeat(20))
- }
-
- // create the object before hand so it can be passed to children
return {
- parent,
+ ...parser,
record,
- re,
- // TODO: handle numbers differently. Maybe take the max one and say there are x unnamed keys
- keys: keys.map(key => String(key.name)),
- resolve,
- score,
+ parent,
}
}