Comparing version 0.1.0 to 0.2.1
145
index.js
@@ -42,10 +42,16 @@ var fs = require('fs'); | ||
if (!Array.isArray(this.globs)) this.globs = [this.glob]; | ||
this.watchers = []; | ||
this.watched = Object.create(null); | ||
this.changeTimers = Object.create(null); | ||
this.dir = dir | ||
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.watchdir(dir); | ||
recReaddir(dir, this.watchdir, this.emit.bind(this, 'ready')); | ||
recReaddir( | ||
dir, | ||
this.watchdir, | ||
this.register, | ||
this.emit.bind(this, 'ready') | ||
); | ||
} | ||
@@ -56,2 +62,87 @@ | ||
/** | ||
* 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 globs = this.globs; | ||
var matched; | ||
if (globs.length) { | ||
for (var i = 0; i < globs.length; i++) { | ||
if (minimatch(filepath, globs[i])) { | ||
matched = true; | ||
break; | ||
} | ||
} | ||
} else { | ||
matched = true; | ||
} | ||
if (!matched) 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); | ||
this.dirRegistery[dir][filename] = null; | ||
} | ||
}; | ||
/** | ||
* Removes a dir from the registery. | ||
* | ||
* @param {string} dirpath | ||
* @private | ||
*/ | ||
Watcher.prototype.unregisterDir = function(dirpath) { | ||
if (this.dirRegistery[dirpath]) { | ||
this.dirRegistery[dirpath] = null; | ||
} | ||
}; | ||
/** | ||
* 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. | ||
@@ -109,21 +200,25 @@ * | ||
var fullPath = path.join(dir, file); | ||
var relativePath = path.join(path.relative(this.dir, dir), file); | ||
var relativePath = path.join(path.relative(this.root, dir), file); | ||
fs.stat(fullPath, function(error, stat) { | ||
if (error && error.code === 'ENOENT') { | ||
this.stopWatching(fullPath); | ||
return; | ||
} else if (error) { | ||
if (error && error.code !== 'ENOENT') { | ||
this.emit('error', error); | ||
return; | ||
} else if (stat.isDirectory()) { | ||
} else if (!error && stat.isDirectory()) { | ||
this.watchdir(fullPath); | ||
} else if (this.globs.length) { | ||
var globs = this.globs; | ||
for (var i = 0; i < globs.length; i++) { | ||
if (minimatch(file, globs[i])) { | ||
this.emitChange(relativePath); | ||
this.emitEvent('add', relativePath); | ||
} 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', relativePath); | ||
} | ||
} else if (registered) { | ||
this.emitEvent('change', relativePath); | ||
} else { | ||
if (this.register(fullPath)) { | ||
this.emitEvent('add', relativePath); | ||
} | ||
} | ||
} else { | ||
this.emitChange(relativePath); | ||
} | ||
@@ -140,7 +235,8 @@ }.bind(this)); | ||
Watcher.prototype.emitChange = function(file) { | ||
clearTimeout(this.changeTimers[file]); | ||
this.changeTimers[file] = setTimeout(function() { | ||
this.changeTimers[file] = null; | ||
this.emit('change', file); | ||
Watcher.prototype.emitEvent = function(type, file) { | ||
var key = type + '-' + file; | ||
clearTimeout(this.changeTimers[key]); | ||
this.changeTimers[key] = setTimeout(function() { | ||
this.changeTimers[key] = null; | ||
this.emit(type, file); | ||
}.bind(this), 100); | ||
@@ -158,6 +254,7 @@ }; | ||
function recReaddir(dir, callback, endCallback) { | ||
function recReaddir(dir, dirCallback, fileCallback, endCallback) { | ||
walker(dir) | ||
.on('dir', callback) | ||
.on('dir', dirCallback) | ||
.on('file', fileCallback) | ||
.on('end', endCallback); | ||
} |
{ | ||
"name": "sane", | ||
"version": "0.1.0", | ||
"version": "0.2.1", | ||
"description": "Sane aims to be fast, small, and reliable file system watcher. No bells and whistles, just change events.", | ||
@@ -26,3 +26,6 @@ "main": "index.js", | ||
"rimraf": "~2.2.6" | ||
}, | ||
"engines": { | ||
"node": ">=0.10.0" | ||
} | ||
} |
@@ -5,4 +5,12 @@ sane | ||
I've been driven to insanity by node filesystem watcher wrappers. | ||
Sane aims to be fast, small, and reliable file system watcher. No bells and whistles, just change events. | ||
Sane aims to be fast, small, and reliable file system watcher. | ||
## Install | ||
Requires node >= v0.10.0. | ||
``` | ||
$ npm install sane | ||
``` | ||
## API | ||
@@ -12,3 +20,4 @@ | ||
Shortcut for `new sane.Watcher(files, {globs: globs})` | ||
Watches a directory and all it's descendant directorys for changes, deletions, and additions on files and directories. | ||
Shortcut for `new sane.Watcher(files, {globs: globs})`. | ||
@@ -19,2 +28,4 @@ ```js | ||
watcher.on('change', function (filepath) { console.log('file changed', filepath); }); | ||
watcher.on('add', function (filepath) { console.log('file added', filepath); }); | ||
watcher.on('delete', function (filepath) { console.log('file deleted', filepath); }); | ||
// close | ||
@@ -35,2 +46,12 @@ watcer.close(); | ||
### sane.Watcher events | ||
Emits the following events: | ||
All events are passed the file/dir path relative to the root directory | ||
* `ready` when the program is ready to detect events in the directory | ||
* `change` when a file changes | ||
* `add` when a file or directory has been added | ||
* `delete` when a file or directory has been deleted | ||
Stops watching. | ||
@@ -37,0 +58,0 @@ |
@@ -66,3 +66,3 @@ var os = require('os'); | ||
var testfile = jo(testdir, 'file_x' + Math.floor(Math.random() * 10000)); | ||
this.watcher.on('change', function(filepath) { | ||
this.watcher.on('add', function(filepath) { | ||
assert.equal(filepath, path.relative(testdir, testfile)); | ||
@@ -76,6 +76,5 @@ done(); | ||
it('adding in a new subdir will trigger a change', function(done) { | ||
var subdir = jo(testdir, 'sub_x' + Math.floor(Math.random() * 10000)); | ||
var testfile = jo(subdir, 'file_x' + Math.floor(Math.random() * 10000)); | ||
this.watcher.on('change', function(filepath) { | ||
it('removing a file will emit delete event', function(done) { | ||
var testfile = jo(testdir, 'file_9'); | ||
this.watcher.on('delete', function(filepath) { | ||
assert.equal(filepath, path.relative(testdir, testfile)); | ||
@@ -85,3 +84,46 @@ done(); | ||
this.watcher.on('ready', function() { | ||
fs.unlink(testfile, 'wow'); | ||
}); | ||
}); | ||
it('removing a dir will emit delete event', function(done) { | ||
var subdir = jo(testdir, 'sub_9'); | ||
this.watcher.on('delete', function(filepath) { | ||
// Ignore delete events for files in the dir. | ||
if (path.dirname(filepath) === path.relative(testdir, subdir)) { | ||
return; | ||
} | ||
assert.equal(filepath, path.relative(testdir, subdir)); | ||
done(); | ||
}); | ||
this.watcher.on('ready', function() { | ||
rimraf.sync(subdir); | ||
}); | ||
}); | ||
it('adding a dir will emit an add event', function(done) { | ||
var subdir = jo(testdir, 'sub_x' + Math.floor(Math.random() * 10000)); | ||
this.watcher.on('add', function(filepath) { | ||
assert.equal(filepath, path.relative(testdir, subdir)); | ||
done(); | ||
}); | ||
this.watcher.on('ready', function() { | ||
fs.mkdirSync(subdir); | ||
}); | ||
}); | ||
it('adding in a new subdir will trigger an add event', function(done) { | ||
var subdir = jo(testdir, 'sub_x' + Math.floor(Math.random() * 10000)); | ||
var testfile = jo(subdir, 'file_x' + Math.floor(Math.random() * 10000)); | ||
var i = 0; | ||
this.watcher.on('add', function(filepath) { | ||
if (++i === 1) { | ||
assert.equal(filepath, path.relative(testdir, subdir)); | ||
} else { | ||
assert.equal(filepath, path.relative(testdir, testfile)); | ||
done(); | ||
} | ||
}); | ||
this.watcher.on('ready', function() { | ||
fs.mkdirSync(subdir); | ||
defer(function() { | ||
@@ -96,5 +138,10 @@ fs.writeFileSync(testfile, 'wow'); | ||
var testfile = jo(subdir, 'file_1'); | ||
this.watcher.on('change', function(filepath) { | ||
assert.equal(filepath, path.relative(testdir, testfile)); | ||
done(); | ||
var i = 0; | ||
this.watcher.on('add', function(filepath) { | ||
if (++i === 1) { | ||
assert.equal(filepath, path.relative(testdir, subdir)); | ||
} else { | ||
assert.equal(filepath, path.relative(testdir, testfile)); | ||
done(); | ||
} | ||
}); | ||
@@ -111,2 +158,20 @@ this.watcher.on('ready', function() { | ||
}); | ||
it('should be ok to remove and then add the same file', function(done) { | ||
var testfile = jo(testdir, 'sub_8', 'file_1'); | ||
var i = 0; | ||
this.watcher.on('add', function(filepath) { | ||
assert.equal(filepath, path.relative(testdir, testfile)); | ||
}); | ||
this.watcher.on('delete', function(filepath) { | ||
assert.equal(filepath, path.relative(testdir, testfile)); | ||
done(); | ||
}); | ||
this.watcher.on('ready', function() { | ||
fs.unlink(testfile); | ||
defer(function() { | ||
fs.writeFileSync(testfile, 'wow'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -113,0 +178,0 @@ |
13346
406
58