Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

watchboy

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

watchboy - npm Package Compare versions

Comparing version 0.0.3 to 0.0.4

229

index.js
const path = require('path');
const EventEmitter = require('events');
const fs = require('fs');
const globby = require('globby');
const diff = require('lodash.difference');
const through = require('through2');
const unixify = require('unixify');
const dirGlob = require('dir-glob');
const micromatch = require('micromatch');
const pify = require('pify');
const readdir = (dir, pattern) => {
return globby(pattern, {
cwd: dir,
deep: 1,
onlyFiles: false,
markDirectories: true
const pReaddir = pify(fs.readdir);
const pStat = pify(fs.stat);
const readdir = async (dir) => {
dir = dir.slice(-1) === '/' ? dir : `${dir}/`;
let result;
if (fs.Dirent) {
result = await pReaddir(dir, { withFileTypes: true });
} else {
const list = await pReaddir(dir);
result = [];
for (let name of list) {
result.push(Object.assign(await pStat(`${dir}${name}`), { name }));
}
}
return result.map(dirent => {
if (dirent.isDirectory()) {
return `${dir}${dirent.name}/`;
} else {
return `${dir}${dirent.name}`;
}
});
};
const exists = (abspath) => {
return new Promise(r => fs.access(abspath, err => r(!err)));
const isMatch = (input, patterns) => {
let failed = false;
for (let p of patterns) {
failed = failed || !micromatch.isMatch(input, p);
}
return !failed;
};
const iterateStream = (stream, iterate) => {
return new Promise((resolve, reject) => {
stream.on('error', err => reject(err));
const isParent = (input, patterns) => {
for (let p of patterns) {
if (p.indexOf(input) === 0) {
return true;
}
}
stream.pipe(through.obj((data, enc, cb) => {
iterate(data).then(() => {
cb();
}).catch(err => {
cb(err);
});
}))
.on('data', () => {})
.on('end', () => resolve())
.on('error', err => reject(err));
});
return false;
};
const globdir = async (dir, patterns) => {
const run = async () => {
const entries = await readdir(dir);
const matches = entries.filter((e) => isMatch(e, patterns) || isParent(e, patterns));
return matches;
};
if (fs.Dirent) {
return await run();
}
// node 8 has this nasty habbit of returning 0 entries on a readdir
// directly after a change even when there are entries, so we need
// to confirm that two runs read the same amount of entries
const one = await run();
const two = await run();
if (one.length === two.length) {
return two;
}
return globdir(dir, patterns);
};
const exists = (abspath) => {
return new Promise(r => fs.access(abspath, err => r(!err)));
};
const evMap = {

@@ -46,2 +95,7 @@ change: 1,

const addDriveLetter = (basePath, str) => {
const drive = (basePath.match(/^([a-z]:)\\/i) || [])[1];
return drive ? `${drive}${str}` : str;
};
module.exports = (pattern, {

@@ -51,2 +105,18 @@ cwd = process.cwd(),

} = {}) => {
// support passing relative paths and '.'
cwd = path.resolve(cwd);
const resolvedPatterns = (Array.isArray(pattern) ? pattern : [pattern]).map(str => {
const negative = str[0] === '!';
if (negative) {
str = str.slice(1);
}
const absPattern = addDriveLetter(cwd, path.posix.resolve(unixify(cwd), str));
return negative ? `!${absPattern}` : absPattern;
});
let absolutePatterns;
const events = new EventEmitter();

@@ -63,9 +133,5 @@ const dirs = {};

const funcKey = `func : ${abspath}`;
if (pending[funcKey]) {
clearTimeout(pending[funcKey]);
} else {
// save only the first set of arguments
pending[abspath] = { evname, evarg, priority: evMap[evname] || 0 };
if (!pending[abspath]) {
// save the first set of arguments
pending[abspath] = { evname, evarg, priority: evMap[evname] || 0, timer: null };
}

@@ -75,11 +141,41 @@

// this event takes precedence over the queued one
pending[abspath] = { evname, evarg, priority: evMap[evname] || 0 };
pending[abspath].evname = evname;
pending[abspath].evarg = evarg;
pending[abspath].priority = evMap[evname] || 0;
}
pending[funcKey] = setTimeout(() => {
if (pending[abspath].timer) {
clearTimeout(pending[abspath].timer);
}
pending[abspath].timer = setTimeout(() => {
if (closed) {
delete pending[abspath];
return;
}
const { evname, evarg } = pending[abspath];
events.emit(evname, evarg);
delete pending[abspath];
delete pending[funcKey];
if (evname !== 'change') {
delete pending[abspath];
return void events.emit(evname, evarg);
}
// always check that this file exists on a change event due to a bug
// in node 12 that fires a delete as a change instead of rename
// https://github.com/nodejs/node/issues/27869
exists(abspath).then(yes => {
if (closed) {
delete pending[abspath];
return;
}
// it is possible file could have been deleted during the check
const { evname, evarg } = pending[abspath];
delete pending[abspath];
events.emit(yes ? evname : 'unlink', evarg);
}).catch(err => {
error(err, abspath);
});
}, 50);

@@ -93,3 +189,3 @@ };

err.path = abspath;
err.path = path.resolve(abspath);

@@ -105,3 +201,3 @@ events.emit('error', err);

delete files[abspath];
throttle(abspath, 'unlink', { path: abspath });
throttle(abspath, 'unlink', { path: path.resolve(abspath) });
}

@@ -116,12 +212,8 @@ };

delete dirs[abspath];
throttle(abspath, 'unlinkDir', { path: abspath });
throttle(abspath, 'unlinkDir', { path: path.resolve(abspath) });
}
};
const onFileChange = (abspath) => (type) => {
if (type === 'rename') {
return removeFile(abspath);
}
throttle(abspath, 'change', { path: abspath });
const onFileChange = (abspath) => () => {
throttle(abspath, 'change', { path: path.resolve(abspath) });
};

@@ -131,8 +223,8 @@

try {
const paths = await readdir(abspath, pattern);
const paths = await globdir(abspath, absolutePatterns);
const [foundFiles, foundDirs] = paths.reduce(([files, dirs], file) => {
if (/\/$/.test(file)) {
dirs.push(path.resolve(abspath, file));
dirs.push(file.slice(0, -1));
} else {
files.push(path.resolve(abspath, file));
files.push(file);
}

@@ -145,4 +237,3 @@

const existingFiles = Object.keys(files)
.filter(file => path.dirname(file) === abspath)
.filter(file => !dirs[file]);
.filter(file => path.posix.dirname(file) === abspath);
// diff returns items in the first array that are not in the second

@@ -154,9 +245,9 @@ diff(existingFiles, foundFiles).forEach(file => removeFile(file));

const existingDirs = Object.keys(dirs)
.filter(dir => path.dirname(dir) === abspath)
.filter(dir => !files[dir]);
.filter(dir => path.posix.dirname(dir) === abspath);
diff(existingDirs, foundDirs).forEach(dir => removeDir(dir));
diff(foundDirs, existingDirs).forEach(dir => {
watchDir(dir);
});
for (let dir of diff(foundDirs, existingDirs)) {
await watchDir(dir);
}
} catch (err) {

@@ -187,3 +278,3 @@ try {

events.emit('add', { path: abspath });
events.emit('add', { path: path.resolve(abspath) });
};

@@ -204,22 +295,18 @@

return onDirChange(abspath)().then(() => {
events.emit('addDir', { path: abspath });
events.emit('addDir', { path: path.resolve(abspath) });
});
};
iterateStream(globby.stream(pattern, {
onlyFiles: false,
markDirectories: true,
cwd,
concurrency: 1
}), async (file) => {
const abspath = path.resolve(cwd, file);
if (/\/$/.test(file)) {
await watchDir(abspath);
} else {
await watchFile(abspath);
}
dirGlob(resolvedPatterns, { cwd }).then((p) => {
absolutePatterns = p;
}).then(() => {
return watchDir(cwd);
const dir = addDriveLetter(cwd, unixify(cwd));
return watchDir(dir);
}).then(() => {
// this is the most annoying part, but it seems that watching does not
// occur immediately, yet there is no event for whenan fs watcher is
// actually ready... some of the internal bits use process.nextTick,
// so we'll wait a very random sad small amount of time here
return new Promise(r => setTimeout(() => r(), 20));
}).then(() => {
events.emit('ready');

@@ -226,0 +313,0 @@ }).catch(err => {

{
"name": "watchboy",
"version": "0.0.3",
"version": "0.0.4",
"description": "watchboy",

@@ -21,5 +21,7 @@ "main": "index.js",

"dependencies": {
"globby": "^10.0.1",
"dir-glob": "^3.0.1",
"lodash.difference": "^4.5.0",
"through2": "^3.0.1"
"micromatch": "^4.0.2",
"pify": "^4.0.1",
"unixify": "^1.0.0"
},

@@ -26,0 +28,0 @@ "devDependencies": {

# watchboy
Watch files and directories for changes. Fast. No hassle. No native dependencies. Works the same way on Windows, Linux, and MacOS. Low memory usage. Everything you've ever wanted in a module.
[![watchboy logo](https://cdn.jsdelivr.net/gh/catdad-experiments/catdad-experiments-org@7005ab/watchboy/logo.jpg)](https://github.com/catdad/watchboy/)
[![travis][travis.svg]][travis.link]
[![npm-downloads][npm-downloads.svg]][npm.link]
[![npm-version][npm-version.svg]][npm.link]
[![dm-david][dm-david.svg]][dm-david.link]
[travis.svg]: https://travis-ci.com/catdad/watchboy.svg?branch=master
[travis.link]: https://travis-ci.com/catdad/watchboy
[npm-downloads.svg]: https://img.shields.io/npm/dm/watchboy.svg
[npm.link]: https://www.npmjs.com/package/watchboy
[npm-version.svg]: https://img.shields.io/npm/v/watchboy.svg
[dm-david.svg]: https://david-dm.org/catdad/watchboy.svg
[dm-david.link]: https://david-dm.org/catdad/watchboy
Watch files and directories for changes. Fast. No hassle. No native dependencies. Works the same way on Windows, Linux, and MacOS. Low memory usage. Shows you a picture of a dog. It's everything you've ever wanted in a module!
## Install

@@ -26,3 +41,3 @@

watcher.on('ready', () => console.log('all initial files and directories found'));
watcher.on('errpr', err => console.error('watcher error:', err));
watcher.on('error', err => console.error('watcher error:', err));

@@ -36,3 +51,3 @@ // stop all watching

### `watchboy(pattern, [options])` → `EventEmitter`
### `watchboy(pattern, [options])` → [`EventEmitter`]

@@ -47,27 +62,27 @@ Watchboy is exposed as a function which returns an event emitter. It takes the following parameters:

### `.on('add', ({ path }) => {})` → `EventEmitter`
### `.on('add', ({ path }) => {})` → [`EventEmitter`]
Indicates that a new file was added. There is a single argument for this event, which has a `path` property containing the absolute path for the file that was added.
### `.on('addDir', ({ path }) => {})` → `EventEmitter`
### `.on('addDir', ({ path }) => {})` → [`EventEmitter`]
Indicates that a new directory was added. There is a single argument for this event, which has a `path` property containing the absolute path for the directory that was added. Files in this new directory will also be watched according to the provided patterns.
### `.on('change', ({ path }) => {})` → `EventEmitter`
### `.on('change', ({ path }) => {})` → [`EventEmitter`]
Indicates that a file has changed. There is a single argument for this event, which has a `path` property containing the absolute path for the file that has changed.
### `.on('unlink', ({ path }) => {})` → `EventEmitter`
### `.on('unlink', ({ path }) => {})` → [`EventEmitter`]
Indicates that a watched file no longer exists. There is a single argument for this event, which has a `path` property containing the absolute path for the file that no longer exists.
### `.on('unlinkDir', ({ path }) => {})` → `EventEmitter`
### `.on('unlinkDir', ({ path }) => {})` → [`EventEmitter`]
Indicates that a watched directory no longer exists. There is a single argument for this event, which has a `path` property containing the absolute path for the directory that no longer exists.
### `.on('ready', () => {})` → `EventEmitter`
### `.on('ready', () => {})` → [`EventEmitter`]
Indicates that all initial files and directories have been discovered. This even has no arguments. Note that new `add` and `addDir` events may fire after this, as new files and directories that match the patterns are created.
### `.on('error', (err) => {})` → `EventEmitter`
### `.on('error', (err) => {})` → [`EventEmitter`]

@@ -79,1 +94,7 @@ Indicates that an error has occurred. You must handle this event so that your application does not crash. This error has a single argument: an error which indicates what happened. Aside from standard error properties, there is an additional `path` property indicating the absolute path of the file or directory which triggered the error.

Stop watching all files. After this method is called, the watcher can no longer be used and no more events will fire.
[`EventEmitter`]: https://nodejs.org/api/events.html#events_class_eventemitter
## Performance
Check out [this benchmark](https://github.com/catdad-experiments/filewatch-benchmarks) comparing `watchboy` to popular alternatives. Spoiler: it fairs really well.

@@ -62,1 +62,4 @@ /* eslint-disable no-console */

// * new files in new subdirectories are watched
// cd coverage
// node ..\test\harness.js "**/*" "!lcov-report"

@@ -9,4 +9,27 @@ /* eslint-env mocha */

const watchboy = require(root);
const log = (...args) => {
if (process.env.TEST_DEBUG) {
// eslint-disable-next-line no-console
console.log(...args);
}
};
const watchboy = (() => {
const lib = require(root);
return (...args) => {
const watcher = lib(...args);
watcher.on('add', ({ path }) => log('add:', path));
watcher.on('addDir', ({ path }) => log('addDir:', path));
watcher.on('change', ({ path }) => log('change:', path));
watcher.on('unlink', ({ path }) => log('unlink:', path));
watcher.on('unlinkDir', ({ path }) => log('unlinkDir:', path));
watcher.on('ready', () => log('ready'));
watcher.on('error', err => log('watcher error:', err.message));
return watcher;
};
})();
describe('watchboy', () => {

@@ -26,3 +49,3 @@ const temp = path.resolve(root, 'temp');

file('pineapples/six.txt'),
].map(f => fs.outputFile(f, Math.random().toString(36))));
].map(f => fs.outputFile(f, '')));
});

@@ -125,3 +148,3 @@ afterEach(async () => {

}),
fs.outputFile(testFile, Math.random().toString(36))
fs.outputFile(testFile, '')
]);

@@ -150,3 +173,3 @@

it('emits an "add" and "addDir" when a new file is added to a new directory in an already watched directory', async () => {
const testFile = file('kiwi/seven.txt');
const testFile = file('pineapple/wedges/seven.txt');

@@ -164,3 +187,3 @@ await new Promise(r => {

}),
fs.outputFile(testFile, Math.random().toString(36))
fs.outputFile(testFile, '')
]);

@@ -181,2 +204,48 @@

it('watches a nested pattern', async () => {
await new Promise(r => {
watcher = watchboy('pineapples/**/*', { cwd: temp, persistent: false }).on('ready', () => r());
});
const actualAddedFile = file('pineapples/seven.txt');
const [addedFile] = await Promise.all([
new Promise(r => {
watcher.once('add', ({ path }) => r(path));
}),
fs.outputFile(actualAddedFile, '')
]);
expect(addedFile).to.equal(actualAddedFile);
const actualChangedFile = file('pineapples/six.txt');
const [changedFile] = await Promise.all([
new Promise(r => {
watcher.once('change', ({ path }) => r(path));
}),
touch(actualChangedFile)
]);
expect(changedFile).to.equal(actualChangedFile);
const actualAddedDir = file('pineapples/slices');
const [addedDir] = await Promise.all([
new Promise(r => {
watcher.once('addDir', ({ path }) => r(path));
}),
fs.ensureDir(actualAddedDir)
]);
expect(addedDir).to.equal(actualAddedDir);
const actualNestedFile = file('pineapples/slices/eight.txt');
const [nestedFile] = await Promise.all([
new Promise(r => {
watcher.once('add', ({ path }) => r(path));
}),
fs.outputFile(actualNestedFile, '')
]);
expect(nestedFile).to.equal(actualNestedFile);
});
describe('close', () => {

@@ -183,0 +252,0 @@ it('stops all listeners');

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc