Skip to content

Commit

Permalink
v1.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
shuritch committed Oct 26, 2023
1 parent 0233a5c commit d523395
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 74 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ jobs:
matrix:
node:
- 18
- 19
- 20
os:
- macos-latest
- windows-latest
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

## [Unreleased][unreleased]

## [1.1.0][] - 2023-10-26

- Methods chaining
- Quality improvements
- Performance improvements
- JSDOC Enhancements
- Readme updates
- CI only for 18
- Support latest:21 node version
- Renamed astrowatch -> filesnitch

## [1.0.0][] - 2023-08-25

- Moved from Leadfisher
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<h1 align="center">Astrowatch - File system watcher</h1>
<h1 align="center">FileSnitch - File system watcher</h1>

<p align="center">
Watch specific files, directories, deeply nested directories <br/>
Expand All @@ -15,25 +15,25 @@ npm i leadwatch --save
<h2 align="center">Usage</h2>

```js
const DirectoryWatcher = require('astrowatch');
const watcher = new DirectoryWatcher({
const Snitch = require('filesnitch');
const snitch = new Snitch({
timeout: 200, // Events debouncing for queue
ignore: [new RegExp(/[\D\d]+\.ignore\D*/)], // Ignore files and directories
deep: false, // Include nested directories
home: process.cwd(), // Removes root path from emits, Warning: ignore will work on full paths
});
watcher.watch('/home/sashapop10/Downloads');
watcher.watch('/home/sashapop10/Documents');
watcher.on('before', updates => console.log({ before: updates }));
watcher.on('change', path => console.log({ changed: path }));
watcher.on('delete', path => console.log({ deleted: path }));
watcher.on('after', updates => console.log({ after: updates }));
snitch.watch('/home/sashapop10/Downloads');
snitch.watch('/home/sashapop10/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('after', updates => console.log({ after: updates }));
```

<h2 align="center">Copyright & contributors</h2>

<p align="center">
Copyright © 2023 <a href="https://github.com/astrohelm/astrowatch/graphs/contributors">Astrohelm contributors</a>.
Astrowatch is <a href="./LICENSE">MIT licensed</a>.<br/>
Astrowatch is part of <a href="https://github.com/astrohelm">Astrohelm ecosystem</a>.
Copyright © 2023 <a href="https://github.com/astrohelm/filesnitch/graphs/contributors">Astrohelm contributors</a>.
This library <a href="./LICENSE">MIT licensed</a>.<br/>
And it is part of <a href="https://github.com/astrohelm">Astrohelm ecosystem</a>.
</p>
52 changes: 24 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,40 @@
'use strict';

const fs = require('node:fs');
const { join, sep } = require('node:path');
const { access: accessible, schedular } = require('./lib');
const READ_OPTS = { withFileTypes: true };
const [fs, { join, sep }] = [require('node:fs'), require('node:path')];
const { access: accessible, scheduler } = require('./lib');
const { EventEmitter } = require('node:events');

module.exports = function (options) {
module.exports = function Watcher(options) {
const [watchers, bridge] = [new Map(), new EventEmitter()];
const emit = schedular(options?.timeout, bridge.emit.bind(bridge));
const emit = scheduler(options?.timeout, bridge.emit.bind(bridge));
const access = accessible.bind(null, options?.ignore ?? []);
const watch = (path, f) => access(f.name) && f.isDirectory() && bridge.watch(join(path, f.name));
const watchFiles = (path, files, handler = watch.bind(null, path)) => files.forEach(handler);

const setWatcher = path => {
if (watchers.has(path)) return;
const watcher = fs.watch(path, (_, filename) => {
const target = path.endsWith(sep + filename) ? path : join(path, filename);
if (!access(target)) return;
fs.stat(target, (err, stats) => {
const parsed = options?.home ? target.replace(options.home, '') : target;
if (err) return void (bridge.unwatch(target), emit('delete', parsed));
stats.isDirectory() && options?.deep && bridge.watch(target), emit('change', parsed);
return void 0;
});
const lookup = path => (err, files) =>
void (!err && files.forEach(f => f.isDirectory() && bridge.watch(join(path, f.name))));

const listener = path => (_, filename) => {
const target = path.endsWith(sep + filename) ? path : join(path, filename);
if (!access(target)) return;
fs.stat(target, (err, stats) => {
const parsed = options?.home ? target.replace(options.home, '') : target;
if (err) return void (bridge.unwatch(target), emit('delete', parsed));
stats.isDirectory() && options?.deep && fs.readdir(target, READ_OPTS, lookup(path));
return void emit('change', parsed);
});
watchers.set(path, watcher);
};

bridge.close = () => void (bridge.clear(), bridge.removeAllListeners());
bridge.clear = () => void (watchers.forEach(watcher => watcher.close()), watchers.clear());
bridge.unwatch = path => void (watchers.get(path)?.close(), watchers.delete(path));
bridge.close = () => (bridge.clear(), bridge.removeAllListeners(), bridge);
bridge.clear = () => (watchers.forEach(watcher => watcher.close()), watchers.clear(), bridge);
bridge.unwatch = path => (watchers.get(path)?.close(), watchers.delete(path), bridge);
bridge.watch = path => {
if (watchers.has(path)) return;
if (watchers.has(path) || !access(path)) return bridge;
fs.stat(path, (err, stats) => {
if (err) return;
setWatcher(path);
if (!stats.isDirectory() || !options?.deep) return;
fs.readdir(path, { withFileTypes: true }, (err, files) => !err && watchFiles(path, files));
if (err || watchers.has(path)) return;
watchers.set(path, fs.watch(path, listener(path)));
stats.isDirectory() && options?.deep && fs.readdir(path, READ_OPTS, lookup(path));
});
return bridge;
};

return bridge;
};
11 changes: 7 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

const TIMEOUT = 1_000;

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

const schedular = (timeout = TIMEOUT, emit) => {
const scheduler = (timeout = TIMEOUT, emit) => {
let timer = null;
const queue = new Map();

Expand All @@ -26,4 +29,4 @@ const schedular = (timeout = TIMEOUT, emit) => {
};
};

module.exports = { access, schedular };
module.exports = { access, scheduler };
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
{
"license": "MIT",
"version": "1.0.0",
"version": "1.1.0",
"type": "commonjs",
"name": "astrowatch",
"name": "filesnitch",
"homepage": "https://astrohelm.ru",
"description": "Astrohelm file system watcher",
"author": "Alexander Ivanov <sashapop101@gmail.com>",
"keywords": [
"node.js",
"nodejs",
"javascript",
"astrohelm",
"fs",
"watcher",
"filesystem",
"zero-dependencies"
"zero-dependencies",
"deduplication",
"snitch"
],

"main": "index.js",
"types": "types/index.d.ts",
"packageManager": "npm@9.6.4",
"readmeFilename": "README.md",
"engines": { "node": "18 || 19 || 20" },
"engines": { "node": ">= 18" },
"browser": {},
"files": ["/lib", "/types"],

Expand All @@ -31,9 +33,9 @@
"eslint:fix": "eslint --fix \"**/*.{js,ts}\""
},

"repository": { "type": "git", "url": "git+https://github.com/astrohelm/astrowatcher.git" },
"repository": { "type": "git", "url": "git+https://github.com/astrohelm/filesnitch.git" },
"bugs": {
"url": "https://github.com/astrohelm/astrowatcher/issues",
"url": "https://github.com/astrohelm/filesnitch/issues",
"email": "sashapop101@gmail.com"
},

Expand Down
87 changes: 66 additions & 21 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { EventEmitter } from 'events';
import { FSWatcher } from 'fs';

type Options = {
timeout?: number; //* Debounce timeout in milliseconds
Expand All @@ -8,34 +7,80 @@ type Options = {
home?: string; //* Root directory (process.cwd)
};

type Events = 'before' | 'after' | 'change' | 'delete';

/**
* @description
* Watch specific files, directories, deeply nested directories.
*
* Rebuild recursive when new directories found or old directories remove.
* Deduplicate events with debounce.
* @example <caption>Example of Usage</caption>
* const DirectoryWatcher = require('astrowatch');
* const watcher = new DirectoryWatcher({
*
* Deduplicate events with debounce.
* @example <caption>Example of Usage</caption>
* const Snitch = require('filesnitch');
* const snitch = new Snitch({
* timeout: 200, // Events debouncing for queue
* ignore: [new RegExp(/[\D\d]+\.ignore\.[\D\d]+/)], // Ignore files and directories
* deep: false, // Include nested directories
* home: process.cwd(), // Removes root path from emits, Warning: ignore will work on full paths
* });
* watcher.watch('/home/sashapop10/Downloads');
* watcher.watch('/home/sashapop10/Documents');
* watcher.on('before', updates => console.log({ before: updates }));
* watcher.on('change', path => console.log({ changed: path }));
* watcher.on('delete', path => console.log({ deleted: path }));
* watcher.on('after', updates => console.log({ after: updates }));
* // Some where later
* // watcher.close(); Or just watcher.clear() to remove all watching files
* snitch
* .watch('/home/sashapop10/Downloads')
* .watch('/home/sashapop10/Documents')
* .on('before', updates => console.log({ before: updates }))
* .on('change', path => console.log({ changed: path }))
* .on('delete', path => console.log({ deleted: path }))
* .on('after', updates => console.log({ after: updates }));
*/
export default class Watcher extends EventEmitter {
export = class Watcher extends EventEmitter {
constructor(options?: Options);
watch(path: string): void;
unwatch(path: string): void;
clear(): void;
close(): void;
}
/**
* @description Add listeners
* @example <caption>Possible events - change, delete, before, after</caption>
* const snitch = new Snitch();
* snitch.watch('./tests');
* snitch.on('before', package => console.log(package)); // STDOUT: [['/tests', 'change']]
* snitch.on('change', package => console.log(package)); // STDOUT: '/tests'
* snitch.on('after', package => console.log(package)); // STDOUT:[['/tests', 'change']]
*/
on(event: Events, handler: (...args: any[]) => void): EventEmitter;
/**
* @description Observe new path
* @example <caption>Allow you to watch file and directories</caption>
* const snitch = new Snitch();
* snitch.watch('./tests');
* snitch.watch('./somefile.js');
*/
watch(path: string): Watcher;
/**
* @description Remove route from observation
* @example <caption>Useful when you need to watch only a period of time</caption>
* const snitch = new Snitch();
* snitch.watch('./tests');
* snitch.on('change', path => console.log(path));
* setTimeout(() => {
* snitch.unwatch('./tests');
* }, 1000);
*/
unwatch(path: string): Watcher;
/**
* @description Removes all watchers
* @example <caption>Useful when you need to restart app</caption>
* const snitch = new Snitch();
* snitch.watch('./tests');
* snitch.on('change', path => console.log(path));
* snitch.clear();
* snitch.emit('change', 'Hello World!'); // STDOUT: Hello World !
*/
clear(): Watcher;
/**
* @description Removes all listeners and paths watchers
* @example <caption>Useful when you need to restart app</caption>
* const snitch = new Snitch();
* snitch.watch('./tests');
* snitch.on('change', path => console.log(path));
* snitch.close();
* snitch.emit('change', 'Hello World!'); // No stdout
*/
close(): Watcher;
};

0 comments on commit d523395

Please sign in to comment.