@neurolint/cli
Version:
Professional React/Next.js modernization platform with CLI, VS Code, and Web App integrations
361 lines (324 loc) • 11.6 kB
JavaScript
#!/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 };