@nodedaemon/core
Version:
Production-ready Node.js process manager with zero external dependencies
263 lines • 9.36 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileWatcher = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const crypto_1 = require("crypto");
const events_1 = require("events");
const helpers_1 = require("../utils/helpers");
const constants_1 = require("../utils/constants");
class FileWatcher extends events_1.EventEmitter {
watchers = new Map();
watchedFiles = new Map();
debouncedHandlers = new Map();
ignorePatterns = [];
isWatching = false;
constructor() {
super();
this.setupIgnorePatterns();
}
setupIgnorePatterns() {
this.ignorePatterns = constants_1.FILE_WATCH_IGNORE.map(pattern => {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
return new RegExp(regexPattern);
});
}
watch(paths, options = {}) {
// Don't unwatch existing paths, just add new ones
// This was the bug - it was unwatching all previous paths!
const pathsArray = Array.isArray(paths) ? paths : [paths];
const { ignoreInitial = true, recursive = true, ignored = [] } = options;
if (ignored.length > 0) {
const additionalPatterns = ignored.map(pattern => {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
return new RegExp(regexPattern);
});
this.ignorePatterns.push(...additionalPatterns);
}
pathsArray.forEach(watchPath => {
this.watchPath((0, path_1.resolve)(watchPath), recursive);
});
if (!ignoreInitial) {
this.scanInitialFiles(pathsArray);
}
this.isWatching = true;
}
watchPath(path, recursive) {
try {
if (!(0, helpers_1.isDirectory)(path) && !(0, helpers_1.isFile)(path)) {
return;
}
console.log(`[FileWatcher] Watching path: ${path} (recursive: ${recursive})`);
const watcher = (0, fs_1.watch)(path, { recursive }, (eventType, filename) => {
if (!filename)
return;
const fullPath = (0, path_1.resolve)(path, filename);
console.log(`[FileWatcher] Event: ${eventType} on ${fullPath}`);
this.handleFileEvent(eventType, fullPath);
});
watcher.on('error', (error) => {
this.emit('error', error);
});
this.watchers.set(path, watcher);
}
catch (error) {
this.emit('error', new Error(`Failed to watch path ${path}: ${error}`));
}
}
handleFileEvent(eventType, filePath) {
if (this.shouldIgnore(filePath)) {
return;
}
const debouncedHandler = this.getDebouncedHandler(filePath);
debouncedHandler(eventType, filePath);
}
getDebouncedHandler(filePath) {
if (!this.debouncedHandlers.has(filePath)) {
const handler = (0, helpers_1.debounce)((eventType, path) => {
this.processFileChange(eventType, path);
}, constants_1.FILE_WATCH_DEBOUNCE);
this.debouncedHandlers.set(filePath, handler);
}
return this.debouncedHandlers.get(filePath);
}
processFileChange(eventType, filePath) {
try {
const existingFile = this.watchedFiles.get(filePath);
if (!(0, helpers_1.isFile)(filePath)) {
if (existingFile) {
this.watchedFiles.delete(filePath);
this.emitFileEvent('unlink', filePath);
}
return;
}
const stats = (0, fs_1.statSync)(filePath);
const currentHash = this.calculateFileHash(filePath);
const currentSize = stats.size;
const currentMtime = stats.mtime.getTime();
if (!existingFile) {
this.watchedFiles.set(filePath, {
path: filePath,
hash: currentHash,
size: currentSize,
mtime: currentMtime
});
this.emitFileEvent('add', filePath, stats);
}
else {
const hasChanged = existingFile.hash !== currentHash ||
existingFile.size !== currentSize ||
existingFile.mtime !== currentMtime;
if (hasChanged) {
this.watchedFiles.set(filePath, {
path: filePath,
hash: currentHash,
size: currentSize,
mtime: currentMtime
});
this.emitFileEvent('change', filePath, stats);
}
}
}
catch (error) {
this.emit('error', new Error(`Failed to process file change for ${filePath}: ${error}`));
}
}
calculateFileHash(filePath) {
try {
const content = (0, fs_1.readFileSync)(filePath);
return (0, crypto_1.createHash)('md5').update(content).digest('hex');
}
catch (error) {
return '';
}
}
shouldIgnore(filePath) {
const relativePath = (0, path_1.relative)(process.cwd(), filePath);
return this.ignorePatterns.some(pattern => pattern.test(relativePath));
}
scanInitialFiles(paths) {
paths.forEach(path => {
this.scanDirectory((0, path_1.resolve)(path));
});
}
scanDirectory(dirPath) {
try {
if (!(0, helpers_1.isDirectory)(dirPath)) {
if ((0, helpers_1.isFile)(dirPath) && !this.shouldIgnore(dirPath)) {
this.processFileChange('add', dirPath);
}
return;
}
const fs = require('fs');
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
entries.forEach((entry) => {
const entryPath = (0, path_1.join)(dirPath, entry.name);
if (this.shouldIgnore(entryPath)) {
return;
}
if (entry.isDirectory()) {
this.scanDirectory(entryPath);
}
else if (entry.isFile()) {
this.processFileChange('add', entryPath);
}
});
}
catch (error) {
this.emit('error', new Error(`Failed to scan directory ${dirPath}: ${error}`));
}
}
emitFileEvent(type, filePath, stats) {
const event = {
type,
filename: require('path').basename(filePath),
path: filePath,
stats
};
this.emit('fileChange', event);
this.emit(type, filePath, stats);
}
unwatch() {
this.watchers.forEach(watcher => {
try {
watcher.close();
}
catch (error) {
this.emit('error', error);
}
});
this.watchers.clear();
this.watchedFiles.clear();
this.debouncedHandlers.clear();
this.isWatching = false;
}
getWatchedFiles() {
return Array.from(this.watchedFiles.keys());
}
isFileWatched(filePath) {
return this.watchedFiles.has((0, path_1.resolve)(filePath));
}
getFileInfo(filePath) {
return this.watchedFiles.get((0, path_1.resolve)(filePath));
}
addIgnorePattern(pattern) {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
this.ignorePatterns.push(new RegExp(regexPattern));
}
removeIgnorePattern(pattern) {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
const regex = new RegExp(regexPattern);
this.ignorePatterns = this.ignorePatterns.filter(existingPattern => existingPattern.source !== regex.source);
}
clearIgnorePatterns() {
this.ignorePatterns = [];
this.setupIgnorePatterns();
}
getIgnorePatterns() {
return this.ignorePatterns.map(pattern => pattern.source);
}
pause() {
this.watchers.forEach(watcher => {
try {
watcher.close();
}
catch (error) {
this.emit('error', error);
}
});
this.isWatching = false;
}
resume() {
if (!this.isWatching) {
const watchedPaths = Array.from(this.watchers.keys());
this.watchers.clear();
watchedPaths.forEach(path => {
this.watchPath(path, true);
});
this.isWatching = true;
}
}
getStats() {
return {
watchedPaths: this.watchers.size,
watchedFiles: this.watchedFiles.size,
isWatching: this.isWatching,
ignorePatterns: this.ignorePatterns.length
};
}
}
exports.FileWatcher = FileWatcher;
//# sourceMappingURL=FileWatcher.js.map