UNPKG

als-watcher

Version:

Watch directory changes with events, file types filters and depth control.

112 lines (95 loc) 3.78 kB
const { watch, statSync, stat } = require('fs'); const { join } = require('path'); const { fileListSync } = require('als-file-list'); const EventEmitter = require('events'); class DirectoryWatcher { constructor(directoryPath, extensions = [], depth = Infinity) { this.directoryPath = directoryPath; this.extensions = extensions; this.depth = depth; this.emitter = new EventEmitter(); this.filesMap = new Map(); this.recentlyDeleted = null; this.lastFilename = null; } init() { const files = fileListSync(this.directoryPath); files.forEach(file => { const filePath = join(this.directoryPath, file); try {this.filesMap.set(file, statSync(filePath).mtimeMs);} catch (error) {this.emitError(`Failed to get stats for file: ${filePath}`, error)} }); } startWatching() { try { watch(this.directoryPath, { recursive: true }, (eventType, filename) => { if (!this.isValidFile(filename)) return; this.lastFilename = filename; const filePath = join(this.directoryPath, filename); stat(filePath, (err, stats) => { if (err) { if (err.code === 'ENOENT') this.handleUnlink(filename); else this.emitError(`Error accessing file: ${filePath}`, err); return; } if (!stats.isFile()) return; if (!this.filesMap.has(filename)) this.handleAddOrRename(filename, stats); else this.handleChange(filename, stats); }); }); console.log(`Started watching directory: ${this.directoryPath}`); } catch (error) { this.emitError(`Failed to watch directory: ${this.directoryPath}`, error); } } isValidFile(filename) { return filename && (this.extensions.length === 0 || this.extensions.some(ext => filename.endsWith(`.${ext}`))) && ((filename.match(/\/|\\/g) || []).length <= this.depth); } handleAddOrRename(filename, stats) { if (this.recentlyDeleted) { this.emitRename(this.recentlyDeleted, filename); this.recentlyDeleted = null; } else this.emitAdd(filename); this.filesMap.set(filename, stats.mtimeMs); } handleChange(filename, stats) { if (stats.mtimeMs > this.filesMap.get(filename)) { this.emitChange(filename); this.filesMap.set(filename, stats.mtimeMs); } } handleUnlink(filename) { if (!this.filesMap.has(filename)) return; this.recentlyDeleted = filename; this.filesMap.delete(filename); this.emitUnlink(filename); } emitAdd(filename) { this.emitter.emit('add', filename); this.emitter.emit('operation', 'add', filename); } emitChange(filename) { this.emitter.emit('change', filename); this.emitter.emit('operation', 'change', filename); } emitUnlink(filename) { this.emitter.emit('unlink', filename); this.emitter.emit('operation', 'unlink', filename); } emitRename(oldFilename, newFilename) { this.emitter.emit('rename', oldFilename, newFilename); this.emitter.emit('operation', 'rename', oldFilename, newFilename); } emitError(message, error) { error.details = { msg: message, lastFilename: this.lastFilename }; this.emitter.emit('error', error); } } function watchDirectory(directoryPath, extensions = [], depth = Infinity) { const watcher = new DirectoryWatcher(directoryPath, extensions, depth); watcher.init(); watcher.startWatching(); return watcher; } module.exports = watchDirectory;