UNPKG

@adamtools/apifinder

Version:

Finds all api endpoints of any nextjs project and adds them into README.md , makes api documentation easier

329 lines (282 loc) 11.4 kB
const fs = require('fs-extra'); const path = require('path'); const { glob } = require('glob'); class ApiFinder { constructor(projectPath = process.cwd()) { this.projectPath = projectPath; this.endpoints = []; } /** * Main function to analyze Next.js project and find all API endpoints */ async analyze() { const apiPaths = await this.findApiPaths(); for (const apiPath of apiPaths) { const endpoint = await this.analyzeFile(apiPath); if (endpoint) { this.endpoints.push(endpoint); } } return this.endpoints; } /** * Find all API route files in the project */ async findApiPaths() { const patterns = [ // Pages Router patterns - any JS/TS file in api directory 'pages/api/**/*.js', 'pages/api/**/*.ts', 'pages/api/**/*.jsx', 'pages/api/**/*.tsx', 'src/pages/api/**/*.js', 'src/pages/api/**/*.ts', 'src/pages/api/**/*.jsx', 'src/pages/api/**/*.tsx', // App Router patterns - any JS/TS file in api directory (not just route files) 'app/api/**/*.js', 'app/api/**/*.ts', 'app/api/**/*.jsx', 'app/api/**/*.tsx', 'src/app/api/**/*.js', 'src/app/api/**/*.ts', 'src/app/api/**/*.jsx', 'src/app/api/**/*.tsx' ]; let files = []; for (const pattern of patterns) { const matches = await glob(pattern, { cwd: this.projectPath }); files = files.concat(matches.map(file => path.join(this.projectPath, file))); } // Remove duplicates return [...new Set(files)]; } /** * Analyze a single API file to determine HTTP methods and route info */ async analyzeFile(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); const relativePath = path.relative(this.projectPath, filePath); // Determine if it's App Router or Pages Router more accurately const isAppRouter = this.determineRouterType(relativePath, content); const route = this.extractRoute(relativePath, isAppRouter); const methods = this.extractHttpMethods(content, isAppRouter); return { file: relativePath, route, methods, type: isAppRouter ? 'App Router' : 'Pages Router' }; } catch (error) { console.warn(`Warning: Could not analyze ${filePath}:`, error.message); return null; } } /** * Determine router type more accurately */ determineRouterType(filePath, content) { // Simple path-based detection if (filePath.includes('/app/api/')) { return true; // App Router } if (filePath.includes('/pages/api/')) { return false; // Pages Router } // If path detection is unclear, check content patterns // App Router typically has named exports for HTTP methods const appRouterPatterns = [ /export\s+(async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(/, /export\s+const\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*=/, /export\s*{\s*[^}]*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)[^}]*}/ ]; // Pages Router typically has default export with req, res parameters const pagesRouterPatterns = [ /export\s+default\s+(async\s+)?function[^(]*\([^)]*req[^)]*res[^)]*\)/, /export\s+default\s*\([^)]*req[^)]*res[^)]*\)\s*=>/, /module\.exports\s*=\s*(async\s+)?function[^(]*\([^)]*req[^)]*res[^)]*\)/ ]; const hasAppRouterPattern = appRouterPatterns.some(pattern => pattern.test(content)); const hasPagesRouterPattern = pagesRouterPatterns.some(pattern => pattern.test(content)); if (hasAppRouterPattern && !hasPagesRouterPattern) { return true; // App Router } if (hasPagesRouterPattern && !hasAppRouterPattern) { return false; // Pages Router } // Fallback: if file is in app directory, assume App Router, otherwise Pages Router return filePath.includes('/app/'); } /** * Extract the API route from file path */ extractRoute(filePath, isAppRouter) { let route; if (isAppRouter) { // App Router: app/api/users/route.js -> /api/users route = filePath .replace(/^(src\/)?app/, '') .replace(/\/route\.(js|ts)$/, '') .replace(/\/page\.(js|ts)$/, ''); } else { // Pages Router: pages/api/users.js -> /api/users route = filePath .replace(/^(src\/)?pages/, '') .replace(/\.(js|ts)$/, '') .replace(/\/index$/, ''); } // Handle dynamic routes route = route.replace(/\[([^\]]+)\]/g, ':$1'); route = route.replace(/\[\.\.\.([^\]]+)\]/g, '*$1'); return route || '/'; } /** * Extract HTTP methods from file content */ extractHttpMethods(content, isAppRouter) { const methods = []; if (isAppRouter) { // App Router: named exports for HTTP methods const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; for (const method of httpMethods) { const patterns = [ // export async function GET new RegExp(`export\\s+async\\s+function\\s+${method}\\s*\\(`, 'gm'), // export function GET new RegExp(`export\\s+function\\s+${method}\\s*\\(`, 'gm'), // export const GET = new RegExp(`export\\s+const\\s+${method}\\s*=\\s*(async\\s+)?\\(`, 'gm'), // export { GET } new RegExp(`export\\s*{[^}]*\\b${method}\\b[^}]*}`, 'gm'), // function GET() followed by export new RegExp(`function\\s+${method}\\s*\\([^)]*\\)\\s*{[^}]*}[\\s\\S]*?export\\s*{[^}]*\\b${method}\\b`, 'gm') ]; if (patterns.some(pattern => pattern.test(content))) { methods.push(method); } } } else { // Pages Router: check req.method in handler // Pattern 1: req.method === 'METHOD' or req.method == 'METHOD' const methodChecks = content.match(/req\.method\s*[!=]==?\s*['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]/gi); if (methodChecks) { methodChecks.forEach(match => { const method = match.match(/['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]/i); if (method && !methods.includes(method[1].toUpperCase())) { methods.push(method[1].toUpperCase()); } }); } // Pattern 2: 'METHOD' === req.method const reverseMethodChecks = content.match(/['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]\s*[!=]==?\s*req\.method/gi); if (reverseMethodChecks) { reverseMethodChecks.forEach(match => { const method = match.match(/['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]/i); if (method && !methods.includes(method[1].toUpperCase())) { methods.push(method[1].toUpperCase()); } }); } // Pattern 3: switch statements const switchMatch = content.match(/switch\s*\(\s*req\.method\s*\)\s*{([^{}]*(?:{[^{}]*}[^{}]*)*)}/s); if (switchMatch) { const cases = switchMatch[1].match(/case\s+['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]/gi); if (cases) { cases.forEach(caseMatch => { const method = caseMatch.match(/['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]/i); if (method && !methods.includes(method[1].toUpperCase())) { methods.push(method[1].toUpperCase()); } }); } } // Pattern 4: if/else chains with method checks const ifStatements = content.match(/if\s*\([^)]*req\.method[^)]*\)/gi); if (ifStatements) { ifStatements.forEach(ifStatement => { const method = ifStatement.match(/['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]/i); if (method && !methods.includes(method[1].toUpperCase())) { methods.push(method[1].toUpperCase()); } }); } // Pattern 5: allowedMethods array or similar const allowedMethodsMatch = content.match(/allowedMethods?\s*[=:]\s*\[([^\]]*)\]/i); if (allowedMethodsMatch) { const methodsInArray = allowedMethodsMatch[1].match(/['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]/gi); if (methodsInArray) { methodsInArray.forEach(method => { const cleanMethod = method.replace(/['"]/g, '').toUpperCase(); if (!methods.includes(cleanMethod)) { methods.push(cleanMethod); } }); } } // If no specific methods found but has a handler, try to infer if (methods.length === 0) { const hasHandler = content.includes('export default') || content.includes('module.exports'); const hasReqRes = content.includes('req') && content.includes('res'); if (hasHandler && hasReqRes) { // Try to guess based on common patterns if (content.includes('req.body') || content.includes('req.json')) { methods.push('POST'); } if (content.includes('res.json') || content.includes('res.send')) { if (!methods.includes('GET')) { methods.push('GET'); } } // If still no methods found, default to GET if (methods.length === 0) { methods.push('GET'); } } } } return methods.length > 0 ? methods : ['GET']; // Default to GET if nothing found } /** * Generate markdown table from endpoints */ generateMarkdownTable() { if (this.endpoints.length === 0) { return '## API Endpoints\n\nNo API endpoints found in this project.\n'; } let markdown = '## API Endpoints\n\n'; markdown += '| Route | Methods | File | Type |\n'; markdown += '|-------|---------|------|------|\n'; // Sort endpoints by route const sortedEndpoints = this.endpoints.sort((a, b) => a.route.localeCompare(b.route)); for (const endpoint of sortedEndpoints) { const methods = endpoint.methods.join(', '); markdown += `| \`${endpoint.route}\` | ${methods} | \`${endpoint.file}\` | ${endpoint.type} |\n`; } markdown += '\n'; return markdown; } /** * Update README.md file with endpoints table */ async updateReadme(readmePath = 'README.md') { const fullReadmePath = path.join(this.projectPath, readmePath); let readmeContent = ''; if (await fs.pathExists(fullReadmePath)) { readmeContent = await fs.readFile(fullReadmePath, 'utf-8'); } const endpointsTable = this.generateMarkdownTable(); // Check if endpoints section already exists const endpointsRegex = /## API Endpoints[\s\S]*?(?=\n##|\n#|$)/; if (endpointsRegex.test(readmeContent)) { // Replace existing section readmeContent = readmeContent.replace(endpointsRegex, endpointsTable.trim()); } else { // Add new section readmeContent += (readmeContent ? '\n\n' : '') + endpointsTable; } await fs.writeFile(fullReadmePath, readmeContent); return fullReadmePath; } } module.exports = { ApiFinder };