orionsoft-react-scripts
Version:
Orionsoft Configuration and scripts for Create React App.
356 lines (311 loc) • 8.06 kB
JavaScript
'use strict';
var fs = require('fs');
var path = require('path');
var walker = require('walker');
var common = require('./common');
var platform = require('os').platform();
var EventEmitter = require('events').EventEmitter;
/**
* Constants
*/
var DEFAULT_DELAY = common.DEFAULT_DELAY;
var CHANGE_EVENT = common.CHANGE_EVENT;
var DELETE_EVENT = common.DELETE_EVENT;
var ADD_EVENT = common.ADD_EVENT;
var ALL_EVENT = common.ALL_EVENT;
/**
* Export `NodeWatcher` class.
*/
module.exports = NodeWatcher;
/**
* Watches `dir`.
*
* @class NodeWatcher
* @param String dir
* @param {Object} opts
* @public
*/
function NodeWatcher(dir, opts) {
opts = common.assignOptions(this, opts);
this.watched = Object.create(null);
this.changeTimers = Object.create(null);
this.dirRegistery = Object.create(null);
this.root = path.resolve(dir);
this.watchdir = this.watchdir.bind(this);
this.register = this.register.bind(this);
this.watchdir(this.root);
recReaddir(
this.root,
this.watchdir,
this.register,
this.emit.bind(this, 'ready')
);
}
NodeWatcher.prototype.__proto__ = EventEmitter.prototype;
/**
* Register files that matches our globs to know what to type of event to
* emit in the future.
*
* Registery looks like the following:
*
* dirRegister => Map {
* dirpath => Map {
* filename => true
* }
* }
*
* @param {string} filepath
* @return {boolean} whether or not we have registered the file.
* @private
*/
NodeWatcher.prototype.register = function(filepath) {
var relativePath = path.relative(this.root, filepath);
if (!common.isFileIncluded(this.globs, this.dot, relativePath)) {
return false;
}
var dir = path.dirname(filepath);
if (!this.dirRegistery[dir]) {
this.dirRegistery[dir] = Object.create(null);
}
var filename = path.basename(filepath);
this.dirRegistery[dir][filename] = true;
return true;
};
/**
* Removes a file from the registery.
*
* @param {string} filepath
* @private
*/
NodeWatcher.prototype.unregister = function(filepath) {
var dir = path.dirname(filepath);
if (this.dirRegistery[dir]) {
var filename = path.basename(filepath);
delete this.dirRegistery[dir][filename];
}
};
/**
* Removes a dir from the registery.
*
* @param {string} dirpath
* @private
*/
NodeWatcher.prototype.unregisterDir = function(dirpath) {
if (this.dirRegistery[dirpath]) {
delete this.dirRegistery[dirpath];
}
};
/**
* Checks if a file or directory exists in the registery.
*
* @param {string} fullpath
* @return {boolean}
* @private
*/
NodeWatcher.prototype.registered = function(fullpath) {
var dir = path.dirname(fullpath);
return this.dirRegistery[fullpath] ||
this.dirRegistery[dir] && this.dirRegistery[dir][path.basename(fullpath)];
};
/**
* Watch a directory.
*
* @param {string} dir
* @private
*/
NodeWatcher.prototype.watchdir = function(dir) {
if (this.watched[dir]) {
return;
}
var watcher = fs.watch(
dir,
{ persistent: true },
this.normalizeChange.bind(this, dir)
);
this.watched[dir] = watcher;
// Workaround Windows node issue #4337.
if (platform === 'win32') {
watcher.on('error', function(error) {
if (error.code !== 'EPERM') {
throw error;
}
});
}
if (this.root !== dir) {
this.register(dir);
}
};
/**
* Stop watching a directory.
*
* @param {string} dir
* @private
*/
NodeWatcher.prototype.stopWatching = function(dir) {
if (this.watched[dir]) {
this.watched[dir].close();
delete this.watched[dir];
}
};
/**
* End watching.
*
* @public
*/
NodeWatcher.prototype.close = function(callback) {
Object.keys(this.watched).forEach(this.stopWatching, this);
this.removeAllListeners();
if (typeof callback === 'function') {
setImmediate(callback.bind(null, null, true));
}
};
/**
* On some platforms, as pointed out on the fs docs (most likely just win32)
* the file argument might be missing from the fs event. Try to detect what
* change by detecting if something was deleted or the most recent file change.
*
* @param {string} dir
* @param {string} event
* @param {string} file
* @public
*/
NodeWatcher.prototype.detectChangedFile = function(dir, event, callback) {
if (!this.dirRegistery[dir]) {
return;
}
var found = false;
var closest = {mtime: 0};
var c = 0;
Object.keys(this.dirRegistery[dir]).forEach(function(file, i, arr) {
fs.lstat(path.join(dir, file), function(error, stat) {
if (found) {
return;
}
if (error) {
if (error.code === 'ENOENT' ||
(platform === 'win32' && error.code === 'EPERM')) {
found = true;
callback(file);
} else {
this.emit('error', error);
}
} else {
if (stat.mtime > closest.mtime) {
stat.file = file;
closest = stat;
}
if (arr.length === ++c) {
callback(closest.file);
}
}
}.bind(this));
}, this);
};
/**
* Normalize fs events and pass it on to be processed.
*
* @param {string} dir
* @param {string} event
* @param {string} file
* @public
*/
NodeWatcher.prototype.normalizeChange = function(dir, event, file) {
if (!file) {
this.detectChangedFile(dir, event, function(actualFile) {
if (actualFile) {
this.processChange(dir, event, actualFile);
}
}.bind(this));
} else {
this.processChange(dir, event, path.normalize(file));
}
};
/**
* Process changes.
*
* @param {string} dir
* @param {string} event
* @param {string} file
* @public
*/
NodeWatcher.prototype.processChange = function(dir, event, file) {
var fullPath = path.join(dir, file);
var relativePath = path.join(path.relative(this.root, dir), file);
fs.lstat(fullPath, function(error, stat) {
if (error && error.code !== 'ENOENT') {
this.emit('error', error);
} else if (!error && stat.isDirectory()) {
// win32 emits usless change events on dirs.
if (event !== 'change') {
this.watchdir(fullPath);
if (common.isFileIncluded(this.globs, this.dot, relativePath)) {
this.emitEvent(ADD_EVENT, relativePath, stat);
}
}
} else {
var registered = this.registered(fullPath);
if (error && error.code === 'ENOENT') {
this.unregister(fullPath);
this.stopWatching(fullPath);
this.unregisterDir(fullPath);
if (registered) {
this.emitEvent(DELETE_EVENT, relativePath);
}
} else if (registered) {
this.emitEvent(CHANGE_EVENT, relativePath, stat);
} else {
if (this.register(fullPath)) {
this.emitEvent(ADD_EVENT, relativePath, stat);
}
}
}
}.bind(this));
};
/**
* Triggers a 'change' event after debounding it to take care of duplicate
* events on os x.
*
* @private
*/
NodeWatcher.prototype.emitEvent = function(type, file, stat) {
var key = type + '-' + file;
clearTimeout(this.changeTimers[key]);
this.changeTimers[key] = setTimeout(function() {
delete this.changeTimers[key];
this.emit(type, file, this.root, stat);
this.emit(ALL_EVENT, type, file, this.root, stat);
}.bind(this), DEFAULT_DELAY);
};
/**
* Traverse a directory recursively calling `callback` on every directory.
*
* @param {string} dir
* @param {function} callback
* @param {function} endCallback
* @private
*/
function recReaddir(dir, dirCallback, fileCallback, endCallback) {
walker(dir)
.on('dir', normalizeProxy(dirCallback))
.on('file', normalizeProxy(fileCallback))
.on('end', function() {
if (platform === 'win32') {
setTimeout(endCallback, 1000);
} else {
endCallback();
}
});
}
/**
* Returns a callback that when called will normalize a path and call the
* original callback
*
* @param {function} callback
* @return {function}
* @private
*/
function normalizeProxy(callback) {
return function(filepath) {
return callback(path.normalize(filepath));
};
}