Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filesnitch v1.6.0 #7

Merged
merged 2 commits into from
Dec 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

## [Unreleased][unreleased]

## [1.6.0][] - 2023-12-10

- Code quality improvements
- Static method watch (fp friendly)
- Delete event renamed to unlink
- More control over watcher with symbols
- Rewritten to class syntax
- Updated packages

## [1.5.0][] - 2023-12-07

- Code perfomance improvements
- Code performance improvements
- Symbolic keywords for isolation

## [1.4.0][] - 2023-12-05
Expand Down Expand Up @@ -44,7 +53,10 @@
- New Tests
- JSDoc

[unreleased]: https://github.com/astrohelm/filesnitch/compare/v1.4.0...HEAD
[unreleased]: https://github.com/astrohelm/filesnitch/compare/v1.6.0...HEAD
[1.6.0]: https://github.com/astrohelm/filesnitch/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/astrohelm/filesnitch/compare/v1.4.0...v1.5.0
[1.4.0]: https://github.com/astrohelm/filesnitch/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/astrohelm/filesnitch/compare/v1.3.0...v1.4.0
[1.3.0]: https://github.com/astrohelm/filesnitch/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/astrohelm/filesnitch/compare/v1.1.0...v1.2.0
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ const snitch = new Snitch({
deep: false, // Include nested directories
home: process.cwd(), // Removes root path from emits, Warning: ignore will work on full paths
});

snitch.watch('/home/user/Downloads').watch('/home/user/Documents');
snitch.on('before', updates => console.log({ before: updates }));
snitch.on('change', path => console.log({ changed: path }));
snitch.on('delete', path => console.log({ deleted: path }));
snitch.on('unlink', path => console.log({ deleted: path }));
snitch.on('after', updates => console.log({ after: updates }));
```

Expand Down
106 changes: 52 additions & 54 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,71 @@
'use strict';

const { access, scheduler } = require('./lib');
const { stat, readdir, watch } = require('node:fs');
const { EventEmitter } = require('node:events');
const { access, scheduler } = require('./lib');
const { join, sep } = require('node:path');
module.exports = Watcher;
module.exports.default = Watcher;

Object.setPrototypeOf(Watcher.prototype, EventEmitter.prototype);
const READ_OPTS = { withFileTypes: true };
const kWatchers = Symbol('watchers');
const kListener = Symbol('listener');
const kOptions = Symbol('options');
const kLookup = Symbol('lookup');
const kEmit = Symbol('emit');

function Watcher(options = {}) {
if (!new.target) return new Watcher(options);
const { timeout, ignore = [], home, deep } = options;
EventEmitter.call(this);
this[kWatchers] = new Map();
this[kEmit] = scheduler(this, timeout);
this[kOptions] = { timeout, ignore, home, deep };
}
module.exports.default = module.exports = class Watcher extends EventEmitter {
static watch = (path, options) => new Watcher(options).watch(path);
static symbols = { kEmit, kListener, kLookup, kOptions, kWatchers };
constructor(options = {}) {
super();
this[kWatchers] = new Map();
this[kEmit] = scheduler(this, options.timeout);
this[kOptions] = { ...options, ignore: options.ignore ?? [] };
}

Watcher.prototype.watch = function (path) {
const { [kWatchers]: watchers, [kOptions]: options } = this;
const { deep, ignore } = options;
if (watchers.has(path) || !access(ignore, path)) return this;
stat(path, (err, stats) => {
if (err || watchers.has(path)) return;
watchers.set(path, watch(path, this[kListener](path)));
stats.isDirectory() && deep && readdir(path, READ_OPTS, this[kLookup](path));
});
return this;
};
[kLookup](path) {
const lookup = f => void (f.isDirectory() && this.watch(join(path, f.name)));
return (err, files) => void (!err && files.forEach(lookup));
}

Watcher.prototype.unwatch = function (path) {
const watchers = this[kWatchers];
watchers.get(path)?.close(), watchers.delete(path);
return this;
};
[kListener](path) {
const { home, deep, ignore } = this[kOptions];
return (_, filename) => {
const target = path.endsWith(sep + filename) ? path : join(path, filename);
if (!access(ignore, target)) return;
stat(target, (err, stats) => {
const parsed = home ? target.replace(home, '') : target;
if (err) return void (this.unwatch(target), this[kEmit]('unlink', parsed));
stats.isDirectory() && deep && readdir(target, READ_OPTS, this[kLookup](path));
return void this[kEmit]('change', parsed);
});
};
}

Watcher.prototype.close = function () {
this.clear(), this.removeAllListeners();
return this;
};
watch(path) {
const { deep, ignore } = this[kOptions];
const watchers = this[kWatchers];
if (watchers.has(path) || !access(ignore, path)) return this;
stat(path, (err, stats) => {
if (err || watchers.has(path)) return;
watchers.set(path, watch(path, this[kListener](path)));
stats.isDirectory() && deep && readdir(path, READ_OPTS, this[kLookup](path));
});
return this;
}

Watcher.prototype.clear = function () {
const watchers = this[kWatchers];
watchers.forEach(watcher => void watcher.close());
return watchers.clear(), this;
};
close() {
this.clear().removeAllListeners();
return this;
}

Watcher.prototype[kLookup] = function (path) {
const lookup = file => void (file.isDirectory() && this.watch(join(path, file.name)));
return (err, files) => void (!err && files.forEach(lookup));
};
clear() {
const watchers = this[kWatchers];
watchers.forEach(watcher => void watcher.close());
return watchers.clear(), this;
}

Watcher.prototype[kListener] = function (path) {
const { home, deep, ignore } = this[kOptions];
return (_, filename) => {
const target = path.endsWith(sep + filename) ? path : join(path, filename);
if (!access(ignore, target)) return;
stat(target, (err, stats) => {
const parsed = home ? target.replace(home, '') : target;
if (err) return void (this.unwatch(target), this[kEmit]('delete', parsed));
stats.isDirectory() && deep && readdir(target, READ_OPTS, this[kLookup](path));
return void this[kEmit]('change', parsed);
});
};
unwatch(path) {
const watchers = this[kWatchers];
watchers.get(path)?.close(), watchers.delete(path);
return this;
}
};
7 changes: 1 addition & 6 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
'use strict';

const TIMEOUT = 1_000;

const access = (list, v) => {
for (var flag = true, i = 0; flag && i < list.length; flag = !new RegExp(list[i]).test(v), ++i);
return flag;
};

const access = (list, v) => !list.some(ignore => new RegExp(ignore).test(v));
const scheduler = (watcher, timeout = TIMEOUT) => {
var timer = null;
const queue = new Map();
Expand Down
52 changes: 26 additions & 26 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"license": "MIT",
"version": "1.5.0",
"version": "1.6.0",
"type": "commonjs",
"name": "filesnitch",
"homepage": "https://astrohelm.ru",
Expand All @@ -23,7 +23,6 @@
"packageManager": "npm@9.6.4",
"readmeFilename": "README.md",
"engines": { "node": ">= 18" },
"browser": {},
"files": ["/lib", "/types"],

"scripts": {
Expand All @@ -40,13 +39,13 @@
},

"devDependencies": {
"@types/node": "^20.10.1",
"eslint": "^8.54.0",
"@types/node": "^20.10.4",
"eslint": "^8.55.0",
"eslint-config-astrohelm": "^1.2.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0",
"typescript": "^5.3.2"
"typescript": "^5.3.3"
}
}
2 changes: 1 addition & 1 deletion tests/unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ test('Remove file', async () => {

await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject('timeout'), TEST_TIMEOUT);
watcher.once('delete', file => {
watcher.once('unlink', file => {
assert.strictEqual(file.endsWith(path.sep + 'file.ext'), true);
clearTimeout(timeout);
flag = true;
Expand Down
20 changes: 20 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export = class Watcher extends EventEmitter {
* snitch.on('after', package => console.log(package)); // STDOUT:[['/tests', 'change']]
*/
on(event: Events, handler: (...args: any[]) => void): EventEmitter;
/**
* @example <caption>Allow you to watch file and directories</caption>
* Snitch.watch('./tests').watch('./somefile.js');
*/
static watch(path: string, options?: Options): Watcher;
/**
* @description Observe new path
* @example <caption>Allow you to watch file and directories</caption>
Expand Down Expand Up @@ -83,4 +88,19 @@ export = class Watcher extends EventEmitter {
* snitch.emit('change', 'Hello World!'); // No stdout
*/
close(): Watcher;
/**
* @example <caption>Symbols for hidden properties</caption>
* @warning You should know what are you doing
* const snitch = new Snitch();
* const watchers = script[Snitch.symbols.kWatchers]; // Access to watchers.
* const options = script[Snitch.symbols.kOptions]; // Access to options.
* const emit = script[Snitch.symbols.kEmit]; // Access to scheduled emit function.
*/
static symbols: {
kEmit: symbol;
kListener: symbol;
kLookup: symbol;
kOptions: symbol;
kWatchers: symbol;
};
};
Loading