UNPKG

@neurolint/cli

Version:

Professional React/Next.js modernization platform with CLI, VS Code, and Web App integrations

361 lines (324 loc) 11.6 kB
#!/usr/bin/env node 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; } async function transform(input, options = {}) { const { dryRun = false, filePath = '', verbose = false } = options; let changeCount = 0; const suggestions = []; try { // Define paths (use provided filePath or project root) const projectRoot = filePath || process.cwd(); const tsConfigPath = path.join(projectRoot, 'tsconfig.json'); const nextConfigPath = path.join(projectRoot, 'next.config.js'); const packageJsonPath = path.join(projectRoot, 'package.json'); // Validate file existence 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 (!files.some(exists => exists)) { return { success: false, error: 'Required configuration files not found', changeCount: 0 }; } // Detect Next.js version and generate Turbopack suggestions const nextVersion = await detectNextJSVersion(projectRoot); if (nextVersion) { const turbopackSuggestions = generateTurbopackSuggestions(nextVersion); suggestions.push(...turbopackSuggestions); if (verbose && turbopackSuggestions.length > 0) { process.stdout.write(`[INFO] Next.js version detected: ${nextVersion}\n`); turbopackSuggestions.forEach(suggestion => { process.stdout.write(`[SUGGESTION] ${suggestion.message}\n`); if (suggestion.recommendation) { process.stdout.write(` ${suggestion.recommendation}\n`); } }); } } // Create centralized backups if not in dry run mode if (!dryRun) { try { const backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); // Create backups for each existing file const backupPromises = []; if (files[0]) { backupPromises.push(backupManager.createBackup(tsConfigPath, 'layer-1-config')); } if (files[1]) { backupPromises.push(backupManager.createBackup(nextConfigPath, 'layer-1-config')); } if (files[2]) { backupPromises.push(backupManager.createBackup(packageJsonPath, 'layer-1-config')); } const backupResults = await Promise.all(backupPromises); backupResults.forEach(result => { if (result.success && verbose) { console.log(`Created centralized backup: ${path.basename(result.backupPath)}`); } }); } catch (error) { if (verbose) { console.warn(`Warning: Backup creation failed: ${error.message}`); } } } // Process TypeScript config let tsConfig = {}; if (files[0]) { const originalTsConfig = JSON.parse(await fs.readFile(tsConfigPath, 'utf8')); tsConfig = { compilerOptions: { ...originalTsConfig.compilerOptions, target: 'ES2022', lib: ['ES2022', 'DOM', 'DOM.Iterable'], strict: true, skipLibCheck: true, esModuleInterop: true, isolatedModules: true, baseUrl: '.', paths: { '@/*': ['src/*'], '@components/*': ['src/components/*'] } } }; // Count changes in tsconfig const tsConfigChanges = Object.keys(tsConfig.compilerOptions).filter(key => JSON.stringify(tsConfig.compilerOptions[key]) !== JSON.stringify(originalTsConfig.compilerOptions?.[key]) ).length; changeCount += tsConfigChanges; } // Process Next.js config with Turbopack support let nextConfig = `/** @type {import('next').NextConfig} */ const nextConfig = { experimental: { // Turbopack configuration for Next.js 15.0+ ${nextVersion && parseVersion(nextVersion)?.major >= 15 ? `turbo: { rules: { '*.svg': { loaders: ['@svgr/webpack'], as: '*.js' } } },` : ''} }, typescript: { ignoreBuildErrors: false }, eslint: { ignoreDuringBuilds: false }, images: { domains: [] }, swcMinify: true, optimizeFonts: true, compress: true, poweredByHeader: false, generateEtags: false, webpack: (config) => { config.module.rules.push({ test: /\\.svg$/, use: ['@svgr/webpack'] }); return config; }, async headers() { return [{ source: '/(.*)', headers: [ { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' } ] }]; } }; module.exports = nextConfig;`; // Process package.json with Turbopack scripts let packageJson = {}; if (files[2]) { const originalPackageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); // Determine if Turbopack should be enabled const enableTurbopack = nextVersion && isTurbopackSupported(nextVersion); packageJson = { ...originalPackageJson, scripts: { ...originalPackageJson.scripts, dev: enableTurbopack ? 'next dev --turbo' : 'next dev', build: enableTurbopack && parseVersion(nextVersion)?.major >= 15 ? 'next build --turbo' : 'next build', start: 'next start', 'type-check': 'tsc --noEmit', lint: 'next lint', test: 'jest --watch' }, dependencies: { ...originalPackageJson.dependencies, next: '^13.4.0', react: '^18.2.0', 'react-dom': '^18.2.0' } }; // Count changes in package.json const scriptChanges = Object.keys(packageJson.scripts).filter(key => packageJson.scripts[key] !== originalPackageJson.scripts?.[key] ).length; const depChanges = Object.keys(packageJson.dependencies).filter(key => packageJson.dependencies[key] !== originalPackageJson.dependencies?.[key] ).length; changeCount += scriptChanges + depChanges; // Next.js 15.5: Migrate next lint to Biome 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'; // 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', recommendation: 'Biome is faster and requires less configuration than ESLint' }); } } if (!dryRun) { // Write changes await Promise.all([ files[0] && fs.writeFile(tsConfigPath, JSON.stringify(tsConfig, null, 2)), files[1] && fs.writeFile(nextConfigPath, nextConfig), files[2] && fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)) ]); } if (verbose) { process.stdout.write(`Changes made: ${changeCount}\n`); if (suggestions.length > 0) { process.stdout.write(`Turbopack suggestions: ${suggestions.length}\n`); } } // Return success only if at least one file was processed and changes were made const success = files.some(exists => exists) && changeCount > 0; return { success, code: input, tsConfig: files[0] ? tsConfig : undefined, nextConfig: files[1] ? nextConfig : undefined, packageJson: files[2] ? packageJson : undefined, changeCount, dryRun, suggestions, error: success ? undefined : 'No changes were made' }; } catch (error) { if (error instanceof SyntaxError) { return { success: false, error: 'Invalid JSON in configuration files', changeCount: 0 }; } if (error.code === 'ENOENT') { return { success: false, error: 'Required configuration files not found', changeCount: 0 }; } return { success: false, error: error.message, changeCount: 0 }; } } module.exports = { transform };