cfn-forge
Version:
CloudFormation deployment automation tool with git-based workflows
278 lines (247 loc) • 6.99 kB
JavaScript
/**
* File Watcher Implementation
* Cross-platform file watching using chokidar
*/
const chokidar = require('chokidar');
const EventEmitter = require('events');
const path = require('path');
const IFileWatcher = require('../interfaces/IFileWatcher');
class FileWatcher extends IFileWatcher {
constructor(options = {}) {
super();
this.eventEmitter = new EventEmitter();
this.watcher = null;
this.isActive = false;
this.watchedPaths = [];
this.ignorePatterns = options.ignorePatterns || [
'node_modules/**',
'.git/**',
'dist/**',
'build/**',
'**/.DS_Store',
'**/Thumbs.db',
'**/desktop.ini',
'**/*.tmp',
'**/*.temp',
'**/coverage/**',
'**/.nyc_output/**',
];
this.defaultOptions = {
ignored: this.ignorePatterns,
persistent: true,
ignoreInitial: true,
followSymlinks: false,
cwd: process.cwd(),
disableGlobbing: false,
usePolling: false,
interval: 1000,
binaryInterval: 300,
awaitWriteFinish: {
stabilityThreshold: 2000,
pollInterval: 100,
},
...options,
};
}
/**
* Start watching for file changes
* @param {string|string[]} paths - Paths to watch
* @param {Object} options - Watch options
* @returns {Promise<void>}
*/
async start(paths, options = {}) {
if (this.isActive) {
await this.stop();
}
const pathsArray = Array.isArray(paths) ? paths : [paths];
this.watchedPaths = pathsArray.map((p) => path.resolve(p));
const watchOptions = {
...this.defaultOptions,
...options,
};
this.watcher = chokidar.watch(pathsArray, watchOptions);
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('File watcher failed to start within timeout'));
}, 10000);
this.watcher
.on('ready', () => {
clearTimeout(timeout);
this.isActive = true;
this.eventEmitter.emit('ready');
resolve();
})
.on('error', (error) => {
clearTimeout(timeout);
this.eventEmitter.emit('error', error);
reject(error);
})
.on('add', (filePath) => {
this.eventEmitter.emit('add', path.resolve(filePath));
})
.on('change', (filePath) => {
this.eventEmitter.emit('change', path.resolve(filePath));
})
.on('unlink', (filePath) => {
this.eventEmitter.emit('unlink', path.resolve(filePath));
})
.on('addDir', (dirPath) => {
this.eventEmitter.emit('addDir', path.resolve(dirPath));
})
.on('unlinkDir', (dirPath) => {
this.eventEmitter.emit('unlinkDir', path.resolve(dirPath));
});
});
}
/**
* Stop watching for file changes
* @returns {Promise<void>}
*/
async stop() {
if (this.watcher) {
await this.watcher.close();
this.watcher = null;
}
this.isActive = false;
this.watchedPaths = [];
this.eventEmitter.emit('stop');
}
/**
* Add a path to watch
* @param {string} path - Path to add
* @returns {Promise<void>}
*/
async addPath(pathToAdd) {
if (!this.watcher) {
throw new Error('Watcher not started');
}
const resolvedPath = path.resolve(pathToAdd);
this.watcher.add(resolvedPath);
if (!this.watchedPaths.includes(resolvedPath)) {
this.watchedPaths.push(resolvedPath);
}
}
/**
* Remove a path from watching
* @param {string} path - Path to remove
* @returns {Promise<void>}
*/
async removePath(pathToRemove) {
if (!this.watcher) {
throw new Error('Watcher not started');
}
const resolvedPath = path.resolve(pathToRemove);
this.watcher.unwatch(resolvedPath);
const index = this.watchedPaths.indexOf(resolvedPath);
if (index > -1) {
this.watchedPaths.splice(index, 1);
}
}
/**
* Register an event handler
* @param {string} event - Event name (change, add, unlink, etc.)
* @param {Function} handler - Event handler function
* @returns {void}
*/
on(event, handler) {
this.eventEmitter.on(event, handler);
}
/**
* Remove an event handler
* @param {string} event - Event name
* @param {Function} handler - Event handler function
* @returns {void}
*/
off(event, handler) {
this.eventEmitter.off(event, handler);
}
/**
* Get list of watched paths
* @returns {string[]} Array of watched paths
*/
getWatchedPaths() {
return [...this.watchedPaths];
}
/**
* Check if watcher is active
* @returns {boolean} True if watching
*/
isWatching() {
return this.isActive;
}
/**
* Set ignore patterns
* @param {string[]} patterns - Glob patterns to ignore
* @returns {void}
*/
setIgnorePatterns(patterns) {
this.ignorePatterns = patterns;
// Note: chokidar doesn't support changing ignore patterns dynamically
// Would need to restart watcher with new patterns
}
/**
* Get current ignore patterns
* @returns {string[]} Array of ignore patterns
*/
getIgnorePatterns() {
return [...this.ignorePatterns];
}
/**
* Wait for specific file change
* @param {string} filePath - File to watch for changes
* @param {number} timeout - Timeout in milliseconds
* @returns {Promise<string>} Path of changed file
*/
waitForChange(filePath = null, timeout = 30000) {
return new Promise((resolve, reject) => {
const cleanup = () => {
clearTimeout(timer);
this.off('change', changeHandler);
this.off('error', errorHandler);
};
const timer = setTimeout(() => {
cleanup();
reject(new Error('Timeout waiting for file change'));
}, timeout);
const changeHandler = (changedPath) => {
if (!filePath || changedPath === path.resolve(filePath)) {
cleanup();
resolve(changedPath);
}
};
const errorHandler = (error) => {
cleanup();
reject(error);
};
this.on('change', changeHandler);
this.on('error', errorHandler);
});
}
/**
* Get watcher statistics
* @returns {Object} Watcher statistics
*/
getStats() {
return {
isActive: this.isActive,
watchedPathsCount: this.watchedPaths.length,
watchedPaths: this.getWatchedPaths(),
ignorePatterns: this.getIgnorePatterns(),
listenerCount: this.eventEmitter.listenerCount('change'),
};
}
/**
* Create a debounced change handler
* @param {Function} handler - Handler function
* @param {number} delay - Debounce delay in milliseconds
* @returns {Function} Debounced handler
*/
createDebouncedHandler(handler, delay = 1000) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => handler(...args), delay);
};
}
}
module.exports = FileWatcher;