diff --git a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj index 2ea052a2e3..6f0493e20b 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; @@ -80,6 +81,13 @@ remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = React; }; + 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTLinking; + }; 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; @@ -91,7 +99,6 @@ /* Begin PBXFileReference section */ 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocketDebugger.xcodeproj; path = ../../Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj; sourceTree = ""; }; - 00481BE91AC0C89D00671115 /* libicucore.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libicucore.dylib; path = usr/lib/libicucore.dylib; sourceTree = SDKROOT; }; 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = main.jsbundle; path = iOS/main.jsbundle; sourceTree = ""; }; 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; 00C302AF1ABCB8E700DB3ED1 /* RCTAdSupport.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAdSupport.xcodeproj; path = ../../Libraries/AdSupport/RCTAdSupport.xcodeproj; sourceTree = ""; }; @@ -107,6 +114,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = iOS/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = iOS/main.m; sourceTree = ""; }; 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../../React/React.xcodeproj; sourceTree = ""; }; + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../../Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ @@ -121,6 +129,7 @@ 00C302E61ABCBA2D00DB3ED1 /* libRCTAdSupport.a in Frameworks */, 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, @@ -208,9 +217,18 @@ name = Products; sourceTree = ""; }; + 78C398B11ACF4ADC00677621 /* Products */ = { + isa = PBXGroup; + children = ( + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, + ); + name = Products; + sourceTree = ""; + }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, 146833FF1AC3E56700842450 /* React.xcodeproj */, 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, @@ -220,7 +238,6 @@ 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */, - 00481BE91AC0C89D00671115 /* libicucore.dylib */, ); name = Libraries; sourceTree = ""; @@ -309,6 +326,10 @@ ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */; ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; }, + { + ProductGroup = 78C398B11ACF4ADC00677621 /* Products */; + ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + }, { ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; @@ -394,6 +415,13 @@ remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTLinking.a; + remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index 4e54c38b82..b033c7d3ba 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -251,7 +251,7 @@ exports.examples = [ return ( - + ); diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index aa5039edbf..fd90847f05 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -50,6 +50,7 @@ var RCTNavigatorItem = createReactIOSNativeComponentClass({ rightButtonTitle: true, onNavRightButtonTap: true, tintColor: true, + navigationBarHidden: true, backButtonTitle: true, titleTextColor: true, style: true, @@ -235,6 +236,11 @@ var NavigatorIOS = React.createClass({ }).isRequired, + /** + * A Boolean value that indicates whether the navigation bar is hidden + */ + navigationBarHidden: PropTypes.bool, + /** * The default wrapper style for components in the navigator. * A common use case is to set the backgroundColor for every page @@ -547,6 +553,7 @@ var NavigatorIOS = React.createClass({ backButtonTitle={route.backButtonTitle} rightButtonTitle={route.rightButtonTitle} onNavRightButtonTap={route.onRightButtonPress} + navigationBarHidden={this.props.navigationBarHidden} tintColor={this.props.tintColor}> ; diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index 82d6f427c5..bfc823f9f8 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -34,8 +34,8 @@ var WebViewState = keyMirror({ var WebView = React.createClass({ propTypes: { - renderError: PropTypes.func.isRequired, // view to show if there's an error - renderLoading: PropTypes.func.isRequired, // loading indicator to show + renderError: PropTypes.func, // view to show if there's an error + renderLoading: PropTypes.func, // loading indicator to show url: PropTypes.string.isRequired, automaticallyAdjustContentInsets: PropTypes.bool, contentInset: EdgeInsetsPropType, @@ -66,10 +66,10 @@ var WebView = React.createClass({ var otherView = null; if (this.state.viewState === WebViewState.LOADING) { - otherView = this.props.renderLoading(); + otherView = this.props.renderLoading && this.props.renderLoading(); } else if (this.state.viewState === WebViewState.ERROR) { var errorEvent = this.state.lastErrorEvent; - otherView = this.props.renderError( + otherView = this.props.renderError && this.props.renderError( errorEvent.domain, errorEvent.code, errorEvent.description); diff --git a/Libraries/LinkingIOS/RCTLinkingManager.h b/Libraries/LinkingIOS/RCTLinkingManager.h index 98e40beeed..81680f056a 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.h +++ b/Libraries/LinkingIOS/RCTLinkingManager.h @@ -13,6 +13,9 @@ @interface RCTLinkingManager : NSObject -+ (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation; ++ (BOOL)application:(UIApplication *)application + openURL:(NSURL *)URL + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation; @end diff --git a/Libraries/LinkingIOS/RCTLinkingManager.m b/Libraries/LinkingIOS/RCTLinkingManager.m index 33fac75f06..990c5900fe 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.m +++ b/Libraries/LinkingIOS/RCTLinkingManager.m @@ -37,11 +37,11 @@ RCT_EXPORT_MODULE() } + (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url + openURL:(NSURL *)URL sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - NSDictionary *payload = @{@"url": [url absoluteString]}; + NSDictionary *payload = @{@"url": [URL absoluteString]}; [[NSNotificationCenter defaultCenter] postNotificationName:RCTOpenURLNotification object:self userInfo:payload]; @@ -54,23 +54,22 @@ RCT_EXPORT_MODULE() body:[notification userInfo]]; } -RCT_EXPORT_METHOD(openURL:(NSURL *)url) +RCT_EXPORT_METHOD(openURL:(NSURL *)URL) { - [[UIApplication sharedApplication] openURL:url]; + [[UIApplication sharedApplication] openURL:URL]; } -RCT_EXPORT_METHOD(canOpenURL:(NSURL *)url +RCT_EXPORT_METHOD(canOpenURL:(NSURL *)URL callback:(RCTResponseSenderBlock)callback) { - BOOL supported = [[UIApplication sharedApplication] canOpenURL:url]; - callback(@[@(supported)]); + BOOL canOpen = [[UIApplication sharedApplication] canOpenURL:URL]; + callback(@[@(canOpen)]); } - (NSDictionary *)constantsToExport { - return @{ - @"initialURL": [[_bridge.launchOptions objectForKey:UIApplicationLaunchOptionsURLKey] absoluteString] ?: [NSNull null] - }; + NSURL *initialURL = _bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; + return @{@"initialURL": [initialURL absoluteString] ?: [NSNull null]}; } @end diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTDataManager.m index 6aa2842a3b..634d325e91 100644 --- a/Libraries/Network/RCTDataManager.m +++ b/Libraries/Network/RCTDataManager.m @@ -10,6 +10,7 @@ #import "RCTDataManager.h" #import "RCTAssert.h" +#import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" @@ -22,27 +23,18 @@ RCT_EXPORT_MODULE() * The responseSender block won't be called on same thread as called. */ RCT_EXPORT_METHOD(queryData:(NSString *)queryType - withQuery:(id)query + withQuery:(NSDictionary *)query queryHash:(__unused NSString *)queryHash responseSender:(RCTResponseSenderBlock)responseSender) { if ([queryType isEqualToString:@"http"]) { - // Parse query - NSDictionary *queryDict = query; - if ([query isKindOfClass:[NSString class]]) { - // TODO: it would be more efficient just to send a dictionary - queryDict = RCTJSONParse(query, NULL); - } - // Build request - NSURL *url = [NSURL URLWithString:queryDict[@"url"]]; - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; - request.HTTPMethod = queryDict[@"method"] ?: @"GET"; - request.allHTTPHeaderFields = queryDict[@"headers"]; - if ([queryDict[@"data"] isKindOfClass:[NSString class]]) { - request.HTTPBody = [queryDict[@"data"] dataUsingEncoding:NSUTF8StringEncoding]; - } + NSURL *URL = [RCTConvert NSURL:query[@"url"]]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; + request.HTTPMethod = [RCTConvert NSString:query[@"method"]] ?: @"GET"; + request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]]; + request.HTTPBody = [RCTConvert NSData:query[@"data"]]; // Build data task NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *connectionError) { @@ -50,18 +42,27 @@ RCT_EXPORT_METHOD(queryData:(NSString *)queryType // Build response NSDictionary *responseJSON; if (connectionError == nil) { - NSStringEncoding encoding; + NSStringEncoding encoding = NSUTF8StringEncoding; if (response.textEncodingName) { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); - } else { - encoding = NSUTF8StringEncoding; } - int responseCode = (int)[((NSHTTPURLResponse *)response) statusCode]; - NSString *returnData = [[NSString alloc] initWithData:data encoding:encoding]; - responseJSON = @{@"status": @(responseCode), @"responseText": returnData}; + NSHTTPURLResponse *httpResponse = nil; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + // Might be a local file request + httpResponse = (NSHTTPURLResponse *)response; + } + responseJSON = @{ + @"status": @([httpResponse statusCode] ?: 200), + @"responseHeaders": [httpResponse allHeaderFields] ?: @{}, + @"responseText": [[NSString alloc] initWithData:data encoding:encoding] ?: @"" + }; } else { - responseJSON = @{@"status": @0, @"responseText": [connectionError localizedDescription]}; + responseJSON = @{ + @"status": @0, + @"responseHeaders": @{}, + @"responseText": [connectionError localizedDescription] + }; } // Send response (won't be sent on same thread as caller) diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index 2287ee7b27..6c7367c18d 100644 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -22,12 +22,13 @@ class XMLHttpRequest extends XMLHttpRequestBase { sendImpl(method: ?string, url: ?string, headers: Object, data: any): void { RCTDataManager.queryData( 'http', - JSON.stringify({ + { method: method, url: url, data: data, headers: headers, - }), + }, + // TODO: Do we need this? is it used anywhere? 'h' + crc32(method + '|' + url + '|' + data), (result) => { result = JSON.parse(result); diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 90f2d87862..17ceb204cf 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -85,51 +85,31 @@ RCT_EXPORT_METHOD(getApplicationIconBadgeNumber:(RCTResponseSenderBlock)callback RCT_EXPORT_METHOD(requestPermissions) { - Class _UIUserNotificationSettings; - if ((_UIUserNotificationSettings = NSClassFromString(@"UIUserNotificationSettings"))) { - UIUserNotificationType types = UIUserNotificationTypeSound | UIUserNotificationTypeBadge | UIUserNotificationTypeAlert; - UIUserNotificationSettings *notificationSettings = [_UIUserNotificationSettings settingsForTypes:types categories:nil]; - [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; - } else { - #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 + // if we are targeting iOS 7, *and* the new UIUserNotificationSettings + // class is not available, then register using the old mechanism + if (![UIUserNotificationSettings class]) { [[UIApplication sharedApplication] registerForRemoteNotificationTypes: UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert]; + return; + } #endif - } + UIUserNotificationType types = UIUserNotificationTypeSound | UIUserNotificationTypeBadge | UIUserNotificationTypeAlert; + UIUserNotificationSettings *notificationSettings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; + [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; } RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback) { - -#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 - -#define UIUserNotificationTypeAlert UIRemoteNotificationTypeAlert -#define UIUserNotificationTypeBadge UIRemoteNotificationTypeBadge -#define UIUserNotificationTypeSound UIRemoteNotificationTypeSound - -#endif - - NSUInteger types; - if ([UIApplication instancesRespondToSelector:@selector(currentUserNotificationSettings)]) { - types = [[[UIApplication sharedApplication] currentUserNotificationSettings] types]; - } else { - -#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 - - types = [[UIApplication sharedApplication] enabledRemoteNotificationTypes]; - -#endif - - } - NSMutableDictionary *permissions = [[NSMutableDictionary alloc] init]; - permissions[@"alert"] = @((types & UIUserNotificationTypeAlert) > 0); - permissions[@"badge"] = @((types & UIUserNotificationTypeBadge) > 0); - permissions[@"sound"] = @((types & UIUserNotificationTypeSound) > 0); + + UIUserNotificationType types = [[[UIApplication sharedApplication] currentUserNotificationSettings] types]; + permissions[@"alert"] = @((BOOL)(types & UIUserNotificationTypeAlert)); + permissions[@"badge"] = @((BOOL)(types & UIUserNotificationTypeBadge)); + permissions[@"sound"] = @((BOOL)(types & UIUserNotificationTypeSound)); callback(@[permissions]); } diff --git a/Libraries/StyleSheet/StyleSheet.js b/Libraries/StyleSheet/StyleSheet.js index 6a8ccb60da..796470ced2 100644 --- a/Libraries/StyleSheet/StyleSheet.js +++ b/Libraries/StyleSheet/StyleSheet.js @@ -47,7 +47,7 @@ var StyleSheetValidation = require('StyleSheetValidation'); * Code quality: * * - By moving styles away from the render function, you're making the code - * code easier to understand. + * easier to understand. * - Naming the styles is a good way to add meaning to the low level components * in the render function. * diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index c48afa5656..b9cee66c31 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -49,6 +49,9 @@ typedef struct section RCTHeaderSection; #define RCTGetSectByNameFromHeader getsectbynamefromheader #endif +NSString *const RCTEnqueueNotification = @"RCTEnqueueNotification"; +NSString *const RCTDequeueNotification = @"RCTDequeueNotification"; + /** * This function returns the module name for a given class. */ @@ -926,10 +929,10 @@ static id _latestJSExecutor; - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args { - [[NSNotificationCenter defaultCenter] postNotificationName:@"JS_PERF_ENQUEUE" object:nil userInfo:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil]; RCTJavaScriptCallback processResponse = ^(id json, NSError *error) { - [[NSNotificationCenter defaultCenter] postNotificationName:@"JS_PERF_DEQUEUE" object:nil userInfo:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTDequeueNotification object:nil userInfo:nil]; [self _handleBuffer:json]; }; @@ -1038,6 +1041,9 @@ static id _latestJSExecutor; } @catch (NSException *exception) { RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); + if ([exception.name rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { + @throw; + } } }); diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index ff5fb970bb..2c1c84ee89 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -38,6 +38,7 @@ + (NSDictionary *)NSDictionary:(id)json; + (NSString *)NSString:(id)json; + (NSNumber *)NSNumber:(id)json; ++ (NSData *)NSData:(id)json; + (NSURL *)NSURL:(id)json; + (NSURLRequest *)NSURLRequest:(id)json; diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index a46379ae80..6fa1c2227b 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -50,6 +50,12 @@ RCT_CONVERTER(NSString *, NSString, description) return nil; } ++ (NSData *)NSData:(id)json +{ + // TODO: should we automatically decode base64 data? Probably not... + return [[self NSString:json] dataUsingEncoding:NSUTF8StringEncoding]; +} + + (NSURL *)NSURL:(id)json { if (![json isKindOfClass:[NSString class]]) { diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index c82eca01c6..fb4e02eae3 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -97,6 +97,12 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveEvent); @"x": @(scrollView.contentOffset.x), @"y": @(scrollView.contentOffset.y) }, + @"contentInset": @{ + @"top": @(scrollView.contentInset.top), + @"left": @(scrollView.contentInset.left), + @"bottom": @(scrollView.contentInset.bottom), + @"right": @(scrollView.contentInset.right) + }, @"contentSize": @{ @"width": @(scrollView.contentSize.width), @"height": @(scrollView.contentSize.height) diff --git a/React/Modules/RCTExceptionsManager.h b/React/Modules/RCTExceptionsManager.h index ddbb44869f..25e0fbefce 100644 --- a/React/Modules/RCTExceptionsManager.h +++ b/React/Modules/RCTExceptionsManager.h @@ -21,4 +21,6 @@ - (instancetype)initWithDelegate:(id)delegate NS_DESIGNATED_INITIALIZER; +@property (nonatomic, assign) NSUInteger maxReloadAttempts; + @end diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index 1ce5c73833..5be80133bc 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -9,19 +9,27 @@ #import "RCTExceptionsManager.h" +#import "RCTLog.h" #import "RCTRedBox.h" +#import "RCTRootView.h" @implementation RCTExceptionsManager { __weak id _delegate; + NSUInteger _reloadRetries; } +#ifndef DEBUG +static NSUInteger RCTReloadRetries = 0; +#endif + RCT_EXPORT_MODULE() - (instancetype)initWithDelegate:(id)delegate { if ((self = [super init])) { _delegate = delegate; + _maxReloadAttempts = 0; } return self; } @@ -36,9 +44,40 @@ RCT_EXPORT_METHOD(reportUnhandledException:(NSString *)message { if (_delegate) { [_delegate unhandledJSExceptionWithMessage:message stack:stack]; - } else { - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; + return; } + +#ifdef DEBUG + [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; +#else + if (RCTReloadRetries < _maxReloadAttempts) { + RCTReloadRetries++; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; + }); + } else { + NSError *error; + const NSUInteger MAX_SANITIZED_LENGTH = 75; + // Filter out numbers so the same base errors are mapped to the same categories independent of incorrect values. + NSString *pattern = @"[+-]?\\d+[,.]?\\d*"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:&error]; + RCTAssert(error == nil, @"Bad regex pattern: %@", pattern); + NSString *sanitizedMessage = [regex stringByReplacingMatchesInString:message + options:0 + range:NSMakeRange(0, message.length) + withTemplate:@""]; + if (sanitizedMessage.length > MAX_SANITIZED_LENGTH) { + sanitizedMessage = [[sanitizedMessage substringToIndex:MAX_SANITIZED_LENGTH] stringByAppendingString:@"..."]; + } + NSMutableString *prettyStack = [@"\n" mutableCopy]; + for (NSDictionary *frame in stack) { + [prettyStack appendFormat:@"%@@%@:%@\n", frame[@"methodName"], frame[@"lineNumber"], frame[@"column"]]; + } + + NSString *name = [@"Unhandled JS Exception: " stringByAppendingString:sanitizedMessage]; + [NSException raise:name format:@"Message: %@, stack: %@", message, prettyStack]; + } +#endif } RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message diff --git a/React/Views/RCTNavItem.h b/React/Views/RCTNavItem.h index 6db429db27..5ae874522e 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -14,6 +14,7 @@ @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *rightButtonTitle; @property (nonatomic, copy) NSString *backButtonTitle; +@property (nonatomic, assign) BOOL navigationBarHidden; @property (nonatomic, copy) UIColor *tintColor; @property (nonatomic, copy) UIColor *barTintColor; @property (nonatomic, copy) UIColor *titleTextColor; diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index b6d38ac007..fc601632f4 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -24,6 +24,7 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(title, NSString) RCT_EXPORT_VIEW_PROPERTY(rightButtonTitle, NSString); RCT_EXPORT_VIEW_PROPERTY(backButtonTitle, NSString); +RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL); RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(titleTextColor, UIColor); diff --git a/React/Views/RCTTextField.m b/React/Views/RCTTextField.m index 0911e069ff..077e75c5d7 100644 --- a/React/Views/RCTTextField.m +++ b/React/Views/RCTTextField.m @@ -35,6 +35,13 @@ return self; } +- (void)setText:(NSString *)text +{ + if (![text isEqualToString:self.text]) { + [super setText:text]; + } +} + - (NSArray *)reactSubviews { // TODO: do we support subviews of textfield in React? diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index 2008c1f83d..f9f3516344 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -65,7 +65,10 @@ if ([self.parentViewController isKindOfClass:[UINavigationController class]]) { - [self.navigationController setNavigationBarHidden:!_navItem animated:animated]; + [self.navigationController + setNavigationBarHidden:_navItem.navigationBarHidden + animated:animated]; + if (!_navItem) { return; } diff --git a/package.json b/package.json index 83fd8ad41e..f372f7882a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native", - "version": "0.3.9", + "version": "0.3.7", "description": "A framework for building native apps using React", "repository": { "type": "git", diff --git a/packager/getFlowTypeCheckMiddleware.js b/packager/getFlowTypeCheckMiddleware.js index b1f6f7dd5b..312da45e21 100644 --- a/packager/getFlowTypeCheckMiddleware.js +++ b/packager/getFlowTypeCheckMiddleware.js @@ -8,8 +8,11 @@ */ 'use strict'; +var chalk = require('chalk'); var exec = require('child_process').exec; +var hasWarned = {}; + function getFlowTypeCheckMiddleware(options) { return function(req, res, next) { if (options.skipflow) { @@ -18,13 +21,19 @@ function getFlowTypeCheckMiddleware(options) { if (options.flowroot || options.projectRoots.length === 1) { var flowroot = options.flowroot || options.projectRoots[0]; } else { - console.warn('flow: No suitable root'); + if (!hasWarned.noRoot) { + hasWarned.noRoot = true; + console.warn('flow: No suitable root'); + } return next(); } exec('command -v flow >/dev/null 2>&1', function(error, stdout) { if (error) { - console.warn('flow: Skipping because not installed. Install with ' + - '`brew install flow`.'); + if (!hasWarned.noFlow) { + hasWarned.noFlow = true; + console.warn(chalk.yellow('flow: Skipping because not installed. Install with ' + + '`brew install flow`.')); + } return next(); } else { return doFlowTypecheck(res, flowroot, next); @@ -34,16 +43,21 @@ function getFlowTypeCheckMiddleware(options) { } function doFlowTypecheck(res, flowroot, next) { - // vjeux: big hack to make it work on the sample app because we don't generate a - // .flowconfig in the init script right now. - return next(); - var flowCmd = 'cd "' + flowroot + '" && flow --json --timeout 20'; var start = Date.now(); - console.log('flow: Running static typechecks.'); - exec(flowCmd, function(flowError, stdout) { + // Log start message if flow is slow to let user know something is happening. + var flowSlow = setTimeout( + function() { + console.log(chalk.gray('flow: Running static typechecks.')); + }, + 500 + ); + exec(flowCmd, function(flowError, stdout, stderr) { + clearTimeout(flowSlow); if (!flowError) { - console.log('flow: Typechecks passed (' + (Date.now() - start) + 'ms).'); + console.log(chalk.gray( + 'flow: Typechecks passed (' + (Date.now() - start) + 'ms).') + ); return next(); } else { try { @@ -65,24 +79,38 @@ function doFlowTypecheck(res, flowroot, next) { }); errorNum++; }); - var message = 'Flow found type errors. If you think these are wrong, ' + - 'make sure flow is up to date, or disable with --skipflow.'; + var error = { + status: 500, + message: 'Flow found type errors. If you think these are wrong, ' + + 'make sure your flow bin and .flowconfig are up to date, or ' + + 'disable with --skipflow.', + type: 'FlowError', + errors: errors, + }; + console.error(chalk.yellow('flow: Error running command `' + flowCmd + + '`:\n' + JSON.stringify(error)) + ); + res.writeHead(error.status, { + 'Content-Type': 'application/json; charset=UTF-8', + }); + res.end(JSON.stringify(error)); } catch (e) { - var message = - 'Flow failed to provide parseable output:\n\n`' + stdout + '`'; - console.error(message, '\nException: `', e, '`\n\n'); + if (stderr.match(/Could not find a \.flowconfig/)) { + if (!hasWarned.noConfig) { + hasWarned.noConfig = true; + console.warn(chalk.yellow('flow: ' + stderr)); + } + } else { + if (!hasWarned.brokenFlow) { + hasWarned.brokenFlow = true; + console.warn(chalk.yellow( + 'Flow failed to provide parseable output:\n\n`' + stdout + + '`.\n' + 'stderr: `' + stderr + '`' + )); + } + } + return next(); } - var error = { - status: 500, - message: message, - type: 'FlowError', - errors: errors, - }; - console.error('flow: Error running command `' + flowCmd + '`:\n', error); - res.writeHead(error.status, { - 'Content-Type': 'application/json; charset=UTF-8', - }); - res.end(JSON.stringify(error)); } }); } diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js index f42f6f8a1b..25343fdc65 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -64,6 +64,42 @@ describe('DependencyGraph', function() { }); }); + pit('should get dependencies with the correct extensions', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")' + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + 'a.js.orig': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['a']}, + {id: 'a', altId: '/root/a.js', path: '/root/a.js', dependencies: []}, + ]); + }); + }); + pit('should get dependencies with deprecated assets', function() { var root = '/root'; fs.__setMockFilesystem({ @@ -674,6 +710,296 @@ describe('DependencyGraph', function() { ]); }); }); + + pit('should support simple browser field in packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + browser: 'client.js', + }), + 'main.js': 'some other code', + 'client.js': 'some code', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage/client', + path: '/root/aPackage/client.js', + dependencies: [] + }, + ]); + }); + }); + + pit('should supportbrowser field in packages w/o .js ext', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + browser: 'client', + }), + 'main.js': 'some other code', + 'client.js': 'some code', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage/client', + path: '/root/aPackage/client.js', + dependencies: [] + }, + ]); + }); + }); + + pit('should support mapping main in browser field json', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: './main.js', + browser: { + './main.js': './client.js', + }, + }), + 'main.js': 'some other code', + 'client.js': 'some code', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage/client', + path: '/root/aPackage/client.js', + dependencies: [] + }, + ]); + }); + }); + + pit('should work do correct browser mapping w/o js ext', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: './main.js', + browser: { + './main': './client.js', + }, + }), + 'main.js': 'some other code', + 'client.js': 'some code', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage/client', + path: '/root/aPackage/client.js', + dependencies: [] + }, + ]); + }); + }); + + pit('should support browser mapping of files', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: './main.js', + browser: { + './main': './client.js', + './node.js': './not-node.js', + './not-browser': './browser.js', + './dir/server.js': './dir/client', + }, + }), + 'main.js': 'some other code', + 'client.js': 'require("./node")\nrequire("./dir/server.js")', + 'not-node.js': 'require("./not-browser")', + 'not-browser.js': 'require("./dir/server")', + 'browser.js': 'some browser code', + 'dir': { + 'server.js': 'some node code', + 'client.js': 'some browser code', + } + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage/client', + path: '/root/aPackage/client.js', + dependencies: ['./node', './dir/server.js'] + }, + { id: 'aPackage/not-node', + path: '/root/aPackage/not-node.js', + dependencies: ['./not-browser'] + }, + { id: 'aPackage/browser', + path: '/root/aPackage/browser.js', + dependencies: [] + }, + { id: 'aPackage/dir/client', + path: '/root/aPackage/dir/client.js', + dependencies: [] + }, + ]); + }); + }); + + pit('should support browser mapping for packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + browser: { + 'node-package': 'browser-package', + } + }), + 'index.js': 'require("node-package")', + 'node-package': { + 'package.json': JSON.stringify({ + 'name': 'node-package', + }), + 'index.js': 'some node code', + }, + 'browser-package': { + 'package.json': JSON.stringify({ + 'name': 'browser-package', + }), + 'index.js': 'some browser code', + }, + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + { id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'] + }, + { id: 'aPackage/index', + path: '/root/aPackage/index.js', + dependencies: ['node-package'] + }, + { id: 'browser-package/index', + path: '/root/aPackage/browser-package/index.js', + dependencies: [] + }, + ]); + }); + }); }); describe('file watch updating', function() { diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index fbc7de712e..26276f5a45 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -65,7 +65,7 @@ function DependecyGraph(options) { this._debugUpdateEvents = []; this._moduleExtPattern = new RegExp( - '.' + ['js'].concat(this._assetExts).join('|') + '$' + '\.(' + ['js'].concat(this._assetExts).join('|') + ')$' ); // Kick off the search process to precompute the dependency graph. @@ -168,15 +168,22 @@ DependecyGraph.prototype.resolveDependency = function( // Package relative modules starts with '.' or '..'. if (depModuleId[0] !== '.') { - // 1. `depModuleId` is simply a top-level `providesModule`. - // 2. `depModuleId` is a package module but given the full path from the - // package, i.e. package_name/module_name + // Check if we need to map the dependency to something else via the + // `browser` field in package.json + var fromPackageJson = this._lookupPackage(fromModule.path); + if (fromPackageJson && fromPackageJson.browser && + fromPackageJson.browser[depModuleId]) { + depModuleId = fromPackageJson.browser[depModuleId]; + } + + // `depModuleId` is simply a top-level `providesModule`. + // `depModuleId` is a package module but given the full path from the + // package, i.e. package_name/module_name if (this._moduleById[sansExtJs(depModuleId)]) { return this._moduleById[sansExtJs(depModuleId)]; } - // 3. `depModuleId` is a package and it's depending on the "main" - // resolution. + // `depModuleId` is a package and it's depending on the "main" resolution. packageJson = this._packagesById[depModuleId]; // We are being forgiving here and raising an error because we could be @@ -190,7 +197,25 @@ DependecyGraph.prototype.resolveDependency = function( return null; } - var main = packageJson.main || 'index'; + var main; + + // We prioritize the `browser` field if it's a module path. + if (typeof packageJson.browser === 'string') { + main = packageJson.browser; + } else { + main = packageJson.main || 'index'; + } + + // If there is a mapping for main in the `browser` field. + if (packageJson.browser && typeof packageJson.browser === 'object') { + var tmpMain = packageJson.browser[main] || + packageJson.browser[withExtJs(main)] || + packageJson.browser[sansExtJs(main)]; + if (tmpMain) { + main = tmpMain; + } + } + modulePath = withExtJs(path.join(packageJson._root, main)); dep = this._graph[modulePath]; @@ -207,8 +232,7 @@ DependecyGraph.prototype.resolveDependency = function( return dep; } else { - // 4. `depModuleId` is a module defined in a package relative to - // `fromModule`. + // `depModuleId` is a module defined in a package relative to `fromModule`. packageJson = this._lookupPackage(fromModule.path); if (packageJson == null) { @@ -224,12 +248,23 @@ DependecyGraph.prototype.resolveDependency = function( var dir = path.dirname(fromModule.path); modulePath = path.join(dir, depModuleId); + if (packageJson.browser && typeof packageJson.browser === 'object') { + var relPath = './' + path.relative(packageJson._root, modulePath); + var tmpModulePath = packageJson.browser[withExtJs(relPath)] || + packageJson.browser[sansExtJs(relPath)]; + if (tmpModulePath) { + modulePath = path.join(packageJson._root, tmpModulePath); + } + } + + // JS modules can be required without extensios. if (this._assetExts.indexOf(extname(modulePath)) === -1) { modulePath = withExtJs(modulePath); } dep = this._graph[modulePath]; + // Maybe the dependency is a directory and there is an index.js inside it. if (dep == null) { modulePath = path.join(dir, depModuleId, 'index.js'); }