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

514 lines (452 loc) 17.5 kB
#!/usr/bin/env node /** * NeuroLint - Licensed under Apache License 2.0 * Copyright (c) 2025 NeuroLint * http://www.apache.org/licenses/LICENSE-2.0 */ const fs = require('fs').promises; const path = require('path'); const BackupManager = require('../backup-manager'); /** * Detect Next.js version from package.json */ async function detectNextJSVersion(projectRoot) { try { const packageJsonPath = path.join(projectRoot, 'package.json'); const packageJson = await fs.readFile(packageJsonPath, 'utf8'); const pkg = JSON.parse(packageJson); const nextVersion = pkg.dependencies?.next || pkg.devDependencies?.next || pkg.peerDependencies?.next; if (!nextVersion) return null; // Extract version from range (e.g., "^15.5.0" -> "15.5.0") const versionMatch = nextVersion.match(/[\d.]+/); return versionMatch ? versionMatch[0] : null; } catch (error) { return null; } } /** * Parse semantic version string */ function 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]) }; } /** * Check if Turbopack is supported for the Next.js version */ function isTurbopackSupported(version) { const parsed = parseVersion(version); if (!parsed) return false; // Turbopack is available in Next.js 13.1+ but stable in 15.0+ return parsed.major >= 13 && parsed.minor >= 1; } /** * Generate Turbopack configuration suggestions */ function generateTurbopackSuggestions(nextVersion) { const suggestions = []; if (!isTurbopackSupported(nextVersion)) { suggestions.push({ type: 'turbopack', message: 'Turbopack requires Next.js 13.1+ for basic support, 15.0+ for stable features', recommendation: 'Upgrade to Next.js 15.0+ for stable Turbopack support' }); return suggestions; } const parsed = parseVersion(nextVersion); // Basic Turbopack configuration suggestions.push({ type: 'turbopack', message: 'Turbopack is available for faster builds', recommendation: 'Add --turbo flag to dev script: "dev": "next dev --turbo"' }); // Next.js 15.0+ specific Turbopack features if (parsed.major >= 15) { suggestions.push({ type: 'turbopack', message: 'Turbopack build is available in Next.js 15.0+', recommendation: 'Add --turbo flag to build script: "build": "next build --turbo"' }); suggestions.push({ type: 'turbopack', message: 'Turbopack configuration can be added to next.config.js', recommendation: `Add experimental.turbo configuration to next.config.js: experimental: { turbo: { rules: { '*.svg': { loaders: ['@svgr/webpack'], as: '*.js' } } } }` }); } return suggestions; } /** * Find project root by traversing up directories looking for config files */ async function findProjectRoot(startDir) { let currentDir = path.resolve(startDir); const rootDir = path.parse(currentDir).root; while (currentDir !== rootDir) { try { // Check for common project root indicators const hasPackageJson = await fs.access(path.join(currentDir, 'package.json')).then(() => true).catch(() => false); const hasTsConfig = await fs.access(path.join(currentDir, 'tsconfig.json')).then(() => true).catch(() => false); const hasNextConfig = await fs.access(path.join(currentDir, 'next.config.js')).then(() => true).catch(() => false); // If we find at least package.json, consider this the project root if (hasPackageJson) { return currentDir; } // Move up one directory currentDir = path.dirname(currentDir); } catch (error) { // If we can't access the directory, move up currentDir = path.dirname(currentDir); } } // If we reach the filesystem root, return the original start directory return startDir; } async function transform(input, options = {}) { const { dryRun = false, verbose = false, filePath = process.cwd() } = options; const results = []; let changeCount = 0; let suggestions = []; try { // Phase 3 Hardening: Enhanced TypeScript Strictness Enforcement const projectRoot = path.dirname(filePath); const tsConfigPath = path.join(projectRoot, 'tsconfig.json'); const nextConfigPath = path.join(projectRoot, 'next.config.js'); const packageJsonPath = path.join(projectRoot, 'package.json'); const files = await Promise.all([ fs.access(tsConfigPath).then(() => true).catch(() => false), fs.access(nextConfigPath).then(() => true).catch(() => false), fs.access(packageJsonPath).then(() => true).catch(() => false) ]); if (verbose) { process.stdout.write(`[INFO] Project root: ${projectRoot}\n`); process.stdout.write(`[INFO] Looking for config files in: ${projectRoot}\n`); process.stdout.write(`[INFO] Config files found: tsconfig.json=${files[0]}, next.config.js=${files[1]}, package.json=${files[2]}\n`); } // Phase 3: Enhanced TypeScript Strictness (Layer 1) let tsConfig = {}; if (files[0]) { const tsConfigContent = await fs.readFile(tsConfigPath, 'utf8'); tsConfig = JSON.parse(tsConfigContent); // Phase 3: Enforce strict TypeScript settings const strictSettings = { strict: true, noUncheckedIndexedAccess: true, noImplicitOverride: true, exactOptionalPropertyTypes: true, noImplicitReturns: true, noFallthroughCasesInSwitch: true, noUncheckedIndexedAccess: true, noImplicitAny: true, strictNullChecks: true, strictFunctionTypes: true, strictBindCallApply: true, strictPropertyInitialization: true, noImplicitThis: true, useUnknownInCatchVariables: true, alwaysStrict: true, noImplicitUseOfImplicitAnyArrayMethods: true, noPropertyAccessFromIndexSignature: true, noUncheckedIndexedAccess: true }; const originalCompilerOptions = tsConfig.compilerOptions || {}; const updatedCompilerOptions = { ...originalCompilerOptions }; let tsChanges = 0; Object.entries(strictSettings).forEach(([key, value]) => { if (originalCompilerOptions[key] !== value) { updatedCompilerOptions[key] = value; tsChanges++; if (verbose) { process.stdout.write(`[INFO] Enforcing TypeScript strictness: ${key} = ${value}\n`); } } }); // React 19: Require modern JSX transform (react-jsx or react-jsxdev) const jsxSetting = originalCompilerOptions.jsx; const isModernJSX = jsxSetting === 'react-jsx' || jsxSetting === 'react-jsxdev'; if (!isModernJSX) { suggestions.push({ type: 'jsx-transform', message: 'Outdated JSX transform detected. React 19 requires the modern JSX transform.', recommendation: 'Set tsconfig.compilerOptions.jsx to "react-jsx" (or "react-jsxdev" in dev)' }); // Auto-upgrade if safe updatedCompilerOptions.jsx = 'react-jsx'; tsChanges++; if (verbose) { process.stdout.write(`[INFO] Set compilerOptions.jsx = react-jsx for React 19 compatibility\n`); } } // Phase 3: Next.js 15.5 specific TypeScript improvements if (originalCompilerOptions.target !== 'ES2022') { updatedCompilerOptions.target = 'ES2022'; tsChanges++; suggestions.push({ type: 'typescript-target', message: 'Updated TypeScript target to ES2022 for Next.js 15.5 compatibility', recommendation: 'ES2022 provides better performance and modern JavaScript features' }); } if (originalCompilerOptions.module !== 'ESNext') { updatedCompilerOptions.module = 'ESNext'; tsChanges++; suggestions.push({ type: 'typescript-module', message: 'Updated TypeScript module to ESNext for Next.js 15.5', recommendation: 'ESNext enables modern module features and better tree-shaking' }); } if (originalCompilerOptions.moduleResolution !== 'bundler') { updatedCompilerOptions.moduleResolution = 'bundler'; tsChanges++; suggestions.push({ type: 'typescript-module-resolution', message: 'Updated module resolution to bundler for Next.js 15.5', recommendation: 'Bundler resolution provides better compatibility with modern bundlers' }); } if (tsChanges > 0) { tsConfig.compilerOptions = updatedCompilerOptions; changeCount += tsChanges; if (verbose) { process.stdout.write(`[INFO] Applied ${tsChanges} TypeScript strictness improvements\n`); } } } // Phase 3: Enhanced Next.js 15.5 Config Updates (Layer 1) let nextConfig = ''; if (files[1]) { const nextConfigContent = await fs.readFile(nextConfigPath, 'utf8'); nextConfig = nextConfigContent; // Phase 3: Remove deprecated experimental flags const deprecatedFlags = [ 'experimental.esmExternals', 'experimental.outputFileTracingRoot', 'experimental.outputFileTracingExcludes', 'experimental.outputFileTracingIncludes', 'experimental.outputFileTracingIgnores', 'experimental.outputFileTracingRoot', 'experimental.outputFileTracingExcludes', 'experimental.outputFileTracingIncludes', 'experimental.outputFileTracingIgnores' ]; let nextConfigChanges = 0; deprecatedFlags.forEach(flag => { const flagRegex = new RegExp(`\\s*${flag.replace(/\./g, '\\.')}\\s*:\\s*[^,}\\n]+`, 'g'); if (flagRegex.test(nextConfig)) { nextConfig = nextConfig.replace(flagRegex, ''); nextConfigChanges++; suggestions.push({ type: 'deprecated-flag', message: `Removed deprecated experimental flag: ${flag}`, recommendation: 'This flag is no longer needed in Next.js 15.5' }); } }); // Phase 3: Add Next.js 15.5 performance optimizations if (!nextConfig.includes('experimental.turbo')) { const turboConfig = ` experimental: { turbo: { rules: { '*.svg': { loaders: ['@svgr/webpack'], as: '*.js' } } } }`; // Insert turbo config before the closing brace const lastBraceIndex = nextConfig.lastIndexOf('}'); if (lastBraceIndex !== -1) { nextConfig = nextConfig.slice(0, lastBraceIndex) + turboConfig + '\n' + nextConfig.slice(lastBraceIndex); nextConfigChanges++; suggestions.push({ type: 'turbo-config', message: 'Added Turbopack configuration for Next.js 15.5', recommendation: 'Turbopack provides faster builds and development experience' }); } } // Phase 3: Add image optimization hints if (!nextConfig.includes('images.remotePatterns')) { const imageConfig = ` images: { remotePatterns: [ { protocol: 'https', hostname: '**', }, ], }`; const lastBraceIndex = nextConfig.lastIndexOf('}'); if (lastBraceIndex !== -1) { nextConfig = nextConfig.slice(0, lastBraceIndex) + imageConfig + '\n' + nextConfig.slice(lastBraceIndex); nextConfigChanges++; suggestions.push({ type: 'image-optimization', message: 'Added image optimization configuration for Next.js 15.5', recommendation: 'Remote patterns enable optimized image loading from external sources' }); } } if (nextConfigChanges > 0) { changeCount += nextConfigChanges; if (verbose) { process.stdout.write(`[INFO] Applied ${nextConfigChanges} Next.js 15.5 config improvements\n`); } } } // Phase 3: Enhanced Linting/Formatting Alignment (Layer 1) let packageJson = {}; if (files[2]) { const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8'); const originalPackageJson = JSON.parse(packageJsonContent); packageJson = { ...originalPackageJson }; // Phase 3: Migrate to Biome for Next.js 15.5 if (packageJson.scripts?.lint === 'next lint') { packageJson.scripts.lint = 'biome lint ./src'; packageJson.scripts.check = 'biome check ./src'; packageJson.scripts.format = 'biome format --write ./src'; packageJson.scripts['type-check'] = 'tsc --noEmit'; // Add Biome dependency packageJson.devDependencies = { ...packageJson.devDependencies, '@biomejs/biome': '^1.4.1' }; changeCount += 1; suggestions.push({ type: 'lint-migration', message: 'Migrated from deprecated "next lint" to Biome for Next.js 15.5', recommendation: 'Biome is faster, requires less configuration, and is the recommended linter for Next.js 15.5' }); } // Phase 3: Add TypeScript strict checking scripts if (!packageJson.scripts['type-check']) { packageJson.scripts['type-check'] = 'tsc --noEmit'; changeCount += 1; suggestions.push({ type: 'typescript-check', message: 'Added TypeScript type checking script', recommendation: 'Run "npm run type-check" to validate TypeScript types' }); } // Phase 3: Add build optimization scripts if (!packageJson.scripts['build:analyze']) { packageJson.scripts['build:analyze'] = 'ANALYZE=true next build'; changeCount += 1; suggestions.push({ type: 'build-analyze', message: 'Added bundle analysis script', recommendation: 'Run "npm run build:analyze" to analyze bundle size' }); } // Phase 3: Update Next.js version if needed const nextVersion = packageJson.dependencies?.next || packageJson.devDependencies?.next; if (nextVersion && !nextVersion.includes('15.5')) { packageJson.dependencies = { ...packageJson.dependencies, next: '^15.5.0' }; changeCount += 1; suggestions.push({ type: 'nextjs-upgrade', message: 'Updated Next.js to version 15.5.0', recommendation: 'Next.js 15.5 provides improved performance and new features' }); } } // Phase 3: Enhanced Turbopack suggestions const nextVersion = await detectNextJSVersion(projectRoot); if (nextVersion && isTurbopackSupported(nextVersion)) { const parsed = parseVersion(nextVersion); if (parsed && parsed.major >= 15) { suggestions.push({ type: 'turbopack-build', message: 'Turbopack build is available in Next.js 15.5+', recommendation: 'Add --turbo flag to build script: "build": "next build --turbo"' }); } suggestions.push({ type: 'turbopack-dev', message: 'Turbopack is available for faster development', recommendation: 'Add --turbo flag to dev script: "dev": "next dev --turbo"' }); } if (!dryRun) { // Create backups and write changes only if files exist const backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); const writeOperations = []; if (files[0]) { writeOperations.push(backupManager.safeWriteFile(tsConfigPath, JSON.stringify(tsConfig, null, 2), 'layer-1-config')); } if (files[1]) { writeOperations.push(backupManager.safeWriteFile(nextConfigPath, nextConfig, 'layer-1-config')); } if (files[2]) { writeOperations.push(backupManager.safeWriteFile(packageJsonPath, JSON.stringify(packageJson, null, 2), 'layer-1-config')); } const results = await Promise.all(writeOperations); // Check for write failures const failures = results.filter(r => !r.success); if (failures.length > 0 && verbose) { failures.forEach(f => console.warn(`Write failed: ${f.error}`)); } } // If no files found (single-file mode), still return success with suggestions const success = true; // Always succeed in single-file mode return { success, code: input, tsConfig: files[0] ? tsConfig : undefined, nextConfig: files[1] ? nextConfig : undefined, packageJson: files[2] ? packageJson : undefined, changeCount, dryRun, suggestions, warnings: files.some(f => !f) ? ['Single-file mode: Configuration suggestions only, no files modified'] : [] }; } catch (error) { if (error instanceof SyntaxError) { return { success: false, error: 'Invalid JSON in configuration files', changeCount: 0 }; } // Handle ENOENT as success in single-file mode if (error.code === 'ENOENT') { return { success: true, changeCount: 0, suggestions, warnings: ['Single-file mode: No config files found, suggestions only'] }; } return { success: false, error: error.message, changeCount: 0 }; } } module.exports = { transform };