diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js index 962a06946..69ed11e65 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -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({ diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index f517236d9..6f09c5e88 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -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); diff --git a/packager/react-packager/src/DependencyResolver/haste/index.js b/packager/react-packager/src/DependencyResolver/haste/index.js index 2edb3b52c..0e46d5e8a 100644 --- a/packager/react-packager/src/DependencyResolver/haste/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/index.js @@ -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(); }; diff --git a/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js b/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js index 213033c5b..e24618dcf 100644 --- a/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js +++ b/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js @@ -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) { diff --git a/packager/react-packager/src/FileWatcher/index.js b/packager/react-packager/src/FileWatcher/index.js index c1633683c..86ec962bd 100644 --- a/packager/react-packager/src/FileWatcher/index.js +++ b/packager/react-packager/src/FileWatcher/index.js @@ -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); }); }); diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js index ac7bd2e2f..843efe75a 100644 --- a/packager/react-packager/src/Packager/index.js +++ b/packager/react-packager/src/Packager/index.js @@ -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) { diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 40281c69e..23af55dbc 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -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);