From b21c685c25ce5c495a98c2b58e3931307ab91328 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 27 Jul 2025 21:35:25 -0500 Subject: [PATCH 01/17] feat: add _mapRoutes method to list registered routes Signed-off-by: Sebastian Beltran --- index.js | 98 +++++++++++++++++++++++++++++++++++++++ lib/layer.js | 3 ++ test/mapRoutes.js | 115 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 test/mapRoutes.js diff --git a/index.js b/index.js index 4358aeb..28e53c1 100644 --- a/index.js +++ b/index.js @@ -441,6 +441,27 @@ Router.prototype.route = function route (path) { return route } +/** + * List all registered routes. + * + * @return {Array} An array of route paths + * @private + */ +Router.prototype._mapRoutes = function mapRoutes () { + const routes = [] + const stack = this.stack + const routeMap = new Map() + + collectRoutes(stack, '', routeMap) + + // Convert Map to array of route objects + for (const [path, methods] of routeMap.entries()) { + routes.push({ path, methods }) + } + + return routes +} + // create Router#VERB functions methods.concat('all').forEach(function (method) { Router.prototype[method] = function (path) { @@ -450,6 +471,83 @@ methods.concat('all').forEach(function (method) { } }) +/** + * Add a route to the map with the given path and methods. + * @param {Map} routeMap + * @param {string} path + * @param {Array} methods + * @private + */ +function addRouteToMap (routeMap, path, methods) { + if (routeMap.has(path)) { + const existingMethods = routeMap.get(path) + for (const method of methods) { + if (!existingMethods.includes(method)) { + existingMethods.push(method) + } + } + } else { + routeMap.set(path, [...methods]) + } +} + +/** + * Normalize a path by removing trailing slashes. + * @param {string} path + * @return {string} normalized path + * @private + */ +function normalizePath (path) { + if (typeof path !== 'string') { + return path + } + + if (path.endsWith('/') && path.length > 1) { + return path.slice(0, -1) + } + + return path +} + +/** + * Collect routes from a router stack recursively. + * + * @param {Array} stack - The router stack to collect routes from + * @param {string} prefix - The path prefix to prepend to routes + * @param {Map} routeMap - The map to store collected routes + * @private + */ +function collectRoutes (stack, prefix, routeMap) { + for (const layer of stack) { + // for routes without a .use + if (layer.pathPatterns && layer.route) { + const methods = Object.keys(layer.route.methods).map((method) => method.toUpperCase()) + if (Array.isArray(layer.pathPatterns)) { + for (const pathPattern of layer.pathPatterns) { + const fullPath = prefix + pathPattern + addRouteToMap(routeMap, fullPath, methods) + } + } else { + const fullPath = prefix + layer.pathPatterns + addRouteToMap(routeMap, fullPath, methods) + } + } + + // for layers with a .use (mounted routers) + if (layer.pathPatterns && layer.handle && layer.handle.stack && !layer.route) { + if (Array.isArray(layer.pathPatterns)) { + for (const pathPattern of layer.pathPatterns) { + const pathPrefix = prefix + normalizePath(pathPattern) + collectRoutes(layer.handle.stack, pathPrefix, routeMap) + } + } else { + const pathPrefix = prefix + normalizePath(layer.pathPatterns) + collectRoutes(layer.handle.stack, pathPrefix, routeMap) + } + } + } +} + /** * Generate a callback that will make an OPTIONS response. * diff --git a/lib/layer.js b/lib/layer.js index 6a4408f..49f7805 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -43,7 +43,10 @@ function Layer (path, options, fn) { this.keys = [] this.name = fn.name || '' this.params = undefined + // path is determinate in runtime execution this.path = undefined + + this.pathPatterns = path this.slash = path === '/' && opts.end === false function matcher (_path) { diff --git a/test/mapRoutes.js b/test/mapRoutes.js new file mode 100644 index 0000000..79099a9 --- /dev/null +++ b/test/mapRoutes.js @@ -0,0 +1,115 @@ +const { it, describe } = require('mocha') +const Router = require('..') +const utils = require('./support/utils') + +const assert = utils.assert + +describe('_mapRoutes', function () { + it('should return empty array for router with no registered routes', function () { + const router = new Router() + + assert.deepStrictEqual(router._mapRoutes(), []) + }) + + it('should map different route types including strings, regex patterns, and parameter routes', function () { + const router = new Router() + + router.all('/', noop) + router.route('/test2/') + router.route('/test/').get(noop) + router.all(/^\/[a-z]oo$/, noop) + router.get(['/foo', '/bar'], noop) + router.post('/:id/setting/:thing', noop) + + assert.deepStrictEqual(router._mapRoutes(), + [ + { path: '/', methods: ['_ALL'] }, + { path: '/test2/', methods: [] }, + { path: '/test/', methods: ['GET'] }, + { path: '/^\\/[a-z]oo$/', methods: ['_ALL'] }, + { path: '/foo', methods: ['GET'] }, + { path: '/bar', methods: ['GET'] }, + { path: '/:id/setting/:thing', methods: ['POST'] } + ]) + }) + + it('should consolidate HTTP methods for routes registered multiple times', function () { + const router = new Router() + router.post(['/test', '/test2'], noop) + + for (let i = 0; i < 20; i++) { + router.get(['/test', '/test3'], noop) + } + + router.put('/test3', noop) + + assert.deepStrictEqual(router._mapRoutes(), [ + { path: '/test', methods: ['POST', 'GET'] }, + { path: '/test2', methods: ['POST'] }, + { path: '/test3', methods: ['GET', 'PUT'] } + ]) + }) + + it('should deduplicate routes and flatten nested router paths correctly', function () { + const router = new Router() + const inner = new Router() + router.post('/test', noop) + + for (let i = 0; i < 100; i++) { + router.get('/test', noop) + } + + for (let i = 0; i < 20; i++) { + inner.get('/test', noop) + } + + router.use(['/test/', '/test2', '/test3'], inner) + router.use('/test4/', inner) + + assert.deepStrictEqual(router._mapRoutes(), [ + { path: '/test', methods: ['POST', 'GET'] }, + { path: '/test/test', methods: ['GET'] }, + { path: '/test2/test', methods: ['GET'] }, + { path: '/test3/test', methods: ['GET'] }, + { path: '/test4/test', methods: ['GET'] } + ]) + }) + + it('should handle complex nested router hierarchies with multiple mount points', function () { + const router = new Router() + const inner = new Router() + const subinner = new Router() + + subinner.put('/t5', noop) + subinner.all(/^\/[a-z]oo$/, noop) + subinner.use(noop) + + inner.use('/t3', subinner) + inner.all('/t4', noop) + inner.get('/', noop) + inner.use(noop) + + router.use('/t2', inner) + router.use(['/t5', '/t7'], inner) + + router.use(noop) + router.use('/test1', noop) + + assert.deepStrictEqual(router._mapRoutes(), [ + { path: '/t2/t3/t5', methods: ['PUT'] }, + { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'] }, + { path: '/t2/t4', methods: ['_ALL'] }, + { path: '/t2/', methods: ['GET'] }, + { path: '/t5/t3/t5', methods: ['PUT'] }, + { path: '/t5/t3/^\\/[a-z]oo$/', methods: ['_ALL'] }, + { path: '/t5/t4', methods: ['_ALL'] }, + { path: '/t5/', methods: ['GET'] }, + { path: '/t7/t3/t5', methods: ['PUT'] }, + { path: '/t7/t3/^\\/[a-z]oo$/', methods: ['_ALL'] }, + { path: '/t7/t4', methods: ['_ALL'] }, + { path: '/t7/', methods: ['GET'] } + ]) + }) +}) + +function noop () {} From 73fae834a588c9925cef6fa25c8ae4f6169f63c4 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 28 Jul 2025 18:30:43 -0500 Subject: [PATCH 02/17] fix: don't create double slashes when mounting at root Signed-off-by: Sebastian Beltran --- index.js | 8 ++++---- test/mapRoutes.js | 26 ++++++++++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index 28e53c1..cd0618b 100644 --- a/index.js +++ b/index.js @@ -445,9 +445,9 @@ Router.prototype.route = function route (path) { * List all registered routes. * * @return {Array} An array of route paths - * @private + * @public */ -Router.prototype._mapRoutes = function mapRoutes () { +Router.prototype.mapRoutes = function mapRoutes () { const routes = [] const stack = this.stack const routeMap = new Map() @@ -524,11 +524,11 @@ function collectRoutes (stack, prefix, routeMap) { const methods = Object.keys(layer.route.methods).map((method) => method.toUpperCase()) if (Array.isArray(layer.pathPatterns)) { for (const pathPattern of layer.pathPatterns) { - const fullPath = prefix + pathPattern + const fullPath = prefix === '/' ? pathPattern : prefix + pathPattern addRouteToMap(routeMap, fullPath, methods) } } else { - const fullPath = prefix + layer.pathPatterns + const fullPath = prefix === '/' ? layer.pathPatterns : prefix + layer.pathPatterns addRouteToMap(routeMap, fullPath, methods) } } diff --git a/test/mapRoutes.js b/test/mapRoutes.js index 79099a9..cb3e7f2 100644 --- a/test/mapRoutes.js +++ b/test/mapRoutes.js @@ -4,11 +4,11 @@ const utils = require('./support/utils') const assert = utils.assert -describe('_mapRoutes', function () { +describe('mapRoutes', function () { it('should return empty array for router with no registered routes', function () { const router = new Router() - assert.deepStrictEqual(router._mapRoutes(), []) + assert.deepStrictEqual(router.mapRoutes(), []) }) it('should map different route types including strings, regex patterns, and parameter routes', function () { @@ -21,7 +21,7 @@ describe('_mapRoutes', function () { router.get(['/foo', '/bar'], noop) router.post('/:id/setting/:thing', noop) - assert.deepStrictEqual(router._mapRoutes(), + assert.deepStrictEqual(router.mapRoutes(), [ { path: '/', methods: ['_ALL'] }, { path: '/test2/', methods: [] }, @@ -43,7 +43,7 @@ describe('_mapRoutes', function () { router.put('/test3', noop) - assert.deepStrictEqual(router._mapRoutes(), [ + assert.deepStrictEqual(router.mapRoutes(), [ { path: '/test', methods: ['POST', 'GET'] }, { path: '/test2', methods: ['POST'] }, { path: '/test3', methods: ['GET', 'PUT'] } @@ -66,7 +66,7 @@ describe('_mapRoutes', function () { router.use(['/test/', '/test2', '/test3'], inner) router.use('/test4/', inner) - assert.deepStrictEqual(router._mapRoutes(), [ + assert.deepStrictEqual(router.mapRoutes(), [ { path: '/test', methods: ['POST', 'GET'] }, { path: '/test/test', methods: ['GET'] }, { path: '/test2/test', methods: ['GET'] }, @@ -95,7 +95,7 @@ describe('_mapRoutes', function () { router.use(noop) router.use('/test1', noop) - assert.deepStrictEqual(router._mapRoutes(), [ + assert.deepStrictEqual(router.mapRoutes(), [ { path: '/t2/t3/t5', methods: ['PUT'] }, { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'] }, { path: '/t2/t4', methods: ['_ALL'] }, @@ -110,6 +110,20 @@ describe('_mapRoutes', function () { { path: '/t7/', methods: ['GET'] } ]) }) + + it('should not create double slashes when mounting at root /', function () { + const router = new Router() + const subRouter = new Router() + + subRouter.get('/api', () => {}) + router.use('/', subRouter) + + const routes = router.mapRoutes() + + assert.deepStrictEqual(routes, [ + { path: '/api', methods: ['GET'] } + ]) + }) }) function noop () {} From f491e42cd9bc0f30474127d44ebdbae18bc1c046 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 28 Jul 2025 20:28:27 -0500 Subject: [PATCH 03/17] [WIP] include route options Signed-off-by: Sebastian Beltran --- index.js | 49 +++++++++++++------ test/mapRoutes.js | 117 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 123 insertions(+), 43 deletions(-) diff --git a/index.js b/index.js index cd0618b..6befa01 100644 --- a/index.js +++ b/index.js @@ -450,13 +450,19 @@ Router.prototype.route = function route (path) { Router.prototype.mapRoutes = function mapRoutes () { const routes = [] const stack = this.stack + + const options = { + strict: this.strict, + caseSensitive: this.caseSensitive + } + const routeMap = new Map() - collectRoutes(stack, '', routeMap) + collectRoutes(stack, '', routeMap, options) // Convert Map to array of route objects for (const [path, methods] of routeMap.entries()) { - routes.push({ path, methods }) + routes.push({ path, ...methods }) } return routes @@ -478,16 +484,22 @@ methods.concat('all').forEach(function (method) { * @param {Array} methods * @private */ -function addRouteToMap (routeMap, path, methods) { +function addRouteToMap (routeMap, path, methods, options) { if (routeMap.has(path)) { const existingMethods = routeMap.get(path) for (const method of methods) { - if (!existingMethods.includes(method)) { - existingMethods.push(method) + if (!existingMethods.methods.includes(method)) { + existingMethods.methods.push(method) } } } else { - routeMap.set(path, [...methods]) + routeMap.set( + path, + { + methods: [...methods], + options: { strict: options.strict, caseSensitive: options.caseSensitive } + } + ) } } @@ -517,19 +529,20 @@ function normalizePath (path) { * @param {Map} routeMap - The map to store collected routes * @private */ -function collectRoutes (stack, prefix, routeMap) { +function collectRoutes (stack, prefix, routeMap, options) { for (const layer of stack) { // for routes without a .use if (layer.pathPatterns && layer.route) { const methods = Object.keys(layer.route.methods).map((method) => method.toUpperCase()) + if (Array.isArray(layer.pathPatterns)) { for (const pathPattern of layer.pathPatterns) { - const fullPath = prefix === '/' ? pathPattern : prefix + pathPattern - addRouteToMap(routeMap, fullPath, methods) + const fullPath = prefix === '/' ? pathPattern : normalizePath(prefix) + pathPattern + addRouteToMap(routeMap, fullPath, methods, options) } } else { const fullPath = prefix === '/' ? layer.pathPatterns : prefix + layer.pathPatterns - addRouteToMap(routeMap, fullPath, methods) + addRouteToMap(routeMap, fullPath, methods, options) } } @@ -537,12 +550,20 @@ function collectRoutes (stack, prefix, routeMap) { if (layer.pathPatterns && layer.handle && layer.handle.stack && !layer.route) { if (Array.isArray(layer.pathPatterns)) { for (const pathPattern of layer.pathPatterns) { - const pathPrefix = prefix + normalizePath(pathPattern) - collectRoutes(layer.handle.stack, pathPrefix, routeMap) + const pathPrefix = prefix === '/' ? normalizePath(pathPattern) : prefix + normalizePath(pathPattern) + + collectRoutes(layer.handle.stack, pathPrefix, routeMap, { + strict: layer.handle.strict, + caseSensitive: layer.handle.caseSensitive + }) } } else { - const pathPrefix = prefix + normalizePath(layer.pathPatterns) - collectRoutes(layer.handle.stack, pathPrefix, routeMap) + const pathPrefix = prefix === '/' ? normalizePath(layer.pathPatterns) : prefix + normalizePath(layer.pathPatterns) + + collectRoutes(layer.handle.stack, pathPrefix, routeMap, { + strict: layer.handle.strict, + caseSensitive: layer.handle.caseSensitive + }) } } } diff --git a/test/mapRoutes.js b/test/mapRoutes.js index cb3e7f2..03cf028 100644 --- a/test/mapRoutes.js +++ b/test/mapRoutes.js @@ -23,13 +23,13 @@ describe('mapRoutes', function () { assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/', methods: ['_ALL'] }, - { path: '/test2/', methods: [] }, - { path: '/test/', methods: ['GET'] }, - { path: '/^\\/[a-z]oo$/', methods: ['_ALL'] }, - { path: '/foo', methods: ['GET'] }, - { path: '/bar', methods: ['GET'] }, - { path: '/:id/setting/:thing', methods: ['POST'] } + { path: '/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test2/', methods: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test/', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/^\\/[a-z]oo$/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/foo', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/bar', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/:id/setting/:thing', methods: ['POST'], options: { strict: undefined, caseSensitive: undefined } } ]) }) @@ -44,9 +44,9 @@ describe('mapRoutes', function () { router.put('/test3', noop) assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/test', methods: ['POST', 'GET'] }, - { path: '/test2', methods: ['POST'] }, - { path: '/test3', methods: ['GET', 'PUT'] } + { path: '/test', methods: ['POST', 'GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test2', methods: ['POST'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test3', methods: ['GET', 'PUT'], options: { strict: undefined, caseSensitive: undefined } } ]) }) @@ -67,11 +67,11 @@ describe('mapRoutes', function () { router.use('/test4/', inner) assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/test', methods: ['POST', 'GET'] }, - { path: '/test/test', methods: ['GET'] }, - { path: '/test2/test', methods: ['GET'] }, - { path: '/test3/test', methods: ['GET'] }, - { path: '/test4/test', methods: ['GET'] } + { path: '/test', methods: ['POST', 'GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test/test', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test2/test', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test3/test', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test4/test', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } } ]) }) @@ -96,22 +96,22 @@ describe('mapRoutes', function () { router.use('/test1', noop) assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/t2/t3/t5', methods: ['PUT'] }, - { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'] }, - { path: '/t2/t4', methods: ['_ALL'] }, - { path: '/t2/', methods: ['GET'] }, - { path: '/t5/t3/t5', methods: ['PUT'] }, - { path: '/t5/t3/^\\/[a-z]oo$/', methods: ['_ALL'] }, - { path: '/t5/t4', methods: ['_ALL'] }, - { path: '/t5/', methods: ['GET'] }, - { path: '/t7/t3/t5', methods: ['PUT'] }, - { path: '/t7/t3/^\\/[a-z]oo$/', methods: ['_ALL'] }, - { path: '/t7/t4', methods: ['_ALL'] }, - { path: '/t7/', methods: ['GET'] } + { path: '/t2/t3/t5', methods: ['PUT'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t2/t4', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t2/', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/t3/t5', methods: ['PUT'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/t3/^\\/[a-z]oo$/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/t4', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/t3/t5', methods: ['PUT'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/t3/^\\/[a-z]oo$/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/t4', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } } ]) }) - it('should not create double slashes when mounting at root /', function () { + it('should avoid double slashes when mounting routers at root path', function () { const router = new Router() const subRouter = new Router() @@ -121,7 +121,66 @@ describe('mapRoutes', function () { const routes = router.mapRoutes() assert.deepStrictEqual(routes, [ - { path: '/api', methods: ['GET'] } + { path: '/api', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } } + ]) + }) + + it('should inherit router options correctly through nested hierarchies', function () { + const router = new Router({ strict: true, caseSensitive: true }) + const inner = new Router({ strict: true, caseSensitive: false }) + const subinner = new Router({ strict: false, caseSensitive: false }) + + subinner.put('/t5', noop) + subinner.use(noop) + + inner.use('/t3', subinner) + inner.all('/t4', noop) + inner.get('/', noop) + inner.use(noop) + + router.use('/t2', inner) + router.use(['/t5', '/t7'], inner) + + router.use(noop) + router.get('/test', noop) + + assert.deepStrictEqual(router.mapRoutes(), [ + { path: '/t2/t3/t5', methods: ['PUT'], options: { strict: false, caseSensitive: false } }, + { path: '/t2/t4', methods: ['_ALL'], options: { strict: true, caseSensitive: false } }, + { path: '/t2/', methods: ['GET'], options: { strict: true, caseSensitive: false } }, + { path: '/t5/t3/t5', methods: ['PUT'], options: { strict: false, caseSensitive: false } }, + { path: '/t5/t4', methods: ['_ALL'], options: { strict: true, caseSensitive: false } }, + { path: '/t5/', methods: ['GET'], options: { strict: true, caseSensitive: false } }, + { path: '/t7/t3/t5', methods: ['PUT'], options: { strict: false, caseSensitive: false } }, + { path: '/t7/t4', methods: ['_ALL'], options: { strict: true, caseSensitive: false } }, + { path: '/t7/', methods: ['GET'], options: { strict: true, caseSensitive: false } }, + { path: '/test', methods: ['GET'], options: { strict: true, caseSensitive: true } } + ]) + }) + + it('should handle routers with mixed options when mounted at same path', function () { + const router = new Router({ strict: true, caseSensitive: true }) + const inner = new Router({ strict: true, caseSensitive: false }) + const otherInner = new Router({ strict: true, caseSensitive: true }) + const otherInner2 = new Router({ strict: true, caseSensitive: false }) + + otherInner2.put('/t5', noop) + otherInner2.get('/t6', noop) + + otherInner.put('/t5', noop) + otherInner.post('/t6', noop) + + inner.use('/t2', otherInner) + inner.use('/t2', otherInner2) + + router.use(inner) + + assert.deepStrictEqual(router.mapRoutes(), [ + { path: '/t2/t5', methods: ['PUT'], options: { strict: true, caseSensitive: true } }, + // The current implementation merges methods for the same path, even if options differ. It shouldn't + { path: '/t2/t6', methods: ['POST', 'GET'], options: { strict: true, caseSensitive: true } } + // { path: '/t2/t6', methods: ['POST'], options: { strict: true, caseSensitive: true } } + // { path: '/t2/t6', methods: ['GET'], options: { strict: true, caseSensitive: false } }, ]) }) }) From 9b19b4da190c83131d374d3120ac9b639f60a919 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Mon, 28 Jul 2025 21:24:06 -0500 Subject: [PATCH 04/17] include keys for parameter routes Signed-off-by: Sebastian Beltran --- index.js | 4 ++ test/mapRoutes.js | 93 ++++++++++++++++++++++++----------------------- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/index.js b/index.js index 6befa01..b3da175 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,7 @@ const Layer = require('./lib/layer') const { METHODS } = require('node:http') const parseUrl = require('parseurl') const Route = require('./lib/route') +const pathRegexp = require('path-to-regexp') const debug = require('debug')('router') const deprecate = require('depd')('router') @@ -493,10 +494,13 @@ function addRouteToMap (routeMap, path, methods, options) { } } } else { + const { keys } = pathRegexp.pathToRegexp(path) + routeMap.set( path, { methods: [...methods], + keys, options: { strict: options.strict, caseSensitive: options.caseSensitive } } ) diff --git a/test/mapRoutes.js b/test/mapRoutes.js index 03cf028..e739517 100644 --- a/test/mapRoutes.js +++ b/test/mapRoutes.js @@ -17,19 +17,20 @@ describe('mapRoutes', function () { router.all('/', noop) router.route('/test2/') router.route('/test/').get(noop) - router.all(/^\/[a-z]oo$/, noop) + // With regex patterns path-to-regexp fail + // router.all(/^\/[a-z]oo$/, noop) router.get(['/foo', '/bar'], noop) router.post('/:id/setting/:thing', noop) assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test2/', methods: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test/', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/^\\/[a-z]oo$/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/foo', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/bar', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/:id/setting/:thing', methods: ['POST'], options: { strict: undefined, caseSensitive: undefined } } + { path: '/', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test2/', methods: [], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + // { path: '/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/foo', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/bar', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/:id/setting/:thing', methods: ['POST'], keys: [{ name: 'id', type: 'param' }, { name: 'thing', type: 'param' }], options: { strict: undefined, caseSensitive: undefined } } ]) }) @@ -44,9 +45,9 @@ describe('mapRoutes', function () { router.put('/test3', noop) assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/test', methods: ['POST', 'GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test2', methods: ['POST'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test3', methods: ['GET', 'PUT'], options: { strict: undefined, caseSensitive: undefined } } + { path: '/test', methods: ['POST', 'GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test2', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test3', methods: ['GET', 'PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } } ]) }) @@ -67,11 +68,11 @@ describe('mapRoutes', function () { router.use('/test4/', inner) assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/test', methods: ['POST', 'GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test/test', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test2/test', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test3/test', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test4/test', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } } + { path: '/test', methods: ['POST', 'GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test2/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test3/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test4/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } } ]) }) @@ -81,7 +82,7 @@ describe('mapRoutes', function () { const subinner = new Router() subinner.put('/t5', noop) - subinner.all(/^\/[a-z]oo$/, noop) + // subinner.all(/^\/[a-z]oo$/, noop) subinner.use(noop) inner.use('/t3', subinner) @@ -96,18 +97,18 @@ describe('mapRoutes', function () { router.use('/test1', noop) assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/t2/t3/t5', methods: ['PUT'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t2/t4', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t2/', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/t3/t5', methods: ['PUT'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/t3/^\\/[a-z]oo$/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/t4', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/t3/t5', methods: ['PUT'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/t3/^\\/[a-z]oo$/', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/t4', methods: ['_ALL'], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } } + { path: '/t2/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + // { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t2/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t2/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + // { path: '/t5/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + // { path: '/t7/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } } ]) }) @@ -121,7 +122,7 @@ describe('mapRoutes', function () { const routes = router.mapRoutes() assert.deepStrictEqual(routes, [ - { path: '/api', methods: ['GET'], options: { strict: undefined, caseSensitive: undefined } } + { path: '/api', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } } ]) }) @@ -145,16 +146,16 @@ describe('mapRoutes', function () { router.get('/test', noop) assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/t2/t3/t5', methods: ['PUT'], options: { strict: false, caseSensitive: false } }, - { path: '/t2/t4', methods: ['_ALL'], options: { strict: true, caseSensitive: false } }, - { path: '/t2/', methods: ['GET'], options: { strict: true, caseSensitive: false } }, - { path: '/t5/t3/t5', methods: ['PUT'], options: { strict: false, caseSensitive: false } }, - { path: '/t5/t4', methods: ['_ALL'], options: { strict: true, caseSensitive: false } }, - { path: '/t5/', methods: ['GET'], options: { strict: true, caseSensitive: false } }, - { path: '/t7/t3/t5', methods: ['PUT'], options: { strict: false, caseSensitive: false } }, - { path: '/t7/t4', methods: ['_ALL'], options: { strict: true, caseSensitive: false } }, - { path: '/t7/', methods: ['GET'], options: { strict: true, caseSensitive: false } }, - { path: '/test', methods: ['GET'], options: { strict: true, caseSensitive: true } } + { path: '/t2/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } }, + { path: '/t2/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } }, + { path: '/t2/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } }, + { path: '/t5/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } }, + { path: '/t5/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } }, + { path: '/t5/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } }, + { path: '/t7/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } }, + { path: '/t7/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } }, + { path: '/t7/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } }, + { path: '/test', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: true } } ]) }) @@ -164,11 +165,11 @@ describe('mapRoutes', function () { const otherInner = new Router({ strict: true, caseSensitive: true }) const otherInner2 = new Router({ strict: true, caseSensitive: false }) - otherInner2.put('/t5', noop) - otherInner2.get('/t6', noop) + otherInner2.put('/:t5', noop) + otherInner2.get('/:t6', noop) - otherInner.put('/t5', noop) - otherInner.post('/t6', noop) + otherInner.put('/:t5', noop) + otherInner.post('/:t6', noop) inner.use('/t2', otherInner) inner.use('/t2', otherInner2) @@ -176,9 +177,9 @@ describe('mapRoutes', function () { router.use(inner) assert.deepStrictEqual(router.mapRoutes(), [ - { path: '/t2/t5', methods: ['PUT'], options: { strict: true, caseSensitive: true } }, + { path: '/t2/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], options: { strict: true, caseSensitive: true } }, // The current implementation merges methods for the same path, even if options differ. It shouldn't - { path: '/t2/t6', methods: ['POST', 'GET'], options: { strict: true, caseSensitive: true } } + { path: '/t2/:t6', methods: ['POST', 'GET'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: true } } // { path: '/t2/t6', methods: ['POST'], options: { strict: true, caseSensitive: true } } // { path: '/t2/t6', methods: ['GET'], options: { strict: true, caseSensitive: false } }, ]) From 440a46e08e33f2e02a80210a6831a7eec619bb91 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 29 Jul 2025 14:10:48 -0500 Subject: [PATCH 05/17] rename function Signed-off-by: Sebastian Beltran --- index.js | 2 +- test/{mapRoutes.js => getRoutes.js} | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) rename test/{mapRoutes.js => getRoutes.js} (95%) diff --git a/index.js b/index.js index b3da175..4f038b5 100644 --- a/index.js +++ b/index.js @@ -448,7 +448,7 @@ Router.prototype.route = function route (path) { * @return {Array} An array of route paths * @public */ -Router.prototype.mapRoutes = function mapRoutes () { +Router.prototype.getRoutes = function getRoutes () { const routes = [] const stack = this.stack diff --git a/test/mapRoutes.js b/test/getRoutes.js similarity index 95% rename from test/mapRoutes.js rename to test/getRoutes.js index e739517..88febf3 100644 --- a/test/mapRoutes.js +++ b/test/getRoutes.js @@ -4,11 +4,11 @@ const utils = require('./support/utils') const assert = utils.assert -describe('mapRoutes', function () { +describe('getRoutes', function () { it('should return empty array for router with no registered routes', function () { const router = new Router() - assert.deepStrictEqual(router.mapRoutes(), []) + assert.deepStrictEqual(router.getRoutes(), []) }) it('should map different route types including strings, regex patterns, and parameter routes', function () { @@ -22,7 +22,7 @@ describe('mapRoutes', function () { router.get(['/foo', '/bar'], noop) router.post('/:id/setting/:thing', noop) - assert.deepStrictEqual(router.mapRoutes(), + assert.deepStrictEqual(router.getRoutes(), [ { path: '/', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, { path: '/test2/', methods: [], keys: [], options: { strict: undefined, caseSensitive: undefined } }, @@ -44,7 +44,7 @@ describe('mapRoutes', function () { router.put('/test3', noop) - assert.deepStrictEqual(router.mapRoutes(), [ + assert.deepStrictEqual(router.getRoutes(), [ { path: '/test', methods: ['POST', 'GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, { path: '/test2', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, { path: '/test3', methods: ['GET', 'PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } } @@ -67,7 +67,7 @@ describe('mapRoutes', function () { router.use(['/test/', '/test2', '/test3'], inner) router.use('/test4/', inner) - assert.deepStrictEqual(router.mapRoutes(), [ + assert.deepStrictEqual(router.getRoutes(), [ { path: '/test', methods: ['POST', 'GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, { path: '/test/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, { path: '/test2/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, @@ -96,7 +96,7 @@ describe('mapRoutes', function () { router.use(noop) router.use('/test1', noop) - assert.deepStrictEqual(router.mapRoutes(), [ + assert.deepStrictEqual(router.getRoutes(), [ { path: '/t2/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, // { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } }, { path: '/t2/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, @@ -119,7 +119,7 @@ describe('mapRoutes', function () { subRouter.get('/api', () => {}) router.use('/', subRouter) - const routes = router.mapRoutes() + const routes = router.getRoutes() assert.deepStrictEqual(routes, [ { path: '/api', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } } @@ -145,7 +145,7 @@ describe('mapRoutes', function () { router.use(noop) router.get('/test', noop) - assert.deepStrictEqual(router.mapRoutes(), [ + assert.deepStrictEqual(router.getRoutes(), [ { path: '/t2/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } }, { path: '/t2/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } }, { path: '/t2/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } }, @@ -176,7 +176,7 @@ describe('mapRoutes', function () { router.use(inner) - assert.deepStrictEqual(router.mapRoutes(), [ + assert.deepStrictEqual(router.getRoutes(), [ { path: '/t2/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], options: { strict: true, caseSensitive: true } }, // The current implementation merges methods for the same path, even if options differ. It shouldn't { path: '/t2/:t6', methods: ['POST', 'GET'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: true } } From faac557ef828ea7234e98a84d791f15810f59fff Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 29 Jul 2025 15:47:22 -0500 Subject: [PATCH 06/17] Let the routes be repeated so that consumers can decide what to do, instead of having opinions about it Signed-off-by: Sebastian Beltran --- index.js | 34 +++++++++------------------------ test/getRoutes.js | 48 +++++++++++++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/index.js b/index.js index 4f038b5..ebad802 100644 --- a/index.js +++ b/index.js @@ -457,14 +457,7 @@ Router.prototype.getRoutes = function getRoutes () { caseSensitive: this.caseSensitive } - const routeMap = new Map() - - collectRoutes(stack, '', routeMap, options) - - // Convert Map to array of route objects - for (const [path, methods] of routeMap.entries()) { - routes.push({ path, ...methods }) - } + collectRoutes(stack, '', routes, options) return routes } @@ -486,25 +479,16 @@ methods.concat('all').forEach(function (method) { * @private */ function addRouteToMap (routeMap, path, methods, options) { - if (routeMap.has(path)) { - const existingMethods = routeMap.get(path) - for (const method of methods) { - if (!existingMethods.methods.includes(method)) { - existingMethods.methods.push(method) - } - } - } else { - const { keys } = pathRegexp.pathToRegexp(path) + const { keys } = pathRegexp.pathToRegexp(path) - routeMap.set( + routeMap.push( + { path, - { - methods: [...methods], - keys, - options: { strict: options.strict, caseSensitive: options.caseSensitive } - } - ) - } + methods: [...methods], + keys, + options: { strict: options.strict, caseSensitive: options.caseSensitive } + } + ) } /** diff --git a/test/getRoutes.js b/test/getRoutes.js index 88febf3..1a208c0 100644 --- a/test/getRoutes.js +++ b/test/getRoutes.js @@ -5,13 +5,13 @@ const utils = require('./support/utils') const assert = utils.assert describe('getRoutes', function () { - it('should return empty array for router with no registered routes', function () { + it('should return an empty array when no routes are registered', function () { const router = new Router() assert.deepStrictEqual(router.getRoutes(), []) }) - it('should map different route types including strings, regex patterns, and parameter routes', function () { + it('should return route information for various route types (strings, arrays, and parameterized paths)', function () { const router = new Router() router.all('/', noop) @@ -34,49 +34,62 @@ describe('getRoutes', function () { ]) }) - it('should consolidate HTTP methods for routes registered multiple times', function () { + it('should track multiple registrations of the same route with different HTTP methods', function () { const router = new Router() + router.post(['/test', '/test2'], noop) - for (let i = 0; i < 20; i++) { + for (let i = 0; i < 2; i++) { router.get(['/test', '/test3'], noop) } router.put('/test3', noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/test', methods: ['POST', 'GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, { path: '/test2', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test3', methods: ['GET', 'PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } } + { path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test3', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test3', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test3', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } } ]) }) - it('should deduplicate routes and flatten nested router paths correctly', function () { + it('should properly handle nested routers and multiple mount points', function () { const router = new Router() const inner = new Router() router.post('/test', noop) - for (let i = 0; i < 100; i++) { + for (let i = 0; i < 2; i++) { router.get('/test', noop) } - for (let i = 0; i < 20; i++) { + for (let i = 0; i < 2; i++) { inner.get('/test', noop) } router.use(['/test/', '/test2', '/test3'], inner) router.use('/test4/', inner) + router.route('/test5').get(noop).post(noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/test', methods: ['POST', 'GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, { path: '/test/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, { path: '/test2/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test2/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test3/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, { path: '/test3/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test4/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } } + { path: '/test4/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test4/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, + { path: '/test5', methods: ['GET', 'POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } } ]) }) - it('should handle complex nested router hierarchies with multiple mount points', function () { + it('should correctly flatten deeply nested router hierarchies with multiple levels', function () { const router = new Router() const inner = new Router() const subinner = new Router() @@ -126,7 +139,7 @@ describe('getRoutes', function () { ]) }) - it('should inherit router options correctly through nested hierarchies', function () { + it('should preserve router configuration options from parent to child routers', function () { const router = new Router({ strict: true, caseSensitive: true }) const inner = new Router({ strict: true, caseSensitive: false }) const subinner = new Router({ strict: false, caseSensitive: false }) @@ -159,7 +172,7 @@ describe('getRoutes', function () { ]) }) - it('should handle routers with mixed options when mounted at same path', function () { + it('should handle multiple routers with different configuration options mounted at the same path', function () { const router = new Router({ strict: true, caseSensitive: true }) const inner = new Router({ strict: true, caseSensitive: false }) const otherInner = new Router({ strict: true, caseSensitive: true }) @@ -178,10 +191,9 @@ describe('getRoutes', function () { assert.deepStrictEqual(router.getRoutes(), [ { path: '/t2/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], options: { strict: true, caseSensitive: true } }, - // The current implementation merges methods for the same path, even if options differ. It shouldn't - { path: '/t2/:t6', methods: ['POST', 'GET'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: true } } - // { path: '/t2/t6', methods: ['POST'], options: { strict: true, caseSensitive: true } } - // { path: '/t2/t6', methods: ['GET'], options: { strict: true, caseSensitive: false } }, + { path: '/t2/:t6', methods: ['POST'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: true } }, + { path: '/t2/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], options: { strict: true, caseSensitive: false } }, + { path: '/t2/:t6', methods: ['GET'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: false } } ]) }) }) From 16b6c77589f02a58e77476627718d0ff715aa8d6 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 29 Jul 2025 16:31:35 -0500 Subject: [PATCH 07/17] docs: add docs for this new api Signed-off-by: Sebastian Beltran --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index 156c380..69d776c 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,47 @@ router.param('user_id', function (req, res, next, id) { }) ``` +### route.getRoutes() + +Returns an array of all the routes registered on this route, including +all the methods, key, and the options of instance of router. + +```js +const router = new Router({ strict: true, caseSensitive: true }) +const admin = new Router({ strict: true, caseSensitive: false }) + +admin.use((req, res, next) => { + // some middleware for admin routes + next() +}) + +admin.get('/', (req, res, next) => { + res.end('Hello') +}) + +router.use("/admin", admin) + +router.all('/:id', function (req, res) { + res.end('Hello') +}) + +console.log(router.getRoutes()) +// [ +// { +// key: '/admin/', +// methods: ['GET'], +// keys: [], +// options: { strict: true, caseSensitive: false }, +// }, +// { +// key: '/:id', +// methods: ['ALL'], +// keys: [{ name: 'id', type: "param" }], +// options: { strict: true, caseSensitive: true }, +// } +// ] +``` + ### router.route(path) Creates an instance of a single `Route` for the given `path`. From 856dff1d7b634ba6bc7388c508b077e67daa3ccd Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 1 Nov 2025 18:28:02 -0500 Subject: [PATCH 08/17] update route collection structure in getRoutes Signed-off-by: Sebastian Beltran --- index.js | 96 +++++++++++++---------- test/getRoutes.js | 191 +++++++++++++++++++++++++++++++++------------- 2 files changed, 194 insertions(+), 93 deletions(-) diff --git a/index.js b/index.js index ebad802..3f86e95 100644 --- a/index.js +++ b/index.js @@ -449,7 +449,6 @@ Router.prototype.route = function route (path) { * @public */ Router.prototype.getRoutes = function getRoutes () { - const routes = [] const stack = this.stack const options = { @@ -457,9 +456,7 @@ Router.prototype.getRoutes = function getRoutes () { caseSensitive: this.caseSensitive } - collectRoutes(stack, '', routes, options) - - return routes + return collectRoutes(stack, '', options) } // create Router#VERB functions @@ -471,26 +468,6 @@ methods.concat('all').forEach(function (method) { } }) -/** - * Add a route to the map with the given path and methods. - * @param {Map} routeMap - * @param {string} path - * @param {Array} methods - * @private - */ -function addRouteToMap (routeMap, path, methods, options) { - const { keys } = pathRegexp.pathToRegexp(path) - - routeMap.push( - { - path, - methods: [...methods], - keys, - options: { strict: options.strict, caseSensitive: options.caseSensitive } - } - ) -} - /** * Normalize a path by removing trailing slashes. * @param {string} path @@ -517,44 +494,85 @@ function normalizePath (path) { * @param {Map} routeMap - The map to store collected routes * @private */ -function collectRoutes (stack, prefix, routeMap, options) { +function collectRoutes (stack, prefix, options) { + const routes = [] + for (const layer of stack) { - // for routes without a .use + // route layer (has methods) if (layer.pathPatterns && layer.route) { const methods = Object.keys(layer.route.methods).map((method) => method.toUpperCase()) if (Array.isArray(layer.pathPatterns)) { for (const pathPattern of layer.pathPatterns) { - const fullPath = prefix === '/' ? pathPattern : normalizePath(prefix) + pathPattern - addRouteToMap(routeMap, fullPath, methods, options) + const path = (prefix === '' ? normalizePath(pathPattern) : normalizePath(prefix) + pathPattern) + let keys + if (!(pathPattern instanceof RegExp)) { + const pathKeys = pathRegexp.pathToRegexp(pathPattern).keys + if (pathKeys.length > 0) { + keys = pathKeys + } + } + routes.push({ + path, + keys, + methods, + router: undefined, + options: { strict: options.strict, caseSensitive: options.caseSensitive } + }) } } else { - const fullPath = prefix === '/' ? layer.pathPatterns : prefix + layer.pathPatterns - addRouteToMap(routeMap, fullPath, methods, options) + let keys + if (!(layer.pathPatterns instanceof RegExp)) { + const pathKeys = pathRegexp.pathToRegexp(layer.pathPatterns).keys + if (pathKeys.length > 0) { + keys = pathKeys + } + } + + const path = (prefix === '' ? normalizePath(layer.pathPatterns) : normalizePath(prefix) + layer.pathPatterns) + routes.push({ + path, + keys, + methods, + router: undefined, + options: { strict: options.strict, caseSensitive: options.caseSensitive } + }) } } - // for layers with a .use (mounted routers) + // mounted router (use) if (layer.pathPatterns && layer.handle && layer.handle.stack && !layer.route) { if (Array.isArray(layer.pathPatterns)) { for (const pathPattern of layer.pathPatterns) { - const pathPrefix = prefix === '/' ? normalizePath(pathPattern) : prefix + normalizePath(pathPattern) + const mountPath = (prefix === '' ? normalizePath(pathPattern) : normalizePath(prefix) + normalizePath(pathPattern)) + + const inner = collectRoutes(layer.handle.stack, '', { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive }) - collectRoutes(layer.handle.stack, pathPrefix, routeMap, { - strict: layer.handle.strict, - caseSensitive: layer.handle.caseSensitive + routes.push({ + path: mountPath, + keys: undefined, + methods: undefined, + router: inner.length ? inner : undefined, + options: { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive } }) } } else { - const pathPrefix = prefix === '/' ? normalizePath(layer.pathPatterns) : prefix + normalizePath(layer.pathPatterns) + const mountPath = (prefix === '' ? normalizePath(layer.pathPatterns) : normalizePath(prefix) + normalizePath(layer.pathPatterns)) + + const inner = collectRoutes(layer.handle.stack, '', { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive }) - collectRoutes(layer.handle.stack, pathPrefix, routeMap, { - strict: layer.handle.strict, - caseSensitive: layer.handle.caseSensitive + routes.push({ + path: mountPath, + keys: undefined, + methods: undefined, + router: inner.length ? inner : undefined, + options: { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive } }) } } } + + return routes } /** diff --git a/test/getRoutes.js b/test/getRoutes.js index 1a208c0..aa76a57 100644 --- a/test/getRoutes.js +++ b/test/getRoutes.js @@ -17,20 +17,19 @@ describe('getRoutes', function () { router.all('/', noop) router.route('/test2/') router.route('/test/').get(noop) - // With regex patterns path-to-regexp fail - // router.all(/^\/[a-z]oo$/, noop) + router.all(/^\/[a-z]oo$/, noop) router.get(['/foo', '/bar'], noop) router.post('/:id/setting/:thing', noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test2/', methods: [], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - // { path: '/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/foo', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/bar', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/:id/setting/:thing', methods: ['POST'], keys: [{ name: 'id', type: 'param' }, { name: 'thing', type: 'param' }], options: { strict: undefined, caseSensitive: undefined } } + { path: '/', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test2', methods: [], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, // Todo: Investigate + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/foo', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/bar', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/:id/setting/:thing', methods: ['POST'], keys: [{ name: 'id', type: 'param' }, { name: 'thing', type: 'param' }], options: { strict: undefined, caseSensitive: undefined }, router: undefined } ]) }) @@ -46,13 +45,13 @@ describe('getRoutes', function () { router.put('/test3', noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/test', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test2', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test3', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test3', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test3', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } } + { path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test2', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test3', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } ]) }) @@ -74,22 +73,94 @@ describe('getRoutes', function () { router.route('/test5').get(noop).post(noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/test', methods: ['POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test2/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test2/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test3/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test3/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test4/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test4/test', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/test5', methods: ['GET', 'POST'], keys: [], options: { strict: undefined, caseSensitive: undefined } } + { path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { + path: '/test', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { + keys: undefined, + methods: ['GET'], + options: { strict: undefined, caseSensitive: undefined }, + path: '/test', + router: undefined + }, { + keys: undefined, + methods: ['GET'], + options: { strict: undefined, caseSensitive: undefined }, + path: '/test', + router: undefined + }] + }, + { + path: '/test2', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { + keys: undefined, + methods: ['GET'], + options: { strict: undefined, caseSensitive: undefined }, + path: '/test', + router: undefined + }, { + keys: undefined, + methods: ['GET'], + options: { strict: undefined, caseSensitive: undefined }, + path: '/test', + router: undefined + }] + }, + { + path: '/test3', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { + keys: undefined, + methods: ['GET'], + options: { strict: undefined, caseSensitive: undefined }, + path: '/test', + router: undefined + }, { + keys: undefined, + methods: ['GET'], + options: { strict: undefined, caseSensitive: undefined }, + path: '/test', + router: undefined + }] + }, + { + path: '/test4', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { + keys: undefined, + methods: ['GET'], + options: { strict: undefined, caseSensitive: undefined }, + path: '/test', + router: undefined + }, { + keys: undefined, + methods: ['GET'], + options: { strict: undefined, caseSensitive: undefined }, + path: '/test', + router: undefined + }] + }, + { path: '/test5', methods: ['GET', 'POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } ]) }) - it('should correctly flatten deeply nested router hierarchies with multiple levels', function () { + it.skip('should correctly flatten deeply nested router hierarchies with multiple levels', function () { const router = new Router() const inner = new Router() const subinner = new Router() @@ -110,18 +181,18 @@ describe('getRoutes', function () { router.use('/test1', noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/t2/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - // { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t2/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t2/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - // { path: '/t5/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/t3/t5', methods: ['PUT'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - // { path: '/t7/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: [],options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/t4', methods: ['_ALL'], keys: [], options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } } + { path: '/t2/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, + // { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t2/t4', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t2/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, + // { path: '/t5/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/t4', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t5/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, + // { path: '/t7/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/t4', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, + { path: '/t7/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } } ]) }) @@ -135,11 +206,23 @@ describe('getRoutes', function () { const routes = router.getRoutes() assert.deepStrictEqual(routes, [ - { path: '/api', methods: ['GET'], keys: [], options: { strict: undefined, caseSensitive: undefined } } + { + path: '/', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [{ + path: '/api', + methods: ['GET'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: undefined + }] + } ]) }) - it('should preserve router configuration options from parent to child routers', function () { + it.skip('should preserve router configuration options from parent to child routers', function () { const router = new Router({ strict: true, caseSensitive: true }) const inner = new Router({ strict: true, caseSensitive: false }) const subinner = new Router({ strict: false, caseSensitive: false }) @@ -159,20 +242,20 @@ describe('getRoutes', function () { router.get('/test', noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/t2/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } }, - { path: '/t2/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } }, - { path: '/t2/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } }, - { path: '/t5/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } }, - { path: '/t5/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } }, - { path: '/t5/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } }, - { path: '/t7/t3/t5', methods: ['PUT'], keys: [], options: { strict: false, caseSensitive: false } }, - { path: '/t7/t4', methods: ['_ALL'], keys: [], options: { strict: true, caseSensitive: false } }, - { path: '/t7/', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: false } }, - { path: '/test', methods: ['GET'], keys: [], options: { strict: true, caseSensitive: true } } + { path: '/t2/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false } }, + { path: '/t2/t4', methods: ['_ALL'], keys: undefined, options: { strict: true, caseSensitive: false } }, + { path: '/t2/', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: false } }, + { path: '/t5/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false } }, + { path: '/t5/t4', methods: ['_ALL'], keys: undefined, options: { strict: true, caseSensitive: false } }, + { path: '/t5/', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: false } }, + { path: '/t7/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false } }, + { path: '/t7/t4', methods: ['_ALL'], keys: undefined, options: { strict: true, caseSensitive: false } }, + { path: '/t7/', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: false } }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: true } } ]) }) - it('should handle multiple routers with different configuration options mounted at the same path', function () { + it.skip('should handle multiple routers with different configuration options mounted at the same path', function () { const router = new Router({ strict: true, caseSensitive: true }) const inner = new Router({ strict: true, caseSensitive: false }) const otherInner = new Router({ strict: true, caseSensitive: true }) From 02c672d4e6e6e6784c4ad52b9dc632db215cef02 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 1 Nov 2025 18:52:24 -0500 Subject: [PATCH 09/17] test: unskip more test Signed-off-by: Sebastian Beltran --- test/getRoutes.js | 113 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/test/getRoutes.js b/test/getRoutes.js index aa76a57..145e687 100644 --- a/test/getRoutes.js +++ b/test/getRoutes.js @@ -160,13 +160,13 @@ describe('getRoutes', function () { ]) }) - it.skip('should correctly flatten deeply nested router hierarchies with multiple levels', function () { + it('should correctly flatten deeply nested router hierarchies with multiple levels', function () { const router = new Router() const inner = new Router() const subinner = new Router() subinner.put('/t5', noop) - // subinner.all(/^\/[a-z]oo$/, noop) + subinner.all(/^\/[a-z]oo$/, noop) subinner.use(noop) inner.use('/t3', subinner) @@ -181,18 +181,103 @@ describe('getRoutes', function () { router.use('/test1', noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/t2/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, - // { path: '/t2/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t2/t4', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t2/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, - // { path: '/t5/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/t4', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t5/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, - // { path: '/t7/t3/^\\/[a-z]oo$/', methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/t4', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } }, - { path: '/t7/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined } } + { + path: '/t2', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { + path: '/t3', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + ] + }, + { + path: '/t4', + methods: ['_ALL'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: undefined + }, + { + path: '/', + methods: ['GET'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: undefined + }, + ] + }, + { + path: '/t5', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { + path: '/t3', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + ] + }, + { + path: '/t4', + methods: ['_ALL'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: undefined + }, + { + path: '/', + methods: ['GET'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: undefined + }, + ] + }, + { + path: '/t7', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { + path: '/t3', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: [ + { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + ] + }, + { + path: '/t4', + methods: ['_ALL'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: undefined + }, + { + path: '/', + methods: ['GET'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined }, + router: undefined + }, + ] + } + ]) }) From 2b19160375bfb436e61417c6a2229fd44c433fb9 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 1 Nov 2025 19:18:50 -0500 Subject: [PATCH 10/17] test: unskip more test Signed-off-by: Sebastian Beltran --- index.js | 6 +-- test/getRoutes.js | 124 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 104 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index 3f86e95..be9a97d 100644 --- a/index.js +++ b/index.js @@ -451,6 +451,7 @@ Router.prototype.route = function route (path) { Router.prototype.getRoutes = function getRoutes () { const stack = this.stack + // TODO: end option const options = { strict: this.strict, caseSensitive: this.caseSensitive @@ -491,7 +492,6 @@ function normalizePath (path) { * * @param {Array} stack - The router stack to collect routes from * @param {string} prefix - The path prefix to prepend to routes - * @param {Map} routeMap - The map to store collected routes * @private */ function collectRoutes (stack, prefix, options) { @@ -553,7 +553,7 @@ function collectRoutes (stack, prefix, options) { keys: undefined, methods: undefined, router: inner.length ? inner : undefined, - options: { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive } + options }) } } else { @@ -566,7 +566,7 @@ function collectRoutes (stack, prefix, options) { keys: undefined, methods: undefined, router: inner.length ? inner : undefined, - options: { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive } + options }) } } diff --git a/test/getRoutes.js b/test/getRoutes.js index 145e687..175eac9 100644 --- a/test/getRoutes.js +++ b/test/getRoutes.js @@ -194,7 +194,7 @@ describe('getRoutes', function () { options: { strict: undefined, caseSensitive: undefined }, router: [ { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } ] }, { @@ -203,14 +203,14 @@ describe('getRoutes', function () { keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined - }, - { + }, + { path: '/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined - }, + } ] }, { @@ -218,7 +218,7 @@ describe('getRoutes', function () { methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined }, - router: [ + router: [ { path: '/t3', methods: undefined, @@ -226,7 +226,7 @@ describe('getRoutes', function () { options: { strict: undefined, caseSensitive: undefined }, router: [ { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } ] }, { @@ -235,14 +235,14 @@ describe('getRoutes', function () { keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined - }, + }, { path: '/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined - }, + } ] }, { @@ -258,7 +258,7 @@ describe('getRoutes', function () { options: { strict: undefined, caseSensitive: undefined }, router: [ { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined,options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } ] }, { @@ -267,14 +267,14 @@ describe('getRoutes', function () { keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined - }, + }, { path: '/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined - }, + } ] } @@ -307,12 +307,12 @@ describe('getRoutes', function () { ]) }) - it.skip('should preserve router configuration options from parent to child routers', function () { + it('should preserve router configuration options from parent to child routers', function () { const router = new Router({ strict: true, caseSensitive: true }) const inner = new Router({ strict: true, caseSensitive: false }) const subinner = new Router({ strict: false, caseSensitive: false }) - subinner.put('/t5', noop) + subinner.put('/t8', noop) subinner.use(noop) inner.use('/t3', subinner) @@ -327,16 +327,94 @@ describe('getRoutes', function () { router.get('/test', noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/t2/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false } }, - { path: '/t2/t4', methods: ['_ALL'], keys: undefined, options: { strict: true, caseSensitive: false } }, - { path: '/t2/', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: false } }, - { path: '/t5/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false } }, - { path: '/t5/t4', methods: ['_ALL'], keys: undefined, options: { strict: true, caseSensitive: false } }, - { path: '/t5/', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: false } }, - { path: '/t7/t3/t5', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false } }, - { path: '/t7/t4', methods: ['_ALL'], keys: undefined, options: { strict: true, caseSensitive: false } }, - { path: '/t7/', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: false } }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: true } } + { + path: '/t2', + methods: undefined, + keys: undefined, + options: { strict: true, caseSensitive: true }, + router: [ + { + path: '/t3', + methods: undefined, + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: [ + { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false }, router: undefined } + ] + }, { + path: '/t4', + methods: ['_ALL'], + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: undefined + }, { + path: '/', + methods: ['GET'], + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: undefined + } + ] + }, + { + path: '/t5', + methods: undefined, + keys: undefined, + options: { strict: true, caseSensitive: true }, + router: [ + { + path: '/t3', + methods: undefined, + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: [ + { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false }, router: undefined } + ] + }, { + path: '/t4', + methods: ['_ALL'], + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: undefined + }, { + path: '/', + methods: ['GET'], + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: undefined + } + ] + }, + { + path: '/t7', + methods: undefined, + keys: undefined, + options: { strict: true, caseSensitive: true }, + router: [ + { + path: '/t3', + methods: undefined, + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: [ + { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false }, router: undefined } + ] + }, { + path: '/t4', + methods: ['_ALL'], + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: undefined + }, { + path: '/', + methods: ['GET'], + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: undefined + } + ] + }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: true }, router: undefined } ]) }) From 45f5da54ae03a96d114dcf717272acc9e281b47d Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 1 Nov 2025 19:27:41 -0500 Subject: [PATCH 11/17] test: unskip more test Signed-off-by: Sebastian Beltran --- test/getRoutes.js | 57 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/test/getRoutes.js b/test/getRoutes.js index 175eac9..e1142ec 100644 --- a/test/getRoutes.js +++ b/test/getRoutes.js @@ -418,7 +418,7 @@ describe('getRoutes', function () { ]) }) - it.skip('should handle multiple routers with different configuration options mounted at the same path', function () { + it('should handle multiple routers with different configuration options mounted at the same path', function () { const router = new Router({ strict: true, caseSensitive: true }) const inner = new Router({ strict: true, caseSensitive: false }) const otherInner = new Router({ strict: true, caseSensitive: true }) @@ -436,10 +436,57 @@ describe('getRoutes', function () { router.use(inner) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/t2/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], options: { strict: true, caseSensitive: true } }, - { path: '/t2/:t6', methods: ['POST'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: true } }, - { path: '/t2/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], options: { strict: true, caseSensitive: false } }, - { path: '/t2/:t6', methods: ['GET'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: false } } + { + path: '/', + methods: undefined, + keys: undefined, + options: { strict: true, caseSensitive: true }, + router: [ + { + path: '/t2', + methods: undefined, + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: [ + { + path: '/:t5', + methods: ['PUT'], + keys: [{ name: 't5', type: 'param' }], + options: { strict: true, caseSensitive: true }, + router: undefined + }, + { + path: '/:t6', + methods: ['POST'], + keys: [{ name: 't6', type: 'param' }], + options: { strict: true, caseSensitive: true }, + router: undefined + } + ] + }, + { + path: '/t2', + methods: undefined, + keys: undefined, + options: { strict: true, caseSensitive: false }, + router: [{ + path: '/:t5', + methods: ['PUT'], + keys: [{ name: 't5', type: 'param' }], + options: { strict: true, caseSensitive: false }, + router: undefined + }, + { + path: '/:t6', + methods: ['GET'], + keys: [{ name: 't6', type: 'param' }], + options: { strict: true, caseSensitive: false }, + router: undefined + }] + + } + ] + } ]) }) }) From 4f611f400edc16fc7930ea21e786d647cf62f0bb Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 1 Nov 2025 20:10:42 -0500 Subject: [PATCH 12/17] feat: add 'end' option to route options Signed-off-by: Sebastian Beltran --- index.js | 21 +++-- lib/layer.js | 1 + test/getRoutes.js | 197 ++++++++++++++++++++++++---------------------- 3 files changed, 115 insertions(+), 104 deletions(-) diff --git a/index.js b/index.js index be9a97d..b86ba67 100644 --- a/index.js +++ b/index.js @@ -451,7 +451,6 @@ Router.prototype.route = function route (path) { Router.prototype.getRoutes = function getRoutes () { const stack = this.stack - // TODO: end option const options = { strict: this.strict, caseSensitive: this.caseSensitive @@ -517,7 +516,7 @@ function collectRoutes (stack, prefix, options) { keys, methods, router: undefined, - options: { strict: options.strict, caseSensitive: options.caseSensitive } + options: { ...options, end: layer.end } }) } } else { @@ -535,7 +534,7 @@ function collectRoutes (stack, prefix, options) { keys, methods, router: undefined, - options: { strict: options.strict, caseSensitive: options.caseSensitive } + options: { ...options, end: layer.end } }) } } @@ -546,27 +545,33 @@ function collectRoutes (stack, prefix, options) { for (const pathPattern of layer.pathPatterns) { const mountPath = (prefix === '' ? normalizePath(pathPattern) : normalizePath(prefix) + normalizePath(pathPattern)) - const inner = collectRoutes(layer.handle.stack, '', { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive }) + const inner = collectRoutes( + layer.handle.stack, + '', + { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive, end: layer.handle.end } + ) routes.push({ path: mountPath, keys: undefined, methods: undefined, router: inner.length ? inner : undefined, - options + options: { ...options, end: layer.end } }) } } else { const mountPath = (prefix === '' ? normalizePath(layer.pathPatterns) : normalizePath(prefix) + normalizePath(layer.pathPatterns)) - - const inner = collectRoutes(layer.handle.stack, '', { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive }) + const inner = collectRoutes( + layer.handle.stack, + '', + { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive, end: layer.handle.end }) routes.push({ path: mountPath, keys: undefined, methods: undefined, router: inner.length ? inner : undefined, - options + options: { ...options, end: layer.end } }) } } diff --git a/lib/layer.js b/lib/layer.js index 49f7805..afd39fc 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -45,6 +45,7 @@ function Layer (path, options, fn) { this.params = undefined // path is determinate in runtime execution this.path = undefined + this.end = opts.end this.pathPatterns = path this.slash = path === '/' && opts.end === false diff --git a/test/getRoutes.js b/test/getRoutes.js index e1142ec..6da9134 100644 --- a/test/getRoutes.js +++ b/test/getRoutes.js @@ -23,13 +23,13 @@ describe('getRoutes', function () { assert.deepStrictEqual(router.getRoutes(), [ - { path: '/', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/test2', methods: [], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, // Todo: Investigate - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/foo', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/bar', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/:id/setting/:thing', methods: ['POST'], keys: [{ name: 'id', type: 'param' }, { name: 'thing', type: 'param' }], options: { strict: undefined, caseSensitive: undefined }, router: undefined } + { path: '/', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test2', methods: [], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, // Todo: Investigate + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/foo', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/bar', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/:id/setting/:thing', methods: ['POST'], keys: [{ name: 'id', type: 'param' }, { name: 'thing', type: 'param' }], options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ]) }) @@ -45,13 +45,13 @@ describe('getRoutes', function () { router.put('/test3', noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/test2', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/test3', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } + { path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test2', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test3', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ]) }) @@ -73,25 +73,25 @@ describe('getRoutes', function () { router.route('/test5').get(noop).post(noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, + { path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, { path: '/test', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { keys: undefined, methods: ['GET'], - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }, { keys: undefined, methods: ['GET'], - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }] @@ -100,18 +100,18 @@ describe('getRoutes', function () { path: '/test2', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { keys: undefined, methods: ['GET'], - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }, { keys: undefined, methods: ['GET'], - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }] @@ -120,18 +120,18 @@ describe('getRoutes', function () { path: '/test3', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { keys: undefined, methods: ['GET'], - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }, { keys: undefined, methods: ['GET'], - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }] @@ -140,23 +140,23 @@ describe('getRoutes', function () { path: '/test4', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { keys: undefined, methods: ['GET'], - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }, { keys: undefined, methods: ['GET'], - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }] }, - { path: '/test5', methods: ['GET', 'POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } + { path: '/test5', methods: ['GET', 'POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ]) }) @@ -185,30 +185,30 @@ describe('getRoutes', function () { path: '/t2', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { path: '/t3', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ - { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } + { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ] }, { path: '/t4', methods: ['_ALL'], keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, { path: '/', methods: ['GET'], keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ] @@ -217,62 +217,61 @@ describe('getRoutes', function () { path: '/t5', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, - router: [ - { - path: '/t3', - methods: undefined, - keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, - router: [ - { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } - ] - }, - { - path: '/t4', - methods: ['_ALL'], - keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, - router: undefined - }, - { - path: '/', - methods: ['GET'], - keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, - router: undefined - } + options: { strict: undefined, caseSensitive: undefined, end: false }, + router: [{ + path: '/t3', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined, end: false }, + router: [ + { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } + ] + }, + { + path: '/t4', + methods: ['_ALL'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined, end: true }, + router: undefined + }, + { + path: '/', + methods: ['GET'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined, end: true }, + router: undefined + } ] }, { path: '/t7', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { path: '/t3', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ - { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined }, router: undefined } + { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ] }, { path: '/t4', methods: ['_ALL'], keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, { path: '/', methods: ['GET'], keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ] @@ -295,12 +294,12 @@ describe('getRoutes', function () { path: '/', methods: undefined, keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: false }, router: [{ path: '/api', methods: ['GET'], keys: undefined, - options: { strict: undefined, caseSensitive: undefined }, + options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }] } @@ -309,7 +308,7 @@ describe('getRoutes', function () { it('should preserve router configuration options from parent to child routers', function () { const router = new Router({ strict: true, caseSensitive: true }) - const inner = new Router({ strict: true, caseSensitive: false }) + const inner = new Router({ strict: true, caseSensitive: false, end: false }) const subinner = new Router({ strict: false, caseSensitive: false }) subinner.put('/t8', noop) @@ -331,27 +330,27 @@ describe('getRoutes', function () { path: '/t2', methods: undefined, keys: undefined, - options: { strict: true, caseSensitive: true }, + options: { strict: true, caseSensitive: true, end: false }, router: [ { path: '/t3', methods: undefined, keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: false }, router: [ - { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false }, router: undefined } + { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false, end: true }, router: undefined } ] }, { path: '/t4', methods: ['_ALL'], keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: true }, router: undefined }, { path: '/', methods: ['GET'], keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: true }, router: undefined } ] @@ -360,27 +359,27 @@ describe('getRoutes', function () { path: '/t5', methods: undefined, keys: undefined, - options: { strict: true, caseSensitive: true }, + options: { strict: true, caseSensitive: true, end: false }, router: [ { path: '/t3', methods: undefined, keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: false }, router: [ - { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false }, router: undefined } + { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false, end: true }, router: undefined } ] }, { path: '/t4', methods: ['_ALL'], keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: true }, router: undefined }, { path: '/', methods: ['GET'], keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: true }, router: undefined } ] @@ -389,39 +388,45 @@ describe('getRoutes', function () { path: '/t7', methods: undefined, keys: undefined, - options: { strict: true, caseSensitive: true }, + options: { strict: true, caseSensitive: true, end: false }, router: [ { path: '/t3', methods: undefined, keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: false }, router: [ - { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false }, router: undefined } + { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false, end: true }, router: undefined } ] }, { path: '/t4', methods: ['_ALL'], keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: true }, router: undefined }, { path: '/', methods: ['GET'], keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: true }, router: undefined } ] }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: true, caseSensitive: true }, router: undefined } + { + path: '/test', + methods: ['GET'], + keys: undefined, + options: { strict: true, caseSensitive: true, end: true }, + router: undefined + } ]) }) it('should handle multiple routers with different configuration options mounted at the same path', function () { const router = new Router({ strict: true, caseSensitive: true }) - const inner = new Router({ strict: true, caseSensitive: false }) - const otherInner = new Router({ strict: true, caseSensitive: true }) + const inner = new Router({ strict: true, caseSensitive: false, end: false }) + const otherInner = new Router({ strict: true, caseSensitive: true, end: false }) const otherInner2 = new Router({ strict: true, caseSensitive: false }) otherInner2.put('/:t5', noop) @@ -440,26 +445,26 @@ describe('getRoutes', function () { path: '/', methods: undefined, keys: undefined, - options: { strict: true, caseSensitive: true }, + options: { strict: true, caseSensitive: true, end: false }, router: [ { path: '/t2', methods: undefined, keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: false }, router: [ { path: '/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], - options: { strict: true, caseSensitive: true }, + options: { strict: true, caseSensitive: true, end: true }, router: undefined }, { path: '/:t6', methods: ['POST'], keys: [{ name: 't6', type: 'param' }], - options: { strict: true, caseSensitive: true }, + options: { strict: true, caseSensitive: true, end: true }, router: undefined } ] @@ -468,19 +473,19 @@ describe('getRoutes', function () { path: '/t2', methods: undefined, keys: undefined, - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: false }, router: [{ path: '/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: true }, router: undefined }, { path: '/:t6', methods: ['GET'], keys: [{ name: 't6', type: 'param' }], - options: { strict: true, caseSensitive: false }, + options: { strict: true, caseSensitive: false, end: true }, router: undefined }] From 376f83df4f2fb079dac7842c834e211e9b1bf06e Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 1 Nov 2025 20:22:17 -0500 Subject: [PATCH 13/17] no normalice paths Signed-off-by: Sebastian Beltran --- index.js | 45 ++++++++++++--------------------------------- test/getRoutes.js | 8 ++++---- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/index.js b/index.js index b86ba67..77e5f7d 100644 --- a/index.js +++ b/index.js @@ -456,7 +456,7 @@ Router.prototype.getRoutes = function getRoutes () { caseSensitive: this.caseSensitive } - return collectRoutes(stack, '', options) + return collectRoutes(stack, options) } // create Router#VERB functions @@ -468,32 +468,14 @@ methods.concat('all').forEach(function (method) { } }) -/** - * Normalize a path by removing trailing slashes. - * @param {string} path - * @return {string} normalized path - * @private - */ -function normalizePath (path) { - if (typeof path !== 'string') { - return path - } - - if (path.endsWith('/') && path.length > 1) { - return path.slice(0, -1) - } - - return path -} - /** * Collect routes from a router stack recursively. * * @param {Array} stack - The router stack to collect routes from - * @param {string} prefix - The path prefix to prepend to routes + * @param {object} options - The router options * @private */ -function collectRoutes (stack, prefix, options) { +function collectRoutes (stack, options) { const routes = [] for (const layer of stack) { @@ -503,16 +485,18 @@ function collectRoutes (stack, prefix, options) { if (Array.isArray(layer.pathPatterns)) { for (const pathPattern of layer.pathPatterns) { - const path = (prefix === '' ? normalizePath(pathPattern) : normalizePath(prefix) + pathPattern) let keys + if (!(pathPattern instanceof RegExp)) { + // TODO: keys for regex paths const pathKeys = pathRegexp.pathToRegexp(pathPattern).keys if (pathKeys.length > 0) { keys = pathKeys } } + routes.push({ - path, + path: pathPattern, keys, methods, router: undefined, @@ -528,9 +512,8 @@ function collectRoutes (stack, prefix, options) { } } - const path = (prefix === '' ? normalizePath(layer.pathPatterns) : normalizePath(prefix) + layer.pathPatterns) routes.push({ - path, + path: layer.pathPatterns, keys, methods, router: undefined, @@ -543,16 +526,13 @@ function collectRoutes (stack, prefix, options) { if (layer.pathPatterns && layer.handle && layer.handle.stack && !layer.route) { if (Array.isArray(layer.pathPatterns)) { for (const pathPattern of layer.pathPatterns) { - const mountPath = (prefix === '' ? normalizePath(pathPattern) : normalizePath(prefix) + normalizePath(pathPattern)) - const inner = collectRoutes( layer.handle.stack, - '', { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive, end: layer.handle.end } ) routes.push({ - path: mountPath, + path: pathPattern, keys: undefined, methods: undefined, router: inner.length ? inner : undefined, @@ -560,14 +540,13 @@ function collectRoutes (stack, prefix, options) { }) } } else { - const mountPath = (prefix === '' ? normalizePath(layer.pathPatterns) : normalizePath(prefix) + normalizePath(layer.pathPatterns)) const inner = collectRoutes( layer.handle.stack, - '', - { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive, end: layer.handle.end }) + { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive, end: layer.handle.end } + ) routes.push({ - path: mountPath, + path: layer.pathPatterns, keys: undefined, methods: undefined, router: inner.length ? inner : undefined, diff --git a/test/getRoutes.js b/test/getRoutes.js index 6da9134..c0186ac 100644 --- a/test/getRoutes.js +++ b/test/getRoutes.js @@ -24,8 +24,8 @@ describe('getRoutes', function () { assert.deepStrictEqual(router.getRoutes(), [ { path: '/', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test2', methods: [], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, // Todo: Investigate - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { path: '/test2/', methods: [], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, // Todo: Investigate + { path: '/test/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, { path: '/foo', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, { path: '/bar', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, @@ -77,7 +77,7 @@ describe('getRoutes', function () { { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, { - path: '/test', + path: '/test/', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, @@ -137,7 +137,7 @@ describe('getRoutes', function () { }] }, { - path: '/test4', + path: '/test4/', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, From 62f14bba05b348a2fe4e29ac28644bc039aaaae3 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 1 Nov 2025 20:24:20 -0500 Subject: [PATCH 14/17] fix: remove 'end' option from route options in collectRoutes function Signed-off-by: Sebastian Beltran --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 77e5f7d..3ccb243 100644 --- a/index.js +++ b/index.js @@ -528,7 +528,7 @@ function collectRoutes (stack, options) { for (const pathPattern of layer.pathPatterns) { const inner = collectRoutes( layer.handle.stack, - { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive, end: layer.handle.end } + { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive } ) routes.push({ @@ -542,7 +542,7 @@ function collectRoutes (stack, options) { } else { const inner = collectRoutes( layer.handle.stack, - { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive, end: layer.handle.end } + { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive } ) routes.push({ From 306eefd0f738b9a683a072679a9426133cdc6415 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Fri, 7 Nov 2025 23:05:12 -0500 Subject: [PATCH 15/17] feat: add route name to collected routes Signed-off-by: Sebastian Beltran --- index.js | 4 ++ test/getRoutes.js | 101 +++++++++++++++++++++++++++++++++------------- 2 files changed, 77 insertions(+), 28 deletions(-) diff --git a/index.js b/index.js index 3ccb243..95260ea 100644 --- a/index.js +++ b/index.js @@ -496,6 +496,7 @@ function collectRoutes (stack, options) { } routes.push({ + name: layer.name, path: pathPattern, keys, methods, @@ -513,6 +514,7 @@ function collectRoutes (stack, options) { } routes.push({ + name: layer.name, path: layer.pathPatterns, keys, methods, @@ -532,6 +534,7 @@ function collectRoutes (stack, options) { ) routes.push({ + name: layer.name, path: pathPattern, keys: undefined, methods: undefined, @@ -546,6 +549,7 @@ function collectRoutes (stack, options) { ) routes.push({ + name: layer.name, path: layer.pathPatterns, keys: undefined, methods: undefined, diff --git a/test/getRoutes.js b/test/getRoutes.js index c0186ac..7e499e9 100644 --- a/test/getRoutes.js +++ b/test/getRoutes.js @@ -23,13 +23,13 @@ describe('getRoutes', function () { assert.deepStrictEqual(router.getRoutes(), [ - { path: '/', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test2/', methods: [], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, // Todo: Investigate - { path: '/test/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/foo', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/bar', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/:id/setting/:thing', methods: ['POST'], keys: [{ name: 'id', type: 'param' }, { name: 'thing', type: 'param' }], options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } + { name: 'handle', path: '/', methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test2/', methods: [], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, // Todo: Investigate + { name: 'handle', path: '/test/', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/foo', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/bar', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/:id/setting/:thing', methods: ['POST'], keys: [{ name: 'id', type: 'param' }, { name: 'thing', type: 'param' }], options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ]) }) @@ -45,13 +45,13 @@ describe('getRoutes', function () { router.put('/test3', noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test2', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test3', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } + { name: 'handle', path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test2', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test3', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test3', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ]) }) @@ -73,22 +73,25 @@ describe('getRoutes', function () { router.route('/test5').get(noop).post(noop) assert.deepStrictEqual(router.getRoutes(), [ - { path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test', methods: ['POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: '/test', methods: ['GET'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, { + name: 'router', path: '/test/', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { + name: 'handle', keys: undefined, methods: ['GET'], options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }, { + name: 'handle', keys: undefined, methods: ['GET'], options: { strict: undefined, caseSensitive: undefined, end: true }, @@ -97,18 +100,21 @@ describe('getRoutes', function () { }] }, { + name: 'router', path: '/test2', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { + name: 'handle', keys: undefined, methods: ['GET'], options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }, { + name: 'handle', keys: undefined, methods: ['GET'], options: { strict: undefined, caseSensitive: undefined, end: true }, @@ -117,18 +123,21 @@ describe('getRoutes', function () { }] }, { + name: 'router', path: '/test3', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { + name: 'handle', keys: undefined, methods: ['GET'], options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }, { + name: 'handle', keys: undefined, methods: ['GET'], options: { strict: undefined, caseSensitive: undefined, end: true }, @@ -137,18 +146,21 @@ describe('getRoutes', function () { }] }, { + name: 'router', path: '/test4/', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { + name: 'handle', keys: undefined, methods: ['GET'], options: { strict: undefined, caseSensitive: undefined, end: true }, path: '/test', router: undefined }, { + name: 'handle', keys: undefined, methods: ['GET'], options: { strict: undefined, caseSensitive: undefined, end: true }, @@ -156,7 +168,7 @@ describe('getRoutes', function () { router: undefined }] }, - { path: '/test5', methods: ['GET', 'POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } + { name: 'handle', path: '/test5', methods: ['GET', 'POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ]) }) @@ -182,22 +194,25 @@ describe('getRoutes', function () { assert.deepStrictEqual(router.getRoutes(), [ { + name: 'router', path: '/t2', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { + name: 'router', path: '/t3', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ - { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } + { name: 'handle', path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ] }, { + name: 'handle', path: '/t4', methods: ['_ALL'], keys: undefined, @@ -205,6 +220,7 @@ describe('getRoutes', function () { router: undefined }, { + name: 'handle', path: '/', methods: ['GET'], keys: undefined, @@ -214,21 +230,24 @@ describe('getRoutes', function () { ] }, { + name: 'router', path: '/t5', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [{ + name: 'router', path: '/t3', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ - { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } + { name: 'handle', path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ] }, { + name: 'handle', path: '/t4', methods: ['_ALL'], keys: undefined, @@ -236,6 +255,7 @@ describe('getRoutes', function () { router: undefined }, { + name: 'handle', path: '/', methods: ['GET'], keys: undefined, @@ -245,22 +265,25 @@ describe('getRoutes', function () { ] }, { + name: 'router', path: '/t7', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ { + name: 'router', path: '/t3', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [ - { path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, - { path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } + { name: 'handle', path: '/t5', methods: ['PUT'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined }, + { name: 'handle', path: /^\/[a-z]oo$/, methods: ['_ALL'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } ] }, { + name: 'handle', path: '/t4', methods: ['_ALL'], keys: undefined, @@ -268,6 +291,7 @@ describe('getRoutes', function () { router: undefined }, { + name: 'handle', path: '/', methods: ['GET'], keys: undefined, @@ -291,11 +315,13 @@ describe('getRoutes', function () { assert.deepStrictEqual(routes, [ { + name: 'router', path: '/', methods: undefined, keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: false }, router: [{ + name: 'handle', path: '/api', methods: ['GET'], keys: undefined, @@ -327,26 +353,30 @@ describe('getRoutes', function () { assert.deepStrictEqual(router.getRoutes(), [ { + name: 'router', path: '/t2', methods: undefined, keys: undefined, options: { strict: true, caseSensitive: true, end: false }, router: [ { + name: 'router', path: '/t3', methods: undefined, keys: undefined, options: { strict: true, caseSensitive: false, end: false }, router: [ - { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false, end: true }, router: undefined } + { name: 'handle', path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false, end: true }, router: undefined } ] }, { + name: 'handle', path: '/t4', methods: ['_ALL'], keys: undefined, options: { strict: true, caseSensitive: false, end: true }, router: undefined }, { + name: 'handle', path: '/', methods: ['GET'], keys: undefined, @@ -356,26 +386,30 @@ describe('getRoutes', function () { ] }, { + name: 'router', path: '/t5', methods: undefined, keys: undefined, options: { strict: true, caseSensitive: true, end: false }, router: [ { + name: 'router', path: '/t3', methods: undefined, keys: undefined, options: { strict: true, caseSensitive: false, end: false }, router: [ - { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false, end: true }, router: undefined } + { name: 'handle', path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false, end: true }, router: undefined } ] }, { + name: 'handle', path: '/t4', methods: ['_ALL'], keys: undefined, options: { strict: true, caseSensitive: false, end: true }, router: undefined }, { + name: 'handle', path: '/', methods: ['GET'], keys: undefined, @@ -385,26 +419,30 @@ describe('getRoutes', function () { ] }, { + name: 'router', path: '/t7', methods: undefined, keys: undefined, options: { strict: true, caseSensitive: true, end: false }, router: [ { + name: 'router', path: '/t3', methods: undefined, keys: undefined, options: { strict: true, caseSensitive: false, end: false }, router: [ - { path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false, end: true }, router: undefined } + { name: 'handle', path: '/t8', methods: ['PUT'], keys: undefined, options: { strict: false, caseSensitive: false, end: true }, router: undefined } ] }, { + name: 'handle', path: '/t4', methods: ['_ALL'], keys: undefined, options: { strict: true, caseSensitive: false, end: true }, router: undefined }, { + name: 'handle', path: '/', methods: ['GET'], keys: undefined, @@ -414,6 +452,7 @@ describe('getRoutes', function () { ] }, { + name: 'handle', path: '/test', methods: ['GET'], keys: undefined, @@ -442,18 +481,21 @@ describe('getRoutes', function () { assert.deepStrictEqual(router.getRoutes(), [ { + name: 'router', path: '/', methods: undefined, keys: undefined, options: { strict: true, caseSensitive: true, end: false }, router: [ { + name: 'router', path: '/t2', methods: undefined, keys: undefined, options: { strict: true, caseSensitive: false, end: false }, router: [ { + name: 'handle', path: '/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], @@ -461,6 +503,7 @@ describe('getRoutes', function () { router: undefined }, { + name: 'handle', path: '/:t6', methods: ['POST'], keys: [{ name: 't6', type: 'param' }], @@ -470,11 +513,13 @@ describe('getRoutes', function () { ] }, { + name: 'router', path: '/t2', methods: undefined, keys: undefined, options: { strict: true, caseSensitive: false, end: false }, router: [{ + name: 'handle', path: '/:t5', methods: ['PUT'], keys: [{ name: 't5', type: 'param' }], @@ -482,13 +527,13 @@ describe('getRoutes', function () { router: undefined }, { + name: 'handle', path: '/:t6', methods: ['GET'], keys: [{ name: 't6', type: 'param' }], options: { strict: true, caseSensitive: false, end: true }, router: undefined }] - } ] } From e75bcb736cfae297201bf809de23166365527426 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 9 Nov 2025 11:33:50 -0500 Subject: [PATCH 16/17] feat: enhance route key extraction for regex and dynamic routes Signed-off-by: Sebastian Beltran --- index.js | 47 +++++++++++++++---------- lib/layer.js | 1 + package.json | 4 +-- test/getRoutes.js | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 95260ea..1e9d13e 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ const isPromise = require('is-promise') const Layer = require('./lib/layer') +const { MATCHING_GROUP_REGEXP } = require('./lib/layer') const { METHODS } = require('node:http') const parseUrl = require('parseurl') const Route = require('./lib/route') @@ -485,15 +486,7 @@ function collectRoutes (stack, options) { if (Array.isArray(layer.pathPatterns)) { for (const pathPattern of layer.pathPatterns) { - let keys - - if (!(pathPattern instanceof RegExp)) { - // TODO: keys for regex paths - const pathKeys = pathRegexp.pathToRegexp(pathPattern).keys - if (pathKeys.length > 0) { - keys = pathKeys - } - } + const keys = extractPatternKeys(pathPattern) routes.push({ name: layer.name, @@ -505,13 +498,7 @@ function collectRoutes (stack, options) { }) } } else { - let keys - if (!(layer.pathPatterns instanceof RegExp)) { - const pathKeys = pathRegexp.pathToRegexp(layer.pathPatterns).keys - if (pathKeys.length > 0) { - keys = pathKeys - } - } + const keys = extractPatternKeys(layer.pathPatterns) routes.push({ name: layer.name, @@ -532,11 +519,12 @@ function collectRoutes (stack, options) { layer.handle.stack, { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive } ) + const keys = extractPatternKeys(pathPattern) routes.push({ name: layer.name, path: pathPattern, - keys: undefined, + keys, methods: undefined, router: inner.length ? inner : undefined, options: { ...options, end: layer.end } @@ -547,11 +535,12 @@ function collectRoutes (stack, options) { layer.handle.stack, { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive } ) + const keys = extractPatternKeys(layer.pathPatterns) routes.push({ name: layer.name, path: layer.pathPatterns, - keys: undefined, + keys, methods: undefined, router: inner.length ? inner : undefined, options: { ...options, end: layer.end } @@ -563,6 +552,28 @@ function collectRoutes (stack, options) { return routes } +function extractPatternKeys (pattern) { + if (pattern instanceof RegExp) { + const keys = [] + let name = 0 + let m + // eslint-disable-next-line no-cond-assign + while (m = MATCHING_GROUP_REGEXP.exec(pattern.source)) { + keys.push({ name: m[1] || name++ }) + } + + return keys.length > 0 ? keys : undefined + } + + const pathKeys = pathRegexp.pathToRegexp(String(pattern)).keys + + if (pathKeys && pathKeys.length > 0) { + return pathKeys + } + + return undefined +} + /** * Generate a callback that will make an OPTIONS response. * diff --git a/lib/layer.js b/lib/layer.js index afd39fc..a443584 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -30,6 +30,7 @@ const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g */ module.exports = Layer +module.exports.MATCHING_GROUP_REGEXP = MATCHING_GROUP_REGEXP function Layer (path, options, fn) { if (!(this instanceof Layer)) { diff --git a/package.json b/package.json index e465e6c..e6be557 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ }, "scripts": { "lint": "standard", - "test": "mocha --reporter spec --bail --check-leaks test/", - "test:debug": "mocha --reporter spec --bail --check-leaks test/ --inspect --inspect-brk", + "test": "mocha --reporter spec --check-leaks test/", + "test:debug": "mocha --reporter spec --check-leaks test/ --inspect --inspect-brk", "test-ci": "nyc --reporter=lcov --reporter=text npm test", "test-cov": "nyc --reporter=text npm test", "version": "node scripts/version-history.js && git add HISTORY.md" diff --git a/test/getRoutes.js b/test/getRoutes.js index 7e499e9..b579092 100644 --- a/test/getRoutes.js +++ b/test/getRoutes.js @@ -332,6 +332,95 @@ describe('getRoutes', function () { ]) }) + it('should return keys for routes with regex', function () { + const router = new Router() + const subRouter = new Router() + + subRouter.get(/\/(?[0-9]+)/, () => {}) + + router.use(/\/page_([0-9]+)/, subRouter) + + const routes = router.getRoutes() + + assert.deepStrictEqual(routes, [ + { + name: 'router', + path: /\/page_([0-9]+)/, + methods: undefined, + keys: [{ name: 0 }], + options: { strict: undefined, caseSensitive: undefined, end: false }, + router: [{ + name: 'handle', + path: /\/(?[0-9]+)/, + methods: ['GET'], + keys: [{ name: 'foo' }], + options: { strict: undefined, caseSensitive: undefined, end: true }, + router: undefined + }] + } + ]) + }) + + it('should return keys for dynamic routes', function () { + const router = new Router() + const subRouter = new Router() + const anotherSubRouter = new Router() + + subRouter.get('/api', () => {}) + anotherSubRouter.get('/api2', () => {}) + + router.use('/:test', subRouter) + router.use(['/:lang', '/ls'], anotherSubRouter) + + const routes = router.getRoutes() + + assert.deepStrictEqual(routes, [ + { + name: 'router', + path: '/:test', + methods: undefined, + keys: [{ name: 'test', type: 'param' }], + options: { strict: undefined, caseSensitive: undefined, end: false }, + router: [{ + name: 'handle', + path: '/api', + methods: ['GET'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined, end: true }, + router: undefined + }] + }, { + name: 'router', + path: '/:lang', + methods: undefined, + keys: [{ name: 'lang', type: 'param' }], + options: { strict: undefined, caseSensitive: undefined, end: false }, + router: [{ + name: 'handle', + path: '/api2', + methods: ['GET'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined, end: true }, + router: undefined + }] + }, { + name: 'router', + path: '/ls', + methods: undefined, + keys: undefined, + options: { strict: undefined, caseSensitive: undefined, end: false }, + router: [{ + name: 'handle', + path: '/api2', + methods: ['GET'], + keys: undefined, + options: { strict: undefined, caseSensitive: undefined, end: true }, + router: undefined + }] + } + ]) + }) + it('should preserve router configuration options from parent to child routers', function () { const router = new Router({ strict: true, caseSensitive: true }) const inner = new Router({ strict: true, caseSensitive: false, end: false }) From 6c071e7b0adc1fc83539ab069993490e66616a07 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 9 Nov 2025 12:03:49 -0500 Subject: [PATCH 17/17] docs: update jsdocs and readmed Signed-off-by: Sebastian Beltran --- README.md | 27 ++++++++++++++++++++------- index.js | 7 +++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 69d776c..a599d99 100644 --- a/README.md +++ b/README.md @@ -170,16 +170,29 @@ router.all('/:id', function (req, res) { console.log(router.getRoutes()) // [ // { -// key: '/admin/', -// methods: ['GET'], -// keys: [], -// options: { strict: true, caseSensitive: false }, +// name: 'router', +// path: '/admin', +// methods: undefined, +// keys: undefined, +// router: [ +// { +// name: 'handle', +// path: '/', +// methods: ['GET'], +// keys: undefined, +// router: undefined, +// options: { strict: true, caseSensitive: false, end: true }, +// } +// ], +// options: { strict: true, caseSensitive: true, end: false } // }, // { -// key: '/:id', -// methods: ['ALL'], +// name: 'handle', +// path: '/:id', +// methods: ['_ALL'], // keys: [{ name: 'id', type: "param" }], -// options: { strict: true, caseSensitive: true }, +// router: undefined, +// options: { strict: true, caseSensitive: true, end: true } // } // ] ``` diff --git a/index.js b/index.js index 1e9d13e..38cba4f 100644 --- a/index.js +++ b/index.js @@ -552,6 +552,13 @@ function collectRoutes (stack, options) { return routes } +/** + * Extracts parameter/key descriptors from a route pattern. + * + * @param {string|RegExp} pattern - Route pattern to analyze (path string or RegExp). + * @returns {Array|undefined} Array of key descriptor objects (each with at least a `name` property), or `undefined` if none found. + * @private + */ function extractPatternKeys (pattern) { if (pattern instanceof RegExp) { const keys = []