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 75bb60c
Show file tree
Hide file tree
Showing 2 changed files with 274 additions and 32 deletions.
88 changes: 57 additions & 31 deletions metawatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,39 @@ const { EventEmitter } = require('node:events');

const WATCH_TIMEOUT = 5000;

const isExcludedFile = (filePath, excludeExts, excludeFiles) => {
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 = (dirPath, excludePaths) => {
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.excludeExts = new Set(exts);
this.excludePaths = new Set(dirs);
this.excludeFiles = new Set(files);
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);
this.timer = null;
}
this.timer = null;
this.sendQueue();
}, this.timeout);
}
Expand All @@ -34,49 +48,61 @@ 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) => {
const target = targetPath.endsWith(path.sep + fileName);
const filePath = target ? targetPath : path.join(targetPath, fileName);
watchDirectory(dirPath) {
if (this.watchers.has(dirPath)) return;
const { excludeExts, excludeFiles } = this;
const watcher = fs.watch(dirPath);
watcher.on('error', () => void this.unwatch(dirPath));
watcher.on('change', (...args) => {
const fileName = args.pop();
const target = dirPath.endsWith(path.sep + fileName);
const filePath = target ? dirPath : path.join(dirPath, fileName);
if (isExcludedFile(filePath, excludeExts, excludeFiles)) 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);
});
});
this.watchers.set(targetPath, watcher);
this.watchers.set(dirPath, watcher);
}

watch(targetPath) {
const watcher = this.watchers.get(targetPath);
if (watcher) return;
unwatch(filePath) {
const watcher = this.watchers.get(filePath);
if (!watcher) return;
watcher.close();
this.watchers.delete(filePath);
}

watch(targetPath = process.cwd()) {
const { excludePaths } = this;
if (isExcludedDir(targetPath, excludePaths)) return this;
fs.readdir(targetPath, { withFileTypes: true }, (err, files) => {
if (err) return;
if (err) return this;
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);
return this;
});
}

unwatch(path) {
const watcher = this.watchers.get(path);
if (!watcher) return;
watcher.close();
this.watchers.delete(path);
return this;
}
}

Expand Down
218 changes: 217 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,219 @@ 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');
fs.openSync(filePath, 'a');

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', (fileName) => {
if (process.platform === 'win32') {
test.strictSame(fileName.includes('example6'), true);
} else {
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 75bb60c

Please sign in to comment.