Skip to content

Commit

Permalink
Add exclude files, exts, paths and add new events
Browse files Browse the repository at this point in the history
  • Loading branch information
timursevimli committed Jan 24, 2024
1 parent 177be38 commit 7898e9b
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 15 deletions.
52 changes: 38 additions & 14 deletions metawatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,36 @@ const { EventEmitter } = require('node:events');

const WATCH_TIMEOUT = 5000;

const isExcludedFile = (excludeExts, excludeFiles) => (filePath) => {
const { ext, base, name } = path.parse(filePath);
const extIsExclude = excludeExts.has(ext.slice(1));
if (extIsExclude) return true;
return excludeFiles.has(name) || excludeFiles.has(base);
};

const isExcludedDir = (excludePaths) => (dirPath) => {
const dirName = path.basename(dirPath);
return excludePaths.has(dirName) || excludePaths.has(dirPath);
};

class DirectoryWatcher extends EventEmitter {
constructor(options = {}) {
super();
this.watchers = new Map();
const { dirs = [], files = [], exts = [] } = options.excludes || {};
const { timeout = WATCH_TIMEOUT } = options;
this.timeout = timeout;
this.watchers = new Map();
this.isExcludedFile = isExcludedFile(new Set(exts), new Set(files));
this.isExcludedDir = isExcludedDir(new Set(dirs));
this.timer = null;
this.queue = new Map();
}

post(event, filePath) {
if (this.timer) clearTimeout(this.timer);
this.queue.set(filePath, event);
if (this.timeout === 0) return void this.sendQueue();
const events = this.queue.get(filePath);
if (events) events.add(event);
else this.queue.set(filePath, new Set(event));
this.timer = setTimeout(() => {
if (this.timer) {
clearTimeout(this.timer);
Expand All @@ -34,21 +50,31 @@ class DirectoryWatcher extends EventEmitter {
const queue = [...this.queue.entries()];
this.queue.clear();
this.emit('before', queue);
for (const [filePath, event] of queue) {
this.emit(event, filePath);
for (const [filePath, events] of queue) {
for (const event of events) {
this.emit(event, filePath);
}
}
this.emit('after', queue);
}

watchDirectory(targetPath) {
if (this.watchers.get(targetPath)) return;
const watcher = fs.watch(targetPath, (event, fileName) => {
if (this.watchers.has(targetPath)) return;
const watcher = fs.watch(targetPath);
watcher.on('error', () => void this.unwatch(targetPath));
watcher.on('change', (...args) => {
const fileName = args.pop();
const target = targetPath.endsWith(path.sep + fileName);
const filePath = target ? targetPath : path.join(targetPath, fileName);
if (this.isExcludedFile(filePath)) return;
this.post('*', filePath);
fs.stat(filePath, (err, stats) => {
if (err) {
const keys = [...this.watchers.keys()];
this.unwatch(filePath);
return void this.post('delete', filePath);
this.post('delete', filePath);
const event = keys.includes(filePath) ? 'rmdir' : 'rm';
return void this.post(event, fileName);
}
if (stats.isDirectory()) this.watch(filePath);
this.post('change', filePath);
Expand All @@ -58,15 +84,13 @@ class DirectoryWatcher extends EventEmitter {
}

watch(targetPath) {
const watcher = this.watchers.get(targetPath);
if (watcher) return;
if (this.isExcludedDir(targetPath)) return;
fs.readdir(targetPath, { withFileTypes: true }, (err, files) => {
if (err) return;
for (const file of files) {
if (file.isDirectory()) {
const dirPath = path.join(targetPath, file.name);
this.watch(dirPath);
}
if (!file.isDirectory()) continue;
const dirPath = path.join(targetPath, file.name);
this.watch(dirPath);
}
this.watchDirectory(targetPath);
});
Expand Down
215 changes: 214 additions & 1 deletion test/unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const cleanup = (dir) => {
});
};

metatests.test('Single file change ', (test) => {
metatests.test('Single file change', (test) => {
const targetPath = path.join(dir, 'test/example1');
fs.mkdirSync(targetPath);

Expand Down Expand Up @@ -91,3 +91,216 @@ metatests.test('Aggregated change', (test) => {
}
}, WRITE_TIMEOUT);
});

metatests.test('Exclude extensions', (test) => {
const targetPath = path.join(dir, 'test/example3');
fs.mkdirSync(targetPath);

const files = ['test.md', 'test.ts', 'test.ext'];

const options = {
excludes: {
exts: ['md', 'ts'],
},
...OPTIONS,
};

const watcher = new metawatch.DirectoryWatcher(options);
watcher.watch(targetPath);

const timeout = setTimeout(() => {
watcher.unwatch(targetPath);
test.fail();
}, TEST_TIMEOUT);

let changeCount = 0;

watcher.on('change', (fileName) => {
const { ext } = path.parse(fileName);
test.strictSame(ext, '.ext');
changeCount++;
});

watcher.on('after', (changes) => {
test.strictEqual(changeCount, 1);
test.strictEqual(changes.length, 1);
clearTimeout(timeout);
watcher.unwatch(targetPath);
cleanup(targetPath);
test.end();
});

setTimeout(() => {
for (const name of files) {
const filePath = path.join(targetPath, name);
fs.writeFile(filePath, 'example', 'utf8', (err) => {
test.error(err, 'Can not write file');
});
}
}, WRITE_TIMEOUT);
});

metatests.test('Exclude files', (test) => {
const targetPath = path.join(dir, 'test/example4');
fs.mkdirSync(targetPath);

const files = ['test1.ext', 'test2.ext', 'test3.ext'];

const options = {
excludes: {
files: ['test2', 'test3'],
},
...OPTIONS,
};

const watcher = new metawatch.DirectoryWatcher(options);
watcher.watch(targetPath);

const timeout = setTimeout(() => {
watcher.unwatch(targetPath);
test.fail();
}, TEST_TIMEOUT);

let changeCount = 0;

watcher.on('change', (fileName) => {
const { name } = path.parse(fileName);
test.strictSame(name, 'test1');
changeCount++;
});

watcher.on('after', (changes) => {
test.strictEqual(changeCount, 1);
test.strictEqual(changes.length, 1);
clearTimeout(timeout);
watcher.unwatch(targetPath);
cleanup(targetPath);
test.end();
});

setTimeout(() => {
for (const name of files) {
const filePath = path.join(targetPath, name);
fs.writeFile(filePath, 'example', 'utf8', (err) => {
test.error(err, 'Can not write file');
});
}
}, WRITE_TIMEOUT);
});

metatests.test('Exclude dirs', (test) => {
const targetPath = path.join(dir, 'test/example5');
fs.mkdirSync(targetPath);

const options = {
excludes: {
dirs: [targetPath],
},
...OPTIONS,
};

const watcher = new metawatch.DirectoryWatcher(options);
watcher.watch(targetPath);

let changeEmitted = false;

setTimeout(() => {
cleanup(targetPath);
test.strictEqual(changeEmitted, false);
test.end();
}, TEST_TIMEOUT);

watcher.on('change', () => {
changeEmitted = true;
});

setTimeout(() => {
const filePath = path.join(targetPath, 'test.ext');
fs.writeFile(filePath, 'example', 'utf8', (err) => {
test.error(err, 'Can not write file');
});
}, WRITE_TIMEOUT);
});

metatests.test('Delete file (rm)', (test) => {
const targetPath = path.join(dir, 'test/example6');
fs.mkdirSync(targetPath);

const filePath = path.join(targetPath, 'test.ext');
const fd = fs.openSync(filePath, 'a');
fs.closeSync(fd);

const watcher = new metawatch.DirectoryWatcher(OPTIONS);
watcher.watch(targetPath);

const timeout = setTimeout(() => {
watcher.unwatch(targetPath);
test.fail();
}, TEST_TIMEOUT);

watcher.on('delete', (fileName) => {
test.strictSame(fileName.endsWith('test.ext'), true);
});

watcher.on('rm', (fileName) => {
test.strictSame(fileName.endsWith('test.ext'), true);
});

watcher.on('rmdir', () => {
test.fail();
});

watcher.on('after', () => {
clearTimeout(timeout);
watcher.unwatch(targetPath);
cleanup(targetPath);
test.end();
});

setTimeout(() => {
fs.unlink(filePath, (err) => {
test.error(err, 'Can not delete file');
});
}, WRITE_TIMEOUT);
});

metatests.test('Delete dir (rmdir)', (test) => {
const targetPath = path.join(dir, 'test/example7');
fs.mkdirSync(targetPath);

const secondPath = path.join(targetPath, 'test');
fs.mkdirSync(secondPath);

const watcher = new metawatch.DirectoryWatcher(OPTIONS);
watcher.watch(targetPath);

const timeout = setTimeout(() => {
watcher.unwatch(targetPath);
test.fail();
}, TEST_TIMEOUT);

watcher.on('delete', (fileName) => {
test.strictSame(fileName.endsWith('test'), true);
});

watcher.on('rmdir', (fileName) => {
test.strictSame(fileName, 'example7/test');
});

watcher.on('rm', (fileName) => {
test.strictSame(fileName, 'test');
});

watcher.on('after', () => {
clearTimeout(timeout);
watcher.unwatch(targetPath);
cleanup(targetPath);
test.end();
});

setTimeout(() => {
fs.rmdir(secondPath, (err) => {
test.error(err, 'Can not delete file');
});
}, WRITE_TIMEOUT);
});

0 comments on commit 7898e9b

Please sign in to comment.