UNPKG

api-scout

Version:

🔍 Automatically scout, discover and generate beautiful interactive API documentation from your codebase. Supports Express.js, NestJS, FastAPI, Spring Boot with interactive testing and security analysis.

259 lines (217 loc) 7.5 kB
const fs = require('fs-extra'); const path = require('path'); const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; class ExpressParser { constructor(options = {}) { this.options = options; } async parseFile(filePath) { const content = await fs.readFile(filePath, 'utf8'); const results = { endpoints: [], schemas: [] }; try { const ast = parser.parse(content, { sourceType: 'module', allowImportExportEverywhere: true, allowReturnOutsideFunction: true, plugins: [ 'jsx', 'typescript', 'decorators-legacy', 'classProperties', 'objectRestSpread', 'asyncGenerators', 'functionBind', 'exportDefaultFrom', 'exportNamespaceFrom', 'dynamicImport' ] }); traverse(ast, { CallExpression: (nodePath) => { const endpoint = this.extractEndpoint(nodePath, filePath); if (endpoint) { results.endpoints.push(endpoint); } }, VariableDeclarator: (nodePath) => { const schema = this.extractSchema(nodePath, filePath); if (schema) { results.schemas.push(schema); } } }); } catch (error) { console.warn(`⚠️ Failed to parse ${filePath}:`, error.message); } return results; } extractEndpoint(nodePath, filePath) { const node = nodePath.node; // Look for app.method() or router.method() calls if (node.callee && node.callee.type === 'MemberExpression') { const object = node.callee.object; const property = node.callee.property; // Check if it's an HTTP method const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head']; if (property && property.type === 'Identifier' && httpMethods.includes(property.name.toLowerCase())) { const method = property.name.toUpperCase(); // Extract path from first argument let path = null; if (node.arguments[0]) { if (node.arguments[0].type === 'StringLiteral') { path = node.arguments[0].value; } else if (node.arguments[0].type === 'TemplateLiteral') { path = this.extractTemplateLiteralValue(node.arguments[0]); } } if (path) { const endpoint = { method, path, file: filePath, line: node.loc ? node.loc.start.line : null, description: this.extractComments(nodePath), parameters: this.extractParameters(node), middleware: this.extractMiddleware(node), handler: this.extractHandler(node) }; return endpoint; } } } return null; } extractParameters(node) { const parameters = []; // Extract path parameters if (node.arguments[0] && node.arguments[0].type === 'StringLiteral') { const path = node.arguments[0].value; const pathParams = path.match(/:([^/]+)/g); if (pathParams) { pathParams.forEach(param => { parameters.push({ name: param.substring(1), in: 'path', required: true, type: 'string' }); }); } } // Look for req.query, req.body, req.params usage in handler const handler = this.getHandlerFunction(node); if (handler) { // This would require more sophisticated analysis // For now, we'll mark common patterns const handlerSource = this.nodeToString(handler); if (handlerSource.includes('req.query')) { parameters.push({ name: 'query', in: 'query', required: false, type: 'object', description: 'Query parameters' }); } if (handlerSource.includes('req.body')) { parameters.push({ name: 'body', in: 'body', required: true, type: 'object', description: 'Request body' }); } } return parameters; } extractMiddleware(node) { const middleware = []; // Extract middleware from arguments (everything except first and last) if (node.arguments.length > 2) { for (let i = 1; i < node.arguments.length - 1; i++) { const arg = node.arguments[i]; if (arg.type === 'Identifier') { middleware.push(arg.name); } else if (arg.type === 'CallExpression' && arg.callee.type === 'Identifier') { middleware.push(arg.callee.name); } } } return middleware; } extractHandler(node) { const handler = this.getHandlerFunction(node); if (!handler) return null; return { type: handler.type, async: handler.async || false, params: handler.params ? handler.params.map(p => p.name) : [], source: this.nodeToString(handler).substring(0, 200) + '...' // Truncate for brevity }; } getHandlerFunction(node) { // The handler is typically the last argument const lastArg = node.arguments[node.arguments.length - 1]; if (lastArg && (lastArg.type === 'FunctionExpression' || lastArg.type === 'ArrowFunctionExpression')) { return lastArg; } return null; } extractComments(nodePath) { // Look for leading comments const comments = nodePath.node.leadingComments; if (comments && comments.length > 0) { return comments.map(c => c.value.trim()).join('\n'); } // Look for comments in parent nodes let parent = nodePath.parent; while (parent && !parent.leadingComments) { parent = parent.parent; } if (parent && parent.leadingComments) { return parent.leadingComments.map(c => c.value.trim()).join('\n'); } return null; } extractSchema(nodePath, filePath) { // Look for schema definitions (Joi, Yup, etc.) const node = nodePath.node; if (node.id && node.id.type === 'Identifier' && node.init) { const name = node.id.name; // Simple heuristic for schema detection if (name.toLowerCase().includes('schema') || name.toLowerCase().includes('validation')) { return { name, type: 'object', file: filePath, line: node.loc ? node.loc.start.line : null, definition: this.nodeToString(node.init).substring(0, 300) + '...' }; } } return null; } extractTemplateLiteralValue(node) { // Simple extraction for template literals if (node.quasis && node.quasis.length === 1) { return node.quasis[0].value.raw; } // For complex template literals, return a placeholder return node.quasis ? node.quasis[0].value.raw + '...' : '...'; } nodeToString(node) { // Simple node-to-string conversion // In a real implementation, you'd want to use a proper code generator try { return JSON.stringify(node, null, 2); } catch { return '[Complex expression]'; } } } module.exports = ExpressParser;