UNPKG

@neurolint/cli

Version:

NeuroLint CLI - Deterministic code fixing for TypeScript, JavaScript, React, and Next.js with 8-layer architecture including Security Forensics, Next.js 16, React Compiler, and Turbopack support

450 lines (382 loc) 11.9 kB
#!/usr/bin/env node /** * NeuroLint - Licensed under Apache License 2.0 * Copyright (c) 2025 NeuroLint * http://www.apache.org/licenses/LICENSE-2.0 */ /** * Router Complexity Assessor * Analyzes Next.js projects and recommends optimal routing setup */ const fs = require('fs').promises; const path = require('path'); class RouterComplexityAssessor { constructor(options = {}) { this.verbose = options.verbose || false; this.projectPath = options.projectPath || process.cwd(); } log(message, level = 'info') { if (this.verbose) { const prefix = level === 'error' ? '[ERROR]' : level === 'success' ? '[SUCCESS]' : '[INFO]'; console.log(`${prefix} ${message}`); } } /** * Main assessment entry point */ async assess() { this.log('Analyzing router complexity...', 'info'); try { const metrics = { hasAppRouter: await this.detectAppRouter(), hasPagesRouter: await this.detectPagesRouter(), routeCount: await this.countRoutes(), hasMiddleware: await this.detectMiddleware(), hasAPIRoutes: await this.detectAPIRoutes(), hasServerComponents: await this.detectServerComponents(), hasClientComponents: await this.detectClientComponents(), usesSSR: await this.detectSSR(), usesSSG: await this.detectSSG(), complexityScore: 0 }; // Calculate complexity score (0-100) metrics.complexityScore = this.calculateComplexity(metrics); // Generate recommendations const recommendations = this.generateRecommendations(metrics); // Print report this.printReport(metrics, recommendations); return { metrics, recommendations, level: this.getComplexityLevel(metrics.complexityScore) }; } catch (error) { this.log(`Assessment failed: ${error.message}`, 'error'); throw error; } } /** * Detect App Router usage */ async detectAppRouter() { const appDirPath = path.join(this.projectPath, 'app'); const srcAppDirPath = path.join(this.projectPath, 'src', 'app'); try { await fs.access(appDirPath); return true; } catch { try { await fs.access(srcAppDirPath); return true; } catch { return false; } } } /** * Detect Pages Router usage */ async detectPagesRouter() { const pagesDirPath = path.join(this.projectPath, 'pages'); const srcPagesDirPath = path.join(this.projectPath, 'src', 'pages'); try { await fs.access(pagesDirPath); return true; } catch { try { await fs.access(srcPagesDirPath); return true; } catch { return false; } } } /** * Count total routes */ async countRoutes() { let count = 0; const dirs = [ path.join(this.projectPath, 'app'), path.join(this.projectPath, 'src', 'app'), path.join(this.projectPath, 'pages'), path.join(this.projectPath, 'src', 'pages') ]; for (const dir of dirs) { try { count += await this.countFilesInDir(dir, /page\.(tsx?|jsx?)$|index\.(tsx?|jsx?)$/); } catch { // Directory doesn't exist } } return count; } /** * Detect middleware */ async detectMiddleware() { const middlewarePaths = [ path.join(this.projectPath, 'middleware.ts'), path.join(this.projectPath, 'middleware.js'), path.join(this.projectPath, 'src', 'middleware.ts'), path.join(this.projectPath, 'src', 'middleware.js'), path.join(this.projectPath, 'proxy.ts'), path.join(this.projectPath, 'proxy.js') ]; for (const middlewarePath of middlewarePaths) { try { await fs.access(middlewarePath); return true; } catch { // Continue } } return false; } /** * Detect API routes */ async detectAPIRoutes() { const apiDirs = [ path.join(this.projectPath, 'pages', 'api'), path.join(this.projectPath, 'src', 'pages', 'api'), path.join(this.projectPath, 'app', 'api'), path.join(this.projectPath, 'src', 'app', 'api') ]; for (const dir of apiDirs) { try { const count = await this.countFilesInDir(dir, /\.(tsx?|jsx?)$/); if (count > 0) return true; } catch { // Continue } } return false; } /** * Detect Server Components */ async detectServerComponents() { const files = await this.findSourceFiles(); for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); const isServerComponent = !content.includes("'use client'") && !content.includes('"use client"') && content.includes('async function') && content.includes('export default'); if (isServerComponent) return true; } catch { // Skip file } } return false; } /** * Detect Client Components */ async detectClientComponents() { const files = await this.findSourceFiles(); for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); if (content.includes("'use client'") || content.includes('"use client"')) { return true; } } catch { // Skip file } } return false; } /** * Detect SSR usage */ async detectSSR() { const files = await this.findSourceFiles(); for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); if (content.includes('getServerSideProps') || content.includes('cookies()') || content.includes('headers()')) { return true; } } catch { // Skip file } } return false; } /** * Detect SSG usage */ async detectSSG() { const files = await this.findSourceFiles(); for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); if (content.includes('getStaticProps') || content.includes('generateStaticParams')) { return true; } } catch { // Skip file } } return false; } /** * Calculate complexity score (0-100) */ calculateComplexity(metrics) { let score = 0; // Base score for using Next.js score += 20; // Router complexity if (metrics.hasAppRouter && metrics.hasPagesRouter) score += 20; // Mixed routers = complex if (metrics.hasAppRouter) score += 10; // Route count if (metrics.routeCount > 50) score += 20; else if (metrics.routeCount > 20) score += 15; else if (metrics.routeCount > 10) score += 10; else if (metrics.routeCount > 5) score += 5; // Features if (metrics.hasMiddleware) score += 10; if (metrics.hasAPIRoutes) score += 10; if (metrics.hasServerComponents) score += 10; if (metrics.usesSSR) score += 10; if (metrics.usesSSG) score += 5; return Math.min(100, score); } /** * Get complexity level */ getComplexityLevel(score) { if (score <= 30) return 'Simple'; if (score <= 60) return 'Moderate'; if (score <= 80) return 'Complex'; return 'Enterprise'; } /** * Generate recommendations */ generateRecommendations(metrics) { const recommendations = []; // Recommendation based on complexity if (metrics.complexityScore <= 30) { recommendations.push({ type: 'simplify', message: 'Consider plain React - Next.js may be overkill for this project', command: 'neurolint simplify ./src --target=react --dry-run' }); } if (metrics.hasAppRouter && metrics.hasPagesRouter) { recommendations.push({ type: 'router-migration', message: 'Mixed App Router and Pages Router detected - consider migrating fully to App Router', command: 'neurolint migrate-nextjs-16 . --dry-run' }); } if (!metrics.hasServerComponents && metrics.hasAppRouter) { recommendations.push({ type: 'underutilized', message: 'App Router detected but no Server Components found - consider using them for better performance', command: null }); } if (!metrics.usesSSR && !metrics.usesSSG && metrics.routeCount < 10) { recommendations.push({ type: 'static-site', message: 'No SSR/SSG detected - consider using a static site generator or plain React', command: 'neurolint assess . --verbose' }); } if (metrics.hasAPIRoutes && !metrics.hasMiddleware) { recommendations.push({ type: 'api-protection', message: 'API routes detected without middleware - consider adding authentication/rate limiting', command: null }); } return recommendations; } /** * Print assessment report */ printReport(metrics, recommendations) { console.log('\n' + '='.repeat(60)); console.log('Next.js Router Complexity Assessment'); console.log('='.repeat(60)); console.log(`\nComplexity Score: ${metrics.complexityScore}/100 (${this.getComplexityLevel(metrics.complexityScore)})`); console.log(`\nRouter Configuration:`); console.log(` App Router: ${metrics.hasAppRouter ? 'Yes' : 'No'}`); console.log(` Pages Router: ${metrics.hasPagesRouter ? 'Yes' : 'No'}`); console.log(` Total Routes: ${metrics.routeCount}`); console.log(`\nFeatures:`); console.log(` Middleware: ${metrics.hasMiddleware ? 'Yes' : 'No'}`); console.log(` API Routes: ${metrics.hasAPIRoutes ? 'Yes' : 'No'}`); console.log(` Server Components: ${metrics.hasServerComponents ? 'Yes' : 'No'}`); console.log(` Client Components: ${metrics.hasClientComponents ? 'Yes' : 'No'}`); console.log(` SSR: ${metrics.usesSSR ? 'Yes' : 'No'}`); console.log(` SSG: ${metrics.usesSSG ? 'Yes' : 'No'}`); if (recommendations.length > 0) { console.log(`\nRecommendations:`); recommendations.forEach((rec, index) => { console.log(` ${index + 1}. ${rec.message}`); if (rec.command) { console.log(` Command: ${rec.command}`); } }); } console.log('\n' + '='.repeat(60) + '\n'); } /** * Helper: Count files in directory */ async countFilesInDir(dir, pattern) { let count = 0; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { count += await this.countFilesInDir(fullPath, pattern); } else if (entry.isFile() && pattern.test(entry.name)) { count++; } } } catch { // Directory doesn't exist } return count; } /** * Helper: Find all source files */ async findSourceFiles() { const files = []; const extensions = ['.ts', '.tsx', '.js', '.jsx']; const ignoreDirs = ['node_modules', '.next', 'dist', 'build', '.git']; const scan = async (dir) => { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { if (!ignoreDirs.includes(entry.name)) { await scan(fullPath); } } else if (entry.isFile()) { const ext = path.extname(entry.name); if (extensions.includes(ext)) { files.push(fullPath); } } } } catch { // Skip inaccessible directories } }; await scan(this.projectPath); return files; } } module.exports = RouterComplexityAssessor;