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

sane

Package Overview
Dependencies
Maintainers
1
Versions
58
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sane - npm Package Compare versions

Comparing version 0.8.1 to 1.0.0-rc1

.jshintrc

460

index.js

@@ -1,452 +0,20 @@

var fs = require('fs');
var path = require('path');
var watch = require('watch');
var walker = require('walker');
var platform = require('os').platform();
var minimatch = require('minimatch');
var EventEmitter = require('events').EventEmitter;
'use strict';
module.exports = sane;
var NodeWatcher = require('./src/node_watcher');
var PollWatcher = require('./src/poll_watcher');
var WatchmanWatcher = require('./src/watchman_watcher');
/**
* Constants
*/
var DEFAULT_DELAY = 100;
var CHANGE_EVENT = 'change';
var DELETE_EVENT = 'delete';
var ADD_EVENT = 'add';
var ALL_EVENT = 'all';
/**
* Sugar for creating a watcher.
*
* @param {String} dir
* @param {Array<String>} glob
* @param {Object} opts
* @return {Watcher}
* @public
*/
function sane(dir, glob, opts) {
opts = opts || {};
opts.glob = glob;
return new Watcher(dir, opts);
}
/**
* Export `Watcher` class.
*/
sane.Watcher = Watcher;
/**
* Watches `dir`.
*
* @class Watcher
* @param String dir
* @param {Object} opts
* @public
*/
function Watcher(dir, opts) {
opts = opts || {};
this.persistent = opts.persistent != null
? opts.persistent
: true;
this.globs = opts.glob || [];
if (!Array.isArray(this.globs)) this.globs = [this.globs];
this.watched = Object.create(null);
this.changeTimers = Object.create(null);
this.dirRegistery = Object.create(null);
this.root = path.resolve(dir);
this.watchdir = this.watchdir.bind(this);
this.register = this.register.bind(this);
this.stopWatching = this.stopWatching.bind(this);
this.filter = this.filter.bind(this);
if (opts.poll) {
this.polling = true;
watch.createMonitor(
this.root,
{ interval: opts.interval || DEFAULT_DELAY , filter: this.filter },
this.initPoller.bind(this)
);
function sane(dir, options) {
if (options.poll) {
return new PollWatcher(dir, options);
} else if (options.watchman) {
return new WatchmanWatcher(dir, options);
} else {
this.watchdir(this.root);
recReaddir(
this.root,
this.watchdir,
this.register,
this.emit.bind(this, 'ready')
);
return new NodeWatcher(dir, options);
}
}
Watcher.prototype.__proto__ = EventEmitter.prototype;
/**
* Checks a file relative path against the globs array.
*
* @param {string} relativePath
* @return {boolean}
* @private
*/
Watcher.prototype.isFileIncluded = function(relativePath) {
var globs = this.globs;
var matched;
if (globs.length) {
for (var i = 0; i < globs.length; i++) {
if (minimatch(relativePath, globs[i])) {
matched = true;
break;
}
}
} else {
matched = true;
}
return matched;
};
/**
* Register files that matches our globs to know what to type of event to
* emit in the future.
*
* Registery looks like the following:
*
* dirRegister => Map {
* dirpath => Map {
* filename => true
* }
* }
*
* @param {string} filepath
* @return {boolean} whether or not we have registered the file.
* @private
*/
Watcher.prototype.register = function(filepath) {
var relativePath = path.relative(this.root, filepath);
if (!this.isFileIncluded(relativePath)) {
return false;
}
var dir = path.dirname(filepath);
if (!this.dirRegistery[dir]) {
this.dirRegistery[dir] = Object.create(null);
}
var filename = path.basename(filepath);
this.dirRegistery[dir][filename] = true;
return true;
};
/**
* Removes a file from the registery.
*
* @param {string} filepath
* @private
*/
Watcher.prototype.unregister = function(filepath) {
var dir = path.dirname(filepath);
if (this.dirRegistery[dir]) {
var filename = path.basename(filepath);
delete this.dirRegistery[dir][filename];
}
};
/**
* Removes a dir from the registery.
*
* @param {string} dirpath
* @private
*/
Watcher.prototype.unregisterDir = function(dirpath) {
if (this.dirRegistery[dirpath]) {
delete this.dirRegistery[dirpath];
}
};
/**
* Checks if a file or directory exists in the registery.
*
* @param {string} fullpath
* @return {boolean}
* @private
*/
Watcher.prototype.registered = function(fullpath) {
var dir = path.dirname(fullpath);
return this.dirRegistery[fullpath] ||
this.dirRegistery[dir] && this.dirRegistery[dir][path.basename(fullpath)];
};
/**
* Watch a directory.
*
* @param {string} dir
* @private
*/
Watcher.prototype.watchdir = function(dir) {
if (this.watched[dir]) return;
var watcher = fs.watch(
dir,
{ persistent: this.persistent },
this.normalizeChange.bind(this, dir)
);
this.watched[dir] = watcher;
// Workaround Windows node issue #4337.
if (platform === 'win32') {
watcher.on('error', function(error) {
if (error.code !== 'EPERM') throw error;
});
}
if (this.root !== dir) {
this.register(dir);
}
};
/**
* In polling mode stop watching files and directories, in normal mode, stop
* watching files.
*
* @param {string} filepath
* @private
*/
Watcher.prototype.stopWatching = function(filepath) {
if (this.polling) {
fs.unwatchFile(filepath);
} else if (this.watched[filepath]) {
this.watched[filepath].close();
delete this.watched[filepath];
}
};
/**
* End watching.
*
* @public
*/
Watcher.prototype.close = function() {
Object.keys(this.watched).forEach(this.stopWatching);
this.removeAllListeners();
};
/**
* On some platforms, as pointed out on the fs docs (most likely just win32)
* the file argument might be missing from the fs event. Try to detect what
* change by detecting if something was deleted or the most recent file change.
*
* @param {string} dir
* @param {string} event
* @param {string} file
* @public
*/
Watcher.prototype.detectChangedFile = function(dir, event, callback) {
if (!this.dirRegistery[dir]) {
throw new Error('Unable to find directory in registery: ' + dir);
}
var found = false;
var closest = {mtime: 0};
var c = 0;
Object.keys(this.dirRegistery[dir]).forEach(function(file, i, arr) {
fs.stat(path.join(dir, file), function(error, stat) {
if (found) return;
if (error) {
if (error.code === 'ENOENT' || (platform === 'win32' && error.code === 'EPERM')) {
found = true;
callback(file);
} else {
this.emit('error', error);
}
} else {
if (stat.mtime > closest.mtime) {
stat.file = file;
closest = stat;
}
if (arr.length === ++c) {
callback(closest.file);
}
}
}.bind(this));
}, this);
};
/**
* Normalize fs events and pass it on to be processed.
*
* @param {string} dir
* @param {string} event
* @param {string} file
* @public
*/
Watcher.prototype.normalizeChange = function(dir, event, file) {
if (!file) {
this.detectChangedFile(dir, event, function(actualFile) {
if (actualFile) {
this.processChange(dir, event, actualFile);
}
}.bind(this));
} else {
this.processChange(dir, event, path.normalize(file));
}
};
/**
* Process changes.
*
* @param {string} dir
* @param {string} event
* @param {string} file
* @public
*/
Watcher.prototype.processChange = function(dir, event, file) {
var fullPath = path.join(dir, file);
var relativePath = path.join(path.relative(this.root, dir), file);
fs.stat(fullPath, function(error, stat) {
if (error && error.code !== 'ENOENT') {
this.emit('error', error);
} else if (!error && stat.isDirectory()) {
// win32 emits usless change events on dirs.
if (event !== 'change') {
this.watchdir(fullPath);
this.emitEvent(ADD_EVENT, relativePath, stat);
}
} else {
var registered = this.registered(fullPath);
if (error && error.code === 'ENOENT') {
this.unregister(fullPath);
this.stopWatching(fullPath);
this.unregisterDir(fullPath);
if (registered) {
this.emitEvent(DELETE_EVENT, relativePath);
}
} else if (registered) {
this.emitEvent(CHANGE_EVENT, relativePath, stat);
} else {
if (this.register(fullPath)) {
this.emitEvent(ADD_EVENT, relativePath, stat);
}
}
}
}.bind(this));
};
/**
* Triggers a 'change' event after debounding it to take care of duplicate
* events on os x.
*
* @private
*/
Watcher.prototype.emitEvent = function(type, file, stat) {
var key = type + '-' + file;
clearTimeout(this.changeTimers[key]);
this.changeTimers[key] = setTimeout(function() {
delete this.changeTimers[key];
this.emit(type, file, this.root, stat);
this.emit(ALL_EVENT, type, file, this.root, stat);
}.bind(this), DEFAULT_DELAY);
};
/**
* Initiate the polling file watcher with the event emitter passed from
* `watch.watchTree`.
*
* @param {EventEmitter} monitor
* @public
*/
Watcher.prototype.initPoller = function(monitor) {
this.watched = monitor.files;
monitor.on('changed', this.pollerEmit.bind(this, CHANGE_EVENT));
monitor.on('removed', this.pollerEmit.bind(this, DELETE_EVENT));
monitor.on('created', this.pollerEmit.bind(this, ADD_EVENT));
// 1 second wait because mtime is second-based.
setTimeout(this.emit.bind(this, 'ready'), 1000);
};
/**
* Transform and emit an event comming from the poller.
*
* @param {EventEmitter} monitor
* @public
*/
Watcher.prototype.pollerEmit = function(type, file, stat) {
file = path.relative(this.root, file);
if (type === DELETE_EVENT) {
// Matching the non-polling API
stat = null;
}
this.emit(type, file, this.root, stat);
this.emit(ALL_EVENT, type, file, this.root, stat);
};
/**
* Given a fullpath of a file or directory check if we need to watch it.
*
* @param {string} filepath
* @param {object} stat
* @public
*/
Watcher.prototype.filter = function(filepath, stat) {
return stat.isDirectory() || this.isFileIncluded(
path.relative(this.root, filepath)
);
};
/**
* Traverse a directory recursively calling `callback` on every directory.
*
* @param {string} dir
* @param {function} callback
* @param {function} endCallback
* @private
*/
function recReaddir(dir, dirCallback, fileCallback, endCallback) {
walker(dir)
.on('dir', normalizeProxy(dirCallback))
.on('file', normalizeProxy(fileCallback))
.on('end', function() {
if (platform === 'win32') {
setTimeout(endCallback, 1000);
} else {
endCallback();
}
});
}
/**
* Returns a callback that when called will normalize a path and call the
* original callback
*
* @param {function} callback
* @return {function}
* @private
*/
function normalizeProxy(callback) {
return function(filepath) {
return callback(path.normalize(filepath));
}
}
module.exports = sane;
sane.NodeWatcher = NodeWatcher;
sane.PollWatcher = PollWatcher;
sane.WatchmanWatcher = WatchmanWatcher;
{
"name": "sane",
"version": "0.8.1",
"version": "1.0.0-rc1",
"description": "Sane aims to be fast, small, and reliable file system watcher.",

@@ -11,3 +11,4 @@ "main": "index.js",

"scripts": {
"test": "mocha --bail"
"prepublish": "jshint --config=.jshintrc src/ index.js && mocha --bail",
"test": "jshint --config=.jshintrc src/ index.js && mocha --bail"
},

@@ -25,4 +26,5 @@ "keywords": [

"dependencies": {
"fb-watchman": "0.0.0",
"minimatch": "~0.2.14",
"walker": "~1.0.5",
"minimatch": "~0.2.14",
"watch": "~0.10.0"

@@ -29,0 +31,0 @@ },

@@ -7,6 +7,8 @@ sane

* Always use fs.watch (unless polling is forced) and sensibly workaround the various issues with it
* Sane is all JavaScript, no native components
* Stay away from polling because it's very slow and cpu intensive
* Support polling for environments like Vagrant shared directory where there are no native filesystem events
* By default stays away from fs polling because it's very slow and cpu intensive
* Uses `fs.watch` by default and sensibly works around the various issues
* Maintains a consistent API across different platforms
* Where `fs.watch` is not reliable you have the choice of using the following alternatives:
* [the facebook watchman library](https://facebook.github.io/watchman/)
* polling

@@ -19,10 +21,18 @@ ## Install

If you're using node < v0.10.0 then make sure to start sane with `poll: true`.
## How to choose a mode
Don't worry too much about choosing the correct mode upfront because sane
maintains the same API across all modes and will be easy to switch.
* If you're only supporting Linux and OS X, `watchman` would be the most reliable mode
* If you're using node > v0.10.0 use the default mode
* If you're running OS X and you're watching a lot of directories and you're running into https://github.com/joyent/node/issues/5463, use `watchman`
* If you're in an environment where native file system events aren't available (like Vagrant), you should use polling
* Otherwise, the default mode should work well for you
## API
### sane(dir, globs, options)
### sane(dir, options)
Watches a directory and all it's descendant directorys for changes, deletions, and additions on files and directories.
Shortcut for `new sane.Watcher(dir, {glob: globs, ..options})`.
Watches a directory and all it's descendant directories for changes, deletions, and additions on files and directories.

@@ -39,20 +49,30 @@ ```js

For `options` see `sane.Watcher`.
### sane.Watcher(dir, options)
options:
* `persistent`: boolean indicating that the process shouldn't die while we're watching files.
* `glob`: a single string glob pattern or an array of them.
* `poll`: puts the watcher in polling mode. Under the hood that means `fs.watchFile`.
* `interval`: indicates how often the files should be polled. (passed to `fs.watchFile`)
* `watchman`: makes the watcher use [watchman](https://facebook.github.io/watchman/)
For the glob pattern documentation, see [minimatch](https://github.com/isaacs/minimatch).
If you choose to use `watchman` you'll have to [install watchman yourself](https://facebook.github.io/watchman/docs/install.html)).
### sane.Watcher#close
### sane.NodeWatcher(dir, options)
The default watcher class. Uses `fs.watch` under the hood, and takes the same options as `sane(options, dir)`.
### sane.WatchmanWatcher(dir, options)
The watchman watcher class. Takes the same options as `sane(options, dir)`.
### sane.PollWatcher(dir, options)
The polling watcher class. Takes the same options as `sane(options, dir)` with the addition of:
* interval: indicates how often the files should be polled. (passed to fs.watchFile)
### sane.{Node|Watchman|Poll}Watcher#close
Stops watching.
### sane.Watcher events
### sane.{Node|Watchman|Poll}Watcher events

@@ -59,0 +79,0 @@ Emits the following events:

@@ -14,10 +14,23 @@ var os = require('os');

describe('sane in polling mode', function() {
harness.call(this, true);
harness.call(this, {poll: true});
});
describe('sane in normal mode', function() {
harness.call(this, false);
harness.call(this, {});
});
describe('sane in watchman mode', function() {
harness.call(this, {watchman: true})
});
function harness(isPolling) {
if (isPolling) this.timeout(5000);
function getWatcherClass(mode) {
if (mode.watchman) {
return sane.WatchmanWatcher;
} else if (mode.poll) {
return sane.PollWatcher;
} else {
return sane.NodeWatcher;
}
}
function harness(mode) {
if (mode.poll) this.timeout(5000);
before(function() {

@@ -42,7 +55,8 @@ rimraf.sync(testdir);

beforeEach(function () {
this.watcher = new sane.Watcher(testdir, { poll: isPolling });
var Watcher = getWatcherClass(mode);
this.watcher = new Watcher(testdir);
});
afterEach(function() {
this.watcher.close();
afterEach(function(done) {
this.watcher.close(done);
});

@@ -68,3 +82,4 @@

it('emits change events for subdir files', function(done) {
var testfile = jo(testdir, 'sub_1', 'file_1');
var subdir = 'sub_1';
var testfile = jo(testdir, subdir, 'file_1');
this.watcher.on('change', function(filepath, dir) {

@@ -80,3 +95,3 @@ assert.equal(filepath, path.relative(testdir, testfile));

it('adding a file will trigger a change', function(done) {
it('adding a file will trigger an add event', function(done) {
var testfile = jo(testdir, 'file_x' + Math.floor(Math.random() * 10000));

@@ -171,3 +186,3 @@ this.watcher.on('add', function(filepath, dir, stat) {

it('adding in a new subdir will trigger an add event', function(done) {
it('adding in a subdir will trigger an add event', function(done) {
var subdir = jo(testdir, 'sub_x' + Math.floor(Math.random() * 10000));

@@ -249,10 +264,11 @@ var testfile = jo(subdir, 'file_x' + Math.floor(Math.random() * 10000));

beforeEach(function () {
this.watcher = new sane.Watcher(
var Watcher = getWatcherClass(mode);
this.watcher = new Watcher(
testdir,
{ glob: ['**/file_1', '**/file_2'], poll: isPolling }
{ glob: ['**/file_1', '**/file_2'] }
);
});
afterEach(function() {
this.watcher.close();
afterEach(function(done) {
this.watcher.close(done);
});

@@ -278,7 +294,11 @@

beforeEach(function () {
this.watcher = sane(testdir, '**/file_1');
this.watcher = sane(testdir, {
glob: '**/file_1',
poll: mode.poll,
watchman: mode.watchman
});
});
afterEach(function() {
this.watcher.close();
afterEach(function(done) {
this.watcher.close(done);
});

@@ -299,4 +319,4 @@

function defer(fn) {
setTimeout(fn, isPolling ? 1000 : 300);
setTimeout(fn, mode.poll ? 1000 : 300);
}
}
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