package/package.json000644 0000002124 3560116604 011545 0ustar00000000 000000 { "name": "gaze", "description": "A globbing fs.watch wrapper built from the best parts of other fine watch libs.", "version": "1.1.3", "homepage": "https://github.com/shama/gaze", "author": { "name": "Kyle Robinson Young", "email": "kyle@dontkry.com" }, "repository": { "type": "git", "url": "https://github.com/shama/gaze.git" }, "bugs": { "url": "https://github.com/shama/gaze/issues" }, "license": "MIT", "main": "lib/gaze", "engines": { "node": ">= 4.0.0" }, "scripts": { "test": "semistandard && grunt nodeunit -v" }, "dependencies": { "globule": "^1.0.0" }, "devDependencies": { "async": "^2.6.1", "grunt": "^1.0.1", "grunt-benchmark": "^1.0.0", "grunt-cli": "^1.2.0", "grunt-contrib-jshint": "^1.1.0", "grunt-contrib-nodeunit": "^2.0.0", "rimraf": "^2.5.2", "semistandard": "^12.0.1" }, "keywords": [ "watch", "glob" ], "files": [ "lib", "LICENSE-MIT" ], "semistandard": { "ignore": [ "benchmarks", "experiments", "build", "test" ] } } package/LICENSE-MIT000644 0000002047 3560116604 010717 0ustar00000000 000000 Copyright (c) 2018 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. package/README.md000644 0000020276 3560116604 010546 0ustar00000000 000000 # gaze [![Build Status](http://img.shields.io/travis/shama/gaze.svg)](https://travis-ci.org/shama/gaze) [![Build status](https://ci.appveyor.com/api/projects/status/vtx65w9eg511tgo4)](https://ci.appveyor.com/project/shama/gaze) A globbing `fs.watch` wrapper built from the best parts of other fine watch libs. Compatible with Node.js >= 4.x, Windows, macOS, and Linux. ![gaze](http://dontkry.com/images/repos/gaze.png) [![NPM](https://nodei.co/npm/gaze.png?downloads=true)](https://nodei.co/npm/gaze/) ## 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 var watched = this.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 var files = this.relative(); }); // 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('**/*'); ``` ### 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 in milliseconds * `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`, `renamed`, 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 `EventEmitter.emit`. `added`|`changed`|`renamed`|`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()` Returns the currently watched files. * `relative([dir, unixify])` 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. ## Similar Projects Other great watch libraries to try are: * [paulmillr's `chokidar`](https://github.com/paulmillr/chokidar) * [amasad's `sane`](https://github.com/amasad/sane) * [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 * 1.1.3 - Fix for Node 10 support (@aredridel). Officially dropping support for Node < 4. * 1.1.2 - Prevent more `ENOENT` errors from escaping (@alexgorbatchev). * 1.1.1 - Prevent `fs.watch` errors from escaping error handler (@rosen-vladimirov). Fix `_addToWatched` without `path.sep` (@wyicwx). * 1.1.0 - Update to `globule@1.0.0` with `minimatch >= 3.0.0`. * 1.0.0 - Revert back to 0.5.2. Drop support for Node.js v0.8. Fix for `maxListeners`. Update `globule` to `0.2.0`. * 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.js 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@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.2 - Fix for `ENOENT` error with non-existent symlinks [BACKPORTED]. * 0.5.1 - Use `setImmediate` (`process.nextTick` for Node.js 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.js v0.6. Use `globule` for file matching. Avoid Node.js 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) 2018 Kyle Robinson Young Licensed under the MIT license. package/lib/gaze.js000644 0000031371 3560116604 011317 0ustar00000000 000000 /* * gaze * https://github.com/shama/gaze * * Copyright (c) 2018 Kyle Robinson Young * Licensed under the MIT license. */ 'use strict'; // libs var util = require('util'); var EE = require('events').EventEmitter; var fs = require('fs'); var path = require('path'); var globule = require('globule'); var helper = require('./helper'); // shim setImmediate for node v0.8 var setImmediate = require('timers').setImmediate; if (typeof setImmediate !== 'function') { setImmediate = process.nextTick; } // globals var delay = 10; // `Gaze` EventEmitter object to return in the callback function Gaze (patterns, opts, done) { var self = this; EE.call(self); // If second arg is the callback 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 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)) { // It's possible that between `isDir` and `readdirSync()` calls the `filepath` // gets removed, which will result in `ENOENT` exception var files; try { files = fs.readdirSync(filepath); } catch (e) { // Rethrow the error if it's anything other than `ENOENT` if (e.code !== 'ENOENT') { throw e; } files = []; } files.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; 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 !== false) { 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 () { return this._watched; }; // Returns `watched` files with relative paths to process.cwd() Gaze.prototype.relative = function (dir, unixify) { 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); } return dir ? relative[dir] || [] : relative; }; // Adds files and dirs to watched Gaze.prototype._addToWatched = function (files) { var dirs = []; 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) && !(dirname in this._watched)) { helper.objectPush(this._watched, dirname, []); } if (file.slice(-1) === '/') { filepath += path.sep; } helper.objectPush(this._watched, path.dirname(filepath) + path.sep, filepath); dirs.push(dirname); } dirs = helper.unique(dirs); for (var k = 0; k < dirs.length; k++) { dirname = dirs[k]; // 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; 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); }); this._watchers[dir].on('error', function (err) { self._handleError(err); }); } 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) { // Defer to emitting to give a chance to attach event handlers. setImmediate(function () { 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 var previous = self.relative(relDir); // 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) { if (err) { self.emit('error', err); return; } // 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); }; package/lib/helper.js000644 0000004310 3560116604 011641 0ustar00000000 000000 '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); }; // 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); } obj[key] = helper.unique(obj[key]); return 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; }; /** * Lo-Dash 1.0.1 * Copyright 2012-2013 The Dojo Foundation * Based on Underscore.js 1.4.4 * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. * Available under MIT 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 */ helper.forEachSeries = function forEachSeries (arr, iterator, callback) { 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(); };