[Pkg-javascript-commits] [node-gaze] 01/03: Imported Upstream version 0.6.4
Sebastiaan Couwenberg
sebastic at moszumanska.debian.org
Fri Feb 27 18:27:20 UTC 2015
This is an automated email from the git hooks/post-receive script.
sebastic pushed a commit to branch master
in repository node-gaze.
commit 122588c83a26878834a44d2ce59352f56f0081d0
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date: Fri Feb 27 18:21:59 2015 +0100
Imported Upstream version 0.6.4
---
.editorconfig | 13 ++
.gitignore | 3 +
.jshintrc | 13 ++
.travis.yml | 6 +
AUTHORS | 19 ++
Gruntfile.js | 25 ++
LICENSE-MIT | 22 ++
README.md | 218 ++++++++++++++++++
benchmarks/benchmarker.js | 70 ++++++
benchmarks/changed.js | 27 +++
benchmarks/relative.js | 24 ++
benchmarks/startup.js | 22 ++
binding.gyp | 52 +++++
index.js | 23 ++
lib/gaze.js | 361 +++++++++++++++++++++++++++++
lib/gaze04.js | 467 ++++++++++++++++++++++++++++++++++++++
lib/helper.js | 122 ++++++++++
lib/pathwatcher.js | 220 ++++++++++++++++++
lib/platform.js | 178 +++++++++++++++
lib/statpoll.js | 69 ++++++
package.json | 66 ++++++
src/common.cc | 146 ++++++++++++
src/common.h | 76 +++++++
src/handle_map.cc | 158 +++++++++++++
src/handle_map.h | 56 +++++
src/main.cc | 35 +++
src/pathwatcher_linux.cc | 105 +++++++++
src/pathwatcher_mac.mm | 102 +++++++++
src/pathwatcher_win.cc | 329 +++++++++++++++++++++++++++
src/unsafe_persistent.h | 82 +++++++
test/add_test.js | 54 +++++
test/api_test.js | 83 +++++++
test/fixtures/Project (LO)/one.js | 1 +
test/fixtures/nested/one.js | 1 +
test/fixtures/nested/sub/two.js | 1 +
test/fixtures/nested/sub2/two.js | 1 +
test/fixtures/nested/three.js | 1 +
test/fixtures/one.js | 1 +
test/fixtures/sub/one.js | 1 +
test/fixtures/sub/two.js | 1 +
test/helper.js | 41 ++++
test/helper_test.js | 57 +++++
test/matching_test.js | 115 ++++++++++
test/patterns_test.js | 77 +++++++
test/platform_test.js | 135 +++++++++++
test/rename_test.js | 43 ++++
test/safewrite_test.js | 52 +++++
test/statpoll_test.js | 53 +++++
test/watch_test.js | 343 ++++++++++++++++++++++++++++
49 files changed, 4170 insertions(+)
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5d12634
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+# editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..93fd502
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+experiments
+build
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..f57a8ff
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,13 @@
+{
+ "curly": true,
+ "eqeqeq": true,
+ "immed": true,
+ "latedef": true,
+ "newcap": true,
+ "noarg": true,
+ "sub": true,
+ "undef": true,
+ "boss": true,
+ "eqnull": true,
+ "node": true
+}
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..049285e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,6 @@
+language: node_js
+node_js:
+ - '0.8'
+ - '0.10'
+before_script:
+ - npm install -g grunt-cli
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..11924e9
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,19 @@
+Kyle Robinson Young (http://dontkry.com)
+Sam Day (http://sam.is-super-awesome.com)
+Roarke Gaskill (http://starkinvestments.com)
+Lance Pollard (http://lancepollard.com/)
+Daniel Fagnan (http://hydrocodedesign.com/)
+Jonas (http://jpommerening.github.io/)
+Chris Chua (http://sirh.cc/)
+Kael Zhang (http://kael.me)
+Krasimir Tsonev (http://krasimirtsonev.com/blog)
+brett-shwom
+Kai Groner
+zeripath
+Tim Schaub (http://tschaub.net/)
+Amjad Masad (http://amasad.github.com/)
+Eric Schoffstall (http://contra.io/)
+Eric O'Connor (http://oco.nnor.org/)
+Cheng Zhao (https://github.com/zcbenz)
+Kevin Sawicki (https://github.com/kevinsawicki)
+Nathan Sobo (https://github.com/nathansobo)
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..521b9a9
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,25 @@
+module.exports = function(grunt) {
+ 'use strict';
+
+ grunt.option('stack', true);
+
+ grunt.initConfig({
+ nodeunit: {
+ files: ['test/*_test.js'],
+ },
+ jshint: {
+ options: { jshintrc: true },
+ all: ['Gruntfile.js', 'lib/**/*.js', 'test/*.js', 'benchmarks/*.js', '!lib/pathwatcher.js'],
+ },
+ });
+
+ // Dynamic alias task to nodeunit. Run individual tests with: grunt test:events
+ grunt.registerTask('test', function(file) {
+ grunt.config('nodeunit.files', String(grunt.config('nodeunit.files')).replace('*', file || '*'));
+ grunt.task.run('nodeunit');
+ });
+
+ grunt.loadNpmTasks('grunt-contrib-jshint');
+ grunt.loadNpmTasks('grunt-contrib-nodeunit');
+ grunt.registerTask('default', ['jshint', 'nodeunit']);
+};
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..39db241
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,22 @@
+Copyright (c) 2014 Kyle Robinson Young
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6652a21
--- /dev/null
+++ b/README.md
@@ -0,0 +1,218 @@
+# gaze [![Build Status](http://img.shields.io/travis/shama/gaze.svg)](https://travis-ci.org/shama/gaze) [![gittip.com/shama](http://img.shields.io/gittip/shama.svg)](https://www.gittip.com/shama)
+
+A globbing fs.watch wrapper built from the best parts of other fine watch libs.
+Compatible with Node.js 0.10/0.8, Windows, OSX and Linux.
+
+![gaze](http://dontkry.com/images/repos/gaze.png)
+
+## Features
+
+[![NPM](https://nodei.co/npm/gaze.png?downloads=true)](https://nodei.co/npm/gaze/)
+
+* Consistent events on OSX, Linux and Windows
+* Very fast start up and response time
+* High test coverage
+* Uses native OS events but falls back to stat polling
+* Option to force stat polling with special file systems such as networked
+* Downloaded over 400K times a month
+* Used by [Grunt](http://gruntjs.com), [gulp](http://gulpjs.com), [Tower](http://tower.github.io/) and many others
+
+## Usage
+Install the module with: `npm install gaze` or place into your `package.json`
+and run `npm install`.
+
+```javascript
+var gaze = require('gaze');
+
+// Watch all .js files/dirs in process.cwd()
+gaze('**/*.js', function(err, watcher) {
+ // Files have all started watching
+ // watcher === this
+
+ // Get all watched files
+ this.watched(function(watched) {
+ console.log(watched);
+ });
+
+ // On file changed
+ this.on('changed', function(filepath) {
+ console.log(filepath + ' was changed');
+ });
+
+ // On file added
+ this.on('added', function(filepath) {
+ console.log(filepath + ' was added');
+ });
+
+ // On file deleted
+ this.on('deleted', function(filepath) {
+ console.log(filepath + ' was deleted');
+ });
+
+ // On changed/added/deleted
+ this.on('all', function(event, filepath) {
+ console.log(filepath + ' was ' + event);
+ });
+
+ // Get watched files with relative paths
+ this.relative(function(err, files) {
+ console.log(files);
+ });
+});
+
+// Also accepts an array of patterns
+gaze(['stylesheets/*.css', 'images/**/*.png'], function() {
+ // Add more patterns later to be watched
+ this.add(['js/*.js']);
+});
+```
+
+### Alternate Interface
+
+```javascript
+var Gaze = require('gaze').Gaze;
+
+var gaze = new Gaze('**/*');
+
+// Files have all started watching
+gaze.on('ready', function(watcher) { });
+
+// A file has been added/changed/deleted has occurred
+gaze.on('all', function(event, filepath) { });
+```
+
+### Errors
+
+```javascript
+gaze('**/*', function(error, watcher) {
+ if (error) {
+ // Handle error if it occurred while starting up
+ }
+});
+
+// Or with the alternative interface
+var gaze = new Gaze();
+gaze.on('error', function(error) {
+ // Handle error here
+});
+gaze.add('**/*');
+```
+
+#### `EMFILE` errors
+
+By default, gaze will use native OS events and then fallback to slower stat polling when an `EMFILE` error is reached. Gaze will still emit or return the error as the first argument of the ready callback for you to handle.
+
+It is recommended to advise your users to increase their file descriptor limits to utilize the faster native OS watching. Especially on OSX where the default descriptor limit is 256.
+
+In some cases, native OS events will not work. Such as with networked file systems or vagrant. It is recommended to set the option `mode: 'poll'` to always stat poll for those situations.
+
+### Minimatch / Glob
+
+See [isaacs's minimatch](https://github.com/isaacs/minimatch) for more
+information on glob patterns.
+
+## Documentation
+
+### gaze([patterns, options, callback])
+
+* `patterns` {String|Array} File patterns to be matched
+* `options` {Object}
+* `callback` {Function}
+ * `err` {Error | null}
+ * `watcher` {Object} Instance of the Gaze watcher
+
+### Class: gaze.Gaze
+
+Create a Gaze object by instancing the `gaze.Gaze` class.
+
+```javascript
+var Gaze = require('gaze').Gaze;
+var gaze = new Gaze(pattern, options, callback);
+```
+
+#### Properties
+
+* `options` The options object passed in.
+ * `interval` {integer} Interval to pass to fs.watchFile
+ * `debounceDelay` {integer} Delay for events called in succession for the same
+ file/event
+ * `mode` {string} Force the watch mode. Either `'auto'` (default), `'watch'` (force native events), or `'poll'` (force stat polling).
+ * `cwd` {string} The current working directory to base file patterns from. Default is `process.cwd()`.
+
+#### Events
+
+* `ready(watcher)` When files have been globbed and watching has begun.
+* `all(event, filepath)` When an `added`, `changed` or `deleted` event occurs.
+* `added(filepath)` When a file has been added to a watch directory.
+* `changed(filepath)` When a file has been changed.
+* `deleted(filepath)` When a file has been deleted.
+* `renamed(newPath, oldPath)` When a file has been renamed.
+* `end()` When the watcher is closed and watches have been removed.
+* `error(err)` When an error occurs.
+* `nomatch` When no files have been matched.
+
+#### Methods
+
+* `emit(event, [...])` Wrapper for the EventEmitter.emit.
+ `added`|`changed`|`deleted` events will also trigger the `all` event.
+* `close()` Unwatch all files and reset the watch instance.
+* `add(patterns, callback)` Adds file(s) patterns to be watched.
+* `remove(filepath)` removes a file or directory from being watched. Does not
+ recurse directories.
+* `watched([callback])` Returns the currently watched files.
+ * `callback` {function} Calls with `function(err, files)`.
+* `relative([dir, unixify, callback])` Returns the currently watched files with relative paths.
+ * `dir` {string} Only return relative files for this directory.
+ * `unixify` {boolean} Return paths with `/` instead of `\\` if on Windows.
+ * `callback` {function} Calls with `function(err, files)`.
+
+## FAQs
+
+### Why Another `fs.watch` Wrapper?
+I liked parts of other `fs.watch` wrappers but none had all the features I
+needed when this library was originally written. This lib once combined the features I needed from other fine watch libs
+but now has taken on a life of it's own (**gaze doesn't wrap `fs.watch` or `fs.watchFile` anymore**).
+
+Other great watch libraries to try are:
+
+* [paulmillr's chokidar](https://github.com/paulmillr/chokidar)
+* [mikeal's watch](https://github.com/mikeal/watch)
+* [github's pathwatcher](https://github.com/atom/node-pathwatcher)
+* [bevry's watchr](https://github.com/bevry/watchr)
+
+## Contributing
+In lieu of a formal styleguide, take care to maintain the existing coding style.
+Add unit tests for any new or changed functionality. Lint and test your code
+using [grunt](http://gruntjs.com/).
+
+## Release History
+* 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).
+* 0.6.3 - Add support for node v0.11
+* 0.6.2 - Fix argument error with watched(). Fix for erroneous added events on folders. Ignore msvs build error 4244.
+* 0.6.1 - Fix for absolute paths.
+* 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 at 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.
+* 0.5.1 - Use setImmediate (process.nextTick for node v0.8) to defer ready/nomatch events (@amasad).
+* 0.5.0 - Process is now kept alive while watching files. Emits a nomatch event when no files are matching.
+* 0.4.3 - Track file additions in newly created folders (@brett-shwom).
+* 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).
+* 0.4.1 - Fix watchDir not respecting close in race condition (@chrisirhc).
+* 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).
+* 0.3.4 - Code clean up. Fix path must be strings errors (@groner). Fix incorrect added events (@groner).
+* 0.3.3 - Fix for multiple patterns with negate.
+* 0.3.2 - Emit `end` before removeAllListeners.
+* 0.3.1 - Fix added events within subfolder patterns.
+* 0.3.0 - Handle safewrite events, `forceWatchMethod` option removed, bug fixes and watch optimizations (@rgaskill).
+* 0.2.2 - Fix issue where subsequent add calls dont get watched (@samcday). removeAllListeners on close.
+* 0.2.1 - Fix issue with invalid `added` events in current working dir.
+* 0.2.0 - Support and mark folders with `path.sep`. Add `forceWatchMethod` option. Support `renamed` events.
+* 0.1.6 - Recognize the `cwd` option properly
+* 0.1.5 - Catch too many open file errors
+* 0.1.4 - Really fix the race condition with 2 watches
+* 0.1.3 - Fix race condition with 2 watches
+* 0.1.2 - Read triggering changed event fix
+* 0.1.1 - Minor fixes
+* 0.1.0 - Initial release
+
+## License
+Copyright (c) 2014 Kyle Robinson Young
+Licensed under the MIT license.
diff --git a/benchmarks/benchmarker.js b/benchmarks/benchmarker.js
new file mode 100644
index 0000000..026bbcb
--- /dev/null
+++ b/benchmarks/benchmarker.js
@@ -0,0 +1,70 @@
+'use strict';
+
+var async = require('async');
+var fs = require('fs');
+var rimraf = require('rimraf');
+var path = require('path');
+var AsciiTable = require('ascii-table');
+var readline = require('readline');
+
+function Benchmarker(opts) {
+ if (!(this instanceof Benchmarker)) return new Benchmarker(opts);
+ opts = opts || {};
+ this.table = new AsciiTable(opts.name || 'benchmark');
+ this.tmpDir = opts.tmpDir || path.resolve(__dirname, 'tmp');
+ var max = opts.max || 2000;
+ var multiplesOf = opts.multiplesOf || 100;
+ this.fileNums = [];
+ for (var i = 0; i <= max / multiplesOf; i++) {
+ this.fileNums.push(i * multiplesOf);
+ }
+ this.startTime = 0;
+ this.files = [];
+}
+module.exports = Benchmarker;
+
+Benchmarker.prototype.log = function() {
+ readline.cursorTo(process.stdout, 0, 0);
+ readline.clearScreenDown(process.stdout);
+ this.table.addRow.apply(this.table, arguments);
+ console.log(this.table.toString());
+};
+
+Benchmarker.prototype.setup = function(num) {
+ this.teardown();
+ fs.mkdirSync(this.tmpDir);
+ this.files = [];
+ for (var i = 0; i <= num; i++) {
+ var file = path.join(this.tmpDir, 'test-' + i + '.txt');
+ fs.writeFileSync(file, String(i));
+ this.files.push(file);
+ }
+};
+
+Benchmarker.prototype.teardown = function() {
+ if (fs.existsSync(this.tmpDir)) {
+ rimraf.sync(this.tmpDir);
+ }
+};
+
+Benchmarker.prototype.run = function(fn, done) {
+ var self = this;
+ async.eachSeries(this.fileNums, function(num, next) {
+ self.setup(num);
+ fn(num, function() {
+ process.nextTick(next);
+ });
+ }, function() {
+ self.teardown();
+ done();
+ });
+};
+
+Benchmarker.prototype.start = function() {
+ this.startTime = process.hrtime();
+};
+
+Benchmarker.prototype.end = function(radix) {
+ var diff = process.hrtime(this.startTime);
+ return ((diff[0] * 1e9 + diff[1]) * 0.000001).toFixed(radix || 2).replace(/\d(?=(\d{3})+\.)/g, '$&,') + 'ms';
+};
diff --git a/benchmarks/changed.js b/benchmarks/changed.js
new file mode 100644
index 0000000..4a84d9d
--- /dev/null
+++ b/benchmarks/changed.js
@@ -0,0 +1,27 @@
+'use strict';
+
+var gaze = require('../');
+var path = require('path');
+var fs = require('fs');
+var Benchmarker = require('./benchmarker');
+
+var b = new Benchmarker({ name: path.basename(__filename) });
+b.table.setHeading('files', 'ms').setAlign(0, 2).setAlign(1, 2);
+b.run(function(num, done) {
+ gaze('**/*', {cwd: b.tmpDir, maxListeners:0}, function(err, watcher) {
+ if (err) {
+ console.error(err.code + ': ' + err.message);
+ return process.exit();
+ }
+ watcher.on('changed', function() {
+ b.log(num, b.end());
+ watcher.close();
+ });
+ watcher.on('end', done);
+ var randFile = path.join(b.tmpDir, 'test-' + Math.floor(Math.random() * num) + '.txt');
+ b.start();
+ fs.writeFileSync(randFile, '1234');
+ });
+}, function() {
+ process.exit();
+});
diff --git a/benchmarks/relative.js b/benchmarks/relative.js
new file mode 100644
index 0000000..fdf49a5
--- /dev/null
+++ b/benchmarks/relative.js
@@ -0,0 +1,24 @@
+'use strict';
+
+var gaze = require('../');
+var path = require('path');
+var Benchmarker = require('./benchmarker');
+
+var b = new Benchmarker({ name: path.basename(__filename) });
+b.table.setHeading('files', 'ms').setAlign(0, 2).setAlign(1, 2);
+b.run(function(num, done) {
+ gaze('**/*', {cwd: b.tmpDir, maxListeners:0}, function(err, watcher) {
+ if (err) {
+ console.error(err.code + ': ' + err.message);
+ return process.exit();
+ }
+ b.start();
+ this.relative('.', function(err, files) {
+ b.log(num, b.end());
+ watcher.on('end', done);
+ watcher.close();
+ });
+ });
+}, function() {
+ process.exit();
+});
diff --git a/benchmarks/startup.js b/benchmarks/startup.js
new file mode 100644
index 0000000..ddbe346
--- /dev/null
+++ b/benchmarks/startup.js
@@ -0,0 +1,22 @@
+'use strict';
+
+var gaze = require('../');
+var path = require('path');
+var Benchmarker = require('./benchmarker');
+
+var b = new Benchmarker({ name: path.basename(__filename) });
+b.table.setHeading('files', 'ms').setAlign(0, 2).setAlign(1, 2);
+b.run(function(num, done) {
+ b.start();
+ gaze('**/*', {cwd: b.tmpDir, maxListeners:0}, function(err, watcher) {
+ if (err) {
+ console.error(err.code + ': ' + err.message);
+ return process.exit();
+ }
+ b.log(num, b.end());
+ watcher.on('end', done);
+ watcher.close();
+ });
+}, function() {
+ process.exit();
+});
diff --git a/binding.gyp b/binding.gyp
new file mode 100644
index 0000000..08ebb1f
--- /dev/null
+++ b/binding.gyp
@@ -0,0 +1,52 @@
+{
+ "targets": [
+ {
+ "target_name": "pathwatcher",
+ "sources": [
+ "src/main.cc",
+ "src/common.cc",
+ "src/common.h",
+ "src/handle_map.cc",
+ "src/handle_map.h",
+ "src/unsafe_persistent.h",
+ ],
+ "include_dirs": [
+ "src",
+ '<!(node -e "require(\'nan\')")'
+ ],
+ "conditions": [
+ ['OS=="win"', {
+ "sources": [
+ "src/pathwatcher_win.cc",
+ ],
+ 'msvs_settings': {
+ 'VCCLCompilerTool': {
+ 'ExceptionHandling': 1, # /EHsc
+ 'WarnAsError': 'true',
+ },
+ },
+ 'msvs_disabled_warnings': [
+ 4018, # signed/unsigned mismatch
+ 4267, 4244, # conversion from 'size_t' to 'int', possible loss of data
+ 4530, # C++ exception handler used, but unwind semantics are not enabled
+ 4506, # no definition for inline function
+ 4996, # function was declared deprecated
+ ],
+ 'defines': [
+ '_WIN32_WINNT=0x0600',
+ ],
+ }], # OS=="win"
+ ['OS=="mac"', {
+ "sources": [
+ "src/pathwatcher_mac.mm",
+ ],
+ }], # OS=="mac"
+ ['OS=="linux"', {
+ "sources": [
+ "src/pathwatcher_linux.cc",
+ ],
+ }], # OS=="linux"
+ ],
+ }
+ ]
+}
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..a2698d2
--- /dev/null
+++ b/index.js
@@ -0,0 +1,23 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2014 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
+// If on node v0.8, serve gaze04
+var version = process.versions.node.split('.');
+if (version[0] === '0' && version[1] === '8') {
+ module.exports = require('./lib/gaze04.js');
+} else {
+ try {
+ // Check whether pathwatcher successfully built without running it
+ require('bindings')({ bindings: 'pathwatcher.node', path: true });
+ // If successfully built, give the better version
+ module.exports = require('./lib/gaze.js');
+ } catch (err) {
+ // Otherwise serve gaze04
+ module.exports = require('./lib/gaze04.js');
+ }
+}
diff --git a/lib/gaze.js b/lib/gaze.js
new file mode 100644
index 0000000..948e962
--- /dev/null
+++ b/lib/gaze.js
@@ -0,0 +1,361 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2014 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+// libs
+var util = require('util');
+var EE = require('events').EventEmitter;
+var fs = require('graceful-fs');
+var path = require('path');
+var globule = require('globule');
+var nextback = require('nextback');
+var helper = require('./helper');
+var platform = require('./platform');
+var isAbsolute = require('absolute-path');
+
+// keep track of instances to call multiple times for backwards compatibility
+var instances = [];
+
+// `Gaze` EventEmitter object to return in the callback
+function Gaze(patterns, opts, done) {
+ var self = this;
+ EE.call(self);
+
+ // Optional arguments
+ if (typeof patterns === 'function') {
+ done = patterns;
+ patterns = null;
+ opts = {};
+ }
+ if (typeof opts === 'function') {
+ done = opts;
+ opts = {};
+ }
+
+ // Default options
+ opts = opts || {};
+ opts.mark = true;
+ opts.interval = opts.interval || 500;
+ opts.debounceDelay = opts.debounceDelay || 500;
+ opts.cwd = opts.cwd || process.cwd();
+ this.options = opts;
+
+ // Default error handler to prevent emit('error') throwing magically for us
+ this.on('error', function(error) {
+ if (self.listeners('error').length > 1) {
+ return self.removeListener('error', this);
+ }
+ nextback(function() {
+ done.call(self, error, self);
+ })();
+ });
+
+ // File watching mode to use when adding files to the platform
+ this._mode = opts.mode || 'auto';
+
+ // Default done callback
+ done = done || function() {};
+
+ // Remember our watched dir:files
+ this._watched = Object.create(null);
+
+ // Store watchers
+ this._watchers = Object.create(null);
+
+ // Store watchFile listeners
+ this._pollers = Object.create(null);
+
+ // Store patterns
+ this._patterns = [];
+
+ // Cached events for debouncing
+ this._cached = Object.create(null);
+
+ // Set maxListeners
+ if (this.options.maxListeners != null) {
+ this.setMaxListeners(this.options.maxListeners);
+ Gaze.super_.prototype.setMaxListeners(this.options.maxListeners);
+ delete this.options.maxListeners;
+ }
+
+ // Initialize the watch on files
+ if (patterns) {
+ this.add(patterns, done);
+ }
+
+ // keep the process alive
+ this._keepalive = setInterval(platform.tick.bind(platform), opts.interval);
+
+ // Keep track of all instances created
+ this._instanceNum = instances.length;
+ instances.push(this);
+
+ // Keep track of safewriting and debounce timeouts
+ this._safewriting = null;
+ this._safewriteTimeout = null;
+ this._timeoutId = null;
+
+ return this;
+}
+util.inherits(Gaze, EE);
+
+// Main entry point. Start watching and call done when setup
+module.exports = function gaze(patterns, opts, done) {
+ return new Gaze(patterns, opts, done);
+};
+module.exports.Gaze = Gaze;
+
+// Override the emit function to emit `all` events
+// and debounce on duplicate events per file
+Gaze.prototype.emit = function() {
+ var self = this;
+ var args = arguments;
+
+ var e = args[0];
+ var filepath = args[1];
+
+ // If not added/deleted/changed/renamed then just emit the event
+ if (e.slice(-2) !== 'ed') {
+ Gaze.super_.prototype.emit.apply(self, args);
+ return this;
+ }
+
+ // Detect rename event, if added and previous deleted is in the cache
+ if (e === 'added') {
+ Object.keys(this._cached).forEach(function(oldFile) {
+ if (self._cached[oldFile].indexOf('deleted') !== -1) {
+ args[0] = e = 'renamed';
+ [].push.call(args, oldFile);
+ delete self._cached[oldFile];
+ return false;
+ }
+ });
+ }
+
+ // Detect safewrite events, if file is deleted and then added/renamed, assume a safewrite happened
+ if (e === 'deleted' && this._safewriting == null) {
+ this._safewriting = filepath;
+ this._safewriteTimeout = setTimeout(function() {
+ // Just a normal delete, carry on
+ Gaze.super_.prototype.emit.apply(self, args);
+ Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1)));
+ self._safewriting = null;
+ }, this.options.debounceDelay);
+ return this;
+ } else if ((e === 'added' || e === 'renamed') && this._safewriting === filepath) {
+ clearTimeout(this._safewriteTimeout);
+ this._safewriteTimeout = setTimeout(function() {
+ self._safewriting = null;
+ }, this.options.debounceDelay);
+ args[0] = e = 'changed';
+ } else if (e === 'deleted' && this._safewriting === filepath) {
+ return this;
+ }
+
+ // If cached doesnt exist, create a delay before running the next
+ // then emit the event
+ var cache = this._cached[filepath] || [];
+ if (cache.indexOf(e) === -1) {
+ helper.objectPush(self._cached, filepath, e);
+ clearTimeout(this._timeoutId);
+ this._timeoutId = setTimeout(function() {
+ delete self._cached[filepath];
+ }, this.options.debounceDelay);
+ // Emit the event and `all` event
+ Gaze.super_.prototype.emit.apply(self, args);
+ Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1)));
+ }
+
+ // Detect if new folder added to trigger for matching files within folder
+ if (e === 'added') {
+ if (helper.isDir(filepath)) {
+ fs.readdirSync(filepath).map(function(file) {
+ return path.join(filepath, file);
+ }).filter(function(file) {
+ return globule.isMatch(self._patterns, file, self.options);
+ }).forEach(function(file) {
+ self.emit('added', file);
+ });
+ }
+ }
+
+ return this;
+};
+
+// Close watchers
+Gaze.prototype.close = function(_reset) {
+ instances.splice(this._instanceNum, 1);
+ platform.closeAll();
+ this.emit('end');
+};
+
+// Add file patterns to be watched
+Gaze.prototype.add = function(files, done) {
+ var self = this;
+ if (typeof files === 'string') { files = [files]; }
+ this._patterns = helper.unique.apply(null, [this._patterns, files]);
+ files = globule.find(this._patterns, this.options);
+
+ // If no matching files
+ if (files.length < 1) {
+ // Defer to emitting to give a chance to attach event handlers.
+ nextback(function() {
+ self.emit('ready', self);
+ if (done) done.call(self, null, self);
+ self.emit('nomatch');
+ })();
+ return;
+ }
+
+ // Set the platform mode before adding files
+ platform.mode = self._mode;
+
+ helper.forEachSeries(files, function(file, next) {
+ try {
+ var filepath = (!isAbsolute(file)) ? path.join(self.options.cwd, file) : file;
+ platform(filepath, self._trigger.bind(self));
+ } catch (err) {
+ self.emit('error', err);
+ }
+ next();
+ }, function() {
+ // A little delay here for backwards compatibility, lol
+ setTimeout(function() {
+ self.emit('ready', self);
+ if (done) done.call(self, null, self);
+ }, 10);
+ });
+};
+
+// Call when the platform has triggered
+Gaze.prototype._trigger = function(error, event, filepath, newFile) {
+ if (error) { return this.emit('error', error); }
+
+ // Set the platform mode before adding files
+ platform.mode = this._mode;
+
+ if (event === 'change' && helper.isDir(filepath)) {
+ this._wasAdded(filepath);
+ } else if (event === 'change') {
+ this._emitAll('changed', filepath);
+ } else if (event === 'delete') {
+ // Close out deleted filepaths (important to make safewrite detection work)
+ platform.close(filepath);
+ this._emitAll('deleted', filepath);
+ } else if (event === 'rename') {
+ // TODO: This occasionally throws, figure out why or use the old style rename detect
+ // The handle(26) returned by watching [filename] is the same with an already watched path([filename])
+ this._emitAll('renamed', newFile, filepath);
+ }
+};
+
+// If a folder received a change event, investigate
+Gaze.prototype._wasAdded = function(dir) {
+ var self = this;
+ var dirstat = fs.statSync(dir);
+ fs.readdir(dir, function(err, current) {
+ if (err) {
+ self.emit('error', err);
+ return;
+ }
+ helper.forEachSeries(current, function(file, next) {
+ var filepath = path.join(dir, file);
+ if (!fs.existsSync(filepath)) return next();
+ var stat = fs.lstatSync(filepath);
+ if ((dirstat.mtime - stat.mtime) <= 0) {
+ var relpath = path.relative(self.options.cwd, filepath);
+ if (stat.isDirectory()) {
+ // If it was a dir, watch the dir and emit that it was added
+ platform(filepath, self._trigger.bind(self));
+ if (globule.isMatch(self._patterns, relpath, self.options)) {
+ self._emitAll('added', filepath);
+ }
+ self._wasAddedSub(filepath);
+ } else if (globule.isMatch(self._patterns, relpath, self.options)) {
+ // Otherwise if the file matches, emit added
+ platform(filepath, self._trigger.bind(self));
+ if (globule.isMatch(self._patterns, relpath, self.options)) {
+ self._emitAll('added', filepath);
+ }
+ }
+ }
+ next();
+ });
+ });
+};
+
+// If a sub folder was added, investigate further
+// Such as with grunt.file.write('new_dir/tmp.js', '') as it will create the folder and file simultaneously
+Gaze.prototype._wasAddedSub = function(dir) {
+ var self = this;
+ fs.readdir(dir, function(err, current) {
+ helper.forEachSeries(current, function(file, next) {
+ var filepath = path.join(dir, file);
+ var relpath = path.relative(self.options.cwd, filepath);
+ try {
+ if (fs.lstatSync(filepath).isDirectory()) {
+ self._wasAdded(filepath);
+ } else if (globule.isMatch(self._patterns, relpath, self.options)) {
+ // Make sure to watch the newly added sub file
+ platform(filepath, self._trigger.bind(self));
+ self._emitAll('added', filepath);
+ }
+ } catch (err) {
+ self.emit('error', err);
+ }
+ next();
+ });
+ });
+};
+
+// Wrapper for emit to ensure we emit on all instances
+Gaze.prototype._emitAll = function() {
+ var args = Array.prototype.slice.call(arguments);
+ for (var i = 0; i < instances.length; i++) {
+ instances[i].emit.apply(instances[i], args);
+ }
+};
+
+// Remove file/dir from `watched`
+Gaze.prototype.remove = function(file) {
+ platform.close(file);
+ return this;
+};
+
+// Return watched files
+Gaze.prototype.watched = function(done) {
+ done = nextback(done || function() {});
+ helper.flatToTree(platform.getWatchedPaths(), this.options.cwd, false, false, done);
+ return this;
+};
+
+// Returns `watched` files with relative paths to cwd
+Gaze.prototype.relative = function(dir, unixify, done) {
+ if (typeof dir === 'function') {
+ done = dir;
+ dir = null;
+ unixify = false;
+ }
+ if (typeof unixify === 'function') {
+ done = unixify;
+ unixify = false;
+ }
+ done = nextback(done || function() {});
+ helper.flatToTree(platform.getWatchedPaths(), this.options.cwd, true, unixify, function(err, relative) {
+ if (dir) {
+ if (unixify) {
+ dir = helper.unixifyPathSep(dir);
+ }
+ // Better guess what to return for backwards compatibility
+ return done(null, relative[dir] || relative[dir + (unixify ? '/' : path.sep)] || []);
+ }
+ return done(null, relative);
+ });
+ return this;
+};
diff --git a/lib/gaze04.js b/lib/gaze04.js
new file mode 100644
index 0000000..64ae83e
--- /dev/null
+++ b/lib/gaze04.js
@@ -0,0 +1,467 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2013 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+// libs
+var util = require('util');
+var EE = require('events').EventEmitter;
+var fs = require('graceful-fs');
+var path = require('path');
+var globule = require('globule');
+var helper = require('./helper');
+var nextback = require('nextback');
+
+// globals
+var delay = 10;
+
+// `Gaze` EventEmitter object to return in the callback
+function Gaze(patterns, opts, done) {
+ var self = this;
+ EE.call(self);
+
+ // Optional arguments
+ if (typeof patterns === 'function') {
+ done = patterns;
+ patterns = null;
+ opts = {};
+ }
+ if (typeof opts === 'function') {
+ done = opts;
+ opts = {};
+ }
+
+ // Default options
+ opts = opts || {};
+ opts.mark = true;
+ opts.interval = opts.interval || 100;
+ opts.debounceDelay = opts.debounceDelay || 500;
+ opts.cwd = opts.cwd || process.cwd();
+ this.options = opts;
+
+ // Default error handler to prevent emit('error') throwing magically for us
+ this.on('error', function(error) {
+ if (self.listeners('error').length > 1) {
+ return self.removeListener('error', this);
+ }
+ nextback(function() {
+ done.call(self, error, self);
+ })();
+ });
+
+ // Default done callback
+ done = done || function() {};
+
+ // Remember our watched dir:files
+ this._watched = Object.create(null);
+
+ // Store watchers
+ this._watchers = Object.create(null);
+
+ // Store watchFile listeners
+ this._pollers = Object.create(null);
+
+ // Store patterns
+ this._patterns = [];
+
+ // Cached events for debouncing
+ this._cached = Object.create(null);
+
+ // Set maxListeners
+ if (this.options.maxListeners != null) {
+ this.setMaxListeners(this.options.maxListeners);
+ Gaze.super_.prototype.setMaxListeners(this.options.maxListeners);
+ delete this.options.maxListeners;
+ }
+
+ // Initialize the watch on files
+ if (patterns) {
+ this.add(patterns, done);
+ }
+
+ // keep the process alive
+ this._keepalive = setInterval(function() {}, 200);
+
+ return this;
+}
+util.inherits(Gaze, EE);
+
+// Main entry point. Start watching and call done when setup
+module.exports = function gaze(patterns, opts, done) {
+ return new Gaze(patterns, opts, done);
+};
+module.exports.Gaze = Gaze;
+
+// Override the emit function to emit `all` events
+// and debounce on duplicate events per file
+Gaze.prototype.emit = function() {
+ var self = this;
+ var args = arguments;
+
+ var e = args[0];
+ var filepath = args[1];
+ var timeoutId;
+
+ // If not added/deleted/changed/renamed then just emit the event
+ if (e.slice(-2) !== 'ed') {
+ Gaze.super_.prototype.emit.apply(self, args);
+ return this;
+ }
+
+ // Detect rename event, if added and previous deleted is in the cache
+ if (e === 'added') {
+ Object.keys(this._cached).forEach(function(oldFile) {
+ if (self._cached[oldFile].indexOf('deleted') !== -1) {
+ args[0] = e = 'renamed';
+ [].push.call(args, oldFile);
+ delete self._cached[oldFile];
+ return false;
+ }
+ });
+ }
+
+ // If cached doesnt exist, create a delay before running the next
+ // then emit the event
+ var cache = this._cached[filepath] || [];
+ if (cache.indexOf(e) === -1) {
+ helper.objectPush(self._cached, filepath, e);
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(function() {
+ delete self._cached[filepath];
+ }, this.options.debounceDelay);
+ // Emit the event and `all` event
+ Gaze.super_.prototype.emit.apply(self, args);
+ Gaze.super_.prototype.emit.apply(self, ['all', e].concat([].slice.call(args, 1)));
+ }
+
+ // Detect if new folder added to trigger for matching files within folder
+ if (e === 'added') {
+ if (helper.isDir(filepath)) {
+ fs.readdirSync(filepath).map(function(file) {
+ return path.join(filepath, file);
+ }).filter(function(file) {
+ return globule.isMatch(self._patterns, file, self.options);
+ }).forEach(function(file) {
+ self.emit('added', file);
+ });
+ }
+ }
+
+ return this;
+};
+
+// Close watchers
+Gaze.prototype.close = function(_reset) {
+ var self = this;
+ _reset = _reset === false ? false : true;
+ Object.keys(self._watchers).forEach(function(file) {
+ self._watchers[file].close();
+ });
+ self._watchers = Object.create(null);
+ Object.keys(this._watched).forEach(function(dir) {
+ self._unpollDir(dir);
+ });
+ if (_reset) {
+ self._watched = Object.create(null);
+ setTimeout(function() {
+ self.emit('end');
+ self.removeAllListeners();
+ clearInterval(self._keepalive);
+ }, delay + 100);
+ }
+ return self;
+};
+
+// Add file patterns to be watched
+Gaze.prototype.add = function(files, done) {
+ if (typeof files === 'string') { files = [files]; }
+ this._patterns = helper.unique.apply(null, [this._patterns, files]);
+ files = globule.find(this._patterns, this.options);
+ this._addToWatched(files);
+ this.close(false);
+ this._initWatched(done);
+};
+
+// Dont increment patterns and dont call done if nothing added
+Gaze.prototype._internalAdd = function(file, done) {
+ var files = [];
+ if (helper.isDir(file)) {
+ files = [helper.markDir(file)].concat(globule.find(this._patterns, this.options));
+ } else {
+ if (globule.isMatch(this._patterns, file, this.options)) {
+ files = [file];
+ }
+ }
+ if (files.length > 0) {
+ this._addToWatched(files);
+ this.close(false);
+ this._initWatched(done);
+ }
+};
+
+// Remove file/dir from `watched`
+Gaze.prototype.remove = function(file) {
+ var self = this;
+ if (this._watched[file]) {
+ // is dir, remove all files
+ this._unpollDir(file);
+ delete this._watched[file];
+ } else {
+ // is a file, find and remove
+ Object.keys(this._watched).forEach(function(dir) {
+ var index = self._watched[dir].indexOf(file);
+ if (index !== -1) {
+ self._unpollFile(file);
+ self._watched[dir].splice(index, 1);
+ return false;
+ }
+ });
+ }
+ if (this._watchers[file]) {
+ this._watchers[file].close();
+ }
+ return this;
+};
+
+// Return watched files
+Gaze.prototype.watched = function(done) {
+ done(null, this._watched);
+ return this;
+};
+
+// Returns `watched` files with relative paths to process.cwd()
+Gaze.prototype.relative = function(dir, unixify, done) {
+ if (typeof dir === 'function') {
+ done = dir;
+ dir = null;
+ unixify = false;
+ }
+ if (typeof unixify === 'function') {
+ done = unixify;
+ unixify = false;
+ }
+ var self = this;
+ var relative = Object.create(null);
+ var relDir, relFile, unixRelDir;
+ var cwd = this.options.cwd || process.cwd();
+ if (dir === '') { dir = '.'; }
+ dir = helper.markDir(dir);
+ unixify = unixify || false;
+ Object.keys(this._watched).forEach(function(dir) {
+ relDir = path.relative(cwd, dir) + path.sep;
+ if (relDir === path.sep) { relDir = '.'; }
+ unixRelDir = unixify ? helper.unixifyPathSep(relDir) : relDir;
+ relative[unixRelDir] = self._watched[dir].map(function(file) {
+ relFile = path.relative(path.join(cwd, relDir) || '', file || '');
+ if (helper.isDir(file)) {
+ relFile = helper.markDir(relFile);
+ }
+ if (unixify) {
+ relFile = helper.unixifyPathSep(relFile);
+ }
+ return relFile;
+ });
+ });
+ if (dir && unixify) {
+ dir = helper.unixifyPathSep(dir);
+ }
+ var result = dir ? relative[dir] || [] : relative;
+ // For consistency. GH-74
+ if (result['.']) {
+ result['./'] = result['.'];
+ delete result['.'];
+ }
+ done(null, result);
+ return self;
+};
+
+// Adds files and dirs to watched
+Gaze.prototype._addToWatched = function(files) {
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+ var filepath = path.resolve(this.options.cwd, file);
+
+ var dirname = (helper.isDir(file)) ? filepath : path.dirname(filepath);
+ dirname = helper.markDir(dirname);
+
+ // If a new dir is added
+ if (helper.isDir(file) && !(filepath in this._watched)) {
+ helper.objectPush(this._watched, filepath, []);
+ }
+
+ if (file.slice(-1) === '/') { filepath += path.sep; }
+ helper.objectPush(this._watched, path.dirname(filepath) + path.sep, filepath);
+
+ // add folders into the mix
+ var readdir = fs.readdirSync(dirname);
+ for (var j = 0; j < readdir.length; j++) {
+ var dirfile = path.join(dirname, readdir[j]);
+ if (fs.lstatSync(dirfile).isDirectory()) {
+ helper.objectPush(this._watched, dirname, dirfile + path.sep);
+ }
+ }
+ }
+ return this;
+};
+
+Gaze.prototype._watchDir = function(dir, done) {
+ var self = this;
+ var timeoutId;
+ // Dont even try watching the dir if it doesnt exist
+ if (!fs.existsSync(dir)) { return; }
+ try {
+ this._watchers[dir] = fs.watch(dir, function(event) {
+ // race condition. Let's give the fs a little time to settle down. so we
+ // don't fire events on non existent files.
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(function() {
+ // race condition. Ensure that this directory is still being watched
+ // before continuing.
+ if ((dir in self._watchers) && fs.existsSync(dir)) {
+ done(null, dir);
+ }
+ }, delay + 100);
+ });
+ } catch (err) {
+ return this._handleError(err);
+ }
+ return this;
+};
+
+Gaze.prototype._unpollFile = function(file) {
+ if (this._pollers[file]) {
+ fs.unwatchFile(file, this._pollers[file] );
+ delete this._pollers[file];
+ }
+ return this;
+};
+
+Gaze.prototype._unpollDir = function(dir) {
+ this._unpollFile(dir);
+ for (var i = 0; i < this._watched[dir].length; i++) {
+ this._unpollFile(this._watched[dir][i]);
+ }
+};
+
+Gaze.prototype._pollFile = function(file, done) {
+ var opts = { persistent: true, interval: this.options.interval };
+ if (!this._pollers[file]) {
+ this._pollers[file] = function(curr, prev) {
+ done(null, file);
+ };
+ try {
+ fs.watchFile(file, opts, this._pollers[file]);
+ } catch (err) {
+ return this._handleError(err);
+ }
+ }
+ return this;
+};
+
+// Initialize the actual watch on `watched` files
+Gaze.prototype._initWatched = function(done) {
+ var self = this;
+ var cwd = this.options.cwd || process.cwd();
+ var curWatched = Object.keys(self._watched);
+
+ // if no matching files
+ if (curWatched.length < 1) {
+ self.emit('ready', self);
+ if (done) { done.call(self, null, self); }
+ self.emit('nomatch');
+ return;
+ }
+
+ helper.forEachSeries(curWatched, function(dir, next) {
+ dir = dir || '';
+ var files = self._watched[dir];
+ // Triggered when a watched dir has an event
+ self._watchDir(dir, function(event, dirpath) {
+ var relDir = cwd === dir ? '.' : path.relative(cwd, dir);
+ relDir = relDir || '';
+
+ fs.readdir(dirpath, function(err, current) {
+ if (err) { return self.emit('error', err); }
+ if (!current) { return; }
+
+ try {
+ // append path.sep to directories so they match previous.
+ current = current.map(function(curPath) {
+ if (fs.existsSync(path.join(dir, curPath)) && fs.lstatSync(path.join(dir, curPath)).isDirectory()) {
+ return curPath + path.sep;
+ } else {
+ return curPath;
+ }
+ });
+ } catch (err) {
+ // race condition-- sometimes the file no longer exists
+ }
+
+ // Get watched files for this dir
+ self.relative(relDir, function(err, previous) {
+
+ // If file was deleted
+ previous.filter(function(file) {
+ return current.indexOf(file) < 0;
+ }).forEach(function(file) {
+ if (!helper.isDir(file)) {
+ var filepath = path.join(dir, file);
+ self.remove(filepath);
+ self.emit('deleted', filepath);
+ }
+ });
+
+ // If file was added
+ current.filter(function(file) {
+ return previous.indexOf(file) < 0;
+ }).forEach(function(file) {
+ // Is it a matching pattern?
+ var relFile = path.join(relDir, file);
+ // Add to watch then emit event
+ self._internalAdd(relFile, function() {
+ self.emit('added', path.join(dir, file));
+ });
+ });
+
+ });
+
+ });
+ });
+
+ // Watch for change/rename events on files
+ files.forEach(function(file) {
+ if (helper.isDir(file)) { return; }
+ self._pollFile(file, function(err, filepath) {
+ // Only emit changed if the file still exists
+ // Prevents changed/deleted duplicate events
+ if (fs.existsSync(filepath)) {
+ self.emit('changed', filepath);
+ }
+ });
+ });
+
+ next();
+ }, function() {
+
+ // Return this instance of Gaze
+ // delay before ready solves a lot of issues
+ setTimeout(function() {
+ self.emit('ready', self);
+ if (done) { done.call(self, null, self); }
+ }, delay + 100);
+
+ });
+};
+
+// If an error, handle it here
+Gaze.prototype._handleError = function(err) {
+ if (err.code === 'EMFILE') {
+ return this.emit('error', new Error('EMFILE: Too many opened files.'));
+ }
+ return this.emit('error', err);
+};
\ No newline at end of file
diff --git a/lib/helper.js b/lib/helper.js
new file mode 100644
index 0000000..526fee7
--- /dev/null
+++ b/lib/helper.js
@@ -0,0 +1,122 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2014 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+var path = require('path');
+var helper = module.exports = {};
+
+// Returns boolean whether filepath is dir terminated
+helper.isDir = function isDir(dir) {
+ if (typeof dir !== 'string') { return false; }
+ return (dir.slice(-(path.sep.length)) === path.sep) || (dir.slice(-1) === '/');
+};
+
+// Create a `key:[]` if doesnt exist on `obj` then push or concat the `val`
+helper.objectPush = function objectPush(obj, key, val) {
+ if (obj[key] == null) { obj[key] = []; }
+ if (Array.isArray(val)) { obj[key] = obj[key].concat(val); }
+ else if (val) { obj[key].push(val); }
+ return obj[key] = helper.unique(obj[key]);
+};
+
+// Ensures the dir is marked with path.sep
+helper.markDir = function markDir(dir) {
+ if (typeof dir === 'string' &&
+ dir.slice(-(path.sep.length)) !== path.sep &&
+ dir !== '.') {
+ dir += path.sep;
+ }
+ return dir;
+};
+
+// Changes path.sep to unix ones for testing
+helper.unixifyPathSep = function unixifyPathSep(filepath) {
+ return (process.platform === 'win32') ? String(filepath).replace(/\\/g, '/') : filepath;
+};
+
+// Converts a flat list of paths to the old style tree
+helper.flatToTree = function flatToTree(files, cwd, relative, unixify, done) {
+ cwd = helper.markDir(cwd);
+ var tree = Object.create(null);
+
+ helper.forEachSeries(files, function(filepath, next) {
+ var parent = path.dirname(filepath) + path.sep;
+
+ // If parent outside cwd, ignore
+ if (path.relative(cwd, parent) === '..') {
+ return next();
+ }
+
+ // If we want relative paths
+ if (relative === true) {
+ if (path.resolve(parent) === path.resolve(cwd)) {
+ parent = './';
+ } else {
+ parent = path.relative(cwd, parent) + path.sep;
+ }
+ filepath = path.relative(path.join(cwd, parent), filepath) + (helper.isDir(filepath) ? path.sep : '');
+ }
+
+ // If we want to transform paths to unix seps
+ if (unixify === true) {
+ filepath = helper.unixifyPathSep(filepath);
+ if (parent !== './') {
+ parent = helper.unixifyPathSep(parent);
+ }
+ }
+
+ if (!parent) { return next(); }
+
+ if (!Array.isArray(tree[parent])) {
+ tree[parent] = [];
+ }
+ tree[parent].push(filepath);
+ next();
+ }, function() {
+ done(null, tree);
+ });
+};
+
+/**
+ * Lo-Dash 1.0.1 <http://lodash.com/>
+ * Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
+ * Based on Underscore.js 1.4.4 <http://underscorejs.org/>
+ * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
+ * Available under MIT license <http://lodash.com/license>
+ */
+helper.unique = function unique() { var array = Array.prototype.concat.apply(Array.prototype, arguments); var result = []; for (var i = 0; i < array.length; i++) { if (result.indexOf(array[i]) === -1) { result.push(array[i]); } } return result; };
+
+/**
+ * Copyright (c) 2010 Caolan McMahon
+ * Available under MIT license <https://raw.github.com/caolan/async/master/LICENSE>
+ */
+helper.forEachSeries = function forEachSeries(arr, iterator, callback) {
+ callback = callback || function () {};
+ if (!arr.length) {
+ return callback();
+ }
+ var completed = 0;
+ var iterate = function () {
+ iterator(arr[completed], function (err) {
+ if (err) {
+ callback(err);
+ callback = function () {};
+ }
+ else {
+ completed += 1;
+ if (completed >= arr.length) {
+ callback(null);
+ } else {
+ iterate();
+ }
+ }
+ });
+ };
+ iterate();
+};
diff --git a/lib/pathwatcher.js b/lib/pathwatcher.js
new file mode 100644
index 0000000..9e54f26
--- /dev/null
+++ b/lib/pathwatcher.js
@@ -0,0 +1,220 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+(function() {
+ var EventEmitter, HandleMap, HandleWatcher, PathWatcher, binding, fs, handleWatchers, path,
+ __hasProp = {}.hasOwnProperty,
+ __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
+
+ binding = require('bindings')('pathwatcher.node');
+
+ HandleMap = binding.HandleMap;
+
+ EventEmitter = require('events').EventEmitter;
+
+ fs = require('graceful-fs');
+
+ path = require('path');
+
+ handleWatchers = new HandleMap;
+
+ binding.setCallback(function(event, handle, filePath, oldFilePath) {
+ if (handleWatchers.has(handle)) {
+ return handleWatchers.get(handle).onEvent(event, filePath, oldFilePath);
+ }
+ });
+
+ HandleWatcher = (function(_super) {
+ __extends(HandleWatcher, _super);
+
+ function HandleWatcher(path) {
+ this.path = path;
+ this.start();
+ }
+
+ HandleWatcher.prototype.onEvent = function(event, filePath, oldFilePath) {
+ var detectRename,
+ _this = this;
+ switch (event) {
+ case 'rename':
+ this.close();
+ detectRename = function() {
+ return fs.stat(_this.path, function(err) {
+ if (err) {
+ _this.path = filePath;
+ _this.start();
+ return _this.emit('change', 'rename', filePath);
+ } else {
+ _this.start();
+ return _this.emit('change', 'change', null);
+ }
+ });
+ };
+ return setTimeout(detectRename, 100);
+ case 'delete':
+ this.emit('change', 'delete', null);
+ return this.close();
+ case 'unknown':
+ throw new Error("Received unknown event for path: " + this.path);
+ break;
+ default:
+ return this.emit('change', event, filePath, oldFilePath);
+ }
+ };
+
+ HandleWatcher.prototype.start = function() {
+ var troubleWatcher;
+ this.handle = binding.watch(this.path);
+ if (handleWatchers.has(this.handle)) {
+ troubleWatcher = handleWatchers.get(this.handle);
+ troubleWatcher.close();
+ //console.error("The handle(" + this.handle + ") returned by watching " + this.path + " is the same with an already watched path(" + troubleWatcher.path + ")");
+ }
+ return handleWatchers.add(this.handle, this);
+ };
+
+ HandleWatcher.prototype.closeIfNoListener = function() {
+ if (this.listeners('change').length === 0) {
+ return this.close();
+ }
+ };
+
+ HandleWatcher.prototype.close = function() {
+ if (handleWatchers.has(this.handle)) {
+ binding.unwatch(this.handle);
+ return handleWatchers.remove(this.handle);
+ }
+ };
+
+ return HandleWatcher;
+
+ })(EventEmitter);
+
+ PathWatcher = (function(_super) {
+ __extends(PathWatcher, _super);
+
+ PathWatcher.prototype.isWatchingParent = false;
+
+ PathWatcher.prototype.path = null;
+
+ PathWatcher.prototype.handleWatcher = null;
+
+ function PathWatcher(filePath, callback) {
+ var stats, watcher, _i, _len, _ref,
+ _this = this;
+ this.path = filePath;
+ if (process.platform === 'win32') {
+ stats = fs.statSync(filePath);
+ this.isWatchingParent = !stats.isDirectory();
+ }
+ if (this.isWatchingParent) {
+ filePath = path.dirname(filePath);
+ }
+ _ref = handleWatchers.values();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ watcher = _ref[_i];
+ if (watcher.path === filePath) {
+ this.handleWatcher = watcher;
+ break;
+ }
+ }
+ if (this.handleWatcher == null) {
+ this.handleWatcher = new HandleWatcher(filePath);
+ }
+ this.onChange = function(event, newFilePath, oldFilePath) {
+ switch (event) {
+ case 'rename':
+ case 'change':
+ case 'delete':
+ if (event === 'rename') {
+ _this.path = newFilePath;
+ }
+ if (typeof callback === 'function') {
+ callback.call(_this, event, newFilePath);
+ }
+ return _this.emit('change', event, newFilePath);
+ case 'child-rename':
+ if (_this.isWatchingParent) {
+ if (_this.path === oldFilePath) {
+ return _this.onChange('rename', newFilePath);
+ }
+ } else {
+ return _this.onChange('change', '');
+ }
+ break;
+ case 'child-delete':
+ if (_this.isWatchingParent) {
+ if (_this.path === newFilePath) {
+ return _this.onChange('delete', null);
+ }
+ } else {
+ return _this.onChange('change', '');
+ }
+ break;
+ case 'child-change':
+ if (_this.isWatchingParent && _this.path === newFilePath) {
+ return _this.onChange('change', '');
+ }
+ break;
+ case 'child-create':
+ if (!_this.isWatchingParent) {
+ return _this.onChange('change', '');
+ }
+ }
+ };
+ this.handleWatcher.on('change', this.onChange);
+ }
+
+ PathWatcher.prototype.close = function() {
+ this.handleWatcher.removeListener('change', this.onChange);
+ return this.handleWatcher.closeIfNoListener();
+ };
+
+ return PathWatcher;
+
+ })(EventEmitter);
+
+ exports.watch = function(path, callback) {
+ path = require('path').resolve(path);
+ return new PathWatcher(path, callback);
+ };
+
+ exports.closeAllWatchers = function() {
+ var watcher, _i, _len, _ref;
+ _ref = handleWatchers.values();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ watcher = _ref[_i];
+ watcher.close();
+ }
+ return handleWatchers.clear();
+ };
+
+ exports.getWatchedPaths = function() {
+ var paths, watcher, _i, _len, _ref;
+ paths = [];
+ _ref = handleWatchers.values();
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ watcher = _ref[_i];
+ paths.push(watcher.path);
+ }
+ return paths;
+ };
+
+}).call(this);
diff --git a/lib/platform.js b/lib/platform.js
new file mode 100644
index 0000000..927fa3b
--- /dev/null
+++ b/lib/platform.js
@@ -0,0 +1,178 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2014 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+var PathWatcher = null;
+var statpoll = require('./statpoll.js');
+var helper = require('./helper');
+var fs = require('graceful-fs');
+var path = require('path');
+
+// on purpose globals
+var watched = Object.create(null);
+var renameWaiting = null;
+var renameWaitingFile = null;
+
+var platform = module.exports = function(file, cb) {
+ if (PathWatcher == null) {
+ PathWatcher = require('./pathwatcher');
+ }
+
+ // Ignore non-existent files
+ if (!fs.existsSync(file)) return;
+
+ // Mark every folder
+ file = markDir(file);
+
+ // Also watch all folders, needed to catch change for detecting added files
+ if (!helper.isDir(file)) {
+ platform(path.dirname(file), cb);
+ }
+
+ // Already watched, move on
+ if (watched[file]) return false;
+
+ // Helper for when to use statpoll
+ function useStatPoll() {
+ statpoll(file, function(event) {
+ var filepath = file;
+ if (process.platform === 'linux') {
+ var go = linuxWorkarounds(event, filepath, cb);
+ if (go === false) { return; }
+ }
+ cb(null, event, filepath);
+ });
+ }
+
+ // By default try using native OS watchers
+ if (platform.mode === 'auto' || platform.mode === 'watch') {
+ // Delay adding files to watch
+ // Fixes the duplicate handle race condition when renaming files
+ // ie: (The handle(26) returned by watching [filename] is the same with an already watched path([filename]))
+ watched[file] = true;
+ setTimeout(function() {
+ if (!fs.existsSync(file)) {
+ delete watched[file];
+ return;
+ }
+ // Workaround for lack of rename support on linux
+ if (process.platform === 'linux' && renameWaiting) {
+ clearTimeout(renameWaiting);
+ cb(null, 'rename', renameWaitingFile, file);
+ renameWaiting = renameWaitingFile = null;
+ return;
+ }
+ try {
+ watched[file] = PathWatcher.watch(file, function(event, newFile) {
+ var filepath = file;
+ if (process.platform === 'linux') {
+ var go = linuxWorkarounds(event, filepath, cb);
+ if (go === false) { return; }
+ }
+ cb(null, event, filepath, newFile);
+ });
+ } catch (error) {
+ // If we hit EMFILE, use stat poll
+ if (error.message.slice(0, 6) === 'EMFILE') { error.code = 'EMFILE'; }
+ if (error.code === 'EMFILE') {
+ // Only fallback to stat poll if not forced in watch mode
+ if (platform.mode !== 'watch') { useStatPoll(); }
+ // Format the error message for EMFILE a bit better
+ error.message = 'Too many open files.\nUnable to watch "' + file + '"\nusing native OS events so falling back to slower stat polling.\n';
+ }
+ cb(error);
+ }
+ }, 10);
+ } else {
+ useStatPoll();
+ }
+};
+
+platform.mode = 'auto';
+
+// Run the stat poller
+// NOTE: Run at minimum of 500ms to adequately capture change event
+// to folders when adding files
+platform.tick = statpoll.tick.bind(statpoll);
+
+// Close up a single watcher
+platform.close = function(filepath, cb) {
+ if (watched[filepath]) {
+ try {
+ watched[filepath].close();
+ delete watched[filepath];
+ } catch (error) {
+ return cb(error);
+ }
+ } else {
+ statpoll.close(filepath);
+ }
+ if (typeof cb === 'function') {
+ cb(null);
+ }
+};
+
+// Close up all watchers
+platform.closeAll = function() {
+ watched = Object.create(null);
+ statpoll.closeAll();
+ PathWatcher.closeAllWatchers();
+};
+
+// Return all watched file paths
+platform.getWatchedPaths = function() {
+ return Object.keys(watched).concat(statpoll.getWatchedPaths());
+};
+
+// Mark folders if not marked
+function markDir(file) {
+ if (file.slice(-1) !== path.sep) {
+ if (fs.lstatSync(file).isDirectory()) {
+ file += path.sep;
+ }
+ }
+ return file;
+}
+
+// Workarounds for lack of rename support on linux and folders emit before files
+// https://github.com/atom/node-pathwatcher/commit/004a202dea89f4303cdef33912902ed5caf67b23
+var linuxQueue = Object.create(null);
+var linuxQueueInterval = null;
+function linuxProcessQueue(cb) {
+ var len = Object.keys(linuxQueue).length;
+ if (len === 1) {
+ var key = Object.keys(linuxQueue).slice(0, 1)[0];
+ cb(null, key, linuxQueue[key]);
+ } else if (len > 1) {
+ if (linuxQueue['delete'] && linuxQueue['change']) {
+ renameWaitingFile = linuxQueue['delete'];
+ renameWaiting = setTimeout(function(filepath) {
+ cb(null, 'delete', filepath);
+ renameWaiting = renameWaitingFile = null;
+ }, 100, linuxQueue['delete']);
+ cb(null, 'change', linuxQueue['change']);
+ } else {
+ // TODO: This might not be needed
+ for (var i in linuxQueue) {
+ if (linuxQueue.hasOwnProperty(i)) {
+ cb(null, i, linuxQueue[i]);
+ }
+ }
+ }
+ }
+ linuxQueue = Object.create(null);
+}
+function linuxWorkarounds(event, filepath, cb) {
+ linuxQueue[event] = filepath;
+ clearTimeout(linuxQueueInterval);
+ linuxQueueInterval = setTimeout(function() {
+ linuxProcessQueue(cb);
+ }, 100);
+ return false;
+}
diff --git a/lib/statpoll.js b/lib/statpoll.js
new file mode 100644
index 0000000..e4134e3
--- /dev/null
+++ b/lib/statpoll.js
@@ -0,0 +1,69 @@
+/*
+ * gaze
+ * https://github.com/shama/gaze
+ *
+ * Copyright (c) 2014 Kyle Robinson Young
+ * Licensed under the MIT license.
+ */
+
+'use strict';
+
+var fs = require('graceful-fs');
+var nextback = require('nextback');
+var helper = require('./helper');
+var polled = Object.create(null);
+var lastTick = Date.now();
+var running = false;
+
+var statpoll = module.exports = function(filepath, cb) {
+ if (!polled[filepath]) {
+ polled[filepath] = { stat: fs.lstatSync(filepath), cb: cb, last: null };
+ }
+};
+
+// Iterate over polled files
+statpoll.tick = function() {
+ var files = Object.keys(polled);
+ if (files.length < 1 || running === true) return;
+ running = true;
+ helper.forEachSeries(files, function(file, next) {
+ // If file deleted
+ if (!fs.existsSync(file)) {
+ polled[file].cb('delete', file);
+ delete polled[file];
+ return next();
+ }
+
+ var stat = fs.lstatSync(file);
+
+ // If file has changed
+ var diff = stat.mtime - polled[file].stat.mtime;
+ if (diff > 0) {
+ polled[file].cb('change', file);
+ }
+
+ // Set new last accessed time
+ polled[file].stat = stat;
+ next();
+ }, nextback(function() {
+ lastTick = Date.now();
+ running = false;
+ }));
+};
+
+// Close up a single watcher
+statpoll.close = nextback(function(file) {
+ delete polled[file];
+ running = false;
+});
+
+// Close up all watchers
+statpoll.closeAll = nextback(function() {
+ polled = Object.create(null);
+ running = false;
+});
+
+// Return all statpolled watched paths
+statpoll.getWatchedPaths = function() {
+ return Object.keys(polled);
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..e4ecd79
--- /dev/null
+++ b/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "gaze",
+ "description": "A globbing fs.watch wrapper built from the best parts of other fine watch libs.",
+ "version": "0.6.4",
+ "homepage": "https://github.com/shama/gaze",
+ "author": {
+ "name": "Kyle Robinson Young",
+ "email": "kyle at dontkry.com"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/shama/gaze.git"
+ },
+ "bugs": {
+ "url": "https://github.com/shama/gaze/issues"
+ },
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "https://github.com/shama/gaze/blob/master/LICENSE-MIT"
+ }
+ ],
+ "main": "index.js",
+ "engines": {
+ "node": ">= 0.8.0"
+ },
+ "scripts": {
+ "test": "grunt nodeunit -v",
+ "install": "node-gyp rebuild"
+ },
+ "dependencies": {
+ "globule": "~0.2.0",
+ "nextback": "~0.1.0",
+ "bindings": "~1.2.0",
+ "nan": "~0.8.0",
+ "absolute-path": "0.0.0",
+ "graceful-fs": "~2.0.3"
+ },
+ "devDependencies": {
+ "grunt": "~0.4.1",
+ "grunt-contrib-nodeunit": "~0.3.3",
+ "grunt-contrib-jshint": "~0.9.2",
+ "grunt-cli": "~0.1.13",
+ "async": "~0.2.10",
+ "rimraf": "~2.2.6",
+ "ascii-table": "0.0.4"
+ },
+ "keywords": [
+ "watch",
+ "watcher",
+ "watching",
+ "fs.watch",
+ "fswatcher",
+ "fs",
+ "glob",
+ "utility"
+ ],
+ "files": [
+ "index.js",
+ "lib",
+ "src",
+ "binding.gyp",
+ "AUTHORS",
+ "LICENSE-MIT"
+ ]
+}
diff --git a/src/common.cc b/src/common.cc
new file mode 100644
index 0000000..7e5245f
--- /dev/null
+++ b/src/common.cc
@@ -0,0 +1,146 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include "common.h"
+
+static uv_async_t g_async;
+static uv_sem_t g_semaphore;
+static uv_thread_t g_thread;
+
+static EVENT_TYPE g_type;
+static WatcherHandle g_handle;
+static std::vector<char> g_new_path;
+static std::vector<char> g_old_path;
+static Persistent<Function> g_callback;
+
+static void CommonThread(void* handle) {
+ WaitForMainThread();
+ PlatformThread();
+}
+
+static void MakeCallbackInMainThread(uv_async_t* handle, int status) {
+ NanScope();
+
+ if (!g_callback.IsEmpty()) {
+ Handle<String> type;
+ switch (g_type) {
+ case EVENT_CHANGE:
+ type = String::New("change");
+ break;
+ case EVENT_DELETE:
+ type = String::New("delete");
+ break;
+ case EVENT_RENAME:
+ type = String::New("rename");
+ break;
+ case EVENT_CHILD_CREATE:
+ type = String::New("child-create");
+ break;
+ case EVENT_CHILD_CHANGE:
+ type = String::New("child-change");
+ break;
+ case EVENT_CHILD_DELETE:
+ type = String::New("child-delete");
+ break;
+ case EVENT_CHILD_RENAME:
+ type = String::New("child-rename");
+ break;
+ default:
+ type = String::New("unknown");
+ fprintf(stderr, "Got unknown event: %d\n", g_type);
+ return;
+ }
+
+ Handle<Value> argv[] = {
+ type,
+ WatcherHandleToV8Value(g_handle),
+ String::New(g_new_path.data(), g_new_path.size()),
+ String::New(g_old_path.data(), g_old_path.size()),
+ };
+ NanPersistentToLocal(g_callback)->Call(
+ Context::GetCurrent()->Global(), 4, argv);
+ }
+
+ WakeupNewThread();
+}
+
+void CommonInit() {
+ uv_sem_init(&g_semaphore, 0);
+ uv_async_init(uv_default_loop(), &g_async, MakeCallbackInMainThread);
+ uv_thread_create(&g_thread, &CommonThread, NULL);
+}
+
+void WaitForMainThread() {
+ uv_sem_wait(&g_semaphore);
+}
+
+void WakeupNewThread() {
+ uv_sem_post(&g_semaphore);
+}
+
+void PostEventAndWait(EVENT_TYPE type,
+ WatcherHandle handle,
+ const std::vector<char>& new_path,
+ const std::vector<char>& old_path) {
+ // FIXME should not pass args by settings globals.
+ g_type = type;
+ g_handle = handle;
+ g_new_path = new_path;
+ g_old_path = old_path;
+
+ uv_async_send(&g_async);
+ WaitForMainThread();
+}
+
+NAN_METHOD(SetCallback) {
+ NanScope();
+
+ if (!args[0]->IsFunction())
+ return NanThrowTypeError("Function required");
+
+ NanAssignPersistent(Function, g_callback, Handle<Function>::Cast(args[0]));
+ NanReturnUndefined();
+}
+
+NAN_METHOD(Watch) {
+ NanScope();
+
+ if (!args[0]->IsString())
+ return NanThrowTypeError("String required");
+
+ Handle<String> path = args[0]->ToString();
+ WatcherHandle handle = PlatformWatch(*String::Utf8Value(path));
+ if (PlatformIsEMFILE(handle))
+ return NanThrowTypeError("EMFILE: Unable to watch path");
+ if (!PlatformIsHandleValid(handle))
+ return NanThrowTypeError("Unable to watch path");
+
+ NanReturnValue(WatcherHandleToV8Value(handle));
+}
+
+NAN_METHOD(Unwatch) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Handle type required");
+
+ PlatformUnwatch(V8ValueToWatcherHandle(args[0]));
+ NanReturnUndefined();
+}
diff --git a/src/common.h b/src/common.h
new file mode 100644
index 0000000..825044b
--- /dev/null
+++ b/src/common.h
@@ -0,0 +1,76 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#ifndef SRC_COMMON_H_
+#define SRC_COMMON_H_
+
+#include <vector>
+
+#include "nan.h"
+using namespace v8;
+
+#ifdef _WIN32
+// Platform-dependent definetion of handle.
+typedef HANDLE WatcherHandle;
+
+// Conversion between V8 value and WatcherHandle.
+Handle<Value> WatcherHandleToV8Value(WatcherHandle handle);
+WatcherHandle V8ValueToWatcherHandle(Handle<Value> value);
+bool IsV8ValueWatcherHandle(Handle<Value> value);
+#else
+// Correspoding definetions on OS X and Linux.
+typedef int32_t WatcherHandle;
+#define WatcherHandleToV8Value(h) Integer::New(h)
+#define V8ValueToWatcherHandle(v) v->Int32Value()
+#define IsV8ValueWatcherHandle(v) v->IsInt32()
+#endif
+
+void PlatformInit();
+void PlatformThread();
+WatcherHandle PlatformWatch(const char* path);
+void PlatformUnwatch(WatcherHandle handle);
+bool PlatformIsHandleValid(WatcherHandle handle);
+bool PlatformIsEMFILE(WatcherHandle handle);
+
+enum EVENT_TYPE {
+ EVENT_NONE,
+ EVENT_CHANGE,
+ EVENT_RENAME,
+ EVENT_DELETE,
+ EVENT_CHILD_CHANGE,
+ EVENT_CHILD_RENAME,
+ EVENT_CHILD_DELETE,
+ EVENT_CHILD_CREATE,
+};
+
+void WaitForMainThread();
+void WakeupNewThread();
+void PostEventAndWait(EVENT_TYPE type,
+ WatcherHandle handle,
+ const std::vector<char>& new_path,
+ const std::vector<char>& old_path = std::vector<char>());
+
+void CommonInit();
+
+NAN_METHOD(SetCallback);
+NAN_METHOD(Watch);
+NAN_METHOD(Unwatch);
+
+#endif // SRC_COMMON_H_
diff --git a/src/handle_map.cc b/src/handle_map.cc
new file mode 100644
index 0000000..805e31f
--- /dev/null
+++ b/src/handle_map.cc
@@ -0,0 +1,158 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include "handle_map.h"
+
+#include <algorithm>
+
+HandleMap::HandleMap() {
+}
+
+HandleMap::~HandleMap() {
+ Clear();
+}
+
+bool HandleMap::Has(WatcherHandle key) const {
+ return map_.find(key) != map_.end();
+}
+
+bool HandleMap::Erase(WatcherHandle key) {
+ Map::iterator iter = map_.find(key);
+ if (iter == map_.end())
+ return false;
+
+ NanDispose(iter->second); // Deprecated, use NanDisposePersistent when v0.12 lands
+ map_.erase(iter);
+ return true;
+}
+
+void HandleMap::Clear() {
+ for (Map::iterator iter = map_.begin(); iter != map_.end(); ++iter)
+ NanDispose(iter->second); // Deprecated, use NanDisposePersistent when v0.12 lands
+ map_.clear();
+}
+
+// static
+NAN_METHOD(HandleMap::New) {
+ NanScope();
+ HandleMap* obj = new HandleMap();
+ obj->Wrap(args.This());
+ NanReturnUndefined();
+}
+
+// static
+NAN_METHOD(HandleMap::Add) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Bad argument");
+
+ HandleMap* obj = ObjectWrap::Unwrap<HandleMap>(args.This());
+ WatcherHandle key = V8ValueToWatcherHandle(args[0]);
+ if (obj->Has(key))
+ return NanThrowError("Duplicate key");
+
+ NanAssignUnsafePersistent(Value, obj->map_[key], args[1]);
+ NanReturnUndefined();
+}
+
+// static
+NAN_METHOD(HandleMap::Get) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Bad argument");
+
+ HandleMap* obj = ObjectWrap::Unwrap<HandleMap>(args.This());
+ WatcherHandle key = V8ValueToWatcherHandle(args[0]);
+ if (!obj->Has(key))
+ return NanThrowError("Invalid key");
+
+ NanReturnValue(NanPersistentToLocal(obj->map_[key]));
+}
+
+// static
+NAN_METHOD(HandleMap::Has) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Bad argument");
+
+ HandleMap* obj = ObjectWrap::Unwrap<HandleMap>(args.This());
+ NanReturnValue(Boolean::New(obj->Has(V8ValueToWatcherHandle(args[0]))));
+}
+
+// static
+NAN_METHOD(HandleMap::Values) {
+ NanScope();
+
+ HandleMap* obj = ObjectWrap::Unwrap<HandleMap>(args.This());
+
+ int i = 0;
+ Handle<Array> keys = Array::New(obj->map_.size());
+ for (Map::const_iterator iter = obj->map_.begin();
+ iter != obj->map_.end();
+ ++iter, ++i)
+ keys->Set(i, NanPersistentToLocal(iter->second));
+
+ NanReturnValue(keys);
+}
+
+// static
+NAN_METHOD(HandleMap::Remove) {
+ NanScope();
+
+ if (!IsV8ValueWatcherHandle(args[0]))
+ return NanThrowTypeError("Bad argument");
+
+ HandleMap* obj = ObjectWrap::Unwrap<HandleMap>(args.This());
+ if (!obj->Erase(V8ValueToWatcherHandle(args[0])))
+ return NanThrowError("Invalid key");
+
+ NanReturnUndefined();
+}
+
+// static
+NAN_METHOD(HandleMap::Clear) {
+ NanScope();
+
+ HandleMap* obj = ObjectWrap::Unwrap<HandleMap>(args.This());
+ obj->Clear();
+
+ NanReturnUndefined();
+}
+
+// static
+void HandleMap::Initialize(Handle<Object> target) {
+ NanScope();
+
+ Local<FunctionTemplate> t = FunctionTemplate::New(HandleMap::New);
+ t->InstanceTemplate()->SetInternalFieldCount(1);
+ t->SetClassName(NanSymbol("HandleMap"));
+
+ NODE_SET_PROTOTYPE_METHOD(t, "add", Add);
+ NODE_SET_PROTOTYPE_METHOD(t, "get", Get);
+ NODE_SET_PROTOTYPE_METHOD(t, "has", Has);
+ NODE_SET_PROTOTYPE_METHOD(t, "values", Values);
+ NODE_SET_PROTOTYPE_METHOD(t, "remove", Remove);
+ NODE_SET_PROTOTYPE_METHOD(t, "clear", Clear);
+
+ target->Set(NanSymbol("HandleMap"), t->GetFunction());
+}
diff --git a/src/handle_map.h b/src/handle_map.h
new file mode 100644
index 0000000..77ce4cc
--- /dev/null
+++ b/src/handle_map.h
@@ -0,0 +1,56 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#ifndef SRC_HANDLE_MAP_H_
+#define SRC_HANDLE_MAP_H_
+
+#include <map>
+
+#include "common.h"
+#include "unsafe_persistent.h"
+
+class HandleMap : public node::ObjectWrap {
+ public:
+ static void Initialize(Handle<Object> target);
+
+ private:
+ typedef std::map<WatcherHandle, NanUnsafePersistent<Value> > Map;
+
+ HandleMap();
+ virtual ~HandleMap();
+
+ bool Has(WatcherHandle key) const;
+ bool Erase(WatcherHandle key);
+ void Clear();
+
+ static void DisposeHandle(NanUnsafePersistent<Value>& value);
+
+ static NAN_METHOD(New);
+ static NAN_METHOD(Add);
+ static NAN_METHOD(Get);
+ static NAN_METHOD(Has);
+ static NAN_METHOD(Values);
+ static NAN_METHOD(Remove);
+ static NAN_METHOD(Clear);
+
+ Map map_;
+};
+
+#endif // SRC_HANDLE_MAP_H_
diff --git a/src/main.cc b/src/main.cc
new file mode 100644
index 0000000..fea6653
--- /dev/null
+++ b/src/main.cc
@@ -0,0 +1,35 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include "common.h"
+#include "handle_map.h"
+
+void Init(Handle<Object> exports) {
+ CommonInit();
+ PlatformInit();
+
+ NODE_SET_METHOD(exports, "setCallback", SetCallback);
+ NODE_SET_METHOD(exports, "watch", Watch);
+ NODE_SET_METHOD(exports, "unwatch", Unwatch);
+
+ HandleMap::Initialize(exports);
+}
+
+NODE_MODULE(pathwatcher, Init)
diff --git a/src/pathwatcher_linux.cc b/src/pathwatcher_linux.cc
new file mode 100644
index 0000000..674ae99
--- /dev/null
+++ b/src/pathwatcher_linux.cc
@@ -0,0 +1,105 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include <errno.h>
+#include <stdio.h>
+
+#include <sys/types.h>
+#include <sys/inotify.h>
+#include <linux/limits.h>
+#include <unistd.h>
+
+#include <algorithm>
+
+#include "common.h"
+
+static int g_inotify;
+
+void PlatformInit() {
+ g_inotify = inotify_init();
+ if (g_inotify == -1) {
+ perror("inotify_init");
+ return;
+ }
+
+ WakeupNewThread();
+}
+
+void PlatformThread() {
+ // Needs to be large enough for sizeof(inotify_event) + strlen(filename).
+ char buf[4096];
+
+ while (true) {
+ int size;
+ do {
+ size = read(g_inotify, buf, sizeof(buf));
+ } while (size == -1 && errno == EINTR);
+
+ if (size == -1) {
+ perror("read");
+ break;
+ } else if (size == 0) {
+ fprintf(stderr, "read returns 0, buffer size is too small\n");
+ break;
+ }
+
+ inotify_event* e;
+ for (char* p = buf; p < buf + size; p += sizeof(*e) + e->len) {
+ e = reinterpret_cast<inotify_event*>(p);
+
+ int fd = e->wd;
+ EVENT_TYPE type;
+ std::vector<char> path;
+
+ // Note that inotify won't tell us where the file or directory has been
+ // moved to, so we just treat IN_MOVE_SELF as file being deleted.
+ if (e->mask & (IN_ATTRIB | IN_CREATE | IN_DELETE | IN_MODIFY | IN_MOVE)) {
+ type = EVENT_CHANGE;
+ } else if (e->mask & (IN_DELETE_SELF | IN_MOVE_SELF)) {
+ type = EVENT_DELETE;
+ } else {
+ continue;
+ }
+
+ PostEventAndWait(type, fd, path);
+ }
+ }
+}
+
+WatcherHandle PlatformWatch(const char* path) {
+ int fd = inotify_add_watch(g_inotify, path, IN_ATTRIB | IN_CREATE |
+ IN_DELETE | IN_MODIFY | IN_MOVE | IN_MOVE_SELF | IN_DELETE_SELF);
+ if (fd == -1)
+ perror("inotify_add_watch");
+
+ return fd;
+}
+
+void PlatformUnwatch(WatcherHandle fd) {
+ inotify_rm_watch(g_inotify, fd);
+}
+
+bool PlatformIsHandleValid(WatcherHandle handle) {
+ return handle >= 0;
+}
+
+bool PlatformIsEMFILE(WatcherHandle handle) {
+ return handle == -24;
+}
diff --git a/src/pathwatcher_mac.mm b/src/pathwatcher_mac.mm
new file mode 100644
index 0000000..75efcfb
--- /dev/null
+++ b/src/pathwatcher_mac.mm
@@ -0,0 +1,102 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include <algorithm>
+
+#include <errno.h>
+#include <unistd.h>
+#include <sys/event.h>
+#include <sys/param.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include "common.h"
+
+static int g_kqueue;
+
+void PlatformInit() {
+ g_kqueue = kqueue();
+
+ WakeupNewThread();
+}
+
+void PlatformThread() {
+ struct kevent event;
+
+ while (true) {
+ int r;
+ do {
+ r = kevent(g_kqueue, NULL, 0, &event, 1, NULL);
+ } while ((r == -1 && errno == EINTR) || r == 0);
+
+ EVENT_TYPE type;
+ int fd = static_cast<int>(event.ident);
+ std::vector<char> path;
+
+ if (event.fflags & NOTE_WRITE) {
+ type = EVENT_CHANGE;
+ } else if (event.fflags & NOTE_DELETE) {
+ type = EVENT_DELETE;
+ } else if (event.fflags & NOTE_RENAME) {
+ type = EVENT_RENAME;
+ char buffer[MAXPATHLEN] = { 0 };
+ fcntl(fd, F_GETPATH, buffer);
+ close(fd);
+
+ int length = strlen(buffer);
+ path.resize(length);
+ std::copy(buffer, buffer + length, path.data());
+ } else {
+ continue;
+ }
+
+ PostEventAndWait(type, fd, path);
+ }
+}
+
+WatcherHandle PlatformWatch(const char* path) {
+ int fd = open(path, O_EVTONLY, 0);
+ if (fd < 0) {
+ // TODO: Maybe this could be handled better?
+ return -(errno);
+ }
+
+ struct timespec timeout = { 0, 0 };
+ struct kevent event;
+ int filter = EVFILT_VNODE;
+ int flags = EV_ADD | EV_ENABLE | EV_CLEAR;
+ int fflags = NOTE_WRITE | NOTE_DELETE | NOTE_RENAME;
+ EV_SET(&event, fd, filter, flags, fflags, 0, (void*)path);
+ kevent(g_kqueue, &event, 1, NULL, 0, &timeout);
+
+ return fd;
+}
+
+void PlatformUnwatch(WatcherHandle fd) {
+ close(fd);
+}
+
+bool PlatformIsHandleValid(WatcherHandle handle) {
+ return handle >= 0;
+}
+
+bool PlatformIsEMFILE(WatcherHandle handle) {
+ return handle == -24;
+}
diff --git a/src/pathwatcher_win.cc b/src/pathwatcher_win.cc
new file mode 100644
index 0000000..729d51c
--- /dev/null
+++ b/src/pathwatcher_win.cc
@@ -0,0 +1,329 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#include <algorithm>
+#include <map>
+#include <memory>
+
+#include "common.h"
+
+// Size of the buffer to store result of ReadDirectoryChangesW.
+static const unsigned int kDirectoryWatcherBufferSize = 4096;
+
+// Object template to create representation of WatcherHandle.
+static Persistent<ObjectTemplate> g_object_template;
+
+// Mutex for the HandleWrapper map.
+static uv_mutex_t g_handle_wrap_map_mutex;
+
+// The events to be waited on.
+static std::vector<HANDLE> g_events;
+
+// The dummy event to wakeup the thread.
+static HANDLE g_wake_up_event;
+
+struct ScopedLocker {
+ explicit ScopedLocker(uv_mutex_t& mutex) : mutex_(&mutex) { uv_mutex_lock(mutex_); }
+ ~ScopedLocker() { Unlock(); }
+
+ void Unlock() { uv_mutex_unlock(mutex_); }
+
+ uv_mutex_t* mutex_;
+};
+
+struct HandleWrapper {
+ HandleWrapper(WatcherHandle handle, const char* path_str)
+ : dir_handle(handle),
+ path(strlen(path_str)),
+ canceled(false) {
+ memset(&overlapped, 0, sizeof(overlapped));
+ overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
+ g_events.push_back(overlapped.hEvent);
+
+ std::copy(path_str, path_str + path.size(), path.data());
+ map_[overlapped.hEvent] = this;
+ }
+
+ ~HandleWrapper() {
+ CloseFile();
+
+ map_.erase(overlapped.hEvent);
+ CloseHandle(overlapped.hEvent);
+ g_events.erase(
+ std::remove(g_events.begin(), g_events.end(), overlapped.hEvent),
+ g_events.end());
+ }
+
+ void CloseFile() {
+ if (dir_handle != INVALID_HANDLE_VALUE) {
+ CloseHandle(dir_handle);
+ dir_handle = INVALID_HANDLE_VALUE;
+ }
+ }
+
+ WatcherHandle dir_handle;
+ std::vector<char> path;
+ bool canceled;
+ OVERLAPPED overlapped;
+ char buffer[kDirectoryWatcherBufferSize];
+
+ static HandleWrapper* Get(HANDLE key) { return map_[key]; }
+
+ static std::map<WatcherHandle, HandleWrapper*> map_;
+};
+
+std::map<WatcherHandle, HandleWrapper*> HandleWrapper::map_;
+
+struct WatcherEvent {
+ EVENT_TYPE type;
+ WatcherHandle handle;
+ std::vector<char> new_path;
+ std::vector<char> old_path;
+};
+
+static bool QueueReaddirchanges(HandleWrapper* handle) {
+ return ReadDirectoryChangesW(handle->dir_handle,
+ handle->buffer,
+ kDirectoryWatcherBufferSize,
+ FALSE,
+ FILE_NOTIFY_CHANGE_FILE_NAME |
+ FILE_NOTIFY_CHANGE_DIR_NAME |
+ FILE_NOTIFY_CHANGE_ATTRIBUTES |
+ FILE_NOTIFY_CHANGE_SIZE |
+ FILE_NOTIFY_CHANGE_LAST_WRITE |
+ FILE_NOTIFY_CHANGE_LAST_ACCESS |
+ FILE_NOTIFY_CHANGE_CREATION |
+ FILE_NOTIFY_CHANGE_SECURITY,
+ NULL,
+ &handle->overlapped,
+ NULL) == TRUE;
+}
+
+Handle<Value> WatcherHandleToV8Value(WatcherHandle handle) {
+ Handle<Value> value = NanPersistentToLocal(g_object_template)->NewInstance();
+ NanSetInternalFieldPointer(value->ToObject(), 0, handle);
+ return value;
+}
+
+WatcherHandle V8ValueToWatcherHandle(Handle<Value> value) {
+ return reinterpret_cast<WatcherHandle>(NanGetInternalFieldPointer(
+ value->ToObject(), 0));
+}
+
+bool IsV8ValueWatcherHandle(Handle<Value> value) {
+ return value->IsObject() && value->ToObject()->InternalFieldCount() == 1;
+}
+
+void PlatformInit() {
+ uv_mutex_init(&g_handle_wrap_map_mutex);
+
+ g_wake_up_event = CreateEvent(NULL, FALSE, FALSE, NULL);
+ g_events.push_back(g_wake_up_event);
+
+ NanAssignPersistent(ObjectTemplate, g_object_template, ObjectTemplate::New());
+ NanPersistentToLocal(g_object_template)->SetInternalFieldCount(1);
+
+ WakeupNewThread();
+}
+
+void PlatformThread() {
+ while (true) {
+ // Do not use g_events directly, since reallocation could happen when there
+ // are new watchers adding to g_events when WaitForMultipleObjects is still
+ // polling.
+ ScopedLocker locker(g_handle_wrap_map_mutex);
+ std::vector<HANDLE> copied_events(g_events);
+ locker.Unlock();
+
+ DWORD r = WaitForMultipleObjects(copied_events.size(),
+ copied_events.data(),
+ FALSE,
+ INFINITE);
+ int i = r - WAIT_OBJECT_0;
+ if (i >= 0 && i < copied_events.size()) {
+ // It's a wake up event, there is no fs events.
+ if (copied_events[i] == g_wake_up_event)
+ continue;
+
+ ScopedLocker locker(g_handle_wrap_map_mutex);
+
+ HandleWrapper* handle = HandleWrapper::Get(copied_events[i]);
+ if (!handle)
+ continue;
+
+ if (handle->canceled) {
+ delete handle;
+ continue;
+ }
+
+ DWORD bytes;
+ if (GetOverlappedResult(handle->dir_handle,
+ &handle->overlapped,
+ &bytes,
+ FALSE) == FALSE)
+ continue;
+
+ std::vector<char> old_path;
+ std::vector<WatcherEvent> events;
+
+ DWORD offset = 0;
+ while (true) {
+ FILE_NOTIFY_INFORMATION* file_info =
+ reinterpret_cast<FILE_NOTIFY_INFORMATION*>(handle->buffer + offset);
+
+ // Emit events for children.
+ EVENT_TYPE event = EVENT_NONE;
+ switch (file_info->Action) {
+ case FILE_ACTION_ADDED:
+ event = EVENT_CHILD_CREATE;
+ break;
+ case FILE_ACTION_REMOVED:
+ event = EVENT_CHILD_DELETE;
+ break;
+ case FILE_ACTION_RENAMED_OLD_NAME:
+ event = EVENT_CHILD_RENAME;
+ break;
+ case FILE_ACTION_RENAMED_NEW_NAME:
+ event = EVENT_CHILD_RENAME;
+ break;
+ case FILE_ACTION_MODIFIED:
+ event = EVENT_CHILD_CHANGE;
+ break;
+ }
+
+ if (event != EVENT_NONE) {
+ // The FileNameLength is in "bytes", but the WideCharToMultiByte
+ // requires the length to be in "characters"!
+ int file_name_length_in_characters =
+ file_info->FileNameLength / sizeof(wchar_t);
+
+ char filename[MAX_PATH] = { 0 };
+ int size = WideCharToMultiByte(CP_UTF8,
+ 0,
+ file_info->FileName,
+ file_name_length_in_characters,
+ filename,
+ MAX_PATH,
+ NULL,
+ NULL);
+
+ // Convert file name to file path, same with:
+ // path = handle->path + '\\' + filename
+ std::vector<char> path(handle->path.size() + 1 + size);
+ std::vector<char>::iterator iter = path.begin();
+ iter = std::copy(handle->path.begin(), handle->path.end(), iter);
+ *(iter++) = '\\';
+ std::copy(filename, filename + size, iter);
+
+ if (file_info->Action == FILE_ACTION_RENAMED_OLD_NAME) {
+ // Do not send rename event until the NEW_NAME event, but still keep
+ // a record of old name.
+ old_path.swap(path);
+ } else if (file_info->Action == FILE_ACTION_RENAMED_NEW_NAME) {
+ WatcherEvent e = { event, handle->overlapped.hEvent };
+ e.new_path.swap(path);
+ e.old_path.swap(old_path);
+ events.push_back(e);
+ } else {
+ WatcherEvent e = { event, handle->overlapped.hEvent };
+ e.new_path.swap(path);
+ events.push_back(e);
+ }
+ }
+
+ if (file_info->NextEntryOffset == 0) break;
+ offset += file_info->NextEntryOffset;
+ }
+
+ // Restart the monitor, it was reset after each call.
+ QueueReaddirchanges(handle);
+
+ locker.Unlock();
+
+ for (size_t i = 0; i < events.size(); ++i)
+ PostEventAndWait(events[i].type,
+ events[i].handle,
+ events[i].new_path,
+ events[i].old_path);
+ }
+ }
+}
+
+WatcherHandle PlatformWatch(const char* path) {
+ wchar_t wpath[MAX_PATH] = { 0 };
+ MultiByteToWideChar(CP_UTF8, 0, path, -1, wpath, MAX_PATH);
+
+ // Requires a directory, file watching is emulated in js.
+ DWORD attr = GetFileAttributesW(wpath);
+ if (attr == INVALID_FILE_ATTRIBUTES || !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
+ fprintf(stderr, "%s is not a directory\n", path);
+ return INVALID_HANDLE_VALUE;
+ }
+
+ WatcherHandle dir_handle = CreateFileW(wpath,
+ FILE_LIST_DIRECTORY,
+ FILE_SHARE_READ | FILE_SHARE_DELETE |
+ FILE_SHARE_WRITE,
+ NULL,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS |
+ FILE_FLAG_OVERLAPPED,
+ NULL);
+ if (!PlatformIsHandleValid(dir_handle)) {
+ fprintf(stderr, "Unable to call CreateFileW for %s\n", path);
+ return INVALID_HANDLE_VALUE;
+ }
+
+ std::unique_ptr<HandleWrapper> handle;
+ {
+ ScopedLocker locker(g_handle_wrap_map_mutex);
+ handle.reset(new HandleWrapper(dir_handle, path));
+ }
+
+ if (!QueueReaddirchanges(handle.get())) {
+ fprintf(stderr, "ReadDirectoryChangesW failed\n");
+ return INVALID_HANDLE_VALUE;
+ }
+
+ // Wake up the thread to add the new event.
+ SetEvent(g_wake_up_event);
+
+ // The pointer is leaked if no error happened.
+ return handle.release()->overlapped.hEvent;
+}
+
+void PlatformUnwatch(WatcherHandle key) {
+ if (PlatformIsHandleValid(key)) {
+ ScopedLocker locker(g_handle_wrap_map_mutex);
+
+ HandleWrapper* handle = HandleWrapper::Get(key);
+ handle->canceled = true;
+ CancelIoEx(handle->dir_handle, &handle->overlapped);
+ handle->CloseFile();
+ }
+}
+
+bool PlatformIsHandleValid(WatcherHandle handle) {
+ return handle != INVALID_HANDLE_VALUE;
+}
+
+bool PlatformIsEMFILE(WatcherHandle handle) {
+ return false;
+}
diff --git a/src/unsafe_persistent.h b/src/unsafe_persistent.h
new file mode 100644
index 0000000..0b290d3
--- /dev/null
+++ b/src/unsafe_persistent.h
@@ -0,0 +1,82 @@
+/*
+Copyright (c) 2013 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+#ifndef UNSAFE_PERSISTENT_H_
+#define UNSAFE_PERSISTENT_H_
+
+#include "nan.h"
+
+#if NODE_VERSION_AT_LEAST(0, 11, 0)
+template<typename TypeName> class NanUnsafePersistent {
+ public:
+ NanUnsafePersistent() : value(0) { }
+ explicit NanUnsafePersistent(v8::Persistent<TypeName>* handle) {
+ value = handle->ClearAndLeak();
+ }
+ explicit NanUnsafePersistent(const v8::Local<TypeName>& handle) {
+ v8::Persistent<TypeName> persistent(nan_isolate, handle);
+ value = persistent.ClearAndLeak();
+ }
+
+ NAN_INLINE(v8::Persistent<TypeName>* persistent()) {
+ v8::Persistent<TypeName>* handle = reinterpret_cast<v8::Persistent<TypeName>*>(&value);
+ return handle;
+ }
+
+ NAN_INLINE(void Dispose()) {
+ NanDispose(*persistent());
+ value = 0;
+ }
+
+ NAN_INLINE(void Clear()) {
+ value = 0;
+ }
+
+ NAN_INLINE(v8::Local<TypeName> NewLocal()) {
+ return v8::Local<TypeName>::New(nan_isolate, *persistent());
+ }
+
+ NAN_INLINE(bool IsEmpty() const) {
+ return value;
+ }
+
+ private:
+ TypeName* value;
+};
+#define NanAssignUnsafePersistent(type, handle, obj) \
+ handle = NanUnsafePersistent<type>(obj)
+template<class T> static NAN_INLINE(void NanDispose(
+ NanUnsafePersistent<T> &handle
+)) {
+ handle.Dispose();
+ handle.Clear();
+}
+template <class TypeName>
+static NAN_INLINE(v8::Local<TypeName> NanPersistentToLocal(
+ const NanUnsafePersistent<TypeName>& persistent
+)) {
+ return const_cast<NanUnsafePersistent<TypeName>&>(persistent).NewLocal();
+}
+#else
+#define NanUnsafePersistent v8::Persistent
+#define NanAssignUnsafePersistent NanAssignPersistent
+#endif
+
+#endif // UNSAFE_PERSISTENT_H_
diff --git a/test/add_test.js b/test/add_test.js
new file mode 100644
index 0000000..b957364
--- /dev/null
+++ b/test/add_test.js
@@ -0,0 +1,54 @@
+'use strict';
+
+var Gaze = require('../index.js').Gaze;
+var path = require('path');
+var fs = require('fs');
+var helper = require('./helper');
+
+var fixtures = path.resolve(__dirname, 'fixtures');
+var sortobj = helper.sortobj;
+
+exports.add = {
+ setUp: function(done) {
+ process.chdir(fixtures);
+ done();
+ },
+ addLater: function(test) {
+ test.expect(3);
+ new Gaze('sub/one.js', function(err, watcher) {
+ watcher.on('changed', function(filepath) {
+ test.equal('two.js', path.basename(filepath));
+ watcher.on('end', test.done);
+ watcher.close();
+ });
+
+ function addLater() {
+ watcher.add('sub/*.js', function() {
+ watcher.relative('sub', function(err, result) {
+ test.deepEqual(result, ['one.js', 'two.js']);
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;');
+ });
+ });
+ }
+
+ watcher.relative('sub', function(err, result) {
+ test.deepEqual(result, ['one.js']);
+ addLater();
+ });
+ });
+ },
+ addNoCallback: function(test) {
+ test.expect(1);
+ new Gaze('sub/one.js', function(err, watcher) {
+ this.add('sub/two.js');
+ this.on('changed', function(filepath) {
+ test.equal('two.js', path.basename(filepath));
+ watcher.on('end', test.done);
+ watcher.close();
+ });
+ setTimeout(function() {
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'), 'var two = true;');
+ }, 500);
+ });
+ },
+};
diff --git a/test/api_test.js b/test/api_test.js
new file mode 100644
index 0000000..3e2ce14
--- /dev/null
+++ b/test/api_test.js
@@ -0,0 +1,83 @@
+'use strict';
+
+var gaze = require('../index.js');
+var path = require('path');
+var fs = require('fs');
+var helper = require('./helper.js');
+
+exports.api = {
+ setUp: function(done) {
+ process.chdir(path.resolve(__dirname, 'fixtures'));
+ done();
+ },
+ newGaze: function(test) {
+ test.expect(2);
+ new gaze.Gaze('**/*', {}, function() {
+ this.relative(null, true, function(err, result) {
+ result = helper.sortobj(result);
+ test.deepEqual(result['./'], ['Project (LO)/', 'nested/', 'one.js', 'sub/']);
+ test.deepEqual(result['sub/'], ['one.js', 'two.js']);
+ this.on('end', test.done);
+ this.close();
+ }.bind(this));
+ });
+ },
+ func: function(test) {
+ test.expect(1);
+ var g = gaze('**/*', function(err, watcher) {
+ watcher.relative('sub', true, function(err, result) {
+ test.deepEqual(result, ['one.js', 'two.js']);
+ g.on('end', test.done);
+ g.close();
+ }.bind(this));
+ });
+ },
+ ready: function(test) {
+ test.expect(1);
+ var g = new gaze.Gaze('**/*');
+ g.on('ready', function(watcher) {
+ watcher.relative('sub', true, function(err, result) {
+ test.deepEqual(result, ['one.js', 'two.js']);
+ this.on('end', test.done);
+ this.close();
+ }.bind(this));
+ });
+ },
+ nomatch: function(test) {
+ test.expect(1);
+ gaze('nomatch.js', function(err, watcher) {
+ watcher.on('nomatch', function() {
+ test.ok(true, 'nomatch was emitted.');
+ watcher.close();
+ });
+ watcher.on('end', test.done);
+ });
+ },
+ cwd: function(test) {
+ test.expect(2);
+ var cwd = path.resolve(__dirname, 'fixtures', 'sub');
+ gaze('two.js', { cwd: cwd }, function(err, watcher) {
+ watcher.on('all', function(event, filepath) {
+ test.equal(path.relative(cwd, filepath), 'two.js');
+ test.equal(event, 'changed');
+ watcher.close();
+ });
+ fs.writeFile(path.join(cwd, 'two.js'), 'var two = true;');
+ watcher.on('end', test.done);
+ });
+ },
+ watched: function(test) {
+ test.expect(1);
+ var expected = ['Project (LO)', 'nested', 'one.js', 'sub'];
+ gaze('**/*', function(err, watcher) {
+ this.watched(function(err, result) {
+ result = helper.sortobj(helper.unixifyobj(result[process.cwd() + '/'].map(function(file) {
+ return path.relative(process.cwd(), file);
+ })));
+ test.deepEqual(result, expected);
+ watcher.close();
+ });
+ watcher.on('end', test.done);
+ });
+ },
+};
diff --git a/test/fixtures/Project (LO)/one.js b/test/fixtures/Project (LO)/one.js
new file mode 100644
index 0000000..fefeeea
--- /dev/null
+++ b/test/fixtures/Project (LO)/one.js
@@ -0,0 +1 @@
+var one = true;
\ No newline at end of file
diff --git a/test/fixtures/nested/one.js b/test/fixtures/nested/one.js
new file mode 100644
index 0000000..fefeeea
--- /dev/null
+++ b/test/fixtures/nested/one.js
@@ -0,0 +1 @@
+var one = true;
\ No newline at end of file
diff --git a/test/fixtures/nested/sub/two.js b/test/fixtures/nested/sub/two.js
new file mode 100644
index 0000000..24ad8a3
--- /dev/null
+++ b/test/fixtures/nested/sub/two.js
@@ -0,0 +1 @@
+var two = true;
\ No newline at end of file
diff --git a/test/fixtures/nested/sub2/two.js b/test/fixtures/nested/sub2/two.js
new file mode 100644
index 0000000..24ad8a3
--- /dev/null
+++ b/test/fixtures/nested/sub2/two.js
@@ -0,0 +1 @@
+var two = true;
\ No newline at end of file
diff --git a/test/fixtures/nested/three.js b/test/fixtures/nested/three.js
new file mode 100644
index 0000000..3392956
--- /dev/null
+++ b/test/fixtures/nested/three.js
@@ -0,0 +1 @@
+var three = true;
\ No newline at end of file
diff --git a/test/fixtures/one.js b/test/fixtures/one.js
new file mode 100644
index 0000000..7a4b5f6
--- /dev/null
+++ b/test/fixtures/one.js
@@ -0,0 +1 @@
+var test = true;
diff --git a/test/fixtures/sub/one.js b/test/fixtures/sub/one.js
new file mode 100644
index 0000000..fefeeea
--- /dev/null
+++ b/test/fixtures/sub/one.js
@@ -0,0 +1 @@
+var one = true;
\ No newline at end of file
diff --git a/test/fixtures/sub/two.js b/test/fixtures/sub/two.js
new file mode 100644
index 0000000..24ad8a3
--- /dev/null
+++ b/test/fixtures/sub/two.js
@@ -0,0 +1 @@
+var two = true;
\ No newline at end of file
diff --git a/test/helper.js b/test/helper.js
new file mode 100644
index 0000000..7673b04
--- /dev/null
+++ b/test/helper.js
@@ -0,0 +1,41 @@
+'use strict';
+
+var helper = module.exports = {};
+
+// Access to the lib helper to prevent confusion with having both in the tests
+helper.lib = require('../lib/helper.js');
+
+helper.sortobj = function sortobj(obj) {
+ if (Array.isArray(obj)) {
+ obj.sort();
+ return obj;
+ }
+ var out = Object.create(null);
+ var keys = Object.keys(obj);
+ keys.sort();
+ keys.forEach(function(key) {
+ var val = obj[key];
+ if (Array.isArray(val)) {
+ val.sort();
+ }
+ out[key] = val;
+ });
+ return out;
+};
+
+helper.unixifyobj = function unixifyobj(obj) {
+ function unixify(filepath) {
+ return (process.platform === 'win32') ? String(filepath).replace(/\\/g, '/') : filepath;
+ }
+ if (typeof obj === 'string') {
+ return unixify(obj);
+ }
+ if (Array.isArray(obj)) {
+ return obj.map(unixify);
+ }
+ var res = Object.create(null);
+ Object.keys(obj).forEach(function(key) {
+ res[unixify(key)] = unixifyobj(obj[key]);
+ });
+ return res;
+};
diff --git a/test/helper_test.js b/test/helper_test.js
new file mode 100644
index 0000000..5c88051
--- /dev/null
+++ b/test/helper_test.js
@@ -0,0 +1,57 @@
+'use strict';
+
+var helper = require('../lib/helper.js');
+var globule = require('globule');
+
+exports.helper = {
+ setUp: function(done) {
+ done();
+ },
+ tearDown: function(done) {
+ done();
+ },
+ flatToTree: function(test) {
+ test.expect(1);
+ var cwd = '/Users/dude/www/';
+ var files = [
+ '/Users/dude/www/',
+ '/Users/dude/www/one.js',
+ '/Users/dude/www/two.js',
+ '/Users/dude/www/sub/',
+ '/Users/dude/www/sub/one.js',
+ '/Users/dude/www/sub/nested/',
+ '/Users/dude/www/sub/nested/one.js',
+ ];
+ var expected = {
+ '/Users/dude/www/': ['/Users/dude/www/one.js', '/Users/dude/www/two.js', '/Users/dude/www/sub/'],
+ '/Users/dude/www/sub/': ['/Users/dude/www/sub/one.js', '/Users/dude/www/sub/nested/'],
+ '/Users/dude/www/sub/nested/': ['/Users/dude/www/sub/nested/one.js'],
+ };
+ helper.flatToTree(files, cwd, false, true, function(err, actual) {
+ test.deepEqual(actual, expected);
+ test.done();
+ });
+ },
+ flatToTreeRelative: function(test) {
+ test.expect(1);
+ var cwd = '/Users/dude/www/';
+ var files = [
+ '/Users/dude/www/',
+ '/Users/dude/www/one.js',
+ '/Users/dude/www/two.js',
+ '/Users/dude/www/sub/',
+ '/Users/dude/www/sub/one.js',
+ '/Users/dude/www/sub/nested/',
+ '/Users/dude/www/sub/nested/one.js',
+ ];
+ var expected = {
+ './': ['one.js', 'two.js', 'sub/'],
+ 'sub/': ['one.js', 'nested/'],
+ 'sub/nested/': ['one.js'],
+ };
+ helper.flatToTree(files, cwd, true, true, function(err, actual) {
+ test.deepEqual(actual, expected);
+ test.done();
+ });
+ },
+};
diff --git a/test/matching_test.js b/test/matching_test.js
new file mode 100644
index 0000000..125fffd
--- /dev/null
+++ b/test/matching_test.js
@@ -0,0 +1,115 @@
+'use strict';
+
+var gaze = require('../index.js');
+var grunt = require('grunt');
+var path = require('path');
+var helper = require('./helper');
+
+var fixtures = path.resolve(__dirname, 'fixtures');
+var sortobj = helper.sortobj;
+
+function cleanUp(done) {
+ [
+ 'newfolder',
+ ].forEach(function(d) {
+ var p = path.join(fixtures, d);
+ if (grunt.file.exists(p)) {
+ grunt.file.delete(p);
+ }
+ });
+ done();
+}
+
+exports.matching = {
+ setUp: function(done) {
+ process.chdir(fixtures);
+ cleanUp(done);
+ },
+ tearDown: cleanUp,
+ globAll: function(test) {
+ test.expect(2);
+ gaze('**/*', function() {
+ this.relative(null, true, function(err, result) {
+ test.deepEqual(sortobj(result['./']), sortobj(['Project (LO)/', 'nested/', 'one.js', 'sub/']));
+ test.deepEqual(sortobj(result['sub/']), sortobj(['one.js', 'two.js']));
+ this.on('end', test.done);
+ this.close();
+ }.bind(this));
+ });
+ },
+ relativeDir: function(test) {
+ test.expect(1);
+ gaze('**/*', function() {
+ this.relative('sub', true, function(err, result) {
+ test.deepEqual(sortobj(result), sortobj(['one.js', 'two.js']));
+ this.on('end', test.done);
+ this.close();
+ }.bind(this));
+ });
+ },
+ globArray: function(test) {
+ test.expect(2);
+ gaze(['*.js', 'sub/*.js'], function() {
+ this.relative(null, true, function(err, result) {
+ test.deepEqual(sortobj(result['./']), sortobj(['one.js', 'sub/']));
+ test.deepEqual(sortobj(result['sub/']), sortobj(['one.js', 'two.js']));
+ this.on('end', test.done);
+ this.close();
+ }.bind(this));
+ });
+ },
+ globArrayDot: function(test) {
+ test.expect(1);
+ gaze(['./sub/*.js'], function() {
+ this.relative(null, true, function(err, result) {
+ test.deepEqual(result['sub/'], ['one.js', 'two.js']);
+ this.on('end', test.done);
+ this.close();
+ }.bind(this));
+ });
+ },
+ oddName: function(test) {
+ test.expect(1);
+ gaze(['Project (LO)/*.js'], function() {
+ this.relative(null, true, function(err, result) {
+ test.deepEqual(result['Project (LO)/'], ['one.js']);
+ this.on('end', test.done);
+ this.close();
+ }.bind(this));
+ });
+ },
+ addedLater: function(test) {
+ var expected = [
+ ['newfolder/', 'added.js'],
+ ['newfolder/', 'added.js', 'addedAnother.js'],
+ ['newfolder/', 'added.js', 'addedAnother.js', 'sub/'],
+ ];
+ test.expect(expected.length);
+ gaze('**/*.js', function(err, watcher) {
+ watcher.on('all', function(status, filepath) {
+ var expect = expected.shift();
+ watcher.relative(expect[0], true, function(err, result) {
+ test.deepEqual(sortobj(result), sortobj(expect.slice(1)));
+ if (expected.length < 1) { watcher.close(); }
+ }.bind(this));
+ });
+ grunt.file.write(path.join(fixtures, 'newfolder', 'added.js'), 'var added = true;');
+ setTimeout(function() {
+ grunt.file.write(path.join(fixtures, 'newfolder', 'addedAnother.js'), 'var added = true;');
+ }, 1000);
+ setTimeout(function() {
+ grunt.file.write(path.join(fixtures, 'newfolder', 'sub', 'lastone.js'), 'var added = true;');
+ }, 2000);
+ watcher.on('end', test.done);
+ });
+ },
+};
+
+// Ignore these tests if node v0.8
+var version = process.versions.node.split('.');
+if (version[0] === '0' && version[1] === '8') {
+ // gaze v0.4 returns watched but unmatched folders
+ // where gaze v0.5 does not
+ delete exports.matching.globArray;
+ delete exports.matching.addedLater;
+}
diff --git a/test/patterns_test.js b/test/patterns_test.js
new file mode 100644
index 0000000..0e87ea7
--- /dev/null
+++ b/test/patterns_test.js
@@ -0,0 +1,77 @@
+'use strict';
+
+var gaze = require('../index.js');
+var path = require('path');
+var fs = require('fs');
+var helper = require('./helper');
+
+var fixtures = path.resolve(__dirname, 'fixtures');
+
+// Clean up helper to call in setUp and tearDown
+function cleanUp(done) {
+ [
+ 'added.js',
+ 'nested/added.js',
+ ].forEach(function(d) {
+ var p = path.resolve(__dirname, 'fixtures', d);
+ if (fs.existsSync(p)) { fs.unlinkSync(p); }
+ });
+ done();
+}
+
+exports.patterns = {
+ setUp: function(done) {
+ process.chdir(path.resolve(__dirname, 'fixtures'));
+ cleanUp(done);
+ },
+ tearDown: cleanUp,
+ negate: function(test) {
+ test.expect(1);
+ gaze(['**/*.js', '!nested/**/*.js'], function(err, watcher) {
+ watcher.on('added', function(filepath) {
+ var expected = path.relative(process.cwd(), filepath);
+ test.equal(path.join('added.js'), expected);
+ watcher.close();
+ });
+ // dont add
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'nested', 'added.js'), 'var added = true;');
+ setTimeout(function() {
+ // should add
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'added.js'), 'var added = true;');
+ }, 1000);
+ watcher.on('end', test.done);
+ });
+ },
+ dotSlash: function(test) {
+ test.expect(2);
+ gaze('./nested/**/*', function(err, watcher) {
+ watcher.on('end', test.done);
+ watcher.on('all', function(status, filepath) {
+ test.equal(status, 'changed');
+ test.equal(path.relative(fixtures, filepath), path.join('nested', 'one.js'));
+ watcher.close();
+ });
+ fs.writeFile(path.join(fixtures, 'nested', 'one.js'), 'var one = true;');
+ });
+ },
+ absolute: function(test) {
+ test.expect(2);
+ var filepath = path.resolve(__dirname, 'fixtures', 'nested', 'one.js');
+ gaze(filepath, function(err, watcher) {
+ watcher.on('end', test.done);
+ watcher.on('all', function(status, filepath) {
+ test.equal(status, 'changed');
+ test.equal(path.relative(fixtures, filepath), path.join('nested', 'one.js'));
+ watcher.close();
+ });
+ fs.writeFile(filepath, 'var one = true;');
+ });
+ },
+};
+
+// Ignore these tests if node v0.8
+var version = process.versions.node.split('.');
+if (version[0] === '0' && version[1] === '8') {
+ // gaze v0.4 is buggy with absolute paths sometimes, wontfix
+ delete exports.patterns.absolute;
+}
diff --git a/test/platform_test.js b/test/platform_test.js
new file mode 100644
index 0000000..7713a6d
--- /dev/null
+++ b/test/platform_test.js
@@ -0,0 +1,135 @@
+'use strict';
+
+var platform = require('../lib/platform.js');
+var helper = require('./helper.js');
+var path = require('path');
+var grunt = require('grunt');
+var async = require('async');
+var globule = require('globule');
+
+var fixturesbase = path.resolve(__dirname, 'fixtures');
+
+// helpers
+function cleanUp() {
+ ['add.js'].forEach(function(file) {
+ grunt.file.delete(path.join(fixturesbase, file));
+ });
+}
+function runWithPoll(mode, cb) {
+ if (mode === 'poll') {
+ // Polling unfortunately needs time to pick up stat
+ setTimeout(cb, 1000);
+ } else {
+ // Watch delays 10ms when adding, so delay double just in case
+ setTimeout(cb, 20);
+ }
+}
+
+exports.platform = {
+ setUp: function(done) {
+ platform.mode = 'auto';
+ this.interval = setInterval(platform.tick.bind(platform), 200);
+ cleanUp();
+ done();
+ },
+ tearDown: function(done) {
+ clearInterval(this.interval);
+ platform.closeAll();
+ cleanUp();
+ done();
+ },
+ change: function(test) {
+ test.expect(4);
+ var expectfilepath = null;
+
+ function runtest(file, mode, done) {
+ var filename = path.join(fixturesbase, file);
+ platform.mode = mode;
+
+ platform(filename, function(error, event, filepath) {
+ test.equal(event, 'change', 'should have been a change event in ' + mode + ' mode.');
+ test.equal(filepath, expectfilepath, 'should have triggered on the correct file in ' + mode + ' mode.');
+ platform.closeAll();
+ done();
+ });
+
+ runWithPoll(mode, function() {
+ expectfilepath = filename;
+ grunt.file.write(filename, grunt.file.read(filename));
+ });
+ }
+
+ async.series([
+ function(next) { runtest('one.js', 'auto', next); },
+ function(next) {
+ // Polling needs a minimum of 500ms to pick up changes.
+ setTimeout(function() {
+ runtest('one.js', 'poll', next);
+ }, 500);
+ },
+ ], test.done);
+ },
+ delete: function(test) {
+ test.expect(4);
+ var expectfilepath = null;
+
+ function runtest(file, mode, done) {
+ var filename = path.join(fixturesbase, file);
+ platform.mode = mode;
+
+ platform(filename, function(error, event, filepath) {
+ // Ignore change events from dirs. This is handled outside of the platform and are safe to ignore here.
+ if (event === 'change' && grunt.file.isDir(filepath)) return;
+ test.equal(event, 'delete', 'should have been a delete event in ' + mode + ' mode.');
+ test.equal(filepath, expectfilepath, 'should have triggered on the correct file in ' + mode + ' mode.');
+ platform.closeAll();
+ done();
+ });
+
+ runWithPoll(mode, function() {
+ expectfilepath = filename;
+ grunt.file.delete(filename);
+ });
+ }
+
+ async.series([
+ function(next) {
+ grunt.file.write(path.join(fixturesbase, 'add.js'), 'var test = true;');
+ runtest('add.js', 'auto', next);
+ },
+ function(next) {
+ grunt.file.write(path.join(fixturesbase, 'add.js'), 'var test = true;');
+ // Polling needs a minimum of 500ms to pick up changes.
+ setTimeout(function() {
+ runtest('add.js', 'poll', next);
+ }, 500);
+ },
+ ], test.done);
+ },
+ getWatchedPaths: function(test) {
+ test.expect(1);
+ var expected = globule.find(['**/*.js'], { cwd: fixturesbase, prefixBase: fixturesbase });
+ var len = expected.length;
+ for (var i = 0; i < len; i++) {
+ platform(expected[i], function() {});
+ var parent = path.dirname(expected[i]);
+ expected.push(parent + '/');
+ }
+ expected = helper.unixifyobj(helper.lib.unique(expected));
+
+ var actual = helper.unixifyobj(platform.getWatchedPaths());
+ test.deepEqual(actual.sort(), expected.sort());
+ test.done();
+ },
+};
+
+// Ignore this test if node v0.8 as platform will never be used there
+var version = process.versions.node.split('.');
+if (version[0] === '0' && version[1] === '8') {
+ exports.platform = {};
+}
+
+// :'| Ignoring these tests on linux for now
+if (process.platform === 'linux') {
+ exports.platform = {};
+}
diff --git a/test/rename_test.js b/test/rename_test.js
new file mode 100644
index 0000000..ce92bc0
--- /dev/null
+++ b/test/rename_test.js
@@ -0,0 +1,43 @@
+'use strict';
+
+var gaze = require('../index.js');
+var path = require('path');
+var fs = require('fs');
+
+// Clean up helper to call in setUp and tearDown
+function cleanUp() {
+ [
+ 'sub/rename.js',
+ 'sub/renamed.js'
+ ].forEach(function(d) {
+ var p = path.resolve(__dirname, 'fixtures', d);
+ if (fs.existsSync(p)) { fs.unlinkSync(p); }
+ });
+}
+
+exports.watch = {
+ setUp: function(done) {
+ process.chdir(path.resolve(__dirname, 'fixtures'));
+ cleanUp();
+ done();
+ },
+ tearDown: function(done) {
+ cleanUp();
+ done();
+ },
+ rename: function(test) {
+ test.expect(2);
+ var oldPath = path.join(__dirname, 'fixtures', 'sub', 'rename.js');
+ var newPath = path.join(__dirname, 'fixtures', 'sub', 'renamed.js');
+ fs.writeFileSync(oldPath, 'var rename = true;');
+ gaze('**/*', function(err, watcher) {
+ watcher.on('renamed', function(newFile, oldFile) {
+ test.equal(newFile, newPath);
+ test.equal(oldFile, oldPath);
+ watcher.on('end', test.done);
+ watcher.close();
+ });
+ fs.renameSync(oldPath, newPath);
+ });
+ }
+};
diff --git a/test/safewrite_test.js b/test/safewrite_test.js
new file mode 100644
index 0000000..b552f53
--- /dev/null
+++ b/test/safewrite_test.js
@@ -0,0 +1,52 @@
+'use strict';
+
+var gaze = require('../index.js');
+var path = require('path');
+var fs = require('fs');
+
+// Clean up helper to call in setUp and tearDown
+function cleanUp(done) {
+ [
+ 'safewrite.js'
+ ].forEach(function(d) {
+ var p = path.resolve(__dirname, 'fixtures', d);
+ if (fs.existsSync(p)) { fs.unlinkSync(p); }
+ });
+ done();
+}
+
+exports.safewrite = {
+ setUp: function(done) {
+ process.chdir(path.resolve(__dirname, 'fixtures'));
+ cleanUp(done);
+ },
+ tearDown: cleanUp,
+ safewrite: function(test) {
+ test.expect(2);
+
+ var file = path.resolve(__dirname, 'fixtures', 'safewrite.js');
+ var backup = path.resolve(__dirname, 'fixtures', 'safewrite.ext~');
+ fs.writeFileSync(file, 'var safe = true;');
+
+ function simSafewrite() {
+ fs.writeFileSync(backup, fs.readFileSync(file));
+ fs.unlinkSync(file);
+ fs.renameSync(backup, file);
+ }
+
+ gaze('**/*', function(err, watcher) {
+ this.on('end', test.done);
+ this.on('all', function(action, filepath) {
+ test.equal(action, 'changed');
+ test.equal(path.basename(filepath), 'safewrite.js');
+ watcher.close();
+ });
+ simSafewrite();
+ });
+ }
+};
+
+// :'| Ignoring these tests on linux for now
+if (process.platform === 'linux') {
+ exports.safewrite = {};
+}
diff --git a/test/statpoll_test.js b/test/statpoll_test.js
new file mode 100644
index 0000000..5b479d6
--- /dev/null
+++ b/test/statpoll_test.js
@@ -0,0 +1,53 @@
+'use strict';
+
+var statpoll = require('../lib/statpoll.js');
+var globule = require('globule');
+var path = require('path');
+var grunt = require('grunt');
+
+var fixturesbase = path.resolve(__dirname, 'fixtures');
+function clean() {
+ [
+ path.join(fixturesbase, 'add.js')
+ ].forEach(grunt.file.delete);
+}
+
+exports.statpoll = {
+ setUp: function(done) {
+ clean();
+ done();
+ },
+ tearDown: function(done) {
+ statpoll.closeAll();
+ clean();
+ done();
+ },
+ change: function(test) {
+ test.expect(2);
+
+ var filepath = path.resolve(fixturesbase, 'one.js');
+ statpoll(filepath, function(event, filepath) {
+ test.equal(event, 'change');
+ test.equal(path.basename(filepath), 'one.js');
+ test.done();
+ });
+
+ grunt.file.write(filepath, grunt.file.read(filepath));
+ statpoll.tick();
+ },
+ delete: function(test) {
+ test.expect(2);
+
+ var filepath = path.resolve(fixturesbase, 'add.js');
+ grunt.file.write(filepath, 'var added = true;');
+
+ statpoll(filepath, function(event, filepath) {
+ test.equal(event, 'delete');
+ test.equal(path.basename(filepath), 'add.js');
+ test.done();
+ });
+
+ grunt.file.delete(filepath);
+ statpoll.tick();
+ },
+};
diff --git a/test/watch_test.js b/test/watch_test.js
new file mode 100644
index 0000000..90c150a
--- /dev/null
+++ b/test/watch_test.js
@@ -0,0 +1,343 @@
+'use strict';
+
+var gaze = require('../index.js');
+var grunt = require('grunt');
+var path = require('path');
+var fs = require('fs');
+var helper = require('./helper.js');
+
+// Clean up helper to call in setUp and tearDown
+function cleanUp(done) {
+ [
+ 'sub/tmp.js',
+ 'sub/tmp',
+ 'sub/renamed.js',
+ 'added.js',
+ 'nested/added.js',
+ 'nested/.tmp',
+ 'nested/sub/added.js',
+ 'new_dir',
+ ].forEach(function(d) {
+ grunt.file.delete(path.resolve(__dirname, 'fixtures', d));
+ });
+
+ // Delay between tests to prevent bleed
+ setTimeout(done, 500);
+}
+
+exports.watch = {
+ setUp: function(done) {
+ process.chdir(path.resolve(__dirname, 'fixtures'));
+ cleanUp(done);
+ },
+ tearDown: cleanUp,
+ remove: function(test) {
+ test.expect(2);
+ gaze('**/*', function() {
+ this.remove(path.resolve(__dirname, 'fixtures', 'sub', 'two.js'));
+ this.remove(path.resolve(__dirname, 'fixtures'));
+ this.relative(null, true, function(err, result) {
+ test.deepEqual(result['sub/'], ['one.js']);
+ test.notDeepEqual(result['./'], ['one.js']);
+ this.on('end', test.done);
+ this.close();
+ }.bind(this));
+ });
+ },
+ changed: function(test) {
+ test.expect(1);
+ gaze('**/*', function(err, watcher) {
+ watcher.on('changed', function(filepath) {
+ var expected = path.relative(process.cwd(), filepath);
+ test.equal(path.join('sub', 'one.js'), expected);
+ watcher.close();
+ });
+ this.on('added', function() { test.ok(false, 'added event should not have emitted.'); });
+ this.on('deleted', function() { test.ok(false, 'deleted event should not have emitted.'); });
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;');
+ watcher.on('end', test.done);
+ });
+ },
+ added: function(test) {
+ test.expect(1);
+ gaze('**/*', function(err, watcher) {
+ this.on('added', function(filepath) {
+ var expected = path.relative(process.cwd(), filepath);
+ test.equal(path.join('sub', 'tmp.js'), expected);
+ watcher.close();
+ });
+ this.on('changed', function() { test.ok(false, 'changed event should not have emitted.'); });
+ this.on('deleted', function() { test.ok(false, 'deleted event should not have emitted.'); });
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'tmp.js'), 'var tmp = true;');
+ watcher.on('end', test.done);
+ });
+ },
+ dontAddUnmatchedFiles: function(test) {
+ // TODO: Code smell
+ test.expect(2);
+ gaze('**/*.js', function(err, watcher) {
+ setTimeout(function() {
+ test.ok(true, 'Ended without adding a file.');
+ watcher.close();
+ }, 1000);
+ this.on('added', function(filepath) {
+ test.equal(path.relative(process.cwd(), filepath), path.join('sub', 'tmp.js'));
+ });
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'tmp'), 'Dont add me!');
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'tmp.js'), 'add me!');
+ watcher.on('end', test.done);
+ });
+ },
+ dontAddCwd: function(test) {
+ test.expect(2);
+ gaze('nested/**', function(err, watcher) {
+ setTimeout(function() {
+ test.ok(true, 'Ended without adding a file.');
+ watcher.close();
+ }, 1000);
+ this.on('all', function(ev, filepath) {
+ test.equal(path.relative(process.cwd(), filepath), path.join('nested', 'sub', 'added.js'));
+ });
+ fs.mkdirSync(path.resolve(__dirname, 'fixtures', 'new_dir'));
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'added.js'), 'Dont add me!');
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'nested', 'sub', 'added.js'), 'add me!');
+ watcher.on('end', test.done);
+ });
+ },
+ dontAddMatchedDirectoriesThatArentReallyAdded: function(test) {
+ // This is a regression test for a bug I ran into where a matching directory would be reported
+ // added when a non-matching file was created along side it. This only happens if the
+ // directory name doesn't occur in $PWD.
+ test.expect(1);
+ gaze('**/*', function(err, watcher) {
+ setTimeout(function() {
+ test.ok(true, 'Ended without adding a file.');
+ watcher.close();
+ }, 1000);
+ this.on('added', function(filepath) {
+ test.notEqual(path.relative(process.cwd(), filepath), path.join('nested', 'sub2'));
+ });
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'nested', '.tmp'), 'Wake up!');
+ watcher.on('end', test.done);
+ });
+ },
+ deleted: function(test) {
+ test.expect(1);
+ var tmpfile = path.resolve(__dirname, 'fixtures', 'sub', 'deleted.js');
+ fs.writeFileSync(tmpfile, 'var tmp = true;');
+ // TODO: This test fails on travis (but not on my local ubuntu) so use polling here
+ // as a way to ignore until this can be fixed
+ var mode = (process.platform === 'linux') ? 'poll' : 'auto';
+ gaze('**/*', { mode: mode }, function(err, watcher) {
+ watcher.on('deleted', function(filepath) {
+ test.equal(path.join('sub', 'deleted.js'), path.relative(process.cwd(), filepath));
+ watcher.close();
+ });
+ this.on('changed', function() { test.ok(false, 'changed event should not have emitted.'); });
+ this.on('added', function() { test.ok(false, 'added event should not have emitted.'); });
+ fs.unlinkSync(tmpfile);
+ watcher.on('end', test.done);
+ });
+ },
+ dontEmitTwice: function(test) {
+ test.expect(2);
+ gaze('**/*', function(err, watcher) {
+ watcher.on('all', function(status, filepath) {
+ var expected = path.relative(process.cwd(), filepath);
+ test.equal(path.join('sub', 'one.js'), expected);
+ test.equal(status, 'changed');
+ fs.readFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'));
+ setTimeout(function() {
+ fs.readFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'));
+ }, 1000);
+ // Give some time to accidentally emit before we close
+ setTimeout(function() { watcher.close(); }, 5000);
+ });
+ setTimeout(function() {
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;');
+ }, 1000);
+ watcher.on('end', test.done);
+ });
+ },
+ emitTwice: function(test) {
+ test.expect(2);
+ var times = 0;
+ gaze('**/*', function(err, watcher) {
+ watcher.on('all', function(status, filepath) {
+ test.equal(status, 'changed');
+ times++;
+ setTimeout(function() {
+ if (times < 2) {
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;');
+ } else {
+ watcher.close();
+ }
+ }, 1000);
+ });
+ setTimeout(function() {
+ fs.writeFileSync(path.resolve(__dirname, 'fixtures', 'sub', 'one.js'), 'var one = true;');
+ }, 1000);
+ watcher.on('end', test.done);
+ });
+ },
+ nonExistent: function(test) {
+ test.expect(1);
+ gaze('non/existent/**/*', function(err, watcher) {
+ test.ok(true);
+ watcher.on('end', test.done);
+ watcher.close();
+ });
+ },
+ differentCWD: function(test) {
+ test.expect(1);
+ var cwd = path.resolve(__dirname, 'fixtures', 'sub');
+ gaze('two.js', {
+ cwd: cwd
+ }, function(err, watcher) {
+ watcher.on('changed', function(filepath) {
+ this.relative(function(err, result) {
+ test.deepEqual(result, {'./':['two.js']});
+ watcher.close();
+ });
+ });
+ fs.writeFileSync(path.resolve(cwd, 'two.js'), 'var two = true;');
+ watcher.on('end', test.done);
+ });
+ },
+ addedEmitInSubFolders: function(test) {
+ test.expect(4);
+ var adds = [
+ { pattern: '**/*', file: path.resolve(__dirname, 'fixtures', 'nested', 'sub', 'added.js') },
+ { pattern: '**/*', file: path.resolve(__dirname, 'fixtures', 'added.js') },
+ { pattern: 'nested/**/*', file: path.resolve(__dirname, 'fixtures', 'nested', 'added.js') },
+ { pattern: 'nested/sub/*.js', file: path.resolve(__dirname, 'fixtures', 'nested', 'sub', 'added.js') },
+ ];
+ grunt.util.async.forEachSeries(adds, function(add, next) {
+ new gaze.Gaze(add.pattern, function(err, watcher) {
+ watcher.on('added', function(filepath) {
+ test.equal('added.js', path.basename(filepath));
+ fs.unlinkSync(filepath);
+ watcher.close();
+ next();
+ });
+ watcher.on('changed', function() { test.ok(false, 'changed event should not have emitted.'); });
+ watcher.on('deleted', function() { test.ok(false, 'deleted event should not have emitted.'); });
+ fs.writeFileSync(add.file, 'var added = true;');
+ });
+ }, function() {
+ test.done();
+ });
+ },
+ multipleWatchersSimultaneously: function(test) {
+ test.expect(2);
+ var did = 0;
+ var ready = 0;
+ var cwd = path.resolve(__dirname, 'fixtures', 'sub');
+ var watchers = [];
+ var timeout = setTimeout(function() {
+ for (var i = 0; i < watchers.length; i++) {
+ watchers[i].close();
+ delete watchers[i];
+ }
+ test.done();
+ }, 1000);
+
+ function isReady() {
+ ready++;
+ if (ready > 1) {
+ fs.writeFileSync(path.resolve(cwd, 'one.js'), 'var one = true;');
+ }
+ }
+ function changed(filepath) {
+ test.equal(path.join('sub', 'one.js'), path.relative(process.cwd(), filepath));
+ }
+ for (var i = 0; i < 2; i++) {
+ watchers[i] = new gaze.Gaze('sub/one.js');
+ watchers[i].on('changed', changed);
+ watchers[i].on('ready', isReady);
+ }
+ },
+ mkdirThenAddFile: function(test) {
+ var expected = [
+ 'new_dir/first.js',
+ 'new_dir/other.js',
+ ];
+ test.expect(expected.length);
+
+ gaze('**/*.js', function(err, watcher) {
+ watcher.on('all', function(status, filepath) {
+ var expect = expected.shift();
+ var actual = helper.unixifyobj(path.relative(process.cwd(), filepath));
+ test.equal(actual, expect);
+
+ if (expected.length === 1) {
+ // Ensure the new folder is being watched correctly after initial add
+ setTimeout(function() {
+ fs.writeFileSync('new_dir/dontmatch.txt', '');
+ setTimeout(function() {
+ fs.writeFileSync('new_dir/other.js', '');
+ }, 1000);
+ }, 1000);
+ }
+
+ if (expected.length < 1) { watcher.close(); }
+ });
+
+ fs.mkdirSync('new_dir'); //fs.mkdirSync([folder]) seems to behave differently than grunt.file.write('[folder]/[file]')
+ fs.writeFileSync(path.join('new_dir', 'first.js'), '');
+
+ watcher.on('end', test.done);
+ });
+ },
+ mkdirThenAddFileWithGruntFileWrite: function(test) {
+ var expected = [
+ 'new_dir/tmp.js',
+ 'new_dir/other.js',
+ ];
+ test.expect(expected.length);
+
+ gaze('**/*.js', function(err, watcher) {
+ watcher.on('all', function(status, filepath) {
+ var expect = expected.shift();
+ var actual = helper.unixifyobj(path.relative(process.cwd(), filepath));
+ test.equal(actual, expect);
+
+ if (expected.length === 1) {
+ // Ensure the new folder is being watched correctly after initial add
+ setTimeout(function() {
+ fs.writeFileSync('new_dir/dontmatch.txt', '');
+ setTimeout(function() {
+ fs.writeFileSync('new_dir/other.js', '');
+ }, 1000);
+ }, 1000);
+ }
+
+ if (expected.length < 1) { watcher.close(); }
+ });
+
+ grunt.file.write('new_dir/tmp.js', '');
+
+ watcher.on('end', test.done);
+ });
+ },
+ enoentSymlink: function(test) {
+ test.expect(1);
+ fs.mkdirSync(path.resolve(__dirname, 'fixtures', 'new_dir'));
+ fs.symlinkSync(path.resolve(__dirname, 'fixtures', 'not-exists.js'), path.resolve(__dirname, 'fixtures', 'new_dir', 'not-exists-symlink.js'));
+ gaze('**/*', function() {
+ test.ok(true);
+ this.on('end', test.done);
+ this.close();
+ });
+ },
+};
+
+// Ignore these tests if node v0.8
+var version = process.versions.node.split('.');
+if (version[0] === '0' && version[1] === '8') {
+ // gaze v0.4 needs to watch the cwd to function
+ delete exports.watch.dontAddCwd;
+ // gaze 0.4 incorrecly matches folders, wontfix
+ delete exports.watch.mkdirThenAddFileWithGruntFileWrite;
+ delete exports.watch.mkdirThenAddFile;
+}
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-gaze.git
More information about the Pkg-javascript-commits
mailing list