/** @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 },
},
)
.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++) {
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`
)
}
}
'/: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',
+ ])
+ })
})
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 /
// 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 ---')
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)