[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() { pit('runs changes through ignore filter', function() {
var root = '/root'; var root = '/root';
var filesystem = fs.__setMockFilesystem({ var filesystem = fs.__setMockFilesystem({

View File

@ -482,7 +482,12 @@ DependecyGraph.prototype._lookupPackage = function(modulePath) {
/** /**
* Process a filewatcher change event. * 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); var absPath = path.join(root, filePath);
if (this._ignoreFilePath(absPath)) { if (this._ignoreFilePath(absPath)) {
return; return;
@ -490,6 +495,11 @@ DependecyGraph.prototype._processFileChange = function(eventType, filePath, root
this._debugUpdateEvents.push({event: eventType, path: filePath}); 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'; var isPackage = path.basename(filePath) === 'package.json';
if (eventType === 'delete') { if (eventType === 'delete') {
if (isPackage) { 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) { DependecyGraph.prototype._getAbsolutePath = function(filePath) {
if (isAbsolutePath(filePath)) { if (isAbsolutePath(filePath)) {
@ -547,12 +558,43 @@ DependecyGraph.prototype._buildAssetMap = function() {
return q(); return q();
} }
var self = this; this._assetMap = Object.create(null);
return buildAssetMap(this._assetRoots, this._assetExts) return buildAssetMap(
.then(function(map) { this._assetRoots,
self._assetMap = map; this._processAsset.bind(this)
return map; );
};
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 * 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. * 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 queue = roots.slice(0);
var map = Object.create(null);
function search() { function search() {
var root = queue.shift(); var root = queue.shift();
if (root == null) { if (root == null) {
return q(map); return q();
} }
return readAndStatDir(root).spread(function(files, stats) { return readAndStatDir(root).spread(function(files, stats) {
@ -643,21 +684,7 @@ function buildAssetMap(roots, exts) {
if (stats[i].isDirectory()) { if (stats[i].isDirectory()) {
queue.push(file); queue.push(file);
} else { } else {
var ext = path.extname(file).replace(/^\./, ''); processAsset(file);
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: [],
});
}
} }
}); });
@ -668,6 +695,15 @@ function buildAssetMap(roots, exts) {
return search(); return search();
} }
function assetName(file, ext) {
return path.basename(file, '.' + ext).replace(/@[\d\.]+x/, '');
}
function extname(name) {
return path.extname(name).replace(/^\./, '');
}
function NotFoundError() { function NotFoundError() {
Error.call(this); Error.call(this);
Error.captureStackTrace(this, this.constructor); Error.captureStackTrace(this, this.constructor);

View File

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

View File

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

View File

@ -16,7 +16,7 @@ var exec = require('child_process').exec;
var Promise = q.Promise; var Promise = q.Promise;
var detectingWatcherClass = new Promise(function(resolve, reject) { var detectingWatcherClass = new Promise(function(resolve) {
exec('which watchman', function(err, out) { exec('which watchman', function(err, out) {
if (err || out.length === 0) { if (err || out.length === 0) {
resolve(sane.NodeWatcher); resolve(sane.NodeWatcher);
@ -30,14 +30,23 @@ module.exports = FileWatcher;
var MAX_WAIT_TIME = 3000; var MAX_WAIT_TIME = 3000;
function FileWatcher(projectRoots) { // Singleton
var self = this; 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( this._loading = q.all(
projectRoots.map(createWatcher) rootConfigs.map(createWatcher)
).then(function(watchers) { ).then(function(watchers) {
watchers.forEach(function(watcher) { watchers.forEach(function(watcher) {
watcher.on('all', function(type, filepath, root) { watcher.on('all', function(type, filepath, root) {
self.emit('all', type, filepath, root); fileWatcher.emit('all', type, filepath, root);
}); });
}); });
return watchers; return watchers;
@ -50,21 +59,14 @@ util.inherits(FileWatcher, EventEmitter);
FileWatcher.prototype.end = function() { FileWatcher.prototype.end = function() {
return this._loading.then(function(watchers) { return this._loading.then(function(watchers) {
watchers.forEach(function(watcher) { watchers.forEach(function(watcher) {
delete watchersByRoot[watcher._root];
return q.ninvoke(watcher, 'close'); return q.ninvoke(watcher, 'close');
}); });
}); });
}; };
var watchersByRoot = Object.create(null); function createWatcher(rootConfig) {
function createWatcher(root) {
if (watchersByRoot[root] != null) {
return Promise.resolve(watchersByRoot[root]);
}
return detectingWatcherClass.then(function(Watcher) { 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) { return new Promise(function(resolve, reject) {
var rejectTimeout = setTimeout(function() { var rejectTimeout = setTimeout(function() {
@ -77,8 +79,6 @@ function createWatcher(root) {
watcher.once('ready', function() { watcher.once('ready', function() {
clearTimeout(rejectTimeout); clearTimeout(rejectTimeout);
watchersByRoot[root] = watcher;
watcher._root = root;
resolve(watcher); resolve(watcher);
}); });
}); });

View File

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

View File

@ -55,18 +55,49 @@ var validateOpts = declareOpts({
type: 'array', type: 'array',
required: false, required: false,
}, },
assetExts: {
type: 'array',
default: ['png'],
},
}); });
function Server(options) { function Server(options) {
var opts = validateOpts(options); var opts = validateOpts(options);
this._projectRoots = opts.projectRoots; this._projectRoots = opts.projectRoots;
this._packages = Object.create(null); this._packages = Object.create(null);
this._packager = new Packager(opts);
this._changeWatchers = []; 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 this._fileWatcher = options.nonPersistent
? FileWatcher.createDummyWatcher() ? 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); var onFileChange = this._onFileChange.bind(this);
this._fileWatcher.on('all', onFileChange); this._fileWatcher.on('all', onFileChange);