Comparing version 0.8.1 to 1.0.0-rc1
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); | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
32202
10
957
87
4
4
2
+ Addedfb-watchman@0.0.0
+ Addedfb-watchman@0.0.0(transitive)
+ Addedjson-stream@0.2.2(transitive)
+ Addednextback@0.1.0(transitive)