aoc/node_modules/jest-haste-map/build/watchers/NodeWatcher.js

370 lines
8.7 KiB
JavaScript

// vendored from https://github.com/amasad/sane/blob/64ff3a870c42e84f744086884bf55a4f9c22d376/src/node_watcher.js
'use strict';
const EventEmitter = require('events').EventEmitter;
const fs = require('fs');
const platform = require('os').platform();
const path = require('path');
const common = require('./common');
/**
* Constants
*/
const DEFAULT_DELAY = common.DEFAULT_DELAY;
const CHANGE_EVENT = common.CHANGE_EVENT;
const DELETE_EVENT = common.DELETE_EVENT;
const ADD_EVENT = common.ADD_EVENT;
const ALL_EVENT = common.ALL_EVENT;
/**
* Export `NodeWatcher` class.
* Watches `dir`.
*
* @class NodeWatcher
* @param {String} dir
* @param {Object} opts
* @public
*/
module.exports = class NodeWatcher extends EventEmitter {
constructor(dir, opts) {
super();
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.checkedEmitError = this.checkedEmitError.bind(this);
this.watchdir(this.root);
common.recReaddir(
this.root,
this.watchdir,
this.register,
this.emit.bind(this, 'ready'),
this.checkedEmitError,
this.ignored
);
}
/**
* 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
*/
register(filepath) {
const relativePath = path.relative(this.root, filepath);
if (
!common.isFileIncluded(this.globs, this.dot, this.doIgnore, relativePath)
) {
return false;
}
const dir = path.dirname(filepath);
if (!this.dirRegistery[dir]) {
this.dirRegistery[dir] = Object.create(null);
}
const filename = path.basename(filepath);
this.dirRegistery[dir][filename] = true;
return true;
}
/**
* Removes a file from the registery.
*
* @param {string} filepath
* @private
*/
unregister(filepath) {
const dir = path.dirname(filepath);
if (this.dirRegistery[dir]) {
const filename = path.basename(filepath);
delete this.dirRegistery[dir][filename];
}
}
/**
* Removes a dir from the registery.
*
* @param {string} dirpath
* @private
*/
unregisterDir(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
*/
registered(fullpath) {
const dir = path.dirname(fullpath);
return (
this.dirRegistery[fullpath] ||
(this.dirRegistery[dir] &&
this.dirRegistery[dir][path.basename(fullpath)])
);
}
/**
* Emit "error" event if it's not an ignorable event
*
* @param error
* @private
*/
checkedEmitError(error) {
if (!isIgnorableFileError(error)) {
this.emit('error', error);
}
}
/**
* Watch a directory.
*
* @param {string} dir
* @private
*/
watchdir(dir) {
if (this.watched[dir]) {
return;
}
const watcher = fs.watch(
dir,
{
persistent: true
},
this.normalizeChange.bind(this, dir)
);
this.watched[dir] = watcher;
watcher.on('error', this.checkedEmitError);
if (this.root !== dir) {
this.register(dir);
}
}
/**
* Stop watching a directory.
*
* @param {string} dir
* @private
*/
stopWatching(dir) {
if (this.watched[dir]) {
this.watched[dir].close();
delete this.watched[dir];
}
}
/**
* End watching.
*
* @public
*/
close() {
Object.keys(this.watched).forEach(this.stopWatching, this);
this.removeAllListeners();
return Promise.resolve();
}
/**
* 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
*/
detectChangedFile(dir, event, callback) {
if (!this.dirRegistery[dir]) {
return;
}
let found = false;
let closest = {
mtime: 0
};
let c = 0;
Object.keys(this.dirRegistery[dir]).forEach(function (file, i, arr) {
fs.lstat(path.join(dir, file), (error, stat) => {
if (found) {
return;
}
if (error) {
if (isIgnorableFileError(error)) {
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);
}
}
});
}, this);
}
/**
* Normalize fs events and pass it on to be processed.
*
* @param {string} dir
* @param {string} event
* @param {string} file
* @public
*/
normalizeChange(dir, event, file) {
if (!file) {
this.detectChangedFile(dir, event, actualFile => {
if (actualFile) {
this.processChange(dir, event, actualFile);
}
});
} else {
this.processChange(dir, event, path.normalize(file));
}
}
/**
* Process changes.
*
* @param {string} dir
* @param {string} event
* @param {string} file
* @public
*/
processChange(dir, event, file) {
const fullPath = path.join(dir, file);
const relativePath = path.join(path.relative(this.root, dir), file);
fs.lstat(fullPath, (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,
this.doIgnore,
relativePath
)
) {
this.emitEvent(ADD_EVENT, relativePath, stat);
}
}
} else {
const 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);
}
}
}
});
}
/**
* Triggers a 'change' event after debounding it to take care of duplicate
* events on os x.
*
* @private
*/
emitEvent(type, file, stat) {
const key = `${type}-${file}`;
const addKey = `${ADD_EVENT}-${file}`;
if (type === CHANGE_EVENT && this.changeTimers[addKey]) {
// Ignore the change event that is immediately fired after an add event.
// (This happens on Linux).
return;
}
clearTimeout(this.changeTimers[key]);
this.changeTimers[key] = setTimeout(() => {
delete this.changeTimers[key];
if (type === ADD_EVENT && stat.isDirectory()) {
// Recursively emit add events and watch for sub-files/folders
common.recReaddir(
path.resolve(this.root, file),
function emitAddDir(dir, stats) {
this.watchdir(dir);
this.rawEmitEvent(ADD_EVENT, path.relative(this.root, dir), stats);
}.bind(this),
function emitAddFile(file, stats) {
this.register(file);
this.rawEmitEvent(ADD_EVENT, path.relative(this.root, file), stats);
}.bind(this),
function endCallback() {},
this.checkedEmitError,
this.ignored
);
} else {
this.rawEmitEvent(type, file, stat);
}
}, DEFAULT_DELAY);
}
/**
* Actually emit the events
*/
rawEmitEvent(type, file, stat) {
this.emit(type, file, this.root, stat);
this.emit(ALL_EVENT, type, file, this.root, stat);
}
};
/**
* Determine if a given FS error can be ignored
*
* @private
*/
function isIgnorableFileError(error) {
return (
error.code === 'ENOENT' ||
// Workaround Windows node issue #4337.
(error.code === 'EPERM' && platform === 'win32')
);
}