apitally
Version:
Simple API monitoring & analytics for REST APIs built with Express, Fastify, NestJS, AdonisJS, Hono, H3, Elysia, Hapi, and Koa.
1 lines • 14.9 kB
Source Map (JSON)
{"version":3,"sources":["../../src/express/utils.js"],"sourcesContent":["// Adapted from https://github.com/AlbertoFdzM/express-list-endpoints/blob/305535d43008b46f34e18b01947762e039af6d2d/src/index.js\n// and also incorporated https://github.com/AlbertoFdzM/express-list-endpoints/pull/96\n// and https://github.com/labithiotis/express-list-routes/blob/5432c83a67c2788c56a1cdfc067ec809961c0c05/index.js\n\n/**\n * @typedef {Object} Route\n * @property {Object} methods\n * @property {string | string[]} path\n * @property {any[]} stack\n *\n * @typedef {Object} Endpoint\n * @property {string} path Path name\n * @property {string[]} methods Methods handled\n * @property {string[]} middlewares Mounted middlewares\n */\n\nconst regExpToParseExpressPathRegExp =\n /^\\/\\^\\\\?\\/?(?:(:?[\\w\\\\.-]*(?:\\\\\\/:?[\\w\\\\.-]*)*)|(\\(\\?:\\\\?\\/?\\([^)]+\\)\\)))\\\\\\/.*/;\nconst regExpToReplaceExpressPathRegExpParams = /\\(\\?:\\\\?\\/?\\([^)]+\\)\\)/;\nconst regExpExpressParamRegExp = /\\(\\?:\\\\?\\\\?\\/?\\([^)]+\\)\\)/g;\nconst regExpExpressPathParamRegExp = /(:[^)]+)\\([^)]+\\)/g;\nconst rootPathRegExpValue = \"/^\\\\/?(?=\\\\/|$)/i\";\nconst stackItemValidNames = [\"router\", \"bound dispatch\", \"mounted_app\"];\n\n/**\n * Strips inline RegExp validators from Express path params, e.g.\n * \"/users/:id(\\\\d+)\" → \"/users/:id\".\n * @param {string} path\n * @returns {string}\n */\nexport const stripExpressPathParamRegex = function (path) {\n return path.replace(regExpExpressPathParamRegExp, \"$1\");\n};\n\n/**\n * Formats a RegExp route as a stable string identifier, e.g.\n * /^\\/foo$/ → \"RegExp(/^\\\\/foo$/)\".\n * @param {RegExp} re\n * @returns {string}\n */\nexport const formatRegExpRoutePath = function (re) {\n return `RegExp(${re})`;\n};\n\n/**\n * Detects Express version and returns router information\n * @param {import('express').Express | import('express').Router | any} app\n * @returns {{stack: any[] | null, version: 'v4' | 'v5'}}\n */\nexport const getRouterInfo = function (app) {\n if (app.stack) {\n // Express 4 router\n return { stack: app.stack, version: \"v4\" };\n } else if (app._router?.stack) {\n // Express 4\n return { stack: app._router.stack, version: \"v4\" };\n } else if (app.router?.stack) {\n // Express 5\n return { stack: app.router.stack, version: \"v5\" };\n }\n return { stack: null, version: \"v4\" };\n};\n\n/**\n * Returns all the verbs detected for the passed route\n * @param {Route} route\n */\nconst getRouteMethods = function (route) {\n let methods = Object.keys(route.methods);\n\n methods = methods.filter((method) => method !== \"_all\");\n methods = methods.map((method) => method.toUpperCase());\n\n return methods;\n};\n\n/**\n * Returns the names (or anonymous) of all the middlewares attached to the\n * passed route\n * @param {Route} route\n * @returns {string[]}\n */\nconst getRouteMiddlewares = function (route) {\n return route.stack.map((item) => {\n return item.handle.name || \"anonymous\";\n });\n};\n\n/**\n * Returns true if found regexp related with express params\n * @param {string} expressPathRegExp\n * @returns {boolean}\n */\nconst hasParams = function (expressPathRegExp) {\n return regExpExpressParamRegExp.test(expressPathRegExp);\n};\n\n/**\n * @param {Route} route Express route object to be parsed\n * @param {string} basePath The basePath the route is on\n * @return {Endpoint[]} Endpoints info\n */\nconst parseExpressRoute = function (route, basePath) {\n // route.path can be a string, a RegExp, or an array of either\n const paths = Array.isArray(route.path) ? route.path : [route.path];\n\n /** @type {Endpoint[]} */\n const endpoints = paths.flatMap((path) => {\n let pathStr;\n if (typeof path === \"string\") {\n pathStr = stripExpressPathParamRegex(path);\n } else if (path instanceof RegExp) {\n pathStr = formatRegExpRoutePath(path);\n } else {\n return [];\n }\n const completePath =\n basePath && pathStr === \"/\" ? basePath : `${basePath}${pathStr}`;\n return [\n {\n path: completePath,\n methods: getRouteMethods(route),\n middlewares: getRouteMiddlewares(route),\n },\n ];\n });\n\n return endpoints;\n};\n\n/**\n * @param {RegExp} expressPathRegExp\n * @param {any[]} keys\n * @returns {string}\n */\nexport const parseExpressPathRegExp = function (expressPathRegExp, keys) {\n let parsedRegExp = expressPathRegExp.toString();\n let expressPathRegExpExec = regExpToParseExpressPathRegExp.exec(parsedRegExp);\n let paramIndex = 0;\n\n while (hasParams(parsedRegExp)) {\n const paramName = keys[paramIndex].name;\n const paramId = `:${paramName}`;\n\n parsedRegExp = parsedRegExp.replace(\n regExpToReplaceExpressPathRegExpParams,\n (str) => {\n // Express >= 4.20.0 uses a different RegExp for parameters: it\n // captures the slash as part of the parameter. We need to check\n // for this case and add the slash to the value that will replace\n // the parameter in the path.\n if (str.startsWith(\"(?:\\\\/\")) {\n return `\\\\/${paramId}`;\n }\n\n return paramId;\n },\n );\n\n paramIndex++;\n }\n\n if (parsedRegExp !== expressPathRegExp.toString()) {\n expressPathRegExpExec = regExpToParseExpressPathRegExp.exec(parsedRegExp);\n }\n\n return expressPathRegExpExec[1].replace(/\\\\\\//g, \"/\");\n};\n\n/**\n * @param {string} expressPath\n * @param {Object.<string, string>} params\n * @returns {string}\n */\nexport const parseExpressPath = function (expressPath, params) {\n let result = expressPath;\n for (const [paramName, paramValue] of Object.entries(params)) {\n result = result.replace(paramValue, `:${paramName}`);\n }\n return result;\n};\n\n/**\n * @param {import('express').Express | import('express').Router | any} app\n * @param {string} [basePath]\n * @param {Endpoint[]} [endpoints]\n * @returns {Endpoint[]}\n */\nconst parseEndpoints = function (app, basePath, endpoints) {\n const routerInfo = getRouterInfo(app);\n const stack = routerInfo.stack;\n const version = routerInfo.version;\n\n endpoints = endpoints || [];\n basePath = basePath || \"\";\n\n if (!stack) {\n if (endpoints.length) {\n endpoints = addEndpoints(endpoints, [\n {\n path: basePath,\n methods: [],\n middlewares: [],\n },\n ]);\n }\n } else {\n endpoints = parseStack(stack, basePath, endpoints, version);\n }\n\n return endpoints;\n};\n\n/**\n * Ensures the path of the new endpoints isn't yet in the array.\n * If the path is already in the array merges the endpoints with the existing\n * one, if not, it adds them to the array.\n *\n * @param {Endpoint[]} currentEndpoints Array of current endpoints\n * @param {Endpoint[]} endpointsToAdd New endpoints to be added to the array\n * @returns {Endpoint[]} Updated endpoints array\n */\nconst addEndpoints = function (currentEndpoints, endpointsToAdd) {\n endpointsToAdd.forEach((newEndpoint) => {\n const existingEndpoint = currentEndpoints.find(\n (endpoint) => endpoint.path === newEndpoint.path,\n );\n\n if (existingEndpoint !== undefined) {\n const newMethods = newEndpoint.methods.filter(\n (method) => !existingEndpoint.methods.includes(method),\n );\n\n existingEndpoint.methods = existingEndpoint.methods.concat(newMethods);\n } else {\n currentEndpoints.push(newEndpoint);\n }\n });\n\n return currentEndpoints;\n};\n\n/**\n * @param {any[]} stack\n * @param {string} basePath\n * @param {Endpoint[]} endpoints\n * @param {'v4' | 'v5'} [version]\n * @returns {Endpoint[]}\n */\nconst parseStack = function (stack, basePath, endpoints, version) {\n stack.forEach((stackItem) => {\n if (stackItem.route) {\n const newEndpoints = parseExpressRoute(stackItem.route, basePath);\n\n endpoints = addEndpoints(endpoints, newEndpoints);\n } else if (stackItemValidNames.includes(stackItem.name)) {\n let newBasePath = basePath;\n\n if (version === \"v4\") {\n const isExpressPathRegExp = regExpToParseExpressPathRegExp.test(\n stackItem.regexp,\n );\n if (isExpressPathRegExp) {\n const parsedPath = parseExpressPathRegExp(\n stackItem.regexp,\n stackItem.keys,\n );\n newBasePath += `/${parsedPath}`;\n } else if (\n !stackItem.path &&\n stackItem.regexp &&\n stackItem.regexp.toString() !== rootPathRegExpValue\n ) {\n newBasePath += `/${formatRegExpRoutePath(stackItem.regexp)}`;\n }\n } else if (version === \"v5\") {\n if (!stackItem.path) {\n return;\n } else if (stackItem.path !== \"/\") {\n newBasePath += stackItem.path.startsWith(\"/\")\n ? stackItem.path\n : `/${stackItem.path}`;\n }\n }\n\n endpoints = parseEndpoints(stackItem.handle, newBasePath, endpoints);\n }\n });\n\n return endpoints;\n};\n\nexport const getEndpoints = function (app, basePath) {\n const endpoints = parseEndpoints(app);\n const standardHttpMethods = [\"GET\", \"POST\", \"PUT\", \"DELETE\", \"PATCH\"];\n return endpoints.flatMap((route) =>\n route.methods\n .filter((method) => standardHttpMethods.includes(method.toUpperCase()))\n .map((method) => ({\n method,\n path: (basePath + route.path).replace(/\\/\\//g, \"/\"),\n })),\n );\n};\n"],"mappings":"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","names":["regExpToParseExpressPathRegExp","regExpToReplaceExpressPathRegExpParams","regExpExpressParamRegExp","regExpExpressPathParamRegExp","rootPathRegExpValue","stackItemValidNames","stripExpressPathParamRegex","path","replace","formatRegExpRoutePath","re","getRouterInfo","app","stack","version","_router","router","getRouteMethods","route","methods","Object","keys","filter","method","map","toUpperCase","getRouteMiddlewares","item","handle","name","hasParams","expressPathRegExp","test","parseExpressRoute","basePath","paths","Array","isArray","endpoints","flatMap","pathStr","RegExp","completePath","middlewares","parseExpressPathRegExp","parsedRegExp","toString","expressPathRegExpExec","exec","paramIndex","paramName","paramId","str","startsWith","parseExpressPath","expressPath","params","result","paramValue","entries","parseEndpoints","routerInfo","length","addEndpoints","parseStack","currentEndpoints","endpointsToAdd","forEach","newEndpoint","existingEndpoint","find","endpoint","undefined","newMethods","includes","concat","push","stackItem","newEndpoints","newBasePath","isExpressPathRegExp","regexp","parsedPath","getEndpoints","standardHttpMethods"]}