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.

538 lines (452 loc) 16.4 kB
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;