@ordojs/cli
Version:
Command-line interface for OrdoJS framework
359 lines (358 loc) • 13.2 kB
JavaScript
/**
* @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