UNPKG

cfn-forge

Version:

CloudFormation deployment automation tool with git-based workflows

278 lines (247 loc) 6.99 kB
/** * 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;