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

1,550 lines (1,346 loc) 64.3 kB
#!/usr/bin/env node /** * NeuroLint - Master Automated Fixing Script * Comprehensive multi-layer fix strategy for React/Next.js codebases * * Layer 1: Configuration fixes (TypeScript, Next.js, package.json) * Layer 2: Bulk pattern fixes (HTML entities, console statements, browser APIs) * Layer 3: Component-specific fixes (Button variants, Tabs props, etc.) * Layer 4: Hydration and SSR fixes (client-side guards, theme providers) * Layer 5: Next.js App Router fixes * Layer 6: Testing and Validation Fixes * * Copyright (c) 2025 NeuroLint * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const fs = require('fs').promises; const path = require('path'); const BackupManager = require('./backup-manager'); // Enhanced output functions to replace emoji-based spinners function logSuccess(message) { console.log(`[SUCCESS] ${message}`); } function logError(message) { console.error(`[ERROR] ${message}`); } function logWarning(message) { console.warn(`[WARNING] ${message}`); } function logInfo(message) { console.log(`[INFO] ${message}`); } function logProgress(message) { process.stdout.write(`[PROCESSING] ${message}...`); } function logComplete(message) { process.stdout.write(`[COMPLETE] ${message}\n`); } /** * Next.js Version Compatibility Checker * Validates and gates operations based on Next.js version compatibility */ class NextJSVersionChecker { constructor() { this.supportedVersions = { min: '13.4.0', max: '16.1.0', recommended: '16.1.0' }; } /** * Parse semantic version string */ parseVersion(version) { const match = version.match(/^(\d+)\.(\d+)\.(\d+)/); if (!match) return null; return { major: parseInt(match[1]), minor: parseInt(match[2]), patch: parseInt(match[3]) }; } /** * Compare two version objects */ compareVersions(v1, v2) { if (v1.major !== v2.major) return v1.major - v2.major; if (v1.minor !== v2.minor) return v1.minor - v2.minor; return v1.patch - v2.patch; } /** * Check if version is within supported range */ isVersionSupported(version) { const parsed = this.parseVersion(version); if (!parsed) return false; const min = this.parseVersion(this.supportedVersions.min); const max = this.parseVersion(this.supportedVersions.max); return this.compareVersions(parsed, min) >= 0 && this.compareVersions(parsed, max) <= 0; } /** * Detect Next.js version from package.json */ async detectNextJSVersion(projectPath = process.cwd()) { try { const packageJsonPath = path.join(projectPath, 'package.json'); const packageJson = await fs.readFile(packageJsonPath, 'utf8'); const pkg = JSON.parse(packageJson); // Check dependencies and devDependencies const nextVersion = pkg.dependencies?.next || pkg.devDependencies?.next || pkg.peerDependencies?.next; if (!nextVersion) { return { version: null, error: 'Next.js not found in package.json' }; } // Extract version from range (e.g., "^16.1.0" -> "16.1.0") const versionMatch = nextVersion.match(/[\d.]+/); if (!versionMatch) { return { version: null, error: 'Invalid Next.js version format' }; } const version = versionMatch[0]; const isSupported = this.isVersionSupported(version); return { version, isSupported, packageJson: pkg, supportedRange: `${this.supportedVersions.min} - ${this.supportedVersions.max}` }; } catch (error) { return { version: null, error: `Failed to detect Next.js version: ${error.message}` }; } } /** * Validate project for Next.js 16 migration */ async validateProjectForMigration(projectPath = process.cwd()) { const versionInfo = await this.detectNextJSVersion(projectPath); if (versionInfo.error) { return { valid: false, error: versionInfo.error, recommendations: ['Ensure package.json exists and contains Next.js dependency'] }; } if (!versionInfo.isSupported) { return { valid: false, error: `Next.js version ${versionInfo.version} is not supported`, recommendations: [ `Upgrade to Next.js ${this.supportedVersions.min} or higher`, `Recommended: Upgrade to Next.js ${this.supportedVersions.recommended}`, 'Run: npm install next@latest' ], currentVersion: versionInfo.version, supportedRange: versionInfo.supportedRange }; } return { valid: true, version: versionInfo.version, supportedRange: versionInfo.supportedRange, recommendations: versionInfo.version !== this.supportedVersions.recommended ? [`Consider upgrading to Next.js ${this.supportedVersions.recommended} for latest features`] : [] }; } /** * Get migration recommendations based on current version */ getMigrationRecommendations(currentVersion) { const parsed = this.parseVersion(currentVersion); if (!parsed) return []; const recommendations = []; if (parsed.major < 14) { recommendations.push('Major version upgrade: Review breaking changes in Next.js 14+'); } if (parsed.major === 14 && parsed.minor < 0) { recommendations.push('App Router: Consider migrating from Pages Router to App Router'); } if (parsed.major === 15 && parsed.minor < 5) { recommendations.push('Performance: Enable Turbopack for faster builds'); recommendations.push('Caching: Review and optimize caching strategies'); } return recommendations; } } // Layer imports const { transform: layer1Transform } = require('./scripts/fix-layer-1-config'); const { transform: layer2Transform } = require('./scripts/fix-layer-2-patterns'); const { transform: layer3Transform } = require('./scripts/fix-layer-3-components'); const { transform: layer4Transform } = require('./scripts/fix-layer-4-hydration'); const { transform: layer5Transform, migrateNextJS16Comprehensive, BiomeMigrationTransformer, NextJS16DeprecationHandler } = require('./scripts/fix-layer-5-nextjs'); const { transform: layer6Transform } = require('./scripts/fix-layer-6-testing'); const { transform: layer7Transform } = require('./scripts/fix-layer-7-adaptive'); const Layer8SecurityForensics = require('./scripts/fix-layer-8-security'); // Layer 8 wrapper function to match layer transform signature const layer8Transform = async (code, options = {}) => { const layer8 = new Layer8SecurityForensics({ verbose: options.verbose, dryRun: options.dryRun, quarantine: options.quarantine || false }); return layer8.transform(code, options); }; // Validator import const TransformationValidator = require('./validator'); class LayerOrchestrator { constructor(options = {}) { this.options = { dryRun: false, verbose: false, ...options }; this.layers = [ { id: 1, name: 'Configuration', transform: layer1Transform }, { id: 2, name: 'Patterns', transform: layer2Transform }, { id: 3, name: 'Components', transform: layer3Transform }, { id: 4, name: 'Hydration', transform: layer4Transform }, { id: 5, name: 'Next.js', transform: layer5Transform }, { id: 6, name: 'Testing', transform: layer6Transform }, { id: 7, name: 'Adaptive Pattern Learning', transform: layer7Transform }, { id: 8, name: 'Security Forensics', transform: layer8Transform } ]; // Initialize centralized backup manager this.backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10, excludePatterns: [ '**/node_modules/**', '**/dist/**', '**/.next/**', '**/build/**', '**/.git/**', '**/coverage/**', '**/.neurolint-backups/**', '**/*.backup-*' ] }); } async executeLayers(code, layerIds, options) { const results = []; let currentCode = code; let successfulLayers = 0; const sortedLayers = this.sortLayers(layerIds); for (const layer of sortedLayers) { const layerTransform = this.layers.find(l => l.id === layer.id)?.transform; if (!layerTransform) { results.push({ type: 'error', layerId: layer.id, success: false, error: `Layer ${layer.id} not found` }); continue; } let attempts = 0; const maxRetries = 3; while (attempts < maxRetries) { try { const transformResult = await layerTransform(currentCode, { ...options, previousResults: results }); const validation = await TransformationValidator.validateTransformation( currentCode, transformResult.code, options.filePath ); if (validation.shouldRevert) { results.push({ type: 'revert', layerId: layer.id, success: false, file: options.filePath, error: validation.reason }); break; } if (transformResult.changeCount > 0) { currentCode = transformResult.code; successfulLayers++; } results.push({ type: 'layer', layerId: layer.id, success: transformResult.success, file: options.filePath, changes: transformResult.changeCount, originalCode: transformResult.originalCode, code: transformResult.code, results: transformResult.results, securityFindings: transformResult.securityFindings || [] }); break; } catch (error) { attempts++; if (attempts < maxRetries) { if (options.verbose) process.stdout.write(`Retrying layer ${layer.id} (attempt ${attempts + 1}/${maxRetries})...\n`); const delay = Math.min(1000 * Math.pow(2, attempts - 1), 5000); await new Promise(resolve => setTimeout(resolve, delay)); } else { results.push({ type: 'error', layerId: layer.id, success: false, file: options.filePath, error: error.message }); } } } } return { finalCode: currentCode, results, successfulLayers }; } sortLayers(layerIds) { // Only sort the requested layers by their natural order // Don't add dependencies unless they're explicitly requested return layerIds .sort((a, b) => a - b) .map(layerId => ({ id: layerId, name: this.layers.find(l => l.id === layerId)?.name || `Layer ${layerId}` })); } } /** * Execute layers with safe rollback and validation */ async function executeLayers(code, layers, options = {}) { const { dryRun = false, verbose = false, filePath = process.cwd(), strictTs = false, apiRoutes = false, ecommerce = false, nextjs16 = false, progressive = false } = options; const results = []; let successfulLayers = 0; let finalCode = code; // Create .neurolint directory for state tracking (only if not in dry-run mode) const stateDir = path.join(process.cwd(), '.neurolint'); if (!dryRun) { await fs.mkdir(stateDir, { recursive: true }); } // Layer configuration const layerConfig = { 1: { transform: layer1Transform, name: 'Configuration' }, 2: { transform: layer2Transform, name: 'Pattern Fixes' }, 3: { transform: layer3Transform, name: 'Component Fixes' }, 4: { transform: layer4Transform, name: 'Hydration Fixes' }, 5: { transform: layer5Transform, name: 'Next.js Fixes' }, 6: { transform: layer6Transform, name: 'Testing Fixes' }, 7: { transform: layer7Transform, name: 'Adaptive Pattern Learning' }, 8: { transform: layer8Transform, name: 'Security Forensics' } }; // Filter layers based on flags let filteredLayers = [...layers]; // Create a copy to avoid mutating original // Define layer dependencies and order const layerDependencies = { 1: [], // Layer 1 has no dependencies 2: [1], // Layer 2 depends on Layer 1 3: [1, 2], // Layer 3 depends on Layers 1, 2 4: [1, 2, 3], // Layer 4 depends on Layers 1, 2, 3 5: [1, 2, 3, 4], // Layer 5 depends on Layers 1, 2, 3, 4 6: [1, 2, 3, 4, 5], // Layer 6 depends on Layers 1, 2, 3, 4, 5 7: [1, 2, 3, 4, 5, 6], // Layer 7 depends on all previous layers 8: [1, 2, 3, 4, 5, 6, 7] // Layer 8 depends on all previous layers }; // Add layers based on flags if (strictTs && !filteredLayers.includes(1)) { filteredLayers.push(1); } if (apiRoutes && !filteredLayers.includes(2)) { filteredLayers.push(2); } if (ecommerce) { if (!filteredLayers.includes(2)) { filteredLayers.push(2); } if (!filteredLayers.includes(7)) { filteredLayers.push(7); } } if (nextjs16 && !filteredLayers.includes(5)) { filteredLayers.push(5); } // Add dependencies for each layer const addDependencies = (layerNum) => { const dependencies = layerDependencies[layerNum] || []; dependencies.forEach(dep => { if (!filteredLayers.includes(dep)) { filteredLayers.push(dep); } }); }; // Add dependencies for all layers (unless --no-deps is specified) if (!options.noDeps) { filteredLayers.forEach(addDependencies); } // Remove duplicates and sort to maintain execution order filteredLayers = [...new Set(filteredLayers)].sort((a, b) => a - b); // Validate flag combinations if (verbose) { process.stdout.write(`[INFO] Original layers: ${layers.join(', ')}\n`); process.stdout.write(`[INFO] Filtered layers: ${filteredLayers.join(', ')}\n`); // Show enabled flags const enabledFlags = []; if (strictTs) enabledFlags.push('TypeScript strictness'); if (apiRoutes) enabledFlags.push('API route patterns'); if (ecommerce) enabledFlags.push('E-commerce patterns'); if (nextjs16) enabledFlags.push('Next.js 16 patterns'); if (enabledFlags.length > 0) { process.stdout.write(`[INFO] Enabled patterns: ${enabledFlags.join(', ')}\n`); } // Show warnings for potential conflicts if (nextjs16 && !strictTs) { process.stderr.write(`[WARNING] Next.js 16 patterns enabled but TypeScript strictness not enabled. Consider adding --strict-ts for better compatibility.\n`); } if (ecommerce && !apiRoutes) { process.stderr.write(`[INFO] E-commerce patterns enabled. API route patterns are automatically included for optimal e-commerce setup.\n`); } } // Track execution state const state = { timestamp: Date.now(), file: filePath, layers: [], backups: [], executionTime: 0 }; const startTime = performance.now(); for (const layerNum of filteredLayers) { // Validate layer number if (layerNum < 1 || layerNum > 8) { if (verbose) { process.stderr.write(`[WARNING] Invalid layer number: ${layerNum}. Skipping.\n`); } continue; } const layer = layerConfig[layerNum]; if (!layer?.transform) { if (verbose) { process.stdout.write(`Layer ${layerNum} not implemented yet\n`); } continue; } try { if (verbose) { process.stdout.write(`Running Layer ${layerNum} (${layer.name}) on ${path.basename(filePath)}\n`); } // Create centralized backup before layer execution if (!dryRun) { try { // Create backup using backup manager (local instance for standalone function) const backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); const backupResult = await backupManager.createBackup(filePath, `layer-${layerNum}`); if (backupResult.success) { state.backups.push(backupResult.backupPath); if (verbose) { process.stdout.write(`Created centralized backup: ${path.basename(backupResult.backupPath)}\n`); } } else { if (verbose) { process.stderr.write(`Warning: Could not create backup: ${backupResult.error}\n`); } } } catch (error) { if (verbose) { process.stderr.write(`Warning: Backup creation failed: ${error.message}\n`); } } } // Capture code before transformation for Layer 7 learning const previousCode = finalCode; // Execute layer transformation with retry logic let attempts = 0; const maxRetries = 3; let result = null; while (attempts < maxRetries) { try { result = await layer.transform(finalCode, { dryRun, verbose, filePath, previousResults: results, // Pass previous results for Layer 7 react19Only: options && options.react19Only === true ? true : false, progressive // Pass progressive flag to layer transformers }); break; } catch (error) { attempts++; if (attempts < maxRetries) { if (verbose) { process.stdout.write(`Retrying Layer ${layerNum} (attempt ${attempts + 1}/${maxRetries})...\n`); } const delay = Math.min(1000 * Math.pow(2, attempts - 1), 5000); await new Promise(resolve => setTimeout(resolve, delay)); } else { throw error; } } } if (result && result.success) { // Validate transformation const validation = await TransformationValidator.validateTransformation(finalCode, result.code, filePath); if (validation.shouldRevert) { if (verbose && process.env.NEUROLINT_DEBUG === 'true') { process.stderr.write(`[DEBUG] Layer ${layerNum} validation failed: ${validation.reason}\n`); } results.push({ layer: layerNum, success: false, error: validation.reason }); continue; } successfulLayers++; finalCode = result.code; // Track layer execution state.layers.push({ id: layerNum, name: layer.name, success: true, changes: result.changes?.length || 0, warnings: result.warnings || [] }); } if (result) { results.push({ layer: layerNum, layerId: layerNum, success: result.success, changes: result.changes?.length || 0, changeCount: result.changeCount || result.changes?.length || 0, warnings: result.warnings || [], error: result.error, originalCode: previousCode, code: result.code, securityFindings: result.securityFindings || [] }); } } catch (error) { if (verbose && process.env.NEUROLINT_DEBUG === 'true') { process.stderr.write(`[DEBUG] Layer ${layerNum} error: ${error.message}\n`); } results.push({ layer: layerNum, success: false, error: error.message }); // Provide guidance for layer failures if (verbose) { process.stderr.write(`[WARNING] Layer ${layerNum} (${layer.name}) failed: ${error.message}\n`); process.stderr.write(`[INFO] Consider running with --dry-run to preview changes or check file compatibility.\n`); } // Restore from centralized backup on error if (!dryRun && state.backups.length > 0) { try { const lastBackup = state.backups[state.backups.length - 1]; const restoreBackupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); const restoreResult = await restoreBackupManager.restoreFromBackup(lastBackup, filePath); if (restoreResult.success) { finalCode = restoreResult.backupInfo.content; if (verbose) { process.stdout.write(`Restored from centralized backup: ${path.basename(lastBackup)}\n`); } } else { // Fallback to direct file read if restore fails finalCode = await fs.readFile(lastBackup, 'utf8'); if (verbose) { process.stdout.write(`Fallback restore from backup: ${path.basename(lastBackup)}\n`); } } } catch (error) { if (verbose) { process.stderr.write(`Warning: Restore failed: ${error.message}\n`); } } } } } // Record final state (only if not in dry-run mode) state.executionTime = performance.now() - startTime; if (!dryRun) { await fs.writeFile( path.join(stateDir, `states-${state.timestamp}.json`), JSON.stringify(state, null, 2) ); } return { success: successfulLayers > 0, successfulLayers, finalCode, results, state }; } /** * Process a single file through enabled layers */ async function processFile(filePath, options = {}) { const { dryRun = false, verbose = false, layers = [1, 2], strictTs = false, apiRoutes = false, ecommerce = false, nextjs16 = false } = options; try { // Validate file exists await fs.access(filePath); // Read file content const code = await fs.readFile(filePath, 'utf8'); // Initial validation const initialValidation = await TransformationValidator.validateFile(filePath); if (!initialValidation.isValid) { logError(`Invalid file: ${initialValidation.error}`); throw new Error(initialValidation.suggestion); } logProgress(`Running ${layers.length} layer(s) on ${path.basename(filePath)}`); // Execute layers const result = await executeLayers(code, layers, { dryRun, verbose, filePath, strictTs, apiRoutes, ecommerce, nextjs16, noDeps: options.noDeps }); if (result.success) { if (!dryRun) { await fs.writeFile(filePath, result.finalCode, 'utf8'); } logSuccess(`Fixed ${path.basename(filePath)} (${result.successfulLayers} layer(s))`); } else { logError(`Failed to fix ${path.basename(filePath)}`); throw new Error(result.results.find(r => !r.success)?.error || 'Unknown error'); } return result; } catch (error) { logError(`Error processing ${path.basename(filePath)}: ${error.message}`); throw error; } } /** * Process all matching files in a directory */ async function processDirectory(dirPath, options = {}) { const { dryRun = false, verbose = false, layers = [1, 2], strictTs = false, apiRoutes = false, ecommerce = false, nextjs16 = false } = options; const validExtensions = ['.ts', '.tsx', '.js', '.jsx']; // Define exclusion patterns (matching backup manager patterns) const excludedDirs = [ 'node_modules', 'dist', '.next', 'build', '.git', 'coverage', '.neurolint-backups', '.vscode', '.idea', 'tmp', 'temp', 'cache', '.cache' ]; const excludedFiles = [ '.DS_Store', 'Thumbs.db', '*.backup-*', '*.min.js', '*.bundle.js', '*.chunk.js' ]; // Performance tracking const startTime = Date.now(); let processedFiles = 0; let skippedFiles = 0; let excludedDirsCount = 0; try { const files = await fs.readdir(dirPath); let successCount = 0; let errorCount = 0; if (verbose) { process.stdout.write(`[INFO] Scanning directory: ${dirPath} (${files.length} items)\n`); } for (const file of files) { const filePath = path.join(dirPath, file); try { const stat = await fs.stat(filePath); if (stat.isDirectory()) { // Check if directory should be excluded if (excludedDirs.includes(file)) { excludedDirsCount++; if (verbose) { process.stdout.write(`[SKIP] Excluded directory: ${file}\n`); } continue; } // Recursively process subdirectories const result = await processDirectory(filePath, options); successCount += result.success; errorCount += result.errors; processedFiles += result.processedFiles || 0; skippedFiles += result.skippedFiles || 0; excludedDirsCount += result.excludedDirsCount || 0; } else if (validExtensions.includes(path.extname(file))) { // Check if file should be excluded const shouldExcludeFile = excludedFiles.some(pattern => { if (pattern.includes('*')) { const regex = new RegExp(pattern.replace(/\*/g, '.*')); return regex.test(file); } return file === pattern; }); if (shouldExcludeFile) { skippedFiles++; if (verbose) { process.stdout.write(`[SKIP] Excluded file: ${file}\n`); } continue; } // Process the file try { await processFile(filePath, options); successCount++; processedFiles++; if (verbose && processedFiles % 10 === 0) { const elapsed = Date.now() - startTime; process.stdout.write(`[PROGRESS] Processed ${processedFiles} files in ${elapsed}ms\n`); } } catch (error) { errorCount++; if (verbose) { process.stderr.write(`[ERROR] Failed to process ${file}: ${error.message}\n`); } } } } catch (statError) { // Handle permission errors or broken symlinks if (verbose) { process.stderr.write(`[WARNING] Cannot access ${file}: ${statError.message}\n`); } skippedFiles++; } } const executionTime = Date.now() - startTime; if (verbose) { process.stdout.write(`[INFO] Directory scan complete: ${dirPath}\n`); process.stdout.write(`[INFO] Processed: ${processedFiles} files, Skipped: ${skippedFiles} files, Excluded dirs: ${excludedDirsCount}\n`); process.stdout.write(`[INFO] Success: ${successCount}, Errors: ${errorCount}, Time: ${executionTime}ms\n`); } return { success: successCount, errors: errorCount, processedFiles, skippedFiles, excludedDirsCount, executionTime }; } catch (error) { throw new Error(`Failed to process directory ${dirPath}: ${error.message}`); } } /** * Next.js 16 Migration Orchestrator * Handles the complete migration process with validation, transformation, and rollback */ class NextJS16MigrationOrchestrator { constructor() { this.versionChecker = new NextJSVersionChecker(); this.orchestrator = new LayerOrchestrator(); } /** * Generate migration report */ generateMigrationReport(results, validation, options = {}) { const { dryRun = false, verbose = false } = options; // Categorize results for better reporting const successfulFiles = results.filter(r => r.success); const failedFiles = results.filter(r => !r.success && !r.incompatible); const incompatibleFiles = results.filter(r => r.incompatible); const skippedFiles = results.filter(r => r.skipped); const filesWithChanges = results.filter(r => r.changes > 0); const filesNoChanges = results.filter(r => r.success && r.changes === 0); const report = { timestamp: new Date().toISOString(), dryRun, validation: { valid: validation.valid, version: validation.version, supportedRange: validation.supportedRange, recommendations: validation.recommendations || [] }, summary: { totalFiles: results.length, successfulFiles: successfulFiles.length, failedFiles: failedFiles.length, incompatibleFiles: incompatibleFiles.length, skippedFiles: skippedFiles.length, filesWithChanges: filesWithChanges.length, filesNoChanges: filesNoChanges.length, totalChanges: results.reduce((sum, r) => sum + (r.changes || 0), 0), totalWarnings: results.reduce((sum, r) => sum + (r.warnings?.length || 0), 0), successRate: results.length > 0 ? Math.round((successfulFiles.length / results.length) * 100) : 0 }, details: results.map(result => ({ file: result.file, success: result.success, changes: result.changes || 0, warnings: result.warnings || [], error: result.error, layers: result.layers || [], skipped: result.skipped, incompatible: result.incompatible, errorType: result.errorType })), recommendations: [] }; // Add specific recommendations based on results with improved categorization if (report.summary.failedFiles > 0) { // Use improved categorization instead of generic "failed files" const categorizedStats = this.generateCategorizedRecommendations(report); if (categorizedStats) { Object.entries(categorizedStats).forEach(([category, data]) => { if (data.count > 0) { report.recommendations.push(`${data.count} files ${data.description}`); } }); } else { // Fallback to generic message if categorization not available report.recommendations.push(`Review ${report.summary.failedFiles} files that need attention before applying changes`); } } if (report.summary.incompatibleFiles > 0) { report.recommendations.push(`${report.summary.incompatibleFiles} files were skipped as incompatible (build artifacts, config files, etc.)`); } if (report.summary.skippedFiles > 0) { report.recommendations.push(`${report.summary.skippedFiles} files were skipped (empty, not application code, etc.)`); } if (report.summary.totalWarnings > 0) { report.recommendations.push('Review warnings and consider manual adjustments'); } if (report.summary.successRate < 80) { report.recommendations.push(`Success rate is ${report.summary.successRate}% - consider reviewing file discovery settings`); } if (validation.version && validation.version !== '16.1.0') { report.recommendations.push('Consider upgrading to Next.js 16.1.0 for latest features'); } if (report.summary.filesWithChanges > 0) { report.recommendations.push(`Successfully applied changes to ${report.summary.filesWithChanges} files`); } return report; } /** * Generate categorized recommendations for better user understanding */ generateCategorizedRecommendations(report) { // If we have categorized data from the migration, use it if (report.categorized) { return report.categorized; } // Otherwise, generate basic categorization based on available data const categorized = {}; if (report.summary.failedFiles > 0) { // Estimate categorization based on typical patterns const totalFiles = report.summary.totalFiles; const failedFiles = report.summary.failedFiles; if (failedFiles > totalFiles * 0.7) { // Most files "failed" - likely means they don't need migration categorized['Skipped (no migration needed)'] = { count: Math.round(failedFiles * 0.7), percentage: '70.0', description: 'don\'t require Next.js 16 specific changes' }; categorized['Skipped (already compatible)'] = { count: Math.round(failedFiles * 0.2), percentage: '20.0', description: 'already have Next.js 16 features implemented' }; categorized['Skipped (not applicable)'] = { count: Math.round(failedFiles * 0.1), percentage: '10.0', description: 'are not relevant for Next.js 16 migration (configs, assets, etc.)' }; } else { // Some files actually failed categorized['Failed to process'] = { count: failedFiles, percentage: ((failedFiles / totalFiles) * 100).toFixed(1), description: 'encountered errors during processing' }; } } return categorized; } /** * Create rollback plan */ async createRollbackPlan(results, projectPath) { const rollbackPlan = { timestamp: new Date().toISOString(), projectPath, files: [], commands: [] }; for (const result of results) { if (result.success && result.backupPath) { rollbackPlan.files.push({ original: result.file, backup: result.backupPath, changes: result.changes || 0 }); } } // Add rollback commands rollbackPlan.commands.push('# Rollback commands:'); rollbackPlan.commands.push('# Run these commands to revert changes:'); rollbackPlan.commands.push(''); for (const file of rollbackPlan.files) { rollbackPlan.commands.push(`cp "${file.backup}" "${file.original}"`); } return rollbackPlan; } /** * Execute Next.js 16 migration */ async migrateNextJS16(projectPath, options = {}) { const { dryRun = false, verbose = false, createRollback = false, layers = [5] // Focus on Layer 5 for Next.js 16 specific changes } = options; const startTime = Date.now(); if (verbose) { logInfo(`Starting Next.js 16 migration for: ${projectPath}`); logInfo(`Mode: ${dryRun ? 'Dry Run' : 'Apply Changes'}`); } // Step 1: Validate project compatibility if (verbose) logProgress('Validating project compatibility'); const validation = await this.versionChecker.validateProjectForMigration(projectPath); if (!validation.valid) { logError(`Project validation failed: ${validation.error}`); if (validation.recommendations) { validation.recommendations.forEach(rec => logWarning(rec)); } throw new Error(`Project not compatible: ${validation.error}`); } if (verbose) { logComplete('Project validation passed'); logInfo(`Current Next.js version: ${validation.version}`); logInfo(`Supported range: ${validation.supportedRange}`); } // Step 2: Discover files to process if (verbose) logProgress('Discovering files to process'); const files = await this.discoverFiles(projectPath, { verbose }); if (verbose) { logComplete(`Found ${files.length} files to process`); } // Step 3: Process files through Layer 5 if (verbose) logProgress('Processing files through Layer 5'); const results = []; for (let i = 0; i < files.length; i++) { const file = files[i]; if (verbose) { logProgress(`Processing file ${i + 1}/${files.length}: ${path.basename(file)}`); } try { const fileContent = await fs.readFile(file, 'utf8'); // Skip empty files if (!fileContent || !fileContent.trim()) { results.push({ file, success: true, changes: 0, warnings: ['Empty file - no changes needed'], layers: [], processed: true, skipped: 'empty' }); continue; } // Skip files that are clearly not application code const isNotAppCode = this.isNotApplicationCode(fileContent, file); if (isNotAppCode) { results.push({ file, success: true, changes: 0, warnings: [`Skipped: ${isNotAppCode}`], layers: [], processed: true, skipped: 'not-app-code' }); continue; } const result = await this.orchestrator.executeLayers( fileContent, layers, { dryRun, verbose, filePath: file } ); // A file is successful if it was processed without errors, regardless of whether changes were made const hasErrors = result.results.some(r => r.type === 'error' || r.type === 'revert'); const totalChanges = result.results.reduce((sum, r) => sum + (r.changes || 0), 0); const totalWarnings = result.results.flatMap(r => r.warnings || []); results.push({ file, success: !hasErrors, // Success means no errors, not necessarily changes changes: totalChanges, warnings: totalWarnings, layers: result.results, backupPath: result.results.find(r => r.backupPath)?.backupPath, processed: true }); if (verbose && totalChanges > 0) { logSuccess(`Applied ${totalChanges} changes to ${path.basename(file)}`); } } catch (error) { // Determine if this is a real error or just an incompatible file const isIncompatibleFile = this.isIncompatibleFile(error.message); const errorType = isIncompatibleFile ? 'incompatible' : 'error'; if (verbose) { if (isIncompatibleFile) { logWarning(`Skipped incompatible file: ${path.basename(file)} - ${error.message}`); } else { logError(`Failed to process file: ${path.basename(file)} - ${error.message}`); } } results.push({ file, success: false, error: error.message, changes: 0, warnings: [], incompatible: isIncompatibleFile, errorType }); } } if (verbose) { const successCount = results.filter(r => r.success).length; logComplete(`Processed ${successCount}/${files.length} files successfully`); } // Step 4: Generate report const report = this.generateMigrationReport(results, validation, { dryRun, verbose }); // Step 5: Create rollback plan if requested let rollbackPlan = null; if (createRollback && !dryRun) { rollbackPlan = await this.createRollbackPlan(results, projectPath); } const executionTime = Date.now() - startTime; if (verbose) { logInfo(`Migration completed in ${executionTime}ms`); logInfo(`Summary: ${report.summary.successfulFiles}/${report.summary.totalFiles} files processed`); logInfo(`Total changes: ${report.summary.totalChanges}`); logInfo(`Total warnings: ${report.summary.totalWarnings}`); } return { success: report.summary.totalFiles === 0 || report.summary.successfulFiles > 0, report, rollbackPlan, executionTime }; } /** * Discover files to process for migration */ async discoverFiles(projectPath, options = {}) { const { verbose = false } = options; const validExtensions = ['.ts', '.tsx', '.js', '.jsx']; const files = []; // Directories to skip entirely const skipDirectories = [ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage', '.nyc_output', '.cache', 'temp', 'tmp', 'releases', 'media', 'docs', 'scripts', 'landing', 'vscode-extension', 'shared-core', '.neurolint', 'test-results', '.jest', '.nyc_output' ]; // File patterns to skip (build artifacts, etc.) const skipPatterns = [ /\.min\.(js|ts|jsx|tsx)$/, /\.bundle\.(js|ts|jsx|tsx)$/, /\.chunk\.(js|ts|jsx|tsx)$/, /\.test\.(js|ts|jsx|tsx)$/, /\.spec\.(js|ts|jsx|tsx)$/, /\.config\.(js|ts)$/, /\.d\.ts$/, /\.backup-/, /\.rollback/, /\.vsix$/, /\.tgz$/, /\.zip$/, /\.tar\.gz$/, /\.lock$/, /\.log$/, /\.tmp$/, /\.temp$/ ]; // Priority directories to process first (application source code) const priorityDirectories = [ 'src', 'app', 'pages', 'components', 'lib', 'utils', 'hooks' ]; // Only process files in specific source directories const sourceDirectories = [ 'src', 'app', 'pages', 'components', 'lib', 'utils', 'hooks', 'styles' ]; // Check if we're in a monorepo structure const isMonorepo = await this.isMonorepoStructure(projectPath); if (isMonorepo && verbose) { logInfo('Detected monorepo structure - will process web-app directory'); } async function scanDirectory(dir, isSourceDir = false) { try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { // Skip unwanted directories if (skipDirectories.includes(entry.name)) { if (verbose) logInfo(`Skipping directory: ${entry.name}`); continue; } // Handle monorepo structure - process web-app directory if (isMonorepo && entry.name === 'web-app') { if (verbose) logInfo(`Processing monorepo web-app directory`); await scanDirectory(fullPath, true); // Treat web-app as source directory continue; } // Check if this is a source directory const isInSourceDir = isSourceDir || sourceDirectories.includes(entry.name); await scanDirectory(fullPath, isInSourceDir); } else if (validExtensions.includes(path.extname(entry.name))) { // Only process files in source directories if (!isSourceDir) { if (verbose) logInfo(`Skipping non-source file: ${entry.name}`); continue; } // Skip files that match skip patterns const shouldSkip = skipPatterns.some(pattern => pattern.test(entry.name)); if (shouldSkip) { if (verbose) logInfo(`Skipping file (matches skip pattern): ${entry.name}`); continue; } // Skip files that are too large (likely build artifacts) try { const stats = await fs.stat(fullPath); if (stats.size > 1024 * 1024) { // Skip files larger than 1MB if (verbose) logInfo(`Skipping large file: ${entry.name} (${Math.round(stats.size / 1024)}KB)`); continue; } } catch (error) { // Skip if we can't get file stats continue; } files.push(fullPath); if (verbose) logInfo(`Added file for processing: ${entry.name}`); } } } catch (error) { // Skip directories that can't be read if (verbose) logWarning(`Skipping directory ${dir}: ${error.message}`); } } await scanDirectory(projectPath); if (verbose) { logInfo(`File discovery complete: ${files.length} files found for processing`); if (files.length > 0) { logInfo(`Sample files: ${files.slice(0, 5).map(f => path.basename(f)).join(', ')}`); } } return files; } /** * Check if a file is incompatible for processing */ isIncompatibleFile(errorMessage) { const incompatiblePatterns = [ 'Syntax error', 'Cannot use import', 'Unexpected token', 'Invalid or unexpected token', 'Missing semicolon', 'Unexpected end of input', 'Parsing error', 'Module parse failed' ]; return incompatiblePatterns.some(pattern => errorMessage.toLowerCase().includes(pattern.toLowerCase()) ); } /** * Check if file content is clearly not application code */ isNotApplicationCode(content, filePath) { const fileName = path.basename(filePath).toLowerCase(); // Skip configuration files if (fileName.includes('config') || fileName.includes('setup') || fileName.includes('init')) { return 'Configuration file'; } // Skip test files if (fileName.includes('test') || fileName.includes('spec')) { return 'Test file'; } // Skip build artifacts if (fileName.includes('bundle') || fileName.includes('chunk') || fileName.includes('min')) { return 'Build artifact'; } // Skip files that are mostly comments or empty const lines = content.split('\n'); const codeLines = lines.filter(line => line.trim() && !line.trim().startsWith('//') && !line.trim().startsWith('/*') && !line.trim().startsWith('*') && !line.trim().startsWith('#') ); if (codeLines.length < 3) { return 'File contains mostly comments or is empty'; } // Skip files that don't contain typical application code patterns const hasAppCodePatterns = content.includes('import') || content.includes('export') || content.includes('function') || content.includes('const') || content.includes('let') || content.includes('var') || content.includes('class') || content.includes('React') || content.includes('useState') || content.includes('useEffect'); if (!hasAppCodePatterns) { return 'File does not contain typical application code patterns'; } return null; // File should be processed } /** * Check if this is a monorepo structure */ async isMonorepoStructure(projectPath) { try { const entries = await fs.readdir(projectPath, { withFileTypes: true }); // Check for monorepo indicators const hasWebApp = entries.some(entry => entry.isDirectory() && entry.name === 'web-app'); const hasLanding = entries.some(entry => entry.isDirectory() && entry.name === 'landing'); const hasVscodeExtension = entries.some(entry => entry.isDirectory() && entry.name === 'vscode-extension'); // If we have multiple app directories, it's likely a monorepo return hasWebApp && (hasLanding || hasVscodeExtension); } catch (error) { return false; } } } /** * Main CLI entry point */ async function main() { const args = process.argv.slice(2); // Check for migration command if (args.includes('migrate-nextjs-16')) { await handleMigrationCommand(args); return; } // Check for Biome migration command if (args.includes('migrate-biome')) { await handleBiomeMigrationCommand(args); return; } // Check for deprecation fixes command if (args.includes('fix-deprecations')) { await handleDeprecationCommand(args); return; } const options = { dryRun: args.includes('--dry-run'), verbose: args.includes('--verbose'), strictTs: args.includes('--strict-ts'), apiRoutes: args.includes('--api-routes'), ecommerce: args.includes('--ecommerce'), nextjs16: args.includes('--nextjs-16'), noDeps: args.includes('--no-deps'), layers: (() => { // Support both --layers=3,4 and --layers 3,4 syntax let layersArg = null; // Check for --layers=3,4 syntax const layersWithEquals = args.find(arg => arg.startsWith('--layers=')); if (layersWithEquals) { layersArg = layersWithEquals.split('=')[1]; } else { // Check for --layers 3,4 syntax const layersIndex = args.indexOf('--layers'); if (layersIndex !== -1 && layersIndex + 1 < args.length) { const nextArg = args[layersIndex + 1]; if (!nextArg.startsWith('--')) { layersArg = nextArg; } } } if (layersArg) { return layersArg.split(',').map(Number); } return [1, 2]; })() }; // Show help text if (args.includes('--help') || args.includes('-h')) { process.stdout.write(` NeuroLint CLI - Automated Code Quality Fixes Usage: neurolint <command> [options] <path> Commands: fix Apply automated fixes to code (default) migrate-nextjs-16 Migrate project to Next.js 16 compatibility migrate-biome Migrate from ESLint to Biome fix-deprecations Fix Next.js 16 deprecations Options: --dry-run Show what would be done without making changes --verbose Show detailed output --layers 1,2 Specify layers to run (default: 1,2) --no-deps Disable automatic layer dependency resolution --strict-ts Enable TypeScript strictness patterns (Layer 1) --api-routes Enable API route structure patterns (Layer 2) --ecommerce Enable e-commerce optimization patterns (Layers 2,7) --nextjs-16 Enable Next.js 16 migration patterns (Layer 5) --help, -h Show this help message Layers: 1. Configuration fixes (TypeScript, Next.js, package.json) 2. Pattern fixes (HTML entities, console statements, browser APIs) 3. Component-specific fixes (coming soon) 4. Hydration and