diff --git a/.flowconfig b/.flowconfig
index a5a80524a..65df8bc75 100644
--- a/.flowconfig
+++ b/.flowconfig
@@ -30,13 +30,15 @@
[libs]
Libraries/react-native/react-native-interface.js
-Examples/UIExplorer/ImageMocks.js
[options]
module.system=haste
munge_underscores=true
+module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
+module.name_mapper='^[./a-zA-Z0-9$_-]+\.png$' -> 'RelativeImageStub'
+
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FixMe
diff --git a/Examples/2048/2048/AppDelegate.m b/Examples/2048/2048/AppDelegate.m
index 393ce83cb..2413b3552 100644
--- a/Examples/2048/2048/AppDelegate.m
+++ b/Examples/2048/2048/AppDelegate.m
@@ -36,14 +36,14 @@
* on the same Wi-Fi network.
*/
- jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle"];
+ jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/2048/Game2048.bundle?platform=ios"];
/**
* OPTION 2
* Load from pre-bundled file on disk. To re-generate the static bundle, `cd`
* to your Xcode project folder in the terminal, and run
*
- * $ curl 'http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle' -o main.jsbundle
+ * $ curl 'http://localhost:8081/Examples/2048/Game2048.bundle?platform=ios' -o main.jsbundle
*
* then add the `main.jsbundle` file to your project and uncomment this line:
*/
diff --git a/Examples/Movies/MovieCell.js b/Examples/Movies/MovieCell.js
index 341460e58..62062cb2a 100644
--- a/Examples/Movies/MovieCell.js
+++ b/Examples/Movies/MovieCell.js
@@ -19,9 +19,11 @@ var React = require('react-native');
var {
Image,
PixelRatio,
+ Platform,
StyleSheet,
Text,
TouchableHighlight,
+ TouchableNativeFeedback,
View
} = React;
@@ -32,9 +34,13 @@ var getTextFromScore = require('./getTextFromScore');
var MovieCell = React.createClass({
render: function() {
var criticsScore = this.props.movie.ratings.critics_score;
+ var TouchableElement = TouchableHighlight;
+ if (Platform.OS === 'android') {
+ TouchableElement = TouchableNativeFeedback;
+ }
return (
-
@@ -59,7 +65,7 @@ var MovieCell = React.createClass({
-
+
);
}
diff --git a/Examples/Movies/MoviesApp.js b/Examples/Movies/MoviesApp.ios.js
similarity index 100%
rename from Examples/Movies/MoviesApp.js
rename to Examples/Movies/MoviesApp.ios.js
diff --git a/Examples/Movies/SearchBar.ios.js b/Examples/Movies/SearchBar.ios.js
new file mode 100644
index 000000000..f4a2354ef
--- /dev/null
+++ b/Examples/Movies/SearchBar.ios.js
@@ -0,0 +1,66 @@
+/**
+ * The examples provided by Facebook are for non-commercial testing and
+ * evaluation purposes only.
+ *
+ * Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @providesModule SearchBar
+ * @flow
+ */
+'use strict';
+
+var React = require('react-native');
+var {
+ ActivityIndicatorIOS,
+ TextInput,
+ StyleSheet,
+ View,
+} = React;
+
+var SearchBar = React.createClass({
+ render: function() {
+ return (
+
+
+
+
+ );
+ }
+});
+
+var styles = StyleSheet.create({
+ searchBar: {
+ marginTop: 64,
+ padding: 3,
+ paddingLeft: 8,
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ searchBarInput: {
+ fontSize: 15,
+ flex: 1,
+ height: 30,
+ },
+ spinner: {
+ width: 30,
+ },
+});
+
+module.exports = SearchBar;
diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js
index 477ed6a50..4fd36790b 100644
--- a/Examples/Movies/SearchScreen.js
+++ b/Examples/Movies/SearchScreen.js
@@ -19,6 +19,8 @@ var React = require('react-native');
var {
ActivityIndicatorIOS,
ListView,
+ Platform,
+ ProgressBarAndroid,
StyleSheet,
Text,
TextInput,
@@ -27,9 +29,11 @@ var {
var TimerMixin = require('react-timer-mixin');
var invariant = require('invariant');
+var dismissKeyboard = require('dismissKeyboard');
var MovieCell = require('./MovieCell');
var MovieScreen = require('./MovieScreen');
+var SearchBar = require('SearchBar');
/**
* This is for demo purposes only, and rate limited.
@@ -219,11 +223,20 @@ var SearchScreen = React.createClass({
},
selectMovie: function(movie: Object) {
- this.props.navigator.push({
- title: movie.title,
- component: MovieScreen,
- passProps: {movie},
- });
+ if (Platform.OS === 'ios') {
+ this.props.navigator.push({
+ title: movie.title,
+ component: MovieScreen,
+ passProps: {movie},
+ });
+ } else {
+ dismissKeyboard();
+ this.props.navigator.push({
+ title: movie.title,
+ name: 'movie',
+ movie: movie,
+ });
+ }
},
onSearchChange: function(event: Object) {
@@ -237,7 +250,15 @@ var SearchScreen = React.createClass({
if (!this.hasMore() || !this.state.isLoadingTail) {
return ;
}
- return ;
+ if (Platform.OS === 'ios') {
+ return ;
+ } else {
+ return (
+
+
+
+ );
+ }
},
renderSeparator: function(
@@ -295,7 +316,8 @@ var SearchScreen = React.createClass({
this.refs.listview.getScrollResponder().scrollTo(0, 0)}
+ onFocus={() =>
+ this.refs.listview && this.refs.listview.getScrollResponder().scrollTo(0, 0)}
/>
{content}
@@ -323,27 +345,6 @@ var NoMovies = React.createClass({
}
});
-var SearchBar = React.createClass({
- render: function() {
- return (
-
-
-
-
- );
- }
-});
-
var styles = StyleSheet.create({
container: {
flex: 1,
@@ -356,25 +357,10 @@ var styles = StyleSheet.create({
marginTop: 80,
color: '#888888',
},
- searchBar: {
- marginTop: 64,
- padding: 3,
- paddingLeft: 8,
- flexDirection: 'row',
- alignItems: 'center',
- },
- searchBarInput: {
- fontSize: 15,
- flex: 1,
- height: 30,
- },
separator: {
height: 1,
backgroundColor: '#eeeeee',
},
- spinner: {
- width: 30,
- },
scrollSpinner: {
marginVertical: 20,
},
diff --git a/Examples/SampleApp/_flowconfig b/Examples/SampleApp/_flowconfig
index 438e495d4..9ca5deb8f 100644
--- a/Examples/SampleApp/_flowconfig
+++ b/Examples/SampleApp/_flowconfig
@@ -34,6 +34,9 @@ module.system=haste
munge_underscores=true
+module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
+module.name_mapper='^[./a-zA-Z0-9$_-]+\.png$' -> 'RelativeImageStub'
+
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FixMe
diff --git a/Examples/TicTacToe/TicTacToe/AppDelegate.m b/Examples/TicTacToe/TicTacToe/AppDelegate.m
index 7fa214fab..aa746ba69 100644
--- a/Examples/TicTacToe/TicTacToe/AppDelegate.m
+++ b/Examples/TicTacToe/TicTacToe/AppDelegate.m
@@ -36,14 +36,14 @@
* on the same Wi-Fi network.
*/
- jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle"];
+ jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/TicTacToe/TicTacToeApp.bundle?platform=ios"];
/**
* OPTION 2
* Load from pre-bundled file on disk. To re-generate the static bundle, `cd`
* to your Xcode project folder in the terminal, and run
*
- * $ curl 'http://localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle' -o main.jsbundle
+ * $ curl 'http://localhost:8081/Examples/TicTacToe/TicTacToeApp.bundle?platform=ios' -o main.jsbundle
*
* then add the `main.jsbundle` file to your project and uncomment this line:
*/
diff --git a/Examples/UIExplorer/ImageMocks.js b/Examples/UIExplorer/ImageMocks.js
deleted file mode 100644
index 670346373..000000000
--- a/Examples/UIExplorer/ImageMocks.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * The examples provided by Facebook are for non-commercial testing and
- * evaluation purposes only.
- *
- * Facebook reserves all rights not expressly granted.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
- * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
- * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
- * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- *
- * @flow
- */
-'use strict';
-
-/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing
- * into a proptype shape */
-declare module 'image!story-background' {
- declare var uri: string;
- declare var isStatic: boolean;
-}
-
-/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing
- * into a proptype shape */
-declare module 'image!uie_comment_highlighted' {
- declare var uri: string;
- declare var isStatic: boolean;
-}
-
-/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing
- * into a proptype shape */
-declare module 'image!uie_comment_normal' {
- declare var uri: string;
- declare var isStatic: boolean;
-}
-
-/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing
- * into a proptype shape */
-declare module 'image!uie_thumb_normal' {
- declare var uri: string;
- declare var isStatic: boolean;
-}
-
-/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing
- * into a proptype shape */
-declare module 'image!uie_thumb_selected' {
- declare var uri: string;
- declare var isStatic: boolean;
-}
-
-declare module 'image!NavBarButtonPlus' {
- declare var uri: string;
- declare var isStatic: boolean;
-}
diff --git a/Examples/UIExplorer/TimerExample.js b/Examples/UIExplorer/TimerExample.js
index 0bfd5d05a..b5923b350 100644
--- a/Examples/UIExplorer/TimerExample.js
+++ b/Examples/UIExplorer/TimerExample.js
@@ -181,8 +181,12 @@ exports.examples = [
render: function() {
if (this.state.showTimer) {
- var timer =
- ;
+ var timer = [
+ ,
+
+ ];
var toggleText = 'Unmount timer';
} else {
var timer = null;
@@ -191,9 +195,6 @@ exports.examples = [
return (
{timer}
-
diff --git a/Examples/UIExplorer/UIExplorer/AppDelegate.m b/Examples/UIExplorer/UIExplorer/AppDelegate.m
index f394c9903..24ada2b56 100644
--- a/Examples/UIExplorer/UIExplorer/AppDelegate.m
+++ b/Examples/UIExplorer/UIExplorer/AppDelegate.m
@@ -59,14 +59,14 @@
* on the same Wi-Fi network.
*/
- sourceURL = [NSURL URLWithString:@"http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.includeRequire.runModule.bundle?dev=true"];
+ sourceURL = [NSURL URLWithString:@"http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.bundle?dev=true"];
/**
* OPTION 2
* Load from pre-bundled file on disk. To re-generate the static bundle, `cd`
* to your Xcode project folder and run
*
- * $ curl 'http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.includeRequire.runModule.bundle' -o main.jsbundle
+ * $ curl 'http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.bundle' -o main.jsbundle
*
* then add the `main.jsbundle` file to your project and uncomment this line:
*/
diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m
index 267ed1409..ccf5e7f74 100644
--- a/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m
+++ b/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m
@@ -57,7 +57,7 @@
expectErrorRegex:@"because shouldThrow"];
}
-- (void)testTimers
+- (void)DISABLED_testTimers // #8192477
{
[_runner runTest:_cmd module:@"TimersTest"];
}
diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m
index dd50708e3..cd44b7229 100644
--- a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m
+++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m
@@ -141,7 +141,7 @@ RCT_EXPORT_METHOD(test:(__unused NSString *)a
XCTAssertNil(weakMethod, @"RCTModuleMethod should have been deallocated");
}
-- (void)testJavaScriptExecutorIsDeallocated
+- (void)DISABLED_testJavaScriptExecutorIsDeallocated // flaky: #8195866
{
__weak id weakExecutor;
@autoreleasepool {
diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js
index 478c2a995..41f6b4d1a 100644
--- a/Examples/UIExplorer/WebViewExample.js
+++ b/Examples/UIExplorer/WebViewExample.js
@@ -75,7 +75,7 @@ var WebViewExample = React.createClass({
@@ -114,48 +115,18 @@ static char *convert_to_json(const JSC::Profile *profile) {
return json_copy;
}
-static char *JSEndProfilingAndRender(JSContextRef ctx, JSStringRef title)
+static const char *JSEndProfilingAndRender(JSContextRef ctx, const char *title)
{
JSC::ExecState *exec = toJS(ctx);
JSC::LegacyProfiler *profiler = JSC::LegacyProfiler::profiler();
- RefPtr rawProfile = profiler->stopProfiling(exec, title->string());
+ RefPtr rawProfile = profiler->stopProfiling(exec, WTF::String(title));
return convert_to_json(rawProfile.get());
}
-JSValueRef nativeProfilerStart(
- JSContextRef ctx,
- JSObjectRef function,
- JSObjectRef thisObject,
- size_t argumentCount,
- const JSValueRef arguments[],
- JSValueRef *exception) {
- if (argumentCount < 1) {
- // Could raise an exception here.
- return JSValueMakeUndefined(ctx);
- }
-
- JSStringRef title = JSValueToStringCopy(ctx, arguments[0], NULL);
- JSStartProfiling(ctx, title);
- JSStringRelease(title);
- return JSValueMakeUndefined(ctx);
+void nativeProfilerStart(JSContextRef ctx, const char *title) {
+ JSStartProfiling(ctx, JSStringCreateWithUTF8CString(title));
}
-JSValueRef nativeProfilerEnd(
- JSContextRef ctx,
- JSObjectRef function,
- JSObjectRef thisObject,
- size_t argumentCount,
- const JSValueRef arguments[],
- JSValueRef *exception) {
- if (argumentCount < 1) {
- // Could raise an exception here.
- return JSValueMakeUndefined(ctx);
- }
-
- JSStringRef title = JSValueToStringCopy(ctx, arguments[0], NULL);
- char *rendered = JSEndProfilingAndRender(ctx, title);
- JSStringRelease(title);
- JSStringRef profile = JSStringCreateWithUTF8CString(rendered);
- free(rendered);
- return JSValueMakeString(ctx, profile);
+const char *nativeProfilerEnd( JSContextRef ctx, const char *title) {
+ return JSEndProfilingAndRender(ctx, title);
}
diff --git a/JSCLegacyProfiler/Makefile b/JSCLegacyProfiler/Makefile
index 0cf8f6afe..a1f704d98 100644
--- a/JSCLegacyProfiler/Makefile
+++ b/JSCLegacyProfiler/Makefile
@@ -110,5 +110,3 @@ armv7:
${HEADER_PATHS} \
-undefined dynamic_lookup \
./JSCLegacyProfiler.mm ./tmp/yajl.a
-
-.PHONY: ios8
diff --git a/Libraries/Animated/Easing.js b/Libraries/Animated/Easing.js
index ae90136d1..58969278b 100644
--- a/Libraries/Animated/Easing.js
+++ b/Libraries/Animated/Easing.js
@@ -59,21 +59,22 @@ class Easing {
return Math.pow(2, 10 * (t - 1));
}
- static elastic(a: number, p: number): (t: number) => number {
- var tau = Math.PI * 2;
- // flow isn't smart enough to figure out that s is always assigned to a
- // number before being used in the returned function
- var s: any;
- if (arguments.length < 2) {
- p = 0.45;
+ /**
+ * A simple elastic interaction, similar to a spring. Default bounciness
+ * is 1, which overshoots a little bit once. 0 bounciness doesn't overshoot
+ * at all, and bounciness of N > 1 will overshoot about N times.
+ *
+ * Wolfram Plots:
+ *
+ * http://tiny.cc/elastic_b_1 (default bounciness = 1)
+ * http://tiny.cc/elastic_b_3 (bounciness = 3)
+ */
+ static elastic(bounciness: number): (t: number) => number {
+ if (arguments.length === 0) {
+ bounciness = 1;
}
- if (arguments.length) {
- s = p / tau * Math.asin(1 / a);
- } else {
- a = 1;
- s = p / 4;
- }
- return (t) => 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * tau / p);
+ var p = bounciness * Math.PI;
+ return (t) => 1 - Math.pow(Math.cos(t * Math.PI / 2), 3) * Math.cos(t * p);
};
static back(s: number): (t: number) => number {
diff --git a/Libraries/Animated/__tests__/Easing-test.js b/Libraries/Animated/__tests__/Easing-test.js
index bee894d6f..b865250d7 100644
--- a/Libraries/Animated/__tests__/Easing-test.js
+++ b/Libraries/Animated/__tests__/Easing-test.js
@@ -71,6 +71,14 @@ describe('Easing', () => {
}
});
+ it('should satisfy boundary conditions with elastic', () => {
+ for (var b = 0; b < 4; b += 0.3) {
+ var easing = Easing.elastic(b);
+ expect(easing(0)).toBe(0);
+ expect(easing(1)).toBe(1);
+ }
+ });
+
function sampleEasingFunction(easing) {
var DURATION = 300;
var tickCount = Math.round(DURATION * 60 / 1000);
diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js
index 8594cfe8b..fdb2ff6ed 100644
--- a/Libraries/Components/View/View.js
+++ b/Libraries/Components/View/View.js
@@ -274,6 +274,29 @@ var View = React.createClass({
* @platform android
*/
collapsable: PropTypes.bool,
+
+ /**
+ * Whether this view needs to rendered offscreen and composited with an alpha
+ * in order to preserve 100% correct colors and blending behavior. The default
+ * (false) falls back to drawing the component and its children with an alpha
+ * applied to the paint used to draw each element instead of rendering the full
+ * component offscreen and compositing it back with an alpha value. This default
+ * may be noticeable and undesired in the case where the View you are setting
+ * an opacity on has multiple overlapping elements (e.g. multiple overlapping
+ * Views, or text and a background).
+ *
+ * Rendering offscreen to preserve correct alpha behavior is extremely
+ * expensive and hard to debug for non-native developers, which is why it is
+ * not turned on by default. If you do need to enable this property for an
+ * animation, consider combining it with renderToHardwareTextureAndroid if the
+ * view **contents** are static (i.e. it doesn't need to be redrawn each frame).
+ * If that property is enabled, this View will be rendered off-screen once,
+ * saved in a hardware texture, and then composited onto the screen with an alpha
+ * each frame without having to switch rendering targets on the GPU.
+ *
+ * @platform android
+ */
+ needsOffscreenAlphaCompositing: PropTypes.bool,
},
render: function() {
diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js
index 3308842a2..5bb0349cc 100644
--- a/Libraries/Components/WebView/WebView.ios.js
+++ b/Libraries/Components/WebView/WebView.ios.js
@@ -210,7 +210,7 @@ var WebView = React.createClass({
onLoadingError: function(event: Event) {
event.persist(); // persist this event because we need to store it
- console.error('Encountered an error loading page', event.nativeEvent);
+ console.warn('Encountered an error loading page', event.nativeEvent);
this.setState({
lastErrorEvent: event.nativeEvent,
diff --git a/Libraries/Image/AssetRegistry.js b/Libraries/Image/AssetRegistry.js
index df4173e78..440f16587 100644
--- a/Libraries/Image/AssetRegistry.js
+++ b/Libraries/Image/AssetRegistry.js
@@ -2,18 +2,32 @@
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule AssetRegistry
+ * @flow
*/
'use strict';
-var assets = [];
+export type PackagerAsset = {
+ __packager_asset: boolean,
+ fileSystemLocation: string,
+ httpServerLocation: string,
+ width: number,
+ height: number,
+ scales: Array,
+ hash: string,
+ name: string,
+ type: string,
+};
-function registerAsset(asset) {
+
+var assets: Array = [];
+
+function registerAsset(asset: PackagerAsset): number {
// `push` returns new array length, so the first asset will
// get id 1 (not 0) to make the value truthy
return assets.push(asset);
}
-function getAssetByID(assetId) {
+function getAssetByID(assetId: number): PackagerAsset {
return assets[assetId - 1];
}
diff --git a/Libraries/Image/GlobalImageStub.js b/Libraries/Image/GlobalImageStub.js
new file mode 100644
index 000000000..5f1eb5194
--- /dev/null
+++ b/Libraries/Image/GlobalImageStub.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule GlobalImageStub
+ * @flow
+ */
+'use strict';
+
+// This is a stub for flow to make it understand require('image!icon')
+// See packager/react-packager/src/Bundler/index.js
+
+module.exports = {
+ __packager_asset: true,
+ isStatic: true,
+ path: '/full/path/to/something.png',
+ uri: 'icon',
+ width: 100,
+ height: 100,
+ deprecated: true,
+};
diff --git a/Libraries/Image/RelativeImageStub.js b/Libraries/Image/RelativeImageStub.js
new file mode 100644
index 000000000..40c30d3c8
--- /dev/null
+++ b/Libraries/Image/RelativeImageStub.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule RelativeImageStub
+ * @flow
+ */
+'use strict';
+
+// This is a stub for flow to make it understand require('./icon.png')
+// See packager/react-packager/src/Bundler/index.js
+
+var AssetRegistry = require('AssetRegistry');
+
+module.exports = AssetRegistry.registerAsset({
+ __packager_asset: true,
+ fileSystemLocation: '/full/path/to/directory',
+ httpServerLocation: '/assets/full/path/to/directory',
+ width: 100,
+ height: 100,
+ scales: [1, 2, 3],
+ hash: 'nonsense',
+ name: 'icon',
+ type: 'png',
+});
diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js
index 54b954dc0..ce1e56cfa 100644
--- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js
+++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js
@@ -138,7 +138,10 @@ function setupProfile() {
function setUpProcessEnv() {
GLOBAL.process = GLOBAL.process || {};
- GLOBAL.process.env = {NODE_ENV: __DEV__ ? 'development' : 'production'};
+ GLOBAL.process.env = GLOBAL.process.env || {};
+ if (!GLOBAL.process.env.NODE_ENV) {
+ GLOBAL.process.env.NODE_ENV = __DEV__ ? 'development' : 'production';
+ }
}
setUpRedBoxErrorHandler();
diff --git a/Libraries/Portal/Portal.js b/Libraries/Portal/Portal.js
index 652e9ed6f..ac80e0410 100644
--- a/Libraries/Portal/Portal.js
+++ b/Libraries/Portal/Portal.js
@@ -18,6 +18,9 @@ var _portalRef: any;
var lastUsedTag = 0;
/*
+ * Note: Only intended for Android at the moment. Just use Modal in your iOS
+ * code.
+ *
* A container that renders all the modals on top of everything else in the application.
*
* Portal makes it possible for application code to pass modal views all the way up to
diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m
index 372858ac9..b0c932bc1 100644
--- a/Libraries/RCTTest/RCTTestRunner.m
+++ b/Libraries/RCTTest/RCTTestRunner.m
@@ -49,7 +49,7 @@
_scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"];
RCTAssert(_scriptURL != nil, @"Could not locate main.jsBundle");
#else
- _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]];
+ _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?dev=true&platform=ios", app]];
#endif
}
return self;
diff --git a/Libraries/ReactNative/ReactNativeViewAttributes.js b/Libraries/ReactNative/ReactNativeViewAttributes.js
index 028284db5..0447c78ab 100644
--- a/Libraries/ReactNative/ReactNativeViewAttributes.js
+++ b/Libraries/ReactNative/ReactNativeViewAttributes.js
@@ -30,6 +30,7 @@ ReactNativeViewAttributes.UIView = {
onAccessibilityTap: true,
onMagicTap: true,
collapsable: true,
+ needsOffscreenAlphaCompositing: true,
};
ReactNativeViewAttributes.RCTView = merge(
diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js
index ffca2bca5..007b0dfca 100644
--- a/Libraries/Utilities/MessageQueue.js
+++ b/Libraries/Utilities/MessageQueue.js
@@ -65,13 +65,11 @@ class MessageQueue {
localModules && this._genLookupTables(
localModules, this._moduleTable, this._methodTable);
- if (__DEV__) {
- this._debugInfo = {};
- this._remoteModuleTable = {};
- this._remoteMethodTable = {};
- this._genLookupTables(
- remoteModules, this._remoteModuleTable, this._remoteMethodTable);
- }
+ this._debugInfo = {};
+ this._remoteModuleTable = {};
+ this._remoteMethodTable = {};
+ this._genLookupTables(
+ remoteModules, this._remoteModuleTable, this._remoteMethodTable);
}
/**
@@ -116,13 +114,11 @@ class MessageQueue {
*/
__nativeCall(module, method, params, onFail, onSucc) {
if (onFail || onSucc) {
- if (__DEV__) {
- // eventually delete old debug info
- (this._callbackID > (1 << 5)) &&
- (this._debugInfo[this._callbackID >> 5] = null);
+ // eventually delete old debug info
+ (this._callbackID > (1 << 5)) &&
+ (this._debugInfo[this._callbackID >> 5] = null);
- this._debugInfo[this._callbackID >> 1] = [module, method];
- }
+ this._debugInfo[this._callbackID >> 1] = [module, method];
onFail && params.push(this._callbackID);
this._callbacks[this._callbackID++] = onFail;
onSucc && params.push(this._callbackID);
@@ -155,13 +151,15 @@ class MessageQueue {
BridgeProfiling.profile(
() => `MessageQueue.invokeCallback(${cbID}, ${stringifySafe(args)})`);
let callback = this._callbacks[cbID];
- if (__DEV__) {
+ if (!callback || __DEV__) {
let debug = this._debugInfo[cbID >> 1];
let module = debug && this._remoteModuleTable[debug[0]];
let method = debug && this._remoteMethodTable[debug[0]][debug[1]];
- if (!callback) {
- console.error(`Callback with id ${cbID}: ${module}.${method}() not found`);
- } else if (SPY_MODE) {
+ invariant(
+ callback,
+ `Callback with id ${cbID}: ${module}.${method}() not found`
+ );
+ if (callback && SPY_MODE) {
console.log('N->JS : (' + JSON.stringify(args) + ')');
}
}
diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js
index 7d005069f..f0b492980 100644
--- a/Libraries/react-native/react-native.js
+++ b/Libraries/react-native/react-native.js
@@ -21,6 +21,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
// Components
ActivityIndicatorIOS: require('ActivityIndicatorIOS'),
DatePickerIOS: require('DatePickerIOS'),
+ DrawerLayoutAndroid: require('DrawerLayoutAndroid'),
Image: require('Image'),
ListView: require('ListView'),
MapView: require('MapView'),
@@ -28,15 +29,19 @@ var ReactNative = Object.assign(Object.create(require('React')), {
Navigator: require('Navigator'),
NavigatorIOS: require('NavigatorIOS'),
PickerIOS: require('PickerIOS'),
+ ProgressBarAndroid: require('ProgressBarAndroid'),
ProgressViewIOS: require('ProgressViewIOS'),
ScrollView: require('ScrollView'),
SegmentedControlIOS: require('SegmentedControlIOS'),
SliderIOS: require('SliderIOS'),
+ SwitchAndroid: require('SwitchAndroid'),
SwitchIOS: require('SwitchIOS'),
TabBarIOS: require('TabBarIOS'),
Text: require('Text'),
TextInput: require('TextInput'),
+ ToolbarAndroid: require('ToolbarAndroid'),
TouchableHighlight: require('TouchableHighlight'),
+ TouchableNativeFeedback: require('TouchableNativeFeedback'),
TouchableOpacity: require('TouchableOpacity'),
TouchableWithoutFeedback: require('TouchableWithoutFeedback'),
View: require('View'),
@@ -50,6 +55,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
AppRegistry: require('AppRegistry'),
AppStateIOS: require('AppStateIOS'),
AsyncStorage: require('AsyncStorage'),
+ BackAndroid: require('BackAndroid'),
CameraRoll: require('CameraRoll'),
Dimensions: require('Dimensions'),
Easing: require('Easing'),
diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m
index ad0cc39ee..5ef4c6502 100644
--- a/React/Base/RCTBatchedBridge.m
+++ b/React/Base/RCTBatchedBridge.m
@@ -407,9 +407,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR
- (NSDictionary *)modules
{
- RCTAssert(!self.isValid || _modulesByName != nil, @"Bridge modules have not yet been initialized. "
- "You may be trying to access a module too early in the startup procedure.");
-
+ if (RCT_DEBUG && self.isValid && _modulesByName == nil) {
+ RCTLogError(@"Bridge modules have not yet been initialized. You may be "
+ "trying to access a module too early in the startup procedure.");
+ }
return _modulesByName;
}
diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m
index a8679da9e..ebbf6a1b8 100644
--- a/React/Base/RCTLog.m
+++ b/React/Base/RCTLog.m
@@ -226,7 +226,11 @@ void _RCTLogFormat(
}
}
}];
- [[RCTBridge currentBridge].redBox showErrorMessage:message withStack:stack];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ // red box is thread safe, but by deferring to main queue we avoid a startup
+ // race condition that causes the module to be accessed before it has loaded
+ [[RCTBridge currentBridge].redBox showErrorMessage:message withStack:stack];
+ });
}
// Log to JS executor
diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m
index 8b051fa67..2dcf3d7b0 100644
--- a/React/Executors/RCTContextExecutor.m
+++ b/React/Executors/RCTContextExecutor.m
@@ -16,6 +16,7 @@
#import "RCTAssert.h"
#import "RCTDefines.h"
+#import "RCTDevMenu.h"
#import "RCTLog.h"
#import "RCTProfile.h"
#import "RCTPerformanceLogger.h"
@@ -89,6 +90,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init)
}
@synthesize valid = _valid;
+@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
@@ -110,7 +112,7 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, __unused JSObjectRe
NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, messageRef);
JSStringRelease(messageRef);
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:
- @"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).includeRequire.runModule.bundle(:[0-9]+:[0-9]+)"
+ @"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).bundle(:[0-9]+:[0-9]+)"
options:NSRegularExpressionCaseInsensitive
error:NULL];
message = [regex stringByReplacingMatchesInString:message
@@ -285,11 +287,18 @@ static JSValueRef RCTNativeTraceEndSection(JSContextRef context, __unused JSObje
#if RCT_JSC_PROFILER
void *JSCProfiler = dlopen(RCT_JSC_PROFILER_DYLIB, RTLD_NOW);
if (JSCProfiler != NULL) {
- JSObjectCallAsFunctionCallback nativeProfilerStart = dlsym(JSCProfiler, "nativeProfilerStart");
- JSObjectCallAsFunctionCallback nativeProfilerEnd = dlsym(JSCProfiler, "nativeProfilerEnd");
+ void (*nativeProfilerStart)(JSContextRef, const char *) = (void (*)(JSContextRef, const char *))dlsym(JSCProfiler, "nativeProfilerStart");
+ const char *(*nativeProfilerEnd)(JSContextRef, const char *) = (const char *(*)(JSContextRef, const char *))dlsym(JSCProfiler, "nativeProfilerEnd");
if (nativeProfilerStart != NULL && nativeProfilerEnd != NULL) {
- [strongSelf _addNativeHook:nativeProfilerStart withName:"nativeProfilerStart"];
- [strongSelf _addNativeHook:nativeProfilerEnd withName:"nativeProfilerStop"];
+ __block BOOL isProfiling = NO;
+ [_bridge.devMenu addItem:@"Profile" handler:^{
+ if (isProfiling) {
+ RCTLogInfo(@"%s", nativeProfilerEnd(strongSelf->_context.ctx, "profile"));
+ } else {
+ nativeProfilerStart(strongSelf->_context.ctx, "profile");
+ }
+ isProfiling = !isProfiling;
+ }];
}
}
#endif
diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m
index 56ef7dfd4..b99bd5554 100644
--- a/React/Modules/RCTDevMenu.m
+++ b/React/Modules/RCTDevMenu.m
@@ -314,7 +314,7 @@ RCT_EXPORT_MODULE()
self.liveReloadEnabled = !_liveReloadEnabled;
}]];
- NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Profiling" : @"Start Profiling";
+ NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Systrace" : @"Start Systrace";
[items addObject:[[RCTDevMenuItem alloc] initWithTitle:profilingTitle handler:^{
self.profilingEnabled = !_profilingEnabled;
}]];
diff --git a/React/Modules/RCTSourceCode.m b/React/Modules/RCTSourceCode.m
index 08b033781..05860e1dd 100644
--- a/React/Modules/RCTSourceCode.m
+++ b/React/Modules/RCTSourceCode.m
@@ -9,6 +9,7 @@
#import "RCTSourceCode.h"
+#import "RCTDefines.h"
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTUtils.h"
@@ -19,20 +20,18 @@ RCT_EXPORT_MODULE()
@synthesize bridge = _bridge;
+#if !RCT_DEV
+- (void)setScriptText:(NSString *)scriptText {}
+#endif
+
RCT_EXPORT_METHOD(getScriptText:(RCTResponseSenderBlock)successCallback
failureCallback:(RCTResponseErrorBlock)failureCallback)
{
- if (self.scriptText && self.scriptURL) {
+ if (RCT_DEV && self.scriptText && self.scriptURL) {
successCallback(@[@{@"text": self.scriptText, @"url": self.scriptURL.absoluteString}]);
} else {
failureCallback(RCTErrorWithMessage(@"Source code is not available"));
}
}
-- (NSDictionary *)constantsToExport
-{
- NSString *URL = self.bridge.bundleURL.absoluteString ?: @"";
- return @{@"scriptURL": URL};
-}
-
@end
diff --git a/packager/debugger.html b/packager/debugger.html
index 24f8aea55..3b905efe3 100644
--- a/packager/debugger.html
+++ b/packager/debugger.html
@@ -85,6 +85,10 @@ function connectToDebuggerProxy() {
ws.onmessage = function(message) {
var object = JSON.parse(message.data);
+ if (!object.method) {
+ return;
+ }
+
var sendReply = function(result) {
ws.send(JSON.stringify({replyID: object.id, result: result}));
};
diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js
index d29ab0f89..8f76ce103 100644
--- a/packager/react-packager/src/Bundler/index.js
+++ b/packager/react-packager/src/Bundler/index.js
@@ -13,6 +13,7 @@ const fs = require('fs');
const path = require('path');
const Promise = require('promise');
const ProgressBar = require('progress');
+const BundlesLayout = require('../BundlesLayout');
const Cache = require('../Cache');
const Transformer = require('../JSTransformer');
const DependencyResolver = require('../DependencyResolver');
@@ -104,6 +105,13 @@ class Bundler {
cache: this._cache,
});
+ this._bundlesLayout = new BundlesLayout({
+ dependencyResolver: this._resolver,
+ resetCache: opts.resetCache,
+ cacheVersion: opts.cacheVersion,
+ projectRoots: opts.projectRoots,
+ });
+
this._transformer = new Transformer({
projectRoots: opts.projectRoots,
blacklistRE: opts.blacklistRE,
@@ -120,6 +128,10 @@ class Bundler {
return this._cache.end();
}
+ getLayout(main, isDev) {
+ return this._bundlesLayout.generateLayout(main, isDev);
+ }
+
bundle(main, runModule, sourceMapUrl, isDev, platform) {
const bundle = new Bundle(sourceMapUrl);
const findEventId = Activity.startEvent('find dependencies');
diff --git a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js
index cca9c8a7e..2f66d6ff6 100644
--- a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js
+++ b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js
@@ -8,249 +8,320 @@
*/
'use strict';
-jest
- .dontMock('../index');
+jest.dontMock('../index');
+jest.mock('fs');
const Promise = require('promise');
describe('BundlesLayout', () => {
- var BundlesLayout;
- var DependencyResolver;
+ let BundlesLayout;
+ let DependencyResolver;
+ let loadCacheSync;
beforeEach(() => {
BundlesLayout = require('../index');
DependencyResolver = require('../../DependencyResolver');
+ loadCacheSync = require('../../lib/loadCacheSync');
});
- describe('generate', () => {
- function newBundlesLayout() {
- return new BundlesLayout({
- dependencyResolver: new DependencyResolver(),
- });
- }
+ function newBundlesLayout(options) {
+ return new BundlesLayout(Object.assign({
+ projectRoots: ['/root'],
+ dependencyResolver: new DependencyResolver(),
+ }, options));
+ }
+ describe('layout', () => {
function isPolyfill() {
return false;
}
- function dep(path) {
- return {
- path: path,
- isPolyfill: isPolyfill,
- };
- }
+ describe('getLayout', () => {
+ function dep(path) {
+ return {
+ path: path,
+ isPolyfill: isPolyfill,
+ };
+ }
- pit('should bundle sync dependencies', () => {
- DependencyResolver.prototype.getDependencies.mockImpl((path) => {
- switch (path) {
- case '/root/index.js':
- return Promise.resolve({
- dependencies: [dep('/root/index.js'), dep('/root/a.js')],
- asyncDependencies: [],
- });
- case '/root/a.js':
- return Promise.resolve({
- dependencies: [dep('/root/a.js')],
- asyncDependencies: [],
- });
- default:
- throw 'Undefined path: ' + path;
- }
+ pit('should bundle sync dependencies', () => {
+ DependencyResolver.prototype.getDependencies.mockImpl((path) => {
+ switch (path) {
+ case '/root/index.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/index.js'), dep('/root/a.js')],
+ asyncDependencies: [],
+ });
+ case '/root/a.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/a.js')],
+ asyncDependencies: [],
+ });
+ default:
+ throw 'Undefined path: ' + path;
+ }
+ });
+
+ return newBundlesLayout({resetCache: true})
+ .getLayout('/root/index.js')
+ .then(bundles =>
+ expect(bundles).toEqual({
+ id: 'bundle.0',
+ modules: ['/root/index.js', '/root/a.js'],
+ children: [],
+ })
+ );
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
- expect(bundles).toEqual({
- id: 'bundle.0',
- modules: ['/root/index.js', '/root/a.js'],
- children: [],
- })
- );
- });
+ pit('should separate async dependencies into different bundle', () => {
+ DependencyResolver.prototype.getDependencies.mockImpl((path) => {
+ switch (path) {
+ case '/root/index.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/index.js')],
+ asyncDependencies: [['/root/a.js']],
+ });
+ case '/root/a.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/a.js')],
+ asyncDependencies: [],
+ });
+ default:
+ throw 'Undefined path: ' + path;
+ }
+ });
- pit('should separate async dependencies into different bundle', () => {
- DependencyResolver.prototype.getDependencies.mockImpl((path) => {
- switch (path) {
- case '/root/index.js':
- return Promise.resolve({
- dependencies: [dep('/root/index.js')],
- asyncDependencies: [['/root/a.js']],
- });
- case '/root/a.js':
- return Promise.resolve({
- dependencies: [dep('/root/a.js')],
- asyncDependencies: [],
- });
- default:
- throw 'Undefined path: ' + path;
- }
- });
-
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
- expect(bundles).toEqual({
- id: 'bundle.0',
- modules: ['/root/index.js'],
- children: [{
- id:'bundle.0.1',
- modules: ['/root/a.js'],
- children: [],
- }],
- })
- );
- });
-
- pit('separate async dependencies of async dependencies', () => {
- DependencyResolver.prototype.getDependencies.mockImpl((path) => {
- switch (path) {
- case '/root/index.js':
- return Promise.resolve({
- dependencies: [dep('/root/index.js')],
- asyncDependencies: [['/root/a.js']],
- });
- case '/root/a.js':
- return Promise.resolve({
- dependencies: [dep('/root/a.js')],
- asyncDependencies: [['/root/b.js']],
- });
- case '/root/b.js':
- return Promise.resolve({
- dependencies: [dep('/root/b.js')],
- asyncDependencies: [],
- });
- default:
- throw 'Undefined path: ' + path;
- }
- });
-
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
- expect(bundles).toEqual({
- id: 'bundle.0',
- modules: ['/root/index.js'],
- children: [{
- id: 'bundle.0.1',
- modules: ['/root/a.js'],
+ return newBundlesLayout({resetCache: true})
+ .getLayout('/root/index.js')
+ .then(bundles =>
+ expect(bundles).toEqual({
+ id: 'bundle.0',
+ modules: ['/root/index.js'],
children: [{
- id: 'bundle.0.1.2',
+ id:'bundle.0.1',
+ modules: ['/root/a.js'],
+ children: [],
+ }],
+ })
+ );
+ });
+
+ pit('separate async dependencies of async dependencies', () => {
+ DependencyResolver.prototype.getDependencies.mockImpl((path) => {
+ switch (path) {
+ case '/root/index.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/index.js')],
+ asyncDependencies: [['/root/a.js']],
+ });
+ case '/root/a.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/a.js')],
+ asyncDependencies: [['/root/b.js']],
+ });
+ case '/root/b.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/b.js')],
+ asyncDependencies: [],
+ });
+ default:
+ throw 'Undefined path: ' + path;
+ }
+ });
+
+ return newBundlesLayout({resetCache: true})
+ .getLayout('/root/index.js')
+ .then(bundles =>
+ expect(bundles).toEqual({
+ id: 'bundle.0',
+ modules: ['/root/index.js'],
+ children: [{
+ id: 'bundle.0.1',
+ modules: ['/root/a.js'],
+ children: [{
+ id: 'bundle.0.1.2',
+ modules: ['/root/b.js'],
+ children: [],
+ }],
+ }],
+ })
+ );
+ });
+
+ pit('separate bundle sync dependencies of async ones on same bundle', () => {
+ DependencyResolver.prototype.getDependencies.mockImpl((path) => {
+ switch (path) {
+ case '/root/index.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/index.js')],
+ asyncDependencies: [['/root/a.js']],
+ });
+ case '/root/a.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/a.js'), dep('/root/b.js')],
+ asyncDependencies: [],
+ });
+ case '/root/b.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/b.js')],
+ asyncDependencies: [],
+ });
+ default:
+ throw 'Undefined path: ' + path;
+ }
+ });
+
+ return newBundlesLayout({resetCache: true})
+ .getLayout('/root/index.js')
+ .then(bundles =>
+ expect(bundles).toEqual({
+ id: 'bundle.0',
+ modules: ['/root/index.js'],
+ children: [{
+ id: 'bundle.0.1',
+ modules: ['/root/a.js', '/root/b.js'],
+ children: [],
+ }],
+ })
+ );
+ });
+
+ pit('separate cache in which bundle is each dependency', () => {
+ DependencyResolver.prototype.getDependencies.mockImpl((path) => {
+ switch (path) {
+ case '/root/index.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/index.js'), dep('/root/a.js')],
+ asyncDependencies: [],
+ });
+ case '/root/a.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/a.js')],
+ asyncDependencies: [['/root/b.js']],
+ });
+ case '/root/b.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/b.js')],
+ asyncDependencies: [],
+ });
+ default:
+ throw 'Undefined path: ' + path;
+ }
+ });
+
+ return newBundlesLayout({resetCache: true})
+ .getLayout('/root/index.js')
+ .then(bundles =>
+ expect(bundles).toEqual({
+ id: 'bundle.0',
+ modules: ['/root/index.js', '/root/a.js'],
+ children: [{
+ id: 'bundle.0.1',
modules: ['/root/b.js'],
children: [],
}],
- }],
- })
- );
- });
-
- pit('separate bundle sync dependencies of async ones on same bundle', () => {
- DependencyResolver.prototype.getDependencies.mockImpl((path) => {
- switch (path) {
- case '/root/index.js':
- return Promise.resolve({
- dependencies: [dep('/root/index.js')],
- asyncDependencies: [['/root/a.js']],
- });
- case '/root/a.js':
- return Promise.resolve({
- dependencies: [dep('/root/a.js'), dep('/root/b.js')],
- asyncDependencies: [],
- });
- case '/root/b.js':
- return Promise.resolve({
- dependencies: [dep('/root/b.js')],
- asyncDependencies: [],
- });
- default:
- throw 'Undefined path: ' + path;
- }
+ })
+ );
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
- expect(bundles).toEqual({
+ pit('separate cache in which bundle is each dependency', () => {
+ DependencyResolver.prototype.getDependencies.mockImpl((path) => {
+ switch (path) {
+ case '/root/index.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/index.js'), dep('/root/a.js')],
+ asyncDependencies: [['/root/b.js'], ['/root/c.js']],
+ });
+ case '/root/a.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/a.js')],
+ asyncDependencies: [],
+ });
+ case '/root/b.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/b.js')],
+ asyncDependencies: [['/root/d.js']],
+ });
+ case '/root/c.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/c.js')],
+ asyncDependencies: [],
+ });
+ case '/root/d.js':
+ return Promise.resolve({
+ dependencies: [dep('/root/d.js')],
+ asyncDependencies: [],
+ });
+ default:
+ throw 'Undefined path: ' + path;
+ }
+ });
+
+ var layout = newBundlesLayout({resetCache: true});
+ return layout.getLayout('/root/index.js').then(() => {
+ expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0');
+ expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0');
+ expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.0.1');
+ expect(layout.getBundleIDForModule('/root/c.js')).toBe('bundle.0.2');
+ expect(layout.getBundleIDForModule('/root/d.js')).toBe('bundle.0.1.3');
+ });
+ });
+ });
+ });
+
+ describe('cache', () => {
+ beforeEach(() => {
+ loadCacheSync.mockReturnValue({
+ '/root/index.js': {
id: 'bundle.0',
modules: ['/root/index.js'],
children: [{
id: 'bundle.0.1',
- modules: ['/root/a.js', '/root/b.js'],
+ modules: ['/root/a.js'],
children: [],
}],
- })
- );
+ },
+ '/root/b.js': {
+ id: 'bundle.2',
+ modules: ['/root/b.js'],
+ children: [],
+ },
+ });
});
- pit('separate cache in which bundle is each dependency', () => {
- DependencyResolver.prototype.getDependencies.mockImpl((path) => {
- switch (path) {
- case '/root/index.js':
- return Promise.resolve({
- dependencies: [dep('/root/index.js'), dep('/root/a.js')],
- asyncDependencies: [],
- });
- case '/root/a.js':
- return Promise.resolve({
- dependencies: [dep('/root/a.js')],
- asyncDependencies: [['/root/b.js']],
- });
- case '/root/b.js':
- return Promise.resolve({
- dependencies: [dep('/root/b.js')],
- asyncDependencies: [],
- });
- default:
- throw 'Undefined path: ' + path;
- }
- });
+ pit('should load layouts', () => {
+ const layout = newBundlesLayout({ resetCache: false });
- return newBundlesLayout().generateLayout(['/root/index.js']).then(
- bundles => expect(bundles).toEqual({
- id: 'bundle.0',
- modules: ['/root/index.js', '/root/a.js'],
- children: [{
- id: 'bundle.0.1',
+ return Promise
+ .all([
+ layout.getLayout('/root/index.js'),
+ layout.getLayout('/root/b.js'),
+ ])
+ .then(([layoutIndex, layoutB]) => {
+ expect(layoutIndex).toEqual({
+ id: 'bundle.0',
+ modules: ['/root/index.js'],
+ children: [{
+ id: 'bundle.0.1',
+ modules: ['/root/a.js'],
+ children: [],
+ }],
+ });
+
+ expect(layoutB).toEqual({
+ id: 'bundle.2',
modules: ['/root/b.js'],
children: [],
- }],
- })
- );
+ });
+ });
});
- pit('separate cache in which bundle is each dependency', () => {
- DependencyResolver.prototype.getDependencies.mockImpl((path) => {
- switch (path) {
- case '/root/index.js':
- return Promise.resolve({
- dependencies: [dep('/root/index.js'), dep('/root/a.js')],
- asyncDependencies: [['/root/b.js'], ['/root/c.js']],
- });
- case '/root/a.js':
- return Promise.resolve({
- dependencies: [dep('/root/a.js')],
- asyncDependencies: [],
- });
- case '/root/b.js':
- return Promise.resolve({
- dependencies: [dep('/root/b.js')],
- asyncDependencies: [['/root/d.js']],
- });
- case '/root/c.js':
- return Promise.resolve({
- dependencies: [dep('/root/c.js')],
- asyncDependencies: [],
- });
- case '/root/d.js':
- return Promise.resolve({
- dependencies: [dep('/root/d.js')],
- asyncDependencies: [],
- });
- default:
- throw 'Undefined path: ' + path;
- }
- });
+ it('should load moduleToBundle map', () => {
+ const layout = newBundlesLayout({ resetCache: false });
- var layout = newBundlesLayout();
- return layout.generateLayout(['/root/index.js']).then(() => {
- expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0');
- expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0');
- expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.0.1');
- expect(layout.getBundleIDForModule('/root/c.js')).toBe('bundle.0.2');
- expect(layout.getBundleIDForModule('/root/d.js')).toBe('bundle.0.1.3');
- });
+ expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0');
+ expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0.1');
+ expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.2');
});
});
});
diff --git a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js
index f834ccf57..a54ee2164 100644
--- a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js
+++ b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js
@@ -75,7 +75,11 @@ describe('BundlesLayout', () => {
assetRoots: ['/root'],
});
- return new BundlesLayout({dependencyResolver: resolver});
+ return new BundlesLayout({
+ dependencyResolver: resolver,
+ resetCache: true,
+ projectRoots: ['/root', '/' + __dirname.split('/')[1]],
+ });
}
function stripPolyfills(bundle) {
@@ -114,7 +118,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -140,7 +144,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -166,7 +170,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -201,7 +205,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -242,7 +246,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -282,7 +286,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -323,7 +327,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -370,7 +374,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -408,7 +412,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -446,7 +450,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -480,7 +484,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -512,7 +516,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -539,7 +543,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
@@ -576,7 +580,7 @@ describe('BundlesLayout', () => {
}
});
- return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles =>
+ return newBundlesLayout().getLayout('/root/index.js').then(bundles =>
stripPolyfills(bundles).then(resolvedBundles =>
expect(resolvedBundles).toEqual({
id: 'bundle.0',
diff --git a/packager/react-packager/src/BundlesLayout/index.js b/packager/react-packager/src/BundlesLayout/index.js
index c6da31573..d946cefe1 100644
--- a/packager/react-packager/src/BundlesLayout/index.js
+++ b/packager/react-packager/src/BundlesLayout/index.js
@@ -8,14 +8,33 @@
*/
'use strict';
+const Activity = require('../Activity');
+
const _ = require('underscore');
const declareOpts = require('../lib/declareOpts');
+const fs = require('fs');
+const getCacheFilePath = require('../lib/getCacheFilePath');
+const loadCacheSync = require('../lib/loadCacheSync');
+const version = require('../../../../package.json').version;
+const path = require('path');
const validateOpts = declareOpts({
dependencyResolver: {
type: 'object',
required: true,
},
+ resetCache: {
+ type: 'boolean',
+ default: false,
+ },
+ cacheVersion: {
+ type: 'string',
+ default: '1.0',
+ },
+ projectRoots: {
+ type: 'array',
+ required: true,
+ },
});
const BUNDLE_PREFIX = 'bundle';
@@ -29,19 +48,37 @@ class BundlesLayout {
const opts = validateOpts(options);
this._resolver = opts.dependencyResolver;
+ // Cache in which bundle is each module.
this._moduleToBundle = Object.create(null);
+
+ // Cache the bundles layouts for each entry point. This entries
+ // are not evicted unless the user explicitly specifies so as
+ // computing them is pretty expensive
+ this._layouts = Object.create(null);
+
+ // TODO: watch for file creations and removals to update this caches
+
+ this._cacheFilePath = this._getCacheFilePath(opts);
+ if (!opts.resetCache) {
+ this._loadCacheSync(this._cacheFilePath);
+ } else {
+ this._persistCacheEventually();
+ }
}
- generateLayout(entryPaths, isDev) {
+ getLayout(entryPath, isDev) {
+ if (this._layouts[entryPath]) {
+ return this._layouts[entryPath];
+ }
var currentBundleID = 0;
const rootBundle = {
id: BUNDLE_PREFIX + '.' + currentBundleID++,
modules: [],
children: [],
};
- var pending = [{paths: entryPaths, bundle: rootBundle}];
+ var pending = [{paths: [entryPath], bundle: rootBundle}];
- return promiseWhile(
+ this._layouts[entryPath] = promiseWhile(
() => pending.length > 0,
() => rootBundle,
() => {
@@ -62,6 +99,9 @@ class BundlesLayout {
if (dependencies.length > 0) {
bundle.modules = dependencies;
}
+
+ // persist changes to layouts
+ this._persistCacheEventually();
},
index => {
const pendingSyncDep = pendingSyncDeps.shift();
@@ -90,11 +130,71 @@ class BundlesLayout {
);
},
);
+
+ return this._layouts[entryPath];
}
getBundleIDForModule(path) {
return this._moduleToBundle[path];
}
+
+ _loadCacheSync(cachePath) {
+ const loadCacheId = Activity.startEvent('Loading bundles layout');
+ const cacheOnDisk = loadCacheSync(cachePath);
+
+ // TODO: create single-module bundles for unexistent modules
+ // TODO: remove modules that no longer exist
+ Object.keys(cacheOnDisk).forEach(entryPath => {
+ this._layouts[entryPath] = Promise.resolve(cacheOnDisk[entryPath]);
+ this._fillModuleToBundleMap(cacheOnDisk[entryPath]);
+ });
+
+ Activity.endEvent(loadCacheId);
+ }
+
+ _fillModuleToBundleMap(bundle) {
+ bundle.modules.forEach(module => this._moduleToBundle[module] = bundle.id);
+ bundle.children.forEach(child => this._fillModuleToBundleMap(child));
+ }
+
+ _persistCacheEventually() {
+ _.debounce(
+ this._persistCache.bind(this),
+ 2000,
+ );
+ }
+
+ _persistCache() {
+ if (this._persisting !== null) {
+ return this._persisting;
+ }
+
+ this._persisting = Promise
+ .all(_.values(this._layouts))
+ .then(bundlesLayout => {
+ var json = Object.create(null);
+ Object.keys(this._layouts).forEach((p, i) =>
+ json[p] = bundlesLayout[i]
+ );
+
+ return Promise.denodeify(fs.writeFile)(
+ this._cacheFilepath,
+ JSON.stringify(json),
+ );
+ })
+ .then(() => this._persisting = null);
+
+ return this._persisting;
+ }
+
+ _getCacheFilePath(options) {
+ return getCacheFilePath(
+ 'react-packager-bundles-cache-',
+ version,
+ options.projectRoots.join(',').split(path.sep).join('-'),
+ options.cacheVersion || '0',
+ );
+ }
}
// Runs the body Promise meanwhile the condition callback is satisfied.
diff --git a/packager/react-packager/src/Cache/__tests__/Cache-test.js b/packager/react-packager/src/Cache/__tests__/Cache-test.js
index f4aef9147..8040e9e66 100644
--- a/packager/react-packager/src/Cache/__tests__/Cache-test.js
+++ b/packager/react-packager/src/Cache/__tests__/Cache-test.js
@@ -11,7 +11,9 @@
jest
.dontMock('underscore')
.dontMock('absolute-path')
- .dontMock('../');
+ .dontMock('../')
+ .dontMock('../../lib/loadCacheSync')
+ .dontMock('../../lib/getCacheFilePath');
jest
.mock('os')
diff --git a/packager/react-packager/src/Cache/index.js b/packager/react-packager/src/Cache/index.js
index ae6d1aa35..708f8cbd7 100644
--- a/packager/react-packager/src/Cache/index.js
+++ b/packager/react-packager/src/Cache/index.js
@@ -8,17 +8,17 @@
*/
'use strict';
-var _ = require('underscore');
-var crypto = require('crypto');
-var declareOpts = require('../lib/declareOpts');
-var fs = require('fs');
-var isAbsolutePath = require('absolute-path');
-var path = require('path');
-var Promise = require('promise');
-var tmpdir = require('os').tmpDir();
-var version = require('../../../../package.json').version;
+const Promise = require('promise');
+const _ = require('underscore');
+const declareOpts = require('../lib/declareOpts');
+const fs = require('fs');
+const getCacheFilePath = require('../lib/getCacheFilePath');
+const isAbsolutePath = require('absolute-path');
+const loadCacheSync = require('../lib/loadCacheSync');
+const path = require('path');
+const version = require('../../../../package.json').version;
-var validateOpts = declareOpts({
+const validateOpts = declareOpts({
resetCache: {
type: 'boolean',
default: false,
@@ -164,21 +164,7 @@ class Cache {
_loadCacheSync(cachePath) {
var ret = Object.create(null);
- if (!fs.existsSync(cachePath)) {
- return ret;
- }
-
- var cacheOnDisk;
- try {
- cacheOnDisk = JSON.parse(fs.readFileSync(cachePath));
- } catch (e) {
- if (e instanceof SyntaxError) {
- console.warn('Unable to parse cache file. Will clear and continue.');
- fs.unlinkSync(cachePath);
- return ret;
- }
- throw e;
- }
+ var cacheOnDisk = loadCacheSync(cachePath);
// Filter outdated cache and convert to promises.
Object.keys(cacheOnDisk).forEach(key => {
@@ -203,20 +189,13 @@ class Cache {
}
_getCacheFilePath(options) {
- var hash = crypto.createHash('md5');
- hash.update(version);
-
- var roots = options.projectRoots.join(',').split(path.sep).join('-');
- hash.update(roots);
-
- var cacheVersion = options.cacheVersion || '0';
- hash.update(cacheVersion);
-
- hash.update(options.transformModulePath);
-
- var name = 'react-packager-cache-' + hash.digest('hex');
-
- return path.join(tmpdir, name);
+ return getCacheFilePath(
+ 'react-packager-cache-',
+ version,
+ options.projectRoots.join(',').split(path.sep).join('-'),
+ options.cacheVersion || '0',
+ options.transformModulePath,
+ );
}
}
diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js
index 47019846d..9bf191390 100644
--- a/packager/react-packager/src/JSTransformer/index.js
+++ b/packager/react-packager/src/JSTransformer/index.js
@@ -23,7 +23,10 @@ const readFile = Promise.denodeify(fs.readFile);
const MAX_CALLS_PER_WORKER = 600;
// Worker will timeout if one of the callers timeout.
-const DEFAULT_MAX_CALL_TIME = 60000;
+const DEFAULT_MAX_CALL_TIME = 120000;
+
+// How may times can we tolerate failures from the worker.
+const MAX_RETRIES = 3;
const validateOpts = declareOpts({
projectRoots: {
@@ -63,6 +66,7 @@ class Transformer {
maxConcurrentCallsPerWorker: 1,
maxCallsPerWorker: MAX_CALLS_PER_WORKER,
maxCallTime: opts.transformTimeoutInterval,
+ maxRetries: MAX_RETRIES,
}, opts.transformModulePath);
this._transform = Promise.denodeify(this._workers);
@@ -118,6 +122,13 @@ class Transformer {
);
timeoutErr.type = 'TimeoutError';
throw timeoutErr;
+ } else if (err.type === 'ProcessTerminatedError') {
+ const uncaughtError = new Error(
+ 'Uncaught error in the transformer worker: ' +
+ this._opts.transformModulePath
+ );
+ uncaughtError.type = 'ProcessTerminatedError';
+ throw uncaughtError;
}
throw formatError(err, filePath);
diff --git a/packager/react-packager/src/SocketInterface/SocketClient.js b/packager/react-packager/src/SocketInterface/SocketClient.js
index 30b599b0f..e20e5b896 100644
--- a/packager/react-packager/src/SocketInterface/SocketClient.js
+++ b/packager/react-packager/src/SocketInterface/SocketClient.js
@@ -13,6 +13,10 @@ const Promise = require('promise');
const bser = require('bser');
const debug = require('debug')('ReactPackager:SocketClient');
const net = require('net');
+const path = require('path');
+const tmpdir = require('os').tmpdir();
+
+const LOG_PATH = path.join(tmpdir, 'react-packager.log');
class SocketClient {
static create(sockPath) {
@@ -81,7 +85,9 @@ class SocketClient {
delete this._resolvers[message.id];
if (message.type === 'error') {
- resolver.reject(new Error(message.data));
+ resolver.reject(new Error(
+ message.data + '\n' + 'See logs ' + LOG_PATH
+ ));
} else {
resolver.resolve(message.data);
}
diff --git a/packager/react-packager/src/SocketInterface/SocketServer.js b/packager/react-packager/src/SocketInterface/SocketServer.js
index f6faf8368..d3cb752d1 100644
--- a/packager/react-packager/src/SocketInterface/SocketServer.js
+++ b/packager/react-packager/src/SocketInterface/SocketServer.js
@@ -32,6 +32,7 @@ class SocketServer {
options
);
resolve(this);
+ process.on('exit', () => fs.unlinkSync(sockPath));
});
});
this._server.on('connection', (sock) => this._handleConnection(sock));
@@ -41,8 +42,6 @@ class SocketServer {
this._packagerServer = new Server(options);
this._jobs = 0;
this._dieEventually();
-
- process.on('exit', () => fs.unlinkSync(sockPath));
}
onReady() {
@@ -72,6 +71,11 @@ class SocketServer {
debug('request error', error);
this._jobs--;
this._reply(sock, m.id, 'error', error.stack);
+
+ // Fatal error from JSTransformer transform workers.
+ if (error.type === 'ProcessTerminatedError') {
+ setImmediate(() => process.exit(1));
+ }
};
switch (m.type) {
@@ -138,12 +142,17 @@ class SocketServer {
process.send({ type: 'createdServer' });
},
error => {
- debug('error creating server', error.code);
if (error.code === 'EADDRINUSE') {
// Server already listening, this may happen if multiple
// clients where started in quick succussion (buck).
process.send({ type: 'createdServer' });
+
+ // Kill this server because some other server with the same
+ // config and socket already started.
+ debug('server already started');
+ setImmediate(() => process.exit());
} else {
+ debug('error creating server', error.code);
throw error;
}
}
diff --git a/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js b/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js
index c898e9272..59477fcf1 100644
--- a/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js
+++ b/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js
@@ -107,6 +107,6 @@ describe('SocketClient', () => {
data: 'some error'
});
- return promise.catch(m => expect(m.message).toBe('some error'));
+ return promise.catch(m => expect(m.message).toContain('some error'));
});
});
diff --git a/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js b/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js
index c03f39992..d43d35861 100644
--- a/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js
+++ b/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js
@@ -26,6 +26,17 @@ describe('SocketInterface', () => {
pit('creates socket path by hashing options', () => {
const fs = require('fs');
fs.existsSync = jest.genMockFn().mockImpl(() => true);
+ fs.unlinkSync = jest.genMockFn();
+ let callback;
+
+ require('child_process').spawn.mockImpl(() => ({
+ on: (event, cb) => callback = cb,
+ send: (message) => {
+ setImmediate(() => callback({ type: 'createdServer' }));
+ },
+ unref: () => undefined,
+ disconnect: () => undefined,
+ }));
// Check that given two equivelant server options, we end up with the same
// socket path.
@@ -49,6 +60,7 @@ describe('SocketInterface', () => {
pit('should fork a server', () => {
const fs = require('fs');
fs.existsSync = jest.genMockFn().mockImpl(() => false);
+ fs.unlinkSync = jest.genMockFn();
let sockPath;
let callback;
diff --git a/packager/react-packager/src/SocketInterface/index.js b/packager/react-packager/src/SocketInterface/index.js
index 470d0fc24..19b39bf09 100644
--- a/packager/react-packager/src/SocketInterface/index.js
+++ b/packager/react-packager/src/SocketInterface/index.js
@@ -13,12 +13,14 @@ const SocketClient = require('./SocketClient');
const SocketServer = require('./SocketServer');
const _ = require('underscore');
const crypto = require('crypto');
+const debug = require('debug')('ReactPackager:SocketInterface');
const fs = require('fs');
+const net = require('net');
const path = require('path');
const tmpdir = require('os').tmpdir();
const {spawn} = require('child_process');
-const CREATE_SERVER_TIMEOUT = 30000;
+const CREATE_SERVER_TIMEOUT = 60000;
const SocketInterface = {
getOrCreateSocketFor(options) {
@@ -38,66 +40,81 @@ const SocketInterface = {
);
if (fs.existsSync(sockPath)) {
- resolve(SocketClient.create(sockPath));
- return;
- }
-
- const logPath = path.join(tmpdir, 'react-packager.log');
-
- const timeout = setTimeout(
- () => reject(
- new Error(
- 'Took too long to start server. Server logs: \n' +
- fs.readFileSync(logPath, 'utf8')
- )
- ),
- CREATE_SERVER_TIMEOUT,
- );
-
- const log = fs.openSync(logPath, 'a');
-
- // Enable server debugging by default since it's going to a log file.
- const env = _.clone(process.env);
- env.DEBUG = 'ReactPackager:SocketServer';
-
- // We have to go through the main entry point to make sure
- // we go through the babel require hook.
- const child = spawn(
- process.execPath,
- [path.join(__dirname, '..', '..', 'index.js')],
- {
- detached: true,
- env: env,
- stdio: ['ipc', log, log]
- }
- );
-
- child.unref();
-
- child.on('message', m => {
- if (m && m.type && m.type === 'createdServer') {
- clearTimeout(timeout);
- child.disconnect();
+ var sock = net.connect(sockPath);
+ sock.on('connect', () => {
+ sock.end();
resolve(SocketClient.create(sockPath));
- }
- });
-
-
- if (options.blacklistRE) {
- options.blacklistRE = { source: options.blacklistRE.source };
+ });
+ sock.on('error', (e) => {
+ try {
+ debug('deleting socket for not responding', sockPath);
+ fs.unlinkSync(sockPath);
+ } catch (err) {
+ // Another client might have deleted it first.
+ }
+ createServer(resolve, reject, options, sockPath);
+ });
+ } else {
+ createServer(resolve, reject, options, sockPath);
}
-
- child.send({
- type: 'createSocketServer',
- data: { sockPath, options }
- });
});
},
listenOnServerMessages() {
return SocketServer.listenOnServerIPCMessages();
}
-
};
+function createServer(resolve, reject, options, sockPath) {
+ const logPath = path.join(tmpdir, 'react-packager.log');
+
+ const timeout = setTimeout(
+ () => reject(
+ new Error(
+ 'Took too long to start server. Server logs: \n' +
+ fs.readFileSync(logPath, 'utf8')
+ )
+ ),
+ CREATE_SERVER_TIMEOUT,
+ );
+
+ const log = fs.openSync(logPath, 'a');
+
+ // Enable server debugging by default since it's going to a log file.
+ const env = _.clone(process.env);
+ env.DEBUG = 'ReactPackager:SocketServer';
+
+ // We have to go through the main entry point to make sure
+ // we go through the babel require hook.
+ const child = spawn(
+ process.execPath,
+ [path.join(__dirname, '..', '..', 'index.js')],
+ {
+ detached: true,
+ env: env,
+ stdio: ['ipc', log, log]
+ }
+ );
+
+ child.unref();
+
+ child.on('message', m => {
+ if (m && m.type && m.type === 'createdServer') {
+ clearTimeout(timeout);
+ child.disconnect();
+
+ resolve(SocketClient.create(sockPath));
+ }
+ });
+
+ if (options.blacklistRE) {
+ options.blacklistRE = { source: options.blacklistRE.source };
+ }
+
+ child.send({
+ type: 'createSocketServer',
+ data: { sockPath, options }
+ });
+}
+
module.exports = SocketInterface;
diff --git a/packager/react-packager/src/lib/getCacheFilePath.js b/packager/react-packager/src/lib/getCacheFilePath.js
new file mode 100644
index 000000000..d5ef19b31
--- /dev/null
+++ b/packager/react-packager/src/lib/getCacheFilePath.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+'use strict';
+
+const crypto = require('crypto');
+const path = require('path');
+const tmpdir = require('os').tmpDir();
+
+function getCacheFilePath(args) {
+ args = Array.prototype.slice.call(args);
+ const prefix = args.shift();
+
+ let hash = crypto.createHash('md5');
+ args.forEach(arg => hash.update(arg));
+
+ return path.join(tmpdir, prefix + hash.digest('hex'));
+}
+
+module.exports = getCacheFilePath;
diff --git a/packager/react-packager/src/lib/loadCacheSync.js b/packager/react-packager/src/lib/loadCacheSync.js
new file mode 100644
index 000000000..64188365c
--- /dev/null
+++ b/packager/react-packager/src/lib/loadCacheSync.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+'use strict';
+
+const fs = require('fs');
+
+function loadCacheSync(cachePath) {
+ if (!fs.existsSync(cachePath)) {
+ return Object.create(null);
+ }
+
+ try {
+ return JSON.parse(fs.readFileSync(cachePath));
+ } catch (e) {
+ if (e instanceof SyntaxError) {
+ console.warn('Unable to parse cache file. Will clear and continue.');
+ fs.unlinkSync(cachePath);
+ return Object.create(null);
+ }
+ throw e;
+ }
+}
+
+module.exports = loadCacheSync;
diff --git a/packager/react-packager/src/transforms/babel-plugin-system-import/__tests__/babel-plugin-system-import-test.js b/packager/react-packager/src/transforms/babel-plugin-system-import/__tests__/babel-plugin-system-import-test.js
new file mode 100644
index 000000000..ddf8f2403
--- /dev/null
+++ b/packager/react-packager/src/transforms/babel-plugin-system-import/__tests__/babel-plugin-system-import-test.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright 2004-present Facebook. All Rights Reserved.
+ *
+ * @emails oncall+jsinfra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+jest.mock('../../../BundlesLayout');
+
+const babel = require('babel-core');
+const BundlesLayout = require('../../../BundlesLayout');
+
+const testData = {
+ isolated: {
+ input: 'System.import("moduleA");',
+ output: 'loadBundles(["bundle.0"]);'
+ },
+ single: {
+ input: 'System.import("moduleA").then(function (bundleA) {});',
+ output: 'loadBundles(["bundle.0"]).then(function (bundleA) {});'
+ },
+ multiple: {
+ input: [
+ 'Promise.all([',
+ 'System.import("moduleA"), System.import("moduleB"),',
+ ']).then(function (bundlesA, bundlesB) {});',
+ ].join('\n'),
+ output: [
+ 'Promise.all([',
+ 'loadBundles(["bundle.0"]), loadBundles(["bundle.1"])',
+ ']).then(function (bundlesA, bundlesB) {});',
+ ].join(''),
+ },
+};
+
+describe('System.import', () => {
+ let layout = new BundlesLayout();
+ BundlesLayout.prototype.getBundleIDForModule.mockImpl(module => {
+ switch (module) {
+ case 'moduleA': return 'bundle.0';
+ case 'moduleB': return 'bundle.1';
+ }
+ });
+
+ function transform(source) {
+ return babel.transform(source, {
+ plugins: [require('../')],
+ blacklist: ['strict'],
+ extra: { bundlesLayout: layout },
+ }).code;
+ }
+
+ function test(data) {
+ // transform and remove new lines
+ expect(transform(data.input).replace(/(\r\n|\n|\r)/gm,'')).toEqual(data.output);
+ }
+
+ it('should transform isolated `System.import`', () => {
+ test(testData.isolated);
+ });
+
+ it('should transform single `System.import`', () => {
+ test(testData.single);
+ });
+
+ it('should transform multiple `System.import`s', () => {
+ test(testData.multiple);
+ });
+});
diff --git a/packager/react-packager/src/transforms/babel-plugin-system-import/index.js b/packager/react-packager/src/transforms/babel-plugin-system-import/index.js
new file mode 100644
index 000000000..21ef64c0c
--- /dev/null
+++ b/packager/react-packager/src/transforms/babel-plugin-system-import/index.js
@@ -0,0 +1,65 @@
+ /**
+ * Copyright (c) 2015-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ */
+/*jslint node: true */
+'use strict';
+
+var t = require('babel-core').types;
+
+/**
+ * Transforms asynchronous module importing into a function call
+ * that includes which bundle needs to be loaded
+ *
+ * Transforms:
+ *
+ * System.import('moduleA')
+ *
+ * to:
+ *
+ * loadBundles('bundleA')
+ */
+module.exports = function systemImportTransform(babel) {
+ return new babel.Transformer('system-import', {
+ CallExpression: function(node, parent, scope, state) {
+ if (!isAppropriateSystemImportCall(node, parent)) {
+ return node;
+ }
+
+ var bundlesLayout = state.opts.extra.bundlesLayout;
+ var bundleID = bundlesLayout.getBundleIDForModule(
+ node.arguments[0].value
+ );
+
+ var bundles = bundleID.split('.');
+ bundles.splice(0, 1);
+ bundles = bundles.map(function(id) {
+ return t.literal('bundle.' + id);
+ });
+
+ return t.callExpression(
+ t.identifier('loadBundles'),
+ [t.arrayExpression(bundles)]
+ );
+ },
+
+ metadata: {
+ group: 'fb'
+ }
+ });
+};
+
+function isAppropriateSystemImportCall(node) {
+ return (
+ node.callee.type === 'MemberExpression' &&
+ node.callee.object.name === 'System' &&
+ node.callee.property.name === 'import' &&
+ node.arguments.length === 1 &&
+ node.arguments[0].type === 'Literal'
+ );
+}