@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
JavaScript
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
};