Skip to content

Commit

Permalink
FSEvents watcher (#97)
Browse files Browse the repository at this point in the history
* FSEvents watcher

* remove only

* fixes

* Track files and deduce change

* move tests

* nextTick

* test

* no need for prepublish now that we have CI
  • Loading branch information
amasad committed Jun 25, 2017
1 parent 5195f7b commit 803407c
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 113 deletions.
4 changes: 4 additions & 0 deletions index.js
Expand Up @@ -3,6 +3,7 @@
var NodeWatcher = require('./src/node_watcher');
var PollWatcher = require('./src/poll_watcher');
var WatchmanWatcher = require('./src/watchman_watcher');
var FSEventsWatcher = require('./src/fsevents_watcher');

function sane(dir, options) {
options = options || {};
Expand All @@ -13,6 +14,8 @@ function sane(dir, options) {
return new PollWatcher(dir, options);
} else if (options.watchman) {
return new WatchmanWatcher(dir, options);
} else if (options.fsevents) {
return new FSEventsWatcher(dir, options);
} else {
return new NodeWatcher(dir, options);
}
Expand All @@ -22,3 +25,4 @@ module.exports = sane;
sane.NodeWatcher = NodeWatcher;
sane.PollWatcher = PollWatcher;
sane.WatchmanWatcher = WatchmanWatcher;
sane.FSEventsWatcher = FSEventsWatcher;
6 changes: 4 additions & 2 deletions package.json
Expand Up @@ -12,7 +12,6 @@
"index.js"
],
"scripts": {
"prepublish": "jshint --config=.jshintrc src/ index.js && mocha --bail",
"test": "npm run format && eslint src/ test/ index.js && mocha --bail test/test.js && mocha --bail test/utils-test.js",
"test:debug": "mocha debug --bail",
"format": "prettier --trailing-comma es5 --single-quote --write index.js 'src/**/*.js' 'test/**/*.js'"
Expand Down Expand Up @@ -50,5 +49,8 @@
"bugs": {
"url": "https://github.com/amasad/sane/issues"
},
"homepage": "https://github.com/amasad/sane"
"homepage": "https://github.com/amasad/sane",
"optionalDependencies": {
"fsevents": "^1.1.1"
}
}
51 changes: 51 additions & 0 deletions src/common.js
@@ -1,7 +1,10 @@
'use strict';

var walker = require('walker');
var anymatch = require('anymatch');
var minimatch = require('minimatch');
var path = require('path');
var platform = require('os').platform();

/**
* Constants
Expand Down Expand Up @@ -69,3 +72,51 @@ exports.isFileIncluded = function(globs, dot, doIgnore, relativePath) {
}
return matched;
};

/**
* Traverse a directory recursively calling `callback` on every directory.
*
* @param {string} dir
* @param {function} dirCallback
* @param {function} fileCallback
* @param {function} endCallback
* @param {*} ignored
* @public
*/

exports.recReaddir = function(
dir,
dirCallback,
fileCallback,
endCallback,
ignored
) {
walker(dir)
.filterDir(function(currentDir) {
return !anymatch(ignored, currentDir);
})
.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, stats) {
return callback(path.normalize(filepath), stats);
};
}
113 changes: 113 additions & 0 deletions src/fsevents_watcher.js
@@ -0,0 +1,113 @@
'use strict';

var fs = require('fs');
var path = require('path');
var common = require('./common');
var EventEmitter = require('events').EventEmitter;
var fsevents = require('fsevents');

/**
* Constants
*/

var CHANGE_EVENT = common.CHANGE_EVENT;
var DELETE_EVENT = common.DELETE_EVENT;
var ADD_EVENT = common.ADD_EVENT;
var ALL_EVENT = common.ALL_EVENT;

/**
* Export `FSEventsWatcher` class.
*/

module.exports = FSEventsWatcher;

/**
* Watches `dir`.
*
* @class FSEventsWatcher
* @param String dir
* @param {Object} opts
* @public
*/

function FSEventsWatcher(dir, opts) {
common.assignOptions(this, opts);

this.root = path.resolve(dir);
this.watcher = fsevents(this.root);

this.watcher.start().on('change', this.handleEvent.bind(this));
this._tracked = Object.create(null);

common.recReaddir(
this.root,
filepath => (this._tracked[filepath] = true),
filepath => (this._tracked[filepath] = true),
this.emit.bind(this, 'ready'),
this.ignored
);
}

FSEventsWatcher.prototype.__proto__ = EventEmitter.prototype;

FSEventsWatcher.prototype.handleEvent = function(filepath) {
var relativePath = path.relative(this.root, filepath);
if (
!common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath)
) {
return;
}

fs.lstat(
filepath,
function(error, stat) {
if (error && error.code !== 'ENOENT') {
this.emit('error', error);
return;
}

if (error) {
// Ignore files that aren't tracked and don't exist.
if (!this._tracked[filepath]) {
return;
}

this._emit(DELETE_EVENT, relativePath);
delete this._tracked[filepath];
return;
}

if (this._tracked[filepath]) {
this._emit(CHANGE_EVENT, relativePath, stat);
} else {
this._tracked[filepath] = true;
this._emit(ADD_EVENT, relativePath, stat);
}
}.bind(this)
);
};

/**
* End watching.
*
* @public
*/

FSEventsWatcher.prototype.close = function(callback) {
this.watcher.stop();
this.removeAllListeners();
if (typeof callback === 'function') {
process.nextTick(callback.bind(null, null, true));
}
};

/**
* Emit events.
*
* @private
*/

FSEventsWatcher.prototype._emit = function(type, file, stat) {
this.emit(type, file, this.root, stat);
this.emit(ALL_EVENT, type, file, this.root, stat);
};
48 changes: 2 additions & 46 deletions src/node_watcher.js
Expand Up @@ -2,11 +2,9 @@

var fs = require('fs');
var path = require('path');
var walker = require('walker');
var common = require('./common');
var platform = require('os').platform();
var EventEmitter = require('events').EventEmitter;
var anymatch = require('anymatch');

/**
* Constants
Expand Down Expand Up @@ -44,7 +42,7 @@ function NodeWatcher(dir, opts) {
this.register = this.register.bind(this);

this.watchdir(this.root);
recReaddir(
common.recReaddir(
this.root,
this.watchdir,
this.register,
Expand Down Expand Up @@ -346,7 +344,7 @@ NodeWatcher.prototype.emitEvent = function(type, file, stat) {
delete this.changeTimers[key];
if (type === ADD_EVENT && stat.isDirectory()) {
// Recursively emit add events and watch for sub-files/folders
recReaddir(
common.recReaddir(
path.resolve(this.root, file),
function emitAddDir(dir, stats) {
this.watchdir(dir);
Expand Down Expand Up @@ -374,45 +372,3 @@ NodeWatcher.prototype.rawEmitEvent = function(type, file, stat) {
this.emit(type, file, this.root, stat);
this.emit(ALL_EVENT, type, file, this.root, stat);
};

/**
* Traverse a directory recursively calling `callback` on every directory.
*
* @param {string} dir
* @param {function} dirCallback
* @param {function} fileCallback
* @param {function} endCallback
* @param {*} ignored
* @private
*/

function recReaddir(dir, dirCallback, fileCallback, endCallback, ignored) {
walker(dir)
.filterDir(function(currentDir) {
return !anymatch(ignored, currentDir);
})
.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, stats) {
return callback(path.normalize(filepath), stats);
};
}

0 comments on commit 803407c

Please sign in to comment.