@neurolint/cli
Version:
NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations
745 lines (628 loc) • 19.8 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const crypto = require('crypto');
const { EventEmitter } = require('events');
/**
* Performance Cache Manager for NeuroLint
* Provides intelligent caching for analysis results, AST parsing, and transformations
*/
class CacheManager extends EventEmitter {
constructor(config = {}) {
super();
this.config = {
cacheDir: config.cacheDir || path.join(process.cwd(), '.neurolint', 'cache'),
maxCacheSize: config.maxCacheSize || 100 * 1024 * 1024, // 100MB
maxAge: config.maxAge || 7 * 24 * 60 * 60 * 1000, // 7 days
compressionEnabled: config.compressionEnabled !== false,
persistentCache: config.persistentCache !== false,
memoryCache: config.memoryCache !== false,
maxMemoryItems: config.maxMemoryItems || 1000,
hashAlgorithm: config.hashAlgorithm || 'sha256',
version: config.version || '1.0.0'
};
// In-memory cache for hot data
this.memoryCache = new Map();
this.memoryStats = {
hits: 0,
misses: 0,
size: 0
};
// Cache metadata
this.metadata = {
version: this.config.version,
created: Date.now(),
lastCleanup: Date.now(),
totalFiles: 0,
totalSize: 0
};
this.initialized = false;
}
/**
* Initialize cache manager
*/
async initialize() {
if (this.initialized) return;
try {
// Ensure cache directory exists
if (this.config.persistentCache) {
await fs.ensureDir(this.config.cacheDir);
await this.loadMetadata();
await this.performMaintenance();
}
this.initialized = true;
this.emit('initialized');
// Schedule periodic maintenance
this.setupMaintenance();
} catch (error) {
console.warn('Cache initialization failed:', error.message);
this.config.persistentCache = false; // Fallback to memory-only
}
}
/**
* Generate cache key from content and options
*/
generateKey(content, options = {}) {
const hash = crypto.createHash(this.config.hashAlgorithm);
// Include content hash
hash.update(content);
// Include options that affect the result
const normalizedOptions = this.normalizeOptions(options);
hash.update(JSON.stringify(normalizedOptions));
// Include version for cache invalidation
hash.update(this.config.version);
return hash.digest('hex');
}
/**
* Cache analysis results
*/
async cacheAnalysisResult(filePath, code, layers, result, options = {}) {
const key = this.generateAnalysisKey(filePath, code, layers, options);
const cacheData = {
type: 'analysis',
filePath,
result,
layers,
options,
timestamp: Date.now(),
version: this.config.version,
fileHash: this.generateContentHash(code),
size: JSON.stringify(result).length
};
await this.set(key, cacheData);
this.emit('analysis-cached', { key, filePath, size: cacheData.size });
}
/**
* Get cached analysis result
*/
async getCachedAnalysisResult(filePath, code, layers, options = {}) {
const key = this.generateAnalysisKey(filePath, code, layers, options);
const cached = await this.get(key);
if (!cached || cached.type !== 'analysis') {
return null;
}
// Verify file hasn't changed
const currentHash = this.generateContentHash(code);
if (cached.fileHash !== currentHash) {
await this.delete(key);
this.emit('cache-invalidated', { key, reason: 'file-changed' });
return null;
}
this.emit('analysis-cache-hit', { key, filePath });
return cached.result;
}
/**
* Cache AST parsing results
*/
async cacheAST(code, filename, ast, parseOptions = {}) {
const key = this.generateASTKey(code, filename, parseOptions);
const cacheData = {
type: 'ast',
filename,
ast: this.serializeAST(ast),
parseOptions,
timestamp: Date.now(),
version: this.config.version,
codeHash: this.generateContentHash(code),
size: JSON.stringify(ast).length
};
await this.set(key, cacheData);
this.emit('ast-cached', { key, filename, size: cacheData.size });
}
/**
* Get cached AST
*/
async getCachedAST(code, filename, parseOptions = {}) {
const key = this.generateASTKey(code, filename, parseOptions);
const cached = await this.get(key);
if (!cached || cached.type !== 'ast') {
return null;
}
// Verify code hasn't changed
const currentHash = this.generateContentHash(code);
if (cached.codeHash !== currentHash) {
await this.delete(key);
this.emit('cache-invalidated', { key, reason: 'code-changed' });
return null;
}
this.emit('ast-cache-hit', { key, filename });
return this.deserializeAST(cached.ast);
}
/**
* Cache transformation results
*/
async cacheTransformations(ast, layerId, transformations, context = {}) {
const key = this.generateTransformationKey(ast, layerId, context);
const cacheData = {
type: 'transformations',
layerId,
transformations,
context,
timestamp: Date.now(),
version: this.config.version,
astHash: this.generateASTHash(ast),
size: JSON.stringify(transformations).length
};
await this.set(key, cacheData);
this.emit('transformations-cached', { key, layerId, count: transformations.length });
}
/**
* Get cached transformations
*/
async getCachedTransformations(ast, layerId, context = {}) {
const key = this.generateTransformationKey(ast, layerId, context);
const cached = await this.get(key);
if (!cached || cached.type !== 'transformations') {
return null;
}
// Verify AST hasn't changed (simplified check)
const currentHash = this.generateASTHash(ast);
if (cached.astHash !== currentHash) {
await this.delete(key);
this.emit('cache-invalidated', { key, reason: 'ast-changed' });
return null;
}
this.emit('transformations-cache-hit', { key, layerId });
return cached.transformations;
}
/**
* Cache plugin results
*/
async cachePluginResult(pluginId, input, result, context = {}) {
const key = this.generatePluginKey(pluginId, input, context);
const cacheData = {
type: 'plugin',
pluginId,
result,
context,
timestamp: Date.now(),
version: this.config.version,
inputHash: this.generateContentHash(JSON.stringify(input)),
size: JSON.stringify(result).length
};
await this.set(key, cacheData);
this.emit('plugin-cached', { key, pluginId, size: cacheData.size });
}
/**
* Get cached plugin result
*/
async getCachedPluginResult(pluginId, input, context = {}) {
const key = this.generatePluginKey(pluginId, input, context);
const cached = await this.get(key);
if (!cached || cached.type !== 'plugin') {
return null;
}
// Verify input hasn't changed
const currentHash = this.generateContentHash(JSON.stringify(input));
if (cached.inputHash !== currentHash) {
await this.delete(key);
this.emit('cache-invalidated', { key, reason: 'input-changed' });
return null;
}
this.emit('plugin-cache-hit', { key, pluginId });
return cached.result;
}
/**
* Low-level cache operations
*/
async set(key, data) {
// Store in memory cache
if (this.config.memoryCache) {
this.setMemoryCache(key, data);
}
// Store in persistent cache
if (this.config.persistentCache) {
await this.setPersistentCache(key, data);
}
}
async get(key) {
// Try memory cache first
if (this.config.memoryCache) {
const memoryResult = this.getMemoryCache(key);
if (memoryResult) {
this.memoryStats.hits++;
return memoryResult;
}
this.memoryStats.misses++;
}
// Try persistent cache
if (this.config.persistentCache) {
const persistentResult = await this.getPersistentCache(key);
if (persistentResult) {
// Populate memory cache for next time
if (this.config.memoryCache) {
this.setMemoryCache(key, persistentResult);
}
return persistentResult;
}
}
return null;
}
async delete(key) {
// Remove from memory cache
if (this.config.memoryCache) {
this.memoryCache.delete(key);
this.updateMemoryStats();
}
// Remove from persistent cache
if (this.config.persistentCache) {
const filePath = this.getCacheFilePath(key);
if (await fs.pathExists(filePath)) {
await fs.remove(filePath);
}
}
}
/**
* Memory cache operations
*/
setMemoryCache(key, data) {
// Check if we need to evict items
if (this.memoryCache.size >= this.config.maxMemoryItems) {
this.evictOldestItems(Math.floor(this.config.maxMemoryItems * 0.1));
}
this.memoryCache.set(key, {
data,
timestamp: Date.now(),
accessCount: 1,
lastAccess: Date.now()
});
this.updateMemoryStats();
}
getMemoryCache(key) {
const cached = this.memoryCache.get(key);
if (!cached) return null;
// Check if expired
if (Date.now() - cached.timestamp > this.config.maxAge) {
this.memoryCache.delete(key);
this.updateMemoryStats();
return null;
}
// Update access statistics
cached.accessCount++;
cached.lastAccess = Date.now();
return cached.data;
}
evictOldestItems(count) {
const entries = Array.from(this.memoryCache.entries())
.sort((a, b) => a[1].lastAccess - b[1].lastAccess)
.slice(0, count);
entries.forEach(([key]) => {
this.memoryCache.delete(key);
});
this.updateMemoryStats();
}
updateMemoryStats() {
this.memoryStats.size = this.memoryCache.size;
}
/**
* Persistent cache operations
*/
async setPersistentCache(key, data) {
try {
const filePath = this.getCacheFilePath(key);
const serialized = this.config.compressionEnabled ?
await this.compress(JSON.stringify(data)) :
JSON.stringify(data);
await fs.writeFile(filePath, serialized);
this.metadata.totalFiles++;
this.metadata.totalSize += serialized.length;
} catch (error) {
console.warn(`Failed to write cache file ${key}:`, error.message);
}
}
async getPersistentCache(key) {
try {
const filePath = this.getCacheFilePath(key);
if (!(await fs.pathExists(filePath))) {
return null;
}
const stats = await fs.stat(filePath);
// Check if expired
if (Date.now() - stats.mtime.getTime() > this.config.maxAge) {
await fs.remove(filePath);
return null;
}
const content = await fs.readFile(filePath, 'utf8');
const data = this.config.compressionEnabled ?
JSON.parse(await this.decompress(content)) :
JSON.parse(content);
return data;
} catch (error) {
console.warn(`Failed to read cache file ${key}:`, error.message);
return null;
}
}
getCacheFilePath(key) {
// Create nested directory structure to avoid too many files in one dir
const prefix = key.slice(0, 2);
const dir = path.join(this.config.cacheDir, prefix);
return path.join(dir, `${key}.json`);
}
/**
* Cache key generation methods
*/
generateAnalysisKey(filePath, code, layers, options) {
const content = JSON.stringify({
filePath,
codeHash: this.generateContentHash(code),
layers,
options: this.normalizeOptions(options)
});
return `analysis_${this.generateKey(content)}`;
}
generateASTKey(code, filename, parseOptions) {
const content = JSON.stringify({
codeHash: this.generateContentHash(code),
filename,
parseOptions: this.normalizeOptions(parseOptions)
});
return `ast_${this.generateKey(content)}`;
}
generateTransformationKey(ast, layerId, context) {
const content = JSON.stringify({
astHash: this.generateASTHash(ast),
layerId,
context: this.normalizeOptions(context)
});
return `transform_${this.generateKey(content)}`;
}
generatePluginKey(pluginId, input, context) {
const content = JSON.stringify({
pluginId,
inputHash: this.generateContentHash(JSON.stringify(input)),
context: this.normalizeOptions(context)
});
return `plugin_${this.generateKey(content)}`;
}
/**
* Utility methods
*/
generateContentHash(content) {
return crypto.createHash('md5').update(content).digest('hex');
}
generateASTHash(ast) {
// Generate hash from AST structure (simplified)
try {
return this.generateContentHash(JSON.stringify(ast, (key, value) => {
// Exclude location information from hash
if (key === 'loc' || key === 'start' || key === 'end') {
return undefined;
}
return value;
}));
} catch (error) {
return this.generateContentHash(String(ast));
}
}
normalizeOptions(options) {
if (!options || typeof options !== 'object') {
return {};
}
// Sort keys for consistent hashing
const sorted = {};
Object.keys(options).sort().forEach(key => {
sorted[key] = options[key];
});
return sorted;
}
serializeAST(ast) {
// In a real implementation, might use more efficient serialization
return ast;
}
deserializeAST(data) {
return data;
}
async compress(data) {
// Simplified compression - in real implementation would use zlib
return data;
}
async decompress(data) {
return data;
}
/**
* Maintenance operations
*/
async performMaintenance() {
if (!this.config.persistentCache) return;
try {
await this.cleanExpiredFiles();
await this.enforceMaxCacheSize();
await this.saveMetadata();
this.metadata.lastCleanup = Date.now();
this.emit('maintenance-completed');
} catch (error) {
console.warn('Cache maintenance failed:', error.message);
}
}
async cleanExpiredFiles() {
const cacheDir = this.config.cacheDir;
const maxAge = this.config.maxAge;
let cleanedCount = 0;
let reclaimedSize = 0;
const cleanDirectory = async (dir) => {
const items = await fs.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const stats = await fs.stat(itemPath);
if (stats.isDirectory()) {
await cleanDirectory(itemPath);
} else if (stats.isFile() && item.endsWith('.json')) {
const age = Date.now() - stats.mtime.getTime();
if (age > maxAge) {
reclaimedSize += stats.size;
await fs.remove(itemPath);
cleanedCount++;
}
}
}
};
if (await fs.pathExists(cacheDir)) {
await cleanDirectory(cacheDir);
}
this.metadata.totalFiles -= cleanedCount;
this.metadata.totalSize -= reclaimedSize;
if (cleanedCount > 0) {
this.emit('cache-cleaned', { cleanedCount, reclaimedSize });
}
}
async enforceMaxCacheSize() {
if (this.metadata.totalSize <= this.config.maxCacheSize) {
return;
}
// Get all cache files with stats
const files = [];
const cacheDir = this.config.cacheDir;
const collectFiles = async (dir) => {
const items = await fs.readdir(dir);
for (const item of items) {
const itemPath = path.join(dir, item);
const stats = await fs.stat(itemPath);
if (stats.isDirectory()) {
await collectFiles(itemPath);
} else if (stats.isFile() && item.endsWith('.json')) {
files.push({
path: itemPath,
mtime: stats.mtime.getTime(),
size: stats.size
});
}
}
};
if (await fs.pathExists(cacheDir)) {
await collectFiles(cacheDir);
}
// Sort by modification time (oldest first)
files.sort((a, b) => a.mtime - b.mtime);
// Remove files until under size limit
let currentSize = this.metadata.totalSize;
let removedCount = 0;
for (const file of files) {
if (currentSize <= this.config.maxCacheSize) {
break;
}
await fs.remove(file.path);
currentSize -= file.size;
removedCount++;
}
this.metadata.totalFiles -= removedCount;
this.metadata.totalSize = currentSize;
if (removedCount > 0) {
this.emit('cache-size-enforced', { removedCount, newSize: currentSize });
}
}
setupMaintenance() {
// Run maintenance every hour
setInterval(() => {
this.performMaintenance().catch(error => {
console.warn('Scheduled maintenance failed:', error.message);
});
}, 60 * 60 * 1000);
}
/**
* Metadata operations
*/
async loadMetadata() {
const metadataFile = path.join(this.config.cacheDir, 'metadata.json');
try {
if (await fs.pathExists(metadataFile)) {
const data = await fs.readJson(metadataFile);
this.metadata = { ...this.metadata, ...data };
}
} catch (error) {
console.warn('Failed to load cache metadata:', error.message);
}
}
async saveMetadata() {
const metadataFile = path.join(this.config.cacheDir, 'metadata.json');
try {
await fs.writeJson(metadataFile, this.metadata, { spaces: 2 });
} catch (error) {
console.warn('Failed to save cache metadata:', error.message);
}
}
/**
* Cache statistics and management
*/
getStatistics() {
return {
initialized: this.initialized,
config: this.config,
metadata: this.metadata,
memoryStats: this.memoryStats,
persistentCache: {
enabled: this.config.persistentCache,
directory: this.config.cacheDir,
size: this.metadata.totalSize,
files: this.metadata.totalFiles
},
memoryCache: {
enabled: this.config.memoryCache,
size: this.memoryStats.size,
maxItems: this.config.maxMemoryItems,
hitRate: this.memoryStats.hits / (this.memoryStats.hits + this.memoryStats.misses) || 0
}
};
}
async clearCache() {
// Clear memory cache
this.memoryCache.clear();
this.updateMemoryStats();
// Clear persistent cache
if (this.config.persistentCache && await fs.pathExists(this.config.cacheDir)) {
await fs.remove(this.config.cacheDir);
await fs.ensureDir(this.config.cacheDir);
}
// Reset metadata
this.metadata = {
version: this.config.version,
created: Date.now(),
lastCleanup: Date.now(),
totalFiles: 0,
totalSize: 0
};
await this.saveMetadata();
this.emit('cache-cleared');
}
async warmupCache(filePaths = []) {
// Pre-populate cache with commonly used files
for (const filePath of filePaths) {
try {
if (await fs.pathExists(filePath)) {
const code = await fs.readFile(filePath, 'utf8');
// Trigger parsing and caching
// This would be called from the main engine
}
} catch (error) {
console.warn(`Failed to warmup cache for ${filePath}:`, error.message);
}
}
}
/**
* Cleanup resources
*/
async cleanup() {
await this.saveMetadata();
this.memoryCache.clear();
this.emit('cleanup');
}
}
module.exports = { CacheManager };