256 lines
7.2 KiB
JavaScript
256 lines
7.2 KiB
JavaScript
var assert = require("assert");
|
|
var path = require("path");
|
|
var fs = require("graceful-fs");
|
|
var spawn = require("child_process").spawn;
|
|
var Q = require("q");
|
|
var EventEmitter = require("events").EventEmitter;
|
|
var ReadFileCache = require("./cache").ReadFileCache;
|
|
var util = require("./util");
|
|
var hasOwn = Object.prototype.hasOwnProperty;
|
|
|
|
function Watcher(readFileCache, persistent) {
|
|
assert.ok(this instanceof Watcher);
|
|
assert.ok(this instanceof EventEmitter);
|
|
assert.ok(readFileCache instanceof ReadFileCache);
|
|
|
|
// During tests (and only during tests), persistent === false so that
|
|
// the test suite can actually finish and exit.
|
|
if (typeof persistent === "undefined") {
|
|
persistent = true;
|
|
}
|
|
|
|
EventEmitter.call(this);
|
|
|
|
var self = this;
|
|
var sourceDir = readFileCache.sourceDir;
|
|
var dirWatcher = new DirWatcher(sourceDir, persistent);
|
|
|
|
Object.defineProperties(self, {
|
|
sourceDir: { value: sourceDir },
|
|
readFileCache: { value: readFileCache },
|
|
dirWatcher: { value: dirWatcher }
|
|
});
|
|
|
|
// Watch everything the readFileCache already knows about, and any new
|
|
// files added in the future.
|
|
readFileCache.subscribe(function(relativePath) {
|
|
self.watch(relativePath);
|
|
});
|
|
|
|
readFileCache.on("changed", function(relativePath) {
|
|
self.emit("changed", relativePath);
|
|
});
|
|
|
|
function handleDirEvent(event, relativePath) {
|
|
if (self.dirWatcher.ready) {
|
|
self.getFileHandler(relativePath)(event);
|
|
}
|
|
}
|
|
|
|
dirWatcher.on("added", function(relativePath) {
|
|
handleDirEvent("added", relativePath);
|
|
}).on("deleted", function(relativePath) {
|
|
handleDirEvent("deleted", relativePath);
|
|
}).on("changed", function(relativePath) {
|
|
handleDirEvent("changed", relativePath);
|
|
});
|
|
}
|
|
|
|
util.inherits(Watcher, EventEmitter);
|
|
var Wp = Watcher.prototype;
|
|
|
|
Wp.watch = function(relativePath) {
|
|
this.dirWatcher.add(path.dirname(path.join(
|
|
this.sourceDir, relativePath)));
|
|
};
|
|
|
|
Wp.readFileP = function(relativePath) {
|
|
return this.readFileCache.readFileP(relativePath);
|
|
};
|
|
|
|
Wp.noCacheReadFileP = function(relativePath) {
|
|
return this.readFileCache.noCacheReadFileP(relativePath);
|
|
};
|
|
|
|
Wp.getFileHandler = util.cachedMethod(function(relativePath) {
|
|
var self = this;
|
|
return function handler(event) {
|
|
self.readFileCache.reportPossiblyChanged(relativePath);
|
|
};
|
|
});
|
|
|
|
function orNull(err) {
|
|
return null;
|
|
}
|
|
|
|
Wp.close = function() {
|
|
this.dirWatcher.close();
|
|
};
|
|
|
|
/**
|
|
* DirWatcher code adapted from Jeffrey Lin's original implementation:
|
|
* https://github.com/jeffreylin/jsx_transformer_fun/blob/master/dirWatcher.js
|
|
*
|
|
* Invariant: this only watches the dir inode, not the actual path.
|
|
* That means the dir can't be renamed and swapped with another dir.
|
|
*/
|
|
function DirWatcher(inputPath, persistent) {
|
|
assert.ok(this instanceof DirWatcher);
|
|
|
|
var self = this;
|
|
var absPath = path.resolve(inputPath);
|
|
|
|
if (!fs.statSync(absPath).isDirectory()) {
|
|
throw new Error(inputPath + "is not a directory!");
|
|
}
|
|
|
|
EventEmitter.call(self);
|
|
|
|
self.ready = false;
|
|
self.on("ready", function(){
|
|
self.ready = true;
|
|
});
|
|
|
|
Object.defineProperties(self, {
|
|
// Map of absDirPaths to fs.FSWatcher objects from fs.watch().
|
|
watchers: { value: {} },
|
|
dirContents: { value: {} },
|
|
rootPath: { value: absPath },
|
|
persistent: { value: !!persistent }
|
|
});
|
|
|
|
process.nextTick(function() {
|
|
self.add(absPath);
|
|
self.emit("ready");
|
|
});
|
|
}
|
|
|
|
util.inherits(DirWatcher, EventEmitter);
|
|
var DWp = DirWatcher.prototype;
|
|
|
|
DWp.add = function(absDirPath) {
|
|
var self = this;
|
|
if (hasOwn.call(self.watchers, absDirPath)) {
|
|
return;
|
|
}
|
|
|
|
self.watchers[absDirPath] = fs.watch(absDirPath, {
|
|
persistent: this.persistent
|
|
}).on("change", function(event, filename) {
|
|
self.updateDirContents(absDirPath, event, filename);
|
|
});
|
|
|
|
// Update internal dir contents.
|
|
self.updateDirContents(absDirPath);
|
|
|
|
// Since we've never seen this path before, recursively add child
|
|
// directories of this path. TODO: Don't do fs.readdirSync on the
|
|
// same dir twice in a row. We already do an fs.statSync in
|
|
// this.updateDirContents() and we're just going to do another one
|
|
// here...
|
|
fs.readdirSync(absDirPath).forEach(function(filename) {
|
|
var filepath = path.join(absDirPath, filename);
|
|
|
|
// Look for directories.
|
|
if (fs.statSync(filepath).isDirectory()) {
|
|
self.add(filepath);
|
|
}
|
|
});
|
|
};
|
|
|
|
DWp.updateDirContents = function(absDirPath, event, fsWatchReportedFilename) {
|
|
var self = this;
|
|
|
|
if (!hasOwn.call(self.dirContents, absDirPath)) {
|
|
self.dirContents[absDirPath] = [];
|
|
}
|
|
|
|
var oldContents = self.dirContents[absDirPath];
|
|
var newContents = fs.readdirSync(absDirPath);
|
|
|
|
var deleted = {};
|
|
var added = {};
|
|
|
|
oldContents.forEach(function(filename) {
|
|
deleted[filename] = true;
|
|
});
|
|
|
|
newContents.forEach(function(filename) {
|
|
if (hasOwn.call(deleted, filename)) {
|
|
delete deleted[filename];
|
|
} else {
|
|
added[filename] = true;
|
|
}
|
|
});
|
|
|
|
var deletedNames = Object.keys(deleted);
|
|
deletedNames.forEach(function(filename) {
|
|
self.emit(
|
|
"deleted",
|
|
path.relative(
|
|
self.rootPath,
|
|
path.join(absDirPath, filename)
|
|
)
|
|
);
|
|
});
|
|
|
|
var addedNames = Object.keys(added);
|
|
addedNames.forEach(function(filename) {
|
|
self.emit(
|
|
"added",
|
|
path.relative(
|
|
self.rootPath,
|
|
path.join(absDirPath, filename)
|
|
)
|
|
);
|
|
});
|
|
|
|
// So changed is not deleted or added?
|
|
if (fsWatchReportedFilename &&
|
|
!hasOwn.call(deleted, fsWatchReportedFilename) &&
|
|
!hasOwn.call(added, fsWatchReportedFilename))
|
|
{
|
|
self.emit(
|
|
"changed",
|
|
path.relative(
|
|
self.rootPath,
|
|
path.join(absDirPath, fsWatchReportedFilename)
|
|
)
|
|
);
|
|
}
|
|
|
|
// If any of the things removed were directories, remove their watchers.
|
|
// If a dir was moved, hopefully two changed events fired?
|
|
// 1) event in dir where it was removed
|
|
// 2) event in dir where it was moved to (added)
|
|
deletedNames.forEach(function(filename) {
|
|
var filepath = path.join(absDirPath, filename);
|
|
delete self.dirContents[filepath];
|
|
delete self.watchers[filepath];
|
|
});
|
|
|
|
// if any of the things added were directories, recursively deal with them
|
|
addedNames.forEach(function(filename) {
|
|
var filepath = path.join(absDirPath, filename);
|
|
if (fs.existsSync(filepath) &&
|
|
fs.statSync(filepath).isDirectory())
|
|
{
|
|
self.add(filepath);
|
|
// mighttttttt need a self.updateDirContents() here in case
|
|
// we're somehow adding a path that replaces another one...?
|
|
}
|
|
});
|
|
|
|
// Update state of internal dir contents.
|
|
self.dirContents[absDirPath] = newContents;
|
|
};
|
|
|
|
DWp.close = function() {
|
|
var watchers = this.watchers;
|
|
Object.keys(watchers).forEach(function(filename) {
|
|
watchers[filename].close();
|
|
});
|
|
};
|
|
|
|
exports.Watcher = Watcher;
|