UNPKG

@ordojs/cli

Version:

Command-line interface for OrdoJS framework

359 lines (358 loc) 13.2 kB
/** * @fileoverview OrdoJS CLI - File Watcher * * Efficient file system monitoring with intelligent change detection * and dependency tracking for hot module replacement. */ import { EventEmitter } from 'events'; import { FSWatcher, watch } from 'fs'; import path from 'path'; import { fileExists } from '../utils/fs.js'; import { logger } from '../utils/index.js'; /** * File change event types */ export var FileChangeType; (function (FileChangeType) { FileChangeType["ADDED"] = "added"; FileChangeType["CHANGED"] = "changed"; FileChangeType["DELETED"] = "deleted"; })(FileChangeType || (FileChangeType = {})); /* * * FileWatcher class for efficient file system monitoring */ export class FileWatcher extends EventEmitter { options; watchers; dependencyGraph; debounceTimers; isWatching; /** * Create a new FileWatcher instance */ constructor(options) { super(); this.options = { watchDir: '.', patterns: ['**/*.ordo', '**/*.ts', '**/*.js', '**/*.css', '**/*.html'], ignorePatterns: ['**/node_modules/**', '**/dist/**', '**/.git/**', '**/coverage/**'], debounceMs: 100, recursive: true, ...options }; this.watchers = new Map(); this.dependencyGraph = new Map(); this.debounceTimers = new Map(); this.isWatching = false; } /** * Start watching files */ async start() { if (this.isWatching) { logger.warn('FileWatcher is already watching'); return; } logger.info(`Starting file watcher for: ${this.options.watchDir}`); try { // Verify the watch directory exists const dirExists = await fileExists(this.options.watchDir); if (!dirExists) { throw new Error(`Watch directory does not exist: ${this.options.watchDir}`); } // Initialize dependency graph await this.buildInitialDependencyGraph(); // Start watching the directory await this.startWatching(); this.isWatching = true; logger.success('File watcher started successfully'); } catch (error) { logger.error(`Failed to start file watcher: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Stop watching files */ async stop() { if (!this.isWatching) { logger.info('FileWatcher is not watching'); return; } logger.info('Stopping file watcher...'); try { // Clear all debounce timers for (const timer of this.debounceTimers.values()) { clearTimeout(timer); } this.debounceTimers.clear(); // Close all watchers for (const [path, watcher] of this.watchers) { watcher.close(); logger.debug(`Closed watcher for: ${path}`); } this.watchers.clear(); this.isWatching = false; logger.success('File watcher stopped'); } catch (error) { logger.error(`Error stopping file watcher: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Get files that are affected by a change to the given file */ getAffectedFiles(filePath) { const absolutePath = path.resolve(filePath); const affected = new Set(); // Add the file itself affected.add(absolutePath); // Add all dependents recursively this.collectDependents(absolutePath, affected); return Array.from(affected); } /** * Add a dependency relationship */ addDependency(dependent, dependency) { const dependentPath = path.resolve(dependent); const dependencyPath = path.resolve(dependency); // Initialize dependency info if not exists if (!this.dependencyGraph.has(dependencyPath)) { this.dependencyGraph.set(dependencyPath, { dependents: new Set(), dependencies: new Set(), lastModified: Date.now() }); } if (!this.dependencyGraph.has(dependentPath)) { this.dependencyGraph.set(dependentPath, { dependents: new Set(), dependencies: new Set(), lastModified: Date.now() }); } // Add the relationship const dependencyInfo = this.dependencyGraph.get(dependencyPath); const dependentInfo = this.dependencyGraph.get(dependentPath); dependencyInfo.dependents.add(dependentPath); dependentInfo.dependencies.add(dependencyPath); logger.debug(`Added dependency: ${dependent} -> ${dependency}`); } /** * Remove a dependency relationship */ removeDependency(dependent, dependency) { const dependentPath = path.resolve(dependent); const dependencyPath = path.resolve(dependency); const dependencyInfo = this.dependencyGraph.get(dependencyPath); const dependentInfo = this.dependencyGraph.get(dependentPath); if (dependencyInfo) { dependencyInfo.dependents.delete(dependentPath); } if (dependentInfo) { dependentInfo.dependencies.delete(dependencyPath); } logger.debug(`Removed dependency: ${dependent} -> ${dependency}`); } /** * Get dependency information for a file */ getDependencyInfo(filePath) { const absolutePath = path.resolve(filePath); return this.dependencyGraph.get(absolutePath); } /* * * Check if a file should be watched based on patterns */ shouldWatchFile(filePath) { const relativePath = path.relative(this.options.watchDir, filePath); // Check ignore patterns first for (const ignorePattern of this.options.ignorePatterns) { if (this.matchesPattern(relativePath, ignorePattern)) { return false; } } // Check include patterns for (const pattern of this.options.patterns) { if (this.matchesPattern(relativePath, pattern)) { return true; } } return false; } /** * Simple glob pattern matching */ matchesPattern(filePath, pattern) { // Normalize paths for comparison const normalizedPath = filePath.replace(/\\/g, '/'); const normalizedPattern = pattern.replace(/\\/g, '/'); // Convert glob pattern to regex const regexPattern = normalizedPattern .replace(/\*\*/g, '§DOUBLESTAR§') // Temporary placeholder .replace(/\*/g, '[^/]*') .replace(/\?/g, '[^/]') .replace(/§DOUBLESTAR§/g, '.*'); // Replace placeholder with .* const regex = new RegExp(`^${regexPattern}$`); return regex.test(normalizedPath); } /** * Start watching the directory */ async startWatching() { const watchPath = path.resolve(this.options.watchDir); logger.debug(`Setting up watcher for: ${watchPath}`); const watcher = watch(watchPath, { recursive: this.options.recursive }, (eventType, filename) => { if (!filename) return; const fullPath = path.join(watchPath, filename); this.handleFileChange(eventType, fullPath); }); watcher.on('error', (error) => { logger.error(`File watcher error: ${error.message}`); this.emit('error', error); }); this.watchers.set(watchPath, watcher); } /** * Handle file change events with debouncing */ handleFileChange(eventType, filePath) { if (!this.shouldWatchFile(filePath)) { return; } // Clear existing debounce timer const existingTimer = this.debounceTimers.get(filePath); if (existingTimer) { clearTimeout(existingTimer); } // Set new debounce timer const timer = setTimeout(async () => { this.debounceTimers.delete(filePath); await this.processFileChange(eventType, filePath); }, this.options.debounceMs); this.debounceTimers.set(filePath, timer); } /** * Process a file change after debouncing */ async processFileChange(eventType, filePath) { try { const relativePath = path.relative(this.options.watchDir, filePath); const extension = path.extname(filePath); const timestamp = Date.now(); // Determine change type let changeType; const exists = await fileExists(filePath); if (eventType === 'rename') { changeType = exists ? FileChangeType.ADDED : FileChangeType.DELETED; } else { changeType = FileChangeType.CHANGED; } // Update dependency graph if (changeType === FileChangeType.DELETED) { this.removeDependencyInfo(filePath); } else { await this.updateDependencyInfo(filePath); } // Create change event const changeEvent = { type: changeType, filePath: path.resolve(filePath), relativePath, timestamp, extension }; logger.debug(`File ${changeType}: ${relativePath}`); // Emit the change event this.emit('change', changeEvent); } catch (error) { logger.error(`Error processing file change for ${filePath}: ${error instanceof Error ? error.message : String(error)}`); this.emit('error', error); } } /** * Build initial dependency graph by scanning existing files */ async buildInitialDependencyGraph() { logger.debug('Building initial dependency graph...'); // This is a placeholder for dependency analysis // In a real implementation, this would scan all files and analyze imports/dependencies // For now, we'll just initialize the graph structure logger.debug('Initial dependency graph built'); } /** * Update dependency information for a file */ async updateDependencyInfo(filePath) { const absolutePath = path.resolve(filePath); try { // Get or create dependency info let depInfo = this.dependencyGraph.get(absolutePath); if (!depInfo) { depInfo = { dependents: new Set(), dependencies: new Set(), lastModified: Date.now() }; this.dependencyGraph.set(absolutePath, depInfo); } // Update last modified time depInfo.lastModified = Date.now(); // TODO: Analyze file content to extract dependencies // This would involve parsing imports, requires, etc. // For now, we'll just update the timestamp logger.debug(`Updated dependency info for: ${path.relative(this.options.watchDir, filePath)}`); } catch (error) { logger.error(`Failed to update dependency info for ${filePath}: ${error instanceof Error ? error.message : String(error)}`); } } /** * Remove dependency information for a deleted file */ removeDependencyInfo(filePath) { const absolutePath = path.resolve(filePath); const depInfo = this.dependencyGraph.get(absolutePath); if (!depInfo) return; // Remove this file from all its dependencies' dependents lists for (const dependency of depInfo.dependencies) { const dependencyInfo = this.dependencyGraph.get(dependency); if (dependencyInfo) { dependencyInfo.dependents.delete(absolutePath); } } // Remove this file from all its dependents' dependencies lists for (const dependent of depInfo.dependents) { const dependentInfo = this.dependencyGraph.get(dependent); if (dependentInfo) { dependentInfo.dependencies.delete(absolutePath); } } // Remove the file from the graph this.dependencyGraph.delete(absolutePath); logger.debug(`Removed dependency info for: ${path.relative(this.options.watchDir, filePath)}`); } /** * Recursively collect all dependents of a file */ collectDependents(filePath, collected) { const depInfo = this.dependencyGraph.get(filePath); if (!depInfo) return; for (const dependent of depInfo.dependents) { if (!collected.has(dependent)) { collected.add(dependent); this.collectDependents(dependent, collected); } } } } //# sourceMappingURL=file-watcher.js.map