From 161db86cd51e456f462f81b69556cbebc4078b2b Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Sun, 10 Dec 2023 00:25:01 +0300 Subject: [PATCH 1/2] v1.6.0 --- CHANGELOG.md | 15 +++++++++-- README.md | 3 ++- index.js | 66 +++++++++++++++++++++++----------------------- lib/index.js | 7 +---- package-lock.json | 52 ++++++++++++++++++------------------ package.json | 11 ++++---- tests/unit.test.js | 2 +- types/index.d.ts | 20 ++++++++++++++ 8 files changed, 101 insertions(+), 75 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f113b1..e071a31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,17 @@ ## [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 +- Updated packages + ## [1.5.0][] - 2023-12-07 -- Code perfomance improvements +- Code performance improvements - Symbolic keywords for isolation ## [1.4.0][] - 2023-12-05 @@ -44,7 +52,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 diff --git a/README.md b/README.md index 70d5046..5b092f0 100644 --- a/README.md +++ b/README.md @@ -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 })); ``` diff --git a/index.js b/index.js index bf64b94..57b3b3e 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,9 @@ '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'); @@ -17,16 +13,34 @@ 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 }; + this[kEmit] = scheduler(this, options.timeout); + this[kOptions] = { ...options, ignore: options.ignore ?? [] }; } +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)); +}; + +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]('unlink', parsed)); + stats.isDirectory() && deep && readdir(target, READ_OPTS, this[kLookup](path)); + return void this[kEmit]('change', parsed); + }); + }; +}; + Watcher.prototype.watch = function (path) { - const { [kWatchers]: watchers, [kOptions]: options } = this; - const { deep, ignore } = options; + 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; @@ -36,14 +50,8 @@ Watcher.prototype.watch = function (path) { return this; }; -Watcher.prototype.unwatch = function (path) { - const watchers = this[kWatchers]; - watchers.get(path)?.close(), watchers.delete(path); - return this; -}; - Watcher.prototype.close = function () { - this.clear(), this.removeAllListeners(); + this.clear().removeAllListeners(); return this; }; @@ -53,21 +61,13 @@ Watcher.prototype.clear = function () { return watchers.clear(), 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)); +Watcher.prototype.unwatch = function (path) { + const watchers = this[kWatchers]; + watchers.get(path)?.close(), watchers.delete(path); + return 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); - }); - }; -}; +Watcher.watch = (path, options) => new Watcher(options).watch(path); +Watcher.symbols = { kEmit, kListener, kLookup, kOptions, kWatchers }; +Object.setPrototypeOf(Watcher.prototype, EventEmitter.prototype); +module.exports.default = module.exports = Watcher; diff --git a/lib/index.js b/lib/index.js index db1d58d..520fc25 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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(); diff --git a/package-lock.json b/package-lock.json index fe8ac2c..25b3a14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "filesnitch", - "version": "1.2.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "filesnitch", - "version": "1.2.0", + "version": "1.6.0", "license": "MIT", "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" }, "engines": { "node": ">= 18" @@ -56,9 +56,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -79,9 +79,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -182,9 +182,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.1.tgz", - "integrity": "sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==", + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -742,15 +742,15 @@ } }, "node_modules/eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -810,9 +810,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -2865,9 +2865,9 @@ } }, "node_modules/typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 14c2889..4a6daa4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "license": "MIT", - "version": "1.5.0", + "version": "1.6.0", "type": "commonjs", "name": "filesnitch", "homepage": "https://astrohelm.ru", @@ -23,7 +23,6 @@ "packageManager": "npm@9.6.4", "readmeFilename": "README.md", "engines": { "node": ">= 18" }, - "browser": {}, "files": ["/lib", "/types"], "scripts": { @@ -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" } } diff --git a/tests/unit.test.js b/tests/unit.test.js index e198abe..61a2edf 100644 --- a/tests/unit.test.js +++ b/tests/unit.test.js @@ -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; diff --git a/types/index.d.ts b/types/index.d.ts index 80de73e..b1fcea3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -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 Allow you to watch file and directories + * Snitch.watch('./tests').watch('./somefile.js'); + */ + static watch(path: string, options?: Options): Watcher; /** * @description Observe new path * @example Allow you to watch file and directories @@ -83,4 +88,19 @@ export = class Watcher extends EventEmitter { * snitch.emit('change', 'Hello World!'); // No stdout */ close(): Watcher; + /** + * @example Symbols for hidden properties + * @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; + }; }; From e18fbeb995e93b6ab8497e3c367c5f3731bedd6e Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Sun, 10 Dec 2023 00:39:37 +0300 Subject: [PATCH 2/2] v1.6.0 --- CHANGELOG.md | 1 + index.js | 104 +++++++++++++++++++++++++-------------------------- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e071a31..1f2894d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - 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 diff --git a/index.js b/index.js index 57b3b3e..ec8c4da 100644 --- a/index.js +++ b/index.js @@ -11,63 +11,61 @@ const kOptions = Symbol('options'); const kLookup = Symbol('lookup'); const kEmit = Symbol('emit'); -function Watcher(options = {}) { - if (!new.target) return new Watcher(options); - EventEmitter.call(this); - this[kWatchers] = new Map(); - this[kEmit] = scheduler(this, options.timeout); - this[kOptions] = { ...options, ignore: options.ignore ?? [] }; -} +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[kLookup] = function (path) { - const lookup = file => void (file.isDirectory() && this.watch(join(path, file.name))); - return (err, files) => void (!err && files.forEach(lookup)); -}; + [kLookup](path) { + const lookup = f => void (f.isDirectory() && this.watch(join(path, f.name))); + return (err, files) => void (!err && files.forEach(lookup)); + } -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]('unlink', parsed)); - stats.isDirectory() && deep && readdir(target, READ_OPTS, this[kLookup](path)); - return void this[kEmit]('change', parsed); - }); - }; -}; + [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.watch = function (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; -}; + 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.close = function () { - this.clear().removeAllListeners(); - return this; -}; + close() { + this.clear().removeAllListeners(); + return this; + } -Watcher.prototype.clear = function () { - const watchers = this[kWatchers]; - watchers.forEach(watcher => void watcher.close()); - return watchers.clear(), this; -}; + clear() { + const watchers = this[kWatchers]; + watchers.forEach(watcher => void watcher.close()); + return watchers.clear(), this; + } -Watcher.prototype.unwatch = function (path) { - const watchers = this[kWatchers]; - watchers.get(path)?.close(), watchers.delete(path); - return this; + unwatch(path) { + const watchers = this[kWatchers]; + watchers.get(path)?.close(), watchers.delete(path); + return this; + } }; - -Watcher.watch = (path, options) => new Watcher(options).watch(path); -Watcher.symbols = { kEmit, kListener, kLookup, kOptions, kWatchers }; -Object.setPrototypeOf(Watcher.prototype, EventEmitter.prototype); -module.exports.default = module.exports = Watcher;