UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

600 lines (542 loc) 19 kB
import chalk from "chalk"; import ora from "ora"; import { glob } from "glob"; import fs from "fs-extra"; import path from "path"; import axios from "axios"; import { loadConfig, validateConfig } from "../utils/config"; /** * Free tier local analysis - basic pattern detection */ async function performLocalAnalysis(code: string, filename: string, layers: number[]) { const issues: any[] = []; const ext = path.extname(filename); // Basic modernization checks for React/Next.js files if (['.tsx', '.jsx', '.ts', '.js'].includes(ext)) { // Layer 1: Configuration & TypeScript modernization if (layers.includes(1)) { if (code.includes('React.FC') && !code.includes('import { FC }')) { issues.push({ layer: 1, rule: 'typescript-modernization', severity: 'warning', message: 'Consider using direct FC import instead of React.FC', line: 1 }); } } // Layer 2: Legacy pattern detection if (layers.includes(2)) { if (code.includes('class ') && code.includes('extends Component')) { issues.push({ layer: 2, rule: 'class-to-hooks', severity: 'info', message: 'Class component detected - consider migrating to hooks', line: code.split('\n').findIndex(line => line.includes('class ')) + 1 }); } if (code.includes('componentDidMount') || code.includes('componentWillUnmount')) { issues.push({ layer: 2, rule: 'lifecycle-hooks', severity: 'info', message: 'Legacy lifecycle methods - consider useEffect hook', line: 1 }); } } // Layer 3: React 18 compatibility if (layers.includes(3)) { const mapMatches = code.match(/\.map\(\([^)]+\)\s*=>\s*<[^>]+(?!\s+key=)/g); if (mapMatches) { issues.push({ layer: 3, rule: 'react-keys', severity: 'warning', message: 'Missing key prop in mapped React elements', line: 1 }); } if (code.includes('ReactDOM.render')) { issues.push({ layer: 3, rule: 'react-18-root', severity: 'warning', message: 'Use createRoot() instead of ReactDOM.render() for React 18', line: 1 }); } } // Layer 4: Next.js App Router migration opportunities if (layers.includes(4) && filename.includes('pages/')) { issues.push({ layer: 4, rule: 'app-router-migration', severity: 'info', message: 'Consider migrating to Next.js App Router (app/ directory)', line: 1 }); } // Layer 5: Modern Next.js patterns if (layers.includes(5)) { if (code.includes('getServerSideProps') || code.includes('getStaticProps')) { issues.push({ layer: 5, rule: 'data-fetching-modernization', severity: 'info', message: 'Consider using App Router data fetching patterns', line: 1 }); } } } return { analysis: { detectedIssues: issues, summary: { totalIssues: issues.length, byLayer: layers.reduce((acc, layer) => { acc[layer] = issues.filter(i => i.layer === layer).length; return acc; }, {} as Record<number, number>) } }, modernizationScore: Math.max(0, 100 - (issues.length * 10)), isFreeAnalysis: true }; } interface AnalyzeOptions { layers?: string; output?: string; recursive?: boolean; include?: string; exclude?: string; config?: string; } interface AnalysisResult { success: boolean; filesAnalyzed: number; issuesFound: number; layersUsed: number[]; issues: Array<{ file: string; layer: number; rule: string; severity: "error" | "warning" | "info"; message: string; line?: number; column?: number; }>; performance: { duration: number; layerTimes: Record<number, number>; }; } /** * Layer Dependency Management (from IMPLEMENTATION_PATTERNS.md) * Validates and corrects layer selection based on dependencies */ function validateAndCorrectLayers(requestedLayers: number[]) { const DEPENDENCIES = { 1: [], // Configuration has no dependencies 2: [1], // Entity cleanup depends on config foundation 3: [1, 2], // Components depend on config + cleanup 4: [1, 2, 3], // Hydration depends on all previous layers 5: [1, 2, 3, 4], // Next.js depends on all core layers 6: [1, 2, 3, 4, 5], // Testing depends on all previous layers }; const LAYER_INFO = { 1: { name: "Configuration", critical: true }, 2: { name: "Entity Cleanup", critical: false }, 3: { name: "Components", critical: false }, 4: { name: "Hydration", critical: false }, 5: { name: "Next.js App Router", critical: false }, 6: { name: "Testing & Validation", critical: false }, }; const warnings: string[] = []; const autoAdded: number[] = []; let correctedLayers = [...requestedLayers]; // Sort layers in execution order correctedLayers.sort((a, b) => a - b); // Check dependencies for each requested layer for (const layerId of requestedLayers) { const dependencies = DEPENDENCIES[layerId as keyof typeof DEPENDENCIES] || []; const missingDeps = dependencies.filter( (dep) => !correctedLayers.includes(dep), ); if (missingDeps.length > 0) { // Auto-add missing dependencies correctedLayers.push(...missingDeps); autoAdded.push(...missingDeps); warnings.push( `Layer ${layerId} (${LAYER_INFO[layerId as keyof typeof LAYER_INFO]?.name}) requires ` + `${missingDeps.map((dep) => `${dep} (${LAYER_INFO[dep as keyof typeof LAYER_INFO]?.name})`).join(", ")}. ` + `Auto-added missing dependencies.`, ); } } // Remove duplicates and sort correctedLayers = [...new Set(correctedLayers)].sort((a, b) => a - b); return { correctedLayers, warnings, autoAdded, }; } export async function analyzeCommand(files: string[], options: AnalyzeOptions) { const spinner = ora("Initializing modernization analysis...").start(); const startTime = Date.now(); try { // Enhanced input validation if (!Array.isArray(files)) { spinner.fail("Invalid input: files must be an array"); console.log(chalk.red("ERROR: Invalid files parameter")); return; } if ( files.some( (file) => !file || typeof file !== "string" || file.trim().length === 0, ) ) { spinner.fail("Invalid file paths provided"); console.log( chalk.red("ERROR: All file paths must be valid non-empty strings"), ); return; } // Validate options if (options && typeof options !== "object") { spinner.fail("Invalid options provided"); console.log(chalk.red("ERROR: Options must be an object")); return; } // Load and validate configuration const config = await loadConfig(options.config); const configValidation = await validateConfig(config); if (!configValidation.valid) { spinner.fail("Configuration validation failed"); configValidation.errors.forEach((error) => console.log(chalk.red(`ERROR: ${error}`)), ); return; } // Check for free tier vs premium mode const isFreeMode = !config.apiKey; if (isFreeMode) { spinner.text = "Running free tier modernization analysis..."; console.log(chalk.cyan("\nFree Tier: Unlimited scanning & basic modernization analysis")); console.log(chalk.gray("Premium: Detailed reports, one-click fixes & team collaboration\n")); } else { spinner.text = "Running premium analysis..."; } // Parse layers const requestedLayers = options.layers ? options.layers .split(",") .map((l: string) => parseInt(l.trim())) .filter((l: number) => l >= 1 && l <= 6) : config.layers.enabled; // Apply Layer Dependency Management (from IMPLEMENTATION_PATTERNS.md) const layerValidation = validateAndCorrectLayers(requestedLayers); const layers = layerValidation.correctedLayers; if (layerValidation.warnings.length > 0) { layerValidation.warnings.forEach((warning) => console.log(chalk.yellow(`DEPENDENCY: ${warning}`)), ); } // Check premium features for layers 5 and 6 const premiumLayers = layers.filter((layer) => layer >= 5); if (premiumLayers.length > 0) { try { const userResponse = await axios.get( `${config.api.url}/auth/api-keys`, { headers: { "X-API-Key": config.apiKey }, timeout: 10000, }, ); // Handle different response structures from API const plan = userResponse.data.plan || userResponse.data.user?.plan || userResponse.data.apiKey?.plan || "free"; if (plan === "free" && premiumLayers.length > 0) { spinner.fail("Premium features required"); console.log( chalk.yellow( `Layers ${premiumLayers.join(", ")} require Professional plan ($29/month)`, ), ); console.log(chalk.gray("Upgrade at: https://neurolint.dev/pricing")); return; } } catch (error) { console.log( chalk.yellow("Unable to verify premium features, continuing..."), ); } } // Determine files to analyze let targetFiles: string[] = []; if (files.length > 0) { targetFiles = files; } else { // Use glob patterns from config spinner.text = "Discovering files..."; try { for (const pattern of config.files.include) { try { const foundFiles = await glob(pattern, { cwd: process.cwd(), ignore: config.files.exclude, }); targetFiles.push(...foundFiles); } catch (globError) { console.warn(`Warning: Could not process pattern ${pattern}`); } } } catch (error) { spinner.fail("File discovery failed"); console.log( chalk.red( `Error: ${error instanceof Error ? error.message : String(error)}`, ), ); return; } } if (targetFiles.length === 0) { spinner.fail("No files found to analyze"); console.log( chalk.yellow( "Try specifying files explicitly or check your include/exclude patterns", ), ); return; } // Remove duplicates and filter existing files targetFiles = [...new Set(targetFiles)]; const existingFiles = []; for (const file of targetFiles) { if (await fs.pathExists(file)) { existingFiles.push(file); } } if (existingFiles.length === 0) { spinner.fail("No valid files found"); return; } spinner.text = `Analyzing ${existingFiles.length} files with layers [${layers.join(", ")}]...`; // Process files one by one since the API expects single files const allResults: any[] = []; let totalIssues = 0; for (const file of existingFiles) { try { const code = await fs.readFile(file, "utf-8"); let result; if (isFreeMode) { // Free tier: Local basic analysis result = await performLocalAnalysis(code, file, layers); } else { // Premium tier: Full API analysis const analysisPayload = { code, filename: file, layers: layers.length === 1 ? layers[0].toString() : layers.join(","), applyFixes: false, metadata: { recursive: options.recursive, outputFormat: options.output || config.output.format, verbose: config.output.verbose, }, }; const response = await axios.post( `${config.api.url}/analyze`, analysisPayload, { headers: { "X-API-Key": config.apiKey, "Content-Type": "application/json", }, timeout: config.api.timeout || 60000, maxContentLength: 50 * 1024 * 1024, // 50MB max response validateStatus: (status) => status < 500, // Don't throw on 4xx errors }, ); result = response.data; } // Basic validation following IMPLEMENTATION_PATTERNS.md if (!result || typeof result !== "object") { console.log(chalk.yellow(`Warning: Invalid response for ${file}`)); continue; } allResults.push({ file, result }); // Handle different response structures const detectedIssues = result.analysis?.detectedIssues || result.detectedIssues || result.layers?.flatMap((l) => l.detectedIssues) || []; totalIssues += detectedIssues.length; } catch (fileError) { console.log(chalk.yellow(`Warning: Could not analyze ${file}`)); if (axios.isAxiosError(fileError)) { if (fileError.response?.status === 401) { console.log( chalk.red( "Authentication failed. Please run 'neurolint login' again.", ), ); } else if (fileError.response?.status === 403) { console.log( chalk.red("Access denied. Check your API permissions."), ); } else { console.log( chalk.gray( `API Error: ${fileError.response?.status} ${fileError.response?.statusText}`, ), ); } } else { console.log( chalk.gray( `Error: ${fileError instanceof Error ? fileError.message : String(fileError)}`, ), ); } } } const processingTime = Date.now() - startTime; spinner.succeed(`Analysis completed for ${existingFiles.length} files`); // Aggregate results const aggregatedResult = { filesAnalyzed: existingFiles.length, issuesFound: totalIssues, layersUsed: layers, results: allResults, performance: { duration: processingTime, layerTimes: {}, }, }; // Display results console.log(); console.log(chalk.white.bold("Analysis Results")); console.log(); console.log( chalk.white("Files analyzed: ") + chalk.cyan(aggregatedResult.filesAnalyzed), ); console.log( chalk.white("Issues found: ") + (aggregatedResult.issuesFound > 0 ? chalk.yellow(aggregatedResult.issuesFound) : chalk.green("0")), ); console.log( chalk.white("Layers used: ") + chalk.gray(`[${aggregatedResult.layersUsed.join(", ")}]`), ); console.log( chalk.white("Duration: ") + chalk.gray(`${aggregatedResult.performance.duration}ms`), ); console.log(); if (aggregatedResult.issuesFound > 0) { // Group issues by layer and file const issuesByLayer: Record<number, any[]> = {}; allResults.forEach(({ file, result }) => { // Handle different response structures const detectedIssues = result.analysis?.detectedIssues || result.detectedIssues || result.layers?.flatMap((l: any) => l.detectedIssues || []) || []; detectedIssues.forEach((issue: any) => { const layer = issue.layer || 1; if (!issuesByLayer[layer]) { issuesByLayer[layer] = []; } issuesByLayer[layer].push({ ...issue, file }); }); }); console.log(chalk.white("Issues by Layer:")); for (const layer of aggregatedResult.layersUsed) { const layerIssues = issuesByLayer[layer] || []; const layerName = config.layers.config[layer]?.name || `Layer ${layer}`; console.log( chalk.gray(` ${layerName}: `) + (layerIssues.length > 0 ? chalk.yellow(`${layerIssues.length} issues`) : chalk.green("PASSED")), ); // Show first few issues for each layer if ( layerIssues.length > 0 && (options.output === "table" || !options.output) ) { layerIssues.slice(0, 3).forEach((issue) => { const location = issue.line ? `:${issue.line}${issue.column ? `:${issue.column}` : ""}` : ""; console.log( chalk.gray( ` ${issue.file}${location} - ${issue.message || issue.description || "Issue detected"}`, ), ); }); if (layerIssues.length > 3) { console.log( chalk.gray(` ... and ${layerIssues.length - 3} more`), ); } } } console.log(); // Output formatted results if requested if (options.output === "json") { console.log(JSON.stringify(aggregatedResult, null, 2)); } if (isFreeMode) { console.log(chalk.white("Next steps:")); console.log(chalk.cyan("Want to fix these issues automatically?")); console.log(chalk.gray("Run 'neurolint login' to access premium features:")); console.log(chalk.gray(" - One-click modernization fixes")); console.log(chalk.gray(" - Detailed migration reports (PDF/CSV)")); console.log(chalk.gray(" - Safe rollback & backup protection")); console.log(chalk.gray(" - Team collaboration & shared dashboards")); console.log(chalk.yellow("TIP: Try layer-specific CLIs: neurolint-config, neurolint-hydration")); console.log(chalk.cyan("\nUpgrade at https://neurolint.dev/pricing (starts at $9/month)")); } else { console.log(chalk.white("Next steps:")); console.log( chalk.gray(" • Run 'neurolint fix' to automatically fix issues"), ); console.log( chalk.gray( " • Run 'neurolint analyze --output=json' for detailed results", ), ); } } else { if (isFreeMode) { console.log(chalk.green("No modernization issues found! Your React/Next.js code is up to date.")); console.log(chalk.yellow("TIP: Try specific areas: neurolint-hydration, neurolint-approuter")); console.log(chalk.cyan("Premium features at https://neurolint.dev/pricing (starts at $9/month)")); } else { console.log(chalk.green("No issues found! Your code looks great.")); } } } catch (error) { spinner.fail("Analysis initialization failed"); console.log( chalk.red( `Error: ${error instanceof Error ? error.message : String(error)}`, ), ); } }