[react-packager] Watch asset roots for changes to update dependency graph

This commit is contained in:
Amjad Masad 2015-03-27 09:34:37 -07:00
parent c74da17c7c
commit 4a67c84426
7 changed files with 175 additions and 56 deletions

View File

@ -821,6 +821,55 @@ describe('DependencyGraph', function() {
});
});
pit('updates module dependencies on asset add', function() {
var root = '/root';
var filesystem = fs.__setMockFilesystem({
'root': {
'index.js': [
'/**',
' * @providesModule index',
' */',
'require("image!foo")'
].join('\n'),
},
});
var dgraph = new DependencyGraph({
roots: [root],
assetRoots: [root],
assetExts: ['png'],
fileWatcher: fileWatcher
});
return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([
{ id: 'index', altId: '/root/index.js',
path: '/root/index.js',
dependencies: ['image!foo']
}
]);
filesystem.root['foo.png'] = '';
triggerFileChange('add', 'foo.png', root);
return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([
{ id: 'index', altId: '/root/index.js',
path: '/root/index.js',
dependencies: ['image!foo']
},
{ id: 'image!foo',
path: '/root/foo.png',
dependencies: [],
isAsset: true,
},
]);
});
});
});
pit('runs changes through ignore filter', function() {
var root = '/root';
var filesystem = fs.__setMockFilesystem({

View File

@ -482,7 +482,12 @@ DependecyGraph.prototype._lookupPackage = function(modulePath) {
/**
* Process a filewatcher change event.
*/
DependecyGraph.prototype._processFileChange = function(eventType, filePath, root, stat) {
DependecyGraph.prototype._processFileChange = function(
eventType,
filePath,
root,
stat
) {
var absPath = path.join(root, filePath);
if (this._ignoreFilePath(absPath)) {
return;
@ -490,6 +495,11 @@ DependecyGraph.prototype._processFileChange = function(eventType, filePath, root
this._debugUpdateEvents.push({event: eventType, path: filePath});
if (this._assetExts.indexOf(extname(filePath)) > -1) {
this._processAssetChange(eventType, absPath);
return;
}
var isPackage = path.basename(filePath) === 'package.json';
if (eventType === 'delete') {
if (isPackage) {
@ -524,7 +534,8 @@ DependecyGraph.prototype.getDebugInfo = function() {
};
/**
* Searches all roots for the file and returns the first one that has file of the same path.
* Searches all roots for the file and returns the first one that has file of
* the same path.
*/
DependecyGraph.prototype._getAbsolutePath = function(filePath) {
if (isAbsolutePath(filePath)) {
@ -547,12 +558,43 @@ DependecyGraph.prototype._buildAssetMap = function() {
return q();
}
var self = this;
return buildAssetMap(this._assetRoots, this._assetExts)
.then(function(map) {
self._assetMap = map;
return map;
this._assetMap = Object.create(null);
return buildAssetMap(
this._assetRoots,
this._processAsset.bind(this)
);
};
DependecyGraph.prototype._processAsset = function(file) {
var ext = extname(file);
if (this._assetExts.indexOf(ext) !== -1) {
var name = assetName(file, ext);
if (this._assetMap[name] != null) {
debug('Conflcting assets', name);
}
this._assetMap[name] = new ModuleDescriptor({
id: 'image!' + name,
path: path.resolve(file),
isAsset: true,
dependencies: [],
});
}
};
DependecyGraph.prototype._processAssetChange = function(eventType, file) {
if (this._assetMap == null) {
return;
}
var name = assetName(file, extname(file));
if (eventType === 'change' || eventType === 'delete') {
delete this._assetMap[name];
}
if (eventType === 'change' || eventType === 'add') {
this._processAsset(file);
}
};
/**
@ -627,15 +669,14 @@ function readAndStatDir(dir) {
* Given a list of roots and list of extensions find all the files in
* the directory with that extension and build a map of those assets.
*/
function buildAssetMap(roots, exts) {
function buildAssetMap(roots, processAsset) {
var queue = roots.slice(0);
var map = Object.create(null);
function search() {
var root = queue.shift();
if (root == null) {
return q(map);
return q();
}
return readAndStatDir(root).spread(function(files, stats) {
@ -643,21 +684,7 @@ function buildAssetMap(roots, exts) {
if (stats[i].isDirectory()) {
queue.push(file);
} else {
var ext = path.extname(file).replace(/^\./, '');
if (exts.indexOf(ext) !== -1) {
var assetName = path.basename(file, '.' + ext)
.replace(/@[\d\.]+x/, '');
if (map[assetName] != null) {
debug('Conflcting assets', assetName);
}
map[assetName] = new ModuleDescriptor({
id: 'image!' + assetName,
path: path.resolve(file),
isAsset: true,
dependencies: [],
});
}
processAsset(file);
}
});
@ -668,6 +695,15 @@ function buildAssetMap(roots, exts) {
return search();
}
function assetName(file, ext) {
return path.basename(file, '.' + ext).replace(/@[\d\.]+x/, '');
}
function extname(name) {
return path.extname(name).replace(/^\./, '');
}
function NotFoundError() {
Error.call(this);
Error.captureStackTrace(this, this.constructor);

View File

@ -51,15 +51,15 @@ var validateOpts = declareOpts({
type: 'array',
default: [],
},
fileWatcher: {
type: 'object',
required: true,
},
});
function HasteDependencyResolver(options) {
var opts = validateOpts(options);
this._fileWatcher = opts.nonPersistent
? FileWatcher.createDummyWatcher()
: new FileWatcher(opts.projectRoots);
this._depGraph = new DependencyGraph({
roots: opts.projectRoots,
assetRoots: opts.assetRoots,
@ -67,7 +67,7 @@ function HasteDependencyResolver(options) {
return filepath.indexOf('__tests__') !== -1 ||
(opts.blacklistRE && opts.blacklistRE.test(filepath));
},
fileWatcher: this._fileWatcher,
fileWatcher: opts.fileWatcher,
});
@ -164,10 +164,6 @@ HasteDependencyResolver.prototype.wrapModule = function(module, code) {
});
};
HasteDependencyResolver.prototype.end = function() {
return this._fileWatcher.end();
};
HasteDependencyResolver.prototype.getDebugInfo = function() {
return this._depGraph.getDebugInfo();
};

View File

@ -21,6 +21,7 @@ describe('FileWatcher', function() {
var Watcher;
beforeEach(function() {
require('mock-modules').dumpCache();
FileWatcher = require('../');
Watcher = require('sane').WatchmanWatcher;
Watcher.prototype.once.mockImplementation(function(type, callback) {

View File

@ -16,7 +16,7 @@ var exec = require('child_process').exec;
var Promise = q.Promise;
var detectingWatcherClass = new Promise(function(resolve, reject) {
var detectingWatcherClass = new Promise(function(resolve) {
exec('which watchman', function(err, out) {
if (err || out.length === 0) {
resolve(sane.NodeWatcher);
@ -30,14 +30,23 @@ module.exports = FileWatcher;
var MAX_WAIT_TIME = 3000;
function FileWatcher(projectRoots) {
var self = this;
// Singleton
var fileWatcher = null;
function FileWatcher(rootConfigs) {
if (fileWatcher) {
// This allows us to optimize watching in the future by merging roots etc.
throw new Error('FileWatcher can only be instantiated once');
}
fileWatcher = this;
this._loading = q.all(
projectRoots.map(createWatcher)
rootConfigs.map(createWatcher)
).then(function(watchers) {
watchers.forEach(function(watcher) {
watcher.on('all', function(type, filepath, root) {
self.emit('all', type, filepath, root);
fileWatcher.emit('all', type, filepath, root);
});
});
return watchers;
@ -50,21 +59,14 @@ util.inherits(FileWatcher, EventEmitter);
FileWatcher.prototype.end = function() {
return this._loading.then(function(watchers) {
watchers.forEach(function(watcher) {
delete watchersByRoot[watcher._root];
return q.ninvoke(watcher, 'close');
});
});
};
var watchersByRoot = Object.create(null);
function createWatcher(root) {
if (watchersByRoot[root] != null) {
return Promise.resolve(watchersByRoot[root]);
}
function createWatcher(rootConfig) {
return detectingWatcherClass.then(function(Watcher) {
var watcher = new Watcher(root, {glob: ['**/*.js', '**/package.json']});
var watcher = new Watcher(rootConfig.dir, rootConfig.globs);
return new Promise(function(resolve, reject) {
var rejectTimeout = setTimeout(function() {
@ -77,8 +79,6 @@ function createWatcher(root) {
watcher.once('ready', function() {
clearTimeout(rejectTimeout);
watchersByRoot[root] = watcher;
watcher._root = root;
resolve(watcher);
});
});

View File

@ -56,6 +56,14 @@ var validateOpts = declareOpts({
type: 'array',
required: false,
},
assetExts: {
type: 'array',
default: ['png'],
},
fileWatcher: {
type: 'object',
required: true,
},
});
function Packager(options) {
@ -70,6 +78,7 @@ function Packager(options) {
nonPersistent: opts.nonPersistent,
moduleFormat: opts.moduleFormat,
assetRoots: opts.assetRoots,
fileWatcher: opts.fileWatcher,
});
this._transformer = new Transformer({
@ -83,10 +92,7 @@ function Packager(options) {
}
Packager.prototype.kill = function() {
return q.all([
this._transformer.kill(),
this._resolver.end(),
]);
return this._transformer.kill();
};
Packager.prototype.package = function(main, runModule, sourceMapUrl, isDev) {

View File

@ -55,18 +55,49 @@ var validateOpts = declareOpts({
type: 'array',
required: false,
},
assetExts: {
type: 'array',
default: ['png'],
},
});
function Server(options) {
var opts = validateOpts(options);
this._projectRoots = opts.projectRoots;
this._packages = Object.create(null);
this._packager = new Packager(opts);
this._changeWatchers = [];
var watchRootConfigs = opts.projectRoots.map(function(dir) {
return {
dir: dir,
globs: [
'**/*.js',
'**/package.json',
]
};
});
if (opts.assetRoots != null) {
watchRootConfigs = watchRootConfigs.concat(
opts.assetRoots.map(function(dir) {
return {
dir: dir,
globs: opts.assetExts.map(function(ext) {
return '**/*.' + ext;
}),
};
})
);
}
this._fileWatcher = options.nonPersistent
? FileWatcher.createDummyWatcher()
: new FileWatcher(options.projectRoots);
: new FileWatcher(watchRootConfigs);
var packagerOpts = Object.create(opts);
packagerOpts.fileWatcher = this._fileWatcher;
this._packager = new Packager(packagerOpts);
var onFileChange = this._onFileChange.bind(this);
this._fileWatcher.on('all', onFileChange);