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

449 lines (386 loc) 13.7 kB
#!/usr/bin/env node /** * NeuroLint - Licensed under Apache License 2.0 * Copyright (c) 2025 NeuroLint * http://www.apache.org/licenses/LICENSE-2.0 */ /** * React 19 Dependency Checker * * Scans package.json for React 19 incompatible dependencies * Provides compatibility reports and suggests fixes */ const fs = require('fs').promises; const path = require('path'); const https = require('https'); const BackupManager = require('../backup-manager'); class React19DependencyChecker { constructor(options = {}) { this.verbose = options.verbose || false; this.projectPath = options.projectPath || process.cwd(); // Known incompatible packages with React 19 this.knownIssues = { 'react-is': { incompatibleVersions: ['<19.0.0'], fix: 'npm install react-is@^19.0.0 --force', description: 'react-is versions below 19.0.0 have peer dependency conflicts' }, '@radix-ui/react-select': { incompatibleVersions: ['<1.2.0'], fix: 'npm install @radix-ui/react-select@latest --force', description: 'Older Radix UI versions throw useEffectEvent errors with React 19' }, '@radix-ui/react-dropdown-menu': { incompatibleVersions: ['<2.1.0'], fix: 'npm install @radix-ui/react-dropdown-menu@latest --force', description: 'Older Radix UI versions throw useEffectEvent errors with React 19' }, 'antd': { incompatibleVersions: ['<5.12.0'], fix: 'npm install antd@latest --force', description: 'Ant Design versions below 5.12.0 have ref handling issues with React 19' }, 'next-auth': { incompatibleVersions: ['<5.0.0'], fix: 'npm install next-auth@beta --force or use --legacy-peer-deps', description: 'next-auth v4 has peer dependency conflicts. Consider upgrading to v5 (Auth.js)' }, 'recharts': { incompatibleVersions: ['<2.10.0'], fix: 'Add override in package.json: "overrides": { "react-is": "19.0.0" }', description: 'recharts has outdated react-is dependency' } }; } log(message, level = 'info') { if (this.verbose || level === 'error' || level === 'warning') { const prefix = level === 'error' ? '[ERROR]' : level === 'warning' ? '[WARNING]' : level === 'success' ? '[SUCCESS]' : '[INFO]'; console.log(`${prefix} ${message}`); } } /** * Main check entry point */ async check() { this.log('Scanning dependencies for React 19 compatibility...', 'info'); try { const packageJsonPath = path.join(this.projectPath, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); const results = { react: await this.checkReactVersion(packageJson), dependencies: await this.checkDependencies(packageJson), peerDependencies: await this.checkPeerDependencies(packageJson), suggestions: [], fixes: [] }; // Generate fix suggestions results.suggestions = this.generateSuggestions(results); results.fixes = this.generateFixes(results); // Print report this.printReport(results); return results; } catch (error) { this.log(`Failed to check dependencies: ${error.message}`, 'error'); throw error; } } /** * Check React version */ async checkReactVersion(packageJson) { const reactVersion = packageJson.dependencies?.react || packageJson.devDependencies?.react || packageJson.peerDependencies?.react; if (!reactVersion) { return { status: 'missing', message: 'React is not listed in dependencies' }; } const version = reactVersion.replace(/[\^~]/g, ''); const majorVersion = parseInt(version.split('.')[0]); if (majorVersion >= 19) { return { status: 'compatible', version: reactVersion, message: 'React 19 or higher detected' }; } else { return { status: 'incompatible', version: reactVersion, message: `React ${reactVersion} is not compatible with this check. Upgrade to React 19+` }; } } /** * Check all dependencies for known React 19 issues */ async checkDependencies(packageJson) { const issues = []; const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; for (const [pkg, version] of Object.entries(allDeps)) { if (this.knownIssues[pkg]) { const issue = this.knownIssues[pkg]; const cleanVersion = version.replace(/[\^~]/g, ''); // Check if version matches incompatible pattern if (this.isIncompatibleVersion(cleanVersion, issue.incompatibleVersions)) { issues.push({ package: pkg, currentVersion: version, issue: issue.description, fix: issue.fix }); } } // Check for Radix UI packages (generic check) if (pkg.startsWith('@radix-ui/')) { const majorVersion = parseInt(version.replace(/[\^~]/g, '').split('.')[0]); if (majorVersion < 1) { issues.push({ package: pkg, currentVersion: version, issue: 'Older Radix UI packages may have compatibility issues with React 19', fix: `npm install ${pkg}@latest --force` }); } } } return issues; } /** * Check peer dependencies */ async checkPeerDependencies(packageJson) { const warnings = []; // Check if using --legacy-peer-deps flag is needed const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; const hasKnownIssues = Object.keys(allDeps).some(pkg => this.knownIssues[pkg] !== undefined ); if (hasKnownIssues) { warnings.push({ type: 'peer_deps', message: 'Some packages may require --legacy-peer-deps flag during installation', suggestion: 'Add "legacy-peer-deps=true" to .npmrc or use npm install --legacy-peer-deps' }); } return warnings; } /** * Check if version is incompatible */ isIncompatibleVersion(version, incompatiblePatterns) { for (const pattern of incompatiblePatterns) { if (pattern.startsWith('<')) { const targetVersion = pattern.replace('<', ''); if (this.compareVersions(version, targetVersion) < 0) { return true; } } } return false; } /** * Compare semantic versions */ compareVersions(v1, v2) { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const part1 = parts1[i] || 0; const part2 = parts2[i] || 0; if (part1 > part2) return 1; if (part1 < part2) return -1; } return 0; } /** * Generate suggestions based on findings */ generateSuggestions(results) { const suggestions = []; if (results.dependencies.length > 0) { suggestions.push({ priority: 'high', type: 'dependency_updates', message: `Update ${results.dependencies.length} incompatible packages`, action: 'Run the suggested npm install commands below' }); } if (results.peerDependencies.length > 0) { suggestions.push({ priority: 'medium', type: 'peer_deps_config', message: 'Configure npm to handle peer dependency conflicts', action: 'Create .npmrc file with legacy-peer-deps=true' }); } // Check for package.json overrides suggestions.push({ priority: 'medium', type: 'package_overrides', message: 'Use package.json overrides for stubborn dependencies', action: 'Add "overrides" field to package.json (npm) or "resolutions" (yarn)' }); return suggestions; } /** * Generate specific fix commands */ generateFixes(results) { const fixes = []; // Individual package fixes results.dependencies.forEach(dep => { fixes.push({ type: 'update', package: dep.package, command: dep.fix }); }); // Create .npmrc file if (results.peerDependencies.length > 0) { fixes.push({ type: 'config', file: '.npmrc', content: 'legacy-peer-deps=true\n', description: 'Create .npmrc to allow legacy peer dependencies' }); } // Package.json overrides fixes.push({ type: 'override', file: 'package.json', content: { overrides: { 'react-is': '^19.0.0' } }, description: 'Add to package.json to force compatible versions' }); return fixes; } /** * Print formatted report */ printReport(results) { console.log('\n' + '='.repeat(60)); console.log(' React 19 Dependency Compatibility Report'); console.log('='.repeat(60) + '\n'); // React version console.log('React Version:'); if (results.react.status === 'compatible') { console.log(` [OK] ${results.react.version} (Compatible)`); } else if (results.react.status === 'incompatible') { console.log(` [X] ${results.react.version} (Needs upgrade)`); } else { console.log(` ? Not found in package.json`); } console.log(''); // Dependency issues if (results.dependencies.length > 0) { console.log(`Found ${results.dependencies.length} potential compatibility issues:\n`); results.dependencies.forEach((dep, i) => { console.log(`${i + 1}. ${dep.package} (${dep.currentVersion})`); console.log(` Issue: ${dep.issue}`); console.log(` Fix: ${dep.fix}`); console.log(''); }); } else { console.log('[OK] No known compatibility issues found\n'); } // Suggestions if (results.suggestions.length > 0) { console.log('Recommendations:\n'); results.suggestions.forEach((sug, i) => { const priority = sug.priority === 'high' ? '[HIGH]' : sug.priority === 'medium' ? '[MEDIUM]' : '[LOW]'; console.log(`${priority} ${sug.message}`); console.log(` ${sug.action}`); console.log(''); }); } // Quick fixes console.log('Quick Fix Commands:\n'); console.log('# Update all packages:'); results.dependencies.forEach(dep => { console.log(` ${dep.fix}`); }); console.log('\n# Or use force install for all:'); console.log(' npm install --force'); console.log('\n# Or use legacy peer deps:'); console.log(' npm install --legacy-peer-deps'); console.log('\n' + '='.repeat(60) + '\n'); } /** * Apply automatic fixes */ async applyFixes(fixes) { this.log('Applying automatic fixes...', 'info'); for (const fix of fixes) { if (fix.type === 'config' && fix.file === '.npmrc') { const npmrcPath = path.join(this.projectPath, '.npmrc'); // Create backup before modifying .npmrc if it exists try { await fs.access(npmrcPath); // File exists, create backup try { const backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); const backupResult = await backupManager.createBackup(npmrcPath, 'react19-dependency-checker'); if (backupResult.success) { this.log(`Created backup: ${path.basename(backupResult.backupPath)}`, 'info'); } else { this.log(`Warning: Could not create backup: ${backupResult.error}`, 'warning'); } } catch (backupError) { this.log(`Warning: Backup creation failed: ${backupError.message}`, 'warning'); } } catch (accessError) { // File doesn't exist (ENOENT), no backup needed - this is expected if (accessError.code !== 'ENOENT') { this.log(`Warning: Could not check .npmrc: ${accessError.message}`, 'warning'); } } await fs.writeFile(npmrcPath, fix.content, 'utf8'); this.log(`Created ${fix.file}`, 'success'); } if (fix.type === 'override' && fix.file === 'package.json') { const packageJsonPath = path.join(this.projectPath, 'package.json'); // Create backup before modifying package.json try { const backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); const backupResult = await backupManager.createBackup(packageJsonPath, 'react19-dependency-checker'); if (backupResult.success) { this.log(`Created backup: ${path.basename(backupResult.backupPath)}`, 'info'); } else { this.log(`Warning: Could not create backup: ${backupResult.error}`, 'warning'); } } catch (backupError) { this.log(`Warning: Backup creation failed: ${backupError.message}`, 'warning'); } const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); packageJson.overrides = { ...(packageJson.overrides || {}), ...fix.content.overrides }; await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'utf8'); this.log(`Updated ${fix.file} with overrides`, 'success'); } } } } module.exports = React19DependencyChecker;