Last active
June 13, 2023 21:30
-
-
Save MichaelFedora/d737352711dbbb82c7a4d046270e4eec to your computer and use it in GitHub Desktop.
Parse router layers into something usable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Parse Express Router Layers into something usable, with | |
* formatted paths, parameters lists, and ancestry tracking. | |
*/ | |
/** | |
* A "reduced layer", the output of `reduceStack`. Has | |
* simplified information in it. | |
*/ | |
export interface ReducedLayer { | |
/** All of the handles for the layers above this one */ | |
ancestry: ((..._) => unknown)[]; | |
/** The handle (logic) */ | |
handle: (..._) => unknown; | |
/** The method (GET, POST, etc) */ | |
method: string; | |
/** The formatted path */ | |
path: string; | |
/** The route parameters */ | |
params: { name: string | number; optional?: boolean }[]; | |
} | |
/** | |
* A very hacky interface for the Express Router Layer type. | |
*/ | |
interface RouteLayer { | |
handle?: ((..._) => void) & { | |
params?: unknown; | |
_params?: unknown[]; | |
caseSensitive?: boolean; | |
mergeParams?: boolean; | |
strict?: boolean; | |
stack?: RouteLayer[]; | |
}; | |
name?: string; | |
params: unknown; | |
path: string; | |
keys: { name: string; optional: boolean; offset: number }[]; | |
regexp: RegExp; | |
method: string; | |
route?: { | |
path: string | RegExp; | |
stack: RouteLayer[]; | |
methods: Record<string, boolean>; | |
}; | |
} | |
/** | |
* Reduce a route layer stack from a route into something more | |
* consumable, with formatted paths and parameter lists. | |
* | |
* @param stack The layer stack from a router | |
* @param base Base information for recursive logic | |
* @returns The reduced layers | |
*/ | |
export function reduceStack( | |
stack: RouteLayer[], | |
base?: Partial<Pick<ReducedLayer, 'ancestry' | 'path' | 'params'>> | |
): ReducedLayer[] { | |
const filledBase: Pick<ReducedLayer, 'ancestry' | 'path' | 'params'> = Object.assign( | |
{ path: '', params: [], ancestry: [] }, | |
base | |
); | |
const basePath = filledBase.path.endsWith('/') ? filledBase.path : (filledBase.path + '/'); | |
const baseParams: Readonly<ReducedLayer['params']> = filledBase.params; | |
const ancestry: ReducedLayer['ancestry'] = filledBase.ancestry.slice(); | |
const ret: ReducedLayer[] = []; | |
for(const layer of stack) { | |
let path = layer.path && typeof layer.path === 'string' | |
? layer.path | |
: regexpToPath(layer.regexp, layer.keys); | |
const params = baseParams.slice(); | |
if(!path) | |
path = basePath.slice(0, -1); // remove trailing / | |
else | |
path = basePath + path.replace(/^\/+|\/+$/g, ''); // replace leading & trailing /'s | |
if(layer.handle && layer.keys?.length) { | |
for(const key of layer.keys) { | |
path = path.replace(':' + key.name, `{${key.name}}`); | |
params.push({ name: key.name, optional: key.optional }); | |
} | |
} | |
if(layer.method) | |
ret.push({ ancestry: ancestry.slice(), handle: layer.handle, method: layer.method, path, params }); | |
// throw this layer onto the ancestry, for children | |
// but also for other layers on this level | |
if(layer.handle) | |
ancestry.push(layer.handle); | |
if(layer.name === 'router' && layer.handle?.stack) | |
for(const subLayer of reduceStack(layer.handle.stack, { path, params, ancestry })) | |
ret.push(subLayer); | |
if(layer.route?.stack) | |
for(const subLayer of reduceStack(layer.route.stack, { path, params, ancestry })) | |
ret.push(subLayer); | |
} | |
return ret; | |
} | |
/** | |
* A heavily modified function to compute a path from a regexp. | |
* | |
* - originally from https://github.com/expressjs/express/issues/3308#issuecomment-300957572 | |
* - also from https://github.com/wesleytodd/express-openapi/blob/main/lib/generate-doc.js | |
* | |
* @param regexp The regexp to consume | |
* @param keys The parameter keys to replace | |
* @returns The formatted path | |
*/ | |
export function regexpToPath(regexp: RegExp & { fast_slash?: boolean }, keys: { name: string }[]): string { | |
if(regexp.fast_slash) | |
return ''; | |
let str = regexp.toString(); | |
if(keys?.length) { | |
for (const key of keys) { | |
const closestGeneric = str.indexOf('/(?:([^\\/]+?))'); | |
const closestOverall = str.indexOf('/('); | |
if(closestOverall === closestGeneric) { // much simpler to do this | |
str = str.replace('(?:([^\\/]+?))', ':' + key.name); | |
} else { // if it's a more complicated match.. | |
// replace all the [...] and all loose parentheses with underscores | |
// so we don't have to worry about them when taking our measurements | |
const simplerStr = str.replace( | |
/[^\\]\[(.+?[^\\])\]/, | |
ss => ss[0] + new Array(ss.length - 1).fill('_').join('') | |
).replace(/\\[)(]/g, '_'); | |
const first = simplerStr.indexOf('/(') + 1; // find the start point | |
if(first === 0) | |
continue; // it broke | |
let len = 1; | |
let depth = 1; | |
// find the end point via checking when our depth of groups reaches 0 | |
for(; depth > 0; len++) { | |
const char = simplerStr.charAt(first + len); | |
if(!char) | |
break; | |
else if(char === '(') | |
depth++; | |
else if(char === ')') | |
depth--; | |
} | |
let pre = simplerStr.slice(0, first - 1); | |
if(pre.endsWith('/')) | |
pre = pre.replace(/\/+$/, ''); | |
let post = simplerStr.slice(first + len); | |
if(post.startsWith('/')) | |
post = post.replace(/^\/+/, ''); | |
// this would be the way to get the regex-like matcher, but for now | |
// it is discarded | |
// matcher = path.slice(first, first + len); | |
// format it nicely | |
str = `${pre}/{${key.name}}/${post}`; | |
} | |
} | |
} | |
return str | |
.replace('(?=\\/|$)', '$') // weird matchers begone | |
.replace(/^\/\^\\?|(?:\/\?)?\$\/i$/g, '') // get rid of the prefix/suffix matchers | |
.replace('\\/?', '') // no weird artifacts | |
.replace(/\\/gi, ''); // no double slashes | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment