├── .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 [](https://travis-ci.org/shama/gaze) [](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 | 
7 |
8 | [](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 |
--------------------------------------------------------------------------------