From b21c685c25ce5c495a98c2b58e3931307ab91328 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 27 Jul 2025 21:35:25 -0500 Subject: [PATCH 01/22] 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/22] 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/22] [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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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 = [] From 5e71b5a02824b5aae143f90605bca66f153f3fc4 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 14 Jul 2026 22:09:23 -0500 Subject: [PATCH 18/22] feat: simplify to minimal listRoutes() API Rename getRoutes() to listRoutes() and reduce the output to { path, methods, router } per registered path, as agreed in the maintainers call: - expose the mounted router instance instead of recursing, so consumers recurse themselves (removes the infinite recursion concern with cyclic routers) - use `methods: undefined` for .all() and mounted routers instead of the `_ALL` magic string - drop keys, options, and name from the output; they can be added later without a breaking change - store the registered path on the layer as `rawPath` --- README.md | 65 ++--- index.js | 154 +++-------- lib/layer.js | 6 +- test/getRoutes.js | 633 --------------------------------------------- test/listRoutes.js | 141 ++++++++++ 5 files changed, 201 insertions(+), 798 deletions(-) delete mode 100644 test/getRoutes.js create mode 100644 test/listRoutes.js diff --git a/README.md b/README.md index a599d99..8ec7620 100644 --- a/README.md +++ b/README.md @@ -143,57 +143,46 @@ router.param('user_id', function (req, res, next, id) { }) ``` -### route.getRoutes() +### router.listRoutes() -Returns an array of all the routes registered on this route, including -all the methods, key, and the options of instance of router. +Returns an array with one `{ path, methods, router }` object per path +registered on this router, in registration order. -```js -const router = new Router({ strict: true, caseSensitive: true }) -const admin = new Router({ strict: true, caseSensitive: false }) +- `path` is the path the route was registered with (a string or a `RegExp`). + Routes registered with an array of paths produce one entry per path. +- `methods` is an array of uppercase HTTP method names, or `undefined` when + the entry matches all methods (routes registered with `.all()` and mounted + routers). +- `router` is the mounted router instance for `.use(path, router)` entries, + otherwise `undefined`. Nested routes are not resolved recursively; consumers + can recurse themselves by calling `router.listRoutes()` on each entry that + exposes one. -admin.use((req, res, next) => { - // some middleware for admin routes - next() -}) +Plain middleware functions registered with `.use()` are not listed. + +```js +const router = new Router() +const admin = new Router() -admin.get('/', (req, res, next) => { +admin.get('/', (req, res) => { res.end('Hello') }) -router.use("/admin", admin) +router.use('/admin', admin) router.all('/:id', function (req, res) { res.end('Hello') }) -console.log(router.getRoutes()) +console.log(router.listRoutes()) +// [ +// { path: '/admin', methods: undefined, router: admin }, +// { path: '/:id', methods: undefined, router: undefined } +// ] + +console.log(admin.listRoutes()) // [ -// { -// 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 } -// }, -// { -// name: 'handle', -// path: '/:id', -// methods: ['_ALL'], -// keys: [{ name: 'id', type: "param" }], -// router: undefined, -// options: { strict: true, caseSensitive: true, end: true } -// } +// { path: '/', methods: ['GET'], router: undefined } // ] ``` diff --git a/index.js b/index.js index 38cba4f..0ea4c0a 100644 --- a/index.js +++ b/index.js @@ -14,11 +14,9 @@ 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') -const pathRegexp = require('path-to-regexp') const debug = require('debug')('router') const deprecate = require('depd')('router') @@ -444,20 +442,44 @@ Router.prototype.route = function route (path) { } /** - * List all registered routes. + * List the routes and mounted routers registered on this router. * - * @return {Array} An array of route paths + * Returns one `{ path, methods, router }` object per registered path. + * `methods` is `undefined` when the layer matches all methods (`.all()` + * or `.use()`). `router` is the mounted router instance for + * `.use(path, router)` layers, so consumers can recurse by calling + * `router.listRoutes()` themselves. + * + * @return {Array<{path: string|RegExp, methods: Array|undefined, router: Router|undefined}>} * @public */ -Router.prototype.getRoutes = function getRoutes () { - const stack = this.stack +Router.prototype.listRoutes = function listRoutes () { + const routes = [] - const options = { - strict: this.strict, - caseSensitive: this.caseSensitive + for (const layer of this.stack) { + const route = layer.route + const router = !route && typeof layer.handle.listRoutes === 'function' + ? layer.handle + : undefined + + if (!route && !router) { + continue + } + + const methods = route && !route.methods._all + ? Object.keys(route.methods).map((method) => method.toUpperCase()) + : undefined + + if (Array.isArray(layer.rawPath)) { + for (const path of layer.rawPath) { + routes.push({ path, methods, router }) + } + } else { + routes.push({ path: layer.rawPath, methods, router }) + } } - return collectRoutes(stack, options) + return routes } // create Router#VERB functions @@ -469,118 +491,6 @@ methods.concat('all').forEach(function (method) { } }) -/** - * Collect routes from a router stack recursively. - * - * @param {Array} stack - The router stack to collect routes from - * @param {object} options - The router options - * @private - */ -function collectRoutes (stack, options) { - const routes = [] - - for (const layer of stack) { - // 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 keys = extractPatternKeys(pathPattern) - - routes.push({ - name: layer.name, - path: pathPattern, - keys, - methods, - router: undefined, - options: { ...options, end: layer.end } - }) - } - } else { - const keys = extractPatternKeys(layer.pathPatterns) - - routes.push({ - name: layer.name, - path: layer.pathPatterns, - keys, - methods, - router: undefined, - options: { ...options, end: layer.end } - }) - } - } - - // 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 inner = collectRoutes( - layer.handle.stack, - { strict: layer.handle.strict, caseSensitive: layer.handle.caseSensitive } - ) - const keys = extractPatternKeys(pathPattern) - - routes.push({ - name: layer.name, - path: pathPattern, - keys, - methods: undefined, - router: inner.length ? inner : undefined, - options: { ...options, end: layer.end } - }) - } - } else { - const inner = collectRoutes( - 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, - methods: undefined, - router: inner.length ? inner : undefined, - options: { ...options, end: layer.end } - }) - } - } - } - - 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 = [] - 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 a443584..72a4a82 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -30,7 +30,6 @@ 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)) { @@ -44,11 +43,8 @@ function Layer (path, options, fn) { this.keys = [] this.name = fn.name || '' this.params = undefined - // path is determinate in runtime execution this.path = undefined - this.end = opts.end - - this.pathPatterns = path + this.rawPath = path this.slash = path === '/' && opts.end === false function matcher (_path) { diff --git a/test/getRoutes.js b/test/getRoutes.js deleted file mode 100644 index b579092..0000000 --- a/test/getRoutes.js +++ /dev/null @@ -1,633 +0,0 @@ -const { it, describe } = require('mocha') -const Router = require('..') -const utils = require('./support/utils') - -const assert = utils.assert - -describe('getRoutes', function () { - it('should return an empty array when no routes are registered', function () { - const router = new Router() - - assert.deepStrictEqual(router.getRoutes(), []) - }) - - it('should return route information for various route types (strings, arrays, and parameterized paths)', 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.getRoutes(), - [ - { 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 } - ]) - }) - - 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 < 2; i++) { - router.get(['/test', '/test3'], noop) - } - - router.put('/test3', noop) - - assert.deepStrictEqual(router.getRoutes(), [ - { 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 } - ]) - }) - - 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 < 2; i++) { - router.get('/test', noop) - } - - 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(), [ - { 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 }, - path: '/test', - router: undefined - }] - }, - { - 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 }, - path: '/test', - router: undefined - }] - }, - { - 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 }, - path: '/test', - router: undefined - }] - }, - { - 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 }, - path: '/test', - router: undefined - }] - }, - { name: 'handle', path: '/test5', methods: ['GET', 'POST'], keys: undefined, options: { strict: undefined, caseSensitive: undefined, end: true }, router: undefined } - ]) - }) - - 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.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.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: [ - { 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, - options: { strict: undefined, caseSensitive: undefined, end: true }, - router: undefined - }, - { - name: 'handle', - path: '/', - methods: ['GET'], - keys: undefined, - options: { strict: undefined, caseSensitive: undefined, end: true }, - router: undefined - } - ] - }, - { - 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: [ - { 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, - options: { strict: undefined, caseSensitive: undefined, end: true }, - router: undefined - }, - { - name: 'handle', - path: '/', - methods: ['GET'], - keys: undefined, - options: { strict: undefined, caseSensitive: undefined, end: true }, - router: undefined - } - ] - }, - { - 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: [ - { 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, - options: { strict: undefined, caseSensitive: undefined, end: true }, - router: undefined - }, - { - name: 'handle', - path: '/', - methods: ['GET'], - keys: undefined, - options: { strict: undefined, caseSensitive: undefined, end: true }, - router: undefined - } - ] - } - - ]) - }) - - it('should avoid double slashes when mounting routers at root path', function () { - const router = new Router() - const subRouter = new Router() - - subRouter.get('/api', () => {}) - router.use('/', subRouter) - - const routes = router.getRoutes() - - 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, - options: { strict: undefined, caseSensitive: undefined, end: true }, - router: undefined - }] - } - ]) - }) - - 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 }) - const subinner = new Router({ strict: false, caseSensitive: false }) - - subinner.put('/t8', 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.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: [ - { 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, - options: { strict: true, caseSensitive: false, end: true }, - router: undefined - } - ] - }, - { - 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: [ - { 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, - options: { strict: true, caseSensitive: false, end: true }, - router: undefined - } - ] - }, - { - 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: [ - { 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, - options: { strict: true, caseSensitive: false, end: true }, - router: undefined - } - ] - }, - { - name: 'handle', - 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, end: false }) - const otherInner = new Router({ strict: true, caseSensitive: true, end: false }) - 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.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' }], - options: { strict: true, caseSensitive: true, end: true }, - router: undefined - }, - { - name: 'handle', - path: '/:t6', - methods: ['POST'], - keys: [{ name: 't6', type: 'param' }], - options: { strict: true, caseSensitive: true, end: true }, - router: undefined - } - ] - }, - { - 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' }], - options: { strict: true, caseSensitive: false, end: true }, - router: undefined - }, - { - name: 'handle', - path: '/:t6', - methods: ['GET'], - keys: [{ name: 't6', type: 'param' }], - options: { strict: true, caseSensitive: false, end: true }, - router: undefined - }] - } - ] - } - ]) - }) -}) - -function noop () {} diff --git a/test/listRoutes.js b/test/listRoutes.js new file mode 100644 index 0000000..24fbc1c --- /dev/null +++ b/test/listRoutes.js @@ -0,0 +1,141 @@ +const { it, describe } = require('mocha') +const Router = require('..') +const utils = require('./support/utils') + +const assert = utils.assert + +describe('listRoutes', function () { + it('should return an empty array when no routes are registered', function () { + const router = new Router() + + assert.deepStrictEqual(router.listRoutes(), []) + }) + + it('should list routes for strings, regexps, arrays, and parameterized paths', function () { + const router = new Router() + + router.get('/foo', noop) + router.post('/:id/setting/:thing', noop) + router.all(/^\/[a-z]oo$/, noop) + router.get(['/bar', '/baz'], noop) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/foo', methods: ['GET'], router: undefined }, + { path: '/:id/setting/:thing', methods: ['POST'], router: undefined }, + { path: /^\/[a-z]oo$/, methods: undefined, router: undefined }, + { path: '/bar', methods: ['GET'], router: undefined }, + { path: '/baz', methods: ['GET'], router: undefined } + ]) + }) + + it('should list all methods registered on a route', function () { + const router = new Router() + + router.route('/test') + .get(noop) + .post(noop) + .put(noop) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/test', methods: ['GET', 'POST', 'PUT'], router: undefined } + ]) + }) + + it('should return undefined methods for .all() routes', function () { + const router = new Router() + + router.all('/test', noop) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/test', methods: undefined, router: undefined } + ]) + }) + + it('should repeat routes registered multiple times', function () { + const router = new Router() + + router.get('/test', noop) + router.get('/test', noop) + router.post('/test', noop) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/test', methods: ['GET'], router: undefined }, + { path: '/test', methods: ['GET'], router: undefined }, + { path: '/test', methods: ['POST'], router: undefined } + ]) + }) + + it('should not list plain middleware', function () { + const router = new Router() + + router.use(noop) + router.use('/admin', noop) + router.get('/test', noop) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/test', methods: ['GET'], router: undefined } + ]) + }) + + it('should expose mounted routers without recursing', function () { + const router = new Router() + const inner = new Router() + + inner.get('/api', noop) + router.use('/inner', inner) + router.get('/test', noop) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/inner', methods: undefined, router: inner }, + { path: '/test', methods: ['GET'], router: undefined } + ]) + + assert.deepStrictEqual(inner.listRoutes(), [ + { path: '/api', methods: ['GET'], router: undefined } + ]) + }) + + it('should use the default path when mounting a router without a path', function () { + const router = new Router() + const inner = new Router() + + inner.get('/api', noop) + router.use(inner) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/', methods: undefined, router: inner } + ]) + }) + + it('should list a mounted router once per path in an array', function () { + const router = new Router() + const inner = new Router() + + router.use(['/foo', '/bar'], inner) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/foo', methods: undefined, router: inner }, + { path: '/bar', methods: undefined, router: inner } + ]) + }) + + it('should allow consumers to recurse into cyclic routers without blowing the stack', function () { + const router = new Router() + const inner = new Router() + + inner.get('/api', noop) + router.use('/inner', inner) + inner.use('/loop', router) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/inner', methods: undefined, router: inner } + ]) + + assert.deepStrictEqual(inner.listRoutes(), [ + { path: '/api', methods: ['GET'], router: undefined }, + { path: '/loop', methods: undefined, router } + ]) + }) +}) + +function noop () {} From 99d3a49baab06e5feca8771e545f84d5a697642c Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 14 Jul 2026 22:25:13 -0500 Subject: [PATCH 19/22] fix: make listRoutes methods output match dispatch behavior - reuse Route.prototype._methods() so the automatic HEAD for GET routes is listed, consistent with the OPTIONS Allow header - keep the specific methods of routes that combine .all() with verbs instead of collapsing them to undefined - copy the methods array per entry when a route is registered with an array of paths, so entries don't share mutable state --- README.md | 10 ++++---- index.js | 14 ++++++++---- test/listRoutes.js | 57 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 8ec7620..0b106e7 100644 --- a/README.md +++ b/README.md @@ -150,9 +150,11 @@ registered on this router, in registration order. - `path` is the path the route was registered with (a string or a `RegExp`). Routes registered with an array of paths produce one entry per path. -- `methods` is an array of uppercase HTTP method names, or `undefined` when - the entry matches all methods (routes registered with `.all()` and mounted - routers). +- `methods` is an array of the uppercase HTTP method names the route responds + to, including the automatic `HEAD` for `GET` routes. It is `undefined` when + the entry matches all methods (routes registered only with `.all()`, and + mounted routers). Routes that combine `.all()` with specific methods list + those methods. - `router` is the mounted router instance for `.use(path, router)` entries, otherwise `undefined`. Nested routes are not resolved recursively; consumers can recurse themselves by calling `router.listRoutes()` on each entry that @@ -182,7 +184,7 @@ console.log(router.listRoutes()) console.log(admin.listRoutes()) // [ -// { path: '/', methods: ['GET'], router: undefined } +// { path: '/', methods: ['GET', 'HEAD'], router: undefined } // ] ``` diff --git a/index.js b/index.js index 0ea4c0a..1c7515d 100644 --- a/index.js +++ b/index.js @@ -445,8 +445,10 @@ Router.prototype.route = function route (path) { * List the routes and mounted routers registered on this router. * * Returns one `{ path, methods, router }` object per registered path. - * `methods` is `undefined` when the layer matches all methods (`.all()` - * or `.use()`). `router` is the mounted router instance for + * `methods` lists the methods the route responds to, including the + * automatic `HEAD` for `GET` routes, and is `undefined` when the layer + * matches all methods (routes registered only with `.all()`, and + * `.use()`). `router` is the mounted router instance for * `.use(path, router)` layers, so consumers can recurse by calling * `router.listRoutes()` themselves. * @@ -466,13 +468,15 @@ Router.prototype.listRoutes = function listRoutes () { continue } - const methods = route && !route.methods._all - ? Object.keys(route.methods).map((method) => method.toUpperCase()) + const allOnly = route !== undefined && + route.methods._all && Object.keys(route.methods).length === 1 + const methods = route && !allOnly + ? route._methods().filter((method) => method !== '_ALL') : undefined if (Array.isArray(layer.rawPath)) { for (const path of layer.rawPath) { - routes.push({ path, methods, router }) + routes.push({ path, methods: methods && methods.slice(), router }) } } else { routes.push({ path: layer.rawPath, methods, router }) diff --git a/test/listRoutes.js b/test/listRoutes.js index 24fbc1c..04b7e07 100644 --- a/test/listRoutes.js +++ b/test/listRoutes.js @@ -20,11 +20,23 @@ describe('listRoutes', function () { router.get(['/bar', '/baz'], noop) assert.deepStrictEqual(router.listRoutes(), [ - { path: '/foo', methods: ['GET'], router: undefined }, + { path: '/foo', methods: ['GET', 'HEAD'], router: undefined }, { path: '/:id/setting/:thing', methods: ['POST'], router: undefined }, { path: /^\/[a-z]oo$/, methods: undefined, router: undefined }, - { path: '/bar', methods: ['GET'], router: undefined }, - { path: '/baz', methods: ['GET'], router: undefined } + { path: '/bar', methods: ['GET', 'HEAD'], router: undefined }, + { path: '/baz', methods: ['GET', 'HEAD'], router: undefined } + ]) + }) + + it('should include the automatic HEAD method for GET routes, but not duplicate an explicit one', function () { + const router = new Router() + + router.get('/implicit', noop) + router.route('/explicit').get(noop).head(noop) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/implicit', methods: ['GET', 'HEAD'], router: undefined }, + { path: '/explicit', methods: ['GET', 'HEAD'], router: undefined } ]) }) @@ -37,7 +49,7 @@ describe('listRoutes', function () { .put(noop) assert.deepStrictEqual(router.listRoutes(), [ - { path: '/test', methods: ['GET', 'POST', 'PUT'], router: undefined } + { path: '/test', methods: ['GET', 'POST', 'PUT', 'HEAD'], router: undefined } ]) }) @@ -51,6 +63,19 @@ describe('listRoutes', function () { ]) }) + it('should keep the specific methods of routes that combine .all() with verbs', function () { + const router = new Router() + + router.route('/users') + .all(noop) + .get(noop) + .post(noop) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/users', methods: ['GET', 'POST', 'HEAD'], router: undefined } + ]) + }) + it('should repeat routes registered multiple times', function () { const router = new Router() @@ -59,12 +84,24 @@ describe('listRoutes', function () { router.post('/test', noop) assert.deepStrictEqual(router.listRoutes(), [ - { path: '/test', methods: ['GET'], router: undefined }, - { path: '/test', methods: ['GET'], router: undefined }, + { path: '/test', methods: ['GET', 'HEAD'], router: undefined }, + { path: '/test', methods: ['GET', 'HEAD'], router: undefined }, { path: '/test', methods: ['POST'], router: undefined } ]) }) + it('should not share the methods array between entries of an array path', function () { + const router = new Router() + + router.get(['/bar', '/baz'], noop) + + const routes = router.listRoutes() + + routes[0].methods.push('POST') + + assert.deepStrictEqual(routes[1].methods, ['GET', 'HEAD']) + }) + it('should not list plain middleware', function () { const router = new Router() @@ -73,7 +110,7 @@ describe('listRoutes', function () { router.get('/test', noop) assert.deepStrictEqual(router.listRoutes(), [ - { path: '/test', methods: ['GET'], router: undefined } + { path: '/test', methods: ['GET', 'HEAD'], router: undefined } ]) }) @@ -87,11 +124,11 @@ describe('listRoutes', function () { assert.deepStrictEqual(router.listRoutes(), [ { path: '/inner', methods: undefined, router: inner }, - { path: '/test', methods: ['GET'], router: undefined } + { path: '/test', methods: ['GET', 'HEAD'], router: undefined } ]) assert.deepStrictEqual(inner.listRoutes(), [ - { path: '/api', methods: ['GET'], router: undefined } + { path: '/api', methods: ['GET', 'HEAD'], router: undefined } ]) }) @@ -132,7 +169,7 @@ describe('listRoutes', function () { ]) assert.deepStrictEqual(inner.listRoutes(), [ - { path: '/api', methods: ['GET'], router: undefined }, + { path: '/api', methods: ['GET', 'HEAD'], router: undefined }, { path: '/loop', methods: undefined, router } ]) }) From 84a6cf2b562ec50558ed7389f8e3d21f80f30ad1 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 14 Jul 2026 22:32:50 -0500 Subject: [PATCH 20/22] fix: harden mounted-router detection and path snapshotting in listRoutes - detect mounted routers by their stack instead of duck-typing on a listRoutes property, so plain middleware carrying that name is not misreported and routers from older versions of this package are still listed - copy registered path arrays in Layer so later mutations of the caller's array can't desync listRoutes from the compiled matchers --- README.md | 3 +-- index.js | 4 ++-- lib/layer.js | 4 +++- test/listRoutes.js | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0b106e7..8446a8d 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,7 @@ registered on this router, in registration order. those methods. - `router` is the mounted router instance for `.use(path, router)` entries, otherwise `undefined`. Nested routes are not resolved recursively; consumers - can recurse themselves by calling `router.listRoutes()` on each entry that - exposes one. + can recurse themselves by calling `router.listRoutes()`. Plain middleware functions registered with `.use()` are not listed. diff --git a/index.js b/index.js index 1c7515d..f7f6e2f 100644 --- a/index.js +++ b/index.js @@ -450,7 +450,7 @@ Router.prototype.route = function route (path) { * matches all methods (routes registered only with `.all()`, and * `.use()`). `router` is the mounted router instance for * `.use(path, router)` layers, so consumers can recurse by calling - * `router.listRoutes()` themselves. + * `router.listRoutes()` themselves when it is available. * * @return {Array<{path: string|RegExp, methods: Array|undefined, router: Router|undefined}>} * @public @@ -460,7 +460,7 @@ Router.prototype.listRoutes = function listRoutes () { for (const layer of this.stack) { const route = layer.route - const router = !route && typeof layer.handle.listRoutes === 'function' + const router = !route && Array.isArray(layer.handle.stack) ? layer.handle : undefined diff --git a/lib/layer.js b/lib/layer.js index 72a4a82..aec8afe 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -44,7 +44,9 @@ function Layer (path, options, fn) { this.name = fn.name || '' this.params = undefined this.path = undefined - this.rawPath = path + // copy arrays so later mutations of the caller's array don't desync + // rawPath from the matchers built below + this.rawPath = Array.isArray(path) ? path.slice() : path this.slash = path === '/' && opts.end === false function matcher (_path) { diff --git a/test/listRoutes.js b/test/listRoutes.js index 04b7e07..42bc511 100644 --- a/test/listRoutes.js +++ b/test/listRoutes.js @@ -114,6 +114,42 @@ describe('listRoutes', function () { ]) }) + it('should not reflect later mutations of a registered path array', function () { + const router = new Router() + const paths = ['/a'] + + router.get(paths, noop) + paths.push('/b') + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/a', methods: ['GET', 'HEAD'], router: undefined } + ]) + }) + + it('should not list middleware that merely has a listRoutes property', function () { + const router = new Router() + + function metrics (req, res, next) { next() } + metrics.listRoutes = () => 'not routes' + + router.use('/status', metrics) + + assert.deepStrictEqual(router.listRoutes(), []) + }) + + it('should list mounted routers that do not implement listRoutes', function () { + const router = new Router() + + function legacy (req, res, next) { next() } + legacy.stack = [] + + router.use('/legacy', legacy) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/legacy', methods: undefined, router: legacy } + ]) + }) + it('should expose mounted routers without recursing', function () { const router = new Router() const inner = new Router() From f51173fd12e2cd7733a6cdeeb7b03b927e37c521 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 14 Jul 2026 22:35:48 -0500 Subject: [PATCH 21/22] docs: document empty methods and prefix matching in listRoutes Also add missing test coverage for handler-less routes and routers mounted at a RegExp path. --- README.md | 7 +++++-- test/listRoutes.js | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8446a8d..11c6006 100644 --- a/README.md +++ b/README.md @@ -154,10 +154,13 @@ registered on this router, in registration order. to, including the automatic `HEAD` for `GET` routes. It is `undefined` when the entry matches all methods (routes registered only with `.all()`, and mounted routers). Routes that combine `.all()` with specific methods list - those methods. + those methods. An empty array means the route was created with + `router.route(path)` but has no handlers yet, so it matches no method. - `router` is the mounted router instance for `.use(path, router)` entries, otherwise `undefined`. Nested routes are not resolved recursively; consumers - can recurse themselves by calling `router.listRoutes()`. + can recurse themselves by calling `router.listRoutes()`. Note that `.use()` + entries match their path as a prefix, while route entries match the full + path. Plain middleware functions registered with `.use()` are not listed. diff --git a/test/listRoutes.js b/test/listRoutes.js index 42bc511..e98a2b2 100644 --- a/test/listRoutes.js +++ b/test/listRoutes.js @@ -53,6 +53,16 @@ describe('listRoutes', function () { ]) }) + it('should return empty methods for routes created without handlers', function () { + const router = new Router() + + router.route('/draft') + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/draft', methods: [], router: undefined } + ]) + }) + it('should return undefined methods for .all() routes', function () { const router = new Router() @@ -180,6 +190,18 @@ describe('listRoutes', function () { ]) }) + it('should list routers mounted at a RegExp path', function () { + const router = new Router() + const inner = new Router() + + inner.get('/api', noop) + router.use(/^\/page_([0-9]+)/, inner) + + assert.deepStrictEqual(router.listRoutes(), [ + { path: /^\/page_([0-9]+)/, methods: undefined, router: inner } + ]) + }) + it('should list a mounted router once per path in an array', function () { const router = new Router() const inner = new Router() From 25376b841c4ea560c058967b060517fa93a90a25 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 14 Jul 2026 22:59:15 -0500 Subject: [PATCH 22/22] fix: flatten nested path arrays and move _all filtering into Route - store rawPath with path.flat(Infinity) so nested array paths list as strings and the caller's inner array isn't kept live - teach Route._methods() to exclude the _all sentinel (the OPTIONS Allow builder can never observe it), simplifying listRoutes - clarify recursion and middleware wording in the docs --- README.md | 3 ++- index.js | 15 +++++++++------ lib/layer.js | 5 ++--- lib/route.js | 2 +- test/listRoutes.js | 13 +++++++++++++ 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 11c6006..347f179 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,8 @@ registered on this router, in registration order. `router.route(path)` but has no handlers yet, so it matches no method. - `router` is the mounted router instance for `.use(path, router)` entries, otherwise `undefined`. Nested routes are not resolved recursively; consumers - can recurse themselves by calling `router.listRoutes()`. Note that `.use()` + can recurse themselves by calling `router.listRoutes()` when the mounted + router implements it. Note that `.use()` entries match their path as a prefix, while route entries match the full path. diff --git a/index.js b/index.js index f7f6e2f..a0bd2a2 100644 --- a/index.js +++ b/index.js @@ -448,7 +448,8 @@ Router.prototype.route = function route (path) { * `methods` lists the methods the route responds to, including the * automatic `HEAD` for `GET` routes, and is `undefined` when the layer * matches all methods (routes registered only with `.all()`, and - * `.use()`). `router` is the mounted router instance for + * mounted routers). Plain middleware is not listed. `router` is the + * mounted router instance for * `.use(path, router)` layers, so consumers can recurse by calling * `router.listRoutes()` themselves when it is available. * @@ -468,11 +469,13 @@ Router.prototype.listRoutes = function listRoutes () { continue } - const allOnly = route !== undefined && - route.methods._all && Object.keys(route.methods).length === 1 - const methods = route && !allOnly - ? route._methods().filter((method) => method !== '_ALL') - : undefined + let methods + if (route) { + methods = route._methods() + if (methods.length === 0 && route.methods._all) { + methods = undefined + } + } if (Array.isArray(layer.rawPath)) { for (const path of layer.rawPath) { diff --git a/lib/layer.js b/lib/layer.js index aec8afe..4370e5a 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -44,9 +44,8 @@ function Layer (path, options, fn) { this.name = fn.name || '' this.params = undefined this.path = undefined - // copy arrays so later mutations of the caller's array don't desync - // rawPath from the matchers built below - this.rawPath = Array.isArray(path) ? path.slice() : path + // flat unnests nested path arrays and snapshots the caller's array + this.rawPath = Array.isArray(path) ? path.flat(Infinity) : path this.slash = path === '/' && opts.end === false function matcher (_path) { diff --git a/lib/route.js b/lib/route.js index 1887d78..5e26960 100644 --- a/lib/route.js +++ b/lib/route.js @@ -74,7 +74,7 @@ Route.prototype._handlesMethod = function _handlesMethod (method) { */ Route.prototype._methods = function _methods () { - const methods = Object.keys(this.methods) + const methods = Object.keys(this.methods).filter((method) => method !== '_all') // append automatic head if (this.methods.get && !this.methods.head) { diff --git a/test/listRoutes.js b/test/listRoutes.js index e98a2b2..509957e 100644 --- a/test/listRoutes.js +++ b/test/listRoutes.js @@ -124,6 +124,19 @@ describe('listRoutes', function () { ]) }) + it('should flatten nested arrays of paths', function () { + const router = new Router() + const nested = ['/b'] + + router.get(['/a', nested], noop) + nested.push('/c') + + assert.deepStrictEqual(router.listRoutes(), [ + { path: '/a', methods: ['GET', 'HEAD'], router: undefined }, + { path: '/b', methods: ['GET', 'HEAD'], router: undefined } + ]) + }) + it('should not reflect later mutations of a registered path array', function () { const router = new Router() const paths = ['/a']