diff --git a/Examples/2048/2048.xcodeproj/project.pbxproj b/Examples/2048/2048.xcodeproj/project.pbxproj index 43fd7518b..48ebefe41 100644 --- a/Examples/2048/2048.xcodeproj/project.pbxproj +++ b/Examples/2048/2048.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 1461632D1AC3E23900C2F5AD /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1461632C1AC3E22900C2F5AD /* libReact.a */; }; - 58C1E40E1ACF54E9006D1A47 /* libRCTAnimationExperimental.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C1E40B1ACF54B4006D1A47 /* libRCTAnimationExperimental.a */; }; + 5F82F1781B85785200FAE87E /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F82F1771B85784500FAE87E /* libRCTWebSocket.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; /* End PBXBuildFile section */ @@ -24,12 +24,12 @@ remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = React; }; - 58C1E40A1ACF54B4006D1A47 /* PBXContainerItemProxy */ = { + 5F82F1761B85784500FAE87E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 13ACB66C1AC2113500FF4204 /* RCTAnimationExperimental.xcodeproj */; + containerPortal = 5F82F1721B85784500FAE87E /* RCTWebSocket.xcodeproj */; proxyType = 2; - remoteGlobalIDString = 134814201AA4EA6300B7C361; - remoteInfo = RCTAnimationExperimental; + remoteGlobalIDString = 3C86DF461ADF2C930047B81A; + remoteInfo = RCTWebSocket; }; 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -41,7 +41,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 13ACB66C1AC2113500FF4204 /* RCTAnimationExperimental.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimationExperimental.xcodeproj; path = ../../Libraries/Animation/RCTAnimationExperimental.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* 2048.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = 2048.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = 2048/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = 2048/AppDelegate.m; sourceTree = ""; }; @@ -50,6 +49,7 @@ 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = 2048/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = 2048/main.m; sourceTree = ""; }; 146163271AC3E22900C2F5AD /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../../React/React.xcodeproj; sourceTree = ""; }; + 5F82F1721B85784500FAE87E /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = ../../Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ @@ -58,8 +58,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5F82F1781B85785200FAE87E /* libRCTWebSocket.a in Frameworks */, 1461632D1AC3E23900C2F5AD /* libReact.a in Frameworks */, - 58C1E40E1ACF54E9006D1A47 /* libRCTAnimationExperimental.a in Frameworks */, 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -88,10 +88,10 @@ name = Products; sourceTree = ""; }; - 58C1E4071ACF54B4006D1A47 /* Products */ = { + 5F82F1731B85784500FAE87E /* Products */ = { isa = PBXGroup; children = ( - 58C1E40B1ACF54B4006D1A47 /* libRCTAnimationExperimental.a */, + 5F82F1771B85784500FAE87E /* libRCTWebSocket.a */, ); name = Products; sourceTree = ""; @@ -99,9 +99,9 @@ 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( + 5F82F1721B85784500FAE87E /* RCTWebSocket.xcodeproj */, 146163271AC3E22900C2F5AD /* React.xcodeproj */, 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, - 13ACB66C1AC2113500FF4204 /* RCTAnimationExperimental.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -174,14 +174,14 @@ productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; projectDirPath = ""; projectReferences = ( - { - ProductGroup = 58C1E4071ACF54B4006D1A47 /* Products */; - ProjectRef = 13ACB66C1AC2113500FF4204 /* RCTAnimationExperimental.xcodeproj */; - }, { ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; }, + { + ProductGroup = 5F82F1731B85784500FAE87E /* Products */; + ProjectRef = 5F82F1721B85784500FAE87E /* RCTWebSocket.xcodeproj */; + }, { ProductGroup = 146163281AC3E22900C2F5AD /* Products */; ProjectRef = 146163271AC3E22900C2F5AD /* React.xcodeproj */; @@ -202,11 +202,11 @@ remoteRef = 1461632B1AC3E22900C2F5AD /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - 58C1E40B1ACF54B4006D1A47 /* libRCTAnimationExperimental.a */ = { + 5F82F1771B85784500FAE87E /* libRCTWebSocket.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; - path = libRCTAnimationExperimental.a; - remoteRef = 58C1E40A1ACF54B4006D1A47 /* PBXContainerItemProxy */; + path = libRCTWebSocket.a; + remoteRef = 5F82F1761B85784500FAE87E /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { @@ -266,10 +266,7 @@ ); INFOPLIST_FILE = "$(SRCROOT)/2048/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "/Users/sahrens/src/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/Libraries/Animation/build/Debug-iphoneos", - ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = 2048; @@ -287,10 +284,7 @@ ); INFOPLIST_FILE = "$(SRCROOT)/2048/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "/Users/sahrens/src/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/Libraries/Animation/build/Debug-iphoneos", - ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_BUNDLE_IDENTIFIER = "com.facebook.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = 2048; diff --git a/Examples/2048/2048/AppDelegate.m b/Examples/2048/2048/AppDelegate.m index 004e854a7..393ce83cb 100644 --- a/Examples/2048/2048/AppDelegate.m +++ b/Examples/2048/2048/AppDelegate.m @@ -52,6 +52,7 @@ RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"Game2048" + initialProperties:nil launchOptions:launchOptions]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; diff --git a/Examples/Movies/Movies/AppDelegate.m b/Examples/Movies/Movies/AppDelegate.m index 74aed2cc4..719cdca66 100644 --- a/Examples/Movies/Movies/AppDelegate.m +++ b/Examples/Movies/Movies/AppDelegate.m @@ -53,6 +53,7 @@ RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"MoviesApp" + initialProperties:nil launchOptions:launchOptions]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; diff --git a/Examples/SampleApp/_watchmanconfig b/Examples/SampleApp/_watchmanconfig new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Examples/SampleApp/_watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/Examples/SampleApp/iOS/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/iOS/SampleApp.xcodeproj/project.pbxproj index bb15add6f..e0225c7c7 100644 --- a/Examples/SampleApp/iOS/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/iOS/SampleApp.xcodeproj/project.pbxproj @@ -565,6 +565,11 @@ "DEBUG=1", "$(inherited)", ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../../React/**", + ); INFOPLIST_FILE = SampleAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -582,6 +587,11 @@ "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../../React/**", + ); INFOPLIST_FILE = SampleAppTests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; diff --git a/Examples/SampleApp/iOS/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme b/Examples/SampleApp/iOS/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme index f0a3ea9ae..6a3c2997a 100644 --- a/Examples/SampleApp/iOS/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme +++ b/Examples/SampleApp/iOS/SampleApp.xcodeproj/xcshareddata/xcschemes/SampleApp.xcscheme @@ -20,6 +20,20 @@ ReferencedContainer = "container:SampleApp.xcodeproj"> + + + + + + + + #import -#import "RCTAssert.h" -#import "RCTRedBox.h" +#import "RCTLog.h" #import "RCTRootView.h" #define TIMEOUT_SECONDS 240 @@ -23,7 +22,6 @@ @implementation SampleAppTests - - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test { if (test(view)) { @@ -37,18 +35,23 @@ return NO; } -- (void)testRendersWelcomeScreen { +- (void)testRendersWelcomeScreen +{ UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; BOOL foundElement = NO; - NSString *redboxError = nil; + + __block NSString *redboxError = nil; + RCTSetLogFunction(^(RCTLogLevel level, NSString *fileName, NSNumber *lineNumber, NSString *message) { + if (level >= RCTLogLevelError) { + redboxError = message; + } + }); while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; - foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { return YES; @@ -57,6 +60,8 @@ }]; } + RCTSetLogFunction(RCTDefaultLogFunction); + XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); } diff --git a/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj b/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj index 61e817e2b..ec0802a83 100644 --- a/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj +++ b/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 144C5F691AC3E5E300B004E7 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 144C5F681AC3E5D800B004E7 /* libReact.a */; }; 58C572511AA6229D00CDF9C8 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C5724D1AA6224400CDF9C8 /* libRCTText.a */; }; + 5FF8942E1B85571A007731BE /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5FF8942D1B8556F8007731BE /* libRCTWebSocket.a */; }; 832044981B492C2500E297FC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832044951B492C1E00E297FC /* libRCTSettings.a */; }; /* End PBXBuildFile section */ @@ -39,6 +40,13 @@ remoteGlobalIDString = 58B5119B1A9E6C1200147676; remoteInfo = RCTText; }; + 5FF8942C1B8556F8007731BE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5FF894281B8556F8007731BE /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3C86DF461ADF2C930047B81A; + remoteInfo = RCTWebSocket; + }; 832044941B492C1E00E297FC /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 8320448F1B492C1E00E297FC /* RCTSettings.xcodeproj */; @@ -59,6 +67,7 @@ 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = TicTacToe/main.m; sourceTree = ""; }; 144C5F631AC3E5D800B004E7 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../../React/React.xcodeproj; sourceTree = ""; }; 587650DA1A9EB0DB008B8F17 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; + 5FF894281B8556F8007731BE /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = ../../Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; }; 8320448F1B492C1E00E297FC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../../Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ @@ -67,6 +76,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5FF8942E1B85571A007731BE /* libRCTWebSocket.a in Frameworks */, 144C5F691AC3E5E300B004E7 /* libReact.a in Frameworks */, 1341803E1AA91802003F314A /* libRCTImage.a in Frameworks */, 58C572511AA6229D00CDF9C8 /* libRCTText.a in Frameworks */, @@ -109,6 +119,7 @@ 58C572071AA6126D00CDF9C8 /* Libraries */ = { isa = PBXGroup; children = ( + 5FF894281B8556F8007731BE /* RCTWebSocket.xcodeproj */, 144C5F631AC3E5D800B004E7 /* React.xcodeproj */, 134180381AA917ED003F314A /* RCTImage.xcodeproj */, 8320448F1B492C1E00E297FC /* RCTSettings.xcodeproj */, @@ -125,6 +136,14 @@ name = Products; sourceTree = ""; }; + 5FF894291B8556F8007731BE /* Products */ = { + isa = PBXGroup; + children = ( + 5FF8942D1B8556F8007731BE /* libRCTWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; 832044901B492C1E00E297FC /* Products */ = { isa = PBXGroup; children = ( @@ -205,6 +224,10 @@ ProductGroup = 58C572481AA6224300CDF9C8 /* Products */; ProjectRef = 587650DA1A9EB0DB008B8F17 /* RCTText.xcodeproj */; }, + { + ProductGroup = 5FF894291B8556F8007731BE /* Products */; + ProjectRef = 5FF894281B8556F8007731BE /* RCTWebSocket.xcodeproj */; + }, { ProductGroup = 144C5F641AC3E5D800B004E7 /* Products */; ProjectRef = 144C5F631AC3E5D800B004E7 /* React.xcodeproj */; @@ -239,6 +262,13 @@ remoteRef = 58C5724C1AA6224400CDF9C8 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 5FF8942D1B8556F8007731BE /* libRCTWebSocket.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTWebSocket.a; + remoteRef = 5FF8942C1B8556F8007731BE /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 832044951B492C1E00E297FC /* libRCTSettings.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/TicTacToe/TicTacToe/AppDelegate.m b/Examples/TicTacToe/TicTacToe/AppDelegate.m index 9c328a3a8..7fa214fab 100644 --- a/Examples/TicTacToe/TicTacToe/AppDelegate.m +++ b/Examples/TicTacToe/TicTacToe/AppDelegate.m @@ -52,6 +52,7 @@ RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"TicTacToeApp" + initialProperties:nil launchOptions:launchOptions]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; diff --git a/Examples/UIExplorer/AccessibilityAndroidExample.js b/Examples/UIExplorer/AccessibilityAndroidExample.js index 92a511bcc..d75907a86 100644 --- a/Examples/UIExplorer/AccessibilityAndroidExample.js +++ b/Examples/UIExplorer/AccessibilityAndroidExample.js @@ -27,6 +27,8 @@ var AccessibilityInfo = require('AccessibilityInfo'); var UIExplorerBlock = require('./UIExplorerBlock'); var UIExplorerPage = require('./UIExplorerPage'); +var importantForAccessibilityValues = ['auto', 'yes', 'no', 'no-hide-descendants']; + var AccessibilityAndroidExample = React.createClass({ statics: { @@ -38,6 +40,8 @@ var AccessibilityAndroidExample = React.createClass({ return { count: 0, talkbackEnabled: false, + backgroundImportantForAcc: 0, + forgroundImportantForAcc: 0, }; }, @@ -79,6 +83,18 @@ var AccessibilityAndroidExample = React.createClass({ }); }, + _changeBackgroundImportantForAcc: function() { + this.setState({ + backgroundImportantForAcc: (this.state.backgroundImportantForAcc + 1) % 4, + }); + }, + + _changeForgroundImportantForAcc: function() { + this.setState({ + forgroundImportantForAcc: (this.state.forgroundImportantForAcc + 1) % 4, + }); + }, + render: function() { return ( @@ -148,6 +164,77 @@ var AccessibilityAndroidExample = React.createClass({ + + + + + + Hello + + + + + + + world + + + + + + + + Change importantForAccessibility for background layout. + + + + + + Background layout importantForAccessibility + + + {importantForAccessibilityValues[this.state.backgroundImportantForAcc]} + + + + + + Change importantForAccessibility for forground layout. + + + + + + Forground layout importantForAccessibility + + + {importantForAccessibilityValues[this.state.forgroundImportantForAcc]} + + + + ); }, @@ -158,6 +245,12 @@ var styles = StyleSheet.create({ backgroundColor: 'yellow', padding:10, }, + container: { + flex: 1, + backgroundColor: 'white', + padding: 10, + height:150, + }, }); module.exports = AccessibilityAndroidExample; diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index 82721993f..cdf664c43 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -340,7 +340,7 @@ exports.examples = [ ]; var fullImage = {uri: 'http://facebook.github.io/react/img/logo_og.png'}; -var smallImage = {uri: 'http://facebook.github.io/react/img/logo_small.png'}; +var smallImage = {uri: 'http://facebook.github.io/react/img/logo_small_2x.png'}; var styles = StyleSheet.create({ base: { diff --git a/Examples/UIExplorer/ImageMocks.js b/Examples/UIExplorer/ImageMocks.js index 8335f93e5..670346373 100644 --- a/Examples/UIExplorer/ImageMocks.js +++ b/Examples/UIExplorer/ImageMocks.js @@ -15,37 +15,37 @@ */ 'use strict'; +/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing + * into a proptype shape */ declare module 'image!story-background' { - /* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ 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' { - /* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ 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' { - /* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ 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' { - /* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ 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' { - /* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ declare var uri: string; declare var isStatic: boolean; } diff --git a/Examples/UIExplorer/LayoutEventsExample.js b/Examples/UIExplorer/LayoutEventsExample.js index c57c4e5f2..3407be48a 100644 --- a/Examples/UIExplorer/LayoutEventsExample.js +++ b/Examples/UIExplorer/LayoutEventsExample.js @@ -138,11 +138,11 @@ var styles = StyleSheet.create({ }, }); -exports.title = 'onLayout'; +exports.title = 'Layout Events'; exports.description = 'Layout events can be used to measure view size and position.'; exports.examples = [ { - title: 'onLayout', + title: 'LayoutEventExample', render: function(): ReactElement { return ; }, diff --git a/Examples/UIExplorer/ModalExample.js b/Examples/UIExplorer/ModalExample.js index 37d2cc245..3202cb622 100644 --- a/Examples/UIExplorer/ModalExample.js +++ b/Examples/UIExplorer/ModalExample.js @@ -19,6 +19,7 @@ var React = require('react-native'); var { Modal, StyleSheet, + SwitchIOS, Text, TouchableHighlight, View, @@ -29,53 +30,98 @@ exports.framework = 'React'; exports.title = ''; exports.description = 'Component for presenting modal views.'; -var ModalExample = React.createClass({ - getInitialState: function() { +var Button = React.createClass({ + getInitialState() { return { - openModal: null, + active: false, }; }, - _closeModal: function() { - this.setState({openModal: null}); + _onHighlight() { + this.setState({active: true}); }, - _openAnimatedModal: function() { - this.setState({openModal: 'animated'}); + _onUnhighlight() { + this.setState({active: false}); }, - _openNotAnimatedModal: function() { - this.setState({openModal: 'not-animated'}); + render() { + var colorStyle = { + color: this.state.active ? '#fff' : '#000', + }; + return ( + + {this.props.children} + + ); + } +}); + +var ModalExample = React.createClass({ + getInitialState() { + return { + animated: true, + modalVisible: false, + transparent: false, + }; }, - render: function() { + _setModalVisible(visible) { + this.setState({modalVisible: visible}); + }, + + _toggleAnimated() { + this.setState({animated: !this.state.animated}); + }, + + _toggleTransparent() { + this.setState({transparent: !this.state.transparent}); + }, + + render() { + var modalBackgroundStyle = { + backgroundColor: this.state.transparent ? 'rgba(0, 0, 0, 0.5)' : '#f5fcff', + }; + var innerContainerTransparentStyle = this.state.transparent + ? {backgroundColor: '#fff', padding: 20} + : null; + return ( - - - This modal was presented with animation. - - Close - + + + + This modal was presented {this.state.animated ? 'with' : 'without'} animation. + + - - - This modal was presented immediately, without animation. - - Close - - - + + Animated + + - - Present Animated - + + Transparent + + - - Present Without Animation - + ); }, @@ -91,9 +137,36 @@ exports.examples = [ var styles = StyleSheet.create({ container: { - alignItems: 'center', - backgroundColor: '#f5fcff', flex: 1, justifyContent: 'center', + padding: 20, + }, + innerContainer: { + borderRadius: 10, + }, + row: { + alignItems: 'center', + flex: 1, + flexDirection: 'row', + marginBottom: 20, + }, + rowTitle: { + flex: 1, + fontWeight: 'bold', + }, + button: { + borderRadius: 5, + flex: 1, + height: 44, + justifyContent: 'center', + overflow: 'hidden', + }, + buttonText: { + fontSize: 18, + margin: 5, + textAlign: 'center', + }, + modalButton: { + marginTop: 10, }, }); diff --git a/Examples/UIExplorer/TabBarIOSExample.js b/Examples/UIExplorer/TabBarIOSExample.js index 999195132..5ba3f0099 100644 --- a/Examples/UIExplorer/TabBarIOSExample.js +++ b/Examples/UIExplorer/TabBarIOSExample.js @@ -23,6 +23,8 @@ var { View, } = React; +var base64Icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACSR7JhAAADtUlEQVR4Ac3YA2Bj6QLH0XPT1Fzbtm29tW3btm3bfLZtv7e2ObZnms7d8Uw098tuetPzrxv8wiISrtVudrG2JXQZ4VOv+qUfmqCGGl1mqLhoA52oZlb0mrjsnhKpgeUNEs91Z0pd1kvihA3ULGVHiQO2narKSHKkEMulm9VgUyE60s1aWoMQUbpZOWE+kaqs4eLEjdIlZTcFZB0ndc1+lhB1lZrIuk5P2aib1NBpZaL+JaOGIt0ls47SKzLC7CqrlGF6RZ09HGoNy1lYl2aRSWL5GuzqWU1KafRdoRp0iOQEiDzgZPnG6DbldcomadViflnl/cL93tOoVbsOLVM2jylvdWjXolWX1hmfZbGR/wjypDjFLSZIRov09BgYmtUqPQPlQrPapecLgTIy0jMgPKtTeob2zWtrGH3xvjUkPCtNg/tm1rjwrMa+mdUkPd3hWbH0jArPGiU9ufCsNNWFZ40wpwn+62/66R2RUtoso1OB34tnLOcy7YB1fUdc9e0q3yru8PGM773vXsuZ5YIZX+5xmHwHGVvlrGPN6ZSiP1smOsMMde40wKv2VmwPPVXNut4sVpUreZiLBHi0qln/VQeI/LTMYXpsJtFiclUN+5HVZazim+Ky+7sAvxWnvjXrJFneVtLWLyPJu9K3cXLWeOlbMTlrIelbMDlrLenrjEQOtIF+fuI9xRp9ZBFp6+b6WT8RrxEpdK64BuvHgDk+vUy+b5hYk6zfyfs051gRoNO1usU12WWRWL73/MMEy9pMi9qIrR4ZpV16Rrvduxazmy1FSvuFXRkqTnE7m2kdb5U8xGjLw/spRr1uTov4uOgQE+0N/DvFrG/Jt7i/FzwxbA9kDanhf2w+t4V97G8lrT7wc08aA2QNUkuTfW/KimT01wdlfK4yEw030VfT0RtZbzjeMprNq8m8tnSTASrTLti64oBNdpmMQm0eEwvfPwRbUBywG5TzjPCsdwk3IeAXjQblLCoXnDVeoAz6SfJNk5TTzytCNZk/POtTSV40NwOFWzw86wNJRpubpXsn60NJFlHeqlYRbslqZm2jnEZ3qcSKgm0kTli3zZVS7y/iivZTweYXJ26Y+RTbV1zh3hYkgyFGSTKPfRVbRqWWVReaxYeSLarYv1Qqsmh1s95S7G+eEWK0f3jYKTbV6bOwepjfhtafsvUsqrQvrGC8YhmnO9cSCk3yuY984F1vesdHYhWJ5FvASlacshUsajFt2mUM9pqzvKGcyNJW0arTKN1GGGzQlH0tXwLDgQTurS8eIQAAAABJRU5ErkJggg=='; + var TabBarExample = React.createClass({ statics: { title: '', @@ -55,6 +57,7 @@ var TabBarExample = React.createClass({ barTintColor="darkslateblue"> { this.setState({ diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.ios.js similarity index 100% rename from Examples/UIExplorer/TextInputExample.js rename to Examples/UIExplorer/TextInputExample.ios.js diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index e636653ed..ba5294412 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -27,6 +27,7 @@ var { } = React; exports.displayName = (undefined: ?string); +exports.description = 'Touchable and onPress examples'; exports.title = ' and onPress'; exports.examples = [ { diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 5b9e04288..fb20657a3 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -25,7 +25,7 @@ 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */; }; 141FC1211B222EBB004D5FFB /* IntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 141FC1201B222EBB004D5FFB /* IntegrationTests.m */; }; 143BC5A11B21E45C00462512 /* UIExplorerSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */; }; - 144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTClipRectTests.m */; }; + 144D21241B2204C5006DB32B /* RCTImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */; }; 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; 1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */; }; 1497CFAD1B21F5E400C1F8F2 /* RCTBridgeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */; }; @@ -54,6 +54,7 @@ 3578590A1B28D2CF00341EDB /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 357859011B28D2C500341EDB /* libRCTLinking.a */; }; 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 834C36D21AF8DA610019C93C /* libRCTSettings.a */; }; 83636F8F1B53F22C009F943E /* RCTUIManagerScenarioTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */; }; + 83A936C81B7E0F08005B9C36 /* RCTConvert_UIColorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A936C71B7E0F08005B9C36 /* RCTConvert_UIColorTests.m */; }; D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; }; /* End PBXBuildFile section */ @@ -191,7 +192,7 @@ 143BC5951B21E3E100462512 /* UIExplorerIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 143BC5981B21E3E100462512 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 143BC5A01B21E45C00462512 /* UIExplorerSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIExplorerSnapshotTests.m; sourceTree = ""; }; - 144D21231B2204C5006DB32B /* RCTClipRectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTClipRectTests.m; sourceTree = ""; }; + 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageUtilTests.m; sourceTree = ""; }; 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAllocationTests.m; sourceTree = ""; }; 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeTests.m; sourceTree = ""; }; 1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutorTests.m; sourceTree = ""; }; @@ -216,6 +217,7 @@ 357858F81B28D2C400341EDB /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../../Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTTest.xcodeproj; path = ../../Libraries/RCTTest/RCTTest.xcodeproj; sourceTree = ""; }; 83636F8E1B53F22C009F943E /* RCTUIManagerScenarioTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManagerScenarioTests.m; sourceTree = ""; }; + 83A936C71B7E0F08005B9C36 /* RCTConvert_UIColorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert_UIColorTests.m; sourceTree = ""; }; D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ @@ -357,12 +359,13 @@ 1497CFA41B21F5E400C1F8F2 /* RCTAllocationTests.m */, 1497CFA51B21F5E400C1F8F2 /* RCTBridgeTests.m */, 138D6A151B53CD440074A87E /* RCTCacheTests.m */, - 144D21231B2204C5006DB32B /* RCTClipRectTests.m */, 1497CFA61B21F5E400C1F8F2 /* RCTContextExecutorTests.m */, 1497CFA71B21F5E400C1F8F2 /* RCTConvert_NSURLTests.m */, + 83A936C71B7E0F08005B9C36 /* RCTConvert_UIColorTests.m */, 1497CFA81B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m */, 1497CFA91B21F5E400C1F8F2 /* RCTEventDispatcherTests.m */, 1300627E1B59179B0043FE5A /* RCTGzipTests.m */, + 144D21231B2204C5006DB32B /* RCTImageUtilTests.m */, 13DB03471B5D2ED500C27245 /* RCTJSONTests.m */, 13DF61B51B67A45000EDB188 /* RCTMethodArgumentTests.m */, 1393D0371B68CD1300E1B601 /* RCTModuleMethodTests.m */, @@ -793,7 +796,7 @@ buildActionMask = 2147483647; files = ( 1497CFB01B21F5E400C1F8F2 /* RCTConvert_UIFontTests.m in Sources */, - 144D21241B2204C5006DB32B /* RCTClipRectTests.m in Sources */, + 144D21241B2204C5006DB32B /* RCTImageUtilTests.m in Sources */, 1393D0381B68CD1300E1B601 /* RCTModuleMethodTests.m in Sources */, 1497CFB21B21F5E400C1F8F2 /* RCTSparseArrayTests.m in Sources */, 1300627F1B59179B0043FE5A /* RCTGzipTests.m in Sources */, @@ -805,6 +808,7 @@ 138D6A171B53CD440074A87E /* RCTCacheTests.m in Sources */, 13DB03481B5D2ED500C27245 /* RCTJSONTests.m in Sources */, 1497CFAC1B21F5E400C1F8F2 /* RCTAllocationTests.m in Sources */, + 83A936C81B7E0F08005B9C36 /* RCTConvert_UIColorTests.m in Sources */, 13DF61B61B67A45000EDB188 /* RCTMethodArgumentTests.m in Sources */, 138D6A181B53CD440074A87E /* RCTShadowViewTests.m in Sources */, ); diff --git a/Examples/UIExplorer/UIExplorer/AppDelegate.m b/Examples/UIExplorer/UIExplorer/AppDelegate.m index f2a83422b..f394c9903 100644 --- a/Examples/UIExplorer/UIExplorer/AppDelegate.m +++ b/Examples/UIExplorer/UIExplorer/AppDelegate.m @@ -30,10 +30,11 @@ launchOptions:launchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge - moduleName:@"UIExplorerApp"]; + moduleName:@"UIExplorerApp" + initialProperties:nil]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; - UIViewController *rootViewController = [[UIViewController alloc] init]; + UIViewController *rootViewController = [UIViewController new]; rootViewController.view = rootView; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m index 85f73a314..267ed1409 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m @@ -29,7 +29,7 @@ RCTAssert(NO, @"Tests should be run on 32-bit device simulators (e.g. iPhone 5)"); #endif - NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion]; + NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion; RCTAssert(version.majorVersion == 8 || version.minorVersion >= 3, @"Tests should be run on iOS 8.3+, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion); _runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerIntegrationTests/js/IntegrationTestsApp", nil); } diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m index c7d30539f..c30b8d1dc 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m @@ -45,12 +45,12 @@ { [super setUp]; - _uiManager = [[RCTUIManager alloc] init]; + _uiManager = [RCTUIManager new]; // Register 20 views to use in the tests for (NSInteger i = 1; i <= 20; i++) { - UIView *registeredView = [[UIView alloc] init]; - [registeredView setReactTag:@(i)]; + UIView *registeredView = [UIView new]; + registeredView.reactTag = @(i); _uiManager.viewRegistry[i] = registeredView; } } @@ -92,7 +92,7 @@ NSArray *removeAtIndices = @[@0, @4, @8, @12, @16]; for (NSNumber *index in removeAtIndices) { - NSNumber *reactTag = @([index integerValue] + 2); + NSNumber *reactTag = @(index.integerValue + 2); [removedViews addObject:_uiManager.viewRegistry[reactTag]]; } for (NSInteger i = 2; i < 20; i++) { diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTabBarExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTabBarExample_1@2x.png index 0f306f683..62425fde9 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTabBarExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testTabBarExample_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerSnapshotTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerSnapshotTests.m index cb5d1771f..1281a4796 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerSnapshotTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/UIExplorerSnapshotTests.m @@ -36,7 +36,7 @@ RCTAssert(NO, @"Tests should be run on 32-bit device simulators (e.g. iPhone 5)"); #endif - NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion]; + NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion; RCTAssert(version.majorVersion == 8 || version.minorVersion >= 3, @"Snapshot tests should be run on iOS 8.3+, found %zd.%zd.%zd", version.majorVersion, version.minorVersion, version.patchVersion); _runner = RCTInitRunnerForApp(@"Examples/UIExplorer/UIExplorerApp.ios", nil); _runner.recordMode = NO; diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index 25f69684d..d45f4ab70 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -49,7 +49,7 @@ var COMPONENTS = [ require('./SwitchIOSExample'), require('./TabBarIOSExample'), require('./TextExample.ios'), - require('./TextInputExample'), + require('./TextInputExample.ios'), require('./TouchableExample'), require('./ViewExample'), require('./WebViewExample'), @@ -74,7 +74,7 @@ var APIS = [ require('./StatusBarIOSExample'), require('./TimerExample'), require('./VibrationIOSExample'), - require('./XHRExample'), + require('./XHRExample.ios'), require('./ImageEditingExample'), ]; @@ -123,32 +123,6 @@ class UIExplorerList extends React.Component { ); } - componentDidMount() { - var wasUIExplorer = false; - var didOpenExample = false; - - this.props.navigator.navigationContext.addListener('didfocus', (event) => { - var isUIExplorer = event.data.route.title === 'UIExplorer'; - - if (!didOpenExample && isUIExplorer) { - didOpenExample = true; - - var visibleExampleTitle = Settings.get('visibleExample'); - if (visibleExampleTitle) { - var predicate = (example) => example.title === visibleExampleTitle; - var foundExample = APIS.find(predicate) || COMPONENTS.find(predicate); - if (foundExample) { - setTimeout(() => this._openExample(foundExample), 100); - } - } else if (!wasUIExplorer && isUIExplorer) { - Settings.set({visibleExample: null}); - } - } - - wasUIExplorer = isUIExplorer; - }); - } - renderAdditionalView(renderRow: Function, renderTextInput: Function): React.Component { return renderTextInput(styles.searchTextInput); } @@ -171,7 +145,6 @@ class UIExplorerList extends React.Component { } onPressRow(example: any) { - Settings.set({visibleExample: example.title}); this._openExample(example); } } diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m index c3a626552..670530114 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m @@ -42,15 +42,16 @@ _Pragma("clang diagnostic pop") @end @interface AllocationTestModule : NSObject + +@property (nonatomic, assign, getter=isValid) BOOL valid; + @end @implementation AllocationTestModule RCT_EXPORT_MODULE(); -@synthesize valid = _valid; - -- (id)init +- (instancetype)init { if ((self = [super init])) { _valid = YES; @@ -81,6 +82,7 @@ RCT_EXPORT_METHOD(test:(__unused NSString *)a @autoreleasepool { RCTRootView *view = [[RCTRootView alloc] initWithBundleURL:nil moduleName:@"" + initialProperties:nil launchOptions:nil]; weakBridge = view.bridge; XCTAssertNotNil(weakBridge, @"RCTBridge should have been created"); @@ -92,7 +94,7 @@ RCT_EXPORT_METHOD(test:(__unused NSString *)a - (void)testModulesAreInvalidated { - AllocationTestModule *module = [[AllocationTestModule alloc] init]; + AllocationTestModule *module = [AllocationTestModule new]; @autoreleasepool { RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ @@ -111,7 +113,7 @@ RCT_EXPORT_METHOD(test:(__unused NSString *)a { __weak AllocationTestModule *weakModule; @autoreleasepool { - AllocationTestModule *module = [[AllocationTestModule alloc] init]; + AllocationTestModule *module = [AllocationTestModule new]; RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[module]; @@ -177,15 +179,15 @@ RCT_EXPORT_METHOD(test:(__unused NSString *)a RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:nil launchOptions:nil]; - __weak id rootContentView; + __weak UIView *rootContentView; @autoreleasepool { - RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@""]; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"" initialProperties:nil]; RUN_RUNLOOP_WHILE(!(rootContentView = [rootView valueForKey:@"contentView"])) - XCTAssertTrue([rootContentView isValid], @"RCTContentView should be valid"); + XCTAssertTrue(rootContentView.userInteractionEnabled, @"RCTContentView should be valid"); (void)rootView; } - XCTAssertFalse([rootContentView isValid], @"RCTContentView should have been invalidated"); + XCTAssertFalse(rootContentView.userInteractionEnabled, @"RCTContentView should have been invalidated"); } - (void)testUnderlyingBridgeIsDeallocated diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTCacheTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTCacheTests.m index 30313d23b..b6bc22188 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTCacheTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTCacheTests.m @@ -37,7 +37,7 @@ - (void)setUp { - self.cache = [[RCTCache alloc] init]; + self.cache = [RCTCache new]; self.cache.countLimit = 3; self.cache.totalCostLimit = 100; } diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m index f7db2d46f..fa48fcfbf 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m @@ -33,7 +33,7 @@ - (void)setUp { [super setUp]; - _executor = [[RCTContextExecutor alloc] init]; + _executor = [RCTContextExecutor new]; [_executor setUp]; } @@ -129,7 +129,7 @@ static uint64_t _get_time_nanoseconds(void) "; [_executor executeApplicationScript:script sourceURL:[NSURL URLWithString:@"http://localhost:8081/"] onComplete:^(__unused NSError *error) { - NSMutableArray *params = [[NSMutableArray alloc] init]; + NSMutableArray *params = [NSMutableArray new]; id data = @1; for (int i = 0; i < 4; i++) { double samples[runs / frequency]; diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIColorTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIColorTests.m new file mode 100644 index 000000000..f74429974 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIColorTests.m @@ -0,0 +1,79 @@ +/** + * 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. + */ + +#import + +#import "RCTConvert.h" + +@interface RCTConvert_UIColorTests : XCTestCase + +@end + +@implementation RCTConvert_UIColorTests + +#define XCTAssertEqualColors(color1, color2) do { \ + CGFloat r1, g1, b1, a1; \ + CGFloat r2, g2, b2, a2; \ + XCTAssertTrue([(color1) getRed:&r1 green:&g1 blue:&b1 alpha:&a1] && \ + [(color2) getRed:&r2 green:&g2 blue:&b2 alpha:&a2] && \ + r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2, \ + @"rgba(%d, %d, %d, %.3f) != rgba(%d, %d, %d, %.3f)", \ + (int)(r1 * 255), (int)(g1 * 255), (int)(b1 * 255), a1, \ + (int)(r2 * 255), (int)(g2 * 255), (int)(b2 * 255), a2 \ + ); \ +} while (0) + +- (void)testHex3 +{ + UIColor *color = [RCTConvert UIColor:@"#333"]; + UIColor *expected = [UIColor colorWithWhite:0.2 alpha:1.0]; + XCTAssertEqualColors(color, expected); +} + +- (void)testHex6 +{ + UIColor *color = [RCTConvert UIColor:@"#666"]; + UIColor *expected = [UIColor colorWithWhite:0.4 alpha:1.0]; + XCTAssertEqualColors(color, expected); +} + +- (void)testRGB +{ + UIColor *color = [RCTConvert UIColor:@"rgb(51, 102, 153)"]; + UIColor *expected = [UIColor colorWithRed:0.2 green:0.4 blue:0.6 alpha:1.0]; + XCTAssertEqualColors(color, expected); +} + +- (void)testRGBA +{ + UIColor *color = [RCTConvert UIColor:@"rgba(51, 102, 153, 0.5)"]; + UIColor *expected = [UIColor colorWithRed:0.2 green:0.4 blue:0.6 alpha:0.5]; + XCTAssertEqualColors(color, expected); +} + +- (void)testHSL +{ + UIColor *color = [RCTConvert UIColor:@"hsl(30, 50%, 50%)"]; + UIColor *expected = [UIColor colorWithHue:30.0 / 360.0 saturation:0.5 brightness:0.5 alpha:1.0]; + XCTAssertEqualColors(color, expected); +} + +- (void)testHSLA +{ + UIColor *color = [RCTConvert UIColor:@"hsla(30, 50%, 50%, 0.5)"]; + UIColor *expected = [UIColor colorWithHue:30.0 / 360.0 saturation:0.5 brightness:0.5 alpha:0.5]; + XCTAssertEqualColors(color, expected); +} + +@end diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIFontTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIFontTests.m index 0ccc13631..8521ab2ef 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIFontTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTConvert_UIFontTests.m @@ -162,8 +162,8 @@ - (void)testFamilyStyleAndWeight { { - UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-UltraLightItalic" size:14]; - UIFont *result = [RCTConvert UIFont:@{@"fontFamily": @"Helvetica Neue", @"fontStyle": @"italic", @"fontWeight": @"100"}]; + UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-LightItalic" size:14]; + UIFont *result = [RCTConvert UIFont:@{@"fontFamily": @"Helvetica Neue", @"fontStyle": @"italic", @"fontWeight": @"300"}]; RCTAssertEqualFonts(expected, result); } { diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m index 96e6bd45c..bfa52b94f 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTEventDispatcherTests.m @@ -61,7 +61,7 @@ [super setUp]; _bridge = [OCMockObject mockForClass:[RCTBridge class]]; - _eventDispatcher = [[RCTEventDispatcher alloc] init]; + _eventDispatcher = [RCTEventDispatcher new]; ((id)_eventDispatcher).bridge = _bridge; _eventName = RCTNormalizeInputEventName(@"sampleEvent"); diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTGzipTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTGzipTests.m index 5e74bf7c4..ceb58b972 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTGzipTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTGzipTests.m @@ -67,7 +67,7 @@ extern BOOL RCTIsGzippedData(NSData *data); @"headers": @{@"Content-Encoding": @"gzip"}, }; - RCTNetworking *networker = [[RCTNetworking alloc] init]; + RCTNetworking *networker = [RCTNetworking new]; __block NSURLRequest *request = nil; [networker buildRequest:query completionBlock:^(NSURLRequest *_request) { request = _request; diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTClipRectTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m similarity index 65% rename from Examples/UIExplorer/UIExplorerUnitTests/RCTClipRectTests.m rename to Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m index 0041a1b46..f1effc8d6 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTClipRectTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTImageUtilTests.m @@ -33,11 +33,11 @@ RCTAssertEqualPoints(a.origin, b.origin); \ RCTAssertEqualSizes(a.size, b.size); \ } -@interface RCTClipRectTests : XCTestCase +@interface RCTImageUtilTests : XCTestCase @end -@implementation RCTClipRectTests +@implementation RCTImageUtilTests - (void)testLandscapeSourceLandscapeTarget { @@ -46,19 +46,19 @@ RCTAssertEqualSizes(a.size, b.size); \ { CGRect expected = {CGPointZero, {100, 20}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleToFill); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); RCTAssertEqualRects(expected, result); } { CGRect expected = {CGPointZero, {100, 10}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFit); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFit); RCTAssertEqualRects(expected, result); } { CGRect expected = {{-50, 0}, {200, 20}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFill); RCTAssertEqualRects(expected, result); } } @@ -69,20 +69,20 @@ RCTAssertEqualSizes(a.size, b.size); \ CGSize target = {100, 20}; { - CGRect expected = {CGPointZero, {10, 20}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleToFill); + CGRect expected = {CGPointZero, {100, 20}}; + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); RCTAssertEqualRects(expected, result); } { CGRect expected = {CGPointZero, {2, 20}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFit); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFit); RCTAssertEqualRects(expected, result); } { - CGRect expected = {{0, -49}, {10, 100}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill); + CGRect expected = {{0, -490}, {100, 1000}}; + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFill); RCTAssertEqualRects(expected, result); } } @@ -93,20 +93,20 @@ RCTAssertEqualSizes(a.size, b.size); \ CGSize target = {20, 50}; { - CGRect expected = {CGPointZero, {10, 50}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleToFill); + CGRect expected = {CGPointZero, {20, 50}}; + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); RCTAssertEqualRects(expected, result); } { CGRect expected = {CGPointZero, {5, 50}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFit); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFit); RCTAssertEqualRects(expected, result); } { - CGRect expected = {{0, -37.5}, {10, 100}}; - CGRect result = RCTClipRect(content, 2, target, 2, UIViewContentModeScaleAspectFill); + CGRect expected = {{0, -75}, {20, 200}}; + CGRect result = RCTTargetRect(content, target, 2, UIViewContentModeScaleAspectFill); RCTAssertEqualRects(expected, result); } } @@ -117,8 +117,8 @@ RCTAssertEqualSizes(a.size, b.size); \ CGSize target = {20, 50}; { - CGRect expected = {{0, -38}, {10, 100}}; - CGRect result = RCTClipRect(content, 1, target, 1, UIViewContentModeScaleAspectFill); + CGRect expected = {{0, -75}, {20, 200}}; + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleAspectFill); RCTAssertEqualRects(expected, result); } } @@ -129,7 +129,7 @@ RCTAssertEqualSizes(a.size, b.size); \ CGSize target = {3, 3}; CGRect expected = {CGPointZero, {3, 3}}; - CGRect result = RCTClipRect(content, 2, target, 1, UIViewContentModeScaleToFill); + CGRect result = RCTTargetRect(content, target, 1, UIViewContentModeScaleToFill); RCTAssertEqualRects(expected, result); } diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m index 9c3949d0e..b11bffa26 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTModuleMethodTests.m @@ -15,6 +15,7 @@ #import #import +#import "RCTBridgeModule.h" #import "RCTModuleMethod.h" #import "RCTLog.h" @@ -30,11 +31,16 @@ static BOOL RCTLogsError(void (^block)(void)) return loggedError; } -@interface RCTModuleMethodTests : XCTestCase +@interface RCTModuleMethodTests : XCTestCase @end @implementation RCTModuleMethodTests +{ + CGRect _s; +} + ++ (NSString *)moduleName { return nil; } - (void)doFooWithBar:(__unused NSString *)bar { } @@ -56,6 +62,7 @@ static BOOL RCTLogsError(void (^block)(void)) - (void)doFooWithNumber:(__unused NSNumber *)n { } - (void)doFooWithDouble:(__unused double)n { } - (void)doFooWithInteger:(__unused NSInteger)n { } +- (void)doFooWithCGRect:(CGRect)s { _s = s; } - (void)testNumbersNonnull { @@ -63,9 +70,11 @@ static BOOL RCTLogsError(void (^block)(void)) // Specifying an NSNumber param without nonnull isn't allowed XCTAssertTrue(RCTLogsError(^{ NSString *methodName = @"doFooWithNumber:(NSNumber *)n"; - (void)[[RCTModuleMethod alloc] initWithObjCMethodName:methodName - JSMethodName:nil - moduleClass:[self class]]; + RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithObjCMethodName:methodName + JSMethodName:nil + moduleClass:[self class]]; + // Invoke method to trigger parsing + [method invokeWithBridge:nil module:self arguments:@[@1]]; })); } @@ -100,4 +109,16 @@ static BOOL RCTLogsError(void (^block)(void)) } } +- (void)testStructArgument +{ + NSString *methodName = @"doFooWithCGRect:(CGRect)s"; + RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithObjCMethodName:methodName + JSMethodName:nil + moduleClass:[self class]]; + + CGRect r = CGRectMake(10, 20, 30, 40); + [method invokeWithBridge:nil module:self arguments:@[@[@10, @20, @30, @40]]]; + XCTAssertTrue(CGRectEqualToRect(r, _s)); +} + @end diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m index f396131d5..9bdda8a8d 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m @@ -99,7 +99,7 @@ - (RCTShadowView *)_shadowViewWithStyle:(void(^)(css_style_t *style))styleBlock { - RCTShadowView *shadowView = [[RCTShadowView alloc] init]; + RCTShadowView *shadowView = [RCTShadowView new]; css_style_t style = shadowView.cssNode->style; styleBlock(&style); diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTSparseArrayTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTSparseArrayTests.m index cee52d036..57e7a0bf2 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTSparseArrayTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTSparseArrayTests.m @@ -25,13 +25,13 @@ - (void)testDictionary { - id myView = [[UIView alloc] init]; + id myView = [UIView new]; myView.reactTag = @4; - id myOtherView = [[UIView alloc] init]; + id myOtherView = [UIView new]; myOtherView.reactTag = @5; - RCTSparseArray *registry = [[RCTSparseArray alloc] init]; + RCTSparseArray *registry = [RCTSparseArray new]; XCTAssertNil(registry[@4], @"how did you have a view when none are registered?"); XCTAssertNil(registry[@5], @"how did you have a view when none are registered?"); diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m index b587d9bca..9be80dca9 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m @@ -44,12 +44,12 @@ { [super setUp]; - _uiManager = [[RCTUIManager alloc] init]; + _uiManager = [RCTUIManager new]; // Register 20 views to use in the tests for (NSInteger i = 1; i <= 20; i++) { - UIView *registeredView = [[UIView alloc] init]; - [registeredView setReactTag:@(i)]; + UIView *registeredView = [UIView new]; + registeredView.reactTag = @(i); _uiManager.viewRegistry[i] = registeredView; } } @@ -91,7 +91,7 @@ NSArray *removeAtIndices = @[@0, @4, @8, @12, @16]; for (NSNumber *index in removeAtIndices) { - NSNumber *reactTag = @([index integerValue] + 2); + NSNumber *reactTag = @(index.integerValue + 2); [removedViews addObject:_uiManager.viewRegistry[reactTag]]; } for (NSInteger i = 2; i < 20; i++) { diff --git a/Examples/UIExplorer/XHRExample.js b/Examples/UIExplorer/XHRExample.ios.js similarity index 100% rename from Examples/UIExplorer/XHRExample.js rename to Examples/UIExplorer/XHRExample.ios.js diff --git a/JSCLegacyProfiler/JSCLegacyProfiler.mm b/JSCLegacyProfiler/JSCLegacyProfiler.mm index 218c5e55d..b946c8fd1 100644 --- a/JSCLegacyProfiler/JSCLegacyProfiler.mm +++ b/JSCLegacyProfiler/JSCLegacyProfiler.mm @@ -8,7 +8,7 @@ #include "JSProfilerPrivate.h" #include "JSStringRef.h" -#include +#include #define GEN_AND_CHECK(expr) \ do { \ diff --git a/JSCLegacyProfiler/Makefile b/JSCLegacyProfiler/Makefile index b825f7764..0cf8f6afe 100644 --- a/JSCLegacyProfiler/Makefile +++ b/JSCLegacyProfiler/Makefile @@ -1,6 +1,12 @@ HEADER_PATHS := `find ./tmp/JavaScriptCore -name '*.h' | xargs -I{} dirname {} | uniq | xargs -I{} echo "-I {}"` +SDK_VERSION=$(shell plutil -convert json -o - /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/SDKSettings.plist | awk -f parseSDKVersion.awk) CERT ?= "iPhone Developer" +ifneq ($(SDK_VERSION), 8) +all: + $(error "Expected to be compiled with iOS SDK version 8, found $(SDK_VERSION)") +endif + ios8: prepare build generate prepare: clean create download @@ -45,8 +51,8 @@ yajl: echo `find . -name '*.c'` cd ./tmp/yajl-2.1.0/src && \ clang -arch arm64 -arch armv7 -std=c99 \ - -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include/ \ - -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include/machine \ + -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/ \ + -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/machine \ -I ../build/yajl-2.1.0/include \ -c `find . -name '*.c'` libtool -static -o ./tmp/yajl.a `find ./tmp/yajl-2.1.0/src/ -name '*.o'` @@ -77,12 +83,12 @@ arm64: -I ./tmp/WebCore-7600.1.25/icu \ -I ./tmp/WTF-7600.1.24 \ -I ./tmp/yajl-2.1.0/build/yajl-2.1.0/include \ - -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include \ - -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include/machine \ + -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include \ + -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include/machine \ -DNDEBUG=1\ -miphoneos-version-min=8.0 \ - -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib \ - -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib/system \ + -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib \ + -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/system \ ${HEADER_PATHS} \ -undefined dynamic_lookup \ ./JSCLegacyProfiler.mm ./tmp/yajl.a @@ -96,11 +102,11 @@ armv7: -I ./tmp/WebCore-7600.1.25/icu \ -I ./tmp/WTF-7600.1.24 \ -I ./tmp/yajl-2.1.0/build/yajl-2.1.0/include \ - -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/include \ + -I /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/include \ -DNDEBUG=1\ -miphoneos-version-min=8.0 \ - -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib \ - -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/usr/lib/system \ + -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib \ + -L /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/usr/lib/system \ ${HEADER_PATHS} \ -undefined dynamic_lookup \ ./JSCLegacyProfiler.mm ./tmp/yajl.a diff --git a/JSCLegacyProfiler/parseSDKVersion.awk b/JSCLegacyProfiler/parseSDKVersion.awk new file mode 100644 index 000000000..73ff20237 --- /dev/null +++ b/JSCLegacyProfiler/parseSDKVersion.awk @@ -0,0 +1,10 @@ +BEGIN { + FS = ":" + RS = "," +} + +/"Version"/ { + version = substr($2, 2, length($2) - 2) + print int(version) + exit 0 +} diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.m b/Libraries/ActionSheetIOS/RCTActionSheetManager.m index 56df9f38d..ecd522cee 100644 --- a/Libraries/ActionSheetIOS/RCTActionSheetManager.m +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.m @@ -27,7 +27,7 @@ RCT_EXPORT_MODULE() - (instancetype)init { if ((self = [super init])) { - _callbacks = [[NSMutableDictionary alloc] init]; + _callbacks = [NSMutableDictionary new]; } return self; } @@ -41,7 +41,7 @@ RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options failureCallback:(__unused RCTResponseSenderBlock)failureCallback successCallback:(RCTResponseSenderBlock)successCallback) { - UIActionSheet *actionSheet = [[UIActionSheet alloc] init]; + UIActionSheet *actionSheet = [UIActionSheet new]; actionSheet.title = options[@"title"]; @@ -60,7 +60,7 @@ RCT_EXPORT_METHOD(showActionSheetWithOptions:(NSDictionary *)options _callbacks[RCTKeyForInstance(actionSheet)] = successCallback; - UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; + UIWindow *appWindow = [UIApplication sharedApplication].delegate.window; if (appWindow == nil) { RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options); return; @@ -81,12 +81,12 @@ RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options if (URL) { [items addObject:URL]; } - if ([items count] == 0) { + if (items.count == 0) { failureCallback(@[@"No `url` or `message` to share"]); return; } UIActivityViewController *share = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; - UIViewController *ctrl = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; + UIViewController *ctrl = [UIApplication sharedApplication].delegate.window.rootViewController; #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 @@ -126,7 +126,7 @@ RCT_EXPORT_METHOD(showShareActionSheetWithOptions:(NSDictionary *)options RCTLogWarn(@"No callback registered for action sheet: %@", actionSheet.title); } - [[[[UIApplication sharedApplication] delegate] window] makeKeyWindow]; + [[UIApplication sharedApplication].delegate.window makeKeyWindow]; } #pragma mark Private diff --git a/Libraries/Animation/Animated/Animated.js b/Libraries/Animated/Animated.js similarity index 95% rename from Libraries/Animation/Animated/Animated.js rename to Libraries/Animated/Animated.js index 221c153f9..ec4977f3b 100644 --- a/Libraries/Animation/Animated/Animated.js +++ b/Libraries/Animated/Animated.js @@ -33,13 +33,13 @@ type EndCallback = (result: EndResult) => void; // Note(vjeux): this would be better as an interface but flow doesn't // support them yet class Animated { - attach(): void {} - detach(): void {} + __attach(): void {} + __detach(): void {} __getValue(): any {} - getAnimatedValue(): any { return this.__getValue(); } - addChild(child: Animated) {} - removeChild(child: Animated) {} - getChildren(): Array { return []; } + __getAnimatedValue(): any { return this.__getValue(); } + __addChild(child: Animated) {} + __removeChild(child: Animated) {} + __getChildren(): Array { return []; } } // Important note: start() and stop() will only be called at most once. @@ -71,14 +71,14 @@ class AnimatedWithChildren extends Animated { this._children = []; } - addChild(child: Animated): void { + __addChild(child: Animated): void { if (this._children.length === 0) { - this.attach(); + this.__attach(); } this._children.push(child); } - removeChild(child: Animated): void { + __removeChild(child: Animated): void { var index = this._children.indexOf(child); if (index === -1) { console.warn('Trying to remove a child that doesn\'t exist'); @@ -86,11 +86,11 @@ class AnimatedWithChildren extends Animated { } this._children.splice(index, 1); if (this._children.length === 0) { - this.detach(); + this.__detach(); } } - getChildren(): Array { + __getChildren(): Array { return this._children; } } @@ -123,7 +123,7 @@ function _flush(rootNode: AnimatedValue): void { if (typeof node.update === 'function') { animatedStyles.add(node); } else { - node.getChildren().forEach(findAnimatedStyles); + node.__getChildren().forEach(findAnimatedStyles); } } findAnimatedStyles(rootNode); @@ -518,7 +518,7 @@ class AnimatedValue extends AnimatedWithChildren { this._listeners = {}; } - detach() { + __detach() { this.stopAnimation(); } @@ -584,7 +584,7 @@ class AnimatedValue extends AnimatedWithChildren { } stopTracking(): void { - this._tracking && this._tracking.detach(); + this._tracking && this._tracking.__detach(); this._tracking = null; } @@ -715,12 +715,12 @@ class AnimatedInterpolation extends AnimatedWithChildren { return new AnimatedInterpolation(this, Interpolation.create(config)); } - attach(): void { - this._parent.addChild(this); + __attach(): void { + this._parent.__addChild(this); } - detach(): void { - this._parent.removeChild(this); + __detach(): void { + this._parent.__removeChild(this); } } @@ -747,13 +747,13 @@ class AnimatedTransform extends AnimatedWithChildren { }); } - getAnimatedValue(): Array { + __getAnimatedValue(): Array { return this._transforms.map(transform => { var result = {}; for (var key in transform) { var value = transform[key]; if (value instanceof Animated) { - result[key] = value.getAnimatedValue(); + result[key] = value.__getAnimatedValue(); } else { // All transform components needed to recompose matrix result[key] = value; @@ -763,23 +763,23 @@ class AnimatedTransform extends AnimatedWithChildren { }); } - attach(): void { + __attach(): void { this._transforms.forEach(transform => { for (var key in transform) { var value = transform[key]; if (value instanceof Animated) { - value.addChild(this); + value.__addChild(this); } } }); } - detach(): void { + __detach(): void { this._transforms.forEach(transform => { for (var key in transform) { var value = transform[key]; if (value instanceof Animated) { - value.removeChild(this); + value.__removeChild(this); } } }); @@ -814,31 +814,31 @@ class AnimatedStyle extends AnimatedWithChildren { return style; } - getAnimatedValue(): Object { + __getAnimatedValue(): Object { var style = {}; for (var key in this._style) { var value = this._style[key]; if (value instanceof Animated) { - style[key] = value.getAnimatedValue(); + style[key] = value.__getAnimatedValue(); } } return style; } - attach(): void { + __attach(): void { for (var key in this._style) { var value = this._style[key]; if (value instanceof Animated) { - value.addChild(this); + value.__addChild(this); } } } - detach(): void { + __detach(): void { for (var key in this._style) { var value = this._style[key]; if (value instanceof Animated) { - value.removeChild(this); + value.__removeChild(this); } } } @@ -861,7 +861,7 @@ class AnimatedProps extends Animated { } this._props = props; this._callback = callback; - this.attach(); + this.__attach(); } __getValue(): Object { @@ -877,31 +877,31 @@ class AnimatedProps extends Animated { return props; } - getAnimatedValue(): Object { + __getAnimatedValue(): Object { var props = {}; for (var key in this._props) { var value = this._props[key]; if (value instanceof Animated) { - props[key] = value.getAnimatedValue(); + props[key] = value.__getAnimatedValue(); } } return props; } - attach(): void { + __attach(): void { for (var key in this._props) { var value = this._props[key]; if (value instanceof Animated) { - value.addChild(this); + value.__addChild(this); } } } - detach(): void { + __detach(): void { for (var key in this._props) { var value = this._props[key]; if (value instanceof Animated) { - value.removeChild(this); + value.__removeChild(this); } } } @@ -918,7 +918,7 @@ function createAnimatedComponent(Component: any): any { _propsAnimated: AnimatedProps; componentWillUnmount() { - this._propsAnimated && this._propsAnimated.detach(); + this._propsAnimated && this._propsAnimated.__detach(); } setNativeProps(props) { @@ -940,7 +940,7 @@ function createAnimatedComponent(Component: any): any { // forceUpdate. var callback = () => { if (this.refs[refName].setNativeProps) { - var value = this._propsAnimated.getAnimatedValue(); + var value = this._propsAnimated.__getAnimatedValue(); this.refs[refName].setNativeProps(value); } else { this.forceUpdate(); @@ -960,7 +960,7 @@ function createAnimatedComponent(Component: any): any { // This way the intermediate state isn't to go to 0 and trigger // this expensive recursive detaching to then re-attach everything on // the very next operation. - oldPropsAnimated && oldPropsAnimated.detach(); + oldPropsAnimated && oldPropsAnimated.__detach(); } componentWillReceiveProps(nextProps) { @@ -1000,19 +1000,19 @@ class AnimatedTracking extends Animated { this._animationClass = animationClass; this._animationConfig = animationConfig; this._callback = callback; - this.attach(); + this.__attach(); } __getValue(): Object { return this._parent.__getValue(); } - attach(): void { - this._parent.addChild(this); + __attach(): void { + this._parent.__addChild(this); } - detach(): void { - this._parent.removeChild(this); + __detach(): void { + this._parent.__removeChild(this); } update(): void { diff --git a/Libraries/Animation/Animated/Easing.js b/Libraries/Animated/Easing.js similarity index 100% rename from Libraries/Animation/Animated/Easing.js rename to Libraries/Animated/Easing.js diff --git a/Libraries/Animation/Animated/Interpolation.js b/Libraries/Animated/Interpolation.js similarity index 100% rename from Libraries/Animation/Animated/Interpolation.js rename to Libraries/Animated/Interpolation.js diff --git a/Libraries/Animation/Animated/SpringConfig.js b/Libraries/Animated/SpringConfig.js similarity index 100% rename from Libraries/Animation/Animated/SpringConfig.js rename to Libraries/Animated/SpringConfig.js diff --git a/Libraries/Animation/Animated/__tests__/Animated-test.js b/Libraries/Animated/__tests__/Animated-test.js similarity index 97% rename from Libraries/Animation/Animated/__tests__/Animated-test.js rename to Libraries/Animated/__tests__/Animated-test.js index d27ff920c..5f24fc546 100644 --- a/Libraries/Animation/Animated/__tests__/Animated-test.js +++ b/Libraries/Animated/__tests__/Animated-test.js @@ -37,7 +37,7 @@ describe('Animated', () => { } }, callback); - expect(anim.getChildren().length).toBe(3); + expect(anim.__getChildren().length).toBe(3); expect(node.__getValue()).toEqual({ style: { @@ -65,8 +65,8 @@ describe('Animated', () => { }, }); - node.detach(); - expect(anim.getChildren().length).toBe(0); + node.__detach(); + expect(anim.__getChildren().length).toBe(0); anim.setValue(1); expect(callback.mock.calls.length).toBe(1); @@ -74,7 +74,7 @@ describe('Animated', () => { it('does not detach on updates', () => { var anim = new Animated.Value(0); - anim.detach = jest.genMockFunction(); + anim.__detach = jest.genMockFunction(); var c = new Animated.View(); c.props = { @@ -84,16 +84,16 @@ describe('Animated', () => { }; c.componentWillMount(); - expect(anim.detach).not.toBeCalled(); + expect(anim.__detach).not.toBeCalled(); c.componentWillReceiveProps({ style: { opacity: anim, }, }); - expect(anim.detach).not.toBeCalled(); + expect(anim.__detach).not.toBeCalled(); c.componentWillUnmount(); - expect(anim.detach).toBeCalled(); + expect(anim.__detach).toBeCalled(); }); @@ -442,7 +442,7 @@ describe('Animated Vectors', () => { }, }); - node.detach(); + node.__detach(); vec.setValue({x: 1, y: 1}); expect(callback.mock.calls.length).toBe(2); diff --git a/Libraries/Animation/Animated/__tests__/Easing-test.js b/Libraries/Animated/__tests__/Easing-test.js similarity index 100% rename from Libraries/Animation/Animated/__tests__/Easing-test.js rename to Libraries/Animated/__tests__/Easing-test.js diff --git a/Libraries/Animation/Animated/__tests__/Interpolation-test.js b/Libraries/Animated/__tests__/Interpolation-test.js similarity index 100% rename from Libraries/Animation/Animated/__tests__/Interpolation-test.js rename to Libraries/Animated/__tests__/Interpolation-test.js diff --git a/Libraries/Animation/Animated/package.json b/Libraries/Animated/package.json similarity index 100% rename from Libraries/Animation/Animated/package.json rename to Libraries/Animated/package.json diff --git a/Libraries/Animation/POPAnimationMixin.js b/Libraries/Animation/POPAnimationMixin.js deleted file mode 100644 index 0e109062c..000000000 --- a/Libraries/Animation/POPAnimationMixin.js +++ /dev/null @@ -1,268 +0,0 @@ -/** - * 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 POPAnimationMixin - * @flow - */ -'use strict'; - -var POPAnimationOrNull = require('POPAnimation'); -var React = require('React'); - -if (!POPAnimationOrNull) { - // POP animation isn't available in the OSS fork - this is a temporary - // workaround to enable its availability to be determined at runtime. - module.exports = (null : ?{}); -} else { - -// At this point, POPAnimationOrNull is guaranteed to be -// non-null. Bring it local to preserve type refinement. -var POPAnimation = POPAnimationOrNull; - -var invariant = require('invariant'); -var warning = require('warning'); - -var POPAnimationMixin = { - /** - * Different ways to interpolate between beginning and end states - * of properties during animation, such as spring, linear, and decay. - */ - AnimationTypes: POPAnimation.Types, - AnimationProperties: POPAnimation.Properties, - - getInitialState: function(): Object { - this._popAnimationEnqueuedAnimationTimeouts = []; - return { - _currentAnimationsByNodeHandle: {}, - }; - }, - - _ensureBookkeepingSetup: function(nodeHandle: any) { - if (!this.state._currentAnimationsByNodeHandle[nodeHandle]) { - this.state._currentAnimationsByNodeHandle[nodeHandle] = []; - } - }, - - /** - * Start animating the View with ref `refKey`. - * - * @param {key} refKey The key to reference the View to be animated. - * - * @param {number|Object} anim Either the identifier returned by - * POPAnimation.create* or an object defining all the necessary - * properties of the animation you wish to start (including type, matching - * an entry in AnimationTypes). - * - * @param {func} doneCallback A callback fired when the animation is done, and - * is passed a `finished` param that indicates whether the animation - * completely finished, or was interrupted. - */ - startAnimation: function( - refKey: string, - anim: number | {type: number; property: number;}, - doneCallback: (finished: bool) => void - ) { - var animID: number = 0; - if (typeof anim === 'number') { - animID = anim; - } else { - invariant( - anim instanceof Object && - anim.type !== undefined && - anim.property !== undefined, - 'Animation definitions must specify a type of animation and a ' + - 'property to animate.' - ); - animID = POPAnimation.createAnimation(anim.type, anim); - } - invariant( - this.refs[refKey], - 'Invalid refKey ' + refKey + ' for anim:\n' + JSON.stringify(anim) + - '\nvalid refs: ' + JSON.stringify(Object.keys(this.refs)) - ); - var refNodeHandle = React.findNodeHandle(this.refs[refKey]); - this.startAnimationWithNodeHandle(refNodeHandle, animID, doneCallback); - }, - - /** - * Starts an animation on a native node. - * - * @param {NodeHandle} nodeHandle Handle to underlying native node. - * @see `startAnimation`. - */ - startAnimationWithNodeHandle: function( - nodeHandle: any, - animID: number, - doneCallback: (finished: bool) => void - ) { - this._ensureBookkeepingSetup(nodeHandle); - var animations = this.state._currentAnimationsByNodeHandle[nodeHandle]; - var animIndex = animations.length; - animations.push(animID); - var cleanupWrapper = (finished) => { - if (!this.isMounted()) { - return; - } - animations[animIndex] = 0; // zero it out so we don't try to stop it - var allDone = true; - for (var ii = 0; ii < animations.length; ii++) { - if (animations[ii]) { - allDone = false; - break; - } - } - if (allDone) { - this.state._currentAnimationsByNodeHandle[nodeHandle] = undefined; - } - doneCallback && doneCallback(finished); - }; - // Hack to aviod race condition in POP: - var animationTimeoutHandler = setTimeout(() => { - POPAnimation.addAnimation(nodeHandle, animID, cleanupWrapper); - }, 1); - this._popAnimationEnqueuedAnimationTimeouts.push(animationTimeoutHandler); - }, - - /** - * Starts multiple animations with one shared callback that is called when all - * animations complete. - * - * @param {Array(Object} animations Array of objects defining all the - * animations to start, each with shape `{ref|nodeHandle, anim}`. - * @param {func} onSuccess A callback fired when all animations have returned, - * and is passed a finished arg that is true if all animations finished - * completely. - * @param {func} onFailure Not supported yet. - */ - startAnimations: function( - animations: Array, - onSuccess: (finished: boolean) => void, - onFailure: () => void - ) { - var numReturned = 0; - var numFinished = 0; - var numAnimations = animations.length; - var metaCallback = (finished) => { - if (finished) { - ++numFinished; - } - if (++numReturned === numAnimations) { - onSuccess && onSuccess(numFinished === numAnimations); - } - }; - animations.forEach((anim) => { - warning( - anim.ref != null || anim.nodeHandle != null && - !anim.ref !== !anim.nodeHandle, - 'Animations must be specified with either ref xor nodeHandle' - ); - if (anim.ref) { - this.startAnimation(anim.ref, anim.anim, metaCallback); - } else if (anim.nodeHandle) { - this.startAnimationWithNodeHandle(anim.nodeHandle, anim.anim, metaCallback); - } - }); - }, - - /** - * Stop any and all animations operating on the View with native node handle - * `nodeHandle`. - * - * @param {NodeHandle} component The instance to stop animations - * on. Do not pass a composite component. - */ - stopNodeHandleAnimations: function(nodeHandle: any) { - if (!this.state._currentAnimationsByNodeHandle[nodeHandle]) { - return; - } - var anims = this.state._currentAnimationsByNodeHandle[nodeHandle]; - for (var i = 0; i < anims.length; i++) { - var anim = anims[i]; - if (anim) { - // Note: Converting the string key to a number `nodeHandle`. - POPAnimation.removeAnimation(+nodeHandle, anim); - } - } - this.state._currentAnimationsByNodeHandle[nodeHandle] = undefined; - }, - - /** - * Stop any and all animations operating on the View with ref `refKey`. - * - * @param {key} refKey The key to reference the View to be animated. - */ - stopAnimations: function(refKey: string) { - invariant(this.refs[refKey], 'invalid ref'); - this.stopNodeHandleAnimations(React.findNodeHandle(this.refs[refKey])); - }, - - /** - * Stop any and all animations created by this component on itself and - * subviews. - */ - stopAllAnimations: function() { - for (var nodeHandle in this.state._currentAnimationsByNodeHandle) { - this.stopNodeHandleAnimations(nodeHandle); - } - }, - - /** - * Animates size and position of a view referenced by `refKey` to a specific - * frame. - * - * @param {key} refKey ref key for view to animate. - * @param {Object} frame The frame to animate the view to, specified as {left, - * top, width, height}. - * @param {const} type What type of interpolation to use, selected from - * `inperpolationTypes`. - * @param {Object} event Event encapsulating synthetic and native data that - * may have triggered this animation. Velocity is extracted from it if - * possible and applied to the animation. - * @param {func} doneCallback A callback fired when the animation is done, and - * is passed a `finished` param that indicates whether the animation - * completely finished, or was interrupted. - */ - animateToFrame: function( - refKey: string, - frame: {left: number; top: number; width: number; height: number;}, - type: number, - velocity: number, - doneCallback: (finished: boolean) => void - ) { - var animFrame = { // Animations use a centered coordinate system. - x: frame.left + frame.width / 2, - y: frame.top + frame.height / 2, - w: frame.width, - h: frame.height - }; - var posAnim = POPAnimation.createAnimation(type, { - property: POPAnimation.Properties.position, - toValue: [animFrame.x, animFrame.y], - velocity: velocity || [0, 0], - }); - var sizeAnim = POPAnimation.createAnimation(type, { - property: POPAnimation.Properties.size, - toValue: [animFrame.w, animFrame.h] - }); - this.startAnimation(refKey, posAnim, doneCallback); - this.startAnimation(refKey, sizeAnim); - }, - - // Cleanup any potentially leaked animations. - componentWillUnmount: function() { - this.stopAllAnimations(); - this._popAnimationEnqueuedAnimationTimeouts.forEach(animationTimeoutHandler => { - clearTimeout(animationTimeoutHandler); - }); - this._popAnimationEnqueuedAnimationTimeouts = []; - } -}; - -module.exports = POPAnimationMixin; - -} diff --git a/Libraries/AppStateIOS/AppStateIOS.ios.js b/Libraries/AppStateIOS/AppStateIOS.ios.js index f7dddcd57..1d64c7d64 100644 --- a/Libraries/AppStateIOS/AppStateIOS.ios.js +++ b/Libraries/AppStateIOS/AppStateIOS.ios.js @@ -122,11 +122,7 @@ var AppStateIOS = { _eventHandlers[type].delete(handler); }, - // TODO: getCurrentAppState callback seems to be called at a really late stage - // after app launch. Trying to get currentState when mounting App component - // will likely to have the initial value here. - // Initialize to 'active' instead of null. - currentState: ('active' : ?string), + currentState: (RCTAppState && RCTAppState.initialAppState : ?string), }; diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js b/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js deleted file mode 100644 index c8e289431..000000000 --- a/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js +++ /dev/null @@ -1,187 +0,0 @@ -/** - * 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 POPAnimation - */ -'use strict'; - -var RCTPOPAnimationManager = require('NativeModules').POPAnimationManager; -if (!RCTPOPAnimationManager) { - // POP animation isn't available in the OSS fork - this is a temporary - // workaround to enable its availability to be determined at runtime. - // For Flow let's pretend like we always export POPAnimation - // so all our users don't need to do null checks - module.exports = null; -} else { - -var ReactPropTypes = require('ReactPropTypes'); -var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker'); -var getObjectValues = require('getObjectValues'); -var invariant = require('invariant'); -var merge = require('merge'); - -var RCTTypes = RCTPOPAnimationManager.Types; -var RCTProperties = RCTPOPAnimationManager.Properties; - -var Properties = { - bounds: RCTProperties.bounds, - opacity: RCTProperties.opacity, - position: RCTProperties.position, - positionX: RCTProperties.positionX, - positionY: RCTProperties.positionY, - zPosition: RCTProperties.zPosition, - rotation: RCTProperties.rotation, - rotationX: RCTProperties.rotationX, - rotationY: RCTProperties.rotationY, - scaleX: RCTProperties.scaleX, - scaleXY: RCTProperties.scaleXY, - scaleY: RCTProperties.scaleY, - shadowColor: RCTProperties.shadowColor, - shadowOffset: RCTProperties.shadowOffset, - shadowOpacity: RCTProperties.shadowOpacity, - shadowRadius: RCTProperties.shadowRadius, - size: RCTProperties.size, - subscaleXY: RCTProperties.subscaleXY, - subtranslationX: RCTProperties.subtranslationX, - subtranslationXY: RCTProperties.subtranslationXY, - subtranslationY: RCTProperties.subtranslationY, - subtranslationZ: RCTProperties.subtranslationZ, - translationX: RCTProperties.translationX, - translationXY: RCTProperties.translationXY, - translationY: RCTProperties.translationY, - translationZ: RCTProperties.translationZ, -}; - -var Types = { - decay: RCTTypes.decay, - easeIn: RCTTypes.easeIn, - easeInEaseOut: RCTTypes.easeInEaseOut, - easeOut: RCTTypes.easeOut, - linear: RCTTypes.linear, - spring: RCTTypes.spring, -}; - -type Attrs = { - type?: $Enum; - property?: $Enum; - fromValue?: any; - toValue?: any; - duration?: any; - velocity?: any; - deceleration?: any; - springBounciness?: any; - dynamicsFriction?: any; - dynamicsMass?: any; - dynamicsTension?: any; -} - -var POPAnimation = { - Types: Types, - Properties: Properties, - - attributeChecker: createStrictShapeTypeChecker({ - type: ReactPropTypes.oneOf(getObjectValues(Types)), - property: ReactPropTypes.oneOf(getObjectValues(Properties)), - fromValue: ReactPropTypes.any, - toValue: ReactPropTypes.any, - duration: ReactPropTypes.any, - velocity: ReactPropTypes.any, - deceleration: ReactPropTypes.any, - springBounciness: ReactPropTypes.any, - dynamicsFriction: ReactPropTypes.any, - dynamicsMass: ReactPropTypes.any, - dynamicsTension: ReactPropTypes.any, - }), - - lastUsedTag: 0, - allocateTagForAnimation: function(): number { - return ++this.lastUsedTag; - }, - - createAnimation: function(typeName: number, attrs: Attrs): number { - var tag = this.allocateTagForAnimation(); - - if (__DEV__) { - POPAnimation.attributeChecker( - {attrs}, - 'attrs', - 'POPAnimation.createAnimation' - ); - POPAnimation.attributeChecker( - {attrs: {type: typeName}}, - 'attrs', - 'POPAnimation.createAnimation' - ); - } - - RCTPOPAnimationManager.createAnimationInternal(tag, typeName, attrs); - return tag; - }, - - createSpringAnimation: function(attrs: Attrs): number { - return this.createAnimation(this.Types.spring, attrs); - }, - - createDecayAnimation: function(attrs: Attrs): number { - return this.createAnimation(this.Types.decay, attrs); - }, - - createLinearAnimation: function(attrs: Attrs): number { - return this.createAnimation(this.Types.linear, attrs); - }, - - createEaseInAnimation: function(attrs: Attrs): number { - return this.createAnimation(this.Types.easeIn, attrs); - }, - - createEaseOutAnimation: function(attrs: Attrs): number { - return this.createAnimation(this.Types.easeOut, attrs); - }, - - createEaseInEaseOutAnimation: function(attrs: Attrs): number { - return this.createAnimation(this.Types.easeInEaseOut, attrs); - }, - - addAnimation: function(nodeHandle: any, anim: number, callback: Function) { - RCTPOPAnimationManager.addAnimation(nodeHandle, anim, callback); - }, - - removeAnimation: function(nodeHandle: any, anim: number) { - RCTPOPAnimationManager.removeAnimation(nodeHandle, anim); - }, -}; - -// Make sure that we correctly propagate RCTPOPAnimationManager constants -// to POPAnimation -if (__DEV__) { - var allProperties = merge( - RCTPOPAnimationManager.Properties, - RCTPOPAnimationManager.Properties - ); - for (var key in allProperties) { - invariant( - POPAnimation.Properties[key] === RCTPOPAnimationManager.Properties[key], - 'POPAnimation doesn\'t copy property ' + key + ' correctly' - ); - } - - var allTypes = merge( - RCTPOPAnimationManager.Types, - RCTPOPAnimationManager.Types - ); - for (var key in allTypes) { - invariant( - POPAnimation.Types[key] === RCTPOPAnimationManager.Types[key], - 'POPAnimation doesn\'t copy type ' + key + ' correctly' - ); - } -} - -module.exports = POPAnimation; - -} diff --git a/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js new file mode 100644 index 000000000..de4dbf9d8 --- /dev/null +++ b/Libraries/Components/ProgressBarAndroid/ProgressBarAndroid.ios.js @@ -0,0 +1,8 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule ProgressBarAndroid + */ +'use strict'; + +module.exports = require('UnimplementedView'); diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 920b56478..b0f132075 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -181,7 +181,7 @@ var ScrollResponderMixin = { scrollResponderHandleStartShouldSetResponderCapture: function(e: Event): boolean { // First see if we want to eat taps while the keyboard is up var currentlyFocusedTextInput = TextInputState.currentlyFocusedField(); - if (!this.props.keyboardShouldPersistTaps && + if (this.props.keyboardShouldPersistTaps === false && currentlyFocusedTextInput != null && e.target !== currentlyFocusedTextInput) { return true; @@ -242,7 +242,7 @@ var ScrollResponderMixin = { // By default scroll views will unfocus a textField // if another touch occurs outside of it var currentlyFocusedTextInput = TextInputState.currentlyFocusedField(); - if (!this.props.keyboardShouldPersistTaps && + if (this.props.keyboardShouldPersistTaps === false && currentlyFocusedTextInput != null && e.target !== currentlyFocusedTextInput && !this.state.observedScrollSinceBecomingResponder && diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 765934bfc..1ee37a84f 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -15,7 +15,6 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType'); var Platform = require('Platform'); var PointPropType = require('PointPropType'); var RCTScrollView = require('NativeModules').UIManager.RCTScrollView; -var RCTScrollViewConsts = RCTScrollView.Constants; var React = require('React'); var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); var RCTUIManager = require('NativeModules').UIManager; @@ -27,6 +26,7 @@ var ViewStylePropTypes = require('ViewStylePropTypes'); var createReactNativeComponentClass = require('createReactNativeComponentClass'); var deepDiffer = require('deepDiffer'); +var dismissKeyboard = require('dismissKeyboard'); var flattenStyle = require('flattenStyle'); var insetsDiffer = require('insetsDiffer'); var invariant = require('invariant'); @@ -156,9 +156,9 @@ var ScrollView = React.createClass({ * Determines whether the keyboard gets dismissed in response to a drag. * - 'none' (the default), drags do not dismiss the keyboard. * - 'on-drag', the keyboard is dismissed when a drag begins. - * - 'interactive', the keyboard is dismissed interactively with the drag - * and moves in synchrony with the touch; dragging upwards cancels the - * dismissal. + * - 'interactive', the keyboard is dismissed interactively with the drag and moves in + * synchrony with the touch; dragging upwards cancels the dismissal. + * On android this is not supported and it will have the same behavior as 'none'. */ keyboardDismissMode: PropTypes.oneOf([ 'none', // default @@ -170,7 +170,6 @@ var ScrollView = React.createClass({ * is up dismisses the keyboard. When true, the scroll view will not catch * taps, and the keyboard will not dismiss automatically. The default value * is false. - * @platform ios */ keyboardShouldPersistTaps: PropTypes.bool, /** @@ -310,6 +309,11 @@ var ScrollView = React.createClass({ ); } } + if (Platform.OS === 'android') { + if (this.props.keyboardDismissMode === 'on-drag') { + dismissKeyboard(); + } + } this.scrollResponderHandleScroll(e); }, @@ -380,13 +384,6 @@ var ScrollView = React.createClass({ } else { ScrollViewClass = AndroidScrollView; } - var keyboardDismissModeConstants = { - 'none': RCTScrollViewConsts.KeyboardDismissMode.None, // default - 'interactive': RCTScrollViewConsts.KeyboardDismissMode.Interactive, - 'on-drag': RCTScrollViewConsts.KeyboardDismissMode.OnDrag, - }; - props.keyboardDismissMode = props.keyboardDismissMode ? - keyboardDismissModeConstants[props.keyboardDismissMode] : undefined; } invariant( ScrollViewClass !== undefined, diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js index b27e22d4b..f44fb0493 100644 --- a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js @@ -7,7 +7,6 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule TabBarItemIOS - * @flow */ 'use strict'; @@ -108,22 +107,15 @@ var TabBarItemIOS = React.createClass({ tabContents = ; } - var icon = this.props.systemIcon || ( - this.props.icon && this.props.icon.uri - ); - var badge = typeof this.props.badge === 'number' ? '' + this.props.badge : this.props.badge; return ( {tabContents} diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index bada73041..487836a29 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -18,6 +18,7 @@ var Touchable = require('Touchable'); var merge = require('merge'); +type Event = Object; type State = { animationID: ?number; }; @@ -41,6 +42,8 @@ var TouchableBounce = React.createClass({ propTypes: { onPress: React.PropTypes.func, + onPressIn: React.PropTypes.func, + onPressOut: React.PropTypes.func, // The function passed takes a callback to start the animation which should // be run after this onPress handler is done. You can use this (for example) // to update UI before starting the animation. @@ -73,15 +76,17 @@ var TouchableBounce = React.createClass({ * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function() { + touchableHandleActivePressIn: function(e: Event) { this.bounceTo(0.93, 0.1, 0); + this.props.onPressIn && this.props.onPressIn(e); }, - touchableHandleActivePressOut: function() { + touchableHandleActivePressOut: function(e: Event) { this.bounceTo(1, 0.4, 0); + this.props.onPressOut && this.props.onPressOut(e); }, - touchableHandlePress: function() { + touchableHandlePress: function(e: Event) { var onPressWithCompletion = this.props.onPressWithCompletion; if (onPressWithCompletion) { onPressWithCompletion(() => { @@ -92,7 +97,7 @@ var TouchableBounce = React.createClass({ } this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete); - this.props.onPress && this.props.onPress(); + this.props.onPress && this.props.onPress(e); }, touchableGetPressRectOffset: function(): typeof PRESS_RECT_OFFSET { diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index f15ac8929..4b6442ed4 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -28,6 +28,8 @@ var keyOf = require('keyOf'); var merge = require('merge'); var onlyChild = require('onlyChild'); +type Event = Object; + var DEFAULT_PROPS = { activeOpacity: 0.8, underlayColor: 'black', @@ -138,30 +140,30 @@ var TouchableHighlight = React.createClass({ * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function() { + touchableHandleActivePressIn: function(e: Event) { this.clearTimeout(this._hideTimeout); this._hideTimeout = null; this._showUnderlay(); - this.props.onPressIn && this.props.onPressIn(); + this.props.onPressIn && this.props.onPressIn(e); }, - touchableHandleActivePressOut: function() { + touchableHandleActivePressOut: function(e: Event) { if (!this._hideTimeout) { this._hideUnderlay(); } - this.props.onPressOut && this.props.onPressOut(); + this.props.onPressOut && this.props.onPressOut(e); }, - touchableHandlePress: function() { + touchableHandlePress: function(e: Event) { this.clearTimeout(this._hideTimeout); this._showUnderlay(); this._hideTimeout = this.setTimeout(this._hideUnderlay, this.props.delayPressOut || 100); - this.props.onPress && this.props.onPress(); + this.props.onPress && this.props.onPress(e); }, - touchableHandleLongPress: function() { - this.props.onLongPress && this.props.onLongPress(); + touchableHandleLongPress: function(e: Event) { + this.props.onLongPress && this.props.onLongPress(e); }, touchableGetPressRectOffset: function() { @@ -214,12 +216,12 @@ var TouchableHighlight = React.createClass({ onResponderGrant={this.touchableHandleResponderGrant} onResponderMove={this.touchableHandleResponderMove} onResponderRelease={this.touchableHandleResponderRelease} - onResponderTerminate={this.touchableHandleResponderTerminate}> + onResponderTerminate={this.touchableHandleResponderTerminate} + testID={this.props.testID}> {cloneWithProps( onlyChild(this.props.children), { ref: CHILD_REF, - testID: this.props.testID, } )} diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 39117aa93..7564a1b6f 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -23,6 +23,8 @@ var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var flattenStyle = require('flattenStyle'); var keyOf = require('keyOf'); +type Event = Object; + /** * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, dimming it. @@ -77,9 +79,6 @@ var TouchableOpacity = React.createClass({ ensurePositiveDelayProps(this.props); }, - componentDidUpdate: function() { - }, - componentWillReceiveProps: function(nextProps) { ensurePositiveDelayProps(nextProps); }, @@ -95,32 +94,32 @@ var TouchableOpacity = React.createClass({ * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. */ - touchableHandleActivePressIn: function() { + touchableHandleActivePressIn: function(e: Event) { this.clearTimeout(this._hideTimeout); this._hideTimeout = null; this._opacityActive(); - this.props.onPressIn && this.props.onPressIn(); + this.props.onPressIn && this.props.onPressIn(e); }, - touchableHandleActivePressOut: function() { + touchableHandleActivePressOut: function(e: Event) { if (!this._hideTimeout) { this._opacityInactive(); } - this.props.onPressOut && this.props.onPressOut(); + this.props.onPressOut && this.props.onPressOut(e); }, - touchableHandlePress: function() { + touchableHandlePress: function(e: Event) { this.clearTimeout(this._hideTimeout); this._opacityActive(); this._hideTimeout = this.setTimeout( this._opacityInactive, this.props.delayPressOut || 100 ); - this.props.onPress && this.props.onPress(); + this.props.onPress && this.props.onPress(e); }, - touchableHandleLongPress: function() { - this.props.onLongPress && this.props.onLongPress(); + touchableHandleLongPress: function(e: Event) { + this.props.onLongPress && this.props.onLongPress(e); }, touchableGetPressRectOffset: function() { diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 227cbeae2..d8b171182 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -17,6 +17,8 @@ var Touchable = require('Touchable'); var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var onlyChild = require('onlyChild'); +type Event = Object; + /** * When the scroll view is disabled, this defines how far your touch may move * off of the button, before deactivating the button. Once deactivated, try @@ -25,8 +27,6 @@ var onlyChild = require('onlyChild'); */ var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; -type Event = Object; - /** * Do not use unless you have a very good reason. All the elements that * respond to press should have a visual feedback when touched. This is @@ -79,16 +79,16 @@ var TouchableWithoutFeedback = React.createClass({ this.props.onPress && this.props.onPress(e); }, - touchableHandleActivePressIn: function() { - this.props.onPressIn && this.props.onPressIn(); + touchableHandleActivePressIn: function(e: Event) { + this.props.onPressIn && this.props.onPressIn(e); }, - touchableHandleActivePressOut: function() { - this.props.onPressOut && this.props.onPressOut(); + touchableHandleActivePressOut: function(e: Event) { + this.props.onPressOut && this.props.onPressOut(e); }, - touchableHandleLongPress: function() { - this.props.onLongPress && this.props.onLongPress(); + touchableHandleLongPress: function(e: Event) { + this.props.onLongPress && this.props.onLongPress(e); }, touchableGetPressRectOffset: function(): typeof PRESS_RECT_OFFSET { diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 5566cc7c9..dc1329658 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -44,13 +44,6 @@ var AccessibilityTraits = [ 'pageTurn', ]; - -// <<<<< WARNING >>>>> -// If adding any properties to View that could change the way layout-only status -// works on iOS, make sure to update ReactNativeViewAttributes.js and -// RCTShadowView.m (in the -[RCTShadowView isLayoutOnly] method). -// <<<<< WARNING >>>>> - /** * The most fundamental component for building UI, `View` is a * container that supports layout with flexbox, style, some touch handling, and @@ -100,10 +93,13 @@ var View = React.createClass({ /** * Indicates to accessibility services to treat UI component like a * native one. Works for Android only. + * @platform android */ accessibilityComponentType: PropTypes.oneOf([ 'none', 'button', + 'radiobutton_checked', + 'radiobutton_unchecked', ]), /** @@ -111,6 +107,7 @@ var View = React.createClass({ * when this view changes. Works for Android API >= 19 only. * See http://developer.android.com/reference/android/view/View.html#attr_android:accessibilityLiveRegion * for references. + * @platform android */ accessibilityLiveRegion: PropTypes.oneOf([ 'none', @@ -118,9 +115,33 @@ var View = React.createClass({ 'assertive', ]), + /** + * Controls how view is important for accessibility which is if it + * fires accessibility events and if it is reported to accessibility services + * that query the screen. Works for Android only. + * See http://developer.android.com/reference/android/R.attr.html#importantForAccessibility + * for references. + * Possible values: + * 'auto' - The system determines whether the view is important for accessibility - + * default (recommended). + * 'yes' - The view is important for accessibility. + * 'no' - The view is not important for accessibility. + * 'no-hide-descendants' - The view is not important for accessibility, + * nor are any of its descendant views. + * + * @platform android + */ + importantForAccessibility: PropTypes.oneOf([ + 'auto', + 'yes', + 'no', + 'no-hide-descendants', + ]), + /** * Provides additional traits to screen reader. By default no traits are * provided unless specified otherwise in element + * @platform ios */ accessibilityTraits: PropTypes.oneOfType([ PropTypes.oneOf(AccessibilityTraits), diff --git a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js index 7f8debc84..ddfd33266 100644 --- a/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js +++ b/Libraries/CustomComponents/Navigator/NavigatorBreadcrumbNavigationBar.js @@ -119,16 +119,16 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({ } if (interpolate.Crumb(CRUMB_PROPS[index].style, amount)) { - this.refs['crumb_' + index].setNativeProps(CRUMB_PROPS[index]); + this._setPropsIfExists('crumb_' + index, CRUMB_PROPS[index]); } if (interpolate.Icon(ICON_PROPS[index].style, amount)) { - this.refs['icon_' + index].setNativeProps(ICON_PROPS[index]); + this._setPropsIfExists('icon_' + index, ICON_PROPS[index]); } if (interpolate.Separator(SEPARATOR_PROPS[index].style, amount)) { - this.refs['separator_' + index].setNativeProps(SEPARATOR_PROPS[index]); + this._setPropsIfExists('separator_' + index, SEPARATOR_PROPS[index]); } if (interpolate.Title(TITLE_PROPS[index].style, amount)) { - this.refs['title_' + index].setNativeProps(TITLE_PROPS[index]); + this._setPropsIfExists('title_' + index, TITLE_PROPS[index]); } var right = this.refs['right_' + index]; if (right && @@ -165,13 +165,10 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({ renderToHardwareTextureAndroid: renderToHardwareTexture, }; - this.refs['icon_' + index].setNativeProps(props); - this.refs['separator_' + index].setNativeProps(props); - this.refs['title_' + index].setNativeProps(props); - var right = this.refs['right_' + index]; - if (right) { - right.setNativeProps(props); - } + this._setPropsIfExists('icon_' + index, props); + this._setPropsIfExists('separator_' + index, props); + this._setPropsIfExists('title_' + index, props); + this._setPropsIfExists('right_' + index, props); }, componentWillMount: function() { @@ -260,6 +257,11 @@ var NavigatorBreadcrumbNavigationBar = React.createClass({ this._descriptors.right = this._descriptors.right.set(route, rightButtonDescriptor); return rightButtonDescriptor; }, + + _setPropsIfExists: function(ref, props) { + var ref = this.refs[ref]; + ref && ref.setNativeProps(props); + }, }); var styles = StyleSheet.create({ diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m index 0fe5fed75..54857c265 100644 --- a/Libraries/Geolocation/RCTLocationObserver.m +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -115,11 +115,11 @@ RCT_EXPORT_MODULE() { if ((self = [super init])) { - _locationManager = [[CLLocationManager alloc] init]; + _locationManager = [CLLocationManager new]; _locationManager.distanceFilter = RCT_DEFAULT_LOCATION_ACCURACY; _locationManager.delegate = self; - _pendingRequests = [[NSMutableArray alloc] init]; + _pendingRequests = [NSMutableArray new]; } return self; } @@ -231,7 +231,7 @@ RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options } // Create request - RCTLocationRequest *request = [[RCTLocationRequest alloc] init]; + RCTLocationRequest *request = [RCTLocationRequest new]; request.successBlock = successBlock; request.errorBlock = errorBlock ?: ^(NSArray *args){}; request.options = options; @@ -252,7 +252,7 @@ RCT_EXPORT_METHOD(getCurrentPosition:(RCTLocationOptions)options - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { // Create event - CLLocation *location = [locations lastObject]; + CLLocation *location = locations.lastObject; _lastLocationEvent = @{ @"coords": @{ @"latitude": @(location.coordinate.latitude), diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 7517b2e96..f641167c7 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -60,9 +60,13 @@ var Image = React.createClass({ * could be an http address, a local file path, or the name of a static image * resource (which should be wrapped in the `require('image!name')` function). */ - source: PropTypes.shape({ - uri: PropTypes.string, - }), + source: PropTypes.oneOfType([ + PropTypes.shape({ + uri: PropTypes.string, + }), + // Opaque type returned by require('./image.jpg') + PropTypes.number, + ]), /** * A static image to display while downloading the final image off the * network. diff --git a/Libraries/Image/RCTCameraRollManager.m b/Libraries/Image/RCTCameraRollManager.m index d28198ac0..e2f2a297f 100644 --- a/Libraries/Image/RCTCameraRollManager.m +++ b/Libraries/Image/RCTCameraRollManager.m @@ -34,12 +34,12 @@ RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag errorCallback(loadError); return; } - [_bridge.assetsLibrary writeImageToSavedPhotosAlbum:[loadedImage CGImage] metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { + [_bridge.assetsLibrary writeImageToSavedPhotosAlbum:loadedImage.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { if (saveError) { RCTLogWarn(@"Error saving cropped image: %@", saveError); errorCallback(saveError); } else { - successCallback(@[[assetURL absoluteString]]); + successCallback(@[assetURL.absoluteString]); } }]; }]; @@ -47,7 +47,7 @@ RCT_EXPORT_METHOD(saveImageWithTag:(NSString *)imageTag - (void)callCallback:(RCTResponseSenderBlock)callback withAssets:(NSArray *)assets hasNextPage:(BOOL)hasNextPage { - if (![assets count]) { + if (!assets.count) { callback(@[@{ @"edges": assets, @"page_info": @{ @@ -94,7 +94,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params BOOL __block foundAfter = NO; BOOL __block hasNextPage = NO; BOOL __block calledCallback = NO; - NSMutableArray *assets = [[NSMutableArray alloc] init]; + NSMutableArray *assets = [NSMutableArray new]; [_bridge.assetsLibrary enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) { if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:ALAssetsGroupPropertyName]])) { @@ -109,14 +109,14 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) { if (result) { - NSString *uri = [(NSURL *)[result valueForProperty:ALAssetPropertyAssetURL] absoluteString]; + NSString *uri = ((NSURL *)[result valueForProperty:ALAssetPropertyAssetURL]).absoluteString; if (afterCursor && !foundAfter) { if ([afterCursor isEqualToString:uri]) { foundAfter = YES; } return; // Skip until we get to the first one } - if (first == [assets count]) { + if (first == assets.count) { *stopAssets = YES; *stopGroups = YES; hasNextPage = YES; @@ -138,7 +138,7 @@ RCT_EXPORT_METHOD(getPhotos:(NSDictionary *)params @"width": @(dimensions.width), @"isStored": @YES, }, - @"timestamp": @([date timeIntervalSince1970]), + @"timestamp": @(date.timeIntervalSince1970), @"location": loc ? @{ @"latitude": @(loc.coordinate.latitude), diff --git a/Libraries/Image/RCTGIFImage.m b/Libraries/Image/RCTGIFImage.m index 99704e305..4258262ed 100644 --- a/Libraries/Image/RCTGIFImage.m +++ b/Libraries/Image/RCTGIFImage.m @@ -85,7 +85,7 @@ CAKeyframeAnimation *RCTGIFImageWithFileURL(NSURL *URL) return nil; } - if (![URL isFileURL]) { + if (!URL.fileURL) { RCTLogError(@"Loading remote image URLs synchronously is a really bad idea."); return nil; } diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index 8bae60112..22bbe9031 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -15,9 +15,6 @@ #import "RCTNetworking.h" #import "RCTUtils.h" -CGSize RCTTargetSizeForClipRect(CGRect); -CGRect RCTClipRect(CGSize, CGFloat, CGSize, CGFloat, UIViewContentMode); - @implementation RCTImageDownloader { NSURLCache *_cache; @@ -33,7 +30,7 @@ RCT_EXPORT_MODULE() static RCTImageDownloader *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - sharedInstance = [[RCTImageDownloader alloc] init]; + sharedInstance = [RCTImageDownloader new]; }); return sharedInstance; } @@ -131,14 +128,14 @@ RCT_EXPORT_MODULE() UIImage *image = [UIImage imageWithData:data scale:scale]; if (image && !CGSizeEqualToSize(size, CGSizeZero)) { - // Get scale and size - CGRect imageRect = RCTClipRect(image.size, scale, size, scale, resizeMode); - CGSize destSize = RCTTargetSizeForClipRect(imageRect); + // Get destination size + CGSize targetSize = RCTTargetSize(image.size, image.scale, + size, scale, resizeMode, NO); // Decompress image at required size BOOL opaque = !RCTImageHasAlpha(image.CGImage); - UIGraphicsBeginImageContextWithOptions(destSize, opaque, scale); - [image drawInRect:imageRect]; + UIGraphicsBeginImageContextWithOptions(targetSize, opaque, scale); + [image drawInRect:(CGRect){CGPointZero, targetSize}]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); } diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m index 34a7dc2fc..a72e01ef2 100644 --- a/Libraries/Image/RCTImageLoader.m +++ b/Libraries/Image/RCTImageLoader.m @@ -85,8 +85,8 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation, } CGSize sourceSize = representation.dimensions; - CGRect targetRect = RCTClipRect(sourceSize, representation.scale, size, scale, resizeMode); - CGSize targetSize = targetRect.size; + CGSize targetSize = RCTTargetSize(sourceSize, representation.scale, + size, scale, resizeMode, NO); NSDictionary *options = @{ (id)kCGImageSourceShouldAllowFloat: @YES, @@ -103,7 +103,7 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation, if (imageRef) { UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale - orientation:(UIImageOrientation)representation.orientation]; + orientation:UIImageOrientationUp]; CGImageRelease(imageRef); return image; } @@ -114,7 +114,7 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation, - (ALAssetsLibrary *)assetsLibrary { if (!_assetsLibrary) { - _assetsLibrary = [[ALAssetsLibrary alloc] init]; + _assetsLibrary = [ALAssetsLibrary new]; } return _assetsLibrary; } @@ -170,7 +170,7 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation, // The 'ph://' prefix is used by FBMediaKit to differentiate between // assets-library. It is prepended to the local ID so that it is in the // form of an, NSURL which is what assets-library uses. - NSString *phAssetID = [imageTag substringFromIndex:[@"ph://" length]]; + NSString *phAssetID = [imageTag substringFromIndex:@"ph://".length]; PHFetchResult *results = [PHAsset fetchAssetsWithLocalIdentifiers:@[phAssetID] options:nil]; if (results.count == 0) { NSString *errorText = [NSString stringWithFormat:@"Failed to fetch PHAsset with local identifier %@ with no error message.", phAssetID]; @@ -179,9 +179,9 @@ static UIImage *RCTScaledImageForAsset(ALAssetRepresentation *representation, return ^{}; } - PHAsset *asset = [results firstObject]; + PHAsset *asset = results.firstObject; - PHImageRequestOptions *imageOptions = [[PHImageRequestOptions alloc] init]; + PHImageRequestOptions *imageOptions = [PHImageRequestOptions new]; BOOL useMaximumSize = CGSizeEqualToSize(size, CGSizeZero); CGSize targetSize; diff --git a/Libraries/Image/RCTImagePickerManager.m b/Libraries/Image/RCTImagePickerManager.m index 7fad953b0..00a28a665 100644 --- a/Libraries/Image/RCTImagePickerManager.m +++ b/Libraries/Image/RCTImagePickerManager.m @@ -31,9 +31,9 @@ RCT_EXPORT_MODULE(ImagePickerIOS); - (instancetype)init { if ((self = [super init])) { - _pickers = [[NSMutableArray alloc] init]; - _pickerCallbacks = [[NSMutableArray alloc] init]; - _pickerCancelCallbacks = [[NSMutableArray alloc] init]; + _pickers = [NSMutableArray new]; + _pickerCallbacks = [NSMutableArray new]; + _pickerCancelCallbacks = [NSMutableArray new]; } return self; } @@ -53,10 +53,10 @@ RCT_EXPORT_METHOD(openCameraDialog:(NSDictionary *)config successCallback:(RCTResponseSenderBlock)callback cancelCallback:(RCTResponseSenderBlock)cancelCallback) { - UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; UIViewController *rootViewController = keyWindow.rootViewController; - UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; + UIImagePickerController *imagePicker = [UIImagePickerController new]; imagePicker.delegate = self; imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; @@ -75,14 +75,14 @@ RCT_EXPORT_METHOD(openSelectDialog:(NSDictionary *)config successCallback:(RCTResponseSenderBlock)callback cancelCallback:(RCTResponseSenderBlock)cancelCallback) { - UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; UIViewController *rootViewController = keyWindow.rootViewController; - UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; + UIImagePickerController *imagePicker = [UIImagePickerController new]; imagePicker.delegate = self; imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - NSMutableArray *allowedTypes = [[NSMutableArray alloc] init]; + NSMutableArray *allowedTypes = [NSMutableArray new]; if ([config[@"showImages"] boolValue]) { [allowedTypes addObject:(NSString *)kUTTypeImage]; } @@ -109,7 +109,7 @@ didFinishPickingMediaWithInfo:(NSDictionary *)info [_pickerCallbacks removeObjectAtIndex:index]; [_pickerCancelCallbacks removeObjectAtIndex:index]; - UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; UIViewController *rootViewController = keyWindow.rootViewController; [rootViewController dismissViewControllerAnimated:YES completion:nil]; @@ -125,7 +125,7 @@ didFinishPickingMediaWithInfo:(NSDictionary *)info [_pickerCallbacks removeObjectAtIndex:index]; [_pickerCancelCallbacks removeObjectAtIndex:index]; - UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; UIViewController *rootViewController = keyWindow.rootViewController; [rootViewController dismissViewControllerAnimated:YES completion:nil]; diff --git a/Libraries/Image/RCTImageRequestHandler.m b/Libraries/Image/RCTImageRequestHandler.m index 595c94247..1b3d76df1 100644 --- a/Libraries/Image/RCTImageRequestHandler.m +++ b/Libraries/Image/RCTImageRequestHandler.m @@ -22,13 +22,13 @@ RCT_EXPORT_MODULE() - (BOOL)canHandleRequest:(NSURLRequest *)request { - return [@[@"assets-library", @"ph"] containsObject:[request.URL.scheme lowercaseString]]; + return [@[@"assets-library", @"ph"] containsObject:request.URL.scheme.lowercaseString]; } - (id)sendRequest:(NSURLRequest *)request withDelegate:(id)delegate { - NSString *URLString = [request.URL absoluteString]; + NSString *URLString = request.URL.absoluteString; __block RCTImageLoaderCancellationBlock requestToken = nil; requestToken = [_bridge.imageLoader loadImageWithTag:URLString callback:^(NSError *error, UIImage *image) { diff --git a/Libraries/Image/RCTImageStoreManager.m b/Libraries/Image/RCTImageStoreManager.m index e751466f9..3ff66ce59 100644 --- a/Libraries/Image/RCTImageStoreManager.m +++ b/Libraries/Image/RCTImageStoreManager.m @@ -21,12 +21,12 @@ RCT_EXPORT_MODULE() -- (id)init +- (instancetype)init { if ((self = [super init])) { // TODO: need a way to clear this store - _store = [[NSMutableDictionary alloc] init]; + _store = [NSMutableDictionary new]; } return self; } @@ -34,7 +34,7 @@ RCT_EXPORT_MODULE() - (NSString *)storeImage:(UIImage *)image { RCTAssertMainThread(); - NSString *tag = [NSString stringWithFormat:@"rct-image-store://%tu", [_store count]]; + NSString *tag = [NSString stringWithFormat:@"rct-image-store://%tu", _store.count]; _store[tag] = image; return tag; } @@ -101,13 +101,13 @@ RCT_EXPORT_METHOD(addImageFromBase64:(NSString *)base64String - (BOOL)canHandleRequest:(NSURLRequest *)request { - return [@[@"rct-image-store"] containsObject:[request.URL.scheme lowercaseString]]; + return [@[@"rct-image-store"] containsObject:request.URL.scheme.lowercaseString]; } - (id)sendRequest:(NSURLRequest *)request withDelegate:(id)delegate { - NSString *imageTag = [request.URL absoluteString]; + NSString *imageTag = request.URL.absoluteString; [self getImageForTag:imageTag withBlock:^(UIImage *image) { if (!image) { NSError *error = RCTErrorWithMessage([NSString stringWithFormat:@"Invalid imageTag: %@", imageTag]); diff --git a/Libraries/Image/RCTImageUtils.h b/Libraries/Image/RCTImageUtils.h index cbb38cda8..1bca23241 100644 --- a/Libraries/Image/RCTImageUtils.h +++ b/Libraries/Image/RCTImageUtils.h @@ -13,20 +13,24 @@ #import "RCTDefines.h" /** - * Returns the optimal context size for an image drawn using the clip rect - * returned by RCTClipRect. + * This function takes an input content size (typically from an image), a target + * size and scale that it will be drawn at (typically in a CGContext) and then + * calculates the rectangle to draw the image into so that it will be sized and + * positioned correctly if drawn using the specified content mode. */ -RCT_EXTERN CGSize RCTTargetSizeForClipRect(CGRect clipRect); +RCT_EXTERN CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, + CGFloat destScale, UIViewContentMode resizeMode); /** * This function takes an input content size & scale (typically from an image), - * a target size & scale that it will be drawn into (typically a CGContext) and - * then calculates the optimal rectangle to draw the image into so that it will - * be sized and positioned correctly if drawn using the specified content mode. + * a target size & scale at which it will be displayed (typically in a + * UIImageView) and then calculates the optimal size at which to redraw the + * image so that it will be displayed correctly with the specified content mode. */ -RCT_EXTERN CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, - CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode); +RCT_EXTERN CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode, + BOOL allowUpscaling); /** * This function takes an input content size & scale (typically from an image), diff --git a/Libraries/Image/RCTImageUtils.m b/Libraries/Image/RCTImageUtils.m index 82a97a778..78008dcad 100644 --- a/Libraries/Image/RCTImageUtils.m +++ b/Libraries/Image/RCTImageUtils.m @@ -29,28 +29,14 @@ static CGSize RCTCeilSize(CGSize size, CGFloat scale) }; } -CGSize RCTTargetSizeForClipRect(CGRect clipRect) -{ - return (CGSize){ - clipRect.size.width + clipRect.origin.x * 2, - clipRect.size.height + clipRect.origin.y * 2 - }; -} - -CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, - CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode) +CGRect RCTTargetRect(CGSize sourceSize, CGSize destSize, + CGFloat destScale, UIViewContentMode resizeMode) { if (CGSizeEqualToSize(destSize, CGSizeZero)) { // Assume we require the largest size available return (CGRect){CGPointZero, sourceSize}; } - // Precompensate for scale - CGFloat scale = sourceScale / destScale; - sourceSize.width *= scale; - sourceSize.height *= scale; - CGFloat aspect = sourceSize.width / sourceSize.height; // If only one dimension in destSize is non-zero (for example, an Image // with `flex: 1` whose height is indeterminate), calculate the unknown @@ -61,7 +47,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, if (destSize.height == 0) { destSize.height = destSize.width / aspect; } - + // Calculate target aspect ratio if needed (don't bother if resizeMode == stretch) CGFloat targetAspect = 0.0; if (resizeMode != UIViewContentModeScaleToFill) { @@ -74,20 +60,18 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, switch (resizeMode) { case UIViewContentModeScaleToFill: // stretch - sourceSize.width = MIN(destSize.width, sourceSize.width); - sourceSize.height = MIN(destSize.height, sourceSize.height); - return (CGRect){CGPointZero, RCTCeilSize(sourceSize, destScale)}; + return (CGRect){CGPointZero, RCTCeilSize(destSize, destScale)}; case UIViewContentModeScaleAspectFit: // contain if (targetAspect <= aspect) { // target is taller than content - sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); + sourceSize.width = destSize.width = destSize.width; sourceSize.height = sourceSize.width / aspect; } else { // target is wider than content - sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); + sourceSize.height = destSize.height = destSize.height; sourceSize.width = sourceSize.height * aspect; } return (CGRect){CGPointZero, RCTCeilSize(sourceSize, destScale)}; @@ -96,7 +80,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, if (targetAspect <= aspect) { // target is taller than content - sourceSize.height = destSize.height = MIN(sourceSize.height, destSize.height); + sourceSize.height = destSize.height = destSize.height; sourceSize.width = sourceSize.height * aspect; destSize.width = destSize.height * targetAspect; return (CGRect){ @@ -106,7 +90,7 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, } else { // target is wider than content - sourceSize.width = destSize.width = MIN(sourceSize.width, destSize.width); + sourceSize.width = destSize.width = destSize.width; sourceSize.height = sourceSize.width / aspect; destSize.height = destSize.width / targetAspect; return (CGRect){ @@ -122,9 +106,39 @@ CGRect RCTClipRect(CGSize sourceSize, CGFloat sourceScale, } } -RCT_EXTERN BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, - CGSize destSize, CGFloat destScale, - UIViewContentMode resizeMode) +CGSize RCTTargetSize(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode, + BOOL allowUpscaling) +{ + switch (resizeMode) { + case UIViewContentModeScaleToFill: // stretch + + if (!allowUpscaling) { + CGFloat scale = sourceScale / destScale; + destSize.width = MIN(sourceSize.width * scale, destSize.width); + destSize.height = MIN(sourceSize.height * scale, destSize.height); + } + return RCTCeilSize(destSize, destScale); + + default: { + + // Get target size + CGSize size = RCTTargetRect(sourceSize, destSize, destScale, resizeMode).size; + if (!allowUpscaling) { + // return sourceSize if target size is larger + if (sourceSize.width * sourceScale < size.width * destScale) { + return sourceSize; + } + } + return size; + } + } +} + +BOOL RCTUpscalingRequired(CGSize sourceSize, CGFloat sourceScale, + CGSize destSize, CGFloat destScale, + UIViewContentMode resizeMode) { if (CGSizeEqualToSize(destSize, CGSizeZero)) { // Assume we require the largest size available diff --git a/Libraries/Image/RCTImageView.m b/Libraries/Image/RCTImageView.m index 56c37b21a..ddbd23d77 100644 --- a/Libraries/Image/RCTImageView.m +++ b/Libraries/Image/RCTImageView.m @@ -14,6 +14,7 @@ #import "RCTEventDispatcher.h" #import "RCTGIFImage.h" #import "RCTImageLoader.h" +#import "RCTImageUtils.h" #import "RCTUtils.h" #import "UIView+React.h" @@ -41,9 +42,9 @@ return self; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) -- (void)_updateImage +- (void)updateImage { UIImage *image = self.image; if (!image) { @@ -72,7 +73,7 @@ RCT_NOT_IMPLEMENTED(-init) image = image ?: _defaultImage; if (image != super.image) { super.image = image; - [self _updateImage]; + [self updateImage]; } } @@ -80,7 +81,7 @@ RCT_NOT_IMPLEMENTED(-init) { if (!UIEdgeInsetsEqualToEdgeInsets(_capInsets, capInsets)) { _capInsets = capInsets; - [self _updateImage]; + [self updateImage]; } } @@ -88,7 +89,7 @@ RCT_NOT_IMPLEMENTED(-init) { if (_renderingMode != renderingMode) { _renderingMode = renderingMode; - [self _updateImage]; + [self updateImage]; } } @@ -100,6 +101,16 @@ RCT_NOT_IMPLEMENTED(-init) } } +- (void)setContentMode:(UIViewContentMode)contentMode +{ + if (self.contentMode != contentMode) { + super.contentMode = contentMode; + if ([RCTImageLoader isAssetLibraryImage:_src] || [RCTImageLoader isRemoteImage:_src]) { + [self reloadImage]; + } + } +} + - (void)reloadImage { if (_src && !CGSizeEqualToSize(self.frame.size, CGSizeZero)) { @@ -165,12 +176,15 @@ RCT_NOT_IMPLEMENTED(-init) if (self.image == nil) { [self reloadImage]; } else if ([RCTImageLoader isAssetLibraryImage:_src] || [RCTImageLoader isRemoteImage:_src]) { - CGSize imageSize = { - self.image.size.width / RCTScreenScale(), - self.image.size.height / RCTScreenScale() - }; - CGFloat widthChangeFraction = imageSize.width ? ABS(imageSize.width - frame.size.width) / imageSize.width : 1; - CGFloat heightChangeFraction = imageSize.height ? ABS(imageSize.height - frame.size.height) / imageSize.height : 1; + + // Get optimal image size + CGSize currentSize = self.image.size; + CGSize idealSize = RCTTargetSize(self.image.size, self.image.scale, frame.size, + RCTScreenScale(), self.contentMode, YES); + + CGFloat widthChangeFraction = ABS(currentSize.width - idealSize.width) / currentSize.width; + CGFloat heightChangeFraction = ABS(currentSize.height - idealSize.height) / currentSize.height; + // If the combined change is more than 20%, reload the asset in case there is a better size. if (widthChangeFraction + heightChangeFraction > 0.2) { [self reloadImage]; diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index 14bdc8240..a284ce6e3 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -80,6 +80,9 @@ function installConsoleErrorReporter() { console.reportException = reportException; console.errorOriginal = console.error.bind(console); console.error = function reactConsoleError() { + // Note that when using the built-in context executor on iOS (i.e., not + // Chrome debugging), console.error is already stubbed out to cause a + // redbox via RCTNativeLoggingHook. console.errorOriginal.apply(null, arguments); if (!console.reportErrorsAsExceptions) { return; @@ -88,6 +91,7 @@ function installConsoleErrorReporter() { if (str.slice(0, 10) === '"Warning: ') { // React warnings use console.error so that a stack trace is shown, but // we don't (currently) want these to show a redbox + // (Note: Logic duplicated in polyfills/console.js.) return; } var error: any = new Error('console.error: ' + str); diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 1aefe0c58..54b954dc0 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -59,6 +59,13 @@ function setUpRedBoxConsoleErrorHandler() { } } +function setupFlowChecker() { + if (__DEV__) { + var checkFlowAtRuntime = require('checkFlowAtRuntime'); + checkFlowAtRuntime(); + } +} + /** * Sets up a set of window environment wrappers that ensure that the * BatchedBridge is flushed after each tick. In both the case of the @@ -124,13 +131,14 @@ function setUpWebSockets() { } function setupProfile() { - console.profile = console.profile || GLOBAL.consoleProfile || function () {}; - console.profileEnd = console.profileEnd || GLOBAL.consoleProfileEnd || function () {}; + console.profile = console.profile || GLOBAL.nativeTraceBeginSection || function () {}; + console.profileEnd = console.profileEnd || GLOBAL.nativeTraceEndSection || function () {}; require('BridgeProfiling').swizzleReactPerf(); } function setUpProcessEnv() { - GLOBAL.process = {env: {NODE_ENV: __DEV__ ? 'development' : 'production'}}; + GLOBAL.process = GLOBAL.process || {}; + GLOBAL.process.env = {NODE_ENV: __DEV__ ? 'development' : 'production'}; } setUpRedBoxErrorHandler(); @@ -143,3 +151,4 @@ setUpGeolocation(); setUpWebSockets(); setupProfile(); setUpProcessEnv(); +setupFlowChecker(); diff --git a/Libraries/JavaScriptAppEngine/Initialization/checkFlowAtRuntime.js b/Libraries/JavaScriptAppEngine/Initialization/checkFlowAtRuntime.js new file mode 100644 index 000000000..8165ae667 --- /dev/null +++ b/Libraries/JavaScriptAppEngine/Initialization/checkFlowAtRuntime.js @@ -0,0 +1,59 @@ +/** + * 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 checkFlowAtRuntime + * + */ +'use strict'; + +function checkFlowAtRuntime() { + var url = getPackagerURL(); + if (!url) { + return; + } + fetch(url + 'flow/') + .then(response => response.json()) + .then(response => { + if (response.silentError) { + return; + } + throw { + message: response.message, + stack: response.errors.map(err => { + return { + ...err, + methodName: err.description, + file: err.filename, + }; + }), + }; + }, + () => { + //if fetch fails, silently give up + }) + .done(); +} + +function getPackagerURL() { + var NativeModules = require('NativeModules'); + var scriptURL = (NativeModules + && NativeModules.SourceCode + && NativeModules.SourceCode.scriptURL) + || ''; + + // extract the url of the packager from the whole scriptURL + // we match until the first / after http(s):// + // i.e. http://www.mypackger.com/debug/my/bundle -> http://www.mypackger.com/ + return getFirstOrNull(scriptURL.match(/^https?:\/\/[^/]+\//)); +} + +function getFirstOrNull(ar) { + return ar ? ar[0] : null; +} + +module.exports = checkFlowAtRuntime; diff --git a/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js b/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js index bbaa1a276..63076c7b8 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js +++ b/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js @@ -32,7 +32,7 @@ function parseErrorStack(e, sourceMapInstance) { return []; } - var stack = stacktraceParser.parse(e.stack); + var stack = Array.isArray(e.stack) ? e.stack : stacktraceParser.parse(e.stack); var framesToPop = e.framesToPop || 0; while (framesToPop--) { diff --git a/Libraries/Animation/LayoutAnimation.js b/Libraries/LayoutAnimation/LayoutAnimation.js similarity index 100% rename from Libraries/Animation/LayoutAnimation.js rename to Libraries/LayoutAnimation/LayoutAnimation.js diff --git a/Libraries/LinkingIOS/RCTLinkingManager.m b/Libraries/LinkingIOS/RCTLinkingManager.m index 5a97f0a60..af1d41752 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.m +++ b/Libraries/LinkingIOS/RCTLinkingManager.m @@ -42,7 +42,7 @@ RCT_EXPORT_MODULE() sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { - NSDictionary *payload = @{@"url": [URL absoluteString]}; + NSDictionary *payload = @{@"url": URL.absoluteString}; [[NSNotificationCenter defaultCenter] postNotificationName:RCTOpenURLNotification object:self userInfo:payload]; @@ -52,7 +52,7 @@ RCT_EXPORT_MODULE() - (void)handleOpenURLNotification:(NSNotification *)notification { [_bridge.eventDispatcher sendDeviceEventWithName:@"openURL" - body:[notification userInfo]]; + body:notification.userInfo]; } RCT_EXPORT_METHOD(openURL:(NSURL *)URL) @@ -72,7 +72,7 @@ RCT_EXPORT_METHOD(canOpenURL:(NSURL *)URL - (NSDictionary *)constantsToExport { NSURL *initialURL = _bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; - return @{@"initialURL": RCTNullIfNil([initialURL absoluteString])}; + return @{@"initialURL": RCTNullIfNil(initialURL.absoluteString)}; } @end diff --git a/Libraries/Modal/Modal.js b/Libraries/Modal/Modal.js index 23bf730ec..2063f0fdd 100644 --- a/Libraries/Modal/Modal.js +++ b/Libraries/Modal/Modal.js @@ -11,6 +11,7 @@ */ 'use strict'; +var PropTypes = require('ReactPropTypes'); var React = require('React'); var StyleSheet = require('StyleSheet'); var View = require('View'); @@ -24,9 +25,16 @@ class Modal extends React.Component { return null; } + if (this.props.transparent) { + var containerBackgroundColor = {backgroundColor: 'transparent'}; + } + return ( - - + + {this.props.children} @@ -34,6 +42,11 @@ class Modal extends React.Component { } } +Modal.propTypes = { + animated: PropTypes.bool, + transparent: PropTypes.bool, +}; + var styles = StyleSheet.create({ modal: { position: 'absolute', diff --git a/Libraries/Network/FormData.js b/Libraries/Network/FormData.js index 8339e49ba..ea5d8cc29 100644 --- a/Libraries/Network/FormData.js +++ b/Libraries/Network/FormData.js @@ -74,7 +74,7 @@ class FormData { var contentDisposition = 'form-data; name="' + name + '"'; var headers: Headers = {'content-disposition': contentDisposition}; if (typeof value === 'string') { - return {string: value, headers}; + return {string: value, headers, fieldName: name}; } // The body part is a "blob", which in React Native just means @@ -87,7 +87,7 @@ class FormData { if (typeof value.type === 'string') { headers['content-type'] = value.type; } - return {...value, headers}; + return {...value, headers, fieldName: name}; }); } } diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js index 63df3802b..f51a95529 100644 --- a/Libraries/Network/NetInfo.js +++ b/Libraries/Network/NetInfo.js @@ -15,14 +15,9 @@ var Map = require('Map'); var NativeModules = require('NativeModules'); var Platform = require('Platform'); var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTNetInfo = NativeModules.NetInfo; -if (Platform.OS === 'ios') { - var RCTNetInfo = NativeModules.Reachability; -} else if (Platform.OS === 'android') { - var RCTNetInfo = NativeModules.NetInfo; -} - -var DEVICE_REACHABILITY_EVENT = 'reachabilityDidChange'; +var DEVICE_REACHABILITY_EVENT = 'networkDidChange'; type ChangeEventName = $Enum<{ change: string; @@ -151,7 +146,7 @@ var NetInfo = { var listener = RCTDeviceEventEmitter.addListener( DEVICE_REACHABILITY_EVENT, (appStateData) => { - handler(appStateData.network_reachability); + handler(appStateData.network_info); } ); _subscriptions.set(handler, listener); diff --git a/Libraries/Network/RCTDownloadTask.m b/Libraries/Network/RCTDownloadTask.m index 5b065675f..e523f1606 100644 --- a/Libraries/Network/RCTDownloadTask.m +++ b/Libraries/Network/RCTDownloadTask.m @@ -51,7 +51,7 @@ _uploadProgressBlock = nil; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)cancel { @@ -101,7 +101,7 @@ RCT_NOT_IMPLEMENTED(-init) { if ([self validateRequestToken:requestToken]) { if (!_data) { - _data = [[NSMutableData alloc] init]; + _data = [NSMutableData new]; } [_data appendData:data]; if (_incrementalDataBlock) { diff --git a/Libraries/Network/RCTHTTPRequestHandler.m b/Libraries/Network/RCTHTTPRequestHandler.m index d5ee89a45..e1ad88ab1 100644 --- a/Libraries/Network/RCTHTTPRequestHandler.m +++ b/Libraries/Network/RCTHTTPRequestHandler.m @@ -47,7 +47,7 @@ RCT_EXPORT_MODULE() - (BOOL)canHandleRequest:(NSURLRequest *)request { - return [@[@"http", @"https", @"file"] containsObject:[request.URL.scheme lowercaseString]]; + return [@[@"http", @"https", @"file"] containsObject:request.URL.scheme.lowercaseString]; } - (NSURLSessionDataTask *)sendRequest:(NSURLRequest *)request @@ -55,7 +55,7 @@ RCT_EXPORT_MODULE() { // Lazy setup if (!_session && [self isValid]) { - NSOperationQueue *callbackQueue = [[NSOperationQueue alloc] init]; + NSOperationQueue *callbackQueue = [NSOperationQueue new]; NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; _session = [NSURLSession sessionWithConfiguration:configuration delegate:self diff --git a/Libraries/Network/RCTReachability.h b/Libraries/Network/RCTNetInfo.h similarity index 89% rename from Libraries/Network/RCTReachability.h rename to Libraries/Network/RCTNetInfo.h index ba74fcb42..6c2556e0a 100644 --- a/Libraries/Network/RCTReachability.h +++ b/Libraries/Network/RCTNetInfo.h @@ -11,7 +11,7 @@ #import "RCTBridgeModule.h" -@interface RCTReachability : NSObject +@interface RCTNetInfo : NSObject - (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Network/RCTReachability.m b/Libraries/Network/RCTNetInfo.m similarity index 90% rename from Libraries/Network/RCTReachability.m rename to Libraries/Network/RCTNetInfo.m index 7c5530127..5aab64432 100644 --- a/Libraries/Network/RCTReachability.m +++ b/Libraries/Network/RCTNetInfo.m @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTReachability.h" +#import "RCTNetInfo.h" #import "RCTAssert.h" #import "RCTBridge.h" @@ -18,7 +18,7 @@ static NSString *const RCTReachabilityStateNone = @"none"; static NSString *const RCTReachabilityStateWifi = @"wifi"; static NSString *const RCTReachabilityStateCell = @"cell"; -@implementation RCTReachability +@implementation RCTNetInfo { SCNetworkReachabilityRef _reachability; NSString *_status; @@ -30,7 +30,7 @@ RCT_EXPORT_MODULE() static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) { - RCTReachability *self = (__bridge id)info; + RCTNetInfo *self = (__bridge id)info; NSString *status = RCTReachabilityStateUnknown; if ((flags & kSCNetworkReachabilityFlagsReachable) == 0 || (flags & kSCNetworkReachabilityFlagsConnectionRequired) != 0) { @@ -51,8 +51,8 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC if (![status isEqualToString:self->_status]) { self->_status = status; - [self->_bridge.eventDispatcher sendDeviceEventWithName:@"reachabilityDidChange" - body:@{@"network_reachability": status}]; + [self->_bridge.eventDispatcher sendDeviceEventWithName:@"networkDidChange" + body:@{@"network_info": status}]; } } @@ -90,7 +90,7 @@ static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SC RCT_EXPORT_METHOD(getCurrentReachability:(RCTResponseSenderBlock)getSuccess withErrorCallback:(__unused RCTResponseSenderBlock)getError) { - getSuccess(@[@{@"network_reachability": _status}]); + getSuccess(@[@{@"network_info": _status}]); } @end diff --git a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj index 8243ac65f..546b301c2 100644 --- a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj +++ b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - 1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7361AB03E7B00659ED6 /* RCTReachability.m */; }; + 1372B7371AB03E7B00659ED6 /* RCTNetInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7361AB03E7B00659ED6 /* RCTNetInfo.m */; }; 13D6D66A1B5FCF8200883BE9 /* RCTDownloadTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */; }; 352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */; }; 58B512081A9E6CE300147676 /* RCTNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512071A9E6CE300147676 /* RCTNetworking.m */; }; @@ -26,8 +26,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1372B7351AB03E7B00659ED6 /* RCTReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTReachability.h; sourceTree = ""; }; - 1372B7361AB03E7B00659ED6 /* RCTReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTReachability.m; sourceTree = ""; }; + 1372B7351AB03E7B00659ED6 /* RCTNetInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = RCTNetInfo.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 1372B7361AB03E7B00659ED6 /* RCTNetInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = RCTNetInfo.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 13D6D6681B5FCF8200883BE9 /* RCTDownloadTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDownloadTask.h; sourceTree = ""; }; 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDownloadTask.m; sourceTree = ""; }; 352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTHTTPRequestHandler.h; sourceTree = ""; }; @@ -55,10 +55,10 @@ 13D6D6691B5FCF8200883BE9 /* RCTDownloadTask.m */, 352DA0B71B17855800AA15A8 /* RCTHTTPRequestHandler.h */, 352DA0B81B17855800AA15A8 /* RCTHTTPRequestHandler.m */, + 1372B7351AB03E7B00659ED6 /* RCTNetInfo.h */, + 1372B7361AB03E7B00659ED6 /* RCTNetInfo.m */, 58B512061A9E6CE300147676 /* RCTNetworking.h */, 58B512071A9E6CE300147676 /* RCTNetworking.m */, - 1372B7351AB03E7B00659ED6 /* RCTReachability.h */, - 1372B7361AB03E7B00659ED6 /* RCTReachability.m */, 58B511DC1A9E6C8500147676 /* Products */, ); indentWidth = 2; @@ -130,7 +130,7 @@ buildActionMask = 2147483647; files = ( 13D6D66A1B5FCF8200883BE9 /* RCTDownloadTask.m in Sources */, - 1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */, + 1372B7371AB03E7B00659ED6 /* RCTNetInfo.m in Sources */, 58B512081A9E6CE300147676 /* RCTNetworking.m in Sources */, 352DA0BA1B17855800AA15A8 /* RCTHTTPRequestHandler.m in Sources */, ); diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index c892363b0..4853beff8 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -65,7 +65,7 @@ static NSString *RCTGenerateFormBoundary() parts = [formData mutableCopy]; _callback = callback; - multipartBody = [[NSMutableData alloc] init]; + multipartBody = [NSMutableData new]; boundary = RCTGenerateFormBoundary(); return [_networker processDataForHTTPQuery:parts[0] callback:^(NSError *error, NSDictionary *result) { @@ -88,7 +88,7 @@ static NSString *RCTGenerateFormBoundary() NSMutableDictionary *headers = [parts[0][@"headers"] mutableCopy]; NSString *partContentType = result[@"contentType"]; if (partContentType != nil) { - [headers setObject:partContentType forKey:@"content-type"]; + headers[@"content-type"] = partContentType; } [headers enumerateKeysAndObjectsUsingBlock:^(NSString *parameterKey, NSString *parameterValue, BOOL *stop) { [multipartBody appendData:[[NSString stringWithFormat:@"%@: %@\r\n", parameterKey, parameterValue] @@ -132,7 +132,7 @@ RCT_EXPORT_MODULE() - (instancetype)init { if ((self = [super init])) { - _tasksByRequestID = [[NSMutableDictionary alloc] init]; + _tasksByRequestID = [NSMutableDictionary new]; } return self; } @@ -140,12 +140,12 @@ RCT_EXPORT_MODULE() - (RCTURLRequestCancellationBlock)buildRequest:(NSDictionary *)query completionBlock:(void (^)(NSURLRequest *request))block { - NSURL *URL = [RCTConvert NSURL:query[@"url"]]; + NSURL *URL = [RCTConvert NSURL:query[@"url"]]; // this is marked as nullable in JS, but should not be null NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL]; - request.HTTPMethod = [[RCTConvert NSString:query[@"method"]] uppercaseString] ?: @"GET"; + request.HTTPMethod = [RCTConvert NSString:RCTNilIfNull(query[@"method"])].uppercaseString ?: @"GET"; request.allHTTPHeaderFields = [RCTConvert NSDictionary:query[@"headers"]]; - NSDictionary *data = [RCTConvert NSDictionary:query[@"data"]]; + NSDictionary *data = [RCTConvert NSDictionary:RCTNilIfNull(query[@"data"])]; return [self processDataForHTTPQuery:data callback:^(NSError *error, NSDictionary *result) { if (error) { RCTLogError(@"Error processing request body: %@", error); @@ -161,7 +161,7 @@ RCT_EXPORT_MODULE() // Gzip the request body if ([request.allHTTPHeaderFields[@"Content-Encoding"] isEqualToString:@"gzip"]) { request.HTTPBody = RCTGzipData(request.HTTPBody, -1 /* default */); - [request setValue:[@(request.HTTPBody.length) description] forHTTPHeaderField:@"Content-Length"]; + [request setValue:(@(request.HTTPBody.length)).description forHTTPHeaderField:@"Content-Length"]; } block(request); @@ -195,7 +195,7 @@ RCT_EXPORT_MODULE() return NSOrderedSame; } }]; - id handler = [handlers lastObject]; + id handler = handlers.lastObject; if (!handler) { RCTLogError(@"No suitable request handler found for %@", request.URL); } @@ -220,7 +220,7 @@ RCT_EXPORT_MODULE() * - @"contentType" (NSString): the content type header of the request * */ -- (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(NSDictionary *)query callback: +- (RCTURLRequestCancellationBlock)processDataForHTTPQuery:(nullable NSDictionary *)query callback: (RCTURLRequestCancellationBlock (^)(NSError *error, NSDictionary *result))callback { if (!query) { @@ -248,7 +248,7 @@ RCT_EXPORT_MODULE() } NSDictionaryArray *formData = [RCTConvert NSDictionaryArray:query[@"formData"]]; if (formData) { - RCTHTTPFormDataHelper *formDataHelper = [[RCTHTTPFormDataHelper alloc] init]; + RCTHTTPFormDataHelper *formDataHelper = [RCTHTTPFormDataHelper new]; formDataHelper.networker = self; return [formDataHelper process:formData callback:callback]; } diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index 7b84e35d1..29b5597ad 100644 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -104,8 +104,7 @@ class XMLHttpRequest extends XMLHttpRequestBase { sendImpl(method: ?string, url: ?string, headers: Object, data: any): void { if (typeof data === 'string') { data = {string: data}; - } - if (data instanceof FormData) { + } else if (data instanceof FormData) { data = {formData: data.getParts()}; } RCTNetworking.sendRequest( diff --git a/Libraries/Portal/Portal.js b/Libraries/Portal/Portal.js index a029bb99f..652e9ed6f 100644 --- a/Libraries/Portal/Portal.js +++ b/Libraries/Portal/Portal.js @@ -6,7 +6,9 @@ */ 'use strict'; +var Platform = require('Platform'); var React = require('React'); +var RCTUIManager = require('NativeModules').UIManager; var StyleSheet = require('StyleSheet'); var View = require('View'); @@ -72,7 +74,15 @@ var Portal = React.createClass({ return []; } return _portalRef._getOpenModals(); - } + }, + + notifyAccessibilityService: function() { + if (!_portalRef) { + console.error('Calling closeModal but no Portal has been rendered.'); + return; + } + _portalRef._notifyAccessibilityService(); + }, }, getInitialState: function() { @@ -80,6 +90,11 @@ var Portal = React.createClass({ }, _showModal: function(tag: string, component: any) { + // We are about to open first modal, so Portal will appear. + // Let's disable accessibility for background view on Android. + if (this._getOpenModals().length === 0) { + this.props.onModalVisibilityChanged(true); + } // This way state is chained through multiple calls to // _showModal, _closeModal correctly. this.setState((state) => { @@ -93,6 +108,11 @@ var Portal = React.createClass({ if (!this.state.modals.hasOwnProperty(tag)) { return; } + // We are about to close last modal, so Portal will disappear. + // Let's enable accessibility for application view on Android. + if (this._getOpenModals().length === 1) { + this.props.onModalVisibilityChanged(false); + } // This way state is chained through multiple calls to // _showModal, _closeModal correctly. this.setState((state) => { @@ -106,6 +126,20 @@ var Portal = React.createClass({ return Object.keys(this.state.modals); }, + _notifyAccessibilityService: function() { + if (Platform.OS === 'android') { + // We need to send accessibility event in a new batch, as otherwise + // TextViews have no text set at the moment of populating event. + setTimeout(() => { + if (this._getOpenModals().length > 0) { + RCTUIManager.sendAccessibilityEvent( + React.findNodeHandle(this), + RCTUIManager.AccessibilityEventTypes.typeWindowStateChanged); + } + }, 0); + } + }, + render: function() { _portalRef = this; if (!this.state.modals) { @@ -119,7 +153,9 @@ var Portal = React.createClass({ return null; } return ( - + {modals} ); diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index ac683fc2a..fda9980f9 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -32,7 +32,7 @@ NSString *const RCTRemoteNotificationsRegistered = @"RemoteNotificationsRegister + (UILocalNotification *)UILocalNotification:(id)json { NSDictionary *details = [self NSDictionary:json]; - UILocalNotification *notification = [[UILocalNotification alloc] init]; + UILocalNotification *notification = [UILocalNotification new]; notification.fireDate = [RCTConvert NSDate:details[@"fireDate"]] ?: [NSDate date]; notification.alertBody = [RCTConvert NSString:details[@"alertBody"]]; return notification; @@ -108,13 +108,13 @@ RCT_EXPORT_MODULE() - (void)handleRemoteNotificationReceived:(NSNotification *)notification { [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationReceived" - body:[notification userInfo]]; + body:notification.userInfo]; } - (void)handleRemoteNotificationsRegistered:(NSNotification *)notification { [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationsRegistered" - body:[notification userInfo]]; + body:notification.userInfo]; } /** @@ -175,7 +175,7 @@ RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback) { NSUInteger types = 0; if ([UIApplication instancesRespondToSelector:@selector(currentUserNotificationSettings)]) { - types = [[[UIApplication sharedApplication] currentUserNotificationSettings] types]; + types = [[UIApplication sharedApplication] currentUserNotificationSettings].types; } else { #if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 @@ -186,7 +186,7 @@ RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback) } - NSMutableDictionary *permissions = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *permissions = [NSMutableDictionary new]; permissions[@"alert"] = @((types & UIUserNotificationTypeAlert) > 0); permissions[@"badge"] = @((types & UIUserNotificationTypeBadge) > 0); permissions[@"sound"] = @((types & UIUserNotificationTypeSound) > 0); diff --git a/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m index 7d12736d1..7d7d32947 100644 --- a/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m +++ b/Libraries/RCTTest/FBSnapshotTestCase/FBSnapshotTestController.m @@ -51,7 +51,7 @@ typedef struct RGBAPixel { { if ((self = [super init])) { _testName = [testName copy]; - _fileManager = [[NSFileManager alloc] init]; + _fileManager = [NSFileManager new]; } return self; } diff --git a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m index e38c6e4e8..0bd83ba3b 100644 --- a/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m +++ b/Libraries/RCTTest/FBSnapshotTestCase/UIImage+Compare.m @@ -65,7 +65,7 @@ (CGBitmapInfo)kCGImageAlphaPremultipliedLast ); - CGFloat scaleFactor = [[UIScreen mainScreen] scale]; + CGFloat scaleFactor = [UIScreen mainScreen].scale; CGContextScaleCTM(referenceImageContext, scaleFactor, scaleFactor); CGContextScaleCTM(imageContext, scaleFactor, scaleFactor); diff --git a/Libraries/RCTTest/RCTTestModule.m b/Libraries/RCTTest/RCTTestModule.m index 9ef60c611..3ad89b827 100644 --- a/Libraries/RCTTest/RCTTestModule.m +++ b/Libraries/RCTTest/RCTTestModule.m @@ -44,7 +44,7 @@ RCT_EXPORT_METHOD(verifySnapshot:(RCTResponseSenderBlock)callback) [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { NSString *testName = NSStringFromSelector(_testSelector); - _snapshotCounter[testName] = [@([_snapshotCounter[testName] integerValue] + 1) stringValue]; + _snapshotCounter[testName] = (@([_snapshotCounter[testName] integerValue] + 1)).stringValue; NSError *error = nil; BOOL success = [_controller compareSnapshotOfView:_view diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index f270c3b81..372858ac9 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -11,7 +11,7 @@ #import "FBSnapshotTestController.h" #import "RCTAssert.h" -#import "RCTRedBox.h" +#import "RCTLog.h" #import "RCTRootView.h" #import "RCTTestModule.h" #import "RCTUtils.h" @@ -55,7 +55,7 @@ return self; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)setRecordMode:(BOOL)recordMode { @@ -83,12 +83,18 @@ RCT_NOT_IMPLEMENTED(-init) - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock { + __block NSString *error = nil; + RCTSetLogFunction(^(RCTLogLevel level, NSString *fileName, NSNumber *lineNumber, NSString *message) { + if (level >= RCTLogLevelError) { + error = message; + } + }); + RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:_scriptURL moduleProvider:_moduleProvider launchOptions:nil]; - RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName]; - rootView.initialProperties = initialProps; + RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProps]; rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices NSString *testModuleName = RCTBridgeModuleNameForClass([RCTTestModule class]); @@ -99,24 +105,23 @@ RCT_NOT_IMPLEMENTED(-init) testModule.view = rootView; UIViewController *vc = [UIApplication sharedApplication].delegate.window.rootViewController; - vc.view = [[UIView alloc] init]; + vc.view = [UIView new]; [vc.view addSubview:rootView]; // Add as subview so it doesn't get resized NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; - NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage]; - while ([date timeIntervalSinceNow] > 0 && testModule.status == RCTTestStatusPending && error == nil) { + while (date.timeIntervalSinceNow > 0 && testModule.status == RCTTestStatusPending && error == nil) { [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - error = [[RCTRedBox sharedInstance] currentErrorMessage]; } [rootView removeFromSuperview]; + RCTSetLogFunction(RCTDefaultLogFunction); + NSArray *nonLayoutSubviews = [vc.view.subviews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id subview, NSDictionary *bindings) { return ![NSStringFromClass([subview class]) isEqualToString:@"_UILayoutGuide"]; }]]; RCTAssert(nonLayoutSubviews.count == 0, @"There shouldn't be any other views: %@", nonLayoutSubviews); - [[RCTRedBox sharedInstance] dismiss]; if (expectErrorBlock) { RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched."); } else { diff --git a/Libraries/ReactIOS/NativeMethodsMixin.js b/Libraries/ReactIOS/NativeMethodsMixin.js index e31b0e43a..7a448ce40 100644 --- a/Libraries/ReactIOS/NativeMethodsMixin.js +++ b/Libraries/ReactIOS/NativeMethodsMixin.js @@ -12,7 +12,6 @@ 'use strict'; var NativeModules = require('NativeModules'); -var RCTPOPAnimationManager = NativeModules.POPAnimationManager; var RCTUIManager = NativeModules.UIManager; var TextInputState = require('TextInputState'); @@ -38,32 +37,8 @@ type MeasureLayoutOnSuccessCallback = ( height: number ) => void -var animationIDInvariant = function( - funcName: string, - anim: number -) { - invariant( - anim, - funcName + ' must be called with a valid animation ID returned from' + - ' POPAnimation.createAnimation, received: "' + anim + '"' - ); -}; var NativeMethodsMixin = { - addAnimation: function(anim: number, callback?: (finished: bool) => void) { - animationIDInvariant('addAnimation', anim); - RCTPOPAnimationManager.addAnimation( - findNodeHandle(this), - anim, - mountSafeCallback(this, callback) - ); - }, - - removeAnimation: function(anim: number) { - animationIDInvariant('removeAnimation', anim); - RCTPOPAnimationManager.removeAnimation(findNodeHandle(this), anim); - }, - measure: function(callback: MeasureOnSuccessCallback) { RCTUIManager.measure( findNodeHandle(this), diff --git a/Libraries/ReactNative/ReactNative.js b/Libraries/ReactNative/ReactNative.js index a57bc3b0d..4b09bb667 100644 --- a/Libraries/ReactNative/ReactNative.js +++ b/Libraries/ReactNative/ReactNative.js @@ -21,6 +21,7 @@ var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactNativeDefaultInjection = require('ReactNativeDefaultInjection'); var ReactNativeMount = require('ReactNativeMount'); var ReactPropTypes = require('ReactPropTypes'); +var ReactUpdates = require('ReactUpdates'); var deprecated = require('deprecated'); var findNodeHandle = require('findNodeHandle'); @@ -94,7 +95,11 @@ var ReactNative = { render: render, unmountComponentAtNode: ReactNativeMount.unmountComponentAtNode, - // Hook for JSX spread, don't use this for anything else. + /* eslint-disable camelcase */ + unstable_batchedUpdates: ReactUpdates.batchedUpdates, + /* eslint-enable camelcase */ + + // Hook for JSX spread, don't use this for anything else. __spread: Object.assign, unmountComponentAtNodeAndRemoveContainer: ReactNativeMount.unmountComponentAtNodeAndRemoveContainer, diff --git a/Libraries/ReactNative/ReactNativeBaseComponent.js b/Libraries/ReactNative/ReactNativeBaseComponent.js index 84baf6753..844923a2b 100644 --- a/Libraries/ReactNative/ReactNativeBaseComponent.js +++ b/Libraries/ReactNative/ReactNativeBaseComponent.js @@ -27,6 +27,7 @@ var warning = require('warning'); var registrationNames = ReactNativeEventEmitter.registrationNames; var putListener = ReactNativeEventEmitter.putListener; +var deleteListener = ReactNativeEventEmitter.deleteListener; var deleteAllListeners = ReactNativeEventEmitter.deleteAllListeners; type ReactNativeBaseComponentViewConfig = { @@ -230,7 +231,11 @@ ReactNativeBaseComponent.Mixin = { _reconcileListenersUponUpdate: function(prevProps, nextProps) { for (var key in nextProps) { if (registrationNames[key] && (nextProps[key] !== prevProps[key])) { - putListener(this._rootNodeID, key, nextProps[key]); + if (nextProps[key]) { + putListener(this._rootNodeID, key, nextProps[key]); + } else { + deleteListener(this._rootNodeID, key); + } } } }, diff --git a/Libraries/ReactNative/ReactNativeViewAttributes.js b/Libraries/ReactNative/ReactNativeViewAttributes.js index b8ad5761c..ae47b7529 100644 --- a/Libraries/ReactNative/ReactNativeViewAttributes.js +++ b/Libraries/ReactNative/ReactNativeViewAttributes.js @@ -22,6 +22,7 @@ ReactNativeViewAttributes.UIView = { accessibilityComponentType: true, accessibilityLiveRegion: true, accessibilityTraits: true, + importantForAccessibility: true, testID: true, shouldRasterizeIOS: true, onLayout: true, diff --git a/Libraries/Text/RCTRawTextManager.m b/Libraries/Text/RCTRawTextManager.m index beb716f75..460b127ba 100644 --- a/Libraries/Text/RCTRawTextManager.m +++ b/Libraries/Text/RCTRawTextManager.m @@ -17,7 +17,7 @@ RCT_EXPORT_MODULE() - (RCTShadowView *)shadowView { - return [[RCTShadowRawText alloc] init]; + return [RCTShadowRawText new]; } RCT_EXPORT_SHADOW_PROPERTY(text, NSString) diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index 61f1373c1..b9c1ff2e7 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -34,8 +34,8 @@ static css_dim_t RCTMeasure(void *context, float width) { RCTShadowText *shadowText = (__bridge RCTShadowText *)context; NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width]; - NSLayoutManager *layoutManager = [textStorage.layoutManagers firstObject]; - NSTextContainer *textContainer = [layoutManager.textContainers firstObject]; + NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; CGSize computedSize = [layoutManager usedRectForTextContainer:textContainer].size; css_dim_t result; @@ -111,12 +111,12 @@ static css_dim_t RCTMeasure(void *context, float width) return _cachedTextStorage; } - NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; + NSLayoutManager *layoutManager = [NSLayoutManager new]; NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedString]; [textStorage addLayoutManager:layoutManager]; - NSTextContainer *textContainer = [[NSTextContainer alloc] init]; + NSTextContainer *textContainer = [NSTextContainer new]; textContainer.lineFragmentPadding = 0.0; textContainer.lineBreakMode = _numberOfLines > 0 ? NSLineBreakByTruncatingTail : NSLineBreakByClipping; textContainer.maximumNumberOfLines = _numberOfLines; @@ -183,14 +183,14 @@ static css_dim_t RCTMeasure(void *context, float width) _effectiveLetterSpacing = letterSpacing.doubleValue; - NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] init]; + NSMutableAttributedString *attributedString = [NSMutableAttributedString new]; for (RCTShadowView *child in [self reactSubviews]) { if ([child isKindOfClass:[RCTShadowText class]]) { RCTShadowText *shadowText = (RCTShadowText *)child; [attributedString appendAttributedString:[shadowText _attributedStringWithFontFamily:fontFamily fontSize:fontSize fontWeight:fontWeight fontStyle:fontStyle letterSpacing:letterSpacing useBackgroundColor:YES]]; } else if ([child isKindOfClass:[RCTShadowRawText class]]) { RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child; - [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:[shadowRawText text] ?: @""]]; + [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:shadowRawText.text ?: @""]]; } else { RCTLogError(@" can't have any children except or raw strings"); } @@ -225,7 +225,7 @@ static css_dim_t RCTMeasure(void *context, float width) - (void)_addAttribute:(NSString *)attribute withValue:(id)attributeValue toAttributedString:(NSMutableAttributedString *)attributedString { - [attributedString enumerateAttribute:attribute inRange:NSMakeRange(0, [attributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { + [attributedString enumerateAttribute:attribute inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { if (!value && attributeValue) { [attributedString addAttribute:attribute value:attributeValue range:range]; } @@ -249,10 +249,10 @@ static css_dim_t RCTMeasure(void *context, float width) } // check for lineHeight on each of our children, update the max as we go (in self.lineHeight) - [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, [attributedString length]) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { + [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:(NSRange){0, attributedString.length} options:0 usingBlock:^(id value, NSRange range, BOOL *stop) { if (value) { NSParagraphStyle *paragraphStyle = (NSParagraphStyle *)value; - CGFloat maximumLineHeight = round([paragraphStyle maximumLineHeight] / self.fontSizeMultiplier); + CGFloat maximumLineHeight = round(paragraphStyle.maximumLineHeight / self.fontSizeMultiplier); if (maximumLineHeight > self.lineHeight) { self.lineHeight = maximumLineHeight; } @@ -265,7 +265,7 @@ static css_dim_t RCTMeasure(void *context, float width) // if we found anything, set it :D if (hasParagraphStyle) { - NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new]; paragraphStyle.alignment = _textAlign; paragraphStyle.baseWritingDirection = _writingDirection; CGFloat lineHeight = round(_lineHeight * self.fontSizeMultiplier); @@ -305,13 +305,13 @@ static css_dim_t RCTMeasure(void *context, float width) - (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex { [super insertReactSubview:subview atIndex:atIndex]; - [self cssNode]->children_count = 0; + self.cssNode->children_count = 0; } - (void)removeReactSubview:(RCTShadowView *)subview { [super removeReactSubview:subview]; - [self cssNode]->children_count = 0; + self.cssNode->children_count = 0; } - (void)setBackgroundColor:(UIColor *)backgroundColor @@ -348,7 +348,7 @@ RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection) _allowFontScaling = allowFontScaling; for (RCTShadowView *child in [self reactSubviews]) { if ([child isKindOfClass:[RCTShadowText class]]) { - [(RCTShadowText *)child setAllowFontScaling:allowFontScaling]; + ((RCTShadowText *)child).allowFontScaling = allowFontScaling; } } [self dirtyText]; @@ -359,7 +359,7 @@ RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection) _fontSizeMultiplier = fontSizeMultiplier; for (RCTShadowView *child in [self reactSubviews]) { if ([child isKindOfClass:[RCTShadowText class]]) { - [(RCTShadowText *)child setFontSizeMultiplier:fontSizeMultiplier]; + ((RCTShadowText *)child).fontSizeMultiplier = fontSizeMultiplier; } } [self dirtyText]; diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 162e16f70..47b7de6a5 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -23,7 +23,7 @@ - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { - _textStorage = [[NSTextStorage alloc] init]; + _textStorage = [NSTextStorage new]; _reactSubviews = [NSMutableArray array]; self.isAccessibilityElement = YES; @@ -75,8 +75,8 @@ - (void)drawRect:(CGRect)rect { - NSLayoutManager *layoutManager = [_textStorage.layoutManagers firstObject]; - NSTextContainer *textContainer = [layoutManager.textContainers firstObject]; + NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, _contentInset); NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; @@ -119,8 +119,8 @@ NSNumber *reactTag = self.reactTag; CGFloat fraction; - NSLayoutManager *layoutManager = [_textStorage.layoutManagers firstObject]; - NSTextContainer *textContainer = [layoutManager.textContainers firstObject]; + NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject; + NSTextContainer *textContainer = layoutManager.textContainers.firstObject; NSUInteger characterIndex = [layoutManager characterIndexForPoint:point inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:&fraction]; diff --git a/Libraries/Text/RCTTextField.m b/Libraries/Text/RCTTextField.m index 261c0cdb2..a75a268e9 100644 --- a/Libraries/Text/RCTTextField.m +++ b/Libraries/Text/RCTTextField.m @@ -31,20 +31,20 @@ [self addTarget:self action:@selector(textFieldBeginEditing) forControlEvents:UIControlEventEditingDidBegin]; [self addTarget:self action:@selector(textFieldEndEditing) forControlEvents:UIControlEventEditingDidEnd]; [self addTarget:self action:@selector(textFieldSubmitEditing) forControlEvents:UIControlEventEditingDidEndOnExit]; - _reactSubviews = [[NSMutableArray alloc] init]; + _reactSubviews = [NSMutableArray new]; } return self; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)setText:(NSString *)text { NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; if (eventLag == 0 && ![text isEqualToString:self.text]) { UITextRange *selection = self.selectedTextRange; - [super setText:text]; + super.text = text; self.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds } else if (eventLag > RCTTextUpdateLagWarningThreshold) { RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 06d52088a..f7150b46b 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -25,12 +25,12 @@ RCT_EXPORT_MODULE() - (UIView *)view { - return [[RCTText alloc] init]; + return [RCTText new]; } - (RCTShadowView *)shadowView { - return [[RCTShadowText alloc] init]; + return [RCTShadowText new]; } #pragma mark - Shadow properties @@ -66,12 +66,12 @@ RCT_EXPORT_SHADOW_PROPERTY(allowFontScaling, BOOL) } NSMutableArray *queue = [NSMutableArray arrayWithObject:rootView]; - for (NSInteger i = 0; i < [queue count]; i++) { + for (NSInteger i = 0; i < queue.count; i++) { RCTShadowView *shadowView = queue[i]; RCTAssert([shadowView isTextDirty], @"Don't process any nodes that don't have dirty text"); if ([shadowView isKindOfClass:[RCTShadowText class]]) { - [(RCTShadowText *)shadowView setFontSizeMultiplier:self.bridge.accessibilityManager.multiplier]; + ((RCTShadowText *)shadowView).fontSizeMultiplier = self.bridge.accessibilityManager.multiplier; [(RCTShadowText *)shadowView recomputeText]; } else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) { RCTLogError(@"Raw text cannot be used outside of a tag. Not rendering string: '%@'", diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index 2363d52dc..bcae5fe0a 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -41,20 +41,31 @@ return self; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)updateFrames { // Adjust the insets so that they are as close as possible to single-line - // RCTTextField defaults - UIEdgeInsets adjustedInset = (UIEdgeInsets){ - _contentInset.top - 5, _contentInset.left - 4, - _contentInset.bottom, _contentInset.right - }; - - [_textView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; - [_placeholderView setFrame:UIEdgeInsetsInsetRect(self.bounds, adjustedInset)]; + // RCTTextField defaults, using the system defaults of font size 17 and a + // height of 31 points. + // + // We apply the left inset to the frame since a negative left text-container + // inset mysteriously causes the text to be hidden until the text view is + // first focused. + UIEdgeInsets adjustedFrameInset = UIEdgeInsetsZero; + adjustedFrameInset.left = _contentInset.left - 5; + + UIEdgeInsets adjustedTextContainerInset = _contentInset; + adjustedTextContainerInset.top += 5; + adjustedTextContainerInset.left = 0; + + CGRect frame = UIEdgeInsetsInsetRect(self.bounds, adjustedFrameInset); + _textView.frame = frame; + _placeholderView.frame = frame; + + _textView.textContainerInset = adjustedTextContainerInset; + _placeholderView.textContainerInset = adjustedTextContainerInset; } - (void)updatePlaceholder @@ -155,7 +166,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; if (eventLag == 0 && ![text isEqualToString:_textView.text]) { UITextRange *selection = _textView.selectedTextRange; - [_textView setText:text]; + _textView.text = text; [self _setPlaceholderVisibility]; _textView.selectedTextRange = selection; // maintain cursor position/selection - this is robust to out of bounds } else if (eventLag > RCTTextUpdateLagWarningThreshold) { diff --git a/Libraries/Utilities/BridgeProfiling.js b/Libraries/Utilities/BridgeProfiling.js index 1b800901f..94ff3b3c6 100644 --- a/Libraries/Utilities/BridgeProfiling.js +++ b/Libraries/Utilities/BridgeProfiling.js @@ -12,26 +12,20 @@ 'use strict'; var GLOBAL = GLOBAL || this; +var TRACE_TAG_REACT_APPS = 1 << 17; var BridgeProfiling = { - profile(profileName?: any, args?: any) { + profile(profileName?: any) { if (GLOBAL.__BridgeProfilingIsProfiling) { - if (args) { - try { - args = JSON.stringify(args); - } catch(err) { - args = err.message; - } - } profileName = typeof profileName === 'function' ? profileName() : profileName; - console.profile(profileName, args); + console.profile(TRACE_TAG_REACT_APPS, profileName); } }, - profileEnd(profileName?: string) { + profileEnd() { if (GLOBAL.__BridgeProfilingIsProfiling) { - console.profileEnd(profileName); + console.profileEnd(TRACE_TAG_REACT_APPS); } }, @@ -39,16 +33,16 @@ var BridgeProfiling = { var ReactPerf = require('ReactPerf'); var originalMeasure = ReactPerf.measure; ReactPerf.measure = function (objName, fnName, func) { - func = originalMeasure.call(ReactPerf, objName, fnName, func); + func = originalMeasure.apply(ReactPerf, arguments); return function (component) { - BridgeProfiling.profile(); - var ret = func.apply(this, arguments); if (GLOBAL.__BridgeProfilingIsProfiling) { var name = this._instance && this._instance.constructor && (this._instance.constructor.displayName || this._instance.constructor.name); - BridgeProfiling.profileEnd(`${objName}.${fnName}(${name})`); + BridgeProfiling.profile(`${objName}.${fnName}(${name})`); } + var ret = func.apply(this, arguments); + BridgeProfiling.profileEnd(); return ret; }; }; diff --git a/Libraries/Utilities/MatrixMath.js b/Libraries/Utilities/MatrixMath.js index 6103e1d1e..21ad74ae2 100755 --- a/Libraries/Utilities/MatrixMath.js +++ b/Libraries/Utilities/MatrixMath.js @@ -31,6 +31,23 @@ var MatrixMath = { ]; }, + createOrthographic: function(left, right, bottom, top, near, far) { + var a = 2 / (right - left); + var b = 2 / (top - bottom); + var c = -2 / (far - near); + + var tx = -(right + left) / (right - left); + var ty = -(top + bottom) / (top - bottom); + var tz = -(far + near) / (far - near); + + return [ + a, 0, 0, 0, + 0, b, 0, 0, + 0, 0, c, 0, + tx, ty, tz, 1 + ]; + }, + createFrustum: function(left, right, bottom, top, near, far) { var r_width = 1 / (right - left); var r_height = 1 / (top - bottom); @@ -49,6 +66,19 @@ var MatrixMath = { ]; }, + createPerspective: function(fovInRadians, aspect, near, far) { + var h = 1 / Math.tan(fovInRadians); + var r_depth = 1 / (near - far); + var C = (far + near) * r_depth; + var D = 2 * (far * near * r_depth); + return [ + h/aspect, 0, 0, 0, + 0, h, 0, 0, + 0, 0, C,-1, + 0, 0, D, 0, + ]; + }, + createTranslate2d: function(x, y) { var mat = MatrixMath.createIdentityMatrix(); MatrixMath.reuseTranslate2dCommand(mat, x, y); diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index 98b0a9fe0..ffca2bca5 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -218,9 +218,10 @@ class MessageQueue { return null; } + let fn = null; let self = this; if (type === MethodTypes.remoteAsync) { - return function(...args) { + fn = function(...args) { return new Promise((resolve, reject) => { self.__nativeCall(module, method, args, resolve, (errorData) => { var error = createErrorFromErrorData(errorData); @@ -229,7 +230,7 @@ class MessageQueue { }); }; } else { - return function(...args) { + fn = function(...args) { let lastArg = args.length > 0 ? args[args.length - 1] : null; let secondLastArg = args.length > 1 ? args[args.length - 2] : null; let hasSuccCB = typeof lastArg === 'function'; @@ -245,6 +246,8 @@ class MessageQueue { return self.__nativeCall(module, method, args, onFail, onSucc); }; } + fn.type = type; + return fn; } } diff --git a/Libraries/Utilities/dismissKeyboard.js b/Libraries/Utilities/dismissKeyboard.js new file mode 100644 index 000000000..d8949e8b3 --- /dev/null +++ b/Libraries/Utilities/dismissKeyboard.js @@ -0,0 +1,16 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule dismissKeyboard + * + * This function dismisses the currently-open keyboard, if any + */ +'use strict'; + +var TextInputState = require('TextInputState'); + +function dismissKeyboard() { + TextInputState.blurTextInput(TextInputState.currentlyFocusedField()); +} + +module.exports = dismissKeyboard; diff --git a/Libraries/WebSocket/RCTSRWebSocket.h b/Libraries/WebSocket/RCTSRWebSocket.h index ccaa46455..c185daca9 100644 --- a/Libraries/WebSocket/RCTSRWebSocket.h +++ b/Libraries/WebSocket/RCTSRWebSocket.h @@ -17,14 +17,14 @@ #import #import -typedef enum { +typedef NS_ENUM(unsigned int, RCTSRReadyState) { RCTSR_CONNECTING = 0, RCTSR_OPEN = 1, RCTSR_CLOSING = 2, RCTSR_CLOSED = 3, -} RCTSRReadyState; +}; -typedef enum RCTSRStatusCode : NSInteger { +typedef NS_ENUM(NSInteger, RCTSRStatusCode) { RCTSRStatusCodeNormal = 1000, RCTSRStatusCodeGoingAway = 1001, RCTSRStatusCodeProtocolError = 1002, @@ -35,7 +35,7 @@ typedef enum RCTSRStatusCode : NSInteger { RCTSRStatusCodeInvalidUTF8 = 1007, RCTSRStatusCodePolicyViolated = 1008, RCTSRStatusCodeMessageTooBig = 1009, -} RCTSRStatusCode; +}; @class RCTSRWebSocket; diff --git a/Libraries/WebSocket/RCTSRWebSocket.m b/Libraries/WebSocket/RCTSRWebSocket.m index b65a847ee..44676b058 100644 --- a/Libraries/WebSocket/RCTSRWebSocket.m +++ b/Libraries/WebSocket/RCTSRWebSocket.m @@ -53,14 +53,14 @@ static inline void RCTSRFastLog(NSString *format, ...); @interface NSData (RCTSRWebSocket) -- (NSString *)stringBySHA1ThenBase64Encoding; +@property (nonatomic, readonly, copy) NSString *stringBySHA1ThenBase64Encoding; @end @interface NSString (RCTSRWebSocket) -- (NSString *)stringBySHA1ThenBase64Encoding; +@property (nonatomic, readonly, copy) NSString *stringBySHA1ThenBase64Encoding; @end @@ -69,7 +69,7 @@ static inline void RCTSRFastLog(NSString *format, ...); // The origin isn't really applicable for a native application. // So instead, just map ws -> http and wss -> https. -- (NSString *)RCTSR_origin; +@property (nonatomic, readonly, copy) NSString *RCTSR_origin; @end @@ -259,7 +259,7 @@ static __strong NSData *CRLFCRLF; return self; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) - (instancetype)initWithURLRequest:(NSURLRequest *)request; { @@ -297,16 +297,16 @@ RCT_NOT_IMPLEMENTED(-init) _delegateDispatchQueue = dispatch_get_main_queue(); - _readBuffer = [[NSMutableData alloc] init]; - _outputBuffer = [[NSMutableData alloc] init]; + _readBuffer = [NSMutableData new]; + _outputBuffer = [NSMutableData new]; - _currentFrameData = [[NSMutableData alloc] init]; + _currentFrameData = [NSMutableData new]; - _consumers = [[NSMutableArray alloc] init]; + _consumers = [NSMutableArray new]; - _consumerPool = [[RCTSRIOConsumerPool alloc] init]; + _consumerPool = [RCTSRIOConsumerPool new]; - _scheduledRunloops = [[NSMutableSet alloc] init]; + _scheduledRunloops = [NSMutableSet new]; [self _initializeStreams]; @@ -506,12 +506,12 @@ RCT_NOT_IMPLEMENTED(-init) if (_secure) { - NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *SSLOptions = [NSMutableDictionary new]; [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; // If we're using pinned certs, don't validate the certificate chain - if ([_urlRequest RCTSR_SSLPinnedCertificates].count) { + if (_urlRequest.RCTSR_SSLPinnedCertificates.count) { [SSLOptions setValue:@NO forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; } @@ -998,7 +998,7 @@ static const uint8_t RCTSRPayloadLenMask = 0x7F; - (void)_readFrameNew; { dispatch_async(_workQueue, ^{ - [_currentFrameData setLength:0]; + _currentFrameData.length = 0; _currentFrameOpcode = 0; _currentFrameCount = 0; @@ -1253,7 +1253,7 @@ static const size_t RCTSRFrameHeaderOverhead = 32; [self closeWithCode:RCTSRStatusCodeMessageTooBig reason:@"Message too big"]; return; } - uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes]; + uint8_t *frame_buffer = (uint8_t *)frame.mutableBytes; // set fin frame_buffer[0] = RCTSRFinMask | opcode; @@ -1318,7 +1318,7 @@ static const size_t RCTSRFrameHeaderOverhead = 32; { if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) { - NSArray *sslCerts = [_urlRequest RCTSR_SSLPinnedCertificates]; + NSArray *sslCerts = _urlRequest.RCTSR_SSLPinnedCertificates; if (sslCerts) { SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust]; if (secTrust) { @@ -1366,11 +1366,11 @@ static const size_t RCTSRFrameHeaderOverhead = 32; } case NSStreamEventErrorOccurred: { - RCTSRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]); + RCTSRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [aStream.streamError copy]); // TODO: specify error better! [self _failWithError:aStream.streamError]; _readBufferOffset = 0; - [_readBuffer setLength:0]; + _readBuffer.length = 0; break; } @@ -1475,10 +1475,10 @@ static const size_t RCTSRFrameHeaderOverhead = 32; { RCTSRIOConsumer *consumer = nil; if (_bufferedConsumers.count) { - consumer = [_bufferedConsumers lastObject]; + consumer = _bufferedConsumers.lastObject; [_bufferedConsumers removeLastObject]; } else { - consumer = [[RCTSRIOConsumer alloc] init]; + consumer = [RCTSRIOConsumer new]; } [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]; @@ -1522,7 +1522,7 @@ static const size_t RCTSRFrameHeaderOverhead = 32; - (NSString *)RCTSR_origin; { - NSString *scheme = [self.scheme lowercaseString]; + NSString *scheme = self.scheme.lowercaseString; if ([scheme isEqualToString:@"wss"]) { scheme = @"https"; @@ -1579,7 +1579,7 @@ static NSRunLoop *networkRunLoop = nil; { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - networkThread = [[_RCTSRRunLoopThread alloc] init]; + networkThread = [_RCTSRRunLoopThread new]; networkThread.name = @"com.squareup.SocketRocket.NetworkThread"; [networkThread start]; networkRunLoop = networkThread.runLoop; diff --git a/Libraries/WebSocket/RCTWebSocketExecutor.m b/Libraries/WebSocket/RCTWebSocketExecutor.m index df661a54b..815ab56d1 100644 --- a/Libraries/WebSocket/RCTWebSocketExecutor.m +++ b/Libraries/WebSocket/RCTWebSocketExecutor.m @@ -57,8 +57,8 @@ RCT_EXPORT_MODULE() _jsQueue = dispatch_queue_create("com.facebook.React.WebSocketExecutor", DISPATCH_QUEUE_SERIAL); _socket = [[RCTSRWebSocket alloc] initWithURL:_url]; _socket.delegate = self; - _callbacks = [[RCTSparseArray alloc] init]; - _injectedObjects = [[NSMutableDictionary alloc] init]; + _callbacks = [RCTSparseArray new]; + _injectedObjects = [NSMutableDictionary new]; [_socket setDelegateDispatchQueue:_jsQueue]; NSURL *startDevToolsURL = [NSURL URLWithString:@"/launch-chrome-devtools" relativeToURL:_url]; @@ -152,7 +152,7 @@ RCT_EXPORT_MODULE() { NSDictionary *message = @{ @"method": @"executeApplicationScript", - @"url": RCTNullIfNil([URL absoluteString]), + @"url": RCTNullIfNil(URL.absoluteString), @"inject": _injectedObjects, }; [self sendMessage:message waitForReply:^(NSError *error, NSDictionary *reply) { diff --git a/Libraries/WebSocket/RCTWebSocketManager.m b/Libraries/WebSocket/RCTWebSocketManager.m index c842cedc6..039dd7f22 100644 --- a/Libraries/WebSocket/RCTWebSocketManager.m +++ b/Libraries/WebSocket/RCTWebSocketManager.m @@ -45,7 +45,7 @@ RCT_EXPORT_MODULE() - (instancetype)init { if ((self = [super init])) { - _sockets = [[RCTSparseArray alloc] init]; + _sockets = [RCTSparseArray new]; } return self; } @@ -98,7 +98,7 @@ RCT_EXPORT_METHOD(close:(nonnull NSNumber *)socketID) - (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error { [_bridge.eventDispatcher sendDeviceEventWithName:@"websocketFailed" body:@{ - @"message":[error localizedDescription], + @"message":error.localizedDescription, @"id": webSocket.reactTag }]; } diff --git a/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js b/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js index 8ab4b866e..b84985e6b 100644 --- a/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js +++ b/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js @@ -430,9 +430,13 @@ function setResponderAndExtractTransfer( * @param {string} topLevelType Record from `EventConstants`. * @return {boolean} True if a transfer of responder could possibly occur. */ -function canTriggerTransfer(topLevelType, topLevelTargetID) { +function canTriggerTransfer(topLevelType, topLevelTargetID, nativeEvent) { return topLevelTargetID && ( - topLevelType === EventConstants.topLevelTypes.topScroll || + // responderIgnoreScroll: We are trying to migrate away from specifically tracking native scroll + // events here and responderIgnoreScroll indicates we will send topTouchCancel to handle + // canceling touch events instead + (topLevelType === EventConstants.topLevelTypes.topScroll && + !nativeEvent.responderIgnoreScroll) || (trackedTouchCount > 0 && topLevelType === EventConstants.topLevelTypes.topSelectionChange) || isStartish(topLevelType) || @@ -509,7 +513,7 @@ var ResponderEventPlugin = { ResponderTouchHistoryStore.recordTouchTrack(topLevelType, nativeEvent, nativeEventTarget); - var extracted = canTriggerTransfer(topLevelType, topLevelTargetID) ? + var extracted = canTriggerTransfer(topLevelType, topLevelTargetID, nativeEvent) ? setResponderAndExtractTransfer( topLevelType, topLevelTargetID, diff --git a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js index 2a0dd1813..93c4a7b59 100644 --- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js +++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js @@ -636,19 +636,19 @@ var TouchableMixin = { } if (IsPressingIn[curState] && signal === Signals.LONG_PRESS_DETECTED) { - this.touchableHandleLongPress && this.touchableHandleLongPress(); + this.touchableHandleLongPress && this.touchableHandleLongPress(e); } if (newIsHighlight && !curIsHighlight) { this._savePressInLocation(e); - this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(); + this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(e); } else if (!newIsHighlight && curIsHighlight && this.touchableHandleActivePressOut) { if (this.touchableGetPressOutDelayMS && this.touchableGetPressOutDelayMS()) { this.pressOutDelayTimeout = this.setTimeout(function() { - this.touchableHandleActivePressOut(); + this.touchableHandleActivePressOut(e); }, this.touchableGetPressOutDelayMS()); } else { - this.touchableHandleActivePressOut(); + this.touchableHandleActivePressOut(e); } } diff --git a/React.podspec b/React.podspec index d5193bfcb..9634b6964 100644 --- a/React.podspec +++ b/React.podspec @@ -50,12 +50,6 @@ Pod::Spec.new do |s| ss.preserve_paths = "Libraries/AdSupport/*.js" end - s.subspec 'RCTAnimationExperimental' do |ss| - ss.dependency 'React/Core' - ss.source_files = "Libraries/Animation/RCTAnimationExperimental*.{h,m}" - ss.preserve_paths = "Libraries/Animation/*.js" - end - s.subspec 'RCTGeolocation' do |ss| ss.dependency 'React/Core' ss.source_files = "Libraries/Geolocation/*.{h,m}" @@ -64,6 +58,7 @@ Pod::Spec.new do |s| s.subspec 'RCTImage' do |ss| ss.dependency 'React/Core' + ss.dependency 'React/RCTNetwork' ss.source_files = "Libraries/Image/*.{h,m}" ss.preserve_paths = "Libraries/Image/*.js" end diff --git a/React/Base/RCTAssert.m b/React/Base/RCTAssert.m index a990f7c1b..4c497614c 100644 --- a/React/Base/RCTAssert.m +++ b/React/Base/RCTAssert.m @@ -60,7 +60,7 @@ static RCTAssertFunction RCTGetLocalAssertFunction() { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; NSArray *functionStack = threadDictionary[RCTAssertFunctionStack]; - RCTAssertFunction assertFunction = [functionStack lastObject]; + RCTAssertFunction assertFunction = functionStack.lastObject; if (assertFunction) { return assertFunction; } @@ -72,7 +72,7 @@ void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction as NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; NSMutableArray *functionStack = threadDictionary[RCTAssertFunctionStack]; if (!functionStack) { - functionStack = [[NSMutableArray alloc] init]; + functionStack = [NSMutableArray new]; threadDictionary[RCTAssertFunctionStack] = functionStack; } [functionStack addObject:assertFunction]; @@ -83,7 +83,7 @@ void RCTPerformBlockWithAssertFunction(void (^block)(void), RCTAssertFunction as NSString *RCTCurrentThreadName(void) { NSThread *thread = [NSThread currentThread]; - NSString *threadName = [thread isMainThread] ? @"main" : thread.name; + NSString *threadName = thread.isMainThread ? @"main" : thread.name; if (threadName.length == 0) { const char *label = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL); if (label && strlen(label) > 0) { diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 108b80cca..ad0cc39ee 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -46,12 +46,12 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCT_EXTERN NSArray *RCTGetModuleClasses(void); -static id RCTLatestExecutor = nil; -id RCTGetLatestExecutor(void); -id RCTGetLatestExecutor(void) -{ - return RCTLatestExecutor; -} +@interface RCTBridge () + ++ (instancetype)currentBridge; ++ (void)setCurrentBridge:(RCTBridge *)bridge; + +@end @interface RCTBatchedBridge : RCTBridge @@ -62,6 +62,7 @@ id RCTGetLatestExecutor(void) @implementation RCTBatchedBridge { BOOL _loading; + BOOL _valid; __weak id _javaScriptExecutor; NSMutableArray *_moduleDataByID; RCTModuleMap *_modulesByName; @@ -72,8 +73,6 @@ id RCTGetLatestExecutor(void) RCTSparseArray *_scheduledCallbacks; } -@synthesize valid = _valid; - - (instancetype)initWithParentBridge:(RCTBridge *)bridge { RCTAssertMainThread(); @@ -90,10 +89,10 @@ id RCTGetLatestExecutor(void) */ _valid = YES; _loading = YES; - _moduleDataByID = [[NSMutableArray alloc] init]; - _frameUpdateObservers = [[NSMutableSet alloc] init]; - _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[RCTSparseArray alloc] init]; + _moduleDataByID = [NSMutableArray new]; + _frameUpdateObservers = [NSMutableSet new]; + _scheduledCalls = [NSMutableArray new]; + _scheduledCallbacks = [RCTSparseArray new]; _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; if (RCT_DEV) { @@ -101,6 +100,7 @@ id RCTGetLatestExecutor(void) [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; } + [RCTBridge setCurrentBridge:self]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptWillStartLoadingNotification object:self @@ -113,27 +113,26 @@ id RCTGetLatestExecutor(void) - (void)start { - __weak RCTBatchedBridge *weakSelf = self; - - __block NSString *sourceCode; - __block NSString *config; - dispatch_queue_t bridgeQueue = dispatch_queue_create("com.facebook.react.RCTBridgeQueue", DISPATCH_QUEUE_CONCURRENT); + dispatch_group_t initModulesAndLoadSource = dispatch_group_create(); - dispatch_group_enter(initModulesAndLoadSource); - [weakSelf loadSource:^(NSError *error, NSString *source) { - if (error) { - RCTLogError(@"%@", error); - } else { - sourceCode = source; - } - + __block NSString *sourceCode; + [self loadSource:^(__unused NSError *error, NSString *source) { + sourceCode = source; dispatch_group_leave(initModulesAndLoadSource); }]; + // Synchronously initialize all native modules [self initModules]; + if (RCTProfileIsProfiling()) { + // Depends on moduleDataByID being loaded + RCTProfileHookModules(self); + } + + __weak RCTBatchedBridge *weakSelf = self; + __block NSString *config; dispatch_group_enter(initModulesAndLoadSource); dispatch_async(bridgeQueue, ^{ dispatch_group_t setupJSExecutorAndModuleConfig = dispatch_group_create(); @@ -144,16 +143,16 @@ id RCTGetLatestExecutor(void) dispatch_group_async(setupJSExecutorAndModuleConfig, bridgeQueue, ^{ if (weakSelf.isValid) { config = [weakSelf moduleConfig]; - - if (RCTProfileIsProfiling()) { - RCTProfileHookModules(weakSelf); - } } }); dispatch_group_notify(setupJSExecutorAndModuleConfig, bridgeQueue, ^{ - [weakSelf injectJSONConfiguration:config onComplete:^(__unused NSError *error) {}]; - + // We're not waiting for this complete to leave the dispatch group, since + // injectJSONConfiguration and executeSourceCode will schedule operations on the + // same queue anyway. + [weakSelf injectJSONConfiguration:config onComplete:^(__unused NSError *error) { + RCTPerformanceLoggerEnd(RCTPLNativeModuleInit); + }]; dispatch_group_leave(initModulesAndLoadSource); }); }); @@ -167,54 +166,55 @@ id RCTGetLatestExecutor(void) - (void)loadSource:(RCTSourceLoadBlock)_onSourceLoad { - RCTPerformanceLoggerStart(RCTPLScriptDownload); - RCTProfileBeginEvent(); + RCTPerformanceLoggerStart(RCTPLScriptDownload); + int cookie = RCTProfileBeginAsyncEvent(0, @"JavaScript download", nil); - RCTSourceLoadBlock onSourceLoad = ^(NSError *error, NSString *source) { - RCTPerformanceLoggerEnd(RCTPLScriptDownload); - RCTProfileEndEvent(@"JavaScript download", @"init,download", @[]); + RCTSourceLoadBlock onSourceLoad = ^(NSError *error, NSString *source) { + RCTProfileEndAsyncEvent(0, @"init,download", cookie, @"JavaScript download", nil); + RCTPerformanceLoggerEnd(RCTPLScriptDownload); - if (error) { - NSArray *stack = [error userInfo][@"stack"]; - if (stack) { - [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] - withStack:stack]; - } else { - [[RCTRedBox sharedInstance] showErrorMessage:[error localizedDescription] - withDetails:[error localizedFailureReason]]; - } - - NSDictionary *userInfo = @{@"bridge": self, @"error": error}; - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification - object:_parentBridge - userInfo:userInfo]; + if (error) { + NSArray *stack = error.userInfo[@"stack"]; + if (stack) { + [self.redBox showErrorMessage:error.localizedDescription + withStack:stack]; + } else { + [self.redBox showErrorMessage:error.localizedDescription + withDetails:error.localizedFailureReason]; } - _onSourceLoad(error, source); - }; - - if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) { - [self.delegate loadSourceForBridge:_parentBridge withBlock:onSourceLoad]; - } else if (self.bundleURL) { - [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:onSourceLoad]; - } else { - // Allow testing without a script - dispatch_async(dispatch_get_main_queue(), ^{ - _loading = NO; - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification - object:_parentBridge - userInfo:@{ @"bridge": self }]; - }); - onSourceLoad(nil, nil); + NSDictionary *userInfo = @{@"bridge": self, @"error": error}; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidFailToLoadNotification + object:_parentBridge + userInfo:userInfo]; } + + _onSourceLoad(error, source); + }; + + if ([self.delegate respondsToSelector:@selector(loadSourceForBridge:withBlock:)]) { + [self.delegate loadSourceForBridge:_parentBridge withBlock:onSourceLoad]; + } else if (self.bundleURL) { + [RCTJavaScriptLoader loadBundleAtURL:self.bundleURL onComplete:onSourceLoad]; + } else { + // Allow testing without a script + dispatch_async(dispatch_get_main_queue(), ^{ + _loading = NO; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification + object:_parentBridge + userInfo:@{ @"bridge": self }]; + }); + onSourceLoad(nil, nil); + } } - (void)initModules { RCTAssertMainThread(); + RCTPerformanceLoggerStart(RCTPLNativeModuleInit); // Register passed-in module instances - NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *preregisteredModules = [NSMutableDictionary new]; NSArray *extraModules = nil; if (self.delegate) { @@ -230,7 +230,7 @@ id RCTGetLatestExecutor(void) } // Instantiate modules - _moduleDataByID = [[NSMutableArray alloc] init]; + _moduleDataByID = [NSMutableArray new]; NSMutableDictionary *modulesByName = [preregisteredModules mutableCopy]; for (Class moduleClass in RCTGetModuleClasses()) { NSString *moduleName = RCTBridgeModuleNameForClass(moduleClass); @@ -242,20 +242,14 @@ id RCTGetLatestExecutor(void) // Preregistered instances takes precedence, no questions asked if (!preregisteredModules[moduleName]) { // It's OK to have a name collision as long as the second instance is nil - RCTAssert([[moduleClass alloc] init] == nil, + RCTAssert([moduleClass new] == nil, @"Attempted to register RCTBridgeModule class %@ for the name " "'%@', but name was already registered by class %@", moduleClass, moduleName, [modulesByName[moduleName] class]); } - if ([module class] != moduleClass) { - RCTLogInfo(@"RCTBridgeModule of class %@ with name '%@' was encountered " - "in the project, but name was already registered by class %@." - "That's fine if it's intentional - just letting you know.", - moduleClass, moduleName, [modulesByName[moduleName] class]); - } } else { // Module name hasn't been used before, so go ahead and instantiate - module = [[moduleClass alloc] init]; + module = [moduleClass new]; } if (module) { modulesByName[moduleName] = module; @@ -270,10 +264,8 @@ id RCTGetLatestExecutor(void) * any other module has access to the bridge */ _javaScriptExecutor = _modulesByName[RCTBridgeModuleNameForClass(self.executorClass)]; - RCTLatestExecutor = _javaScriptExecutor; for (id module in _modulesByName.allValues) { - // Bridge must be set before moduleData is set up, as methodQueue // initialization requires it (View Managers get their queue by calling // self.bridge.uiManager.methodQueue) @@ -295,12 +287,11 @@ id RCTGetLatestExecutor(void) - (void)setupExecutor { [_javaScriptExecutor setUp]; - } - (NSString *)moduleConfig { - NSMutableDictionary *config = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *config = [NSMutableDictionary new]; for (RCTModuleData *moduleData in _moduleDataByID) { config[moduleData.name] = moduleData.config; if ([moduleData.instance conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) { @@ -316,7 +307,7 @@ id RCTGetLatestExecutor(void) - (void)injectJSONConfiguration:(NSString *)configJSON onComplete:(void (^)(NSError *))onComplete { - if (!self.isValid) { + if (!self.valid) { return; } @@ -324,7 +315,7 @@ id RCTGetLatestExecutor(void) asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:^(NSError *error) { if (error) { - [[RCTRedBox sharedInstance] showError:error]; + [self.redBox showError:error]; } onComplete(error); }]; @@ -332,9 +323,7 @@ id RCTGetLatestExecutor(void) - (void)executeSourceCode:(NSString *)sourceCode { - _loading = NO; - - if (!self.isValid || !_javaScriptExecutor) { + if (!self.valid || !_javaScriptExecutor) { return; } @@ -342,37 +331,29 @@ id RCTGetLatestExecutor(void) sourceCodeModule.scriptURL = self.bundleURL; sourceCodeModule.scriptText = sourceCode; - static BOOL shouldDismiss = NO; - if (shouldDismiss) { - [[RCTRedBox sharedInstance] dismiss]; - } - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - shouldDismiss = YES; - }); - [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:^(NSError *loadError) { - if (loadError) { - [[RCTRedBox sharedInstance] showError:loadError]; + [self.redBox showError:loadError]; return; } - /** - * Register the display link to start sending js calls after everything - * is setup - */ + // Register the display link to start sending js calls after everything is setup NSRunLoop *targetRunLoop = [_javaScriptExecutor isKindOfClass:[RCTContextExecutor class]] ? [NSRunLoop currentRunLoop] : [NSRunLoop mainRunLoop]; [_jsDisplayLink addToRunLoop:targetRunLoop forMode:NSRunLoopCommonModes]; - [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification - object:_parentBridge - userInfo:@{ @"bridge": self }]; + // Perform the state update and notification on the main thread, so we can't run into + // timing issues with RCTRootView + dispatch_async(dispatch_get_main_queue(), ^{ + _loading = NO; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTJavaScriptDidLoadNotification + object:_parentBridge + userInfo:@{ @"bridge": self }]; + }); }]; } -RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL +RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleURL moduleProvider:(__unused RCTBridgeModuleProviderBlock)block launchOptions:(__unused NSDictionary *)launchOptions) @@ -436,15 +417,16 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL - (void)invalidate { - if (!self.isValid) { + if (!self.valid) { return; } RCTAssertMainThread(); + _loading = NO; _valid = NO; - if (RCTLatestExecutor == _javaScriptExecutor) { - RCTLatestExecutor = nil; + if ([RCTBridge currentBridge] == self) { + [RCTBridge setCurrentBridge:nil]; } [_mainDisplayLink invalidate]; @@ -484,6 +466,16 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL }); } +- (void)logMessage:(NSString *)message level:(NSString *)level +{ + if (RCT_DEBUG) { + [_javaScriptExecutor executeJSCall:@"RCTLog" + method:@"logIfNoNativeHook" + arguments:@[level, message] + callback:^(__unused id json, __unused NSError *error) {}]; + } +} + #pragma mark - RCTBridge methods /** @@ -518,7 +510,9 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL } } -- (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete +- (void)enqueueApplicationScript:(NSString *)script + url:(NSURL *)url + onComplete:(RCTJavaScriptCompleteBlock)onComplete { RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); @@ -532,20 +526,21 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL return; } - RCTProfileBeginEvent(); + RCTProfileBeginEvent(0, @"FetchApplicationScriptCallbacks", nil); [_javaScriptExecutor executeJSCall:@"BatchedBridge" method:@"flushedQueue" arguments:@[] - callback:^(id json, NSError *error) { - RCTProfileEndEvent(@"FetchApplicationScriptCallbacks", @"js_call,init", @{ - @"json": RCTNullIfNil(json), - @"error": RCTNullIfNil(error), - }); + callback:^(id json, NSError *error) + { + RCTProfileEndEvent(0, @"js_call,init", @{ + @"json": RCTNullIfNil(json), + @"error": RCTNullIfNil(error), + }); - [self _handleBuffer:json]; + [self _handleBuffer:json]; - onComplete(error); - }]; + onComplete(error); + }]; }]; } @@ -566,7 +561,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL __weak RCTBatchedBridge *weakSelf = self; [_javaScriptExecutor executeBlockOnJavaScriptQueue:^{ RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(); + RCTProfileBeginEvent(0, @"enqueue_call", nil); RCTBatchedBridge *strongSelf = weakSelf; if (!strongSelf.isValid || !strongSelf->_scheduledCallbacks || !strongSelf->_scheduledCalls) { @@ -589,11 +584,13 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL [strongSelf->_scheduledCalls addObject:call]; } - RCTProfileEndEvent(@"enqueue_call", @"objc_call", call); + RCTProfileEndEvent(0, @"objc_call", call); }]; } -- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args +- (void)_actuallyInvokeAndProcessModule:(NSString *)module + method:(NSString *)method + arguments:(NSArray *)args { RCTAssertJSThread(); @@ -601,7 +598,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL RCTJavaScriptCallback processResponse = ^(id json, NSError *error) { if (error) { - [[RCTRedBox sharedInstance] showError:error]; + [self.redBox showError:error]; } if (!self.isValid) { @@ -637,7 +634,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL } for (NSUInteger fieldIndex = RCTBridgeFieldRequestModuleIDs; fieldIndex <= RCTBridgeFieldParamss; fieldIndex++) { - id field = [requestsArray objectAtIndex:fieldIndex]; + id field = requestsArray[fieldIndex]; if (![field isKindOfClass:[NSArray class]]) { RCTLogError(@"Field at index %zd in buffer must be an instance of NSArray, got %@", fieldIndex, NSStringFromClass([field class])); return; @@ -650,7 +647,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs]; NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss]; - NSUInteger numRequests = [moduleIDs count]; + NSUInteger numRequests = moduleIDs.count; if (RCT_DEBUG && (numRequests != methodIDs.count || numRequests != paramsArrays.count)) { RCTLogError(@"Invalid data message - all must be length: %zd", numRequests); @@ -668,7 +665,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL } NSMutableOrderedSet *set = [buckets objectForKey:moduleData]; if (!set) { - set = [[NSMutableOrderedSet alloc] init]; + set = [NSMutableOrderedSet new]; [buckets setObject:set forKey:moduleData]; } [set addObject:@(i)]; @@ -679,7 +676,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL [moduleData dispatchBlock:^{ RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(); + RCTProfileBeginEvent(0, RCTCurrentThreadName(), nil); NSOrderedSet *calls = [buckets objectForKey:moduleData]; @autoreleasepool { @@ -692,7 +689,9 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL } } - RCTProfileEndEvent(RCTCurrentThreadName(), @"objc_call,dispatch_async", @{ @"calls": @(calls.count) }); + RCTProfileEndEvent(0, @"objc_call,dispatch_async", @{ + @"calls": @(calls.count), + }); }]; } @@ -720,7 +719,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL return NO; } - RCTProfileBeginEvent(); + RCTProfileBeginEvent(0, @"Invoke callback", nil); RCTModuleData *moduleData = _moduleDataByID[moduleID]; if (RCT_DEBUG && !moduleData) { @@ -744,7 +743,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL } } - RCTProfileEndEvent(@"Invoke callback", @"objc_call", @{ + RCTProfileEndEvent(0, @"objc_call", @{ @"module": NSStringFromClass(method.moduleClass), @"method": method.JSMethodName, @"selector": NSStringFromSelector(method.selector), @@ -757,20 +756,20 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL - (void)_jsThreadUpdate:(CADisplayLink *)displayLink { RCTAssertJSThread(); - RCTProfileBeginEvent(); + RCTProfileBeginEvent(0, @"DispatchFrameUpdate", nil); RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; for (RCTModuleData *moduleData in _frameUpdateObservers) { id observer = (id)moduleData.instance; - if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { + if (![observer respondsToSelector:@selector(isPaused)] || !observer.paused) { RCT_IF_DEV(NSString *name = [NSString stringWithFormat:@"[%@ didUpdateFrame:%f]", observer, displayLink.timestamp];) RCTProfileBeginFlowEvent(); [moduleData dispatchBlock:^{ RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(); + RCTProfileBeginEvent(0, name, nil); [observer didUpdateFrame:frameUpdate]; - RCTProfileEndEvent(name, @"objc_call,fps", nil); + RCTProfileEndEvent(0, @"objc_call,fps", nil); }]; } } @@ -778,7 +777,7 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL NSArray *calls = [_scheduledCallbacks.allObjects arrayByAddingObjectsFromArray:_scheduledCalls]; RCT_IF_DEV( - RCTProfileImmediateEvent(@"JS Thread Tick", displayLink.timestamp, @"g"); + RCTProfileImmediateEvent(0, @"JS Thread Tick", 'g'); for (NSDictionary *call in calls) { _RCTProfileEndFlowEvent(call[@"call_id"]); @@ -786,25 +785,27 @@ RCT_NOT_IMPLEMENTED(-initWithBundleURL:(__unused NSURL *)bundleURL ) if (calls.count > 0) { - _scheduledCalls = [[NSMutableArray alloc] init]; - _scheduledCallbacks = [[RCTSparseArray alloc] init]; + _scheduledCalls = [NSMutableArray new]; + _scheduledCallbacks = [RCTSparseArray new]; [self _actuallyInvokeAndProcessModule:@"BatchedBridge" method:@"processBatch" arguments:@[[calls valueForKey:@"js_args"]]]; } - RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); + RCTProfileEndEvent(0, @"objc_call", nil); - dispatch_async(dispatch_get_main_queue(), ^{ - [self.perfStats.jsGraph onTick:displayLink.timestamp]; - }); + RCT_IF_DEV( + dispatch_async(dispatch_get_main_queue(), ^{ + [self.perfStats.jsGraph onTick:displayLink.timestamp]; + }); + ) } - (void)_mainThreadUpdate:(CADisplayLink *)displayLink { RCTAssertMainThread(); - RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); + RCTProfileImmediateEvent(0, @"VSYNC", 'g'); _modulesByName == nil ?: [self.perfStats.uiGraph onTick:displayLink.timestamp]; } diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index dce1c1f5f..88bcf8971 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -145,6 +145,11 @@ RCT_EXTERN NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); */ @property (nonatomic, readonly, getter=isLoading) BOOL loading; +/** + * Use this to check if the bridge has been invalidated. + */ +@property (nonatomic, readonly, getter=isValid) BOOL valid; + /** * The block passed in the constructor with pre-initialized modules */ diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index de6caaf70..4334be311 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -39,8 +39,6 @@ NSString *const RCTDidCreateNativeModules = @"RCTDidCreateNativeModules"; @end -RCT_EXTERN id RCTGetLatestExecutor(void); - static NSMutableArray *RCTModuleClasses; NSArray *RCTGetModuleClasses(void); NSArray *RCTGetModuleClasses(void) @@ -57,7 +55,7 @@ void RCTRegisterModule(Class moduleClass) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - RCTModuleClasses = [[NSMutableArray alloc] init]; + RCTModuleClasses = [NSMutableArray new]; }); RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)], @@ -73,11 +71,12 @@ void RCTRegisterModule(Class moduleClass) */ NSString *RCTBridgeModuleNameForClass(Class cls) { - NSString *name = nil; - if ([cls respondsToSelector:NSSelectorFromString(@"moduleName")]) { - name = [cls valueForKey:@"moduleName"]; - } - if ([name length] == 0) { +#if RCT_DEV + RCTAssert([cls conformsToProtocol:@protocol(RCTBridgeModule)], @"Bridge module classes must conform to RCTBridgeModule"); +#endif + + NSString *name = [cls moduleName]; + if (name.length == 0) { name = NSStringFromClass(cls); } if ([name hasPrefix:@"RK"]) { @@ -142,6 +141,24 @@ dispatch_queue_t RCTJSThread; }); } +static RCTBridge *RCTCurrentBridgeInstance = nil; + +/** + * The last current active bridge instance. This is set automatically whenever + * the bridge is accessed. It can be useful for static functions or singletons + * that need to access the bridge for purposes such as logging, but should not + * be relied upon to return any particular instance, due to race conditions. + */ ++ (instancetype)currentBridge +{ + return RCTCurrentBridgeInstance; +} + ++ (void)setCurrentBridge:(RCTBridge *)currentBridge +{ + RCTCurrentBridgeInstance = currentBridge; +} + - (instancetype)initWithDelegate:(id)delegate launchOptions:(NSDictionary *)launchOptions { @@ -170,13 +187,13 @@ dispatch_queue_t RCTJSThread; _bundleURL = bundleURL; _moduleProvider = block; _launchOptions = [launchOptions copy]; - [self bindKeys]; [self setUp]; + [self bindKeys]; } return self; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) - (void)dealloc { @@ -198,21 +215,18 @@ RCT_NOT_IMPLEMENTED(-init) object:nil]; #if TARGET_IPHONE_SIMULATOR - RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; // reload in current mode [commands registerKeyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand - action:^(__unused UIKeyCommand *command) - { + action:^(__unused UIKeyCommand *command) { [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil userInfo:nil]; }]; #endif - } - (RCTEventDispatcher *)eventDispatcher @@ -246,7 +260,7 @@ RCT_NOT_IMPLEMENTED(-init) - (BOOL)isValid { - return _batchedBridge.isValid; + return _batchedBridge.valid; } - (void)invalidate @@ -257,18 +271,9 @@ RCT_NOT_IMPLEMENTED(-init) _batchedBridge = nil; } -+ (void)logMessage:(NSString *)message level:(NSString *)level +- (void)logMessage:(NSString *)message level:(NSString *)level { - dispatch_async(dispatch_get_main_queue(), ^{ - if (!RCTGetLatestExecutor().isValid) { - return; - } - - [RCTGetLatestExecutor() executeJSCall:@"RCTLog" - method:@"logIfNoNativeHook" - arguments:@[level, message] - callback:^(__unused id json, __unused NSError *error) {}]; - }); + [_batchedBridge logMessage:message level:level]; } - (NSDictionary *)modules diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 4d9c4c1fa..782c5dffa 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -52,6 +52,21 @@ extern dispatch_queue_t RCTJSThread; * Provides the interface needed to register a bridge module. */ @protocol RCTBridgeModule + +/** + * Place this macro in your class implementation to automatically register + * your module with the bridge when it loads. The optional js_name argument + * will be used as the JS module name. If omitted, the JS module name will + * match the Objective-C class name. + */ +#define RCT_EXPORT_MODULE(js_name) \ +RCT_EXTERN void RCTRegisterModule(Class); \ ++ (NSString *)moduleName { return @#js_name; } \ ++ (void)load { RCTRegisterModule(self); } + +// Implemented by RCT_EXPORT_MODULE ++ (NSString *)moduleName; + @optional /** @@ -85,17 +100,6 @@ extern dispatch_queue_t RCTJSThread; */ @property (nonatomic, strong, readonly) dispatch_queue_t methodQueue; -/** - * Place this macro in your class implementation to automatically register - * your module with the bridge when it loads. The optional js_name argument - * will be used as the JS module name. If omitted, the JS module name will - * match the Objective-C class name. - */ -#define RCT_EXPORT_MODULE(js_name) \ - RCT_EXTERN void RCTRegisterModule(Class); \ - + (NSString *)moduleName { return @#js_name; } \ - + (void)load { RCTRegisterModule(self); } - /** * Wrap the parameter line of your method implementation with this macro to * expose it to JS. By default the exposed method will match the first part of diff --git a/React/Base/RCTCache.m b/React/Base/RCTCache.m index a0646a401..473dcd406 100644 --- a/React/Base/RCTCache.m +++ b/React/Base/RCTCache.m @@ -52,14 +52,14 @@ NSLock *_lock; } -- (id)init +- (instancetype)init { if ((self = [super init])) { //create storage - _cache = [[NSMutableDictionary alloc] init]; - _entryPool = [[NSMutableArray alloc] init]; - _lock = [[NSLock alloc] init]; + _cache = [NSMutableDictionary new]; + _entryPool = [NSMutableArray new]; + _lock = [NSLock new]; _totalCost = 0; #if TARGET_OS_IPHONE @@ -103,7 +103,7 @@ - (NSUInteger)count { - return [_cache count]; + return _cache.count; } - (void)cleanUp:(BOOL)keepEntries @@ -162,7 +162,7 @@ [_lock lock]; if (_delegateRespondsToShouldEvictObject || _delegateRespondsToWillEvictObject) { - NSArray *keys = [_cache allKeys]; + NSArray *keys = _cache.allKeys; if (_delegateRespondsToShouldEvictObject) { //sort, oldest first (in case we want to use that information in our eviction test) @@ -202,7 +202,7 @@ - (void)resequence { //sort, oldest first - NSArray *entries = [[_cache allValues] sortedArrayUsingComparator:^NSComparisonResult(RCTCacheEntry *entry1, RCTCacheEntry *entry2) { + NSArray *entries = [_cache.allValues sortedArrayUsingComparator:^NSComparisonResult(RCTCacheEntry *entry1, RCTCacheEntry *entry2) { return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber)); }]; @@ -256,7 +256,7 @@ _totalCost += g; RCTCacheEntry *entry = _cache[key]; if (!entry) { - entry = [[RCTCacheEntry alloc] init]; + entry = [RCTCacheEntry new]; _cache[key] = entry; } entry.object = obj; diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 415a5968f..cad32b860 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -17,7 +17,6 @@ #import "RCTLog.h" #import "RCTPointerEvents.h" - /** * This class provides a collection of conversion functions for mapping * JSON objects to native types and classes. These are useful when writing @@ -39,10 +38,11 @@ + (NSUInteger)NSUInteger:(id)json; + (NSArray *)NSArray:(id)json; -+ (NSSet *)NSSet:(id)json; + (NSDictionary *)NSDictionary:(id)json; + (NSString *)NSString:(id)json; + (NSNumber *)NSNumber:(id)json; + ++ (NSSet *)NSSet:(id)json; + (NSData *)NSData:(id)json; + (NSIndexSet *)NSIndexSet:(id)json; @@ -80,10 +80,10 @@ typedef NSURL RCTFileURL; + (CGAffineTransform)CGAffineTransform:(id)json; + (UIColor *)UIColor:(id)json; -+ (CGColorRef)CGColor:(id)json; ++ (CGColorRef)CGColor:(id)json CF_RETURNS_NOT_RETAINED; + (UIImage *)UIImage:(id)json; -+ (CGImageRef)CGImage:(id)json; ++ (CGImageRef)CGImage:(id)json CF_RETURNS_NOT_RETAINED; + (UIFont *)UIFont:(id)json; + (UIFont *)UIFont:(UIFont *)font withSize:(id)json; @@ -94,6 +94,9 @@ typedef NSURL RCTFileURL; size:(id)size weight:(id)weight style:(id)style scaleMultiplier:(CGFloat)scaleMultiplier; +typedef NSArray NSArrayArray; ++ (NSArrayArray *)NSArrayArray:(id)json; + typedef NSArray NSStringArray; + (NSStringArray *)NSStringArray:(id)json; @@ -164,7 +167,6 @@ RCT_CUSTOM_CONVERTER(type, name, [json getter]) #define RCT_CUSTOM_CONVERTER(type, name, code) \ + (type)name:(id)json \ { \ - json = (json == (id)kCFNull) ? nil : json; \ if (!RCT_DEBUG) { \ return code; \ } else { \ @@ -185,7 +187,7 @@ RCT_CUSTOM_CONVERTER(type, name, [json getter]) * detailed error reporting if an invalid value is passed in. */ #define RCT_NUMBER_CONVERTER(type, getter) \ -RCT_CUSTOM_CONVERTER(type, type, [[self NSNumber:json] getter]) +RCT_CUSTOM_CONVERTER(type, type, [RCT_DEBUG ? [self NSNumber:json] : json getter]) /** * This macro is used for creating converters for enum types. diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index ca091592e..8141b1386 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -29,43 +29,38 @@ RCT_NUMBER_CONVERTER(uint64_t, unsignedLongLongValue); RCT_NUMBER_CONVERTER(NSInteger, integerValue) RCT_NUMBER_CONVERTER(NSUInteger, unsignedIntegerValue) -RCT_CUSTOM_CONVERTER(NSArray *, NSArray, [NSArray arrayWithArray:json]) +/** + * This macro is used for creating converter functions for directly + * representable json values that require no conversion. + */ +#if RCT_DEBUG +#define RCT_JSON_CONVERTER(type) \ ++ (type *)type:(id)json \ +{ \ + if ([json isKindOfClass:[type class]]) { \ + return json; \ + } else if (json) { \ + RCTLogConvertError(json, @#type); \ + } \ + return nil; \ +} +#else +#define RCT_JSON_CONVERTER(type) \ ++ (type *)type:(id)json { return json; } +#endif + +RCT_JSON_CONVERTER(NSArray) +RCT_JSON_CONVERTER(NSDictionary) +RCT_JSON_CONVERTER(NSString) +RCT_JSON_CONVERTER(NSNumber) + RCT_CUSTOM_CONVERTER(NSSet *, NSSet, [NSSet setWithArray:json]) -RCT_CUSTOM_CONVERTER(NSDictionary *, NSDictionary, [NSDictionary dictionaryWithDictionary:json]) -RCT_CONVERTER(NSString *, NSString, description) - -+ (NSNumber *)NSNumber:(id)json -{ - if ([json isKindOfClass:[NSNumber class]]) { - return json; - } else if ([json isKindOfClass:[NSString class]]) { - static NSNumberFormatter *formatter; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - formatter = [[NSNumberFormatter alloc] init]; - formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; - }); - NSNumber *number = [formatter numberFromString:json]; - if (!number) { - RCTLogConvertError(json, @"a number"); - } - return number; - } else if (json && json != (id)kCFNull) { - RCTLogConvertError(json, @"a number"); - } - return nil; -} - -+ (NSData *)NSData:(id)json -{ - // TODO: should we automatically decode base64 data? Probably not... - return [[self NSString:json] dataUsingEncoding:NSUTF8StringEncoding]; -} +RCT_CUSTOM_CONVERTER(NSData *, NSData, [json dataUsingEncoding:NSUTF8StringEncoding]) + (NSIndexSet *)NSIndexSet:(id)json { json = [self NSNumberArray:json]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] init]; + NSMutableIndexSet *indexSet = [NSMutableIndexSet new]; for (NSNumber *number in json) { NSInteger index = number.integerValue; if (RCT_DEBUG && index < 0) { @@ -79,7 +74,7 @@ RCT_CONVERTER(NSString *, NSString, description) + (NSURL *)NSURL:(id)json { NSString *path = [self NSString:json]; - if (!path.length) { + if (!path) { return nil; } @@ -100,13 +95,13 @@ RCT_CONVERTER(NSString *, NSString, description) } // Assume that it's a local path - path = [path stringByRemovingPercentEncoding]; + path = path.stringByRemovingPercentEncoding; if ([path hasPrefix:@"~"]) { // Path is inside user directory - path = [path stringByExpandingTildeInPath]; - } else if (![path isAbsolutePath]) { + path = path.stringByExpandingTildeInPath; + } else if (!path.absolutePath) { // Assume it's a resource path - path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:path]; + path = [[NSBundle mainBundle].resourcePath stringByAppendingPathComponent:path]; } return [NSURL fileURLWithPath:path]; } @@ -125,7 +120,7 @@ RCT_CONVERTER(NSString *, NSString, description) + (RCTFileURL *)RCTFileURL:(id)json { NSURL *fileURL = [self NSURL:json]; - if (![fileURL isFileURL]) { + if (!fileURL.fileURL) { RCTLogError(@"URI must be a local file, '%@' isn't.", fileURL); return nil; } @@ -144,7 +139,7 @@ RCT_CONVERTER(NSString *, NSString, description) static NSDateFormatter *formatter; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - formatter = [[NSDateFormatter alloc] init]; + formatter = [NSDateFormatter new]; formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"; formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; @@ -155,7 +150,7 @@ RCT_CONVERTER(NSString *, NSString, description) "Expected format: YYYY-MM-DD'T'HH:mm:ss.sssZ", json); } return date; - } else if (json && json != (id)kCFNull) { + } else if (json) { RCTLogConvertError(json, @"a date"); } return nil; @@ -169,24 +164,23 @@ RCT_CUSTOM_CONVERTER(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFro NSNumber *RCTConvertEnumValue(const char *typeName, NSDictionary *mapping, NSNumber *defaultValue, id json) { - if (!json || json == (id)kCFNull) { + if (!json) { return defaultValue; } if ([json isKindOfClass:[NSNumber class]]) { - NSArray *allValues = [mapping allValues]; - if ([[mapping allValues] containsObject:json] || [json isEqual:defaultValue]) { + NSArray *allValues = mapping.allValues; + if ([mapping.allValues containsObject:json] || [json isEqual:defaultValue]) { return json; } RCTLogError(@"Invalid %s '%@'. should be one of: %@", typeName, json, allValues); return defaultValue; } - - if (![json isKindOfClass:[NSString class]]) { + if (RCT_DEBUG && ![json isKindOfClass:[NSString class]]) { RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", typeName, [json classForCoder], json); } id value = mapping[json]; - if (!value && [json description].length > 0) { + if (RCT_DEBUG && !value && [json description].length > 0) { RCTLogError(@"Invalid %s '%@'. should be one of: %@", typeName, json, [[mapping allKeys] sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)]); } return value ?: defaultValue; @@ -201,7 +195,7 @@ NSNumber *RCTConvertMultiEnumValue(const char *typeName, NSDictionary *mapping, long long result = 0; for (id arrayElement in json) { NSNumber *value = RCTConvertEnumValue(typeName, mapping, defaultValue, arrayElement); - result |= [value longLongValue]; + result |= value.longLongValue; } return @(result); } @@ -332,7 +326,7 @@ static void RCTConvertCGStructValue(const char *type, NSArray *fields, NSDiction for (NSUInteger i = 0; i < count; i++) { result[i] = [RCTConvert CGFloat:json[fields[i]]]; } - } else if (RCT_DEBUG && json && json != (id)kCFNull) { + } else if (json) { RCTLogConvertError(json, @(type)); } } @@ -389,7 +383,7 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ static RCTCache *colorCache = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - colorCache = [[RCTCache alloc] init]; + colorCache = [RCTCache new]; colorCache.countLimit = 128; }); UIColor *color = colorCache[json]; @@ -565,37 +559,66 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ } // Parse color - double red = 0, green = 0, blue = 0; - double alpha = 1.0; + enum { + MODE_RGB = 0, + MODE_HSB = 1, + }; + struct { + union { + struct { + double r, g, b; + } rgb; + struct { + double h, s, b; + } hsb; + }; + double a; + unsigned int mode: 1; + } components = { + .a = 1.0, + .mode = MODE_RGB, + }; + if ([colorString hasPrefix:@"#"]) { uint32_t redInt = 0, greenInt = 0, blueInt = 0; if (colorString.length == 4) { // 3 digit hex - sscanf([colorString UTF8String], "#%01x%01x%01x", &redInt, &greenInt, &blueInt); + sscanf(colorString.UTF8String, "#%01x%01x%01x", &redInt, &greenInt, &blueInt); // expand to 6 digit hex - red = redInt | (redInt << 4); - green = greenInt | (greenInt << 4); - blue = blueInt | (blueInt << 4); + components.rgb.r = redInt / 15.0; + components.rgb.g = greenInt / 15.0; + components.rgb.b = blueInt / 15.0; } else if (colorString.length == 7) { // 6 digit hex sscanf(colorString.UTF8String, "#%02x%02x%02x", &redInt, &greenInt, &blueInt); - red = redInt; - green = greenInt; - blue = blueInt; + components.rgb.r = redInt / 255.0; + components.rgb.g = greenInt / 255.0; + components.rgb.b = blueInt / 255.0; } else { RCTLogError(@"Invalid hex color %@. Hex colors should be 3 or 6 digits long.", colorString); - alpha = -1; + components.a = -1; } - } else if ([colorString hasPrefix:@"rgba("]) { - sscanf(colorString.UTF8String, "rgba(%lf,%lf,%lf,%lf)", &red, &green, &blue, &alpha); - } else if ([colorString hasPrefix:@"rgb("]) { - sscanf(colorString.UTF8String, "rgb(%lf,%lf,%lf)", &red, &green, &blue); + } else if (4 == sscanf(colorString.UTF8String, "rgba(%lf,%lf,%lf,%lf)", &components.rgb.r, &components.rgb.g, &components.rgb.b, &components.a) || + 3 == sscanf(colorString.UTF8String, "rgb(%lf,%lf,%lf)", &components.rgb.r, &components.rgb.g, &components.rgb.b)) { + components.rgb.r /= 255.0; + components.rgb.g /= 255.0; + components.rgb.b /= 255.0; + } else if (4 == sscanf(colorString.UTF8String, "hsla(%lf,%lf%%,%lf%%,%lf)", &components.hsb.h, &components.hsb.s, &components.hsb.b, &components.a) || + 3 == sscanf(colorString.UTF8String, "hsl(%lf,%lf%%,%lf%%)", &components.hsb.h, &components.hsb.s, &components.hsb.b)) { + components.hsb.h /= 360.0; + components.hsb.s /= 100.0; + components.hsb.b /= 100.0; + components.mode = MODE_HSB; } else { RCTLogError(@"Unrecognized color format '%@', must be one of #hex|rgba|rgb or a valid CSS color name.", colorString); - alpha = -1; + components.a = -1; } - if (alpha < 0) { + if (components.a < 0) { RCTLogError(@"Invalid color string '%@'", colorString); } else { - color = [UIColor colorWithRed:red / 255.0 green:green / 255.0 blue:blue / 255.0 alpha:alpha]; + if (components.mode == MODE_RGB) { + color = [UIColor colorWithRed:components.rgb.r green:components.rgb.g blue:components.rgb.b alpha:components.a]; + } else { + color = [UIColor colorWithHue:components.hsb.h saturation:components.hsb.s brightness:components.hsb.b alpha:components.a]; + } } } else if ([json isKindOfClass:[NSArray class]]) { @@ -614,13 +637,21 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ } else if ([json isKindOfClass:[NSDictionary class]]) { // Color dictionary - color = [UIColor colorWithRed:[self CGFloat:json[@"r"]] - green:[self CGFloat:json[@"g"]] - blue:[self CGFloat:json[@"b"]] - alpha:[self CGFloat:json[@"a"] ?: @1]]; + if (json[@"r"]) { + color = [UIColor colorWithRed:[self CGFloat:json[@"r"]] + green:[self CGFloat:json[@"g"]] + blue:[self CGFloat:json[@"b"]] + alpha:[self CGFloat:json[@"a"] ?: @1]]; + } else if (json[@"h"]) { + color = [UIColor colorWithHue:[self CGFloat:json[@"h"]] + saturation:[self CGFloat:json[@"s"]] + brightness:[self CGFloat:json[@"b"]] + alpha:[self CGFloat:json[@"a"] ?: @1]]; + } else { + RCTLogError(@"Expected dictionary with keys {r,g,b} or {h,s,b}, got: %@", [json allKeys]); + } - } - else if (RCT_DEBUG && json && json != (id)kCFNull) { + } else if (json) { RCTLogConvertError(json, @"a color"); } @@ -646,7 +677,7 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ // TODO: we might as well cache the result of these checks (and possibly the // image itself) so as to reduce overhead on subsequent checks of the same input - if (!json || json == (id)kCFNull) { + if (!json) { return nil; } @@ -663,30 +694,40 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ } NSURL *URL = [self NSURL:path]; - NSString *scheme = [URL.scheme lowercaseString]; - if ([scheme isEqualToString:@"file"]) { - - if ([NSThread currentThread] == [NSThread mainThread]) { - // Image may reside inside a .car file, in which case we have no choice - // but to use +[UIImage imageNamed] - but this method isn't thread safe - image = [UIImage imageNamed:path]; + NSString *scheme = URL.scheme.lowercaseString; + if (path && [scheme isEqualToString:@"file"]) { + if (RCT_DEBUG || [NSThread currentThread] == [NSThread mainThread]) { + if ([URL.path hasPrefix:[NSBundle mainBundle].resourcePath]) { + // Image may reside inside a .car file, in which case we have no choice + // but to use +[UIImage imageNamed] - but this method isn't thread safe + static NSMutableDictionary *XCAssetMap = nil; + if (!XCAssetMap) { + XCAssetMap = [NSMutableDictionary new]; + } + NSNumber *isAsset = XCAssetMap[path]; + if (!isAsset || isAsset.boolValue) { + image = [UIImage imageNamed:path]; + if (RCT_DEBUG && image) { + // If we succeeded in loading the image via imageNamed, and the + // method wasn't called on the main thread, that's a coding error + RCTAssertMainThread(); + } + } + if (!isAsset) { + // Avoid calling `+imageNamed` again in future if it's not needed. + XCAssetMap[path] = @(image != nil); + } + } } if (!image) { // Attempt to load from the file system - if ([path pathExtension].length == 0) { + if (path.pathExtension.length == 0) { path = [path stringByAppendingPathExtension:@"png"]; } image = [UIImage imageWithContentsOfFile:path]; } - // We won't warn about nil images because there are legitimate cases - // where we find out if a string is an image by using this method, but - // we do enforce thread-safe API usage with the following check - if (RCT_DEBUG && !image && [UIImage imageNamed:path]) { - RCTAssertMainThread(); - } - } else if ([scheme isEqualToString:@"data"]) { image = [UIImage imageWithData:[NSData dataWithContentsOfURL:URL]]; } else { @@ -923,17 +964,29 @@ NSArray *RCTConvertArrayValue(SEL type, id json) return values; } -RCT_ARRAY_CONVERTER(NSString) -RCT_ARRAY_CONVERTER(NSDictionary) RCT_ARRAY_CONVERTER(NSURL) RCT_ARRAY_CONVERTER(RCTFileURL) -RCT_ARRAY_CONVERTER(NSNumber) RCT_ARRAY_CONVERTER(UIColor) +/** + * This macro is used for creating converter functions for directly + * representable json array values that require no conversion. + */ +#if RCT_DEBUG +#define RCT_JSON_ARRAY_CONVERTER(type) RCT_ARRAY_CONVERTER(type) +#else +#define RCT_JSON_ARRAY_CONVERTER(type) + (NSArray *)type##Array:(id)json { return json; } +#endif + +RCT_JSON_ARRAY_CONVERTER(NSArray) +RCT_JSON_ARRAY_CONVERTER(NSString) +RCT_JSON_ARRAY_CONVERTER(NSDictionary) +RCT_JSON_ARRAY_CONVERTER(NSNumber) + // Can't use RCT_ARRAY_CONVERTER due to bridged cast + (NSArray *)CGColorArray:(id)json { - NSMutableArray *colors = [[NSMutableArray alloc] init]; + NSMutableArray *colors = [NSMutableArray new]; for (id value in [self NSArray:json]) { [colors addObject:(__bridge id)[self CGColor:value]]; } diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 6f390d55c..114d91586 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -69,7 +69,7 @@ RCT_EXTERN NSString *RCTNormalizeInputEventName(NSString *eventName); * This class wraps the -[RCTBridge enqueueJSCall:args:] method, and * provides some convenience methods for generating event calls. */ -@interface RCTEventDispatcher : NSObject +@interface RCTEventDispatcher : NSObject /** * Send an application-specific event that does not relate to a specific diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index 135bf93a5..484cf2f0a 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -29,7 +29,7 @@ NSString *RCTNormalizeInputEventName(NSString *eventName) static NSNumber *RCTGetEventID(id event) { return @( - [event.viewTag intValue] | + event.viewTag.intValue | (((uint64_t)event.eventName.hash & 0xFFFF) << 32) | (((uint64_t)event.coalescingKey) << 48) ); @@ -57,7 +57,7 @@ static NSNumber *RCTGetEventID(id event) return self; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) - (uint16_t)coalescingKey { @@ -81,7 +81,7 @@ RCT_NOT_IMPLEMENTED(-init) @end -@interface RCTEventDispatcher() +@interface RCTEventDispatcher() @end @@ -99,8 +99,8 @@ RCT_EXPORT_MODULE() - (instancetype)init { if ((self = [super init])) { - _eventQueue = [[NSMutableDictionary alloc] init]; - _eventQueueLock = [[NSLock alloc] init]; + _eventQueue = [NSMutableDictionary new]; + _eventQueueLock = [NSLock new]; } return self; } @@ -176,7 +176,7 @@ RCT_EXPORT_MODULE() - (void)dispatchEvent:(id)event { - NSMutableArray *arguments = [[NSMutableArray alloc] init]; + NSMutableArray *arguments = [NSMutableArray new]; if (event.viewTag) { [arguments addObject:event.viewTag]; @@ -201,7 +201,7 @@ RCT_EXPORT_MODULE() { [_eventQueueLock lock]; NSDictionary *eventQueue = _eventQueue; - _eventQueue = [[NSMutableDictionary alloc] init]; + _eventQueue = [NSMutableDictionary new]; _paused = YES; [_eventQueueLock unlock]; diff --git a/React/Base/RCTFPSGraph.m b/React/Base/RCTFPSGraph.m index 1aa5efd7d..1cd33432f 100644 --- a/React/Base/RCTFPSGraph.m +++ b/React/Base/RCTFPSGraph.m @@ -56,8 +56,8 @@ return self; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)dealloc { @@ -72,10 +72,10 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) - (CAShapeLayer *)createGraph:(UIColor *)color { CGFloat left = _position & RCTFPSGraphPositionLeft ? 0 : _length; - CAShapeLayer *graph = [[CAShapeLayer alloc] init]; + CAShapeLayer *graph = [CAShapeLayer new]; graph.frame = CGRectMake(left, 0, 2 * _margin + _length, self.frame.size.height); - graph.backgroundColor = [[color colorWithAlphaComponent:.2] CGColor]; - graph.fillColor = [color CGColor]; + graph.backgroundColor = [color colorWithAlphaComponent:0.2].CGColor; + graph.fillColor = color.CGColor; return graph; } diff --git a/React/Base/RCTFrameUpdate.m b/React/Base/RCTFrameUpdate.m index 2d9f63825..dbbcc4a32 100644 --- a/React/Base/RCTFrameUpdate.m +++ b/React/Base/RCTFrameUpdate.m @@ -15,7 +15,7 @@ @implementation RCTFrameUpdate -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) - (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink { diff --git a/React/Base/RCTInvalidating.h b/React/Base/RCTInvalidating.h index e961a3979..1c8cf7920 100644 --- a/React/Base/RCTInvalidating.h +++ b/React/Base/RCTInvalidating.h @@ -9,13 +9,8 @@ #import -// TODO (#5906496): This protocol is only used to add method definitions to -// classes. We should decide if it's actually necessary. - @protocol RCTInvalidating -@property (nonatomic, assign, readonly, getter = isValid) BOOL valid; - - (void)invalidate; @end diff --git a/React/Base/RCTJavaScriptExecutor.h b/React/Base/RCTJavaScriptExecutor.h index af4b4cef6..87d4dda78 100644 --- a/React/Base/RCTJavaScriptExecutor.h +++ b/React/Base/RCTJavaScriptExecutor.h @@ -29,6 +29,11 @@ typedef void (^RCTJavaScriptCallback)(id json, NSError *error); */ - (void)setUp; +/** + * Whether the executor has been invalidated + */ +@property (nonatomic, readonly, getter=isValid) BOOL valid; + /** * Executes given method with arguments on JS thread and calls the given callback * with JSValue and JSContext as a result of the JS module call. diff --git a/React/Base/RCTJavaScriptLoader.m b/React/Base/RCTJavaScriptLoader.m index 3a69aaf96..0aa73c68d 100755 --- a/React/Base/RCTJavaScriptLoader.m +++ b/React/Base/RCTJavaScriptLoader.m @@ -16,7 +16,7 @@ @implementation RCTJavaScriptLoader -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) + (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComplete { @@ -24,7 +24,7 @@ RCT_NOT_IMPLEMENTED(-init) scriptURL = [RCTConvert NSURL:scriptURL.absoluteString]; if (!scriptURL || - ([scriptURL isFileURL] && ![[NSFileManager defaultManager] fileExistsAtPath:scriptURL.path])) { + (scriptURL.fileURL && ![[NSFileManager defaultManager] fileExistsAtPath:scriptURL.path])) { NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{ NSLocalizedDescriptionKey: scriptURL ? [NSString stringWithFormat:@"Script at '%@' could not be found.", scriptURL] : @"No script URL provided" }]; @@ -37,11 +37,11 @@ RCT_NOT_IMPLEMENTED(-init) // Handle general request errors if (error) { - if ([[error domain] isEqualToString:NSURLErrorDomain]) { - NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:[scriptURL absoluteString]]; + if ([error.domain isEqualToString:NSURLErrorDomain]) { + NSString *desc = [@"Could not connect to development server. Ensure node server is running and available on the same network - run 'npm start' from react-native root\n\nURL: " stringByAppendingString:scriptURL.absoluteString]; NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: desc, - NSLocalizedFailureReasonErrorKey: [error localizedDescription], + NSLocalizedFailureReasonErrorKey: error.localizedDescription, NSUnderlyingErrorKey: error, }; error = [NSError errorWithDomain:@"JSServer" @@ -63,12 +63,12 @@ RCT_NOT_IMPLEMENTED(-init) NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding]; // Handle HTTP errors - if ([response isKindOfClass:[NSHTTPURLResponse class]] && [(NSHTTPURLResponse *)response statusCode] != 200) { + if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) { NSDictionary *userInfo; NSDictionary *errorDetails = RCTJSONParse(rawText, nil); if ([errorDetails isKindOfClass:[NSDictionary class]] && [errorDetails[@"errors"] isKindOfClass:[NSArray class]]) { - NSMutableArray *fakeStack = [[NSMutableArray alloc] init]; + NSMutableArray *fakeStack = [NSMutableArray new]; for (NSDictionary *err in errorDetails[@"errors"]) { [fakeStack addObject: @{ @"methodName": err[@"description"] ?: @"", @@ -84,7 +84,7 @@ RCT_NOT_IMPLEMENTED(-init) userInfo = @{NSLocalizedDescriptionKey: rawText}; } error = [NSError errorWithDomain:@"JSServer" - code:[(NSHTTPURLResponse *)response statusCode] + code:((NSHTTPURLResponse *)response).statusCode userInfo:userInfo]; onComplete(error, nil); diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index a9d358482..36516bbc1 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -40,7 +40,7 @@ static BOOL RCTIsIOS8OrEarlier() return self; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) - (id)copyWithZone:(__unused NSZone *)zone { @@ -152,7 +152,7 @@ RCT_NOT_IMPLEMENTED(-init) static RCTKeyCommands *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - sharedInstance = [[self alloc] init]; + sharedInstance = [self new]; }); return sharedInstance; @@ -161,7 +161,7 @@ RCT_NOT_IMPLEMENTED(-init) - (instancetype)init { if ((self = [super init])) { - _commands = [[NSMutableSet alloc] init]; + _commands = [NSMutableSet new]; } return self; } diff --git a/packager/react-packager/example_project/index.js b/React/Base/RCTKeyboardObserver.h similarity index 70% rename from packager/react-packager/example_project/index.js rename to React/Base/RCTKeyboardObserver.h index d63b51932..8fad021f3 100644 --- a/packager/react-packager/example_project/index.js +++ b/React/Base/RCTKeyboardObserver.h @@ -5,12 +5,12 @@ * 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 index */ -require('main'); -require('code'); +#import -var foo = require('foo'); -foo('secret'); +#import "RCTBridgeModule.h" + +@interface RCTKeyboardObserver : NSObject + +@end diff --git a/React/Base/RCTKeyboardObserver.m b/React/Base/RCTKeyboardObserver.m new file mode 100644 index 000000000..5b0174cdf --- /dev/null +++ b/React/Base/RCTKeyboardObserver.m @@ -0,0 +1,87 @@ +/** + * 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. + */ + +#import "RCTKeyboardObserver.h" + +#import "RCTEventDispatcher.h" + +static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification); + +@implementation RCTKeyboardObserver + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +- (instancetype)init +{ + if ((self = [super init])) { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + +#define ADD_KEYBOARD_HANDLER(NAME, SELECTOR) \ + [nc addObserver:self selector:@selector(SELECTOR:) name:NAME object:nil] + + ADD_KEYBOARD_HANDLER(UIKeyboardWillShowNotification, keyboardWillShow); + ADD_KEYBOARD_HANDLER(UIKeyboardDidShowNotification, keyboardDidShow); + ADD_KEYBOARD_HANDLER(UIKeyboardWillHideNotification, keyboardWillHide); + ADD_KEYBOARD_HANDLER(UIKeyboardDidHideNotification, keyboardDidHide); + ADD_KEYBOARD_HANDLER(UIKeyboardWillChangeFrameNotification, keyboardWillChangeFrame); + ADD_KEYBOARD_HANDLER(UIKeyboardDidChangeFrameNotification, keyboardDidChangeFrame); + +#undef ADD_KEYBOARD_HANDLER + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#define IMPLEMENT_KEYBOARD_HANDLER(EVENT) \ +- (void)EVENT:(NSNotification *)notification \ +{ \ + [_bridge.eventDispatcher \ + sendDeviceEventWithName:@#EVENT \ + body:RCTParseKeyboardNotification(notification)]; \ +} + +IMPLEMENT_KEYBOARD_HANDLER(keyboardWillShow) +IMPLEMENT_KEYBOARD_HANDLER(keyboardDidShow) +IMPLEMENT_KEYBOARD_HANDLER(keyboardWillHide) +IMPLEMENT_KEYBOARD_HANDLER(keyboardDidHide) +IMPLEMENT_KEYBOARD_HANDLER(keyboardWillChangeFrame) +IMPLEMENT_KEYBOARD_HANDLER(keyboardDidChangeFrame) + +@end + +static NSDictionary *RCTRectDictionaryValue(CGRect rect) +{ + return @{ + @"screenX": @(rect.origin.x), + @"screenY": @(rect.origin.y), + @"width": @(rect.size.width), + @"height": @(rect.size.height), + }; +} + +static NSDictionary *RCTParseKeyboardNotification(NSNotification *notification) +{ + NSDictionary *userInfo = notification.userInfo; + CGRect beginFrame = [userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; + CGRect endFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; + NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + return @{ + @"startCoordinates": RCTRectDictionaryValue(beginFrame), + @"endCoordinates": RCTRectDictionaryValue(endFrame), + @"duration": @(duration * 1000.0), // ms + }; +} diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index 0cb095494..a8679da9e 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -16,9 +16,10 @@ #import "RCTDefines.h" #import "RCTRedBox.h" -@interface RCTBridge (Logging) +@interface RCTBridge () -+ (void)logMessage:(NSString *)message level:(NSString *)level; ++ (RCTBridge *)currentBridge; +- (void)logMessage:(NSString *)message level:(NSString *)level; @end @@ -115,7 +116,7 @@ static RCTLogFunction RCTGetLocalLogFunction() { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; NSArray *functionStack = threadDictionary[RCTLogFunctionStack]; - RCTLogFunction logFunction = [functionStack lastObject]; + RCTLogFunction logFunction = functionStack.lastObject; if (logFunction) { return logFunction; } @@ -127,7 +128,7 @@ void RCTPerformBlockWithLogFunction(void (^block)(void), RCTLogFunction logFunct NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; NSMutableArray *functionStack = threadDictionary[RCTLogFunctionStack]; if (!functionStack) { - functionStack = [[NSMutableArray alloc] init]; + functionStack = [NSMutableArray new]; threadDictionary[RCTLogFunctionStack] = functionStack; } [functionStack addObject:logFunction]; @@ -153,12 +154,12 @@ NSString *RCTFormatLog( NSString *message ) { - NSMutableString *log = [[NSMutableString alloc] init]; + NSMutableString *log = [NSMutableString new]; if (timestamp) { static NSDateFormatter *formatter; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - formatter = [[NSDateFormatter alloc] init]; + formatter = [NSDateFormatter new]; formatter.dateFormat = formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS "; }); [log appendString:[formatter stringFromDate:timestamp]]; @@ -170,7 +171,7 @@ NSString *RCTFormatLog( [log appendFormat:@"[tid:%@]", RCTCurrentThreadName()]; if (fileName) { - fileName = [fileName lastPathComponent]; + fileName = fileName.lastPathComponent; if (lineNumber) { [log appendFormat:@"[%@:%@]", fileName, lineNumber]; } else { @@ -188,7 +189,8 @@ void _RCTLogFormat( RCTLogLevel level, const char *fileName, int lineNumber, - NSString *format, ...) + NSString *format, ... +) { RCTLogFunction logFunction = RCTGetLocalLogFunction(); BOOL log = RCT_DEBUG || (logFunction != nil); @@ -217,18 +219,18 @@ void _RCTLogFormat( NSRange addressRange = [frameSymbols rangeOfString:address]; NSString *methodName = [frameSymbols substringFromIndex:(addressRange.location + addressRange.length + 1)]; if (idx == 1) { - NSString *file = [[@(fileName) componentsSeparatedByString:@"/"] lastObject]; + NSString *file = [@(fileName) componentsSeparatedByString:@"/"].lastObject; [stack addObject:@{@"methodName": methodName, @"file": file, @"lineNumber": @(lineNumber)}]; } else { [stack addObject:@{@"methodName": methodName}]; } } }]; - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; + [[RCTBridge currentBridge].redBox showErrorMessage:message withStack:stack]; } // Log to JS executor - [RCTBridge logMessage:message level:level ? @(RCTLogLevels[level - 1]) : @"info"]; + [[RCTBridge currentBridge] logMessage:message level:level ? @(RCTLogLevels[level - 1]) : @"info"]; #endif diff --git a/React/Base/RCTModuleData.m b/React/Base/RCTModuleData.m index a5f929aea..dfc21ea3f 100644 --- a/React/Base/RCTModuleData.m +++ b/React/Base/RCTModuleData.m @@ -38,18 +38,17 @@ } // Must be done at init time due to race conditions - // Also, the queue setup isn't thread safe due ti static name cache - [self queue]; + (void)self.queue; } return self; } -RCT_NOT_IMPLEMENTED(-init); +RCT_NOT_IMPLEMENTED(- (instancetype)init); - (NSArray *)methods { if (!_methods) { - NSMutableArray *moduleMethods = [[NSMutableArray alloc] init]; + NSMutableArray *moduleMethods = [NSMutableArray new]; unsigned int methodCount; Method *methods = class_copyMethodList(object_getClass(_moduleClass), &methodCount); @@ -77,18 +76,18 @@ RCT_NOT_IMPLEMENTED(-init); - (NSDictionary *)config { - NSMutableDictionary *config = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *config = [NSMutableDictionary new]; config[@"moduleID"] = _moduleID; if (_constants) { config[@"constants"] = _constants; } - NSMutableDictionary *methodconfig = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *methodconfig = [NSMutableDictionary new]; [self.methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger idx, __unused BOOL *stop) { methodconfig[method.JSMethodName] = @{ @"methodID": @(idx), - @"type": method.functionKind == RCTJavaScriptFunctionKindAsync ? @"remoteAsync" : @"remote", + @"type": method.functionType == RCTFunctionTypePromise ? @"remoteAsync" : @"remote", }; }]; config[@"methods"] = [methodconfig copy]; @@ -101,7 +100,7 @@ RCT_NOT_IMPLEMENTED(-init); if (!_queue) { BOOL implementsMethodQueue = [_instance respondsToSelector:@selector(methodQueue)]; if (implementsMethodQueue) { - _queue = [_instance methodQueue]; + _queue = _instance.methodQueue; } if (!_queue) { diff --git a/React/Base/RCTModuleMap.m b/React/Base/RCTModuleMap.m index cd10c5b88..5491c1256 100644 --- a/React/Base/RCTModuleMap.m +++ b/React/Base/RCTModuleMap.m @@ -19,11 +19,11 @@ NSDictionary *_modulesByName; } -RCT_NOT_IMPLEMENTED(-init) -RCT_NOT_IMPLEMENTED(-initWithCoder:aDecoder) -RCT_NOT_IMPLEMENTED(-initWithObjects:(const id [])objects - forKeys:(const id [])keys - count:(NSUInteger)cnt) +RCT_NOT_IMPLEMENTED(- (instancetype)init) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithObjects:(const id [])objects + forKeys:(const id [])keys + count:(NSUInteger)cnt) - (instancetype)initWithDictionary:(NSDictionary *)modulesByName { diff --git a/React/Base/RCTModuleMethod.h b/React/Base/RCTModuleMethod.h index ddb459056..5768ac4bc 100644 --- a/React/Base/RCTModuleMethod.h +++ b/React/Base/RCTModuleMethod.h @@ -11,9 +11,9 @@ @class RCTBridge; -typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { - RCTJavaScriptFunctionKindNormal, - RCTJavaScriptFunctionKindAsync, +typedef NS_ENUM(NSUInteger, RCTFunctionType) { + RCTFunctionTypeNormal, + RCTFunctionTypePromise, }; typedef NS_ENUM(NSUInteger, RCTNullability) { @@ -35,7 +35,7 @@ typedef NS_ENUM(NSUInteger, RCTNullability) { @property (nonatomic, copy, readonly) NSString *JSMethodName; @property (nonatomic, readonly) Class moduleClass; @property (nonatomic, readonly) SEL selector; -@property (nonatomic, readonly) RCTJavaScriptFunctionKind functionKind; +@property (nonatomic, readonly) RCTFunctionType functionType; - (instancetype)initWithObjCMethodName:(NSString *)objCMethodName JSMethodName:(NSString *)JSMethodName diff --git a/React/Base/RCTModuleMethod.m b/React/Base/RCTModuleMethod.m index c876f4c0a..2cb0ce476 100644 --- a/React/Base/RCTModuleMethod.m +++ b/React/Base/RCTModuleMethod.m @@ -17,7 +17,7 @@ #import "RCTLog.h" #import "RCTUtils.h" -typedef void (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id); +typedef BOOL (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id); @implementation RCTMethodArgument @@ -46,9 +46,10 @@ typedef void (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id); @implementation RCTModuleMethod { Class _moduleClass; - SEL _selector; NSInvocation *_invocation; NSArray *_argumentBlocks; + NSString *_objCMethodName; + SEL _selector; } static void RCTLogArgumentError(RCTModuleMethod *method, NSUInteger index, @@ -59,7 +60,7 @@ static void RCTLogArgumentError(RCTModuleMethod *method, NSUInteger index, method->_JSMethodName, issue); } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) void RCTParseObjCMethodName(NSString **, NSArray **); void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) @@ -117,13 +118,8 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) { if ((self = [super init])) { - NSArray *arguments; - RCTParseObjCMethodName(&objCMethodName, &arguments); - _moduleClass = moduleClass; - _selector = NSSelectorFromString(objCMethodName); - RCTAssert(_selector, @"%@ is not a valid selector", objCMethodName); - + _objCMethodName = [objCMethodName copy]; _JSMethodName = JSMethodName.length > 0 ? JSMethodName : ({ NSString *methodName = objCMethodName; NSRange colonRange = [methodName rangeOfString:@":"]; @@ -135,230 +131,253 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) methodName; }); - // Create method invocation - NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector]; - RCTAssert(methodSignature, @"%@ is not a recognized Objective-C method.", objCMethodName); - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; - [invocation setSelector:_selector]; - [invocation retainArguments]; - _invocation = invocation; + if ([_objCMethodName rangeOfString:@"RCTPromise"].length) { + _functionType = RCTFunctionTypePromise; + } else { + _functionType = RCTFunctionTypeNormal; + } + } - // Process arguments - NSUInteger numberOfArguments = methodSignature.numberOfArguments; - NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; + return self; +} + +- (void)processMethodSignature +{ + NSArray *arguments; + NSString *objCMethodName = _objCMethodName; + RCTParseObjCMethodName(&objCMethodName, &arguments); + + _selector = NSSelectorFromString(objCMethodName); + RCTAssert(_selector, @"%@ is not a valid selector", objCMethodName); + + // Create method invocation + NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector]; + RCTAssert(methodSignature, @"%@ is not a recognized Objective-C method.", objCMethodName); + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; + invocation.selector = _selector; + [invocation retainArguments]; + _invocation = invocation; + + // Process arguments + NSUInteger numberOfArguments = methodSignature.numberOfArguments; + NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; #define RCT_ARG_BLOCK(_logic) \ - [argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) { \ - _logic \ - [invocation setArgument:&value atIndex:(index) + 2]; \ - }]; +[argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) { \ + _logic \ + [invocation setArgument:&value atIndex:(index) + 2]; \ + return YES; \ +}]; - __weak RCTModuleMethod *weakSelf = self; - void (^addBlockArgument)(void) = ^{ + __weak RCTModuleMethod *weakSelf = self; + void (^addBlockArgument)(void) = ^{ + RCT_ARG_BLOCK( + + if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { + RCTLogArgumentError(weakSelf, index, json, "should be a function"); + return NO; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing id value = (json ? ^(NSArray *args) { + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, args]]; + } : ^(__unused NSArray *unused) {}); + ) + }; + + for (NSUInteger i = 2; i < numberOfArguments; i++) { + const char *objcType = [methodSignature getArgumentTypeAtIndex:i]; + BOOL isNullableType = NO; + RCTMethodArgument *argument = arguments[i - 2]; + NSString *typeName = argument.type; + SEL selector = NSSelectorFromString([typeName stringByAppendingString:@":"]); + if ([RCTConvert respondsToSelector:selector]) { + switch (objcType[0]) { + +#define RCT_CASE(_value, _type) \ + case _value: { \ + _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ + RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ + break; \ + } + + RCT_CASE(_C_CHR, char) + RCT_CASE(_C_UCHR, unsigned char) + RCT_CASE(_C_SHT, short) + RCT_CASE(_C_USHT, unsigned short) + RCT_CASE(_C_INT, int) + RCT_CASE(_C_UINT, unsigned int) + RCT_CASE(_C_LNG, long) + RCT_CASE(_C_ULNG, unsigned long) + RCT_CASE(_C_LNG_LNG, long long) + RCT_CASE(_C_ULNG_LNG, unsigned long long) + RCT_CASE(_C_FLT, float) + RCT_CASE(_C_DBL, double) + RCT_CASE(_C_BOOL, BOOL) + +#define RCT_NULLABLE_CASE(_value, _type) \ + case _value: { \ + isNullableType = YES; \ + _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ + RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ + break; \ + } + + RCT_NULLABLE_CASE(_C_SEL, SEL) + RCT_NULLABLE_CASE(_C_CHARPTR, const char *) + RCT_NULLABLE_CASE(_C_PTR, void *) + RCT_NULLABLE_CASE(_C_ID, id) + + case _C_STRUCT_B: { + + NSMethodSignature *typeSignature = [RCTConvert methodSignatureForSelector:selector]; + NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature]; + typeInvocation.selector = selector; + typeInvocation.target = [RCTConvert class]; + + [argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) { + void *returnValue = malloc(typeSignature.methodReturnLength); + [typeInvocation setArgument:&json atIndex:2]; + [typeInvocation invoke]; + [typeInvocation getReturnValue:returnValue]; + [invocation setArgument:returnValue atIndex:index + 2]; + free(returnValue); + return YES; + }]; + break; + } + + default: { + static const char *blockType = @encode(typeof(^{})); + if (!strcmp(objcType, blockType)) { + addBlockArgument(); + } else { + RCTLogError(@"Unsupported argument type '%@' in method %@.", + typeName, [self methodName]); + } + } + } + } else if ([typeName isEqualToString:@"RCTResponseSenderBlock"]) { + addBlockArgument(); + } else if ([typeName isEqualToString:@"RCTResponseErrorBlock"]) { RCT_ARG_BLOCK( if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { RCTLogArgumentError(weakSelf, index, json, "should be a function"); - return; + return NO; } // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing id value = (json ? ^(NSArray *args) { + __autoreleasing id value = (json ? ^(NSError *error) { + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, @[RCTJSErrorFromNSError(error)]]]; + } : ^(__unused NSError *error) {}); + ) + } else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) { + RCTAssert(i == numberOfArguments - 2, + @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]", + _moduleClass, objCMethodName); + RCT_ARG_BLOCK( + if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { + RCTLogArgumentError(weakSelf, index, json, "should be a promise resolver function"); + return NO; + } + + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing RCTPromiseResolveBlock value = (^(id result) { [bridge _invokeAndProcessModule:@"BatchedBridge" method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, args]]; - } : ^(__unused NSArray *unused) {}); + arguments:@[json, result ? @[result] : @[]]]; + }); ) - }; - - for (NSUInteger i = 2; i < numberOfArguments; i++) { - const char *objcType = [methodSignature getArgumentTypeAtIndex:i]; - BOOL isNullableType = NO; - RCTMethodArgument *argument = arguments[i - 2]; - NSString *typeName = argument.type; - SEL selector = NSSelectorFromString([typeName stringByAppendingString:@":"]); - if ([RCTConvert respondsToSelector:selector]) { - switch (objcType[0]) { - -#define RCT_CASE(_value, _type) \ - case _value: { \ - _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ - RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ - break; \ - } - - RCT_CASE(_C_CHR, char) - RCT_CASE(_C_UCHR, unsigned char) - RCT_CASE(_C_SHT, short) - RCT_CASE(_C_USHT, unsigned short) - RCT_CASE(_C_INT, int) - RCT_CASE(_C_UINT, unsigned int) - RCT_CASE(_C_LNG, long) - RCT_CASE(_C_ULNG, unsigned long) - RCT_CASE(_C_LNG_LNG, long long) - RCT_CASE(_C_ULNG_LNG, unsigned long long) - RCT_CASE(_C_FLT, float) - RCT_CASE(_C_DBL, double) - RCT_CASE(_C_BOOL, BOOL) - -#define RCT_NULLABLE_CASE(_value, _type) \ - case _value: { \ - isNullableType = YES; \ - _type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \ - RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \ - break; \ - } - - RCT_NULLABLE_CASE(_C_SEL, SEL) - RCT_NULLABLE_CASE(_C_CHARPTR, const char *) - RCT_NULLABLE_CASE(_C_PTR, void *) - RCT_NULLABLE_CASE(_C_ID, id) - - case _C_STRUCT_B: { - - NSMethodSignature *typeSignature = [RCTConvert methodSignatureForSelector:selector]; - NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature]; - [typeInvocation setSelector:selector]; - [typeInvocation setTarget:[RCTConvert class]]; - - [argumentBlocks addObject: - ^(__unused RCTBridge *bridge, NSUInteger index, id json) { - - void *returnValue = malloc(typeSignature.methodReturnLength); - [typeInvocation setArgument:&json atIndex:2]; - [typeInvocation invoke]; - [typeInvocation getReturnValue:returnValue]; - - [invocation setArgument:returnValue atIndex:index + 2]; - - free(returnValue); - }]; - break; - } - - default: { - static const char *blockType = @encode(typeof(^{})); - if (!strcmp(objcType, blockType)) { - addBlockArgument(); - } else { - RCTLogError(@"Unsupported argument type '%@' in method %@.", - typeName, [self methodName]); - } - } - } - } else if ([typeName isEqualToString:@"RCTResponseSenderBlock"]) { - addBlockArgument(); - } else if ([typeName isEqualToString:@"RCTResponseErrorBlock"]) { - RCT_ARG_BLOCK( - - if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { - RCTLogArgumentError(weakSelf, index, json, "should be a function"); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing id value = (json ? ^(NSError *error) { - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, @[RCTJSErrorFromNSError(error)]]]; - } : ^(__unused NSError *error) {}); - ) - } else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) { - RCTAssert(i == numberOfArguments - 2, - @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]", - _moduleClass, objCMethodName); - RCT_ARG_BLOCK( - if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { - RCTLogArgumentError(weakSelf, index, json, "should be a promise resolver function"); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseResolveBlock value = (^(id result) { - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, result ? @[result] : @[]]]; - }); - ) - _functionKind = RCTJavaScriptFunctionKindAsync; - } else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) { - RCTAssert(i == numberOfArguments - 1, - @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]", - _moduleClass, objCMethodName); - RCT_ARG_BLOCK( - if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { - RCTLogArgumentError(weakSelf, index, json, "should be a promise rejecter function"); - return; - } - - // Marked as autoreleasing, because NSInvocation doesn't retain arguments - __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) { - NSDictionary *errorJSON = RCTJSErrorFromNSError(error); - [bridge _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[json, @[errorJSON]]]; - }); - ) - _functionKind = RCTJavaScriptFunctionKindAsync; - } else { - - // Unknown argument type - RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert" - " to support this type.", typeName, [self methodName]); - } - - if (RCT_DEBUG) { - - RCTNullability nullability = argument.nullability; - if (!isNullableType) { - if (nullability == RCTNullable) { - RCTLogArgumentError(weakSelf, i - 2, typeName, "is marked as " - "nullable, but is not a nullable type."); - } - nullability = RCTNonnullable; + } else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) { + RCTAssert(i == numberOfArguments - 1, + @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]", + _moduleClass, objCMethodName); + RCT_ARG_BLOCK( + if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) { + RCTLogArgumentError(weakSelf, index, json, "should be a promise rejecter function"); + return NO; } - /** - * Special case - Numbers are not nullable in Android, so we - * don't support this for now. In future we may allow it. - */ - if ([typeName isEqualToString:@"NSNumber"]) { - BOOL unspecified = (nullability == RCTNullabilityUnspecified); - if (!argument.unused && (nullability == RCTNullable || unspecified)) { - RCTLogArgumentError(weakSelf, i - 2, typeName, - [unspecified ? @"has unspecified nullability" : @"is marked as nullable" - stringByAppendingString: @" but React requires that all NSNumber " - "arguments are explicitly marked as `nonnull` to ensure " - "compatibility with Android."].UTF8String); - } - nullability = RCTNonnullable; - } + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) { + NSDictionary *errorJSON = RCTJSErrorFromNSError(error); + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, @[errorJSON]]]; + }); + ) + } else { - if (nullability == RCTNonnullable) { - RCTArgumentBlock oldBlock = argumentBlocks[i - 2]; - argumentBlocks[i - 2] = ^(RCTBridge *bridge, NSUInteger index, id json) { - if (json == nil || json == (id)kCFNull) { - RCTLogArgumentError(weakSelf, index, typeName, "must not be null"); - id null = nil; - [invocation setArgument:&null atIndex:index + 2]; - } else { - oldBlock(bridge, index, json); - } - }; - } - } + // Unknown argument type + RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert" + " to support this type.", typeName, [self methodName]); } - _argumentBlocks = [argumentBlocks copy]; + if (RCT_DEBUG) { + + RCTNullability nullability = argument.nullability; + if (!isNullableType) { + if (nullability == RCTNullable) { + RCTLogArgumentError(weakSelf, i - 2, typeName, "is marked as " + "nullable, but is not a nullable type."); + } + nullability = RCTNonnullable; + } + + /** + * Special case - Numbers are not nullable in Android, so we + * don't support this for now. In future we may allow it. + */ + if ([typeName isEqualToString:@"NSNumber"]) { + BOOL unspecified = (nullability == RCTNullabilityUnspecified); + if (!argument.unused && (nullability == RCTNullable || unspecified)) { + RCTLogArgumentError(weakSelf, i - 2, typeName, + [unspecified ? @"has unspecified nullability" : @"is marked as nullable" + stringByAppendingString: @" but React requires that all NSNumber " + "arguments are explicitly marked as `nonnull` to ensure " + "compatibility with Android."].UTF8String); + } + nullability = RCTNonnullable; + } + + if (nullability == RCTNonnullable) { + RCTArgumentBlock oldBlock = argumentBlocks[i - 2]; + argumentBlocks[i - 2] = ^(RCTBridge *bridge, NSUInteger index, id json) { + if (json == nil) { + RCTLogArgumentError(weakSelf, index, typeName, "must not be null"); + return NO; + } else { + return oldBlock(bridge, index, json); + } + }; + } + } } - return self; + _argumentBlocks = [argumentBlocks copy]; +} + +- (SEL)selector +{ + if (_selector == NULL) { + [self processMethodSignature]; + } + return _selector; } - (void)invokeWithBridge:(RCTBridge *)bridge module:(id)module arguments:(NSArray *)arguments { + if (_argumentBlocks == nil) { + [self processMethodSignature]; + } + if (RCT_DEBUG) { // Sanity check @@ -371,7 +390,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) NSInteger expectedCount = _argumentBlocks.count; // Subtract the implicit Promise resolver and rejecter functions for implementations of async functions - if (_functionKind == RCTJavaScriptFunctionKindAsync) { + if (_functionType == RCTFunctionTypePromise) { actualCount -= 2; expectedCount -= 2; } @@ -386,9 +405,13 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) // Set arguments NSUInteger index = 0; for (id json in arguments) { - id arg = RCTNilIfNull(json); RCTArgumentBlock block = _argumentBlocks[index]; - block(bridge, index, arg); + if (!block(bridge, index, RCTNilIfNull(json))) { + // Invalid argument, abort + RCTLogArgumentError(self, index, json, + "could not be processed. Aborting method call."); + return; + } index++; } @@ -398,6 +421,9 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments) - (NSString *)methodName { + if (_selector == NULL) { + [self processMethodSignature]; + } return [NSString stringWithFormat:@"-[%@ %@]", _moduleClass, NSStringFromSelector(_selector)]; } diff --git a/React/Base/RCTPerfStats.m b/React/Base/RCTPerfStats.m index 39f72928f..ba841dc67 100644 --- a/React/Base/RCTPerfStats.m +++ b/React/Base/RCTPerfStats.m @@ -32,7 +32,7 @@ RCT_EXPORT_MODULE() - (UIView *)container { if (!_container) { - _container = [[UIView alloc] init]; + _container = [UIView new]; _container.backgroundColor = [UIColor colorWithRed:0 green:0 blue:34/255.0 alpha:1]; _container.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; } @@ -66,7 +66,7 @@ RCT_EXPORT_MODULE() - (void)show { - UIView *targetView = [[[[[UIApplication sharedApplication] delegate] window] rootViewController] view]; + UIView *targetView = [UIApplication sharedApplication].delegate.window.rootViewController.view; targetView.frame = (CGRect){ targetView.frame.origin, @@ -112,6 +112,15 @@ RCT_EXPORT_MODULE() @end +@implementation RCTBridge (RCTPerfStats) + +- (RCTPerfStats *)perfStats +{ + return self.modules[RCTBridgeModuleNameForClass([RCTPerfStats class])]; +} + +@end + #else @implementation RCTPerfStats @@ -121,13 +130,13 @@ RCT_EXPORT_MODULE() @end -#endif - @implementation RCTBridge (RCTPerfStats) - (RCTPerfStats *)perfStats { - return self.modules[RCTBridgeModuleNameForClass([RCTPerfStats class])]; + return nil; } @end + +#endif diff --git a/React/Base/RCTPerformanceLogger.h b/React/Base/RCTPerformanceLogger.h index 4c220d170..66d3dd863 100644 --- a/React/Base/RCTPerformanceLogger.h +++ b/React/Base/RCTPerformanceLogger.h @@ -10,11 +10,11 @@ #import #import "RCTDefines.h" -#import "RCTBridgeModule.h" typedef NS_ENUM(NSUInteger, RCTPLTag) { RCTPLScriptDownload = 0, - RCTPLAppScriptExecution, + RCTPLScriptExecution, + RCTPLNativeModuleInit, RCTPLTTI, RCTPLSize }; diff --git a/React/Base/RCTPerformanceLogger.m b/React/Base/RCTPerformanceLogger.m index c19f4938a..87e643355 100644 --- a/React/Base/RCTPerformanceLogger.m +++ b/React/Base/RCTPerformanceLogger.m @@ -27,12 +27,14 @@ void RCTPerformanceLoggerEnd(RCTPLTag tag) NSArray *RCTPerformanceLoggerOutput(void) { return @[ - @(RCTPLData[0][0]), - @(RCTPLData[0][1]), - @(RCTPLData[1][0]), - @(RCTPLData[1][1]), - @(RCTPLData[2][0]), - @(RCTPLData[2][1]), + @(RCTPLData[RCTPLScriptDownload][0]), + @(RCTPLData[RCTPLScriptDownload][1]), + @(RCTPLData[RCTPLScriptExecution][0]), + @(RCTPLData[RCTPLScriptExecution][1]), + @(RCTPLData[RCTPLNativeModuleInit][0]), + @(RCTPLData[RCTPLNativeModuleInit][1]), + @(RCTPLData[RCTPLTTI][0]), + @(RCTPLData[RCTPLTTI][1]), ]; } @@ -71,6 +73,7 @@ RCT_EXPORT_MODULE() @[ @"ScriptDownload", @"ScriptExecution", + @"NativeModuleInit", @"TTI", ], ]]; diff --git a/React/Base/RCTProfile.h b/React/Base/RCTProfile.h index 66cf40bf4..794f4a910 100644 --- a/React/Base/RCTProfile.h +++ b/React/Base/RCTProfile.h @@ -59,36 +59,42 @@ RCT_EXTERN NSString *RCTProfileEnd(RCTBridge *); /** * Collects the initial event information for the event and returns a reference ID */ -RCT_EXTERN NSNumber *_RCTProfileBeginEvent(void); +RCT_EXTERN void RCTProfileBeginEvent(uint64_t tag, + NSString *name, + NSDictionary *args); /** * The ID returned by BeginEvent should then be passed into EndEvent, with the * rest of the event information. Just at this point the event will actually be * registered */ -RCT_EXTERN void _RCTProfileEndEvent(NSNumber *, NSString *, NSString *, id); +RCT_EXTERN void RCTProfileEndEvent(uint64_t tag, + NSString *category, + NSDictionary *args); /** - * This pair of macros implicitly handle the event ID when beginning and ending - * an event, for both simplicity and performance reasons, this method is preferred - * - * NOTE: The EndEvent call has to be either, in the same scope of BeginEvent, - * or in a sub-scope, otherwise the ID stored by BeginEvent won't be accessible - * for EndEvent, in this case you may want to use the actual C functions. + * Collects the initial event information for the event and returns a reference ID */ -#define RCTProfileBeginEvent() \ -_Pragma("clang diagnostic push") \ -_Pragma("clang diagnostic ignored \"-Wshadow\"") \ -NSNumber *__rct_profile_id = _RCTProfileBeginEvent(); \ -_Pragma("clang diagnostic pop") - -#define RCTProfileEndEvent(name, category, args...) \ -_RCTProfileEndEvent(__rct_profile_id, name, category, args) +RCT_EXTERN int RCTProfileBeginAsyncEvent(uint64_t tag, + NSString *name, + NSDictionary *args); +/** + * The ID returned by BeginEvent should then be passed into EndEvent, with the + * rest of the event information. Just at this point the event will actually be + * registered + */ +RCT_EXTERN void RCTProfileEndAsyncEvent(uint64_t tag, + NSString *category, + int cookie, + NSString *name, + NSDictionary *args); /** * An event that doesn't have a duration (i.e. Notification, VSync, etc) */ -RCT_EXTERN void RCTProfileImmediateEvent(NSString *, NSTimeInterval , NSString *); +RCT_EXTERN void RCTProfileImmediateEvent(uint64_t tag, + NSString *name, + char scope); /** * Helper to profile the duration of the execution of a block. This method uses @@ -96,11 +102,11 @@ RCT_EXTERN void RCTProfileImmediateEvent(NSString *, NSTimeInterval , NSString * * * NOTE: The block can't expect any argument */ -#define RCTProfileBlock(block, category, arguments) \ +#define RCTProfileBlock(block, tag, category, arguments) \ ^{ \ - RCTProfileBeginEvent(); \ + RCTProfileBeginEvent(tag, @(__PRETTY_FUNCTION__), nil); \ block(); \ - RCTProfileEndEvent([NSString stringWithFormat:@"[%@ %@]", NSStringFromClass([self class]), NSStringFromSelector(_cmd)], category, arguments); \ + RCTProfileEndEvent(tag, category, arguments); \ } /** @@ -125,12 +131,12 @@ RCT_EXTERN void RCTProfileUnhookModules(RCTBridge *); #define RCTProfileInit(...) #define RCTProfileEnd(...) @"" -#define _RCTProfileBeginEvent(...) @0 #define RCTProfileBeginEvent(...) - -#define _RCTProfileEndEvent(...) #define RCTProfileEndEvent(...) +#define RCTProfileBeginAsyncEvent(...) 0 +#define RCTProfileEndAsyncEvent(...) + #define RCTProfileImmediateEvent(...) #define RCTProfileBlock(block, ...) block diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m index a7021415f..86aab5652 100644 --- a/React/Base/RCTProfile.m +++ b/React/Base/RCTProfile.m @@ -26,12 +26,6 @@ NSString *const RCTProfileDidEndProfiling = @"RCTProfileDidEndProfiling"; #if RCT_DEV -#pragma mark - Prototypes - -NSNumber *RCTProfileTimestamp(NSTimeInterval); -NSString *RCTProfileMemory(vm_size_t); -NSDictionary *RCTProfileGetMemoryUsage(void); - #pragma mark - Constants NSString const *RCTProfileTraceEvents = @"traceEvents"; @@ -67,18 +61,18 @@ __VA_ARGS__ \ #pragma mark - Private Helpers -NSNumber *RCTProfileTimestamp(NSTimeInterval timestamp) +static NSNumber *RCTProfileTimestamp(NSTimeInterval timestamp) { return @((timestamp - RCTProfileStartTime) * 1e6); } -NSString *RCTProfileMemory(vm_size_t memory) +static NSString *RCTProfileMemory(vm_size_t memory) { double mem = ((double)memory) / 1024 / 1024; return [NSString stringWithFormat:@"%.2lfmb", mem]; } -NSDictionary *RCTProfileGetMemoryUsage(void) +static NSDictionary *RCTProfileGetMemoryUsage(void) { struct task_basic_info info; mach_msg_type_number_t size = sizeof(info); @@ -97,6 +91,22 @@ NSDictionary *RCTProfileGetMemoryUsage(void) } } +static NSDictionary *RCTProfileMergeArgs(NSDictionary *args0, NSDictionary *args1) +{ + args0 = RCTNilIfNull(args0); + args1 = RCTNilIfNull(args1); + + if (!args0 && args1) { + args0 = args1; + } else if (args0 && args1) { + NSMutableDictionary *d = [args0 mutableCopy]; + [d addEntriesFromDictionary:args1]; + args0 = [d copy]; + } + + return RCTNullIfNil(args0); +} + #pragma mark - Module hooks static const char *RCTProfileProxyClassName(Class); @@ -120,9 +130,9 @@ static void RCTProfileForwardInvocation(NSObject *self, __unused SEL cmd, NSInvo if ([object_getClass(self) instancesRespondToSelector:newSel]) { invocation.selector = newSel; - RCTProfileBeginEvent(); + RCTProfileBeginEvent(0, name, nil); [invocation invoke]; - RCTProfileEndEvent(name, @"objc_call,modules,auto", nil); + RCTProfileEndEvent(0, @"objc_call,modules,auto", nil); } else if ([self respondsToSelector:invocation.selector]) { [invocation invoke]; } else { @@ -216,14 +226,14 @@ void RCTProfileInit(RCTBridge *bridge) static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - _RCTProfileLock = [[NSRecursiveLock alloc] init]; + _RCTProfileLock = [NSRecursiveLock new]; }); RCTProfileLock( RCTProfileStartTime = CACurrentMediaTime(); - RCTProfileOngoingEvents = [[NSMutableDictionary alloc] init]; + RCTProfileOngoingEvents = [NSMutableDictionary new]; RCTProfileInfo = @{ - RCTProfileTraceEvents: [[NSMutableArray alloc] init], - RCTProfileSamples: [[NSMutableArray alloc] init], + RCTProfileTraceEvents: [NSMutableArray new], + RCTProfileSamples: [NSMutableArray new], }; ); @@ -248,45 +258,116 @@ NSString *RCTProfileEnd(RCTBridge *bridge) return log; } -NSNumber *_RCTProfileBeginEvent(void) +static NSMutableArray *RCTProfileGetThreadEvents(void) { - CHECK(@0); - RCTProfileLock( - NSNumber *eventID = @(++RCTProfileEventID); - RCTProfileOngoingEvents[eventID] = RCTProfileTimestamp(CACurrentMediaTime()); - ); - return eventID; + static NSString *const RCTProfileThreadEventsKey = @"RCTProfileThreadEventsKey"; + NSMutableArray *threadEvents = [NSThread currentThread].threadDictionary[RCTProfileThreadEventsKey]; + if (!threadEvents) { + threadEvents = [[NSMutableArray alloc] init]; + [NSThread currentThread].threadDictionary[RCTProfileThreadEventsKey] = threadEvents; + } + return threadEvents; } -void _RCTProfileEndEvent(NSNumber *eventID, NSString *name, NSString *categories, id args) +void RCTProfileBeginEvent(uint64_t tag, NSString *name, NSDictionary *args) { CHECK(); + NSMutableArray *events = RCTProfileGetThreadEvents(); + [events addObject:@[ + RCTProfileTimestamp(CACurrentMediaTime()), + @(tag), + name, + RCTNullIfNil(args), + ]]; +} + +void RCTProfileEndEvent( + __unused uint64_t tag, + NSString *category, + NSDictionary *args +) { + CHECK(); + + NSMutableArray *events = RCTProfileGetThreadEvents(); + NSArray *event = events.lastObject; + [events removeLastObject]; + + if (!event) { + return; + } + + NSNumber *start = event[0]; + RCTProfileLock( - NSNumber *startTimestamp = RCTProfileOngoingEvents[eventID]; - if (startTimestamp) { + RCTProfileAddEvent(RCTProfileTraceEvents, + @"name": event[2], + @"cat": category, + @"ph": @"X", + @"ts": start, + @"dur": @(RCTProfileTimestamp(CACurrentMediaTime()).doubleValue - start.doubleValue), + @"args": RCTProfileMergeArgs(event[3], args), + ); + ); +} + +int RCTProfileBeginAsyncEvent( + __unused uint64_t tag, + NSString *name, + NSDictionary *args +) { + CHECK(0); + + static int eventID = 0; + + RCTProfileLock( + RCTProfileOngoingEvents[@(eventID)] = @[ + RCTProfileTimestamp(CACurrentMediaTime()), + name, + RCTNullIfNil(args), + ]; + ); + + return eventID++; +} + +void RCTProfileEndAsyncEvent( + __unused uint64_t tag, + NSString *category, + int cookie, + __unused NSString *name, + NSDictionary *args +) { + CHECK(); + RCTProfileLock( + NSArray *event = RCTProfileOngoingEvents[@(cookie)]; + if (event) { NSNumber *endTimestamp = RCTProfileTimestamp(CACurrentMediaTime()); RCTProfileAddEvent(RCTProfileTraceEvents, - @"name": name, - @"cat": categories, + @"name": event[1], + @"cat": category, @"ph": @"X", - @"ts": startTimestamp, - @"dur": @(endTimestamp.doubleValue - startTimestamp.doubleValue), - @"args": args ?: @[], + @"ts": event[0], + @"dur": @(endTimestamp.doubleValue - [event[0] doubleValue]), + @"args": RCTProfileMergeArgs(event[2], args), ); - [RCTProfileOngoingEvents removeObjectForKey:eventID]; + [RCTProfileOngoingEvents removeObjectForKey:@(cookie)]; } ); } -void RCTProfileImmediateEvent(NSString *name, NSTimeInterval timestamp, NSString *scope) -{ +void RCTProfileImmediateEvent( + __unused uint64_t tag, + NSString *name, + char scope +) { CHECK(); + RCTProfileLock( RCTProfileAddEvent(RCTProfileTraceEvents, @"name": name, - @"ts": RCTProfileTimestamp(timestamp), - @"scope": scope, + @"ts": RCTProfileTimestamp(CACurrentMediaTime()), + @"scope": @(scope), @"ph": @"i", @"args": RCTProfileGetMemoryUsage(), ); diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index d18ac176c..80f86d0cb 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -29,7 +29,8 @@ extern NSString *const RCTContentDidAppearNotification; * - Designated initializer - */ - (instancetype)initWithBridge:(RCTBridge *)bridge - moduleName:(NSString *)moduleName NS_DESIGNATED_INITIALIZER; + moduleName:(NSString *)moduleName + initialProperties:(NSDictionary *)initialProperties NS_DESIGNATED_INITIALIZER; /** * - Convenience initializer - @@ -40,6 +41,7 @@ extern NSString *const RCTContentDidAppearNotification; */ - (instancetype)initWithBundleURL:(NSURL *)bundleURL moduleName:(NSString *)moduleName + initialProperties:(NSDictionary *)initialProperties launchOptions:(NSDictionary *)launchOptions; /** @@ -60,7 +62,7 @@ extern NSString *const RCTContentDidAppearNotification; * The default properties to apply to the view when the script bundle * is first loaded. Defaults to nil/empty. */ -@property (nonatomic, copy) NSDictionary *initialProperties; +@property (nonatomic, copy, readonly) NSDictionary *initialProperties; /** * The class of the RCTJavaScriptExecutor to use with this view. diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 0cdd1b78c..930aaa13c 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -44,7 +44,7 @@ NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotificat @property (nonatomic, readonly) BOOL contentHasAppeared; -- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge; +- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; @end @@ -58,6 +58,7 @@ NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotificat - (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName + initialProperties:(NSDictionary *)initialProperties { RCTAssertMainThread(); RCTAssert(bridge, @"A bridge instance is required to create an RCTRootView"); @@ -69,6 +70,7 @@ NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotificat _bridge = bridge; _moduleName = moduleName; + _initialProperties = [initialProperties copy]; _loadingViewFadeDelay = 0.25; _loadingViewFadeDuration = 0.25; @@ -81,7 +83,7 @@ NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotificat selector:@selector(hideLoadingView) name:RCTContentDidAppearNotification object:self]; - if (!_bridge.batchedBridge.isLoading) { + if (!_bridge.loading) { [self bundleFinishedLoading:_bridge.batchedBridge]; } @@ -92,17 +94,18 @@ NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotificat - (instancetype)initWithBundleURL:(NSURL *)bundleURL moduleName:(NSString *)moduleName + initialProperties:(NSDictionary *)initialProperties launchOptions:(NSDictionary *)launchOptions { RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL moduleProvider:nil launchOptions:launchOptions]; - return [self initWithBridge:bridge moduleName:moduleName]; + return [self initWithBridge:bridge moduleName:moduleName initialProperties:initialProperties]; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)setBackgroundColor:(UIColor *)backgroundColor { @@ -157,31 +160,30 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) - (void)javaScriptDidLoad:(NSNotification *)notification { + RCTAssertMainThread(); RCTBridge *bridge = notification.userInfo[@"bridge"]; [self bundleFinishedLoading:bridge]; } - (void)bundleFinishedLoading:(RCTBridge *)bridge { - dispatch_async(dispatch_get_main_queue(), ^{ - if (!bridge.isValid) { - return; - } + if (!bridge.valid) { + return; + } - [_contentView removeFromSuperview]; - _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds bridge:bridge]; - _contentView.backgroundColor = self.backgroundColor; - [self insertSubview:_contentView atIndex:0]; + [_contentView removeFromSuperview]; + _contentView = [[RCTRootContentView alloc] initWithFrame:self.bounds bridge:bridge]; + _contentView.backgroundColor = self.backgroundColor; + [self insertSubview:_contentView atIndex:0]; - NSString *moduleName = _moduleName ?: @""; - NSDictionary *appParameters = @{ - @"rootTag": _contentView.reactTag, - @"initialProps": _initialProperties ?: @{}, - }; + NSString *moduleName = _moduleName ?: @""; + NSDictionary *appParameters = @{ + @"rootTag": _contentView.reactTag, + @"initialProps": _initialProperties ?: @{}, + }; - [bridge enqueueJSCall:@"AppRegistry.runApplication" - args:@[moduleName, appParameters]]; - }); + [bridge enqueueJSCall:@"AppRegistry.runApplication" + args:@[moduleName, appParameters]]; } - (void)layoutSubviews @@ -235,7 +237,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) - (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge { - if ((self = [super init])) { + if ((self = [super initWithFrame:frame])) { _bridge = bridge; [self setUp]; self.frame = frame; @@ -244,6 +246,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) return self; } +RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder:(nonnull NSCoder *)aDecoder) + - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex { [super insertReactSubview:subview atIndex:atIndex]; @@ -292,14 +296,9 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) [_bridge.uiManager registerRootView:self]; } -- (BOOL)isValid -{ - return self.userInteractionEnabled; -} - - (void)invalidate { - if (self.isValid) { + if (self.userInteractionEnabled) { self.userInteractionEnabled = NO; [(RCTRootView *)self.superview contentViewInvalidated]; [_bridge enqueueJSCall:@"ReactNative.unmountComponentAtNodeAndRemoveContainer" diff --git a/React/Base/RCTSparseArray.m b/React/Base/RCTSparseArray.m index 2332f96c5..1c67df644 100644 --- a/React/Base/RCTSparseArray.m +++ b/React/Base/RCTSparseArray.m @@ -37,7 +37,7 @@ + (instancetype)sparseArray { - return [[self alloc] init]; + return [self new]; } + (instancetype)sparseArrayWithCapacity:(NSUInteger)capacity @@ -117,7 +117,7 @@ - (NSString *)description { - return [[super description] stringByAppendingString:[_storage description]]; + return [super.description stringByAppendingString:_storage.description]; } @end diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index cf84ecdf7..5c6333f21 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -48,9 +48,9 @@ _bridge = bridge; _dispatchedInitialTouches = NO; - _nativeTouches = [[NSMutableOrderedSet alloc] init]; - _reactTouches = [[NSMutableArray alloc] init]; - _touchViews = [[NSMutableArray alloc] init]; + _nativeTouches = [NSMutableOrderedSet new]; + _reactTouches = [NSMutableArray new]; + _touchViews = [NSMutableArray new]; // `cancelsTouchesInView` is needed in order to be used as a top level // event delegated recognizer. Otherwise, lower-level components not built @@ -60,7 +60,7 @@ return self; } -RCT_NOT_IMPLEMENTED(-initWithTarget:(id)target action:(SEL)action) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithTarget:(id)target action:(SEL)action) typedef NS_ENUM(NSInteger, RCTTouchEventType) { RCTTouchEventTypeStart, @@ -168,7 +168,7 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { originatingTime:(__unused CFTimeInterval)originatingTime { // Update touches - NSMutableArray *changedIndexes = [[NSMutableArray alloc] init]; + NSMutableArray *changedIndexes = [NSMutableArray new]; for (UITouch *touch in touches) { NSInteger index = [_nativeTouches indexOfObject:touch]; if (index == NSNotFound) { @@ -224,7 +224,7 @@ static BOOL RCTAnyTouchesChanged(NSSet *touches) { // If gesture just recognized, send all touches to JS as if they just began. if (self.state == UIGestureRecognizerStateBegan) { - [self _updateAndDispatchTouches:[_nativeTouches set] eventName:@"topTouchStart" originatingTime:0]; + [self _updateAndDispatchTouches:_nativeTouches.set eventName:@"topTouchStart" originatingTime:0]; // We store this flag separately from `state` because after a gesture is // recognized, its `state` changes immediately but its action (this diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 8f5875359..2fba55f00 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -171,7 +171,7 @@ id RCTJSONClean(id object) NSString *RCTMD5Hash(NSString *string) { - const char *str = [string UTF8String]; + const char *str = string.UTF8String; unsigned char result[CC_MD5_DIGEST_LENGTH]; CC_MD5(str, (CC_LONG)strlen(str), result); diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index b31c7e98c..8b051fa67 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -41,7 +41,7 @@ @property (nonatomic, assign, readonly) JSGlobalContextRef ctx; -- (instancetype)initWithJSContext:(JSGlobalContextRef)context; +- (instancetype)initWithJSContext:(JSGlobalContextRef)context NS_DESIGNATED_INITIALIZER; @end @@ -59,6 +59,8 @@ return self; } +RCT_NOT_IMPLEMENTED(-(instancetype)init) + - (BOOL)isValid { return _ctx != NULL; @@ -134,57 +136,6 @@ static JSValueRef RCTNoop(JSContextRef context, __unused JSObjectRef object, __u return JSValueMakeUndefined(context); } -#if RCT_DEV - -static NSMutableArray *profiles; - -static JSValueRef RCTConsoleProfile(JSContextRef context, __unused JSObjectRef object, __unused JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], __unused JSValueRef *exception) -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - profiles = [[NSMutableArray alloc] init]; - }); - - static int profileCounter = 1; - NSString *profileName; - NSNumber *profileID = _RCTProfileBeginEvent(); - - if (argumentCount > 0) { - profileName = RCTJSValueToNSString(context, arguments[0]); - } else { - profileName = [NSString stringWithFormat:@"Profile %d", profileCounter++]; - } - - id profileInfo = (id)kCFNull; - if (argumentCount > 1 && !JSValueIsUndefined(context, arguments[1])) { - profileInfo = @[RCTJSValueToNSString(context, arguments[1])]; - } - - [profiles addObjectsFromArray:@[profileName, profileID, profileInfo]]; - - return JSValueMakeUndefined(context); -} - -static JSValueRef RCTConsoleProfileEnd(JSContextRef context, __unused JSObjectRef object, __unused JSObjectRef thisObject, __unused size_t argumentCount, __unused const JSValueRef arguments[], __unused JSValueRef *exception) -{ - NSString *profileInfo = [profiles lastObject]; - [profiles removeLastObject]; - NSNumber *profileID = [profiles lastObject]; - [profiles removeLastObject]; - NSString *profileName = [profiles lastObject]; - [profiles removeLastObject]; - - if (argumentCount > 0 && !JSValueIsUndefined(context, arguments[0])) { - profileName = RCTJSValueToNSString(context, arguments[0]); - } - - _RCTProfileEndEvent(profileID, profileName, @"console", profileInfo); - - return JSValueMakeUndefined(context); -} - -#endif - static NSString *RCTJSValueToNSString(JSContextRef context, JSValueRef value) { JSStringRef JSString = JSValueToStringCopy(context, value, NULL); @@ -210,11 +161,56 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) return [NSError errorWithDomain:@"JS" code:1 userInfo:@{NSLocalizedDescriptionKey: errorMessage, NSLocalizedFailureReasonErrorKey: details}]; } +#if RCT_DEV + +static JSValueRef RCTNativeTraceBeginSection(JSContextRef context, __unused JSObjectRef object, __unused JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], __unused JSValueRef *exception) +{ + static int profileCounter = 1; + NSString *profileName; + double tag = 0; + + if (argumentCount > 0) { + if (JSValueIsNumber(context, arguments[0])) { + tag = JSValueToNumber(context, arguments[0], NULL); + } else { + profileName = RCTJSValueToNSString(context, arguments[0]); + } + } else { + profileName = [NSString stringWithFormat:@"Profile %d", profileCounter++]; + } + + if (argumentCount > 1 && JSValueIsString(context, arguments[1])) { + profileName = RCTJSValueToNSString(context, arguments[1]); + } + + if (profileName) { + RCTProfileBeginEvent(tag, profileName, nil); + } + + return JSValueMakeUndefined(context); +} + +static JSValueRef RCTNativeTraceEndSection(JSContextRef context, __unused JSObjectRef object, __unused JSObjectRef thisObject, __unused size_t argumentCount, __unused const JSValueRef arguments[], __unused JSValueRef *exception) +{ + if (argumentCount > 0) { + JSValueRef *error = NULL; + double tag = JSValueToNumber(context, arguments[0], error); + + if (error == NULL) { + RCTProfileEndEvent((uint64_t)tag, @"console", nil); + } + } + + return JSValueMakeUndefined(context); +} + +#endif + + (void)runRunLoopThread { @autoreleasepool { // copy thread name to pthread name - pthread_setname_np([[[NSThread currentThread] name] UTF8String]); + pthread_setname_np([NSThread currentThread].name.UTF8String); // Set up a dummy runloop source to avoid spinning CFRunLoopSourceContext noSpinCtx = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}; @@ -223,7 +219,7 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) CFRelease(noSpinSource); // run the run loop - while (kCFRunLoopRunStopped != CFRunLoopRunInMode(kCFRunLoopDefaultMode, [[NSDate distantFuture] timeIntervalSinceReferenceDate], NO)) { + while (kCFRunLoopRunStopped != CFRunLoopRunInMode(kCFRunLoopDefaultMode, ((NSDate *)[NSDate distantFuture]).timeIntervalSinceReferenceDate, NO)) { RCTAssert(NO, @"not reached assertion"); // runloop spun. that's bad. } } @@ -234,8 +230,8 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) NSThread *javaScriptThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(runRunLoopThread) object:nil]; - [javaScriptThread setName:@"com.facebook.React.JavaScript"]; - [javaScriptThread setThreadPriority:[[NSThread mainThread] threadPriority]]; + javaScriptThread.name = @"com.facebook.React.JavaScript"; + javaScriptThread.threadPriority = [NSThread mainThread].threadPriority; [javaScriptThread start]; return [self initWithJavaScriptThread:javaScriptThread globalContextRef:NULL]; @@ -283,8 +279,8 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) [strongSelf _addNativeHook:RCTNativeLoggingHook withName:"nativeLoggingHook"]; [strongSelf _addNativeHook:RCTNoop withName:"noop"]; #if RCT_DEV - [strongSelf _addNativeHook:RCTConsoleProfile withName:"consoleProfile"]; - [strongSelf _addNativeHook:RCTConsoleProfileEnd withName:"consoleProfileEnd"]; + [strongSelf _addNativeHook:RCTNativeTraceBeginSection withName:"nativeTraceBeginSection"]; + [strongSelf _addNativeHook:RCTNativeTraceEndSection withName:"nativeTraceEndSection"]; #if RCT_JSC_PROFILER void *JSCProfiler = dlopen(RCT_JSC_PROFILER_DYLIB, RTLD_NOW); @@ -461,7 +457,7 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) } onComplete(objcValue, nil); - }), @"js_call", (@{@"module":name, @"method": method, @"args": arguments}))]; + }), 0, @"js_call", (@{@"module":name, @"method": method, @"args": arguments}))]; } - (void)executeApplicationScript:(NSString *)script @@ -477,14 +473,15 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) if (!strongSelf || !strongSelf.isValid) { return; } - RCTPerformanceLoggerStart(RCTPLAppScriptExecution); + + RCTPerformanceLoggerStart(RCTPLScriptExecution); JSValueRef jsError = NULL; JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script); JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString); JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError); JSStringRelease(jsURL); JSStringRelease(execJSString); - RCTPerformanceLoggerEnd(RCTPLAppScriptExecution); + RCTPerformanceLoggerEnd(RCTPLScriptExecution); if (onComplete) { NSError *error; @@ -493,7 +490,7 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) } onComplete(error); } - }), @"js_call", (@{ @"url": sourceURL.absoluteString }))]; + }), 0, @"js_call", (@{ @"url": sourceURL.absoluteString }))]; } - (void)executeBlockOnJavaScriptQueue:(dispatch_block_t)block @@ -555,10 +552,10 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError) if (onComplete) { onComplete(nil); } - }), @"js_call,json_call", (@{@"objectName": objectName}))]; + }), 0, @"js_call,json_call", (@{@"objectName": objectName}))]; } -RCT_EXPORT_METHOD(setContextName:(NSString *)name) +RCT_EXPORT_METHOD(setContextName:(nonnull NSString *)name) { if (JSGlobalContextSetName != NULL) { JSStringRef JSName = JSStringCreateWithCFString((__bridge CFStringRef)name); diff --git a/React/Executors/RCTWebViewExecutor.m b/React/Executors/RCTWebViewExecutor.m index 539b9e779..edc3846a0 100644 --- a/React/Executors/RCTWebViewExecutor.m +++ b/React/Executors/RCTWebViewExecutor.m @@ -58,7 +58,7 @@ RCT_EXPORT_MODULE() return self; } -- (id)init +- (instancetype)init { return [self initWithWebView:nil]; } @@ -66,10 +66,10 @@ RCT_EXPORT_MODULE() - (void)setUp { if (!_webView) { - _webView = [[UIWebView alloc] init]; + _webView = [UIWebView new]; } - _objectsToInject = [[NSMutableDictionary alloc] init]; + _objectsToInject = [NSMutableDictionary new]; _commentsRegex = [NSRegularExpression regularExpressionWithPattern:@"(^ *?\\/\\/.*?$|\\/\\*\\*[\\s\\S]*?\\*\\/)" options:NSRegularExpressionAnchorsMatchLines error:NULL], _scriptTagsRegex = [NSRegularExpression regularExpressionWithPattern:@"<(\\/?script[^>]*?)>" options:0 error:NULL], _webView.delegate = self; diff --git a/React/Layout/Layout.c b/React/Layout/Layout.c index fb50d1fdc..11ca898ee 100644 --- a/React/Layout/Layout.c +++ b/React/Layout/Layout.c @@ -448,11 +448,12 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // We want to execute the next two loops one per line with flex-wrap int startLine = 0; int endLine = 0; - int nextLine = 0; + // int nextOffset = 0; + int alreadyComputedNextLayout = 0; // We aggregate the total dimensions of the container in those two variables float linesCrossDim = 0; float linesMainDim = 0; - while (endLine != node->children_count) { + while (endLine < node->children_count) { // Layout non flexible children and count children by type // mainContentDim is accumulation of the dimensions and margin of all the @@ -496,7 +497,7 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { } // This is the main recursive call. We layout non flexible children. - if (nextLine == 0) { + if (alreadyComputedNextLayout == 0) { layoutNode(child, maxWidth); } @@ -512,12 +513,15 @@ static void layoutNodeImpl(css_node_t *node, float parentMaxWidth) { // The element we are about to add would make us go to the next line if (isFlexWrap(node) && !isUndefined(node->layout.dimensions[dim[mainAxis]]) && - mainContentDim + nextContentDim > definedMainDim) { + mainContentDim + nextContentDim > definedMainDim && + // If there's only one element, then it's bigger than the content + // and needs its own line + i != startLine) { nonFlexibleChildrenCount--; - nextLine = i + 1; + alreadyComputedNextLayout = 1; break; } - nextLine = 0; + alreadyComputedNextLayout = 0; mainContentDim += nextContentDim; endLine = i + 1; } diff --git a/React/Modules/RCTAccessibilityManager.m b/React/Modules/RCTAccessibilityManager.m index 7fc58786a..12330c849 100644 --- a/React/Modules/RCTAccessibilityManager.m +++ b/React/Modules/RCTAccessibilityManager.m @@ -61,7 +61,7 @@ RCT_EXPORT_MODULE() selector:@selector(didReceiveNewContentSizeCategory:) name:UIContentSizeCategoryDidChangeNotification object:[UIApplication sharedApplication]]; - self.contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory]; + self.contentSizeCategory = [UIApplication sharedApplication].preferredContentSizeCategory; } return self; } @@ -129,7 +129,7 @@ RCT_EXPORT_MODULE() RCT_EXPORT_METHOD(setAccessibilityContentSizeMultipliers:(NSDictionary *)JSMultipliers) { - NSMutableDictionary *multipliers = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *multipliers = [NSMutableDictionary new]; for (NSString *__nonnull JSCategory in JSMultipliers) { NSNumber *m = JSMultipliers[JSCategory]; NSString *UIKitCategory = [self.class UIKitCategoryFromJSCategory:JSCategory]; diff --git a/React/Modules/RCTAlertManager.m b/React/Modules/RCTAlertManager.m index f93ef812a..261418b0d 100644 --- a/React/Modules/RCTAlertManager.m +++ b/React/Modules/RCTAlertManager.m @@ -28,9 +28,9 @@ RCT_EXPORT_MODULE() - (instancetype)init { if ((self = [super init])) { - _alerts = [[NSMutableArray alloc] init]; - _alertCallbacks = [[NSMutableArray alloc] init]; - _alertButtonKeys = [[NSMutableArray alloc] init]; + _alerts = [NSMutableArray new]; + _alertCallbacks = [NSMutableArray new]; + _alertButtonKeys = [NSMutableArray new]; } return self; } @@ -90,7 +90,7 @@ RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args if (button.count != 1) { RCTLogError(@"Button definitions should have exactly one key."); } - NSString *buttonKey = [button.allKeys firstObject]; + NSString *buttonKey = button.allKeys.firstObject; NSString *buttonTitle = [button[buttonKey] description]; [alertView addButtonWithTitle:buttonTitle]; if ([buttonKey isEqualToString: @"cancel"]) { diff --git a/React/Modules/RCTAppState.m b/React/Modules/RCTAppState.m index cf8f95910..a72ea2cc3 100644 --- a/React/Modules/RCTAppState.m +++ b/React/Modules/RCTAppState.m @@ -25,7 +25,7 @@ static NSString *RCTCurrentAppBackgroundState() }; }); - return states[@([[UIApplication sharedApplication] applicationState])] ?: @"unknown"; + return states[@([UIApplication sharedApplication].applicationState)] ?: @"unknown"; } @implementation RCTAppState @@ -96,4 +96,13 @@ RCT_EXPORT_METHOD(getCurrentAppState:(RCTResponseSenderBlock)callback callback(@[@{@"app_state": _lastKnownState}]); } +#pragma mark - RCTBridgeModule + +- (NSDictionary *)constantsToExport +{ + return @{ + @"initialAppState" : RCTCurrentAppBackgroundState() + }; +} + @end diff --git a/React/Modules/RCTAsyncLocalStorage.h b/React/Modules/RCTAsyncLocalStorage.h index 4fd1064ad..e7e871b02 100644 --- a/React/Modules/RCTAsyncLocalStorage.h +++ b/React/Modules/RCTAsyncLocalStorage.h @@ -25,6 +25,8 @@ @property (nonatomic, assign) BOOL clearOnInvalidate; +@property (nonatomic, readonly, getter=isValid) BOOL valid; + - (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; - (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; - (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; diff --git a/React/Modules/RCTAsyncLocalStorage.m b/React/Modules/RCTAsyncLocalStorage.m index 8300cc869..1ef86eb84 100644 --- a/React/Modules/RCTAsyncLocalStorage.m +++ b/React/Modules/RCTAsyncLocalStorage.m @@ -66,7 +66,7 @@ static NSString *RCTGetStorageDirectory() static NSString *storageDirectory = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - storageDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + storageDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject; storageDirectory = [storageDirectory stringByAppendingPathComponent:RCTStorageDirectory]; }); return storageDirectory; @@ -161,9 +161,10 @@ RCT_EXPORT_MODULE() RCTDeleteStorageDirectory(); } _clearOnInvalidate = NO; - _manifest = [[NSMutableDictionary alloc] init]; + _manifest = [NSMutableDictionary new]; _haveSetup = NO; } + - (BOOL)isValid { return _haveSetup; @@ -198,10 +199,10 @@ RCT_EXPORT_MODULE() if (!_haveSetup) { NSDictionary *errorOut; NSString *serialized = RCTReadFile(RCTGetManifestFilePath(), nil, &errorOut); - _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [[NSMutableDictionary alloc] init]; + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; if (error) { RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); - _manifest = [[NSMutableDictionary alloc] init]; + _manifest = [NSMutableDictionary new]; } _haveSetup = YES; } @@ -376,7 +377,7 @@ RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - _manifest = [[NSMutableDictionary alloc] init]; + _manifest = [NSMutableDictionary new]; NSError *error = RCTDeleteStorageDirectory(); if (callback) { callback(@[RCTNullIfNil(error)]); @@ -389,7 +390,7 @@ RCT_EXPORT_METHOD(getAllKeys:(RCTResponseSenderBlock)callback) if (errorOut) { callback(@[errorOut, (id)kCFNull]); } else { - callback(@[(id)kCFNull, [_manifest allKeys]]); + callback(@[(id)kCFNull, _manifest.allKeys]); } } diff --git a/React/Modules/RCTDevLoadingView.m b/React/Modules/RCTDevLoadingView.m index d9e30b667..b716b2483 100644 --- a/React/Modules/RCTDevLoadingView.m +++ b/React/Modules/RCTDevLoadingView.m @@ -61,7 +61,7 @@ RCT_EXPORT_MODULE() _showDate = [NSDate date]; if (!_window && !RCTRunningInTestEnvironment()) { - CGFloat screenWidth = [[UIScreen mainScreen] bounds].size.width; + CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; _window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, screenWidth, 22)]; _window.backgroundColor = [UIColor blackColor]; _window.windowLevel = UIWindowLevelStatusBar + 1; @@ -76,10 +76,10 @@ RCT_EXPORT_MODULE() } NSString *source; - if ([URL isFileURL]) { + if (URL.fileURL) { source = @"pre-bundled file"; } else { - source = [NSString stringWithFormat:@"%@:%@", [URL host], [URL port]]; + source = [NSString stringWithFormat:@"%@:%@", URL.host, URL.port]; } _label.text = [NSString stringWithFormat:@"Loading from %@...", source]; @@ -116,6 +116,8 @@ RCT_EXPORT_MODULE() @implementation RCTDevLoadingView ++ (NSString *)moduleName { return nil; } + @end #endif diff --git a/React/Modules/RCTDevMenu.h b/React/Modules/RCTDevMenu.h index ed1ff90b8..13ddb0689 100644 --- a/React/Modules/RCTDevMenu.h +++ b/React/Modules/RCTDevMenu.h @@ -11,7 +11,6 @@ #import "RCTBridge.h" #import "RCTBridgeModule.h" -#import "RCTInvalidating.h" /** * Developer menu, useful for exposing extra functionality when debugging. diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index e8762a62d..56ef7dfd4 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -64,7 +64,7 @@ static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu"; return self; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) @end @@ -125,7 +125,7 @@ RCT_EXPORT_MODULE() object:nil]; _defaults = [NSUserDefaults standardUserDefaults]; - _settings = [[NSMutableDictionary alloc] init]; + _settings = [NSMutableDictionary new]; _extraMenuItems = [NSMutableArray array]; // Delay setup until after Bridge init @@ -210,7 +210,7 @@ RCT_EXPORT_MODULE() } else { RCTLogWarn(@"RCTSourceCode module scriptURL has not been set"); } - } else if (![sourceCodeModule.scriptURL isFileURL]) { + } else if (!(sourceCodeModule.scriptURL).fileURL) { // Live reloading is disabled when running from bundled JS file _liveReloadURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:sourceCodeModule.scriptURL]; } @@ -223,11 +223,6 @@ RCT_EXPORT_MODULE() }); } -- (BOOL)isValid -{ - return NO; -} - - (void)dealloc { [_updateTask cancel]; @@ -336,7 +331,7 @@ RCT_EXPORT_METHOD(show) return; } - UIActionSheet *actionSheet = [[UIActionSheet alloc] init]; + UIActionSheet *actionSheet = [UIActionSheet new]; actionSheet.title = @"React Native: Development"; actionSheet.delegate = self; @@ -346,7 +341,7 @@ RCT_EXPORT_METHOD(show) } [actionSheet addButtonWithTitle:@"Cancel"]; - actionSheet.cancelButtonIndex = [actionSheet numberOfButtons] - 1; + actionSheet.cancelButtonIndex = actionSheet.numberOfButtons - 1; actionSheet.actionSheetStyle = UIBarStyleBlack; [actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view]; diff --git a/React/Modules/RCTExceptionsManager.m b/React/Modules/RCTExceptionsManager.m index b353420f8..c8df2689c 100644 --- a/React/Modules/RCTExceptionsManager.m +++ b/React/Modules/RCTExceptionsManager.m @@ -20,6 +20,8 @@ NSUInteger _reloadRetries; } +@synthesize bridge = _bridge; + RCT_EXPORT_MODULE() - (instancetype)initWithDelegate:(id)delegate @@ -44,7 +46,7 @@ RCT_EXPORT_METHOD(reportSoftException:(NSString *)message [_delegate handleSoftJSExceptionWithMessage:message stack:stack]; return; } - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; + [_bridge.redBox showErrorMessage:message withStack:stack]; } RCT_EXPORT_METHOD(reportFatalException:(NSString *)message @@ -56,7 +58,7 @@ RCT_EXPORT_METHOD(reportFatalException:(NSString *)message return; } - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; + [_bridge.redBox showErrorMessage:message withStack:stack]; if (!RCT_DEBUG) { @@ -95,7 +97,7 @@ RCT_EXPORT_METHOD(updateExceptionMessage:(NSString *)message return; } - [[RCTRedBox sharedInstance] updateErrorMessage:message withStack:stack]; + [_bridge.redBox updateErrorMessage:message withStack:stack]; } // Deprecated. Use reportFatalException directly instead. diff --git a/React/Base/RCTRedBox.h b/React/Modules/RCTRedBox.h similarity index 65% rename from React/Base/RCTRedBox.h rename to React/Modules/RCTRedBox.h index 168772a91..a90f43deb 100644 --- a/React/Base/RCTRedBox.h +++ b/React/Modules/RCTRedBox.h @@ -9,9 +9,10 @@ #import -@interface RCTRedBox : NSObject +#import "RCTBridge.h" +#import "RCTBridgeModule.h" -+ (instancetype)sharedInstance; +@interface RCTRedBox : NSObject - (void)showError:(NSError *)error; - (void)showErrorMessage:(NSString *)message; @@ -19,10 +20,16 @@ - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack; - (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack; -- (void)setNextBackgroundColor:(UIColor *)color; - -- (NSString *)currentErrorMessage; - - (void)dismiss; @end + +/** + * This category makes the red box instance available via the RCTBridge, which + * is useful for any class that needs to access the red box or error log. + */ +@interface RCTBridge (RCTRedBox) + +@property (nonatomic, readonly) RCTRedBox *redBox; + +@end diff --git a/React/Base/RCTRedBox.m b/React/Modules/RCTRedBox.m similarity index 75% rename from React/Base/RCTRedBox.m rename to React/Modules/RCTRedBox.m index 862b360a7..810830c9d 100644 --- a/React/Base/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -18,68 +18,65 @@ @interface RCTRedBoxWindow : UIWindow -@property (nonatomic, copy) NSString *lastErrorMessage; - @end @implementation RCTRedBoxWindow { - UIView *_rootView; UITableView *_stackTraceTableView; - + NSString *_lastErrorMessage; NSArray *_lastStackTrace; - - UITableViewCell *_cachedMessageCell; - UIColor *_redColor; } -- (id)initWithFrame:(CGRect)frame +- (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { - _redColor = [UIColor colorWithRed:0.8 green:0 blue:0 alpha:1]; self.windowLevel = UIWindowLevelAlert + 1000; - self.backgroundColor = _redColor; + self.backgroundColor = [UIColor colorWithRed:0.8 green:0 blue:0 alpha:1]; self.hidden = YES; - UIViewController *rootController = [[UIViewController alloc] init]; + UIViewController *rootController = [UIViewController new]; self.rootViewController = rootController; - _rootView = rootController.view; - _rootView.backgroundColor = [UIColor clearColor]; + UIView *rootView = rootController.view; + rootView.backgroundColor = [UIColor clearColor]; const CGFloat buttonHeight = 60; - CGRect detailsFrame = _rootView.bounds; + CGRect detailsFrame = rootView.bounds; detailsFrame.size.height -= buttonHeight; _stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain]; + _stackTraceTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _stackTraceTableView.delegate = self; _stackTraceTableView.dataSource = self; _stackTraceTableView.backgroundColor = [UIColor clearColor]; - _stackTraceTableView.separatorColor = [[UIColor whiteColor] colorWithAlphaComponent:0.3]; + _stackTraceTableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.3]; _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone; - [_rootView addSubview:_stackTraceTableView]; + _stackTraceTableView.indicatorStyle = UIScrollViewIndicatorStyleWhite; + [rootView addSubview:_stackTraceTableView]; UIButton *dismissButton = [UIButton buttonWithType:UIButtonTypeCustom]; + dismissButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin; dismissButton.accessibilityIdentifier = @"redbox-dismiss"; dismissButton.titleLabel.font = [UIFont systemFontOfSize:14]; [dismissButton setTitle:@"Dismiss (ESC)" forState:UIControlStateNormal]; - [dismissButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5] forState:UIControlStateNormal]; + [dismissButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal]; [dismissButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; [dismissButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside]; UIButton *reloadButton = [UIButton buttonWithType:UIButtonTypeCustom]; + reloadButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin; reloadButton.accessibilityIdentifier = @"redbox-reload"; reloadButton.titleLabel.font = [UIFont systemFontOfSize:14]; [reloadButton setTitle:@"Reload JS (\u2318R)" forState:UIControlStateNormal]; - [reloadButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5] forState:UIControlStateNormal]; + [reloadButton setTitleColor:[UIColor colorWithWhite:1 alpha:0.5] forState:UIControlStateNormal]; [reloadButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; [reloadButton addTarget:self action:@selector(reload) forControlEvents:UIControlEventTouchUpInside]; CGFloat buttonWidth = self.bounds.size.width / 2; dismissButton.frame = CGRectMake(0, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); reloadButton.frame = CGRectMake(buttonWidth, self.bounds.size.height - buttonHeight, buttonWidth, buttonHeight); - [_rootView addSubview:dismissButton]; - [_rootView addSubview:reloadButton]; + [rootView addSubview:dismissButton]; + [rootView addSubview:reloadButton]; NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; @@ -91,18 +88,20 @@ return self; } -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)dealloc { + _stackTraceTableView.dataSource = nil; + _stackTraceTableView.delegate = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)openStackFrameInEditor:(NSDictionary *)stackFrame { NSData *stackFrameJSON = [RCTJSONStringify(stackFrame, nil) dataUsingEncoding:NSUTF8StringEncoding]; - NSString *postLength = [NSString stringWithFormat:@"%lu", (unsigned long)[stackFrameJSON length]]; - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; + NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length]; + NSMutableURLRequest *request = [NSMutableURLRequest new]; request.URL = [RCTConvert NSURL:@"http://localhost:8081/open-stack-frame"]; request.HTTPMethod = @"POST"; request.HTTPBody = stackFrameJSON; @@ -118,9 +117,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) _lastStackTrace = stack; _lastErrorMessage = message; - _cachedMessageCell = [self reuseCell:nil forErrorMessage:message]; [_stackTraceTableView reloadData]; - [_stackTraceTableView setNeedsLayout]; if (self.hidden) { [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] @@ -136,9 +133,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) - (void)dismiss { self.hidden = YES; - self.backgroundColor = _redColor; [self resignFirstResponder]; - [[[[UIApplication sharedApplication] delegate] window] makeKeyWindow]; + [[UIApplication sharedApplication].delegate.window makeKeyWindow]; } - (void)reload @@ -155,17 +151,17 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) - (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return section == 0 ? 1 : [_lastStackTrace count]; + return section == 0 ? 1 : _lastStackTrace.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - if ([indexPath section] == 0) { + if (indexPath.section == 0) { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"msg-cell"]; return [self reuseCell:cell forErrorMessage:_lastErrorMessage]; } UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; - NSUInteger index = [indexPath row]; + NSUInteger index = indexPath.row; NSDictionary *stackFrame = _lastStackTrace[index]; return [self reuseCell:cell forStackFrame:stackFrame]; } @@ -193,30 +189,27 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) { if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"cell"]; - cell.textLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9]; + cell.textLabel.textColor = [UIColor colorWithWhite:1 alpha:0.9]; cell.textLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:14]; cell.textLabel.numberOfLines = 2; - cell.detailTextLabel.textColor = [[UIColor whiteColor] colorWithAlphaComponent:0.7]; + cell.detailTextLabel.textColor = [UIColor colorWithWhite:1 alpha:0.7]; cell.detailTextLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:11]; cell.detailTextLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; cell.backgroundColor = [UIColor clearColor]; - cell.selectedBackgroundView = [[UIView alloc] init]; - cell.selectedBackgroundView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2]; + cell.selectedBackgroundView = [UIView new]; + cell.selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2]; } cell.textLabel.text = stackFrame[@"methodName"]; - NSString *fileAndLine = stackFrame[@"file"]; - if (fileAndLine) { - fileAndLine = [fileAndLine stringByAppendingFormat:@":%@", stackFrame[@"lineNumber"]]; - cell.detailTextLabel.text = cell.detailTextLabel.text = fileAndLine; - } + NSString *fileAndLine = [stackFrame[@"file"] lastPathComponent]; + cell.detailTextLabel.text = fileAndLine ? [fileAndLine stringByAppendingFormat:@":%@", stackFrame[@"lineNumber"]] : nil; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { - if ([indexPath section] == 0) { + if (indexPath.section == 0) { NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; @@ -231,8 +224,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - if ([indexPath section] == 1) { - NSUInteger row = [indexPath row]; + if (indexPath.section == 1) { + NSUInteger row = indexPath.row; NSDictionary *stackFrame = _lastStackTrace[row]; [self openStackFrameInEditor:stackFrame]; } @@ -271,23 +264,9 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) @implementation RCTRedBox { RCTRedBoxWindow *_window; - UIColor *_nextBackgroundColor; } -+ (instancetype)sharedInstance -{ - static RCTRedBox *_sharedInstance; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - _sharedInstance = [[RCTRedBox alloc] init]; - }); - return _sharedInstance; -} - -- (void)setNextBackgroundColor:(UIColor *)color -{ - _nextBackgroundColor = color; -} +RCT_EXPORT_MODULE() - (void)showError:(NSError *)error { @@ -322,25 +301,12 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) { dispatch_async(dispatch_get_main_queue(), ^{ if (!_window) { - _window = [[RCTRedBoxWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + _window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; } [_window showErrorMessage:message withStack:stack showIfHidden:shouldShow]; - if (_nextBackgroundColor) { - _window.backgroundColor = _nextBackgroundColor; - _nextBackgroundColor = nil; - } }); } -- (NSString *)currentErrorMessage -{ - if (_window && !_window.hidden) { - return _window.lastErrorMessage; - } else { - return nil; - } -} - - (void)dismiss { [_window dismiss]; @@ -348,21 +314,34 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) @end +@implementation RCTBridge (RCTRedBox) + +- (RCTRedBox *)redBox +{ + return self.modules[RCTBridgeModuleNameForClass([RCTRedBox class])]; +} + +@end + #else // Disabled @implementation RCTRedBox -+ (instancetype)sharedInstance { return nil; } ++ (NSString *)moduleName { return nil; } - (void)showError:(NSError *)message {} - (void)showErrorMessage:(NSString *)message {} - (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details {} - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack {} - (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack {} - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow {} -- (NSString *)currentErrorMessage { return nil; } -- (void)setNextBackgroundColor:(UIColor *)color {} - (void)dismiss {} @end +@implementation RCTBridge (RCTRedBox) + +- (RCTRedBox *)redBox { return nil; } + +@end + #endif diff --git a/React/Modules/RCTSourceCode.m b/React/Modules/RCTSourceCode.m index 7498728f2..08b033781 100644 --- a/React/Modules/RCTSourceCode.m +++ b/React/Modules/RCTSourceCode.m @@ -23,7 +23,7 @@ RCT_EXPORT_METHOD(getScriptText:(RCTResponseSenderBlock)successCallback failureCallback:(RCTResponseErrorBlock)failureCallback) { if (self.scriptText && self.scriptURL) { - successCallback(@[@{@"text": self.scriptText, @"url":[self.scriptURL absoluteString]}]); + successCallback(@[@{@"text": self.scriptText, @"url": self.scriptURL.absoluteString}]); } else { failureCallback(RCTErrorWithMessage(@"Source code is not available")); } @@ -31,7 +31,7 @@ RCT_EXPORT_METHOD(getScriptText:(RCTResponseSenderBlock)successCallback - (NSDictionary *)constantsToExport { - NSString *URL = [self.bridge.bundleURL absoluteString] ?: @""; + NSString *URL = self.bridge.bundleURL.absoluteString ?: @""; return @{@"scriptURL": URL}; } diff --git a/React/Modules/RCTStatusBarManager.m b/React/Modules/RCTStatusBarManager.m index bf32f835c..ce2364e2b 100644 --- a/React/Modules/RCTStatusBarManager.m +++ b/React/Modules/RCTStatusBarManager.m @@ -9,6 +9,7 @@ #import "RCTStatusBarManager.h" +#import "RCTEventDispatcher.h" #import "RCTLog.h" @implementation RCTConvert (UIStatusBar) @@ -42,11 +43,53 @@ static BOOL RCTViewControllerBasedStatusBarAppearance() RCT_EXPORT_MODULE() +@synthesize bridge = _bridge; + +- (instancetype)init +{ + if ((self = [super init])) { + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc addObserver:self selector:@selector(applicationDidChangeStatusBarFrame:) name:UIApplicationDidChangeStatusBarFrameNotification object:nil]; + [nc addObserver:self selector:@selector(applicationWillChangeStatusBarFrame:) name:UIApplicationWillChangeStatusBarFrameNotification object:nil]; + + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } +- (void)emitEvent:(NSString *)eventName forNotification:(NSNotification *)notification +{ + CGRect frame = [notification.userInfo[UIApplicationStatusBarFrameUserInfoKey] CGRectValue]; + NSDictionary *event = @{ + @"frame": @{ + @"x": @(frame.origin.x), + @"y": @(frame.origin.y), + @"width": @(frame.size.width), + @"height": @(frame.size.height), + }, + }; + [_bridge.eventDispatcher sendDeviceEventWithName:eventName body:event]; +} + +- (void)applicationDidChangeStatusBarFrame:(NSNotification *)notification +{ + [self emitEvent:@"statusBarFrameDidChange" forNotification:notification]; +} + +- (void)applicationWillChangeStatusBarFrame:(NSNotification *)notification +{ + [self emitEvent:@"statusBarFrameWillChange" forNotification:notification]; +} + RCT_EXPORT_METHOD(setStyle:(UIStatusBarStyle)statusBarStyle animated:(BOOL)animated) { @@ -73,7 +116,7 @@ RCT_EXPORT_METHOD(setHidden:(BOOL)hidden RCT_EXPORT_METHOD(setNetworkActivityIndicatorVisible:(BOOL)visible) { - [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:visible]; + [UIApplication sharedApplication].networkActivityIndicatorVisible = visible; } @end diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index 29e3b3f92..2754925e2 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -78,7 +78,7 @@ RCT_EXPORT_MODULE() { if ((self = [super init])) { _paused = YES; - _timers = [[RCTSparseArray alloc] init]; + _timers = [RCTSparseArray new]; for (NSString *name in @[UIApplicationWillResignActiveNotification, UIApplicationDidEnterBackgroundNotification, @@ -112,11 +112,6 @@ RCT_EXPORT_MODULE() return RCTJSThread; } -- (BOOL)isValid -{ - return _bridge != nil; -} - - (void)invalidate { [self stopTimers]; @@ -130,7 +125,7 @@ RCT_EXPORT_MODULE() - (void)startTimers { - if (![self isValid] || _timers.count == 0) { + if (!_bridge || _timers.count == 0) { return; } @@ -139,7 +134,7 @@ RCT_EXPORT_MODULE() - (void)didUpdateFrame:(__unused RCTFrameUpdate *)update { - NSMutableArray *timersToCall = [[NSMutableArray alloc] init]; + NSMutableArray *timersToCall = [NSMutableArray new]; for (RCTTimer *timer in _timers.allObjects) { if ([timer updateFoundNeedsJSUpdate]) { [timersToCall addObject:timer.callbackID]; @@ -150,7 +145,7 @@ RCT_EXPORT_MODULE() } // call timers that need to be called - if ([timersToCall count] > 0) { + if (timersToCall.count > 0) { [_bridge enqueueJSCall:@"JSTimersExecution.callTimers" args:@[timersToCall]]; } diff --git a/React/Modules/RCTUIManager.h b/React/Modules/RCTUIManager.h index beb9e5d5a..9fd8cb7e8 100644 --- a/React/Modules/RCTUIManager.h +++ b/React/Modules/RCTUIManager.h @@ -20,6 +20,23 @@ */ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification; +/** + * Posted whenever a new root view is registered with RCTUIManager. The userInfo property + * will contain a RCTUIManagerRootViewKey with the registered root view. + */ +RCT_EXTERN NSString *const RCTUIManagerDidRegisterRootViewNotification; + +/** + * Posted whenever a root view is removed from the RCTUIManager. The userInfo property + * will contain a RCTUIManagerRootViewKey with the removed root view. + */ +RCT_EXTERN NSString *const RCTUIManagerDidRemoveRootViewNotification; + +/** + * Key for the root view property in the above notifications + */ +RCT_EXTERN NSString *const RCTUIManagerRootViewKey; + @protocol RCTScrollableProtocol; /** @@ -27,6 +44,11 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier */ @interface RCTUIManager : NSObject +/** + * The UIIManager has the concept of a designated "main scroll view", which is + * useful for apps built around a central scrolling content area (e.g. a + * timeline). + */ @property (nonatomic, weak) id mainScrollView; /** diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 3a1c2f25a..9af3a6ceb 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -41,6 +41,9 @@ static void RCTTraverseViewNodes(id view, void (^block)(id_shadowViewRegistry[shadowView.reactTag] = shadowView; [strongSelf->_rootViewTags addObject:reactTag]; }); + + [[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerDidRegisterRootViewNotification + object:self + userInfo:@{ RCTUIManagerRootViewKey: rootView }]; } - (UIView *)viewForReactTag:(NSNumber *)reactTag @@ -369,7 +371,7 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); __weak RCTUIManager *weakSelf = self; dispatch_async(_shadowQueue, ^{ RCTUIManager *strongSelf = weakSelf; - if (!strongSelf.isValid) { + if (!_viewRegistry) { return; } RCTShadowView *rootShadowView = strongSelf->_shadowViewRegistry[reactTag]; @@ -410,14 +412,14 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); return; } - if (!self.isValid) { + if (!_viewRegistry) { return; } __weak RCTUIManager *weakViewManager = self; dispatch_block_t outerBlock = ^{ RCTUIManager *strongViewManager = weakViewManager; - if (strongViewManager && strongViewManager.isValid) { + if (strongViewManager && strongViewManager->_viewRegistry) { block(strongViewManager, strongViewManager->_viewRegistry); } }; @@ -477,7 +479,7 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); // reactSetFrame: has been called. Note that if reactSetFrame: is not called, // these won't be called either, so this is not a suitable place to update // properties that aren't related to layout. - NSMutableArray *updateBlocks = [[NSMutableArray alloc] init]; + NSMutableArray *updateBlocks = [NSMutableArray new]; for (RCTShadowView *shadowView in viewsWithNewFrames) { RCTViewManager *manager = [_componentDataByName[shadowView.viewName] manager]; RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView]; @@ -642,6 +644,10 @@ RCT_EXPORT_METHOD(removeRootView:(nonnull NSNumber *)rootReactTag) UIView *rootView = viewRegistry[rootReactTag]; [uiManager _purgeChildren:rootView.reactSubviews fromRegistry:viewRegistry]; viewRegistry[rootReactTag] = nil; + + [[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerDidRemoveRootViewNotification + object:uiManager + userInfo:@{ RCTUIManagerRootViewKey: rootView }]; }]; } @@ -725,7 +731,7 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerReactTag } } - NSArray *sortedIndices = [[destinationsToChildrenToAdd allKeys] sortedArrayUsingSelector:@selector(compare:)]; + NSArray *sortedIndices = [destinationsToChildrenToAdd.allKeys sortedArrayUsingSelector:@selector(compare:)]; for (NSNumber *reactIndex in sortedIndices) { [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; } @@ -752,9 +758,9 @@ RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag UIColor *backgroundColor = shadowView.backgroundColor; [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ - id view = [componentData createViewWithTag:reactTag]; + id view = [componentData createViewWithTag:reactTag props:props]; if ([view respondsToSelector:@selector(setBackgroundColor:)]) { - [(UIView *)view setBackgroundColor:backgroundColor]; + ((UIView *)view).backgroundColor = backgroundColor; } [componentData setProps:props forView:view]; if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) { @@ -765,11 +771,11 @@ RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag } RCT_EXPORT_METHOD(updateView:(nonnull NSNumber *)reactTag - viewName:(__unused NSString *)viewName // not reliable, do not use + viewName:(NSString *)viewName // not always reliable, use shadowView.viewName if available props:(NSDictionary *)props) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; - RCTComponentData *componentData = _componentDataByName[shadowView.viewName]; + RCTComponentData *componentData = _componentDataByName[shadowView.viewName ?: viewName]; [componentData setProps:props forShadowView:shadowView]; [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { @@ -804,7 +810,7 @@ RCT_EXPORT_METHOD(findSubviewIn:(nonnull NSNumber *)reactTag atPoint:(CGPoint)po CGRect frame = [target convertRect:target.bounds toView:view]; while (target.reactTag == nil && target.superview != nil) { - target = [target superview]; + target = target.superview; } callback(@[ @@ -819,7 +825,8 @@ RCT_EXPORT_METHOD(findSubviewIn:(nonnull NSNumber *)reactTag atPoint:(CGPoint)po - (void)batchDidComplete { - RCTProfileBeginEvent(); + RCTProfileBeginEvent(0, @"[RCTUIManager batchDidComplete]", nil); + // Gather blocks to be executed now that all view hierarchy manipulations have // been completed (note that these may still take place before layout has finished) for (RCTComponentData *componentData in _componentDataByName.allValues) { @@ -850,8 +857,8 @@ RCT_EXPORT_METHOD(findSubviewIn:(nonnull NSNumber *)reactTag atPoint:(CGPoint)po _nextLayoutAnimation = nil; } - RCTProfileEndEvent(@"[RCTUIManager batchDidComplete]", @"uimanager", @{ - @"view_count": @([_viewRegistry count]), + RCTProfileEndEvent(0, @"uimanager", @{ + @"view_count": @(_viewRegistry.count), }); [self flushUIBlocks]; } @@ -863,18 +870,23 @@ RCT_EXPORT_METHOD(findSubviewIn:(nonnull NSNumber *)reactTag atPoint:(CGPoint)po // processing the pending blocks in another thread. [_pendingUIBlocksLock lock]; NSArray *previousPendingUIBlocks = _pendingUIBlocks; - _pendingUIBlocks = [[NSMutableArray alloc] init]; + _pendingUIBlocks = [NSMutableArray new]; [_pendingUIBlocksLock unlock]; // Execute the previously queued UI blocks RCTProfileBeginFlowEvent(); dispatch_async(dispatch_get_main_queue(), ^{ RCTProfileEndFlowEvent(); - RCTProfileBeginEvent(); - for (dispatch_block_t block in previousPendingUIBlocks) { - block(); + RCTProfileBeginEvent(0, @"UIManager flushUIBlocks", nil); + @try { + for (dispatch_block_t block in previousPendingUIBlocks) { + block(); + } } - RCTProfileEndEvent(@"UIManager flushUIBlocks", @"objc_call", @{ + @catch (NSException *exception) { + RCTLogError(@"Exception thrown while executing UI block: %@", exception); + } + RCTProfileEndEvent(0, @"objc_call", @{ @"count": @(previousPendingUIBlocks.count), }); }); @@ -993,7 +1005,7 @@ RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect return; } NSArray *childShadowViews = [shadowView reactSubviews]; - NSMutableArray *results = [[NSMutableArray alloc] initWithCapacity:[childShadowViews count]]; + NSMutableArray *results = [[NSMutableArray alloc] initWithCapacity:childShadowViews.count]; [childShadowViews enumerateObjectsUsingBlock: ^(RCTShadowView *childShadowView, NSUInteger idx, __unused BOOL *stop) { @@ -1043,7 +1055,7 @@ RCT_EXPORT_METHOD(setMainScrollViewTag:(nonnull NSNumber *)reactTag) uiManager.mainScrollView = (id)view; uiManager.mainScrollView.nativeMainScrollDelegate = uiManager.nativeMainScrollDelegate; } else { - RCTAssert(NO, @"Tag #%@ does not conform to RCTScrollableProtocol", reactTag); + RCTLogError(@"Tag #%@ does not conform to RCTScrollableProtocol", reactTag); } } else { uiManager.mainScrollView = nil; @@ -1118,7 +1130,7 @@ RCT_EXPORT_METHOD(clearJSResponder) - (NSDictionary *)bubblingEventsConfig { - NSMutableDictionary *customBubblingEventTypesConfigs = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *customBubblingEventTypesConfigs = [NSMutableDictionary new]; for (RCTComponentData *componentData in _componentDataByName.allValues) { RCTViewManager *manager = componentData.manager; if (RCTClassOverridesInstanceMethod([manager class], @selector(customBubblingEventTypes))) { @@ -1148,7 +1160,7 @@ RCT_EXPORT_METHOD(clearJSResponder) - (NSDictionary *)directEventsConfig { - NSMutableDictionary *customDirectEventTypes = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *customDirectEventTypes = [NSMutableDictionary new]; for (RCTComponentData *componentData in _componentDataByName.allValues) { RCTViewManager *manager = componentData.manager; if (RCTClassOverridesInstanceMethod([manager class], @selector(customDirectEventTypes))) { diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index d3a404dab..97a278353 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067501A70F44B002CDEE1 /* RCTView.m */; }; 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+React.m */; }; + 13F17A851B8493E5007D4C75 /* RCTRedBox.m in Sources */ = {isa = PBXBuildFile; fileRef = 13F17A841B8493E5007D4C75 /* RCTRedBox.m */; }; 1403F2B31B0AE60700C2A9A4 /* RCTPerfStats.m in Sources */ = {isa = PBXBuildFile; fileRef = 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */; }; 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */; }; 142014191B32094000CC17BA /* RCTPerformanceLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 142014171B32094000CC17BA /* RCTPerformanceLogger.m */; }; @@ -70,12 +71,12 @@ 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; 83392EB31B6634E10013B15F /* RCTModalHostViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 83392EB21B6634E10013B15F /* RCTModalHostViewController.m */; }; + 8385CF351B8B77CD00C6273E /* RCTKeyboardObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 8385CF341B8B77CD00C6273E /* RCTKeyboardObserver.m */; }; 83A1FE8C1B62640A00BE0E65 /* RCTModalHostView.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A1FE8B1B62640A00BE0E65 /* RCTModalHostView.m */; }; 83A1FE8F1B62643A00BE0E65 /* RCTModalHostViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 83A1FE8E1B62643A00BE0E65 /* RCTModalHostViewManager.m */; }; 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */; }; 83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */; }; 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA501A601E3B00E9B192 /* RCTUtils.m */; }; - 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA591A601E9000E9B192 /* RCTRedBox.m */; }; 83CBBA601A601EAA00E9B192 /* RCTBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA5F1A601EAA00E9B192 /* RCTBridge.m */; }; 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */; }; 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */; }; @@ -188,6 +189,8 @@ 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = ""; }; 13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; + 13F17A831B8493E5007D4C75 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = ""; }; + 13F17A841B8493E5007D4C75 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = ""; }; 1403F2B11B0AE60700C2A9A4 /* RCTPerfStats.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPerfStats.h; sourceTree = ""; }; 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPerfStats.m; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; @@ -231,6 +234,8 @@ 830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = ""; }; 83392EB11B6634E10013B15F /* RCTModalHostViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModalHostViewController.h; sourceTree = ""; }; 83392EB21B6634E10013B15F /* RCTModalHostViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModalHostViewController.m; sourceTree = ""; }; + 8385CF331B8B77CD00C6273E /* RCTKeyboardObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTKeyboardObserver.h; sourceTree = ""; }; + 8385CF341B8B77CD00C6273E /* RCTKeyboardObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTKeyboardObserver.m; sourceTree = ""; }; 83A1FE8A1B62640A00BE0E65 /* RCTModalHostView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModalHostView.h; sourceTree = ""; }; 83A1FE8B1B62640A00BE0E65 /* RCTModalHostView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTModalHostView.m; sourceTree = ""; }; 83A1FE8D1B62643A00BE0E65 /* RCTModalHostViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTModalHostViewManager.h; sourceTree = ""; }; @@ -245,8 +250,6 @@ 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTLog.m; sourceTree = ""; }; 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUtils.h; sourceTree = ""; }; 83CBBA501A601E3B00E9B192 /* RCTUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUtils.m; sourceTree = ""; }; - 83CBBA581A601E9000E9B192 /* RCTRedBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRedBox.h; sourceTree = ""; }; - 83CBBA591A601E9000E9B192 /* RCTRedBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRedBox.m; sourceTree = ""; }; 83CBBA5E1A601EAA00E9B192 /* RCTBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBridge.h; sourceTree = ""; }; 83CBBA5F1A601EAA00E9B192 /* RCTBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridge.m; sourceTree = ""; }; 83CBBA631A601ECA00E9B192 /* RCTJavaScriptExecutor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptExecutor.h; sourceTree = ""; }; @@ -256,6 +259,7 @@ 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTouchHandler.m; sourceTree = ""; }; 83CBBACA1A6023D300E9B192 /* RCTConvert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTConvert.h; sourceTree = ""; }; 83CBBACB1A6023D300E9B192 /* RCTConvert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert.m; sourceTree = ""; }; + 83F15A171B7CC46900F10295 /* UIView+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIView+Private.h"; sourceTree = ""; }; E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = ""; }; E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAccessibilityManager.h; sourceTree = ""; }; E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAccessibilityManager.m; sourceTree = ""; }; @@ -311,6 +315,8 @@ 13B07FEA1A69327A00A75B9A /* RCTExceptionsManager.m */, 63F014BE1B02080B003B75D2 /* RCTPointAnnotation.h */, 63F014BF1B02080B003B75D2 /* RCTPointAnnotation.m */, + 13F17A831B8493E5007D4C75 /* RCTRedBox.h */, + 13F17A841B8493E5007D4C75 /* RCTRedBox.m */, 000E6CE91AB0E97F000CDF4D /* RCTSourceCode.h */, 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */, 13723B4E1A82FD3C00F88898 /* RCTStatusBarManager.h */, @@ -405,6 +411,7 @@ 13C156041AB1A2840079392D /* RCTWebViewManager.m */, 13B080231A694A8400A75B9A /* RCTWrapperViewController.h */, 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */, + 83F15A171B7CC46900F10295 /* UIView+Private.h */, 13E067531A70F44B002CDEE1 /* UIView+React.h */, 13E067541A70F44B002CDEE1 /* UIView+React.m */, ); @@ -444,14 +451,15 @@ 83CBBA491A601E3B00E9B192 /* Base */ = { isa = PBXGroup; children = ( - 138D6A121B53CD290074A87E /* RCTCache.h */, - 138D6A131B53CD290074A87E /* RCTCache.m */, 83CBBA4A1A601E3B00E9B192 /* RCTAssert.h */, 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */, 14C2CA771B3ACB0400E6CBB2 /* RCTBatchedBridge.m */, 83CBBA5E1A601EAA00E9B192 /* RCTBridge.h */, 83CBBA5F1A601EAA00E9B192 /* RCTBridge.m */, + 1482F9E61B55B927000ADFF3 /* RCTBridgeDelegate.h */, 830213F31A654E0800B993E6 /* RCTBridgeModule.h */, + 138D6A121B53CD290074A87E /* RCTCache.h */, + 138D6A131B53CD290074A87E /* RCTCache.m */, 83CBBACA1A6023D300E9B192 /* RCTConvert.h */, 83CBBACB1A6023D300E9B192 /* RCTConvert.m */, 13AF1F851AE6E777005F5298 /* RCTDefines.h */, @@ -465,6 +473,8 @@ 83CBBA631A601ECA00E9B192 /* RCTJavaScriptExecutor.h */, 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */, 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */, + 8385CF331B8B77CD00C6273E /* RCTKeyboardObserver.h */, + 8385CF341B8B77CD00C6273E /* RCTKeyboardObserver.m */, 13A1F71C1A75392D00D3D453 /* RCTKeyCommands.h */, 13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */, 83CBBA4D1A601E3B00E9B192 /* RCTLog.h */, @@ -481,8 +491,6 @@ 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */, 14F4D3891AE1B7E40049C042 /* RCTProfile.h */, 14F4D38A1AE1B7E40049C042 /* RCTProfile.m */, - 83CBBA581A601E9000E9B192 /* RCTRedBox.h */, - 83CBBA591A601E9000E9B192 /* RCTRedBox.m */, 830A229C1A66C68A008503DA /* RCTRootView.h */, 830A229D1A66C68A008503DA /* RCTRootView.m */, 83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */, @@ -493,7 +501,6 @@ 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */, 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */, 83CBBA501A601E3B00E9B192 /* RCTUtils.m */, - 1482F9E61B55B927000ADFF3 /* RCTBridgeDelegate.h */, ); path = Base; sourceTree = ""; @@ -596,12 +603,12 @@ 131B6AF41AF1093D00FFC3E0 /* RCTSegmentedControl.m in Sources */, 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, - 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, 13A0C28A1B74F71200B29F6F /* RCTDevMenu.m in Sources */, 14C2CA711B3AC63800E6CBB2 /* RCTModuleMethod.m in Sources */, 13CC8A821B17642100940AE7 /* RCTBorderDrawing.m in Sources */, 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */, 13AF20451AE707F9005F5298 /* RCTSlider.m in Sources */, + 8385CF351B8B77CD00C6273E /* RCTKeyboardObserver.m in Sources */, 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, 832348161A77A5AA00B55238 /* Layout.c in Sources */, 14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */, @@ -649,6 +656,7 @@ 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */, 63F014C01B02080B003B75D2 /* RCTPointAnnotation.m in Sources */, + 13F17A851B8493E5007D4C75 /* RCTRedBox.m in Sources */, 83392EB31B6634E10013B15F /* RCTModalHostViewController.m in Sources */, 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */, 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */, diff --git a/React/Views/RCTActivityIndicatorViewManager.m b/React/Views/RCTActivityIndicatorViewManager.m index 3876400df..2a7cadf35 100644 --- a/React/Views/RCTActivityIndicatorViewManager.m +++ b/React/Views/RCTActivityIndicatorViewManager.m @@ -29,7 +29,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - return [[UIActivityIndicatorView alloc] init]; + return [UIActivityIndicatorView new]; } RCT_EXPORT_VIEW_PROPERTY(color, UIColor) diff --git a/React/Views/RCTComponentData.h b/React/Views/RCTComponentData.h index 20a54ad7b..d510956cd 100644 --- a/React/Views/RCTComponentData.h +++ b/React/Views/RCTComponentData.h @@ -22,7 +22,7 @@ - (instancetype)initWithManager:(RCTViewManager *)manager NS_DESIGNATED_INITIALIZER; -- (id)createViewWithTag:(NSNumber *)tag; +- (id)createViewWithTag:(NSNumber *)tag props:(NSDictionary *)props; - (RCTShadowView *)createShadowViewWithTag:(NSNumber *)tag; - (void)setProps:(NSDictionary *)props forView:(id)view; - (void)setProps:(NSDictionary *)props forShadowView:(RCTShadowView *)shadowView; diff --git a/React/Views/RCTComponentData.m b/React/Views/RCTComponentData.m index 2f3aa8458..b01b0268e 100644 --- a/React/Views/RCTComponentData.m +++ b/React/Views/RCTComponentData.m @@ -48,8 +48,8 @@ typedef void (^RCTPropBlock)(id view, id json); { if ((self = [super init])) { _manager = manager; - _viewPropBlocks = [[NSMutableDictionary alloc] init]; - _shadowPropBlocks = [[NSMutableDictionary alloc] init]; + _viewPropBlocks = [NSMutableDictionary new]; + _shadowPropBlocks = [NSMutableDictionary new]; _name = RCTBridgeModuleNameForClass([manager class]); RCTAssert(_name.length, @"Invalid moduleName '%@'", _name); @@ -60,13 +60,13 @@ typedef void (^RCTPropBlock)(id view, id json); return self; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) -- (id)createViewWithTag:(NSNumber *)tag +- (id)createViewWithTag:(NSNumber *)tag props:(NSDictionary *)props { RCTAssertMainThread(); - id view = (id)[_manager view]; + id view = (id)(props ? [_manager viewWithProps:props] : [_manager view]); view.reactTag = tag; if ([view isKindOfClass:[UIView class]]) { ((UIView *)view).multipleTouchEnabled = YES; @@ -126,7 +126,7 @@ RCT_NOT_IMPLEMENTED(-init) NSString *key = name; NSArray *parts = [keyPath componentsSeparatedByString:@"."]; if (parts) { - key = [parts lastObject]; + key = parts.lastObject; parts = [parts subarrayWithRange:(NSRange){0, parts.count - 1}]; } @@ -135,7 +135,7 @@ RCT_NOT_IMPLEMENTED(-init) // Get property setter SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", - [[key substringToIndex:1] uppercaseString], + [key substringToIndex:1].uppercaseString, [key substringFromIndex:1]]); // Build setter block @@ -176,8 +176,8 @@ RCT_NOT_IMPLEMENTED(-init) default: { NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature]; - [typeInvocation setSelector:type]; - [typeInvocation setTarget:[RCTConvert class]]; + typeInvocation.selector = type; + typeInvocation.target = [RCTConvert class]; __block NSInvocation *sourceInvocation = nil; __block NSInvocation *targetInvocation = nil; @@ -194,7 +194,7 @@ RCT_NOT_IMPLEMENTED(-init) if (!sourceInvocation && source) { NSMethodSignature *signature = [source methodSignatureForSelector:getter]; sourceInvocation = [NSInvocation invocationWithMethodSignature:signature]; - [sourceInvocation setSelector:getter]; + sourceInvocation.selector = getter; } [sourceInvocation invokeWithTarget:source]; [sourceInvocation getReturnValue:value]; @@ -204,7 +204,7 @@ RCT_NOT_IMPLEMENTED(-init) if (!targetInvocation && target) { NSMethodSignature *signature = [target methodSignatureForSelector:setter]; targetInvocation = [NSInvocation invocationWithMethodSignature:signature]; - [targetInvocation setSelector:setter]; + targetInvocation.selector = setter; } [targetInvocation setArgument:value atIndex:2]; [targetInvocation invokeWithTarget:target]; @@ -264,7 +264,7 @@ RCT_NOT_IMPLEMENTED(-init) } if (!_defaultView) { - _defaultView = [self createViewWithTag:nil]; + _defaultView = [self createViewWithTag:nil props:nil]; } [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id json, __unused BOOL *stop) { @@ -292,7 +292,7 @@ RCT_NOT_IMPLEMENTED(-init) - (NSDictionary *)viewConfig { Class managerClass = [_manager class]; - NSMutableDictionary *propTypes = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *propTypes = [NSMutableDictionary new]; unsigned int count = 0; Method *methods = class_copyMethodList(object_getClass(managerClass), &count); diff --git a/React/Views/RCTConvert+MapKit.m b/React/Views/RCTConvert+MapKit.m index a1ba0ac98..9206c3a40 100644 --- a/React/Views/RCTConvert+MapKit.m +++ b/React/Views/RCTConvert+MapKit.m @@ -35,7 +35,7 @@ json = [self NSDictionary:json]; // TODO: more shape types - MKShape *shape = [[MKPointAnnotation alloc] init]; + MKShape *shape = [MKPointAnnotation new]; shape.coordinate = [self CLLocationCoordinate2D:json]; shape.title = [RCTConvert NSString:json[@"title"]]; shape.subtitle = [RCTConvert NSString:json[@"subtitle"]]; @@ -53,7 +53,7 @@ RCT_ENUM_CONVERTER(MKMapType, (@{ + (RCTPointAnnotation *)RCTPointAnnotation:(id)json { json = [self NSDictionary:json]; - RCTPointAnnotation *shape = [[RCTPointAnnotation alloc] init]; + RCTPointAnnotation *shape = [RCTPointAnnotation new]; shape.coordinate = [self CLLocationCoordinate2D:json]; shape.title = [RCTConvert NSString:json[@"title"]]; shape.subtitle = [RCTConvert NSString:json[@"subtitle"]]; diff --git a/React/Views/RCTDatePickerManager.m b/React/Views/RCTDatePickerManager.m index a5d63786f..f2bef39e3 100644 --- a/React/Views/RCTDatePickerManager.m +++ b/React/Views/RCTDatePickerManager.m @@ -34,7 +34,7 @@ RCT_EXPORT_MODULE() // while the UIDatePicker is still sending onChange events. To // fix this we should maybe subclass UIDatePicker and make it // be its own event target. - UIDatePicker *picker = [[UIDatePicker alloc] init]; + UIDatePicker *picker = [UIDatePicker new]; [picker addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged]; return picker; @@ -51,14 +51,14 @@ RCT_REMAP_VIEW_PROPERTY(timeZoneOffsetInMinutes, timeZone, NSTimeZone) { NSDictionary *event = @{ @"target": sender.reactTag, - @"timestamp": @([sender.date timeIntervalSince1970] * 1000.0) + @"timestamp": @(sender.date.timeIntervalSince1970 * 1000.0) }; [self.bridge.eventDispatcher sendInputEventWithName:@"change" body:event]; } - (NSDictionary *)constantsToExport { - UIDatePicker *view = [[UIDatePicker alloc] init]; + UIDatePicker *view = [UIDatePicker new]; return @{ @"ComponentHeight": @(view.intrinsicContentSize.height), @"ComponentWidth": @(view.intrinsicContentSize.width), diff --git a/React/Views/RCTMap.m b/React/Views/RCTMap.m index d51af5a01..16208957b 100644 --- a/React/Views/RCTMap.m +++ b/React/Views/RCTMap.m @@ -80,7 +80,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; { if (self.showsUserLocation != showsUserLocation) { if (showsUserLocation && !_locationManager) { - _locationManager = [[CLLocationManager alloc] init]; + _locationManager = [CLLocationManager new]; if ([_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) { [_locationManager requestWhenInUseAuthorization]; } @@ -114,9 +114,9 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; - (void)setAnnotations:(RCTPointAnnotationArray *)annotations { - NSMutableArray *newAnnotationIds = [[NSMutableArray alloc] init]; - NSMutableArray *annotationsToDelete = [[NSMutableArray alloc] init]; - NSMutableArray *annotationsToAdd = [[NSMutableArray alloc] init]; + NSMutableArray *newAnnotationIds = [NSMutableArray new]; + NSMutableArray *annotationsToDelete = [NSMutableArray new]; + NSMutableArray *annotationsToAdd = [NSMutableArray new]; for (RCTPointAnnotation *annotation in annotations) { if (![annotation isKindOfClass:[RCTPointAnnotation class]]) { @@ -150,7 +150,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01; [self addAnnotations:annotationsToAdd]; } - NSMutableArray *newIds = [[NSMutableArray alloc] init]; + NSMutableArray *newIds = [NSMutableArray new]; for (RCTPointAnnotation *anno in self.annotations) { if ([anno isKindOfClass:[MKUserLocation class]]) { continue; diff --git a/React/Views/RCTMapManager.m b/React/Views/RCTMapManager.m index 032da4d7a..130446df0 100644 --- a/React/Views/RCTMapManager.m +++ b/React/Views/RCTMapManager.m @@ -31,7 +31,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - RCTMap *map = [[RCTMap alloc] init]; + RCTMap *map = [RCTMap new]; map.delegate = self; return map; } diff --git a/React/Views/RCTModalHostView.h b/React/Views/RCTModalHostView.h index a39df2815..cafe771c8 100644 --- a/React/Views/RCTModalHostView.h +++ b/React/Views/RCTModalHostView.h @@ -9,11 +9,14 @@ #import +#import "RCTInvalidating.h" + @class RCTBridge; -@interface RCTModalHostView : UIView +@interface RCTModalHostView : UIView @property (nonatomic, assign, getter=isAnimated) BOOL animated; +@property (nonatomic, assign, getter=isTransparent) BOOL transparent; - (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; diff --git a/React/Views/RCTModalHostView.m b/React/Views/RCTModalHostView.m index dd16f9e6c..6ccbf3261 100644 --- a/React/Views/RCTModalHostView.m +++ b/React/Views/RCTModalHostView.m @@ -24,14 +24,14 @@ RCTTouchHandler *_touchHandler; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:coder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:coder) - (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super initWithFrame:CGRectZero])) { _bridge = bridge; - _modalViewController = [[RCTModalHostViewController alloc] init]; + _modalViewController = [RCTModalHostViewController new]; _touchHandler = [[RCTTouchHandler alloc] initWithBridge:bridge]; __weak RCTModalHostView *weakSelf = self; @@ -74,10 +74,28 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:coder) [super didMoveToSuperview]; if (self.superview) { + RCTAssert(self.reactViewController, @"Can't present modal view controller without a presenting view controller"); [self.reactViewController presentViewController:_modalViewController animated:self.animated completion:nil]; } else { [_modalViewController dismissViewControllerAnimated:self.animated completion:nil]; } } +- (void)invalidate +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [_modalViewController dismissViewControllerAnimated:self.animated completion:nil]; + }); +} + +- (BOOL)isTransparent +{ + return _modalViewController.modalPresentationStyle == UIModalPresentationCustom; +} + +- (void)setTransparent:(BOOL)transparent +{ + _modalViewController.modalPresentationStyle = transparent ? UIModalPresentationCustom : UIModalPresentationFullScreen; +} + @end diff --git a/React/Views/RCTModalHostViewManager.h b/React/Views/RCTModalHostViewManager.h index e47794542..fb5972d34 100644 --- a/React/Views/RCTModalHostViewManager.h +++ b/React/Views/RCTModalHostViewManager.h @@ -9,6 +9,8 @@ #import "RCTViewManager.h" -@interface RCTModalHostViewManager : RCTViewManager +#import "RCTInvalidating.h" + +@interface RCTModalHostViewManager : RCTViewManager @end diff --git a/React/Views/RCTModalHostViewManager.m b/React/Views/RCTModalHostViewManager.m index 458e65081..3ad022b21 100644 --- a/React/Views/RCTModalHostViewManager.m +++ b/React/Views/RCTModalHostViewManager.m @@ -14,14 +14,37 @@ #import "RCTTouchHandler.h" @implementation RCTModalHostViewManager +{ + NSHashTable *_hostViews; +} RCT_EXPORT_MODULE() +- (instancetype)init +{ + if ((self = [super init])) { + _hostViews = [NSHashTable weakObjectsHashTable]; + } + + return self; +} + - (UIView *)view { - return [[RCTModalHostView alloc] initWithBridge:self.bridge]; + UIView *view = [[RCTModalHostView alloc] initWithBridge:self.bridge]; + [_hostViews addObject:view]; + return view; +} + +- (void)invalidate +{ + for (RCTModalHostView *hostView in _hostViews) { + [hostView invalidate]; + } + [_hostViews removeAllObjects]; } RCT_EXPORT_VIEW_PROPERTY(animated, BOOL) +RCT_EXPORT_VIEW_PROPERTY(transparent, BOOL) @end diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index 8350af2e9..f785934af 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -18,7 +18,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - return [[RCTNavItem alloc] init]; + return [RCTNavItem new]; } RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL) diff --git a/React/Views/RCTNavigator.h b/React/Views/RCTNavigator.h index c59c9a3d3..a1905a87d 100644 --- a/React/Views/RCTNavigator.h +++ b/React/Views/RCTNavigator.h @@ -10,7 +10,6 @@ #import #import "RCTFrameUpdate.h" -#import "RCTInvalidating.h" @class RCTBridge; diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index db9087b85..fcbc14225 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -293,8 +293,8 @@ NSInteger kNeverProgressed = -10000; return self; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)didUpdateFrame:(__unused RCTFrameUpdate *)update { @@ -501,13 +501,13 @@ BOOL jsGettingtooSlow = } if (jsGettingAhead) { if (reactPushOne) { - UIView *lastView = [_currentViews lastObject]; + UIView *lastView = _currentViews.lastObject; RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView eventDispatcher:_bridge.eventDispatcher]; vc.navigationListener = self; _numberOfViewControllerMovesToIgnore = 1; [_navigationController pushViewController:vc animated:(currentReactCount > 1)]; } else if (reactPopN) { - UIViewController *viewControllerToPopTo = [[_navigationController viewControllers] objectAtIndex:(currentReactCount - 1)]; + UIViewController *viewControllerToPopTo = _navigationController.viewControllers[(currentReactCount - 1)]; _numberOfViewControllerMovesToIgnore = viewControllerCount - currentReactCount; [_navigationController popToViewController:viewControllerToPopTo animated:YES]; } else { diff --git a/React/Views/RCTPicker.m b/React/Views/RCTPicker.m index 4134eae66..09c7247d2 100644 --- a/React/Views/RCTPicker.m +++ b/React/Views/RCTPicker.m @@ -39,8 +39,8 @@ const NSInteger UNINITIALIZED_INDEX = -1; return self; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)setItems:(NSArray *)items { diff --git a/React/Views/RCTPickerManager.m b/React/Views/RCTPickerManager.m index 8e31e5fe9..6de76bcb7 100644 --- a/React/Views/RCTPickerManager.m +++ b/React/Views/RCTPickerManager.m @@ -27,7 +27,7 @@ RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSInteger) - (NSDictionary *)constantsToExport { - UIPickerView *view = [[UIPickerView alloc] init]; + UIPickerView *view = [UIPickerView new]; return @{ @"ComponentHeight": @(view.intrinsicContentSize.height), @"ComponentWidth": @(view.intrinsicContentSize.width) diff --git a/React/Views/RCTProgressViewManager.m b/React/Views/RCTProgressViewManager.m index deb6285a6..0cd9049d3 100644 --- a/React/Views/RCTProgressViewManager.m +++ b/React/Views/RCTProgressViewManager.m @@ -26,7 +26,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - return [[UIProgressView alloc] init]; + return [UIProgressView new]; } RCT_EXPORT_VIEW_PROPERTY(progressViewStyle, UIProgressViewStyle) @@ -38,7 +38,7 @@ RCT_EXPORT_VIEW_PROPERTY(trackImage, UIImage) - (NSDictionary *)constantsToExport { - UIProgressView *view = [[UIProgressView alloc] init]; + UIProgressView *view = [UIProgressView new]; return @{ @"ComponentHeight": @(view.intrinsicContentSize.height), }; diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index 98dec5271..8c649da56 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -16,6 +16,7 @@ #import "RCTLog.h" #import "RCTUIManager.h" #import "RCTUtils.h" +#import "UIView+Private.h" #import "UIView+React.h" CGFloat const ZINDEX_DEFAULT = 0; @@ -55,7 +56,7 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; return self; } -RCT_NOT_IMPLEMENTED(-init) +RCT_NOT_IMPLEMENTED(- (instancetype)init) - (uint16_t)coalescingKey { @@ -265,7 +266,7 @@ RCT_NOT_IMPLEMENTED(-init) contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0; } } - [super setContentOffset:contentOffset]; + super.contentOffset = contentOffset; } - (void)dockClosestSectionHeader @@ -353,10 +354,6 @@ RCT_NOT_IMPLEMENTED(-init) @end -@interface RCTScrollView (Private) -- (NSArray *)calculateChildFramesData; -@end - @implementation RCTScrollView { RCTEventDispatcher *_eventDispatcher; @@ -365,6 +362,7 @@ RCT_NOT_IMPLEMENTED(-init) NSTimeInterval _lastScrollDispatchTime; NSMutableArray *_cachedChildFrames; BOOL _allowNextScrollNoMatterWhat; + CGRect _lastClippedToRect; } @synthesize nativeMainScrollDelegate = _nativeMainScrollDelegate; @@ -374,7 +372,6 @@ RCT_NOT_IMPLEMENTED(-init) RCTAssertParam(eventDispatcher); if ((self = [super initWithFrame:CGRectZero])) { - _eventDispatcher = eventDispatcher; _scrollView = [[RCTCustomScrollView alloc] initWithFrame:CGRectZero]; _scrollView.delegate = self; @@ -382,18 +379,19 @@ RCT_NOT_IMPLEMENTED(-init) _automaticallyAdjustContentInsets = YES; _contentInset = UIEdgeInsetsZero; _contentSize = CGSizeZero; + _lastClippedToRect = CGRectNull; _scrollEventThrottle = 0.0; _lastScrollDispatchTime = CACurrentMediaTime(); - _cachedChildFrames = [[NSMutableArray alloc] init]; + _cachedChildFrames = [NSMutableArray new]; [self addSubview:_scrollView]; } return self; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews { @@ -443,8 +441,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) - (void)setClipsToBounds:(BOOL)clipsToBounds { - [super setClipsToBounds:clipsToBounds]; - [_scrollView setClipsToBounds:clipsToBounds]; + super.clipsToBounds = clipsToBounds; + _scrollView.clipsToBounds = clipsToBounds; } - (void)dealloc @@ -469,6 +467,33 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) [self updateClippedSubviews]; } +- (void)updateClippedSubviews +{ + // Find a suitable view to use for clipping + UIView *clipView = [self react_findClipView]; + if (!clipView) { + return; + } + + static const CGFloat leeway = 50.0; + + const CGSize contentSize = _scrollView.contentSize; + const CGRect bounds = _scrollView.bounds; + const BOOL scrollsHorizontally = contentSize.width > bounds.size.width; + const BOOL scrollsVertically = contentSize.height > bounds.size.height; + + const BOOL shouldClipAgain = + CGRectIsNull(_lastClippedToRect) || + (scrollsHorizontally && (bounds.size.width < leeway || fabs(_lastClippedToRect.origin.x - bounds.origin.x) >= leeway)) || + (scrollsVertically && (bounds.size.height < leeway || fabs(_lastClippedToRect.origin.y - bounds.origin.y) >= leeway)); + + if (shouldClipAgain) { + const CGRect clipRect = CGRectInset(clipView.bounds, -leeway, -leeway); + [self react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + _lastClippedToRect = bounds; + } +} + - (void)setContentInset:(UIEdgeInsets)contentInset { CGPoint contentOffset = _scrollView.contentOffset; @@ -553,7 +578,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) - (NSArray *)calculateChildFramesData { - NSMutableArray *updatedChildFrames = [[NSMutableArray alloc] init]; + NSMutableArray *updatedChildFrames = [NSMutableArray new]; [[_contentView reactSubviews] enumerateObjectsUsingBlock: ^(UIView *subview, NSUInteger idx, __unused BOOL *stop) { diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index 668ea26a3..71736a344 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -15,7 +15,9 @@ #import "RCTUIManager.h" @interface RCTScrollView (Private) + - (NSArray *)calculateChildFramesData; + @end @implementation RCTConvert (UIScrollView) diff --git a/React/Views/RCTSegmentedControl.m b/React/Views/RCTSegmentedControl.m index 28160808f..1b28bacc7 100644 --- a/React/Views/RCTSegmentedControl.m +++ b/React/Views/RCTSegmentedControl.m @@ -18,7 +18,7 @@ RCTEventDispatcher *_eventDispatcher; } -- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = eventDispatcher; diff --git a/React/Views/RCTSegmentedControlManager.m b/React/Views/RCTSegmentedControlManager.m index d7e1156ff..1c5621e7a 100644 --- a/React/Views/RCTSegmentedControlManager.m +++ b/React/Views/RCTSegmentedControlManager.m @@ -30,7 +30,7 @@ RCT_EXPORT_VIEW_PROPERTY(enabled, BOOL) - (NSDictionary *)constantsToExport { - RCTSegmentedControl *view = [[RCTSegmentedControl alloc] init]; + RCTSegmentedControl *view = [RCTSegmentedControl new]; return @{ @"ComponentHeight": @(view.intrinsicContentSize.height), }; diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 4defde806..f2870e22a 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -20,7 +20,7 @@ typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf); const NSString *const RCTBackgroundColorProp = @"backgroundColor"; -typedef enum { +typedef NS_ENUM(unsigned int, meta_prop_t) { META_PROP_LEFT, META_PROP_TOP, META_PROP_RIGHT, @@ -29,7 +29,7 @@ typedef enum { META_PROP_VERTICAL, META_PROP_ALL, META_PROP_COUNT, -} meta_prop_t; +}; @implementation RCTShadowView { @@ -133,13 +133,13 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st _layoutLifecycle = RCTUpdateLifecycleComputed; CGPoint absoluteTopLeft = { - RCTRoundPixelValue(absolutePosition.x + node->layout.position[CSS_LEFT]), - RCTRoundPixelValue(absolutePosition.y + node->layout.position[CSS_TOP]) + absolutePosition.x + node->layout.position[CSS_LEFT], + absolutePosition.y + node->layout.position[CSS_TOP] }; CGPoint absoluteBottomRight = { - RCTRoundPixelValue(absolutePosition.x + node->layout.position[CSS_LEFT] + node->layout.dimensions[CSS_WIDTH]), - RCTRoundPixelValue(absolutePosition.y + node->layout.position[CSS_TOP] + node->layout.dimensions[CSS_HEIGHT]) + absolutePosition.x + node->layout.position[CSS_LEFT] + node->layout.dimensions[CSS_WIDTH], + absolutePosition.y + node->layout.position[CSS_TOP] + node->layout.dimensions[CSS_HEIGHT] }; CGRect frame = {{ @@ -246,7 +246,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st _frame = CGRectMake(0, 0, CSS_UNDEFINED, CSS_UNDEFINED); - for (int ii = 0; ii < META_PROP_COUNT; ii++) { + for (unsigned int ii = 0; ii < META_PROP_COUNT; ii++) { _paddingMetaProps[ii] = CSS_UNDEFINED; _marginMetaProps[ii] = CSS_UNDEFINED; _borderMetaProps[ii] = CSS_UNDEFINED; @@ -326,7 +326,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st - (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex { [_reactSubviews insertObject:subview atIndex:atIndex]; - _cssNode->children_count = (int)[_reactSubviews count]; + _cssNode->children_count = (int)_reactSubviews.count; subview->_superview = self; [self dirtyText]; [self dirtyLayout]; @@ -340,7 +340,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st [subview dirtyPropagation]; subview->_superview = nil; [_reactSubviews removeObject:subview]; - _cssNode->children_count = (int)[_reactSubviews count]; + _cssNode->children_count = (int)_reactSubviews.count; } - (NSArray *)reactSubviews diff --git a/React/Views/RCTSliderManager.m b/React/Views/RCTSliderManager.m index 100c2b4c3..9ce7699d1 100644 --- a/React/Views/RCTSliderManager.m +++ b/React/Views/RCTSliderManager.m @@ -20,7 +20,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - RCTSlider *slider = [[RCTSlider alloc] init]; + RCTSlider *slider = [RCTSlider new]; [slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged]; [slider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:UIControlEventTouchUpInside]; [slider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:UIControlEventTouchUpOutside]; diff --git a/React/Views/RCTSwitchManager.m b/React/Views/RCTSwitchManager.m index 7b39afa89..42a54210c 100644 --- a/React/Views/RCTSwitchManager.m +++ b/React/Views/RCTSwitchManager.m @@ -20,7 +20,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - RCTSwitch *switcher = [[RCTSwitch alloc] init]; + RCTSwitch *switcher = [RCTSwitch new]; [switcher addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged]; diff --git a/React/Views/RCTTabBar.m b/React/Views/RCTTabBar.m index 4f14a943f..75e017a98 100644 --- a/React/Views/RCTTabBar.m +++ b/React/Views/RCTTabBar.m @@ -36,16 +36,16 @@ if ((self = [super initWithFrame:CGRectZero])) { _eventDispatcher = eventDispatcher; - _tabViews = [[NSMutableArray alloc] init]; - _tabController = [[UITabBarController alloc] init]; + _tabViews = [NSMutableArray new]; + _tabController = [UITabBarController new]; _tabController.delegate = self; [self addSubview:_tabController.view]; } return self; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (UIViewController *)reactViewController { diff --git a/React/Views/RCTTabBarItem.m b/React/Views/RCTTabBarItem.m index 24a22cf5f..3695f24a7 100644 --- a/React/Views/RCTTabBarItem.m +++ b/React/Views/RCTTabBarItem.m @@ -20,7 +20,7 @@ - (UITabBarItem *)barItem { if (!_barItem) { - _barItem = [[UITabBarItem alloc] init]; + _barItem = [UITabBarItem new]; } return _barItem; } @@ -72,7 +72,7 @@ RCTLogError(@"The tab bar icon '%@' did not match any known image or system icon", icon); return; } - _barItem = [[UITabBarItem alloc] initWithTabBarSystemItem:[systemIcon integerValue] tag:oldItem.tag]; + _barItem = [[UITabBarItem alloc] initWithTabBarSystemItem:systemIcon.integerValue tag:oldItem.tag]; } // Reapply previous properties diff --git a/React/Views/RCTTabBarItemManager.m b/React/Views/RCTTabBarItemManager.m index d1a96ef2a..f91153efd 100644 --- a/React/Views/RCTTabBarItemManager.m +++ b/React/Views/RCTTabBarItemManager.m @@ -18,7 +18,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - return [[RCTTabBarItem alloc] init]; + return [RCTTabBarItem new]; } RCT_EXPORT_VIEW_PROPERTY(selected, BOOL); @@ -28,7 +28,7 @@ RCT_REMAP_VIEW_PROPERTY(badge, barItem.badgeValue, NSString); RCT_CUSTOM_VIEW_PROPERTY(title, NSString, RCTTabBarItem) { view.barItem.title = json ? [RCTConvert NSString:json] : defaultView.barItem.title; - view.barItem.imageInsets = [view.barItem.title length] ? UIEdgeInsetsZero : (UIEdgeInsets){6, 0, -6, 0}; + view.barItem.imageInsets = view.barItem.title.length ? UIEdgeInsetsZero : (UIEdgeInsets){6, 0, -6, 0}; } @end diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index af1f7842e..d242faa44 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -49,7 +49,7 @@ static UIView *RCTViewHitTest(UIView *view, CGPoint point, UIEvent *event) // we do support clipsToBounds, so if that's enabled // we'll update the clipping - if (self.clipsToBounds && [self.subviews count] > 0) { + if (self.clipsToBounds && self.subviews.count > 0) { clipRect = [clipView convertRect:clipRect toView:self]; clipRect = CGRectIntersection(clipRect, self.bounds); clipView = self; @@ -93,7 +93,7 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) { NSMutableString *str = [NSMutableString stringWithString:@""]; for (UIView *subview in view.subviews) { - NSString *label = [subview accessibilityLabel]; + NSString *label = subview.accessibilityLabel; if (label) { [str appendString:@" "]; [str appendString:label]; @@ -123,13 +123,13 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) _borderBottomLeftRadius = -1; _borderBottomRightRadius = -1; - _backgroundColor = [super backgroundColor]; + _backgroundColor = super.backgroundColor; } return self; } -RCT_NOT_IMPLEMENTED(-initWithCoder:unused) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) - (NSString *)accessibilityLabel { @@ -210,8 +210,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:unused) baseInset.left += autoInset.left; baseInset.right += autoInset.right; } - [scrollView setContentInset:baseInset]; - [scrollView setScrollIndicatorInsets:baseInset]; + scrollView.contentInset = baseInset; + scrollView.scrollIndicatorInsets = baseInset; if (updateOffset) { // If we're adjusting the top inset, then let's also adjust the contentOffset so that the view @@ -333,7 +333,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:unused) return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; } - if ([_reactSubviews count] == 0) { + if (_reactSubviews.count == 0) { // Do nothing if we have no subviews return; } @@ -405,7 +405,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:unused) // offscreen views. If _reactSubviews is nil, we can assume // that [self reactSubviews] and [self subviews] are the same - return _reactSubviews ?: [self subviews]; + return _reactSubviews ?: self.subviews; } - (void)updateClippedSubviews diff --git a/React/Views/RCTViewManager.h b/React/Views/RCTViewManager.h index 9e3316415..f3c370b94 100644 --- a/React/Views/RCTViewManager.h +++ b/React/Views/RCTViewManager.h @@ -39,6 +39,15 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v */ - (UIView *)view; +/** + * This method instantiates a native view using the props passed into the component. + * This method should be used when you need to know about specific props in order to + * initialize a view. By default, this just calls the -view method. Each prop will + * still be set individually, after the view is created. Like the -view method, + * -viewWithProps: should return a fresh instance each time it is called. + */ +- (UIView *)viewWithProps:(NSDictionary *)props; + /** * This method instantiates a shadow view to be managed by the module. If omitted, * an ordinary RCTShadowView instance will be created, which is typically fine for diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index e6feb5ca9..b4562f689 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -51,17 +51,22 @@ RCT_EXPORT_MODULE() - (dispatch_queue_t)methodQueue { - return [_bridge.uiManager methodQueue]; + return _bridge.uiManager.methodQueue; +} + +- (UIView *)viewWithProps:(NSDictionary *)props +{ + return [self view]; } - (UIView *)view { - return [[RCTView alloc] init]; + return [RCTView new]; } - (RCTShadowView *)shadowView { - return [[RCTShadowView alloc] init]; + return [RCTShadowView new]; } - (NSArray *)customBubblingEventTypes @@ -126,7 +131,7 @@ RCT_REMAP_VIEW_PROPERTY(overflow, clipsToBounds, css_clip_t) RCT_CUSTOM_VIEW_PROPERTY(shouldRasterizeIOS, BOOL, RCTView) { view.layer.shouldRasterize = json ? [RCTConvert BOOL:json] : defaultView.layer.shouldRasterize; - view.layer.rasterizationScale = view.layer.shouldRasterize ? view.window.screen.scale : defaultView.layer.rasterizationScale; + view.layer.rasterizationScale = view.layer.shouldRasterize ? [UIScreen mainScreen].scale : defaultView.layer.rasterizationScale; } RCT_CUSTOM_VIEW_PROPERTY(transformMatrix, CATransform3D, RCTView) { diff --git a/React/Views/RCTWebView.m b/React/Views/RCTWebView.m index 0a9e6a8d8..196284d24 100644 --- a/React/Views/RCTWebView.m +++ b/React/Views/RCTWebView.m @@ -47,8 +47,8 @@ NSString *const RCTJSNavigationScheme = @"react-js-navigation"; return self; } -RCT_NOT_IMPLEMENTED(-initWithFrame:(CGRect)frame) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)goForward { @@ -127,11 +127,11 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) NSString *title = [_webView stringByEvaluatingJavaScriptFromString:@"document.title"]; NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary: @{ @"target": self.reactTag, - @"url": url ? [url absoluteString] : @"", + @"url": url ? url.absoluteString : @"", @"loading" : @(_webView.loading), @"title": title, - @"canGoBack": @([_webView canGoBack]), - @"canGoForward" : @([_webView canGoForward]), + @"canGoBack": @(_webView.canGoBack), + @"canGoForward" : @(_webView.canGoForward), }]; return event; @@ -148,7 +148,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) if (isTopFrame) { NSMutableDictionary *event = [self baseEvent]; [event addEntriesFromDictionary: @{ - @"url": [request.URL absoluteString], + @"url": (request.URL).absoluteString, @"navigationType": @(navigationType) }]; [_eventDispatcher sendInputEventWithName:@"loadingStart" body:event]; @@ -172,7 +172,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) [event addEntriesFromDictionary: @{ @"domain": error.domain, @"code": @(error.code), - @"description": [error localizedDescription], + @"description": error.localizedDescription, }]; [_eventDispatcher sendInputEventWithName:@"loadingError" body:event]; } diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m index 6cd6e6502..1fd2cfe3f 100644 --- a/React/Views/RCTWebViewManager.m +++ b/React/Views/RCTWebViewManager.m @@ -62,8 +62,9 @@ RCT_EXPORT_METHOD(goBack:(nonnull NSNumber *)reactTag) RCTWebView *view = viewRegistry[reactTag]; if (![view isKindOfClass:[RCTWebView class]]) { RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); + } else { + [view goBack]; } - [view goBack]; }]; } @@ -73,8 +74,9 @@ RCT_EXPORT_METHOD(goForward:(nonnull NSNumber *)reactTag) id view = viewRegistry[reactTag]; if (![view isKindOfClass:[RCTWebView class]]) { RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); + } else { + [view goForward]; } - [view goForward]; }]; } @@ -84,9 +86,10 @@ RCT_EXPORT_METHOD(reload:(nonnull NSNumber *)reactTag) [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTWebView *view = viewRegistry[reactTag]; if (![view isKindOfClass:[RCTWebView class]]) { - RCTLogMustFix(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); + RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view); + } else { + [view reload]; } - [view reload]; }]; } diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index 169c1a7db..00af3b53d 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -52,8 +52,8 @@ return self; } -RCT_NOT_IMPLEMENTED(-initWithNibName:(NSString *)nn bundle:(NSBundle *)nb) -RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithNibName:(NSString *)nn bundle:(NSBundle *)nb) +RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) - (void)viewWillLayoutSubviews { diff --git a/packager/react-packager/example_project/bar.js b/React/Views/UIView+Private.h similarity index 56% rename from packager/react-packager/example_project/bar.js rename to React/Views/UIView+Private.h index 6653bdf7c..14e6fcd2b 100644 --- a/packager/react-packager/example_project/bar.js +++ b/React/Views/UIView+Private.h @@ -5,8 +5,14 @@ * 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 bar */ - module.exports = setInterval; +#import + +@interface UIView (RCTViewUnmounting) + +- (void)react_remountAllSubviews; +- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView; +- (UIView *)react_findClipView; + +@end diff --git a/local-cli/generator-ios/templates/app/AppDelegate.m b/local-cli/generator-ios/templates/app/AppDelegate.m index aa83b1423..49055c198 100644 --- a/local-cli/generator-ios/templates/app/AppDelegate.m +++ b/local-cli/generator-ios/templates/app/AppDelegate.m @@ -47,6 +47,7 @@ RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation moduleName:@"<%= name %>" + initialProperties:nil launchOptions:launchOptions]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; diff --git a/local-cli/generator-ios/templates/tests/Tests.m b/local-cli/generator-ios/templates/tests/Tests.m index 818c17fb5..0f4d1bfae 100644 --- a/local-cli/generator-ios/templates/tests/Tests.m +++ b/local-cli/generator-ios/templates/tests/Tests.m @@ -10,8 +10,7 @@ #import #import -#import "RCTAssert.h" -#import "RCTRedBox.h" +#import "RCTLog.h" #import "RCTRootView.h" #define TIMEOUT_SECONDS 240 @@ -23,7 +22,6 @@ @implementation <%= name %>Tests - - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test { if (test(view)) { @@ -37,18 +35,23 @@ return NO; } -- (void)testRendersWelcomeScreen { +- (void)testRendersWelcomeScreen +{ UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; BOOL foundElement = NO; - NSString *redboxError = nil; + + __block NSString *redboxError = nil; + RCTSetLogFunction(^(RCTLogLevel level, NSString *fileName, NSNumber *lineNumber, NSString *message) { + if (level >= RCTLogLevelError) { + redboxError = message; + } + }); while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; - foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { return YES; @@ -57,6 +60,8 @@ }]; } + RCTSetLogFunction(RCTDefaultLogFunction); + XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); } diff --git a/package.json b/package.json index db609d22e..0cd24e7f7 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "scripts": { "test": "jest", "lint": "node linter.js Examples/ Libraries/", - "start": "./packager/packager.sh" + "start": "./packager/packager.sh || true" }, "bin": { "react-native-start": "packager/packager.sh" @@ -49,16 +49,18 @@ "absolute-path": "0.0.0", "babel": "5.8.21", "babel-core": "5.8.21", + "bser": "1.0.0", "chalk": "1.0.0", "connect": "2.8.3", "debug": "2.1.0", - "graceful-fs": "3.0.6", + "graceful-fs": "4.1.2", "image-size": "0.3.5", "immutable": "^3.7.4", "joi": "5.1.0", "jstransform": "11.0.1", "module-deps": "3.5.6", "optimist": "0.6.1", + "progress": "^1.1.8", "promise": "^7.0.3", "react-timer-mixin": "^0.13.1", "react-tools": "git://github.com/facebook/react#b4e74e38e43ac53af8acd62c78c9213be0194245", diff --git a/packager/debugger.html b/packager/debugger.html index 8d377f640..24f8aea55 100644 --- a/packager/debugger.html +++ b/packager/debugger.html @@ -23,7 +23,7 @@ window.localStorage.removeItem('sessionID'); window.onbeforeunload = function() { if (sessionID) { return 'If you reload this page, it is going to break the debugging session. ' + - 'You should ⌘+R in the iOS simulator to reload.'; + 'You should press ⌘R in simulator to reload.'; } }; @@ -76,10 +76,10 @@ function connectToDebuggerProxy() { ws.onopen = function() { if (sessionID) { - setStatus('Debugger session #' + sessionID + ' active'); + setStatus('Debugger session #' + sessionID + ' active.'); ws.send(JSON.stringify({replyID: parseInt(sessionID, 10)})); } else { - setStatus('Waiting, press ⌘R in simulator to reload and connect'); + setStatus('Waiting, press ⌘R in simulator to reload and connect.'); } }; @@ -126,7 +126,8 @@ function loadScript(src, callback) { font-weight: 200; } .shortcut { - font-family: monospace; + font-family: "Monaco", monospace; + font-size: medium; color: #eee; background-color: #333; padding: 4px; @@ -175,10 +176,10 @@ function loadScript(src, callback) {

- React Native JS code runs inside this Chrome tab + React Native JS code runs inside this Chrome tab.

Press ⌘⌥J to open Developer Tools. Enable Pause On Caught Exceptions for a better debugging experience.

-

Status: Loading

+

Status: Loading...

diff --git a/packager/getFlowTypeCheckMiddleware.js b/packager/getFlowTypeCheckMiddleware.js index c7f3e2b1c..f34815512 100644 --- a/packager/getFlowTypeCheckMiddleware.js +++ b/packager/getFlowTypeCheckMiddleware.js @@ -10,17 +10,23 @@ var chalk = require('chalk'); var exec = require('child_process').exec; -var Activity = require('./react-packager/src/Activity'); +var url = require('url'); +var Activity = require('./react-packager').Activity; var hasWarned = {}; -var DISABLE_FLOW_CHECK = true; // temporarily disable while we figure out versioning issues. function getFlowTypeCheckMiddleware(options) { return function(req, res, next) { - var isBundle = req.url.indexOf('.bundle') !== -1; - if (DISABLE_FLOW_CHECK || options.skipflow || !isBundle) { + var reqObj = url.parse(req.url); + var isFlowCheck = (reqObj.path.match(/^\/flow\//)); + + if (!isFlowCheck) { return next(); } + if (options.skipflow) { + _endSkipFlow(res); + return; + } if (options.flowroot || options.projectRoots.length === 1) { var flowroot = options.flowroot || options.projectRoots[0]; } else { @@ -28,7 +34,8 @@ function getFlowTypeCheckMiddleware(options) { hasWarned.noRoot = true; console.warn('flow: No suitable root'); } - return next(); + _endFlowBad(res); + return; } exec('command -v flow >/dev/null 2>&1', function(error, stdout) { if (error) { @@ -37,7 +44,8 @@ function getFlowTypeCheckMiddleware(options) { console.warn(chalk.yellow('flow: Skipping because not installed. Install with ' + '`brew install flow`.')); } - return next(); + _endFlowBad(res); + return; } else { return doFlowTypecheck(res, flowroot, next); } @@ -51,7 +59,8 @@ function doFlowTypecheck(res, flowroot, next) { exec(flowCmd, function(flowError, stdout, stderr) { Activity.endEvent(eventId); if (!flowError) { - return next(); + _endFlowOk(res); + return; } else { try { var flowResponse = JSON.parse(stdout); @@ -73,16 +82,13 @@ function doFlowTypecheck(res, flowroot, next) { errorNum++; }); var error = { - status: 500, + status: 200, 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', }); @@ -93,6 +99,13 @@ function doFlowTypecheck(res, flowroot, next) { hasWarned.noConfig = true; console.warn(chalk.yellow('flow: ' + stderr)); } + _endFlowBad(res); + } else if (flowError.code === 3) { + if (!hasWarned.timeout) { + hasWarned.timeout = true; + console.warn(chalk.yellow('flow: ' + stdout)); + } + _endSkipFlow(res); } else { if (!hasWarned.brokenFlow) { hasWarned.brokenFlow = true; @@ -101,11 +114,37 @@ function doFlowTypecheck(res, flowroot, next) { '`.\n' + 'stderr: `' + stderr + '`' )); } + _endFlowBad(res); } - return next(); + return; } } }); } +function _endRes(res, message, code, silentError) { + res.writeHead(code, { + 'Content-Type': 'application/json; charset=UTF-8', + }); + res.end(JSON.stringify({ + message: message, + errors: [], + silentError: silentError, + })); +} + +function _endFlowOk(res) { + _endRes(res, 'No Flow Error', '200', true); +} + +function _endFlowBad(res) { + // we want to show that flow failed + // status 200 is need for the fetch to not be rejected + _endRes(res, 'Flow failed to run! Please look at the console for more details.', '200', false); +} + +function _endSkipFlow(res) { + _endRes(res, 'Flow was skipped, check the server options', '200', true); +} + module.exports = getFlowTypeCheckMiddleware; diff --git a/packager/react-packager/__mocks__/net.js b/packager/react-packager/__mocks__/net.js deleted file mode 100644 index 43f518289..000000000 --- a/packager/react-packager/__mocks__/net.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * 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. - */ - -var EventEmitter = require('events').EventEmitter; -var servers = {}; -exports.createServer = function(listener) { - var server = { - _listener: listener, - - socket: new EventEmitter(), - - listen: function(path) { - listener(this.socket); - servers[path] = this; - } - }; - - server.socket.setEncoding = function() {}; - server.socket.write = function(data) { - this.emit('data', data); - }; - - return server; -}; - -exports.connect = function(options) { - var server = servers[options.path || options.port]; - return server.socket; -}; diff --git a/packager/react-packager/example_project/config.json b/packager/react-packager/example_project/config.json deleted file mode 100644 index 0acdcb514..000000000 --- a/packager/react-packager/example_project/config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "port": 3000, - "devPort": 3001, - "publicDir": "./public", - "rootPath": "../example_project", - "moduleOptions": { - "format": "haste", - "main": "index.js" - } -} diff --git a/packager/react-packager/example_project/foo/foo.js b/packager/react-packager/example_project/foo/foo.js deleted file mode 100644 index fe3c8cd13..000000000 --- a/packager/react-packager/example_project/foo/foo.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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 foo - */ - - -var bar = require('bar'); - -class Logger { - log() { - console.log('youll have to change me lol'); - } -} - -class SecretLogger extends Logger { - log(secret) { - console.log('logging ', secret); - } -} - -module.exports = (secret) => { - if (secret !== 'secret') throw new Error('wrong secret'); - bar(new SecretLogger().log.bind(SecretLogger, secret), 400); -}; diff --git a/packager/react-packager/example_project/js/Channel.js b/packager/react-packager/example_project/js/Channel.js deleted file mode 100644 index 6cbfce6f9..000000000 --- a/packager/react-packager/example_project/js/Channel.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * 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 Channel - */ - -var XHR = require('XHR'); - -/** - * Client implementation of a server-push channel. - * - * @see Channel.js for full documentation - */ -var channel = null, at = null, delay = 0; -var Channel = {}; - -Channel.connect = function() { - var url = '/pull'; - if (channel) { - url += '?channel=' + channel + '&at=' + at; - } - XHR.get(url, function(err, xhr) { - if (err) { - delay = Math.min(Math.max(1000, delay * 2), 30000); - } else { - var res = xhr.responseText; - res = JSON.parse(res); - - delay = 0; - - // Cache channel state - channel = res.channel; - at = res.at; - - var messages = res.messages; - messages.forEach(function(message) { - var ev = document.createEvent('CustomEvent'); - ev.initCustomEvent(message.event, true, true, message.detail); - window.dispatchEvent(ev); - }); - } - - // Reconnect - setTimeout(Channel.connect, delay); - }); -}; - -module.exports = Channel; diff --git a/packager/react-packager/example_project/js/XHR.js b/packager/react-packager/example_project/js/XHR.js deleted file mode 100644 index bede8ca50..000000000 --- a/packager/react-packager/example_project/js/XHR.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * 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 XHR - */ - -function request(method, url, callback) { - var xhr = new XMLHttpRequest(); - xhr.open(method, url); - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - callback(null, xhr); - } else { - callback(new Error('status = ' + xhr.status, xhr)); - } - } - }; - xhr.send(); -} - -exports.get = function(url, callback) { - request('GET', url, callback); -}; diff --git a/packager/react-packager/example_project/js/code.js b/packager/react-packager/example_project/js/code.js deleted file mode 100644 index f99a90c9c..000000000 --- a/packager/react-packager/example_project/js/code.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 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 code - */ -var XHR = require('XHR'); - -var $ = function(sel) {return document.querySelector(sel);}; - -function getListItems(files) { - var items = []; - files.forEach(function(file) { - var displayName = file.name + (file.type == 1 ? '/' : ''); - items.push( - React.DOM.li({ - className: 'type' + file.type, - key: file.ino - }, displayName) - ); - if (file.type === 1) { - items.push(getListItems(file.nodes)); - } - }); - - return React.DOM.ol(null, items); -} - -var FileList = React.createClass({ - getInitialState: function() { - return {files: []}; - }, - - componentDidMount: function() { - XHR.get( - this.props.source, - function(err, xhr) { - if (err) {throw err;} - - var files = JSON.parse(xhr.responseText); - this.setState({files: files}); - }.bind(this) - ); - }, - - render: function() { - return getListItems(this.state.files); - } -}); - -window.addEventListener('load', function() { - React.render(React.createElement(FileList, {source: '/files'}), - $('#code')); -}); diff --git a/packager/react-packager/example_project/js/main.js b/packager/react-packager/example_project/js/main.js deleted file mode 100644 index 405d015e6..000000000 --- a/packager/react-packager/example_project/js/main.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * 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 main - */ -var Channel = require('Channel'); - -function toArray(arr) {return Array.prototype.slice.apply(arr);} -function $(sel) {return document.querySelector(sel);} -function $$(sel) {return toArray(document.querySelectorAll(sel));} - -window.addEventListener('load', function() { - function channelLog() { - var args = Array.prototype.slice.apply(arguments); - var ts = new Date(); - var el = document.createElement('li'); - args.unshift(ts.getHours() + ':' + - ('0' + ts.getMinutes()).substr(0,2) + ':' + - ('0' + ts.getSeconds()).substr(0,2)); - el.className = 'console-entry'; - el.innerHTML = args.join(' '); - $('#console').appendChild(el); - el.scrollIntoView(); - } - - global.addEventListener('ChannelInit', function(event) { - $('#console').innerHTML = ''; - channelLog(event.type); - }); - - global.addEventListener('ChannelLog', function(event) { - channelLog.apply(null, event.detail); - }); - - // Tab pane support - function showTab(paneId) { - paneId = paneId.replace(/\W/g, ''); - if (paneId) { - $$('#nav-panes > div').forEach(function(pane) { - pane.classList.toggle('active', pane.id === paneId); - }); - $$('#nav-tabs li').forEach(function(tab) { - tab.classList.toggle('active', - tab.getAttribute('data-pane') === paneId); - }); - global.history.replaceState(null, null, '#' + paneId); - } - } - - $('#nav-tabs').onclick = function(e) { - showTab(e.target.getAttribute('data-pane')); - }; - - // Show current pane - showTab(location.hash); - - // Connect to server-push channel - Channel.connect(); -}); diff --git a/packager/react-packager/example_project/public/css/index.css b/packager/react-packager/example_project/public/css/index.css deleted file mode 100644 index 651f33263..000000000 --- a/packager/react-packager/example_project/public/css/index.css +++ /dev/null @@ -1,104 +0,0 @@ -/** - * 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. - */ - - -html { - font-family: sans-serif; -} -body { - margin-right: 200px -} - -#nav-tabs { - margin: 0; - padding: 0; - position: absolute; - top: 0px; - left: 0px; - right: 0px; - background-color: #eee; - border-bottom: solid 1px black; - font-size: 10pt; - font-weight: bold; - vertical-align: bottom; - line-height: 20px; - height: 29px; -} -#nav-tabs li { - padding: 0 10px; - margin: 0; - border-bottom-width: 0; - display:inline-block; - cursor: pointer; - line-height: 29px; -} -#nav-tabs li:first-child { - color: #666; -} -#nav-tabs li.active { - background-color: #fff; -} - -#nav-panes { - position: absolute; - top: 30px; - left: 0px; - right: 0px; - bottom: 0px; - scroll: auto; - overflow: auto; - background-color: #fff; -} - -#nav-panes .pane { - display: none; -} -#nav-panes .active { - display: block; -} - -.pane { - padding: 10px; -} - -#console { - padding-left: 5px; -} -#console li { - font-size: 10pt; - font-family: monospace; - white-space: nowrap; - margin: 0; - list-style: none; -} - -#code > ol { - font-size: 10pt; - font-family: monospace; - margin: 0; - padding: 0; - cursor: pointer; -} -#code ol ol { - margin-left: 1em; - padding-left: 1em; - border-left: dashed 1px #ddd; -} -#code li { - color: #000; - font-weight: normal; - list-style: none; - line-height: 1.2em; -} -#code .type1 { - color: #009; -} -#code .type2 { - color: #909; -} diff --git a/packager/react-packager/example_project/public/index.html b/packager/react-packager/example_project/public/index.html deleted file mode 100644 index e0e2ce7fb..000000000 --- a/packager/react-packager/example_project/public/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/packager/react-packager/index.js b/packager/react-packager/index.js index c47d762a1..c3e5829f3 100644 --- a/packager/react-packager/index.js +++ b/packager/react-packager/index.js @@ -16,24 +16,31 @@ useGracefulFs(); var Activity = require('./src/Activity'); var Server = require('./src/Server'); +var SocketInterface = require('./src/SocketInterface'); exports.middleware = function(options) { var server = new Server(options); return server.processRequest.bind(server); }; -exports.buildPackage = function(options, packageOptions) { +exports.Activity = Activity; + +// Renamed "package" to "bundle". But maintain backwards +// compat. +exports.buildPackage = +exports.buildBundle = function(options, bundleOptions) { var server = createServer(options); - return server.buildPackage(packageOptions) + return server.buildBundle(bundleOptions) .then(function(p) { server.end(); return p; }); }; -exports.buildPackageFromUrl = function(options, reqUrl) { +exports.buildPackageFromUrl = +exports.buildBundleFromUrl = function(options, reqUrl) { var server = createServer(options); - return server.buildPackageFromUrl(reqUrl) + return server.buildBundleFromUrl(reqUrl) .then(function(p) { server.end(); return p; @@ -49,17 +56,44 @@ exports.getDependencies = function(options, main) { }); }; +exports.createClientFor = function(options) { + return SocketInterface.getOrCreateSocketFor(options); +}; + +process.on('message', function(m) { + if (m && m.type && m.type === 'createSocketServer') { + console.log('server got ipc message', m); + var options = m.data.options; + + // regexp doesn't naturally serialize to json. + options.blacklistRE = new RegExp(options.blacklistRE.source); + + SocketInterface.createSocketServer( + m.data.sockPath, + m.data.options + ).then( + function() { + console.log('succesfully created server', m); + process.send({ type: 'createdServer' }); + }, + function(error) { + console.log('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' }); + } else { + throw error; + } + } + ).done(); + } +}); + function useGracefulFs() { var fs = require('fs'); var gracefulFs = require('graceful-fs'); - - // A bit sneaky but it's not straightforward to update all the - // modules we depend on. - Object.keys(fs).forEach(function(method) { - if (typeof fs[method] === 'function' && gracefulFs[method]) { - fs[method] = gracefulFs[method]; - } - }); + gracefulFs.gracefulify(fs); } function createServer(options) { diff --git a/packager/react-packager/src/Activity/__tests__/Activity-test.js b/packager/react-packager/src/Activity/__tests__/Activity-test.js index e8fda3dc5..08254f792 100644 --- a/packager/react-packager/src/Activity/__tests__/Activity-test.js +++ b/packager/react-packager/src/Activity/__tests__/Activity-test.js @@ -13,64 +13,63 @@ jest.setMock('chalk', { dim: function(s) { return s; }, }); -describe('Activity', function() { - var Activity; +describe('Activity', () => { + const origConsoleLog = console.log; + let Activity; - var origConsoleLog = console.log; - - beforeEach(function() { + beforeEach(() => { console.log = jest.genMockFn(); Activity = require('../'); jest.runOnlyPendingTimers(); }); - afterEach(function() { + afterEach(() => { console.log = origConsoleLog; }); - describe('startEvent', function() { - it('writes a START event out to the console', function() { - var EVENT_NAME = 'EVENT_NAME'; - var DATA = {someData: 42}; + describe('startEvent', () => { + it('writes a START event out to the console', () => { + const EVENT_NAME = 'EVENT_NAME'; + const DATA = {someData: 42}; Activity.startEvent(EVENT_NAME, DATA); jest.runOnlyPendingTimers(); expect(console.log.mock.calls.length).toBe(1); - var consoleMsg = console.log.mock.calls[0][0]; + const consoleMsg = console.log.mock.calls[0][0]; expect(consoleMsg).toContain('START'); expect(consoleMsg).toContain(EVENT_NAME); expect(consoleMsg).toContain(JSON.stringify(DATA)); }); }); - describe('endEvent', function() { - it('writes an END event out to the console', function() { - var EVENT_NAME = 'EVENT_NAME'; - var DATA = {someData: 42}; + describe('endEvent', () => { + it('writes an END event out to the console', () => { + const EVENT_NAME = 'EVENT_NAME'; + const DATA = {someData: 42}; - var eventID = Activity.startEvent(EVENT_NAME, DATA); + const eventID = Activity.startEvent(EVENT_NAME, DATA); Activity.endEvent(eventID); jest.runOnlyPendingTimers(); expect(console.log.mock.calls.length).toBe(2); - var consoleMsg = console.log.mock.calls[1][0]; + const consoleMsg = console.log.mock.calls[1][0]; expect(consoleMsg).toContain('END'); expect(consoleMsg).toContain(EVENT_NAME); expect(consoleMsg).toContain(JSON.stringify(DATA)); }); - it('throws when called with an invalid eventId', function() { - expect(function() { - Activity.endEvent(42); - }).toThrow('event(42) is not a valid event id!'); + it('throws when called with an invalid eventId', () => { + expect(() => Activity.endEvent(42)).toThrow( + 'event(42) is not a valid event id!', + ); }); - it('throws when called with an expired eventId', function() { - var eid = Activity.startEvent('', ''); + it('throws when called with an expired eventId', () => { + const eid = Activity.startEvent('', ''); Activity.endEvent(eid); - expect(function() { + expect(() => { Activity.endEvent(eid); }).toThrow('event(3) has already ended!'); @@ -78,17 +77,16 @@ describe('Activity', function() { }); }); - describe('signal', function() { - it('writes a SIGNAL event out to the console', function() { - - var EVENT_NAME = 'EVENT_NAME'; - var DATA = {someData: 42}; + describe('signal', () => { + it('writes a SIGNAL event out to the console', () => { + const EVENT_NAME = 'EVENT_NAME'; + const DATA = {someData: 42}; Activity.signal(EVENT_NAME, DATA); jest.runOnlyPendingTimers(); expect(console.log.mock.calls.length).toBe(1); - var consoleMsg = console.log.mock.calls[0][0]; + const consoleMsg = console.log.mock.calls[0][0]; expect(consoleMsg).toContain(EVENT_NAME); expect(consoleMsg).toContain(JSON.stringify(DATA)); }); diff --git a/packager/react-packager/src/Activity/index.js b/packager/react-packager/src/Activity/index.js index 8e593f9ff..eccebd28b 100644 --- a/packager/react-packager/src/Activity/index.js +++ b/packager/react-packager/src/Activity/index.js @@ -8,19 +8,22 @@ */ 'use strict'; -var chalk = require('chalk'); +const chalk = require('chalk'); +const events = require('events'); -var COLLECTION_PERIOD = 1000; +const COLLECTION_PERIOD = 1000; -var _endedEvents = Object.create(null); -var _eventStarts = Object.create(null); -var _queuedActions = []; -var _scheduledCollectionTimer = null; -var _uuid = 1; -var _enabled = true; +const _endedEvents = Object.create(null); +const _eventStarts = Object.create(null); +const _queuedActions = []; +const _eventEmitter = new events.EventEmitter(); + +let _scheduledCollectionTimer = null; +let _uuid = 1; +let _enabled = true; function endEvent(eventId) { - var eventEndTime = Date.now(); + const eventEndTime = Date.now(); if (!_eventStarts[eventId]) { _throw('event(' + eventId + ') is not a valid event id!'); @@ -39,7 +42,7 @@ function endEvent(eventId) { } function signal(eventName, data) { - var signalTime = Date.now(); + const signalTime = Date.now(); if (eventName == null) { _throw('No event name specified'); @@ -58,7 +61,7 @@ function signal(eventName, data) { } function startEvent(eventName, data) { - var eventStartTime = Date.now(); + const eventStartTime = Date.now(); if (eventName == null) { _throw('No event name specified'); @@ -68,8 +71,8 @@ function startEvent(eventName, data) { data = null; } - var eventId = _uuid++; - var action = { + const eventId = _uuid++; + const action = { action: 'startEvent', data: data, eventId: eventId, @@ -88,7 +91,7 @@ function disable() { function _runCollection() { /* jshint -W084 */ - var action; + let action; while ((action = _queuedActions.shift())) { _writeAction(action); } @@ -98,6 +101,7 @@ function _runCollection() { function _scheduleAction(action) { _queuedActions.push(action); + _eventEmitter.emit(action.action, action); if (_scheduledCollectionTimer === null) { _scheduledCollectionTimer = setTimeout(_runCollection, COLLECTION_PERIOD); @@ -114,10 +118,10 @@ function _scheduleAction(action) { * won't be adding such a non-trivial optimization anytime soon) */ function _throw(msg) { - var err = new Error(msg); + const err = new Error(msg); // Strip off the call to _throw() - var stack = err.stack.split('\n'); + const stack = err.stack.split('\n'); stack.splice(1, 1); err.stack = stack.join('\n'); @@ -129,8 +133,8 @@ function _writeAction(action) { return; } - var data = action.data ? ': ' + JSON.stringify(action.data) : ''; - var fmtTime = new Date(action.tstamp).toLocaleTimeString(); + const data = action.data ? ': ' + JSON.stringify(action.data) : ''; + const fmtTime = new Date(action.tstamp).toLocaleTimeString(); switch (action.action) { case 'startEvent': @@ -142,8 +146,8 @@ function _writeAction(action) { break; case 'endEvent': - var startAction = _eventStarts[action.eventId]; - var startData = startAction.data ? ': ' + JSON.stringify(startAction.data) : ''; + const startAction = _eventStarts[action.eventId]; + const startData = startAction.data ? ': ' + JSON.stringify(startAction.data) : ''; console.log(chalk.dim( '[' + fmtTime + '] ' + ' ' + startAction.eventName + @@ -171,3 +175,4 @@ exports.endEvent = endEvent; exports.signal = signal; exports.startEvent = startEvent; exports.disable = disable; +exports.eventEmitter = _eventEmitter; diff --git a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js index 95916c9ea..54ef70120 100644 --- a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js +++ b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -8,22 +8,22 @@ jest .mock('crypto') .mock('fs'); -var Promise = require('promise'); +const Promise = require('promise'); -describe('AssetServer', function() { - var AssetServer; - var crypto; - var fs; +describe('AssetServer', () => { + let AssetServer; + let crypto; + let fs; - beforeEach(function() { + beforeEach(() => { AssetServer = require('../'); crypto = require('crypto'); fs = require('fs'); }); - describe('assetServer.get', function() { - pit('should work for the simple case', function() { - var server = new AssetServer({ + describe('assetServer.get', () => { + pit('should work for the simple case', () => { + const server = new AssetServer({ projectRoots: ['/root'], assetExts: ['png'], }); @@ -40,15 +40,15 @@ describe('AssetServer', function() { return Promise.all([ server.get('imgs/b.png'), server.get('imgs/b@1x.png'), - ]).then(function(resp) { - resp.forEach(function(data) { - expect(data).toBe('b image'); - }); - }); + ]).then(resp => + resp.forEach(data => + expect(data).toBe('b image') + ) + ); }); - pit('should work for the simple case with jpg', function() { - var server = new AssetServer({ + pit('should work for the simple case with jpg', () => { + const server = new AssetServer({ projectRoots: ['/root'], assetExts: ['png', 'jpg'], }); @@ -65,16 +65,16 @@ describe('AssetServer', function() { return Promise.all([ server.get('imgs/b.jpg'), server.get('imgs/b.png'), - ]).then(function(data) { + ]).then(data => expect(data).toEqual([ 'jpeg image', 'png image', - ]); - }); + ]) + ); }); - pit('should pick the bigger one', function() { - var server = new AssetServer({ + pit('should pick the bigger one', () => { + const server = new AssetServer({ projectRoots: ['/root'], assetExts: ['png'], }); @@ -90,13 +90,13 @@ describe('AssetServer', function() { } }); - return server.get('imgs/b@3x.png').then(function(data) { - expect(data).toBe('b4 image'); - }); + return server.get('imgs/b@3x.png').then(data => + expect(data).toBe('b4 image') + ); }); - pit('should support multiple project roots', function() { - var server = new AssetServer({ + pit('should support multiple project roots', () => { + const server = new AssetServer({ projectRoots: ['/root', '/root2'], assetExts: ['png'], }); @@ -116,27 +116,23 @@ describe('AssetServer', function() { }, }); - return server.get('newImages/imgs/b.png').then(function(data) { - expect(data).toBe('b1 image'); - }); + return server.get('newImages/imgs/b.png').then(data => + expect(data).toBe('b1 image') + ); }); }); - describe('assetSerer.getAssetData', function() { - pit('should get assetData', function() { - var hash = { + describe('assetSerer.getAssetData', () => { + pit('should get assetData', () => { + const hash = { update: jest.genMockFn(), digest: jest.genMockFn(), }; - hash.digest.mockImpl(function() { - return 'wow such hash'; - }); - crypto.createHash.mockImpl(function() { - return hash; - }); + hash.digest.mockImpl(() => 'wow such hash'); + crypto.createHash.mockImpl(() => hash); - var server = new AssetServer({ + const server = new AssetServer({ projectRoots: ['/root'], assetExts: ['png'], }); @@ -152,7 +148,7 @@ describe('AssetServer', function() { } }); - return server.getAssetData('imgs/b.png').then(function(data) { + return server.getAssetData('imgs/b.png').then(data => { expect(hash.update.mock.calls.length).toBe(4); expect(data).toEqual({ type: 'png', @@ -163,20 +159,16 @@ describe('AssetServer', function() { }); }); - pit('should get assetData for non-png images', function() { - var hash = { + pit('should get assetData for non-png images', () => { + const hash = { update: jest.genMockFn(), digest: jest.genMockFn(), }; - hash.digest.mockImpl(function() { - return 'wow such hash'; - }); - crypto.createHash.mockImpl(function() { - return hash; - }); + hash.digest.mockImpl(() => 'wow such hash'); + crypto.createHash.mockImpl(() => hash); - var server = new AssetServer({ + const server = new AssetServer({ projectRoots: ['/root'], assetExts: ['png', 'jpeg'], }); @@ -192,7 +184,7 @@ describe('AssetServer', function() { } }); - return server.getAssetData('imgs/b.jpg').then(function(data) { + return server.getAssetData('imgs/b.jpg').then(data => { expect(hash.update.mock.calls.length).toBe(4); expect(data).toEqual({ type: 'jpg', diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js index 2cd365fdc..f442f6b89 100644 --- a/packager/react-packager/src/AssetServer/index.js +++ b/packager/react-packager/src/AssetServer/index.js @@ -8,20 +8,20 @@ */ 'use strict'; -var declareOpts = require('../lib/declareOpts'); -var getAssetDataFromName = require('../lib/getAssetDataFromName'); -var path = require('path'); -var Promise = require('promise'); -var fs = require('fs'); -var crypto = require('crypto'); +const Promise = require('promise'); -var stat = Promise.denodeify(fs.stat); -var readDir = Promise.denodeify(fs.readdir); -var readFile = Promise.denodeify(fs.readFile); +const crypto = require('crypto'); +const declareOpts = require('../lib/declareOpts'); +const fs = require('fs'); +const getAssetDataFromName = require('../lib/getAssetDataFromName'); +const path = require('path'); -module.exports = AssetServer; +const stat = Promise.denodeify(fs.stat); +const readDir = Promise.denodeify(fs.readdir); +const readFile = Promise.denodeify(fs.readFile); -var validateOpts = declareOpts({ + +const validateOpts = declareOpts({ projectRoots: { type: 'array', required: true, @@ -32,135 +32,136 @@ var validateOpts = declareOpts({ }, }); -function AssetServer(options) { - var opts = validateOpts(options); - this._roots = opts.projectRoots; - this._assetExts = opts.assetExts; -} +class AssetServer { + constructor(options) { + const opts = validateOpts(options); + this._roots = opts.projectRoots; + this._assetExts = opts.assetExts; + } -/** - * Given a request for an image by path. That could contain a resolution - * postfix, we need to find that image (or the closest one to it's resolution) - * in one of the project roots: - * - * 1. We first parse the directory of the asset - * 2. We check to find a matching directory in one of the project roots - * 3. We then build a map of all assets and their scales in this directory - * 4. Then pick the closest resolution (rounding up) to the requested one - */ - -AssetServer.prototype._getAssetRecord = function(assetPath) { - var filename = path.basename(assetPath); - - return findRoot( - this._roots, - path.dirname(assetPath) - ).then(function(dir) { - return Promise.all([ - dir, - readDir(dir), - ]); - }).then(function(res) { - var dir = res[0]; - var files = res[1]; - var assetData = getAssetDataFromName(filename); - - var map = buildAssetMap(dir, files); - var record = map[assetData.assetName]; - - if (!record) { - throw new Error('Asset not found'); - } - - return record; - }); -}; - -AssetServer.prototype.get = function(assetPath) { - var assetData = getAssetDataFromName(assetPath); - return this._getAssetRecord(assetPath).then(function(record) { - for (var i = 0; i < record.scales.length; i++) { - if (record.scales[i] >= assetData.resolution) { - return readFile(record.files[i]); + get(assetPath) { + const assetData = getAssetDataFromName(assetPath); + return this._getAssetRecord(assetPath).then(record => { + for (let i = 0; i < record.scales.length; i++) { + if (record.scales[i] >= assetData.resolution) { + return readFile(record.files[i]); + } } - } - return readFile(record.files[record.files.length - 1]); - }); -}; + return readFile(record.files[record.files.length - 1]); + }); + } -AssetServer.prototype.getAssetData = function(assetPath) { - var nameData = getAssetDataFromName(assetPath); - var data = { - name: nameData.name, - type: nameData.type, - }; + getAssetData(assetPath) { + const nameData = getAssetDataFromName(assetPath); + const data = { + name: nameData.name, + type: nameData.type, + }; - return this._getAssetRecord(assetPath).then(function(record) { - data.scales = record.scales; + return this._getAssetRecord(assetPath).then(record => { + data.scales = record.scales; - return Promise.all( - record.files.map(function(file) { - return stat(file); + return Promise.all( + record.files.map(file => stat(file)) + ); + }).then(stats => { + const hash = crypto.createHash('md5'); + + stats.forEach(fstat => + hash.update(fstat.mtime.getTime().toString()) + ); + + data.hash = hash.digest('hex'); + return data; + }); + } + + /** + * Given a request for an image by path. That could contain a resolution + * postfix, we need to find that image (or the closest one to it's resolution) + * in one of the project roots: + * + * 1. We first parse the directory of the asset + * 2. We check to find a matching directory in one of the project roots + * 3. We then build a map of all assets and their scales in this directory + * 4. Then pick the closest resolution (rounding up) to the requested one + */ + _getAssetRecord(assetPath) { + const filename = path.basename(assetPath); + + return ( + this._findRoot( + this._roots, + path.dirname(assetPath) + ) + .then(dir => Promise.all([ + dir, + readDir(dir), + ])) + .then(res => { + const dir = res[0]; + const files = res[1]; + const assetData = getAssetDataFromName(filename); + + const map = this._buildAssetMap(dir, files); + const record = map[assetData.assetName]; + + if (!record) { + throw new Error('Asset not found'); + } + + return record; }) ); - }).then(function(stats) { - var hash = crypto.createHash('md5'); + } - stats.forEach(function(fstat) { - hash.update(fstat.mtime.getTime().toString()); - }); - - data.hash = hash.digest('hex'); - return data; - }); -}; - -function findRoot(roots, dir) { - return Promise.all( - roots.map(function(root) { - var absPath = path.join(root, dir); - return stat(absPath).then(function(fstat) { - return {path: absPath, isDirectory: fstat.isDirectory()}; - }, function (err) { - return {path: absPath, isDirectory: false}; - }); - }) - ).then( - function(stats) { - for (var i = 0; i < stats.length; i++) { + _findRoot(roots, dir) { + return Promise.all( + roots.map(root => { + const absPath = path.join(root, dir); + return stat(absPath).then(fstat => { + return {path: absPath, isDirectory: fstat.isDirectory()}; + }, err => { + return {path: absPath, isDirectory: false}; + }); + }) + ).then(stats => { + for (let i = 0; i < stats.length; i++) { if (stats[i].isDirectory) { return stats[i].path; } } throw new Error('Could not find any directories'); - } - ); -} + }); + } -function buildAssetMap(dir, files) { - var assets = files.map(getAssetDataFromName); - var map = Object.create(null); - assets.forEach(function(asset, i) { - var file = files[i]; - var record = map[asset.assetName]; - if (!record) { - record = map[asset.assetName] = { - scales: [], - files: [], - }; - } - - var insertIndex; - var length = record.scales.length; - for (insertIndex = 0; insertIndex < length; insertIndex++) { - if (asset.resolution < record.scales[insertIndex]) { - break; + _buildAssetMap(dir, files) { + const assets = files.map(getAssetDataFromName); + const map = Object.create(null); + assets.forEach(function(asset, i) { + const file = files[i]; + let record = map[asset.assetName]; + if (!record) { + record = map[asset.assetName] = { + scales: [], + files: [], + }; } - } - record.scales.splice(insertIndex, 0, asset.resolution); - record.files.splice(insertIndex, 0, path.join(dir, file)); - }); - return map; + let insertIndex; + const length = record.scales.length; + for (insertIndex = 0; insertIndex < length; insertIndex++) { + if (asset.resolution < record.scales[insertIndex]) { + break; + } + } + record.scales.splice(insertIndex, 0, asset.resolution); + record.files.splice(insertIndex, 0, path.join(dir, file)); + }); + + return map; + } } + +module.exports = AssetServer; diff --git a/packager/react-packager/src/Bundler/Bundle.js b/packager/react-packager/src/Bundler/Bundle.js new file mode 100644 index 000000000..b7920ebb8 --- /dev/null +++ b/packager/react-packager/src/Bundler/Bundle.js @@ -0,0 +1,337 @@ +/** + * 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 _ = require('underscore'); +const base64VLQ = require('./base64-vlq'); +const UglifyJS = require('uglify-js'); +const ModuleTransport = require('../lib/ModuleTransport'); + +const SOURCEMAPPING_URL = '\n\/\/@ sourceMappingURL='; + +class Bundle { + constructor(sourceMapUrl) { + this._finalized = false; + this._modules = []; + this._assets = []; + this._sourceMapUrl = sourceMapUrl; + this._shouldCombineSourceMaps = false; + } + + setMainModuleId(moduleId) { + this._mainModuleId = moduleId; + } + + addModule(module) { + if (!(module instanceof ModuleTransport)) { + throw new Error('Expeceted a ModuleTransport object'); + } + + // If we get a map from the transformer we'll switch to a mode + // were we're combining the source maps as opposed to + if (!this._shouldCombineSourceMaps && module.map != null) { + this._shouldCombineSourceMaps = true; + } + + this._modules.push(module); + } + + getModules() { + return this._modules; + } + + addAsset(asset) { + this._assets.push(asset); + } + + finalize(options) { + options = options || {}; + if (options.runMainModule) { + const runCode = ';require("' + this._mainModuleId + '");'; + this.addModule(new ModuleTransport({ + code: runCode, + virtual: true, + sourceCode: runCode, + sourcePath: 'RunMainModule.js' + })); + } + + Object.freeze(this._modules); + Object.seal(this._modules); + Object.freeze(this._assets); + Object.seal(this._assets); + this._finalized = true; + } + + _assertFinalized() { + if (!this._finalized) { + throw new Error('Bundle needs to be finalized before getting any source'); + } + } + + _getSource() { + if (this._source == null) { + this._source = _.pluck(this._modules, 'code').join('\n'); + } + return this._source; + } + + _getInlineSourceMap() { + if (this._inlineSourceMap == null) { + const sourceMap = this.getSourceMap({excludeSource: true}); + /*eslint-env node*/ + const encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64'); + this._inlineSourceMap = 'data:application/json;base64,' + encoded; + } + return this._inlineSourceMap; + } + + getSource(options) { + this._assertFinalized(); + + options = options || {}; + + if (options.minify) { + return this.getMinifiedSourceAndMap().code; + } + + let source = this._getSource(); + + if (options.inlineSourceMap) { + source += SOURCEMAPPING_URL + this._getInlineSourceMap(); + } else if (this._sourceMapUrl) { + source += SOURCEMAPPING_URL + this._sourceMapUrl; + } + + return source; + } + + getMinifiedSourceAndMap() { + this._assertFinalized(); + + const source = this._getSource(); + try { + return UglifyJS.minify(source, { + fromString: true, + outSourceMap: 'bundle.js', + inSourceMap: this.getSourceMap(), + }); + } catch(e) { + // Sometimes, when somebody is using a new syntax feature that we + // don't yet have transform for, the untransformed line is sent to + // uglify, and it chokes on it. This code tries to print the line + // and the module for easier debugging + let errorMessage = 'Error while minifying JS\n'; + if (e.line) { + errorMessage += 'Transformed code line: "' + + source.split('\n')[e.line - 1] + '"\n'; + } + if (e.pos) { + let fromIndex = source.lastIndexOf('__d(\'', e.pos); + if (fromIndex > -1) { + fromIndex += '__d(\''.length; + const toIndex = source.indexOf('\'', fromIndex); + errorMessage += 'Module name (best guess): ' + + source.substring(fromIndex, toIndex) + '\n'; + } + } + errorMessage += e.toString(); + throw new Error(errorMessage); + } + } + + /** + * I found a neat trick in the sourcemap spec that makes it easy + * to concat sourcemaps. The `sections` field allows us to combine + * the sourcemap easily by adding an offset. Tested on chrome. + * Seems like it's not yet in Firefox but that should be fine for + * now. + */ + _getCombinedSourceMaps(options) { + const result = { + version: 3, + file: 'bundle.js', + sections: [], + }; + + let line = 0; + this._modules.forEach(function(module) { + let map = module.map; + if (module.virtual) { + map = generateSourceMapForVirtualModule(module); + } + + if (options.excludeSource) { + map = _.extend({}, map, {sourcesContent: []}); + } + + result.sections.push({ + offset: { line: line, column: 0 }, + map: map, + }); + line += module.code.split('\n').length; + }); + + return result; + } + + getSourceMap(options) { + this._assertFinalized(); + + options = options || {}; + + if (this._shouldCombineSourceMaps) { + return this._getCombinedSourceMaps(options); + } + + const mappings = this._getMappings(); + const map = { + file: 'bundle.js', + sources: _.pluck(this._modules, 'sourcePath'), + version: 3, + names: [], + mappings: mappings, + sourcesContent: options.excludeSource + ? [] : _.pluck(this._modules, 'sourceCode') + }; + return map; + } + + getAssets() { + return this._assets; + } + + _getMappings() { + const modules = this._modules; + + // The first line mapping in our package is basically the base64vlq code for + // zeros (A). + const firstLine = 'AAAA'; + + // Most other lines in our mappings are all zeros (for module, column etc) + // except for the lineno mappinp: curLineno - prevLineno = 1; Which is C. + const line = 'AACA'; + + const moduleLines = Object.create(null); + let mappings = ''; + for (let i = 0; i < modules.length; i++) { + const module = modules[i]; + const code = module.code; + let lastCharNewLine = false; + moduleLines[module.sourcePath] = 0; + for (let t = 0; t < code.length; t++) { + if (t === 0 && i === 0) { + mappings += firstLine; + } else if (t === 0) { + mappings += 'AC'; + + // This is the only place were we actually don't know the mapping ahead + // of time. When it's a new module (and not the first) the lineno + // mapping is 0 (current) - number of lines in prev module. + mappings += base64VLQ.encode( + 0 - moduleLines[modules[i - 1].sourcePath] + ); + mappings += 'A'; + } else if (lastCharNewLine) { + moduleLines[module.sourcePath]++; + mappings += line; + } + lastCharNewLine = code[t] === '\n'; + if (lastCharNewLine) { + mappings += ';'; + } + } + if (i !== modules.length - 1) { + mappings += ';'; + } + } + return mappings; + } + + getJSModulePaths() { + return this._modules.filter(function(module) { + // Filter out non-js files. Like images etc. + return !module.virtual; + }).map(function(module) { + return module.sourcePath; + }); + } + + getDebugInfo() { + return [ + '

Main Module:

' + this._mainModuleId + '
', + '', + '

Module paths and transformed code:

', + this._modules.map(function(m) { + return '

Path:

' + m.sourcePath + '

Source:

' + + '
'; + }).join('\n'), + ].join('\n'); + } + + toJSON() { + if (!this._finalized) { + throw new Error('Cannot serialize bundle unless finalized'); + } + + return { + modules: this._modules, + assets: this._assets, + sourceMapUrl: this._sourceMapUrl, + shouldCombineSourceMaps: this._shouldCombineSourceMaps, + mainModuleId: this._mainModuleId, + }; + } + + static fromJSON(json) { + const bundle = new Bundle(json.sourceMapUrl); + bundle._mainModuleId = json.mainModuleId; + bundle._assets = json.assets; + bundle._modules = json.modules; + bundle._sourceMapUrl = json.sourceMapUrl; + + Object.freeze(bundle._modules); + Object.seal(bundle._modules); + Object.freeze(bundle._assets); + Object.seal(bundle._assets); + bundle._finalized = true; + + return bundle; + } +} + +function generateSourceMapForVirtualModule(module) { + // All lines map 1-to-1 + let mappings = 'AAAA;'; + + for (let i = 1; i < module.code.split('\n').length; i++) { + mappings += 'AACA;'; + } + + return { + version: 3, + sources: [ module.sourcePath ], + names: [], + mappings: mappings, + file: module.sourcePath, + sourcesContent: [ module.sourceCode ], + }; +} + +module.exports = Bundle; diff --git a/packager/react-packager/src/Packager/__tests__/Package-test.js b/packager/react-packager/src/Bundler/__tests__/Bundle-test.js similarity index 82% rename from packager/react-packager/src/Packager/__tests__/Package-test.js rename to packager/react-packager/src/Bundler/__tests__/Bundle-test.js index d43c65c0f..74d189240 100644 --- a/packager/react-packager/src/Packager/__tests__/Package-test.js +++ b/packager/react-packager/src/Bundler/__tests__/Bundle-test.js @@ -12,35 +12,35 @@ jest.autoMockOff(); var SourceMapGenerator = require('source-map').SourceMapGenerator; -describe('Package', function() { +describe('Bundle', function() { var ModuleTransport; - var Package; - var ppackage; + var Bundle; + var bundle; beforeEach(function() { - Package = require('../Package'); + Bundle = require('../Bundle'); ModuleTransport = require('../../lib/ModuleTransport'); - ppackage = new Package('test_url'); - ppackage.getSourceMap = jest.genMockFn().mockImpl(function() { + bundle = new Bundle('test_url'); + bundle.getSourceMap = jest.genMockFn().mockImpl(function() { return 'test-source-map'; }); }); - describe('source package', function() { - it('should create a package and get the source', function() { - ppackage.addModule(new ModuleTransport({ + describe('source bundle', function() { + it('should create a bundle and get the source', function() { + bundle.addModule(new ModuleTransport({ code: 'transformed foo;', sourceCode: 'source foo', sourcePath: 'foo path', })); - ppackage.addModule(new ModuleTransport({ + bundle.addModule(new ModuleTransport({ code: 'transformed bar;', sourceCode: 'source bar', sourcePath: 'bar path', })); - ppackage.finalize({}); - expect(ppackage.getSource()).toBe([ + bundle.finalize({}); + expect(bundle.getSource()).toBe([ 'transformed foo;', 'transformed bar;', '\/\/@ sourceMappingURL=test_url' @@ -48,7 +48,7 @@ describe('Package', function() { }); it('should be ok to leave out the source map url', function() { - var p = new Package(); + var p = new Bundle(); p.addModule(new ModuleTransport({ code: 'transformed foo;', sourceCode: 'source foo', @@ -67,22 +67,22 @@ describe('Package', function() { ].join('\n')); }); - it('should create a package and add run module code', function() { - ppackage.addModule(new ModuleTransport({ + it('should create a bundle and add run module code', function() { + bundle.addModule(new ModuleTransport({ code: 'transformed foo;', sourceCode: 'source foo', sourcePath: 'foo path' })); - ppackage.addModule(new ModuleTransport({ + bundle.addModule(new ModuleTransport({ code: 'transformed bar;', sourceCode: 'source bar', sourcePath: 'bar path' })); - ppackage.setMainModuleId('foo'); - ppackage.finalize({runMainModule: true}); - expect(ppackage.getSource()).toBe([ + bundle.setMainModuleId('foo'); + bundle.finalize({runMainModule: true}); + expect(bundle.getSource()).toBe([ 'transformed foo;', 'transformed bar;', ';require("foo");', @@ -100,19 +100,19 @@ describe('Package', function() { return minified; }; - ppackage.addModule(new ModuleTransport({ + bundle.addModule(new ModuleTransport({ code: 'transformed foo;', sourceCode: 'source foo', sourcePath: 'foo path' })); - ppackage.finalize(); - expect(ppackage.getMinifiedSourceAndMap()).toBe(minified); + bundle.finalize(); + expect(bundle.getMinifiedSourceAndMap()).toBe(minified); }); }); - describe('sourcemap package', function() { + describe('sourcemap bundle', function() { it('should create sourcemap', function() { - var p = new Package('test_url'); + var p = new Bundle('test_url'); p.addModule(new ModuleTransport({ code: [ 'transformed foo', @@ -143,11 +143,11 @@ describe('Package', function() { p.setMainModuleId('foo'); p.finalize({runMainModule: true}); var s = p.getSourceMap(); - expect(s).toEqual(genSourceMap(p._modules)); + expect(s).toEqual(genSourceMap(p.getModules())); }); it('should combine sourcemaps', function() { - var p = new Package('test_url'); + var p = new Bundle('test_url'); p.addModule(new ModuleTransport({ code: 'transformed foo;\n', @@ -215,7 +215,7 @@ describe('Package', function() { describe('getAssets()', function() { it('should save and return asset objects', function() { - var p = new Package('test_url'); + var p = new Bundle('test_url'); var asset1 = {}; var asset2 = {}; p.addAsset(asset1); @@ -227,7 +227,7 @@ describe('Package', function() { describe('getJSModulePaths()', function() { it('should return module paths', function() { - var p = new Package('test_url'); + var p = new Bundle('test_url'); p.addModule(new ModuleTransport({ code: 'transformed foo;\n', sourceCode: 'source foo', @@ -248,7 +248,7 @@ describe('Package', function() { function genSourceMap(modules) { var sourceMapGen = new SourceMapGenerator({file: 'bundle.js', version: 3}); - var packageLineNo = 0; + var bundleLineNo = 0; for (var i = 0; i < modules.length; i++) { var module = modules[i]; var transformedCode = module.code; @@ -259,7 +259,7 @@ describe('Package', function() { for (var t = 0; t < transformedCode.length; t++) { if (t === 0 || lastCharNewLine) { sourceMapGen.addMapping({ - generated: {line: packageLineNo + 1, column: 0}, + generated: {line: bundleLineNo + 1, column: 0}, original: {line: transformedLineCount + 1, column: 0}, source: sourcePath }); @@ -267,10 +267,10 @@ describe('Package', function() { lastCharNewLine = transformedCode[t] === '\n'; if (lastCharNewLine) { transformedLineCount++; - packageLineNo++; + bundleLineNo++; } } - packageLineNo++; + bundleLineNo++; sourceMapGen.setSourceContent( sourcePath, sourceCode diff --git a/packager/react-packager/src/Packager/__tests__/Packager-test.js b/packager/react-packager/src/Bundler/__tests__/Bundler-test.js similarity index 76% rename from packager/react-packager/src/Packager/__tests__/Packager-test.js rename to packager/react-packager/src/Bundler/__tests__/Bundler-test.js index 00c76870b..f70eb4b58 100644 --- a/packager/react-packager/src/Packager/__tests__/Packager-test.js +++ b/packager/react-packager/src/Bundler/__tests__/Bundler-test.js @@ -9,7 +9,7 @@ 'use strict'; jest - .setMock('worker-farm', function() { return function() {};}) + .setMock('worker-farm', () => () => undefined) .dontMock('underscore') .dontMock('../../lib/ModuleTransport') .setMock('uglify-js') @@ -20,13 +20,14 @@ jest.mock('fs'); var Promise = require('promise'); -describe('Packager', function() { +describe('Bundler', function() { var getDependencies; var wrapModule; - var Packager; - var packager; + var Bundler; + var bundler; var assetServer; var modules; + var ProgressBar; beforeEach(function() { getDependencies = jest.genMockFn(); @@ -38,7 +39,7 @@ describe('Packager', function() { }; }); - Packager = require('../'); + Bundler = require('../'); require('fs').statSync.mockImpl(function() { return { @@ -50,38 +51,61 @@ describe('Packager', function() { callback(null, '{"json":true}'); }); + ProgressBar = require('progress'); + assetServer = { getAssetData: jest.genMockFn(), }; - packager = new Packager({ + bundler = new Bundler({ projectRoots: ['/root'], assetServer: assetServer, }); + + function createModule({ + path, + id, + dependencies, + isAsset, + isAsset_DEPRECATED, + isJSON, + resolution, + }) { + return { + path, + resolution, + getDependencies() { return Promise.resolve(dependencies); }, + getName() { return Promise.resolve(id); }, + isJSON() { return isJSON; }, + isAsset() { return isAsset; }, + isAsset_DEPRECATED() { return isAsset_DEPRECATED; }, + }; + } + modules = [ - {id: 'foo', path: '/root/foo.js', dependencies: []}, - {id: 'bar', path: '/root/bar.js', dependencies: []}, - { - id: 'image!img', + createModule({id: 'foo', path: '/root/foo.js', dependencies: []}), + createModule({id: 'bar', path: '/root/bar.js', dependencies: []}), + createModule({ path: '/root/img/img.png', + id: 'image!img', isAsset_DEPRECATED: true, dependencies: [], resolution: 2, - }, - { + }), + createModule({ id: 'new_image.png', path: '/root/img/new_image.png', isAsset: true, resolution: 2, dependencies: [] - }, - { + }), + createModule({ id: 'package/file.json', path: '/root/file.json', isJSON: true, dependencies: [], - }, + }), ]; getDependencies.mockImpl(function() { @@ -119,8 +143,8 @@ describe('Packager', function() { }); }); - pit('create a package', function() { - return packager.package('/root/foo.js', true, 'source_map_url') + pit('create a bundle', function() { + return bundler.bundle('/root/foo.js', true, 'source_map_url') .then(function(p) { expect(p.addModule.mock.calls[0][0]).toEqual({ code: 'lol transformed /root/foo.js lol', @@ -194,51 +218,24 @@ describe('Packager', function() { {runMainModule: true} ]); - expect(p.addAsset.mock.calls[0]).toEqual([ + expect(p.addAsset.mock.calls).toContain([ imgModule_DEPRECATED ]); - expect(p.addAsset.mock.calls[1]).toEqual([ + expect(p.addAsset.mock.calls).toContain([ imgModule ]); + + // TODO(amasad) This fails with 0 != 5 in OSS + //expect(ProgressBar.prototype.tick.mock.calls.length).toEqual(modules.length); }); }); - pit('gets the list of dependencies', function() { - return packager.getDependencies('/root/foo.js', true) - .then(({dependencies}) => { - expect(dependencies).toEqual([ - { - dependencies: [], - id: 'foo', - path: '/root/foo.js', - }, - { - dependencies: [], - id: 'bar', - path: '/root/bar.js', - }, - { - dependencies: [], - id: 'image!img', - isAsset_DEPRECATED: true, - path: '/root/img/img.png', - resolution: 2, - }, - { - dependencies: [], - id: 'new_image.png', - isAsset: true, - path: '/root/img/new_image.png', - resolution: 2, - }, - { - dependencies: [], - id: 'package/file.json', - isJSON: true, - path: '/root/file.json', - }, - ]); - }); + pit('gets the list of dependencies from the resolver', function() { + return bundler.getDependencies('/root/foo.js', true) + .then( + () => expect(getDependencies) + .toBeCalledWith('/root/foo.js', { dev: true }) + ); }); }); diff --git a/packager/react-packager/src/Packager/base64-vlq.js b/packager/react-packager/src/Bundler/base64-vlq.js similarity index 100% rename from packager/react-packager/src/Packager/base64-vlq.js rename to packager/react-packager/src/Bundler/base64-vlq.js diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js new file mode 100644 index 000000000..d29ab0f89 --- /dev/null +++ b/packager/react-packager/src/Bundler/index.js @@ -0,0 +1,309 @@ +/** + * 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 assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const Promise = require('promise'); +const ProgressBar = require('progress'); +const Cache = require('../Cache'); +const Transformer = require('../JSTransformer'); +const DependencyResolver = require('../DependencyResolver'); +const Bundle = require('./Bundle'); +const Activity = require('../Activity'); +const ModuleTransport = require('../lib/ModuleTransport'); +const declareOpts = require('../lib/declareOpts'); +const imageSize = require('image-size'); + +const sizeOf = Promise.denodeify(imageSize); +const readFile = Promise.denodeify(fs.readFile); + +const validateOpts = declareOpts({ + projectRoots: { + type: 'array', + required: true, + }, + blacklistRE: { + type: 'object', // typeof regex is object + }, + moduleFormat: { + type: 'string', + default: 'haste', + }, + polyfillModuleNames: { + type: 'array', + default: [], + }, + cacheVersion: { + type: 'string', + default: '1.0', + }, + resetCache: { + type: 'boolean', + default: false, + }, + transformModulePath: { + type:'string', + required: false, + }, + nonPersistent: { + type: 'boolean', + default: false, + }, + assetRoots: { + type: 'array', + required: false, + }, + assetExts: { + type: 'array', + default: ['png'], + }, + fileWatcher: { + type: 'object', + required: true, + }, + assetServer: { + type: 'object', + required: true, + }, + transformTimeoutInterval: { + type: 'number', + required: false, + }, +}); + +class Bundler { + + constructor(options) { + const opts = this._opts = validateOpts(options); + + opts.projectRoots.forEach(verifyRootExists); + + this._cache = new Cache({ + resetCache: opts.resetCache, + cacheVersion: opts.cacheVersion, + projectRoots: opts.projectRoots, + transformModulePath: opts.transformModulePath, + }); + + this._resolver = new DependencyResolver({ + projectRoots: opts.projectRoots, + blacklistRE: opts.blacklistRE, + polyfillModuleNames: opts.polyfillModuleNames, + moduleFormat: opts.moduleFormat, + assetRoots: opts.assetRoots, + fileWatcher: opts.fileWatcher, + assetExts: opts.assetExts, + cache: this._cache, + }); + + this._transformer = new Transformer({ + projectRoots: opts.projectRoots, + blacklistRE: opts.blacklistRE, + cache: this._cache, + transformModulePath: opts.transformModulePath, + }); + + this._projectRoots = opts.projectRoots; + this._assetServer = opts.assetServer; + } + + kill() { + this._transformer.kill(); + return this._cache.end(); + } + + bundle(main, runModule, sourceMapUrl, isDev, platform) { + const bundle = new Bundle(sourceMapUrl); + const findEventId = Activity.startEvent('find dependencies'); + let transformEventId; + + return this.getDependencies(main, isDev, platform).then((result) => { + Activity.endEvent(findEventId); + transformEventId = Activity.startEvent('transform'); + + let bar; + if (process.stdout.isTTY) { + bar = new ProgressBar('transforming [:bar] :percent :current/:total', { + complete: '=', + incomplete: ' ', + width: 40, + total: result.dependencies.length, + }); + } + + bundle.setMainModuleId(result.mainModuleId); + return Promise.all( + result.dependencies.map( + module => this._transformModule(bundle, module).then(transformed => { + if (bar) { + bar.tick(); + } + return transformed; + }) + ) + ); + }).then((transformedModules) => { + Activity.endEvent(transformEventId); + + transformedModules.forEach(function(moduleTransport) { + bundle.addModule(moduleTransport); + }); + + bundle.finalize({ runMainModule: runModule }); + return bundle; + }); + } + + invalidateFile(filePath) { + this._transformer.invalidateFile(filePath); + } + + getDependencies(main, isDev, platform) { + return this._resolver.getDependencies(main, { dev: isDev, platform }); + } + + _transformModule(bundle, module) { + let transform; + + if (module.isAsset_DEPRECATED()) { + transform = this.generateAssetModule_DEPRECATED(bundle, module); + } else if (module.isAsset()) { + transform = this.generateAssetModule(bundle, module); + } else if (module.isJSON()) { + transform = generateJSONModule(module); + } else { + transform = this._transformer.loadFileAndTransform( + path.resolve(module.path) + ); + } + + const resolver = this._resolver; + return transform.then( + transformed => resolver.wrapModule(module, transformed.code).then( + code => new ModuleTransport({ + code: code, + map: transformed.map, + sourceCode: transformed.sourceCode, + sourcePath: transformed.sourcePath, + virtual: transformed.virtual, + }) + ) + ); + } + + getGraphDebugInfo() { + return this._resolver.getDebugInfo(); + } + + generateAssetModule_DEPRECATED(bundle, module) { + return Promise.all([ + sizeOf(module.path), + module.getName(), + ]).then(([dimensions, id]) => { + const img = { + __packager_asset: true, + isStatic: true, + path: module.path, + uri: id.replace(/^[^!]+!/, ''), + width: dimensions.width / module.resolution, + height: dimensions.height / module.resolution, + deprecated: true, + }; + + bundle.addAsset(img); + + const code = 'module.exports = ' + JSON.stringify(img) + ';'; + + return new ModuleTransport({ + code: code, + sourceCode: code, + sourcePath: module.path, + virtual: true, + }); + }); + } + + generateAssetModule(bundle, module) { + const relPath = getPathRelativeToRoot(this._projectRoots, module.path); + + return Promise.all([ + sizeOf(module.path), + this._assetServer.getAssetData(relPath), + ]).then(function(res) { + const dimensions = res[0]; + const assetData = res[1]; + const img = { + __packager_asset: true, + fileSystemLocation: path.dirname(module.path), + httpServerLocation: path.join('/assets', path.dirname(relPath)), + width: dimensions.width / module.resolution, + height: dimensions.height / module.resolution, + scales: assetData.scales, + hash: assetData.hash, + name: assetData.name, + type: assetData.type, + }; + + bundle.addAsset(img); + + const ASSET_TEMPLATE = 'module.exports = require("AssetRegistry").registerAsset(%json);'; + const code = ASSET_TEMPLATE.replace('%json', JSON.stringify(img)); + + return new ModuleTransport({ + code: code, + sourceCode: code, + sourcePath: module.path, + virtual: true, + }); + }); + } +} + +function generateJSONModule(module) { + return readFile(module.path).then(function(data) { + const code = 'module.exports = ' + data.toString('utf8') + ';'; + + return new ModuleTransport({ + code: code, + sourceCode: code, + sourcePath: module.path, + virtual: true, + }); + }); +} + +function getPathRelativeToRoot(roots, absPath) { + for (let i = 0; i < roots.length; i++) { + const relPath = path.relative(roots[i], absPath); + if (relPath[0] !== '.') { + return relPath; + } + } + + throw new Error( + 'Expected root module to be relative to one of the project roots' + ); +} + +function verifyRootExists(root) { + // Verify that the root exists. + assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory'); +} + +class DummyCache { + get(filepath, field, loaderCb) { + return loaderCb(); + } + + end(){} + invalidate(filepath){} +} +module.exports = Bundler; diff --git a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js new file mode 100644 index 000000000..cca9c8a7e --- /dev/null +++ b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js @@ -0,0 +1,256 @@ +/** + * 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'; + +jest + .dontMock('../index'); + +const Promise = require('promise'); + +describe('BundlesLayout', () => { + var BundlesLayout; + var DependencyResolver; + + beforeEach(() => { + BundlesLayout = require('../index'); + DependencyResolver = require('../../DependencyResolver'); + }); + + describe('generate', () => { + function newBundlesLayout() { + return new BundlesLayout({ + dependencyResolver: new DependencyResolver(), + }); + } + + function isPolyfill() { + return false; + } + + 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; + } + }); + + 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; + } + }); + + 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'], + 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().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', '/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().generateLayout(['/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 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(); + 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'); + }); + }); + }); +}); diff --git a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js new file mode 100644 index 000000000..f834ccf57 --- /dev/null +++ b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js @@ -0,0 +1,612 @@ +/** + * 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'; + +jest + .dontMock('absolute-path') + .dontMock('crypto') + .dontMock('underscore') + .dontMock('path') + .dontMock('../index') + .dontMock('../../lib/getAssetDataFromName') + .dontMock('../../DependencyResolver/crawlers') + .dontMock('../../DependencyResolver/crawlers/node') + .dontMock('../../DependencyResolver/DependencyGraph/docblock') + .dontMock('../../DependencyResolver/fastfs') + .dontMock('../../DependencyResolver/replacePatterns') + .dontMock('../../DependencyResolver') + .dontMock('../../DependencyResolver/DependencyGraph') + .dontMock('../../DependencyResolver/AssetModule_DEPRECATED') + .dontMock('../../DependencyResolver/AssetModule') + .dontMock('../../DependencyResolver/Module') + .dontMock('../../DependencyResolver/Package') + .dontMock('../../DependencyResolver/Polyfill') + .dontMock('../../DependencyResolver/ModuleCache'); + +const Promise = require('promise'); +const path = require('path'); + +jest.mock('fs'); + +describe('BundlesLayout', () => { + var BundlesLayout; + var Cache; + var DependencyResolver; + var fileWatcher; + var fs; + + const polyfills = [ + 'polyfills/prelude_dev.js', + 'polyfills/prelude.js', + 'polyfills/require.js', + 'polyfills/polyfills.js', + 'polyfills/console.js', + 'polyfills/error-guard.js', + 'polyfills/String.prototype.es6.js', + 'polyfills/Array.prototype.es6.js', + ]; + const baseFs = getBaseFs(); + + beforeEach(() => { + fs = require('fs'); + BundlesLayout = require('../index'); + Cache = require('../../Cache'); + DependencyResolver = require('../../DependencyResolver'); + + fileWatcher = { + on: () => this, + isWatchman: () => Promise.resolve(false) + }; + }); + + describe('generate', () => { + function newBundlesLayout() { + const resolver = new DependencyResolver({ + projectRoots: ['/root', '/' + __dirname.split('/')[1]], + fileWatcher: fileWatcher, + cache: new Cache(), + assetExts: ['js', 'png'], + assetRoots: ['/root'], + }); + + return new BundlesLayout({dependencyResolver: resolver}); + } + + function stripPolyfills(bundle) { + return Promise + .all(bundle.children.map(childModule => stripPolyfills(childModule))) + .then(children => { + const modules = bundle.modules + .filter(moduleName => { // filter polyfills + for (let p of polyfills) { + if (moduleName.indexOf(p) !== -1) { + return false; + } + } + return true; + }); + + return { + id: bundle.id, + modules: modules, + children: children, + }; + }); + } + + function setMockFilesystem(mockFs) { + fs.__setMockFilesystem(Object.assign(mockFs, baseFs)); + } + + pit('should bundle single-module app', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */`, + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [], + }) + ) + ); + }); + + pit('should bundle dependant modules', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + require("a");`, + 'a.js': ` + /** + * @providesModule a + */`, + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js', '/root/a.js'], + children: [], + }) + ) + ); + }); + + pit('should split bundles for async dependencies', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("a");`, + 'a.js': ` + /**, + * @providesModule a + */`, + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/a.js'], + children: [], + }], + }) + ) + ); + }); + + pit('should split into multiple bundles separate async dependencies', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("a"); + System.import("b");`, + 'a.js': ` + /**, + * @providesModule a + */`, + 'b.js': ` + /** + * @providesModule b + */`, + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [ + { + id: 'bundle.0.1', + modules: ['/root/a.js'], + children: [], + }, { + id: 'bundle.0.2', + modules: ['/root/b.js'], + children: [], + }, + ], + }) + ) + ); + }); + + pit('should fully traverse sync dependencies', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + require("a"); + System.import("b");`, + 'a.js': ` + /**, + * @providesModule a + */`, + 'b.js': ` + /** + * @providesModule b + */`, + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js', '/root/a.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/b.js'], + children: [], + }], + }) + ) + ); + }); + + pit('should include sync dependencies async dependencies might have', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("a");`, + 'a.js': ` + /**, + * @providesModule a + */, + require("b");`, + 'b.js': ` + /** + * @providesModule b + */ + require("c");`, + 'c.js': ` + /** + * @providesModule c + */`, + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/a.js', '/root/b.js', '/root/c.js'], + children: [], + }], + }) + ) + ); + }); + + pit('should allow duplicated dependencies across bundles', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("a"); + System.import("b");`, + 'a.js': ` + /**, + * @providesModule a + */, + require("c");`, + 'b.js': ` + /** + * @providesModule b + */ + require("c");`, + 'c.js': ` + /** + * @providesModule c + */`, + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [ + { + id: 'bundle.0.1', + modules: ['/root/a.js', '/root/c.js'], + children: [], + }, + { + id: 'bundle.0.2', + modules: ['/root/b.js', '/root/c.js'], + children: [], + }, + ], + }) + ) + ); + }); + + pit('should put in separate bundles async dependencies of async dependencies', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("a");`, + 'a.js': ` + /**, + * @providesModule a + */, + System.import("b");`, + 'b.js': ` + /** + * @providesModule b + */ + require("c");`, + 'c.js': ` + /** + * @providesModule c + */`, + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).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', '/root/c.js'], + children: [], + }], + }, + ], + }) + ) + ); + }); + + pit('should put image dependencies into separate bundles', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("a");`, + 'a.js':` + /**, + * @providesModule a + */, + require("./img.png");`, + 'img.png': '', + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/a.js', '/root/img.png'], + children: [], + }], + }) + ) + ); + }); + + pit('should put image dependencies across bundles', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("a"); + System.import("b");`, + 'a.js':` + /**, + * @providesModule a + */, + require("./img.png");`, + 'b.js':` + /**, + * @providesModule b + */, + require("./img.png");`, + 'img.png': '', + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [ + { + id: 'bundle.0.1', + modules: ['/root/a.js', '/root/img.png'], + children: [], + }, + { + id: 'bundle.0.2', + modules: ['/root/b.js', '/root/img.png'], + children: [], + }, + ], + }) + ) + ); + }); + + pit('could async require asset', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("./img.png");`, + 'img.png': '', + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/img.png'], + children: [], + }], + }) + ) + ); + }); + + pit('should include deprecated assets into separate bundles', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("a");`, + 'a.js':` + /**, + * @providesModule a + */, + require("image!img");`, + 'img.png': '', + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/a.js', '/root/img.png'], + children: [], + }], + }) + ) + ); + }); + + pit('could async require deprecated asset', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("image!img");`, + 'img.png': '', + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/img.png'], + children: [], + }], + }) + ) + ); + }); + + pit('should put packages into bundles', () => { + setMockFilesystem({ + 'root': { + 'index.js': ` + /** + * @providesModule index + */ + System.import("aPackage");`, + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: './main.js', + browser: { + './main.js': './client.js', + }, + }), + 'main.js': 'some other code', + 'client.js': 'some code', + }, + } + }); + + return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + stripPolyfills(bundles).then(resolvedBundles => + expect(resolvedBundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/aPackage/client.js'], + children: [], + }], + }) + ) + ); + }); + }); + + function getBaseFs() { + const p = path.join(__dirname, '../../../DependencyResolver/polyfills').substring(1); + const root = {}; + let currentPath = root; + + p.split('/').forEach(part => { + const child = {}; + currentPath[part] = child; + currentPath = child; + }); + + polyfills.forEach(polyfill => + currentPath[polyfill.split('/')[1]] = '' + ); + + return root; + } +}); diff --git a/packager/react-packager/src/BundlesLayout/index.js b/packager/react-packager/src/BundlesLayout/index.js new file mode 100644 index 000000000..c6da31573 --- /dev/null +++ b/packager/react-packager/src/BundlesLayout/index.js @@ -0,0 +1,117 @@ +/** + * 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 _ = require('underscore'); +const declareOpts = require('../lib/declareOpts'); + +const validateOpts = declareOpts({ + dependencyResolver: { + type: 'object', + required: true, + }, +}); + +const BUNDLE_PREFIX = 'bundle'; + +/** + * Class that takes care of separating the graph of dependencies into + * separate bundles + */ +class BundlesLayout { + constructor(options) { + const opts = validateOpts(options); + this._resolver = opts.dependencyResolver; + + this._moduleToBundle = Object.create(null); + } + + generateLayout(entryPaths, isDev) { + var currentBundleID = 0; + const rootBundle = { + id: BUNDLE_PREFIX + '.' + currentBundleID++, + modules: [], + children: [], + }; + var pending = [{paths: entryPaths, bundle: rootBundle}]; + + return promiseWhile( + () => pending.length > 0, + () => rootBundle, + () => { + const {paths, bundle} = pending.shift(); + + // pending sync dependencies we still need to explore for the current + // pending dependency + const pendingSyncDeps = paths; + + // accum variable for sync dependencies of the current pending + // dependency we're processing + const syncDependencies = Object.create(null); + + return promiseWhile( + () => pendingSyncDeps.length > 0, + () => { + const dependencies = Object.keys(syncDependencies); + if (dependencies.length > 0) { + bundle.modules = dependencies; + } + }, + index => { + const pendingSyncDep = pendingSyncDeps.shift(); + return this._resolver + .getDependencies(pendingSyncDep, {dev: isDev}) + .then(deps => { + deps.dependencies.forEach(dep => { + if (dep.path !== pendingSyncDep && !dep.isPolyfill()) { + pendingSyncDeps.push(dep.path); + } + syncDependencies[dep.path] = true; + this._moduleToBundle[dep.path] = bundle.id; + }); + deps.asyncDependencies.forEach(asyncDeps => { + const childBundle = { + id: bundle.id + '.' + currentBundleID++, + modules: [], + children: [], + }; + + bundle.children.push(childBundle); + pending.push({paths: asyncDeps, bundle: childBundle}); + }); + }); + }, + ); + }, + ); + } + + getBundleIDForModule(path) { + return this._moduleToBundle[path]; + } +} + +// Runs the body Promise meanwhile the condition callback is satisfied. +// Once it's not satisfied anymore, it returns what the results callback +// indicates +function promiseWhile(condition, result, body) { + return _promiseWhile(condition, result, body, 0); +} + +function _promiseWhile(condition, result, body, index) { + if (!condition()) { + return Promise.resolve(result()); + } + + return body(index).then(() => + _promiseWhile(condition, result, body, index + 1) + ); +} + +module.exports = BundlesLayout; diff --git a/packager/react-packager/src/Cache/__mocks__/Cache.js b/packager/react-packager/src/Cache/__mocks__/Cache.js index 6f7632f66..1376f2754 100644 --- a/packager/react-packager/src/Cache/__mocks__/Cache.js +++ b/packager/react-packager/src/Cache/__mocks__/Cache.js @@ -8,13 +8,26 @@ */ 'use strict'; -class Cache { - get(filepath, field, cb) { - return cb(filepath); - } +const mockColor = () => { + return { + bold: () => { return { }; }, + }; +}; - invalidate(filepath) { } - end() { } -} +mockColor.bold = function() { + return {}; +}; -module.exports = Cache; +module.exports = { + dim: s => s, + magenta: mockColor, + white: mockColor, + blue: mockColor, + yellow: mockColor, + green: mockColor, + bold: mockColor, + red: mockColor, + cyan: mockColor, + gray: mockColor, + black: mockColor, +}; diff --git a/packager/react-packager/src/Cache/index.js b/packager/react-packager/src/Cache/index.js index 2ed3575e4..ae6d1aa35 100644 --- a/packager/react-packager/src/Cache/index.js +++ b/packager/react-packager/src/Cache/index.js @@ -215,6 +215,7 @@ class Cache { hash.update(options.transformModulePath); var name = 'react-packager-cache-' + hash.digest('hex'); + return path.join(tmpdir, name); } } diff --git a/packager/react-packager/src/DependencyResolver/AssetModule.js b/packager/react-packager/src/DependencyResolver/AssetModule.js index bfe4b6f88..2e1031599 100644 --- a/packager/react-packager/src/DependencyResolver/AssetModule.js +++ b/packager/react-packager/src/DependencyResolver/AssetModule.js @@ -5,6 +5,13 @@ const Promise = require('promise'); const getAssetDataFromName = require('../lib/getAssetDataFromName'); class AssetModule extends Module { + constructor(...args) { + super(...args); + const { resolution, name, type } = getAssetDataFromName(this.path); + this.resolution = resolution; + this._name = name; + this._type = type; + } isHaste() { return Promise.resolve(false); @@ -14,33 +21,31 @@ class AssetModule extends Module { return Promise.resolve([]); } + getAsyncDependencies() { + return Promise.resolve([]); + } + _read() { return Promise.resolve({}); } getName() { - return super.getName().then(id => { - const {name, type} = getAssetDataFromName(this.path); - return id.replace(/\/[^\/]+$/, `/${name}.${type}`); - }); - } - - getPlainObject() { - return this.getName().then(name => this.addReference({ - path: this.path, - isJSON: false, - isAsset: true, - isAsset_DEPRECATED: false, - isPolyfill: false, - resolution: getAssetDataFromName(this.path).resolution, - id: name, - dependencies: [], - })); + return super.getName().then( + id => id.replace(/\/[^\/]+$/, `/${this._name}.${this._type}`) + ); } hash() { return `AssetModule : ${this.path}`; } + + isJSON() { + return false; + } + + isAsset() { + return true; + } } module.exports = AssetModule; diff --git a/packager/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js b/packager/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js index fd4cb7081..19817d4b7 100644 --- a/packager/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js +++ b/packager/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js @@ -5,36 +5,45 @@ const Promise = require('promise'); const getAssetDataFromName = require('../lib/getAssetDataFromName'); class AssetModule_DEPRECATED extends Module { + constructor(...args) { + super(...args); + const {resolution, name} = getAssetDataFromName(this.path); + this.resolution = resolution; + this.name = name; + } + isHaste() { return Promise.resolve(false); } getName() { - return Promise.resolve(this.name); + return Promise.resolve(`image!${this.name}`); } getDependencies() { return Promise.resolve([]); } - getPlainObject() { - const {name, resolution} = getAssetDataFromName(this.path); - - return Promise.resolve(this.addReference({ - path: this.path, - id: `image!${name}`, - resolution, - isAsset_DEPRECATED: true, - dependencies: [], - isJSON: false, - isPolyfill: false, - isAsset: false, - })); + getAsyncDependencies() { + return Promise.resolve([]); } hash() { return `AssetModule_DEPRECATED : ${this.path}`; } + + isJSON() { + return false; + } + + isAsset_DEPRECATED() { + return true; + } + + resolution() { + return getAssetDataFromName(this.path).resolution; + } + } module.exports = AssetModule_DEPRECATED; diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js index c5f060322..eab913839 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js @@ -36,6 +36,24 @@ describe('DependencyGraph', function() { var fileWatcher; var fs; + function getOrderedDependenciesAsJSON(dgraph, entry) { + return dgraph.getOrderedDependencies(entry).then( + deps => Promise.all(deps.map(dep => Promise.all([ + dep.getName(), + dep.getDependencies(), + ]).then(([name, dependencies]) => ({ + path: dep.path, + isJSON: dep.isJSON(), + isAsset: dep.isAsset(), + isAsset_DEPRECATED: dep.isAsset_DEPRECATED(), + isPolyfill: dep.isPolyfill(), + resolution: dep.resolution, + id: name, + dependencies + }))) + )); + } + beforeEach(function() { fs = require('fs'); Cache = require('../../../Cache'); @@ -76,7 +94,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -134,7 +152,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -186,7 +204,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -246,7 +264,7 @@ describe('DependencyGraph', function() { assetRoots_DEPRECATED: ['/root/imgs'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -298,7 +316,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -355,7 +373,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -433,7 +451,7 @@ describe('DependencyGraph', function() { assetRoots_DEPRECATED: ['/root/imgs'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -495,7 +513,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -548,7 +566,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -601,7 +619,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -662,7 +680,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -719,7 +737,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -770,7 +788,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -820,7 +838,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -867,7 +885,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -918,7 +936,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -967,7 +985,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1021,7 +1039,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/somedir/somefile.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/somedir/somefile.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1079,7 +1097,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1127,7 +1145,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1174,7 +1192,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1233,7 +1251,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1292,7 +1310,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1371,7 +1389,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1428,7 +1446,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1485,7 +1503,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1542,7 +1560,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1614,7 +1632,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { id: 'index', @@ -1717,7 +1735,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { id: 'index', @@ -1798,7 +1816,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1880,7 +1898,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.ios.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { expect(deps) .toEqual([ { @@ -1963,7 +1981,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2060,7 +2078,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2146,7 +2164,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2251,7 +2269,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2320,7 +2338,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/react-tools/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/react-tools/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2377,7 +2395,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2422,7 +2440,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2483,7 +2501,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.ios.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2539,7 +2557,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.ios.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2588,7 +2606,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.ios.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.ios.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2672,11 +2690,11 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function() { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { filesystem.root['index.js'] = filesystem.root['index.js'].replace('require("foo")', ''); triggerFileChange('change', 'index.js', root, mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2737,11 +2755,11 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function() { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { filesystem.root['index.js'] = filesystem.root['index.js'].replace('require("foo")', ''); triggerFileChange('change', 'index.js', root, mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2802,10 +2820,10 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function() { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { delete filesystem.root.foo; triggerFileChange('delete', 'foo.js', root); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2866,7 +2884,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function() { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { filesystem.root['bar.js'] = [ '/**', ' * @providesModule bar', @@ -2878,7 +2896,7 @@ describe('DependencyGraph', function() { filesystem.root.aPackage['main.js'] = 'require("bar")'; triggerFileChange('change', 'aPackage/main.js', root, mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2949,7 +2967,7 @@ describe('DependencyGraph', function() { cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -2968,7 +2986,7 @@ describe('DependencyGraph', function() { filesystem.root['foo.png'] = ''; triggerFileChange('add', 'foo.png', root, mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) { expect(deps2) .toEqual([ { @@ -3021,7 +3039,7 @@ describe('DependencyGraph', function() { cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { id: 'index', @@ -3039,7 +3057,7 @@ describe('DependencyGraph', function() { filesystem.root['foo.png'] = ''; triggerFileChange('add', 'foo.png', root, mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) { expect(deps2) .toEqual([ { @@ -3108,7 +3126,7 @@ describe('DependencyGraph', function() { }, cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function() { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { filesystem.root['bar.js'] = [ '/**', ' * @providesModule bar', @@ -3120,7 +3138,7 @@ describe('DependencyGraph', function() { filesystem.root.aPackage['main.js'] = 'require("bar")'; triggerFileChange('change', 'aPackage/main.js', root, mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -3193,11 +3211,11 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function() { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { triggerFileChange('change', 'aPackage', '/root', { isDirectory: function(){ return true; } }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -3264,7 +3282,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function() { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { filesystem.root['index.js'] = filesystem.root['index.js'].replace(/aPackage/, 'bPackage'); triggerFileChange('change', 'index.js', root, mockStat); @@ -3274,7 +3292,7 @@ describe('DependencyGraph', function() { }); triggerFileChange('change', 'package.json', '/root/aPackage', mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -3331,7 +3349,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function() { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { filesystem.root.aPackage['package.json'] = JSON.stringify({ name: 'aPackage', main: 'main.js', @@ -3339,7 +3357,7 @@ describe('DependencyGraph', function() { }); triggerFileChange('change', 'package.json', '/root/aPackage', mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -3396,14 +3414,14 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function() { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function() { filesystem.root.aPackage['package.json'] = JSON.stringify({ name: 'bPackage', main: 'main.js', }); triggerFileChange('change', 'package.json', '/root/aPackage', mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -3459,7 +3477,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { expect(deps) .toEqual([ { @@ -3500,7 +3518,7 @@ describe('DependencyGraph', function() { filesystem.root.node_modules.foo['main.js'] = 'lol'; triggerFileChange('change', 'main.js', '/root/node_modules/foo', mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) { expect(deps2) .toEqual([ { @@ -3559,7 +3577,7 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], cache: cache, }); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) { filesystem.root.node_modules.foo['package.json'] = JSON.stringify({ name: 'foo', main: 'main.js', @@ -3567,7 +3585,7 @@ describe('DependencyGraph', function() { }); triggerFileChange('change', 'package.json', '/root/node_modules/foo', mockStat); - return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps2) { expect(deps2) .toEqual([ { diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js index 7e7185829..f2b77f4d5 100644 --- a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js @@ -13,7 +13,6 @@ const AssetModule_DEPRECATED = require('../AssetModule_DEPRECATED'); const Fastfs = require('../fastfs'); const ModuleCache = require('../ModuleCache'); const Promise = require('promise'); -const _ = require('underscore'); const crawl = require('../crawlers'); const debug = require('debug')('DependencyGraph'); const declareOpts = require('../../lib/declareOpts'); @@ -70,7 +69,7 @@ class DependencyGraph { constructor(options) { this._opts = validateOpts(options); this._hasteMap = Object.create(null); - this._immediateResolutionCache = Object.create(null); + this._resetResolutionCache(); this._cache = this._opts.cache; this.load(); } @@ -80,7 +79,8 @@ class DependencyGraph { return this._loading; } - const crawlActivity = Activity.startEvent('fs crawl'); + const depGraphActivity = Activity.startEvent('Building Dependency Graph'); + const crawlActivity = Activity.startEvent('Crawling File System'); const allRoots = this._opts.roots.concat(this._opts.assetRoots_DEPRECATED); this._crawling = crawl(allRoots, { ignore: this._opts.ignoreFilePath, @@ -89,10 +89,15 @@ class DependencyGraph { }); this._crawling.then((files) => Activity.endEvent(crawlActivity)); - this._fastfs = new Fastfs(this._opts.roots, this._opts.fileWatcher, { - ignore: this._opts.ignoreFilePath, - crawling: this._crawling, - }); + this._fastfs = new Fastfs( + 'JavaScript', + this._opts.roots, + this._opts.fileWatcher, + { + ignore: this._opts.ignoreFilePath, + crawling: this._crawling, + } + ); this._fastfs.on('change', this._processFileChange.bind(this)); @@ -102,19 +107,31 @@ class DependencyGraph { this._fastfs.build() .then(() => { const hasteActivity = Activity.startEvent('Building Haste Map'); - this._buildHasteMap().then(() => Activity.endEvent(hasteActivity)); + return this._buildHasteMap().then(() => Activity.endEvent(hasteActivity)); }), this._buildAssetMap_DEPRECATED(), - ]); + ]).then(() => + Activity.endEvent(depGraphActivity) + ); return this._loading; } - resolveDependency(fromModule, toModuleName) { - if (fromModule._ref) { - fromModule = fromModule._ref; + setup({ platform }) { + if (platform && this._opts.platforms.indexOf(platform) === -1) { + throw new Error('Unrecognized platform: ' + platform); } + // TODO(amasad): This is a potential race condition. Mutliple requests could + // interfere with each other. This needs a refactor to fix -- which will + // follow this diff. + if (this._platformExt !== platform) { + this._resetResolutionCache(); + } + this._platformExt = platform; + } + + resolveDependency(fromModule, toModuleName) { const resHash = resolutionHash(fromModule.path, toModuleName); if (this._immediateResolutionCache[resHash]) { @@ -163,33 +180,7 @@ class DependencyGraph { getOrderedDependencies(entryPath) { return this.load().then(() => { - const absPath = this._getAbsolutePath(entryPath); - - if (absPath == null) { - throw new NotFoundError( - 'Could not find source file at %s', - entryPath - ); - } - - const absolutePath = path.resolve(absPath); - - if (absolutePath == null) { - throw new NotFoundError( - 'Cannot find entry file %s in any of the roots: %j', - entryPath, - this._opts.roots - ); - } - - const platformExt = getPlatformExt(entryPath); - if (platformExt && this._opts.platforms.indexOf(platformExt) > -1) { - this._platformExt = platformExt; - } else { - this._platformExt = null; - } - - const entry = this._moduleCache.getModule(absolutePath); + const entry = this._getModuleForEntryPath(entryPath); const deps = []; const visited = Object.create(null); visited[entry.hash()] = true; @@ -226,7 +217,22 @@ class DependencyGraph { }; return collect(entry) - .then(() => Promise.all(deps.map(dep => dep.getPlainObject()))); + .then(() => deps); + }); + } + + getAsyncDependencies(entryPath) { + return this.load().then(() => { + const mod = this._getModuleForEntryPath(entryPath); + return mod.getAsyncDependencies().then(bundles => + Promise + .all(bundles.map(bundle => + Promise.all(bundle.map( + dep => this.resolveDependency(mod, dep) + )) + )) + .then(bs => bs.map(bundle => bundle.map(dep => dep.path))) + ); }); } @@ -246,6 +252,39 @@ class DependencyGraph { return null; } + _getModuleForEntryPath(entryPath) { + const absPath = this._getAbsolutePath(entryPath); + + if (absPath == null) { + throw new NotFoundError( + 'Could not find source file at %s', + entryPath + ); + } + + const absolutePath = path.resolve(absPath); + + if (absolutePath == null) { + throw new NotFoundError( + 'Cannot find entry file %s in any of the roots: %j', + entryPath, + this._opts.roots + ); + } + + // `platformExt` could be set in the `setup` method. + if (!this._platformExt) { + const platformExt = getPlatformExt(entryPath); + if (platformExt && this._opts.platforms.indexOf(platformExt) > -1) { + this._platformExt = platformExt; + } else { + this._platformExt = null; + } + } + + return this._moduleCache.getModule(absolutePath); + } + _resolveHasteDependency(fromModule, toModuleName) { toModuleName = normalizePath(toModuleName); @@ -510,6 +549,7 @@ class DependencyGraph { this._assetMap_DEPRECATED = Object.create(null); const fastfs = new Fastfs( + 'Assets', this._opts.assetRoots_DEPRECATED, this._opts.fileWatcher, { ignore: this._opts.ignoreFilePath, crawling: this._crawling } @@ -549,7 +589,7 @@ class DependencyGraph { _processFileChange(type, filePath, root, fstat) { // It's really hard to invalidate the right module resolution cache // so we just blow it up with every file change. - this._immediateResolutionCache = Object.create(null); + this._resetResolutionCache(); const absPath = path.join(root, filePath); if ((fstat && fstat.isDirectory()) || @@ -585,6 +625,10 @@ class DependencyGraph { }); } } + + _resetResolutionCache() { + this._immediateResolutionCache = Object.create(null); + } } function assetName(file, ext) { diff --git a/packager/react-packager/src/DependencyResolver/Module.js b/packager/react-packager/src/DependencyResolver/Module.js index 3f1b13efa..745ab3fb0 100644 --- a/packager/react-packager/src/DependencyResolver/Module.js +++ b/packager/react-packager/src/DependencyResolver/Module.js @@ -1,6 +1,13 @@ +/** + * 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 Promise = require('promise'); const docblock = require('./DependencyGraph/docblock'); const isAbsolutePath = require('absolute-path'); const path = require('path'); @@ -69,6 +76,10 @@ class Module { this._cache.invalidate(this.path); } + getAsyncDependencies() { + return this._read().then(data => data.asyncDependencies); + } + _read() { if (!this._reading) { this._reading = this._fastfs.readFile(this.path).then(content => { @@ -85,7 +96,9 @@ class Module { if ('extern' in moduleDocBlock) { data.dependencies = []; } else { - data.dependencies = extractRequires(content); + var dependencies = extractRequires(content); + data.dependencies = dependencies.sync; + data.asyncDependencies = dependencies.async; } return data; @@ -95,49 +108,73 @@ class Module { return this._reading; } - getPlainObject() { - return Promise.all([ - this.getName(), - this.getDependencies(), - ]).then(([name, dependencies]) => this.addReference({ - path: this.path, - isJSON: path.extname(this.path) === '.json', - isAsset: false, - isAsset_DEPRECATED: false, - isPolyfill: false, - resolution: undefined, - id: name, - dependencies - })); - } - hash() { return `Module : ${this.path}`; } - addReference(obj) { - Object.defineProperty(obj, '_ref', { value: this }); - return obj; + isJSON() { + return path.extname(this.path) === '.json'; + } + + isAsset() { + return false; + } + + isPolyfill() { + return false; + } + + isAsset_DEPRECATED() { + return false; + } + + toJSON() { + return { + hash: this.hash(), + isJSON: this.isJSON(), + isAsset: this.isAsset(), + isAsset_DEPRECATED: this.isAsset_DEPRECATED(), + type: this.type, + path: this.path, + }; } } /** * Extract all required modules from a `code` string. */ -var blockCommentRe = /\/\*(.|\n)*?\*\//g; -var lineCommentRe = /\/\/.+(\n|$)/g; +const blockCommentRe = /\/\*(.|\n)*?\*\//g; +const lineCommentRe = /\/\/.+(\n|$)/g; function extractRequires(code /*: string*/) /*: Array*/ { - var deps = []; + var deps = { + sync: [], + async: [], + }; code .replace(blockCommentRe, '') .replace(lineCommentRe, '') + // Parse sync dependencies. See comment below for further detils. .replace(replacePatterns.IMPORT_RE, (match, pre, quot, dep, post) => { - deps.push(dep); + deps.sync.push(dep); return match; }) - .replace(replacePatterns.REQUIRE_RE, function(match, pre, quot, dep, post) { - deps.push(dep); + // Parse the sync dependencies this module has. When the module is + // required, all it's sync dependencies will be loaded into memory. + // Sync dependencies can be defined either using `require` or the ES6 + // `import` syntax: + // var dep1 = require('dep1'); + .replace(replacePatterns.REQUIRE_RE, (match, pre, quot, dep, post) => { + deps.sync.push(dep); + }) + // Parse async dependencies this module has. As opposed to what happens + // with sync dependencies, when the module is required, it's async + // dependencies won't be loaded into memory. This is deferred till the + // code path gets to the import statement: + // System.import('dep1') + .replace(replacePatterns.SYSTEM_IMPORT_RE, (match, pre, quot, dep, post) => { + deps.async.push([dep]); + return match; }); return deps; diff --git a/packager/react-packager/src/DependencyResolver/Polyfill.js b/packager/react-packager/src/DependencyResolver/Polyfill.js new file mode 100644 index 000000000..752b864b8 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/Polyfill.js @@ -0,0 +1,38 @@ +'use strict'; + +const Promise = require('promise'); +const Module = require('./Module'); + +class Polyfill extends Module { + constructor({ path, id, dependencies }) { + super(path); + this._id = id; + this._dependencies = dependencies; + } + + isHaste() { + return Promise.resolve(false); + } + + getName() { + return Promise.resolve(this._id); + } + + getPackage() { + return null; + } + + getDependencies() { + return Promise.resolve(this._dependencies); + } + + isJSON() { + return false; + } + + isPolyfill() { + return true; + } +} + +module.exports = Polyfill; diff --git a/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js b/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js index 2ac854e4c..be71b3a00 100644 --- a/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js +++ b/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js @@ -9,23 +9,24 @@ 'use strict'; jest.dontMock('../') - .dontMock('q') - .dontMock('../replacePatterns') - .setMock('chalk', { dim: function(s) { return s; } }); + .dontMock('underscore') + .dontMock('../replacePatterns'); jest.mock('path'); var Promise = require('promise'); +var _ = require('underscore'); describe('HasteDependencyResolver', function() { var HasteDependencyResolver; - - function createModule(o) { - o.getPlainObject = () => Promise.resolve(o); - return o; - } + var Module; + var Polyfill; beforeEach(function() { + Module = require('../Module'); + Polyfill = require('../Polyfill'); + Polyfill.mockClear(); + // For the polyfillDeps require('path').join.mockImpl(function(a, b) { return b; @@ -33,12 +34,16 @@ describe('HasteDependencyResolver', function() { HasteDependencyResolver = require('../'); }); + function createModule(id, dependencies) { + var module = new Module(); + module.getName.mockImpl(() => Promise.resolve(id)); + module.getDependencies.mockImpl(() => Promise.resolve(dependencies)); + return module; + } + describe('getDependencies', function() { pit('should get dependencies with polyfills', function() { - var module = createModule({ - id: 'index', - path: '/root/index.js', dependencies: ['a'] - }); + var module = createModule('index'); var deps = [module]; var depResolver = new HasteDependencyResolver({ @@ -57,7 +62,8 @@ describe('HasteDependencyResolver', function() { return depResolver.getDependencies('/root/index.js', { dev: false }) .then(function(result) { expect(result.mainModuleId).toEqual('index'); - expect(result.dependencies).toEqual([ + expect(result.dependencies[result.dependencies.length - 1]).toBe(module); + expect(_.pluck(Polyfill.mock.calls, 0)).toEqual([ { path: 'polyfills/prelude.js', id: 'polyfills/prelude.js', isPolyfill: true, @@ -115,18 +121,12 @@ describe('HasteDependencyResolver', function() { 'polyfills/String.prototype.es6.js', ], }, - module ]); }); }); pit('should get dependencies with polyfills', function() { - var module = createModule({ - id: 'index', - path: '/root/index.js', - dependencies: ['a'], - }); - + var module = createModule('index'); var deps = [module]; var depResolver = new HasteDependencyResolver({ @@ -145,75 +145,15 @@ describe('HasteDependencyResolver', function() { return depResolver.getDependencies('/root/index.js', { dev: true }) .then(function(result) { expect(result.mainModuleId).toEqual('index'); - expect(result.dependencies).toEqual([ - { path: 'polyfills/prelude_dev.js', - id: 'polyfills/prelude_dev.js', - isPolyfill: true, - dependencies: [] - }, - { path: 'polyfills/require.js', - id: 'polyfills/require.js', - isPolyfill: true, - dependencies: ['polyfills/prelude_dev.js'] - }, - { path: 'polyfills/polyfills.js', - id: 'polyfills/polyfills.js', - isPolyfill: true, - dependencies: ['polyfills/prelude_dev.js', 'polyfills/require.js'] - }, - { id: 'polyfills/console.js', - isPolyfill: true, - path: 'polyfills/console.js', - dependencies: [ - 'polyfills/prelude_dev.js', - 'polyfills/require.js', - 'polyfills/polyfills.js' - ], - }, - { id: 'polyfills/error-guard.js', - isPolyfill: true, - path: 'polyfills/error-guard.js', - dependencies: [ - 'polyfills/prelude_dev.js', - 'polyfills/require.js', - 'polyfills/polyfills.js', - 'polyfills/console.js' - ], - }, - { id: 'polyfills/String.prototype.es6.js', - isPolyfill: true, - path: 'polyfills/String.prototype.es6.js', - dependencies: [ - 'polyfills/prelude_dev.js', - 'polyfills/require.js', - 'polyfills/polyfills.js', - 'polyfills/console.js', - 'polyfills/error-guard.js' - ], - }, - { id: 'polyfills/Array.prototype.es6.js', - isPolyfill: true, - path: 'polyfills/Array.prototype.es6.js', - dependencies: [ - 'polyfills/prelude_dev.js', - 'polyfills/require.js', - 'polyfills/polyfills.js', - 'polyfills/console.js', - 'polyfills/error-guard.js', - 'polyfills/String.prototype.es6.js' - ], - }, - module - ]); + expect(depGraph.getOrderedDependencies).toBeCalledWith('/root/index.js'); + expect(result.dependencies[0]).toBe(Polyfill.mock.instances[0]); + expect(result.dependencies[result.dependencies.length - 1]) + .toBe(module); }); }); pit('should pass in more polyfills', function() { - var module = createModule({ - id: 'index', - path: '/root/index.js', - dependencies: ['a'] - }); + var module = createModule('index'); var deps = [module]; var depResolver = new HasteDependencyResolver({ @@ -231,66 +171,9 @@ describe('HasteDependencyResolver', function() { }); return depResolver.getDependencies('/root/index.js', { dev: false }) - .then(function(result) { + .then((result) => { expect(result.mainModuleId).toEqual('index'); - expect(result.dependencies).toEqual([ - { path: 'polyfills/prelude.js', - id: 'polyfills/prelude.js', - isPolyfill: true, - dependencies: [] - }, - { path: 'polyfills/require.js', - id: 'polyfills/require.js', - isPolyfill: true, - dependencies: ['polyfills/prelude.js'] - }, - { path: 'polyfills/polyfills.js', - id: 'polyfills/polyfills.js', - isPolyfill: true, - dependencies: ['polyfills/prelude.js', 'polyfills/require.js'] - }, - { id: 'polyfills/console.js', - isPolyfill: true, - path: 'polyfills/console.js', - dependencies: [ - 'polyfills/prelude.js', - 'polyfills/require.js', - 'polyfills/polyfills.js' - ], - }, - { id: 'polyfills/error-guard.js', - isPolyfill: true, - path: 'polyfills/error-guard.js', - dependencies: [ - 'polyfills/prelude.js', - 'polyfills/require.js', - 'polyfills/polyfills.js', - 'polyfills/console.js' - ], - }, - { id: 'polyfills/String.prototype.es6.js', - isPolyfill: true, - path: 'polyfills/String.prototype.es6.js', - dependencies: [ - 'polyfills/prelude.js', - 'polyfills/require.js', - 'polyfills/polyfills.js', - 'polyfills/console.js', - 'polyfills/error-guard.js' - ], - }, - { id: 'polyfills/Array.prototype.es6.js', - isPolyfill: true, - path: 'polyfills/Array.prototype.es6.js', - dependencies: [ - 'polyfills/prelude.js', - 'polyfills/require.js', - 'polyfills/polyfills.js', - 'polyfills/console.js', - 'polyfills/error-guard.js', - 'polyfills/String.prototype.es6.js', - ], - }, + expect(Polyfill.mock.calls[result.dependencies.length - 2]).toEqual([ { path: 'some module', id: 'some module', isPolyfill: true, @@ -304,7 +187,6 @@ describe('HasteDependencyResolver', function() { 'polyfills/Array.prototype.es6.js' ] }, - module ]); }); }); @@ -463,25 +345,21 @@ describe('HasteDependencyResolver', function() { depGraph.resolveDependency.mockImpl(function(fromModule, toModuleName) { if (toModuleName === 'x') { - return Promise.resolve(createModule({ - id: 'changed' - })); + return Promise.resolve(createModule('changed')); } else if (toModuleName === 'y') { - return Promise.resolve(createModule({ id: 'Y' })); + return Promise.resolve(createModule('Y')); } return Promise.resolve(null); }); - return depResolver.wrapModule({ - id: 'test module', - path: '/root/test.js', - dependencies: dependencies - }, code).then(processedCode => { - + return depResolver.wrapModule( + createModule('test module', ['x', 'y']), + code + ).then(processedCode => { expect(processedCode).toEqual([ - '__d(\'test module\',["changed","Y"],function(global,' + - ' require, requireDynamic, requireLazy, module, exports) { ' + + '__d(\'test module\',["changed","Y"],function(global, require,' + + ' module, exports) { ' + "import'x';", "import 'changed';", "import 'changed' ;", diff --git a/packager/react-packager/src/DependencyResolver/__tests__/Module-test.js b/packager/react-packager/src/DependencyResolver/__tests__/Module-test.js new file mode 100644 index 000000000..640157d94 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/__tests__/Module-test.js @@ -0,0 +1,104 @@ +/** + * 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'; + +jest + .dontMock('absolute-path') + .dontMock('../fastfs') + .dontMock('../replacePatterns') + .dontMock('../DependencyGraph/docblock') + .dontMock('../../FileWatcher') + .dontMock('../Module'); + +jest + .mock('fs'); + +describe('Module', () => { + var Fastfs; + var Module; + var ModuleCache; + var Promise; + var fs; + + const FileWatcher = require('../../FileWatcher'); + const fileWatcher = new FileWatcher(['/root']); + + beforeEach(function() { + Fastfs = require('../fastfs'); + Module = require('../Module'); + ModuleCache = require('../ModuleCache'); + Promise = require('promise'); + fs = require('fs'); + }); + + describe('Async Dependencies', () => { + function expectAsyncDependenciesToEqual(expected) { + var fastfs = new Fastfs( + 'test', + ['/root'], + fileWatcher, + {crawling: Promise.resolve(['/root/index.js']), ignore: []}, + ); + + return fastfs.build().then(() => { + var module = new Module('/root/index.js', fastfs, new ModuleCache(fastfs)); + + return module.getAsyncDependencies().then(actual => + expect(actual).toEqual(expected) + ); + }); + } + + pit('should recognize single dependency', () => { + fs.__setMockFilesystem({ + 'root': { + 'index.js': 'System.import("dep1")', + } + }); + + return expectAsyncDependenciesToEqual([['dep1']]); + }); + + pit('should parse single quoted dependencies', () => { + fs.__setMockFilesystem({ + 'root': { + 'index.js': 'System.import(\'dep1\')', + } + }); + + return expectAsyncDependenciesToEqual([['dep1']]); + }); + + pit('should parse multiple async dependencies on the same module', () => { + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + 'System.import("dep1")', + 'System.import("dep2")', + ].join('\n'), + } + }); + + return expectAsyncDependenciesToEqual([ + ['dep1'], + ['dep2'], + ]); + }); + + pit('parse fine new lines', () => { + fs.__setMockFilesystem({ + 'root': { + 'index.js': 'System.import(\n"dep1"\n)', + } + }); + + return expectAsyncDependenciesToEqual([['dep1']]); + }); + }); +}); diff --git a/packager/react-packager/src/DependencyResolver/fastfs.js b/packager/react-packager/src/DependencyResolver/fastfs.js index 884296023..a4c04232e 100644 --- a/packager/react-packager/src/DependencyResolver/fastfs.js +++ b/packager/react-packager/src/DependencyResolver/fastfs.js @@ -14,8 +14,9 @@ const stat = Promise.denodeify(fs.stat); const hasOwn = Object.prototype.hasOwnProperty; class Fastfs extends EventEmitter { - constructor(roots, fileWatcher, {ignore, crawling}) { + constructor(name, roots, fileWatcher, {ignore, crawling}) { super(); + this._name = name; this._fileWatcher = fileWatcher; this._ignore = ignore; this._roots = roots.map(root => new File(root, { isDir: true })); @@ -29,7 +30,7 @@ class Fastfs extends EventEmitter { ); return this._crawling.then(files => { - const fastfsActivity = Activity.startEvent('Building in-memory fs'); + const fastfsActivity = Activity.startEvent('Building in-memory fs for ' + this._name); files.forEach(filePath => { if (filePath.match(rootsPattern)) { const newFile = new File(filePath, { isDir: false }); @@ -155,7 +156,6 @@ class Fastfs extends EventEmitter { this._getAndAssertRoot(file.path).addChild(file); } - _processFileChange(type, filePath, root, fstat) { const absPath = path.join(root, filePath); if (this._ignore(absPath) || (fstat && fstat.isDirectory())) { diff --git a/packager/react-packager/src/DependencyResolver/index.js b/packager/react-packager/src/DependencyResolver/index.js index eae2e3da0..cc7f9468a 100644 --- a/packager/react-packager/src/DependencyResolver/index.js +++ b/packager/react-packager/src/DependencyResolver/index.js @@ -11,6 +11,7 @@ var path = require('path'); var DependencyGraph = require('./DependencyGraph'); var replacePatterns = require('./replacePatterns'); +var Polyfill = require('./Polyfill'); var declareOpts = require('../lib/declareOpts'); var Promise = require('promise'); @@ -26,10 +27,6 @@ var validateOpts = declareOpts({ type: 'array', default: [], }, - nonPersistent: { - type: 'boolean', - default: false, - }, moduleFormat: { type: 'string', default: 'haste', @@ -76,6 +73,10 @@ var getDependenciesValidateOpts = declareOpts({ type: 'boolean', default: true, }, + platform: { + type: 'string', + required: false, + }, }); HasteDependencyResolver.prototype.getDependencies = function(main, options) { @@ -83,18 +84,24 @@ HasteDependencyResolver.prototype.getDependencies = function(main, options) { var depGraph = this._depGraph; var self = this; - return depGraph.load().then( - () => depGraph.getOrderedDependencies(main).then( - dependencies => { - const mainModuleId = dependencies[0].id; + + depGraph.setup({ platform: opts.platform }); + + return Promise.all([ + depGraph.getOrderedDependencies(main), + depGraph.getAsyncDependencies(main), + ]).then( + ([dependencies, asyncDependencies]) => dependencies[0].getName().then( + mainModuleId => { self._prependPolyfillDependencies( dependencies, - opts.dev + opts.dev, ); return { - mainModuleId: mainModuleId, - dependencies: dependencies + mainModuleId, + dependencies, + asyncDependencies, }; } ) @@ -118,7 +125,7 @@ HasteDependencyResolver.prototype._prependPolyfillDependencies = function( ].concat(this._polyfillModuleNames); var polyfillModules = polyfillModuleNames.map( - (polyfillModuleName, idx) => ({ + (polyfillModuleName, idx) => new Polyfill({ path: polyfillModuleName, id: polyfillModuleName, dependencies: polyfillModuleNames.slice(0, idx), @@ -130,23 +137,26 @@ HasteDependencyResolver.prototype._prependPolyfillDependencies = function( }; HasteDependencyResolver.prototype.wrapModule = function(module, code) { - if (module.isPolyfill) { + if (module.isPolyfill()) { return Promise.resolve(code); } const resolvedDeps = Object.create(null); const resolvedDepsArr = []; - return Promise.all( - module.dependencies.map(depName => { - return this._depGraph.resolveDependency(module, depName) - .then((dep) => dep && dep.getPlainObject().then(mod => { - if (mod) { - resolvedDeps[depName] = mod.id; - resolvedDepsArr.push(mod.id); - } - })); - }) + return module.getDependencies().then( + dependencies => Promise.all(dependencies.map( + depName => this._depGraph.resolveDependency(module, depName) + .then(depModule => { + if (depModule) { + return depModule.getName().then(name => { + resolvedDeps[depName] = name; + resolvedDepsArr.push(name); + }); + } + }) + ) + ) ).then(() => { const relativizeCode = (codeMatch, pre, quot, depName, post) => { const depId = resolvedDeps[depName]; @@ -157,13 +167,15 @@ HasteDependencyResolver.prototype.wrapModule = function(module, code) { } }; - return defineModuleCode({ - code: code + return module.getName().then( + name => defineModuleCode({ + code: code .replace(replacePatterns.IMPORT_RE, relativizeCode) .replace(replacePatterns.REQUIRE_RE, relativizeCode), - deps: JSON.stringify(resolvedDepsArr), - moduleName: module.id, - }); + deps: JSON.stringify(resolvedDepsArr), + moduleName: name, + }) + ); }); }; @@ -176,8 +188,7 @@ function defineModuleCode({moduleName, code, deps}) { `__d(`, `'${moduleName}',`, `${deps},`, - 'function(global, require, ', - 'requireDynamic, requireLazy, module, exports) {', + 'function(global, require, module, exports) {', ` ${code}`, '\n});', ].join(''); diff --git a/packager/react-packager/src/DependencyResolver/polyfills/console.js b/packager/react-packager/src/DependencyResolver/polyfills/console.js index ff2ff39ff..e04597402 100644 --- a/packager/react-packager/src/DependencyResolver/polyfills/console.js +++ b/packager/react-packager/src/DependencyResolver/polyfills/console.js @@ -376,6 +376,12 @@ var str = Array.prototype.map.call(arguments, function(arg) { return inspect(arg, {depth: 10}); }).join(', '); + if (str.slice(0, 10) === "'Warning: " && level >= LOG_LEVELS.error) { + // React warnings use console.error so that a stack trace is shown, + // but we don't (currently) want these to show a redbox + // (Note: Logic duplicated in ExceptionsManager.js.) + level = LOG_LEVELS.warn; + } global.nativeLoggingHook(str, level); }; } diff --git a/packager/react-packager/src/DependencyResolver/polyfills/require.js b/packager/react-packager/src/DependencyResolver/polyfills/require.js index 04a0bff75..daedb4eaa 100644 --- a/packager/react-packager/src/DependencyResolver/polyfills/require.js +++ b/packager/react-packager/src/DependencyResolver/polyfills/require.js @@ -303,6 +303,18 @@ return _totalFactories; }; + /** + * Asynchronously loads any missing dependency and executes the provided + * callback once all of them are satisfied. + * + * Note that the dependencies on the provided array must be string literals + * as the packager uses this information to figure out how the modules are + * packaged into different bundles. + */ + require.ensure = function(dependencies, callback) { + throw '`require.ensure` is still not supported'; + }; + /** * The define function conforming to CommonJS proposal: * http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition @@ -464,56 +476,6 @@ } } - /** - * Special version of define that executes the factory as soon as all - * dependencies are met. - * - * define() does just that, defines a module. Module's factory will not be - * called until required by other module. This makes sense for most of our - * library modules: we do not want to execute the factory unless it's being - * used by someone. - * - * On the other hand there are modules, that you can call "entrance points". - * You want to run the "factory" method for them as soon as all dependencies - * are met. - * - * @example - * - * define('BaseClass', [], function() { return ... }); - * // ^^ factory for BaseClass was just stored in modulesMap - * - * define('SubClass', ['BaseClass'], function() { ... }); - * // SubClass module is marked as ready (waiting == 0), factory is just - * // stored - * - * define('OtherClass, ['BaseClass'], function() { ... }); - * // OtherClass module is marked as ready (waiting == 0), factory is just - * // stored - * - * requireLazy(['SubClass', 'ChatConfig'], - * function() { ... }); - * // ChatRunner is waiting for ChatConfig to come - * - * define('ChatConfig', [], { foo: 'bar' }); - * // at this point ChatRunner is marked as ready, and its factory - * // executed + all dependent factories are executed too: BaseClass, - * // SubClass, ChatConfig notice that OtherClass's factory won't be - * // executed unless explicitly required by someone - * - * @param {Array} dependencies - * @param {Object|Function} factory - */ - function requireLazy(dependencies, factory, context) { - return define( - dependencies, - factory, - undefined, - REQUIRE_WHEN_READY, - context, - 1 - ); - } - function _uid() { return '__mod__' + _counter++; } @@ -595,12 +557,8 @@ _register('global', global); _register('require', require); - _register('requireDynamic', require); - _register('requireLazy', requireLazy); global.require = require; - global.requireDynamic = require; - global.requireLazy = requireLazy; require.__debug = { modules: modulesMap, @@ -621,8 +579,7 @@ * out for every module which would be a lot of extra bytes. */ global.__d = function(id, deps, factory, _special, _inlineRequires) { - var defaultDeps = ['global', 'require', 'requireDynamic', 'requireLazy', - 'module', 'exports']; + var defaultDeps = ['global', 'require', 'module', 'exports']; define(id, defaultDeps.concat(deps), factory, _special || USED_AS_TRANSPORT, null, null, _inlineRequires); }; diff --git a/packager/react-packager/src/DependencyResolver/replacePatterns.js b/packager/react-packager/src/DependencyResolver/replacePatterns.js index cde2d873c..c27d7c770 100644 --- a/packager/react-packager/src/DependencyResolver/replacePatterns.js +++ b/packager/react-packager/src/DependencyResolver/replacePatterns.js @@ -11,3 +11,5 @@ exports.IMPORT_RE = /(\bimport\s+?(?:.+\s+?from\s+?)?)(['"])([^'"]+)(\2)/g; exports.REQUIRE_RE = /(\brequire\s*?\(\s*?)(['"])([^'"]+)(\2\s*?\))/g; +exports.SYSTEM_IMPORT_RE = /(\bSystem\.import\s*?\(\s*?)(['"])([^'"]+)(\2\s*?\))/g; + diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js index f78840163..6906d69f4 100644 --- a/packager/react-packager/src/JSTransformer/index.js +++ b/packager/react-packager/src/JSTransformer/index.js @@ -8,19 +8,24 @@ */ 'use strict'; -var fs = require('fs'); -var Promise = require('promise'); -var workerFarm = require('worker-farm'); -var declareOpts = require('../lib/declareOpts'); -var util = require('util'); -var ModuleTransport = require('../lib/ModuleTransport'); +const ModuleTransport = require('../lib/ModuleTransport'); +const Promise = require('promise'); +const declareOpts = require('../lib/declareOpts'); +const fs = require('fs'); +const util = require('util'); +const workerFarm = require('worker-farm'); -var readFile = Promise.denodeify(fs.readFile); +const readFile = Promise.denodeify(fs.readFile); -module.exports = Transformer; -Transformer.TransformError = TransformError; +// Avoid memory leaks caused in workers. This number seems to be a good enough number +// to avoid any memory leak while not slowing down initial builds. +// TODO(amasad): Once we get bundle splitting, we can drive this down a bit more. +const MAX_CALLS_PER_WORKER = 600; -var validateOpts = declareOpts({ +// Worker will timeout if one of the callers timeout. +const DEFAULT_MAX_CALL_TIME = 30000; + +const validateOpts = declareOpts({ projectRoots: { type: 'array', required: true, @@ -40,51 +45,58 @@ var validateOpts = declareOpts({ type: 'object', required: true, }, + transformTimeoutInterval: { + type: 'number', + default: DEFAULT_MAX_CALL_TIME, + } }); -function Transformer(options) { - var opts = validateOpts(options); +class Transformer { + constructor(options) { + const opts = this._opts = validateOpts(options); - this._cache = opts.cache; + this._cache = opts.cache; - if (options.transformModulePath != null) { - this._workers = workerFarm( - {autoStart: true, maxConcurrentCallsPerWorker: 1}, - options.transformModulePath - ); + if (opts.transformModulePath != null) { + this._workers = workerFarm({ + autoStart: true, + maxConcurrentCallsPerWorker: 1, + maxCallsPerWorker: MAX_CALLS_PER_WORKER, + maxCallTime: opts.transformTimeoutInterval, + }, opts.transformModulePath); - this._transform = Promise.denodeify(this._workers); - } -} - -Transformer.prototype.kill = function() { - this._workers && workerFarm.end(this._workers); -}; - -Transformer.prototype.invalidateFile = function(filePath) { - this._cache.invalidate(filePath); -}; - -Transformer.prototype.loadFileAndTransform = function(filePath) { - if (this._transform == null) { - return Promise.reject(new Error('No transfrom module')); + this._transform = Promise.denodeify(this._workers); + } } - var transform = this._transform; - return this._cache.get(filePath, 'transformedSource', function() { - // TODO: use fastfs to avoid reading file from disk again - return readFile(filePath) - .then(function(buffer) { - var sourceCode = buffer.toString(); + kill() { + this._workers && workerFarm.end(this._workers); + } - return transform({ - sourceCode: sourceCode, - filename: filePath, - }).then( - function(res) { + invalidateFile(filePath) { + this._cache.invalidate(filePath); + } + + loadFileAndTransform(filePath) { + if (this._transform == null) { + return Promise.reject(new Error('No transfrom module')); + } + + return this._cache.get( + filePath, + 'transformedSource', + // TODO: use fastfs to avoid reading file from disk again + () => readFile(filePath).then( + buffer => { + const sourceCode = buffer.toString('utf8'); + + return this._transform({ + sourceCode, + filename: filePath, + }).then(res => { if (res.error) { console.warn( - 'Error property on the result value form the transformer', + 'Error property on the result value from the transformer', 'module is deprecated and will be removed in future versions.', 'Please pass an error object as the first argument to the callback' ); @@ -97,13 +109,28 @@ Transformer.prototype.loadFileAndTransform = function(filePath) { sourcePath: filePath, sourceCode: sourceCode, }); - } - ); - }).catch(function(err) { - throw formatError(err, filePath); - }); - }); -}; + }).catch(err => { + if (err.type === 'TimeoutError') { + const timeoutErr = new Error( + `TimeoutError: transforming ${filePath} took longer than ` + + `${this._opts.transformTimeoutInterval / 1000} seconds.\n` + + `You can adjust timeout via the 'transformTimeoutInterval' option` + ); + timeoutErr.type = 'TimeoutError'; + throw timeoutErr; + } + + throw formatError(err, filePath); + }); + }) + ); + } +} + + +module.exports = Transformer; + +Transformer.TransformError = TransformError; function TransformError() { Error.captureStackTrace && Error.captureStackTrace(this, TransformError); diff --git a/packager/react-packager/src/Packager/Package.js b/packager/react-packager/src/Packager/Package.js deleted file mode 100644 index 6b5389461..000000000 --- a/packager/react-packager/src/Packager/Package.js +++ /dev/null @@ -1,300 +0,0 @@ -/** - * 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'; - -var _ = require('underscore'); -var base64VLQ = require('./base64-vlq'); -var UglifyJS = require('uglify-js'); -var ModuleTransport = require('../lib/ModuleTransport'); - -module.exports = Package; - -var SOURCEMAPPING_URL = '\n\/\/@ sourceMappingURL='; - -function Package(sourceMapUrl) { - this._finalized = false; - this._modules = []; - this._assets = []; - this._sourceMapUrl = sourceMapUrl; - this._shouldCombineSourceMaps = false; -} - -Package.prototype.setMainModuleId = function(moduleId) { - this._mainModuleId = moduleId; -}; - -Package.prototype.addModule = function(module) { - if (!(module instanceof ModuleTransport)) { - throw new Error('Expeceted a ModuleTransport object'); - } - - // If we get a map from the transformer we'll switch to a mode - // were we're combining the source maps as opposed to - if (!this._shouldCombineSourceMaps && module.map != null) { - this._shouldCombineSourceMaps = true; - } - - this._modules.push(module); -}; - -Package.prototype.addAsset = function(asset) { - this._assets.push(asset); -}; - -Package.prototype.finalize = function(options) { - options = options || {}; - if (options.runMainModule) { - var runCode = ';require("' + this._mainModuleId + '");'; - this.addModule(new ModuleTransport({ - code: runCode, - virtual: true, - sourceCode: runCode, - sourcePath: 'RunMainModule.js' - })); - } - - Object.freeze(this._modules); - Object.seal(this._modules); - Object.freeze(this._assets); - Object.seal(this._assets); - this._finalized = true; -}; - -Package.prototype._assertFinalized = function() { - if (!this._finalized) { - throw new Error('Package need to be finalized before getting any source'); - } -}; - -Package.prototype._getSource = function() { - if (this._source == null) { - this._source = _.pluck(this._modules, 'code').join('\n'); - } - return this._source; -}; - -Package.prototype._getInlineSourceMap = function() { - if (this._inlineSourceMap == null) { - var sourceMap = this.getSourceMap({excludeSource: true}); - var encoded = new Buffer(JSON.stringify(sourceMap)).toString('base64'); - this._inlineSourceMap = 'data:application/json;base64,' + encoded; - } - return this._inlineSourceMap; -}; - -Package.prototype.getSource = function(options) { - this._assertFinalized(); - - options = options || {}; - - if (options.minify) { - return this.getMinifiedSourceAndMap().code; - } - - var source = this._getSource(); - - if (options.inlineSourceMap) { - source += SOURCEMAPPING_URL + this._getInlineSourceMap(); - } else if (this._sourceMapUrl) { - source += SOURCEMAPPING_URL + this._sourceMapUrl; - } - - return source; -}; - -Package.prototype.getMinifiedSourceAndMap = function() { - this._assertFinalized(); - - var source = this._getSource(); - try { - return UglifyJS.minify(source, { - fromString: true, - outSourceMap: 'bundle.js', - inSourceMap: this.getSourceMap(), - }); - } catch(e) { - // Sometimes, when somebody is using a new syntax feature that we - // don't yet have transform for, the untransformed line is sent to - // uglify, and it chokes on it. This code tries to print the line - // and the module for easier debugging - var errorMessage = 'Error while minifying JS\n'; - if (e.line) { - errorMessage += 'Transformed code line: "' + - source.split('\n')[e.line - 1] + '"\n'; - } - if (e.pos) { - var fromIndex = source.lastIndexOf('__d(\'', e.pos); - if (fromIndex > -1) { - fromIndex += '__d(\''.length; - var toIndex = source.indexOf('\'', fromIndex); - errorMessage += 'Module name (best guess): ' + - source.substring(fromIndex, toIndex) + '\n'; - } - } - errorMessage += e.toString(); - throw new Error(errorMessage); - } -}; - -/** - * I found a neat trick in the sourcemap spec that makes it easy - * to concat sourcemaps. The `sections` field allows us to combine - * the sourcemap easily by adding an offset. Tested on chrome. - * Seems like it's not yet in Firefox but that should be fine for - * now. - */ -Package.prototype._getCombinedSourceMaps = function(options) { - var result = { - version: 3, - file: 'bundle.js', - sections: [], - }; - - var line = 0; - this._modules.forEach(function(module) { - var map = module.map; - if (module.virtual) { - map = generateSourceMapForVirtualModule(module); - } - - if (options.excludeSource) { - map = _.extend({}, map, {sourcesContent: []}); - } - - result.sections.push({ - offset: { line: line, column: 0 }, - map: map, - }); - line += module.code.split('\n').length; - }); - - return result; -}; - -Package.prototype.getSourceMap = function(options) { - this._assertFinalized(); - - options = options || {}; - - if (this._shouldCombineSourceMaps) { - return this._getCombinedSourceMaps(options); - } - - var mappings = this._getMappings(); - var map = { - file: 'bundle.js', - sources: _.pluck(this._modules, 'sourcePath'), - version: 3, - names: [], - mappings: mappings, - sourcesContent: options.excludeSource - ? [] : _.pluck(this._modules, 'sourceCode') - }; - return map; -}; - -Package.prototype.getAssets = function() { - return this._assets; -}; - -Package.prototype._getMappings = function() { - var modules = this._modules; - - // The first line mapping in our package is basically the base64vlq code for - // zeros (A). - var firstLine = 'AAAA'; - - // Most other lines in our mappings are all zeros (for module, column etc) - // except for the lineno mappinp: curLineno - prevLineno = 1; Which is C. - var line = 'AACA'; - - var moduleLines = Object.create(null); - var mappings = ''; - for (var i = 0; i < modules.length; i++) { - var module = modules[i]; - var code = module.code; - var lastCharNewLine = false; - moduleLines[module.sourcePath] = 0; - for (var t = 0; t < code.length; t++) { - if (t === 0 && i === 0) { - mappings += firstLine; - } else if (t === 0) { - mappings += 'AC'; - - // This is the only place were we actually don't know the mapping ahead - // of time. When it's a new module (and not the first) the lineno - // mapping is 0 (current) - number of lines in prev module. - mappings += base64VLQ.encode( - 0 - moduleLines[modules[i - 1].sourcePath] - ); - mappings += 'A'; - } else if (lastCharNewLine) { - moduleLines[module.sourcePath]++; - mappings += line; - } - lastCharNewLine = code[t] === '\n'; - if (lastCharNewLine) { - mappings += ';'; - } - } - if (i !== modules.length - 1) { - mappings += ';'; - } - } - return mappings; -}; - -Package.prototype.getJSModulePaths = function() { - return this._modules.filter(function(module) { - // Filter out non-js files. Like images etc. - return !module.virtual; - }).map(function(module) { - return module.sourcePath; - }); -}; - -Package.prototype.getDebugInfo = function() { - return [ - '

Main Module:

' + this._mainModuleId + '
', - '', - '

Module paths and transformed code:

', - this._modules.map(function(m) { - return '

Path:

' + m.sourcePath + '

Source:

' + - '
'; - }).join('\n'), - ].join('\n'); -}; - -function generateSourceMapForVirtualModule(module) { - // All lines map 1-to-1 - var mappings = 'AAAA;'; - - for (var i = 1; i < module.code.split('\n').length; i++) { - mappings += 'AACA;'; - } - - return { - version: 3, - sources: [ module.sourcePath ], - names: [], - mappings: mappings, - file: module.sourcePath, - sourcesContent: [ module.sourceCode ], - }; -} diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js deleted file mode 100644 index a718bd264..000000000 --- a/packager/react-packager/src/Packager/index.js +++ /dev/null @@ -1,289 +0,0 @@ -/** - * 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'; - -var assert = require('assert'); -var fs = require('fs'); -var path = require('path'); -var Promise = require('promise'); -var Cache = require('../Cache'); -var Transformer = require('../JSTransformer'); -var DependencyResolver = require('../DependencyResolver'); -var Package = require('./Package'); -var Activity = require('../Activity'); -var ModuleTransport = require('../lib/ModuleTransport'); -var declareOpts = require('../lib/declareOpts'); -var imageSize = require('image-size'); - -var sizeOf = Promise.denodeify(imageSize); -var readFile = Promise.denodeify(fs.readFile); - -var validateOpts = declareOpts({ - projectRoots: { - type: 'array', - required: true, - }, - blacklistRE: { - type: 'object', // typeof regex is object - }, - moduleFormat: { - type: 'string', - default: 'haste', - }, - polyfillModuleNames: { - type: 'array', - default: [], - }, - cacheVersion: { - type: 'string', - default: '1.0', - }, - resetCache: { - type: 'boolean', - default: false, - }, - transformModulePath: { - type:'string', - required: false, - }, - nonPersistent: { - type: 'boolean', - default: false, - }, - assetRoots: { - type: 'array', - required: false, - }, - assetExts: { - type: 'array', - default: ['png'], - }, - fileWatcher: { - type: 'object', - required: true, - }, - assetServer: { - type: 'object', - required: true, - } -}); - -function Packager(options) { - var opts = this._opts = validateOpts(options); - - opts.projectRoots.forEach(verifyRootExists); - - this._cache = opts.nonPersistent - ? new DummyCache() - : new Cache({ - resetCache: opts.resetCache, - cacheVersion: opts.cacheVersion, - projectRoots: opts.projectRoots, - transformModulePath: opts.transformModulePath, - }); - - this._resolver = new DependencyResolver({ - projectRoots: opts.projectRoots, - blacklistRE: opts.blacklistRE, - polyfillModuleNames: opts.polyfillModuleNames, - nonPersistent: opts.nonPersistent, - moduleFormat: opts.moduleFormat, - assetRoots: opts.assetRoots, - fileWatcher: opts.fileWatcher, - assetExts: opts.assetExts, - cache: this._cache, - }); - - this._transformer = new Transformer({ - projectRoots: opts.projectRoots, - blacklistRE: opts.blacklistRE, - cache: this._cache, - transformModulePath: opts.transformModulePath, - }); - - this._projectRoots = opts.projectRoots; - this._assetServer = opts.assetServer; -} - -Packager.prototype.kill = function() { - this._transformer.kill(); - return this._cache.end(); -}; - -Packager.prototype.package = function(main, runModule, sourceMapUrl, isDev) { - var ppackage = new Package(sourceMapUrl); - - var transformModule = this._transformModule.bind(this, ppackage); - var findEventId = Activity.startEvent('find dependencies'); - var transformEventId; - - return this.getDependencies(main, isDev) - .then(function(result) { - Activity.endEvent(findEventId); - transformEventId = Activity.startEvent('transform'); - - ppackage.setMainModuleId(result.mainModuleId); - return Promise.all( - result.dependencies.map(transformModule) - ); - }) - .then(function(transformedModules) { - Activity.endEvent(transformEventId); - - transformedModules.forEach(function(moduleTransport) { - ppackage.addModule(moduleTransport); - }); - - ppackage.finalize({ runMainModule: runModule }); - return ppackage; - }); -}; - -Packager.prototype.invalidateFile = function(filePath) { - this._transformer.invalidateFile(filePath); -}; - -Packager.prototype.getDependencies = function(main, isDev) { - return this._resolver.getDependencies(main, { dev: isDev }); -}; - -Packager.prototype._transformModule = function(ppackage, module) { - var transform; - - if (module.isAsset_DEPRECATED) { - transform = this.generateAssetModule_DEPRECATED(ppackage, module); - } else if (module.isAsset) { - transform = this.generateAssetModule(ppackage, module); - } else if (module.isJSON) { - transform = generateJSONModule(module); - } else { - transform = this._transformer.loadFileAndTransform( - path.resolve(module.path) - ); - } - - var resolver = this._resolver; - return transform.then( - transformed => resolver.wrapModule(module, transformed.code).then( - code => new ModuleTransport({ - code: code, - map: transformed.map, - sourceCode: transformed.sourceCode, - sourcePath: transformed.sourcePath, - virtual: transformed.virtual, - }) - ) - ); -}; - -Packager.prototype.getGraphDebugInfo = function() { - return this._resolver.getDebugInfo(); -}; - -Packager.prototype.generateAssetModule_DEPRECATED = function(ppackage, module) { - return sizeOf(module.path).then(function(dimensions) { - var img = { - __packager_asset: true, - isStatic: true, - path: module.path, - uri: module.id.replace(/^[^!]+!/, ''), - width: dimensions.width / module.resolution, - height: dimensions.height / module.resolution, - deprecated: true, - }; - - ppackage.addAsset(img); - - var code = 'module.exports = ' + JSON.stringify(img) + ';'; - - return new ModuleTransport({ - code: code, - sourceCode: code, - sourcePath: module.path, - virtual: true, - }); - }); -}; - -Packager.prototype.generateAssetModule = function(ppackage, module) { - var relPath = getPathRelativeToRoot(this._projectRoots, module.path); - - return Promise.all([ - sizeOf(module.path), - this._assetServer.getAssetData(relPath), - ]).then(function(res) { - var dimensions = res[0]; - var assetData = res[1]; - var img = { - __packager_asset: true, - fileSystemLocation: path.dirname(module.path), - httpServerLocation: path.join('/assets', path.dirname(relPath)), - width: dimensions.width / module.resolution, - height: dimensions.height / module.resolution, - scales: assetData.scales, - hash: assetData.hash, - name: assetData.name, - type: assetData.type, - }; - - ppackage.addAsset(img); - - var ASSET_TEMPLATE = 'module.exports = require("AssetRegistry").registerAsset(%json);'; - var code = ASSET_TEMPLATE.replace('%json', JSON.stringify(img)); - - return new ModuleTransport({ - code: code, - sourceCode: code, - sourcePath: module.path, - virtual: true, - }); - }); -}; - -function generateJSONModule(module) { - return readFile(module.path).then(function(data) { - var code = 'module.exports = ' + data.toString('utf8') + ';'; - - return new ModuleTransport({ - code: code, - sourceCode: code, - sourcePath: module.path, - virtual: true, - }); - }); -} - -function getPathRelativeToRoot(roots, absPath) { - for (var i = 0; i < roots.length; i++) { - var relPath = path.relative(roots[i], absPath); - if (relPath[0] !== '.') { - return relPath; - } - } - - throw new Error( - 'Expected root module to be relative to one of the project roots' - ); -} - -function verifyRootExists(root) { - // Verify that the root exists. - assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory'); -} - -class DummyCache { - get(filepath, field, loaderCb) { - return loaderCb(); - } - - end(){} - invalidate(filepath){} -} - -module.exports = Packager; diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index f0761d310..da8e803f5 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -8,70 +8,55 @@ */ 'use strict'; -jest.setMock('worker-farm', function() { return function() {}; }) +jest.setMock('worker-farm', function() { return () => {}; }) .dontMock('os') .dontMock('path') .dontMock('url') - .setMock('timers', { - setImmediate: function(fn) { - return setTimeout(fn, 0); - } - }) + .setMock('timers', { setImmediate: (fn) => setTimeout(fn, 0) }) .setMock('uglify-js') .dontMock('../') .setMock('chalk', { dim: function(s) { return s; } }); -var Promise = require('promise'); +const Promise = require('promise'); -describe('processRequest', function() { +describe('processRequest', () => { var server; - var Packager; + var Bundler; var FileWatcher; - var options = { + const options = { projectRoots: ['root'], blacklistRE: null, cacheVersion: null, polyfillModuleNames: null }; - var makeRequest = function(requestHandler, requrl) { - return new Promise(function(resolve) { - requestHandler( - { url: requrl }, - { - setHeader: jest.genMockFunction(), - end: function(res) { - resolve(res); - } - }, - { - next: function() {} - } - ); - }); - }; + const makeRequest = (reqHandler, requrl) => new Promise(resolve => + reqHandler( + { url: requrl }, + { + setHeader: jest.genMockFunction(), + end: res => resolve(res), + }, + { next: () => {} }, + ) + ); - var invalidatorFunc = jest.genMockFunction(); - var watcherFunc = jest.genMockFunction(); + const invalidatorFunc = jest.genMockFunction(); + const watcherFunc = jest.genMockFunction(); var requestHandler; var triggerFileChange; - beforeEach(function() { - Packager = require('../../Packager'); + beforeEach(() => { + Bundler = require('../../Bundler'); FileWatcher = require('../../FileWatcher'); - Packager.prototype.package = jest.genMockFunction().mockImpl(function() { - return Promise.resolve({ - getSource: function() { - return 'this is the source'; - }, - getSourceMap: function() { - return 'this is the source map'; - }, - }); - }); - + Bundler.prototype.bundle = jest.genMockFunction().mockImpl(() => + Promise.resolve({ + getSource: () => 'this is the source', + getSourceMap: () => 'this is the source map', + }) + ); FileWatcher.prototype.on = function(eventType, callback) { if (eventType !== 'all') { @@ -82,130 +67,141 @@ describe('processRequest', function() { return this; }; - Packager.prototype.invalidateFile = invalidatorFunc; + Bundler.prototype.invalidateFile = invalidatorFunc; - var Server = require('../'); + const Server = require('../'); server = new Server(options); requestHandler = server.processRequest.bind(server); }); - pit('returns JS bundle source on request of *.bundle',function() { + pit('returns JS bundle source on request of *.bundle', () => { return makeRequest( requestHandler, 'mybundle.bundle?runModule=true' - ).then(function(response) { - expect(response).toEqual('this is the source'); - }); + ).then(response => + expect(response).toEqual('this is the source') + ); }); - pit('returns JS bundle source on request of *.bundle (compat)',function() { + pit('returns JS bundle source on request of *.bundle (compat)', () => { return makeRequest( requestHandler, 'mybundle.runModule.bundle' - ).then(function(response) { - expect(response).toEqual('this is the source'); - }); + ).then(response => + expect(response).toEqual('this is the source') + ); }); - pit('returns sourcemap on request of *.map', function() { + pit('returns sourcemap on request of *.map', () => { return makeRequest( requestHandler, 'mybundle.map?runModule=true' - ).then(function(response) { - expect(response).toEqual('"this is the source map"'); - }); + ).then(response => + expect(response).toEqual('"this is the source map"') + ); }); - pit('works with .ios.js extension', function() { + pit('works with .ios.js extension', () => { return makeRequest( requestHandler, 'index.ios.includeRequire.bundle' - ).then(function(response) { + ).then(response => { expect(response).toEqual('this is the source'); - expect(Packager.prototype.package).toBeCalledWith( + expect(Bundler.prototype.bundle).toBeCalledWith( 'index.ios.js', true, 'index.ios.includeRequire.map', - true + true, + undefined ); }); }); - pit('watches all files in projectRoot', function() { + pit('passes in the platform param', function() { + return makeRequest( + requestHandler, + 'index.bundle?platform=ios' + ).then(function(response) { + expect(response).toEqual('this is the source'); + expect(Bundler.prototype.bundle).toBeCalledWith( + 'index.js', + true, + 'index.map', + true, + 'ios', + ); + }); + }); + + pit('watches all files in projectRoot', () => { return makeRequest( requestHandler, 'mybundle.bundle?runModule=true' - ).then(function() { + ).then(() => { expect(watcherFunc.mock.calls[0][0]).toEqual('all'); expect(watcherFunc.mock.calls[0][1]).not.toBe(null); }); }); - - describe('file changes', function() { - pit('invalides files in package when file is updated', function() { + describe('file changes', () => { + pit('invalides files in bundle when file is updated', () => { return makeRequest( requestHandler, 'mybundle.bundle?runModule=true' - ).then(function() { - var onFileChange = watcherFunc.mock.calls[0][1]; + ).then(() => { + const onFileChange = watcherFunc.mock.calls[0][1]; onFileChange('all','path/file.js', options.projectRoots[0]); expect(invalidatorFunc.mock.calls[0][0]).toEqual('root/path/file.js'); }); }); - pit('rebuilds the packages that contain a file when that file is changed', function() { - var packageFunc = jest.genMockFunction(); - packageFunc + pit('rebuilds the bundles that contain a file when that file is changed', () => { + const bundleFunc = jest.genMockFunction(); + bundleFunc .mockReturnValueOnce( Promise.resolve({ - getSource: function() { - return 'this is the first source'; - }, - getSourceMap: function() {}, + getSource: () => 'this is the first source', + getSourceMap: () => {}, }) ) .mockReturnValue( Promise.resolve({ - getSource: function() { - return 'this is the rebuilt source'; - }, - getSourceMap: function() {}, + getSource: () => 'this is the rebuilt source', + getSourceMap: () => {}, }) ); - Packager.prototype.package = packageFunc; + Bundler.prototype.bundle = bundleFunc; - var Server = require('../../Server'); + const Server = require('../../Server'); server = new Server(options); requestHandler = server.processRequest.bind(server); - return makeRequest(requestHandler, 'mybundle.bundle?runModule=true') - .then(function(response) { + .then(response => { expect(response).toEqual('this is the first source'); - expect(packageFunc.mock.calls.length).toBe(1); + expect(bundleFunc.mock.calls.length).toBe(1); triggerFileChange('all','path/file.js', options.projectRoots[0]); jest.runAllTimers(); jest.runAllTimers(); }) - .then(function() { - expect(packageFunc.mock.calls.length).toBe(2); + .then(() => { + expect(bundleFunc.mock.calls.length).toBe(2); return makeRequest(requestHandler, 'mybundle.bundle?runModule=true') - .then(function(response) { - expect(response).toEqual('this is the rebuilt source'); - }); + .then(response => + expect(response).toEqual('this is the rebuilt source') + ); }); }); }); - describe('/onchange endpoint', function() { + describe('/onchange endpoint', () => { var EventEmitter; var req; var res; - beforeEach(function() { + beforeEach(() => { EventEmitter = require.requireActual('events').EventEmitter; req = new EventEmitter(); req.url = '/onchange'; @@ -215,14 +211,14 @@ describe('processRequest', function() { }; }); - it('should hold on to request and inform on change', function() { + it('should hold on to request and inform on change', () => { server.processRequest(req, res); triggerFileChange('all', 'path/file.js', options.projectRoots[0]); jest.runAllTimers(); expect(res.end).toBeCalledWith(JSON.stringify({changed: true})); }); - it('should not inform changes on disconnected clients', function() { + it('should not inform changes on disconnected clients', () => { server.processRequest(req, res); req.emit('close'); jest.runAllTimers(); @@ -232,56 +228,52 @@ describe('processRequest', function() { }); }); - describe('/assets endpoint', function() { + describe('/assets endpoint', () => { var AssetServer; - beforeEach(function() { + beforeEach(() => { AssetServer = require('../../AssetServer'); }); - it('should serve simple case', function() { - var req = { - url: '/assets/imgs/a.png', - }; - var res = { - end: jest.genMockFn(), - }; + it('should serve simple case', () => { + const req = {url: '/assets/imgs/a.png'}; + const res = {end: jest.genMockFn()}; - AssetServer.prototype.get.mockImpl(function() { - return Promise.resolve('i am image'); - }); + AssetServer.prototype.get.mockImpl(() => Promise.resolve('i am image')); server.processRequest(req, res); jest.runAllTimers(); expect(res.end).toBeCalledWith('i am image'); }); - it('should return 404', function() { + it('should return 404', () => { }); }); - describe('buildPackage(options)', function() { - it('Calls the packager with the correct args', function() { - server.buildPackage({ + describe('buildbundle(options)', () => { + it('Calls the bundler with the correct args', () => { + server.buildBundle({ entryFile: 'foo file' }); - expect(Packager.prototype.package).toBeCalledWith( + expect(Bundler.prototype.bundle).toBeCalledWith( 'foo file', true, undefined, - true + true, + undefined ); }); }); - describe('buildPackageFromUrl(options)', function() { - it('Calls the packager with the correct args', function() { - server.buildPackageFromUrl('/path/to/foo.bundle?dev=false&runModule=false'); - expect(Packager.prototype.package).toBeCalledWith( + describe('buildBundleFromUrl(options)', () => { + it('Calls the bundler with the correct args', () => { + server.buildBundleFromUrl('/path/to/foo.bundle?dev=false&runModule=false'); + expect(Bundler.prototype.bundle).toBeCalledWith( 'path/to/foo.js', false, '/path/to/foo.map', - false + false, + undefined ); }); }); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index fdeffc65e..1ad6d862b 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -8,21 +8,20 @@ */ 'use strict'; -var url = require('url'); -var path = require('path'); -var declareOpts = require('../lib/declareOpts'); -var FileWatcher = require('../FileWatcher'); -var Packager = require('../Packager'); -var Activity = require('../Activity'); -var AssetServer = require('../AssetServer'); -var Promise = require('promise'); -var _ = require('underscore'); -var exec = require('child_process').exec; -var fs = require('fs'); +const Activity = require('../Activity'); +const AssetServer = require('../AssetServer'); +const FileWatcher = require('../FileWatcher'); +const Bundler = require('../Bundler'); +const Promise = require('promise'); -module.exports = Server; +const _ = require('underscore'); +const declareOpts = require('../lib/declareOpts'); +const exec = require('child_process').exec; +const fs = require('fs'); +const path = require('path'); +const url = require('url'); -var validateOpts = declareOpts({ +const validateOpts = declareOpts({ projectRoots: { type: 'array', required: true, @@ -62,117 +61,13 @@ var validateOpts = declareOpts({ type: 'array', default: ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp'], }, + transformTimeoutInterval: { + type: 'number', + required: false, + }, }); -function Server(options) { - var opts = validateOpts(options); - - this._projectRoots = opts.projectRoots; - this._packages = Object.create(null); - this._changeWatchers = []; - - var assetGlobs = opts.assetExts.map(function(ext) { - return '**/*.' + ext; - }); - - var watchRootConfigs = opts.projectRoots.map(function(dir) { - return { - dir: dir, - globs: [ - '**/*.js', - '**/*.json', - ].concat(assetGlobs), - }; - }); - - if (opts.assetRoots != null) { - watchRootConfigs = watchRootConfigs.concat( - opts.assetRoots.map(function(dir) { - return { - dir: dir, - globs: assetGlobs, - }; - }) - ); - } - - this._fileWatcher = options.nonPersistent - ? FileWatcher.createDummyWatcher() - : new FileWatcher(watchRootConfigs); - - this._assetServer = new AssetServer({ - projectRoots: opts.projectRoots, - assetExts: opts.assetExts, - }); - - var packagerOpts = Object.create(opts); - packagerOpts.fileWatcher = this._fileWatcher; - packagerOpts.assetServer = this._assetServer; - this._packager = new Packager(packagerOpts); - - var onFileChange = this._onFileChange.bind(this); - this._fileWatcher.on('all', onFileChange); - - var self = this; - this._debouncedFileChangeHandler = _.debounce(function(filePath) { - self._rebuildPackages(filePath); - self._informChangeWatchers(); - }, 50); -} - -Server.prototype._onFileChange = function(type, filepath, root) { - var absPath = path.join(root, filepath); - this._packager.invalidateFile(absPath); - // Make sure the file watcher event runs through the system before - // we rebuild the packages. - this._debouncedFileChangeHandler(absPath); -}; - -Server.prototype._rebuildPackages = function() { - var buildPackage = this.buildPackage.bind(this); - var packages = this._packages; - - Object.keys(packages).forEach(function(optionsJson) { - var options = JSON.parse(optionsJson); - // Wait for a previous build (if exists) to finish. - packages[optionsJson] = (packages[optionsJson] || Promise.resolve()).finally(function() { - // With finally promise callback we can't change the state of the promise - // so we need to reassign the promise. - packages[optionsJson] = buildPackage(options).then(function(p) { - // Make a throwaway call to getSource to cache the source string. - p.getSource({ - inlineSourceMap: options.inlineSourceMap, - minify: options.minify, - }); - return p; - }); - }); - return packages[optionsJson]; - }); -}; - -Server.prototype._informChangeWatchers = function() { - var watchers = this._changeWatchers; - var headers = { - 'Content-Type': 'application/json; charset=UTF-8', - }; - - watchers.forEach(function(w) { - w.res.writeHead(205, headers); - w.res.end(JSON.stringify({ changed: true })); - }); - - this._changeWatchers = []; -}; - -Server.prototype.end = function() { - Promise.all([ - this._fileWatcher.end(), - this._packager.kill(), - ]); -}; - -var packageOpts = declareOpts({ +const bundleOpts = declareOpts({ sourceMapUrl: { type: 'string', required: false, @@ -197,247 +92,365 @@ var packageOpts = declareOpts({ type: 'boolean', default: false, }, + platform: { + type: 'string', + required: false, + } }); -Server.prototype.buildPackage = function(options) { - var opts = packageOpts(options); +class Server { + constructor(options) { + const opts = validateOpts(options); - return this._packager.package( - opts.entryFile, - opts.runModule, - opts.sourceMapUrl, - opts.dev - ); -}; + this._projectRoots = opts.projectRoots; + this._bundles = Object.create(null); + this._changeWatchers = []; -Server.prototype.buildPackageFromUrl = function(reqUrl) { - var options = getOptionsFromUrl(reqUrl); - return this.buildPackage(options); -}; + const assetGlobs = opts.assetExts.map(ext => '**/*.' + ext); -Server.prototype.getDependencies = function(main) { - return this._packager.getDependencies(main); -}; + var watchRootConfigs = opts.projectRoots.map(dir => { + return { + dir: dir, + globs: [ + '**/*.js', + '**/*.json', + ].concat(assetGlobs), + }; + }); -Server.prototype._processDebugRequest = function(reqUrl, res) { - var ret = ''; - var pathname = url.parse(reqUrl).pathname; - var parts = pathname.split('/').filter(Boolean); - if (parts.length === 1) { - ret += ''; - ret += ''; - res.end(ret); - } else if (parts[1] === 'packages') { - ret += '

Cached Packages

'; - Promise.all(Object.keys(this._packages).map(function(optionsJson) { - return this._packages[optionsJson].then(function(p) { - ret += '

' + optionsJson + '

'; - ret += p.getDebugInfo(); - }); - }, this)).then( - function() { res.end(ret); }, - function(e) { - res.writeHead(500); - res.end('Internal Error'); - console.log(e.stack); - } - ); - } else if (parts[1] === 'graph'){ - ret += '

Dependency Graph

'; - ret += this._packager.getGraphDebugInfo(); - res.end(ret); - } else { - res.writeHead('404'); - res.end('Invalid debug request'); - return; - } -}; - -Server.prototype._processOnChangeRequest = function(req, res) { - var watchers = this._changeWatchers; - - watchers.push({ - req: req, - res: res, - }); - - req.on('close', function() { - for (var i = 0; i < watchers.length; i++) { - if (watchers[i] && watchers[i].req === req) { - watchers.splice(i, 1); - break; - } + if (opts.assetRoots != null) { + watchRootConfigs = watchRootConfigs.concat( + opts.assetRoots.map(dir => { + return { + dir: dir, + globs: assetGlobs, + }; + }) + ); } - }); -}; -Server.prototype._processAssetsRequest = function(req, res) { - var urlObj = url.parse(req.url, true); - var assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/); - this._assetServer.get(assetPath[1]) - .then( - function(data) { - res.end(data); - }, - function(error) { - console.error(error.stack); - res.writeHead('404'); - res.end('Asset not found'); - } - ).done(); -}; + this._fileWatcher = options.nonPersistent + ? FileWatcher.createDummyWatcher() + : new FileWatcher(watchRootConfigs); -Server.prototype._processProfile = function(req, res) { - console.log('Dumping profile information...'); - var dumpName = '/tmp/dump_' + Date.now() + '.json'; - var prefix = process.env.TRACE_VIEWER_PATH || ''; - var cmd = path.join(prefix, 'trace2html') + ' ' + dumpName; - fs.writeFileSync(dumpName, req.rawBody); - exec(cmd, function (error) { - if (error) { - if (error.code === 127) { - console.error( - '\n** Failed executing `' + cmd + '` **\n\n' + - 'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' + - 'You can get it at:\n\n' + - ' https://github.com/google/trace-viewer\n\n' + - 'If it\'s not in your path, you can set a custom path with:\n\n' + - ' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' + - 'NOTE: Your profile data was kept at:\n\n' + - ' ' + dumpName - ); - } else { - console.error('Unknown error', error); - } - res.end(); - return; + this._assetServer = new AssetServer({ + projectRoots: opts.projectRoots, + assetExts: opts.assetExts, + }); + + const bundlerOpts = Object.create(opts); + bundlerOpts.fileWatcher = this._fileWatcher; + bundlerOpts.assetServer = this._assetServer; + this._bundler = new Bundler(bundlerOpts); + + this._fileWatcher.on('all', this._onFileChange.bind(this)); + + this._debouncedFileChangeHandler = _.debounce(filePath => { + this._rebuildBundles(filePath); + this._informChangeWatchers(); + }, 50); + } + + end() { + Promise.all([ + this._fileWatcher.end(), + this._bundler.kill(), + ]); + } + + buildBundle(options) { + const opts = bundleOpts(options); + return this._bundler.bundle( + opts.entryFile, + opts.runModule, + opts.sourceMapUrl, + opts.dev, + opts.platform + ); + } + + buildBundleFromUrl(reqUrl) { + const options = this._getOptionsFromUrl(reqUrl); + return this.buildBundle(options); + } + + getDependencies(main) { + return this._bundler.getDependencies(main); + } + + _onFileChange(type, filepath, root) { + const absPath = path.join(root, filepath); + this._bundler.invalidateFile(absPath); + // Make sure the file watcher event runs through the system before + // we rebuild the bundles. + this._debouncedFileChangeHandler(absPath); + } + + _rebuildBundles() { + const buildBundle = this.buildBundle.bind(this); + const bundles = this._bundles; + + Object.keys(bundles).forEach(function(optionsJson) { + const options = JSON.parse(optionsJson); + // Wait for a previous build (if exists) to finish. + bundles[optionsJson] = (bundles[optionsJson] || Promise.resolve()).finally(function() { + // With finally promise callback we can't change the state of the promise + // so we need to reassign the promise. + bundles[optionsJson] = buildBundle(options).then(function(p) { + // Make a throwaway call to getSource to cache the source string. + p.getSource({ + inlineSourceMap: options.inlineSourceMap, + minify: options.minify, + }); + return p; + }); + }); + return bundles[optionsJson]; + }); + } + + _informChangeWatchers() { + const watchers = this._changeWatchers; + const headers = { + 'Content-Type': 'application/json; charset=UTF-8', + }; + + watchers.forEach(function(w) { + w.res.writeHead(205, headers); + w.res.end(JSON.stringify({ changed: true })); + }); + + this._changeWatchers = []; + } + + _processDebugRequest(reqUrl, res) { + var ret = ''; + const pathname = url.parse(reqUrl).pathname; + const parts = pathname.split('/').filter(Boolean); + if (parts.length === 1) { + ret += ''; + ret += ''; + res.end(ret); + } else if (parts[1] === 'bundles') { + ret += '

Cached Bundles

'; + Promise.all(Object.keys(this._bundles).map(optionsJson => + this._bundles[optionsJson].then(p => { + ret += '

' + optionsJson + '

'; + ret += p.getDebugInfo(); + }) + )).then( + () => res.end(ret), + e => { + res.writeHead(500); + res.end('Internal Error'); + console.log(e.stack); + } + ); + } else if (parts[1] === 'graph'){ + ret += '

Dependency Graph

'; + ret += this._bundler.getGraphDebugInfo(); + res.end(ret); } else { - exec('rm ' + dumpName); - exec('open ' + dumpName.replace(/json$/, 'html'), function (error) { - if (error) { - console.error(error); + res.writeHead('404'); + res.end('Invalid debug request'); + return; + } + } + + _processOnChangeRequest(req, res) { + const watchers = this._changeWatchers; + + watchers.push({ + req: req, + res: res, + }); + + req.on('close', () => { + for (let i = 0; i < watchers.length; i++) { + if (watchers[i] && watchers[i].req === req) { + watchers.splice(i, 1); + break; + } + } + }); + } + + _processAssetsRequest(req, res) { + const urlObj = url.parse(req.url, true); + const assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/); + this._assetServer.get(assetPath[1]) + .then( + data => res.end(data), + error => { + console.error(error.stack); + res.writeHead('404'); + res.end('Asset not found'); + } + ).done(); + } + + _processProfile(req, res) { + console.log('Dumping profile information...'); + const dumpName = '/tmp/dump_' + Date.now() + '.json'; + const prefix = process.env.TRACE_VIEWER_PATH || ''; + const cmd = path.join(prefix, 'trace2html') + ' ' + dumpName; + fs.writeFileSync(dumpName, req.rawBody); + exec(cmd, error => { + if (error) { + if (error.code === 127) { + console.error( + '\n** Failed executing `' + cmd + '` **\n\n' + + 'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' + + 'You can get it at:\n\n' + + ' https://github.com/google/trace-viewer\n\n' + + 'If it\'s not in your path, you can set a custom path with:\n\n' + + ' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' + + 'NOTE: Your profile data was kept at:\n\n' + + ' ' + dumpName + ); + } else { + console.error('Unknown error', error); } res.end(); - }); - } - }); -}; - -Server.prototype.processRequest = function(req, res, next) { - var urlObj = url.parse(req.url, true); - var pathname = urlObj.pathname; - - var requestType; - if (pathname.match(/\.bundle$/)) { - requestType = 'bundle'; - } else if (pathname.match(/\.map$/)) { - requestType = 'map'; - } else if (pathname.match(/^\/debug/)) { - this._processDebugRequest(req.url, res); - return; - } else if (pathname.match(/^\/onchange\/?$/)) { - this._processOnChangeRequest(req, res); - return; - } else if (pathname.match(/^\/assets\//)) { - this._processAssetsRequest(req, res); - return; - } else if (pathname.match(/^\/profile\/?$/)) { - this._processProfile(req, res); - return; - } else { - next(); - return; - } - - var startReqEventId = Activity.startEvent('request:' + req.url); - var options = getOptionsFromUrl(req.url); - var optionsJson = JSON.stringify(options); - var building = this._packages[optionsJson] || this.buildPackage(options); - - this._packages[optionsJson] = building; - building.then( - function(p) { - if (requestType === 'bundle') { - var bundleSource = p.getSource({ - inlineSourceMap: options.inlineSourceMap, - minify: options.minify, + return; + } else { + exec('rm ' + dumpName); + exec('open ' + dumpName.replace(/json$/, 'html'), err => { + if (err) { + console.error(err); + } + res.end(); }); - res.setHeader('Content-Type', 'application/javascript'); - res.end(bundleSource); - Activity.endEvent(startReqEventId); - } else if (requestType === 'map') { - var sourceMap = JSON.stringify(p.getSourceMap()); - res.setHeader('Content-Type', 'application/json'); - res.end(sourceMap); - Activity.endEvent(startReqEventId); } - }, - this._handleError.bind(this, res, optionsJson) - ).done(); -}; - -Server.prototype._handleError = function(res, packageID, error) { - res.writeHead(error.status || 500, { - 'Content-Type': 'application/json; charset=UTF-8', - }); - - if (error.type === 'TransformError' || error.type === 'NotFoundError') { - error.errors = [{ - description: error.description, - filename: error.filename, - lineNumber: error.lineNumber, - }]; - res.end(JSON.stringify(error)); - - if (error.type === 'NotFoundError') { - delete this._packages[packageID]; - } - } else { - console.error(error.stack || error); - res.end(JSON.stringify({ - type: 'InternalError', - message: 'react-packager has encountered an internal error, ' + - 'please check your terminal error output for more details', - })); - } -}; - -function getOptionsFromUrl(reqUrl) { - // `true` to parse the query param as an object. - var urlObj = url.parse(reqUrl, true); - // node v0.11.14 bug see https://github.com/facebook/react-native/issues/218 - urlObj.query = urlObj.query || {}; - - var pathname = decodeURIComponent(urlObj.pathname); - - // Backwards compatibility. Options used to be as added as '.' to the - // entry module name. We can safely remove these options. - var entryFile = pathname.replace(/^\//, '').split('.').filter(function(part) { - if (part === 'includeRequire' || part === 'runModule' || - part === 'bundle' || part === 'map') { - return false; - } - return true; - }).join('.') + '.js'; - - return { - sourceMapUrl: pathname.replace(/\.bundle$/, '.map'), - entryFile: entryFile, - dev: getBoolOptionFromQuery(urlObj.query, 'dev', true), - minify: getBoolOptionFromQuery(urlObj.query, 'minify'), - runModule: getBoolOptionFromQuery(urlObj.query, 'runModule', true), - inlineSourceMap: getBoolOptionFromQuery( - urlObj.query, - 'inlineSourceMap', - false - ), - }; -} - -function getBoolOptionFromQuery(query, opt, defaultVal) { - if (query[opt] == null && defaultVal != null) { - return defaultVal; + }); } - return query[opt] === 'true' || query[opt] === '1'; + processRequest(req, res, next) { + const urlObj = url.parse(req.url, true); + var pathname = urlObj.pathname; + + var requestType; + if (pathname.match(/\.bundle$/)) { + requestType = 'bundle'; + } else if (pathname.match(/\.map$/)) { + requestType = 'map'; + } else if (pathname.match(/\.assets$/)) { + requestType = 'assets'; + } else if (pathname.match(/^\/debug/)) { + this._processDebugRequest(req.url, res); + return; + } else if (pathname.match(/^\/onchange\/?$/)) { + this._processOnChangeRequest(req, res); + return; + } else if (pathname.match(/^\/assets\//)) { + this._processAssetsRequest(req, res); + return; + } else if (pathname.match(/^\/profile\/?$/)) { + this._processProfile(req, res); + return; + } else { + next(); + return; + } + + const startReqEventId = Activity.startEvent('request:' + req.url); + const options = this._getOptionsFromUrl(req.url); + const optionsJson = JSON.stringify(options); + const building = this._bundles[optionsJson] || this.buildBundle(options); + + this._bundles[optionsJson] = building; + building.then( + p => { + if (requestType === 'bundle') { + var bundleSource = p.getSource({ + inlineSourceMap: options.inlineSourceMap, + minify: options.minify, + }); + res.setHeader('Content-Type', 'application/javascript'); + res.end(bundleSource); + Activity.endEvent(startReqEventId); + } else if (requestType === 'map') { + var sourceMap = JSON.stringify(p.getSourceMap()); + res.setHeader('Content-Type', 'application/json'); + res.end(sourceMap); + Activity.endEvent(startReqEventId); + } else if (requestType === 'assets') { + var assetsList = JSON.stringify(p.getAssets()); + res.setHeader('Content-Type', 'application/json'); + res.end(assetsList); + Activity.endEvent(startReqEventId); + } + }, + this._handleError.bind(this, res, optionsJson) + ).done(); + } + + _handleError(res, bundleID, error) { + res.writeHead(error.status || 500, { + 'Content-Type': 'application/json; charset=UTF-8', + }); + + if (error.type === 'TransformError' || error.type === 'NotFoundError') { + error.errors = [{ + description: error.description, + filename: error.filename, + lineNumber: error.lineNumber, + }]; + res.end(JSON.stringify(error)); + + if (error.type === 'NotFoundError') { + delete this._bundles[bundleID]; + } + } else { + console.error(error.stack || error); + res.end(JSON.stringify({ + type: 'InternalError', + message: 'react-packager has encountered an internal error, ' + + 'please check your terminal error output for more details', + })); + } + } + + _getOptionsFromUrl(reqUrl) { + // `true` to parse the query param as an object. + const urlObj = url.parse(reqUrl, true); + // node v0.11.14 bug see https://github.com/facebook/react-native/issues/218 + urlObj.query = urlObj.query || {}; + + const pathname = decodeURIComponent(urlObj.pathname); + + // Backwards compatibility. Options used to be as added as '.' to the + // entry module name. We can safely remove these options. + const entryFile = pathname.replace(/^\//, '').split('.').filter(part => { + if (part === 'includeRequire' || part === 'runModule' || + part === 'bundle' || part === 'map' || part === 'assets') { + return false; + } + return true; + }).join('.') + '.js'; + + return { + sourceMapUrl: pathname.replace(/\.bundle$/, '.map'), + entryFile: entryFile, + dev: this._getBoolOptionFromQuery(urlObj.query, 'dev', true), + minify: this._getBoolOptionFromQuery(urlObj.query, 'minify'), + runModule: this._getBoolOptionFromQuery(urlObj.query, 'runModule', true), + inlineSourceMap: this._getBoolOptionFromQuery( + urlObj.query, + 'inlineSourceMap', + false + ), + platform: urlObj.query.platform, + }; + } + + _getBoolOptionFromQuery(query, opt, defaultVal) { + if (query[opt] == null && defaultVal != null) { + return defaultVal; + } + + return query[opt] === 'true' || query[opt] === '1'; + } } + +module.exports = Server; diff --git a/packager/react-packager/src/SocketInterface/SocketClient.js b/packager/react-packager/src/SocketInterface/SocketClient.js new file mode 100644 index 000000000..474223adc --- /dev/null +++ b/packager/react-packager/src/SocketInterface/SocketClient.js @@ -0,0 +1,101 @@ +/** + * 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 Bundle = require('../Bundler/Bundle'); +const Promise = require('promise'); +const bser = require('bser'); +const debug = require('debug')('ReactPackager:SocketClient'); +const net = require('net'); + +class SocketClient { + static create(sockPath) { + return new SocketClient(sockPath).onReady(); + } + + constructor(sockPath) { + debug('connecting to', sockPath); + + this._sock = net.connect(sockPath); + this._ready = new Promise((resolve, reject) => { + this._sock.on('connect', () => resolve(this)); + this._sock.on('error', (e) => reject(e)); + }); + + this._resolvers = Object.create(null); + const bunser = new bser.BunserBuf(); + this._sock.on('data', (buf) => bunser.append(buf)); + + bunser.on('value', (message) => this._handleMessage(message)); + } + + onReady() { + return this._ready; + } + + getDependencies(main) { + return this._send({ + type: 'getDependencies', + data: main, + }); + } + + buildBundle(options) { + return this._send({ + type: 'buildBundle', + data: options, + }).then(json => Bundle.fromJSON(json)); + } + + _send(message) { + message.id = uid(); + this._sock.write(bser.dumpToBuffer(message)); + return new Promise((resolve, reject) => { + this._resolvers[message.id] = {resolve, reject}; + }); + } + + _handleMessage(message) { + if (!(message && message.id && message.type)) { + throw new Error( + 'Malformed message from server ' + JSON.stringify(message) + ); + } + + debug('got message with type', message.type); + + const resolver = this._resolvers[message.id]; + if (!resolver) { + throw new Error( + 'Unrecognized message id (message already resolved or never existed' + ); + } + + delete this._resolvers[message.id]; + + if (message.type === 'error') { + // TODO convert to an error + resolver.reject(message.data); + } else { + resolver.resolve(message.data); + } + } + + close() { + debug('closing connection'); + this._sock.end(); + } +} + +module.exports = SocketClient; + +function uid(len) { + len = len || 7; + return Math.random().toString(35).substr(2, len); +} diff --git a/packager/react-packager/src/SocketInterface/SocketServer.js b/packager/react-packager/src/SocketInterface/SocketServer.js new file mode 100644 index 000000000..693c8e0c6 --- /dev/null +++ b/packager/react-packager/src/SocketInterface/SocketServer.js @@ -0,0 +1,141 @@ +/** + * 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 Promise = require('promise'); +const Server = require('../Server'); +const bser = require('bser'); +const debug = require('debug')('ReactPackager:SocketServer'); +const fs = require('fs'); +const net = require('net'); + +const MAX_IDLE_TIME = 10 * 60 * 1000; + +class SocketServer { + constructor(sockPath, options) { + this._server = net.createServer(); + this._server.listen(sockPath); + this._ready = new Promise((resolve, reject) => { + this._server.on('error', (e) => reject(e)); + this._server.on('listening', () => { + debug( + 'Process %d listening on socket path %s ' + + 'for server with options %j', + process.pid, + sockPath, + options + ); + resolve(this); + }); + }); + this._server.on('connection', (sock) => this._handleConnection(sock)); + + // Disable the file watcher. + options.nonPersistent = true; + this._packagerServer = new Server(options); + this._jobs = 0; + this._dieEventually(); + + process.on('exit', () => fs.unlinkSync(sockPath)); + } + + onReady() { + return this._ready; + } + + _handleConnection(sock) { + debug('connection to server', process.pid); + + const bunser = new bser.BunserBuf(); + sock.on('data', (buf) => bunser.append(buf)); + + bunser.on('value', (m) => this._handleMessage(sock, m)); + } + + _handleMessage(sock, m) { + if (!m || !m.id || !m.data) { + console.error('SocketServer recieved a malformed message: %j', m); + } + + debug('got request', m); + + // Debounce the kill timer. + this._dieEventually(); + + const handleError = (error) => { + debug('request error', error); + this._jobs--; + this._reply(sock, m.id, 'error', error.stack); + }; + + switch (m.type) { + case 'getDependencies': + this._jobs++; + this._packagerServer.getDependencies(m.data).then( + ({ dependencies }) => this._reply(sock, m.id, 'result', dependencies), + handleError, + ); + break; + + case 'buildBundle': + this._jobs++; + this._packagerServer.buildBundle(m.data).then( + (result) => this._reply(sock, m.id, 'result', result), + handleError, + ); + break; + + default: + this._reply(sock, m.id, 'error', 'Unknown message type: ' + m.type); + } + } + + _reply(sock, id, type, data) { + debug('request finished', type); + + this._jobs--; + data = toJSON(data); + + sock.write(bser.dumpToBuffer({ + id, + type, + data, + })); + } + + _dieEventually() { + clearTimeout(this._deathTimer); + this._deathTimer = setTimeout(() => { + if (this._jobs <= 0) { + debug('server dying', process.pid); + process.exit(1); + } + this._dieEventually(); + }, MAX_IDLE_TIME); + } +} + +module.exports = SocketServer; + +// TODO move this to bser code. +function toJSON(object) { + if (!(object && typeof object === 'object')) { + return object; + } + + if (object.toJSON) { + return object.toJSON(); + } + + for (var p in object) { + object[p] = toJSON(object[p]); + } + + return object; +} diff --git a/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js b/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js new file mode 100644 index 000000000..b77d92a05 --- /dev/null +++ b/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js @@ -0,0 +1,112 @@ +/** + * 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'; + +jest.setMock('worker-farm', function() { return () => {}; }) + .setMock('uglify-js') + .mock('net') + .dontMock('../SocketClient'); + +describe('SocketClient', () => { + let SocketClient; + let sock; + let bunser; + + beforeEach(() => { + SocketClient = require('../SocketClient'); + + const {EventEmitter} = require.requireActual('events'); + sock = new EventEmitter(); + sock.write = jest.genMockFn(); + + require('net').connect.mockImpl(() => sock); + + const bser = require('bser'); + bunser = new EventEmitter(); + require('bser').BunserBuf.mockImpl(() => bunser); + bser.dumpToBuffer.mockImpl((a) => a); + + require('../../Bundler/Bundle').fromJSON.mockImpl((a) => a); + }); + + pit('create a connection', () => { + const client = new SocketClient('/sock'); + sock.emit('connect'); + return client.onReady().then(c => { + expect(c).toBe(client); + expect(require('net').connect).toBeCalledWith('/sock'); + }); + }); + + pit('buildBundle', () => { + const client = new SocketClient('/sock'); + sock.emit('connect'); + const options = { entryFile: '/main' }; + + const promise = client.buildBundle(options); + + expect(sock.write).toBeCalled(); + const message = sock.write.mock.calls[0][0]; + expect(message.type).toBe('buildBundle'); + expect(message.data).toEqual(options); + expect(typeof message.id).toBe('string'); + + bunser.emit('value', { + id: message.id, + type: 'result', + data: { bundle: 'foo' }, + }); + + return promise.then(bundle => expect(bundle).toEqual({ bundle: 'foo' })); + }); + + pit('getDependencies', () => { + const client = new SocketClient('/sock'); + sock.emit('connect'); + const main = '/main'; + + const promise = client.getDependencies(main); + + expect(sock.write).toBeCalled(); + const message = sock.write.mock.calls[0][0]; + expect(message.type).toBe('getDependencies'); + expect(message.data).toEqual(main); + expect(typeof message.id).toBe('string'); + + bunser.emit('value', { + id: message.id, + type: 'result', + data: ['a', 'b', 'c'], + }); + + return promise.then(result => expect(result).toEqual(['a', 'b', 'c'])); + }); + + pit('handle errors', () => { + const client = new SocketClient('/sock'); + sock.emit('connect'); + const main = '/main'; + + const promise = client.getDependencies(main); + + expect(sock.write).toBeCalled(); + const message = sock.write.mock.calls[0][0]; + expect(message.type).toBe('getDependencies'); + expect(message.data).toEqual(main); + expect(typeof message.id).toBe('string'); + + bunser.emit('value', { + id: message.id, + type: 'error', + data: 'some error' + }); + + return promise.catch(m => expect(m).toBe('some error')); + }); +}); diff --git a/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js b/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js new file mode 100644 index 000000000..f0940023a --- /dev/null +++ b/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js @@ -0,0 +1,87 @@ +/** + * 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'; + +jest.setMock('worker-farm', function() { return () => {}; }) + .setMock('uglify-js') + .mock('child_process') + .dontMock('../'); + +describe('SocketInterface', () => { + let SocketInterface; + let SocketClient; + + beforeEach(() => { + SocketInterface = require('../'); + SocketClient = require('../SocketClient'); + }); + + describe('getOrCreateSocketFor', () => { + pit('creates socket path by hashing options', () => { + const fs = require('fs'); + fs.existsSync = jest.genMockFn().mockImpl(() => true); + + // Check that given two equivelant server options, we end up with the same + // socket path. + const options1 = { projectRoots: ['/root'], transformModulePath: '/root/foo' }; + const options2 = { transformModulePath: '/root/foo', projectRoots: ['/root'] }; + const options3 = { projectRoots: ['/root', '/root2'] }; + + return SocketInterface.getOrCreateSocketFor(options1).then(() => { + expect(SocketClient.create).toBeCalled(); + return SocketInterface.getOrCreateSocketFor(options2).then(() => { + expect(SocketClient.create.mock.calls.length).toBe(2); + expect(SocketClient.create.mock.calls[0]).toEqual(SocketClient.create.mock.calls[1]); + return SocketInterface.getOrCreateSocketFor(options3).then(() => { + expect(SocketClient.create.mock.calls.length).toBe(3); + expect(SocketClient.create.mock.calls[1]).not.toEqual(SocketClient.create.mock.calls[2]); + }); + }); + }); + }); + + pit('should fork a server', () => { + const fs = require('fs'); + fs.existsSync = jest.genMockFn().mockImpl(() => false); + let sockPath; + let callback; + + require('child_process').spawn.mockImpl(() => ({ + on: (event, cb) => callback = cb, + send: (message) => { + expect(message.type).toBe('createSocketServer'); + expect(message.data.options).toEqual({ projectRoots: ['/root'] }); + expect(message.data.sockPath).toContain('react-packager'); + sockPath = message.data.sockPath; + + setImmediate(() => callback({ type: 'createdServer' })); + }, + unref: () => undefined, + disconnect: () => undefined, + })); + + return SocketInterface.getOrCreateSocketFor({ projectRoots: ['/root'] }) + .then(() => { + expect(SocketClient.create).toBeCalledWith(sockPath); + }); + }); + }); + + describe('createSocketServer', () => { + pit('creates a server', () => { + require('../SocketServer').mockImpl((sockPath, options) => { + expect(sockPath).toBe('/socket'); + expect(options).toEqual({ projectRoots: ['/root'] }); + return { onReady: () => Promise.resolve() }; + }); + + return SocketInterface.createSocketServer('/socket', { projectRoots: ['/root'] }); + }); + }); +}); diff --git a/packager/react-packager/src/SocketInterface/__tests__/SocketServer-test.js b/packager/react-packager/src/SocketInterface/__tests__/SocketServer-test.js new file mode 100644 index 000000000..ae3e8e248 --- /dev/null +++ b/packager/react-packager/src/SocketInterface/__tests__/SocketServer-test.js @@ -0,0 +1,94 @@ +/** + * 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'; + +jest.setMock('worker-farm', function() { return () => {}; }) + .setMock('uglify-js') + .mock('net') + .mock('fs') + .dontMock('../SocketServer'); + +describe('SocketServer', () => { + let PackagerServer; + let SocketServer; + let netServer; + let bunser; + + beforeEach(() => { + SocketServer = require('../SocketServer'); + + const {EventEmitter} = require.requireActual('events'); + netServer = new EventEmitter(); + netServer.listen = jest.genMockFn(); + require('net').createServer.mockImpl(() => netServer); + + const bser = require('bser'); + bunser = new EventEmitter(); + bser.BunserBuf.mockImpl(() => bunser); + bser.dumpToBuffer.mockImpl((a) => a); + + PackagerServer = require('../../Server'); + }); + + pit('create a server', () => { + const server = new SocketServer('/sock', { projectRoots: ['/root'] }); + netServer.emit('listening'); + return server.onReady().then(s => { + expect(s).toBe(server); + expect(netServer.listen).toBeCalledWith('/sock'); + }); + }); + + pit('handles getDependencies message', () => { + const server = new SocketServer('/sock', { projectRoots: ['/root'] }); + netServer.emit('listening'); + return server.onReady().then(() => { + const sock = { on: jest.genMockFn(), write: jest.genMockFn() }; + netServer.emit('connection', sock); + PackagerServer.prototype.getDependencies.mockImpl( + () => Promise.resolve({ dependencies: ['a', 'b', 'c'] }) + ); + bunser.emit('value', { type: 'getDependencies', id: 1, data: '/main' }); + expect(PackagerServer.prototype.getDependencies).toBeCalledWith('/main'); + + // Run pending promises. + return Promise.resolve().then(() => { + expect(sock.write).toBeCalledWith( + { id: 1, type: 'result', data: ['a', 'b', 'c']} + ); + }); + }); + }); + + pit('handles buildBundle message', () => { + const server = new SocketServer('/sock', { projectRoots: ['/root'] }); + netServer.emit('listening'); + return server.onReady().then(() => { + const sock = { on: jest.genMockFn(), write: jest.genMockFn() }; + netServer.emit('connection', sock); + PackagerServer.prototype.buildBundle.mockImpl( + () => Promise.resolve({ bundle: 'foo' }) + ); + bunser.emit( + 'value', + { type: 'buildBundle', id: 1, data: { options: 'bar' } } + ); + expect(PackagerServer.prototype.buildBundle).toBeCalledWith( + { options: 'bar' } + ); + + // Run pending promises. + return Promise.resolve().then(() => { + expect(sock.write).toBeCalledWith( + { id: 1, type: 'result', data: { bundle: 'foo' }} + ); + }); + }); + }); +}); diff --git a/packager/react-packager/src/SocketInterface/index.js b/packager/react-packager/src/SocketInterface/index.js new file mode 100644 index 000000000..87837b291 --- /dev/null +++ b/packager/react-packager/src/SocketInterface/index.js @@ -0,0 +1,102 @@ +/** + * 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 Promise = require('promise'); +const SocketClient = require('./SocketClient'); +const SocketServer = require('./SocketServer'); +const _ = require('underscore'); +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const tmpdir = require('os').tmpdir(); +const {spawn} = require('child_process'); + +const CREATE_SERVER_TIMEOUT = 10000; + +const SocketInterface = { + getOrCreateSocketFor(options) { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('md5'); + Object.keys(options).sort().forEach(key => { + if (options[key] && typeof options[key] !== 'string') { + hash.update(JSON.stringify(options[key])); + } else { + hash.update(options[key]); + } + }); + + const sockPath = path.join( + tmpdir, + 'react-packager-' + hash.digest('hex') + ); + + 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(); + resolve(SocketClient.create(sockPath)); + } + }); + + + if (options.blacklistRE) { + options.blacklistRE = { source: options.blacklistRE.source }; + } + + child.send({ + type: 'createSocketServer', + data: { sockPath, options } + }); + }); + }, + + createSocketServer(sockPath, options) { + return new SocketServer(sockPath, options).onReady(); + } +}; + +module.exports = SocketInterface; diff --git a/packager/react-packager/src/__mocks__/fs.js b/packager/react-packager/src/__mocks__/fs.js index b5251a447..ced46a984 100644 --- a/packager/react-packager/src/__mocks__/fs.js +++ b/packager/react-packager/src/__mocks__/fs.js @@ -10,7 +10,14 @@ var fs = jest.genMockFromModule('fs'); +function asyncCallback(callback) { + return function() { + setImmediate(() => callback.apply(this, arguments)); + }; +} + fs.realpath.mockImpl(function(filepath, callback) { + callback = asyncCallback(callback); var node; try { node = getToNode(filepath); @@ -24,6 +31,7 @@ fs.realpath.mockImpl(function(filepath, callback) { }); fs.readdir.mockImpl(function(filepath, callback) { + callback = asyncCallback(callback); var node; try { node = getToNode(filepath); @@ -42,6 +50,7 @@ fs.readdir.mockImpl(function(filepath, callback) { }); fs.readFile.mockImpl(function(filepath, encoding, callback) { + callback = asyncCallback(callback); if (arguments.length === 2) { callback = encoding; encoding = null; @@ -60,6 +69,7 @@ fs.readFile.mockImpl(function(filepath, encoding, callback) { }); fs.stat.mockImpl(function(filepath, callback) { + callback = asyncCallback(callback); var node; try { node = getToNode(filepath); diff --git a/packager/transformer.js b/packager/transformer.js index a77119312..8f7a48c29 100644 --- a/packager/transformer.js +++ b/packager/transformer.js @@ -13,6 +13,12 @@ var babel = require('babel-core'); function transform(srcTxt, filename, options) { + var plugins = []; + + if (process.env.NODE_ENV === 'production') { + plugins = plugins.concat(['node-env-inline', 'dunderscore-dev-inline']); + } + var result = babel.transform(srcTxt, { retainLines: true, compact: true, @@ -35,6 +41,7 @@ function transform(srcTxt, filename, options) { 'react', 'regenerator', ], + plugins: plugins, sourceFileName: filename, sourceMaps: false, extra: options || {}, diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index 03e9b6903..52e751e9d 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -190,7 +190,7 @@ var apis = [ '../Libraries/Storage/AsyncStorage.ios.js', '../Libraries/CameraRoll/CameraRoll.js', '../Libraries/Interaction/InteractionManager.js', - '../Libraries/Animation/LayoutAnimation.js', + '../Libraries/LayoutAnimation/LayoutAnimation.js', '../Libraries/LinkingIOS/LinkingIOS.js', '../Libraries/Network/NetInfo.js', '../Libraries/vendor/react/browser/eventPlugins/PanResponder.js',