Updates from Mon Aug 31st.

This commit is contained in:
Spencer Ahrens 2015-08-31 16:31:41 -07:00
commit 465f539057
52 changed files with 1050 additions and 544 deletions

View File

@ -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

View File

@ -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:
*/

View File

@ -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 (
<View>
<TouchableHighlight
<TouchableElement
onPress={this.props.onSelect}
onShowUnderlay={this.props.onHighlight}
onHideUnderlay={this.props.onUnhighlight}>
@ -59,7 +65,7 @@ var MovieCell = React.createClass({
</Text>
</View>
</View>
</TouchableHighlight>
</TouchableElement>
</View>
);
}

View File

@ -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 (
<View style={styles.searchBar}>
<TextInput
autoCapitalize="none"
autoCorrect={false}
onChange={this.props.onSearchChange}
placeholder="Search a movie..."
onFocus={this.props.onFocus}
style={styles.searchBarInput}
/>
<ActivityIndicatorIOS
animating={this.props.isLoading}
style={styles.spinner}
/>
</View>
);
}
});
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;

View File

@ -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 <View style={styles.scrollSpinner} />;
}
return <ActivityIndicatorIOS style={styles.scrollSpinner} />;
if (Platform.OS === 'ios') {
return <ActivityIndicatorIOS style={styles.scrollSpinner} />;
} else {
return (
<View style={{alignItems: 'center'}}>
<ProgressBarAndroid styleAttr="Large"/>
</View>
);
}
},
renderSeparator: function(
@ -295,7 +316,8 @@ var SearchScreen = React.createClass({
<SearchBar
onSearchChange={this.onSearchChange}
isLoading={this.state.isLoading}
onFocus={() => this.refs.listview.getScrollResponder().scrollTo(0, 0)}
onFocus={() =>
this.refs.listview && this.refs.listview.getScrollResponder().scrollTo(0, 0)}
/>
<View style={styles.separator} />
{content}
@ -323,27 +345,6 @@ var NoMovies = React.createClass({
}
});
var SearchBar = React.createClass({
render: function() {
return (
<View style={styles.searchBar}>
<TextInput
autoCapitalize="none"
autoCorrect={false}
onChange={this.props.onSearchChange}
placeholder="Search a movie..."
onFocus={this.props.onFocus}
style={styles.searchBarInput}
/>
<ActivityIndicatorIOS
animating={this.props.isLoading}
style={styles.spinner}
/>
</View>
);
}
});
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,
},

View File

@ -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

View File

@ -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:
*/

View File

@ -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;
}

View File

@ -181,8 +181,12 @@ exports.examples = [
render: function() {
if (this.state.showTimer) {
var timer =
<TimerTester ref="interval" dt={25} type="setInterval" />;
var timer = [
<TimerTester ref="interval" dt={25} type="setInterval" />,
<Button onPress={() => this.refs.interval.clear() }>
Clear interval
</Button>
];
var toggleText = 'Unmount timer';
} else {
var timer = null;
@ -191,9 +195,6 @@ exports.examples = [
return (
<View>
{timer}
<Button onPress={() => this.refs.interval.clear() }>
Clear interval
</Button>
<Button onPress={this._toggleTimer}>
{toggleText}
</Button>

View File

@ -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:
*/

View File

@ -57,7 +57,7 @@
expectErrorRegex:@"because shouldThrow"];
}
- (void)testTimers
- (void)DISABLED_testTimers // #8192477
{
[_runner runTest:_cmd module:@"TimersTest"];
}

View File

@ -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<RCTJavaScriptExecutor> weakExecutor;
@autoreleasepool {

View File

@ -75,7 +75,7 @@ var WebViewExample = React.createClass({
<TextInput
ref={TEXT_INPUT_REF}
autoCapitalize="none"
value={this.state.url}
defaultValue={this.state.url}
onSubmitEditing={this.onSubmitEditing}
onChange={this.handleTextInputChange}
clearButtonMode="while-editing"

View File

@ -6,20 +6,7 @@
extern "C" {
JSValueRef nativeProfilerStart(
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception);
JSValueRef nativeProfilerEnd(
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception);
void nativeProfilerStart(JSContextRef ctx, const char *title);
const char *nativeProfilerEnd(JSContextRef ctx, const char *title);
}

View File

@ -7,6 +7,7 @@
#include "OpaqueJSString.h"
#include "JSProfilerPrivate.h"
#include "JSStringRef.h"
#include "String.h"
#include <YAJL/yajl_gen.h>
@ -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<JSC::Profile> rawProfile = profiler->stopProfiling(exec, title->string());
RefPtr<JSC::Profile> 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);
}

View File

@ -110,5 +110,3 @@ armv7:
${HEADER_PATHS} \
-undefined dynamic_lookup \
./JSCLegacyProfiler.mm ./tmp/yajl.a
.PHONY: ios8

View File

@ -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 {

View File

@ -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);

View File

@ -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() {

View File

@ -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,

View File

@ -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<number>,
hash: string,
name: string,
type: string,
};
function registerAsset(asset) {
var assets: Array<PackagerAsset> = [];
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];
}

View File

@ -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,
};

View File

@ -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',
});

View File

@ -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();

View File

@ -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

View File

@ -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;

View File

@ -30,6 +30,7 @@ ReactNativeViewAttributes.UIView = {
onAccessibilityTap: true,
onMagicTap: true,
collapsable: true,
needsOffscreenAlphaCompositing: true,
};
ReactNativeViewAttributes.RCTView = merge(

View File

@ -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 : <callback for ' + module + '.' + method + '>(' + JSON.stringify(args) + ')');
}
}

View File

@ -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'),

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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;
}]];

View File

@ -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

View File

@ -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}));
};

View File

@ -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');

View File

@ -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');
});
});
});

View File

@ -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',

View File

@ -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.

View File

@ -11,7 +11,9 @@
jest
.dontMock('underscore')
.dontMock('absolute-path')
.dontMock('../');
.dontMock('../')
.dontMock('../../lib/loadCacheSync')
.dontMock('../../lib/getCacheFilePath');
jest
.mock('os')

View File

@ -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,
);
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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'));
});
});

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);
});
});

View File

@ -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'
);
}