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.
538 lines (452 loc) • 16.4 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
class NestJSParser {
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: [
'typescript',
'decorators-legacy',
'classProperties',
'objectRestSpread',
'asyncGenerators',
'functionBind',
'exportDefaultFrom',
'exportNamespaceFrom',
'dynamicImport'
]
});
traverse(ast, {
ClassDeclaration: (nodePath) => {
const classNode = nodePath.node;
// Check if class has @Controller decorator
if (this.hasDecorator(classNode, 'Controller')) {
const controllerEndpoints = this.parseController(classNode, filePath);
results.endpoints.push(...controllerEndpoints);
}
// Parse DTOs and entities
const schema = this.extractSchema(nodePath, filePath);
if (schema) {
results.schemas.push(schema);
}
}
});
} catch (error) {
console.warn(`⚠️ Failed to parse NestJS file ${filePath}:`, error.message);
}
return results;
}
parseController(classNode, filePath) {
const endpoints = [];
const controllerDecorator = this.getDecorator(classNode, 'Controller');
const basePath = this.extractDecoratorPath(controllerDecorator) || '';
// Parse each method in the controller
classNode.body.body.forEach((member) => {
if ((member.type === 'MethodDefinition' || member.type === 'ClassMethod') && member.kind === 'method') {
const endpoint = this.parseControllerMethod(member, basePath, filePath);
if (endpoint) {
endpoints.push(endpoint);
}
}
});
return endpoints;
}
parseControllerMethod(methodNode, basePath, filePath) {
const decorators = methodNode.decorators || [];
const httpDecorators = ['Get', 'Post', 'Put', 'Delete', 'Patch', 'Options', 'Head'];
let httpMethod = null;
let methodPath = '';
let routeDecorator = null;
// Find HTTP method decorator
for (const decorator of decorators) {
if (decorator.expression.type === 'CallExpression') {
const decoratorName = decorator.expression.callee.name;
if (httpDecorators.includes(decoratorName)) {
httpMethod = decoratorName.toUpperCase();
routeDecorator = decorator;
methodPath = this.extractDecoratorPath(decorator) || '';
break;
}
} else if (decorator.expression.type === 'Identifier') {
const decoratorName = decorator.expression.name;
if (httpDecorators.includes(decoratorName)) {
httpMethod = decoratorName.toUpperCase();
routeDecorator = decorator;
break;
}
}
}
if (!httpMethod) return null;
// Combine base path and method path
const fullPath = this.combinePaths(basePath, methodPath);
// Extract method information
const methodName = methodNode.key.name;
const parameters = this.extractMethodParameters(methodNode);
const guards = this.extractGuards(decorators);
const interceptors = this.extractInterceptors(decorators);
const pipes = this.extractPipes(decorators);
return {
method: httpMethod,
path: fullPath,
file: filePath,
line: methodNode.loc ? methodNode.loc.start.line : null,
description: this.extractMethodComments(methodNode),
methodName,
parameters,
guards,
interceptors,
pipes,
isAsync: methodNode.async || methodNode.value?.async,
returnType: this.extractReturnType(methodNode)
};
}
extractMethodParameters(methodNode) {
const parameters = [];
// Handle both MethodDefinition and ClassMethod
const methodParams = methodNode.params || methodNode.value?.params || [];
methodParams.forEach(param => {
if (param.type === 'Identifier') {
const paramInfo = this.analyzeParameter(param, methodNode);
if (paramInfo) {
parameters.push(paramInfo);
}
}
});
return parameters;
}
analyzeParameter(param, methodNode) {
const decorators = param.decorators || [];
const paramName = param.name;
// Check for parameter decorators
for (const decorator of decorators) {
if (decorator.expression.type === 'CallExpression') {
const decoratorName = decorator.expression.callee.name;
switch (decoratorName) {
case 'Param':
return {
name: this.extractDecoratorArgument(decorator) || paramName,
in: 'path',
required: true,
type: this.extractParameterType(param),
decorator: decoratorName
};
case 'Query':
return {
name: this.extractDecoratorArgument(decorator) || paramName,
in: 'query',
required: false,
type: this.extractParameterType(param),
decorator: decoratorName
};
case 'Body':
return {
name: this.extractDecoratorArgument(decorator) || 'body',
in: 'body',
required: true,
type: this.extractParameterType(param),
decorator: decoratorName
};
case 'Headers':
return {
name: this.extractDecoratorArgument(decorator) || paramName,
in: 'header',
required: false,
type: this.extractParameterType(param),
decorator: decoratorName
};
case 'Req':
case 'Request':
return {
name: 'request',
in: 'internal',
type: 'Request',
decorator: decoratorName
};
case 'Res':
case 'Response':
return {
name: 'response',
in: 'internal',
type: 'Response',
decorator: decoratorName
};
}
} else if (decorator.expression.type === 'Identifier') {
const decoratorName = decorator.expression.name;
switch (decoratorName) {
case 'Body':
return {
name: 'body',
in: 'body',
required: true,
type: this.extractParameterType(param),
decorator: decoratorName
};
}
}
}
return null;
}
extractParameterType(param) {
if (param.typeAnnotation && param.typeAnnotation.typeAnnotation) {
const typeNode = param.typeAnnotation.typeAnnotation;
switch (typeNode.type) {
case 'TSStringKeyword':
return 'string';
case 'TSNumberKeyword':
return 'number';
case 'TSBooleanKeyword':
return 'boolean';
case 'TSArrayType':
return 'array';
case 'TSTypeReference':
return typeNode.typeName.name || 'object';
default:
return 'any';
}
}
return 'any';
}
extractGuards(decorators) {
const guards = [];
decorators.forEach(decorator => {
if (decorator.expression.type === 'CallExpression') {
const decoratorName = decorator.expression.callee.name;
if (decoratorName === 'UseGuards') {
const args = decorator.expression.arguments;
args.forEach(arg => {
if (arg.type === 'Identifier') {
guards.push(arg.name);
}
});
}
}
});
return guards;
}
extractInterceptors(decorators) {
const interceptors = [];
decorators.forEach(decorator => {
if (decorator.expression.type === 'CallExpression') {
const decoratorName = decorator.expression.callee.name;
if (decoratorName === 'UseInterceptors') {
const args = decorator.expression.arguments;
args.forEach(arg => {
if (arg.type === 'Identifier') {
interceptors.push(arg.name);
}
});
}
}
});
return interceptors;
}
extractPipes(decorators) {
const pipes = [];
decorators.forEach(decorator => {
if (decorator.expression.type === 'CallExpression') {
const decoratorName = decorator.expression.callee.name;
if (decoratorName === 'UsePipes') {
const args = decorator.expression.arguments;
args.forEach(arg => {
if (arg.type === 'Identifier') {
pipes.push(arg.name);
}
});
}
}
});
return pipes;
}
extractReturnType(methodNode) {
// Handle both MethodDefinition and ClassMethod
const returnType = methodNode.returnType || methodNode.value?.returnType;
if (returnType && returnType.typeAnnotation) {
const typeNode = returnType.typeAnnotation;
if (typeNode.type === 'TSTypeReference') {
// Handle Promise<Type> or Observable<Type>
if (typeNode.typeName.name === 'Promise' || typeNode.typeName.name === 'Observable') {
if (typeNode.typeParameters && typeNode.typeParameters.params.length > 0) {
const innerType = typeNode.typeParameters.params[0];
if (innerType.type === 'TSTypeReference') {
return innerType.typeName.name;
}
}
}
return typeNode.typeName.name;
}
}
return 'any';
}
extractSchema(nodePath, filePath) {
const node = nodePath.node;
if (!node.decorators) return null;
// Check for Entity, DTO, or Schema decorators
const isSchema = node.decorators.some(decorator => {
if (decorator.expression.type === 'CallExpression') {
const name = decorator.expression.callee.name;
return ['Entity', 'Schema'].includes(name);
} else if (decorator.expression.type === 'Identifier') {
const name = decorator.expression.name;
return ['Entity', 'Schema'].includes(name);
}
return false;
});
// Check for DTO pattern (class name ends with Dto)
const isDtoPattern = node.id.name.endsWith('Dto') || node.id.name.endsWith('DTO');
if (isSchema || isDtoPattern) {
return {
name: node.id.name,
type: 'object',
file: filePath,
line: node.loc ? node.loc.start.line : null,
properties: this.extractClassProperties(node),
isEntity: this.hasDecorator(node, 'Entity'),
isDto: isDtoPattern,
isSchema: this.hasDecorator(node, 'Schema')
};
}
return null;
}
extractClassProperties(classNode) {
const properties = {};
classNode.body.body.forEach(member => {
if (member.type === 'ClassProperty' || member.type === 'PropertyDefinition') {
const propName = member.key.name;
const propType = this.extractPropertyType(member);
const isOptional = member.optional || false;
const decorators = this.extractPropertyDecorators(member);
properties[propName] = {
type: propType,
required: !isOptional,
decorators,
description: this.extractPropertyComments(member)
};
}
});
return properties;
}
extractPropertyType(propertyNode) {
if (propertyNode.typeAnnotation && propertyNode.typeAnnotation.typeAnnotation) {
const typeNode = propertyNode.typeAnnotation.typeAnnotation;
return this.typeScriptTypeToOpenAPI(typeNode);
}
return 'any';
}
extractPropertyDecorators(propertyNode) {
const decorators = [];
if (propertyNode.decorators) {
propertyNode.decorators.forEach(decorator => {
if (decorator.expression.type === 'CallExpression') {
decorators.push(decorator.expression.callee.name);
} else if (decorator.expression.type === 'Identifier') {
decorators.push(decorator.expression.name);
}
});
}
return decorators;
}
typeScriptTypeToOpenAPI(typeNode) {
switch (typeNode.type) {
case 'TSStringKeyword':
return 'string';
case 'TSNumberKeyword':
return 'number';
case 'TSBooleanKeyword':
return 'boolean';
case 'TSArrayType':
return 'array';
case 'TSTypeReference':
const typeName = typeNode.typeName.name;
if (typeName === 'Date') return 'string';
return 'object';
case 'TSUnionType':
// For union types, return the first type or 'string' as fallback
if (typeNode.types.length > 0) {
return this.typeScriptTypeToOpenAPI(typeNode.types[0]);
}
return 'string';
default:
return 'any';
}
}
hasDecorator(node, decoratorName) {
if (!node.decorators) return false;
return node.decorators.some(decorator => {
if (decorator.expression.type === 'CallExpression') {
return decorator.expression.callee.name === decoratorName;
} else if (decorator.expression.type === 'Identifier') {
return decorator.expression.name === decoratorName;
}
return false;
});
}
getDecorator(node, decoratorName) {
if (!node.decorators) return null;
return node.decorators.find(decorator => {
if (decorator.expression.type === 'CallExpression') {
return decorator.expression.callee.name === decoratorName;
} else if (decorator.expression.type === 'Identifier') {
return decorator.expression.name === decoratorName;
}
return false;
});
}
extractDecoratorPath(decorator) {
if (!decorator || !decorator.expression) return '';
if (decorator.expression.type === 'CallExpression') {
const args = decorator.expression.arguments;
if (args.length > 0 && args[0].type === 'StringLiteral') {
return args[0].value;
}
}
return '';
}
extractDecoratorArgument(decorator) {
if (decorator.expression.type === 'CallExpression') {
const args = decorator.expression.arguments;
if (args.length > 0 && args[0].type === 'StringLiteral') {
return args[0].value;
}
}
return null;
}
extractMethodComments(methodNode) {
// Look for leading comments
const comments = methodNode.leadingComments;
if (comments && comments.length > 0) {
return comments.map(c => c.value.trim()).join('\n');
}
return null;
}
extractPropertyComments(propertyNode) {
// Look for leading comments
const comments = propertyNode.leadingComments;
if (comments && comments.length > 0) {
return comments.map(c => c.value.trim()).join('\n');
}
return null;
}
combinePaths(basePath, methodPath) {
if (!basePath && !methodPath) return '/';
if (!basePath) return methodPath.startsWith('/') ? methodPath : '/' + methodPath;
if (!methodPath) return basePath.startsWith('/') ? basePath : '/' + basePath;
const base = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
const method = methodPath.startsWith('/') ? methodPath : '/' + methodPath;
return base + method;
}
}
module.exports = NestJSParser;