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
JavaScript
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;