UNPKG

@tw-enigma/core

Version:

CSS optimization engine for tw-enigma

1 lines 2.43 MB
{"version":3,"sources":["../src/utils/logger.ts","../src/utils/pathUtils.ts","../src/processors/htmlExtractor.ts","../src/processors/jsExtractor.ts","../src/patternValidator.ts","../src/utils/errors.ts","../src/utils/fileIntegrity.ts","../src/config/configBackup.ts","../src/processors/cssInjector.ts","../src/runtimeValidator.ts","../src/config/configMigration.ts","../src/config/configValidator.ts","../src/config/configWatcher.ts","../src/config/config.ts","../src/config/configDefaults.ts","../src/detectors/reactDetector.ts","../src/detectors/nextjsDetector.ts","../src/detectors/viteDetector.ts","../src/integrations/core/hmrHandler.ts","../src/integrations/webpack/webpackPlugin.ts","../src/integrations/vite/vitePlugin.ts","../src/index.ts","../src/engine/cssGeneration.ts","../src/pluginApi.ts","../src/core/pluginManager.ts","../src/core/postcssPlugin.ts","../src/security/pluginSandbox.ts","../src/errorHandler/pluginErrorHandler.ts","../src/errorHandler/types.ts","../src/core/plugins/cssMinifier.ts","../src/core/plugins/sourceMapper.ts","../src/core/plugins/tailwindOptimizer.ts","../src/postcssIntegration.ts","../src/pluginConfig.ts","../src/engine/cssInjector.ts","../src/engine/optimizationCache.ts","../src/performance/cacheManager.ts","../src/engine/optimizationCacheIntegration.ts","../src/processors/htmlRewriter.ts","../src/processors/jsRewriter.ts","../src/processors/nameGeneration.ts","../src/processors/patternAnalysis.ts","../src/utils/debugUtils.ts","../src/utils/fileDiscovery.ts","../src/errorHandler/errorHandler.ts","../src/errorHandler/circuitBreaker.ts","../src/errorHandler/index.ts","../src/config/configSafeUpdater.ts","../src/output/cssOutputConfig.ts","../src/output/cssReportGenerator.ts","../src/types/legacy/atomicOps.ts","../src/types/legacy/plugins.ts","../src/integrations/core/configDetector.ts","../src/frameworkDetector.ts","../src/integrations/core/buildToolPlugin.ts","../src/integrations/core/index.ts","../src/integrations/core/integrationManager.ts","../src/performance/workerManager.ts","../src/performance/regexOptimizer.ts","../src/performance/streamOptimizer.ts","../src/performance/batchCoordinator.ts","../src/performance/memoryProfiler.ts","../src/performance/profiler.ts","../src/performance/config.ts","../src/performance/index.ts","../src/reporter.js","../src/tailwindPlugin.js"],"sourcesContent":["/**\n * Copyright (c) 2025 Rowan Cardow\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport chalk from 'chalk';\nimport { createWriteStream, WriteStream, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';\nimport { dirname } from 'path';\nimport { gzipSync } from 'zlib';\n\n/**\n * Log levels following Log4j standard\n */\nexport const LogLevel = {\n TRACE: 0,\n DEBUG: 1,\n INFO: 2,\n WARN: 3,\n ERROR: 4,\n FATAL: 5,\n} as const;\n\nexport type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];\n\n/**\n * Log level names for easy reference\n */\nexport const LogLevelNames: Record<LogLevel, string> = {\n [LogLevel.TRACE]: 'TRACE',\n [LogLevel.DEBUG]: 'DEBUG',\n [LogLevel.INFO]: 'INFO',\n [LogLevel.WARN]: 'WARN',\n [LogLevel.ERROR]: 'ERROR',\n [LogLevel.FATAL]: 'FATAL',\n};\n\n/**\n * Chalk color functions for each log level\n */\nconst LogLevelColors = {\n [LogLevel.TRACE]: chalk.gray,\n [LogLevel.DEBUG]: chalk.cyan,\n [LogLevel.INFO]: chalk.blue,\n [LogLevel.WARN]: chalk.yellow,\n [LogLevel.ERROR]: chalk.red,\n [LogLevel.FATAL]: chalk.magenta,\n};\n\n/**\n * File output options for logging\n */\nexport interface FileOutputOptions {\n filePath: string;\n format?: 'human' | 'json' | 'csv';\n maxSize?: number; // in bytes, default 10MB\n maxFiles?: number; // default 5\n compress?: boolean; // compress rotated files\n}\n\n/**\n * Progress tracking options\n */\nexport interface ProgressOptions {\n total: number;\n current?: number;\n label?: string;\n showPercentage?: boolean;\n showETA?: boolean;\n}\n\n/**\n * Configuration options for the logger\n */\nexport interface LoggerOptions {\n level?: LogLevel;\n verbose?: boolean;\n veryVerbose?: boolean;\n quiet?: boolean;\n silent?: boolean;\n outputFormat?: 'human' | 'json';\n colorize?: boolean;\n timestamp?: boolean;\n component?: string;\n fileOutput?: FileOutputOptions;\n enableProgressTracking?: boolean;\n}\n\n/**\n * Structured log entry for JSON output\n */\nexport interface LogEntry {\n level: string;\n message: string;\n timestamp: string;\n component?: string;\n context?: Record<string, unknown>;\n error?: {\n name: string;\n message: string;\n stack?: string;\n code?: string;\n };\n}\n\n/**\n * Enhanced error context for detailed logging\n */\nexport interface ErrorContext {\n component?: string;\n operation?: string;\n userId?: string;\n requestId?: string;\n filePath?: string;\n processingTime?: number;\n memoryUsage?: number;\n fileSize?: number;\n compressionRatio?: number;\n [key: string]: unknown;\n}\n\n/**\n * Performance metrics for detailed logging\n */\nexport interface PerformanceMetrics {\n memoryUsage: NodeJS.MemoryUsage;\n processingTime: number;\n fileCount?: number;\n totalFileSize?: number;\n optimizationRatio?: number;\n}\n\n/**\n * Centralized logger class for the Tailwind Enigma Core application\n */\nexport class Logger {\n private level: LogLevel;\n private verbose: boolean;\n private veryVerbose: boolean;\n private quiet: boolean;\n private silent: boolean;\n private outputFormat: 'human' | 'json';\n private colorize: boolean;\n private timestamp: boolean;\n private component?: string;\n private fileOutput?: FileOutputOptions;\n private fileStream?: WriteStream;\n private enableProgressTracking: boolean;\n private progressStates: Map<string, ProgressOptions & { startTime: number }> = new Map();\n\n constructor(options: LoggerOptions = {}) {\n this.level = options.level ?? LogLevel.INFO;\n this.verbose = options.verbose ?? false;\n this.veryVerbose = options.veryVerbose ?? false;\n this.quiet = options.quiet ?? false;\n this.silent = options.silent ?? false;\n this.outputFormat = options.outputFormat ?? 'human';\n this.colorize = options.colorize ?? true;\n this.timestamp = options.timestamp ?? true;\n this.component = options.component;\n this.fileOutput = options.fileOutput;\n this.enableProgressTracking = options.enableProgressTracking ?? false;\n\n // Adjust verbosity levels\n if (this.veryVerbose) {\n this.verbose = true;\n if (this.level > LogLevel.TRACE) {\n this.level = LogLevel.TRACE;\n }\n } else if (this.verbose && this.level > LogLevel.DEBUG) {\n this.level = LogLevel.DEBUG;\n }\n\n // Quiet mode overrides verbose\n if (this.quiet) {\n this.verbose = false;\n this.veryVerbose = false;\n if (this.level < LogLevel.WARN) {\n this.level = LogLevel.WARN;\n }\n }\n\n // Initialize file output if configured\n if (this.fileOutput) {\n this.initializeFileOutput();\n }\n }\n\n /**\n * Initialize file output with rotation support\n */\n private initializeFileOutput(): void {\n if (!this.fileOutput) return;\n\n try {\n // Ensure directory exists\n const dir = dirname(this.fileOutput.filePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Check if rotation is needed\n this.rotateLogsIfNeeded();\n\n // Create file stream\n this.fileStream = createWriteStream(this.fileOutput.filePath, {\n flags: 'a',\n });\n\n // Handle stream errors\n this.fileStream.on('error', (error) => {\n console.error(`Logger file stream error: ${error.message}`);\n this.fileStream = undefined;\n });\n } catch (error) {\n console.error(\n `Failed to initialize file output: ${error instanceof Error ? error.message : String(error)}`\n );\n this.fileStream = undefined;\n }\n }\n\n /**\n * Rotate log files if size limit exceeded\n */\n private rotateLogsIfNeeded(): void {\n if (!this.fileOutput || !existsSync(this.fileOutput.filePath)) return;\n\n const maxSize = this.fileOutput.maxSize ?? 10 * 1024 * 1024; // 10MB default\n const maxFiles = this.fileOutput.maxFiles ?? 5;\n const stats = statSync(this.fileOutput.filePath);\n\n if (stats.size >= maxSize) {\n this.rotateLogFiles(maxFiles);\n }\n }\n\n /**\n * Perform log file rotation with optional compression\n */\n private rotateLogFiles(maxFiles: number): void {\n if (!this.fileOutput) return;\n\n const basePath = this.fileOutput.filePath;\n const compress = this.fileOutput.compress ?? false;\n\n // Close existing stream\n if (this.fileStream) {\n this.fileStream.end();\n this.fileStream = undefined;\n }\n\n // Rotate existing files\n for (let i = maxFiles - 1; i >= 1; i--) {\n const oldPath = `${basePath}.${i}${compress ? '.gz' : ''}`;\n const newPath = `${basePath}.${i + 1}${compress ? '.gz' : ''}`;\n\n if (existsSync(oldPath)) {\n if (i === maxFiles - 1) {\n // Delete oldest file\n unlinkSync(oldPath);\n } else {\n // Rename to next number\n unlinkSync(newPath); // Remove if exists\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require('fs').renameSync(oldPath, newPath);\n }\n }\n }\n\n // Move current log to .1\n if (existsSync(basePath)) {\n const rotatedPath = `${basePath}.1`;\n if (compress) {\n // Compress and save\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const content = require('fs').readFileSync(basePath);\n const compressed = gzipSync(content);\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require('fs').writeFileSync(`${rotatedPath}.gz`, compressed);\n unlinkSync(basePath);\n } else {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require('fs').renameSync(basePath, rotatedPath);\n }\n }\n }\n\n /**\n * Set the minimum log level\n */\n setLevel(level: LogLevel): void {\n this.level = level;\n }\n\n /**\n * Enable or disable verbose logging\n */\n setVerbose(verbose: boolean): void {\n this.verbose = verbose;\n if (verbose && this.level > LogLevel.DEBUG) {\n this.level = LogLevel.DEBUG;\n }\n }\n\n /**\n * Enable or disable very verbose logging\n */\n setVeryVerbose(veryVerbose: boolean): void {\n this.veryVerbose = veryVerbose;\n if (veryVerbose) {\n this.verbose = true;\n if (this.level > LogLevel.TRACE) {\n this.level = LogLevel.TRACE;\n }\n }\n }\n\n /**\n * Enable or disable quiet mode\n */\n setQuiet(quiet: boolean): void {\n this.quiet = quiet;\n if (quiet) {\n this.verbose = false;\n this.veryVerbose = false;\n if (this.level < LogLevel.WARN) {\n this.level = LogLevel.WARN;\n }\n }\n }\n\n /**\n * Enable or disable silent mode\n */\n setSilent(silent: boolean): void {\n this.silent = silent;\n }\n\n /**\n * Set output format\n */\n setOutputFormat(format: 'human' | 'json'): void {\n this.outputFormat = format;\n }\n\n /**\n * Configure file output\n */\n setFileOutput(options: FileOutputOptions): void {\n // Close existing stream\n if (this.fileStream) {\n this.fileStream.end();\n this.fileStream = undefined;\n }\n\n this.fileOutput = options;\n this.initializeFileOutput();\n }\n\n /**\n * Disable file output\n */\n disableFileOutput(): void {\n if (this.fileStream) {\n this.fileStream.end();\n this.fileStream = undefined;\n }\n this.fileOutput = undefined;\n }\n\n /**\n * Start progress tracking for an operation\n */\n startProgress(id: string, options: ProgressOptions): void {\n if (!this.enableProgressTracking) return;\n\n this.progressStates.set(id, {\n ...options,\n startTime: Date.now(),\n });\n\n if (this.verbose) {\n const label = options.label || id;\n this.info(`📊 Starting ${label} (0/${options.total})`);\n }\n }\n\n /**\n * Update progress for an operation\n */\n updateProgress(id: string, current: number, additionalInfo?: string): void {\n if (!this.enableProgressTracking) return;\n\n const progress = this.progressStates.get(id);\n if (!progress) return;\n\n progress.current = current;\n const percentage = Math.round((current / progress.total) * 100);\n const elapsed = Date.now() - progress.startTime;\n\n let message = `📈 ${progress.label || id}: ${current}/${progress.total}`;\n\n if (progress.showPercentage !== false) {\n message += ` (${percentage}%)`;\n }\n\n if (progress.showETA !== false && current > 0) {\n const estimatedTotal = (elapsed / current) * progress.total;\n const eta = Math.round((estimatedTotal - elapsed) / 1000);\n message += ` ETA: ${eta}s`;\n }\n\n if (additionalInfo) {\n message += ` - ${additionalInfo}`;\n }\n\n if (this.verbose) {\n this.debug(message);\n }\n }\n\n /**\n * Complete progress tracking for an operation\n */\n completeProgress(id: string, summary?: string): void {\n if (!this.enableProgressTracking) return;\n\n const progress = this.progressStates.get(id);\n if (!progress) return;\n\n const elapsed = Date.now() - progress.startTime;\n const duration = Math.round(elapsed / 1000);\n\n let message = `✅ Completed ${progress.label || id} (${progress.total} items in ${duration}s)`;\n if (summary) {\n message += ` - ${summary}`;\n }\n\n if (this.verbose) {\n this.info(message);\n }\n\n this.progressStates.delete(id);\n }\n\n /**\n * Log performance metrics\n */\n performanceMetrics(operation: string, metrics: PerformanceMetrics, context?: ErrorContext): void {\n const extendedContext = {\n ...context,\n operation,\n processingTime: metrics.processingTime,\n memoryUsage: metrics.memoryUsage.heapUsed,\n fileCount: metrics.fileCount,\n totalFileSize: metrics.totalFileSize,\n optimizationRatio: metrics.optimizationRatio,\n };\n\n const heapMB = Math.round(metrics.memoryUsage.heapUsed / 1024 / 1024);\n let message = `⚡ ${operation} completed in ${metrics.processingTime}ms (heap: ${heapMB}MB)`;\n\n if (metrics.fileCount) {\n message += `, processed ${metrics.fileCount} files`;\n }\n\n if (metrics.totalFileSize) {\n const sizeMB = Math.round((metrics.totalFileSize / 1024 / 1024) * 100) / 100;\n message += `, total size: ${sizeMB}MB`;\n }\n\n if (metrics.optimizationRatio) {\n const ratio = Math.round(metrics.optimizationRatio * 100);\n message += `, optimization: ${ratio}%`;\n }\n\n if (this.veryVerbose) {\n this.trace(message, extendedContext);\n } else if (this.verbose) {\n this.debug(message, extendedContext);\n }\n }\n\n /**\n * Log detailed file operation\n */\n fileOperation(\n operation: string,\n filePath: string,\n details?: { size?: number; processingTime?: number; result?: string }\n ): void {\n if (!this.veryVerbose) return;\n\n let message = `📁 ${operation}: ${filePath}`;\n const context: ErrorContext = { operation, filePath };\n\n if (details?.size) {\n const sizeKB = Math.round(details.size / 1024);\n message += ` (${sizeKB}KB)`;\n context.fileSize = details.size;\n }\n\n if (details?.processingTime) {\n message += ` - ${details.processingTime}ms`;\n context.processingTime = details.processingTime;\n }\n\n if (details?.result) {\n message += ` → ${details.result}`;\n }\n\n this.trace(message, context);\n }\n\n /**\n * Log step-by-step process details\n */\n processStep(step: string, details?: string, context?: ErrorContext): void {\n if (!this.verbose) return;\n\n let message = `🔄 ${step}`;\n if (details) {\n message += `: ${details}`;\n }\n\n this.debug(message, context);\n }\n\n /**\n * Create a child logger with additional context\n */\n child(component: string, options: Partial<LoggerOptions> = {}): Logger {\n return new Logger({\n level: this.level,\n verbose: this.verbose,\n veryVerbose: this.veryVerbose,\n quiet: this.quiet,\n silent: this.silent,\n outputFormat: this.outputFormat,\n colorize: this.colorize,\n timestamp: this.timestamp,\n component,\n fileOutput: this.fileOutput,\n enableProgressTracking: this.enableProgressTracking,\n ...options,\n });\n }\n\n /**\n * Check if a log level should be output\n */\n private shouldLog(level: LogLevel): boolean {\n if (this.silent) return false;\n return level >= this.level;\n }\n\n /**\n * Format timestamp\n */\n private getTimestamp(): string {\n return new Date().toISOString();\n }\n\n /**\n * Create a structured log entry\n */\n private createLogEntry(\n level: LogLevel,\n message: string,\n context?: ErrorContext,\n error?: Error\n ): LogEntry {\n const entry: LogEntry = {\n level: LogLevelNames[level],\n message,\n timestamp: this.getTimestamp(),\n };\n\n if (this.component) {\n entry.component = this.component;\n }\n\n if (context && Object.keys(context).length > 0) {\n entry.context = { ...context };\n }\n\n if (error) {\n entry.error = {\n name: error.name,\n message: error.message,\n stack: error.stack,\n code: (error as any).code,\n };\n }\n\n return entry;\n }\n\n /**\n * Format log entry for human-readable output\n */\n private formatHuman(entry: LogEntry): string {\n const levelName = entry.level.padEnd(5);\n const colorFn = LogLevelColors[LogLevel[entry.level as keyof typeof LogLevel]];\n\n let output = '';\n\n if (this.timestamp) {\n output += chalk.gray(`[${entry.timestamp}] `);\n }\n\n if (this.colorize) {\n output += colorFn(`${levelName} `);\n } else {\n output += `${levelName} `;\n }\n\n if (entry.component) {\n output += chalk.gray(`[${entry.component}] `);\n }\n\n output += entry.message;\n\n if (entry.context && Object.keys(entry.context).length > 0) {\n output += chalk.gray(` ${JSON.stringify(entry.context)}`);\n }\n\n if (entry.error) {\n output += '\\n' + (entry.error.stack || `${entry.error.name}: ${entry.error.message}`);\n }\n\n return output;\n }\n\n /**\n * Format log entry for CSV output\n */\n private formatCSV(entry: LogEntry): string {\n const timestamp = entry.timestamp;\n const level = entry.level;\n const component = entry.component || '';\n const message = entry.message.replace(/\"/g, '\"\"'); // Escape quotes\n const context = entry.context ? JSON.stringify(entry.context).replace(/\"/g, '\"\"') : '';\n const errorMessage = entry.error\n ? `${entry.error.name}: ${entry.error.message}`.replace(/\"/g, '\"\"')\n : '';\n\n return `\"${timestamp}\",\"${level}\",\"${component}\",\"${message}\",\"${context}\",\"${errorMessage}\"`;\n }\n\n /**\n * Output a log entry to console and/or file\n */\n private output(entry: LogEntry): void {\n // Console output - send errors to stderr, others to stdout\n if (this.outputFormat === 'json') {\n if (entry.level === 'ERROR' || entry.level === 'FATAL') {\n console.error(JSON.stringify(entry));\n } else {\n console.log(JSON.stringify(entry));\n }\n } else {\n const formattedMessage = this.formatHuman(entry);\n if (entry.level === 'ERROR' || entry.level === 'FATAL') {\n console.error(formattedMessage);\n } else {\n console.log(formattedMessage);\n }\n }\n\n // File output\n if (this.fileStream && !this.fileStream.destroyed) {\n try {\n let fileContent: string;\n const fileFormat = this.fileOutput?.format || 'human';\n\n switch (fileFormat) {\n case 'json':\n fileContent = JSON.stringify(entry) + '\\n';\n break;\n case 'csv':\n fileContent = this.formatCSV(entry) + '\\n';\n break;\n default:\n fileContent = this.formatHuman(entry) + '\\n';\n }\n\n this.fileStream.write(fileContent);\n\n // Check if rotation is needed after write\n this.rotateLogsIfNeeded();\n } catch (error) {\n console.error(\n `Failed to write to log file: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n }\n\n /**\n * Core logging method\n */\n private log(level: LogLevel, message: string, context?: ErrorContext, _error?: Error): void {\n if (!this.shouldLog(level)) return;\n\n const entry = this.createLogEntry(level, message, context, _error);\n this.output(entry);\n }\n\n /**\n * Log a trace message (most verbose)\n */\n trace(message: string, context?: ErrorContext): void {\n this.log(LogLevel.TRACE, message, context);\n }\n\n /**\n * Log a debug message\n */\n debug(message: string, context?: ErrorContext): void {\n this.log(LogLevel.DEBUG, message, context);\n }\n\n /**\n * Log an info message\n */\n info(message: string, context?: ErrorContext): void {\n this.log(LogLevel.INFO, message, context);\n }\n\n /**\n * Log a warning message\n */\n warn(message: string, context?: ErrorContext): void {\n this.log(LogLevel.WARN, message, context);\n }\n\n /**\n * Log an error message\n */\n error(messageOrError: string | Error, context?: ErrorContext): void {\n if (messageOrError instanceof Error) {\n this.log(LogLevel.ERROR, messageOrError.message, context, messageOrError);\n } else {\n this.log(LogLevel.ERROR, messageOrError, context);\n }\n }\n\n /**\n * Log a fatal error message\n */\n fatal(messageOrError: string | Error, context?: ErrorContext): void {\n if (messageOrError instanceof Error) {\n this.log(LogLevel.FATAL, messageOrError.message, context, messageOrError);\n } else {\n this.log(LogLevel.FATAL, messageOrError, context);\n }\n }\n\n /**\n * Log performance timing\n */\n timing(operation: string, duration: number, context?: ErrorContext): void {\n const extendedContext = {\n ...context,\n operation,\n processingTime: duration,\n };\n this.debug(`Operation \"${operation}\" completed in ${duration}ms`, extendedContext);\n }\n\n /**\n * Clean up resources (close file streams)\n */\n cleanup(): void {\n if (this.fileStream) {\n this.fileStream.end();\n this.fileStream = undefined;\n }\n this.progressStates.clear();\n }\n\n /**\n * Get current logger state for debugging\n */\n getState(): {\n level: LogLevel;\n verbose: boolean;\n veryVerbose: boolean;\n quiet: boolean;\n silent: boolean;\n fileOutputEnabled: boolean;\n progressTrackingEnabled: boolean;\n activeProgressCount: number;\n } {\n return {\n level: this.level,\n verbose: this.verbose,\n veryVerbose: this.veryVerbose,\n quiet: this.quiet,\n silent: this.silent,\n fileOutputEnabled: !!this.fileOutput,\n progressTrackingEnabled: this.enableProgressTracking,\n activeProgressCount: this.progressStates.size,\n };\n }\n}\n\n/**\n * Parse log level from string\n */\nfunction parseLogLevel(level?: string): LogLevel {\n if (!level) return LogLevel.INFO;\n\n const upperLevel = level.toUpperCase();\n switch (upperLevel) {\n case 'TRACE':\n return LogLevel.TRACE;\n case 'DEBUG':\n return LogLevel.DEBUG;\n case 'INFO':\n return LogLevel.INFO;\n case 'WARN':\n return LogLevel.WARN;\n case 'ERROR':\n return LogLevel.ERROR;\n case 'FATAL':\n return LogLevel.FATAL;\n default:\n return LogLevel.INFO;\n }\n}\n\n/**\n * Create file output options from environment variables\n */\nfunction createFileOutputFromEnv(): FileOutputOptions | undefined {\n const filePath = process.env.ENIGMA_LOG_FILE;\n if (!filePath) return undefined;\n\n return {\n filePath,\n format: (process.env.ENIGMA_LOG_FORMAT as 'human' | 'json' | 'csv') || 'human',\n maxSize: process.env.ENIGMA_LOG_MAX_SIZE\n ? parseInt(process.env.ENIGMA_LOG_MAX_SIZE)\n : undefined,\n maxFiles: process.env.ENIGMA_LOG_MAX_FILES\n ? parseInt(process.env.ENIGMA_LOG_MAX_FILES)\n : undefined,\n compress: process.env.ENIGMA_LOG_COMPRESS === 'true',\n };\n}\n\n/**\n * Default logger instance\n */\nexport const logger = new Logger({\n level: process.env.ENIGMA_LOG_LEVEL\n ? parseLogLevel(process.env.ENIGMA_LOG_LEVEL)\n : process.env.NODE_ENV === 'development'\n ? LogLevel.DEBUG\n : LogLevel.INFO,\n verbose: process.env.ENIGMA_VERBOSE === 'true',\n veryVerbose: process.env.ENIGMA_VERY_VERBOSE === 'true',\n quiet: process.env.ENIGMA_QUIET === 'true',\n colorize: process.stdout.isTTY,\n timestamp: true,\n fileOutput: createFileOutputFromEnv(),\n enableProgressTracking: process.env.ENIGMA_PROGRESS_TRACKING !== 'false',\n});\n\n/**\n * Create a logger with specific component context\n */\nexport function createLogger(component: string, _options?: Partial<LoggerOptions>): Logger {\n return logger.child(component, _options);\n}\n","/**\n * Copyright (c) 2025 Rowan Cardow\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport * as path from 'node:path';\nimport { z } from 'zod';\n\n/**\n * Path calculation options schema\n */\nexport const PathCalculationOptionsSchema = z.object({\n /** Use relative paths instead of absolute paths */\n useRelativePaths: z.boolean().default(true),\n /** Base path for resolving relative paths */\n basePath: z.string().optional(),\n /** Whether to normalize paths for web use (forward slashes) */\n normalizeForWeb: z.boolean().default(true),\n /** Maximum allowed path depth to prevent excessive nesting */\n maxDepth: z.number().min(1).max(100).default(50),\n /** Whether to resolve symbolic links */\n resolveSymlinks: z.boolean().default(false),\n /** Enable path traversal protection */\n enableSecurity: z.boolean().default(true),\n});\n\nexport type PathCalculationOptions = z.infer<typeof PathCalculationOptionsSchema>;\n\n/**\n * Path validation result\n */\nexport interface PathValidationResult {\n isValid: boolean;\n normalizedPath: string;\n errors: string[];\n warnings: string[];\n security: {\n hasTraversal: boolean;\n isAbsolute: boolean;\n depth: number;\n };\n}\n\n/**\n * Relative path calculation result\n */\nexport interface RelativePathResult {\n relativePath: string;\n isValid: boolean;\n normalizedPath: string;\n metadata: {\n fromPath: string;\n toPath: string;\n basePath?: string;\n platformSeparators: string;\n webPath: string;\n depth: number;\n };\n}\n\n/**\n * Custom error classes for path operations\n */\nexport class PathUtilsError extends Error {\n public code: string;\n public cause?: Error;\n\n constructor(message: string, code: string, cause?: Error) {\n super(message);\n this.name = 'PathUtilsError';\n this.code = code;\n this.cause = cause;\n }\n}\n\nexport class PathSecurityError extends PathUtilsError {\n public path: string;\n\n constructor(message: string, path: string, cause?: Error) {\n super(message, 'PATH_SECURITY_ERROR', cause);\n this.name = 'PathSecurityError';\n this.path = path;\n }\n}\n\nexport class PathValidationError extends PathUtilsError {\n public path: string;\n\n constructor(message: string, path: string, cause?: Error) {\n super(message, 'PATH_VALIDATION_ERROR', cause);\n this.name = 'PathValidationError';\n this.path = path;\n }\n}\n\n/**\n * Enhanced path utilities class with caching and security features\n */\nexport class PathUtils {\n private readonly options: PathCalculationOptions;\n private pathCache = new Map<string, RelativePathResult>();\n private validationCache = new Map<string, PathValidationResult>();\n private readonly maxCacheSize = 1000;\n\n constructor(options: Partial<PathCalculationOptions> = {}) {\n this.options = PathCalculationOptionsSchema.parse(options);\n }\n\n /**\n * Calculate relative path from one file to another\n */\n calculateRelativePath(\n fromPath: string,\n toPath: string,\n options?: Partial<PathCalculationOptions>\n ): RelativePathResult {\n const mergedOptions = { ...this.options, ...options };\n const cacheKey = `${fromPath}::${toPath}::${JSON.stringify(mergedOptions, Object.keys(mergedOptions).sort())}`;\n\n // Check cache first\n if (this.pathCache.has(cacheKey)) {\n return this.pathCache.get(cacheKey)!;\n }\n\n try {\n // Validate input paths - throw if invalid\n const fromValidation = this.validatePath(fromPath, 'fromPath');\n if (!fromValidation.isValid) {\n throw new PathValidationError(\n `Invalid fromPath: ${fromValidation.errors.join(', ')}`,\n fromPath\n );\n }\n\n const toValidation = this.validatePath(toPath, 'toPath');\n if (!toValidation.isValid) {\n throw new PathValidationError(`Invalid toPath: ${toValidation.errors.join(', ')}`, toPath);\n }\n\n // If not using relative paths, return normalized toPath\n if (!mergedOptions.useRelativePaths) {\n let normalizedPath = toPath;\n if (mergedOptions.normalizeForWeb) {\n normalizedPath = this.normalizeForWeb(toPath);\n if (process.platform === 'win32') {\n normalizedPath = normalizedPath.toLowerCase();\n }\n }\n\n const result: RelativePathResult = {\n relativePath: normalizedPath,\n isValid: true,\n normalizedPath,\n metadata: {\n fromPath,\n toPath,\n basePath: mergedOptions.basePath,\n platformSeparators: path.sep,\n webPath: normalizedPath,\n depth: this.calculatePathDepth(normalizedPath),\n },\n };\n\n this.cacheResult(cacheKey, result);\n return result;\n }\n\n // Normalize input paths to use platform-appropriate separators\n let normalizedFromPath = this.normalizePlatformPath(fromPath);\n let normalizedToPath = this.normalizePlatformPath(toPath);\n\n // Apply base path if provided\n if (mergedOptions.basePath) {\n if (!path.isAbsolute(normalizedFromPath)) {\n normalizedFromPath = path.resolve(mergedOptions.basePath, normalizedFromPath);\n }\n if (!path.isAbsolute(normalizedToPath)) {\n normalizedToPath = path.resolve(mergedOptions.basePath, normalizedToPath);\n }\n }\n\n // Calculate relative path from directory of fromPath to toPath\n const fromDir = path.dirname(normalizedFromPath);\n const relativePath = path.relative(fromDir, normalizedToPath);\n\n // Normalize for web use if requested\n const webPath = mergedOptions.normalizeForWeb\n ? this.normalizeForWeb(relativePath)\n : relativePath;\n\n // Security check\n if (mergedOptions.enableSecurity) {\n this.performSecurityCheck(webPath, fromPath, toPath);\n }\n\n const result: RelativePathResult = {\n relativePath: webPath,\n isValid: true,\n normalizedPath: webPath,\n metadata: {\n fromPath: normalizedFromPath,\n toPath: normalizedToPath,\n basePath: mergedOptions.basePath,\n platformSeparators: path.sep,\n webPath,\n depth: this.calculatePathDepth(webPath),\n },\n };\n\n this.cacheResult(cacheKey, result);\n return result;\n } catch (error) {\n // Don't cache error results\n throw new PathUtilsError(\n `Failed to calculate relative path: ${error instanceof Error ? error.message : String(error)}`,\n 'CALCULATION_ERROR',\n error instanceof Error ? error : undefined\n );\n }\n }\n\n /**\n * Validate a path for security and correctness\n */\n validatePath(inputPath: string, context = 'path'): PathValidationResult {\n const cacheKey = `validate::${inputPath}::${context}`;\n\n if (this.validationCache.has(cacheKey)) {\n return this.validationCache.get(cacheKey)!;\n }\n\n const result: PathValidationResult = {\n isValid: true,\n normalizedPath: inputPath,\n errors: [],\n warnings: [],\n security: {\n hasTraversal: false,\n isAbsolute: false,\n depth: 0,\n },\n };\n\n try {\n // Basic validation\n if (!inputPath || typeof inputPath !== 'string') {\n result.isValid = false;\n result.errors.push(`${context} must be a non-empty string`);\n return result;\n }\n\n // Trim whitespace\n const trimmedPath = inputPath.trim();\n if (trimmedPath !== inputPath) {\n result.warnings.push(`${context} has leading/trailing whitespace`);\n }\n\n // Security checks\n if (this.options.enableSecurity) {\n // Check for path traversal attempts\n if (trimmedPath.includes('..')) {\n result.security.hasTraversal = true;\n result.warnings.push(`${context} contains path traversal sequences`);\n }\n\n // Check for null bytes (security vulnerability)\n if (trimmedPath.includes('\\0')) {\n result.isValid = false;\n result.errors.push(`${context} contains null bytes (security risk)`);\n return result;\n }\n\n // Check for suspicious patterns\n const suspiciousPatterns = [\n /\\.(\\.)+/, // Multiple dots\n /[<>:\"|?*]/, // Invalid Windows characters\n /^\\s*$/, // Whitespace only\n ];\n\n for (const pattern of suspiciousPatterns) {\n if (pattern.test(trimmedPath)) {\n result.warnings.push(`${context} contains potentially problematic characters`);\n break;\n }\n }\n }\n\n // Normalize and analyze\n result.normalizedPath = this.normalizePath(trimmedPath, true);\n // Use original trimmed path for isAbsolute check before any normalization\n result.security.isAbsolute = path.isAbsolute(trimmedPath);\n result.security.depth = this.calculatePathDepth(result.normalizedPath);\n\n // Check depth limits\n if (result.security.depth > this.options.maxDepth) {\n result.isValid = false;\n result.errors.push(`${context} exceeds maximum depth of ${this.options.maxDepth}`);\n }\n\n // Cache the result\n this.cacheValidationResult(cacheKey, result);\n return result;\n } catch (error) {\n result.isValid = false;\n result.errors.push(\n `Validation failed: ${error instanceof Error ? error.message : String(error)}`\n );\n return result;\n }\n }\n\n /**\n * Normalize path for comparison and consistency\n */\n normalizePath(inputPath: string, forWeb = false): string {\n if (!inputPath) return '';\n\n let normalized = inputPath;\n\n // Handle case sensitivity based on platform and web normalization\n if (forWeb) {\n // Always lowercase for web normalization\n normalized = normalized.toLowerCase();\n } else if (process.platform === 'win32') {\n // Windows is case-insensitive\n normalized = normalized.toLowerCase();\n }\n // Unix-like systems preserve case when not normalizing for web\n\n // Normalize path separators based on target format\n if (forWeb) {\n // For web normalization, always use forward slashes\n normalized = normalized.replace(/\\\\/g, '/');\n } else {\n // For platform normalization, use Node.js normalize but preserve format\n normalized = path.normalize(normalized);\n\n // On Windows, if we get backslashes and the input had forward slashes,\n // and we're not explicitly going for web format, maintain forward slashes\n // This is crucial for test compatibility\n if (process.platform === 'win32' && inputPath.includes('/') && !inputPath.includes('\\\\')) {\n normalized = normalized.replace(/\\\\/g, '/');\n }\n }\n\n // Remove leading ./ if present\n normalized = normalized.replace(/^\\.\\//, '');\n normalized = normalized.replace(/^\\.\\\\/, ''); // Windows version\n\n // Special handling for root path - always return \"/\" for consistency across platforms\n // This ensures tests pass consistently regardless of the platform\n if (normalized === '/' || normalized === '\\\\') {\n return '/';\n }\n\n // Remove leading slash for web normalization (except root)\n if (forWeb && normalized.startsWith('/') && normalized.length > 1) {\n normalized = normalized.substring(1);\n }\n if (forWeb && normalized.startsWith('\\\\') && normalized.length > 1) {\n normalized = normalized.substring(1);\n }\n\n // Normalize multiple slashes to single slash\n const separator = forWeb ? '/' : normalized.includes('/') ? '/' : path.sep;\n if (separator === '/') {\n normalized = normalized.replace(/\\/+/g, '/');\n } else {\n normalized = normalized.replace(/\\\\+/g, '\\\\');\n }\n\n // Remove trailing slash if present (except for root)\n if (normalized.length > 1) {\n normalized = normalized.replace(/\\/$/, '');\n normalized = normalized.replace(/\\\\$/, '');\n }\n\n return normalized;\n }\n\n /**\n * Normalize path for web use (forward slashes only)\n */\n private normalizeForWeb(inputPath: string): string {\n return inputPath.replace(/\\\\/g, '/');\n }\n\n /**\n * Normalize path using platform-specific separators\n */\n private normalizePlatformPath(inputPath: string): string {\n return inputPath.replace(/[/\\\\]/g, path.sep);\n }\n\n /**\n * Calculate the depth of a path (number of directory levels)\n */\n private calculatePathDepth(inputPath: string): number {\n if (!inputPath || inputPath === '.' || inputPath === '/') return 0;\n\n const normalizedPath = this.normalizePath(inputPath, true);\n const segments = normalizedPath.split('/').filter((segment) => segment && segment !== '.');\n return segments.length;\n }\n\n /**\n * Perform security checks on calculated paths\n */\n private performSecurityCheck(calculatedPath: string, _fromPath: string, _toPath: string): void {\n // Check for path traversal in the result\n if (calculatedPath.includes('..')) {\n const depth = (calculatedPath.match(/\\.\\./g) || []).length;\n if (depth > 10) {\n // Arbitrary limit for excessive traversal\n throw new PathSecurityError(\n `Excessive path traversal detected (${depth} levels up)`,\n calculatedPath\n );\n }\n }\n\n // Check for absolute paths in result when relative expected\n if (this.options.useRelativePaths && path.isAbsolute(calculatedPath)) {\n throw new PathSecurityError(\n 'Unexpected absolute path in relative calculation result',\n calculatedPath\n );\n }\n }\n\n /**\n * Cache management\n */\n private cacheResult(key: string, result: RelativePathResult): void {\n if (this.pathCache.size >= this.maxCacheSize) {\n // Simple LRU: delete oldest entry\n const firstKey = this.pathCache.keys().next().value;\n if (firstKey) this.pathCache.delete(firstKey);\n }\n this.pathCache.set(key, result);\n }\n\n private cacheValidationResult(key: string, result: PathValidationResult): void {\n if (this.validationCache.size >= this.maxCacheSize) {\n // Simple LRU: delete oldest entry\n const firstKey = this.validationCache.keys().next().value;\n if (firstKey) this.validationCache.delete(firstKey);\n }\n this.validationCache.set(key, result);\n }\n\n /**\n * Clear all caches\n */\n clearCache(): void {\n this.pathCache.clear();\n this.validationCache.clear();\n }\n\n /**\n * Get cache statistics\n */\n getCacheStats(): { paths: number; validations: number; maxSize: number } {\n return {\n paths: this.pathCache.size,\n validations: this.validationCache.size,\n maxSize: this.maxCacheSize,\n };\n }\n}\n\n/**\n * Utility functions for common path operations\n */\n\n/**\n * Create a PathUtils instance with default options\n */\nexport function createPathUtils(options: Partial<PathCalculationOptions> = {}): PathUtils {\n return new PathUtils(options);\n}\n\n/**\n * Quick relative path calculation\n */\nexport function calculateRelativePath(\n fromPath: string,\n toPath: string,\n options: Partial<PathCalculationOptions> = {}\n): string {\n const utils = createPathUtils(options);\n const result = utils.calculateRelativePath(fromPath, toPath);\n return result.relativePath;\n}\n\n/**\n * Quick path validation\n */\nexport function validatePath(inputPath: string, context = 'path'): PathValidationResult {\n const utils = createPathUtils();\n return utils.validatePath(inputPath, context);\n}\n\n/**\n * Quick path normalization\n */\nexport function normalizePath(inputPath: string, forWeb = false): string {\n const utils = createPathUtils();\n return utils.normalizePath(inputPath, forWeb);\n}\n\n/**\n * Check if a path is safe (no security issues)\n */\nexport function isPathSafe(inputPath: string): boolean {\n const validation = validatePath(inputPath);\n return validation.isValid && !validation.security.hasTraversal;\n}\n\n/**\n * Batch path operations for performance\n */\nexport function calculateRelativePathsBatch(\n pairs: Array<{ from: string; to: string }>,\n options: Partial<PathCalculationOptions> = {}\n): RelativePathResult[] {\n const utils = createPathUtils(options);\n return pairs.map(({ from, to }) => utils.calculateRelativePath(from, to));\n}\n","/**\n * Copyright (c) 2025 Rowan Cardow\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport * as cheerio from 'cheerio';\nimport { Node } from 'domhandler';\nimport * as fs from 'fs/promises';\nimport { z } from 'zod';\n\n/**\n * Configuration options for HTML class extraction\n */\nexport const HtmlExtractionOptionsSchema = z.object({\n preserveWhitespace: z.boolean().default(false),\n caseSensitive: z.boolean().default(true),\n ignoreEmpty: z.boolean().default(true),\n maxFileSize: z\n .number()\n .min(1)\n .default(10 * 1024 * 1024), // 10MB\n timeout: z.number().min(1).default(5000), // 5 seconds\n});\n\nexport type HtmlExtractionOptions = z.infer<typeof HtmlExtractionOptionsSchema>;\n\n/**\n * Data structure for individual class information\n */\nexport interface ClassData {\n name: string;\n frequency: number;\n contexts: Array<{\n tagName: string;\n attributes: Record<string, string>;\n depth: number;\n }>;\n}\n\n/**\n * Result of HTML class extraction operation\n */\nexport interface HtmlClassExtractionResult {\n classes: Map<string, ClassData>;\n totalElements: number;\n totalClasses: number;\n uniqueClasses: number;\n metadata: {\n source: string;\n processedAt: Date;\n processingTime: number;\n fileSize?: number;\n errors: string[];\n };\n}\n\n/**\n * Custom error classes for HTML parsing operations\n */\nexport class HtmlParsingError extends Error {\n public source?: string;\n public cause?: Error;\n\n constructor(message: string, source?: string, cause?: Error) {\n super(message);\n this.name = 'HtmlParsingError';\n this.source = source;\n this.cause = cause;\n }\n}\n\nexport class FileReadError extends Error {\n public filePath?: string;\n public cause?: Error;\n\n constructor(message: string, filePath?: string, cause?: Error) {\n super(message);\n this.name = 'FileReadError';\n this.filePath = filePath;\n this.cause = cause;\n }\n}\n\n/**\n * Main HTML class extractor class\n */\nexport class HtmlExtractor {\n private options: HtmlExtractionOptions;\n\n constructor(options: Partial<HtmlExtractionOptions> = {}) {\n this.options = HtmlExtractionOptionsSchema.parse(options);\n }\n\n /**\n * Extract classes from HTML string\n */\n async extractFromString(html: string, source = 'string'): Promise<HtmlClassExtractionResult> {\n const startTime = Date.now();\n const metadata = {\n source,\n processedAt: new Date(),\n processingTime: 0,\n errors: [] as string[],\n };\n\n try {\n // Load HTML with cheerio\n const $ = cheerio.load(html, {\n xml: {\n xmlMode: false,\n decodeEntities: true,\n withStartIndices: false,\n withEndIndices: false,\n },\n });\n\n const classes = new Map<string, ClassData>();\n let totalElements = 0;\n let totalClasses = 0;\n\n // Find all elements with class attributes\n $('[class]').each((index, element) => {\n totalElements++;\n const $element = $(element);\n const classAttr = $element.attr('class');\n\n if (!classAttr) return;\n\n // Parse class attribute\n const elementClasses = this.parseClassAttribute(classAttr);\n totalClasses += elementClasses.length;\n\n // Get element context\n const tagName = element.tagName?.toLowerCase() || 'unknown';\n const attributes = element.attribs || {};\n const depth = this.calculateDepth($element);\n\n // Process each class\n elementClasses.forEach((className) => {\n if (!this.options.caseSensitive) {\n className = className.toLowerCase();\n }\n\n if (!classes.has(className)) {\n classes.set(className, {\n name: className,\n frequency: 0,\n contexts: [],\n });\n }\n\n const classData = classes.get(className)!;\n classData.frequency++;\n\n // Add context (limit to avoid memory issues)\n if (classData.contexts.length < 10) {\n classData.contexts.push({\n tagName,\n attributes: this.sanitizeAttributes(attributes),\n depth,\n });\n }\n });\n });\n\n metadata.processingTime = Date.now() - startTime;\n\n return {\n classes,\n totalElements,\n totalClasses,\n uniqueClasses: classes.size,\n metadata,\n };\n } catch (error) {\n metadata.errors.push(error instanceof Error ? error.message : String(error));\n metadata.processingTime = Date.now() - startTime;\n\n throw new HtmlParsingError(\n `Failed to parse HTML: ${error instanceof Error ? error.message : String(error)}`,\n source,\n error instanceof Error ? error : undefined\n );\n }\n }\n\n /**\n * Extract classes from HTML file\n */\n async extractFromFile(filePath: string): Promise<HtmlClassExtractionResult> {\n try {\n // Check file size\n const stats = await fs.stat(filePath);\n if (stats.size > this.options.maxFileSize) {\n throw new FileReadError(\n `File size (${stats.size} bytes) exceeds maximum allowed size (${this.options.maxFileSize} bytes)`,\n filePath\n );\n }\n\n // Read file with timeout\n const html = await this.readFileWithTimeout(filePath, this.options.timeout);\n const result = await this.extractFromString(html, filePath);\n\n // Add file metadata\n result.metadata.fileSize = stats.size;\n\n return result;\n } catch (error) {\n if (error instanceof HtmlParsingError || error instanceof FileReadError) {\n throw error;\n }\n\n throw new FileReadError(\n `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,\n filePath,\n error instanceof Error ? error : undefined\n );\n }\n }\n\n /**\n * Extract classes from multiple HTML files\n */\n async extractFromFiles(filePaths: string[]): Promise<HtmlClassExtractionResult[]> {\n const results: HtmlClassExtractionResult[] = [];\n\n for (const filePath of filePaths) {\n try {\n const result = await this.extractFromFile(filePath);\n results.push(result);\n } catch (error) {\n // Create error result for failed files\n results.push({\n classes: new Map(),\n totalElements: 0,\n totalClasses: 0,\n uniqueClasses: 0,\n metadata: {\n source: filePath,\n processedAt: new Date(),\n processingTime: 0,\n errors: [error instanceof Error ? error.message : String(error)],\n },\n });\n }\n }\n\n return results;\n }\n\n /**\n * Parse class attribute string into individual class names\n */\n private parseClassAttribute(classAttr: string): string[] {\n if (!classAttr || (!this.options.preserveWhitespace && !classAttr.trim())) {\n return [];\n }\n\n // Split by whitespace and filter empty strings\n const classes = classAttr\n .split(/\\s+/)\n .map((cls) => (this.options.preserveWhitespace ? cls : cls.trim()))\n .filter((cls) => (this.options.ignoreEmpty ? cls.length > 0 : true));\n\n return classes;\n }\n\n /**\n * Calculate the depth of an element in the DOM tree\n */\n private calculateDepth($element: cheerio.Cheerio<Node>): number {\n let depth = 0;\n let current = ($element as any).parent();\n\n while (curre