Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b21c685
feat: add _mapRoutes method to list registered routes
bjohansebas Jul 28, 2025
73fae83
fix: don't create double slashes when mounting at root
bjohansebas Jul 28, 2025
f491e42
[WIP] include route options
bjohansebas Jul 29, 2025
9b19b4d
include keys for parameter routes
bjohansebas Jul 29, 2025
440a46e
rename function
bjohansebas Jul 29, 2025
faac557
Let the routes be repeated so that consumers can decide what to do, i…
bjohansebas Jul 29, 2025
16b6c77
docs: add docs for this new api
bjohansebas Jul 29, 2025
856dff1
update route collection structure in getRoutes
bjohansebas Nov 1, 2025
02c672d
test: unskip more test
bjohansebas Nov 1, 2025
2b19160
test: unskip more test
bjohansebas Nov 2, 2025
45f5da5
test: unskip more test
bjohansebas Nov 2, 2025
4f611f4
feat: add 'end' option to route options
bjohansebas Nov 2, 2025
376f83d
no normalice paths
bjohansebas Nov 2, 2025
62f14bb
fix: remove 'end' option from route options in collectRoutes function
bjohansebas Nov 2, 2025
306eefd
feat: add route name to collected routes
bjohansebas Nov 8, 2025
e75bcb7
feat: enhance route key extraction for regex and dynamic routes
bjohansebas Nov 9, 2025
6c071e7
docs: update jsdocs and readmed
bjohansebas Nov 9, 2025
5e71b5a
feat: simplify to minimal listRoutes() API
bjohansebas Jul 15, 2026
99d3a49
fix: make listRoutes methods output match dispatch behavior
bjohansebas Jul 15, 2026
84a6cf2
fix: harden mounted-router detection and path snapshotting in listRoutes
bjohansebas Jul 15, 2026
f51173f
docs: document empty methods and prefix matching in listRoutes
bjohansebas Jul 15, 2026
25376b8
fix: flatten nested path arrays and move _all filtering into Route
bjohansebas Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,27 @@ Router.prototype.route = function route (path) {
return route
}

/**
* List all registered routes.
*
* @return {Array} An array of route paths
* @private
Comment thread
bjohansebas marked this conversation as resolved.
Outdated
*/
Router.prototype._mapRoutes = function mapRoutes () {
const routes = []
const stack = this.stack
const routeMap = new Map()
Comment thread
bjohansebas marked this conversation as resolved.
Outdated

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) {
Expand All @@ -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) {
Comment thread
bjohansebas marked this conversation as resolved.
Outdated
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
Comment thread
bjohansebas marked this conversation as resolved.
Outdated
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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For .use, could it make sense to allow consumers to iterate recursively themselves? It's something that could be added in a follow up, but the MVP could be simply { path, method, router }. Every field could also be optional, I guess, since path: undefined with .use(fn), method: undefined when all is used, and router: undefined when no nested router.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path will never be undefined .use always sets the path to '/' when no path is explicitly provided in the arguments.

@blakeembrey blakeembrey Jul 29, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the kind of thing that should be nailed down for the API, but understood. It probably is reasonable to keep it as / and the object was intended to be hypothetical.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. why / vs ""? Does one make it harder for consumers than the other? How do these interact with internal routing behaviors that aren't being exposed in this API? How much can move these expectations/behaviors to be static instead of magic (e.g. removing the trailing / is the one that comes to mind, people need to know how the package works internally).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed here, lets just simplify this down into the bare minimum api before merging this PR. Then we can take on nesting routers and deeply iterating the stack more directly later if we deem it worth it. We want to unstuck this progress, and I think these details is what I got hung up on last time leading to my original blocking review. Better to land what we can for sure agree on adds value and move this forward.

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.
*
Expand Down
3 changes: 3 additions & 0 deletions lib/layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ function Layer (path, options, fn) {
this.keys = []
this.name = fn.name || '<anonymous>'
this.params = undefined
// path is determinate in runtime execution
this.path = undefined

this.pathPatterns = path
Comment thread
bjohansebas marked this conversation as resolved.
Outdated
this.slash = path === '/' && opts.end === false

function matcher (_path) {
Expand Down
115 changes: 115 additions & 0 deletions test/mapRoutes.js
Original file line number Diff line number Diff line change
@@ -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'] }
])
})

Comment thread
bjohansebas marked this conversation as resolved.
Outdated
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 () {}