[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