From 1e3419b81ad487403779be020b8e255b4dc56517 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 13:49:32 -0300 Subject: [PATCH 01/17] feat(otimized matcher): WIP Optimized matcher. --- src/create-matcher.js | 97 ++++++++++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 28 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index f7d72b93e..a92cd278f 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -7,7 +7,7 @@ import { createRoute } from './util/route' import { fillParams } from './util/params' import { createRouteMap } from './create-route-map' import { normalizeLocation } from './util/location' -import { decode } from './util/query' +// import { decode } from './util/query' export type Matcher = { match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; @@ -22,14 +22,18 @@ export function createMatcher ( ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) + const _optimizedMatcher = optimizedMatcher(routes) + function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) + _optimizedMatcher.addRoutes(routes) } function addRoute (parentOrRoute, route) { const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined // $flow-disable-line createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent) + _optimizedMatcher.addRoute(parent, route || parentOrRoute) // add aliases of parent if (parent && parent.alias.length) { @@ -81,13 +85,9 @@ export function createMatcher ( location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } else if (location.path) { - location.params = {} - for (let i = 0; i < pathList.length; i++) { - const path = pathList[i] - const record = pathMap[path] - if (matchRoute(record.regex, location.path, location.params)) { - return _createRoute(record, location, redirectedFrom) - } + const record = _optimizedMatcher.match(location) + if (record) { + return _createRoute(record, location, redirectedFrom) } } // no match @@ -197,30 +197,71 @@ export function createMatcher ( } } -function matchRoute ( - regex: RouteRegExp, - path: string, - params: Object -): boolean { - const m = path.match(regex) - - if (!m) { - return false - } else if (!params) { - return true +// function matchRoute ( +// regex: RouteRegExp, +// path: string, +// params: Object +// ): boolean { +// const m = path.match(regex) + +// if (!m) { +// return false +// } else if (!params) { +// return true +// } + +// for (let i = 1, len = m.length; i < len; ++i) { +// const key = regex.keys[i - 1] +// if (key) { +// // Fix #1994: using * with props: true generates a param named 0 +// params[key.name || 'pathMatch'] = typeof m[i] === 'string' ? decode(m[i]) : m[i] +// } +// } + +// return true +// } + +function resolveRecordPath (path: string, record: RouteRecord): string { + return resolvePath(path, record.parent ? record.parent.path : '/', true) +} + +// function isStaticPath (path) { +// // Dynamic paths have /:placeholder or anonymous placeholder /(.+) or wildcard *. +// return !/[:(*]/.exec(path) +// } + +function optimizedMatcher (routes: Array): Matcher { + // staticMap keys are always full paths. + const staticMap = {} + // dynamicMap keys are prefixes. e.g. /, /onelevel/, /onelevel/twolevel/ + // const dynamicMap = {} + // const staticLevels = 0 + + routes.forEach(route => { + staticMap[route.path] = route + }) + + function addRoutes (routes) { + routes.forEach(route => addRoute(null, route)) } - for (let i = 1, len = m.length; i < len; ++i) { - const key = regex.keys[i - 1] - if (key) { - // Fix #1994: using * with props: true generates a param named 0 - params[key.name || 'pathMatch'] = typeof m[i] === 'string' ? decode(m[i]) : m[i] + function addRoute (parent, route) { + console.log('OPT ADDROUTE', parent, route) + let path = route.path + if (parent) { + path = `${parent.path}/${route.path}` } + staticMap[path] = route } - return true -} + function match (location) { + console.log('STATICMAP', location, staticMap) + return staticMap[location.path] + } -function resolveRecordPath (path: string, record: RouteRecord): string { - return resolvePath(path, record.parent ? record.parent.path : '/', true) + return { + addRoutes, + addRoute, + match + } } From 4fd451813c6286959eb7ccc4db0fcd630a79bbd6 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 14:51:04 -0300 Subject: [PATCH 02/17] feat(otimized matcher): Pass all create-matcher.spec. --- src/create-matcher.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index a92cd278f..c409e64e4 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -33,7 +33,7 @@ export function createMatcher ( const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined // $flow-disable-line createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent) - _optimizedMatcher.addRoute(parent, route || parentOrRoute) + _optimizedMatcher.addRoute(parent && parent.path, route || parentOrRoute) // add aliases of parent if (parent && parent.alias.length) { @@ -85,6 +85,7 @@ export function createMatcher ( location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } else if (location.path) { + location.params = {} const record = _optimizedMatcher.match(location) if (record) { return _createRoute(record, location, redirectedFrom) @@ -237,26 +238,30 @@ function optimizedMatcher (routes: Array): Matcher { // const dynamicMap = {} // const staticLevels = 0 - routes.forEach(route => { - staticMap[route.path] = route - }) + addRoutes(routes) function addRoutes (routes) { routes.forEach(route => addRoute(null, route)) } - function addRoute (parent, route) { - console.log('OPT ADDROUTE', parent, route) + function addRoute (parentPath, route) { let path = route.path - if (parent) { - path = `${parent.path}/${route.path}` + if (parentPath) { + path = `${parentPath}/${route.path}` } staticMap[path] = route + + if (route.children) { + route.children.forEach(child => { + addRoute(path, child) + }) + } } function match (location) { - console.log('STATICMAP', location, staticMap) - return staticMap[location.path] + const record = staticMap[location.path] || staticMap['*'] + location.params['pathMatch'] = location.path + return record } return { From b0fa787ca46c9992035032b40abffca273dcd10d Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 17:10:12 -0300 Subject: [PATCH 03/17] feat(optimized matcher): More tests passing. --- src/create-matcher.js | 97 ++++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index c409e64e4..e121cfb7e 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -7,7 +7,7 @@ import { createRoute } from './util/route' import { fillParams } from './util/params' import { createRouteMap } from './create-route-map' import { normalizeLocation } from './util/location' -// import { decode } from './util/query' +import { decode } from './util/query' export type Matcher = { match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; @@ -22,18 +22,18 @@ export function createMatcher ( ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) - const _optimizedMatcher = optimizedMatcher(routes) + var _optimizedMatcher = optimizedMatcher(pathMap) function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) - _optimizedMatcher.addRoutes(routes) + _optimizedMatcher = optimizedMatcher(pathMap) } function addRoute (parentOrRoute, route) { const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined // $flow-disable-line createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent) - _optimizedMatcher.addRoute(parent && parent.path, route || parentOrRoute) + _optimizedMatcher = optimizedMatcher(pathMap) // add aliases of parent if (parent && parent.alias.length) { @@ -198,47 +198,45 @@ export function createMatcher ( } } -// function matchRoute ( -// regex: RouteRegExp, -// path: string, -// params: Object -// ): boolean { -// const m = path.match(regex) - -// if (!m) { -// return false -// } else if (!params) { -// return true -// } - -// for (let i = 1, len = m.length; i < len; ++i) { -// const key = regex.keys[i - 1] -// if (key) { -// // Fix #1994: using * with props: true generates a param named 0 -// params[key.name || 'pathMatch'] = typeof m[i] === 'string' ? decode(m[i]) : m[i] -// } -// } - -// return true -// } +function matchRoute ( + regex: RouteRegExp, + path: string, + params: Object +): boolean { + const m = path.match(regex) + if (!m) { + return false + } else if (!params) { + return true + } + + for (let i = 1, len = m.length; i < len; ++i) { + const key = regex.keys[i - 1] + if (key) { + // Fix #1994: using * with props: true generates a param named 0 + params[key.name || 'pathMatch'] = typeof m[i] === 'string' ? decode(m[i]) : m[i] + } + } + + return true +} function resolveRecordPath (path: string, record: RouteRecord): string { return resolvePath(path, record.parent ? record.parent.path : '/', true) } -// function isStaticPath (path) { -// // Dynamic paths have /:placeholder or anonymous placeholder /(.+) or wildcard *. -// return !/[:(*]/.exec(path) -// } +function isStaticPath (path) { + // Dynamic paths have /:placeholder or anonymous placeholder /(.+) or wildcard *. + return !/[:(*]/.exec(path) +} -function optimizedMatcher (routes: Array): Matcher { +function optimizedMatcher (pathMap: Dictionary): Matcher { // staticMap keys are always full paths. const staticMap = {} - // dynamicMap keys are prefixes. e.g. /, /onelevel/, /onelevel/twolevel/ - // const dynamicMap = {} - // const staticLevels = 0 - addRoutes(routes) + const dynamics = [] + + addRoutes(Object.values(pathMap)) function addRoutes (routes) { routes.forEach(route => addRoute(null, route)) @@ -249,7 +247,11 @@ function optimizedMatcher (routes: Array): Matcher { if (parentPath) { path = `${parentPath}/${route.path}` } - staticMap[path] = route + if (isStaticPath(path)) { + staticMap[path] = route + } else { + dynamics.push(route) + } if (route.children) { route.children.forEach(child => { @@ -259,8 +261,25 @@ function optimizedMatcher (routes: Array): Matcher { } function match (location) { - const record = staticMap[location.path] || staticMap['*'] - location.params['pathMatch'] = location.path + let record = staticMap[location.path] + let key + if (!record) { + for (var i = 0; i < dynamics.length; i++) { + const dynamicRoute = dynamics[i] + if (matchRoute(dynamicRoute.regex, location.path, location.params)) { + return dynamicRoute + } + } + + if (staticMap['*']) { + record = staticMap['*'] + key = 'pathMatch' + } else { + key = location.path + } + } + + location.params[key] = location.path return record } From f440d4ff5c5f59043e692a65a14ddec36d7578ab Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 17:29:42 -0300 Subject: [PATCH 04/17] feat(otimized matcher): The * is not static. --- src/create-matcher.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index e121cfb7e..8e919ae7c 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -261,7 +261,7 @@ function optimizedMatcher (pathMap: Dictionary): Matcher { } function match (location) { - let record = staticMap[location.path] + const record = staticMap[location.path.replace(/\/$/, '')] let key if (!record) { for (var i = 0; i < dynamics.length; i++) { @@ -270,13 +270,7 @@ function optimizedMatcher (pathMap: Dictionary): Matcher { return dynamicRoute } } - - if (staticMap['*']) { - record = staticMap['*'] - key = 'pathMatch' - } else { - key = location.path - } + key = location.path } location.params[key] = location.path From 69d8eefee30878eadb1ac3a54cb689cfc114914e Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 17:43:44 -0300 Subject: [PATCH 05/17] feat(optimized matcher): Remove hard coded params. --- src/create-matcher.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index 8e919ae7c..dc988ca85 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -262,7 +262,6 @@ function optimizedMatcher (pathMap: Dictionary): Matcher { function match (location) { const record = staticMap[location.path.replace(/\/$/, '')] - let key if (!record) { for (var i = 0; i < dynamics.length; i++) { const dynamicRoute = dynamics[i] @@ -270,10 +269,7 @@ function optimizedMatcher (pathMap: Dictionary): Matcher { return dynamicRoute } } - key = location.path } - - location.params[key] = location.path return record } From fb73454b66640cd7addd08b695baf40909ceee6f Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 17:58:37 -0300 Subject: [PATCH 06/17] feat(optimized matcher): Add match ordering test. --- test/unit/specs/create-matcher.spec.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/unit/specs/create-matcher.spec.js b/test/unit/specs/create-matcher.spec.js index fcc2587c2..284c35aea 100644 --- a/test/unit/specs/create-matcher.spec.js +++ b/test/unit/specs/create-matcher.spec.js @@ -151,4 +151,26 @@ describe('Creating Matcher', function () { expect(pathForErrorRoute).toEqual('/error/') expect(pathForNotFoundRoute).toEqual('/') }) + + it('respect creation ordering', function () { + const matcher = createMatcher([ + { + path: '/p/staticbefore', + name: 'staticbefore', + component: { name: 'staticbefore' } + }, + { + path: '/p/:id', + name: 'dynamic', + component: { name: 'dynamic' } + }, + { + path: '/p/staticafter', + name: 'staticafter', + component: { name: 'staticafter' } + } + ]) + expect(matcher.match('/p/staticbefore').name).toBe('staticbefore') + expect(matcher.match('/p/staticafter').name).toBe('dynamic') + }) }) From 88dbaa79348e462a801b30184bf82dc59a89188e Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 18:55:18 -0300 Subject: [PATCH 07/17] feat(optimized matcher): Use pathList to respect ordering. --- src/create-matcher.js | 64 ++++++++++---------------- test/unit/specs/create-matcher.spec.js | 2 +- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index dc988ca85..53961c3c2 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -22,18 +22,18 @@ export function createMatcher ( ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) - var _optimizedMatcher = optimizedMatcher(pathMap) + var _optimizedMatcher = optimizedMatcher(pathList, pathMap) function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) - _optimizedMatcher = optimizedMatcher(pathMap) + _optimizedMatcher = optimizedMatcher(pathList, pathMap) } function addRoute (parentOrRoute, route) { const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined // $flow-disable-line createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent) - _optimizedMatcher = optimizedMatcher(pathMap) + _optimizedMatcher = optimizedMatcher(pathList, pathMap) // add aliases of parent if (parent && parent.alias.length) { @@ -86,7 +86,7 @@ export function createMatcher ( return _createRoute(record, location, redirectedFrom) } else if (location.path) { location.params = {} - const record = _optimizedMatcher.match(location) + const record = _optimizedMatcher(location) if (record) { return _createRoute(record, location, redirectedFrom) } @@ -230,52 +230,36 @@ function isStaticPath (path) { return !/[:(*]/.exec(path) } -function optimizedMatcher (pathMap: Dictionary): Matcher { - // staticMap keys are always full paths. +function optimizedMatcher ( + pathList: Array, + pathMap: Dictionary +) { const staticMap = {} - const dynamics = [] - addRoutes(Object.values(pathMap)) - - function addRoutes (routes) { - routes.forEach(route => addRoute(null, route)) - } - - function addRoute (parentPath, route) { - let path = route.path - if (parentPath) { - path = `${parentPath}/${route.path}` - } + pathList.forEach((path, index) => { + const route = pathMap[path] if (isStaticPath(path)) { - staticMap[path] = route + staticMap[path] = { index, route } } else { - dynamics.push(route) + dynamics.push({ index, route }) } - - if (route.children) { - route.children.forEach(child => { - addRoute(path, child) - }) - } - } + }) function match (location) { - const record = staticMap[location.path.replace(/\/$/, '')] - if (!record) { - for (var i = 0; i < dynamics.length; i++) { - const dynamicRoute = dynamics[i] - if (matchRoute(dynamicRoute.regex, location.path, location.params)) { - return dynamicRoute - } + const staticRecord = staticMap[location.path.replace(/\/$/, '')] + + for (var i = 0; i < dynamics.length; i++) { + const { index, route } = dynamics[i] + if (staticRecord && index >= staticRecord.index) { + break + } + if (matchRoute(route.regex, location.path, location.params)) { + return route } } - return record + return staticRecord && staticRecord.route } - return { - addRoutes, - addRoute, - match - } + return match } diff --git a/test/unit/specs/create-matcher.spec.js b/test/unit/specs/create-matcher.spec.js index 284c35aea..2d9a8f729 100644 --- a/test/unit/specs/create-matcher.spec.js +++ b/test/unit/specs/create-matcher.spec.js @@ -152,7 +152,7 @@ describe('Creating Matcher', function () { expect(pathForNotFoundRoute).toEqual('/') }) - it('respect creation ordering', function () { + it('respect ordering', function () { const matcher = createMatcher([ { path: '/p/staticbefore', From 1330f4c6de47faf889bee550576655bb84ff1dbb Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 19:18:53 -0300 Subject: [PATCH 08/17] feat(optimized matcher): Fix flaky test. 1) error handling async component errors Message: Expected spy warn to have been called once. It was called 0 times. Stack: Error: Expected spy warn to have been called once. It was called 0 times. at /home/iurisilvio/code/oss/vue-router/test/unit/specs/error-handling.spec.js:157:30 at at processTicksAndRejections (internal/process/task_queues.js:95:5) --- test/unit/specs/error-handling.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/unit/specs/error-handling.spec.js b/test/unit/specs/error-handling.spec.js index 52d61e210..a9b8e11e8 100644 --- a/test/unit/specs/error-handling.spec.js +++ b/test/unit/specs/error-handling.spec.js @@ -5,6 +5,10 @@ import { NavigationFailureType } from '../../../src/util/errors' Vue.use(VueRouter) describe('error handling', () => { + beforeEach(function () { + process.env.NODE_ENV = 'development' + }) + it('onReady errors', done => { const router = new VueRouter() const err = new Error('foo') From 44f4bf59a119e3c829c0999a6b3a3a976a817658 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 22:30:39 -0300 Subject: [PATCH 09/17] feat(optimized matcher): Test sensitive flag for static routes. --- src/create-matcher.js | 13 ++++++++++++- test/unit/specs/create-matcher.spec.js | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index 53961c3c2..fa9e49d94 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -241,13 +241,24 @@ function optimizedMatcher ( const route = pathMap[path] if (isStaticPath(path)) { staticMap[path] = { index, route } + if (route.regex.ignoreCase) { + staticMap[path.toLowerCase()] = { index, route } + } } else { dynamics.push({ index, route }) } }) function match (location) { - const staticRecord = staticMap[location.path.replace(/\/$/, '')] + const cleanPath = location.path.replace(/\/$/, '') + let staticRecord = staticMap[cleanPath] + + if (!staticRecord) { + const staticRecordInsensitive = staticMap[cleanPath.toLowerCase()] + if (staticRecordInsensitive && staticRecordInsensitive.route.regex.ignoreCase) { + staticRecord = staticRecordInsensitive + } + } for (var i = 0; i < dynamics.length; i++) { const { index, route } = dynamics[i] diff --git a/test/unit/specs/create-matcher.spec.js b/test/unit/specs/create-matcher.spec.js index 2d9a8f729..4d2500dba 100644 --- a/test/unit/specs/create-matcher.spec.js +++ b/test/unit/specs/create-matcher.spec.js @@ -173,4 +173,28 @@ describe('Creating Matcher', function () { expect(matcher.match('/p/staticbefore').name).toBe('staticbefore') expect(matcher.match('/p/staticafter').name).toBe('dynamic') }) + + it('static can be sensitive', function () { + const matcher = createMatcher([ + { + path: '/p/sensitive', + name: 'sensitive', + pathToRegexpOptions: { + sensitive: true + }, + component: { name: 'sensitive' } + }, + { + path: '/p/insensitive', + name: 'insensitive', + component: { name: 'insensitive' } + }, + { + path: '*', name: 'not-found', component: { name: 'not-found ' } + } + ]) + + expect(matcher.match('/p/SENSITIVE').name).toBe('not-found') + expect(matcher.match('/p/INSENSITIVE').name).toBe('insensitive') + }) }) From 5ba726c42d76a4261f55491292acdd05024bd25e Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sat, 29 Jan 2022 23:07:17 -0300 Subject: [PATCH 10/17] feat(optimized matcher): Handle custom regexp options as dynamic routes. --- src/create-matcher.js | 12 ++++--- src/create-route-map.js | 1 + test/unit/specs/create-matcher.spec.js | 50 +++++++++++++++++++++++++- types/router.d.ts | 1 + 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index fa9e49d94..7f536e2b1 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -225,9 +225,13 @@ function resolveRecordPath (path: string, record: RouteRecord): string { return resolvePath(path, record.parent ? record.parent.path : '/', true) } -function isStaticPath (path) { - // Dynamic paths have /:placeholder or anonymous placeholder /(.+) or wildcard *. - return !/[:(*]/.exec(path) +function isStatic (route) { + return ( + // Custom regex options are dynamic. + Object.keys(route.pathToRegexpOptions).length === 0 && + // Dynamic paths have /:placeholder or anonymous placeholder /(.+) or wildcard *. + !/[:(*]/.exec(route.path) + ) } function optimizedMatcher ( @@ -239,7 +243,7 @@ function optimizedMatcher ( pathList.forEach((path, index) => { const route = pathMap[path] - if (isStaticPath(path)) { + if (isStatic(route)) { staticMap[path] = { index, route } if (route.regex.ignoreCase) { staticMap[path.toLowerCase()] = { index, route } diff --git a/src/create-route-map.js b/src/create-route-map.js index 93a558f8a..61affa3f1 100644 --- a/src/create-route-map.js +++ b/src/create-route-map.js @@ -92,6 +92,7 @@ function addRouteRecord ( const record: RouteRecord = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), + pathToRegexpOptions, components: route.components || { default: route.component }, alias: route.alias ? typeof route.alias === 'string' diff --git a/test/unit/specs/create-matcher.spec.js b/test/unit/specs/create-matcher.spec.js index 4d2500dba..af9a1f30e 100644 --- a/test/unit/specs/create-matcher.spec.js +++ b/test/unit/specs/create-matcher.spec.js @@ -174,7 +174,7 @@ describe('Creating Matcher', function () { expect(matcher.match('/p/staticafter').name).toBe('dynamic') }) - it('static can be sensitive', function () { + it('static can use sensitive flag', function () { const matcher = createMatcher([ { path: '/p/sensitive', @@ -197,4 +197,52 @@ describe('Creating Matcher', function () { expect(matcher.match('/p/SENSITIVE').name).toBe('not-found') expect(matcher.match('/p/INSENSITIVE').name).toBe('insensitive') }) + + it('static can use strict flag', function () { + const matcher = createMatcher([ + { + path: '/p/strict', + name: 'strict', + pathToRegexpOptions: { + strict: true + }, + component: { name: 'strict' } + }, + { + path: '/p/unstrict', + name: 'unstrict', + component: { name: 'unstrict' } + }, + { + path: '*', name: 'not-found', component: { name: 'not-found ' } + } + ]) + + expect(matcher.match('/p/strict/').name).toBe('not-found') + expect(matcher.match('/p/unstrict/').name).toBe('unstrict') + }) + + it('static can use end flag', function () { + const matcher = createMatcher([ + { + path: '/p/end', + name: 'end', + component: { name: 'end' } + }, + { + path: '/p/not-end', + name: 'not-end', + pathToRegexpOptions: { + end: false + }, + component: { name: 'not-end' } + }, + { + path: '*', name: 'not-found', component: { name: 'not-found ' } + } + ]) + + expect(matcher.match('/p/end/foo').name).toBe('not-found') + expect(matcher.match('/p/not-end/foo').name).toBe('not-end') + }) }) diff --git a/types/router.d.ts b/types/router.d.ts index f8588f42f..4c5b63f3f 100644 --- a/types/router.d.ts +++ b/types/router.d.ts @@ -147,6 +147,7 @@ export type RouteConfig = RouteConfigSingleView | RouteConfigMultipleViews export interface RouteRecord { path: string regex: RegExp + pathToRegexpOptions: PathToRegexpOptions components: Dictionary instances: Dictionary name?: string From 4cd937fbe6ee49863d4d2a7533c4bc22a47f5b1b Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sun, 30 Jan 2022 00:35:58 -0300 Subject: [PATCH 11/17] feat(optimized matcher): minors. --- src/create-matcher.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/create-matcher.js b/src/create-matcher.js index 7f536e2b1..2c2c731c5 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -204,6 +204,7 @@ function matchRoute ( params: Object ): boolean { const m = path.match(regex) + if (!m) { return false } else if (!params) { From 56e0011c686ab62f6b81b08f0dd9a79815bdfbd0 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sun, 30 Jan 2022 00:46:18 -0300 Subject: [PATCH 12/17] feat(optimized matcher): minors. --- src/create-matcher.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index 2c2c731c5..b331056a5 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -86,7 +86,7 @@ export function createMatcher ( return _createRoute(record, location, redirectedFrom) } else if (location.path) { location.params = {} - const record = _optimizedMatcher(location) + const record = _optimizedMatcher(location.path, location.params) if (record) { return _createRoute(record, location, redirectedFrom) } @@ -254,8 +254,8 @@ function optimizedMatcher ( } }) - function match (location) { - const cleanPath = location.path.replace(/\/$/, '') + function match (path, params) { + const cleanPath = path.replace(/\/$/, '') let staticRecord = staticMap[cleanPath] if (!staticRecord) { @@ -270,7 +270,7 @@ function optimizedMatcher ( if (staticRecord && index >= staticRecord.index) { break } - if (matchRoute(route.regex, location.path, location.params)) { + if (matchRoute(route.regex, path, params)) { return route } } From bc46fa86d258593555f7c098ba4550ba7444ae28 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sun, 30 Jan 2022 01:03:46 -0300 Subject: [PATCH 13/17] feat(optimized matcher): remove insensitive handling. It is handled as dynamic route. --- src/create-matcher.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index b331056a5..b39c903ec 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -256,14 +256,7 @@ function optimizedMatcher ( function match (path, params) { const cleanPath = path.replace(/\/$/, '') - let staticRecord = staticMap[cleanPath] - - if (!staticRecord) { - const staticRecordInsensitive = staticMap[cleanPath.toLowerCase()] - if (staticRecordInsensitive && staticRecordInsensitive.route.regex.ignoreCase) { - staticRecord = staticRecordInsensitive - } - } + const staticRecord = staticMap[cleanPath] for (var i = 0; i < dynamics.length; i++) { const { index, route } = dynamics[i] From 9090cfdf1bd4446927c24cecf592cc39a76bb181 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sun, 30 Jan 2022 01:09:21 -0300 Subject: [PATCH 14/17] Revert "feat(optimized matcher): remove insensitive handling." This reverts commit bc46fa86d258593555f7c098ba4550ba7444ae28. --- src/create-matcher.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index b39c903ec..b331056a5 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -256,7 +256,14 @@ function optimizedMatcher ( function match (path, params) { const cleanPath = path.replace(/\/$/, '') - const staticRecord = staticMap[cleanPath] + let staticRecord = staticMap[cleanPath] + + if (!staticRecord) { + const staticRecordInsensitive = staticMap[cleanPath.toLowerCase()] + if (staticRecordInsensitive && staticRecordInsensitive.route.regex.ignoreCase) { + staticRecord = staticRecordInsensitive + } + } for (var i = 0; i < dynamics.length; i++) { const { index, route } = dynamics[i] From d3fba78f82e8a6f47f4f702f439d0bf2bb8b03c5 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sun, 30 Jan 2022 10:14:46 -0300 Subject: [PATCH 15/17] feat(optimized matcher): First level path buckets. --- src/create-matcher.js | 62 ++++++++++++++++++------ test/unit/specs/create-matcher.spec.js | 65 ++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index b331056a5..1127b169d 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -22,7 +22,7 @@ export function createMatcher ( ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) - var _optimizedMatcher = optimizedMatcher(pathList, pathMap) + let _optimizedMatcher = optimizedMatcher(pathList, pathMap) function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) @@ -235,46 +235,78 @@ function isStatic (route) { ) } +function firstLevelStatic (path) { + // firstLevel = ['/p/', '/p', '/' index: 0, input: '/p/b/c', groups: undefined] + const firstLevel = /^(\/[^:(*/]+)(\/|$)/.exec(path) + return firstLevel && firstLevel[1] +} + function optimizedMatcher ( pathList: Array, pathMap: Dictionary ) { + // Lookup table + // e.g. Route /p/b is mapped as {"/p/b": route} const staticMap = {} - const dynamics = [] + // Buckets with the first level static path. + // e.g. Route /p/b/:id is mapped as {"/p": [route]} + const firstLevelStaticMap = {} + // Array of everything else. + const dynamic = [] pathList.forEach((path, index) => { const route = pathMap[path] + const normalizedPath = route.regex.ignoreCase ? path.toLowerCase() : path if (isStatic(route)) { - staticMap[path] = { index, route } - if (route.regex.ignoreCase) { - staticMap[path.toLowerCase()] = { index, route } - } + staticMap[normalizedPath] = { index, route } } else { - dynamics.push({ index, route }) + const firstLevel = firstLevelStatic(normalizedPath.toLowerCase()) + if (firstLevel) { + if (!firstLevelStaticMap[firstLevel]) { + firstLevelStaticMap[firstLevel] = [] + } + firstLevelStaticMap[firstLevel].push({ index, route }) + } else { + dynamic.push({ index, route }) + } } }) function match (path, params) { const cleanPath = path.replace(/\/$/, '') - let staticRecord = staticMap[cleanPath] + let record = staticMap[cleanPath.toLowerCase()] - if (!staticRecord) { + if (!record) { const staticRecordInsensitive = staticMap[cleanPath.toLowerCase()] if (staticRecordInsensitive && staticRecordInsensitive.route.regex.ignoreCase) { - staticRecord = staticRecordInsensitive + record = staticRecordInsensitive + } + } + + const firstLevel = firstLevelStatic(path) + const firstLevelRoutes = firstLevelStaticMap[firstLevel && firstLevel.toLowerCase()] || [] + + for (let i = 0; i < firstLevelRoutes.length; i++) { + const { index, route } = firstLevelRoutes[i] + if (record && index >= record.index) { + break + } + if (matchRoute(route.regex, path, params)) { + record = firstLevelRoutes[i] + break } } - for (var i = 0; i < dynamics.length; i++) { - const { index, route } = dynamics[i] - if (staticRecord && index >= staticRecord.index) { + for (let j = 0; j < dynamic.length; j++) { + const { index, route } = dynamic[j] + if (record && index >= record.index) { break } if (matchRoute(route.regex, path, params)) { - return route + record = dynamic[j] } } - return staticRecord && staticRecord.route + return record && record.route } return match diff --git a/test/unit/specs/create-matcher.spec.js b/test/unit/specs/create-matcher.spec.js index af9a1f30e..97da43b51 100644 --- a/test/unit/specs/create-matcher.spec.js +++ b/test/unit/specs/create-matcher.spec.js @@ -174,6 +174,50 @@ describe('Creating Matcher', function () { expect(matcher.match('/p/staticafter').name).toBe('dynamic') }) + it('respect ordering for full dynamic', function () { + const matcher = createMatcher([ + { + path: '/before/static', + name: 'staticbefore', + component: { name: 'staticbefore' } + }, + { + path: '/:foo/static', + name: 'dynamic', + component: { name: 'dynamic' } + }, + { + path: '/after/static', + name: 'staticafter', + component: { name: 'staticafter' } + } + ]) + expect(matcher.match('/before/static').name).toBe('staticbefore') + expect(matcher.match('/after/static').name).toBe('dynamic') + }) + + it('respect ordering between full dynamic and first level static', function () { + const matcher = createMatcher([ + { + path: '/before/:foo', + name: 'staticbefore', + component: { name: 'staticbefore' } + }, + { + path: '/:foo/static', + name: 'dynamic', + component: { name: 'dynamic' } + }, + { + path: '/after/:foo', + name: 'staticafter', + component: { name: 'staticafter' } + } + ]) + expect(matcher.match('/before/static').name).toBe('staticbefore') + expect(matcher.match('/after/static').name).toBe('dynamic') + }) + it('static can use sensitive flag', function () { const matcher = createMatcher([ { @@ -245,4 +289,25 @@ describe('Creating Matcher', function () { expect(matcher.match('/p/end/foo').name).toBe('not-found') expect(matcher.match('/p/not-end/foo').name).toBe('not-end') }) + + it('first level dynamic must work', function () { + const matcher = createMatcher([ + { + path: '/:foo/b', + name: 'b', + component: { name: 'b' } + }, + { + path: '/p/c', + name: 'c', + component: { name: 'c' } + }, + { + path: '*', name: 'not-found', component: { name: 'not-found ' } + } + ]) + + expect(matcher.match('/p/b').name).toBe('b') + expect(matcher.match('/p/c').name).toBe('c') + }) }) From 0cdab2afe311877c4453a1db85d5e6c325966f18 Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sun, 30 Jan 2022 17:06:41 -0300 Subject: [PATCH 16/17] feat(optimized matcher): flow --- flow/declarations.js | 1 + 1 file changed, 1 insertion(+) diff --git a/flow/declarations.js b/flow/declarations.js index 824c3eca0..8377f789f 100644 --- a/flow/declarations.js +++ b/flow/declarations.js @@ -77,6 +77,7 @@ declare type RouteRecord = { beforeEnter: ?NavigationGuard; meta: any; props: boolean | Object | Function | Dictionary; + pathToRegexpOptions: PathToRegexpOptions; } declare type Location = { From 7b4b0843bd08c25a3b10da5fa828f01ade7bc5cd Mon Sep 17 00:00:00 2001 From: Iuri de Silvio Date: Sun, 30 Jan 2022 17:20:44 -0300 Subject: [PATCH 17/17] feat(optimized matcher): Optimization works only when it is insensitive. --- src/create-matcher.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/create-matcher.js b/src/create-matcher.js index 1127b169d..306294e7b 100644 --- a/src/create-matcher.js +++ b/src/create-matcher.js @@ -260,7 +260,7 @@ function optimizedMatcher ( if (isStatic(route)) { staticMap[normalizedPath] = { index, route } } else { - const firstLevel = firstLevelStatic(normalizedPath.toLowerCase()) + const firstLevel = firstLevelStatic(path.toLowerCase()) if (firstLevel) { if (!firstLevelStaticMap[firstLevel]) { firstLevelStaticMap[firstLevel] = [] @@ -273,18 +273,11 @@ function optimizedMatcher ( }) function match (path, params) { - const cleanPath = path.replace(/\/$/, '') - let record = staticMap[cleanPath.toLowerCase()] - - if (!record) { - const staticRecordInsensitive = staticMap[cleanPath.toLowerCase()] - if (staticRecordInsensitive && staticRecordInsensitive.route.regex.ignoreCase) { - record = staticRecordInsensitive - } - } + const cleanPath = path.replace(/\/$/, '').toLowerCase() + let record = staticMap[cleanPath] const firstLevel = firstLevelStatic(path) - const firstLevelRoutes = firstLevelStaticMap[firstLevel && firstLevel.toLowerCase()] || [] + const firstLevelRoutes = firstLevelStaticMap[firstLevel] || [] for (let i = 0; i < firstLevelRoutes.length; i++) { const { index, route } = firstLevelRoutes[i]