diff options
Diffstat (limited to 'node_modules/gaze/lib')
-rw-r--r-- | node_modules/gaze/lib/gaze.js | 469 | ||||
-rw-r--r-- | node_modules/gaze/lib/helper.js | 84 |
2 files changed, 553 insertions, 0 deletions
diff --git a/node_modules/gaze/lib/gaze.js b/node_modules/gaze/lib/gaze.js new file mode 100644 index 0000000..1f556a6 --- /dev/null +++ b/node_modules/gaze/lib/gaze.js @@ -0,0 +1,469 @@ +/* + * 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); +}; diff --git a/node_modules/gaze/lib/helper.js b/node_modules/gaze/lib/helper.js new file mode 100644 index 0000000..8e5727d --- /dev/null +++ b/node_modules/gaze/lib/helper.js @@ -0,0 +1,84 @@ +'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 <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) { + 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(); +}; |