├── .gitignore ├── test ├── fixtures │ ├── one.js │ ├── sub │ │ ├── one.js │ │ └── two.js │ ├── nested │ │ ├── one.js │ │ ├── sub │ │ │ └── two.js │ │ ├── sub2 │ │ │ └── two.js │ │ └── three.js │ └── Project (LO) │ │ └── one.js ├── file_poller.js ├── helper.js ├── relative_test.js ├── rename_test.js ├── patterns_test.js ├── safewrite_test.js ├── api_test.js ├── add_test.js ├── watch_race_test.js ├── matching_test.js └── watch_test.js ├── .travis.yml ├── .jshintrc ├── .editorconfig ├── appveyor.yml ├── AUTHORS ├── LICENSE-MIT ├── Gruntfile.js ├── package.json ├── benchmarks ├── gaze100s.js └── startup.js ├── lib ├── helper.js └── gaze.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /test/fixtures/one.js: -------------------------------------------------------------------------------- 1 | var test = true; 2 | -------------------------------------------------------------------------------- /test/fixtures/sub/one.js: -------------------------------------------------------------------------------- 1 | var one = true; -------------------------------------------------------------------------------- /test/fixtures/sub/two.js: -------------------------------------------------------------------------------- 1 | var two = true; -------------------------------------------------------------------------------- /test/fixtures/nested/one.js: -------------------------------------------------------------------------------- 1 | var one = true; 2 | -------------------------------------------------------------------------------- /test/fixtures/Project (LO)/one.js: -------------------------------------------------------------------------------- 1 | var one = true; 2 | -------------------------------------------------------------------------------- /test/fixtures/nested/sub/two.js: -------------------------------------------------------------------------------- 1 | var two = true; 2 | -------------------------------------------------------------------------------- /test/fixtures/nested/sub2/two.js: -------------------------------------------------------------------------------- 1 | var two = true; 2 | -------------------------------------------------------------------------------- /test/fixtures/nested/three.js: -------------------------------------------------------------------------------- 1 | var three = true; 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - '4.0' 6 | - '4.1' 7 | - '4.2' 8 | before_script: 9 | - npm install -g grunt-cli 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: "0.10" 4 | - nodejs_version: "0.12" 5 | - nodejs_version: "4" 6 | platform: 7 | - x86 8 | - x64 9 | install: 10 | - ps: Install-Product node $env:nodejs_version 11 | - npm install 12 | test_script: 13 | - node --version 14 | - npm --version 15 | - npm test 16 | build: off 17 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Kyle Robinson Young (http://dontkry.com) 2 | Sam Day (http://sam.is-super-awesome.com) 3 | Roarke Gaskill (http://starkinvestments.com) 4 | Lance Pollard (http://lancepollard.com/) 5 | Daniel Fagnan (http://hydrocodedesign.com/) 6 | Jonas (http://jpommerening.github.io/) 7 | Chris Chua (http://sirh.cc/) 8 | Kael Zhang (http://kael.me) 9 | Krasimir Tsonev (http://krasimirtsonev.com/blog) 10 | brett-shwom 11 | -------------------------------------------------------------------------------- /test/file_poller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | 6 | var timeout = +process.argv[2]; 7 | if (!timeout || isNaN(timeout)) { 8 | throw 'No specified timeout'; 9 | } 10 | setTimeout(function () { 11 | process.exit(); 12 | }, timeout); 13 | 14 | var pathArg = process.argv.slice(3); 15 | if (!pathArg.length) { 16 | throw 'No path arguments'; 17 | } 18 | var filepath = path.resolve.apply(path, [ __dirname ].concat(pathArg)); 19 | 20 | function writeToFile () { 21 | setTimeout(function () { 22 | fs.writeFile(filepath, ''); 23 | return writeToFile(); 24 | }, 0); 25 | } 26 | 27 | writeToFile(); 28 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var helper = module.exports = {}; 4 | 5 | helper.sortobj = function sortobj (obj) { 6 | if (Array.isArray(obj)) { 7 | obj.sort(); 8 | return obj; 9 | } 10 | var out = Object.create(null); 11 | var keys = Object.keys(obj); 12 | keys.sort(); 13 | keys.forEach(function (key) { 14 | var val = obj[key]; 15 | if (Array.isArray(val)) { 16 | val.sort(); 17 | } 18 | out[key] = val; 19 | }); 20 | return out; 21 | }; 22 | 23 | // The sorting is different on Windows, we are 24 | // ignoring the sort order for now 25 | helper.deepEqual = function deepEqual (test, a, b, message) { 26 | a.sort(); 27 | b.sort(); 28 | test.deepEqual(a, b, message); 29 | }; -------------------------------------------------------------------------------- /test/relative_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Gaze = require('../lib/gaze.js').Gaze; 4 | var helper = require('./helper.js'); 5 | var path = require('path'); 6 | 7 | exports.relative = { 8 | setUp: function (done) { 9 | process.chdir(path.resolve(__dirname, 'fixtures')); 10 | done(); 11 | }, 12 | relative: function (test) { 13 | test.expect(1); 14 | var files = [ 15 | 'Project (LO)/', 16 | 'Project (LO)/one.js', 17 | 'nested/', 18 | 'nested/one.js', 19 | 'nested/three.js', 20 | 'nested/sub/', 21 | 'nested/sub/two.js', 22 | 'one.js' 23 | ]; 24 | var gaze = new Gaze('addnothingtowatch'); 25 | gaze._addToWatched(files); 26 | helper.deepEqual(test, gaze.relative('.', true), ['Project (LO)/', 'nested/', 'one.js', 'sub/']); 27 | gaze.on('end', test.done); 28 | gaze.close(); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Kyle Robinson Young 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 'use strict'; 3 | grunt.option('stack', true); 4 | grunt.initConfig({ 5 | benchmark: { 6 | all: { 7 | src: ['benchmarks/*.js'], 8 | options: { times: 10 } 9 | } 10 | }, 11 | nodeunit: { 12 | files: ['test/*_test.js'] 13 | }, 14 | jshint: { 15 | options: { 16 | jshintrc: '.jshintrc' 17 | }, 18 | gruntfile: { 19 | src: 'Gruntfile.js' 20 | }, 21 | lib: { 22 | src: ['lib/**/*.js'] 23 | }, 24 | test: { 25 | src: ['test/**/*_test.js'] 26 | } 27 | } 28 | }); 29 | 30 | // Dynamic alias task to nodeunit. Run individual tests with: grunt test:events 31 | grunt.registerTask('test', function (file) { 32 | grunt.config('nodeunit.files', String(grunt.config('nodeunit.files')).replace('*', file || '*')); 33 | grunt.task.run('nodeunit'); 34 | }); 35 | 36 | grunt.loadNpmTasks('grunt-benchmark'); 37 | grunt.loadNpmTasks('grunt-contrib-jshint'); 38 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 39 | grunt.registerTask('default', ['jshint', 'nodeunit']); 40 | }; 41 | -------------------------------------------------------------------------------- /test/rename_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gaze = require('../lib/gaze.js'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | 7 | // Clean up helper to call in setUp and tearDown 8 | function cleanUp (done) { 9 | [ 10 | 'sub/rename.js', 11 | 'sub/renamed.js' 12 | ].forEach(function (d) { 13 | var p = path.resolve(__dirname, 'fixtures', d); 14 | if (fs.existsSync(p)) { fs.unlinkSync(p); } 15 | }); 16 | done(); 17 | } 18 | 19 | exports.watch = { 20 | setUp: function (done) { 21 | process.chdir(path.resolve(__dirname, 'fixtures')); 22 | cleanUp(done); 23 | }, 24 | tearDown: cleanUp, 25 | rename: function (test) { 26 | test.expect(2); 27 | var oldPath = path.join(__dirname, 'fixtures', 'sub', 'rename.js'); 28 | var newPath = path.join(__dirname, 'fixtures', 'sub', 'renamed.js'); 29 | fs.writeFileSync(oldPath, 'var rename = true;'); 30 | gaze('**/*', function (err, watcher) { 31 | watcher.on('renamed', function (newFile, oldFile) { 32 | test.equal(newFile, newPath); 33 | test.equal(oldFile, oldPath); 34 | watcher.on('end', test.done); 35 | watcher.close(); 36 | }); 37 | fs.renameSync(oldPath, newPath); 38 | }); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gaze", 3 | "description": "A globbing fs.watch wrapper built from the best parts of other fine watch libs.", 4 | "version": "0.5.2", 5 | "homepage": "https://github.com/shama/gaze", 6 | "author": { 7 | "name": "Kyle Robinson Young", 8 | "email": "kyle@dontkry.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/shama/gaze.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/shama/gaze/issues" 16 | }, 17 | "license": "MIT", 18 | "main": "lib/gaze", 19 | "engines": { 20 | "node": ">= 0.10.0" 21 | }, 22 | "scripts": { 23 | "test": "semistandard && grunt nodeunit -v" 24 | }, 25 | "dependencies": { 26 | "globule": "^0.2.0" 27 | }, 28 | "devDependencies": { 29 | "async": "^1.5.2", 30 | "grunt": "^0.4.5", 31 | "grunt-benchmark": "~0.2.0", 32 | "grunt-cli": "~0.1.13", 33 | "grunt-contrib-jshint": "^0.11.3", 34 | "grunt-contrib-nodeunit": "^0.4.1", 35 | "rimraf": "^2.5.0", 36 | "semistandard": "^7.0.5" 37 | }, 38 | "keywords": [ 39 | "watch", 40 | "glob" 41 | ], 42 | "files": [ 43 | "lib", 44 | "LICENSE-MIT" 45 | ], 46 | "semistandard": { 47 | "ignore": [ 48 | "benchmarks", 49 | "experiments", 50 | "build", 51 | "test" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/patterns_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gaze = require('../lib/gaze.js'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | 7 | // Clean up helper to call in setUp and tearDown 8 | function cleanUp (done) { 9 | [ 10 | 'added.js', 11 | 'nested/added.js', 12 | ].forEach(function (d) { 13 | var p = path.resolve(__dirname, 'fixtures', d); 14 | if (fs.existsSync(p)) { fs.unlinkSync(p); } 15 | }); 16 | done(); 17 | } 18 | 19 | exports.patterns = { 20 | setUp: function (done) { 21 | process.chdir(path.resolve(__dirname, 'fixtures')); 22 | cleanUp(done); 23 | }, 24 | tearDown: cleanUp, 25 | negate: function (test) { 26 | test.expect(1); 27 | gaze(['**/*.js', '!nested/**/*.js'], function (err, watcher) { 28 | watcher.on('added', function (filepath) { 29 | var expected = path.relative(process.cwd(), filepath); 30 | test.equal(path.join('added.js'), expected); 31 | watcher.close(); 32 | }); 33 | // dont add 34 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'nested', 'added.js'), 'var added = true;'); 35 | setTimeout(function () { 36 | // should add 37 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'added.js'), 'var added = true;'); 38 | }, 1000); 39 | watcher.on('end', test.done); 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /benchmarks/gaze100s.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gaze = require('../lib/gaze'); 4 | var grunt = require('grunt'); 5 | var path = require('path'); 6 | 7 | // Folder to watch 8 | var watchDir = path.resolve(__dirname, 'watch'); 9 | 10 | // Helper for creating mock files 11 | function createFiles (num, dir) { 12 | for (var i = 0; i < num; i++) { 13 | grunt.file.write(path.join(dir, 'test-' + i + '.js'), 'var test = ' + i + ';'); 14 | } 15 | } 16 | 17 | module.exports = { 18 | 'setUp': function (done) { 19 | // ensure that your `ulimit -n` is higher than amount of files 20 | if (grunt.file.exists(watchDir)) { 21 | grunt.file.delete(watchDir, {force: true}); 22 | } 23 | createFiles(100, path.join(watchDir, 'one')); 24 | createFiles(100, path.join(watchDir, 'two')); 25 | createFiles(100, path.join(watchDir, 'three')); 26 | createFiles(100, path.join(watchDir, 'three', 'four')); 27 | createFiles(100, path.join(watchDir, 'three', 'four', 'five', 'six')); 28 | process.chdir(watchDir); 29 | done(); 30 | }, 31 | 'tearDown': function (done) { 32 | if (grunt.file.exists(watchDir)) { 33 | grunt.file.delete(watchDir, {force: true}); 34 | } 35 | done(); 36 | }, 37 | changed: function (done) { 38 | gaze('**/*', {maxListeners: 0}, function (err, watcher) { 39 | this.on('changed', done); 40 | setTimeout(function () { 41 | var rand = String(new Date().getTime()).replace(/[^\w]+/g, ''); 42 | grunt.file.write(path.join(watchDir, 'one', 'test-99.js'), 'var test = "' + rand + '"'); 43 | }, 100); 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /test/safewrite_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gaze = require('../lib/gaze.js'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | 7 | // Clean up helper to call in setUp and tearDown 8 | function cleanUp (done) { 9 | [ 10 | 'safewrite.js' 11 | ].forEach(function (d) { 12 | var p = path.resolve(__dirname, 'fixtures', d); 13 | if (fs.existsSync(p)) { fs.unlinkSync(p); } 14 | }); 15 | done(); 16 | } 17 | 18 | exports.safewrite = { 19 | setUp: function (done) { 20 | process.chdir(path.resolve(__dirname, 'fixtures')); 21 | cleanUp(done); 22 | }, 23 | tearDown: cleanUp, 24 | safewrite: function (test) { 25 | test.expect(4); 26 | 27 | var times = 0; 28 | var file = path.resolve(__dirname, 'fixtures', 'safewrite.js'); 29 | var backup = path.resolve(__dirname, 'fixtures', 'safewrite.ext~'); 30 | fs.writeFileSync(file, 'var safe = true;'); 31 | 32 | function simSafewrite () { 33 | fs.writeFileSync(backup, fs.readFileSync(file)); 34 | fs.unlinkSync(file); 35 | fs.renameSync(backup, file); 36 | times++; 37 | } 38 | 39 | gaze('**/*', function () { 40 | this.on('all', function (action, filepath) { 41 | test.equal(action, 'changed'); 42 | test.equal(path.basename(filepath), 'safewrite.js'); 43 | 44 | if (times < 2) { 45 | setTimeout(simSafewrite, 1000); 46 | } else { 47 | this.on('end', test.done); 48 | this.close(); 49 | } 50 | }); 51 | 52 | setTimeout(function () { 53 | simSafewrite(); 54 | }, 1000); 55 | 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /benchmarks/startup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gaze = require('../lib/gaze'); 4 | var async = require('async'); 5 | var fs = require('fs'); 6 | var rimraf = require('rimraf'); 7 | var path = require('path'); 8 | 9 | // Folder to watch 10 | var watchDir = path.resolve(__dirname, 'watch'); 11 | var multiplesOf = 100; 12 | var max = 2000; 13 | var numFiles = []; 14 | for (var i = 0; i <= max / multiplesOf; i++) { 15 | numFiles.push(i * multiplesOf); 16 | } 17 | 18 | var modFile = path.join(watchDir, 'test-' + numFiles + '.txt'); 19 | 20 | // Helper for creating mock files 21 | function createFiles (num, dir) { 22 | for (var i = 0; i <= num; i++) { 23 | fs.writeFileSync(path.join(dir, 'test-' + i + '.txt'), String(i)); 24 | } 25 | } 26 | 27 | function teardown () { 28 | if (fs.existsSync(watchDir)) { 29 | rimraf.sync(watchDir); 30 | } 31 | } 32 | 33 | function setup (num) { 34 | teardown(); 35 | fs.mkdirSync(watchDir); 36 | createFiles(num, watchDir); 37 | } 38 | 39 | function measureStart (cb) { 40 | var start = Date.now(); 41 | var blocked, ready, watcher; 42 | // workaround #77 43 | var check = function () { 44 | if (ready && blocked) { 45 | cb(ready, blocked, watcher); 46 | } 47 | }; 48 | gaze(watchDir + '/**/*', {maxListeners: 0}, function (err) { 49 | ready = Date.now() - start; 50 | watcher = this; 51 | check(); 52 | }); 53 | blocked = Date.now() - start; 54 | check(); 55 | } 56 | 57 | function bench (num, cb) { 58 | setup(num); 59 | measureStart(function (time, blocked, watcher) { 60 | console.log(num, time); 61 | watcher.close(); 62 | cb(); 63 | }); 64 | } 65 | 66 | console.log('numFiles startTime'); 67 | async.eachSeries(numFiles, bench, function () { 68 | teardown(); 69 | console.log('done!'); 70 | process.exit(); 71 | }); 72 | -------------------------------------------------------------------------------- /test/api_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gaze = require('../lib/gaze.js'); 4 | var helper = require('./helper.js'); 5 | var path = require('path'); 6 | 7 | exports.api = { 8 | setUp: function (done) { 9 | process.chdir(path.resolve(__dirname, 'fixtures')); 10 | done(); 11 | }, 12 | newGaze: function (test) { 13 | test.expect(2); 14 | new gaze.Gaze('**/*', {}, function () { 15 | var result = this.relative(null, true); 16 | helper.deepEqual(test, result['.'], ['Project (LO)/', 'nested/', 'one.js', 'sub/']); 17 | helper.deepEqual(test, result['sub/'], ['one.js', 'two.js']); 18 | this.on('end', test.done); 19 | this.close(); 20 | }); 21 | }, 22 | func: function (test) { 23 | test.expect(1); 24 | var g = gaze('**/*', function (err, watcher) { 25 | test.deepEqual(watcher.relative('sub', true), ['one.js', 'two.js']); 26 | g.on('end', test.done); 27 | g.close(); 28 | }); 29 | }, 30 | ready: function (test) { 31 | test.expect(1); 32 | var g = new gaze.Gaze('**/*'); 33 | g.on('ready', function (watcher) { 34 | test.deepEqual(watcher.relative('sub', true), ['one.js', 'two.js']); 35 | this.on('end', test.done); 36 | this.close(); 37 | }); 38 | }, 39 | newGazeNomatch: function (test) { 40 | test.expect(1); 41 | var g = new gaze.Gaze('nomatch.js'); 42 | g.on('nomatch', function (watcher) { 43 | test.ok(true, 'nomatch was emitted.'); 44 | this.on('end', test.done); 45 | this.close(); 46 | }); 47 | }, 48 | nomatch: function (test) { 49 | test.expect(1); 50 | gaze('nomatch.js', function (err, watcher) { 51 | watcher.on('nomatch', function () { 52 | test.ok(true, 'nomatch was emitted.'); 53 | watcher.close(); 54 | }); 55 | watcher.on('end', test.done); 56 | }); 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /test/add_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Gaze = require('../lib/gaze.js').Gaze; 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var helper = require('./helper'); 7 | 8 | var fixtures = path.resolve(__dirname, 'fixtures'); 9 | var sortobj = helper.sortobj; 10 | 11 | exports.add = { 12 | setUp: function (done) { 13 | process.chdir(fixtures); 14 | done(); 15 | }, 16 | addToWatched: function (test) { 17 | test.expect(1); 18 | var files = [ 19 | 'Project (LO)/', 20 | 'Project (LO)/one.js', 21 | 'nested/', 22 | 'nested/one.js', 23 | 'nested/three.js', 24 | 'nested/sub/', 25 | 'nested/sub/two.js', 26 | 'one.js', 27 | ]; 28 | var expected = { 29 | 'Project (LO)/': ['one.js'], 30 | '.': ['Project (LO)/', 'nested/', 'one.js', 'sub/'], 31 | 'nested/': ['sub/', 'sub2/', 'one.js', 'three.js'], 32 | 'nested/sub/': ['two.js'], 33 | }; 34 | var gaze = new Gaze('addnothingtowatch'); 35 | gaze._addToWatched(files); 36 | var result = gaze.relative(null, true); 37 | test.deepEqual(sortobj(result), sortobj(expected)); 38 | gaze.on('end', test.done); 39 | gaze.close(); 40 | }, 41 | addLater: function (test) { 42 | test.expect(3); 43 | new Gaze('sub/one.js', function (err, watcher) { 44 | test.deepEqual(watcher.relative('sub'), ['one.js']); 45 | watcher.add('sub/*.js', function () { 46 | test.deepEqual(watcher.relative('sub'), ['one.js', 'two.js']); 47 | watcher.on('changed', function (filepath) { 48 | test.equal('two.js', path.basename(filepath)); 49 | watcher.on('end', test.done); 50 | watcher.close(); 51 | }); 52 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); 53 | }); 54 | }); 55 | }, 56 | addNoCallback: function (test) { 57 | test.expect(1); 58 | new Gaze('sub/one.js', function (err, watcher) { 59 | this.add('sub/two.js'); 60 | this.on('changed', function (filepath) { 61 | test.equal('two.js', path.basename(filepath)); 62 | watcher.on('end', test.done); 63 | watcher.close(); 64 | }); 65 | setTimeout(function () { 66 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;'); 67 | }, 500); 68 | }); 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /lib/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var helper = module.exports = {}; 5 | 6 | // Returns boolean whether filepath is dir terminated 7 | helper.isDir = function isDir (dir) { 8 | if (typeof dir !== 'string') { 9 | return false; 10 | } 11 | return (dir.slice(-(path.sep.length)) === path.sep); 12 | }; 13 | 14 | // Create a `key:[]` if doesnt exist on `obj` then push or concat the `val` 15 | helper.objectPush = function objectPush (obj, key, val) { 16 | if (obj[key] == null) { 17 | obj[key] = []; 18 | } 19 | if (Array.isArray(val)) { 20 | obj[key] = obj[key].concat(val); 21 | } else if (val) { 22 | obj[key].push(val); 23 | } 24 | obj[key] = helper.unique(obj[key]); 25 | return obj[key]; 26 | }; 27 | 28 | // Ensures the dir is marked with path.sep 29 | helper.markDir = function markDir (dir) { 30 | if (typeof dir === 'string' && 31 | dir.slice(-(path.sep.length)) !== path.sep && 32 | dir !== '.') { 33 | dir += path.sep; 34 | } 35 | return dir; 36 | }; 37 | 38 | // Changes path.sep to unix ones for testing 39 | helper.unixifyPathSep = function unixifyPathSep (filepath) { 40 | return (process.platform === 'win32') ? String(filepath).replace(/\\/g, '/') : filepath; 41 | }; 42 | 43 | /** 44 | * Lo-Dash 1.0.1 45 | * Copyright 2012-2013 The Dojo Foundation 46 | * Based on Underscore.js 1.4.4 47 | * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. 48 | * Available under MIT license 49 | */ 50 | helper.unique = function unique () { 51 | var array = Array.prototype.concat.apply(Array.prototype, arguments); 52 | var result = []; 53 | for (var i = 0; i < array.length; i++) { 54 | if (result.indexOf(array[i]) === -1) { 55 | result.push(array[i]); 56 | } 57 | } 58 | return result; 59 | }; 60 | 61 | /** 62 | * Copyright (c) 2010 Caolan McMahon 63 | * Available under MIT license 64 | */ 65 | helper.forEachSeries = function forEachSeries (arr, iterator, callback) { 66 | if (!arr.length) { return callback(); } 67 | var completed = 0; 68 | var iterate = function () { 69 | iterator(arr[completed], function (err) { 70 | if (err) { 71 | callback(err); 72 | callback = function () {}; 73 | } else { 74 | completed += 1; 75 | if (completed === arr.length) { 76 | callback(null); 77 | } else { 78 | iterate(); 79 | } 80 | } 81 | }); 82 | }; 83 | iterate(); 84 | }; 85 | -------------------------------------------------------------------------------- /test/watch_race_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gaze = require('../lib/gaze.js'); 4 | var grunt = require('grunt'); 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | var cp = require('child_process'); 8 | 9 | // Clean up helper to call in setUp and tearDown 10 | function cleanUp (done) { 11 | [ 12 | 'nested/sub/poller.js' 13 | ].forEach(function (d) { 14 | var p = path.resolve(__dirname, 'fixtures', d); 15 | if (fs.existsSync(p)) { fs.unlinkSync(p); } 16 | }); 17 | done(); 18 | } 19 | 20 | exports.watch_race = { 21 | setUp: function (done) { 22 | process.chdir(path.resolve(__dirname, 'fixtures')); 23 | cleanUp(done); 24 | }, 25 | tearDown: cleanUp, 26 | initWatchDirOnClose: function (test) { 27 | var times = 5, 28 | TIMEOUT = 5000, 29 | firedWhenClosed = 0, 30 | watchers = [], 31 | watcherIdxes = [], 32 | polled_file = ['fixtures', 'nested', 'sub', 'poller.js'], 33 | expected_path = path.join.apply(path, polled_file.slice(1)); 34 | test.expect(times); 35 | for (var i = times; i--;) { 36 | watcherIdxes.unshift(i); 37 | } 38 | // Create the file so that it can be watched 39 | fs.writeFileSync(path.resolve.apply(path, [__dirname].concat(polled_file)), ''); 40 | // Create a poller that keeps making changes to the file until timeout 41 | var child_poller = cp.fork( 42 | '../file_poller.js', 43 | [times * TIMEOUT].concat(polled_file) 44 | ); 45 | grunt.util.async.forEachSeries(watcherIdxes, function (idx, next) { 46 | var watcher = new gaze.Gaze('**/poller.js', function (err, watcher) { 47 | var timeout = setTimeout(function () { 48 | test.ok(false, 'watcher ' + idx + ' did not fire event on polled file.'); 49 | watcher.close(); 50 | }, TIMEOUT); 51 | watcher.on('all', function (status, filepath) { 52 | if (!filepath) { return; } 53 | var expected = path.relative(process.cwd(), filepath); 54 | test.equal(expected_path, expected, 'watcher ' + idx + 55 | ' emitted unexpected event.'); 56 | clearTimeout(timeout); 57 | watcher.close(); 58 | }); 59 | watcher.on('end', function () { 60 | // After watcher is closed and all event listeners have been removed, 61 | // re-add a listener to see if anything is going on on this watcher. 62 | process.nextTick(function () { 63 | watcher.once('added', function () { 64 | test.ok(false, 'watcher ' + idx + ' should not fire added' + 65 | ' event on polled file after being closed.'); 66 | }); 67 | }); 68 | next(); 69 | }); 70 | }); 71 | watchers.push(watcher); 72 | }, function () { 73 | child_poller.kill(); 74 | watchers.forEach(function (watcher) { 75 | try { 76 | watcher.close(); 77 | } catch (e) { 78 | // Ignore if this fails 79 | } 80 | }); 81 | test.done(); 82 | }); 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /test/matching_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gaze = require('../lib/gaze.js'); 4 | var grunt = require('grunt'); 5 | var path = require('path'); 6 | var helper = require('./helper'); 7 | 8 | var fixtures = path.resolve(__dirname, 'fixtures'); 9 | var sortobj = helper.sortobj; 10 | 11 | function cleanUp (done) { 12 | [ 13 | 'newfolder', 14 | ].forEach(function (d) { 15 | var p = path.join(fixtures, d); 16 | if (grunt.file.exists(p)) { 17 | grunt.file.delete(p); 18 | } 19 | }); 20 | done(); 21 | } 22 | 23 | exports.matching = { 24 | setUp: function (done) { 25 | process.chdir(fixtures); 26 | cleanUp(done); 27 | }, 28 | tearDown: cleanUp, 29 | globAll: function (test) { 30 | test.expect(2); 31 | gaze('**/*', {nosort:true}, function () { 32 | var result = this.relative(null, true); 33 | helper.deepEqual(test, result['.'], ['Project (LO)/', 'nested/', 'one.js', 'sub/']); 34 | helper.deepEqual(test, result['sub/'], ['one.js', 'two.js']); 35 | this.on('end', test.done); 36 | this.close(); 37 | }); 38 | }, 39 | relativeDir: function (test) { 40 | test.expect(1); 41 | gaze('**/*', function () { 42 | test.deepEqual(this.relative('sub', true), ['one.js', 'two.js']); 43 | this.on('end', test.done); 44 | this.close(); 45 | }); 46 | }, 47 | globArray: function (test) { 48 | test.expect(2); 49 | gaze(['*.js', 'sub/*.js'], function () { 50 | var result = this.relative(null, true); 51 | test.deepEqual(sortobj(result['.']), sortobj(['one.js', 'Project (LO)/', 'nested/', 'sub/'])); 52 | test.deepEqual(sortobj(result['sub/']), sortobj(['one.js', 'two.js'])); 53 | this.on('end', test.done); 54 | this.close(); 55 | }); 56 | }, 57 | globArrayDot: function (test) { 58 | test.expect(1); 59 | gaze(['./sub/*.js'], function () { 60 | var result = this.relative(null, true); 61 | test.deepEqual(result['sub/'], ['one.js', 'two.js']); 62 | this.on('end', test.done); 63 | this.close(); 64 | }); 65 | }, 66 | oddName: function (test) { 67 | test.expect(1); 68 | gaze(['Project (LO)/*.js'], function () { 69 | var result = this.relative(null, true); 70 | test.deepEqual(result['Project (LO)/'], ['one.js']); 71 | this.on('end', test.done); 72 | this.close(); 73 | }); 74 | }, 75 | addedLater: function (test) { 76 | test.expect(2); 77 | var times = 0; 78 | gaze('**/*.js', function (err, watcher) { 79 | watcher.on('all', function (status, filepath) { 80 | times++; 81 | var result = watcher.relative(null, true); 82 | test.deepEqual(result['newfolder/'], ['added.js']); 83 | if (times > 1) { watcher.close(); } 84 | }); 85 | grunt.file.write(path.join(fixtures, 'newfolder', 'added.js'), 'var added = true;'); 86 | setTimeout(function () { 87 | grunt.file.write(path.join(fixtures, 'newfolder', 'added.js'), 'var added = true;'); 88 | }, 1000); 89 | watcher.on('end', function () { 90 | // TODO: Figure out why this test is finicky leaking it's newfolder into the other tests 91 | setTimeout(test.done, 2000); 92 | }); 93 | }); 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gaze [![Build Status](http://img.shields.io/travis/shama/gaze.svg)](https://travis-ci.org/shama/gaze) [![Build status](https://ci.appveyor.com/api/projects/status/vtx65w9eg511tgo4)](https://ci.appveyor.com/project/shama/gaze) 2 | 3 | A globbing fs.watch wrapper built from the best parts of other fine watch libs. 4 | Compatible with Node.js 4.x/0.12/0.10, Windows, OSX and Linux. 5 | 6 | ![gaze](http://dontkry.com/images/repos/gaze.png) 7 | 8 | [![NPM](https://nodei.co/npm/gaze.png?downloads=true)](https://nodei.co/npm/gaze/) 9 | 10 | ## Usage 11 | Install the module with: `npm install gaze` or place into your `package.json` 12 | and run `npm install`. 13 | 14 | ```javascript 15 | var gaze = require('gaze'); 16 | 17 | // Watch all .js files/dirs in process.cwd() 18 | gaze('**/*.js', function(err, watcher) { 19 | // Files have all started watching 20 | // watcher === this 21 | 22 | // Get all watched files 23 | var watched = this.watched(); 24 | 25 | // On file changed 26 | this.on('changed', function(filepath) { 27 | console.log(filepath + ' was changed'); 28 | }); 29 | 30 | // On file added 31 | this.on('added', function(filepath) { 32 | console.log(filepath + ' was added'); 33 | }); 34 | 35 | // On file deleted 36 | this.on('deleted', function(filepath) { 37 | console.log(filepath + ' was deleted'); 38 | }); 39 | 40 | // On changed/added/deleted 41 | this.on('all', function(event, filepath) { 42 | console.log(filepath + ' was ' + event); 43 | }); 44 | 45 | // Get watched files with relative paths 46 | var files = this.relative(); 47 | }); 48 | 49 | // Also accepts an array of patterns 50 | gaze(['stylesheets/*.css', 'images/**/*.png'], function() { 51 | // Add more patterns later to be watched 52 | this.add(['js/*.js']); 53 | }); 54 | ``` 55 | 56 | ### Alternate Interface 57 | 58 | ```javascript 59 | var Gaze = require('gaze').Gaze; 60 | 61 | var gaze = new Gaze('**/*'); 62 | 63 | // Files have all started watching 64 | gaze.on('ready', function(watcher) { }); 65 | 66 | // A file has been added/changed/deleted has occurred 67 | gaze.on('all', function(event, filepath) { }); 68 | ``` 69 | 70 | ### Errors 71 | 72 | ```javascript 73 | gaze('**/*', function(error, watcher) { 74 | if (error) { 75 | // Handle error if it occurred while starting up 76 | } 77 | }); 78 | 79 | // Or with the alternative interface 80 | var gaze = new Gaze(); 81 | gaze.on('error', function(error) { 82 | // Handle error here 83 | }); 84 | gaze.add('**/*'); 85 | ``` 86 | 87 | ### Minimatch / Glob 88 | 89 | See [isaacs's minimatch](https://github.com/isaacs/minimatch) for more 90 | information on glob patterns. 91 | 92 | ## Documentation 93 | 94 | ### gaze([patterns, options, callback]) 95 | 96 | * `patterns` {String|Array} File patterns to be matched 97 | * `options` {Object} 98 | * `callback` {Function} 99 | * `err` {Error | null} 100 | * `watcher` {Object} Instance of the Gaze watcher 101 | 102 | ### Class: gaze.Gaze 103 | 104 | Create a Gaze object by instancing the `gaze.Gaze` class. 105 | 106 | ```javascript 107 | var Gaze = require('gaze').Gaze; 108 | var gaze = new Gaze(pattern, options, callback); 109 | ``` 110 | 111 | #### Properties 112 | 113 | * `options` The options object passed in. 114 | * `interval` {integer} Interval to pass to fs.watchFile 115 | * `debounceDelay` {integer} Delay for events called in succession for the same 116 | file/event in milliseconds 117 | * `mode` {string} Force the watch mode. Either `'auto'` (default), `'watch'` (force native events), or `'poll'` (force stat polling). 118 | * `cwd` {string} The current working directory to base file patterns from. Default is `process.cwd()`. 119 | 120 | #### Events 121 | 122 | * `ready(watcher)` When files have been globbed and watching has begun. 123 | * `all(event, filepath)` When an `added`, `changed` or `deleted` event occurs. 124 | * `added(filepath)` When a file has been added to a watch directory. 125 | * `changed(filepath)` When a file has been changed. 126 | * `deleted(filepath)` When a file has been deleted. 127 | * `renamed(newPath, oldPath)` When a file has been renamed. 128 | * `end()` When the watcher is closed and watches have been removed. 129 | * `error(err)` When an error occurs. 130 | * `nomatch` When no files have been matched. 131 | 132 | #### Methods 133 | 134 | * `emit(event, [...])` Wrapper for the EventEmitter.emit. 135 | `added`|`changed`|`deleted` events will also trigger the `all` event. 136 | * `close()` Unwatch all files and reset the watch instance. 137 | * `add(patterns, callback)` Adds file(s) patterns to be watched. 138 | * `remove(filepath)` removes a file or directory from being watched. Does not 139 | recurse directories. 140 | * `watched()` Returns the currently watched files. 141 | * `relative([dir, unixify])` Returns the currently watched files with relative paths. 142 | * `dir` {string} Only return relative files for this directory. 143 | * `unixify` {boolean} Return paths with `/` instead of `\\` if on Windows. 144 | 145 | ## Similar Projects 146 | 147 | Other great watch libraries to try are: 148 | 149 | * [paulmillr's chokidar](https://github.com/paulmillr/chokidar) 150 | * [amasad's sane](https://github.com/amasad/sane) 151 | * [mikeal's watch](https://github.com/mikeal/watch) 152 | * [github's pathwatcher](https://github.com/atom/node-pathwatcher) 153 | * [bevry's watchr](https://github.com/bevry/watchr) 154 | 155 | ## Contributing 156 | In lieu of a formal styleguide, take care to maintain the existing coding style. 157 | Add unit tests for any new or changed functionality. Lint and test your code 158 | using [grunt](http://gruntjs.com/). 159 | 160 | ## Release History 161 | * 0.6.4 - Catch and emit error from readdir (@oconnore). Fix for 0 maxListeners. Use graceful-fs to avoid EMFILE errors in other places fs is used. Better method to determine if pathwatcher was built. Fix keeping process alive too much, only init pathwatcher if a file is being watched. Set min required to Windows Vista when building on Windows (@pvolok). 162 | * 0.6.3 - Add support for node v0.11 163 | * 0.6.2 - Fix argument error with watched(). Fix for erroneous added events on folders. Ignore msvs build error 4244. 164 | * 0.6.1 - Fix for absolute paths. 165 | * 0.6.0 - Uses native OS events (fork of pathwatcher) but can fall back to stat polling. Everything is async to avoid blocking, including `relative()` and `watched()`. Better error handling. Update to globule@0.2.0. No longer watches `cwd` by default. Added `mode` option. Better `EMFILE` message. Avoids `ENOENT` errors with symlinks. All constructor arguments are optional. 166 | * 0.5.2 - Fix for ENOENT error with non-existent symlinks [BACKPORTED]. 167 | * 0.5.1 - Use setImmediate (process.nextTick for node v0.8) to defer ready/nomatch events (@amasad). 168 | * 0.5.0 - Process is now kept alive while watching files. Emits a nomatch event when no files are matching. 169 | * 0.4.3 - Track file additions in newly created folders (@brett-shwom). 170 | * 0.4.2 - Fix .remove() method to remove a single file in a directory (@kaelzhang). Fixing Cannot call method 'call' of undefined (@krasimir). Track new file additions within folders (@brett-shwom). 171 | * 0.4.1 - Fix watchDir not respecting close in race condition (@chrisirhc). 172 | * 0.4.0 - Drop support for node v0.6. Use globule for file matching. Avoid node v0.10 path.resolve/join errors. Register new files when added to non-existent folder. Multiple instances can now poll the same files (@jpommerening). 173 | * 0.3.4 - Code clean up. Fix path must be strings errors (@groner). Fix incorrect added events (@groner). 174 | * 0.3.3 - Fix for multiple patterns with negate. 175 | * 0.3.2 - Emit `end` before removeAllListeners. 176 | * 0.3.1 - Fix added events within subfolder patterns. 177 | * 0.3.0 - Handle safewrite events, `forceWatchMethod` option removed, bug fixes and watch optimizations (@rgaskill). 178 | * 0.2.2 - Fix issue where subsequent add calls dont get watched (@samcday). removeAllListeners on close. 179 | * 0.2.1 - Fix issue with invalid `added` events in current working dir. 180 | * 0.2.0 - Support and mark folders with `path.sep`. Add `forceWatchMethod` option. Support `renamed` events. 181 | * 0.1.6 - Recognize the `cwd` option properly 182 | * 0.1.5 - Catch too many open file errors 183 | * 0.1.4 - Really fix the race condition with 2 watches 184 | * 0.1.3 - Fix race condition with 2 watches 185 | * 0.1.2 - Read triggering changed event fix 186 | * 0.1.1 - Minor fixes 187 | * 0.1.0 - Initial release 188 | 189 | ## License 190 | Copyright (c) 2015 Kyle Robinson Young 191 | Licensed under the MIT license. 192 | -------------------------------------------------------------------------------- /test/watch_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gaze = require('../lib/gaze.js'); 4 | var grunt = require('grunt'); 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | 8 | // Clean up helper to call in setUp and tearDown 9 | function cleanUp (done) { 10 | [ 11 | 'sub/tmp.js', 12 | 'sub/tmp', 13 | 'sub/renamed.js', 14 | 'added.js', 15 | 'nested/added.js', 16 | 'nested/.tmp', 17 | 'nested/sub/added.js', 18 | ].forEach(function (d) { 19 | var p = path.resolve(__dirname, 'fixtures', d); 20 | if (fs.existsSync(p)) { fs.unlinkSync(p); } 21 | }); 22 | 23 | grunt.file.delete(path.resolve(__dirname, 'fixtures', 'new_dir')); 24 | 25 | done(); 26 | } 27 | 28 | exports.watch = { 29 | setUp: function (done) { 30 | process.chdir(path.resolve(__dirname, 'fixtures')); 31 | cleanUp(done); 32 | }, 33 | tearDown: cleanUp, 34 | remove: function (test) { 35 | test.expect(2); 36 | gaze('**/*', function () { 37 | this.remove(path.resolve(__dirname, 'fixtures', 'sub', 'two.js')); 38 | this.remove(path.resolve(__dirname, 'fixtures')); 39 | var result = this.relative(null, true); 40 | test.deepEqual(result['sub/'], ['one.js']); 41 | test.notDeepEqual(result['.'], ['one.js']); 42 | this.on('end', test.done); 43 | this.close(); 44 | }); 45 | }, 46 | changed: function (test) { 47 | test.expect(1); 48 | gaze('**/*', function (err, watcher) { 49 | watcher.on('changed', function (filepath) { 50 | var expected = path.relative(process.cwd(), filepath); 51 | test.equal(path.join('sub', 'one.js'), expected); 52 | watcher.close(); 53 | }); 54 | this.on('added', function () { test.ok(false, 'added event should not have emitted.'); }); 55 | this.on('deleted', function () { test.ok(false, 'deleted event should not have emitted.'); }); 56 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;'); 57 | watcher.on('end', test.done); 58 | }); 59 | }, 60 | added: function (test) { 61 | test.expect(1); 62 | gaze('**/*', function (err, watcher) { 63 | watcher.on('added', function (filepath) { 64 | var expected = path.relative(process.cwd(), filepath); 65 | test.equal(path.join('sub', 'tmp.js'), expected); 66 | watcher.close(); 67 | }); 68 | this.on('changed', function () { test.ok(false, 'changed event should not have emitted.'); }); 69 | this.on('deleted', function () { test.ok(false, 'deleted event should not have emitted.'); }); 70 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'tmp.js'), 'var tmp = true;'); 71 | watcher.on('end', test.done); 72 | }); 73 | }, 74 | dontAddUnmatchedFiles: function (test) { 75 | test.expect(2); 76 | gaze('**/*.js', function (err, watcher) { 77 | setTimeout(function () { 78 | test.ok(true, 'Ended without adding a file.'); 79 | watcher.close(); 80 | }, 1000); 81 | this.on('added', function (filepath) { 82 | test.equal(path.relative(process.cwd(), filepath), path.join('sub', 'tmp.js')); 83 | }); 84 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'tmp'), 'Dont add me!'); 85 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'tmp.js'), 'add me!'); 86 | watcher.on('end', test.done); 87 | }); 88 | }, 89 | dontAddMatchedDirectoriesThatArentReallyAdded: function (test) { 90 | // This is a regression test for a bug I ran into where a matching directory would be reported 91 | // added when a non-matching file was created along side it. This only happens if the 92 | // directory name doesn't occur in $PWD. 93 | test.expect(1); 94 | gaze('**/*', function (err, watcher) { 95 | setTimeout(function () { 96 | test.ok(true, 'Ended without adding a file.'); 97 | watcher.close(); 98 | }, 1000); 99 | this.on('added', function (filepath) { 100 | test.notEqual(path.relative(process.cwd(), filepath), path.join('nested', 'sub2')); 101 | }); 102 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'nested', '.tmp'), 'Wake up!'); 103 | watcher.on('end', test.done); 104 | }); 105 | }, 106 | deleted: function (test) { 107 | test.expect(1); 108 | var tmpfile = path.resolve(__dirname, 'fixtures', 'sub', 'deleted.js'); 109 | fs.writeFileSync(tmpfile, 'var tmp = true;'); 110 | gaze('**/*', function (err, watcher) { 111 | watcher.on('deleted', function (filepath) { 112 | test.equal(path.join('sub', 'deleted.js'), path.relative(process.cwd(), filepath)); 113 | watcher.close(); 114 | }); 115 | this.on('changed', function () { test.ok(false, 'changed event should not have emitted.'); }); 116 | this.on('added', function () { test.ok(false, 'added event should not have emitted.'); }); 117 | fs.unlinkSync(tmpfile); 118 | watcher.on('end', test.done); 119 | }); 120 | }, 121 | dontEmitTwice: function (test) { 122 | test.expect(2); 123 | gaze('**/*', function (err, watcher) { 124 | watcher.on('all', function (status, filepath) { 125 | var expected = path.relative(process.cwd(), filepath); 126 | test.equal(path.join('sub', 'one.js'), expected); 127 | test.equal(status, 'changed'); 128 | fs.readFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js')); 129 | setTimeout(function () { 130 | fs.readFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js')); 131 | }, 1000); 132 | // Give some time to accidentally emit before we close 133 | setTimeout(function () { watcher.close(); }, 5000); 134 | }); 135 | setTimeout(function () { 136 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;'); 137 | }, 1000); 138 | watcher.on('end', test.done); 139 | }); 140 | }, 141 | emitTwice: function (test) { 142 | test.expect(2); 143 | var times = 0; 144 | gaze('**/*', function (err, watcher) { 145 | watcher.on('all', function (status, filepath) { 146 | test.equal(status, 'changed'); 147 | times++; 148 | setTimeout(function () { 149 | if (times < 2) { 150 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;'); 151 | } else { 152 | watcher.close(); 153 | } 154 | }, 1000); 155 | }); 156 | setTimeout(function () { 157 | fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;'); 158 | }, 1000); 159 | watcher.on('end', test.done); 160 | }); 161 | }, 162 | nonExistent: function (test) { 163 | test.expect(1); 164 | gaze('non/existent/**/*', function (err, watcher) { 165 | test.ok(true); 166 | watcher.on('end', test.done); 167 | watcher.close(); 168 | }); 169 | }, 170 | differentCWD: function (test) { 171 | test.expect(1); 172 | var cwd = path.resolve(__dirname, 'fixtures', 'sub'); 173 | gaze('two.js', { 174 | cwd: cwd 175 | }, function (err, watcher) { 176 | watcher.on('changed', function (filepath) { 177 | test.deepEqual(this.relative(), {'.': ['two.js']}); 178 | watcher.close(); 179 | }); 180 | fs.writeFileSync(path.resolve(cwd, 'two.js'), 'var two = true;'); 181 | watcher.on('end', test.done); 182 | }); 183 | }, 184 | addedEmitInSubFolders: function (test) { 185 | test.expect(4); 186 | var adds = [ 187 | { pattern: '**/*', file: path.resolve(__dirname, 'fixtures', 'nested', 'sub', 'added.js') }, 188 | { pattern: '**/*', file: path.resolve(__dirname, 'fixtures', 'added.js') }, 189 | { pattern: 'nested/**/*', file: path.resolve(__dirname, 'fixtures', 'nested', 'added.js') }, 190 | { pattern: 'nested/sub/*.js', file: path.resolve(__dirname, 'fixtures', 'nested', 'sub', 'added.js') }, 191 | ]; 192 | grunt.util.async.forEachSeries(adds, function (add, next) { 193 | new gaze.Gaze(add.pattern, function (err, watcher) { 194 | watcher.on('added', function (filepath) { 195 | test.equal('added.js', path.basename(filepath)); 196 | fs.unlinkSync(filepath); 197 | watcher.close(); 198 | next(); 199 | }); 200 | watcher.on('changed', function () { test.ok(false, 'changed event should not have emitted.'); }); 201 | watcher.on('deleted', function () { test.ok(false, 'deleted event should not have emitted.'); }); 202 | fs.writeFileSync(add.file, 'var added = true;'); 203 | }); 204 | }, function () { 205 | test.done(); 206 | }); 207 | }, 208 | multipleWatchersSimultaneously: function (test) { 209 | test.expect(2); 210 | var did = 0; 211 | var ready = 0; 212 | var cwd = path.resolve(__dirname, 'fixtures', 'sub'); 213 | var watchers = []; 214 | var timeout = setTimeout(function () { 215 | test.ok(false, 'Only ' + did + ' of ' + ready + ' watchers fired.'); 216 | test.done(); 217 | watchers.forEach(function (watcher) { 218 | watcher.close(); 219 | }); 220 | }, 1000); 221 | 222 | function isReady () { 223 | ready++; 224 | if (ready > 1) { 225 | fs.writeFileSync(path.resolve(cwd, 'one.js'), 'var one = true;'); 226 | } 227 | } 228 | function isDone () { 229 | did++; 230 | if (did > 1) { 231 | clearTimeout(timeout); 232 | watchers.forEach(function (watcher) { 233 | watcher.close(); 234 | }); 235 | test.done(); 236 | } 237 | } 238 | function changed (filepath) { 239 | test.equal(path.join('sub', 'one.js'), path.relative(process.cwd(), filepath)); 240 | isDone(); 241 | } 242 | for (var i = 0; i < 2; i++) { 243 | watchers[i] = new gaze.Gaze('sub/one.js'); 244 | watchers[i].on('changed', changed); 245 | watchers[i].on('ready', isReady); 246 | } 247 | }, 248 | mkdirThenAddFile: function (test) { 249 | test.expect(2); 250 | 251 | var expected = [ 252 | 'new_dir', 253 | 'new_dir/other.js', 254 | ]; 255 | 256 | gaze('**/*.js', function (err, watcher) { 257 | watcher.on('all', function (status, filepath) { 258 | var expect = expected.shift(); 259 | test.equal(path.relative(process.cwd(), filepath), expect); 260 | 261 | if (expected.length === 1) { 262 | // Ensure the new folder is being watched correctly after initial add 263 | setTimeout(function () { 264 | fs.writeFileSync('new_dir/dontmatch.txt', ''); 265 | setTimeout(function () { 266 | fs.writeFileSync('new_dir/other.js', ''); 267 | }, 1000); 268 | }, 1000); 269 | } 270 | 271 | if (expected.length < 1) { watcher.close(); } 272 | }); 273 | 274 | fs.mkdirSync('new_dir'); // fs.mkdirSync([folder]) seems to behave differently than grunt.file.write('[folder]/[file]') 275 | 276 | watcher.on('end', test.done); 277 | }); 278 | }, 279 | mkdirThenAddFileWithGruntFileWrite: function (test) { 280 | test.expect(3); 281 | 282 | var expected = [ 283 | 'new_dir', 284 | 'new_dir/tmp.js', 285 | 'new_dir/other.js', 286 | ]; 287 | 288 | gaze('**/*.js', function (err, watcher) { 289 | watcher.on('all', function (status, filepath) { 290 | var expect = expected.shift(); 291 | test.equal(path.relative(process.cwd(), filepath), expect); 292 | 293 | if (expected.length === 1) { 294 | // Ensure the new folder is being watched correctly after initial add 295 | setTimeout(function () { 296 | fs.writeFileSync('new_dir/dontmatch.txt', ''); 297 | setTimeout(function () { 298 | fs.writeFileSync('new_dir/other.js', ''); 299 | }, 1000); 300 | }, 1000); 301 | } 302 | 303 | if (expected.length < 1) { watcher.close(); } 304 | }); 305 | 306 | grunt.file.write('new_dir/tmp.js', ''); 307 | 308 | watcher.on('end', test.done); 309 | }); 310 | }, 311 | enoentSymlink: function (test) { 312 | test.expect(1); 313 | fs.mkdirSync(path.resolve(__dirname, 'fixtures', 'new_dir')); 314 | try { 315 | fs.symlinkSync(path.resolve(__dirname, 'fixtures', 'not-exists.js'), path.resolve(__dirname, 'fixtures', 'new_dir', 'not-exists-symlink.js')); 316 | } catch (err) { 317 | // If we cant create symlinks, just ignore this tests (likely needs admin on win) 318 | test.ok(true); 319 | return test.done(); 320 | } 321 | gaze('**/*', function () { 322 | test.ok(true); 323 | this.on('end', test.done); 324 | this.close(); 325 | }); 326 | }, 327 | }; 328 | -------------------------------------------------------------------------------- /lib/gaze.js: -------------------------------------------------------------------------------- 1 | /* 2 | * gaze 3 | * https://github.com/shama/gaze 4 | * 5 | * Copyright (c) 2016 Kyle Robinson Young 6 | * Licensed under the MIT license. 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // libs 12 | var util = require('util'); 13 | var EE = require('events').EventEmitter; 14 | var fs = require('fs'); 15 | var path = require('path'); 16 | var globule = require('globule'); 17 | var helper = require('./helper'); 18 | 19 | // shim setImmediate for node v0.8 20 | var setImmediate = require('timers').setImmediate; 21 | if (typeof setImmediate !== 'function') { 22 | setImmediate = process.nextTick; 23 | } 24 | 25 | // globals 26 | var delay = 10; 27 | 28 | // `Gaze` EventEmitter object to return in the callback 29 | function Gaze (patterns, opts, done) { 30 | var self = this; 31 | EE.call(self); 32 | 33 | // If second arg is the callback 34 | if (typeof opts === 'function') { 35 | done = opts; 36 | opts = {}; 37 | } 38 | 39 | // Default options 40 | opts = opts || {}; 41 | opts.mark = true; 42 | opts.interval = opts.interval || 100; 43 | opts.debounceDelay = opts.debounceDelay || 500; 44 | opts.cwd = opts.cwd || process.cwd(); 45 | this.options = opts; 46 | 47 | // Default done callback 48 | done = done || function () {}; 49 | 50 | // Remember our watched dir:files 51 | this._watched = Object.create(null); 52 | 53 | // Store watchers 54 | this._watchers = Object.create(null); 55 | 56 | // Store watchFile listeners 57 | this._pollers = Object.create(null); 58 | 59 | // Store patterns 60 | this._patterns = []; 61 | 62 | // Cached events for debouncing 63 | this._cached = Object.create(null); 64 | 65 | // Set maxListeners 66 | if (this.options.maxListeners != null) { 67 | this.setMaxListeners(this.options.maxListeners); 68 | Gaze.super_.prototype.setMaxListeners(this.options.maxListeners); 69 | delete this.options.maxListeners; 70 | } 71 | 72 | // Initialize the watch on files 73 | if (patterns) { 74 | this.add(patterns, done); 75 | } 76 | 77 | // keep the process alive 78 | this._keepalive = setInterval(function () {}, 200); 79 | 80 | return this; 81 | } 82 | util.inherits(Gaze, EE); 83 | 84 | // Main entry point. Start watching and call done when setup 85 | module.exports = function gaze (patterns, opts, done) { 86 | return new Gaze(patterns, opts, done); 87 | }; 88 | module.exports.Gaze = Gaze; 89 | 90 | // Override the emit function to emit `all` events 91 | // and debounce on duplicate events per file 92 | Gaze.prototype.emit = function () { 93 | var self = this; 94 | var args = arguments; 95 | 96 | var e = args[0]; 97 | var filepath = args[1]; 98 | var timeoutId; 99 | 100 | // If not added/deleted/changed/renamed then just emit the event 101 | if (e.slice(-2) !== 'ed') { 102 | Gaze.super_.prototype.emit.apply(self, args); 103 | return this; 104 | } 105 | 106 | // Detect rename event, if added and previous deleted is in the cache 107 | if (e === 'added') { 108 | Object.keys(this._cached).forEach(function (oldFile) { 109 | if (self._cached[oldFile].indexOf('deleted') !== -1) { 110 | args[0] = e = 'renamed'; 111 | [].push.call(args, oldFile); 112 | delete self._cached[oldFile]; 113 | return false; 114 | } 115 | }); 116 | } 117 | 118 | // If cached doesnt exist, create a delay before running the next 119 | // then emit the event 120 | var cache = this._cached[filepath] || []; 121 | if (cache.indexOf(e) === -1) { 122 | helper.objectPush(self._cached, filepath, e); 123 | clearTimeout(timeoutId); 124 | timeoutId = setTimeout(function () { 125 | delete self._cached[filepath]; 126 | }, this.options.debounceDelay); 127 | // Emit the event and `all` event 128 | Gaze.super_.prototype.emit.apply(self, args); 129 | Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1))); 130 | } 131 | 132 | // Detect if new folder added to trigger for matching files within folder 133 | if (e === 'added') { 134 | if (helper.isDir(filepath)) { 135 | fs.readdirSync(filepath).map(function (file) { 136 | return path.join(filepath, file); 137 | }).filter(function (file) { 138 | return globule.isMatch(self._patterns, file, self.options); 139 | }).forEach(function (file) { 140 | self.emit('added', file); 141 | }); 142 | } 143 | } 144 | 145 | return this; 146 | }; 147 | 148 | // Close watchers 149 | Gaze.prototype.close = function (_reset) { 150 | var self = this; 151 | Object.keys(self._watchers).forEach(function (file) { 152 | self._watchers[file].close(); 153 | }); 154 | self._watchers = Object.create(null); 155 | Object.keys(this._watched).forEach(function (dir) { 156 | self._unpollDir(dir); 157 | }); 158 | if (_reset !== false) { 159 | self._watched = Object.create(null); 160 | setTimeout(function () { 161 | self.emit('end'); 162 | self.removeAllListeners(); 163 | clearInterval(self._keepalive); 164 | }, delay + 100); 165 | } 166 | return self; 167 | }; 168 | 169 | // Add file patterns to be watched 170 | Gaze.prototype.add = function (files, done) { 171 | if (typeof files === 'string') { files = [files]; } 172 | this._patterns = helper.unique.apply(null, [this._patterns, files]); 173 | files = globule.find(this._patterns, this.options); 174 | this._addToWatched(files); 175 | this.close(false); 176 | this._initWatched(done); 177 | }; 178 | 179 | // Dont increment patterns and dont call done if nothing added 180 | Gaze.prototype._internalAdd = function (file, done) { 181 | var files = []; 182 | if (helper.isDir(file)) { 183 | files = [helper.markDir(file)].concat(globule.find(this._patterns, this.options)); 184 | } else { 185 | if (globule.isMatch(this._patterns, file, this.options)) { 186 | files = [file]; 187 | } 188 | } 189 | if (files.length > 0) { 190 | this._addToWatched(files); 191 | this.close(false); 192 | this._initWatched(done); 193 | } 194 | }; 195 | 196 | // Remove file/dir from `watched` 197 | Gaze.prototype.remove = function (file) { 198 | var self = this; 199 | if (this._watched[file]) { 200 | // is dir, remove all files 201 | this._unpollDir(file); 202 | delete this._watched[file]; 203 | } else { 204 | // is a file, find and remove 205 | Object.keys(this._watched).forEach(function (dir) { 206 | var index = self._watched[dir].indexOf(file); 207 | if (index !== -1) { 208 | self._unpollFile(file); 209 | self._watched[dir].splice(index, 1); 210 | return false; 211 | } 212 | }); 213 | } 214 | if (this._watchers[file]) { 215 | this._watchers[file].close(); 216 | } 217 | return this; 218 | }; 219 | 220 | // Return watched files 221 | Gaze.prototype.watched = function () { 222 | return this._watched; 223 | }; 224 | 225 | // Returns `watched` files with relative paths to process.cwd() 226 | Gaze.prototype.relative = function (dir, unixify) { 227 | var self = this; 228 | var relative = Object.create(null); 229 | var relDir, relFile, unixRelDir; 230 | var cwd = this.options.cwd || process.cwd(); 231 | if (dir === '') { dir = '.'; } 232 | dir = helper.markDir(dir); 233 | unixify = unixify || false; 234 | Object.keys(this._watched).forEach(function (dir) { 235 | relDir = path.relative(cwd, dir) + path.sep; 236 | if (relDir === path.sep) { relDir = '.'; } 237 | unixRelDir = unixify ? helper.unixifyPathSep(relDir) : relDir; 238 | relative[unixRelDir] = self._watched[dir].map(function (file) { 239 | relFile = path.relative(path.join(cwd, relDir) || '', file || ''); 240 | if (helper.isDir(file)) { 241 | relFile = helper.markDir(relFile); 242 | } 243 | if (unixify) { 244 | relFile = helper.unixifyPathSep(relFile); 245 | } 246 | return relFile; 247 | }); 248 | }); 249 | if (dir && unixify) { 250 | dir = helper.unixifyPathSep(dir); 251 | } 252 | return dir ? relative[dir] || [] : relative; 253 | }; 254 | 255 | // Adds files and dirs to watched 256 | Gaze.prototype._addToWatched = function (files) { 257 | for (var i = 0; i < files.length; i++) { 258 | var file = files[i]; 259 | var filepath = path.resolve(this.options.cwd, file); 260 | 261 | var dirname = (helper.isDir(file)) ? filepath : path.dirname(filepath); 262 | dirname = helper.markDir(dirname); 263 | 264 | // If a new dir is added 265 | if (helper.isDir(file) && !(filepath in this._watched)) { 266 | helper.objectPush(this._watched, filepath, []); 267 | } 268 | 269 | if (file.slice(-1) === '/') { filepath += path.sep; } 270 | helper.objectPush(this._watched, path.dirname(filepath) + path.sep, filepath); 271 | 272 | // add folders into the mix 273 | var readdir = fs.readdirSync(dirname); 274 | for (var j = 0; j < readdir.length; j++) { 275 | var dirfile = path.join(dirname, readdir[j]); 276 | if (fs.lstatSync(dirfile).isDirectory()) { 277 | helper.objectPush(this._watched, dirname, dirfile + path.sep); 278 | } 279 | } 280 | } 281 | return this; 282 | }; 283 | 284 | Gaze.prototype._watchDir = function (dir, done) { 285 | var self = this; 286 | var timeoutId; 287 | try { 288 | this._watchers[dir] = fs.watch(dir, function (event) { 289 | // race condition. Let's give the fs a little time to settle down. so we 290 | // don't fire events on non existent files. 291 | clearTimeout(timeoutId); 292 | timeoutId = setTimeout(function () { 293 | // race condition. Ensure that this directory is still being watched 294 | // before continuing. 295 | if ((dir in self._watchers) && fs.existsSync(dir)) { 296 | done(null, dir); 297 | } 298 | }, delay + 100); 299 | }); 300 | } catch (err) { 301 | return this._handleError(err); 302 | } 303 | return this; 304 | }; 305 | 306 | Gaze.prototype._unpollFile = function (file) { 307 | if (this._pollers[file]) { 308 | fs.unwatchFile(file, this._pollers[file]); 309 | delete this._pollers[file]; 310 | } 311 | return this; 312 | }; 313 | 314 | Gaze.prototype._unpollDir = function (dir) { 315 | this._unpollFile(dir); 316 | for (var i = 0; i < this._watched[dir].length; i++) { 317 | this._unpollFile(this._watched[dir][i]); 318 | } 319 | }; 320 | 321 | Gaze.prototype._pollFile = function (file, done) { 322 | var opts = { persistent: true, interval: this.options.interval }; 323 | if (!this._pollers[file]) { 324 | this._pollers[file] = function (curr, prev) { 325 | done(null, file); 326 | }; 327 | try { 328 | fs.watchFile(file, opts, this._pollers[file]); 329 | } catch (err) { 330 | return this._handleError(err); 331 | } 332 | } 333 | return this; 334 | }; 335 | 336 | // Initialize the actual watch on `watched` files 337 | Gaze.prototype._initWatched = function (done) { 338 | var self = this; 339 | var cwd = this.options.cwd || process.cwd(); 340 | var curWatched = Object.keys(self._watched); 341 | 342 | // if no matching files 343 | if (curWatched.length < 1) { 344 | // Defer to emitting to give a chance to attach event handlers. 345 | setImmediate(function () { 346 | self.emit('ready', self); 347 | if (done) { done.call(self, null, self); } 348 | self.emit('nomatch'); 349 | }); 350 | return; 351 | } 352 | 353 | helper.forEachSeries(curWatched, function (dir, next) { 354 | dir = dir || ''; 355 | var files = self._watched[dir]; 356 | // Triggered when a watched dir has an event 357 | self._watchDir(dir, function (event, dirpath) { 358 | var relDir = cwd === dir ? '.' : path.relative(cwd, dir); 359 | relDir = relDir || ''; 360 | 361 | fs.readdir(dirpath, function (err, current) { 362 | if (err) { return self.emit('error', err); } 363 | if (!current) { return; } 364 | 365 | try { 366 | // append path.sep to directories so they match previous. 367 | current = current.map(function (curPath) { 368 | if (fs.existsSync(path.join(dir, curPath)) && fs.lstatSync(path.join(dir, curPath)).isDirectory()) { 369 | return curPath + path.sep; 370 | } else { 371 | return curPath; 372 | } 373 | }); 374 | } catch (err) { 375 | // race condition-- sometimes the file no longer exists 376 | } 377 | 378 | // Get watched files for this dir 379 | var previous = self.relative(relDir); 380 | 381 | // If file was deleted 382 | previous.filter(function (file) { 383 | return current.indexOf(file) < 0; 384 | }).forEach(function (file) { 385 | if (!helper.isDir(file)) { 386 | var filepath = path.join(dir, file); 387 | self.remove(filepath); 388 | self.emit('deleted', filepath); 389 | } 390 | }); 391 | 392 | // If file was added 393 | current.filter(function (file) { 394 | return previous.indexOf(file) < 0; 395 | }).forEach(function (file) { 396 | // Is it a matching pattern? 397 | var relFile = path.join(relDir, file); 398 | // Add to watch then emit event 399 | self._internalAdd(relFile, function () { 400 | self.emit('added', path.join(dir, file)); 401 | }); 402 | }); 403 | }); 404 | }); 405 | 406 | // Watch for change/rename events on files 407 | files.forEach(function (file) { 408 | if (helper.isDir(file)) { return; } 409 | self._pollFile(file, function (err, filepath) { 410 | if (err) { 411 | self.emit('error', err); 412 | return; 413 | } 414 | // Only emit changed if the file still exists 415 | // Prevents changed/deleted duplicate events 416 | if (fs.existsSync(filepath)) { 417 | self.emit('changed', filepath); 418 | } 419 | }); 420 | }); 421 | 422 | next(); 423 | }, function () { 424 | // Return this instance of Gaze 425 | // delay before ready solves a lot of issues 426 | setTimeout(function () { 427 | self.emit('ready', self); 428 | if (done) { done.call(self, null, self); } 429 | }, delay + 100); 430 | }); 431 | }; 432 | 433 | // If an error, handle it here 434 | Gaze.prototype._handleError = function (err) { 435 | if (err.code === 'EMFILE') { 436 | return this.emit('error', new Error('EMFILE: Too many opened files.')); 437 | } 438 | return this.emit('error', err); 439 | }; 440 | --------------------------------------------------------------------------------