UNPKG

@neurolint/cli

Version:

NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations

463 lines (399 loc) 13.4 kB
const fs = require('fs-extra'); const path = require('path'); /** * Configuration file loader for NeuroLint CLI * Supports .neurolintrc.json and package.json neurolint field */ const DEFAULT_CONFIG = { include: ['**/*.{ts,tsx,js,jsx}'], exclude: ['**/node_modules/**', '**/dist/**', '**/.next/**', '**/coverage/**'], layers: 'all', backup: true, verbose: false, format: 'console', maxFiles: 1000, maxFileSize: '10MB', parallel: true, timeout: 30000, rules: {}, ignore: [], extends: [], env: { development: true, production: false, test: false }, plugins: [], output: { directory: './neurolint-reports', filename: 'neurolint-{timestamp}', includeTimestamp: true }, cache: { enabled: true, directory: './.neurolint-cache', ttl: 3600 // 1 hour in seconds } }; /** * Load configuration from file system with hierarchical lookup */ async function loadConfig(configPath) { let config = { ...DEFAULT_CONFIG }; const configSources = []; try { // 1. Try custom config path first (highest priority) if (configPath && await fs.pathExists(configPath)) { const customConfig = await loadConfigFile(configPath); config = mergeDeep(config, customConfig); configSources.push(configPath); } // 2. Try .neurolintrc.json in current directory const rcPath = path.join(process.cwd(), '.neurolintrc.json'); if (await fs.pathExists(rcPath)) { const rcConfig = await loadConfigFile(rcPath); config = mergeDeep(config, rcConfig); configSources.push(rcPath); } // 3. Try .neurolintrc.js (for dynamic configuration) const rcJsPath = path.join(process.cwd(), '.neurolintrc.js'); if (await fs.pathExists(rcJsPath)) { delete require.cache[require.resolve(rcJsPath)]; // Clear cache const rcJsConfig = require(rcJsPath); const dynamicConfig = typeof rcJsConfig === 'function' ? rcJsConfig() : rcJsConfig; config = mergeDeep(config, dynamicConfig); configSources.push(rcJsPath); } // 4. Try package.json neurolint field const packagePath = path.join(process.cwd(), 'package.json'); if (await fs.pathExists(packagePath)) { const packageJson = await fs.readJson(packagePath); if (packageJson.neurolint) { config = mergeDeep(config, packageJson.neurolint); configSources.push('package.json'); } } // 5. Look for config in parent directories (monorepo support) const parentConfig = await findParentConfig(process.cwd()); if (parentConfig) { config = mergeDeep(parentConfig.config, config); // Parent has lower priority configSources.unshift(`${parentConfig.path} (parent)`); } // 6. Process extends field for config inheritance if (config.extends && config.extends.length > 0) { config = await processExtends(config); } // 7. Process environment-specific overrides if (config.env) { const currentEnv = process.env.NODE_ENV || 'development'; if (config.env[currentEnv]) { config = mergeDeep(config, config.env[currentEnv]); } } // Log configuration sources if (configSources.length > 0) { console.log(`Configuration loaded from: ${configSources.join(' → ')}`); } else { console.log('Using default configuration'); } return config; } catch (error) { console.warn('Failed to load config file, using defaults:', error.message); return config; } } /** * Merge CLI options with config file */ function mergeConfig(config, options) { const merged = { ...config }; // CLI options override config file if (options.include) merged.include = options.include.split(','); if (options.exclude) merged.exclude = options.exclude.split(','); if (options.layers) merged.layers = options.layers; if (options.backup !== undefined) merged.backup = options.backup; if (options.verbose) merged.verbose = options.verbose; if (options.format) merged.format = options.format; if (options.output) merged.output = options.output; return merged; } /** * Validate configuration with comprehensive checks */ function validateConfig(config) { const errors = []; const warnings = []; // Required fields validation if (!Array.isArray(config.include)) { errors.push('include must be an array of patterns'); } if (!Array.isArray(config.exclude)) { errors.push('exclude must be an array of patterns'); } // Format validation if (!['console', 'json', 'html'].includes(config.format)) { errors.push('format must be one of: console, json, html'); } // Layers validation if (config.layers !== 'all') { const layers = config.layers.toString().split(',').map(l => parseInt(l.trim())); const invalidLayers = layers.filter(l => l < 1 || l > 6 || isNaN(l)); if (invalidLayers.length > 0) { errors.push(`Invalid layers: ${invalidLayers.join(', ')}. Must be 1-6`); } } // File limits validation if (typeof config.maxFiles !== 'number' || config.maxFiles < 1) { errors.push('maxFiles must be a positive number'); } if (config.maxFiles > 10000) { warnings.push('maxFiles is very high (>10,000), this may cause performance issues'); } // File size validation if (typeof config.maxFileSize === 'string') { const sizeMatch = config.maxFileSize.match(/^(\d+)(KB|MB|GB)?$/i); if (!sizeMatch) { errors.push('maxFileSize must be a number followed by KB, MB, or GB (e.g., "10MB")'); } } else if (typeof config.maxFileSize !== 'number') { errors.push('maxFileSize must be a string with units or a number in bytes'); } // Timeout validation if (typeof config.timeout !== 'number' || config.timeout < 1000) { errors.push('timeout must be a number >= 1000 milliseconds'); } // Rules validation if (config.rules && typeof config.rules !== 'object') { errors.push('rules must be an object'); } if (config.rules) { Object.entries(config.rules).forEach(([rule, severity]) => { if (!['error', 'warn', 'warning', 'info', 'off'].includes(severity)) { errors.push(`Invalid rule severity "${severity}" for rule "${rule}". Must be: error, warn, info, or off`); } }); } // Cache validation if (config.cache) { if (typeof config.cache.enabled !== 'boolean') { errors.push('cache.enabled must be a boolean'); } if (typeof config.cache.ttl !== 'number' || config.cache.ttl < 0) { errors.push('cache.ttl must be a non-negative number'); } } // Output validation if (config.output) { if (config.output.directory && typeof config.output.directory !== 'string') { errors.push('output.directory must be a string'); } if (config.output.filename && typeof config.output.filename !== 'string') { errors.push('output.filename must be a string'); } } // Plugins validation if (config.plugins && !Array.isArray(config.plugins)) { errors.push('plugins must be an array'); } return { errors, warnings }; } /** * Generate example config file */ function generateExampleConfig() { return { $schema: "https://neurolint.dev/schema.json", // File patterns include: ["src/**/*.{ts,tsx,js,jsx}"], exclude: ["**/node_modules/**", "**/dist/**", "**/.next/**", "**/coverage/**"], ignore: [ "**/*.test.{ts,tsx,js,jsx}", "**/*.spec.{ts,tsx,js,jsx}", "**/*.d.ts" ], // Processing options layers: "all", // or [1, 2, 3, 4, 5, 6] for specific layers maxFiles: 1000, maxFileSize: "10MB", parallel: true, timeout: 30000, // Output options format: "console", // console, json, html verbose: false, backup: true, // Rule configuration rules: { "config/typescript-strict": "error", "content/emoji-standardization": "warn", "components/missing-keys": "error", "components/prop-types-removal": "warn", "hydration/client-server-safety": "error", "hydration/use-effect-cleanup": "warn", "approuter/use-client-directive": "warn", "approuter/metadata-optimization": "info", "quality/error-boundaries": "warn", "quality/accessibility-checks": "info" }, // Environment-specific overrides env: { development: { verbose: true, rules: { "quality/accessibility-checks": "warn" } }, production: { rules: { "config/typescript-strict": "error", "quality/error-boundaries": "error" } }, test: { include: ["**/*.test.{ts,tsx,js,jsx}"], rules: { "components/missing-keys": "off" } } }, // Output configuration output: { directory: "./neurolint-reports", filename: "neurolint-{timestamp}", includeTimestamp: true }, // Caching configuration cache: { enabled: true, directory: "./.neurolint-cache", ttl: 3600 // 1 hour }, // Plugin system (for future extensibility) plugins: [], // Configuration inheritance extends: [] }; } /** * Helper functions for advanced configuration loading */ // Deep merge utility for nested objects function mergeDeep(target, source) { const result = { ...target }; for (const key in source) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { result[key] = mergeDeep(result[key] || {}, source[key]); } else { result[key] = source[key]; } } return result; } // Load config file with error handling async function loadConfigFile(filePath) { try { if (filePath.endsWith('.js')) { // Clear require cache for dynamic reloading delete require.cache[require.resolve(filePath)]; const config = require(filePath); return typeof config === 'function' ? config() : config; } else { return await fs.readJson(filePath); } } catch (error) { throw new Error(`Failed to load config from ${filePath}: ${error.message}`); } } // Find parent configuration files (monorepo support) async function findParentConfig(startDir) { let currentDir = startDir; const root = path.parse(startDir).root; while (currentDir !== root) { const parentDir = path.dirname(currentDir); if (parentDir === currentDir) break; const configPath = path.join(parentDir, '.neurolintrc.json'); if (await fs.pathExists(configPath)) { try { const config = await fs.readJson(configPath); return { config, path: configPath }; } catch (error) { // Continue searching in parent directories } } currentDir = parentDir; } return null; } // Process extends field for configuration inheritance async function processExtends(config) { if (!config.extends || !Array.isArray(config.extends)) { return config; } let extendedConfig = { ...DEFAULT_CONFIG }; for (const extendPath of config.extends) { try { let extendConfigPath; if (extendPath.startsWith('.')) { // Relative path extendConfigPath = path.resolve(process.cwd(), extendPath); } else { // Try to load from node_modules (preset configs) extendConfigPath = require.resolve(extendPath); } const extendedFromFile = await loadConfigFile(extendConfigPath); extendedConfig = mergeDeep(extendedConfig, extendedFromFile); } catch (error) { console.warn(`Failed to extend config from "${extendPath}": ${error.message}`); } } // Current config overrides extended config const { extends: _, ...configWithoutExtends } = config; return mergeDeep(extendedConfig, configWithoutExtends); } // Convert file size string to bytes function parseFileSize(sizeStr) { if (typeof sizeStr === 'number') return sizeStr; const match = sizeStr.match(/^(\d+)(KB|MB|GB)?$/i); if (!match) return null; const size = parseInt(match[1]); const unit = (match[2] || '').toUpperCase(); switch (unit) { case 'KB': return size * 1024; case 'MB': return size * 1024 * 1024; case 'GB': return size * 1024 * 1024 * 1024; default: return size; } } // Create JSON schema for config validation function getConfigSchema() { return { type: 'object', properties: { include: { type: 'array', items: { type: 'string' } }, exclude: { type: 'array', items: { type: 'string' } }, ignore: { type: 'array', items: { type: 'string' } }, layers: { oneOf: [{ type: 'string', enum: ['all'] }, { type: 'array', items: { type: 'number', minimum: 1, maximum: 6 } }] }, maxFiles: { type: 'number', minimum: 1 }, maxFileSize: { oneOf: [{ type: 'string' }, { type: 'number' }] }, parallel: { type: 'boolean' }, timeout: { type: 'number', minimum: 1000 }, format: { type: 'string', enum: ['console', 'json', 'html'] }, verbose: { type: 'boolean' }, backup: { type: 'boolean' }, rules: { type: 'object' }, plugins: { type: 'array', items: { type: 'string' } }, extends: { type: 'array', items: { type: 'string' } }, env: { type: 'object' }, output: { type: 'object' }, cache: { type: 'object' } } }; } module.exports = { loadConfig, mergeConfig, validateConfig, generateExampleConfig, parseFileSize, getConfigSchema, DEFAULT_CONFIG };