2015-02-09 updates

- [react-server] Support multiple roots | Amjad Masad
- [react-packager] Add a helpful error message when watcher fails to start | Amjad Masad
- [madman] Fix review screen button and animation | Eric Vicenti
This commit is contained in:
Christopher Chedeau 2015-02-10 16:27:57 -08:00
parent d29d07ee06
commit ff2f1ce945
15 changed files with 242 additions and 147 deletions

View File

@ -88,7 +88,7 @@ class Subscribable {
this._internalEmitter = new EventEmitter(); this._internalEmitter = new EventEmitter();
this._eventMapping = eventMapping || (data => data); this._eventMapping = eventMapping || (data => data);
eventEmitter.addListener( this._upstreamSubscription = eventEmitter.addListener(
eventName, eventName,
this._handleEmit, this._handleEmit,
this this
@ -105,6 +105,13 @@ class Subscribable {
return this._lastData; return this._lastData;
} }
/**
* Unsubscribe from the upstream EventEmitter
*/
cleanup() {
this._upstreamSubscription && this._upstreamSubscription.remove();
}
/** /**
* Add a new listener to the subscribable. This should almost never be used * Add a new listener to the subscribable. This should almost never be used
* directly, and instead through Subscribable.Mixin.subscribeTo * directly, and instead through Subscribable.Mixin.subscribeTo
@ -229,6 +236,55 @@ Subscribable.Mixin = {
); );
}, },
/**
* Gets a Subscribable store, scoped to the component, that can be passed to
* children. The component will automatically clean up the subscribable's
* subscription to the eventEmitter when unmounting.
*
* `provideSubscribable` will always return the same Subscribable for any
* particular emitter/eventName combo, so it can be called directly from
* render, and it will never create duplicate Subscribables.
*
* @param {EventEmitter} eventEmitter Emitter to trigger subscription events.
* @param {string} eventName Name of emitted event that triggers subscription
* events.
* @param {function} eventMapping (optional) Function to convert the output
* of the eventEmitter to the subscription output.
* @param {function} getInitData (optional) Async function to grab the initial
* data to publish. Signature `function(successCallback, errorCallback)`.
* The resolved data will be transformed with the eventMapping before it
* gets emitted.
*/
provideSubscribable: function(eventEmitter, eventName, eventMapping, getInitData) {
this._localSubscribables = this._localSubscribables || {};
this._localSubscribables[eventEmitter] =
this._localSubscribables[eventEmitter] || {};
if (!this._localSubscribables[eventEmitter][eventName]) {
this._localSubscribables[eventEmitter][eventName] =
new Subscribable(eventEmitter, eventName, eventMapping, getInitData);
}
return this._localSubscribables[eventEmitter][eventName];
},
/**
* Removes any local Subscribables created with `provideSubscribable`, so the
* component can unmount without leaving any dangling listeners on
* eventEmitters
*/
_cleanupLocalSubscribables: function() {
if (!this._localSubscribables) {
return;
}
var emitterSubscribables;
Object.keys(this._localSubscribables).forEach((eventEmitter) => {
emitterSubscribables = this._localSubscribables[eventEmitter];
Object.keys(emitterSubscribables).forEach((eventName) => {
emitterSubscribables[eventName].cleanup();
});
});
this._localSubscribables = null;
},
componentWillMount: function() { componentWillMount: function() {
this._endSubscribableLifespanCallbacks = []; this._endSubscribableLifespanCallbacks = [];
@ -241,6 +297,8 @@ Subscribable.Mixin = {
// remaining subscriptions // remaining subscriptions
this._endSubscribableLifespan && this._endSubscribableLifespan(); this._endSubscribableLifespan && this._endSubscribableLifespan();
this._cleanupLocalSubscribables();
// DEPRECATED addListenerOn* usage uses _subscribableSubscriptions array // DEPRECATED addListenerOn* usage uses _subscribableSubscriptions array
// instead of lifespan // instead of lifespan
this._subscribableSubscriptions.forEach( this._subscribableSubscriptions.forEach(

View File

@ -34,9 +34,9 @@ products with no compromises in quality or capability.
Get up and running with our Movies sample app: Get up and running with our Movies sample app:
1. Once you have the repo cloned and met all the requirements above, start the 1. Once you have the repo cloned and met all the requirements above, start the
packager that will transform your JS code on-the-fly: packager that will transform your JS code on-the-fly:
`npm install` `npm install`
`npm start` `npm start`
2. Open the `Examples/Movies/Movies.xcodeproj` project in Xcode. 2. Open the `Examples/Movies/Movies.xcodeproj` project in Xcode.
3. Make sure the target is set to `Movies` and that you have an iOS simulator 3. Make sure the target is set to `Movies` and that you have an iOS simulator
selected to run the app. selected to run the app.
@ -93,27 +93,27 @@ the responder system.
# FAQ # FAQ
Q. How does debugging work? Can I set breakpoints in my JS? Q. How does debugging work? Can I set breakpoints in my JS?
A. We are going to add the ability to use the Chrome developer tools soon. We A. We are going to add the ability to use the Chrome developer tools soon. We
are very passionate about building the best possible developer experience. are very passionate about building the best possible developer experience.
Q. When is this coming to Android/Windows/OS X/etc? Q. When is this coming to Android/Windows/OS X/etc?
A. We're working on Android, and we are excited to release it as soon as we can. A. We're working on Android, and we are excited to release it as soon as we can.
We are looking forward to the community helping us target other platforms as We are looking forward to the community helping us target other platforms as
well :) well :)
Q. How do I create my own app? Q. How do I create my own app?
A. Copy the entire `Examples/TicTacToe` folder, rename stuff in Xcode, and A. Copy the entire `Examples/TicTacToe` folder, rename stuff in Xcode, and
replace the `TicTacToeApp.js` with your own. Then, in `AppDelegate.m`, update replace the `TicTacToeApp.js` with your own. Then, in `AppDelegate.m`, update
`moduleName` to match your call to `moduleName` to match your call to
`Bundler.registerComponent(<moduleName>, <componentName>)` at the bottom of your `Bundler.registerComponent(<moduleName>, <componentName>)` at the bottom of your
JS file, and update `jsCodeLocation` to match your JS file name and location. JS file, and update `jsCodeLocation` to match your JS file name and location.
Q. Can I submit my own React Native app to the App Store? Q. Can I submit my own React Native app to the App Store?
A. Not yet, but you will be able to soon. If you build something you want to A. Not yet, but you will be able to soon. If you build something you want to
submit to the App Store, come talk to us ASAP. submit to the App Store, come talk to us ASAP.
Q. How do I deploy to my device? Q. How do I deploy to my device?
A. You can change `localhost` in `AppDelegate.m` to your laptop's IP address and A. You can change `localhost` in `AppDelegate.m` to your laptop's IP address and
grab the bundle over the same Wi-Fi network. You can also download the bundle grab the bundle over the same Wi-Fi network. You can also download the bundle
that the React packager generates, save it to the file `main.jsbundle`, and add it that the React packager generates, save it to the file `main.jsbundle`, and add it
@ -121,20 +121,20 @@ as a static resource in your Xcode project. Then set the `jsCodeLocation` in
`AppDelegate.m` to point to that file and deploy to your device like you would `AppDelegate.m` to point to that file and deploy to your device like you would
any other app. any other app.
Q. What's up with this private repo? Why aren't you just open sourcing it now? Q. What's up with this private repo? Why aren't you just open sourcing it now?
A. We want input from the React community before we open the floodgates so we A. We want input from the React community before we open the floodgates so we
can incorporate your feedback, and we also have a bunch more features we want to can incorporate your feedback, and we also have a bunch more features we want to
add to make a more complete offering before we open source. add to make a more complete offering before we open source.
Q. Do you have to ship a JS runtime with your apps? Q. Do you have to ship a JS runtime with your apps?
A. No, we just use the JavaScriptCore public API that is part of iOS 7 and A. No, we just use the JavaScriptCore public API that is part of iOS 7 and
later. later.
Q. How do I add more native capabilities? Q. How do I add more native capabilities?
A. React Native is designed to be extensible - come talk to us, we would love to A. React Native is designed to be extensible - come talk to us, we would love to
work with you. work with you.
Q. Can I reuse existing iOS code? Q. Can I reuse existing iOS code?
A. Yes, React Native is designed to be extensible and allow integration of all A. Yes, React Native is designed to be extensible and allow integration of all
sorts of native components, such as `UINavigationController` (available as sorts of native components, such as `UINavigationController` (available as
`<NavigatorIOS>`), `MKMapView` (not available yet), or your own custom `<NavigatorIOS>`), `MKMapView` (not available yet), or your own custom

View File

@ -28,8 +28,8 @@ var options = parseCommandLine([{
default: 8081, default: 8081,
}]); }]);
if (!options.projectRoot) { if (!options.projectRoots) {
options.projectRoot = path.resolve(__dirname, '..'); options.projectRoots = [path.resolve(__dirname, '..')];
} }
console.log('\n' + console.log('\n' +

View File

View File

@ -21,12 +21,8 @@ describe('DependencyGraph', function() {
DependencyGraph = require('../index'); DependencyGraph = require('../index');
fileWatcher = { fileWatcher = {
getWatcher: function() { on: function() {
return q({ return this;
on: function() {
return this;
}
});
} }
}; };
}); });
@ -50,7 +46,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -79,7 +75,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -109,7 +105,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -179,7 +175,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/somedir/somefile.js')) expect(dgraph.getOrderedDependencies('/root/somedir/somefile.js'))
.toEqual([ .toEqual([
@ -220,7 +216,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -249,7 +245,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -284,7 +280,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -324,7 +320,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -364,7 +360,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -395,16 +391,12 @@ describe('DependencyGraph', function() {
beforeEach(function() { beforeEach(function() {
fileWatcher = { fileWatcher = {
getWatcher: function() { on: function(eventType, callback) {
return q({ if (eventType !== 'all') {
on: function(eventType, callback) { throw new Error('Can only handle "all" event in watcher.');
if (eventType !== 'all') { }
throw new Error('Can only handle "all" event in watcher.'); triggerFileChange = callback;
} return this;
triggerFileChange = callback;
return this;
}
});
} }
}; };
}); });
@ -436,11 +428,11 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
filesystem.root['index.js'] = filesystem.root['index.js'] =
filesystem.root['index.js'].replace('require("foo")', ''); filesystem.root['index.js'].replace('require("foo")', '');
triggerFileChange('change', 'index.js'); triggerFileChange('change', 'index.js', root);
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -484,11 +476,11 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
filesystem.root['index.js'] = filesystem.root['index.js'] =
filesystem.root['index.js'].replace('require("foo")', ''); filesystem.root['index.js'].replace('require("foo")', '');
triggerFileChange('change', 'index.js'); triggerFileChange('change', 'index.js', root);
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -532,10 +524,10 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
delete filesystem.root.foo; delete filesystem.root.foo;
triggerFileChange('delete', 'foo.js'); triggerFileChange('delete', 'foo.js', root);
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
.toEqual([ .toEqual([
@ -579,7 +571,7 @@ describe('DependencyGraph', function() {
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
filesystem.root['bar.js'] = [ filesystem.root['bar.js'] = [
'/**', '/**',
@ -587,10 +579,10 @@ describe('DependencyGraph', function() {
' */', ' */',
'require("foo")' 'require("foo")'
].join('\n'); ].join('\n');
triggerFileChange('add', 'bar.js'); triggerFileChange('add', 'bar.js', root);
filesystem.root.aPackage['main.js'] = 'require("bar")'; filesystem.root.aPackage['main.js'] = 'require("bar")';
triggerFileChange('change', 'aPackage/main.js'); triggerFileChange('change', 'aPackage/main.js', root);
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
@ -644,7 +636,7 @@ describe('DependencyGraph', function() {
}); });
var dgraph = new DependencyGraph({ var dgraph = new DependencyGraph({
root: root, roots: [root],
fileWatcher: fileWatcher, fileWatcher: fileWatcher,
ignoreFilePath: function(filePath) { ignoreFilePath: function(filePath) {
if (filePath === '/root/bar.js') { if (filePath === '/root/bar.js') {
@ -660,10 +652,10 @@ describe('DependencyGraph', function() {
' */', ' */',
'require("foo")' 'require("foo")'
].join('\n'); ].join('\n');
triggerFileChange('add', 'bar.js'); triggerFileChange('add', 'bar.js', root);
filesystem.root.aPackage['main.js'] = 'require("bar")'; filesystem.root.aPackage['main.js'] = 'require("bar")';
triggerFileChange('change', 'aPackage/main.js'); triggerFileChange('change', 'aPackage/main.js', root);
return dgraph.load().then(function() { return dgraph.load().then(function() {
expect(dgraph.getOrderedDependencies('/root/index.js')) expect(dgraph.getOrderedDependencies('/root/index.js'))
@ -711,7 +703,7 @@ describe('DependencyGraph', function() {
} }
} }
}); });
var dgraph = new DependencyGraph({root: root, fileWatcher: fileWatcher}); var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher});
return dgraph.load().then(function() { return dgraph.load().then(function() {
triggerFileChange('change', 'aPackage', '/root', { triggerFileChange('change', 'aPackage', '/root', {
isDirectory: function(){ return true; } isDirectory: function(){ return true; }

View File

@ -14,10 +14,10 @@ var lstat = q.nfbind(fs.lstat);
var realpath = q.nfbind(fs.realpath); var realpath = q.nfbind(fs.realpath);
function DependecyGraph(options) { function DependecyGraph(options) {
this._root = options.root; this._roots = options.roots;
this._ignoreFilePath = options.ignoreFilePath || function(){}; this._ignoreFilePath = options.ignoreFilePath || function(){};
this._loaded = false; this._loaded = false;
this._queue = [this._root]; this._queue = this._roots.slice();
this._graph = Object.create(null); this._graph = Object.create(null);
this._packageByRoot = Object.create(null); this._packageByRoot = Object.create(null);
this._packagesById = Object.create(null); this._packagesById = Object.create(null);
@ -36,16 +36,14 @@ DependecyGraph.prototype.load = function() {
* Given an entry file return an array of all the dependent module descriptors. * Given an entry file return an array of all the dependent module descriptors.
*/ */
DependecyGraph.prototype.getOrderedDependencies = function(entryPath) { DependecyGraph.prototype.getOrderedDependencies = function(entryPath) {
var absolutePath; var absolutePath = this._getAbsolutePath(entryPath);
if (!isAbsolutePath(entryPath)) { if (absolutePath == null) {
absolutePath = path.join(this._root, entryPath); throw new Error('Cannot find entry file in any of the roots: ' + entryPath);
} else {
absolutePath = entryPath;
} }
var module = this._graph[absolutePath]; var module = this._graph[absolutePath];
if (module == null) { if (module == null) {
throw new Error('Module with path "' + absolutePath + '" is not in graph'); throw new Error('Module with path "' + entryPath + '" is not in graph');
} }
var self = this; var self = this;
@ -172,15 +170,11 @@ DependecyGraph.prototype.resolveDependency = function(
*/ */
DependecyGraph.prototype._init = function() { DependecyGraph.prototype._init = function() {
var processChange = this._processFileChange.bind(this); var processChange = this._processFileChange.bind(this);
var loadingWatcher = this._fileWatcher.getWatcher(); var watcher = this._fileWatcher;
this._loading = this.load() this._loading = this.load().then(function() {
.then(function() { watcher.on('all', processChange);
return loadingWatcher; });
})
.then(function(watcher) {
watcher.on('all', processChange);
});
}; };
/** /**
@ -370,7 +364,6 @@ DependecyGraph.prototype._updateGraphWithModule = function(module) {
* Find the nearest package to a module. * Find the nearest package to a module.
*/ */
DependecyGraph.prototype._lookupPackage = function(modulePath) { DependecyGraph.prototype._lookupPackage = function(modulePath) {
var root = this._root;
var packageByRoot = this._packageByRoot; var packageByRoot = this._packageByRoot;
/** /**
@ -380,8 +373,7 @@ DependecyGraph.prototype._lookupPackage = function(modulePath) {
// ideally we stop once we're outside root and this can be a simple child // ideally we stop once we're outside root and this can be a simple child
// dir check. However, we have to support modules that was symlinked inside // dir check. However, we have to support modules that was symlinked inside
// our project root. // our project root.
if (!path.relative(root, currDir) === '' || currDir === '.' if (currDir === '/') {
|| currDir === '/') {
return null; return null;
} else { } else {
var packageJson = packageByRoot[currDir]; var packageJson = packageByRoot[currDir];
@ -400,7 +392,7 @@ 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(this._root, filePath); var absPath = path.join(root, filePath);
if (this._ignoreFilePath(absPath)) { if (this._ignoreFilePath(absPath)) {
return; return;
} }
@ -420,6 +412,24 @@ DependecyGraph.prototype._processFileChange = function(eventType, filePath, root
} }
}; };
/**
* 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)) {
return filePath;
}
for (var i = 0, root; root = this._roots[i]; i++) {
var absPath = path.join(root, filePath);
if (this._graph[absPath]) {
return absPath;
}
}
return null;
};
/** /**
* Extract all required modules from a `code` string. * Extract all required modules from a `code` string.
*/ */

View File

@ -23,13 +23,14 @@ var DEFINE_MODULE_REPLACE_RE = /_moduleName_|_code_|_deps_/g;
var REL_REQUIRE_STMT = /require\(['"]([\.\/0-9A-Z_$\-]*)['"]\)/gi; var REL_REQUIRE_STMT = /require\(['"]([\.\/0-9A-Z_$\-]*)['"]\)/gi;
function HasteDependencyResolver(config) { function HasteDependencyResolver(config) {
this._fileWatcher = new FileWatcher(config.projectRoots);
this._depGraph = new DependencyGraph({ this._depGraph = new DependencyGraph({
root: config.projectRoot, roots: config.projectRoots,
ignoreFilePath: function(filepath) { ignoreFilePath: function(filepath) {
return filepath.indexOf('__tests__') !== -1 || return filepath.indexOf('__tests__') !== -1 ||
(config.blacklistRE && config.blacklistRE.test(filepath)); (config.blacklistRE && config.blacklistRE.test(filepath));
}, },
fileWatcher: new FileWatcher(config.projectRoot) fileWatcher: this._fileWatcher
}); });
this._polyfillModuleNames = [ this._polyfillModuleNames = [
@ -118,4 +119,8 @@ HasteDependencyResolver.prototype.wrapModule = function(module, code) {
}); });
}; };
HasteDependencyResolver.prototype.end = function() {
return this._fileWatcher.end();
};
module.exports = HasteDependencyResolver; module.exports = HasteDependencyResolver;

View File

@ -16,15 +16,17 @@ describe('FileWatcher', function() {
}); });
}); });
pit('it should get the watcher instance when ready', function() { it('it should get the watcher instance when ready', function() {
var fileWatcher = new FileWatcher('rootDir'); var fileWatcher = new FileWatcher(['rootDir']);
return fileWatcher.getWatcher().then(function(watcher) { return fileWatcher._loading.then(function(watchers) {
expect(watcher instanceof Watcher).toBe(true); watchers.forEach(function(watcher) {
expect(watcher instanceof Watcher).toBe(true);
});
}); });
}); });
pit('it should end the watcher', function() { pit('it should end the watcher', function() {
var fileWatcher = new FileWatcher('rootDir'); var fileWatcher = new FileWatcher(['rootDir']);
Watcher.prototype.close.mockImplementation(function(callback) { Watcher.prototype.close.mockImplementation(function(callback) {
callback(); callback();
}); });

View File

@ -1,7 +1,9 @@
'use strict'; 'use strict';
var EventEmitter = require('events').EventEmitter;
var sane = require('sane'); var sane = require('sane');
var q = require('q'); var q = require('q');
var util = require('util');
var exec = require('child_process').exec; var exec = require('child_process').exec;
var Promise = q.Promise; var Promise = q.Promise;
@ -20,37 +22,57 @@ module.exports = FileWatcher;
var MAX_WAIT_TIME = 3000; var MAX_WAIT_TIME = 3000;
var memoizedInstances = Object.create(null); function FileWatcher(projectRoots) {
var self = this;
this._loading = q.all(
projectRoots.map(createWatcher)
).then(function(watchers) {
watchers.forEach(function(watcher) {
watcher.on('all', function(type, filepath, root) {
self.emit('all', type, filepath, root);
});
});
return watchers;
});
this._loading.done();
}
function FileWatcher(projectRoot) { util.inherits(FileWatcher, EventEmitter);
if (memoizedInstances[projectRoot]) {
return memoizedInstances[projectRoot]; FileWatcher.prototype.end = function() {
} else { return this._loading.then(function(watchers) {
memoizedInstances[projectRoot] = this; 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]);
} }
this._loadingWatcher = detectingWatcherClass.then(function(Watcher) { return detectingWatcherClass.then(function(Watcher) {
var watcher = new Watcher(projectRoot, {glob: '**/*.js'}); var watcher = new Watcher(root, {glob: '**/*.js'});
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var rejectTimeout = setTimeout(function() { var rejectTimeout = setTimeout(function() {
reject(new Error('Watcher took too long to load.')); reject(new Error([
'Watcher took too long to load',
'Try running `watchman` from your terminal',
'https://facebook.github.io/watchman/docs/troubleshooting.html',
].join('\n')));
}, MAX_WAIT_TIME); }, MAX_WAIT_TIME);
watcher.once('ready', function() { watcher.once('ready', function() {
clearTimeout(rejectTimeout); clearTimeout(rejectTimeout);
watchersByRoot[root] = watcher;
watcher._root = root;
resolve(watcher); resolve(watcher);
}); });
}); });
}); });
} }
FileWatcher.prototype.getWatcher = function() {
return this._loadingWatcher;
};
FileWatcher.prototype.end = function() {
return this._loadingWatcher.then(function(watcher) {
return q.ninvoke(watcher, 'close');
});
};

View File

@ -13,12 +13,7 @@ var Promise = q.Promise;
module.exports = Cache; module.exports = Cache;
function Cache(projectConfig) { function Cache(projectConfig) {
this._cacheFilePath = path.join( this._cacheFilePath = cacheFilePath(projectConfig);
tmpdir,
'React-Packager-JSTransformer-' + version + '-' +
projectConfig.projectRoot.split(path.sep).join('-') +
'-' + (projectConfig.cacheVersion || '0')
);
var data; var data;
if (!projectConfig.resetCache) { if (!projectConfig.resetCache) {
@ -118,3 +113,17 @@ function loadCacheSync(cacheFilepath) {
return ret; return ret;
} }
function cacheFilePath(projectConfig) {
var roots = projectConfig.projectRoots.join(',').split(path.sep).join('-');
var cacheVersion = projectConfig.cacheVersion || '0';
return path.join(
tmpdir,
[
'react-packager-cache',
version,
cacheVersion,
roots,
].join('-')
);
}

View File

@ -23,7 +23,7 @@ describe('JSTransformer Cache', function() {
describe('getting/settig', function() { describe('getting/settig', function() {
it('calls loader callback for uncached file', function() { it('calls loader callback for uncached file', function() {
var cache = new Cache({projectRoot: '/rootDir'}); var cache = new Cache({projectRoots: ['/rootDir']});
var loaderCb = jest.genMockFn().mockImpl(function() { var loaderCb = jest.genMockFn().mockImpl(function() {
return q(); return q();
}); });
@ -39,7 +39,7 @@ describe('JSTransformer Cache', function() {
} }
}); });
}); });
var cache = new Cache({projectRoot: '/rootDir'}); var cache = new Cache({projectRoots: ['/rootDir']});
var loaderCb = jest.genMockFn().mockImpl(function() { var loaderCb = jest.genMockFn().mockImpl(function() {
return q('lol'); return q('lol');
}); });
@ -56,7 +56,7 @@ describe('JSTransformer Cache', function() {
} }
}); });
}); });
var cache = new Cache({projectRoot: '/rootDir'}); var cache = new Cache({projectRoots: ['/rootDir']});
var loaderCb = jest.genMockFn().mockImpl(function() { var loaderCb = jest.genMockFn().mockImpl(function() {
return q('lol'); return q('lol');
}); });
@ -117,7 +117,7 @@ describe('JSTransformer Cache', function() {
}); });
pit('should load cache from disk', function() { pit('should load cache from disk', function() {
var cache = new Cache({projectRoot: '/rootDir'}); var cache = new Cache({projectRoots: ['/rootDir']});
var loaderCb = jest.genMockFn(); var loaderCb = jest.genMockFn();
return cache.get('/rootDir/someFile', loaderCb).then(function(value) { return cache.get('/rootDir/someFile', loaderCb).then(function(value) {
expect(loaderCb).not.toBeCalled(); expect(loaderCb).not.toBeCalled();
@ -143,7 +143,7 @@ describe('JSTransformer Cache', function() {
return 123; return 123;
}; };
var cache = new Cache({projectRoot: '/rootDir'}); var cache = new Cache({projectRoots: ['/rootDir']});
var loaderCb = jest.genMockFn().mockImpl(function() { var loaderCb = jest.genMockFn().mockImpl(function() {
return q('new value'); return q('new value');
}); });
@ -184,7 +184,7 @@ describe('JSTransformer Cache', function() {
}); });
}); });
var cache = new Cache({projectRoot: '/rootDir'}); var cache = new Cache({projectRoots: ['/rootDir']});
cache.get('/rootDir/bar', function() { cache.get('/rootDir/bar', function() {
return q('bar value'); return q('bar value');
}); });

View File

@ -35,7 +35,7 @@ describe('Packager', function() {
}; };
}); });
var packager = new Packager({}); var packager = new Packager({projectRoots: []});
var modules = [ var modules = [
{id: 'foo', path: '/root/foo.js', dependencies: []}, {id: 'foo', path: '/root/foo.js', dependencies: []},
{id: 'bar', path: '/root/bar.js', dependencies: []}, {id: 'bar', path: '/root/bar.js', dependencies: []},

View File

@ -3,6 +3,7 @@
var assert = require('assert'); var assert = require('assert');
var fs = require('fs'); var fs = require('fs');
var path = require('path'); var path = require('path');
var q = require('q');
var Promise = require('q').Promise; var Promise = require('q').Promise;
var Transformer = require('../JSTransformer'); var Transformer = require('../JSTransformer');
var DependencyResolver = require('../DependencyResolver'); var DependencyResolver = require('../DependencyResolver');
@ -45,10 +46,7 @@ var DEFAULT_CONFIG = {
}; };
function Packager(projectConfig) { function Packager(projectConfig) {
// Verify that the root exists. projectConfig.projectRoots.forEach(verifyRootExists);
var root = projectConfig.projectRoot;
assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory');
this._rootPath = root;
this._config = Object.create(DEFAULT_CONFIG); this._config = Object.create(DEFAULT_CONFIG);
for (var key in projectConfig) { for (var key in projectConfig) {
@ -61,7 +59,10 @@ function Packager(projectConfig) {
} }
Packager.prototype.kill = function() { Packager.prototype.kill = function() {
return this._transformer.kill(); return q.all([
this._transformer.kill(),
this._resolver.end(),
]);
}; };
Packager.prototype.package = function(main, runModule, sourceMapUrl) { Packager.prototype.package = function(main, runModule, sourceMapUrl) {
@ -116,4 +117,9 @@ Packager.prototype._transformModule = function(module) {
}); });
}; };
function verifyRootExists(root) {
// Verify that the root exists.
assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory');
}
module.exports = Packager; module.exports = Packager;

View File

@ -1,6 +1,4 @@
'use strict'; jest.setMock('worker-farm', function(){ return function(){}; })
jest.dontMock('worker-farm')
.dontMock('q') .dontMock('q')
.dontMock('os') .dontMock('os')
.dontMock('errno/custom') .dontMock('errno/custom')
@ -19,7 +17,7 @@ describe('processRequest', function(){
var FileWatcher; var FileWatcher;
var options = { var options = {
projectRoot: 'root', projectRoots: ['root'],
blacklistRE: null, blacklistRE: null,
cacheVersion: null, cacheVersion: null,
polyfillModuleNames: null polyfillModuleNames: null
@ -59,11 +57,8 @@ describe('processRequest', function(){
} }
}) })
}; };
FileWatcher.prototype.getWatcher = function() {
return q({ FileWatcher.prototype.on = watcherFunc;
on: watcherFunc
});
};
Packager.prototype.invalidateFile = invalidatorFunc; Packager.prototype.invalidateFile = invalidatorFunc;
@ -98,16 +93,12 @@ describe('processRequest', function(){
describe('file changes', function() { describe('file changes', function() {
var triggerFileChange; var triggerFileChange;
beforeEach(function() { beforeEach(function() {
FileWatcher.prototype.getWatcher = function() { FileWatcher.prototype.on = function(eventType, callback) {
return q({ if (eventType !== 'all') {
on: function(eventType, callback) { throw new Error('Can only handle "all" event in watcher.');
if (eventType !== 'all') { }
throw new Error('Can only handle "all" event in watcher.'); triggerFileChange = callback;
} return this;
triggerFileChange = callback;
return this;
}
});
}; };
}); });
@ -115,7 +106,7 @@ describe('processRequest', function(){
result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle'); result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle');
return result.then(function(response){ return result.then(function(response){
var onFileChange = watcherFunc.mock.calls[0][1]; var onFileChange = watcherFunc.mock.calls[0][1];
onFileChange('all','path/file.js'); onFileChange('all','path/file.js', options.projectRoots[0]);
expect(invalidatorFunc.mock.calls[0][0]).toEqual('root/path/file.js'); expect(invalidatorFunc.mock.calls[0][0]).toEqual('root/path/file.js');
}); });
}); });
@ -152,7 +143,7 @@ describe('processRequest', function(){
.then(function(response){ .then(function(response){
expect(response).toEqual("this is the first source"); expect(response).toEqual("this is the first source");
expect(packageFunc.mock.calls.length).toBe(1); expect(packageFunc.mock.calls.length).toBe(1);
triggerFileChange('all','path/file.js'); triggerFileChange('all','path/file.js', options.projectRoots[0]);
}) })
.then(function(){ .then(function(){
expect(packageFunc.mock.calls.length).toBe(2); expect(packageFunc.mock.calls.length).toBe(2);

View File

@ -8,10 +8,10 @@ var q = require('q');
module.exports = Server; module.exports = Server;
function Server(options) { function Server(options) {
this._projectRoot = options.projectRoot; this._projectRoots = options.projectRoots;
this._packages = Object.create(null); this._packages = Object.create(null);
this._packager = new Packager({ this._packager = new Packager({
projectRoot: options.projectRoot, projectRoots: options.projectRoots,
blacklistRE: options.blacklistRE, blacklistRE: options.blacklistRE,
polyfillModuleNames: options.polyfillModuleNames || [], polyfillModuleNames: options.polyfillModuleNames || [],
runtimeCode: options.runtimeCode, runtimeCode: options.runtimeCode,
@ -20,18 +20,18 @@ function Server(options) {
dev: options.dev, dev: options.dev,
}); });
this._fileWatcher = new FileWatcher(options.projectRoot); this._fileWatcher = new FileWatcher(options.projectRoots);
var onFileChange = this._onFileChange.bind(this); var onFileChange = this._onFileChange.bind(this);
this._fileWatcher.getWatcher().done(function(watcher) { this._fileWatcher.on('all', onFileChange);
watcher.on('all', onFileChange);
});
} }
Server.prototype._onFileChange = function(type, filepath) { Server.prototype._onFileChange = function(type, filepath, root) {
var absPath = path.join(this._projectRoot, filepath); var absPath = path.join(root, filepath);
this._packager.invalidateFile(absPath); this._packager.invalidateFile(absPath);
this._rebuildPackages(absPath); // Make sure the file watcher event runs through the system before
// we rebuild the packages.
setImmediate(this._rebuildPackages.bind(this, absPath))
}; };
Server.prototype._rebuildPackages = function(filepath) { Server.prototype._rebuildPackages = function(filepath) {