From 425882d1037ffa6453f1dbf1e14db19e150be184 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Wed, 30 Jan 2013 16:12:11 -0400 Subject: [PATCH 1/2] Adds Exclusion Support to Gaze. Fixes GH-7 Add test case for exclusions in Gaze. Testcase.. Fix test cases on this branch. Code Review mchoy. Ran local tests, 42 passes. Yay. Tested locally as well using my project, and it worked the way I expected it too. Yay! Minor fixes. --- Gruntfile.js | 2 +- lib/gaze.js | 108 ++++++++++++++++++++++++++++++++--------- package.json | 3 +- test/exclusion_test.js | 43 ++++++++++++++++ 4 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 test/exclusion_test.js diff --git a/Gruntfile.js b/Gruntfile.js index 0206147..a9b7d64 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -8,7 +8,7 @@ module.exports = function(grunt) { } }, nodeunit: { - files: ['test/**/*_test.js'], + files: ['test/**/*_test.js'] }, jshint: { options: { diff --git a/lib/gaze.js b/lib/gaze.js index a31b7b9..be4a777 100644 --- a/lib/gaze.js +++ b/lib/gaze.js @@ -9,6 +9,7 @@ 'use strict'; // libs +var _ = require('lodash'); var events = require('events'); var fs = require('fs'); var path = require('path'); @@ -16,7 +17,7 @@ var Glob = require('glob').Glob; var minimatch = require('minimatch'); var Gaze, gaze; -// globals +// globalsP var delay = 10; // exports @@ -86,7 +87,7 @@ function isString(obj) { function isEmpty(obj) { if (obj == null) { return true; } - + if (Array.isArray(obj) || isString(obj)) { return obj.length === 0; } for (var key in obj) { @@ -100,7 +101,7 @@ function isEmpty(obj) { function forEachSeries(arr, iterator, callback) { if (!arr.length) { return callback(); } - + var completed = 0; var iterate = function() { @@ -110,7 +111,7 @@ function forEachSeries(arr, iterator, callback) { callback = function() {}; } else { completed += 1; - + if (completed === arr.length) { callback(null); } else { @@ -155,7 +156,7 @@ function _unixifyPathSep(filepath) { } // `Gaze` EventEmitter object to return in the callback -Gaze = gaze.Gaze = __extends(function(files, opts, done) { +Gaze = gaze.Gaze = __extends(function(patterns, opts, done) { var _this = this; // If second arg is the callback @@ -197,7 +198,7 @@ Gaze = gaze.Gaze = __extends(function(files, opts, done) { } // Initialize the watch on files - this.add(files, done); + this.add(patterns, done); return this; }, events.EventEmitter); @@ -263,6 +264,9 @@ Gaze.prototype.close = function(_reset) { }); if (_reset) { _this._watched = Object.create(null); + _this._watchers = Object.create(null); + _this._patterns = []; + _this._cached = Object.create(null); setTimeout(function() { _this.emit('end'); _this.removeAllListeners(); @@ -271,27 +275,77 @@ Gaze.prototype.close = function(_reset) { return _this; }; -// Add file patterns to be watched -Gaze.prototype.add = function(files, done) { +// Processing Patterns +// "Inspired from Grunt" +Gaze.prototype.processPatterns = function(patterns, processFn, done) { var _this = this; - if (typeof files === 'string') { - files = [files]; - } - this._patterns = union(this._patterns, files); - forEachSeries(files, function(pattern, next) { - if (isEmpty(pattern)) { return; } - _this._glob = new Glob(pattern, _this.options, function(err, files) { + var result = []; + + var patterns = _.flatten(patterns); + + forEachSeries(patterns, function(pattern, next) { + processFn(pattern, function(err, matches) { if (err) { _this.emit('error', err); return done(err); } - _this._addToWatched(files); + var exclusion = pattern.indexOf('!') === 0; + if (exclusion) { pattern = pattern.slice(1); } + if (exclusion) { + //There has to be a better way to handle exclusion. + //Here we add the matched ! onto the result array, then diff them off + //If we didn't concat matches on first, we could end up adding our "excluded" + result = result.concat(matches); + result = _.difference(result, matches); + } else { + result = _.union(result, matches); + } next(); }); + + }, function() { + _this._addToWatched(result) + return done(result); + }); +} + +Gaze.prototype._negationsLast = function(patterns) { + var result = []; + patterns.forEach(function(pattern) { + if (pattern.indexOf('!') === 0) { + result.push(pattern); + } else { + result.unshift(pattern); + } + }) + return result; +} + +// Add file patterns to be watched +Gaze.prototype.add = function(patterns, done) { + var _this = this; + if (typeof patterns === 'string') { + patterns = [patterns]; + } + //if we don't want to mash our patterns with individual files that already matched + //an existing pattern, pass null to this function. + if (patterns != null) { + this._patterns = union(this._patterns, patterns); + this._patterns = this._negationsLast(this._patterns); + } + + _this.processPatterns(this._patterns, function(pattern, done) { + if (_.isEmpty(pattern)) {return;} + _this._glob = new Glob(pattern, _this.options, function(err, files) { + if (err) { + done(err); + } + done(null, files); + }) }, function() { _this.close(false); _this._initWatched(done); - }); + }) }; // Remove file/dir from `watched` @@ -388,11 +442,20 @@ Gaze.prototype._addToWatched = function(files) { // Returns true if the file matches this._patterns Gaze.prototype._isMatch = function(file) { var matched = false; - this._patterns.forEach(function(pattern) { - if (matched || (matched = minimatch(file, pattern)) ) { - return false; + + //using for to allow early exit on first negation match. + //backwards because this will allow early exits if our negation match + for (var i = this._patterns.length - 1; i >= 0; i--) { + if (minimatch(file, this._patterns[i])) { + if (this._patterns[i].indexOf('!') === 0) { + //First time a negation is hit, we can hop out of the forEach. + matched = false; + continue; + } else { + matched = true; + } } - }); + }; return matched; }; @@ -434,6 +497,7 @@ Gaze.prototype._initWatched = function(done) { var curWatched = Object.keys(_this._watched); forEachSeries(curWatched, function(dir, next) { var files = _this._watched[dir]; + // Triggered when a watched dir has an event _this._watchDir(dir, function(event, dirpath) { var relDir = cwd === dir ? '.' : path.relative(cwd, dir); @@ -477,7 +541,7 @@ Gaze.prototype._initWatched = function(done) { var relFile = path.join(relDir, file); if (_this._isMatch(relFile)) { // Add to watch then emit event - _this.add(relFile, function() { + _this.add(null, function() { _this.emit('added', path.join(dir, file)); }); } diff --git a/package.json b/package.json index c7fa377..32d38de 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ }, "dependencies": { "glob": "~3.1.14", - "minimatch": "~0.2.9" + "minimatch": "~0.2.9", + "lodash": "~1.0.0-rc.3" }, "devDependencies": { "grunt": "~0.4.0rc6", diff --git a/test/exclusion_test.js b/test/exclusion_test.js new file mode 100644 index 0000000..bf3b32d --- /dev/null +++ b/test/exclusion_test.js @@ -0,0 +1,43 @@ +var Gaze = require('../lib/gaze.js').Gaze; +var path = require('path'); +var fs = require('fs'); + +exports.exclusion = { + setUp: function(done) { + process.chdir(path.resolve(__dirname, 'fixtures')); + done(); + }, + tearDown: function(done) { + fs.unlinkSync(path.resolve(__dirname, 'fixtures', 'two.txt')); + done(); + }, + exclusionTest: function(test) { + var expected = { + 'Project (LO)/': ['one.js'], + '.': ['one.js'], + 'sub/': ['one.js', 'two.js'] + }; + test.expect(2); + var gaze = new Gaze(['**/*.js', '!nested/**/*.js'], function(err, watcher) { + test.deepEqual(this.relative(null, true), expected); + this.on('all', function(status, filepath) { + test.equal('one.js', path.basename(filepath)); + watcher.close(); + test.done(); + }); + //Write a file that shouldn't ever match a pattern, but is in the same directory as one that does. + console.log('Write TXT'); + fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'two.txt'), 'Will I be watched?'); + + //Write a file that is part of the exclusion. + console.log('Write Excluded File'); + fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'nested', 'sub', 'two.js'), 'var two = true;'); + + //Give time for watcher to respond and see if it responds to either file that shouldn't be watched. + setTimeout(function() { + console.log('WATCH ME!'); + fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;'); + }, 2000); + }) + } +} \ No newline at end of file From 6b8f0bd1207f3bb4fbc2036de528168b88569bd2 Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Fri, 1 Feb 2013 15:43:05 -0400 Subject: [PATCH 2/2] Removed test case from this commit, fixed JSHint warnings. --- lib/gaze.js | 16 ++++++++-------- test/exclusion_test.js | 43 ------------------------------------------ 2 files changed, 8 insertions(+), 51 deletions(-) delete mode 100644 test/exclusion_test.js diff --git a/lib/gaze.js b/lib/gaze.js index be4a777..4a65807 100644 --- a/lib/gaze.js +++ b/lib/gaze.js @@ -281,7 +281,7 @@ Gaze.prototype.processPatterns = function(patterns, processFn, done) { var _this = this; var result = []; - var patterns = _.flatten(patterns); + patterns = _.flatten(patterns); forEachSeries(patterns, function(pattern, next) { processFn(pattern, function(err, matches) { @@ -304,10 +304,10 @@ Gaze.prototype.processPatterns = function(patterns, processFn, done) { }); }, function() { - _this._addToWatched(result) + _this._addToWatched(result); return done(result); }); -} +}; Gaze.prototype._negationsLast = function(patterns) { var result = []; @@ -317,9 +317,9 @@ Gaze.prototype._negationsLast = function(patterns) { } else { result.unshift(pattern); } - }) + }); return result; -} +}; // Add file patterns to be watched Gaze.prototype.add = function(patterns, done) { @@ -341,11 +341,11 @@ Gaze.prototype.add = function(patterns, done) { done(err); } done(null, files); - }) + }); }, function() { _this.close(false); _this._initWatched(done); - }) + }); }; // Remove file/dir from `watched` @@ -455,7 +455,7 @@ Gaze.prototype._isMatch = function(file) { matched = true; } } - }; + } return matched; }; diff --git a/test/exclusion_test.js b/test/exclusion_test.js deleted file mode 100644 index bf3b32d..0000000 --- a/test/exclusion_test.js +++ /dev/null @@ -1,43 +0,0 @@ -var Gaze = require('../lib/gaze.js').Gaze; -var path = require('path'); -var fs = require('fs'); - -exports.exclusion = { - setUp: function(done) { - process.chdir(path.resolve(__dirname, 'fixtures')); - done(); - }, - tearDown: function(done) { - fs.unlinkSync(path.resolve(__dirname, 'fixtures', 'two.txt')); - done(); - }, - exclusionTest: function(test) { - var expected = { - 'Project (LO)/': ['one.js'], - '.': ['one.js'], - 'sub/': ['one.js', 'two.js'] - }; - test.expect(2); - var gaze = new Gaze(['**/*.js', '!nested/**/*.js'], function(err, watcher) { - test.deepEqual(this.relative(null, true), expected); - this.on('all', function(status, filepath) { - test.equal('one.js', path.basename(filepath)); - watcher.close(); - test.done(); - }); - //Write a file that shouldn't ever match a pattern, but is in the same directory as one that does. - console.log('Write TXT'); - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'two.txt'), 'Will I be watched?'); - - //Write a file that is part of the exclusion. - console.log('Write Excluded File'); - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'nested', 'sub', 'two.js'), 'var two = true;'); - - //Give time for watcher to respond and see if it responds to either file that shouldn't be watched. - setTimeout(function() { - console.log('WATCH ME!'); - fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;'); - }, 2000); - }) - } -} \ No newline at end of file