diff --git a/.eslintrc b/.eslintrc index b037ffe48..9dd9df8a4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,7 +21,7 @@ "Map": true, "module": false, "process": false, - "Promise": false, + "Promise": true, "requestAnimationFrame": true, "require": false, "Set": true, @@ -29,7 +29,8 @@ "setInterval": false, "setTimeout": false, "window": false, - "XMLHttpRequest": false + "XMLHttpRequest": false, + "pit": false }, "rules": { @@ -148,7 +149,7 @@ "no-multi-spaces": 0, "brace-style": 0, // enforce one true brace style (off by default) "camelcase": 0, // require camel case names - "consistent-this": 1, // enforces consistent naming when capturing the current execution context (off by default) + "consistent-this": [1, "self"], // enforces consistent naming when capturing the current execution context (off by default) "eol-last": 1, // enforce newline at the end of file, with no multiple empty lines "func-names": 0, // require function expressions to have a name (off by default) "func-style": 0, // enforces use of function declarations or expressions (off by default) diff --git a/.flowconfig b/.flowconfig index 20bb68756..82d818dcc 100644 --- a/.flowconfig +++ b/.flowconfig @@ -9,7 +9,7 @@ # Ignore react-tools where there are overlaps, but don't ignore anything that # react-native relies on -.*/node_modules/react-tools/src/vendor/.* +.*/node_modules/react-tools/src/vendor/core/ExecutionEnvironment.js .*/node_modules/react-tools/src/browser/.* .*/node_modules/react-tools/src/core/ReactInstanceHandles.js .*/node_modules/react-tools/src/event/.* @@ -17,9 +17,6 @@ # Ignore jest .*/react-native/node_modules/jest-cli/.* -# Ignore Libraries -.*/Libraries/.* - [include] [libs] diff --git a/.gitignore b/.gitignore index 392c6ee02..6f1bc0b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # Xcode -# !**/*.xcodeproj !**/*.pbxproj !**/*.xcworkspacedata @@ -22,4 +21,8 @@ DerivedData *.ipa *.xcuserstate +# OS X +.DS_Store + +# Node node_modules diff --git a/.travis.yml b/.travis.yml index 6e5919de3..4af3669f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,28 @@ -language: node_js -node_js: - - "0.10" +language: objective-c +before_install: + - brew update + - brew reinstall xctool +install: + - brew install watchman + - npm install +before_script: + - npm test + - (npm start > packager.log 2>&1 &) && echo $! > packager.pid + +script: ./build.sh 8.1 + +after_script: + - pkill -9 -F packager.pid + - cat packager.log + - rm packager.log + - rm packager.pid + +# Automatically publish the website +after_success: '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && (cd website; npm install; ./setup.sh; ./publish.sh; echo) || echo' + +env: + - secure: "g8Xjbslq4R+3oLVgBvXM5QhiJ+7q+H+dH4+sXReyyZ64M5gf32U7oOjQNVkdx79dnGFc6a619otGsX4RiEkQlkiR+4uCgJUIgPwN4YFCIgYu4Z/0FnbbRu5yyywh5zv4WwGmAtMkgGztoMBnps5gCiPUM/RGIqwVk0Ghbmh5c2k=" + +branches: + only: + - master diff --git a/Examples/2048/2048.xcodeproj/project.pbxproj b/Examples/2048/2048.xcodeproj/project.pbxproj new file mode 100644 index 000000000..ba1907ff1 --- /dev/null +++ b/Examples/2048/2048.xcodeproj/project.pbxproj @@ -0,0 +1,358 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; + 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832348291A77B50100B55238 /* libReactKit.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; + 832348281A77B50100B55238 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; + remoteInfo = ReactKit; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 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 = ""; }; + 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = 2048/Images.xcassets; sourceTree = ""; }; + 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 = ""; }; + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; + 834D32361A76971A00F38302 /* ReactKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactKit.xcodeproj; path = ../../ReactKit/ReactKit.xcodeproj; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */, + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* 2048 */ = { + isa = PBXGroup; + children = ( + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, + 13B07FB71A68108700A75B9A /* main.m */, + ); + name = 2048; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + 834D32361A76971A00F38302 /* ReactKit.xcodeproj */, + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, + ); + name = Libraries; + sourceTree = ""; + }; + 832341B11AAA6A8300B99B32 /* Products */ = { + isa = PBXGroup; + children = ( + 832341B51AAA6A8300B99B32 /* libRCTText.a */, + ); + name = Products; + sourceTree = ""; + }; + 832348241A77B50100B55238 /* Products */ = { + isa = PBXGroup; + children = ( + 832348291A77B50100B55238 /* libReactKit.a */, + ); + name = Products; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* 2048 */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + ); + sourceTree = ""; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* 2048.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* 2048 */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "2048" */; + buildPhases = ( + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = 2048; + productName = "Hello World"; + productReference = 13B07F961A680F5B00A75B9A /* 2048.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "2048" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; + ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + }, + { + ProductGroup = 832348241A77B50100B55238 /* Products */; + ProjectRef = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* 2048 */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 832348291A77B50100B55238 /* libReactKit.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReactKit.a; + remoteRef = 832348281A77B50100B55238 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 13B07FB21A68108700A75B9A /* Base */, + ); + name = LaunchScreen.xib; + path = 2048; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "$(SRCROOT)/2048/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = 2048; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "$(SRCROOT)/2048/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = 2048; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "2048" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "2048" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/Examples/TicTacToe/AppDelegate.h b/Examples/2048/2048/AppDelegate.h similarity index 100% rename from Examples/TicTacToe/AppDelegate.h rename to Examples/2048/2048/AppDelegate.h diff --git a/Examples/2048/2048/AppDelegate.m b/Examples/2048/2048/AppDelegate.m new file mode 100644 index 000000000..32d9dd85c --- /dev/null +++ b/Examples/2048/2048/AppDelegate.m @@ -0,0 +1,44 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "AppDelegate.h" + +#import "RCTRootView.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + NSURL *jsCodeLocation; + RCTRootView *rootView = [[RCTRootView alloc] init]; + + // Loading JavaScript code - uncomment the one you want. + + // OPTION 1 + // Load from development server. Start the server from the repository root: + // + // $ npm start + // + // To run on device, change `localhost` to the IP address of your computer, and make sure your computer and + // iOS device are on the same Wi-Fi network. + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle"]; + + // OPTION 2 + // Load from pre-bundled file on disk. To re-generate the static bundle, run + // + // $ curl http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle -o main.jsbundle + // + // and uncomment the next following line + // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + + rootView.scriptURL = jsCodeLocation; + rootView.moduleName = @"Game2048"; + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [[UIViewController alloc] init]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/Examples/2048/2048/Base.lproj/LaunchScreen.xib b/Examples/2048/2048/Base.lproj/LaunchScreen.xib new file mode 100644 index 000000000..351e21c59 --- /dev/null +++ b/Examples/2048/2048/Base.lproj/LaunchScreen.xib @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Movies/Images.xcassets/AppIcon.appiconset/Contents.json b/Examples/2048/2048/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Examples/Movies/Images.xcassets/AppIcon.appiconset/Contents.json rename to Examples/2048/2048/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/Examples/UIExplorer/Info.plist b/Examples/2048/2048/Info.plist similarity index 94% rename from Examples/UIExplorer/Info.plist rename to Examples/2048/2048/Info.plist index 9a7ca7e3c..1c298405a 100644 --- a/Examples/UIExplorer/Info.plist +++ b/Examples/2048/2048/Info.plist @@ -34,7 +34,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - diff --git a/Examples/2048/2048/main.m b/Examples/2048/2048/main.m new file mode 100644 index 000000000..3c8987ce3 --- /dev/null +++ b/Examples/2048/2048/main.m @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/Examples/2048/Game2048.js b/Examples/2048/Game2048.js new file mode 100644 index 000000000..f4ad51b41 --- /dev/null +++ b/Examples/2048/Game2048.js @@ -0,0 +1,305 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Game2048 + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Animation, + AppRegistry, + StyleSheet, + Text, + View, +} = React; + +var GameBoard = require('GameBoard'); +var TouchableBounce = require('TouchableBounce'); + +var BOARD_PADDING = 3; +var CELL_MARGIN = 4; +var CELL_SIZE = 60; + +class Cell extends React.Component { + render() { + return ; + } +} + +class Board extends React.Component { + render() { + return ( + + + + + + {this.props.children} + + ); + } +} + +class Tile extends React.Component { + calculateOffset(): {top: number; left: number; opacity: number} { + var tile = this.props.tile; + + var pos = (i) => { + return BOARD_PADDING + (i * (CELL_SIZE + CELL_MARGIN * 2) + CELL_MARGIN); + }; + + var animationPosition = (i) => { + return pos(i) + (CELL_SIZE / 2); + }; + + var offset = { + top: pos(tile.toRow()), + left: pos(tile.toColumn()), + opacity: 1, + }; + + if (tile.isNew()) { + offset.opacity = 0; + } else { + var point = [ + animationPosition(tile.toColumn()), + animationPosition(tile.toRow()), + ]; + Animation.startAnimation(this.refs['this'], 100, 0, 'easeInOutQuad', {position: point}); + } + + return offset; + } + + componentDidMount() { + setTimeout(() => { + Animation.startAnimation(this.refs['this'], 300, 0, 'easeInOutQuad', {scaleXY: [1, 1]}); + Animation.startAnimation(this.refs['this'], 100, 0, 'easeInOutQuad', {opacity: 1}); + }, 0); + } + + render() { + var tile = this.props.tile; + + var tileStyles = [ + styles.tile, + styles['tile' + tile.value], + this.calculateOffset() + ]; + + var textStyles = [ + styles.value, + tile.value > 4 && styles.whiteText, + tile.value > 100 && styles.threeDigits, + tile.value > 1000 && styles.fourDigits, + ]; + + return ( + + {tile.value} + + ); + } +} + +class GameEndOverlay extends React.Component { + render() { + var board = this.props.board; + + if (!board.hasWon() && !board.hasLost()) { + return ; + } + + var message = board.hasWon() ? + 'Good Job!' : 'Game Over'; + + return ( + + {message} + + + Try Again? + + + + ); + } +} + +class Game2048 extends React.Component { + startX: number; + startY: number; + + constructor(props) { + super(props); + this.state = { + board: new GameBoard(), + }; + this.startX = 0; + this.startY = 0; + } + + restartGame() { + this.setState({board: new GameBoard()}); + } + + handleTouchStart(event: Object) { + if (this.state.board.hasWon()) { + return; + } + + this.startX = event.nativeEvent.pageX; + this.startY = event.nativeEvent.pageY; + } + + handleTouchEnd(event: Object) { + if (this.state.board.hasWon()) { + return; + } + + var deltaX = event.nativeEvent.pageX - this.startX; + var deltaY = event.nativeEvent.pageY - this.startY; + + var direction = -1; + if (Math.abs(deltaX) > 3 * Math.abs(deltaY) && Math.abs(deltaX) > 30) { + direction = deltaX > 0 ? 2 : 0; + } else if (Math.abs(deltaY) > 3 * Math.abs(deltaX) && Math.abs(deltaY) > 30) { + direction = deltaY > 0 ? 3 : 1; + } + + if (direction !== -1) { + this.setState({board: this.state.board.move(direction)}); + } + } + + render() { + var tiles = this.state.board.tiles + .filter((tile) => tile.value) + .map((tile) => ); + + return ( + this.handleTouchStart(event)} + onTouchEnd={(event) => this.handleTouchEnd(event)}> + + {tiles} + + this.restartGame()} /> + + ); + } +} + +var styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + board: { + padding: BOARD_PADDING, + backgroundColor: '#bbaaaa', + borderRadius: 5, + }, + overlay: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + backgroundColor: 'rgba(221, 221, 221, 0.5)', + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + overlayMessage: { + fontSize: 40, + marginBottom: 20, + }, + tryAgain: { + backgroundColor: '#887766', + padding: 20, + borderRadius: 5, + }, + tryAgainText: { + color: '#ffffff', + fontSize: 20, + fontWeight: 'bold', + }, + cell: { + width: CELL_SIZE, + height: CELL_SIZE, + borderRadius: 5, + backgroundColor: '#ddccbb', + margin: CELL_MARGIN, + }, + row: { + flexDirection: 'row', + }, + tile: { + position: 'absolute', + width: CELL_SIZE, + height: CELL_SIZE, + backgroundColor: '#ddccbb', + borderRadius: 5, + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + value: { + fontSize: 24, + color: '#776666', + fontFamily: 'Verdana', + fontWeight: 'bold', + }, + tile2: { + backgroundColor: '#eeeeee', + }, + tile4: { + backgroundColor: '#eeeecc', + }, + tile8: { + backgroundColor: '#ffbb88', + }, + tile16: { + backgroundColor: '#ff9966', + }, + tile32: { + backgroundColor: '#ff7755', + }, + tile64: { + backgroundColor: '#ff5533', + }, + tile128: { + backgroundColor: '#eecc77', + }, + tile256: { + backgroundColor: '#eecc66', + }, + tile512: { + backgroundColor: '#eecc55', + }, + tile1024: { + backgroundColor: '#eecc33', + }, + tile2048: { + backgroundColor: '#eecc22', + }, + whiteText: { + color: '#ffffff', + }, + threeDigits: { + fontSize: 20, + }, + fourDigits: { + fontSize: 18, + }, +}); + +AppRegistry.registerComponent('Game2048', () => Game2048); + +module.exports = Game2048; diff --git a/Examples/2048/GameBoard.js b/Examples/2048/GameBoard.js new file mode 100644 index 000000000..39b72141a --- /dev/null +++ b/Examples/2048/GameBoard.js @@ -0,0 +1,191 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule GameBoard + * @flow + */ +'use strict'; + +// NB: Taken straight from: https://github.com/IvanVergiliev/2048-react/blob/master/src/board.js +// with no modificiation except to format it for CommonJS and fix lint/flow errors + +var rotateLeft = function (matrix) { + var rows = matrix.length; + var columns = matrix[0].length; + var res = []; + for (var row = 0; row < rows; ++row) { + res.push([]); + for (var column = 0; column < columns; ++column) { + res[row][column] = matrix[column][columns - row - 1]; + } + } + return res; +}; + +var Tile = function (value?: number, row?: number, column?: number) { + this.value = value || 0; + this.row = row || -1; + + this.column = column || -1; + this.oldRow = -1; + this.oldColumn = -1; + this.markForDeletion = false; + this.mergedInto = null; + this.id = Tile.id++; +}; + +Tile.id = 0; + +Tile.prototype.moveTo = function (row, column) { + this.oldRow = this.row; + this.oldColumn = this.column; + this.row = row; + this.column = column; +}; + +Tile.prototype.isNew = function () { + return this.oldRow === -1 && !this.mergedInto; +}; + +Tile.prototype.hasMoved = function () { + return (this.fromRow() !== -1 && (this.fromRow() !== this.toRow() || this.fromColumn() !== this.toColumn())) || + this.mergedInto; +}; + +Tile.prototype.fromRow = function () { + return this.mergedInto ? this.row : this.oldRow; +}; + +Tile.prototype.fromColumn = function () { + return this.mergedInto ? this.column : this.oldColumn; +}; + +Tile.prototype.toRow = function () { + return this.mergedInto ? this.mergedInto.row : this.row; +}; + +Tile.prototype.toColumn = function () { + return this.mergedInto ? this.mergedInto.column : this.column; +}; + +var Board = function () { + this.tiles = []; + this.cells = []; + for (var i = 0; i < Board.size; ++i) { + this.cells[i] = [this.addTile(), this.addTile(), this.addTile(), this.addTile()]; + } + this.addRandomTile(); + this.setPositions(); + this.won = false; +}; + +Board.prototype.addTile = function () { + var res = new Tile(); + Tile.apply(res, arguments); + this.tiles.push(res); + return res; +}; + +Board.size = 4; + +Board.prototype.moveLeft = function () { + var hasChanged = false; + for (var row = 0; row < Board.size; ++row) { + var currentRow = this.cells[row].filter(function (tile) { return tile.value !== 0; }); + var resultRow = []; + for (var target = 0; target < Board.size; ++target) { + var targetTile = currentRow.length ? currentRow.shift() : this.addTile(); + if (currentRow.length > 0 && currentRow[0].value === targetTile.value) { + var tile1 = targetTile; + targetTile = this.addTile(targetTile.value); + tile1.mergedInto = targetTile; + var tile2 = currentRow.shift(); + tile2.mergedInto = targetTile; + targetTile.value += tile2.value; + } + resultRow[target] = targetTile; + this.won = this.won || (targetTile.value === 2048); + hasChanged = hasChanged || (targetTile.value !== this.cells[row][target].value); + } + this.cells[row] = resultRow; + } + return hasChanged; +}; + +Board.prototype.setPositions = function () { + this.cells.forEach(function (row, rowIndex) { + row.forEach(function (tile, columnIndex) { + tile.oldRow = tile.row; + tile.oldColumn = tile.column; + tile.row = rowIndex; + tile.column = columnIndex; + tile.markForDeletion = false; + }); + }); +}; + +Board.fourProbability = 0.1; + +Board.prototype.addRandomTile = function () { + var emptyCells = []; + for (var r = 0; r < Board.size; ++r) { + for (var c = 0; c < Board.size; ++c) { + if (this.cells[r][c].value === 0) { + emptyCells.push({r: r, c: c}); + } + } + } + var index = Math.floor(Math.random() * emptyCells.length); + var cell = emptyCells[index]; + var newValue = Math.random() < Board.fourProbability ? 4 : 2; + this.cells[cell.r][cell.c] = this.addTile(newValue); +}; + +Board.prototype.move = function (direction) { + // 0 -> left, 1 -> up, 2 -> right, 3 -> down + this.clearOldTiles(); + for (var i = 0; i < direction; ++i) { + this.cells = rotateLeft(this.cells); + } + var hasChanged = this.moveLeft(); + for (var i = direction; i < 4; ++i) { + this.cells = rotateLeft(this.cells); + } + if (hasChanged) { + this.addRandomTile(); + } + this.setPositions(); + return this; +}; + +Board.prototype.clearOldTiles = function () { + this.tiles = this.tiles.filter(function (tile) { return tile.markForDeletion === false; }); + this.tiles.forEach(function (tile) { tile.markForDeletion = true; }); +}; + +Board.prototype.hasWon = function () { + return this.won; +}; + +Board.deltaX = [-1, 0, 1, 0]; +Board.deltaY = [0, -1, 0, 1]; + +Board.prototype.hasLost = function () { + var canMove = false; + for (var row = 0; row < Board.size; ++row) { + for (var column = 0; column < Board.size; ++column) { + canMove = canMove || (this.cells[row][column].value === 0); + for (var dir = 0; dir < 4; ++dir) { + var newRow = row + Board.deltaX[dir]; + var newColumn = column + Board.deltaY[dir]; + if (newRow < 0 || newRow >= Board.size || newColumn < 0 || newColumn >= Board.size) { + continue; + } + canMove = canMove || (this.cells[row][column].value === this.cells[newRow][newColumn].value); + } + } + } + return !canMove; +}; + +module.exports = Board; diff --git a/Examples/Movies/MovieScreen.js b/Examples/Movies/MovieScreen.js index 53c8879dc..8584d8753 100644 --- a/Examples/Movies/MovieScreen.js +++ b/Examples/Movies/MovieScreen.js @@ -6,7 +6,6 @@ var React = require('react-native'); var { - ExpandingText, Image, PixelRatio, ScrollView, @@ -40,10 +39,9 @@ var MovieScreen = React.createClass({ - + + {this.props.movie.synopsis} + diff --git a/Examples/Movies/Movies.xcodeproj/project.pbxproj b/Examples/Movies/Movies.xcodeproj/project.pbxproj index e2fd6fc01..3d22844d8 100644 --- a/Examples/Movies/Movies.xcodeproj/project.pbxproj +++ b/Examples/Movies/Movies.xcodeproj/project.pbxproj @@ -7,17 +7,41 @@ objects = { /* Begin PBXBuildFile section */ + 1341801E1AA91750003F314A /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1341801B1AA91740003F314A /* libRCTNetwork.a */; }; + 13442C061AA90EA00037E5B0 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13442C051AA90E7D0037E5B0 /* libRCTImage.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832348291A77B50100B55238 /* libReactKit.a */; }; + 58C5726B1AA6239E00CDF9C8 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C5725B1AA6236500CDF9C8 /* libRCTText.a */; }; + 58C5726C1AA623A200CDF9C8 /* libReactKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C572681AA6236600CDF9C8 /* libReactKit.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 832348281A77B50100B55238 /* PBXContainerItemProxy */ = { + 1341801A1AA91740003F314A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + containerPortal = 134180151AA91740003F314A /* RCTNetwork.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B511DB1A9E6C8500147676; + remoteInfo = RCTNetwork; + }; + 13442C041AA90E7D0037E5B0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13442C001AA90E7D0037E5B0 /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5115D1A9E6B3D00147676; + remoteInfo = RCTImage; + }; + 58C5725A1AA6236500CDF9C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 587650F61A9EB120008B8F17 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; + 58C572671AA6236600CDF9C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 587650F91A9EB120008B8F17 /* ReactKit.xcodeproj */; proxyType = 2; remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = ReactKit; @@ -25,14 +49,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 134180151AA91740003F314A /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; + 13442C001AA90E7D0037E5B0 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../../Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* Movies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Movies.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Movies/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Movies/AppDelegate.m; sourceTree = ""; }; 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; - 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 834D32361A76971A00F38302 /* ReactKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactKit.xcodeproj; path = ../../ReactKit/ReactKit.xcodeproj; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Movies/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Movies/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Movies/main.m; sourceTree = ""; }; + 587650F61A9EB120008B8F17 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = SOURCE_ROOT; }; + 587650F91A9EB120008B8F17 /* ReactKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactKit.xcodeproj; path = ../../ReactKit/ReactKit.xcodeproj; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -40,13 +67,32 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */, + 1341801E1AA91750003F314A /* libRCTNetwork.a in Frameworks */, + 13442C061AA90EA00037E5B0 /* libRCTImage.a in Frameworks */, + 58C5726B1AA6239E00CDF9C8 /* libRCTText.a in Frameworks */, + 58C5726C1AA623A200CDF9C8 /* libReactKit.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 134180161AA91740003F314A /* Products */ = { + isa = PBXGroup; + children = ( + 1341801B1AA91740003F314A /* libRCTNetwork.a */, + ); + name = Products; + sourceTree = ""; + }; + 13442C011AA90E7D0037E5B0 /* Products */ = { + isa = PBXGroup; + children = ( + 13442C051AA90E7D0037E5B0 /* libRCTImage.a */, + ); + name = Products; + sourceTree = ""; + }; 13B07FAE1A68108700A75B9A /* Movies */ = { isa = PBXGroup; children = ( @@ -60,10 +106,29 @@ name = Movies; sourceTree = ""; }; - 832348241A77B50100B55238 /* Products */ = { + 58C571FC1AA6124500CDF9C8 /* Libraries */ = { isa = PBXGroup; children = ( - 832348291A77B50100B55238 /* libReactKit.a */, + 134180151AA91740003F314A /* RCTNetwork.xcodeproj */, + 13442C001AA90E7D0037E5B0 /* RCTImage.xcodeproj */, + 587650F61A9EB120008B8F17 /* RCTText.xcodeproj */, + 587650F91A9EB120008B8F17 /* ReactKit.xcodeproj */, + ); + name = Libraries; + sourceTree = ""; + }; + 58C572571AA6236500CDF9C8 /* Products */ = { + isa = PBXGroup; + children = ( + 58C5725B1AA6236500CDF9C8 /* libRCTText.a */, + ); + name = Products; + sourceTree = ""; + }; + 58C5725C1AA6236500CDF9C8 /* Products */ = { + isa = PBXGroup; + children = ( + 58C572681AA6236600CDF9C8 /* libReactKit.a */, ); name = Products; sourceTree = ""; @@ -71,8 +136,8 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( - 834D32361A76971A00F38302 /* ReactKit.xcodeproj */, 13B07FAE1A68108700A75B9A /* Movies */, + 58C571FC1AA6124500CDF9C8 /* Libraries */, 83CBBA001A601CBA00E9B192 /* Products */, ); sourceTree = ""; @@ -127,8 +192,20 @@ projectDirPath = ""; projectReferences = ( { - ProductGroup = 832348241A77B50100B55238 /* Products */; - ProjectRef = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + ProductGroup = 13442C011AA90E7D0037E5B0 /* Products */; + ProjectRef = 13442C001AA90E7D0037E5B0 /* RCTImage.xcodeproj */; + }, + { + ProductGroup = 134180161AA91740003F314A /* Products */; + ProjectRef = 134180151AA91740003F314A /* RCTNetwork.xcodeproj */; + }, + { + ProductGroup = 58C572571AA6236500CDF9C8 /* Products */; + ProjectRef = 587650F61A9EB120008B8F17 /* RCTText.xcodeproj */; + }, + { + ProductGroup = 58C5725C1AA6236500CDF9C8 /* Products */; + ProjectRef = 587650F91A9EB120008B8F17 /* ReactKit.xcodeproj */; }, ); projectRoot = ""; @@ -139,11 +216,32 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - 832348291A77B50100B55238 /* libReactKit.a */ = { + 1341801B1AA91740003F314A /* libRCTNetwork.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTNetwork.a; + remoteRef = 1341801A1AA91740003F314A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 13442C051AA90E7D0037E5B0 /* libRCTImage.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTImage.a; + remoteRef = 13442C041AA90E7D0037E5B0 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 58C5725B1AA6236500CDF9C8 /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 58C5725A1AA6236500CDF9C8 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 58C572681AA6236600CDF9C8 /* libReactKit.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libReactKit.a; - remoteRef = 832348281A77B50100B55238 /* PBXContainerItemProxy */; + remoteRef = 58C572671AA6236600CDF9C8 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ @@ -179,6 +277,7 @@ 13B07FB21A68108700A75B9A /* Base */, ); name = LaunchScreen.xib; + path = Movies; sourceTree = ""; }; /* End PBXVariantGroup section */ @@ -188,8 +287,17 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/Movies/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "/Users/nicklockwood/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/Libraries/Network/build/Debug-iphoneos", + ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = Movies; }; @@ -199,8 +307,17 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/Movies/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "/Users/nicklockwood/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/Libraries/Network/build/Debug-iphoneos", + ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = Movies; }; diff --git a/Examples/Movies/AppDelegate.h b/Examples/Movies/Movies/AppDelegate.h similarity index 100% rename from Examples/Movies/AppDelegate.h rename to Examples/Movies/Movies/AppDelegate.h diff --git a/Examples/Movies/AppDelegate.m b/Examples/Movies/Movies/AppDelegate.m similarity index 100% rename from Examples/Movies/AppDelegate.m rename to Examples/Movies/Movies/AppDelegate.m diff --git a/Examples/Movies/Base.lproj/LaunchScreen.xib b/Examples/Movies/Movies/Base.lproj/LaunchScreen.xib similarity index 100% rename from Examples/Movies/Base.lproj/LaunchScreen.xib rename to Examples/Movies/Movies/Base.lproj/LaunchScreen.xib diff --git a/Examples/TicTacToe/Images.xcassets/AppIcon.appiconset/Contents.json b/Examples/Movies/Movies/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Examples/TicTacToe/Images.xcassets/AppIcon.appiconset/Contents.json rename to Examples/Movies/Movies/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/Examples/Movies/Info.plist b/Examples/Movies/Movies/Info.plist similarity index 100% rename from Examples/Movies/Info.plist rename to Examples/Movies/Movies/Info.plist diff --git a/Examples/Movies/main.m b/Examples/Movies/Movies/main.m similarity index 100% rename from Examples/Movies/main.m rename to Examples/Movies/Movies/main.m diff --git a/Examples/Movies/MoviesApp.js b/Examples/Movies/MoviesApp.js index 768ad895a..6c74c45b5 100644 --- a/Examples/Movies/MoviesApp.js +++ b/Examples/Movies/MoviesApp.js @@ -6,7 +6,7 @@ */ 'use strict'; -var React = require('react-native/addons'); +var React = require('react-native'); var { AppRegistry, NavigatorIOS, diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index 2cd67aa19..700be18a1 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -7,7 +7,6 @@ var React = require('react-native'); var { ListView, - ListViewDataSource, ScrollView, ActivityIndicatorIOS, StyleSheet, @@ -40,11 +39,13 @@ var LOADING = {}; var SearchScreen = React.createClass({ mixins: [TimerMixin], + timeoutID: (null: any), + getInitialState: function() { return { isLoading: false, isLoadingTail: false, - dataSource: new ListViewDataSource({ + dataSource: new ListView.DataSource({ rowHasChanged: (row1, row2) => row1 !== row2, }), filter: '', @@ -100,6 +101,15 @@ var SearchScreen = React.createClass({ fetch(this._urlForQueryAndPage(query, 1)) .then((response) => response.json()) + .catch((error) => { + LOADING[query] = false; + resultsCache.dataForQuery[query] = undefined; + + this.setState({ + dataSource: this.getDataSource([]), + isLoading: false, + }); + }) .then((responseData) => { LOADING[query] = false; resultsCache.totalForQuery[query] = responseData.total; @@ -116,15 +126,7 @@ var SearchScreen = React.createClass({ dataSource: this.getDataSource(responseData.movies), }); }) - .catch((error) => { - LOADING[query] = false; - resultsCache.dataForQuery[query] = undefined; - - this.setState({ - dataSource: this.getDataSource([]), - isLoading: false, - }); - }); + .done(); }, hasMore: function(): boolean { @@ -158,6 +160,13 @@ var SearchScreen = React.createClass({ var page = resultsCache.nextPageNumberForQuery[query]; fetch(this._urlForQueryAndPage(query, page)) .then((response) => response.json()) + .catch((error) => { + console.error(error); + LOADING[query] = false; + this.setState({ + isLoadingTail: false, + }); + }) .then((responseData) => { var moviesForQuery = resultsCache.dataForQuery[query].slice(); @@ -183,16 +192,10 @@ var SearchScreen = React.createClass({ dataSource: this.getDataSource(resultsCache.dataForQuery[query]), }); }) - .catch((error) => { - console.error(error); - LOADING[query] = false; - this.setState({ - isLoadingTail: false, - }); - }); + .done(); }, - getDataSource: function(movies: Array): ListViewDataSource { + getDataSource: function(movies: Array): ListView.DataSource { return this.state.dataSource.cloneWithRows(movies); }, @@ -240,7 +243,7 @@ var SearchScreen = React.createClass({ renderRow={this.renderRow} onEndReached={this.onEndReached} automaticallyAdjustContentInsets={false} - keyboardDismissMode={ScrollView.keyboardDismissMode.OnDrag} + keyboardDismissMode="onDrag" keyboardShouldPersistTaps={true} showsVerticalScrollIndicator={false} />; @@ -283,7 +286,7 @@ var SearchBar = React.createClass({ return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/SampleApp/SampleApp/AppDelegate.h b/Examples/SampleApp/SampleApp/AppDelegate.h new file mode 100644 index 000000000..062fb99c0 --- /dev/null +++ b/Examples/SampleApp/SampleApp/AppDelegate.h @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@interface AppDelegate : UIResponder + +@property (nonatomic, strong) UIWindow *window; + +@end diff --git a/Examples/SampleApp/SampleApp/AppDelegate.m b/Examples/SampleApp/SampleApp/AppDelegate.m new file mode 100644 index 000000000..3fbc77902 --- /dev/null +++ b/Examples/SampleApp/SampleApp/AppDelegate.m @@ -0,0 +1,44 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "AppDelegate.h" + +#import "RCTRootView.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + NSURL *jsCodeLocation; + RCTRootView *rootView = [[RCTRootView alloc] init]; + + // Loading JavaScript code - uncomment the one you want. + + // OPTION 1 + // Load from development server. Start the server from the repository root: + // + // $ npm start + // + // To run on device, change `localhost` to the IP address of your computer, and make sure your computer and + // iOS device are on the same Wi-Fi network. + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/SampleApp/index.ios.runModule.bundle"]; + + // OPTION 2 + // Load from pre-bundled file on disk. To re-generate the static bundle, run + // + // $ curl http://localhost:8081/Examples/SampleApp/index.ios.runModule.bundle -o main.jsbundle + // + // and uncomment the next following line + // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + + rootView.scriptURL = jsCodeLocation; + rootView.moduleName = @"SampleApp"; + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [[UIViewController alloc] init]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/Examples/SampleApp/SampleApp/Base.lproj/LaunchScreen.xib b/Examples/SampleApp/SampleApp/Base.lproj/LaunchScreen.xib new file mode 100644 index 000000000..9821f97e4 --- /dev/null +++ b/Examples/SampleApp/SampleApp/Base.lproj/LaunchScreen.xib @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/SampleApp/SampleApp/Images.xcassets/AppIcon.appiconset/Contents.json b/Examples/SampleApp/SampleApp/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..118c98f74 --- /dev/null +++ b/Examples/SampleApp/SampleApp/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Examples/SampleApp/SampleApp/Info.plist b/Examples/SampleApp/SampleApp/Info.plist new file mode 100644 index 000000000..15c0472c4 --- /dev/null +++ b/Examples/SampleApp/SampleApp/Info.plist @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.react.native.example.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Examples/SampleApp/SampleApp/main.m b/Examples/SampleApp/SampleApp/main.m new file mode 100644 index 000000000..3c8987ce3 --- /dev/null +++ b/Examples/SampleApp/SampleApp/main.m @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/Examples/SampleApp/index.ios.js b/Examples/SampleApp/index.ios.js new file mode 100644 index 000000000..97ff012e7 --- /dev/null +++ b/Examples/SampleApp/index.ios.js @@ -0,0 +1,49 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + StyleSheet, + Text, + View, +} = React; + +var SampleApp = React.createClass({ + render: function() { + return ( + + + Welcome to React Native! + + + To get started, edit index.ios.js{'\n'} + Press Cmd+R to reload + + + ); + } +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#F5FCFF', + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 10, + }, + instructions: { + textAlign: 'center', + color: '#333333', + }, +}); + +AppRegistry.registerComponent('SampleApp', () => SampleApp); diff --git a/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj b/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj index 646876092..48b484437 100644 --- a/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj +++ b/Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj @@ -7,32 +7,50 @@ objects = { /* Begin PBXBuildFile section */ + 1341803E1AA91802003F314A /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1341803D1AA917ED003F314A /* libRCTImage.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832348291A77B50100B55238 /* libReactKit.a */; }; + 58C572501AA6229900CDF9C8 /* libReactKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C572471AA6224300CDF9C8 /* libReactKit.a */; }; + 58C572511AA6229D00CDF9C8 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58C5724D1AA6224400CDF9C8 /* libRCTText.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 832348281A77B50100B55238 /* PBXContainerItemProxy */ = { + 1341803C1AA917ED003F314A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + containerPortal = 134180381AA917ED003F314A /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5115D1A9E6B3D00147676; + remoteInfo = RCTImage; + }; + 58C572461AA6224300CDF9C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 587650E31A9EB0DF008B8F17 /* ReactKit.xcodeproj */; proxyType = 2; remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = ReactKit; }; + 58C5724C1AA6224400CDF9C8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 587650DA1A9EB0DB008B8F17 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 134180381AA917ED003F314A /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../../Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* TicTacToe.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TicTacToe.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = TicTacToe/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = TicTacToe/AppDelegate.m; sourceTree = ""; }; 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; - 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 834D32361A76971A00F38302 /* ReactKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactKit.xcodeproj; path = ../../ReactKit/ReactKit.xcodeproj; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = TicTacToe/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = TicTacToe/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = TicTacToe/main.m; sourceTree = ""; }; + 587650DA1A9EB0DB008B8F17 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; + 587650E31A9EB0DF008B8F17 /* ReactKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactKit.xcodeproj; path = ../../ReactKit/ReactKit.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -40,13 +58,23 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */, + 1341803E1AA91802003F314A /* libRCTImage.a in Frameworks */, + 58C572501AA6229900CDF9C8 /* libReactKit.a in Frameworks */, + 58C572511AA6229D00CDF9C8 /* libRCTText.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 134180391AA917ED003F314A /* Products */ = { + isa = PBXGroup; + children = ( + 1341803D1AA917ED003F314A /* libRCTImage.a */, + ); + name = Products; + sourceTree = ""; + }; 13B07FAE1A68108700A75B9A /* TicTacToe */ = { isa = PBXGroup; children = ( @@ -60,10 +88,28 @@ name = TicTacToe; sourceTree = ""; }; - 832348241A77B50100B55238 /* Products */ = { + 58C572071AA6126D00CDF9C8 /* Libraries */ = { isa = PBXGroup; children = ( - 832348291A77B50100B55238 /* libReactKit.a */, + 134180381AA917ED003F314A /* RCTImage.xcodeproj */, + 587650DA1A9EB0DB008B8F17 /* RCTText.xcodeproj */, + 587650E31A9EB0DF008B8F17 /* ReactKit.xcodeproj */, + ); + name = Libraries; + sourceTree = ""; + }; + 58C572411AA6224300CDF9C8 /* Products */ = { + isa = PBXGroup; + children = ( + 58C572471AA6224300CDF9C8 /* libReactKit.a */, + ); + name = Products; + sourceTree = ""; + }; + 58C572481AA6224300CDF9C8 /* Products */ = { + isa = PBXGroup; + children = ( + 58C5724D1AA6224400CDF9C8 /* libRCTText.a */, ); name = Products; sourceTree = ""; @@ -71,8 +117,8 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( - 834D32361A76971A00F38302 /* ReactKit.xcodeproj */, 13B07FAE1A68108700A75B9A /* TicTacToe */, + 58C572071AA6126D00CDF9C8 /* Libraries */, 83CBBA001A601CBA00E9B192 /* Products */, ); sourceTree = ""; @@ -127,8 +173,16 @@ projectDirPath = ""; projectReferences = ( { - ProductGroup = 832348241A77B50100B55238 /* Products */; - ProjectRef = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + ProductGroup = 134180391AA917ED003F314A /* Products */; + ProjectRef = 134180381AA917ED003F314A /* RCTImage.xcodeproj */; + }, + { + ProductGroup = 58C572481AA6224300CDF9C8 /* Products */; + ProjectRef = 587650DA1A9EB0DB008B8F17 /* RCTText.xcodeproj */; + }, + { + ProductGroup = 58C572411AA6224300CDF9C8 /* Products */; + ProjectRef = 587650E31A9EB0DF008B8F17 /* ReactKit.xcodeproj */; }, ); projectRoot = ""; @@ -139,11 +193,25 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - 832348291A77B50100B55238 /* libReactKit.a */ = { + 1341803D1AA917ED003F314A /* libRCTImage.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTImage.a; + remoteRef = 1341803C1AA917ED003F314A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 58C572471AA6224300CDF9C8 /* libReactKit.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libReactKit.a; - remoteRef = 832348281A77B50100B55238 /* PBXContainerItemProxy */; + remoteRef = 58C572461AA6224300CDF9C8 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 58C5724D1AA6224400CDF9C8 /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 58C5724C1AA6224400CDF9C8 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ @@ -179,6 +247,7 @@ 13B07FB21A68108700A75B9A /* Base */, ); name = LaunchScreen.xib; + path = TicTacToe; sourceTree = ""; }; /* End PBXVariantGroup section */ @@ -188,8 +257,9 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + INFOPLIST_FILE = "$(SRCROOT)/TicTacToe/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = TicTacToe; }; @@ -199,8 +269,9 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + INFOPLIST_FILE = "$(SRCROOT)/TicTacToe/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = TicTacToe; }; diff --git a/Examples/TicTacToe/TicTacToe/AppDelegate.h b/Examples/TicTacToe/TicTacToe/AppDelegate.h new file mode 100644 index 000000000..062fb99c0 --- /dev/null +++ b/Examples/TicTacToe/TicTacToe/AppDelegate.h @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@interface AppDelegate : UIResponder + +@property (nonatomic, strong) UIWindow *window; + +@end diff --git a/Examples/TicTacToe/AppDelegate.m b/Examples/TicTacToe/TicTacToe/AppDelegate.m similarity index 100% rename from Examples/TicTacToe/AppDelegate.m rename to Examples/TicTacToe/TicTacToe/AppDelegate.m diff --git a/Examples/TicTacToe/Base.lproj/LaunchScreen.xib b/Examples/TicTacToe/TicTacToe/Base.lproj/LaunchScreen.xib similarity index 100% rename from Examples/TicTacToe/Base.lproj/LaunchScreen.xib rename to Examples/TicTacToe/TicTacToe/Base.lproj/LaunchScreen.xib diff --git a/Examples/TicTacToe/TicTacToe/Images.xcassets/AppIcon.appiconset/Contents.json b/Examples/TicTacToe/TicTacToe/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..118c98f74 --- /dev/null +++ b/Examples/TicTacToe/TicTacToe/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Examples/TicTacToe/Info.plist b/Examples/TicTacToe/TicTacToe/Info.plist similarity index 100% rename from Examples/TicTacToe/Info.plist rename to Examples/TicTacToe/TicTacToe/Info.plist diff --git a/Examples/TicTacToe/main.m b/Examples/TicTacToe/TicTacToe/main.m similarity index 100% rename from Examples/TicTacToe/main.m rename to Examples/TicTacToe/TicTacToe/main.m diff --git a/Examples/UIExplorer/ActionSheetIOSExample.js b/Examples/UIExplorer/ActionSheetIOSExample.js new file mode 100644 index 000000000..1ee487ed5 --- /dev/null +++ b/Examples/UIExplorer/ActionSheetIOSExample.js @@ -0,0 +1,113 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + Text, + View, +} = React; +var ActionSheetIOS = require('ActionSheetIOS'); +var BUTTONS = [ + 'Button Index: 0', + 'Button Index: 1', + 'Button Index: 2', + 'Destruct', + 'Cancel', +]; +var DESTRUCTIVE_INDEX = 3; +var CANCEL_INDEX = 4; + +var ActionSheetExample = React.createClass({ + getInitialState() { + return { + clicked: 'none', + }; + }, + + render() { + return ( + + + Click to show the ActionSheet + + + Clicked button at index: "{this.state.clicked}" + + + ); + }, + + showActionSheet() { + ActionSheetIOS.showActionSheetWithOptions({ + options: BUTTONS, + cancelButtonIndex: CANCEL_INDEX, + destructiveButtonIndex: DESTRUCTIVE_INDEX, + }, + (buttonIndex) => { + this.setState({ clicked: BUTTONS[buttonIndex] }); + }); + } +}); + +var ShareActionSheetExample = React.createClass({ + getInitialState() { + return { + text: '' + }; + }, + + render() { + return ( + + + Click to show the Share ActionSheet + + + {this.state.text} + + + ); + }, + + showShareActionSheet() { + ActionSheetIOS.showShareActionSheetWithOptions({ + url: 'https://code.facebook.com', + }, + (error) => { + console.error(error); + }, + (success, method) => { + var text; + if (success) { + text = `Shared via ${method}`; + } else { + text = 'You didn\'t share'; + } + this.setState({text}) + }); + } +}); + +var style = StyleSheet.create({ + button: { + marginBottom: 10, + fontWeight: 'bold', + } +}); + +exports.title = 'ActionSheetIOS'; +exports.description = 'Interface to show iOS\' action sheets'; +exports.examples = [ + { + title: 'Show Action Sheet', + render(): ReactElement { return ; } + }, + { + title: 'Show Share Action Sheet', + render(): ReactElement { return ; } + } +]; diff --git a/Examples/UIExplorer/ActivityIndicatorExample.js b/Examples/UIExplorer/ActivityIndicatorExample.js index 4f068254d..60f69843c 100644 --- a/Examples/UIExplorer/ActivityIndicatorExample.js +++ b/Examples/UIExplorer/ActivityIndicatorExample.js @@ -1,7 +1,6 @@ /** * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule ActivityIndicatorExample + * @flow */ 'use strict'; @@ -41,7 +40,7 @@ var ToggleAnimatingActivityIndicator = React.createClass({ ); } @@ -98,7 +97,7 @@ exports.examples = [ ); } @@ -109,19 +108,19 @@ exports.examples = [ return ( @@ -130,7 +129,7 @@ exports.examples = [ }, { title: 'Start/stop', - render: function() { + render: function(): ReactElement { return ; } }, diff --git a/Examples/UIExplorer/AdSupportIOSExample.js b/Examples/UIExplorer/AdSupportIOSExample.js new file mode 100644 index 000000000..4047dd43a --- /dev/null +++ b/Examples/UIExplorer/AdSupportIOSExample.js @@ -0,0 +1,93 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * @flow + */ +'use strict'; + +var AdSupportIOS = require('AdSupportIOS'); + +var React = require('react-native'); +var { + StyleSheet, + Text, + View, +} = React; + +exports.framework = 'React'; +exports.title = 'Advertising ID'; +exports.description = 'Example of using the ad support API.'; + +exports.examples = [ + { + title: 'Ad Support IOS', + render: function(): ReactElement { + return ; + }, + } +]; + +var AdSupportIOSExample = React.createClass({ + getInitialState: function() { + return { + deviceID: 'No IDFA yet', + hasAdvertiserTracking: 'unset', + }; + }, + + componentDidMount: function() { + AdSupportIOS.getAdvertisingId( + this._onDeviceIDSuccess, + this._onDeviceIDFailure + ); + + AdSupportIOS.getAdvertisingTrackingEnabled( + this._onHasTrackingSuccess, + this._onHasTrackingFailure + ); + }, + + _onHasTrackingSuccess: function(hasTracking) { + this.setState({ + 'hasAdvertiserTracking': hasTracking, + }); + }, + + _onHasTrackingFailure: function(e) { + this.setState({ + 'hasAdvertiserTracking': 'Error!', + }); + }, + + _onDeviceIDSuccess: function(deviceID) { + this.setState({ + 'deviceID': deviceID, + }); + }, + + _onDeviceIDFailure: function(e) { + this.setState({ + 'deviceID': 'Error!', + }); + }, + + render: function() { + return ( + + + Advertising ID: + {JSON.stringify(this.state.deviceID)} + + + Has Advertiser Tracking: + {JSON.stringify(this.state.hasAdvertiserTracking)} + + + ); + } +}); + +var styles = StyleSheet.create({ + title: { + fontWeight: 'bold', + }, +}); diff --git a/Examples/UIExplorer/AlertIOSExample.js b/Examples/UIExplorer/AlertIOSExample.js new file mode 100644 index 000000000..6897007fc --- /dev/null +++ b/Examples/UIExplorer/AlertIOSExample.js @@ -0,0 +1,99 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + View, + Text, + TouchableHighlight, + AlertIOS, +} = React; + +exports.framework = 'React'; +exports.title = 'AlertIOS'; +exports.description = 'iOS alerts and action sheets'; +exports.examples = [{ + title: 'Alerts', + render() { + return ( + + AlertIOS.alert( + 'Foo Title', + 'My Alert Msg' + )}> + + Alert with message and default button + + + AlertIOS.alert( + null, + null, + [ + {text: 'Button', onPress: () => console.log('Button Pressed!')}, + ] + )}> + + Alert with only one button + + + AlertIOS.alert( + 'Foo Title', + 'My Alert Msg', + [ + {text: 'Foo', onPress: () => console.log('Foo Pressed!')}, + {text: 'Bar', onPress: () => console.log('Bar Pressed!')}, + ] + )}> + + Alert with two buttons + + + AlertIOS.alert( + 'Foo Title', + null, + [ + {text: 'Foo', onPress: () => console.log('Foo Pressed!')}, + {text: 'Bar', onPress: () => console.log('Bar Pressed!')}, + {text: 'Baz', onPress: () => console.log('Baz Pressed!')}, + ] + )}> + + Alert with 3 buttons + + + AlertIOS.alert( + 'Foo Title', + 'My Alert Msg', + '..............'.split('').map((dot, index) => ({ + text: 'Button ' + index, + onPress: () => console.log('Pressed ' + index) + })) + )}> + + Alert with too many buttons + + + + ); + }, +}]; + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 10, + }, +}); diff --git a/Examples/UIExplorer/AppStateExample.js b/Examples/UIExplorer/AppStateExample.js new file mode 100644 index 000000000..9e93a57fc --- /dev/null +++ b/Examples/UIExplorer/AppStateExample.js @@ -0,0 +1,61 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + AppState, + StyleSheet, + Text, + TouchableHighlight, + View, +} = React; + +var Button = React.createClass({ + render: function() { + return ( + + + {this.props.label} + + + ); + } +}); + +var styles = StyleSheet.create({ + button: { + padding: 10, + alignItems: 'center', + justifyContent: 'center', + }, + buttonLabel: { + color: 'blue', + }, +}); + +exports.title = 'AppState'; +exports.description = 'App background status and badge value'; +exports.examples = [ +{ + title: 'Set Badge Number', + render: function() { + return ( + + + ); + }, + + _run: function() { + if (!this._start) { + var d = new Date(); + this._start = d.getTime(); + this._iters = 100; + this._ii = 0; + if (this.props.type === 'setTimeout') { + if (this.props.dt < 1) { + this._iters = 5000; + } else if (this.props.dt > 20) { + this._iters = 10; + } + this._timerFn = () => this.setTimeout(this._run, this.props.dt); + } else if (this.props.type === 'requestAnimationFrame') { + this._timerFn = () => this.requestAnimationFrame(this._run); + } else if (this.props.type === 'setImmediate') { + this._iters = 5000; + this._timerFn = () => this.setImmediate(this._run); + } else if (this.props.type === 'setInterval') { + this._iters = 30; // Only used for forceUpdate periodicidy + this._timerFn = null; + this._handle = this.setInterval(this._run, this.props.dt); + } + } + if (this._ii >= this._iters && !this._handle) { + var d = new Date(); + var e = (d.getTime() - this._start); + var msg = 'Finished ' + this._ii + ' ' + this.props.type + ' calls.\n' + + 'Elapsed time: ' + e + ' ms\n' + (e / this._ii) + ' ms / iter'; + console.log(msg); + AlertIOS.alert(msg); + this._start = null; + this.forceUpdate(() => { this._ii = 0; }); + return; + } + this._ii++; + // Only re-render occasionally so we don't slow down timers. + if (this._ii % (this._iters / 5) === 0) { + this.forceUpdate(); + } + this._timerFn && this._timerFn(); + }, + + clear: function() { + this.clearInterval(this._handle); // invalid handles are ignored + if (this._handle) { + // Configure things so we can do a final run to update UI and reset state. + this._handle = null; + this._iters = this._ii; + this._run(); + } + }, +}); + +var styles = StyleSheet.create({ + button: { + borderColor: 'gray', + borderRadius: 8, + borderWidth: 1, + padding: 10, + margin: 5, + alignItems: 'center', + justifyContent: 'center', + }, +}); + +exports.framework = 'React'; +exports.title = 'Timers, TimerMixin'; +exports.description = 'The TimerMixin provides timer functions for executing ' + + 'code in the future that are safely cleaned up when the component unmounts.'; + +exports.examples = [ + { + title: 'this.setTimeout(fn, t)', + description: 'Execute function fn t milliseconds in the future. If ' + + 't === 0, it will be enqueued immediately in the next event loop. ' + + 'Larger values will fire on the closest frame.', + render: function() { + return ( + + + + + + ); + }, + }, + { + title: 'this.requestAnimationFrame(fn)', + description: 'Execute function fn on the next frame.', + render: function() { + return ( + + + + ); + }, + }, + { + title: 'this.setImmediate(fn)', + description: 'Execute function fn at the end of the current JS event loop.', + render: function() { + return ( + + + + ); + }, + }, + { + title: 'this.setInterval(fn, t)', + description: 'Execute function fn every t milliseconds until cancelled ' + + 'or component is unmounted.', + render: function() { + var IntervalExample = React.createClass({ + getInitialState: function() { + return { + showTimer: true, + }; + }, + + render: function() { + if (this.state.showTimer) { + var timer = + ; + var toggleText = 'Unmount timer'; + } else { + var timer = null; + var toggleText = 'Mount new timer'; + } + return ( + + {timer} + + + + ); + }, + + _toggleTimer: function() { + this.setState({showTimer: !this.state.showTimer}); + }, + }); + return ; + }, + }, +]; diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index 7aa2be01c..4226ec18a 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -1,7 +1,5 @@ /** * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule TouchableExample */ 'use strict'; @@ -12,6 +10,7 @@ var { StyleSheet, Text, TouchableHighlight, + TouchableOpacity, View, } = React; @@ -57,6 +56,13 @@ exports.examples = [ render: function() { return ; }, +}, { + title: 'Touchable feedback events', + description: ' components accept onPress, onPressIn, ' + + 'onPressOut, and onLongPress as props.', + render: function() { + return ; + }, }]; var TextOnPressBox = React.createClass({ @@ -95,11 +101,46 @@ var TextOnPressBox = React.createClass({ } }); +var TouchableFeedbackEvents = React.createClass({ + getInitialState: function() { + return { + eventLog: [], + }; + }, + render: function() { + return ( + + + this._appendEvent('press')} + onPressIn={() => this._appendEvent('pressIn')} + onPressOut={() => this._appendEvent('pressOut')} + onLongPress={() => this._appendEvent('longPress')}> + + Press Me + + + + + {this.state.eventLog.map((e, ii) => {e})} + + + ); + }, + _appendEvent: function(eventName) { + var limit = 6; + var eventLog = this.state.eventLog.slice(0, limit - 1); + eventLog.unshift(eventName); + this.setState({eventLog}); + }, +}); + var heartImage = {uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small'}; var styles = StyleSheet.create({ row: { - alignItems: 'center', + justifyContent: 'center', flexDirection: 'row', }, icon: { @@ -113,6 +154,9 @@ var styles = StyleSheet.create({ text: { fontSize: 16, }, + button: { + color: '#007AFF', + }, wrapper: { borderRadius: 8, }, @@ -127,6 +171,14 @@ var styles = StyleSheet.create({ borderColor: '#f0f0f0', backgroundColor: '#f9f9f9', }, + eventLogBox: { + padding: 10, + margin: 10, + height: 120, + borderWidth: 1 / PixelRatio.get(), + borderColor: '#f0f0f0', + backgroundColor: '#f9f9f9', + }, textBlock: { fontWeight: 'bold', color: 'blue', diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 73048cad9..59adccff3 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -7,46 +7,237 @@ objects = { /* Begin PBXBuildFile section */ + 004D28A31AAF61C70097A701 /* UIExplorerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 004D28A21AAF61C70097A701 /* UIExplorerTests.m */; }; + 00D2771A1AB8C3E100DC1E48 /* libRCTWebSocketDebugger.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00D277131AB8C2C700DC1E48 /* libRCTWebSocketDebugger.a */; }; + 00D2771C1AB8C55500DC1E48 /* libicucore.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 00D2771B1AB8C55500DC1E48 /* libicucore.dylib */; }; + 13417FE91AA91432003F314A /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FE81AA91428003F314A /* libRCTImage.a */; }; + 134180011AA9153C003F314A /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FEF1AA914B8003F314A /* libRCTText.a */; }; + 134180021AA9153C003F314A /* libReactKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FFF1AA91531003F314A /* libReactKit.a */; }; + 1341802C1AA9178B003F314A /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1341802B1AA91779003F314A /* libRCTNetwork.a */; }; + 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1344545A1AAFCAAE003F0779 /* libRCTAdSupport.a */; }; + 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 134A8A251AACED6A00945AAE /* libRCTGeolocation.a */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832348291A77B50100B55238 /* libReactKit.a */; }; + 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; + D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 832348281A77B50100B55238 /* PBXContainerItemProxy */ = { + 004D28A41AAF61C70097A701 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; - containerPortal = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = UIExplorer; + }; + 00D277121AB8C2C700DC1E48 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; + remoteInfo = RCTWebSocketDebugger; + }; + 13417FE71AA91428003F314A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13417FE31AA91428003F314A /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5115D1A9E6B3D00147676; + remoteInfo = RCTImage; + }; + 13417FEE1AA914B8003F314A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; + 13417FFE1AA91531003F314A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */; proxyType = 2; remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; remoteInfo = ReactKit; }; + 1341802A1AA91779003F314A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 134180261AA91779003F314A /* RCTNetwork.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B511DB1A9E6C8500147676; + remoteInfo = RCTNetwork; + }; + 134454591AAFCAAE003F0779 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; + remoteInfo = RCTAdSupport; + }; + 134A8A241AACED6A00945AAE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTGeolocation; + }; + 147CED4A1AB34F8C00DA3E4C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTActionSheet; + }; + D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; + remoteInfo = RCTVibration; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 004D289E1AAF61C70097A701 /* UIExplorerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 004D28A11AAF61C70097A701 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 004D28A21AAF61C70097A701 /* UIExplorerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIExplorerTests.m; sourceTree = ""; }; + 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocketDebugger.xcodeproj; path = ../../Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj; sourceTree = ""; }; + 00D2771B1AB8C55500DC1E48 /* libicucore.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libicucore.dylib; path = usr/lib/libicucore.dylib; sourceTree = SDKROOT; }; + 13417FE31AA91428003F314A /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../../Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; }; + 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; + 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactKit.xcodeproj; path = ../../ReactKit/ReactKit.xcodeproj; sourceTree = ""; }; + 134180261AA91779003F314A /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; + 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAdSupport.xcodeproj; path = ../../Libraries/AdSupport/RCTAdSupport.xcodeproj; sourceTree = ""; }; + 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../../Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* UIExplorer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIExplorer.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = UIExplorer/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = UIExplorer/AppDelegate.m; sourceTree = ""; }; 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; - 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 834D32361A76971A00F38302 /* ReactKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactKit.xcodeproj; path = ../../ReactKit/ReactKit.xcodeproj; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = UIExplorer/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = UIExplorer/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = UIExplorer/main.m; sourceTree = ""; }; + 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; + D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 004D289B1AAF61C70097A701 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8323482C1A77B59500B55238 /* libReactKit.a in Frameworks */, + 00D2771C1AB8C55500DC1E48 /* libicucore.dylib in Frameworks */, + 00D2771A1AB8C3E100DC1E48 /* libRCTWebSocketDebugger.a in Frameworks */, + D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */, + 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */, + 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */, + 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */, + 1341802C1AA9178B003F314A /* libRCTNetwork.a in Frameworks */, + 134180011AA9153C003F314A /* libRCTText.a in Frameworks */, + 134180021AA9153C003F314A /* libReactKit.a in Frameworks */, + 13417FE91AA91432003F314A /* libRCTImage.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 004D289F1AAF61C70097A701 /* UIExplorerTests */ = { + isa = PBXGroup; + children = ( + 004D28A21AAF61C70097A701 /* UIExplorerTests.m */, + 004D28A01AAF61C70097A701 /* Supporting Files */, + ); + path = UIExplorerTests; + sourceTree = ""; + }; + 004D28A01AAF61C70097A701 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 004D28A11AAF61C70097A701 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 00D2770F1AB8C2C700DC1E48 /* Products */ = { + isa = PBXGroup; + children = ( + 00D277131AB8C2C700DC1E48 /* libRCTWebSocketDebugger.a */, + ); + name = Products; + sourceTree = ""; + }; + 1316A21D1AA397F400C0188E /* Libraries */ = { + isa = PBXGroup; + children = ( + D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */, + 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */, + 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */, + 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */, + 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */, + 13417FE31AA91428003F314A /* RCTImage.xcodeproj */, + 134180261AA91779003F314A /* RCTNetwork.xcodeproj */, + 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */, + 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */, + 00D2771B1AB8C55500DC1E48 /* libicucore.dylib */, + ); + name = Libraries; + sourceTree = ""; + }; + 13417FE41AA91428003F314A /* Products */ = { + isa = PBXGroup; + children = ( + 13417FE81AA91428003F314A /* libRCTImage.a */, + ); + name = Products; + sourceTree = ""; + }; + 13417FEB1AA914B8003F314A /* Products */ = { + isa = PBXGroup; + children = ( + 13417FEF1AA914B8003F314A /* libRCTText.a */, + ); + name = Products; + sourceTree = ""; + }; + 13417FFB1AA91531003F314A /* Products */ = { + isa = PBXGroup; + children = ( + 13417FFF1AA91531003F314A /* libReactKit.a */, + ); + name = Products; + sourceTree = ""; + }; + 134180271AA91779003F314A /* Products */ = { + isa = PBXGroup; + children = ( + 1341802B1AA91779003F314A /* libRCTNetwork.a */, + ); + name = Products; + sourceTree = ""; + }; + 134454561AAFCAAE003F0779 /* Products */ = { + isa = PBXGroup; + children = ( + 1344545A1AAFCAAE003F0779 /* libRCTAdSupport.a */, + ); + name = Products; + sourceTree = ""; + }; + 134A8A211AACED6A00945AAE /* Products */ = { + isa = PBXGroup; + children = ( + 134A8A251AACED6A00945AAE /* libRCTGeolocation.a */, + ); + name = Products; + sourceTree = ""; + }; 13B07FAE1A68108700A75B9A /* UIExplorer */ = { isa = PBXGroup; children = ( @@ -60,10 +251,10 @@ name = UIExplorer; sourceTree = ""; }; - 832348241A77B50100B55238 /* Products */ = { + 147CED471AB34F8C00DA3E4C /* Products */ = { isa = PBXGroup; children = ( - 832348291A77B50100B55238 /* libReactKit.a */, + 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */, ); name = Products; sourceTree = ""; @@ -71,8 +262,9 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( - 834D32361A76971A00F38302 /* ReactKit.xcodeproj */, 13B07FAE1A68108700A75B9A /* UIExplorer */, + 1316A21D1AA397F400C0188E /* Libraries */, + 004D289F1AAF61C70097A701 /* UIExplorerTests */, 83CBBA001A601CBA00E9B192 /* Products */, ); sourceTree = ""; @@ -81,6 +273,15 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* UIExplorer.app */, + 004D289E1AAF61C70097A701 /* UIExplorerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + D85B82921AB6D5CE003F4FE2 /* Products */ = { + isa = PBXGroup; + children = ( + D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */, ); name = Products; sourceTree = ""; @@ -88,6 +289,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 004D289D1AAF61C70097A701 /* UIExplorerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 004D28AD1AAF61C70097A701 /* Build configuration list for PBXNativeTarget "UIExplorerTests" */; + buildPhases = ( + 004D289A1AAF61C70097A701 /* Sources */, + 004D289B1AAF61C70097A701 /* Frameworks */, + 004D289C1AAF61C70097A701 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 004D28A51AAF61C70097A701 /* PBXTargetDependency */, + ); + name = UIExplorerTests; + productName = UIExplorerTests; + productReference = 004D289E1AAF61C70097A701 /* UIExplorerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 13B07F861A680F5B00A75B9A /* UIExplorer */ = { isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "UIExplorer" */; @@ -113,6 +332,12 @@ attributes = { LastUpgradeCheck = 0610; ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 004D289D1AAF61C70097A701 = { + CreatedOnToolsVersion = 6.1.1; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + }; }; buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "UIExplorer" */; compatibilityVersion = "Xcode 3.2"; @@ -127,28 +352,124 @@ projectDirPath = ""; projectReferences = ( { - ProductGroup = 832348241A77B50100B55238 /* Products */; - ProjectRef = 834D32361A76971A00F38302 /* ReactKit.xcodeproj */; + ProductGroup = 147CED471AB34F8C00DA3E4C /* Products */; + ProjectRef = 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */; + }, + { + ProductGroup = 134454561AAFCAAE003F0779 /* Products */; + ProjectRef = 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */; + }, + { + ProductGroup = 134A8A211AACED6A00945AAE /* Products */; + ProjectRef = 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */; + }, + { + ProductGroup = 13417FE41AA91428003F314A /* Products */; + ProjectRef = 13417FE31AA91428003F314A /* RCTImage.xcodeproj */; + }, + { + ProductGroup = 134180271AA91779003F314A /* Products */; + ProjectRef = 134180261AA91779003F314A /* RCTNetwork.xcodeproj */; + }, + { + ProductGroup = 13417FEB1AA914B8003F314A /* Products */; + ProjectRef = 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */; + }, + { + ProductGroup = D85B82921AB6D5CE003F4FE2 /* Products */; + ProjectRef = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */; + }, + { + ProductGroup = 00D2770F1AB8C2C700DC1E48 /* Products */; + ProjectRef = 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */; + }, + { + ProductGroup = 13417FFB1AA91531003F314A /* Products */; + ProjectRef = 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */; }, ); projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* UIExplorer */, + 004D289D1AAF61C70097A701 /* UIExplorerTests */, ); }; /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - 832348291A77B50100B55238 /* libReactKit.a */ = { + 00D277131AB8C2C700DC1E48 /* libRCTWebSocketDebugger.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTWebSocketDebugger.a; + remoteRef = 00D277121AB8C2C700DC1E48 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 13417FE81AA91428003F314A /* libRCTImage.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTImage.a; + remoteRef = 13417FE71AA91428003F314A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 13417FEF1AA914B8003F314A /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 13417FEE1AA914B8003F314A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 13417FFF1AA91531003F314A /* libReactKit.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; path = libReactKit.a; - remoteRef = 832348281A77B50100B55238 /* PBXContainerItemProxy */; + remoteRef = 13417FFE1AA91531003F314A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1341802B1AA91779003F314A /* libRCTNetwork.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTNetwork.a; + remoteRef = 1341802A1AA91779003F314A /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1344545A1AAFCAAE003F0779 /* libRCTAdSupport.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTAdSupport.a; + remoteRef = 134454591AAFCAAE003F0779 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 134A8A251AACED6A00945AAE /* libRCTGeolocation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTGeolocation.a; + remoteRef = 134A8A241AACED6A00945AAE /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTActionSheet.a; + remoteRef = 147CED4A1AB34F8C00DA3E4C /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTVibration.a; + remoteRef = D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ + 004D289C1AAF61C70097A701 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F8E1A680F5B00A75B9A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -161,6 +482,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 004D289A1AAF61C70097A701 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 004D28A31AAF61C70097A701 /* UIExplorerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F871A680F5B00A75B9A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -172,6 +501,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 004D28A51AAF61C70097A701 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* UIExplorer */; + targetProxy = 004D28A41AAF61C70097A701 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { isa = PBXVariantGroup; @@ -179,17 +516,60 @@ 13B07FB21A68108700A75B9A /* Base */, ); name = LaunchScreen.xib; + path = UIExplorer; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 004D28A61AAF61C70097A701 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = UIExplorerTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UIExplorer.app/UIExplorer"; + }; + name = Debug; + }; + 004D28A71AAF61C70097A701 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = UIExplorerTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UIExplorer.app/UIExplorer"; + }; + name = Release; + }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/UIExplorer/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = UIExplorer; }; @@ -199,8 +579,14 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/UIExplorer/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = UIExplorer; }; @@ -295,6 +681,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 004D28AD1AAF61C70097A701 /* Build configuration list for PBXNativeTarget "UIExplorerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 004D28A61AAF61C70097A701 /* Debug */, + 004D28A71AAF61C70097A701 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "UIExplorer" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj.rej b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj.rej new file mode 100644 index 000000000..fa002c8b0 --- /dev/null +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj.rej @@ -0,0 +1,72 @@ +diff a/Libraries/FBReactKit/js/react-native-github/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Libraries/FBReactKit/js/react-native-github/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj (rejected hunks) +@@ -19,6 +19,7 @@ + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 147CED4B1AB34F8C00DA3E4C /* libRCTActionSheet.a */; }; ++ D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; }; + /* End PBXBuildFile section */ + + /* Begin PBXContainerItemProxy section */ +@@ -78,6 +79,13 @@ + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTActionSheet; + }; ++ D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */ = { ++ isa = PBXContainerItemProxy; ++ containerPortal = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */; ++ proxyType = 2; ++ remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; ++ remoteInfo = RCTVibration; ++ }; + /* End PBXContainerItemProxy section */ + + /* Begin PBXFileReference section */ +@@ -98,6 +106,7 @@ + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; ++ D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; + /* End PBXFileReference section */ + + /* Begin PBXFrameworksBuildPhase section */ +@@ -112,6 +121,7 @@ + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( ++ D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */, + 147CED4C1AB3532B00DA3E4C /* libRCTActionSheet.a in Frameworks */, + 134454601AAFCABD003F0779 /* libRCTAdSupport.a in Frameworks */, + 134A8A2A1AACED7A00945AAE /* libRCTGeolocation.a in Frameworks */, +@@ -145,6 +155,7 @@ + 1316A21D1AA397F400C0188E /* Libraries */ = { + isa = PBXGroup; + children = ( ++ D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */, + 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */, + 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */, + 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */, +@@ -334,6 +353,10 @@ + ProjectRef = 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */; + }, + { ++ ProductGroup = D85B82921AB6D5CE003F4FE2 /* Products */; ++ ProjectRef = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */; ++ }, ++ { + ProductGroup = 13417FFB1AA91531003F314A /* Products */; + ProjectRef = 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */; + }, +@@ -396,6 +419,13 @@ + remoteRef = 147CED4A1AB34F8C00DA3E4C /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; ++ D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */ = { ++ isa = PBXReferenceProxy; ++ fileType = archive.ar; ++ path = libRCTVibration.a; ++ remoteRef = D85B829B1AB6D5CE003F4FE2 /* PBXContainerItemProxy */; ++ sourceTree = BUILT_PRODUCTS_DIR; ++ }; + /* End PBXReferenceProxy section */ + + /* Begin PBXResourcesBuildPhase section */ diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index 403b11e6f..2189d2d0e 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -20,6 +20,20 @@ ReferencedContainer = "container:UIExplorer.xcodeproj"> + + + + + + + + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSLocationWhenInUseUsageDescription + You need to add NSLocationWhenInUseUsageDescription key in Info.plist to enable geolocation, otherwise it is going to *fail silently*! + UIViewControllerBasedStatusBarAppearance + + + diff --git a/Examples/UIExplorer/main.m b/Examples/UIExplorer/UIExplorer/main.m similarity index 100% rename from Examples/UIExplorer/main.m rename to Examples/UIExplorer/UIExplorer/main.m diff --git a/Examples/UIExplorer/UIExplorerApp.js b/Examples/UIExplorer/UIExplorerApp.js index 7c73d4ee3..c234d54b6 100644 --- a/Examples/UIExplorer/UIExplorerApp.js +++ b/Examples/UIExplorer/UIExplorerApp.js @@ -5,7 +5,7 @@ */ 'use strict'; -var React = require('react-native/addons'); +var React = require('react-native'); var UIExplorerList = require('./UIExplorerList'); var { @@ -16,6 +16,7 @@ var { var UIExplorerApp = React.createClass({ + render: function() { return ( + tintColor='#008888' + /> ); } }); diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index 56734c70f..5ca5254ec 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -3,51 +3,109 @@ */ 'use strict'; -var React = require('react-native/addons'); +var React = require('react-native'); var { + ListView, PixelRatio, ScrollView, StyleSheet, Text, + TextInput, TouchableHighlight, View, - invariant, } = React; var createExamplePage = require('./createExamplePage'); -var EXAMPLES = [ - require('./ViewExample'), - require('./LayoutExample'), +var COMPONENTS = [ + require('./ActivityIndicatorExample'), + require('./DatePickerExample'), + require('./ImageExample'), + require('./ListViewPagingExample'), + require('./ListViewSimpleExample'), + require('./MapViewExample'), + require('./NavigatorIOSExample'), + require('./PickerExample'), + require('./ScrollViewExample'), + require('./SliderIOSExample'), + require('./SwitchExample'), + require('./TabBarExample'), require('./TextExample.ios'), require('./TextInputExample'), - require('./ExpandingTextExample'), - require('./ImageExample'), - require('./ListViewSimpleExample'), - require('./ListViewPagingExample'), - require('./NavigatorIOSExample'), - require('./StatusBarIOSExample'), - require('./PointerEventsExample'), require('./TouchableExample'), - require('./ActivityIndicatorExample'), - require('./ScrollViewExample'), + require('./ViewExample'), + require('./WebViewExample'), ]; -var UIExplorerList = React.createClass({ - render: function() { - return ( - - - - {EXAMPLES.map(this._renderRow)} - - - - ); - }, +var APIS = [ + require('./ActionSheetIOSExample'), + require('./AdSupportIOSExample'), + require('./AlertIOSExample'), + require('./AppStateExample'), + require('./AppStateIOSExample'), + require('./AsyncStorageExample'), + require('./CameraRollExample.ios'), + require('./GeolocationExample'), + require('./LayoutExample'), + require('./NetInfoExample'), + require('./PointerEventsExample'), + require('./StatusBarIOSExample'), + require('./TimerExample'), + require('./VibrationIOSExample'), +]; - _renderRow: function(example, i) { - invariant(example.title, 'Example must provide a title.'); +var ds = new ListView.DataSource({ + rowHasChanged: (r1, r2) => r1 !== r2, + sectionHeaderHasChanged: (h1, h2) => h1 !== h2, +}); + +class UIExplorerList extends React.Component { + + constructor(props) { + super(props); + this.state = { + dataSource: ds.cloneWithRowsAndSections({ + components: COMPONENTS, + apis: APIS, + }), + }; + } + + render() { + return ( + + + + + + + ); + } + + _renderSectionHeader(data, section) { + return ( + + + {section.toUpperCase()} + + + ); + } + + _renderRow(example, i) { return ( this._onPressRow(example)}> @@ -63,9 +121,21 @@ var UIExplorerList = React.createClass({ ); - }, + } - _onPressRow: function(example) { + _search(text) { + var regex = new RegExp(text, 'i'); + var filter = (component) => regex.test(component.title); + + this.setState({ + dataSource: ds.cloneWithRowsAndSections({ + components: COMPONENTS.filter(filter), + apis: APIS.filter(filter), + }) + }); + } + + _onPressRow(example) { var Component = example.examples ? createExamplePage(null, example) : example; @@ -73,20 +143,25 @@ var UIExplorerList = React.createClass({ title: Component.title, component: Component, }); - }, -}); + } +} var styles = StyleSheet.create({ + listContainer: { + flex: 1, + }, list: { backgroundColor: '#eeeeee', }, + sectionHeader: { + padding: 5, + }, group: { backgroundColor: 'white', - marginVertical: 25, }, - line: { - backgroundColor: '#bbbbbb', - height: 1 / PixelRatio.get(), + sectionHeaderTitle: { + fontWeight: 'bold', + fontSize: 11, }, row: { backgroundColor: 'white', @@ -108,6 +183,21 @@ var styles = StyleSheet.create({ color: '#888888', lineHeight: 20, }, + searchRow: { + backgroundColor: '#eeeeee', + paddingTop: 75, + paddingLeft: 10, + paddingRight: 10, + paddingBottom: 10, + }, + searchTextInput: { + backgroundColor: 'white', + borderColor: '#cccccc', + borderRadius: 3, + borderWidth: 1, + height: 30, + paddingLeft: 8, + }, }); module.exports = UIExplorerList; diff --git a/Examples/UIExplorer/UIExplorerPage.js b/Examples/UIExplorer/UIExplorerPage.js index 2d72f62c7..5d619dcec 100644 --- a/Examples/UIExplorer/UIExplorerPage.js +++ b/Examples/UIExplorer/UIExplorerPage.js @@ -30,8 +30,7 @@ var UIExplorerPage = React.createClass({ } else { ContentWrapper = ScrollView; wrapperProps.keyboardShouldPeristTaps = true; - wrapperProps.keyboardDismissMode = - ScrollView.keyboardDismissMode.Interactive; + wrapperProps.keyboardDismissMode = 'interactive'; } var title = this.props.title ? : diff --git a/Examples/UIExplorer/UIExplorerTests/Info.plist b/Examples/UIExplorer/UIExplorerTests/Info.plist new file mode 100644 index 000000000..87e3a6175 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m new file mode 100644 index 000000000..854ee445a --- /dev/null +++ b/Examples/UIExplorer/UIExplorerTests/UIExplorerTests.m @@ -0,0 +1,58 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import +#import + +#import "RCTRedBox.h" + +#define TIMEOUT_SECONDS 240 + +@interface UIExplorerTests : XCTestCase + +@end + +@implementation UIExplorerTests + +- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test +{ + if (test(view)) { + return YES; + } + for (UIView *subview in [view subviews]) { + if ([self findSubviewInView:subview matching:test]) { + return YES; + } + } + return NO; +} + +- (void)testRootViewLoadsAndRenders { + UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; + + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; + BOOL foundElement = NO; + NSString *redboxError = nil; + + while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; + + redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; + + foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { + if ([view respondsToSelector:@selector(attributedText)]) { + NSString *text = [(id)view attributedText].string; + if ([text isEqualToString:@""]) { + return YES; + } + } + return NO; + }]; + } + + XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); + XCTAssertTrue(foundElement, @"Cound't find element with '' text in %d seconds", TIMEOUT_SECONDS); +} + + +@end diff --git a/Examples/UIExplorer/VibrationIOSExample.js b/Examples/UIExplorer/VibrationIOSExample.js new file mode 100644 index 000000000..bd768bcbb --- /dev/null +++ b/Examples/UIExplorer/VibrationIOSExample.js @@ -0,0 +1,42 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var React = require('react-native'); +var { + StyleSheet, + View, + Text, + TouchableHighlight, + VibrationIOS +} = React; + +exports.framework = 'React'; +exports.title = 'VibrationIOS'; +exports.description = 'Vibration API for iOS'; +exports.examples = [{ + title: 'VibrationIOS.vibrate()', + render() { + return ( + VibrationIOS.vibrate()}> + + Vibrate + + + ); + }, +}]; + +var styles = StyleSheet.create({ + wrapper: { + borderRadius: 5, + marginBottom: 5, + }, + button: { + backgroundColor: '#eeeeee', + padding: 10, + }, +}); diff --git a/Examples/UIExplorer/ViewExample.js b/Examples/UIExplorer/ViewExample.js index 660c191bb..eb2288a49 100644 --- a/Examples/UIExplorer/ViewExample.js +++ b/Examples/UIExplorer/ViewExample.js @@ -1,7 +1,5 @@ /** * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule ViewExample */ 'use strict'; diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js new file mode 100644 index 000000000..28246cdc3 --- /dev/null +++ b/Examples/UIExplorer/WebViewExample.js @@ -0,0 +1,264 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var React = require('react-native'); +var StyleSheet = require('StyleSheet'); +var { + ActivityIndicatorIOS, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, + WebView +} = React; + +var HEADER = '#3b5998'; +var BGWASH = 'rgba(255,255,255,0.8)'; +var DISABLED_WASH = 'rgba(255,255,255,0.25)'; + +var TEXT_INPUT_REF = 'urlInput'; +var WEBVIEW_REF = 'webview'; +var DEFAULT_URL = 'https://m.facebook.com'; + +var WebViewExample = React.createClass({ + + getInitialState: function() { + return { + url: DEFAULT_URL, + status: 'No Page Loaded', + backButtonEnabled: false, + forwardButtonEnabled: false, + loading: true, + }; + }, + + handleTextInputChange: function(event) { + this.inputText = event.nativeEvent.text; + }, + + render: function() { + this.inputText = this.state.url; + + return ( + + + + + + {'<'} + + + + + + + {'>'} + + + + + + + + Go! + + + + + + + {this.state.status} + + + ); + }, + + goBack: function() { + this.refs[WEBVIEW_REF].goBack(); + }, + + goForward: function() { + this.refs[WEBVIEW_REF].goForward(); + }, + + reload: function() { + this.refs[WEBVIEW_REF].reload(); + }, + + onNavigationStateChange: function(navState) { + this.setState({ + backButtonEnabled: navState.canGoBack, + forwardButtonEnabled: navState.canGoForward, + url: navState.url, + status: navState.title, + loading: navState.loading, + }); + }, + + renderError: function(errorDomain, errorCode, errorDesc) { + return ( + + + Error loading page + + + {'Domain: ' + errorDomain} + + + {'Error Code: ' + errorCode} + + + {'Description: ' + errorDesc} + + + ); + }, + + renderLoading: function() { + return ( + + + + ); + }, + + onSubmitEditing: function(event) { + this.pressGoButton(); + }, + + pressGoButton: function() { + var url = this.inputText.toLowerCase(); + if (url === this.state.url) { + this.reload(); + } else { + this.setState({ + url: url, + }); + } + // dismiss keyoard + this.refs[TEXT_INPUT_REF].blur(); + }, + +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: HEADER, + }, + addressBarRow: { + flexDirection: 'row', + padding: 8, + }, + webView: { + backgroundColor: BGWASH, + height: 350, + }, + addressBarTextInput: { + backgroundColor: BGWASH, + borderColor: 'transparent', + borderRadius: 3, + borderWidth: 1, + height: 24, + paddingLeft: 10, + paddingTop: 3, + paddingBottom: 3, + flex: 1, + fontSize: 14, + }, + navButton: { + width: 20, + padding: 3, + marginRight: 3, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: BGWASH, + borderColor: 'transparent', + borderRadius: 3, + }, + disabledButton: { + width: 20, + padding: 3, + marginRight: 3, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: DISABLED_WASH, + borderColor: 'transparent', + borderRadius: 3, + }, + goButton: { + height: 24, + padding: 3, + marginLeft: 8, + alignItems: 'center', + backgroundColor: BGWASH, + borderColor: 'transparent', + borderRadius: 3, + alignSelf: 'stretch', + }, + loadingView: { + backgroundColor: BGWASH, + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: BGWASH, + }, + errorTextTitle: { + fontSize: 15, + fontWeight: 'bold', + marginBottom: 10, + }, + errorText: { + fontSize: 14, + textAlign: 'center', + marginBottom: 2, + }, + statusBar: { + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 5, + height: 22, + }, + statusBarText: { + color: 'white', + fontSize: 13, + }, + spinner: { + width: 20, + marginRight: 6, + }, +}); + +exports.title = ''; +exports.description = 'Base component to display web content'; +exports.examples = [ + { + title: 'WebView', + render() { return ; } + } +]; diff --git a/IntegrationTests/AppDelegate.h b/IntegrationTests/AppDelegate.h new file mode 100644 index 000000000..f0ec66bdb --- /dev/null +++ b/IntegrationTests/AppDelegate.h @@ -0,0 +1,10 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@interface AppDelegate : UIResponder + +@property (nonatomic, strong) UIWindow *window; + +@end + diff --git a/IntegrationTests/AppDelegate.m b/IntegrationTests/AppDelegate.m new file mode 100644 index 000000000..db988faf8 --- /dev/null +++ b/IntegrationTests/AppDelegate.m @@ -0,0 +1,44 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "AppDelegate.h" + +#import "RCTRootView.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + NSURL *jsCodeLocation; + RCTRootView *rootView = [[RCTRootView alloc] init]; + + // Loading JavaScript code - uncomment the one you want. + + // OPTION 1 + // Load from development server. Start the server from the repository root: + // + // $ npm start + // + // To run on device, change `localhost` to the IP address of your computer, and make sure your computer and + // iOS device are on the same Wi-Fi network. + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/IntegrationTests/IntegrationTestsApp.includeRequire.runModule.bundle?dev=true"]; + + // OPTION 2 + // Load from pre-bundled file on disk. To re-generate the static bundle, run + // + // $ curl http://localhost:8081/IntegrationTests/IntegrationTestsApp.includeRequire.runModule.bundle -o main.jsbundle + // + // and uncomment the next following line + // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + + rootView.scriptURL = jsCodeLocation; + rootView.moduleName = @"IntegrationTestsApp"; + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [[UIViewController alloc] init]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/IntegrationTests/AsyncStorageTest.js b/IntegrationTests/AsyncStorageTest.js new file mode 100644 index 000000000..82fb348b2 --- /dev/null +++ b/IntegrationTests/AsyncStorageTest.js @@ -0,0 +1,148 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var RCTTestModule = require('NativeModules').TestModule; +var React = require('react-native'); +var { + AsyncStorage, + Text, + View, +} = React; + +var DEBUG = false; + +var KEY_1 = 'key_1'; +var VAL_1 = 'val_1'; +var KEY_2 = 'key_2'; +var VAL_2 = 'val_2'; + +// setup in componentDidMount +var done; +var updateMessage; + +function runTestCase(description, fn) { + updateMessage(description); + fn(); +} + +function expectTrue(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function expectEqual(lhs, rhs, testname) { + expectTrue( + lhs === rhs, + 'Error in test ' + testname + ': expected ' + rhs + ', got ' + lhs + ); +} + +function expectAsyncNoError(err) { + expectTrue(err === null, 'Unexpected Async error: ' + JSON.stringify(err)); +} + +function testSetAndGet() { + AsyncStorage.setItem(KEY_1, VAL_1, (err1) => { + expectAsyncNoError(err1); + AsyncStorage.getItem(KEY_1, (err2, result) => { + expectAsyncNoError(err2); + expectEqual(result, VAL_1, 'testSetAndGet setItem'); + updateMessage('get(key_1) correctly returned ' + result); + runTestCase('should get null for missing key', testMissingGet); + }); + }); +} + +function testMissingGet() { + AsyncStorage.getItem(KEY_2, (err, result) => { + expectAsyncNoError(err); + expectEqual(result, null, 'testMissingGet'); + updateMessage('missing get(key_2) correctly returned ' + result); + runTestCase('check set twice results in a single key', testSetTwice); + }); +} + +function testSetTwice() { + AsyncStorage.setItem(KEY_1, VAL_1, ()=>{ + AsyncStorage.setItem(KEY_1, VAL_1, ()=>{ + AsyncStorage.getItem(KEY_1, (err, result) => { + expectAsyncNoError(err); + expectEqual(result, VAL_1, 'testSetTwice'); + updateMessage('setTwice worked as expected'); + runTestCase('test removeItem', testRemoveItem); + }); + }); + }); +} + +function testRemoveItem() { + AsyncStorage.setItem(KEY_1, VAL_1, ()=>{ + AsyncStorage.setItem(KEY_2, VAL_2, ()=>{ + AsyncStorage.getAllKeys((err, result) => { + expectAsyncNoError(err); + expectTrue( + result.indexOf(KEY_1) >= 0 && result.indexOf(KEY_2) >= 0, + 'Missing KEY_1 or KEY_2 in ' + '(' + result + ')' + ); + updateMessage('testRemoveItem - add two items'); + AsyncStorage.removeItem(KEY_1, (err) => { + expectAsyncNoError(err); + updateMessage('delete successful '); + AsyncStorage.getItem(KEY_1, (err, result) => { + expectAsyncNoError(err); + expectEqual( + result, + null, + 'testRemoveItem: key_1 present after delete' + ); + updateMessage('key properly removed '); + AsyncStorage.getAllKeys((err, result2) => { + expectAsyncNoError(err); + expectTrue( + result2.indexOf(KEY_1) === -1, + 'Unexpected: KEY_1 present in ' + result2 + ); + updateMessage('proper length returned.\nDone!'); + done(); + }); + }); + }); + }); + }); + }); +} + +var AsyncStorageTest = React.createClass({ + getInitialState() { + return { + messages: 'Initializing...', + done: false, + }; + }, + + componentDidMount() { + done = () => this.setState({done: true}, RCTTestModule.markTestCompleted); + updateMessage = (msg) => { + this.setState({messages: this.state.messages.concat('\n' + msg)}); + DEBUG && console.log(msg); + }; + AsyncStorage.clear(testSetAndGet); + }, + + render() { + return ( + + + {this.constructor.displayName + ': '} + {this.state.done ? 'Done' : 'Testing...'} + {'\n\n' + this.state.messages} + + + ); + } +}); + +module.exports = AsyncStorageTest; diff --git a/IntegrationTests/Base.lproj/LaunchScreen.xib b/IntegrationTests/Base.lproj/LaunchScreen.xib new file mode 100644 index 000000000..52cc0828d --- /dev/null +++ b/IntegrationTests/Base.lproj/LaunchScreen.xib @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IntegrationTests/Images.xcassets/AppIcon.appiconset/Contents.json b/IntegrationTests/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..413d60e76 --- /dev/null +++ b/IntegrationTests/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,44 @@ +{ + "images" : [ + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "uie_icon@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "uie_icon@2x-1.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "uie_icon@2x-2.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "uie_icon@2x-3.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "uie_icon@2x-5.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "uie_icon@2x-4.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-1.png b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-1.png new file mode 100644 index 000000000..08a42699d Binary files /dev/null and b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-1.png differ diff --git a/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-2.png b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-2.png new file mode 100644 index 000000000..08a42699d Binary files /dev/null and b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-2.png differ diff --git a/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-3.png b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-3.png new file mode 100644 index 000000000..08a42699d Binary files /dev/null and b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-3.png differ diff --git a/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-4.png b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-4.png new file mode 100644 index 000000000..08a42699d Binary files /dev/null and b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-4.png differ diff --git a/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-5.png b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-5.png new file mode 100644 index 000000000..08a42699d Binary files /dev/null and b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x-5.png differ diff --git a/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x.png b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x.png new file mode 100644 index 000000000..08a42699d Binary files /dev/null and b/IntegrationTests/Images.xcassets/AppIcon.appiconset/uie_icon@2x.png differ diff --git a/IntegrationTests/Info.plist b/IntegrationTests/Info.plist new file mode 100644 index 000000000..245054621 --- /dev/null +++ b/IntegrationTests/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSLocationWhenInUseUsageDescription + You need to add NSLocationWhenInUseUsageDescription key in Info.plist to enable geolocation, otherwise it is going to *fail silently*! + UIViewControllerBasedStatusBarAppearance + + + diff --git a/IntegrationTests/IntegrationTestHarnessTest.js b/IntegrationTests/IntegrationTestHarnessTest.js new file mode 100644 index 000000000..5ae78cf9c --- /dev/null +++ b/IntegrationTests/IntegrationTestHarnessTest.js @@ -0,0 +1,57 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var RCTTestModule = require('NativeModules').TestModule; +var React = require('react-native'); +var { + Text, + View, +} = React; + +var IntegrationTestHarnessTest = React.createClass({ + propTypes: { + shouldThrow: React.PropTypes.bool, + waitOneFrame: React.PropTypes.bool, + }, + + getInitialState() { + return { + done: false, + }; + }, + + componentDidMount() { + if (this.props.waitOneFrame) { + requestAnimationFrame(this.runTest); + } else { + this.runTest(); + } + }, + + runTest() { + if (this.props.shouldThrow) { + throw new Error('Throwing error because shouldThrow'); + } + if (!RCTTestModule) { + throw new Error('RCTTestModule is not registered.'); + } else if (!RCTTestModule.markTestCompleted) { + throw new Error('RCTTestModule.markTestCompleted not defined.'); + } + this.setState({done: true}, RCTTestModule.markTestCompleted); + }, + + render() { + return ( + + + {this.constructor.displayName + ': '} + {this.state.done ? 'Done' : 'Testing...'} + + + ); + } +}); + +module.exports = IntegrationTestHarnessTest; diff --git a/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj new file mode 100644 index 000000000..c850360b1 --- /dev/null +++ b/IntegrationTests/IntegrationTests.xcodeproj/project.pbxproj @@ -0,0 +1,651 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 004D28A31AAF61C70097A701 /* IntegrationTestsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 004D28A21AAF61C70097A701 /* IntegrationTestsTests.m */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 580C37601AB0F6180015E709 /* libReactKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C37461AB0F5320015E709 /* libReactKit.a */; }; + 580C37611AB0F61E0015E709 /* libRCTAdSupport.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C374B1AB0F54A0015E709 /* libRCTAdSupport.a */; }; + 580C37621AB0F6260015E709 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C37501AB0F55C0015E709 /* libRCTGeolocation.a */; }; + 580C37631AB0F62C0015E709 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C37551AB0F56E0015E709 /* libRCTImage.a */; }; + 580C37641AB0F6350015E709 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C375A1AB0F5970015E709 /* libRCTNetwork.a */; }; + 580C37651AB0F63E0015E709 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C375F1AB0F5D10015E709 /* libRCTText.a */; }; + 580C37921AB1090B0015E709 /* libRCTTest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 580C378F1AB104B00015E709 /* libRCTTest.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 004D28A41AAF61C70097A701 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = IntegrationTests; + }; + 580C37451AB0F5320015E709 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; + remoteInfo = ReactKit; + }; + 580C374A1AB0F54A0015E709 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; + remoteInfo = RCTAdSupport; + }; + 580C374F1AB0F55C0015E709 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTGeolocation; + }; + 580C37541AB0F56E0015E709 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13417FE31AA91428003F314A /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5115D1A9E6B3D00147676; + remoteInfo = RCTImage; + }; + 580C37591AB0F5970015E709 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 134180261AA91779003F314A /* RCTNetwork.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B511DB1A9E6C8500147676; + remoteInfo = RCTNetwork; + }; + 580C375E1AB0F5D10015E709 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; + 580C378E1AB104B00015E709 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 580C37891AB104AF0015E709 /* RCTTest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 580C376F1AB104AF0015E709; + remoteInfo = RCTTest; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 004D289E1AAF61C70097A701 /* IntegrationTestsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTestsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 004D28A11AAF61C70097A701 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 004D28A21AAF61C70097A701 /* IntegrationTestsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IntegrationTestsTests.m; sourceTree = ""; }; + 13417FE31AA91428003F314A /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; }; + 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; + 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ReactKit.xcodeproj; path = ../ReactKit/ReactKit.xcodeproj; sourceTree = ""; }; + 134180261AA91779003F314A /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; + 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAdSupport.xcodeproj; path = ../Libraries/AdSupport/RCTAdSupport.xcodeproj; sourceTree = ""; }; + 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../Libraries/GeoLocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* IntegrationTests.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationTests.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 580C37891AB104AF0015E709 /* RCTTest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTTest.xcodeproj; path = ../Libraries/RCTTest/RCTTest.xcodeproj; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 004D289B1AAF61C70097A701 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 580C37601AB0F6180015E709 /* libReactKit.a in Frameworks */, + 580C37611AB0F61E0015E709 /* libRCTAdSupport.a in Frameworks */, + 580C37621AB0F6260015E709 /* libRCTGeolocation.a in Frameworks */, + 580C37631AB0F62C0015E709 /* libRCTImage.a in Frameworks */, + 580C37641AB0F6350015E709 /* libRCTNetwork.a in Frameworks */, + 580C37651AB0F63E0015E709 /* libRCTText.a in Frameworks */, + 580C37921AB1090B0015E709 /* libRCTTest.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 004D289F1AAF61C70097A701 /* IntegrationTestsTests */ = { + isa = PBXGroup; + children = ( + 004D28A21AAF61C70097A701 /* IntegrationTestsTests.m */, + 004D28A01AAF61C70097A701 /* Supporting Files */, + ); + path = IntegrationTestsTests; + sourceTree = ""; + }; + 004D28A01AAF61C70097A701 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 004D28A11AAF61C70097A701 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 1316A21D1AA397F400C0188E /* Libraries */ = { + isa = PBXGroup; + children = ( + 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */, + 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */, + 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */, + 13417FE31AA91428003F314A /* RCTImage.xcodeproj */, + 134180261AA91779003F314A /* RCTNetwork.xcodeproj */, + 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */, + 580C37891AB104AF0015E709 /* RCTTest.xcodeproj */, + ); + name = Libraries; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* IntegrationTests */ = { + isa = PBXGroup; + children = ( + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, + 13B07FB71A68108700A75B9A /* main.m */, + ); + name = IntegrationTests; + sourceTree = ""; + }; + 580C37421AB0F5320015E709 /* Products */ = { + isa = PBXGroup; + children = ( + 580C37461AB0F5320015E709 /* libReactKit.a */, + ); + name = Products; + sourceTree = ""; + }; + 580C37471AB0F54A0015E709 /* Products */ = { + isa = PBXGroup; + children = ( + 580C374B1AB0F54A0015E709 /* libRCTAdSupport.a */, + ); + name = Products; + sourceTree = ""; + }; + 580C374C1AB0F55C0015E709 /* Products */ = { + isa = PBXGroup; + children = ( + 580C37501AB0F55C0015E709 /* libRCTGeolocation.a */, + ); + name = Products; + sourceTree = ""; + }; + 580C37511AB0F56E0015E709 /* Products */ = { + isa = PBXGroup; + children = ( + 580C37551AB0F56E0015E709 /* libRCTImage.a */, + ); + name = Products; + sourceTree = ""; + }; + 580C37561AB0F5970015E709 /* Products */ = { + isa = PBXGroup; + children = ( + 580C375A1AB0F5970015E709 /* libRCTNetwork.a */, + ); + name = Products; + sourceTree = ""; + }; + 580C375B1AB0F5D10015E709 /* Products */ = { + isa = PBXGroup; + children = ( + 580C375F1AB0F5D10015E709 /* libRCTText.a */, + ); + name = Products; + sourceTree = ""; + }; + 580C378A1AB104AF0015E709 /* Products */ = { + isa = PBXGroup; + children = ( + 580C378F1AB104B00015E709 /* libRCTTest.a */, + ); + name = Products; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* IntegrationTests */, + 1316A21D1AA397F400C0188E /* Libraries */, + 004D289F1AAF61C70097A701 /* IntegrationTestsTests */, + 83CBBA001A601CBA00E9B192 /* Products */, + ); + sourceTree = ""; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* IntegrationTests.app */, + 004D289E1AAF61C70097A701 /* IntegrationTestsTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 004D289D1AAF61C70097A701 /* IntegrationTestsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 004D28AD1AAF61C70097A701 /* Build configuration list for PBXNativeTarget "IntegrationTestsTests" */; + buildPhases = ( + 004D289A1AAF61C70097A701 /* Sources */, + 004D289B1AAF61C70097A701 /* Frameworks */, + 004D289C1AAF61C70097A701 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 004D28A51AAF61C70097A701 /* PBXTargetDependency */, + ); + name = IntegrationTestsTests; + productName = IntegrationTestsTests; + productReference = 004D289E1AAF61C70097A701 /* IntegrationTestsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 13B07F861A680F5B00A75B9A /* IntegrationTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "IntegrationTests" */; + buildPhases = ( + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IntegrationTests; + productName = "Hello World"; + productReference = 13B07F961A680F5B00A75B9A /* IntegrationTests.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 004D289D1AAF61C70097A701 = { + CreatedOnToolsVersion = 6.1.1; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "IntegrationTests" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 580C37471AB0F54A0015E709 /* Products */; + ProjectRef = 134454551AAFCAAE003F0779 /* RCTAdSupport.xcodeproj */; + }, + { + ProductGroup = 580C374C1AB0F55C0015E709 /* Products */; + ProjectRef = 134A8A201AACED6A00945AAE /* RCTGeolocation.xcodeproj */; + }, + { + ProductGroup = 580C37511AB0F56E0015E709 /* Products */; + ProjectRef = 13417FE31AA91428003F314A /* RCTImage.xcodeproj */; + }, + { + ProductGroup = 580C37561AB0F5970015E709 /* Products */; + ProjectRef = 134180261AA91779003F314A /* RCTNetwork.xcodeproj */; + }, + { + ProductGroup = 580C378A1AB104AF0015E709 /* Products */; + ProjectRef = 580C37891AB104AF0015E709 /* RCTTest.xcodeproj */; + }, + { + ProductGroup = 580C375B1AB0F5D10015E709 /* Products */; + ProjectRef = 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */; + }, + { + ProductGroup = 580C37421AB0F5320015E709 /* Products */; + ProjectRef = 13417FFA1AA91531003F314A /* ReactKit.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* IntegrationTests */, + 004D289D1AAF61C70097A701 /* IntegrationTestsTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 580C37461AB0F5320015E709 /* libReactKit.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReactKit.a; + remoteRef = 580C37451AB0F5320015E709 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 580C374B1AB0F54A0015E709 /* libRCTAdSupport.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTAdSupport.a; + remoteRef = 580C374A1AB0F54A0015E709 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 580C37501AB0F55C0015E709 /* libRCTGeolocation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTGeolocation.a; + remoteRef = 580C374F1AB0F55C0015E709 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 580C37551AB0F56E0015E709 /* libRCTImage.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTImage.a; + remoteRef = 580C37541AB0F56E0015E709 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 580C375A1AB0F5970015E709 /* libRCTNetwork.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTNetwork.a; + remoteRef = 580C37591AB0F5970015E709 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 580C375F1AB0F5D10015E709 /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 580C375E1AB0F5D10015E709 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 580C378F1AB104B00015E709 /* libRCTTest.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTTest.a; + remoteRef = 580C378E1AB104B00015E709 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 004D289C1AAF61C70097A701 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 004D289A1AAF61C70097A701 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 004D28A31AAF61C70097A701 /* IntegrationTestsTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 004D28A51AAF61C70097A701 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* IntegrationTests */; + targetProxy = 004D28A41AAF61C70097A701 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 13B07FB21A68108700A75B9A /* Base */, + ); + name = LaunchScreen.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 004D28A61AAF61C70097A701 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)/Developer/Library/Frameworks", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = IntegrationTestsTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IntegrationTests.app/IntegrationTests"; + }; + name = Debug; + }; + 004D28A71AAF61C70097A701 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)/Developer/Library/Frameworks", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + INFOPLIST_FILE = IntegrationTestsTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IntegrationTests.app/IntegrationTests"; + }; + name = Release; + }; + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = IntegrationTests; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = IntegrationTests; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 004D28AD1AAF61C70097A701 /* Build configuration list for PBXNativeTarget "IntegrationTestsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 004D28A61AAF61C70097A701 /* Debug */, + 004D28A71AAF61C70097A701 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "IntegrationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "IntegrationTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/IntegrationTests/IntegrationTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/IntegrationTests/IntegrationTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..d698e9ecd --- /dev/null +++ b/IntegrationTests/IntegrationTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ReactKit/ReactKit.xcodeproj/xcshareddata/xcschemes/ReactKit.xcscheme b/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme similarity index 63% rename from ReactKit/ReactKit.xcodeproj/xcshareddata/xcschemes/ReactKit.xcscheme rename to IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme index 17a608c33..8c75e37ee 100644 --- a/ReactKit/ReactKit.xcodeproj/xcshareddata/xcschemes/ReactKit.xcscheme +++ b/IntegrationTests/IntegrationTests.xcodeproj/xcshareddata/xcschemes/IntegrationTests.xcscheme @@ -14,10 +14,10 @@ buildForAnalyzing = "YES"> + BlueprintIdentifier = "13B07F861A680F5B00A75B9A" + BuildableName = "IntegrationTests.app" + BlueprintName = "IntegrationTests" + ReferencedContainer = "container:IntegrationTests.xcodeproj"> + BlueprintIdentifier = "004D289D1AAF61C70097A701" + BuildableName = "IntegrationTestsTests.xctest" + BlueprintName = "IntegrationTestsTests" + ReferencedContainer = "container:IntegrationTests.xcodeproj"> @@ -46,20 +46,20 @@ skipped = "NO"> + BlueprintIdentifier = "004D289D1AAF61C70097A701" + BuildableName = "IntegrationTestsTests.xctest" + BlueprintName = "IntegrationTestsTests" + ReferencedContainer = "container:IntegrationTests.xcodeproj"> + BlueprintIdentifier = "13B07F861A680F5B00A75B9A" + BuildableName = "IntegrationTests.app" + BlueprintName = "IntegrationTests" + ReferencedContainer = "container:IntegrationTests.xcodeproj"> @@ -72,15 +72,15 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" allowLocationSimulation = "YES"> - + + BlueprintIdentifier = "13B07F861A680F5B00A75B9A" + BuildableName = "IntegrationTests.app" + BlueprintName = "IntegrationTests" + ReferencedContainer = "container:IntegrationTests.xcodeproj"> - + @@ -90,15 +90,15 @@ useCustomWorkingDirectory = "NO" buildConfiguration = "Release" debugDocumentVersioning = "YES"> - + + BlueprintIdentifier = "13B07F861A680F5B00A75B9A" + BuildableName = "IntegrationTests.app" + BlueprintName = "IntegrationTests" + ReferencedContainer = "container:IntegrationTests.xcodeproj"> - + diff --git a/IntegrationTests/IntegrationTestsApp.js b/IntegrationTests/IntegrationTestsApp.js new file mode 100644 index 000000000..30464bbcc --- /dev/null +++ b/IntegrationTests/IntegrationTestsApp.js @@ -0,0 +1,86 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule IntegrationTestsApp + */ +'use strict'; + +var React = require('react-native'); + +var { + AppRegistry, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} = React; + +var TESTS = [ + require('./IntegrationTestHarnessTest'), + require('./TimersTest'), + require('./AsyncStorageTest'), +]; + +TESTS.forEach( + (test) => AppRegistry.registerComponent(test.displayName, () => test) +); + +var IntegrationTestsApp = React.createClass({ + getInitialState: function() { + return { + test: null, + }; + }, + render: function() { + if (this.state.test) { + return ( + + + + ); + } + return ( + + + Click on a test to run it in this shell for easier debugging and + development. Run all tests in the testing envirnment with cmd+U in + Xcode. + + + + {TESTS.map((test) => [ + this.setState({test})}> + + + {test.displayName} + + + , + + ])} + + + ); + } +}); + +var styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + marginTop: 40, + margin: 15, + }, + row: { + padding: 10, + }, + testName: { + fontWeight: 'bold', + }, + separator: { + height: 1, + backgroundColor: '#bbbbbb', + }, +}); + +AppRegistry.registerComponent('IntegrationTestsApp', () => IntegrationTestsApp); diff --git a/IntegrationTests/IntegrationTestsTests/Info.plist b/IntegrationTests/IntegrationTestsTests/Info.plist new file mode 100644 index 000000000..87e3a6175 --- /dev/null +++ b/IntegrationTests/IntegrationTestsTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.facebook.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m new file mode 100644 index 000000000..4cb894480 --- /dev/null +++ b/IntegrationTests/IntegrationTestsTests/IntegrationTestsTests.m @@ -0,0 +1,50 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import +#import + +#import + +#import "RCTAssert.h" + +@interface IntegrationTestsTests : XCTestCase + +@end + +@implementation IntegrationTestsTests { + RCTTestRunner *_runner; +} + +- (void)setUp +{ + _runner = [[RCTTestRunner alloc] initWithApp:@"IntegrationTests/IntegrationTestsApp"]; +} + +- (void)testTheTester +{ + [_runner runTest:@"IntegrationTestHarnessTest"]; +} + +- (void)testTheTester_waitOneFrame +{ + [_runner runTest:@"IntegrationTestHarnessTest" initialProps:@{@"waitOneFrame": @YES} expectErrorBlock:nil]; +} + +- (void)testTheTester_ExpectError +{ + [_runner runTest:@"IntegrationTestHarnessTest" + initialProps:@{@"shouldThrow": @YES} + expectErrorRegex:[NSRegularExpression regularExpressionWithPattern:@"because shouldThrow" options:0 error:nil]]; +} + +- (void)testTimers +{ + [_runner runTest:@"TimersTest"]; +} + +- (void)testAsyncStorage +{ + [_runner runTest:@"AsyncStorageTest"]; +} + +@end diff --git a/IntegrationTests/TimersTest.js b/IntegrationTests/TimersTest.js new file mode 100644 index 000000000..db376304b --- /dev/null +++ b/IntegrationTests/TimersTest.js @@ -0,0 +1,150 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ +'use strict'; + +var RCTTestModule = require('NativeModules').TestModule; +var React = require('react-native'); +var { + StyleSheet, + Text, + TimerMixin, + View, +} = React; + +var TimersTest = React.createClass({ + mixins: [TimerMixin], + + getInitialState() { + return { + count: 0, + done: false, + }; + }, + + componentDidMount() { + this.testSetTimeout0(); + }, + + testSetTimeout0() { + this.setTimeout(this.testSetTimeout1, 0); + }, + + testSetTimeout1() { + this.setTimeout(this.testSetTimeout50, 1); + }, + + testSetTimeout50() { + this.setTimeout(this.testRequestAnimationFrame, 50); + }, + + testRequestAnimationFrame() { + this.requestAnimationFrame(this.testSetInterval0); + }, + + testSetInterval0() { + this._nextTest = this.testSetInterval20; + this._interval = this.setInterval(this._incrementInterval, 0); + }, + + testSetInterval20() { + this._nextTest = this.testSetImmediate; + this._interval = this.setInterval(this._incrementInterval, 20); + }, + + testSetImmediate() { + this.setImmediate(this.testClearTimeout0); + }, + + testClearTimeout0() { + var timeout = this.setTimeout(() => this._fail('testClearTimeout0'), 0); + this.clearTimeout(timeout); + this.testClearTimeout30(); + }, + + testClearTimeout30() { + var timeout = this.setTimeout(() => this._fail('testClearTimeout30'), 30); + this.clearTimeout(timeout); + this.setTimeout(this.testClearMulti, 50); + }, + + testClearMulti() { + var fails = [this.setTimeout(() => this._fail('testClearMulti-1'), 20)]; + fails.push(this.setTimeout(() => this._fail('testClearMulti-2'), 50)); + var delayClear = this.setTimeout(() => this._fail('testClearMulti-3'), 50); + fails.push(this.setTimeout(() => this._fail('testClearMulti-4'), 0)); + + this.setTimeout(this.testOrdering, 100); // Next test interleaved + + fails.push(this.setTimeout(() => this._fail('testClearMulti-5'), 10)); + + fails.forEach((timeout) => this.clearTimeout(timeout)); + this.setTimeout(() => this.clearTimeout(delayClear), 20); + }, + + testOrdering() { + // Clear timers are set first because it's more likely to uncover bugs. + var fail0; + this.setImmediate(() => this.clearTimeout(fail0)); + fail0 = this.setTimeout( + () => this._fail('testOrdering-t0, setImmediate should happen before ' + + 'setTimeout 0'), + 0 + ); + var failAnim; // This should fail without the t=0 fastpath feature. + this.setTimeout(() => this.cancelAnimationFrame(failAnim), 0); + failAnim = this.requestAnimationFrame( + () => this._fail('testOrdering-Anim, setTimeout 0 should happen before ' + + 'requestAnimationFrame') + ); + var fail50; + this.setTimeout(() => this.clearTimeout(fail50), 20); + fail50 = this.setTimeout( + () => this._fail('testOrdering-t50, setTimeout 20 should happen before ' + + 'setTimeout 50'), + 50 + ); + this.setTimeout(this.done, 75); + }, + + done() { + this.setState({done: true}, RCTTestModule.markTestCompleted); + }, + + render() { + return ( + + + {this.constructor.displayName + ': \n'} + Intervals: {this.state.count + '\n'} + {this.state.done ? 'Done' : 'Testing...'} + + + ); + }, + + _incrementInterval() { + if (this.state.count > 3) { + throw new Error('interval incremented past end.'); + } + if (this.state.count === 3) { + this.clearInterval(this._interval); + this.setState({count: 0}, this._nextTest); + return; + } + this.setState({count: this.state.count + 1}); + }, + + _fail(caller) { + throw new Error('_fail called by ' + caller); + }, +}); + +var styles = StyleSheet.create({ + container: { + backgroundColor: 'white', + padding: 40, + }, +}); + +module.exports = TimersTest; diff --git a/IntegrationTests/main.m b/IntegrationTests/main.m new file mode 100644 index 000000000..357a233b1 --- /dev/null +++ b/IntegrationTests/main.m @@ -0,0 +1,10 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/Libraries/ActionSheetIOS/ActionSheetIOS.js b/Libraries/ActionSheetIOS/ActionSheetIOS.js new file mode 100644 index 000000000..adbb8e3ab --- /dev/null +++ b/Libraries/ActionSheetIOS/ActionSheetIOS.js @@ -0,0 +1,50 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule ActionSheetIOS + */ +'use strict'; + +var RCTActionSheetManager = require('NativeModules').ActionSheetManager; + +var invariant = require('invariant'); + +var ActionSheetIOS = { + showActionSheetWithOptions(options, callback) { + invariant( + typeof options === 'object' && options !== null, + 'Options must a valid object' + ); + invariant( + typeof callback === 'function', + 'Must provide a valid callback' + ); + RCTActionSheetManager.showActionSheetWithOptions( + options, + () => {}, // RKActionSheet compatibility hack + callback + ); + }, + + showShareActionSheetWithOptions(options, failureCallback, successCallback) { + invariant( + typeof options === 'object' && options !== null, + 'Options must a valid object' + ); + invariant( + typeof failureCallback === 'function', + 'Must provide a valid failureCallback' + ); + invariant( + typeof successCallback === 'function', + 'Must provide a valid successCallback' + ); + RCTActionSheetManager.showShareActionSheetWithOptions( + options, + failureCallback, + successCallback + ); + } +}; + +module.exports = ActionSheetIOS; diff --git a/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj/project.pbxproj b/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj/project.pbxproj new file mode 100644 index 000000000..4b05e5f43 --- /dev/null +++ b/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj/project.pbxproj @@ -0,0 +1,250 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 14C644C41AB0DFC900DE3C65 /* RCTActionSheetManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14C644C21AB0DFC900DE3C65 /* RCTActionSheetManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814201AA4EA6300B7C361 /* libRCTActionSheet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTActionSheet.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 14C644C11AB0DFC900DE3C65 /* RCTActionSheetManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTActionSheetManager.h; sourceTree = ""; }; + 14C644C21AB0DFC900DE3C65 /* RCTActionSheetManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTActionSheetManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libRCTActionSheet.a */, + ); + name = Products; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + 14C644C11AB0DFC900DE3C65 /* RCTActionSheetManager.h */, + 14C644C21AB0DFC900DE3C65 /* RCTActionSheetManager.m */, + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* RCTActionSheet */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTActionSheet" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTActionSheet; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libRCTActionSheet.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTActionSheet" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* RCTActionSheet */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 14C644C41AB0DFC900DE3C65 /* RCTActionSheetManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTActionSheet; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = "$(inherited)"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTActionSheet; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTActionSheet" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTActionSheet" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.h b/Libraries/ActionSheetIOS/RCTActionSheetManager.h new file mode 100644 index 000000000..0ef25b520 --- /dev/null +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.h @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "RCTBridge.h" + +@interface RCTActionSheetManager : NSObject + +@end diff --git a/Libraries/ActionSheetIOS/RCTActionSheetManager.m b/Libraries/ActionSheetIOS/RCTActionSheetManager.m new file mode 100644 index 000000000..2c628e2be --- /dev/null +++ b/Libraries/ActionSheetIOS/RCTActionSheetManager.m @@ -0,0 +1,120 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTActionSheetManager.h" + +#import "RCTLog.h" + +@interface RCTActionSheetManager() + +@end + +@implementation RCTActionSheetManager { + NSMutableDictionary *_callbacks; +} + +- (instancetype)init +{ + if ((self = [super init])) { + _callbacks = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)showActionSheetWithOptions:(NSDictionary *)options + failureCallback:(RCTResponseSenderBlock)failureCallback + successCallback:(RCTResponseSenderBlock)successCallback +{ + RCT_EXPORT(); + + dispatch_async(dispatch_get_main_queue(), ^{ + UIActionSheet *actionSheet = [[UIActionSheet alloc] init]; + + actionSheet.title = options[@"title"]; + + for (NSString *option in options[@"options"]) { + [actionSheet addButtonWithTitle:option]; + } + + if (options[@"destructiveButtonIndex"]) { + actionSheet.destructiveButtonIndex = [options[@"destructiveButtonIndex"] integerValue]; + } + if (options[@"cancelButtonIndex"]) { + actionSheet.cancelButtonIndex = [options[@"cancelButtonIndex"] integerValue]; + } + + actionSheet.delegate = self; + + _callbacks[keyForInstance(actionSheet)] = successCallback; + + UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; + if (appWindow == nil) { + RCTLogError(@"Tried to display action sheet but there is no application window. options: %@", options); + return; + } + [actionSheet showInView:appWindow]; + }); +} + +- (void)showShareActionSheetWithOptions:(NSDictionary *)options + failureCallback:(RCTResponseSenderBlock)failureCallback + successCallback:(RCTResponseSenderBlock)successCallback +{ + RCT_EXPORT(); + + dispatch_async(dispatch_get_main_queue(), ^{ + NSMutableArray *items = [NSMutableArray array]; + id message = options[@"message"]; + id url = options[@"url"]; + if ([message isKindOfClass:[NSString class]]) { + [items addObject:message]; + } + if ([url isKindOfClass:[NSString class]]) { + [items addObject:[NSURL URLWithString:url]]; + } + 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]; + if ([share respondsToSelector:@selector(setCompletionWithItemsHandler:)]) { + share.completionWithItemsHandler = ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { + if (activityError) { + failureCallback(@[[activityError localizedDescription]]); + } else { + successCallback(@[@(completed), (activityType ?: [NSNull null])]); + } + }; + } else { + share.completionHandler = ^(NSString *activityType, BOOL completed) { + successCallback(@[@(completed), (activityType ?: [NSNull null])]); + }; + } + [ctrl presentViewController:share animated:YES completion:nil]; + }); +} + +#pragma mark UIActionSheetDelegate Methods + +- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex +{ + NSString *key = keyForInstance(actionSheet); + RCTResponseSenderBlock callback = _callbacks[key]; + if (callback) { + callback(@[@(buttonIndex)]); + [_callbacks removeObjectForKey:key]; + } else { + RCTLogWarn(@"No callback registered for action sheet: %@", actionSheet.title); + } + + [[[[UIApplication sharedApplication] delegate] window] makeKeyWindow]; +} + +#pragma mark Private + +static NSString *keyForInstance(id instance) +{ + return [NSString stringWithFormat:@"%p", instance]; +} + +@end diff --git a/Libraries/AdSupport/AdSupportIOS.js b/Libraries/AdSupport/AdSupportIOS.js new file mode 100644 index 000000000..a470f05cd --- /dev/null +++ b/Libraries/AdSupport/AdSupportIOS.js @@ -0,0 +1,18 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AdSupportIOS + */ +'use strict'; + +var AdSupport = require('NativeModules').AdSupport; + +module.exports = { + getAdvertisingId: function(onSuccess, onFailure) { + AdSupport.getAdvertisingId(onSuccess, onFailure); + }, + + getAdvertisingTrackingEnabled: function(onSuccess, onFailure) { + AdSupport.getAdvertisingTrackingEnabled(onSuccess, onFailure); + }, +}; diff --git a/Libraries/AdSupport/RCTAdSupport.h b/Libraries/AdSupport/RCTAdSupport.h new file mode 100644 index 000000000..0d52b4d2e --- /dev/null +++ b/Libraries/AdSupport/RCTAdSupport.h @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "RCTBridgeModule.h" + +@interface RCTAdSupport : NSObject + +@end diff --git a/Libraries/AdSupport/RCTAdSupport.m b/Libraries/AdSupport/RCTAdSupport.m new file mode 100644 index 000000000..22ad6aacb --- /dev/null +++ b/Libraries/AdSupport/RCTAdSupport.m @@ -0,0 +1,30 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTAdSupport.h" + +@implementation RCTAdSupport + +- (void)getAdvertisingId:(RCTResponseSenderBlock)callback withErrorCallback:(RCTResponseSenderBlock)errorCallback +{ + RCT_EXPORT(); + + if ([ASIdentifierManager class]) { + callback(@[[[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString]]); + } else { + return errorCallback(@[@"as_identifier_unavailable"]); + } +} + +- (void)getAdvertisingTrackingEnabled:(RCTResponseSenderBlock)callback withErrorCallback:(RCTResponseSenderBlock)errorCallback +{ + RCT_EXPORT(); + + if ([ASIdentifierManager class]) { + bool hasTracking = [[ASIdentifierManager sharedManager] isAdvertisingTrackingEnabled]; + callback(@[@(hasTracking)]); + } else { + return errorCallback(@[@"as_identifier_unavailable"]); + } +} + +@end diff --git a/Libraries/AdSupport/RCTAdSupport.xcodeproj/project.pbxproj b/Libraries/AdSupport/RCTAdSupport.xcodeproj/project.pbxproj new file mode 100644 index 000000000..dc1f0393b --- /dev/null +++ b/Libraries/AdSupport/RCTAdSupport.xcodeproj/project.pbxproj @@ -0,0 +1,248 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 832C819C1AAF6E1A007FA2F7 /* RCTAdSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 832C819B1AAF6E1A007FA2F7 /* RCTAdSupport.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 832C817E1AAF6DEF007FA2F7 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 832C81801AAF6DEF007FA2F7 /* libRCTAdSupport.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTAdSupport.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 832C819A1AAF6E1A007FA2F7 /* RCTAdSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAdSupport.h; sourceTree = ""; }; + 832C819B1AAF6E1A007FA2F7 /* RCTAdSupport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAdSupport.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 832C817D1AAF6DEF007FA2F7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 832C81771AAF6DEF007FA2F7 = { + isa = PBXGroup; + children = ( + 832C819A1AAF6E1A007FA2F7 /* RCTAdSupport.h */, + 832C819B1AAF6E1A007FA2F7 /* RCTAdSupport.m */, + 832C81811AAF6DEF007FA2F7 /* Products */, + ); + sourceTree = ""; + }; + 832C81811AAF6DEF007FA2F7 /* Products */ = { + isa = PBXGroup; + children = ( + 832C81801AAF6DEF007FA2F7 /* libRCTAdSupport.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 832C817F1AAF6DEF007FA2F7 /* RCTAdSupport */ = { + isa = PBXNativeTarget; + buildConfigurationList = 832C81941AAF6DF0007FA2F7 /* Build configuration list for PBXNativeTarget "RCTAdSupport" */; + buildPhases = ( + 832C817C1AAF6DEF007FA2F7 /* Sources */, + 832C817D1AAF6DEF007FA2F7 /* Frameworks */, + 832C817E1AAF6DEF007FA2F7 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTAdSupport; + productName = RCTAdSupport; + productReference = 832C81801AAF6DEF007FA2F7 /* libRCTAdSupport.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 832C81781AAF6DEF007FA2F7 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0620; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 832C817F1AAF6DEF007FA2F7 = { + CreatedOnToolsVersion = 6.2; + }; + }; + }; + buildConfigurationList = 832C817B1AAF6DEF007FA2F7 /* Build configuration list for PBXProject "RCTAdSupport" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 832C81771AAF6DEF007FA2F7; + productRefGroup = 832C81811AAF6DEF007FA2F7 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 832C817F1AAF6DEF007FA2F7 /* RCTAdSupport */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 832C817C1AAF6DEF007FA2F7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 832C819C1AAF6E1A007FA2F7 /* RCTAdSupport.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 832C81921AAF6DF0007FA2F7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 832C81931AAF6DF0007FA2F7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 832C81951AAF6DF0007FA2F7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 832C81961AAF6DF0007FA2F7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 832C817B1AAF6DEF007FA2F7 /* Build configuration list for PBXProject "RCTAdSupport" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 832C81921AAF6DF0007FA2F7 /* Debug */, + 832C81931AAF6DF0007FA2F7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 832C81941AAF6DF0007FA2F7 /* Build configuration list for PBXNativeTarget "RCTAdSupport" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 832C81951AAF6DF0007FA2F7 /* Debug */, + 832C81961AAF6DF0007FA2F7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 832C81781AAF6DEF007FA2F7 /* Project object */; +} diff --git a/Libraries/Animation/Animation.js b/Libraries/Animation/Animation.js new file mode 100644 index 000000000..3fbfb757f --- /dev/null +++ b/Libraries/Animation/Animation.js @@ -0,0 +1,35 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Animation + * @flow + */ +'use strict'; + +var RCTAnimationManager = require('NativeModules').AnimationManager; +var AnimationUtils = require('AnimationUtils'); + +type EasingFunction = (t: number) => number; + +var Animation = { + Mixin: require('AnimationMixin'), + + startAnimation: function( + node: any, + duration: number, + delay: number, + easing: (string | EasingFunction), + properties: {[key: string]: any} + ): number { + var nodeHandle = +node.getNodeHandle(); + var easingSample = AnimationUtils.evaluateEasingFunction(duration, easing); + var tag: number = RCTAnimationManager.startAnimation(nodeHandle, AnimationUtils.allocateTag(), duration, delay, easingSample, properties); + return tag; + }, + + stopAnimation: function(tag: number) { + RCTAnimationManager.stopAnimation(tag); + }, +}; + +module.exports = Animation; diff --git a/Libraries/Animation/AnimationMixin.js b/Libraries/Animation/AnimationMixin.js new file mode 100644 index 000000000..c33d630a9 --- /dev/null +++ b/Libraries/Animation/AnimationMixin.js @@ -0,0 +1,46 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AnimationMixin + * @flow + */ +'use strict'; + +var AnimationUtils = require('AnimationUtils'); +var RCTAnimationManager = require('NativeModules').AnimationManager; + +var invariant = require('invariant'); + +type EasingFunction = (t: number) => number; + +var AnimationMixin = { + getInitialState: function(): Object { + return {}; + }, + + startAnimation: function( + refKey: string, + duration: number, + delay: number, + easing: (string | EasingFunction), + properties: {[key: string]: any} + ): number { + var ref = this.refs[refKey]; + invariant( + ref, + 'Invalid refKey ' + refKey + '; ' + + 'valid refs: ' + JSON.stringify(Object.keys(this.refs)) + ); + + var nodeHandle = +ref.getNodeHandle(); + var easingSample = AnimationUtils.evaluateEasingFunction(duration, easing); + var tag: number = RCTAnimationManager.startAnimation(nodeHandle, AnimationUtils.allocateTag(), duration, delay, easingSample, properties); + return tag; + }, + + stopAnimation: function(tag: number) { + RCTAnimationManager.stopAnimation(tag); + }, +}; + +module.exports = AnimationMixin; diff --git a/Libraries/Animation/AnimationUtils.js b/Libraries/Animation/AnimationUtils.js new file mode 100644 index 000000000..d36e817cf --- /dev/null +++ b/Libraries/Animation/AnimationUtils.js @@ -0,0 +1,220 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AnimationUtils + * @flow + */ +'use strict'; + +type EasingFunction = (t: number) => number; + +var defaults = { + easeInQuad: function(t) { + return t * t; + }, + easeOutQuad: function(t) { + return -t * (t - 2); + }, + easeInOutQuad: function(t) { + t = t * 2; + if (t < 1) { + return 0.5 * t * t; + } + return -((t - 1) * (t - 3) - 1) / 2; + }, + easeInCubic: function(t) { + return t * t * t; + }, + easeOutCubic: function(t) { + t -= 1; + return t * t * t + 1; + }, + easeInOutCubic: function(t) { + t *= 2; + if (t < 1) { + return 0.5 * t * t * t; + } + t -= 2; + return (t * t * t + 2) / 2; + }, + easeInQuart: function(t) { + return t * t * t * t; + }, + easeOutQuart: function(t) { + t -= 1; + return -(t * t * t * t - 1); + }, + easeInOutQuart: function(t) { + t *= 2; + if (t < 1) { + return 0.5 * t * t * t * t; + } + t -= 2; + return -(t * t * t * t - 2) / 2; + }, + easeInQuint: function(t) { + return t * t * t * t * t; + }, + easeOutQuint: function(t) { + t -= 1; + return t * t * t * t * t + 1; + }, + easeInOutQuint: function(t) { + t *= 2; + if (t < 1) { + return (t * t * t * t * t) / 2; + } + t -= 2; + return (t * t * t * t * t + 2) / 2; + }, + easeInSine: function(t) { + return -Math.cos(t * (Math.PI / 2)) + 1; + }, + easeOutSine: function(t) { + return Math.sin(t * (Math.PI / 2)); + }, + easeInOutSine: function(t) { + return -(Math.cos(Math.PI * t) - 1) / 2; + }, + easeInExpo: function(t) { + return (t === 0) ? 0 : Math.pow(2, 10 * (t - 1)); + }, + easeOutExpo: function(t) { + return (t === 1) ? 1 : (-Math.pow(2, -10 * t) + 1); + }, + easeInOutExpo: function(t) { + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + t *= 2; + if (t < 1) { + return 0.5 * Math.pow(2, 10 * (t - 1)); + } + return (-Math.pow(2, -10 * (t - 1)) + 2) / 2; + }, + easeInCirc: function(t) { + return -(Math.sqrt(1 - t * t) - 1); + }, + easeOutCirc: function(t) { + t -= 1; + return Math.sqrt(1 - t * t); + }, + easeInOutCirc: function(t) { + t *= 2; + if (t < 1) { + return -(Math.sqrt(1 - t * t) - 1) / 2; + } + t -= 2; + return (Math.sqrt(1 - t * t) + 1) / 2; + }, + easeInElastic: function(t) { + var s = 1.70158; + var p = 0.3; + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + var s = p / (2 * Math.PI) * Math.asin(1); + t -= 1; + return -(Math.pow(2, 10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); + }, + easeOutElastic: function(t) { + var s = 1.70158; + var p = 0.3; + if (t === 0) { + return 0; + } + if (t === 1) { + return 1; + } + var s = p / (2 * Math.PI) * Math.asin(1); + return Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; + }, + easeInOutElastic: function(t) { + var s = 1.70158; + var p = 0.3 * 1.5; + if (t === 0) { + return 0; + } + t *= 2; + if (t === 2) { + return 1; + } + var s = p / (2 * Math.PI) * Math.asin(1); + if (t < 1) { + t -= 1; + return -(Math.pow(2, 10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)) / 2; + } + t -= 1; + return Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) / 2 + 1; + }, + easeInBack: function(t) { + var s = 1.70158; + return t * t * ((s + 1) * t - s); + }, + easeOutBack: function(t) { + var s = 1.70158; + t -= 1; + return (t * t * ((s + 1) * t + s) + 1); + }, + easeInOutBack: function(t) { + var s = 1.70158 * 1.525; + t *= 2; + if (t < 1) { + return (t * t * ((s + 1) * t - s)) / 2; + } + t -= 2; + return (t * t * ((s + 1) * t + s) + 2) / 2; + }, + easeInBounce: function(t) { + return 1 - this.easeOutBounce(1 - t); + }, + easeOutBounce: function(t) { + if (t < (1 / 2.75)) { + return 7.5625 * t * t; + } else if (t < (2 / 2.75)) { + t -= 1.5 / 2.75; + return 7.5625 * t * t + 0.75; + } else if (t < (2.5 / 2.75)) { + t -= 2.25 / 2.75; + return 7.5625 * t * t + 0.9375; + } else { + t -= 2.625 / 2.75; + return 7.5625 * t * t + 0.984375; + } + }, + easeInOutBounce: function(t) { + if (t < 0.5) { + return this.easeInBounce(t * 2) / 2; + } + return this.easeOutBounce(t * 2 - 1) / 2 + 0.5; + }, +}; + +var ticksPerSecond = 60; +var lastUsedTag = 0; + +module.exports = { + allocateTag: function(): number { + return ++lastUsedTag; + }, + + evaluateEasingFunction: function(duration: number, easing: string | EasingFunction): Array { + if (typeof easing === 'string') { + easing = defaults[easing] || defaults.easeOutQuad; + } + + var tickCount = Math.round(duration * ticksPerSecond / 1000); + var sample = []; + for (var i = 0; i <= tickCount; i++) { + sample.push(easing(i / tickCount)); + } + + return sample; + }, +}; diff --git a/Libraries/Animation/LayoutAnimation.js b/Libraries/Animation/LayoutAnimation.js index 5ce5648b3..16f737ba3 100644 --- a/Libraries/Animation/LayoutAnimation.js +++ b/Libraries/Animation/LayoutAnimation.js @@ -6,7 +6,7 @@ 'use strict'; var PropTypes = require('ReactPropTypes'); -var RKUIManager = require('NativeModules').RKUIManager; +var RCTUIManager = require('NativeModules').UIManager; var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker'); var keyMirror = require('keyMirror'); @@ -47,7 +47,7 @@ var configChecker = createStrictShapeTypeChecker({ var LayoutAnimation = { configureNext(config, onAnimationDidEnd, onError) { configChecker({config}, 'config', 'LayoutAnimation.configureNext'); - RKUIManager.configureNextLayoutAnimation(config, onAnimationDidEnd, onError); + RCTUIManager.configureNextLayoutAnimation(config, onAnimationDidEnd, onError); }, create(duration, type, creationProp) { return { diff --git a/Libraries/Animation/POPAnimationMixin.js b/Libraries/Animation/POPAnimationMixin.js index a3f4b7def..49145ebd4 100644 --- a/Libraries/Animation/POPAnimationMixin.js +++ b/Libraries/Animation/POPAnimationMixin.js @@ -7,10 +7,11 @@ 'use strict'; var POPAnimation = require('POPAnimation'); + if (!POPAnimation) { // 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; + module.exports = (null : ?{}); } else { var invariant = require('invariant'); @@ -224,12 +225,10 @@ var POPAnimationMixin = { w: frame.width, h: frame.height }; - frame = undefined; - var velocity = velocity || [0, 0]; var posAnim = POPAnimation.createAnimation(type, { property: POPAnimation.Properties.position, toValue: [animFrame.x, animFrame.y], - velocity: velocity, + velocity: velocity || [0, 0], }); var sizeAnim = POPAnimation.createAnimation(type, { property: POPAnimation.Properties.size, diff --git a/Libraries/Animation/__tests__/AnimationUtils-test.js b/Libraries/Animation/__tests__/AnimationUtils-test.js new file mode 100644 index 000000000..00ca67dab --- /dev/null +++ b/Libraries/Animation/__tests__/AnimationUtils-test.js @@ -0,0 +1,46 @@ +'use strict'; + +jest.autoMockOff(); + +var AnimationUtils = require('AnimationUtils'); + +describe('AnimationUtils', function() { + var DURATION = 300; + + var Samples = { + easeInQuad: [0,0.0030864197530864196,0.012345679012345678,0.027777777777777776,0.04938271604938271,0.0771604938271605,0.1111111111111111,0.15123456790123457,0.19753086419753085,0.25,0.308641975308642,0.37345679012345684,0.4444444444444444,0.5216049382716049,0.6049382716049383,0.6944444444444445,0.7901234567901234,0.8919753086419753,1], + easeOutQuad: [0,0.10802469135802469,0.20987654320987653,0.3055555555555555,0.3950617283950617,0.47839506172839513,0.5555555555555556,0.6265432098765432,0.691358024691358,0.75,0.8024691358024691,0.8487654320987654,0.888888888888889,0.9228395061728394,0.9506172839506174,0.9722222222222221,0.9876543209876543,0.9969135802469136,1], + easeInOutQuad: [0,0.006172839506172839,0.024691358024691357,0.05555555555555555,0.09876543209876543,0.154320987654321,0.2222222222222222,0.30246913580246915,0.3950617283950617,0.5,0.6049382716049383,0.697530864197531,0.7777777777777777,0.845679012345679,0.9012345679012346,0.9444444444444444,0.9753086419753086,0.9938271604938271,1], + easeInCubic: [0,0.00017146776406035664,0.0013717421124828531,0.004629629629629629,0.010973936899862825,0.021433470507544586,0.037037037037037035,0.05881344307270234,0.0877914951989026,0.125,0.1714677640603567,0.22822359396433475,0.2962962962962963,0.37671467764060357,0.4705075445816187,0.5787037037037038,0.7023319615912208,0.8424211248285322,1], + easeOutCubic: [0,0.15757887517146785,0.2976680384087792,0.42129629629629617,0.5294924554183813,0.6232853223593964,0.7037037037037036,0.7717764060356652,0.8285322359396433,0.875,0.9122085048010974,0.9411865569272977,0.9629629629629629,0.9785665294924554,0.9890260631001372,0.9953703703703703,0.9986282578875172,0.9998285322359396,1], + easeInOutCubic: [0,0.0006858710562414266,0.0054869684499314125,0.018518518518518517,0.0438957475994513,0.08573388203017834,0.14814814814814814,0.23525377229080935,0.3511659807956104,0.5,0.6488340192043895,0.7647462277091908,0.8518518518518519,0.9142661179698217,0.9561042524005487,0.9814814814814815,0.9945130315500685,0.9993141289437586,1], + easeInQuart: [0,0.000009525986892242035,0.00015241579027587256,0.0007716049382716049,0.002438652644413961,0.005953741807651274,0.012345679012345678,0.02287189452827313,0.039018442310623375,0.0625,0.09525986892242039,0.1394699740893157,0.19753086419753085,0.2720717116293248,0.3659503124523701,0.48225308641975323,0.624295076969974,0.7956199512269471,1], + easeOutQuart: [0,0.20438004877305294,0.375704923030026,0.5177469135802468,0.6340496875476299,0.7279282883706752,0.802469135802469,0.8605300259106843,0.9047401310775796,0.9375,0.9609815576893767,0.9771281054717269,0.9876543209876543,0.9940462581923487,0.997561347355586,0.9992283950617284,0.9998475842097241,0.9999904740131078,1], + easeInOutQuart: [0,0.00007620789513793628,0.0012193263222069805,0.006172839506172839,0.019509221155311687,0.047629934461210194,0.09876543209876543,0.18297515622618504,0.312147538484987,0.5,0.687852461515013,0.8170248437738151,0.9012345679012346,0.9523700655387898,0.9804907788446883,0.9938271604938271,0.998780673677793,0.999923792104862,1], + easeInQuint: [0,5.292214940134463e-7,0.000016935087808430282,0.00012860082304526747,0.000541922809869769,0.0016538171687920206,0.004115226337448559,0.008894625649883995,0.01734152991583261,0.03125,0.05292214940134466,0.08523165083235959,0.1316872427983539,0.1964962361767346,0.28462802079628785,0.401877572016461,0.5549289573066435,0.75141884282545,1], + easeOutQuint: [0,0.24858115717454998,0.4450710426933565,0.598122427983539,0.7153719792037121,0.8035037638232654,0.868312757201646,0.9147683491676404,0.9470778505986553,0.96875,0.9826584700841674,0.991105374350116,0.9958847736625515,0.998346182831208,0.9994580771901302,0.9998713991769548,0.9999830649121916,0.999999470778506,1], + easeInOutQuint: [0,0.000008467543904215141,0.0002709614049348845,0.0020576131687242796,0.008670764957916305,0.02646107470067233,0.06584362139917695,0.14231401039814393,0.27746447865332174,0.5,0.7225355213466782,0.8576859896018563,0.934156378600823,0.9735389252993276,0.9913292350420837,0.9979423868312757,0.9997290385950651,0.9999915324560957,1], + easeInSine: [0,0.003805301908254455,0.01519224698779198,0.03407417371093169,0.06030737921409157,0.09369221296335006,0.1339745962155613,0.1808479557110082,0.233955556881022,0.2928932188134524,0.35721239031346064,0.42642356364895384,0.4999999999999999,0.5773817382593005,0.6579798566743311,0.7411809548974793,0.8263518223330696,0.9128442572523416,0.9999999999999999], + easeOutSine: [0,0.08715574274765817,0.17364817766693033,0.25881904510252074,0.3420201433256687,0.42261826174069944,0.49999999999999994,0.573576436351046,0.6427876096865393,0.7071067811865475,0.766044443118978,0.8191520442889918,0.8660254037844386,0.9063077870366499,0.9396926207859083,0.9659258262890683,0.984807753012208,0.9961946980917455,1], + easeInOutSine: [0,0.00759612349389599,0.030153689607045786,0.06698729810778065,0.116977778440511,0.17860619515673032,0.24999999999999994,0.32898992833716556,0.4131759111665348,0.49999999999999994,0.5868240888334652,0.6710100716628343,0.7499999999999999,0.8213938048432696,0.883022221559489,0.9330127018922194,0.9698463103929542,0.9924038765061041,1], + easeInExpo: [0,0.0014352875901128893,0.002109491677524035,0.0031003926796253885,0.004556754060844206,0.006697218616039631,0.009843133202303688,0.014466792379488908,0.021262343752724643,0.03125,0.045929202883612456,0.06750373368076916,0.09921256574801243,0.1458161299470146,0.2143109957132682,0.31498026247371835,0.46293735614364506,0.6803950000871883,1], + easeOutExpo: [0,0.31960499991281155,0.5370626438563548,0.6850197375262816,0.7856890042867318,0.8541838700529854,0.9007874342519875,0.9324962663192309,0.9540707971163875,0.96875,0.9787376562472754,0.9855332076205111,0.9901568667976963,0.9933027813839603,0.9954432459391558,0.9968996073203746,0.9978905083224759,0.9985647124098871,1], + easeInOutExpo: [0,0.0010547458387620175,0.002278377030422103,0.004921566601151844,0.010631171876362321,0.022964601441806228,0.049606282874006216,0.1071554978566341,0.23146867807182253,0.5,0.7685313219281775,0.892844502143366,0.9503937171259937,0.9770353985581938,0.9893688281236377,0.9950784333988482,0.9977216229695779,0.998945254161238,1], + easeInCirc: [0,0.0015444024660317135,0.006192010000093506,0.013986702816730645,0.025003956956430873,0.03935464078941209,0.057190958417936644,0.07871533601238889,0.10419358352238339,0.1339745962155614,0.1685205807169019,0.20845517506805522,0.2546440075000701,0.3083389112228482,0.37146063894529113,0.4472292016074334,0.5418771527091488,0.6713289009389102,1], + easeOutCirc: [0,0.3286710990610898,0.45812284729085123,0.5527707983925666,0.6285393610547089,0.6916610887771518,0.7453559924999298,0.7915448249319448,0.8314794192830981,0.8660254037844386,0.8958064164776166,0.9212846639876111,0.9428090415820634,0.9606453592105879,0.9749960430435691,0.9860132971832694,0.9938079899999065,0.9984555975339683,1], + easeInOutCirc: [0,0.003096005000046753,0.012501978478215436,0.028595479208968322,0.052096791761191696,0.08426029035845095,0.12732200375003505,0.18573031947264557,0.2709385763545744,0.5,0.7290614236454256,0.8142696805273546,0.8726779962499649,0.915739709641549,0.9479032082388084,0.9714045207910317,0.9874980215217846,0.9969039949999532,1], + easeInElastic: [0,0.0008570943160003016,0.0020526300563455885,0.0005383775388688477,-0.003807112477441741,-0.005595444524068916,0.0017092421431128787,0.014076838118604966,0.012696991251677569,-0.015625000000000045,-0.045618646044515744,-0.01936028903971309,0.07600123467884114,0.13030605320629246,-0.012461076179381799,-0.29598462833976175,-0.3176868895106366,0.2694906924487451,1], + easeOutElastic: [0,0.7305093075512543,1.3176868895106364,1.2959846283397618,1.0124610761793817,0.8696939467937076,0.9239987653211588,1.019360289039713,1.0456186460445158,1.015625,0.9873030087483224,0.9859231618813951,0.9982907578568871,1.005595444524069,1.0038071124774417,0.9994616224611311,0.9979473699436544,0.9991429056839997,1], + easeInOutElastic: [0,0.0010420781824747765,-0.0003083357248478688,-0.004888288728445655,0.0010292130059457788,0.022895545534212507,-0.0028843488305936938,-0.10707491183281304,0.004488485931276091,0.5,0.995511514068724,1.107074911832813,1.0028843488305939,0.9771044544657875,0.9989707869940542,1.0048882887284456,1.000308335724848,0.9989579218175252,1], + easeInBack: [0,-0.004788556241426612,-0.017301289437585736,-0.0347587962962963,-0.05438167352537723,-0.07339051783264748,-0.08900592592592595,-0.09844849451303156,-0.0989388203017833,-0.08769750000000004,-0.06194513031550073,-0.018902307956104283,0.044210370370370254,0.13017230795610413,0.2417629080932785,0.3817615740740742,0.5529477091906719,0.7581007167352535,0.9999999999999998], + easeOutBack: [2.220446049250313e-16,0.24189928326474652,0.44705229080932807,0.6182384259259258,0.7582370919067215,0.8698276920438959,0.9557896296296297,1.0189023079561044,1.0619451303155008,1.0876975,1.0989388203017834,1.0984484945130315,1.089005925925926,1.0733905178326475,1.0543816735253773,1.0347587962962963,1.0173012894375857,1.0047885562414267,1], + easeInOutBack: [0,-0.01355231550068587,-0.04434668449931412,-0.07758924074074074,-0.09848611796982167,-0.0922434499314129,-0.0440673703703704,0.060835986968449905,0.237260488340192,0.5,0.762739511659808,0.9391640130315503,1.0440673703703702,1.0922434499314129,1.0984861179698218,1.0775892407407408,1.0443466844993141,1.0135523155006858,1], + }; + + Object.keys(Samples).forEach(function(type) { + it('should interpolate ' + type, function() { + expect(AnimationUtils.evaluateEasingFunction(DURATION, type)) + .toEqual(Samples[type]); + }); + }); +}); diff --git a/Libraries/AppRegistry/AppRegistry.js b/Libraries/AppRegistry/AppRegistry.js index a11f5d494..346bc302c 100644 --- a/Libraries/AppRegistry/AppRegistry.js +++ b/Libraries/AppRegistry/AppRegistry.js @@ -53,10 +53,12 @@ var AppRegistry = { runApplication: function(appKey, appParameters) { console.log( - 'Running application "' + appKey + '" with appParams: ', - appParameters + 'Running application "' + appKey + '" with appParams: ' + + JSON.stringify(appParameters) + '. ' + + '__DEV__ === ' + __DEV__ + + ', development-level warning are ' + (__DEV__ ? 'ON' : 'OFF') + + ', performance optimizations are ' + (__DEV__ ? 'OFF' : 'ON') ); - invariant( runnables[appKey] && runnables[appKey].run, 'Application ' + appKey + ' has not been registered.' diff --git a/Libraries/AppState/AppState.js b/Libraries/AppState/AppState.js new file mode 100644 index 000000000..043cdba55 --- /dev/null +++ b/Libraries/AppState/AppState.js @@ -0,0 +1,23 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AppState + */ +'use strict'; + +var NativeModules = require('NativeModules'); +var RCTAppState = NativeModules.AppState; + +var AppState = { + + setApplicationIconBadgeNumber: function(number) { + RCTAppState.setApplicationIconBadgeNumber(number); + }, + + getApplicationIconBadgeNumber: function(callback) { + RCTAppState.getApplicationIconBadgeNumber(callback); + }, + +}; + +module.exports = AppState; diff --git a/Libraries/AppStateIOS/AppStateIOS.android.js b/Libraries/AppStateIOS/AppStateIOS.android.js new file mode 100644 index 000000000..4bc7e691c --- /dev/null +++ b/Libraries/AppStateIOS/AppStateIOS.android.js @@ -0,0 +1,24 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AppStateIOS + */ +'use strict'; + +var warning = require('warning'); + +class AppStateIOS { + + static addEventListener(type, handler) { + warning('Cannot listen to AppStateIOS events on Android.'); + } + + static removeEventListener(type, handler) { + warning('Cannot remove AppStateIOS listener on Android.'); + } + +} + +AppStateIOS.currentState = null; + +module.exports = AppStateIOS; diff --git a/Libraries/AppStateIOS/AppStateIOS.ios.js b/Libraries/AppStateIOS/AppStateIOS.ios.js new file mode 100644 index 000000000..9b3e44472 --- /dev/null +++ b/Libraries/AppStateIOS/AppStateIOS.ios.js @@ -0,0 +1,55 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AppStateIOS + */ +'use strict'; + +var NativeModules = require('NativeModules'); +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTAppState = NativeModules.AppState; + +var logError = require('logError'); + +var DEVICE_APPSTATE_EVENT = 'appStateDidChange'; + +var _appStateHandlers = {}; + +class AppStateIOS { + + static addEventListener(type, handler) { + _appStateHandlers[handler] = RCTDeviceEventEmitter.addListener( + DEVICE_APPSTATE_EVENT, + (appStateData) => { + handler(appStateData.app_state); + } + ); + } + + static removeEventListener(type, handler) { + if (!_appStateHandlers[handler]) { + return; + } + _appStateHandlers[handler].remove(); + _appStateHandlers[handler] = null; + } + +} + +AppStateIOS.currentState = null; + +RCTDeviceEventEmitter.addListener( + DEVICE_APPSTATE_EVENT, + (appStateData) => { + AppStateIOS.currentState = appStateData.app_state; + } +); + +RCTAppState.getCurrentAppState( + (appStateData) => { + AppStateIOS.currentState = appStateData.app_state; + }, + logError +); + +module.exports = AppStateIOS; diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/NativeModules.js b/Libraries/BatchedBridge/BatchedBridgedModules/NativeModules.js index 680528575..be36f8663 100644 --- a/Libraries/BatchedBridge/BatchedBridgedModules/NativeModules.js +++ b/Libraries/BatchedBridge/BatchedBridgedModules/NativeModules.js @@ -7,10 +7,8 @@ var NativeModules = require('BatchedBridge').RemoteModules; -// Dirty hack to support old (RK) and new (RCT) native module name conventions -Object.keys(NativeModules).forEach((moduleName) => { - var rkModuleName = moduleName.replace(/^RCT/, 'RK'); - NativeModules[rkModuleName] = NativeModules[moduleName]; -}); +var nativeModulePrefixNormalizer = require('nativeModulePrefixNormalizer'); + +nativeModulePrefixNormalizer(NativeModules); module.exports = NativeModules; diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/NativeModulesDeprecated.js b/Libraries/BatchedBridge/BatchedBridgedModules/NativeModulesDeprecated.js deleted file mode 100644 index 2d83d6f9a..000000000 --- a/Libraries/BatchedBridge/BatchedBridgedModules/NativeModulesDeprecated.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule NativeModulesDeprecated - */ -'use strict'; - -var RemoteModulesDeprecated = require('BatchedBridge').RemoteModulesDeprecated; - -// Dirty hack to support old (RK) and new (RCT) native module name conventions -Object.keys(RemoteModulesDeprecated).forEach((moduleName) => { - var rkModuleName = moduleName.replace(/^RCT/, 'RK'); - RemoteModulesDeprecated[rkModuleName] = RemoteModulesDeprecated[moduleName]; -}); - -module.exports = RemoteModulesDeprecated; diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js b/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js index 8ec0bd62f..97158ecfa 100644 --- a/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js +++ b/Libraries/BatchedBridge/BatchedBridgedModules/POPAnimation.js @@ -5,8 +5,8 @@ */ 'use strict'; -var RKPOPAnimationManager = require('NativeModulesDeprecated').RKPOPAnimationManager; -if (!RKPOPAnimationManager) { +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. module.exports = null; @@ -18,45 +18,45 @@ var getObjectValues = require('getObjectValues'); var invariant = require('invariant'); var merge = require('merge'); -var RKTypes = RKPOPAnimationManager.Types; -var RKProperties = RKPOPAnimationManager.Properties; +var RCTTypes = RCTPOPAnimationManager.Types; +var RCTProperties = RCTPOPAnimationManager.Properties; var Properties = { - bounds: RKProperties.bounds, - opacity: RKProperties.opacity, - position: RKProperties.position, - positionX: RKProperties.positionX, - positionY: RKProperties.positionY, - zPosition: RKProperties.zPosition, - rotation: RKProperties.rotation, - rotationX: RKProperties.rotationX, - rotationY: RKProperties.rotationY, - scaleX: RKProperties.scaleX, - scaleXY: RKProperties.scaleXY, - scaleY: RKProperties.scaleY, - shadowColor: RKProperties.shadowColor, - shadowOffset: RKProperties.shadowOffset, - shadowOpacity: RKProperties.shadowOpacity, - shadowRadius: RKProperties.shadowRadius, - size: RKProperties.size, - subscaleXY: RKProperties.subscaleXY, - subtranslationX: RKProperties.subtranslationX, - subtranslationXY: RKProperties.subtranslationXY, - subtranslationY: RKProperties.subtranslationY, - subtranslationZ: RKProperties.subtranslationZ, - translationX: RKProperties.translationX, - translationXY: RKProperties.translationXY, - translationY: RKProperties.translationY, - translationZ: RKProperties.translationZ, + 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: RKTypes.decay, - easeIn: RKTypes.easeIn, - easeInEaseOut: RKTypes.easeInEaseOut, - easeOut: RKTypes.easeOut, - linear: RKTypes.linear, - spring: RKTypes.spring, + decay: RCTTypes.decay, + easeIn: RCTTypes.easeIn, + easeInEaseOut: RCTTypes.easeInEaseOut, + easeOut: RCTTypes.easeOut, + linear: RCTTypes.linear, + spring: RCTTypes.spring, }; var POPAnimation = { @@ -98,7 +98,7 @@ var POPAnimation = { ); } - RKPOPAnimationManager.createAnimationInternal(tag, typeName, attrs); + RCTPOPAnimationManager.createAnimationInternal(tag, typeName, attrs); return tag; }, @@ -127,35 +127,35 @@ var POPAnimation = { }, addAnimation: function(nodeHandle, anim, callback) { - RKPOPAnimationManager.addAnimation(nodeHandle, anim, callback); + RCTPOPAnimationManager.addAnimation(nodeHandle, anim, callback); }, removeAnimation: function(nodeHandle, anim) { - RKPOPAnimationManager.removeAnimation(nodeHandle, anim); + RCTPOPAnimationManager.removeAnimation(nodeHandle, anim); }, }; -// Make sure that we correctly propagate RKPOPAnimationManager constants +// Make sure that we correctly propagate RCTPOPAnimationManager constants // to POPAnimation if (__DEV__) { var allProperties = merge( - RKPOPAnimationManager.Properties, - RKPOPAnimationManager.Properties + RCTPOPAnimationManager.Properties, + RCTPOPAnimationManager.Properties ); for (var key in allProperties) { invariant( - POPAnimation.Properties[key] === RKPOPAnimationManager.Properties[key], + POPAnimation.Properties[key] === RCTPOPAnimationManager.Properties[key], 'POPAnimation doesn\'t copy property ' + key + ' correctly' ); } var allTypes = merge( - RKPOPAnimationManager.Types, - RKPOPAnimationManager.Types + RCTPOPAnimationManager.Types, + RCTPOPAnimationManager.Types ); for (var key in allTypes) { invariant( - POPAnimation.Types[key] === RKPOPAnimationManager.Types[key], + POPAnimation.Types[key] === RCTPOPAnimationManager.Types[key], 'POPAnimation doesn\'t copy type ' + key + ' correctly' ); } diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/RCTAlertManager.ios.js b/Libraries/BatchedBridge/BatchedBridgedModules/RCTAlertManager.ios.js new file mode 100644 index 000000000..e8b3bcd27 --- /dev/null +++ b/Libraries/BatchedBridge/BatchedBridgedModules/RCTAlertManager.ios.js @@ -0,0 +1,10 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule RCTAlertManager + */ +'use strict'; + +var RCTAlertManager = require('NativeModules').AlertManager; + +module.exports = RCTAlertManager; diff --git a/Libraries/BatchedBridge/BatchedBridgedModules/RKAlertManager.ios.js b/Libraries/BatchedBridge/BatchedBridgedModules/RKAlertManager.ios.js deleted file mode 100644 index 39d3cbc27..000000000 --- a/Libraries/BatchedBridge/BatchedBridgedModules/RKAlertManager.ios.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule RKAlertManager - */ -'use strict'; - -var RKAlertManager = require('NativeModulesDeprecated').RKAlertManager; - -module.exports = RKAlertManager; diff --git a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js index d6c7f5f3f..a41a13226 100644 --- a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js +++ b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js @@ -22,33 +22,6 @@ var MethodTypes = keyMirror({ */ var BatchedBridgeFactory = { MethodTypes: MethodTypes, - /** - * @deprecated: Remove callsites and delete this method. - * - * @param {MessageQueue} messageQueue Message queue that has been created with - * the `moduleConfig` (among others perhaps). - * @param {object} moduleConfig Configuration of module names/method - * names to callback types. - * @return {object} Remote representation of configured module. - */ - _createDeprecatedBridgedModule: function(messageQueue, moduleConfig, moduleName) { - var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) { - return methodConfig.type === MethodTypes.local ? null : function() { - var lastArg = arguments.length ? arguments[arguments.length - 1] : null; - var hasCB = - typeof lastArg == 'function'; - var args = slice.call(arguments, 0, arguments.length - (hasCB ? 1 : 0)); - var cb = hasCB ? lastArg : null; - return messageQueue.callDeprecated(moduleName, memberName, args, cb); - }; - }); - for (var constName in moduleConfig.constants) { - warning(!remoteModule[constName], 'saw constant and method named %s', constName); - remoteModule[constName] = moduleConfig.constants[constName]; - } - return remoteModule; - }, - /** * @param {MessageQueue} messageQueue Message queue that has been created with * the `moduleConfig` (among others perhaps). @@ -63,14 +36,14 @@ var BatchedBridgeFactory = { var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null; var hasSuccCB = typeof lastArg === 'function'; var hasErrorCB = typeof secondLastArg === 'function'; - var hasCBs = hasSuccCB; - invariant( - (hasSuccCB && hasErrorCB) || (!hasSuccCB && !hasErrorCB), - 'You must supply error callbacks and success callbacks or neither' + hasErrorCB && invariant( + hasSuccCB, + 'Cannot have a non-function arg after a function arg.' ); - var args = slice.call(arguments, 0, arguments.length - (hasCBs ? 2 : 0)); - var onSucc = hasCBs ? lastArg : null; - var onFail = hasCBs ? secondLastArg : null; + var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0); + var args = slice.call(arguments, 0, arguments.length - numCBs); + var onSucc = hasSuccCB ? lastArg : null; + var onFail = hasErrorCB ? secondLastArg : null; return messageQueue.call(moduleName, memberName, args, onFail, onSucc); }; }); @@ -92,8 +65,6 @@ var BatchedBridgeFactory = { invokeCallbackAndReturnFlushedQueue: messageQueue.invokeCallbackAndReturnFlushedQueue.bind(messageQueue), flushedQueue: messageQueue.flushedQueue.bind(messageQueue), - // These deprecated modules do not accept an error callback. - RemoteModulesDeprecated: mapObject(modulesConfig, this._createDeprecatedBridgedModule.bind(this, messageQueue)), RemoteModules: mapObject(modulesConfig, this._createBridgedModule.bind(this, messageQueue)), setLoggingEnabled: messageQueue.setLoggingEnabled.bind(messageQueue), getLoggedOutgoingItems: messageQueue.getLoggedOutgoingItems.bind(messageQueue), diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js new file mode 100644 index 000000000..c8499fc97 --- /dev/null +++ b/Libraries/CameraRoll/CameraRoll.js @@ -0,0 +1,149 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule CameraRoll + */ +'use strict'; + +var ReactPropTypes = require('ReactPropTypes'); +var RCTCameraRollManager = require('NativeModules').CameraRollManager; + +var createStrictShapeTypeChecker = require('createStrictShapeTypeChecker'); +var deepFreezeAndThrowOnMutationInDev = + require('deepFreezeAndThrowOnMutationInDev'); +var invariant = require('invariant'); + +var GROUP_TYPES_OPTIONS = [ + 'Album', + 'All', + 'Event', + 'Faces', + 'Library', + 'PhotoStream', + 'SavedPhotos', // default +]; + +deepFreezeAndThrowOnMutationInDev(GROUP_TYPES_OPTIONS); + +/** + * Shape of the param arg for the `getPhotos` function. + */ +var getPhotosParamChecker = createStrictShapeTypeChecker({ + /** + * The number of photos wanted in reverse order of the photo application + * (i.e. most recent first for SavedPhotos). + */ + first: ReactPropTypes.number.isRequired, + + /** + * A cursor that matches `page_info { end_cursor }` returned from a previous + * call to `getPhotos` + */ + after: ReactPropTypes.string, + + /** + * Specifies which group types to filter the results to. + */ + groupTypes: ReactPropTypes.oneOf(GROUP_TYPES_OPTIONS), + + /** + * Specifies filter on group names, like 'Recent Photos' or custom album + * titles. + */ + groupName: ReactPropTypes.string, +}); + +/** + * Shape of the return value of the `getPhotos` function. + */ +var getPhotosReturnChecker = createStrictShapeTypeChecker({ + edges: ReactPropTypes.arrayOf(createStrictShapeTypeChecker({ + node: createStrictShapeTypeChecker({ + type: ReactPropTypes.string.isRequired, + group_name: ReactPropTypes.string.isRequired, + image: createStrictShapeTypeChecker({ + uri: ReactPropTypes.string.isRequired, + height: ReactPropTypes.number.isRequired, + width: ReactPropTypes.number.isRequired, + isStored: ReactPropTypes.bool, + }).isRequired, + timestamp: ReactPropTypes.number.isRequired, + location: createStrictShapeTypeChecker({ + latitude: ReactPropTypes.number, + longitude: ReactPropTypes.number, + altitude: ReactPropTypes.number, + heading: ReactPropTypes.number, + speed: ReactPropTypes.number, + }), + }).isRequired, + })).isRequired, + page_info: createStrictShapeTypeChecker({ + has_next_page: ReactPropTypes.bool.isRequired, + start_cursor: ReactPropTypes.string, + end_cursor: ReactPropTypes.string, + }).isRequired, +}); + +class CameraRoll { + /** + * Saves the image with tag `tag` to the camera roll. + * + * @param {string} tag - Can be any of the three kinds of tags we accept: + * 1. URL + * 2. assets-library tag + * 3. tag returned from storing an image in memory + */ + static saveImageWithTag(tag, successCallback, errorCallback) { + invariant( + typeof tag === 'string', + 'CameraRoll.saveImageWithTag tag must be a valid string.' + ); + RCTCameraRollManager.saveImageWithTag( + tag, + (imageTag) => { + successCallback && successCallback(imageTag); + }, + (errorMessage) => { + errorCallback && errorCallback(errorMessage); + }); + } + + /** + * Invokes `callback` with photo identifier objects from the local camera + * roll of the device matching shape defined by `getPhotosReturnChecker`. + * + * @param {object} params - See `getPhotosParamChecker`. + * @param {function} callback - Invoked with arg of shape defined by + * `getPhotosReturnChecker` on success. + * @param {function} errorCallback - Invoked with error message on error. + */ + static getPhotos(params, callback, errorCallback) { + var metaCallback = callback; + if (__DEV__) { + getPhotosParamChecker({params}, 'params', 'CameraRoll.getPhotos'); + invariant( + typeof callback === 'function', + 'CameraRoll.getPhotos callback must be a valid function.' + ); + invariant( + typeof errorCallback === 'function', + 'CameraRoll.getPhotos errorCallback must be a valid function.' + ); + } + if (__DEV__) { + metaCallback = (response) => { + getPhotosReturnChecker( + {response}, + 'response', + 'CameraRoll.getPhotos callback' + ); + callback(response); + }; + } + RCTCameraRollManager.getPhotos(params, metaCallback, errorCallback); + } +} + +CameraRoll.GroupTypesOptions = GROUP_TYPES_OPTIONS; + +module.exports = CameraRoll; diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js index a281cf97a..8c09ac551 100644 --- a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js +++ b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js @@ -6,7 +6,7 @@ 'use strict'; var NativeMethodsMixin = require('NativeMethodsMixin'); -var NativeModulesDeprecated = require('NativeModulesDeprecated'); +var NativeModules = require('NativeModules'); var PropTypes = require('ReactPropTypes'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); @@ -15,7 +15,6 @@ var View = require('View'); var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); var keyMirror = require('keyMirror'); -var keyOf = require('keyOf'); var merge = require('merge'); var SpinnerSize = keyMirror({ @@ -37,12 +36,14 @@ var ActivityIndicatorIOS = React.createClass({ * The foreground color of the spinner (default is gray). */ color: PropTypes.string, + /** - * The size of the spinner, must be one of: - * - ActivityIndicatorIOS.size.large - * - ActivityIndicatorIOS.size.small (default) + * Size of the indicator. Small has a height of 20, large has a height of 36. */ - size: PropTypes.oneOf([SpinnerSize.large, SpinnerSize.small]), + size: PropTypes.oneOf([ + 'small', + 'large', + ]), }, getDefaultProps: function() { @@ -53,15 +54,11 @@ var ActivityIndicatorIOS = React.createClass({ }; }, - statics: { - size: SpinnerSize, - }, - render: function() { var style = styles.sizeSmall; - var NativeConstants = NativeModulesDeprecated.RKUIManager.UIActivityIndicatorView.Constants; + var NativeConstants = NativeModules.UIManager.UIActivityIndicatorView.Constants; var activityIndicatorViewStyle = NativeConstants.StyleWhite; - if (this.props.size == SpinnerSize.large) { + if (this.props.size === 'large') { style = styles.sizeLarge; activityIndicatorViewStyle = NativeConstants.StyleWhiteLarge; } diff --git a/Libraries/Components/DatePicker/DatePickerIOS.ios.js b/Libraries/Components/DatePicker/DatePickerIOS.ios.js new file mode 100644 index 000000000..a010870b1 --- /dev/null +++ b/Libraries/Components/DatePicker/DatePickerIOS.ios.js @@ -0,0 +1,153 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule DatePickerIOS + * + * This is a controlled component version of RCTDatePickerIOS + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var RCTDatePickerIOSConsts = require('NativeModules').UIManager.RCTDatePicker.Constants; +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactIOSNativeComponentClass = + require('createReactIOSNativeComponentClass'); +var merge = require('merge'); + +var DATEPICKER = 'datepicker'; + +/** + * Use `DatePickerIOS` to render a date/time picker (selector) on iOS. This is + * a controlled component, so you must hook in to the `onDateChange` callback + * and update the `date` prop in order for the component to update, otherwise + * the user's change will be reverted immediately to reflect `props.date` as the + * source of truth. + */ +var DatePickerIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * The currently selected date. + */ + date: PropTypes.instanceOf(Date).isRequired, + + /** + * Date change handler. + * + * This is called when the user changes the date or time in the UI. + * The first and only argument is a Date object representing the new + * date and time. + */ + onDateChange: PropTypes.func.isRequired, + + /** + * Maximum date. + * + * Restricts the range of possible date/time values. + */ + maximumDate: PropTypes.instanceOf(Date), + + /** + * Minimum date. + * + * Restricts the range of possible date/time values. + */ + minimumDate: PropTypes.instanceOf(Date), + + /** + * The date picker mode. + */ + mode: PropTypes.oneOf(['date', 'time', 'datetime']), + + /** + * The interval at which minutes can be selected. + */ + minuteInterval: PropTypes.oneOf([1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30]), + + /** + * Timezone offset in minutes. + * + * By default, the date picker will use the device's timezone. With this + * parameter, it is possible to force a certain timezone offset. For + * instance, to show times in Pacific Standard Time, pass -7 * 60. + */ + timeZoneOffsetInMinutes: PropTypes.number, + }, + + getDefaultProps: function() { + return { + mode: 'datetime', + }; + }, + + _onChange: function(event) { + var nativeTimeStamp = event.nativeEvent.timestamp; + this.props.onDateChange && this.props.onDateChange( + new Date(nativeTimeStamp) + ); + this.props.onChange && this.props.onChange(event); + + // We expect the onChange* handlers to be in charge of updating our `date` + // prop. That way they can also disallow/undo/mutate the selection of + // certain values. In other words, the embedder of this component should + // be the source of truth, not the native component. + var propsTimeStamp = this.props.date.getTime(); + if (nativeTimeStamp !== propsTimeStamp) { + this.refs[DATEPICKER].setNativeProps({ + date: propsTimeStamp, + }); + } + }, + + render: function() { + var props = this.props; + return ( + + + + ); + } +}); + +var styles = StyleSheet.create({ + rkDatePickerIOS: { + height: RCTDatePickerIOSConsts.ComponentHeight, + width: RCTDatePickerIOSConsts.ComponentWidth, + }, +}); + +var rkDatePickerIOSAttributes = merge(ReactIOSViewAttributes.UIView, { + date: true, + maximumDate: true, + minimumDate: true, + mode: true, + minuteInterval: true, + timeZoneOffsetInMinutes: true, +}); + +var RCTDatePickerIOS = createReactIOSNativeComponentClass({ + validAttributes: rkDatePickerIOSAttributes, + uiViewClassName: 'RCTDatePicker', +}); + +module.exports = DatePickerIOS; diff --git a/Libraries/Components/Image/ImageSourcePropType.js b/Libraries/Components/Image/ImageSourcePropType.js deleted file mode 100644 index 84eaf6629..000000000 --- a/Libraries/Components/Image/ImageSourcePropType.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule ImageSourcePropType - * @flow - */ -'use strict'; - -var { PropTypes } = require('React'); - -var ImageSourcePropType = PropTypes.shape({ - /** - * uri - A string representing the resource identifier for the image, which - * could be an http address, a local file path, or the name of a static image - * resource (which should be wrapped in the `ix` function). - */ - uri: PropTypes.string.isRequired, - /** - * width/height - Used to store the size of the image itself, but unused by - * the component - use normal style layout properties to define the - * size of the frame. - */ - width: PropTypes.number, - height: PropTypes.number, -}); - -module.exports = ImageSourcePropType; diff --git a/Libraries/Components/Image/ix.js b/Libraries/Components/Image/ix.js deleted file mode 100644 index 52cbbc4e2..000000000 --- a/Libraries/Components/Image/ix.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule ix - */ -'use strict'; - -/** - * This function is used to mark string literals that are image paths. The - * return value is a blob of data that core image components understand how to - * render. - * - * The arguments to ix() must be string literals so that they can be parsed - * statically. - * - * @param string Image path to render - * @return object Data blob to be used by core UI components - */ -function ix(path) { - return { - uri: path, - isStatic: true, - }; -} - -module.exports = ix; diff --git a/Libraries/Components/MapView/MapView.js b/Libraries/Components/MapView/MapView.js new file mode 100644 index 000000000..77eaf3de1 --- /dev/null +++ b/Libraries/Components/MapView/MapView.js @@ -0,0 +1,165 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule MapView + */ +'use strict'; + +var EdgeInsetsPropType = require('EdgeInsetsPropType'); +var NativeMethodsMixin = require('NativeMethodsMixin'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var View = require('View'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var deepDiffer = require('deepDiffer'); +var insetsDiffer = require('insetsDiffer'); +var merge = require('merge'); + +var MapView = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * Used to style and layout the `MapView`. See `StyleSheet.js` and + * `ViewStylePropTypes.js` for more info. + */ + style: View.propTypes.style, + + /** + * If `true` the app will ask for the user's location and focus on it. + * Default value is `false`. + * + * **NOTE**: You need to add NSLocationWhenInUseUsageDescription key in + * Info.plist to enable geolocation, otherwise it is going + * to *fail silently*! + */ + showsUserLocation: React.PropTypes.bool, + + /** + * If `false` the user won't be able to pinch/zoom the map. + * Default `value` is true. + */ + zoomEnabled: React.PropTypes.bool, + + /** + * When this property is set to `true` and a valid camera is associated with + * the map, the camera’s heading angle is used to rotate the plane of the + * map around its center point. When this property is set to `false`, the + * camera’s heading angle is ignored and the map is always oriented so + * that true north is situated at the top of the map view + */ + rotateEnabled: React.PropTypes.bool, + + /** + * When this property is set to `true` and a valid camera is associated + * with the map, the camera’s pitch angle is used to tilt the plane + * of the map. When this property is set to `false`, the camera’s pitch + * angle is ignored and the map is always displayed as if the user + * is looking straight down onto it. + */ + pitchEnabled: React.PropTypes.bool, + + /** + * If `false` the user won't be able to change the map region being displayed. + * Default value is `true`. + */ + scrollEnabled: React.PropTypes.bool, + + /** + * The region to be displayed by the map. + * + * The region is defined by the center coordinates and the span of + * coordinates to display. + */ + region: React.PropTypes.shape({ + /** + * Coordinates for the center of the map. + */ + latitude: React.PropTypes.number.isRequired, + longitude: React.PropTypes.number.isRequired, + + /** + * Distance between the minimun and the maximum latitude/longitude + * to be displayed. + */ + latitudeDelta: React.PropTypes.number.isRequired, + longitudeDelta: React.PropTypes.number.isRequired, + }), + + /** + * Maximum size of area that can be displayed. + */ + maxDelta: React.PropTypes.number, + + /** + * Minimum size of area that can be displayed. + */ + minDelta: React.PropTypes.number, + + /** + * Insets for the map's legal label, originally at bottom left of the map. + * See `EdgeInsetsPropType.js` for more information. + */ + legalLabelInsets: EdgeInsetsPropType, + + /** + * Callback that is called continuously when the user is dragging the map. + */ + onRegionChange: React.PropTypes.func, + + /** + * Callback that is called once, when the user is done moving the map. + */ + onRegionChangeComplete: React.PropTypes.func, + }, + + _onChange: function(event) { + if (event.nativeEvent.continuous) { + this.props.onRegionChange && + this.props.onRegionChange(event.nativeEvent.region); + } else { + this.props.onRegionChangeComplete && + this.props.onRegionChangeComplete(event.nativeEvent.region); + } + }, + + render: function() { + return ( + + ); + }, + +}); + +var RCTMap = createReactIOSNativeComponentClass({ + validAttributes: merge( + ReactIOSViewAttributes.UIView, { + showsUserLocation: true, + zoomEnabled: true, + rotateEnabled: true, + pitchEnabled: true, + scrollEnabled: true, + region: {diff: deepDiffer}, + maxDelta: true, + minDelta: true, + legalLabelInsets: {diff: insetsDiffer}, + } + ), + uiViewClassName: 'RCTMap', +}); + +module.exports = MapView; diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index efd983203..30082cf21 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -8,7 +8,7 @@ var EventEmitter = require('EventEmitter'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); -var { RKUIManager } = require('NativeModules'); +var RCTNavigatorManager = require('NativeModules').NavigatorManager; var StyleSheet = require('StyleSheet'); var StaticContainer = require('StaticContainer.react'); var View = require('View'); @@ -19,13 +19,72 @@ var invariant = require('invariant'); var logError = require('logError'); var merge = require('merge'); +var TRANSITIONER_REF = 'transitionerRef'; + +var PropTypes = React.PropTypes; + +var __uid = 0; +function getuid() { + return __uid++; +} + +var RCTNavigator = createReactIOSNativeComponentClass({ + validAttributes: merge(ReactIOSViewAttributes.UIView, { + requestedTopOfStack: true + }), + uiViewClassName: 'RCTNavigator', +}); + +var RCTNavigatorItem = createReactIOSNativeComponentClass({ + validAttributes: { + // TODO: Remove or fix the attributes that are not fully functional. + // NavigatorIOS does not use them all, because some are problematic + title: true, + barTintColor: true, + rightButtonTitle: true, + onNavRightButtonTap: true, + tintColor: true, + backButtonTitle: true, + titleTextColor: true, + style: true, + }, + uiViewClassName: 'RCTNavItem', +}); + +var NavigatorTransitionerIOS = React.createClass({ + requestSchedulingNavigation: function(cb) { + RCTNavigatorManager.requestSchedulingJavaScriptNavigation( + this.getNodeHandle(), + logError, + cb + ); + }, + + render: function() { + return ( + + ); + }, +}); + + +/** + * Think of `` as simply a component that renders an + * `RCTNavigator`, and moves the `RCTNavigator`'s `requestedTopOfStack` pointer + * forward and backward. The `RCTNavigator` interprets changes in + * `requestedTopOfStack` to be pushes and pops of children that are rendered. + * `` always ensures that whenever the `requestedTopOfStack` + * pointer is moved, that we've also rendered enough children so that the + * `RCTNavigator` can carry out the push/pop with those children. + * `` also removes children that will no longer be needed + * (after the pop of a child has been fully completed/animated out). + */ + /** * NavigatorIOS wraps UIKit navigation and allows you to add back-swipe * functionality across your app. * - * See UIExplorerApp and NavigatorIOSExample for a full example - * - * ======================= NavigatorIOS Routes ================================ + * #### Routes * A route is an object used to describe each page in the navigator. The first * route is provided to NavigatorIOS as `initialRoute`: * @@ -37,7 +96,8 @@ var merge = require('merge'); * component: MyView, * title: 'My View Title', * passProps: { myProp: 'foo' }, - * }}/> + * }} + * /> * ); * }, * ``` @@ -48,7 +108,8 @@ var merge = require('merge'); * * See the initialRoute propType for a complete definition of a route. * - * ====================== NavigatorIOS Navigator ============================== + * #### Navigator + * * A `navigator` is an object of navigation functions that a view can call. It * is passed as a prop to any component rendered by NavigatorIOS. * @@ -65,6 +126,7 @@ var merge = require('merge'); * ``` * * A navigation object contains the following functions: + * * - `push(route)` - Navigate forward to a new route * - `pop()` - Go back one page * - `popN(n)` - Go back N pages at once. When N=1, behavior matches `pop()` @@ -86,73 +148,14 @@ var merge = require('merge'); * }, * render: () => ( * + * ref="nav" + * initialRoute={...} + * /> * ), * }); * ``` * */ -var TRANSITIONER_REF = 'transitionerRef'; - -var PropTypes = React.PropTypes; - -var __uid = 0; -function getuid() { - return __uid++; -} - -var RKNavigator = createReactIOSNativeComponentClass({ - validAttributes: merge(ReactIOSViewAttributes.UIView, { - requestedTopOfStack: true - }), - uiViewClassName: 'RCTNavigator', -}); - -var RKNavigatorItem = createReactIOSNativeComponentClass({ - validAttributes: { - // TODO: Remove or fix the attributes that are not fully functional. - // NavigatorIOS does not use them all, because some are problematic - title: true, - barTintColor: true, - rightButtonTitle: true, - onNavRightButtonTap: true, - tintColor: true, - backButtonTitle: true, - titleTextColor: true, - style: true, - }, - uiViewClassName: 'RCTNavItem', -}); - -var NavigatorTransitionerIOS = React.createClass({ - requestSchedulingNavigation: function(cb) { - RKUIManager.requestSchedulingJavaScriptNavigation( - this.getNodeHandle(), - logError, - cb - ); - }, - - render: function() { - return ( - - ); - }, -}); - - -/** - * Think of `` as simply a component that renders an - * `RKNavigator`, and moves the `RKNavigator`'s `requestedTopOfStack` pointer - * forward and backward. The `RKNavigator` interprets changes in - * `requestedTopOfStack` to be pushes and pops of children that are rendered. - * `` always ensures that whenever the `requestedTopOfStack` - * pointer is moved, that we've also rendered enough children so that the - * `RKNavigator` can carry out the push/pop with those children. - * `` also removes children that will no longer be needed - * (after the pop of a child has been fully completed/animated out). - */ var NavigatorIOS = React.createClass({ propTypes: { @@ -199,7 +202,7 @@ var NavigatorIOS = React.createClass({ /** * Styles for the navigation item containing the component */ - wrapperStyle: View.stylePropType, + wrapperStyle: View.propTypes.style, }).isRequired, @@ -207,7 +210,7 @@ var NavigatorIOS = React.createClass({ * The default wrapper style for components in the navigator. * A common use case is to set the backgroundColor for every page */ - itemWrapperStyle: View.stylePropType, + itemWrapperStyle: View.propTypes.style, /** * The color used for buttons in the navigation bar @@ -493,7 +496,7 @@ var NavigatorIOS = React.createClass({ return ( - - + ); }, diff --git a/Libraries/Components/ScrollResponder.js b/Libraries/Components/ScrollResponder.js index 4221722bd..b6e55e078 100644 --- a/Libraries/Components/ScrollResponder.js +++ b/Libraries/Components/ScrollResponder.js @@ -6,14 +6,14 @@ 'use strict'; var NativeModules = require('NativeModules'); -var NativeModulesDeprecated = require('NativeModulesDeprecated'); +var NativeModules = require('NativeModules'); var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); var Subscribable = require('Subscribable'); var TextInputState = require('TextInputState'); -var RKUIManager = NativeModules.RKUIManager; -var RKUIManagerDeprecated = NativeModulesDeprecated.RKUIManager; -var RKScrollViewConsts = RKUIManager.RCTScrollView.Constants; +var RCTUIManager = NativeModules.UIManager; +var RCTUIManagerDeprecated = NativeModules.UIManager; +var RCTScrollViewConsts = RCTUIManager.RCTScrollView.Constants; var warning = require('warning'); @@ -99,7 +99,7 @@ var IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16; var ScrollResponderMixin = { mixins: [Subscribable.Mixin], - statics: RKScrollViewConsts, + statics: RCTScrollViewConsts, scrollResponderMixinGetInitialState: function() { return { isTouching: false, @@ -336,7 +336,7 @@ var ScrollResponderMixin = { * can also be used to quickly scroll to any element we want to focus */ scrollResponderScrollTo: function(offsetX, offsetY) { - RKUIManagerDeprecated.scrollTo(this.getNodeHandle(), offsetX, offsetY); + RCTUIManagerDeprecated.scrollTo(this.getNodeHandle(), offsetX, offsetY); }, /** @@ -344,7 +344,7 @@ var ScrollResponderMixin = { * @param {object} rect Should have shape {x, y, w, h} */ scrollResponderZoomTo: function(rect) { - RKUIManagerDeprecated.zoomToRect(this.getNodeHandle(), rect); + RCTUIManagerDeprecated.zoomToRect(this.getNodeHandle(), rect); }, /** @@ -354,7 +354,7 @@ var ScrollResponderMixin = { */ scrollResponderScrollNativeHandleToKeyboard: function(nodeHandle, additionalOffset) { this.additionalScrollOffset = additionalOffset || 0; - RKUIManager.measureLayout( + RCTUIManager.measureLayout( nodeHandle, this.getNodeHandle(), this.scrollResponderTextInputFocusError, diff --git a/Libraries/Components/ScrollView/ScrollView.ios.js b/Libraries/Components/ScrollView/ScrollView.js similarity index 54% rename from Libraries/Components/ScrollView/ScrollView.ios.js rename to Libraries/Components/ScrollView/ScrollView.js index 3e425162a..a069988ce 100644 --- a/Libraries/Components/ScrollView/ScrollView.ios.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -5,62 +5,80 @@ */ 'use strict'; -var ArrayOfPropType = require('ArrayOfPropType'); +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 ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var ReactIOSTagHandles = require('ReactIOSTagHandles'); -var RKScrollView = require('NativeModules').RKUIManager.RCTScrollView; -var RKScrollViewConsts = RKScrollView.Constants; -var RKUIManager = require('NativeModulesDeprecated').RKUIManager; +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var RCTUIManager = require('NativeModules').UIManager; var ScrollResponder = require('ScrollResponder'); -var ScrollViewPropTypes = require('ScrollViewPropTypes'); -var StyleSheetPropType = require('StyleSheetPropType'); var StyleSheet = require('StyleSheet'); +var StyleSheetPropType = require('StyleSheetPropType'); var View = require('View'); var ViewStylePropTypes = require('ViewStylePropTypes'); var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var deepDiffer = require('deepDiffer'); var flattenStyle = require('flattenStyle'); +var insetsDiffer = require('insetsDiffer'); var invariant = require('invariant'); -var merge = require('merge'); -var nativePropType = require('nativePropType'); -var validAttributesFromPropTypes = require('validAttributesFromPropTypes'); +var pointsDiffer = require('pointsDiffer'); var PropTypes = React.PropTypes; var SCROLLVIEW = 'ScrollView'; var INNERVIEW = 'InnerScrollView'; +var keyboardDismissModeConstants = { + 'none': RCTScrollViewConsts.KeyboardDismissMode.None, // default + 'interactive': RCTScrollViewConsts.KeyboardDismissMode.Interactive, + 'onDrag': RCTScrollViewConsts.KeyboardDismissMode.OnDrag, +}; + /** - * `React` component that wraps platform `RKScrollView` while providing + * Component that wraps platform ScrollView while providing * integration with touch locking "responder" system. * * Doesn't yet support other contained responders from blocking this scroll * view from becoming the responder. */ - var RKScrollViewPropTypes = merge( - ScrollViewPropTypes, - { +var ScrollView = React.createClass({ + propTypes: { + automaticallyAdjustContentInsets: PropTypes.bool, // true + contentInset: EdgeInsetsPropType, // zeros + contentOffset: PointPropType, // zeros + onScroll: PropTypes.func, + onScrollAnimationEnd: PropTypes.func, + scrollEnabled: PropTypes.bool, // tre + scrollIndicatorInsets: EdgeInsetsPropType, // zeros + showsHorizontalScrollIndicator: PropTypes.bool, + showsVerticalScrollIndicator: PropTypes.bool, + style: StyleSheetPropType(ViewStylePropTypes), + throttleScrollCallbackMS: PropTypes.number, // null + /** * When true, the scroll view bounces horizontally when it reaches the end * even if the content is smaller than the scroll view itself. The default * value is true when `horizontal={true}` and false otherwise. */ - alwaysBounceHorizontal: nativePropType(PropTypes.bool), + alwaysBounceHorizontal: PropTypes.bool, /** * When true, the scroll view bounces vertically when it reaches the end * even if the content is smaller than the scroll view itself. The default * value is false when `horizontal={true}` and true otherwise. */ - alwaysBounceVertical: nativePropType(PropTypes.bool), + alwaysBounceVertical: PropTypes.bool, /** * When true, the scroll view automatically centers the content when the * content is smaller than the scroll view bounds; when the content is * larger than the scroll view, this property has no effect. The default * value is false. */ - centerContent: nativePropType(PropTypes.bool), + centerContent: PropTypes.bool, /** * These styles will be applied to the scroll view content container which * wraps all of the child views. Example: @@ -80,10 +98,10 @@ var INNERVIEW = 'InnerScrollView'; /** * A floating-point number that determines how quickly the scroll view * decelerates after the user lifts their finger. Reasonable choices include - * `RKScrollView.Constants.DecelerationRate.Normal` (the default) and - * `RKScrollView.Constants.DecelerationRate.Fast`. + * - Normal: 0.998 (the default) + * - Fast: 0.9 */ - decelerationRate: nativePropType(PropTypes.number), + decelerationRate: PropTypes.number, /** * When true, the scroll view's children are arranged horizontally in a row * instead of vertically in a column. The default value is false. @@ -91,44 +109,43 @@ var INNERVIEW = 'InnerScrollView'; horizontal: PropTypes.bool, /** * Determines whether the keyboard gets dismissed in response to a drag. - * When `ScrollView.keyboardDismissMode.None` (the default), drags do not - * dismiss the keyboard. When `ScrollView.keyboardDismissMode.OnDrag`, the - * keyboard is dismissed when a drag begins. When - * `ScrollView.keyboardDismissMode.Interactive`, the keyboard is dismissed - * interactively with the drag and moves in synchrony with the touch; - * dragging upwards cancels the dismissal. + * - 'none' (the default), drags do not dismiss the keyboard. + * - 'onDrag', 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. */ - keyboardDismissMode: nativePropType(PropTypes.oneOf([ - RKScrollViewConsts.KeyboardDismissMode.None, // default - RKScrollViewConsts.KeyboardDismissMode.Interactive, - RKScrollViewConsts.KeyboardDismissMode.OnDrag, - ])), + keyboardDismissMode: PropTypes.oneOf([ + 'none', // default + 'interactive', + 'onDrag', + ]), /** * When false, tapping outside of the focused text input when the keyboard * 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. */ - keyboardShouldPersistTaps: nativePropType(PropTypes.bool), + keyboardShouldPersistTaps: PropTypes.bool, /** * The maximum allowed zoom scale. The default value is 1.0. */ - maximumZoomScale: nativePropType(PropTypes.number), + maximumZoomScale: PropTypes.number, /** * The minimum allowed zoom scale. The default value is 1.0. */ - minimumZoomScale: nativePropType(PropTypes.number), + minimumZoomScale: PropTypes.number, /** * When true, the scroll view stops on multiples of the scroll view's size * when scrolling. This can be used for horizontal pagination. The default * value is false. */ - pagingEnabled: nativePropType(PropTypes.bool), + pagingEnabled: PropTypes.bool, /** * When true, the scroll view scrolls to top when the status bar is tapped. * The default value is true. */ - scrollsToTop: nativePropType(PropTypes.bool), + scrollsToTop: PropTypes.bool, /** * An array of child indices determining which children get docked to the * top of the screen when scrolling. For example, passing @@ -136,7 +153,7 @@ var INNERVIEW = 'InnerScrollView'; * top of the scroll view. This property is not supported in conjunction * with `horizontal={true}`. */ - stickyHeaderIndices: nativePropType(ArrayOfPropType(PropTypes.number)), + stickyHeaderIndices: PropTypes.arrayOf(PropTypes.number), /** * Experimental: When true, offscreen child views (whose `overflow` value is * `hidden`) are removed from their native backing superview when offscreen. @@ -147,18 +164,9 @@ var INNERVIEW = 'InnerScrollView'; /** * The current scale of the scroll view content. The default value is 1.0. */ - zoomScale: nativePropType(PropTypes.number), - } -); - -var ScrollView = React.createClass({ - statics: { - PropTypes: RKScrollViewPropTypes, - keyboardDismissMode: RKScrollViewConsts.KeyboardDismissMode, + zoomScale: PropTypes.number, }, - propTypes: RKScrollViewPropTypes, - mixins: [ScrollResponder.Mixin], getInitialState: function() { @@ -174,7 +182,11 @@ var ScrollView = React.createClass({ }, scrollTo: function(destY, destX) { - RKUIManager.scrollTo(ReactIOSTagHandles.rootNodeIDToTag[this._rootNodeID], destX, destY); + RCTUIManager.scrollTo( + this.getNodeHandle(), + destX || 0, + destY || 0 + ); }, render: function() { @@ -226,33 +238,47 @@ var ScrollView = React.createClass({ this.props.alwaysBounceVertical : !this.props.horizontal; - var props = merge( - this.props, { - alwaysBounceHorizontal, - alwaysBounceVertical, - style: [styles.base, this.props.style], - onTouchStart: this.scrollResponderHandleTouchStart, - onTouchMove: this.scrollResponderHandleTouchMove, - onTouchEnd: this.scrollResponderHandleTouchEnd, - onScrollBeginDrag: this.scrollResponderHandleScrollBeginDrag, - onScrollEndDrag: this.scrollResponderHandleScrollEndDrag, - onMomentumScrollBegin: this.scrollResponderHandleMomentumScrollBegin, - onMomentumScrollEnd: this.scrollResponderHandleMomentumScrollEnd, - onStartShouldSetResponder: this.scrollResponderHandleStartShouldSetResponder, - onStartShouldSetResponderCapture: this.scrollResponderHandleStartShouldSetResponderCapture, - onScrollShouldSetResponder: this.scrollResponderHandleScrollShouldSetResponder, - onScroll: this.scrollResponderHandleScroll, - onResponderGrant: this.scrollResponderHandleResponderGrant, - onResponderTerminationRequest: this.scrollResponderHandleTerminationRequest, - onResponderTerminate: this.scrollResponderHandleTerminate, - onResponderRelease: this.scrollResponderHandleResponderRelease, - onResponderReject: this.scrollResponderHandleResponderReject, + var props = { + ...this.props, + alwaysBounceHorizontal, + alwaysBounceVertical, + keyboardDismissMode: this.props.keyboardDismissMode ? + keyboardDismissModeConstants[this.props.keyboardDismissMode] : + undefined, + style: [styles.base, this.props.style], + onTouchStart: this.scrollResponderHandleTouchStart, + onTouchMove: this.scrollResponderHandleTouchMove, + onTouchEnd: this.scrollResponderHandleTouchEnd, + onScrollBeginDrag: this.scrollResponderHandleScrollBeginDrag, + onScrollEndDrag: this.scrollResponderHandleScrollEndDrag, + onMomentumScrollBegin: this.scrollResponderHandleMomentumScrollBegin, + onMomentumScrollEnd: this.scrollResponderHandleMomentumScrollEnd, + onStartShouldSetResponder: this.scrollResponderHandleStartShouldSetResponder, + onStartShouldSetResponderCapture: this.scrollResponderHandleStartShouldSetResponderCapture, + onScrollShouldSetResponder: this.scrollResponderHandleScrollShouldSetResponder, + onScroll: this.scrollResponderHandleScroll, + onResponderGrant: this.scrollResponderHandleResponderGrant, + onResponderTerminationRequest: this.scrollResponderHandleTerminationRequest, + onResponderTerminate: this.scrollResponderHandleTerminate, + onResponderRelease: this.scrollResponderHandleResponderRelease, + onResponderReject: this.scrollResponderHandleResponderReject, + }; + + var ScrollViewClass; + if (Platform.OS === 'ios') { + ScrollViewClass = RCTScrollView; + } else if (Platform.OS === 'android') { + if (this.props.horizontal) { + ScrollViewClass = AndroidHorizontalScrollView; + } else { + ScrollViewClass = AndroidScrollView; } - ); + } + return ( - + {contentContainer} - + ); } }); @@ -267,12 +293,46 @@ var styles = StyleSheet.create({ }, }); -var RKScrollView = createReactIOSNativeComponentClass({ - validAttributes: merge( - ReactIOSViewAttributes.UIView, - validAttributesFromPropTypes(ScrollView.propTypes) - ), - uiViewClassName: 'RCTScrollView', -}); +var validAttributes = { + ...ReactIOSViewAttributes.UIView, + alwaysBounceHorizontal: true, + alwaysBounceVertical: true, + automaticallyAdjustContentInsets: true, + centerContent: true, + contentInset: {diff: insetsDiffer}, + contentOffset: {diff: pointsDiffer}, + decelerationRate: true, + horizontal: true, + keyboardDismissMode: true, + keyboardShouldPersistTaps: true, + maximumZoomScale: true, + minimumZoomScale: true, + pagingEnabled: true, + removeClippedSubviews: true, + scrollEnabled: true, + scrollIndicatorInsets: {diff: insetsDiffer}, + scrollsToTop: true, + showsHorizontalScrollIndicator: true, + showsVerticalScrollIndicator: true, + stickyHeaderIndices: {diff: deepDiffer}, + throttleScrollCallbackMS: true, + zoomScale: true, +}; + +if (Platform.OS === 'android') { + var AndroidScrollView = createReactIOSNativeComponentClass({ + validAttributes: validAttributes, + uiViewClassName: 'AndroidScrollView', + }); + var AndroidHorizontalScrollView = createReactIOSNativeComponentClass({ + validAttributes: validAttributes, + uiViewClassName: 'AndroidHorizontalScrollView', + }); +} else if (Platform.OS === 'ios') { + var RCTScrollView = createReactIOSNativeComponentClass({ + validAttributes: validAttributes, + uiViewClassName: 'RCTScrollView', + }); +} module.exports = ScrollView; diff --git a/Libraries/Components/ScrollViewPropTypes.js b/Libraries/Components/ScrollViewPropTypes.js deleted file mode 100644 index 12af36528..000000000 --- a/Libraries/Components/ScrollViewPropTypes.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule ScrollViewPropTypes - */ -'use strict'; - -var EdgeInsetsPropType = require('EdgeInsetsPropType'); -var PointPropType = require('PointPropType'); -var PropTypes = require('ReactPropTypes'); -var StyleSheetPropType = require('StyleSheetPropType'); -var ViewStylePropTypes = require('ViewStylePropTypes'); - -var nativePropType = require('nativePropType'); - -var ScrollViewPropTypes = { - automaticallyAdjustContentInsets: nativePropType(PropTypes.bool), // true - contentInset: nativePropType(EdgeInsetsPropType), // zeroes - contentOffset: nativePropType(PointPropType), // zeroes - onScroll: PropTypes.func, - onScrollAnimationEnd: PropTypes.func, - scrollEnabled: nativePropType(PropTypes.bool), // true - scrollIndicatorInsets: nativePropType(EdgeInsetsPropType), // zeros - showsHorizontalScrollIndicator: nativePropType(PropTypes.bool), - showsVerticalScrollIndicator: nativePropType(PropTypes.bool), - style: StyleSheetPropType(ViewStylePropTypes), - throttleScrollCallbackMS: nativePropType(PropTypes.number), // 200ms -}; - -module.exports = ScrollViewPropTypes; diff --git a/Libraries/Components/SliderIOS/SliderIOS.js b/Libraries/Components/SliderIOS/SliderIOS.js new file mode 100644 index 000000000..2e5f14bc1 --- /dev/null +++ b/Libraries/Components/SliderIOS/SliderIOS.js @@ -0,0 +1,83 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule SliderIOS + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactIOSNativeComponentClass = + require('createReactIOSNativeComponentClass'); +var merge = require('merge'); + +var SliderIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * Used to style and layout the `Slider`. See `StyleSheet.js` and + * `ViewStylePropTypes.js` for more info. + */ + style: View.propTypes.style, + + /** + * Initial value of the slider. The value should be between 0 and 1. + * Default value is 0. + * + * *This is not a controlled component*, e.g. if you don't update + * the value, the component won't be reseted to it's inital value. + */ + value: PropTypes.number, + + /** + * Callback continuously called while the user is dragging the slider. + */ + onValueChange: PropTypes.func, + + /** + * Callback called when the user finishes changing the value (e.g. when + * the slider is released). + */ + onSlidingComplete: PropTypes.func, + }, + + _onValueChange: function(event) { + this.props.onChange && this.props.onChange(event); + if (event.nativeEvent.continuous) { + this.props.onValueChange && + this.props.onValueChange(event.nativeEvent.value); + } else { + this.props.onSlidingComplete && event.nativeEvent.value !== undefined && + this.props.onSlidingComplete(event.nativeEvent.value); + } + }, + + render: function() { + return ( + + ); + } +}); + +var styles = StyleSheet.create({ + slider: { + height: 40, + }, +}); + +var RCTSlider = createReactIOSNativeComponentClass({ + validAttributes: merge(ReactIOSViewAttributes.UIView, {value: true}), + uiViewClassName: 'RCTSlider', +}); + +module.exports = SliderIOS; diff --git a/Libraries/Components/StatusBar/StatusBarIOS.ios.js b/Libraries/Components/StatusBar/StatusBarIOS.ios.js index eaea1cc7a..55cb1608c 100644 --- a/Libraries/Components/StatusBar/StatusBarIOS.ios.js +++ b/Libraries/Components/StatusBar/StatusBarIOS.ios.js @@ -6,29 +6,29 @@ */ 'use strict'; -var { RKStatusBarManager } = require('NativeModules'); +var RCTStatusBarManager = require('NativeModules').StatusBarManager; var StatusBarIOS = { Style: { - default: RKStatusBarManager.Style.default, - lightContent: RKStatusBarManager.Style.lightContent + default: RCTStatusBarManager.Style.default, + lightContent: RCTStatusBarManager.Style.lightContent }, Animation: { - none: RKStatusBarManager.Animation.none, - fade: RKStatusBarManager.Animation.fade, - slide: RKStatusBarManager.Animation.slide, + none: RCTStatusBarManager.Animation.none, + fade: RCTStatusBarManager.Animation.fade, + slide: RCTStatusBarManager.Animation.slide, }, setStyle(style: number, animated: boolean) { animated = animated || false; - RKStatusBarManager.setStyle(style, animated); + RCTStatusBarManager.setStyle(style, animated); }, setHidden(hidden: boolean, animation: number) { animation = animation || StatusBarIOS.Animation.none; - RKStatusBarManager.setHidden(hidden, animation); + RCTStatusBarManager.setHidden(hidden, animation); }, }; diff --git a/Libraries/Components/Subscribable.js b/Libraries/Components/Subscribable.js index 8c4bbe823..7b1018f9e 100644 --- a/Libraries/Components/Subscribable.js +++ b/Libraries/Components/Subscribable.js @@ -29,7 +29,7 @@ * RCTDeviceEventEmitter, * 'reachabilityDidChange', * (resp) => resp.network_reachability, - * RKReachability.getCurrentReachability + * RCTReachability.getCurrentReachability * ); * * var myComponent = React.createClass({ diff --git a/Libraries/Components/SwitchIOS/SwitchIOS.android.js b/Libraries/Components/SwitchIOS/SwitchIOS.android.js new file mode 100644 index 000000000..72c24fa28 --- /dev/null +++ b/Libraries/Components/SwitchIOS/SwitchIOS.android.js @@ -0,0 +1,41 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule SwitchIOS + */ + +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); + +var DummySwitchIOS = React.createClass({ + render: function() { + return ( + + SwitchIOS is not supported on this platform! + + ); + }, +}); + +var styles = StyleSheet.create({ + dummySwitchIOS: { + width: 120, + height: 50, + backgroundColor: '#ffbcbc', + borderWidth: 1, + borderColor: 'red', + alignItems: 'center', + justifyContent: 'center', + }, + text: { + color: '#333333', + margin: 5, + fontSize: 10, + } +}); + +module.exports = DummySwitchIOS; diff --git a/Libraries/Components/SwitchIOS/SwitchIOS.ios.js b/Libraries/Components/SwitchIOS/SwitchIOS.ios.js new file mode 100644 index 000000000..15c0af5aa --- /dev/null +++ b/Libraries/Components/SwitchIOS/SwitchIOS.ios.js @@ -0,0 +1,117 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule SwitchIOS + * + * This is a controlled component version of RCTSwitch. + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var StyleSheet = require('StyleSheet'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var merge = require('merge'); + +var SWITCH = 'switch'; + +/** + * Use `SwitchIOS` to render a boolean input on iOS. This is + * a controlled component, so you must hook in to the `onValueChange` callback + * and update the `value` prop in order for the component to update, otherwise + * the user's change will be reverted immediately to reflect `props.value` as the + * source of truth. + */ +var SwitchIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * The value of the switch, if true the switch will be turned on. + * Default value is false. + */ + value: PropTypes.bool, + + /** + * If true the user won't be able to toggle the switch. + * Default value is false. + */ + disabled: PropTypes.bool, + + /** + * Callback that is called when the user toggles the switch. + */ + onValueChange: PropTypes.func, + + /** + * Background color when the switch is turned on. + */ + onTintColor: PropTypes.string, + + /** + * Background color for the switch round button. + */ + thumbTintColor: PropTypes.string, + + /** + * Background color when the switch is turned off. + */ + tintColor: PropTypes.string, + }, + + getDefaultProps: function() { + return { + value: false, + disabled: false, + }; + }, + + _onChange: function(event) { + this.props.onChange && this.props.onChange(event); + this.props.onValueChange && this.props.onValueChange(event.nativeEvent.value); + + // The underlying switch might have changed, but we're controlled, + // and so want to ensure it represents our value. + this.refs[SWITCH].setNativeProps({on: this.props.value}); + }, + + render: function() { + return ( + + ); + } +}); + +var styles = StyleSheet.create({ + rkSwitch: { + height: 31, + width: 51, + }, +}); + +var rkSwitchAttributes = merge(ReactIOSViewAttributes.UIView, { + onTintColor: true, + tintColor: true, + thumbTintColor: true, + on: true, + enabled: true, +}); + +var RCTSwitch = createReactIOSNativeComponentClass({ + validAttributes: rkSwitchAttributes, + uiViewClassName: 'RCTSwitch', +}); + +module.exports = SwitchIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.android.js b/Libraries/Components/TabBarIOS/TabBarIOS.android.js new file mode 100644 index 000000000..7bddc1e92 --- /dev/null +++ b/Libraries/Components/TabBarIOS/TabBarIOS.android.js @@ -0,0 +1,29 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule TabBarIOS + */ + +'use strict'; + +var React = require('React'); +var View = require('View'); +var StyleSheet = require('StyleSheet'); + +var DummyTabBarIOS = React.createClass({ + render: function() { + return ( + + {this.props.children} + + ); + } +}); + +var styles = StyleSheet.create({ + tabGroup: { + flex: 1, + } +}); + +module.exports = DummyTabBarIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js new file mode 100644 index 000000000..e6e6f10f3 --- /dev/null +++ b/Libraries/Components/TabBarIOS/TabBarIOS.ios.js @@ -0,0 +1,36 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule TabBarIOS + */ +'use strict'; + +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var StyleSheet = require('StyleSheet'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); + +var TabBarIOS = React.createClass({ + render: function() { + return ( + + {this.props.children} + + ); + } +}); + +var styles = StyleSheet.create({ + tabGroup: { + flex: 1, + } +}); + +var config = { + validAttributes: ReactIOSViewAttributes.UIView, + uiViewClassName: 'RCTTabBar', +}; +var RCTTabBar = createReactIOSNativeComponentClass(config); + +module.exports = TabBarIOS; diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js new file mode 100644 index 000000000..634cc06c4 --- /dev/null +++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.android.js @@ -0,0 +1,38 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule TabBarItemIOS + */ + +'use strict'; + +var Dimensions = require('Dimensions'); +var React = require('React'); +var View = require('View'); +var StyleSheet = require('StyleSheet'); + +var DummyTab = React.createClass({ + render: function() { + if (!this.props.selected) { + return ; + } + return ( + + {this.props.children} + + ); + } +}); + +var styles = StyleSheet.create({ + tab: { + // TODO(5405356): Implement overflow: visible so position: absolute isn't useless + // position: 'absolute', + width: Dimensions.get('window').width, + height: Dimensions.get('window').height, + borderColor: 'red', + borderWidth: 1, + } +}); + +module.exports = DummyTab; diff --git a/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js new file mode 100644 index 000000000..70333d54e --- /dev/null +++ b/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js @@ -0,0 +1,94 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule TabBarItemIOS + */ +'use strict'; + +var Image = require('Image'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var Dimensions = require('Dimensions'); +var StaticContainer = require('StaticContainer.react'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var merge = require('merge'); + +var TabBarItemIOS = React.createClass({ + propTypes: { + icon: Image.propTypes.source.isRequired, + onPress: React.PropTypes.func.isRequired, + selected: React.PropTypes.bool.isRequired, + badgeValue: React.PropTypes.string, + title: React.PropTypes.string, + style: View.propTypes.style, + }, + + getInitialState: function() { + return { + hasBeenSelected: false, + }; + }, + + componentWillMount: function() { + if (this.props.selected) { + this.setState({hasBeenSelected: true}); + } + }, + + componentWillReceiveProps: function(nextProps) { + if (this.state.hasBeenSelected || nextProps.selected) { + this.setState({hasBeenSelected: true}); + } + }, + + render: function() { + var tabContents = null; + // if the tab has already been shown once, always continue to show it so we + // preserve state between tab transitions + if (this.state.hasBeenSelected) { + tabContents = + + {this.props.children} + ; + } else { + tabContents = ; + } + + return ( + + {tabContents} + + ); + } +}); + +var styles = StyleSheet.create({ + tab: { + position: 'absolute', + width: Dimensions.get('window').width, + height: Dimensions.get('window').height, + } +}); + +var RCTTabBarItem = createReactIOSNativeComponentClass({ + validAttributes: merge(ReactIOSViewAttributes.UIView, { + title: true, + icon: true, + selectedIcon: true, + selected: true, + badgeValue: true, + }), + uiViewClassName: 'RCTTabBarItem', +}); + +module.exports = TabBarItemIOS; diff --git a/Libraries/Components/Text/ExpandingText.js b/Libraries/Components/Text/ExpandingText.js deleted file mode 100644 index 80b06492a..000000000 --- a/Libraries/Components/Text/ExpandingText.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule ExpandingText - */ -'use strict'; - -var React = require('React'); -var StyleSheet = require('StyleSheet'); -var Text = require('Text'); -var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); -var View = require('View'); - -var truncate = require('truncate'); - -var styles = StyleSheet.create({ - boldText: { - fontWeight: 'bold', - }, -}); - -/** - * - A react component for displaying text which supports truncating - * based on a set truncLength. In the following example, the text will truncate - * to show only the first 17 characters plus '...' with a See More button to - * expand the text to its full length - * - * renderText: function() { - * return ; - * }, - * - * More example code in `ExpandingTextExample.js` - */ -var ExpandingText = React.createClass({ - PropTypes: { - /** - * Text to be displayed. Text will be truncated if the character length - * is greater than the truncLength property. - */ - text: React.PropTypes.string.isRequired, - /** - * The styles that will be applied to the text (both truncated and expanded). - */ - textStyle: Text.stylePropType, - /** - * The styles that will be applied to the See More button - */ - seeMoreStyle: Text.stylePropType, - /** - * The maximum character length for the text that will - * be displayed by default. Note that ... will be - * appended to the truncated text which is counted towards - * the total truncLength of the default displayed string - */ - truncLength: React.PropTypes.number - }, - - getDefaultProps: function() { - return { - truncLength: 130, - seeMoreText: 'See More', - seeMoreStyle: styles.boldText, - }; - }, - - getInitialState: function() { - return { - truncated: true, - }; - }, - - onTapSeeMore: function() { - this.setState({ - truncated: !this.state.truncated, - }); - }, - - isTruncated: function() { - return ( - this.props.text.length > this.props.truncLength && - this.state.truncated - ); - }, - - getText: function() { - var text = this.props.text; - if (!this.isTruncated()) { - return text; - } - - return truncate(text, this.props.truncLength) + ' '; - }, - - renderSeeMore: function() { - if (!this.isTruncated()) { - return null; - } - - return ( - - {this.props.seeMoreText} - - ); - }, - - render: function() { - return ( - - - - {this.getText()} - {this.renderSeeMore()} - - - - ); - } -}); - -module.exports = ExpandingText; diff --git a/Libraries/Components/Text/TextStylePropTypes.js b/Libraries/Components/Text/TextStylePropTypes.js deleted file mode 100644 index 0421794d2..000000000 --- a/Libraries/Components/Text/TextStylePropTypes.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule TextStylePropTypes - */ -'use strict'; - -var ReactPropTypes = require('ReactPropTypes'); -var ViewStylePropTypes = require('ViewStylePropTypes'); - -var merge = require('merge'); - -var TextStylePropTypes = merge( - ViewStylePropTypes, { - fontFamily: ReactPropTypes.string, - fontSize: ReactPropTypes.number, - fontWeight: ReactPropTypes.oneOf(['normal' /*default*/, 'bold']), - fontStyle: ReactPropTypes.oneOf(['normal', 'italic']), - lineHeight: ReactPropTypes.number, - color: ReactPropTypes.string, - containerBackgroundColor: ReactPropTypes.string, - textAlign: ReactPropTypes.oneOf( - ['auto' /*default*/, 'left', 'right', 'center'] - ), - writingDirection: ReactPropTypes.oneOf( - ['auto' /*default*/, 'ltr', 'rtl'] - ), - } -); - -// Text doesn't support padding correctly (#4841912) -var unsupportedProps = Object.keys({ - padding: null, - paddingTop: null, - paddingLeft: null, - paddingRight: null, - paddingBottom: null, - paddingVertical: null, - paddingHorizontal: null, -}); - -for (var key in unsupportedProps) { - delete TextStylePropTypes[key]; -} - -module.exports = TextStylePropTypes; diff --git a/Libraries/Components/TextInput/TextInput.ios.js b/Libraries/Components/TextInput/TextInput.ios.js index 2bd194d17..2f2b24dd6 100644 --- a/Libraries/Components/TextInput/TextInput.ios.js +++ b/Libraries/Components/TextInput/TextInput.ios.js @@ -8,13 +8,12 @@ var DocumentSelectionState = require('DocumentSelectionState'); var EventEmitter = require('EventEmitter'); var NativeMethodsMixin = require('NativeMethodsMixin'); -var NativeModulesDeprecated = require('NativeModulesDeprecated'); +var RCTUIManager = require('NativeModules').UIManager; var PropTypes = require('ReactPropTypes'); var React = require('React'); var ReactChildren = require('ReactChildren'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); -var Subscribable = require('Subscribable'); var Text = require('Text'); var TextInputState = require('TextInputState'); var TimerMixin = require('TimerMixin'); @@ -26,54 +25,10 @@ var getObjectValues = require('getObjectValues'); var invariant = require('invariant'); var merge = require('merge'); -/** - * - A foundational component for inputting text into the app via a - * keyboard. Props provide configurability for several features, such as auto- - * correction, auto-capitalization, placeholder text, and different keyboard - * types, such as a numeric keypad. - * - * The simplest use case is to plop down a `TextInput` and subscribe to the - * `onChangeText` events to read the user input. There are also other events, such - * as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple - * example: - * - * - * this.setState({input: text})} - * /> - * {'user input: ' + this.state.input} - * - * - * The `value` prop can be used to set the value of the input in order to make - * the state of the component clear, but does not behave as a true - * controlled component by default because all operations are asynchronous. - * Setting `value` once is like setting the default value, but you can change it - * continuously based on `onChangeText` events as well. If you really want to - * force the component to always revert to the value you are setting, you can - * set `controlled={true}`. - * - * The `multiline` prop is not supported in all releases, and some props are - * multiline only. - * - * More example code in `TextInputExample.js`. - */ +var autoCapitalizeConsts = RCTUIManager.UIText.AutocapitalizationType; +var clearButtonModeConsts = RCTUIManager.UITextField.clearButtonMode; -var nativeConstants = NativeModulesDeprecated.RKUIManager.UIText.AutocapitalizationType; - -var autoCapitalizeMode = { - none: nativeConstants.None, - sentences: nativeConstants.Sentences, - words: nativeConstants.Words, - characters: nativeConstants.AllCharacters -}; - -var keyboardType = { - default: 'default', - numeric: 'numeric', -}; - -var RKTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { +var RCTTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { autoCorrect: true, autoCapitalize: true, color: true, @@ -87,9 +42,10 @@ var RKTextViewAttributes = merge(ReactIOSViewAttributes.UIView, { text: true, }); -var RKTextFieldAttributes = merge(RKTextViewAttributes, { +var RCTTextFieldAttributes = merge(RCTTextViewAttributes, { caretHidden: true, enabled: true, + clearButtonMode: true, }); var onlyMultiline = { @@ -102,12 +58,40 @@ var notMultiline = { onSubmitEditing: true, }; -var TextInput = React.createClass({ - statics: { - autoCapitalizeMode: autoCapitalizeMode, - keyboardType: keyboardType, - }, +/** + * A foundational component for inputting text into the app via a + * keyboard. Props provide configurability for several features, such as auto- + * correction, auto-capitalization, placeholder text, and different keyboard + * types, such as a numeric keypad. + * + * The simplest use case is to plop down a `TextInput` and subscribe to the + * `onChangeText` events to read the user input. There are also other events, such + * as `onSubmitEditing` and `onFocus` that can be subscribed to. A simple + * example: + * + * ``` + * + * this.setState({input: text})} + * /> + * {'user input: ' + this.state.input} + * + * ``` + * + * The `value` prop can be used to set the value of the input in order to make + * the state of the component clear, but does not behave as a true + * controlled component by default because all operations are asynchronous. + * Setting `value` once is like setting the default value, but you can change it + * continuously based on `onChangeText` events as well. If you really want to + * force the component to always revert to the value you are setting, you can + * set `controlled={true}`. + * + * The `multiline` prop is not supported in all releases, and some props are + * multiline only. + */ +var TextInput = React.createClass({ propTypes: { /** * Can tell TextInput to automatically capitalize certain characters. @@ -116,11 +100,13 @@ var TextInput = React.createClass({ * - words: first letter of each word * - sentences: first letter of each sentence (default) * - none: don't auto capitalize anything - * - * example: - * autoCapitalize={TextInput.autoCapitalizeMode.words} */ - autoCapitalize: PropTypes.oneOf(getObjectValues(autoCapitalizeMode)), + autoCapitalize: PropTypes.oneOf([ + 'none', + 'sentences', + 'words', + 'characters', + ]), /** * If false, disables auto-correct. Default value is true. */ @@ -134,9 +120,12 @@ var TextInput = React.createClass({ */ editable: PropTypes.bool, /** - * Determines which keyboard to open, e.g.`TextInput.keyboardType.numeric`. + * Determines which keyboard to open, e.g.`numeric`. */ - keyboardType: PropTypes.oneOf(getObjectValues(keyboardType)), + keyboardType: PropTypes.oneOf([ + 'default', + 'numeric', + ]), /** * If true, the text input can be multiple lines. Default value is false. */ @@ -188,19 +177,28 @@ var TextInput = React.createClass({ * and/or laggy typing, depending on how you process onChange events. */ controlled: PropTypes.bool, + /** + * When the clear button should appear on the right side of the text view + */ + clearButtonMode: PropTypes.oneOf([ + 'never', + 'while-editing', + 'unless-editing', + 'always', + ]), - style: Text.stylePropType, + style: Text.propTypes.style, }, /** * `NativeMethodsMixin` will look for this when invoking `setNativeProps`. We * make `this` look like an actual native component class. */ - mixins: [NativeMethodsMixin, TimerMixin, Subscribable.Mixin], + mixins: [NativeMethodsMixin, TimerMixin], viewConfig: { uiViewClassName: 'RCTTextField', - validAttributes: RKTextFieldAttributes, + validAttributes: RCTTextFieldAttributes, }, isFocused: function() { @@ -233,18 +231,25 @@ var TextInput = React.createClass({ } return; } - this.addListenerOn(this.context.focusEmitter, 'focus', (el) => { - if (this === el) { - this.requestAnimationFrame(this.focus); - } else if (this.isFocused()) { - this.blur(); + this._focusSubscription = this.context.focusEmitter.addListener( + 'focus', + (el) => { + if (this === el) { + this.requestAnimationFrame(this.focus); + } else if (this.isFocused()) { + this.blur(); + } } - }); + ); if (this.props.autoFocus) { this.context.onFocusRequested(this); } }, + componentWillUnmount: function() { + this._focusSubscription && this._focusSubscription.remove(); + }, + componentWillReceiveProps: function(newProps) { if (newProps.value !== this.props.value) { if (!this.isFocused()) { @@ -292,6 +297,9 @@ var TextInput = React.createClass({ render: function() { var textContainer; + var autoCapitalize = autoCapitalizeConsts[this.props.autoCapitalize]; + var clearButtonMode = clearButtonModeConsts[this.props.clearButtonMode]; + if (!this.props.multiline) { for (var propKey in onlyMultiline) { if (this.props[propKey]) { @@ -301,7 +309,7 @@ var TextInput = React.createClass({ } } textContainer = - true} placeholder={this.props.placeholder} text={this.state.bufferedValue} - autoCapitalize={this.props.autoCapitalize} + autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} + clearButtonMode={clearButtonMode} />; } else { for (var propKey in notMultiline) { @@ -340,7 +349,7 @@ var TextInput = React.createClass({ children = [children, this.props.inputView]; } textContainer = - ; } @@ -418,13 +428,13 @@ var styles = StyleSheet.create({ }, }); -var RKTextView = createReactIOSNativeComponentClass({ - validAttributes: RKTextViewAttributes, +var RCTTextView = createReactIOSNativeComponentClass({ + validAttributes: RCTTextViewAttributes, uiViewClassName: 'RCTTextView', }); -var RKTextField = createReactIOSNativeComponentClass({ - validAttributes: RKTextFieldAttributes, +var RCTTextField = createReactIOSNativeComponentClass({ + validAttributes: RCTTextFieldAttributes, uiViewClassName: 'RCTTextField', }); diff --git a/Libraries/Components/TextInput/TextInputState.js b/Libraries/Components/TextInput/TextInputState.js index a8c80ee66..2bbe8a3d0 100644 --- a/Libraries/Components/TextInput/TextInputState.js +++ b/Libraries/Components/TextInput/TextInputState.js @@ -9,7 +9,7 @@ */ 'use strict'; -var RKUIManager = require('NativeModulesDeprecated').RKUIManager; +var RCTUIManager = require('NativeModules').UIManager; var TextInputState = { /** @@ -33,7 +33,7 @@ var TextInputState = { focusTextInput: function(textFieldID) { if (this._currentlyFocusedID != textFieldID && textFieldID != null) { this._currentlyFocusedID = textFieldID; - RKUIManager.focus(textFieldID); + RCTUIManager.focus(textFieldID); } }, @@ -45,7 +45,7 @@ var TextInputState = { blurTextInput: function(textFieldID) { if (this._currentlyFocusedID == textFieldID && textFieldID != null) { this._currentlyFocusedID = null; - RKUIManager.blur(textFieldID); + RCTUIManager.blur(textFieldID); } } }; diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js new file mode 100644 index 000000000..2eb052fd8 --- /dev/null +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -0,0 +1,124 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule TouchableBounce + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var React = require('React'); +var POPAnimation = require('POPAnimation'); +var Animation = require('Animation'); +var Touchable = require('Touchable'); + +var merge = require('merge'); +var copyProperties = require('copyProperties'); +var onlyChild = require('onlyChild'); + +/** + * 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 + * moving it back and you'll see that the button is once again reactivated! + * Move it back and forth several times while the scroll view is disabled. + */ +var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; +/** + * Example of using the `TouchableMixin` to play well with other responder + * locking views including `ScrollView`. `TouchableMixin` provides touchable + * hooks (`this.touchableHandle*`) that we forward events to. In turn, + * `TouchableMixin` expects us to implement some abstract methods to handle + * interesting interactions such as `handleTouchablePress`. + */ +var TouchableBounce = React.createClass({ + mixins: [Touchable.Mixin, NativeMethodsMixin], + + propTypes: { + onPress: 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. + onPressWithCompletion: React.PropTypes.func, + // the function passed is called after the animation is complete + onPressAnimationComplete: React.PropTypes.func, + }, + + getInitialState: function() { + return merge(this.touchableGetInitialState(), {animationID: null}); + }, + + bounceTo: function(value, velocity, bounciness, fromValue, callback) { + if (POPAnimation) { + this.state.animationID && this.removeAnimation(this.state.animationID); + var anim = { + property: POPAnimation.Properties.scaleXY, + dynamicsTension: 0, + toValue: [value, value], + velocity: [velocity, velocity], + springBounciness: bounciness, + }; + if (fromValue) { + anim.fromValue = [fromValue, fromValue]; + } + this.state.animationID = POPAnimation.createSpringAnimation(anim); + this.addAnimation(this.state.animationID, callback); + } else { + Animation.startAnimation(this, 300, 0, 'easeOutBack', {scaleXY: [value, value]}); + if (fromValue && typeof fromValue === 'function') { + callback = fromValue; + } + if (callback) { + setTimeout(callback, 300); + } + } + }, + + /** + * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are + * defined on your component. + */ + touchableHandleActivePressIn: function() { + this.bounceTo(0.93, 0.1, 0); + }, + + touchableHandleActivePressOut: function() { + this.bounceTo(1, 0.4, 0); + }, + + touchableHandlePress: function() { + if (this.props.onPressWithCompletion) { + this.props.onPressWithCompletion( + this.bounceTo.bind(this, 1, 10, 10, 0.93, this.props.onPressAnimationComplete) + ); + return; + } + + this.bounceTo(1, 10, 10, undefined, this.props.onPressAnimationComplete); + this.props.onPress && this.props.onPress(); + }, + + touchableGetPressRectOffset: function() { + return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! + }, + + touchableGetHighlightDelayMS: function() { + return 0; + }, + + render: function() { + // Note(vjeux): use cloneWithProps once React has been upgraded + var child = onlyChild(this.props.children); + copyProperties(child.props, { + accessible: true, + testID: this.props.testID, + onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, + onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, + onResponderGrant: this.touchableHandleResponderGrant, + onResponderMove: this.touchableHandleResponderMove, + onResponderRelease: this.touchableHandleResponderRelease, + onResponderTerminate: this.touchableHandleResponderTerminate + }); + return child; + } +}); + +module.exports = TouchableBounce; diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index da721cff8..0794e19ae 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -11,6 +11,7 @@ var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheet = require('StyleSheet'); var TimerMixin = require('TimerMixin'); var Touchable = require('Touchable'); +var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var View = require('View'); var cloneWithProps = require('cloneWithProps'); @@ -19,42 +20,38 @@ var keyOf = require('keyOf'); var merge = require('merge'); var onlyChild = require('onlyChild'); -/** - * TouchableHighlight - A wrapper for making views respond properly to touches. - * On press down, the opacity of the wrapped view is decreased, which allows - * the underlay color to show through, darkening or tinting the view. The - * underlay comes from adding a view to the view hierarchy, which can sometimes - * cause unwanted visual artifacts if not used correctly, for example if the - * backgroundColor of the wrapped view isn't explicitly set to an opaque color. - * Example: - * - * renderButton: function() { - * return ( - * - * - * - * ); - * }, - * - * More example code in TouchableExample.js, and more in-depth discussion in - * Touchable.js. See also TouchableWithoutFeedback.js. - */ - var DEFAULT_PROPS = { activeOpacity: 0.8, underlayColor: 'black', }; +/** + * A wrapper for making views respond properly to touches. + * On press down, the opacity of the wrapped view is decreased, which allows + * the underlay color to show through, darkening or tinting the view. The + * underlay comes from adding a view to the view hierarchy, which can sometimes + * cause unwanted visual artifacts if not used correctly, for example if the + * backgroundColor of the wrapped view isn't explicitly set to an opaque color. + * + * Example: + * + * ``` + * renderButton: function() { + * return ( + * + * + * + * ); + * }, + * ``` + */ + var TouchableHighlight = React.createClass({ propTypes: { - /** - * Called when the touch is released, but not if cancelled (e.g. by - * a scroll that steals the responder lock). - */ - onPress: React.PropTypes.func.isRequired, + ...TouchableWithoutFeedback.propTypes, /** * Determines what the opacity of the wrapped view should be when touch is * active. @@ -65,7 +62,7 @@ var TouchableHighlight = React.createClass({ * active. */ underlayColor: React.PropTypes.string, - style: View.stylePropType, + style: View.propTypes.style, }, mixins: [NativeMethodsMixin, TimerMixin, Touchable.Mixin], @@ -116,7 +113,7 @@ var TouchableHighlight = React.createClass({ viewConfig: { uiViewClassName: 'RCTView', - validAttributes: ReactIOSViewAttributes.RKView + validAttributes: ReactIOSViewAttributes.RCTView }, /** @@ -127,12 +124,14 @@ var TouchableHighlight = React.createClass({ this.clearTimeout(this._hideTimeout); this._hideTimeout = null; this._showUnderlay(); + this.props.onPressIn && this.props.onPressIn(); }, touchableHandleActivePressOut: function() { if (!this._hideTimeout) { this._hideUnderlay(); } + this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { @@ -142,6 +141,10 @@ var TouchableHighlight = React.createClass({ this.props.onPress && this.props.onPress(); }, + touchableHandleLongPress: function() { + this.props.onLongPress && this.props.onLongPress(); + }, + touchableGetPressRectOffset: function() { return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! }, @@ -156,7 +159,10 @@ var TouchableHighlight = React.createClass({ this._hideTimeout = null; if (this.refs[UNDERLAY_REF]) { this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS); - this.refs[UNDERLAY_REF].setNativeProps(INACTIVE_UNDERLAY_PROPS); + this.refs[UNDERLAY_REF].setNativeProps({ + ...INACTIVE_UNDERLAY_PROPS, + style: this.state.underlayStyle, + }); } }, diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index a1bd8f4e3..680fc87f5 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -9,6 +9,7 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var POPAnimationMixin = require('POPAnimationMixin'); var React = require('React'); var Touchable = require('Touchable'); +var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); @@ -16,36 +17,32 @@ var keyOf = require('keyOf'); var onlyChild = require('onlyChild'); /** - * TouchableOpacity - A wrapper for making views respond properly to touches. + * A wrapper for making views respond properly to touches. * On press down, the opacity of the wrapped view is decreased, dimming it. * This is done without actually changing the view hierarchy, and in general is - * easy to add to an app without weird side-effects. Example: + * easy to add to an app without weird side-effects. * - * renderButton: function() { - * return ( - * - * - * - * ); - * }, + * Example: * - * More example code in TouchableExample.js, and more in-depth discussion in - * Touchable.js. See also TouchableHighlight.js and - * TouchableWithoutFeedback.js. + * ``` + * renderButton: function() { + * return ( + * + * + * + * ); + * }, + * ``` */ var TouchableOpacity = React.createClass({ mixins: [Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin], propTypes: { - /** - * Called when the touch is released, but not if cancelled (e.g. by - * a scroll that steals the responder lock). - */ - onPress: React.PropTypes.func, + ...TouchableWithoutFeedback.propTypes, /** * Determines what the opacity of the wrapped view should be when touch is * active. @@ -97,10 +94,12 @@ var TouchableOpacity = React.createClass({ this.refs[CHILD_REF].setNativeProps({ opacity: this.props.activeOpacity }); + this.props.onPressIn && this.props.onPressIn(); }, touchableHandleActivePressOut: function() { this.setOpacityTo(1.0); + this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { @@ -108,6 +107,10 @@ var TouchableOpacity = React.createClass({ this.props.onPress && this.props.onPress(); }, + touchableHandleLongPress: function() { + this.props.onLongPress && this.props.onLongPress(); + }, + touchableGetPressRectOffset: function() { return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! }, diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 66a82d59c..78544dbc6 100644 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -7,9 +7,7 @@ var React = require('React'); var Touchable = require('Touchable'); -var View = require('View'); -var copyProperties = require('copyProperties'); var onlyChild = require('onlyChild'); /** @@ -30,6 +28,10 @@ var TouchableWithoutFeedback = React.createClass({ mixins: [Touchable.Mixin], propTypes: { + /** + * Called when the touch is released, but not if cancelled (e.g. by a scroll + * that steals the responder lock). + */ onPress: React.PropTypes.func, onPressIn: React.PropTypes.func, onPressOut: React.PropTypes.func, @@ -71,7 +73,7 @@ var TouchableWithoutFeedback = React.createClass({ render: function() { // Note(vjeux): use cloneWithProps once React has been upgraded var child = onlyChild(this.props.children); - copyProperties(child.props, { + return React.cloneElement(child, { accessible: true, testID: this.props.testID, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, @@ -81,7 +83,6 @@ var TouchableWithoutFeedback = React.createClass({ onResponderRelease: this.touchableHandleResponderRelease, onResponderTerminate: this.touchableHandleResponderTerminate }); - return child; } }); diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 643eef68a..437448d45 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -6,15 +6,19 @@ 'use strict'; var NativeMethodsMixin = require('NativeMethodsMixin'); -var NativeModules = require('NativeModules'); var PropTypes = require('ReactPropTypes'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); var StyleSheetPropType = require('StyleSheetPropType'); var ViewStylePropTypes = require('ViewStylePropTypes'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); + +var stylePropType = StyleSheetPropType(ViewStylePropTypes); + /** - * - The most fundamental component for building UI, `View` is a + * The most fundamental component for building UI, `View` is a * container that supports layout with flexbox, style, some touch handling, and * accessibility controls, and is designed to be nested inside other views and * to have 0 to many children of any type. `View` maps directly to the native @@ -22,11 +26,13 @@ var ViewStylePropTypes = require('ViewStylePropTypes'); * `UIView`, `
`, `android.view`, etc. This example creates a `View` that * wraps two colored boxes and custom component in a row with padding. * - * - * - * - * - * + * ``` + * + * + * + * + * + * ``` * * By default, `View`s have a primary flex direction of 'column', so children * will stack up vertically by default. `View`s also expand to fill the parent @@ -40,23 +46,8 @@ var ViewStylePropTypes = require('ViewStylePropTypes'); * `View`s are designed to be used with `StyleSheet`s for clarity and * performance, although inline styles are also supported. It is common for * `StyleSheet`s to be combined dynamically. See `StyleSheet.js` for more info. - * - * Check out `ViewExample.js`, `LayoutExample.js`, and other apps for more code - * examples. */ - -var StyleConstants = NativeModules.RKUIManager.StyleConstants; - -var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); - -var stylePropType = StyleSheetPropType(ViewStylePropTypes); - var View = React.createClass({ - statics: { - pointerEvents: StyleConstants.PointerEventsValues, - stylePropType, - }, - mixins: [NativeMethodsMixin], /** @@ -65,7 +56,7 @@ var View = React.createClass({ */ viewConfig: { uiViewClassName: 'RCTView', - validAttributes: ReactIOSViewAttributes.RKView + validAttributes: ReactIOSViewAttributes.RCTView }, propTypes: { @@ -75,7 +66,7 @@ var View = React.createClass({ accessible: PropTypes.bool, /** - * This string can be used to identify the accessible element. + * Used to locate this view in end-to-end tests. */ testID: PropTypes.string, @@ -96,7 +87,7 @@ var View = React.createClass({ /** * In the absence of `auto` property, `none` is much like `CSS`'s `none` - * value. `boxNone` is as if you had applied the `CSS` class: + * value. `box-none` is as if you had applied the `CSS` class: * * .cantTouchThis * { * pointer-events: auto; @@ -112,10 +103,10 @@ var View = React.createClass({ * implementation detail of the platform. */ pointerEvents: PropTypes.oneOf([ - StyleConstants.PointerEventsValues.boxNone, - StyleConstants.PointerEventsValues.none, - StyleConstants.PointerEventsValues.boxOnly, - StyleConstants.PointerEventsValues.unspecified + 'box-none', + 'none', + 'box-only', + 'auto', ]), /** @@ -125,7 +116,7 @@ var View = React.createClass({ style: stylePropType, /** - * This is a special performance property exposed by RKView and is useful + * This is a special performance property exposed by RCTView and is useful * for scrolling content when there are many subviews, most of which are * offscreen. For this property to be effective, it must be applied to a * view that contains many subviews that extend outside its bound. The @@ -136,22 +127,21 @@ var View = React.createClass({ }, render: function() { - return ; + return ; }, }); -var RKView = createReactIOSNativeComponentClass({ - validAttributes: ReactIOSViewAttributes.RKView, +var RCTView = createReactIOSNativeComponentClass({ + validAttributes: ReactIOSViewAttributes.RCTView, uiViewClassName: 'RCTView', }); +RCTView.propTypes = View.propTypes; -var ViewToExport = RKView; +var ViewToExport = RCTView; if (__DEV__) { ViewToExport = View; } -ViewToExport.pointerEvents = View.pointerEvents; -ViewToExport.stylePropType = stylePropType; module.exports = ViewToExport; diff --git a/Libraries/Components/View/ViewStylePropTypes.js b/Libraries/Components/View/ViewStylePropTypes.js index c049c3d53..3c226bcde 100644 --- a/Libraries/Components/View/ViewStylePropTypes.js +++ b/Libraries/Components/View/ViewStylePropTypes.js @@ -8,13 +8,11 @@ var LayoutPropTypes = require('LayoutPropTypes'); var ReactPropTypes = require('ReactPropTypes'); -var merge = require('merge'); - /** * Warning: Some of these properties may not be supported in all releases. */ -var ViewStylePropTypes = merge( - LayoutPropTypes, { +var ViewStylePropTypes = { + ...LayoutPropTypes, backgroundColor: ReactPropTypes.string, borderColor: ReactPropTypes.string, borderTopColor: ReactPropTypes.string, @@ -36,6 +34,6 @@ var ViewStylePropTypes = merge( scaleY: ReactPropTypes.number, translateX: ReactPropTypes.number, translateY: ReactPropTypes.number, -}); +}; module.exports = ViewStylePropTypes; diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js new file mode 100644 index 000000000..9eab4e1cb --- /dev/null +++ b/Libraries/Components/WebView/WebView.android.js @@ -0,0 +1,169 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule WebView + */ +'use strict'; + +var EdgeInsetsPropType = require('EdgeInsetsPropType'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var keyMirror = require('keyMirror'); +var merge = require('merge'); + +var PropTypes = React.PropTypes; +var RCTUIManager = require('NativeModules').UIManager; + +var RCT_WEBVIEW_REF = 'webview'; + +var WebViewState = keyMirror({ + IDLE: null, + LOADING: null, + ERROR: null, +}); + +var WebView = React.createClass({ + + propTypes: { + renderError: PropTypes.func.isRequired, // view to show if there's an error + renderLoading: PropTypes.func.isRequired, // loading indicator to show + url: PropTypes.string.isRequired, + automaticallyAdjustContentInsets: PropTypes.bool, + contentInset: EdgeInsetsPropType, + onNavigationStateChange: PropTypes.func, + startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load + style: View.propTypes.style, + /** + * Used to locate this view in end-to-end tests. + */ + testID: PropTypes.string, + }, + + getInitialState: function() { + return { + viewState: WebViewState.IDLE, + lastErrorEvent: null, + startInLoadingState: true, + }; + }, + + componentWillMount: function() { + if (this.props.startInLoadingState) { + this.setState({viewState: WebViewState.LOADING}); + } + }, + + render: function() { + var otherView = null; + + if (this.state.viewState === WebViewState.LOADING) { + otherView = this.props.renderLoading(); + } else if (this.state.viewState === WebViewState.ERROR) { + var errorEvent = this.state.lastErrorEvent; + otherView = this.props.renderError( + errorEvent.domain, + errorEvent.code, + errorEvent.description); + } else if (this.state.viewState !== WebViewState.IDLE) { + console.error("RCTWebView invalid state encountered: " + this.state.loading); + } + + var webViewStyles = [styles.container, this.props.style]; + if (this.state.viewState === WebViewState.LOADING || + this.state.viewState === WebViewState.ERROR) { + // if we're in either LOADING or ERROR states, don't show the webView + webViewStyles.push(styles.hidden); + } + + var webView = + ; + + return ( + + {webView} + {otherView} + + ); + }, + + goForward: function() { + RCTUIManager.webViewGoForward(this.getWebWiewHandle()); + }, + + goBack: function() { + RCTUIManager.webViewGoBack(this.getWebWiewHandle()); + }, + + reload: function() { + RCTUIManager.webViewReload(this.getWebWiewHandle()); + }, + + /** + * We return an event with a bunch of fields including: + * url, title, loading, canGoBack, canGoForward + */ + updateNavigationState: function(event) { + if (this.props.onNavigationStateChange) { + this.props.onNavigationStateChange(event.nativeEvent); + } + }, + + getWebWiewHandle: function() { + return this.refs[RCT_WEBVIEW_REF].getNodeHandle(); + }, + + onLoadingStart: function(event) { + this.updateNavigationState(event); + }, + + onLoadingError: function(event) { + event.persist(); // persist this event because we need to store it + console.error("encountered an error loading page", event.nativeEvent); + + this.setState({ + lastErrorEvent: event.nativeEvent, + viewState: WebViewState.ERROR + }); + }, + + onLoadingFinish: function(event) { + this.setState({ + viewState: WebViewState.IDLE, + }); + this.updateNavigationState(event); + }, +}); + +var RCTWebView = createReactIOSNativeComponentClass({ + validAttributes: merge(ReactIOSViewAttributes.UIView, { + url: true, + }), + uiViewClassName: 'RCTWebView', +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + hidden: { + height: 0, + flex: 0, // disable 'flex:1' when hiding a View + }, +}); + +module.exports = WebView; diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js new file mode 100644 index 000000000..be6457e01 --- /dev/null +++ b/Libraries/Components/WebView/WebView.ios.js @@ -0,0 +1,182 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule WebView + */ +'use strict'; + +var EdgeInsetsPropType = require('EdgeInsetsPropType'); +var React = require('React'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); +var keyMirror = require('keyMirror'); +var insetsDiffer = require('insetsDiffer'); +var merge = require('merge'); + +var PropTypes = React.PropTypes; +var RCTWebViewManager = require('NativeModules').WebViewManager; + +var RCT_WEBVIEW_REF = 'webview'; + +var WebViewState = keyMirror({ + IDLE: null, + LOADING: null, + ERROR: null, +}); + +var NavigationType = { + click: RCTWebViewManager.NavigationType.LinkClicked, + formsubmit: RCTWebViewManager.NavigationType.FormSubmitted, + backforward: RCTWebViewManager.NavigationType.BackForward, + reload: RCTWebViewManager.NavigationType.Reload, + formresubmit: RCTWebViewManager.NavigationType.FormResubmitted, + other: RCTWebViewManager.NavigationType.Other, +}; + +var WebView = React.createClass({ + statics: { + NavigationType: NavigationType, + }, + + propTypes: { + renderError: PropTypes.func.isRequired, // view to show if there's an error + renderLoading: PropTypes.func.isRequired, // loading indicator to show + url: PropTypes.string.isRequired, + automaticallyAdjustContentInsets: PropTypes.bool, + shouldInjectAJAXHandler: PropTypes.bool, + contentInset: EdgeInsetsPropType, + onNavigationStateChange: PropTypes.func, + startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load + style: View.propTypes.style, + }, + + getInitialState: function() { + return { + viewState: WebViewState.IDLE, + lastErrorEvent: null, + startInLoadingState: true, + }; + }, + + componentWillMount: function() { + if (this.props.startInLoadingState) { + this.setState({viewState: WebViewState.LOADING}); + } + }, + + render: function() { + var otherView = null; + + if (this.state.viewState === WebViewState.LOADING) { + otherView = this.props.renderLoading(); + } else if (this.state.viewState === WebViewState.ERROR) { + var errorEvent = this.state.lastErrorEvent; + otherView = this.props.renderError( + errorEvent.domain, + errorEvent.code, + errorEvent.description); + } else if (this.state.viewState !== WebViewState.IDLE) { + console.error('RCTWebView invalid state encountered: ' + this.state.loading); + } + + var webViewStyles = [styles.container, this.props.style]; + if (this.state.viewState === WebViewState.LOADING || + this.state.viewState === WebViewState.ERROR) { + // if we're in either LOADING or ERROR states, don't show the webView + webViewStyles.push(styles.hidden); + } + + var webView = + ; + + return ( + + {webView} + {otherView} + + ); + }, + + goForward: function() { + RCTWebViewManager.goForward(this.getWebWiewHandle()); + }, + + goBack: function() { + RCTWebViewManager.goBack(this.getWebWiewHandle()); + }, + + reload: function() { + RCTWebViewManager.reload(this.getWebWiewHandle()); + }, + + /** + * We return an event with a bunch of fields including: + * url, title, loading, canGoBack, canGoForward + */ + updateNavigationState: function(event) { + if (this.props.onNavigationStateChange) { + this.props.onNavigationStateChange(event.nativeEvent); + } + }, + + getWebWiewHandle: function() { + return this.refs[RCT_WEBVIEW_REF].getNodeHandle(); + }, + + onLoadingStart: function(event) { + this.updateNavigationState(event); + }, + + onLoadingError: function(event) { + event.persist(); // persist this event because we need to store it + console.error("encountered an error loading page", event.nativeEvent); + + this.setState({ + lastErrorEvent: event.nativeEvent, + viewState: WebViewState.ERROR + }); + }, + + onLoadingFinish: function(event) { + this.setState({ + viewState: WebViewState.IDLE, + }); + this.updateNavigationState(event); + }, +}); + +var RCTWebView = createReactIOSNativeComponentClass({ + validAttributes: merge(ReactIOSViewAttributes.UIView, { + url: true, + contentInset: {diff: insetsDiffer}, + automaticallyAdjustContentInsets: true, + shouldInjectAJAXHandler: true + }), + uiViewClassName: 'RCTWebView', +}); + +var styles = StyleSheet.create({ + container: { + flex: 1, + }, + hidden: { + height: 0, + flex: 0, // disable 'flex:1' when hiding a View + }, +}); + +module.exports = WebView; diff --git a/Libraries/CustomComponents/LICENSE b/Libraries/CustomComponents/LICENSE new file mode 100644 index 000000000..01f2fbc02 --- /dev/null +++ b/Libraries/CustomComponents/LICENSE @@ -0,0 +1,9 @@ +LICENSE AGREEMENT + +For React Native Custom Components software + +Copyright (c) 2015, Facebook, Inc. All rights reserved. + +Facebook, Inc. (“Facebook”) owns all right, title and interest, including all intellectual property and other proprietary rights, in and to the React Native Custom Components software (the “Software”). Subject to your compliance with these terms, you are hereby granted a non-exclusive, worldwide, royalty-free copyright license to (1) use and copy the Software; and (2) reproduce and distribute the Software as part of your own software (“Your Software”). Facebook reserves all rights not expressly granted to you in this license agreement. + +THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Libraries/Components/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js similarity index 74% rename from Libraries/Components/ListView/ListView.js rename to Libraries/CustomComponents/ListView/ListView.js index 45539586b..e0c067a18 100644 --- a/Libraries/Components/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -7,7 +7,7 @@ var ListViewDataSource = require('ListViewDataSource'); var React = require('React'); -var RKUIManager = require('NativeModules').RKUIManager; +var RCTUIManager = require('NativeModules').UIManager; var ScrollView = require('ScrollView'); var ScrollResponder = require('ScrollResponder'); var StaticRenderer = require('StaticRenderer'); @@ -19,29 +19,42 @@ var isEmpty = require('isEmpty'); var PropTypes = React.PropTypes; +var DEFAULT_PAGE_SIZE = 1; +var DEFAULT_INITIAL_ROWS = 10; +var DEFAULT_SCROLL_RENDER_AHEAD = 1000; +var DEFAULT_END_REACHED_THRESHOLD = 1000; +var DEFAULT_SCROLL_CALLBACK_THROTTLE = 50; +var RENDER_INTERVAL = 20; +var SCROLLVIEW_REF = 'listviewscroll'; + + /** * ListView - A core component designed for efficient display of vertically * scrolling lists of changing data. The minimal API is to create a - * `ListViewDataSource`, populate it with a simple array of data blobs, and + * `ListView.DataSource`, populate it with a simple array of data blobs, and * instantiate a `ListView` component with that data source and a `renderRow` * callback which takes a blob from the data array and returns a renderable - * component. Minimal example: + * component. * - * getInitialState: function() { - * var ds = new ListViewDataSource({rowHasChanged: (r1, r2) => r1 !== r2}); - * return { - * dataSource: ds.cloneWithRows(['row 1', 'row 2']), - * }; - * }, + * Minimal example: * - * render: function() { - * return ( - * {rowData}} - * /> - * ); - * }, + * ``` + * getInitialState: function() { + * var ds = new ListViewDataSource({rowHasChanged: (r1, r2) => r1 !== r2}); + * return { + * dataSource: ds.cloneWithRows(['row 1', 'row 2']), + * }; + * }, + * + * render: function() { + * return ( + * {rowData}} + * /> + * ); + * }, + * ``` * * ListView also supports more advanced features, including sections with sticky * section headers, header and footer support, callbacks on reaching the end of @@ -61,22 +74,15 @@ var PropTypes = React.PropTypes; * event-loop (customizable with the `pageSize` prop). This breaks up the * work into smaller chunks to reduce the chance of dropping frames while * rendering rows. - * - * Check out `ListViewSimpleExample.js`, `ListViewDataSource.js`, and the Movies - * app for more info and example usage. */ -var DEFAULT_PAGE_SIZE = 1; -var DEFAULT_INITIAL_ROWS = 10; -var DEFAULT_SCROLL_RENDER_AHEAD = 1000; -var DEFAULT_END_REACHED_THRESHOLD = 1000; -var DEFAULT_SCROLL_CALLBACK_THROTTLE = 50; -var RENDER_INTERVAL = 20; -var SCROLLVIEW_REF = 'listviewscroll'; - var ListView = React.createClass({ mixins: [ScrollResponder.Mixin, TimerMixin], + statics: { + DataSource: ListViewDataSource, + }, + /** * You must provide a renderRow function. If you omit any of the other render * functions, ListView will simply skip rendering them. @@ -84,80 +90,80 @@ var ListView = React.createClass({ * - renderRow(rowData, sectionID, rowID); * - renderSectionHeader(sectionData, sectionID); */ - propTypes: - merge( - ScrollView.PropTypes, { - dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired, - /** - * (rowData, sectionID, rowID) => renderable - * Takes a data entry from the data source and its ids and should return - * a renderable component to be rendered as the row. By default the data - * is exactly what was put into the data source, but it's also possible to - * provide custom extractors. - */ - renderRow: PropTypes.func.isRequired, - /** - * How many rows to render on initial component mount. Use this to make - * it so that the first screen worth of data apears at one time instead of - * over the course of multiple frames. - */ - initialListSize: PropTypes.number, - /** - * Called when all rows have been rendered and the list has been scrolled - * to within onEndReachedThreshold of the bottom. The native scroll - * event is provided. - */ - onEndReached: PropTypes.func, - /** - * Threshold in pixels for onEndReached. - */ - onEndReachedThreshold: PropTypes.number, - /** - * Number of rows to render per event loop. - */ - pageSize: PropTypes.number, - /** - * () => renderable - * - * The header and footer are always rendered (if these props are provided) - * on every render pass. If they are expensive to re-render, wrap them - * in StaticContainer or other mechanism as appropriate. Footer is always - * at the bottom of the list, and header at the top, on every render pass. - */ - renderFooter: PropTypes.func, - renderHeader: PropTypes.func, - /** - * (sectionData, sectionID) => renderable - * - * If provided, a sticky header is rendered for this section. The sticky - * behavior means that it will scroll with the content at the top of the - * section until it reaches the top of the screen, at which point it will - * stick to the top until it is pushed off the screen by the next section - * header. - */ - renderSectionHeader: PropTypes.func, - /** - * How early to start rendering rows before they come on screen, in - * pixels. - */ - scrollRenderAheadDistance: React.PropTypes.number, - /** - * (visibleRows, changedRows) => void - * - * Called when the set of visible rows changes. `visibleRows` maps - * { sectionID: { rowID: true }} for all the visible rows, and - * `changedRows` maps { sectionID: { rowID: true | false }} for the rows - * that have changed their visibility, with true indicating visible, and - * false indicating the view has moved out of view. - */ - onChangeVisibleRows: React.PropTypes.func, - /** - * An experimental performance optimization for improving scroll perf of - * large lists, used in conjunction with overflow: 'hidden' on the row - * containers. Use at your own risk. - */ - removeClippedSubviews: React.PropTypes.bool, - }), + propTypes: { + ...ScrollView.propTypes, + + dataSource: PropTypes.instanceOf(ListViewDataSource).isRequired, + /** + * (rowData, sectionID, rowID) => renderable + * Takes a data entry from the data source and its ids and should return + * a renderable component to be rendered as the row. By default the data + * is exactly what was put into the data source, but it's also possible to + * provide custom extractors. + */ + renderRow: PropTypes.func.isRequired, + /** + * How many rows to render on initial component mount. Use this to make + * it so that the first screen worth of data apears at one time instead of + * over the course of multiple frames. + */ + initialListSize: PropTypes.number, + /** + * Called when all rows have been rendered and the list has been scrolled + * to within onEndReachedThreshold of the bottom. The native scroll + * event is provided. + */ + onEndReached: PropTypes.func, + /** + * Threshold in pixels for onEndReached. + */ + onEndReachedThreshold: PropTypes.number, + /** + * Number of rows to render per event loop. + */ + pageSize: PropTypes.number, + /** + * () => renderable + * + * The header and footer are always rendered (if these props are provided) + * on every render pass. If they are expensive to re-render, wrap them + * in StaticContainer or other mechanism as appropriate. Footer is always + * at the bottom of the list, and header at the top, on every render pass. + */ + renderFooter: PropTypes.func, + renderHeader: PropTypes.func, + /** + * (sectionData, sectionID) => renderable + * + * If provided, a sticky header is rendered for this section. The sticky + * behavior means that it will scroll with the content at the top of the + * section until it reaches the top of the screen, at which point it will + * stick to the top until it is pushed off the screen by the next section + * header. + */ + renderSectionHeader: PropTypes.func, + /** + * How early to start rendering rows before they come on screen, in + * pixels. + */ + scrollRenderAheadDistance: React.PropTypes.number, + /** + * (visibleRows, changedRows) => void + * + * Called when the set of visible rows changes. `visibleRows` maps + * { sectionID: { rowID: true }} for all the visible rows, and + * `changedRows` maps { sectionID: { rowID: true | false }} for the rows + * that have changed their visibility, with true indicating visible, and + * false indicating the view has moved out of view. + */ + onChangeVisibleRows: React.PropTypes.func, + /** + * An experimental performance optimization for improving scroll perf of + * large lists, used in conjunction with overflow: 'hidden' on the row + * containers. Use at your own risk. + */ + removeClippedSubviews: React.PropTypes.bool, + }, /** * Exports some data, e.g. for perf investigations or analytics. @@ -317,13 +323,13 @@ var ListView = React.createClass({ */ _measureAndUpdateScrollProps: function() { - RKUIManager.measureLayout( + RCTUIManager.measureLayout( this.refs[SCROLLVIEW_REF].getInnerViewNode(), this.refs[SCROLLVIEW_REF].getNodeHandle(), logError, this._setScrollContentHeight ); - RKUIManager.measureLayoutRelativeToParent( + RCTUIManager.measureLayoutRelativeToParent( this.refs[SCROLLVIEW_REF].getNodeHandle(), logError, this._setScrollVisibleHeight @@ -395,8 +401,8 @@ var ListView = React.createClass({ var header = this.props.renderHeader && this.props.renderHeader(); var totalIndex = header ? 1 : 0; var visibilityChanged = false; - var changedRows = {}; - for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) { + var changedRows = {}; + for (var sectionIdx = 0; sectionIdx < allRowIDs.length; sectionIdx++) { var rowIDs = allRowIDs[sectionIdx]; if (rowIDs.length === 0) { continue; diff --git a/Libraries/Components/ListView/ListViewDataSource.js b/Libraries/CustomComponents/ListView/ListViewDataSource.js similarity index 97% rename from Libraries/Components/ListView/ListViewDataSource.js rename to Libraries/CustomComponents/ListView/ListViewDataSource.js index 3be109443..b1916bc64 100644 --- a/Libraries/Components/ListView/ListViewDataSource.js +++ b/Libraries/CustomComponents/ListView/ListViewDataSource.js @@ -215,9 +215,10 @@ class ListViewDataSource { /** * @param {number} index * - * Gets the rowID at index provided if the dataSource arrays were flattened + * Gets the rowID at index provided if the dataSource arrays were flattened, + * or null of out of range indexes. */ - getRowIDForFlatIndex(index: number): string { + getRowIDForFlatIndex(index: number): ?string { var accessIndex = index; for (var ii = 0; ii < this.sectionIdentities.length; ii++) { if (accessIndex >= this.rowIdentities[ii].length) { @@ -226,14 +227,16 @@ class ListViewDataSource { return this.rowIdentities[ii][accessIndex]; } } + return null; } /** * @param {number} index * - * Gets the sectionID at index provided if the dataSource arrays were flattened + * Gets the sectionID at index provided if the dataSource arrays were flattened, + * or null for out of range indexes. */ - getSectionIDForFlatIndex(index: number): string { + getSectionIDForFlatIndex(index: number): ?string { var accessIndex = index; for (var ii = 0; ii < this.sectionIdentities.length; ii++) { if (accessIndex >= this.rowIdentities[ii].length) { @@ -242,6 +245,7 @@ class ListViewDataSource { return this.sectionIdentities[ii]; } } + return null; } /** diff --git a/Libraries/Geolocation/Geolocation.ios.js b/Libraries/Geolocation/Geolocation.ios.js new file mode 100644 index 000000000..37134dd31 --- /dev/null +++ b/Libraries/Geolocation/Geolocation.ios.js @@ -0,0 +1,98 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule Geolocation + */ +'use strict'; + +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTLocationObserver = require('NativeModules').LocationObserver; + +var invariant = require('invariant'); +var logError = require('logError'); +var warning = require('warning'); + +var subscriptions = []; + +var updatesEnabled = false; + +/** + * /!\ ATTENTION /!\ + * You need to add NSLocationWhenInUseUsageDescription key + * in Info.plist to enable geolocation, otherwise it's going + * to *fail silently*! + * \!/ \!/ + * + * Geolocation follows the MDN specification: + * https://developer.mozilla.org/en-US/docs/Web/API/Geolocation + */ +var Geolocation = { + + getCurrentPosition: function(geo_success, geo_error, geo_options) { + invariant( + typeof geo_success === 'function', + 'Must provide a valid geo_success callback.' + ); + RCTLocationObserver.getCurrentPosition( + geo_options || {}, + geo_success, + geo_error || logError + ); + }, + + watchPosition: function(success, error, options) { + if (!updatesEnabled) { + RCTLocationObserver.startObserving(options || {}); + updatesEnabled = true; + } + var watchID = subscriptions.length; + subscriptions.push([ + RCTDeviceEventEmitter.addListener( + 'geolocationDidChange', + success + ), + error ? RCTDeviceEventEmitter.addListener( + 'geolocationError', + error + ) : null, + ]); + return watchID; + }, + + clearWatch: function(watchID) { + var sub = subscriptions[watchID]; + if (!sub) { + // Silently exit when the watchID is invalid or already cleared + // This is consistent with timers + return; + } + sub[0].remove(); + sub[1] && sub[1].remove(); + subscriptions[watchID] = undefined; + var noWatchers = true; + for (var ii = 0; ii < subscriptions.length; ii++) { + if (subscriptions[ii]) { + noWatchers = false; // still valid subscriptions + } + } + if (noWatchers) { + Geolocation.stopObserving(); + } + }, + + stopObserving: function() { + if (updatesEnabled) { + RCTLocationObserver.stopObserving(); + updatesEnabled = false; + for (var ii = 0; ii < subscriptions.length; ii++) { + if (subscriptions[ii]) { + warning('Called stopObserving with existing subscriptions.'); + subscriptions[ii].remove(); + } + } + subscriptions = []; + } + } +} + +module.exports = Geolocation; diff --git a/Libraries/Geolocation/RCTGeolocation.xcodeproj/project.pbxproj b/Libraries/Geolocation/RCTGeolocation.xcodeproj/project.pbxproj new file mode 100644 index 000000000..96545a34c --- /dev/null +++ b/Libraries/Geolocation/RCTGeolocation.xcodeproj/project.pbxproj @@ -0,0 +1,256 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 134814061AA4E45400B7C361 /* RCTLocationObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 134814051AA4E45400B7C361 /* RCTLocationObserver.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 134814041AA4E45400B7C361 /* RCTLocationObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTLocationObserver.h; sourceTree = ""; }; + 134814051AA4E45400B7C361 /* RCTLocationObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTLocationObserver.m; sourceTree = ""; }; + 134814201AA4EA6300B7C361 /* libRCTGeolocation.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTGeolocation.a; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 134814211AA4EA7D00B7C361 /* Products */ = { + isa = PBXGroup; + children = ( + 134814201AA4EA6300B7C361 /* libRCTGeolocation.a */, + ); + name = Products; + sourceTree = ""; + }; + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + 134814041AA4E45400B7C361 /* RCTLocationObserver.h */, + 134814051AA4E45400B7C361 /* RCTLocationObserver.m */, + 134814211AA4EA7D00B7C361 /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* RCTGeolocation */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTGeolocation" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTGeolocation; + productName = RCTDataManager; + productReference = 134814201AA4EA6300B7C361 /* libRCTGeolocation.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTGeolocation" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511D21A9E6C8500147676; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* RCTGeolocation */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 134814061AA4E45400B7C361 /* RCTLocationObserver.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "/Users/nicklockwood/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/ReactKit/build/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTGeolocation; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "/Users/nicklockwood/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/ReactKit/build/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTGeolocation; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTGeolocation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTGeolocation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/Libraries/Geolocation/RCTLocationObserver.h b/Libraries/Geolocation/RCTLocationObserver.h new file mode 100644 index 000000000..ad3ba2ce2 --- /dev/null +++ b/Libraries/Geolocation/RCTLocationObserver.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTBridgeModule.h" + +@interface RCTLocationObserver : NSObject + +@end diff --git a/Libraries/Geolocation/RCTLocationObserver.m b/Libraries/Geolocation/RCTLocationObserver.m new file mode 100644 index 000000000..c2a2d0ee1 --- /dev/null +++ b/Libraries/Geolocation/RCTLocationObserver.m @@ -0,0 +1,319 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTLocationObserver.h" + +#import +#import +#import + +#import "RCTAssert.h" +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "RCTLog.h" + +typedef NS_ENUM(NSInteger, RCTPositionErrorCode) { + RCTPositionErrorDenied = 1, + RCTPositionErrorUnavailable, + RCTPositionErrorTimeout, +}; + +#define RCT_DEFAULT_LOCATION_ACCURACY kCLLocationAccuracyHundredMeters + +typedef struct { + NSTimeInterval timeout; + NSTimeInterval maximumAge; + CLLocationAccuracy accuracy; +} RCTLocationOptions; + +static RCTLocationOptions RCTLocationOptionsWithJSON(id json) +{ + NSDictionary *options = [RCTConvert NSDictionary:json]; + return (RCTLocationOptions){ + .timeout = [RCTConvert NSTimeInterval:options[@"timeout"]] ?: INFINITY, + .maximumAge = [RCTConvert NSTimeInterval:options[@"maximumAge"]] ?: INFINITY, + .accuracy = [RCTConvert BOOL:options[@"enableHighAccuracy"]] ? kCLLocationAccuracyBest : RCT_DEFAULT_LOCATION_ACCURACY + }; +} + +static NSDictionary *RCTPositionError(RCTPositionErrorCode code, NSString *msg /* nil for default */) +{ + if (!msg) { + switch (code) { + case RCTPositionErrorDenied: + msg = @"User denied access to location services."; + break; + case RCTPositionErrorUnavailable: + msg = @"Unable to retrieve location."; + break; + case RCTPositionErrorTimeout: + msg = @"The location request timed out."; + break; + } + } + + return @{ + @"code": @(code), + @"message": msg, + @"PERMISSION_DENIED": @(RCTPositionErrorDenied), + @"POSITION_UNAVAILABLE": @(RCTPositionErrorUnavailable), + @"TIMEOUT": @(RCTPositionErrorTimeout) + }; +} + +@interface RCTLocationRequest : NSObject + +@property (nonatomic, copy) RCTResponseSenderBlock successBlock; +@property (nonatomic, copy) RCTResponseSenderBlock errorBlock; +@property (nonatomic, assign) RCTLocationOptions options; +@property (nonatomic, strong) NSTimer *timeoutTimer; + +@end + +@implementation RCTLocationRequest + +- (void)dealloc +{ + [_timeoutTimer invalidate]; +} + +@end + +@interface RCTLocationObserver () + +@end + +@implementation RCTLocationObserver +{ + CLLocationManager *_locationManager; + NSDictionary *_lastLocationEvent; + NSMutableArray *_pendingRequests; + BOOL _observingLocation; + RCTLocationOptions _observerOptions; +} + +@synthesize bridge = _bridge; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if ((self = [super init])) { + + _locationManager = [[CLLocationManager alloc] init]; + _locationManager.distanceFilter = RCT_DEFAULT_LOCATION_ACCURACY; + _locationManager.delegate = self; + + _pendingRequests = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)dealloc +{ + [_locationManager stopUpdatingLocation]; +} + +#pragma mark - Private API + +- (void)beginLocationUpdates +{ + // Request location access permission + if ([_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) { + [_locationManager requestWhenInUseAuthorization]; + } + + // Start observing location + [_locationManager startUpdatingLocation]; +} + +#pragma mark - Timeout handler + +- (void)timeout:(NSTimer *)timer +{ + RCTLocationRequest *request = timer.userInfo; + NSString *message = [NSString stringWithFormat: @"Unable to fetch location within %zds.", (NSInteger)(timer.timeInterval * 1000.0)]; + request.errorBlock(@[RCTPositionError(RCTPositionErrorTimeout, message)]); + [_pendingRequests removeObject:request]; + + // Stop updating if no pending requests + if (_pendingRequests.count == 0 && !_observingLocation) { + [_locationManager stopUpdatingLocation]; + } +} + +#pragma mark - Public API + +- (void)startObserving:(NSDictionary *)optionsJSON +{ + RCT_EXPORT(); + + dispatch_async(dispatch_get_main_queue(), ^{ + + // Select best options + _observerOptions = RCTLocationOptionsWithJSON(optionsJSON); + for (RCTLocationRequest *request in _pendingRequests) { + _observerOptions.accuracy = MIN(_observerOptions.accuracy, request.options.accuracy); + } + + _locationManager.desiredAccuracy = _observerOptions.accuracy; + [self beginLocationUpdates]; + _observingLocation = YES; + + }); +} + +- (void)stopObserving +{ + RCT_EXPORT(); + + dispatch_async(dispatch_get_main_queue(), ^{ + + // Stop observing + _observingLocation = NO; + + // Stop updating if no pending requests + if (_pendingRequests.count == 0) { + [_locationManager stopUpdatingLocation]; + } + + }); +} + +- (void)getCurrentPosition:(NSDictionary *)optionsJSON + withSuccessCallback:(RCTResponseSenderBlock)successBlock + errorCallback:(RCTResponseSenderBlock)errorBlock +{ + RCT_EXPORT(); + + if (!successBlock) { + RCTLogError(@"%@.getCurrentPosition called with nil success parameter.", [self class]); + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + + if (![CLLocationManager locationServicesEnabled]) { + if (errorBlock) { + errorBlock(@[ + RCTPositionError(RCTPositionErrorUnavailable, @"Location services disabled.") + ]); + return; + } + } + + if (![CLLocationManager authorizationStatus] == kCLAuthorizationStatusDenied) { + if (errorBlock) { + errorBlock(@[ + RCTPositionError(RCTPositionErrorDenied, nil) + ]); + return; + } + } + + // Get options + RCTLocationOptions options = RCTLocationOptionsWithJSON(optionsJSON); + + // Check if previous recorded location exists and is good enough + if (_lastLocationEvent && + CFAbsoluteTimeGetCurrent() - [RCTConvert NSTimeInterval:_lastLocationEvent[@"timestamp"]] < options.maximumAge && + [_lastLocationEvent[@"coords"][@"accuracy"] doubleValue] >= options.accuracy) { + + // Call success block with most recent known location + successBlock(@[_lastLocationEvent]); + return; + } + + // Create request + RCTLocationRequest *request = [[RCTLocationRequest alloc] init]; + request.successBlock = successBlock; + request.errorBlock = errorBlock ?: ^(NSArray *args){}; + request.options = options; + request.timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:options.timeout + target:self + selector:@selector(timeout:) + userInfo:request + repeats:NO]; + [_pendingRequests addObject:request]; + + // Configure location manager and begin updating location + _locationManager.desiredAccuracy = MIN(_locationManager.desiredAccuracy, options.accuracy); + [self beginLocationUpdates]; + + }); +} + +#pragma mark - CLLocationManagerDelegate + +- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations +{ + // Create event + CLLocation *location = [locations lastObject]; + _lastLocationEvent = @{ + @"coords": @{ + @"latitude": @(location.coordinate.latitude), + @"longitude": @(location.coordinate.longitude), + @"altitude": @(location.altitude), + @"accuracy": @(location.horizontalAccuracy), + @"altitudeAccuracy": @(location.verticalAccuracy), + @"heading": @(location.course), + @"speed": @(location.speed), + }, + @"timestamp": @(CFAbsoluteTimeGetCurrent() * 1000.0) // in ms + }; + + // Send event + if (_observingLocation) { + [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationDidChange" + body:_lastLocationEvent]; + } + + // Fire all queued callbacks + for (RCTLocationRequest *request in _pendingRequests) { + request.successBlock(@[_lastLocationEvent]); + } + [_pendingRequests removeAllObjects]; + + // Stop updating if not not observing + if (!_observingLocation) { + [_locationManager stopUpdatingLocation]; + } + + // Reset location accuracy + _locationManager.desiredAccuracy = RCT_DEFAULT_LOCATION_ACCURACY; +} + +- (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error +{ + // Check error type + NSDictionary *jsError = nil; + switch (error.code) { + case kCLErrorDenied: + jsError = RCTPositionError(RCTPositionErrorDenied, nil); + break; + case kCLErrorNetwork: + jsError = RCTPositionError(RCTPositionErrorUnavailable, @"Unable to retrieve location due to a network failure"); + break; + case kCLErrorLocationUnknown: + default: + jsError = RCTPositionError(RCTPositionErrorUnavailable, nil); + break; + } + + // Send event + if (_observingLocation) { + [_bridge.eventDispatcher sendDeviceEventWithName:@"geolocationError" + body:jsError]; + } + + // Fire all queued error callbacks + for (RCTLocationRequest *request in _pendingRequests) { + request.errorBlock(@[jsError]); + } + [_pendingRequests removeAllObjects]; + + // Reset location accuracy + _locationManager.desiredAccuracy = RCT_DEFAULT_LOCATION_ACCURACY; +} + +@end diff --git a/Libraries/Components/Image/Image.ios.js b/Libraries/Image/Image.ios.js similarity index 77% rename from Libraries/Components/Image/Image.ios.js rename to Libraries/Image/Image.ios.js index 5926cb144..c25e92654 100644 --- a/Libraries/Components/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -7,10 +7,9 @@ var EdgeInsetsPropType = require('EdgeInsetsPropType'); var NativeMethodsMixin = require('NativeMethodsMixin'); -var NativeModulesDeprecated = require('NativeModulesDeprecated'); +var NativeModules = require('NativeModules'); var PropTypes = require('ReactPropTypes'); var ImageResizeMode = require('ImageResizeMode'); -var ImageSourcePropType = require('ImageSourcePropType'); var ImageStylePropTypes = require('ImageStylePropTypes'); var React = require('React'); var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); @@ -25,31 +24,40 @@ var merge = require('merge'); var warning = require('warning'); /** - * - A react component for displaying different types of images, + * A react component for displaying different types of images, * including network images, static resources, temporary local images, and - * images from local disk, such as the camera roll. Example usage: + * images from local disk, such as the camera roll. * - * renderImages: function() { - * return ( - * - * - * - * - * ); - * }, + * Example usage: * - * More example code in ImageExample.js + * ``` + * renderImages: function() { + * return ( + * + * + * + * + * ); + * }, + * ``` */ var Image = React.createClass({ propTypes: { - source: ImageSourcePropType, + source: PropTypes.shape({ + /** + * A string representing the resource identifier for the image, which + * could be an http address, a local file path, or the name of a static image + * resource (which should be wrapped in the `ix` function). + */ + uri: PropTypes.string, + }), /** * accessible - Whether this element should be revealed as an accessible * element. @@ -78,7 +86,6 @@ var Image = React.createClass({ statics: { resizeMode: ImageResizeMode, - sourcePropType: ImageSourcePropType, }, mixins: [NativeMethodsMixin], @@ -101,13 +108,13 @@ var Image = React.createClass({ 'static image uris cannot start with "http": "' + source.uri + '"' ); var isStored = !source.isStatic && !isNetwork; - var RawImage = isNetwork ? RKNetworkImage : RKStaticImage; + var RawImage = isNetwork ? RCTNetworkImage : RCTStaticImage; if (this.props.style && this.props.style.tintColor) { - warning(RawImage === RKStaticImage, 'tintColor style only supported on static images.'); + warning(RawImage === RCTStaticImage, 'tintColor style only supported on static images.'); } - var contentModes = NativeModulesDeprecated.RKUIManager.UIView.ContentMode; + var contentModes = NativeModules.UIManager.UIView.ContentMode; var resizeMode; if (style.resizeMode === ImageResizeMode.stretch) { resizeMode = contentModes.ScaleToFill; @@ -148,12 +155,12 @@ var CommonImageViewAttributes = merge(ReactIOSViewAttributes.UIView, { testID: PropTypes.string, }); -var RKStaticImage = createReactIOSNativeComponentClass({ +var RCTStaticImage = createReactIOSNativeComponentClass({ validAttributes: merge(CommonImageViewAttributes, { tintColor: true }), uiViewClassName: 'RCTStaticImage', }); -var RKNetworkImage = createReactIOSNativeComponentClass({ +var RCTNetworkImage = createReactIOSNativeComponentClass({ validAttributes: merge(CommonImageViewAttributes, { defaultImageSrc: true }), uiViewClassName: 'RCTNetworkImageView', }); diff --git a/Libraries/Components/Image/ImageResizeMode.js b/Libraries/Image/ImageResizeMode.js similarity index 100% rename from Libraries/Components/Image/ImageResizeMode.js rename to Libraries/Image/ImageResizeMode.js diff --git a/Libraries/Components/Image/ImageStylePropTypes.js b/Libraries/Image/ImageStylePropTypes.js similarity index 54% rename from Libraries/Components/Image/ImageStylePropTypes.js rename to Libraries/Image/ImageStylePropTypes.js index 3c67a7795..2973a72d6 100644 --- a/Libraries/Components/Image/ImageStylePropTypes.js +++ b/Libraries/Image/ImageStylePropTypes.js @@ -9,23 +9,19 @@ var ImageResizeMode = require('ImageResizeMode'); var LayoutPropTypes = require('LayoutPropTypes'); var ReactPropTypes = require('ReactPropTypes'); -var merge = require('merge'); +var ImageStylePropTypes = { + ...LayoutPropTypes, + resizeMode: ReactPropTypes.oneOf(Object.keys(ImageResizeMode)), + backgroundColor: ReactPropTypes.string, + borderColor: ReactPropTypes.string, + borderWidth: ReactPropTypes.number, + borderRadius: ReactPropTypes.number, -var ImageStylePropTypes = merge( - LayoutPropTypes, - { - resizeMode: ReactPropTypes.oneOf(Object.keys(ImageResizeMode)), - backgroundColor: ReactPropTypes.string, - borderColor: ReactPropTypes.string, - borderWidth: ReactPropTypes.number, - borderRadius: ReactPropTypes.number, - - // iOS-Specific style to "tint" an image. - // It changes the color of all the non-transparent pixels to the tintColor - tintColor: ReactPropTypes.string, - opacity: ReactPropTypes.number, - } -); + // iOS-Specific style to "tint" an image. + // It changes the color of all the non-transparent pixels to the tintColor + tintColor: ReactPropTypes.string, + opacity: ReactPropTypes.number, +}; // Image doesn't support padding correctly (#4841912) var unsupportedProps = Object.keys({ diff --git a/Libraries/Image/RCTCameraRollManager.h b/Libraries/Image/RCTCameraRollManager.h new file mode 100644 index 000000000..4a957d6a2 --- /dev/null +++ b/Libraries/Image/RCTCameraRollManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTBridgeModule.h" + +@interface RCTCameraRollManager : NSObject + +@end diff --git a/Libraries/Image/RCTCameraRollManager.m b/Libraries/Image/RCTCameraRollManager.m new file mode 100644 index 000000000..9f86ffb69 --- /dev/null +++ b/Libraries/Image/RCTCameraRollManager.m @@ -0,0 +1,148 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTCameraRollManager.h" + +#import +#import +#import +#import + + #import "RCTImageLoader.h" +#import "RCTLog.h" + +@implementation RCTCameraRollManager + +- (void)saveImageWithTag:(NSString *)imageTag successCallback:(RCTResponseSenderBlock)successCallback errorCallback:(RCTResponseSenderBlock)errorCallback +{ + RCT_EXPORT(); + + [RCTImageLoader loadImageWithTag:imageTag callback:^(NSError *loadError, UIImage *loadedImage) { + if (loadError) { + errorCallback(@[[loadError localizedDescription]]); + return; + } + [[RCTImageLoader assetsLibrary] writeImageToSavedPhotosAlbum:[loadedImage CGImage] metadata:nil completionBlock:^(NSURL *assetURL, NSError *saveError) { + if (saveError) { + NSString *errorMessage = [NSString stringWithFormat:@"Error saving cropped image: %@", saveError]; + RCTLogWarn(@"%@", errorMessage); + errorCallback(@[errorMessage]); + return; + } + successCallback(@[[assetURL absoluteString]]); + }]; + }]; +} + +- (void)callCallback:(RCTResponseSenderBlock)callback withAssets:(NSArray *)assets hasNextPage:(BOOL)hasNextPage +{ + if (![assets count]) { + callback(@[@{ + @"edges": assets, + @"page_info": @{ + @"has_next_page": @NO} + }]); + return; + } + callback(@[@{ + @"edges": assets, + @"page_info": @{ + @"start_cursor": assets[0][@"node"][@"image"][@"uri"], + @"end_cursor": assets[assets.count - 1][@"node"][@"image"][@"uri"], + @"has_next_page": @(hasNextPage)} + }]); +} + +- (void)getPhotos:(NSDictionary *)params callback:(RCTResponseSenderBlock)callback errorCallback:(RCTResponseSenderBlock)errorCallback +{ + RCT_EXPORT(); + + NSUInteger first = [params[@"first"] integerValue]; + NSString *afterCursor = params[@"after"]; + NSString *groupTypesStr = params[@"groupTypes"]; + NSString *groupName = params[@"groupName"]; + ALAssetsGroupType groupTypes; + if ([groupTypesStr isEqualToString:@"Album"]) { + groupTypes = ALAssetsGroupAlbum; + } else if ([groupTypesStr isEqualToString:@"All"]) { + groupTypes = ALAssetsGroupAll; + } else if ([groupTypesStr isEqualToString:@"Event"]) { + groupTypes = ALAssetsGroupEvent; + } else if ([groupTypesStr isEqualToString:@"Faces"]) { + groupTypes = ALAssetsGroupFaces; + } else if ([groupTypesStr isEqualToString:@"Library"]) { + groupTypes = ALAssetsGroupLibrary; + } else if ([groupTypesStr isEqualToString:@"PhotoStream"]) { + groupTypes = ALAssetsGroupPhotoStream; + } else { + groupTypes = ALAssetsGroupSavedPhotos; + } + + BOOL __block foundAfter = NO; + BOOL __block hasNextPage = NO; + BOOL __block calledCallback = NO; + NSMutableArray *assets = [[NSMutableArray alloc] init]; + + [[RCTImageLoader assetsLibrary] enumerateGroupsWithTypes:groupTypes usingBlock:^(ALAssetsGroup *group, BOOL *stopGroups) { + if (group && (groupName == nil || [groupName isEqualToString:[group valueForProperty:ALAssetsGroupPropertyName]])) { + [group setAssetsFilter:ALAssetsFilter.allPhotos]; + [group enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stopAssets) { + if (result) { + 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]) { + *stopAssets = YES; + *stopGroups = YES; + hasNextPage = YES; + RCTAssert(calledCallback == NO, @"Called the callback before we finished processing the results."); + [self callCallback:callback withAssets:assets hasNextPage:hasNextPage]; + calledCallback = YES; + return; + } + CGSize dimensions = [result defaultRepresentation].dimensions; + CLLocation *loc = [result valueForProperty:ALAssetPropertyLocation]; + NSDate *date = [result valueForProperty:ALAssetPropertyDate]; + [assets addObject:@{ + @"node": @{ + @"type": [result valueForProperty:ALAssetPropertyType], + @"group_name": [group valueForProperty:ALAssetsGroupPropertyName], + @"image": @{ + @"uri": uri, + @"height": @(dimensions.height), + @"width": @(dimensions.width), + @"isStored": @YES, + }, + @"timestamp": @([date timeIntervalSince1970]), + @"location": loc ? + @{ + @"latitude": @(loc.coordinate.latitude), + @"longitude": @(loc.coordinate.longitude), + @"altitude": @(loc.altitude), + @"heading": @(loc.course), + @"speed": @(loc.speed), + } : @{}, + } + }]; + } + }]; + } else { + // Sometimes the enumeration continues even if we set stop above, so we guard against calling the callback + // multiple times here. + if (!calledCallback) { + [self callCallback:callback withAssets:assets hasNextPage:hasNextPage]; + calledCallback = YES; + } + } + } failureBlock:^(NSError *error) { + if (error.code != ALAssetsLibraryAccessUserDeniedError) { + RCTLogError(@"Failure while iterating through asset groups %@", error); + } + errorCallback(@[error.description]); + }]; +} + +@end diff --git a/Libraries/Image/RCTGIFImage.h b/Libraries/Image/RCTGIFImage.h new file mode 100644 index 000000000..38f290462 --- /dev/null +++ b/Libraries/Image/RCTGIFImage.h @@ -0,0 +1,8 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import +#import +#import + +extern CAKeyframeAnimation *RCTGIFImageWithData(NSData *data); +extern CAKeyframeAnimation *RCTGIFImageWithFileURL(NSURL *URL); diff --git a/Libraries/Image/RCTGIFImage.m b/Libraries/Image/RCTGIFImage.m new file mode 100644 index 000000000..8fe49f6bc --- /dev/null +++ b/Libraries/Image/RCTGIFImage.m @@ -0,0 +1,91 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTGIFImage.h" + +#import "RCTLog.h" + +static CAKeyframeAnimation *RCTGIFImageWithImageSource(CGImageSourceRef imageSource) +{ + if (!UTTypeConformsTo(CGImageSourceGetType(imageSource), kUTTypeGIF)) { + CFRelease(imageSource); + return nil; + } + + NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL); + NSUInteger loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; + + size_t imageCount = CGImageSourceGetCount(imageSource); + NSTimeInterval duration = 0; + NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount]; + NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount]; + for (size_t i = 0; i < imageCount; i++) { + CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); + NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL); + NSDictionary *frameGIFProperties = frameProperties[(id)kCGImagePropertyGIFDictionary]; + + const NSTimeInterval kDelayTimeIntervalDefault = 0.1; + NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime]; + if (delayTime == nil) { + if (i == 0) { + delayTime = @(kDelayTimeIntervalDefault); + } else { + delayTime = delays[i - 1]; + } + } + + const NSTimeInterval kDelayTimeIntervalMinimum = 0.02; + if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) { + delayTime = @(kDelayTimeIntervalDefault); + } + + duration += delayTime.doubleValue; + delays[i] = delayTime; + images[i] = (__bridge_transfer id)image; + } + + NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count]; + NSTimeInterval runningDuration = 0; + for (NSNumber *delayNumber in delays) { + [keyTimes addObject:@(runningDuration / duration)]; + runningDuration += delayNumber.doubleValue; + } + + [keyTimes addObject:@1.0]; + + CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"]; + animation.calculationMode = kCAAnimationDiscrete; + animation.repeatCount = loopCount == 0 ? HUGE_VALF : loopCount; + animation.keyTimes = keyTimes; + animation.values = images; + animation.duration = duration; + return animation; +} + +CAKeyframeAnimation *RCTGIFImageWithData(NSData *data) +{ + if (data.length == 0) { + return nil; + } + + CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)data, NULL); + CAKeyframeAnimation *animation = RCTGIFImageWithImageSource(imageSource); + CFRelease(imageSource); + return animation; +} + +CAKeyframeAnimation *RCTGIFImageWithFileURL(NSURL *URL) +{ + if (!URL) { + return nil; + } + + if (![URL isFileURL]) { + RCTLogError(@"Loading remote image URLs synchronously is a really bad idea."); + return nil; + } + + CGImageSourceRef imageSource = CGImageSourceCreateWithURL((CFURLRef)URL, NULL); + CAKeyframeAnimation *animation = RCTGIFImageWithImageSource(imageSource); + CFRelease(imageSource); + return animation; +} diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj new file mode 100644 index 000000000..fa6c8c084 --- /dev/null +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -0,0 +1,298 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */; }; + 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */; }; + 1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; }; + 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */; }; + 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 143879371AAD32A300F088A5 /* RCTImageLoader.m */; }; + 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */; }; + 58B511901A9E6BD600147676 /* RCTNetworkImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */; }; + 58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B5115B1A9E6B3D00147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1304D5A71AA8C4A30002E2BE /* RCTStaticImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImage.h; sourceTree = ""; }; + 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImage.m; sourceTree = ""; }; + 1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImageManager.h; sourceTree = ""; }; + 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImageManager.m; sourceTree = ""; }; + 1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTGIFImage.h; sourceTree = ""; }; + 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImage.m; sourceTree = ""; }; + 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCameraRollManager.h; sourceTree = ""; }; + 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCameraRollManager.m; sourceTree = ""; }; + 143879361AAD32A300F088A5 /* RCTImageLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageLoader.h; sourceTree = ""; }; + 143879371AAD32A300F088A5 /* RCTImageLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageLoader.m; sourceTree = ""; }; + 58B5115D1A9E6B3D00147676 /* libRCTImage.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTImage.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 58B511891A9E6BD600147676 /* RCTImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageDownloader.h; sourceTree = ""; }; + 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageDownloader.m; sourceTree = ""; }; + 58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageView.h; sourceTree = ""; }; + 58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageView.m; sourceTree = ""; }; + 58B5118D1A9E6BD600147676 /* RCTNetworkImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageViewManager.h; sourceTree = ""; }; + 58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageViewManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B5115A1A9E6B3D00147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 58B511541A9E6B3D00147676 = { + isa = PBXGroup; + children = ( + 143879361AAD32A300F088A5 /* RCTImageLoader.h */, + 143879371AAD32A300F088A5 /* RCTImageLoader.m */, + 143879331AAD238D00F088A5 /* RCTCameraRollManager.h */, + 143879341AAD238D00F088A5 /* RCTCameraRollManager.m */, + 1304D5B01AA8C50D0002E2BE /* RCTGIFImage.h */, + 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */, + 58B511891A9E6BD600147676 /* RCTImageDownloader.h */, + 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */, + 58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */, + 58B5118C1A9E6BD600147676 /* RCTNetworkImageView.m */, + 58B5118D1A9E6BD600147676 /* RCTNetworkImageViewManager.h */, + 58B5118E1A9E6BD600147676 /* RCTNetworkImageViewManager.m */, + 1304D5A71AA8C4A30002E2BE /* RCTStaticImage.h */, + 1304D5A81AA8C4A30002E2BE /* RCTStaticImage.m */, + 1304D5A91AA8C4A30002E2BE /* RCTStaticImageManager.h */, + 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */, + 58B5115E1A9E6B3D00147676 /* Products */, + ); + sourceTree = ""; + }; + 58B5115E1A9E6B3D00147676 /* Products */ = { + isa = PBXGroup; + children = ( + 58B5115D1A9E6B3D00147676 /* libRCTImage.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B5115C1A9E6B3D00147676 /* RCTImage */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511711A9E6B3D00147676 /* Build configuration list for PBXNativeTarget "RCTImage" */; + buildPhases = ( + 58B511591A9E6B3D00147676 /* Sources */, + 58B5115A1A9E6B3D00147676 /* Frameworks */, + 58B5115B1A9E6B3D00147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTImage; + productName = RCTNetworkImage; + productReference = 58B5115D1A9E6B3D00147676 /* libRCTImage.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511551A9E6B3D00147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B5115C1A9E6B3D00147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RCTImage" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511541A9E6B3D00147676; + productRefGroup = 58B5115E1A9E6B3D00147676 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B5115C1A9E6B3D00147676 /* RCTImage */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511591A9E6B3D00147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */, + 58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */, + 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */, + 58B511901A9E6BD600147676 /* RCTNetworkImageView.m in Sources */, + 1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */, + 143879351AAD238D00F088A5 /* RCTCameraRollManager.m in Sources */, + 143879381AAD32A300F088A5 /* RCTImageLoader.m in Sources */, + 1304D5AB1AA8C4A30002E2BE /* RCTStaticImage.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B5116F1A9E6B3D00147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511701A9E6B3D00147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511721A9E6B3D00147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(USER_LIBRARY_DIR)/Developer/Xcode/DerivedData/UIExplorer-gjaibsjtheitasdxdtcvxxqavkvy/Build/Products/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTImage; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511731A9E6B3D00147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(USER_LIBRARY_DIR)/Developer/Xcode/DerivedData/UIExplorer-gjaibsjtheitasdxdtcvxxqavkvy/Build/Products/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTImage; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511581A9E6B3D00147676 /* Build configuration list for PBXProject "RCTImage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B5116F1A9E6B3D00147676 /* Debug */, + 58B511701A9E6B3D00147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511711A9E6B3D00147676 /* Build configuration list for PBXNativeTarget "RCTImage" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511721A9E6B3D00147676 /* Debug */, + 58B511731A9E6B3D00147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511551A9E6B3D00147676 /* Project object */; +} diff --git a/ReactKit/Base/RCTImageDownloader.h b/Libraries/Image/RCTImageDownloader.h similarity index 100% rename from ReactKit/Base/RCTImageDownloader.h rename to Libraries/Image/RCTImageDownloader.h diff --git a/ReactKit/Base/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m similarity index 87% rename from ReactKit/Base/RCTImageDownloader.m rename to Libraries/Image/RCTImageDownloader.m index 9f065aaa3..ea801dea7 100644 --- a/ReactKit/Base/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -109,39 +109,37 @@ typedef void (^RCTCachedDataDownloadBlock)(BOOL cached, NSData *data, NSError *e - (id)downloadImageForURL:(NSURL *)url size:(CGSize)size scale:(CGFloat)scale block:(RCTImageDownloadBlock)block { - NSString *cacheKey = [self cacheKeyForURL:url]; - __weak RCTImageDownloader *weakSelf = self; - return [self _downloadDataForURL:url block:^(BOOL cached, NSData *data, NSError *error) { - if (!data) { - return dispatch_async(dispatch_get_main_queue(), ^{ - block(nil, error); - }); - } + return [self downloadDataForURL:url block:^(NSData *data, NSError *error) { UIImage *image = [UIImage imageWithData:data scale:scale]; - if (image) { + + // Resize (TODO: should we take aspect ratio into account?) CGSize imageSize = size; if (CGSizeEqualToSize(imageSize, CGSizeZero)) { imageSize = image.size; + } else { + imageSize = (CGSize){ + MIN(size.width, image.size.width), + MIN(size.height, image.size.height) + }; } + // Rescale image if required size is smaller CGFloat imageScale = scale; - if (imageScale == 0 || imageScale > image.scale) { + if (imageScale == 0 || imageScale < image.scale) { imageScale = image.scale; } + // Decompress image at required size UIGraphicsBeginImageContextWithOptions(imageSize, NO, imageScale); [image drawInRect:(CGRect){{0, 0}, imageSize}]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); - - if (!cached) { - RCTImageDownloader *strongSelf = weakSelf; - [strongSelf->_cache setData:UIImagePNGRepresentation(image) forKey:cacheKey]; - } } + // TODO: should we cache the decompressed image? + dispatch_async(dispatch_get_main_queue(), ^{ block(image, nil); }); diff --git a/Libraries/Image/RCTImageLoader.h b/Libraries/Image/RCTImageLoader.h new file mode 100644 index 000000000..3554f4b46 --- /dev/null +++ b/Libraries/Image/RCTImageLoader.h @@ -0,0 +1,13 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@class ALAssetsLibrary; +@class UIImage; + +@interface RCTImageLoader : NSObject + ++ (ALAssetsLibrary *)assetsLibrary; ++ (void)loadImageWithTag:(NSString *)tag callback:(void (^)(NSError *error, UIImage *image))callback; + +@end diff --git a/Libraries/Image/RCTImageLoader.m b/Libraries/Image/RCTImageLoader.m new file mode 100644 index 000000000..ec3e1dda2 --- /dev/null +++ b/Libraries/Image/RCTImageLoader.m @@ -0,0 +1,98 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTImageLoader.h" + +#import +#import +#import +#import +#import + +#import "RCTConvert.h" +#import "RCTImageDownloader.h" +#import "RCTLog.h" + +NSError *errorWithMessage(NSString *message) { + NSDictionary *errorInfo = @{NSLocalizedDescriptionKey: message}; + NSError *error = [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]; + return error; +} + +@implementation RCTImageLoader + ++ (ALAssetsLibrary *)assetsLibrary +{ + static ALAssetsLibrary *assetsLibrary = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + assetsLibrary = [[ALAssetsLibrary alloc] init]; + }); + return assetsLibrary; +} + ++ (void)loadImageWithTag:(NSString *)imageTag callback:(void (^)(NSError *error, UIImage *image))callback +{ + if ([imageTag hasPrefix:@"assets-library"]) { + [[RCTImageLoader assetsLibrary] assetForURL:[NSURL URLWithString:imageTag] resultBlock:^(ALAsset *asset) { + if (asset) { + ALAssetRepresentation *representation = [asset defaultRepresentation]; + ALAssetOrientation orientation = [representation orientation]; + UIImage *image = [UIImage imageWithCGImage:[representation fullResolutionImage] scale:1.0f orientation:(UIImageOrientation)orientation]; + callback(nil, image); + } else { + NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@ with no error message.", imageTag]; + NSError *error = errorWithMessage(errorText); + callback(error, nil); + } + } failureBlock:^(NSError *loadError) { + NSString *errorText = [NSString stringWithFormat:@"Failed to load asset at URL %@.\niOS Error: %@", imageTag, loadError]; + NSError *error = errorWithMessage(errorText); + callback(error, nil); + }]; + } else if ([imageTag hasPrefix:@"ph://"]) { + // Using PhotoKit for iOS 8+ + // '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 NSURL which is what assets-library is based on. + // This means if we use any FB standard photo picker, we will get this prefix =( + 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]; + NSError *error = errorWithMessage(errorText); + callback(error, nil); + return; + } + + PHAsset *asset = [results firstObject]; + [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:nil resultHandler:^(UIImage *result, NSDictionary *info) { + if (result) { + callback(nil, result); + } else { + NSString *errorText = [NSString stringWithFormat:@"Failed to load PHAsset with local identifier %@ with no error message.", phAssetID]; + NSError *error = errorWithMessage(errorText); + callback(error, nil); + return; + } + }]; + } else if ([imageTag hasPrefix:@"http"]) { + NSURL *url = [NSURL URLWithString:imageTag]; + if (!url) { + NSString *errorMessage = [NSString stringWithFormat:@"Invalid URL: %@", imageTag]; + callback(errorWithMessage(errorMessage), nil); + return; + } + [[RCTImageDownloader sharedInstance] downloadDataForURL:url block:^(NSData *data, NSError *error) { + if (error) { + callback(error, nil); + } else { + callback(nil, [UIImage imageWithData:data]); + } + }]; + } else { + NSString *errorMessage = [NSString stringWithFormat:@"Unrecognized tag protocol: %@", imageTag]; + NSError *error = errorWithMessage(errorMessage); + callback(error, nil); + } +} + +@end diff --git a/Libraries/Image/RCTNetworkImage.podspec b/Libraries/Image/RCTNetworkImage.podspec new file mode 100644 index 000000000..47eb0e5f6 --- /dev/null +++ b/Libraries/Image/RCTNetworkImage.podspec @@ -0,0 +1,28 @@ +Pod::Spec.new do |spec| + spec.name = 'RCTNetworkImage' + spec.version = '0.0.1' + spec.summary = 'Provides basic Text capabilities in ReactNative apps.' + spec.description = <<-DESC + Text can be rendered in ReactNative apps with the component using this module. + DESC + spec.homepage = 'https://facebook.github.io/react-native/' + spec.license = { :type => 'BSD' } + spec.author = 'Facebook' + spec.platform = :ios, '7.0' + spec.requires_arc = true + spec.source_files = '**/*.{h,m,c}' + spec.dependency "ReactKit", "~> 0.0.1" + + # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + # + # Link your library with frameworks, or libraries. Libraries do not include + # the lib prefix of their name. + # + + # s.framework = "SomeFramework" + # s.frameworks = "SomeFramework", "AnotherFramework" + + # s.library = "iconv" + #spec.libraries = "RCTNetworkImage", "ReactKit" + +end diff --git a/ReactKit/Views/RCTNetworkImageView.h b/Libraries/Image/RCTNetworkImageView.h similarity index 88% rename from ReactKit/Views/RCTNetworkImageView.h rename to Libraries/Image/RCTNetworkImageView.h index c99ed0689..920bf705c 100644 --- a/ReactKit/Views/RCTNetworkImageView.h +++ b/Libraries/Image/RCTNetworkImageView.h @@ -6,7 +6,8 @@ @interface RCTNetworkImageView : UIView -- (instancetype)initWithFrame:(CGRect)frame imageDownloader:(RCTImageDownloader *)imageDownloader; +- (instancetype)initWithFrame:(CGRect)frame + imageDownloader:(RCTImageDownloader *)imageDownloader NS_DESIGNATED_INITIALIZER; /** * An image that will appear while the view is loading the image from the network, diff --git a/ReactKit/Views/RCTNetworkImageView.m b/Libraries/Image/RCTNetworkImageView.m similarity index 91% rename from ReactKit/Views/RCTNetworkImageView.m rename to Libraries/Image/RCTNetworkImageView.m index 2a739cea7..1b19dfe19 100644 --- a/ReactKit/Views/RCTNetworkImageView.m +++ b/Libraries/Image/RCTNetworkImageView.m @@ -2,9 +2,10 @@ #import "RCTNetworkImageView.h" +#import "RCTConvert.h" +#import "RCTGIFImage.h" #import "RCTImageDownloader.h" #import "RCTUtils.h" -#import "RCTConvert.h" @implementation RCTNetworkImageView { @@ -27,11 +28,6 @@ return self; } -- (instancetype)initWithFrame:(CGRect)frame -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - - (NSURL *)imageURL { // We clear our backing layer's imageURL when we are not in a window for a while, @@ -52,15 +48,19 @@ if (reset) { self.layer.contentsScale = _defaultImage.scale; self.layer.contents = (__bridge id)_defaultImage.CGImage; + self.layer.minificationFilter = kCAFilterTrilinear; + self.layer.magnificationFilter = kCAFilterTrilinear; } if ([imageURL.pathExtension caseInsensitiveCompare:@"gif"] == NSOrderedSame) { _downloadToken = [_imageDownloader downloadDataForURL:imageURL block:^(NSData *data, NSError *error) { if (data) { - CAKeyframeAnimation *animation = [RCTConvert GIF:data]; + CAKeyframeAnimation *animation = RCTGIFImageWithData(data); CGImageRef firstFrame = (__bridge CGImageRef)animation.values.firstObject; self.layer.bounds = CGRectMake(0, 0, CGImageGetWidth(firstFrame), CGImageGetHeight(firstFrame)); self.layer.contentsScale = 1.0; self.layer.contentsGravity = kCAGravityResizeAspect; + self.layer.minificationFilter = kCAFilterLinear; + self.layer.magnificationFilter = kCAFilterLinear; [self.layer addAnimation:animation forKey:@"contents"]; } // TODO: handle errors diff --git a/ReactKit/Views/RCTNetworkImageViewManager.h b/Libraries/Image/RCTNetworkImageViewManager.h similarity index 100% rename from ReactKit/Views/RCTNetworkImageViewManager.h rename to Libraries/Image/RCTNetworkImageViewManager.h diff --git a/ReactKit/Views/RCTNetworkImageViewManager.m b/Libraries/Image/RCTNetworkImageViewManager.m similarity index 99% rename from ReactKit/Views/RCTNetworkImageViewManager.m rename to Libraries/Image/RCTNetworkImageViewManager.m index 5f8ad5d35..96d09c0cd 100644 --- a/ReactKit/Views/RCTNetworkImageViewManager.m +++ b/Libraries/Image/RCTNetworkImageViewManager.m @@ -23,4 +23,3 @@ RCT_REMAP_VIEW_PROPERTY(src, imageURL) RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode) @end - diff --git a/ReactKit/Views/RCTStaticImage.h b/Libraries/Image/RCTStaticImage.h similarity index 100% rename from ReactKit/Views/RCTStaticImage.h rename to Libraries/Image/RCTStaticImage.h diff --git a/ReactKit/Views/RCTStaticImage.m b/Libraries/Image/RCTStaticImage.m similarity index 86% rename from ReactKit/Views/RCTStaticImage.m rename to Libraries/Image/RCTStaticImage.m index 829044de5..e8378fc72 100644 --- a/ReactKit/Views/RCTStaticImage.m +++ b/Libraries/Image/RCTStaticImage.m @@ -20,7 +20,11 @@ if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, _capInsets)) { image = [image resizableImageWithCapInsets:_capInsets resizingMode:UIImageResizingModeStretch]; } - + + // Apply trilinear filtering to smooth out mis-sized images + self.layer.minificationFilter = kCAFilterTrilinear; + self.layer.magnificationFilter = kCAFilterTrilinear; + super.image = image; } diff --git a/ReactKit/Views/RCTStaticImageManager.h b/Libraries/Image/RCTStaticImageManager.h similarity index 100% rename from ReactKit/Views/RCTStaticImageManager.h rename to Libraries/Image/RCTStaticImageManager.h diff --git a/ReactKit/Views/RCTStaticImageManager.m b/Libraries/Image/RCTStaticImageManager.m similarity index 57% rename from ReactKit/Views/RCTStaticImageManager.m rename to Libraries/Image/RCTStaticImageManager.m index 2cd08e1e2..e292c2064 100644 --- a/ReactKit/Views/RCTStaticImageManager.m +++ b/Libraries/Image/RCTStaticImageManager.m @@ -4,8 +4,10 @@ #import -#import "RCTStaticImage.h" #import "RCTConvert.h" +#import "RCTGIFImage.h" +#import "RCTImageLoader.h" +#import "RCTStaticImage.h" @implementation RCTStaticImageManager @@ -16,12 +18,11 @@ RCT_EXPORT_VIEW_PROPERTY(capInsets) RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode) - -- (void)set_src:(id)json forView:(RCTStaticImage *)view withDefaultView:(RCTStaticImage *)defaultView +RCT_CUSTOM_VIEW_PROPERTY(src, RCTStaticImage) { if (json) { if ([[[json description] pathExtension] caseInsensitiveCompare:@"gif"] == NSOrderedSame) { - [view.layer addAnimation:[RCTConvert GIF:json] forKey:@"contents"]; + [view.layer addAnimation:RCTGIFImageWithFileURL([RCTConvert NSURL:json]) forKey:@"contents"]; } else { view.image = [RCTConvert UIImage:json]; } @@ -29,8 +30,7 @@ RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode) view.image = defaultView.image; } } - -- (void)set_tintColor:(id)json forView:(RCTStaticImage *)view withDefaultView:(RCTStaticImage *)defaultView +RCT_CUSTOM_VIEW_PROPERTY(tintColor, RCTStaticImage) { if (json) { view.renderingMode = UIImageRenderingModeAlwaysTemplate; @@ -40,6 +40,19 @@ RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode) view.tintColor = defaultView.tintColor; } } +RCT_CUSTOM_VIEW_PROPERTY(imageTag, RCTStaticImage) +{ + if (json) { + [RCTImageLoader loadImageWithTag:[RCTConvert NSString:json] callback:^(NSError *error, UIImage *image) { + if (error) { + RCTLogWarn(@"%@", error.localizedDescription); + } else { + view.image = image; + } + }]; + } else { + view.image = defaultView.image; + } +} @end - diff --git a/Libraries/Interaction/InteractionManager.js b/Libraries/Interaction/InteractionManager.js index 3e895f3b0..fbdd0cc9f 100644 --- a/Libraries/Interaction/InteractionManager.js +++ b/Libraries/Interaction/InteractionManager.js @@ -13,6 +13,14 @@ var invariant = require('invariant'); var keyMirror = require('keyMirror'); var setImmediate = require('setImmediate'); +var _emitter = new EventEmitter(); +var _interactionSet = new Set(); +var _addInteractionSet = new Set(); +var _deleteInteractionSet = new Set(); +var _nextUpdateHandle = null; +var _queue = []; +var _inc = 0; + /** * InteractionManager allows long-running work to be scheduled after any * interactions/animations have completed. In particular, this allows JavaScript @@ -20,11 +28,14 @@ var setImmediate = require('setImmediate'); * * Applications can schedule tasks to run after interactions with the following: * - * InteractionManager.runAfterInteractions(() => { - * // ...long-running synchronous task... - * }); + * ``` + * InteractionManager.runAfterInteractions(() => { + * // ...long-running synchronous task... + * }); + * ``` * * Compare this to other scheduling alternatives: + * * - requestAnimationFrame(): for code that animates a view over time. * - setImmediate/setTimeout(): run code later, note this may delay animations. * - runAfterInteractions(): run code later, without delaying active animations. @@ -37,27 +48,32 @@ var setImmediate = require('setImmediate'); * creating an interaction 'handle' on animation start, and clearing it upon * completion: * - * var handle = InteractionManager.createInteractionHandle(); - * // run animation... (`runAfterInteractions` tasks are queued) - * // later, on animation completion: - * InteractionManager.clearInteractionHandle(handle); - * // queued tasks run if all handles were cleared + * ``` + * var handle = InteractionManager.createInteractionHandle(); + * // run animation... (`runAfterInteractions` tasks are queued) + * // later, on animation completion: + * InteractionManager.clearInteractionHandle(handle); + * // queued tasks run if all handles were cleared + * ``` */ - -var _emitter = new EventEmitter(); -var _interactionSet = new Set(); -var _addInteractionSet = new Set(); -var _deleteInteractionSet = new Set(); -var _nextUpdateHandle = null; -var _queue = []; -var _inc = 0; - var InteractionManager = { Events: keyMirror({ interactionStart: true, interactionComplete: true, }), + /** + * Schedule a function to run after all interactions have completed. + */ + runAfterInteractions(callback) { + invariant( + typeof callback === 'function', + 'Must specify a function to schedule.' + ); + scheduleUpdate(); + _queue.push(callback); + }, + /** * Notify manager that an interaction has started. */ @@ -81,20 +97,6 @@ var InteractionManager = { _deleteInteractionSet.add(handle); }, - /** - * Schedule a function to run after all interactions have completed. - * - * @param {function} callback - */ - runAfterInteractions(callback) { - invariant( - typeof callback === 'function', - 'Must specify a function to schedule.' - ); - scheduleUpdate(); - _queue.push(callback); - }, - addListener: _emitter.addListener.bind(_emitter), }; diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js new file mode 100644 index 000000000..3b3a20e4e --- /dev/null +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -0,0 +1,71 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule ExceptionsManager + */ +'use strict'; + +var Platform = require('Platform'); +var RCTExceptionsManager = require('NativeModules').ExceptionsManager; + +var loadSourceMap = require('loadSourceMap'); +var parseErrorStack = require('parseErrorStack'); + +var sourceMapPromise; + +function handleException(e) { + var stack = parseErrorStack(e); + console.error( + 'Error: ' + + '\n stack: \n' + stackToString(stack) + + '\n URL: ' + e.sourceURL + + '\n line: ' + e.line + + '\n message: ' + e.message + ); + + if (RCTExceptionsManager) { + RCTExceptionsManager.reportUnhandledException(e.message, format(stack)); + if (__DEV__) { + (sourceMapPromise = sourceMapPromise || loadSourceMap()) + .then(map => { + var prettyStack = parseErrorStack(e, map); + RCTExceptionsManager.updateExceptionMessage(e.message, format(prettyStack)); + }) + .then(null, error => { + console.error('#CLOWNTOWN (error while displaying error): ' + error.message); + }); + } + } +} + +function stackToString(stack) { + var maxLength = Math.max.apply(null, stack.map(frame => frame.methodName.length)); + return stack.map(frame => stackFrameToString(frame, maxLength)).join('\n'); +} + +function stackFrameToString(stackFrame, maxLength) { + var fileNameParts = stackFrame.file.split('/'); + var fileName = fileNameParts[fileNameParts.length - 1]; + + if (fileName.length > 18) { + fileName = fileName.substr(0, 17) + '\u2026' /* ... */; + } + + var spaces = fillSpaces(maxLength - stackFrame.methodName.length); + return ' ' + stackFrame.methodName + spaces + ' ' + fileName + ':' + stackFrame.lineNumber; +} + +function fillSpaces(n) { + return new Array(n + 1).join(' '); +} + +// HACK(frantic) Android currently expects stack trace to be a string #5920439 +function format(stack) { + if (Platform.OS === 'android') { + return stackToString(stack); + } else { + return stack; + } +} + +module.exports = { handleException }; diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index b529460ee..e011ac089 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -17,16 +17,8 @@ /* eslint global-strict: 0 */ /* globals GLOBAL: true, window: true */ -var JSTimers = require('JSTimers'); - -// Just to make sure the JS gets packaged up +// Just to make sure the JS gets packaged up. require('RCTDeviceEventEmitter'); -var ErrorUtils = require('ErrorUtils'); -var RKAlertManager = require('RKAlertManager'); -var RKExceptionsManager = require('NativeModules').RKExceptionsManager; - -var errorToString = require('errorToString'); -var loadSourceMap = require('loadSourceMap'); if (typeof GLOBAL === 'undefined') { GLOBAL = this; @@ -36,33 +28,10 @@ if (typeof window === 'undefined') { window = GLOBAL; } -function handleErrorWithRedBox(e) { - GLOBAL.console.error( - 'Error: ' + - '\n stack: \n' + e.stack + - '\n URL: ' + e.sourceURL + - '\n line: ' + e.line + - '\n message: ' + e.message - ); - - if (RKExceptionsManager) { - RKExceptionsManager.reportUnhandledException(e.message, errorToString(e)); - if (__DEV__) { - try { - var sourceMapInstance = loadSourceMap(); - var prettyStack = errorToString(e, sourceMapInstance); - RKExceptionsManager.updateExceptionMessage(e.message, prettyStack); - } catch (ee) { - GLOBAL.console.error('#CLOWNTOWN (error while displaying error): ' + ee.message); - } - } - } -} - -function setupRedBoxErrorHandler() { - ErrorUtils.setGlobalHandler(handleErrorWithRedBox); -} - +/** + * The document must be shimmed before anything else that might define the + * `ExecutionEnvironment` module (which checks for `document.createElement`). + */ function setupDocumentShim() { // The browser defines Text and Image globals by default. If you forget to // require them, then the error message is very confusing. @@ -82,24 +51,34 @@ function setupDocumentShim() { throw getInvalidGlobalUseError('Image'); } }; + // Force `ExecutionEnvironment.canUseDOM` to be false. + if (GLOBAL.document) { + GLOBAL.document.createElement = null; + } +} - GLOBAL.document = { - // This shouldn't be needed but scroller library fails without it. If - // we fixed the scroller, we wouldn't need this. - body: {}, - // Workaround for setImmediate - createElement: function() {return {};} - }; +function handleErrorWithRedBox(e) { + try { + require('ExceptionsManager').handleException(e); + } catch(ee) { + console.log('Failed to print error: ', ee.message); + } +} + +function setupRedBoxErrorHandler() { + var ErrorUtils = require('ErrorUtils'); + ErrorUtils.setGlobalHandler(handleErrorWithRedBox); } /** * Sets up a set of window environment wrappers that ensure that the * BatchedBridge is flushed after each tick. In both the case of the - * `UIWebView` based `RKJavaScriptCaller` and `RKContextCaller`, we + * `UIWebView` based `RCTJavaScriptCaller` and `RCTContextCaller`, we * implement our own custom timing bridge that should be immune to * unexplainably dropped timing signals. */ function setupTimers() { + var JSTimers = require('JSTimers'); GLOBAL.setTimeout = JSTimers.setTimeout; GLOBAL.setInterval = JSTimers.setInterval; GLOBAL.setImmediate = JSTimers.setImmediate; @@ -114,6 +93,7 @@ function setupTimers() { } function setupAlert() { + var RCTAlertManager = require('NativeModules').AlertManager; if (!GLOBAL.alert) { GLOBAL.alert = function(text) { var alertOpts = { @@ -121,7 +101,7 @@ function setupAlert() { message: '' + text, buttons: [{'cancel': 'Okay'}], }; - RKAlertManager.alertWithArgs(alertOpts, null); + RCTAlertManager.alertWithArgs(alertOpts, null); }; } } @@ -139,9 +119,15 @@ function setupXHR() { GLOBAL.fetch = require('fetch'); } -setupRedBoxErrorHandler(); +function setupGeolocation() { + GLOBAL.navigator = GLOBAL.navigator || {}; + GLOBAL.navigator.geolocation = require('Geolocation'); +} + setupDocumentShim(); +setupRedBoxErrorHandler(); setupTimers(); setupAlert(); setupPromise(); setupXHR(); +setupGeolocation(); diff --git a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js index 076f9fa5c..0af19b27e 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js +++ b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js @@ -2,23 +2,42 @@ * Copyright 2004-present Facebook. All Rights Reserved. * * @providesModule loadSourceMap + * @flow */ 'use strict'; +var Promise = require('Promise'); +var RCTSourceCode = require('NativeModules').SourceCode; var SourceMapConsumer = require('SourceMap').SourceMapConsumer; +var SourceMapURL = require('./source-map-url'); -var sourceMapInstance; +var fetch = require('fetch'); -function loadSourceMap() { - if (sourceMapInstance !== undefined) { - return sourceMapInstance; +function loadSourceMap(): Promise { + return fetchSourceMap() + .then(map => new SourceMapConsumer(map)); +} + +function fetchSourceMap(): Promise { + if (global.RAW_SOURCE_MAP) { + return Promise.resolve(global.RAW_SOURCE_MAP); } - if (!global.RAW_SOURCE_MAP) { - return null; + + if (!RCTSourceCode) { + return Promise.reject(new Error('RCTSourceCode module is not available')); } - sourceMapInstance = new SourceMapConsumer(global.RAW_SOURCE_MAP); - return sourceMapInstance; + + return new Promise(RCTSourceCode.getScriptText) + .then(extractSourceMapURL) + .then(fetch) + .then(response => response.text()) +} + +function extractSourceMapURL({url, text}): string { + var mapURL = SourceMapURL.getFrom(text); + var baseURL = url.match(/(.+:\/\/.*?)\//)[1]; + return baseURL + mapURL; } module.exports = loadSourceMap; diff --git a/Libraries/JavaScriptAppEngine/Initialization/errorToString.js b/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js similarity index 55% rename from Libraries/JavaScriptAppEngine/Initialization/errorToString.js rename to Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js index 652d6c2af..85a836705 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/errorToString.js +++ b/Libraries/JavaScriptAppEngine/Initialization/parseErrorStack.js @@ -1,21 +1,12 @@ /** * Copyright 2004-present Facebook. All Rights Reserved. * - * @providesModule errorToString + * @providesModule parseErrorStack */ 'use strict'; -var Platform = require('Platform'); - var stacktraceParser = require('stacktrace-parser'); -function stackFrameToString(stackFrame) { - var fileNameParts = stackFrame.file.split('/'); - var fileName = fileNameParts[fileNameParts.length - 1]; - - return stackFrame.methodName + '\n in ' + fileName + ':' + stackFrame.lineNumber + '\n'; -} - function resolveSourceMaps(sourceMapInstance, stackFrame) { try { var orig = sourceMapInstance.originalPositionFor({ @@ -31,7 +22,7 @@ function resolveSourceMaps(sourceMapInstance, stackFrame) { } } -function errorToString(e, sourceMapInstance) { +function parseErrorStack(e, sourceMapInstance) { var stack = stacktraceParser.parse(e.stack); var framesToPop = e.framesToPop || 0; @@ -43,12 +34,7 @@ function errorToString(e, sourceMapInstance) { stack.forEach(resolveSourceMaps.bind(null, sourceMapInstance)); } - // HACK(frantic) Android currently expects stack trace to be a string #5920439 - if (Platform.OS === 'android') { - return stack.map(stackFrameToString).join('\n'); - } else { - return stack; - } + return stack; } -module.exports = errorToString; +module.exports = parseErrorStack; diff --git a/Libraries/JavaScriptAppEngine/Initialization/source-map-url.js b/Libraries/JavaScriptAppEngine/Initialization/source-map-url.js new file mode 100644 index 000000000..c3a53f234 --- /dev/null +++ b/Libraries/JavaScriptAppEngine/Initialization/source-map-url.js @@ -0,0 +1,73 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * This is a third-party micro-library grabbed from: + * https://github.com/lydell/source-map-url + * + * @nolint + */ + +(function() { + var define = null; // Hack to make it work with our packager + +// Copyright 2014 Simon Lydell +// X11 (“MIT”) Licensed. (See LICENSE.) + +void (function(root, factory) { + if (typeof define === "function" && define.amd) { + define(factory) + } else if (typeof exports === "object") { + module.exports = factory() + } else { + root.sourceMappingURL = factory() + } +}(this, function() { + + var innerRegex = /[#@] sourceMappingURL=([^\s'"]*)/ + + var regex = RegExp( + "(?:" + + "/\\*" + + "(?:\\s*\r?\n(?://)?)?" + + "(?:" + innerRegex.source + ")" + + "\\s*" + + "\\*/" + + "|" + + "//(?:" + innerRegex.source + ")" + + ")" + + "\\s*$" + ) + + return { + + regex: regex, + _innerRegex: innerRegex, + + getFrom: function(code) { + var match = code.match(regex) + return (match ? match[1] || match[2] || "" : null) + }, + + existsIn: function(code) { + return regex.test(code) + }, + + removeFrom: function(code) { + return code.replace(regex, "") + }, + + insertBefore: function(code, string) { + var match = code.match(regex) + if (match) { + return code.slice(0, match.index) + string + code.slice(match.index) + } else { + return code + string + } + } + } + +})); + +/** End of the third-party code */ + +})(); diff --git a/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js b/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js index 13297ef00..4fa4b8587 100644 --- a/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js +++ b/Libraries/JavaScriptAppEngine/System/JSTimers/JSTimers.js @@ -7,7 +7,7 @@ // Note that the module JSTimers is split into two in order to solve a cycle // in dependencies. NativeModules > BatchedBridge > MessageQueue > JSTimersExecution -var RKTiming = require('NativeModules').RKTiming; +var RCTTiming = require('NativeModules').Timing; var JSTimersExecution = require('JSTimersExecution'); /** @@ -43,7 +43,7 @@ var JSTimers = { return func.apply(undefined, args); }; JSTimersExecution.types[freeIndex] = JSTimersExecution.Type.setTimeout; - RKTiming.createTimer(newID, duration, Date.now(), /** recurring */ false); + RCTTiming.createTimer(newID, duration, Date.now(), /** recurring */ false); return newID; }, @@ -60,7 +60,7 @@ var JSTimers = { return func.apply(undefined, args); }; JSTimersExecution.types[freeIndex] = JSTimersExecution.Type.setInterval; - RKTiming.createTimer(newID, duration, Date.now(), /** recurring */ true); + RCTTiming.createTimer(newID, duration, Date.now(), /** recurring */ true); return newID; }, @@ -90,7 +90,7 @@ var JSTimers = { JSTimersExecution.timerIDs[freeIndex] = newID; JSTimersExecution.callbacks[freeIndex] = func; JSTimersExecution.types[freeIndex] = JSTimersExecution.Type.requestAnimationFrame; - RKTiming.createTimer(newID, 0, Date.now(), /** recurring */ false); + RCTTiming.createTimer(newID, 1, Date.now(), /** recurring */ false); return newID; }, @@ -126,7 +126,7 @@ var JSTimers = { if (index !== -1) { JSTimersExecution._clearIndex(index); if (JSTimersExecution.types[index] !== JSTimersExecution.Type.setImmediate) { - RKTiming.deleteTimer(timerID); + RCTTiming.deleteTimer(timerID); } } }, diff --git a/Libraries/Network/NetInfo.js b/Libraries/Network/NetInfo.js new file mode 100644 index 000000000..b228547a6 --- /dev/null +++ b/Libraries/Network/NetInfo.js @@ -0,0 +1,143 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule NetInfo + * @flow + */ +'use strict'; + +var NativeModules = require('NativeModules'); +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTReachability = NativeModules.Reachability; + +var DEVICE_REACHABILITY_EVENT = 'reachabilityDidChange'; + +type ChangeEventName = $Enum<{ + change: string; +}>; + + +/** + * NetInfo exposes info about online/offline status + * + * == iOS Reachability + * + * Asyncronously determine if the device is online and on a cellular network. + * + * - "none" - device is offline + * - "wifi" - device is online and connected via wifi, or is the iOS simulator + * - "cell" - device is connected via Edge, 3G, WiMax, or LTE + * - "unknown" - error case and the network status is unknown + * + * ``` + * NetInfo.reachabilityIOS.fetch().done((reach) => { + * console.log('Initial: ' + reach); + * }); + * function handleFirstReachabilityChange(reach) { + * console.log('First change: ' + reach); + * NetInfo.reachabilityIOS.removeEventListener( + * 'change', + * handleFirstReachabilityChange + * ); + * } + * NetInfo.reachabilityIOS.addEventListener( + * 'change', + * handleFirstReachabilityChange + * ); + * ``` + */ + +var NetInfo = {}; + +if (RCTReachability) { + var _reachabilitySubscriptions = {}; + + NetInfo.reachabilityIOS = { + addEventListener: function ( + eventName: ChangeEventName, + handler: Function + ): void { + _reachabilitySubscriptions[handler] = RCTDeviceEventEmitter.addListener( + DEVICE_REACHABILITY_EVENT, + (appStateData) => { + handler(appStateData.network_reachability); + } + ); + }, + + removeEventListener: function( + eventName: ChangeEventName, + handler: Function + ): void { + if (!_reachabilitySubscriptions[handler]) { + return; + } + _reachabilitySubscriptions[handler].remove(); + _reachabilitySubscriptions[handler] = null; + }, + + fetch: function(): Promise { + return new Promise((resolve, reject) => { + RCTReachability.getCurrentReachability( + (resp) => { + resolve(resp.network_reachability); + }, + reject + ); + }); + }, + }; + + /** + * + * == NetInfo.isConnected + * + * Available on all platforms. Asyncronously fetch a boolean to determine + * internet connectivity. + * + * ``` + * NetInfo.isConnected.fetch().done((isConnected) => { + * console.log('First, is ' + (isConnected ? 'online' : 'offline')); + * }); + * function handleFirstConnectivityChange(isConnected) { + * console.log('Then, is ' + (isConnected ? 'online' : 'offline')); + * NetInfo.isConnected.removeEventListener( + * 'change', + * handleFirstConnectivityChange + * ); + * } + * NetInfo.isConnected.addEventListener( + * 'change', + * handleFirstConnectivityChange + * ); + * ``` + * + */ + var _isConnectedSubscriptions = {}; + NetInfo.isConnected = { + addEventListener: function ( + eventName: ChangeEventName, + handler: Function + ): void { + _isConnectedSubscriptions[handler] = (reachability) => { + handler(reachability !== 'none'); + }; + NetInfo.reachabilityIOS.addEventListener(eventName, _isConnectedSubscriptions[handler]); + }, + + removeEventListener: function( + eventName: ChangeEventName, + handler: Function + ): void { + NetInfo.reachabilityIOS.removeEventListener(eventName, _isConnectedSubscriptions[handler]); + }, + + fetch: function(): Promise { + return NetInfo.reachabilityIOS.fetch().then( + (reachability) => reachability !== 'none' + ); + }, + }; +} + +module.exports = NetInfo; diff --git a/ReactKit/Modules/RCTDataManager.h b/Libraries/Network/RCTDataManager.h similarity index 100% rename from ReactKit/Modules/RCTDataManager.h rename to Libraries/Network/RCTDataManager.h diff --git a/ReactKit/Modules/RCTDataManager.m b/Libraries/Network/RCTDataManager.m similarity index 91% rename from ReactKit/Modules/RCTDataManager.m rename to Libraries/Network/RCTDataManager.m index 22872fa91..9bfd9824b 100644 --- a/ReactKit/Modules/RCTDataManager.m +++ b/Libraries/Network/RCTDataManager.m @@ -50,8 +50,9 @@ } else { encoding = NSUTF8StringEncoding; } + int responseCode = (int)[((NSHTTPURLResponse *)response) statusCode]; NSString *returnData = [[NSString alloc] initWithData:data encoding:encoding]; - responseJSON = @{@"status": @200, @"responseText": returnData}; + responseJSON = @{@"status": @(responseCode), @"responseText": returnData}; } else { responseJSON = @{@"status": @0, @"responseText": [connectionError localizedDescription]}; } @@ -65,8 +66,7 @@ } else { - RCTLogMustFix(@"unsupported query type %@", queryType); - return; + RCTLogError(@"unsupported query type %@", queryType); } } diff --git a/Libraries/Network/RCTDataManager.podspec b/Libraries/Network/RCTDataManager.podspec new file mode 100644 index 000000000..84df787b8 --- /dev/null +++ b/Libraries/Network/RCTDataManager.podspec @@ -0,0 +1,28 @@ +Pod::Spec.new do |spec| + spec.name = 'RCTDataManager' + spec.version = '0.0.1' + spec.summary = 'Provides basic networking capabilities in ReactNative apps.' + spec.description = <<-DESC + Use XMLHttpRequest to fetch data over the network. + DESC + spec.homepage = 'https://facebook.github.io/react-native/' + spec.license = { :type => 'BSD' } + spec.author = 'Facebook' + spec.platform = :ios, '7.0' + spec.requires_arc = true + spec.source_files = '**/*.{h,m,c}' + spec.dependency "ReactKit", "~> 0.0.1" + + # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + # + # Link your library with frameworks, or libraries. Libraries do not include + # the lib prefix of their name. + # + + # s.framework = "SomeFramework" + # s.frameworks = "SomeFramework", "AnotherFramework" + + # s.library = "iconv" + #spec.libraries = "RCTDataManager", "ReactKit" + +end diff --git a/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj new file mode 100644 index 000000000..f2f3f6a33 --- /dev/null +++ b/Libraries/Network/RCTNetwork.xcodeproj/project.pbxproj @@ -0,0 +1,262 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7361AB03E7B00659ED6 /* RCTReachability.m */; }; + 58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512071A9E6CE300147676 /* RCTDataManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511D91A9E6C8500147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* 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 = ""; }; + 58B511DB1A9E6C8500147676 /* libRCTNetwork.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTNetwork.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 58B512061A9E6CE300147676 /* RCTDataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataManager.h; sourceTree = ""; }; + 58B512071A9E6CE300147676 /* RCTDataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDataManager.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511D81A9E6C8500147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 58B511D21A9E6C8500147676 = { + isa = PBXGroup; + children = ( + 58B512061A9E6CE300147676 /* RCTDataManager.h */, + 58B512071A9E6CE300147676 /* RCTDataManager.m */, + 1372B7351AB03E7B00659ED6 /* RCTReachability.h */, + 1372B7361AB03E7B00659ED6 /* RCTReachability.m */, + 58B511DC1A9E6C8500147676 /* Products */, + ); + sourceTree = ""; + }; + 58B511DC1A9E6C8500147676 /* Products */ = { + isa = PBXGroup; + children = ( + 58B511DB1A9E6C8500147676 /* libRCTNetwork.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B511DA1A9E6C8500147676 /* RCTNetwork */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTNetwork" */; + buildPhases = ( + 58B511D71A9E6C8500147676 /* Sources */, + 58B511D81A9E6C8500147676 /* Frameworks */, + 58B511D91A9E6C8500147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTNetwork; + productName = RCTDataManager; + productReference = 58B511DB1A9E6C8500147676 /* libRCTNetwork.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511D31A9E6C8500147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B511DA1A9E6C8500147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTNetwork" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511D21A9E6C8500147676; + productRefGroup = 58B511DC1A9E6C8500147676 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B511DA1A9E6C8500147676 /* RCTNetwork */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511D71A9E6C8500147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1372B7371AB03E7B00659ED6 /* RCTReachability.m in Sources */, + 58B512081A9E6CE300147676 /* RCTDataManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511ED1A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511EE1A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511F01A9E6C8500147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "/Users/nicklockwood/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/ReactKit/build/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTNetwork; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511F11A9E6C8500147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "/Users/nicklockwood/fbobjc-hg/Libraries/FBReactKit/js/react-native-github/ReactKit/build/Debug-iphoneos", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = RCTNetwork; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511D61A9E6C8500147676 /* Build configuration list for PBXProject "RCTNetwork" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511ED1A9E6C8500147676 /* Debug */, + 58B511EE1A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511EF1A9E6C8500147676 /* Build configuration list for PBXNativeTarget "RCTNetwork" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511F01A9E6C8500147676 /* Debug */, + 58B511F11A9E6C8500147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511D31A9E6C8500147676 /* Project object */; +} diff --git a/Libraries/Network/RCTReachability.h b/Libraries/Network/RCTReachability.h new file mode 100644 index 000000000..c0bca96c7 --- /dev/null +++ b/Libraries/Network/RCTReachability.h @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "RCTBridgeModule.h" + +@interface RCTReachability : NSObject + +- (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER; + +@end diff --git a/Libraries/Network/RCTReachability.m b/Libraries/Network/RCTReachability.m new file mode 100644 index 000000000..921d339bb --- /dev/null +++ b/Libraries/Network/RCTReachability.m @@ -0,0 +1,85 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTReachability.h" + +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" + +static NSString *const RCTReachabilityStateUnknown = @"unknown"; +static NSString *const RCTReachabilityStateNone = @"none"; +static NSString *const RCTReachabilityStateWifi = @"wifi"; +static NSString *const RCTReachabilityStateCell = @"cell"; + +@implementation RCTReachability +{ + SCNetworkReachabilityRef _reachability; + NSString *_status; +} + +@synthesize bridge = _bridge; + +static void RCTReachabilityCallback(__unused SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void *info) +{ + RCTReachability *self = (__bridge id)info; + NSString *status = RCTReachabilityStateUnknown; + if ((flags & kSCNetworkReachabilityFlagsReachable) == 0 || + (flags & kSCNetworkReachabilityFlagsConnectionRequired) != 0) { + status = RCTReachabilityStateNone; + } + +#if TARGET_OS_IPHONE + + else if ((flags & kSCNetworkReachabilityFlagsIsWWAN) != 0) { + status = RCTReachabilityStateCell; + } + +#endif + + else { + status = RCTReachabilityStateWifi; + } + + if (![status isEqualToString:self->_status]) { + self->_status = status; + [self->_bridge.eventDispatcher sendDeviceEventWithName:@"reachabilityDidChange" + body:@{@"network_reachability": status}]; + } +} + +#pragma mark - Lifecycle + +- (instancetype)initWithHost:(NSString *)host +{ + if ((self = [super init])) { + _status = RCTReachabilityStateUnknown; + _reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [host UTF8String]); + SCNetworkReachabilityContext context = { 0, ( __bridge void *)self, NULL, NULL, NULL }; + SCNetworkReachabilitySetCallback(_reachability, RCTReachabilityCallback, &context); + SCNetworkReachabilityScheduleWithRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); + } + return self; +} + +- (instancetype)init +{ + return [self initWithHost:@"http://apple.com"]; +} + +- (void)dealloc +{ + SCNetworkReachabilityUnscheduleFromRunLoop(_reachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); + CFRelease(_reachability); +} + +#pragma mark - Public API + +// TODO: remove error callback - not needed except by Subscribable interface +- (void)getCurrentReachability:(RCTResponseSenderBlock)getSuccess + withErrorCallback:(__unused RCTResponseSenderBlock)getError +{ + RCT_EXPORT(); + + getSuccess(@[@{@"network_reachability": _status}]); +} + +@end diff --git a/Libraries/XMLHttpRequest/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js similarity index 97% rename from Libraries/XMLHttpRequest/XMLHttpRequest.ios.js rename to Libraries/Network/XMLHttpRequest.ios.js index 042cbe4b9..d1e2e92e8 100644 --- a/Libraries/XMLHttpRequest/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -6,7 +6,7 @@ */ 'use strict'; -var RKDataManager = require('NativeModulesDeprecated').RKDataManager; +var RCTDataManager = require('NativeModules').DataManager; var crc32 = require('crc32'); @@ -91,7 +91,7 @@ class XMLHttpRequest { } this._sent = true; - RKDataManager.queryData( + RCTDataManager.queryData( 'http', JSON.stringify({ method: this._method, diff --git a/Libraries/Picker/PickerIOS.android.js b/Libraries/Picker/PickerIOS.android.js new file mode 100644 index 000000000..51103157a --- /dev/null +++ b/Libraries/Picker/PickerIOS.android.js @@ -0,0 +1,10 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule PickerIOS + * + * This is a controlled component version of RCTPickerIOS + */ +'use strict'; + +module.exports = require('UnimplementedView'); diff --git a/Libraries/Picker/PickerIOS.ios.js b/Libraries/Picker/PickerIOS.ios.js new file mode 100644 index 000000000..69d163cf5 --- /dev/null +++ b/Libraries/Picker/PickerIOS.ios.js @@ -0,0 +1,120 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule PickerIOS + * + * This is a controlled component version of RCTPickerIOS + */ +'use strict'; + +var NativeMethodsMixin = require('NativeMethodsMixin'); +var React = require('React'); +var ReactChildren = require('ReactChildren'); +var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); +var RCTPickerIOSConsts = require('NativeModules').UIManager.RCTPicker.Constants; +var StyleSheet = require('StyleSheet'); +var View = require('View'); + +var createReactIOSNativeComponentClass = + require('createReactIOSNativeComponentClass'); +var merge = require('merge'); + +var PICKER = 'picker'; + +var PickerIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + onValueChange: React.PropTypes.func, + selectedValue: React.PropTypes.any, // string or integer basically + }, + + getInitialState: function() { + return this._stateFromProps(this.props); + }, + + componentWillReceiveProps: function(nextProps) { + this.setState(this._stateFromProps(nextProps)); + }, + + // Translate PickerIOS prop and children into stuff that RCTPickerIOS understands. + _stateFromProps: function(props) { + var selectedIndex = 0; + var items = []; + ReactChildren.forEach(props.children, function (child, index) { + if (child.props.value === props.selectedValue) { + selectedIndex = index; + } + items.push({value: child.props.value, label: child.props.label}); + }); + return {selectedIndex, items}; + }, + + render: function() { + return ( + + + + ); + }, + + _onChange: function(event) { + if (this.props.onChange) { + this.props.onChange(event); + } + if (this.props.onValueChange) { + this.props.onValueChange(event.nativeEvent.newValue); + } + + // The picker is a controlled component. This means we expect the + // on*Change handlers to be in charge of updating our + // `selectedValue` prop. That way they can also + // disallow/undo/mutate the selection of certain values. In other + // words, the embedder of this component should be the source of + // truth, not the native component. + if (this.state.selectedIndex !== event.nativeEvent.newIndex) { + this.refs[PICKER].setNativeProps({ + selectedIndex: this.state.selectedIndex + }); + } + }, +}); + +PickerIOS.Item = React.createClass({ + propTypes: { + value: React.PropTypes.any, // string or integer basically + label: React.PropTypes.string, + }, + + render: function() { + // These items don't get rendered directly. + return null; + }, +}); + +var styles = StyleSheet.create({ + rkPickerIOS: { + // The picker will conform to whatever width is given, but we do + // have to set the component's height explicitly on the + // surrounding view to ensure it gets rendered. + height: RCTPickerIOSConsts.ComponentHeight, + }, +}); + +var rkPickerIOSAttributes = merge(ReactIOSViewAttributes.UIView, { + items: true, + selectedIndex: true, +}); + +var RCTPickerIOS = createReactIOSNativeComponentClass({ + validAttributes: rkPickerIOSAttributes, + uiViewClassName: 'RCTPicker', +}); + +module.exports = PickerIOS; diff --git a/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj b/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj new file mode 100644 index 000000000..f377b4c98 --- /dev/null +++ b/Libraries/RCTTest/RCTTest.xcodeproj/project.pbxproj @@ -0,0 +1,264 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 585135371AB3C56F00882537 /* RCTTestModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 585135341AB3C56F00882537 /* RCTTestModule.m */; }; + 585135381AB3C57000882537 /* RCTTestRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 585135361AB3C56F00882537 /* RCTTestRunner.m */; }; + 585135391AB3C59A00882537 /* RCTTestRunner.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 585135351AB3C56F00882537 /* RCTTestRunner.h */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 580C376D1AB104AF0015E709 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + 585135391AB3C59A00882537 /* RCTTestRunner.h in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 580C376F1AB104AF0015E709 /* libRCTTest.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTTest.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 585135331AB3C56F00882537 /* RCTTestModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTestModule.h; sourceTree = ""; }; + 585135341AB3C56F00882537 /* RCTTestModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTestModule.m; sourceTree = ""; }; + 585135351AB3C56F00882537 /* RCTTestRunner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTestRunner.h; sourceTree = ""; }; + 585135361AB3C56F00882537 /* RCTTestRunner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTestRunner.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 580C376C1AB104AF0015E709 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 580C37661AB104AF0015E709 = { + isa = PBXGroup; + children = ( + 585135331AB3C56F00882537 /* RCTTestModule.h */, + 585135341AB3C56F00882537 /* RCTTestModule.m */, + 585135351AB3C56F00882537 /* RCTTestRunner.h */, + 585135361AB3C56F00882537 /* RCTTestRunner.m */, + 580C37701AB104AF0015E709 /* Products */, + ); + sourceTree = ""; + }; + 580C37701AB104AF0015E709 /* Products */ = { + isa = PBXGroup; + children = ( + 580C376F1AB104AF0015E709 /* libRCTTest.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 580C376E1AB104AF0015E709 /* RCTTest */ = { + isa = PBXNativeTarget; + buildConfigurationList = 580C37831AB104AF0015E709 /* Build configuration list for PBXNativeTarget "RCTTest" */; + buildPhases = ( + 580C376B1AB104AF0015E709 /* Sources */, + 580C376C1AB104AF0015E709 /* Frameworks */, + 580C376D1AB104AF0015E709 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTTest; + productName = RCTTest; + productReference = 580C376F1AB104AF0015E709 /* libRCTTest.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 580C37671AB104AF0015E709 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 580C376E1AB104AF0015E709 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 580C376A1AB104AF0015E709 /* Build configuration list for PBXProject "RCTTest" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 580C37661AB104AF0015E709; + productRefGroup = 580C37701AB104AF0015E709 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 580C376E1AB104AF0015E709 /* RCTTest */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 580C376B1AB104AF0015E709 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 585135371AB3C56F00882537 /* RCTTestModule.m in Sources */, + 585135381AB3C57000882537 /* RCTTestRunner.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 580C37811AB104AF0015E709 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 580C37821AB104AF0015E709 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 8.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 580C37841AB104AF0015E709 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "-ObjC", + "-framework", + XCTest, + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 580C37851AB104AF0015E709 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "-ObjC", + "-framework", + XCTest, + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 580C376A1AB104AF0015E709 /* Build configuration list for PBXProject "RCTTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 580C37811AB104AF0015E709 /* Debug */, + 580C37821AB104AF0015E709 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 580C37831AB104AF0015E709 /* Build configuration list for PBXNativeTarget "RCTTest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 580C37841AB104AF0015E709 /* Debug */, + 580C37851AB104AF0015E709 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 580C37671AB104AF0015E709 /* Project object */; +} diff --git a/Libraries/RCTTest/RCTTestModule.h b/Libraries/RCTTest/RCTTestModule.h new file mode 100644 index 000000000..f439df7b2 --- /dev/null +++ b/Libraries/RCTTest/RCTTestModule.h @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTBridgeModule.h" + +@interface RCTTestModule : NSObject + +@property (nonatomic, readonly, getter=isDone) BOOL done; + +@end diff --git a/Libraries/RCTTest/RCTTestModule.m b/Libraries/RCTTest/RCTTestModule.m new file mode 100644 index 000000000..eeb73734c --- /dev/null +++ b/Libraries/RCTTest/RCTTestModule.m @@ -0,0 +1,14 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTTestModule.h" + +@implementation RCTTestModule + +- (void)markTestCompleted +{ + RCT_EXPORT(); + + _done = YES; +} + +@end diff --git a/Libraries/RCTTest/RCTTestRunner.h b/Libraries/RCTTest/RCTTestRunner.h new file mode 100644 index 000000000..a774088dc --- /dev/null +++ b/Libraries/RCTTest/RCTTestRunner.h @@ -0,0 +1,14 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@interface RCTTestRunner : NSObject + +@property (nonatomic, copy) NSString *script; + +- (instancetype)initWithApp:(NSString *)app; +- (void)runTest:(NSString *)moduleName; +- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)expectErrorRegex; +- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock; + +@end diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m new file mode 100644 index 000000000..87da43010 --- /dev/null +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -0,0 +1,64 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTTestRunner.h" + +#import "RCTRedBox.h" +#import "RCTRootView.h" +#import "RCTTestModule.h" +#import "RCTUtils.h" + +#define TIMEOUT_SECONDS 240 + +@implementation RCTTestRunner + +- (instancetype)initWithApp:(NSString *)app +{ + if (self = [super init]) { + _script = [NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]; + } + return self; +} + +- (void)runTest:(NSString *)moduleName +{ + [self runTest:moduleName initialProps:nil expectErrorBlock:nil]; +} + +- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorRegex:(NSRegularExpression *)errorRegex +{ + [self runTest:moduleName initialProps:initialProps expectErrorBlock:^BOOL(NSString *error){ + return [errorRegex numberOfMatchesInString:error options:0 range:NSMakeRange(0, [error length])] > 0; + }]; +} + +- (void)runTest:(NSString *)moduleName initialProps:(NSDictionary *)initialProps expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock +{ + RCTTestModule *testModule = [[RCTTestModule alloc] init]; + RCTRootView *rootView = [[RCTRootView alloc] init]; + UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; + vc.view = rootView; + rootView.moduleProvider = ^(void){ + return @[testModule]; + }; + rootView.moduleName = moduleName; + rootView.initialProperties = initialProps; + rootView.scriptURL = [NSURL URLWithString:_script]; + + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; + NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage]; + while ([date timeIntervalSinceNow] > 0 && ![testModule isDone] && error == nil) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date]; + error = [[RCTRedBox sharedInstance] currentErrorMessage]; + } + [[RCTRedBox sharedInstance] dismiss]; + if (expectErrorBlock) { + RCTAssert(expectErrorBlock(error), @"Expected an error but got none."); + } else if (error) { + RCTAssert(error == nil, @"RedBox error: %@", error); + } else { + RCTAssert([testModule isDone], @"Test didn't finish within %d seconds", TIMEOUT_SECONDS); + } +} + +@end diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj b/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj new file mode 100644 index 000000000..07d24eb36 --- /dev/null +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj @@ -0,0 +1,260 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 00D277161AB8C32C00DC1E48 /* RCTWebSocketExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */; }; + 00D277191AB8C35800DC1E48 /* SRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D277181AB8C35800DC1E48 /* SRWebSocket.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 832C817E1AAF6DEF007FA2F7 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 00D277141AB8C32C00DC1E48 /* RCTWebSocketExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketExecutor.h; sourceTree = ""; }; + 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketExecutor.m; sourceTree = ""; }; + 00D277171AB8C35800DC1E48 /* SRWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRWebSocket.h; sourceTree = ""; }; + 00D277181AB8C35800DC1E48 /* SRWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRWebSocket.m; sourceTree = ""; }; + 832C81801AAF6DEF007FA2F7 /* libRCTWebSocketDebugger.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTWebSocketDebugger.a; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 832C817D1AAF6DEF007FA2F7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 832C81771AAF6DEF007FA2F7 = { + isa = PBXGroup; + children = ( + 00D277171AB8C35800DC1E48 /* SRWebSocket.h */, + 00D277181AB8C35800DC1E48 /* SRWebSocket.m */, + 00D277141AB8C32C00DC1E48 /* RCTWebSocketExecutor.h */, + 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */, + 832C81811AAF6DEF007FA2F7 /* Products */, + ); + sourceTree = ""; + }; + 832C81811AAF6DEF007FA2F7 /* Products */ = { + isa = PBXGroup; + children = ( + 832C81801AAF6DEF007FA2F7 /* libRCTWebSocketDebugger.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 832C817F1AAF6DEF007FA2F7 /* RCTWebSocketDebugger */ = { + isa = PBXNativeTarget; + buildConfigurationList = 832C81941AAF6DF0007FA2F7 /* Build configuration list for PBXNativeTarget "RCTWebSocketDebugger" */; + buildPhases = ( + 832C817C1AAF6DEF007FA2F7 /* Sources */, + 832C817D1AAF6DEF007FA2F7 /* Frameworks */, + 832C817E1AAF6DEF007FA2F7 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTWebSocketDebugger; + productName = RCTWebSocketDebugger; + productReference = 832C81801AAF6DEF007FA2F7 /* libRCTWebSocketDebugger.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 832C81781AAF6DEF007FA2F7 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0620; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 832C817F1AAF6DEF007FA2F7 = { + CreatedOnToolsVersion = 6.2; + }; + }; + }; + buildConfigurationList = 832C817B1AAF6DEF007FA2F7 /* Build configuration list for PBXProject "RCTWebSocketDebugger" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 832C81771AAF6DEF007FA2F7; + productRefGroup = 832C81811AAF6DEF007FA2F7 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 832C817F1AAF6DEF007FA2F7 /* RCTWebSocketDebugger */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 832C817C1AAF6DEF007FA2F7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00D277191AB8C35800DC1E48 /* SRWebSocket.m in Sources */, + 00D277161AB8C32C00DC1E48 /* RCTWebSocketExecutor.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 832C81921AAF6DF0007FA2F7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 832C81931AAF6DF0007FA2F7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 832C81951AAF6DF0007FA2F7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "-ObjC", + "-llibicucore", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 832C81961AAF6DF0007FA2F7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = ( + "-ObjC", + "-llibicucore", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 832C817B1AAF6DEF007FA2F7 /* Build configuration list for PBXProject "RCTWebSocketDebugger" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 832C81921AAF6DF0007FA2F7 /* Debug */, + 832C81931AAF6DF0007FA2F7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 832C81941AAF6DF0007FA2F7 /* Build configuration list for PBXNativeTarget "RCTWebSocketDebugger" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 832C81951AAF6DF0007FA2F7 /* Debug */, + 832C81961AAF6DF0007FA2F7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 832C81781AAF6DEF007FA2F7 /* Project object */; +} diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h new file mode 100644 index 000000000..15f8f32e1 --- /dev/null +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTJavaScriptExecutor.h" + +@interface RCTWebSocketExecutor : NSObject + +- (instancetype)initWithURL:(NSURL *)url; + +@end diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m new file mode 100644 index 000000000..d40713ee3 --- /dev/null +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m @@ -0,0 +1,179 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTWebSocketExecutor.h" + +#import "RCTLog.h" +#import "RCTUtils.h" +#import "SRWebSocket.h" + +typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); + +@interface RCTWebSocketExecutor () +@end + +@implementation RCTWebSocketExecutor { + SRWebSocket *_socket; + NSOperationQueue *_jsQueue; + NSMutableDictionary *_callbacks; + dispatch_semaphore_t _socketOpenSemaphore; + NSMutableDictionary *_injectedObjects; +} + +- (instancetype)init +{ + return [self initWithURL:[NSURL URLWithString:@"http://localhost:8081/debugger-proxy"]]; +} + +- (instancetype)initWithURL:(NSURL *)url +{ + if (self = [super init]) { + _jsQueue = [[NSOperationQueue alloc] init]; + _jsQueue.maxConcurrentOperationCount = 1; + _socket = [[SRWebSocket alloc] initWithURL:url]; + _socket.delegate = self; + _callbacks = [NSMutableDictionary dictionary]; + _injectedObjects = [NSMutableDictionary dictionary]; + [_socket setDelegateOperationQueue:_jsQueue]; + + + NSURL *startDevToolsURL = [NSURL URLWithString:@"/launch-chrome-devtools" relativeToURL:url]; + [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:startDevToolsURL] delegate:nil]; + + if (![self connectToProxy]) { + RCTLogError(@"Connection to %@ timed out. Are you running node proxy?", url); + [self invalidate]; + return nil; + } + + NSInteger retries = 3; + BOOL runtimeIsReady = [self prepareJSRuntime]; + while (!runtimeIsReady && retries > 0) { + runtimeIsReady = [self prepareJSRuntime]; + retries--; + } + if (!runtimeIsReady) { + RCTLogError(@"Runtime is not ready. Do you have Chrome open?"); + [self invalidate]; + return nil; + } + } + return self; +} + +- (BOOL)connectToProxy +{ + _socketOpenSemaphore = dispatch_semaphore_create(0); + [_socket open]; + long connected = dispatch_semaphore_wait(_socketOpenSemaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 2)); + return connected == 0; +} + +- (BOOL)prepareJSRuntime +{ + __block NSError *initError; + dispatch_semaphore_t s = dispatch_semaphore_create(0); + [self sendMessage:@{@"method": @"prepareJSRuntime"} waitForReply:^(NSError *error, NSDictionary *reply) { + initError = error; + dispatch_semaphore_signal(s); + }]; + long runtimeIsReady = dispatch_semaphore_wait(s, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); + return runtimeIsReady == 0 && initError == nil; +} + +- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message +{ + NSError *error = nil; + NSDictionary *reply = RCTJSONParse(message, &error); + NSUInteger messageID = [reply[@"replyID"] integerValue]; + WSMessageCallback callback = [_callbacks objectForKey:@(messageID)]; + if (callback) { + callback(error, reply); + } +} + +- (void)webSocketDidOpen:(SRWebSocket *)webSocket +{ + dispatch_semaphore_signal(_socketOpenSemaphore); +} + +- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error +{ + RCTLogError(@"WebSocket connection failed with error %@", error); +} + +- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean +{ + +} + +- (void)sendMessage:(NSDictionary *)message waitForReply:(WSMessageCallback)callback +{ + static NSUInteger lastID = 10000; + + [_jsQueue addOperationWithBlock:^{ + if (!self.valid) { + NSError *error = [NSError errorWithDomain:@"WS" code:1 userInfo:@{NSLocalizedDescriptionKey:@"socket closed"}]; + callback(error, nil); + return; + } + + NSUInteger expectedID = lastID++; + + _callbacks[@(expectedID)] = [callback copy]; + + NSMutableDictionary *messageWithID = [message mutableCopy]; + messageWithID[@"id"] = @(expectedID); + [_socket send:RCTJSONStringify(messageWithID, NULL)]; + }]; +} + +- (void)executeApplicationScript:(NSString *)script sourceURL:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete +{ + NSDictionary *message = @{@"method": NSStringFromSelector(_cmd), @"url": [url absoluteString], @"inject": _injectedObjects}; + [self sendMessage:message waitForReply:^(NSError *error, NSDictionary *reply) { + onComplete(error); + }]; +} + +- (void)executeJSCall:(NSString *)name method:(NSString *)method arguments:(NSArray *)arguments callback:(RCTJavaScriptCallback)onComplete +{ + RCTAssert(onComplete != nil, @"callback was missing for exec JS call"); + NSDictionary *message = @{@"method": NSStringFromSelector(_cmd), @"moduleName": name, @"moduleMethod": method, @"arguments": arguments}; + [self sendMessage:message waitForReply:^(NSError *socketError, NSDictionary *reply) { + if (socketError) { + onComplete(nil, socketError); + return; + } + + NSString *result = reply[@"result"]; + id objcValue = RCTJSONParse(result, NULL); + onComplete(objcValue, nil); + }]; +} + +- (void)injectJSONText:(NSString *)script asGlobalObjectNamed:(NSString *)objectName callback:(RCTJavaScriptCompleteBlock)onComplete +{ + [_jsQueue addOperationWithBlock:^{ + [_injectedObjects setObject:script forKey:objectName]; + onComplete(nil); + }]; +} + +- (void)invalidate +{ + _socket.delegate = nil; + [_socket closeWithCode:1000 reason:@"Invalidated"]; + _socket = nil; +} + +- (BOOL)isValid +{ + return _socket != nil && _socket.readyState == SR_OPEN; +} + +- (void)dealloc +{ + RCTAssert(!self.valid, @"-invalidate must be called before -dealloc"); +} + +@end diff --git a/Libraries/RCTWebSocketDebugger/SRWebSocket.h b/Libraries/RCTWebSocketDebugger/SRWebSocket.h new file mode 100644 index 000000000..5cce725a3 --- /dev/null +++ b/Libraries/RCTWebSocketDebugger/SRWebSocket.h @@ -0,0 +1,132 @@ +// +// Copyright 2012 Square Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import + +typedef enum { + SR_CONNECTING = 0, + SR_OPEN = 1, + SR_CLOSING = 2, + SR_CLOSED = 3, +} SRReadyState; + +typedef enum SRStatusCode : NSInteger { + SRStatusCodeNormal = 1000, + SRStatusCodeGoingAway = 1001, + SRStatusCodeProtocolError = 1002, + SRStatusCodeUnhandledType = 1003, + // 1004 reserved. + SRStatusNoStatusReceived = 1005, + // 1004-1006 reserved. + SRStatusCodeInvalidUTF8 = 1007, + SRStatusCodePolicyViolated = 1008, + SRStatusCodeMessageTooBig = 1009, +} SRStatusCode; + +@class SRWebSocket; + +extern NSString *const SRWebSocketErrorDomain; +extern NSString *const SRHTTPResponseErrorKey; + +#pragma mark - SRWebSocketDelegate + +@protocol SRWebSocketDelegate; + +#pragma mark - SRWebSocket + +@interface SRWebSocket : NSObject + +@property (nonatomic, weak) id delegate; + +@property (nonatomic, readonly) SRReadyState readyState; +@property (nonatomic, readonly, retain) NSURL *url; + +// This returns the negotiated protocol. +// It will be nil until after the handshake completes. +@property (nonatomic, readonly, copy) NSString *protocol; + +// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol. +- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; +- (id)initWithURLRequest:(NSURLRequest *)request; + +// Some helper constructors. +- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +- (id)initWithURL:(NSURL *)url; + +// Delegate queue will be dispatch_main_queue by default. +// You cannot set both OperationQueue and dispatch_queue. +- (void)setDelegateOperationQueue:(NSOperationQueue*) queue; +- (void)setDelegateDispatchQueue:(dispatch_queue_t) queue; + +// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes. +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; + +// SRWebSockets are intended for one-time-use only. Open should be called once and only once. +- (void)open; + +- (void)close; +- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; + +// Send a UTF8 String or Data. +- (void)send:(id)data; + +// Send Data (can be nil) in a ping message. +- (void)sendPing:(NSData *)data; + +@end + +#pragma mark - SRWebSocketDelegate + +@protocol SRWebSocketDelegate + +// message will either be an NSString if the server is using text +// or NSData if the server is using binary. +- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message; + +@optional + +- (void)webSocketDidOpen:(SRWebSocket *)webSocket; +- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error; +- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean; +- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload; + +@end + +#pragma mark - NSURLRequest (CertificateAdditions) + +@interface NSURLRequest (CertificateAdditions) + +@property (nonatomic, retain, readonly) NSArray *SR_SSLPinnedCertificates; + +@end + +#pragma mark - NSMutableURLRequest (CertificateAdditions) + +@interface NSMutableURLRequest (CertificateAdditions) + +@property (nonatomic, retain) NSArray *SR_SSLPinnedCertificates; + +@end + +#pragma mark - NSRunLoop (SRWebSocket) + +@interface NSRunLoop (SRWebSocket) + ++ (NSRunLoop *)SR_networkRunLoop; + +@end diff --git a/Libraries/RCTWebSocketDebugger/SRWebSocket.m b/Libraries/RCTWebSocketDebugger/SRWebSocket.m new file mode 100644 index 000000000..d643d63b9 --- /dev/null +++ b/Libraries/RCTWebSocketDebugger/SRWebSocket.m @@ -0,0 +1,1761 @@ +// +// Copyright 2012 Square Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +#import "SRWebSocket.h" + +#if TARGET_OS_IPHONE +#define HAS_ICU +#endif + +#ifdef HAS_ICU +#import +#endif + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +#import +#import + +#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE +#define sr_dispatch_retain(x) +#define sr_dispatch_release(x) +#define maybe_bridge(x) ((__bridge void *) x) +#else +#define sr_dispatch_retain(x) dispatch_retain(x) +#define sr_dispatch_release(x) dispatch_release(x) +#define maybe_bridge(x) (x) +#endif + +#if !__has_feature(objc_arc) +#error SocketRocket must be compiled with ARC enabled +#endif + + +typedef enum { + SROpCodeTextFrame = 0x1, + SROpCodeBinaryFrame = 0x2, + // 3-7 reserved. + SROpCodeConnectionClose = 0x8, + SROpCodePing = 0x9, + SROpCodePong = 0xA, + // B-F reserved. +} SROpCode; + +typedef struct { + BOOL fin; +// BOOL rsv1; +// BOOL rsv2; +// BOOL rsv3; + uint8_t opcode; + BOOL masked; + uint64_t payload_length; +} frame_header; + +static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +static inline int32_t validate_dispatch_data_partial_string(NSData *data); +static inline void SRFastLog(NSString *format, ...); + +@interface NSData (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; + +@end + + +@interface NSString (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; + +@end + + +@interface NSURL (SRWebSocket) + +// The origin isn't really applicable for a native application. +// So instead, just map ws -> http and wss -> https. +- (NSString *)SR_origin; + +@end + + +@interface _SRRunLoopThread : NSThread + +@property (nonatomic, readonly) NSRunLoop *runLoop; + +@end + + +static NSString *newSHA1String(const char *bytes, size_t length) { + uint8_t md[CC_SHA1_DIGEST_LENGTH]; + + assert(length >= 0); + assert(length <= UINT32_MAX); + CC_SHA1(bytes, (CC_LONG)length, md); + + NSData *data = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH]; + + if ([data respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { + return [data base64EncodedStringWithOptions:0]; + } + + return [data base64Encoding]; +} + +@implementation NSData (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; +{ + return newSHA1String(self.bytes, self.length); +} + +@end + + +@implementation NSString (SRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; +{ + return newSHA1String(self.UTF8String, self.length); +} + +@end + +NSString *const SRWebSocketErrorDomain = @"SRWebSocketErrorDomain"; +NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode"; + +// Returns number of bytes consumed. Returning 0 means you didn't match. +// Sends bytes to callback handler; +typedef size_t (^stream_scanner)(NSData *collected_data); + +typedef void (^data_callback)(SRWebSocket *webSocket, NSData *data); + +@interface SRIOConsumer : NSObject { + stream_scanner _scanner; + data_callback _handler; + size_t _bytesNeeded; + BOOL _readToCurrentFrame; + BOOL _unmaskBytes; +} +@property (nonatomic, copy, readonly) stream_scanner consumer; +@property (nonatomic, copy, readonly) data_callback handler; +@property (nonatomic, assign) size_t bytesNeeded; +@property (nonatomic, assign, readonly) BOOL readToCurrentFrame; +@property (nonatomic, assign, readonly) BOOL unmaskBytes; + +@end + +// This class is not thread-safe, and is expected to always be run on the same queue. +@interface SRIOConsumerPool : NSObject + +- (id)initWithBufferCapacity:(NSUInteger)poolSize; + +- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +- (void)returnConsumer:(SRIOConsumer *)consumer; + +@end + +@interface SRWebSocket () + +- (void)_writeData:(NSData *)data; +- (void)_closeWithProtocolError:(NSString *)message; +- (void)_failWithError:(NSError *)error; + +- (void)_disconnect; + +- (void)_readFrameNew; +- (void)_readFrameContinue; + +- (void)_pumpScanner; + +- (void)_pumpWriting; + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; +- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; +- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; +- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; + +- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; + +- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; +- (void)_SR_commonInit; + +- (void)_initializeStreams; +- (void)_connect; + +@property (nonatomic) SRReadyState readyState; + +@property (nonatomic) NSOperationQueue *delegateOperationQueue; +@property (nonatomic) dispatch_queue_t delegateDispatchQueue; + +@end + + +@implementation SRWebSocket { + NSInteger _webSocketVersion; + + NSOperationQueue *_delegateOperationQueue; + dispatch_queue_t _delegateDispatchQueue; + + dispatch_queue_t _workQueue; + NSMutableArray *_consumers; + + NSInputStream *_inputStream; + NSOutputStream *_outputStream; + + NSMutableData *_readBuffer; + NSUInteger _readBufferOffset; + + NSMutableData *_outputBuffer; + NSUInteger _outputBufferOffset; + + uint8_t _currentFrameOpcode; + size_t _currentFrameCount; + size_t _readOpCount; + uint32_t _currentStringScanPosition; + NSMutableData *_currentFrameData; + + NSString *_closeReason; + + NSString *_secKey; + + BOOL _pinnedCertFound; + + uint8_t _currentReadMaskKey[4]; + size_t _currentReadMaskOffset; + + BOOL _consumerStopped; + + BOOL _closeWhenFinishedWriting; + BOOL _failed; + + BOOL _secure; + NSURLRequest *_urlRequest; + + CFHTTPMessageRef _receivedHTTPHeaders; + + BOOL _sentClose; + BOOL _didFail; + int _closeCode; + + BOOL _isPumping; + + NSMutableSet *_scheduledRunloops; + + // We use this to retain ourselves. + __strong SRWebSocket *_selfRetain; + + NSArray *_requestedProtocols; + SRIOConsumerPool *_consumerPool; +} + +@synthesize delegate = _delegate; +@synthesize url = _url; +@synthesize readyState = _readyState; +@synthesize protocol = _protocol; + +static __strong NSData *CRLFCRLF; + ++ (void)initialize; +{ + CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; +} + +- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; +{ + self = [super init]; + if (self) { + assert(request.URL); + _url = request.URL; + _urlRequest = request; + + _requestedProtocols = [protocols copy]; + + [self _SR_commonInit]; + } + + return self; +} + +- (id)initWithURLRequest:(NSURLRequest *)request; +{ + return [self initWithURLRequest:request protocols:nil]; +} + +- (id)initWithURL:(NSURL *)url; +{ + return [self initWithURL:url protocols:nil]; +} + +- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +{ + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + return [self initWithURLRequest:request protocols:protocols]; +} + +- (void)_SR_commonInit; +{ + + NSString *scheme = _url.scheme.lowercaseString; + assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]); + + if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) { + _secure = YES; + } + + _readyState = SR_CONNECTING; + _consumerStopped = YES; + _webSocketVersion = 13; + + _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + + // Going to set a specific on the queue so we can validate we're on the work queue + dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL); + + _delegateDispatchQueue = dispatch_get_main_queue(); + sr_dispatch_retain(_delegateDispatchQueue); + + _readBuffer = [[NSMutableData alloc] init]; + _outputBuffer = [[NSMutableData alloc] init]; + + _currentFrameData = [[NSMutableData alloc] init]; + + _consumers = [[NSMutableArray alloc] init]; + + _consumerPool = [[SRIOConsumerPool alloc] init]; + + _scheduledRunloops = [[NSMutableSet alloc] init]; + + [self _initializeStreams]; + + // default handlers +} + +- (void)assertOnWorkQueue; +{ + assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue)); +} + +- (void)dealloc +{ + _inputStream.delegate = nil; + _outputStream.delegate = nil; + + [_inputStream close]; + [_outputStream close]; + + sr_dispatch_release(_workQueue); + _workQueue = NULL; + + if (_receivedHTTPHeaders) { + CFRelease(_receivedHTTPHeaders); + _receivedHTTPHeaders = NULL; + } + + if (_delegateDispatchQueue) { + sr_dispatch_release(_delegateDispatchQueue); + _delegateDispatchQueue = NULL; + } +} + +#ifndef NDEBUG + +- (void)setReadyState:(SRReadyState)aReadyState; +{ + [self willChangeValueForKey:@"readyState"]; + assert(aReadyState > _readyState); + _readyState = aReadyState; + [self didChangeValueForKey:@"readyState"]; +} + +#endif + +- (void)open; +{ + assert(_url); + NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once"); + + _selfRetain = self; + + [self _connect]; +} + +// Calls block on delegate queue +- (void)_performDelegateBlock:(dispatch_block_t)block; +{ + if (_delegateOperationQueue) { + [_delegateOperationQueue addOperationWithBlock:block]; + } else { + assert(_delegateDispatchQueue); + dispatch_async(_delegateDispatchQueue, block); + } +} + +- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue; +{ + if (queue) { + sr_dispatch_retain(queue); + } + + if (_delegateDispatchQueue) { + sr_dispatch_release(_delegateDispatchQueue); + } + + _delegateDispatchQueue = queue; +} + +- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; +{ + NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept"))); + + if (acceptHeader == nil) { + return NO; + } + + NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString]; + NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding]; + + return [acceptHeader isEqualToString:expectedAccept]; +} + +- (void)_HTTPHeadersDidFinish; +{ + NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); + + if (responseCode >= 400) { + SRFastLog(@"Request failed with response code %d", responseCode); + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2132 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"received bad response code from server %ld", (long)responseCode], SRHTTPResponseErrorKey:@(responseCode)}]]; + return; + } + + if(![self _checkHandshake:_receivedHTTPHeaders]) { + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]]; + return; + } + + NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol"))); + if (negotiatedProtocol) { + // Make sure we requested the protocol + if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) { + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]]; + return; + } + + _protocol = negotiatedProtocol; + } + + self.readyState = SR_OPEN; + + if (!_didFail) { + [self _readFrameNew]; + } + + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) { + [self.delegate webSocketDidOpen:self]; + }; + }]; +} + + +- (void)_readHTTPHeader; +{ + if (_receivedHTTPHeaders == NULL) { + _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); + } + + [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *self, NSData *data) { + CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); + + if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { + SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); + [self _HTTPHeadersDidFinish]; + } else { + [self _readHTTPHeader]; + } + }]; +} + +- (void)didConnect +{ + SRFastLog(@"Connected"); + CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1); + + // Set host first so it defaults + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host)); + + NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16]; + SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes); + + if ([keyBytes respondsToSelector:@selector(base64EncodedStringWithOptions:)]) { + _secKey = [keyBytes base64EncodedStringWithOptions:0]; + } else { + _secKey = [keyBytes base64Encoding]; + } + + assert([_secKey length] == 24); + + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket")); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade")); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]); + + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin); + + if (_requestedProtocols) { + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]); + } + + [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); + }]; + + NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)); + + CFRelease(request); + + [self _writeData:message]; + [self _readHTTPHeader]; +} + +- (void)_initializeStreams; +{ + assert(_url.port.unsignedIntValue <= UINT32_MAX); + uint32_t port = _url.port.unsignedIntValue; + if (port == 0) { + if (!_secure) { + port = 80; + } else { + port = 443; + } + } + NSString *host = _url.host; + + CFReadStreamRef readStream = NULL; + CFWriteStreamRef writeStream = NULL; + + CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream); + + _outputStream = CFBridgingRelease(writeStream); + _inputStream = CFBridgingRelease(readStream); + + + if (_secure) { + NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init]; + + [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; + + // If we're using pinned certs, don't validate the certificate chain + if ([_urlRequest SR_SSLPinnedCertificates].count) { + [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; + } + +#if DEBUG + [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; + NSLog(@"SocketRocket: In debug mode. Allowing connection to any root cert"); +#endif + + [_outputStream setProperty:SSLOptions + forKey:(__bridge id)kCFStreamPropertySSLSettings]; + } + + _inputStream.delegate = self; + _outputStream.delegate = self; +} + +- (void)_connect; +{ + if (!_scheduledRunloops.count) { + [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; + } + + + [_outputStream open]; + [_inputStream open]; +} + +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +{ + [_outputStream scheduleInRunLoop:aRunLoop forMode:mode]; + [_inputStream scheduleInRunLoop:aRunLoop forMode:mode]; + + [_scheduledRunloops addObject:@[aRunLoop, mode]]; +} + +- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +{ + [_outputStream removeFromRunLoop:aRunLoop forMode:mode]; + [_inputStream removeFromRunLoop:aRunLoop forMode:mode]; + + [_scheduledRunloops removeObject:@[aRunLoop, mode]]; +} + +- (void)close; +{ + [self closeWithCode:SRStatusCodeNormal reason:nil]; +} + +- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; +{ + assert(code); + dispatch_async(_workQueue, ^{ + if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) { + return; + } + + BOOL wasConnecting = self.readyState == SR_CONNECTING; + + self.readyState = SR_CLOSING; + + SRFastLog(@"Closing with code %d reason %@", code, reason); + + if (wasConnecting) { + [self _disconnect]; + return; + } + + size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize]; + NSData *payload = mutablePayload; + + ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(code); + + if (reason) { + NSRange remainingRange = {0}; + + NSUInteger usedLength = 0; + + BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange]; + + assert(success); + assert(remainingRange.length == 0); + + if (usedLength != maxMsgSize) { + payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))]; + } + } + + + [self _sendFrameWithOpcode:SROpCodeConnectionClose data:payload]; + }); +} + +- (void)_closeWithProtocolError:(NSString *)message; +{ + // Need to shunt this on the _callbackQueue first to see if they received any messages + [self _performDelegateBlock:^{ + [self closeWithCode:SRStatusCodeProtocolError reason:message]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + }]; +} + +- (void)_failWithError:(NSError *)error; +{ + dispatch_async(_workQueue, ^{ + if (self.readyState != SR_CLOSED) { + _failed = YES; + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) { + [self.delegate webSocket:self didFailWithError:error]; + } + }]; + + self.readyState = SR_CLOSED; + _selfRetain = nil; + + SRFastLog(@"Failing with error %@", error.localizedDescription); + + [self _disconnect]; + } + }); +} + +- (void)_writeData:(NSData *)data; +{ + [self assertOnWorkQueue]; + + if (_closeWhenFinishedWriting) { + return; + } + [_outputBuffer appendData:data]; + [self _pumpWriting]; +} + +- (void)send:(id)data; +{ + NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open"); + // TODO: maybe not copy this for performance + data = [data copy]; + dispatch_async(_workQueue, ^{ + if ([data isKindOfClass:[NSString class]]) { + [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]]; + } else if ([data isKindOfClass:[NSData class]]) { + [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data]; + } else if (data == nil) { + [self _sendFrameWithOpcode:SROpCodeTextFrame data:data]; + } else { + assert(NO); + } + }); +} + +- (void)sendPing:(NSData *)data; +{ + NSAssert(self.readyState == SR_OPEN, @"Invalid State: Cannot call send: until connection is open"); + // TODO: maybe not copy this for performance + data = [data copy] ?: [NSData data]; // It's okay for a ping to be empty + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:SROpCodePing data:data]; + }); +} + +- (void)handlePing:(NSData *)pingData; +{ + // Need to pingpong this off _callbackQueue first to make sure messages happen in order + [self _performDelegateBlock:^{ + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:SROpCodePong data:pingData]; + }); + }]; +} + +- (void)handlePong:(NSData *)pongData; +{ + SRFastLog(@"Received pong"); + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didReceivePong:)]) { + [self.delegate webSocket:self didReceivePong:pongData]; + } + }]; +} + +- (void)_handleMessage:(id)message +{ + SRFastLog(@"Received message"); + [self _performDelegateBlock:^{ + [self.delegate webSocket:self didReceiveMessage:message]; + }]; +} + + +static inline BOOL closeCodeIsValid(int closeCode) { + if (closeCode < 1000) { + return NO; + } + + if (closeCode >= 1000 && closeCode <= 1011) { + if (closeCode == 1004 || + closeCode == 1005 || + closeCode == 1006) { + return NO; + } + return YES; + } + + if (closeCode >= 3000 && closeCode <= 3999) { + return YES; + } + + if (closeCode >= 4000 && closeCode <= 4999) { + return YES; + } + + return NO; +} + +// Note from RFC: +// +// If there is a body, the first two +// bytes of the body MUST be a 2-byte unsigned integer (in network byte +// order) representing a status code with value /code/ defined in +// Section 7.4. Following the 2-byte integer the body MAY contain UTF-8 +// encoded data with value /reason/, the interpretation of which is not +// defined by this specification. + +- (void)handleCloseWithData:(NSData *)data; +{ + size_t dataSize = data.length; + __block uint16_t closeCode = 0; + + SRFastLog(@"Received close frame"); + + if (dataSize == 1) { + // TODO handle error + [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"]; + return; + } else if (dataSize >= 2) { + [data getBytes:&closeCode length:sizeof(closeCode)]; + _closeCode = EndianU16_BtoN(closeCode); + if (!closeCodeIsValid(_closeCode)) { + [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]]; + return; + } + if (dataSize > 2) { + _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding]; + if (!_closeReason) { + [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"]; + return; + } + } + } else { + _closeCode = SRStatusNoStatusReceived; + } + + [self assertOnWorkQueue]; + + if (self.readyState == SR_OPEN) { + [self closeWithCode:1000 reason:nil]; + } + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); +} + +- (void)_disconnect; +{ + [self assertOnWorkQueue]; + SRFastLog(@"Trying to disconnect"); + _closeWhenFinishedWriting = YES; + [self _pumpWriting]; +} + +- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode; +{ + // Check that the current data is valid UTF8 + + BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose); + if (!isControlFrame) { + [self _readFrameNew]; + } else { + dispatch_async(_workQueue, ^{ + [self _readFrameContinue]; + }); + } + + switch (opcode) { + case SROpCodeTextFrame: { + NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding]; + if (str == nil && frameData) { + [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + + return; + } + [self _handleMessage:str]; + break; + } + case SROpCodeBinaryFrame: + [self _handleMessage:[frameData copy]]; + break; + case SROpCodeConnectionClose: + [self handleCloseWithData:frameData]; + break; + case SROpCodePing: + [self handlePing:frameData]; + break; + case SROpCodePong: + [self handlePong:frameData]; + break; + default: + [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %ld", (long)opcode]]; + // TODO: Handle invalid opcode + break; + } +} + +- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData; +{ + assert(frame_header.opcode != 0); + + if (self.readyState != SR_OPEN) { + return; + } + + + BOOL isControlFrame = (frame_header.opcode == SROpCodePing || frame_header.opcode == SROpCodePong || frame_header.opcode == SROpCodeConnectionClose); + + if (isControlFrame && !frame_header.fin) { + [self _closeWithProtocolError:@"Fragmented control frames not allowed"]; + return; + } + + if (isControlFrame && frame_header.payload_length >= 126) { + [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"]; + return; + } + + if (!isControlFrame) { + _currentFrameOpcode = frame_header.opcode; + _currentFrameCount += 1; + } + + if (frame_header.payload_length == 0) { + if (isControlFrame) { + [self _handleFrameWithData:curData opCode:frame_header.opcode]; + } else { + if (frame_header.fin) { + [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode]; + } else { + // TODO add assert that opcode is not a control; + [self _readFrameContinue]; + } + } + } else { + assert(frame_header.payload_length <= SIZE_T_MAX); + [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(SRWebSocket *self, NSData *newData) { + if (isControlFrame) { + [self _handleFrameWithData:newData opCode:frame_header.opcode]; + } else { + if (frame_header.fin) { + [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode]; + } else { + // TODO add assert that opcode is not a control; + [self _readFrameContinue]; + } + + } + } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked]; + } +} + +/* From RFC: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-------+-+-------------+-------------------------------+ + |F|R|R|R| opcode|M| Payload len | Extended payload length | + |I|S|S|S| (4) |A| (7) | (16/64) | + |N|V|V|V| |S| | (if payload len==126/127) | + | |1|2|3| |K| | | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + | Extended payload length continued, if payload len == 127 | + + - - - - - - - - - - - - - - - +-------------------------------+ + | |Masking-key, if MASK set to 1 | + +-------------------------------+-------------------------------+ + | Masking-key (continued) | Payload Data | + +-------------------------------- - - - - - - - - - - - - - - - + + : Payload Data continued ... : + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + | Payload Data continued ... | + +---------------------------------------------------------------+ + */ + +static const uint8_t SRFinMask = 0x80; +static const uint8_t SROpCodeMask = 0x0F; +static const uint8_t SRRsvMask = 0x70; +static const uint8_t SRMaskMask = 0x80; +static const uint8_t SRPayloadLenMask = 0x7F; + + +- (void)_readFrameContinue; +{ + assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0)); + + [self _addConsumerWithDataLength:2 callback:^(SRWebSocket *self, NSData *data) { + __block frame_header header = {0}; + + const uint8_t *headerBuffer = data.bytes; + assert(data.length >= 2); + + if (headerBuffer[0] & SRRsvMask) { + [self _closeWithProtocolError:@"Server used RSV bits"]; + return; + } + + uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]); + + BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose); + + if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) { + [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"]; + return; + } + + if (receivedOpcode == 0 && self->_currentFrameCount == 0) { + [self _closeWithProtocolError:@"cannot continue a message"]; + return; + } + + header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode; + + header.fin = !!(SRFinMask & headerBuffer[0]); + + + header.masked = !!(SRMaskMask & headerBuffer[1]); + header.payload_length = SRPayloadLenMask & headerBuffer[1]; + + headerBuffer = NULL; + + if (header.masked) { + [self _closeWithProtocolError:@"Client must receive unmasked data"]; + } + + size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0; + + if (header.payload_length == 126) { + extra_bytes_needed += sizeof(uint16_t); + } else if (header.payload_length == 127) { + extra_bytes_needed += sizeof(uint64_t); + } + + if (extra_bytes_needed == 0) { + [self _handleFrameHeader:header curData:self->_currentFrameData]; + } else { + [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) { + size_t mapped_size = data.length; + const void *mapped_buffer = data.bytes; + size_t offset = 0; + + if (header.payload_length == 126) { + assert(mapped_size >= sizeof(uint16_t)); + uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer)); + header.payload_length = newLen; + offset += sizeof(uint16_t); + } else if (header.payload_length == 127) { + assert(mapped_size >= sizeof(uint64_t)); + header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer)); + offset += sizeof(uint64_t); + } else { + assert(header.payload_length < 126 && header.payload_length >= 0); + } + + + if (header.masked) { + assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset); + memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey)); + } + + [self _handleFrameHeader:header curData:self->_currentFrameData]; + } readToCurrentFrame:NO unmaskBytes:NO]; + } + } readToCurrentFrame:NO unmaskBytes:NO]; +} + +- (void)_readFrameNew; +{ + dispatch_async(_workQueue, ^{ + [_currentFrameData setLength:0]; + + _currentFrameOpcode = 0; + _currentFrameCount = 0; + _readOpCount = 0; + _currentStringScanPosition = 0; + + [self _readFrameContinue]; + }); +} + +- (void)_pumpWriting; +{ + [self assertOnWorkQueue]; + + NSUInteger dataLength = _outputBuffer.length; + if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) { + NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset]; + if (bytesWritten == -1) { + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]]; + return; + } + + _outputBufferOffset += bytesWritten; + + if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) { + _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset]; + _outputBufferOffset = 0; + } + } + + if (_closeWhenFinishedWriting && + _outputBuffer.length - _outputBufferOffset == 0 && + (_inputStream.streamStatus != NSStreamStatusNotOpen && + _inputStream.streamStatus != NSStreamStatusClosed) && + !_sentClose) { + _sentClose = YES; + + [_outputStream close]; + [_inputStream close]; + + + for (NSArray *runLoop in [_scheduledRunloops copy]) { + [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]]; + } + + if (!_failed) { + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { + [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES]; + } + }]; + } + + _selfRetain = nil; + } +} + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; +{ + [self assertOnWorkQueue]; + [self _addConsumerWithScanner:consumer callback:callback dataLength:0]; +} + +- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + [self assertOnWorkQueue]; + assert(dataLength); + + [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]]; + [self _pumpScanner]; +} + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; +{ + [self assertOnWorkQueue]; + [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]]; + [self _pumpScanner]; +} + + +static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'}; + +- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; +{ + [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler]; +} + +- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; +{ + // TODO optimize so this can continue from where we last searched + stream_scanner consumer = ^size_t(NSData *data) { + __block size_t found_size = 0; + __block size_t match_count = 0; + + size_t size = data.length; + const unsigned char *buffer = data.bytes; + for (size_t i = 0; i < size; i++ ) { + if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) { + match_count += 1; + if (match_count == length) { + found_size = i + 1; + break; + } + } else { + match_count = 0; + } + } + return found_size; + }; + [self _addConsumerWithScanner:consumer callback:dataHandler]; +} + + +// Returns true if did work +- (BOOL)_innerPumpScanner { + + BOOL didWork = NO; + + if (self.readyState >= SR_CLOSING) { + return didWork; + } + + if (!_consumers.count) { + return didWork; + } + + size_t curSize = _readBuffer.length - _readBufferOffset; + if (!curSize) { + return didWork; + } + + SRIOConsumer *consumer = [_consumers objectAtIndex:0]; + + size_t bytesNeeded = consumer.bytesNeeded; + + size_t foundSize = 0; + if (consumer.consumer) { + NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO]; + foundSize = consumer.consumer(tempView); + } else { + assert(consumer.bytesNeeded); + if (curSize >= bytesNeeded) { + foundSize = bytesNeeded; + } else if (consumer.readToCurrentFrame) { + foundSize = curSize; + } + } + + NSData *slice = nil; + if (consumer.readToCurrentFrame || foundSize) { + NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize); + slice = [_readBuffer subdataWithRange:sliceRange]; + + _readBufferOffset += foundSize; + + if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) { + _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0; + } + + if (consumer.unmaskBytes) { + NSMutableData *mutableSlice = [slice mutableCopy]; + + NSUInteger len = mutableSlice.length; + uint8_t *bytes = mutableSlice.mutableBytes; + + for (NSUInteger i = 0; i < len; i++) { + bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)]; + _currentReadMaskOffset += 1; + } + + slice = mutableSlice; + } + + if (consumer.readToCurrentFrame) { + [_currentFrameData appendData:slice]; + + _readOpCount += 1; + + if (_currentFrameOpcode == SROpCodeTextFrame) { + // Validate UTF8 stuff. + size_t currentDataSize = _currentFrameData.length; + if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) { + // TODO: Optimize the crap out of this. Don't really have to copy all the data each time + + size_t scanSize = currentDataSize - _currentStringScanPosition; + + NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)]; + int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data); + + if (valid_utf8_size == -1) { + [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + return didWork; + } else { + _currentStringScanPosition += valid_utf8_size; + } + } + + } + + consumer.bytesNeeded -= foundSize; + + if (consumer.bytesNeeded == 0) { + [_consumers removeObjectAtIndex:0]; + consumer.handler(self, nil); + [_consumerPool returnConsumer:consumer]; + didWork = YES; + } + } else if (foundSize) { + [_consumers removeObjectAtIndex:0]; + consumer.handler(self, slice); + [_consumerPool returnConsumer:consumer]; + didWork = YES; + } + } + return didWork; +} + +-(void)_pumpScanner; +{ + [self assertOnWorkQueue]; + + if (!_isPumping) { + _isPumping = YES; + } else { + return; + } + + while ([self _innerPumpScanner]) { + + } + + _isPumping = NO; +} + +//#define NOMASK + +static const size_t SRFrameHeaderOverhead = 32; + +- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; +{ + [self assertOnWorkQueue]; + + if (nil == data) { + return; + } + + NSAssert([data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"NSString or NSData"); + + size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length]; + + NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead]; + if (!frame) { + [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"]; + return; + } + uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes]; + + // set fin + frame_buffer[0] = SRFinMask | opcode; + + BOOL useMask = YES; +#ifdef NOMASK + useMask = NO; +#endif + + if (useMask) { + // set the mask and header + frame_buffer[1] |= SRMaskMask; + } + + size_t frame_buffer_size = 2; + + const uint8_t *unmasked_payload = NULL; + if ([data isKindOfClass:[NSData class]]) { + unmasked_payload = (uint8_t *)[data bytes]; + } else if ([data isKindOfClass:[NSString class]]) { + unmasked_payload = (const uint8_t *)[data UTF8String]; + } else { + return; + } + + if (payloadLength < 126) { + frame_buffer[1] |= payloadLength; + } else if (payloadLength <= UINT16_MAX) { + frame_buffer[1] |= 126; + *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength); + frame_buffer_size += sizeof(uint16_t); + } else { + frame_buffer[1] |= 127; + *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength); + frame_buffer_size += sizeof(uint64_t); + } + + if (!useMask) { + for (size_t i = 0; i < payloadLength; i++) { + frame_buffer[frame_buffer_size] = unmasked_payload[i]; + frame_buffer_size += 1; + } + } else { + uint8_t *mask_key = frame_buffer + frame_buffer_size; + SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key); + frame_buffer_size += sizeof(uint32_t); + + // TODO: could probably optimize this with SIMD + for (size_t i = 0; i < payloadLength; i++) { + frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)]; + frame_buffer_size += 1; + } + } + + assert(frame_buffer_size <= [frame length]); + frame.length = frame_buffer_size; + + [self _writeData:frame]; +} + +- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; +{ + if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) { + + NSArray *sslCerts = [_urlRequest SR_SSLPinnedCertificates]; + if (sslCerts) { + SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust]; + if (secTrust) { + NSInteger numCerts = SecTrustGetCertificateCount(secTrust); + for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) { + SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i); + NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert)); + + for (id ref in sslCerts) { + SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref; + NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert)); + + if ([trustedCertData isEqualToData:certData]) { + _pinnedCertFound = YES; + break; + } + } + } + } + + if (!_pinnedCertFound) { + dispatch_async(_workQueue, ^{ + [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:23556 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid server cert"] forKey:NSLocalizedDescriptionKey]]]; + }); + return; + } + } + } + + dispatch_async(_workQueue, ^{ + switch (eventCode) { + case NSStreamEventOpenCompleted: { + SRFastLog(@"NSStreamEventOpenCompleted %@", aStream); + if (self.readyState >= SR_CLOSING) { + return; + } + assert(_readBuffer); + + if (self.readyState == SR_CONNECTING && aStream == _inputStream) { + [self didConnect]; + } + [self _pumpWriting]; + [self _pumpScanner]; + break; + } + + case NSStreamEventErrorOccurred: { + SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]); + /// TODO specify error better! + [self _failWithError:aStream.streamError]; + _readBufferOffset = 0; + [_readBuffer setLength:0]; + break; + + } + + case NSStreamEventEndEncountered: { + [self _pumpScanner]; + SRFastLog(@"NSStreamEventEndEncountered %@", aStream); + if (aStream.streamError) { + [self _failWithError:aStream.streamError]; + } else { + if (self.readyState != SR_CLOSED) { + self.readyState = SR_CLOSED; + _selfRetain = nil; + } + + if (!_sentClose && !_failed) { + _sentClose = YES; + // If we get closed in this state it's probably not clean because we should be sending this when we send messages + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { + [self.delegate webSocket:self didCloseWithCode:SRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO]; + } + }]; + } + } + + break; + } + + case NSStreamEventHasBytesAvailable: { + SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream); + const int bufferSize = 2048; + uint8_t buffer[bufferSize]; + + while (_inputStream.hasBytesAvailable) { + NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize]; + + if (bytes_read > 0) { + [_readBuffer appendBytes:buffer length:bytes_read]; + } else if (bytes_read < 0) { + [self _failWithError:_inputStream.streamError]; + } + + if (bytes_read != bufferSize) { + break; + } + }; + [self _pumpScanner]; + break; + } + + case NSStreamEventHasSpaceAvailable: { + SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream); + [self _pumpWriting]; + break; + } + + default: + SRFastLog(@"(default) %@", aStream); + break; + } + }); +} + +@end + + +@implementation SRIOConsumer + +@synthesize bytesNeeded = _bytesNeeded; +@synthesize consumer = _scanner; +@synthesize handler = _handler; +@synthesize readToCurrentFrame = _readToCurrentFrame; +@synthesize unmaskBytes = _unmaskBytes; + +- (void)setupWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + _scanner = [scanner copy]; + _handler = [handler copy]; + _bytesNeeded = bytesNeeded; + _readToCurrentFrame = readToCurrentFrame; + _unmaskBytes = unmaskBytes; + assert(_scanner || _bytesNeeded); +} + + +@end + + +@implementation SRIOConsumerPool { + NSUInteger _poolSize; + NSMutableArray *_bufferedConsumers; +} + +- (id)initWithBufferCapacity:(NSUInteger)poolSize; +{ + self = [super init]; + if (self) { + _poolSize = poolSize; + _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize]; + } + return self; +} + +- (id)init +{ + return [self initWithBufferCapacity:8]; +} + +- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + SRIOConsumer *consumer = nil; + if (_bufferedConsumers.count) { + consumer = [_bufferedConsumers lastObject]; + [_bufferedConsumers removeLastObject]; + } else { + consumer = [[SRIOConsumer alloc] init]; + } + + [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]; + + return consumer; +} + +- (void)returnConsumer:(SRIOConsumer *)consumer; +{ + if (_bufferedConsumers.count < _poolSize) { + [_bufferedConsumers addObject:consumer]; + } +} + +@end + + +@implementation NSURLRequest (CertificateAdditions) + +- (NSArray *)SR_SSLPinnedCertificates; +{ + return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; +} + +@end + +@implementation NSMutableURLRequest (CertificateAdditions) + +- (NSArray *)SR_SSLPinnedCertificates; +{ + return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; +} + +- (void)setSR_SSLPinnedCertificates:(NSArray *)SR_SSLPinnedCertificates; +{ + [NSURLProtocol setProperty:SR_SSLPinnedCertificates forKey:@"SR_SSLPinnedCertificates" inRequest:self]; +} + +@end + +@implementation NSURL (SRWebSocket) + +- (NSString *)SR_origin; +{ + NSString *scheme = [self.scheme lowercaseString]; + + if ([scheme isEqualToString:@"wss"]) { + scheme = @"https"; + } else if ([scheme isEqualToString:@"ws"]) { + scheme = @"http"; + } + + if (self.port) { + return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port]; + } else { + return [NSString stringWithFormat:@"%@://%@/", scheme, self.host]; + } +} + +@end + +//#define SR_ENABLE_LOG + +static inline void SRFastLog(NSString *format, ...) { +#ifdef SR_ENABLE_LOG + __block va_list arg_list; + va_start (arg_list, format); + + NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; + + va_end(arg_list); + + NSLog(@"[SR] %@", formattedString); +#endif +} + + +#ifdef HAS_ICU + +static inline int32_t validate_dispatch_data_partial_string(NSData *data) { + if ([data length] > INT32_MAX) { + // INT32_MAX is the limit so long as this Framework is using 32 bit ints everywhere. + return -1; + } + + int32_t size = (int32_t)[data length]; + + const void * contents = [data bytes]; + const uint8_t *str = (const uint8_t *)contents; + + UChar32 codepoint = 1; + int32_t offset = 0; + int32_t lastOffset = 0; + while(offset < size && codepoint > 0) { + lastOffset = offset; + U8_NEXT(str, offset, size, codepoint); + } + + if (codepoint == -1) { + // Check to see if the last byte is valid or whether it was just continuing + if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) { + + size = -1; + } else { + uint8_t leadByte = str[lastOffset]; + U8_MASK_LEAD_BYTE(leadByte, U8_COUNT_TRAIL_BYTES(leadByte)); + + for (int i = lastOffset + 1; i < offset; i++) { + if (U8_IS_SINGLE(str[i]) || U8_IS_LEAD(str[i]) || !U8_IS_TRAIL(str[i])) { + size = -1; + } + } + + if (size != -1) { + size = lastOffset; + } + } + } + + if (size != -1 && ![[NSString alloc] initWithBytesNoCopy:(char *)[data bytes] length:size encoding:NSUTF8StringEncoding freeWhenDone:NO]) { + size = -1; + } + + return size; +} + +#else + +// This is a hack, and probably not optimal +static inline int32_t validate_dispatch_data_partial_string(NSData *data) { + static const int maxCodepointSize = 3; + + for (int i = 0; i < maxCodepointSize; i++) { + NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO]; + if (str) { + return data.length - i; + } + } + + return -1; +} + +#endif + +static _SRRunLoopThread *networkThread = nil; +static NSRunLoop *networkRunLoop = nil; + +@implementation NSRunLoop (SRWebSocket) + ++ (NSRunLoop *)SR_networkRunLoop { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + networkThread = [[_SRRunLoopThread alloc] init]; + networkThread.name = @"com.squareup.SocketRocket.NetworkThread"; + [networkThread start]; + networkRunLoop = networkThread.runLoop; + }); + + return networkRunLoop; +} + +@end + + +@implementation _SRRunLoopThread { + dispatch_group_t _waitGroup; +} + +@synthesize runLoop = _runLoop; + +- (void)dealloc +{ + sr_dispatch_release(_waitGroup); +} + +- (id)init +{ + self = [super init]; + if (self) { + _waitGroup = dispatch_group_create(); + dispatch_group_enter(_waitGroup); + } + return self; +} + +- (void)main; +{ + @autoreleasepool { + _runLoop = [NSRunLoop currentRunLoop]; + dispatch_group_leave(_waitGroup); + + NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:nil selector:nil userInfo:nil repeats:NO]; + [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode]; + + while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { + + } + assert(NO); + } +} + +- (NSRunLoop *)runLoop; +{ + dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER); + return _runLoop; +} + +@end diff --git a/Libraries/RKBackendNode/queryLayoutByID.js b/Libraries/RKBackendNode/queryLayoutByID.js index 7dcd2487e..0d8893eb6 100644 --- a/Libraries/RKBackendNode/queryLayoutByID.js +++ b/Libraries/RKBackendNode/queryLayoutByID.js @@ -6,7 +6,7 @@ 'use strict'; var ReactIOSTagHandles = require('ReactIOSTagHandles'); -var RKUIManager = require('NativeModulesDeprecated').RKUIManager; +var RCTUIManager = require('NativeModules').UIManager; /** * Queries the layout of a view. The layout does not reflect the element as @@ -29,7 +29,7 @@ var RKUIManager = require('NativeModulesDeprecated').RKUIManager; */ var queryLayoutByID = function(rootNodeID, onError, onSuccess) { // Native bridge doesn't *yet* surface errors. - RKUIManager.measure( + RCTUIManager.measure( ReactIOSTagHandles.rootNodeIDToTag[rootNodeID], onSuccess ); diff --git a/Libraries/ReactIOS/IOSNativeBridgeEventPlugin.js b/Libraries/ReactIOS/IOSNativeBridgeEventPlugin.js index e698ad196..fea12db6c 100644 --- a/Libraries/ReactIOS/IOSNativeBridgeEventPlugin.js +++ b/Libraries/ReactIOS/IOSNativeBridgeEventPlugin.js @@ -7,16 +7,16 @@ "use strict"; var EventPropagators = require('EventPropagators'); -var NativeModulesDeprecated = require('NativeModulesDeprecated'); +var NativeModules = require('NativeModules'); var SyntheticEvent = require('SyntheticEvent'); var merge = require('merge'); var warning = require('warning'); -var RKUIManager = NativeModulesDeprecated.RKUIManager; +var RCTUIManager = NativeModules.UIManager; -var customBubblingEventTypes = RKUIManager.customBubblingEventTypes; -var customDirectEventTypes = RKUIManager.customDirectEventTypes; +var customBubblingEventTypes = RCTUIManager.customBubblingEventTypes; +var customDirectEventTypes = RCTUIManager.customDirectEventTypes; var allTypesByEventName = {}; diff --git a/Libraries/ReactIOS/NativeMethodsMixin.js b/Libraries/ReactIOS/NativeMethodsMixin.js index ebfc246f0..9edc06413 100644 --- a/Libraries/ReactIOS/NativeMethodsMixin.js +++ b/Libraries/ReactIOS/NativeMethodsMixin.js @@ -6,10 +6,9 @@ 'use strict'; var NativeModules = require('NativeModules'); -var NativeModulesDeprecated = require('NativeModulesDeprecated'); -var RKUIManager = NativeModules.RKUIManager; -var RKUIManagerDeprecated = NativeModulesDeprecated.RKUIManager; -var RKPOPAnimationManagerDeprecated = NativeModulesDeprecated.RKPOPAnimationManager; +var NativeModules = require('NativeModules'); +var RCTPOPAnimationManager = NativeModules.POPAnimationManager; +var RCTUIManager = NativeModules.UIManager; var TextInputState = require('TextInputState'); var flattenStyle = require('flattenStyle'); @@ -27,20 +26,20 @@ var animationIDInvariant = function(funcName, anim) { var NativeMethodsMixin = { addAnimation: function(anim, callback) { animationIDInvariant('addAnimation', anim); - RKPOPAnimationManagerDeprecated.addAnimation(this.getNodeHandle(), anim, callback); + RCTPOPAnimationManager.addAnimation(this.getNodeHandle(), anim, callback); }, removeAnimation: function(anim) { animationIDInvariant('removeAnimation', anim); - RKPOPAnimationManagerDeprecated.removeAnimation(this.getNodeHandle(), anim); + RCTPOPAnimationManager.removeAnimation(this.getNodeHandle(), anim); }, measure: function(callback) { - RKUIManagerDeprecated.measure(this.getNodeHandle(), callback); + RCTUIManager.measure(this.getNodeHandle(), callback); }, measureLayout: function(relativeToNativeNode, onSuccess, onFail) { - RKUIManager.measureLayout( + RCTUIManager.measureLayout( this.getNodeHandle(), relativeToNativeNode, onFail, @@ -77,7 +76,7 @@ var NativeMethodsMixin = { props = mergeFast(nativeProps, style); } - RKUIManagerDeprecated.updateView( + RCTUIManager.updateView( this.getNodeHandle(), this.viewConfig.uiViewClassName, props diff --git a/Libraries/ReactIOS/NativeModules/RKRawText.js b/Libraries/ReactIOS/NativeModules/RKRawText.js deleted file mode 100644 index c56f0f68f..000000000 --- a/Libraries/ReactIOS/NativeModules/RKRawText.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule RKRawText - * @typechecks static-only - */ - -"use strict"; - -var ReactIOSViewAttributes = require('ReactIOSViewAttributes'); - -var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass'); - -var RKRawText = createReactIOSNativeComponentClass({ - validAttributes: { - text: true, - }, - uiViewClassName: 'RCTRawText', -}); - -module.exports = RKRawText; diff --git a/Libraries/ReactIOS/ReactIOS.js b/Libraries/ReactIOS/ReactIOS.js index f0a1ed782..2dfcb2604 100644 --- a/Libraries/ReactIOS/ReactIOS.js +++ b/Libraries/ReactIOS/ReactIOS.js @@ -6,8 +6,9 @@ "use strict"; +var ReactChildren = require('ReactChildren'); +var ReactClass = require('ReactClass'); var ReactComponent = require('ReactComponent'); -var ReactCompositeComponent = require('ReactCompositeComponent'); var ReactContext = require('ReactContext'); var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactElement = require('ReactElement'); @@ -15,30 +16,24 @@ var ReactElementValidator = require('ReactElementValidator'); var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactIOSDefaultInjection = require('ReactIOSDefaultInjection'); var ReactIOSMount = require('ReactIOSMount'); -var ReactLegacyElement = require('ReactLegacyElement'); var ReactPropTypes = require('ReactPropTypes'); var deprecated = require('deprecated'); var invariant = require('invariant'); +var onlyChild = require('onlyChild'); ReactIOSDefaultInjection.inject(); var createElement = ReactElement.createElement; var createFactory = ReactElement.createFactory; +var cloneElement = ReactElement.cloneElement; if (__DEV__) { createElement = ReactElementValidator.createElement; createFactory = ReactElementValidator.createFactory; + cloneElement = ReactElementValidator.cloneElement; } -// TODO: Drop legacy elements once classes no longer export these factories -createElement = ReactLegacyElement.wrapCreateElement( - createElement -); -createFactory = ReactLegacyElement.wrapCreateFactory( - createFactory -); - var resolveDefaultProps = function(element) { // Could be optimized, but not currently in heavy use. var defaultProps = element.type.defaultProps; @@ -73,10 +68,18 @@ var render = function(component, mountInto) { var ReactIOS = { hasReactIOSInitialized: false, + Children: { + map: ReactChildren.map, + forEach: ReactChildren.forEach, + count: ReactChildren.count, + only: onlyChild + }, + Component: ReactComponent, PropTypes: ReactPropTypes, - createClass: ReactCompositeComponent.createClass, + createClass: ReactClass.createClass, createElement: createElement, createFactory: createFactory, + cloneElement: cloneElement, _augmentElement: augmentElement, render: render, unmountComponentAtNode: ReactIOSMount.unmountComponentAtNode, diff --git a/Libraries/ReactIOS/ReactIOSComponentEnvironment.js b/Libraries/ReactIOS/ReactIOSComponentEnvironment.js index 67da68b13..b27863f5a 100644 --- a/Libraries/ReactIOS/ReactIOSComponentEnvironment.js +++ b/Libraries/ReactIOS/ReactIOSComponentEnvironment.js @@ -4,19 +4,15 @@ * @providesModule ReactIOSComponentEnvironment */ 'use strict'; -var RKUIManager = require('NativeModulesDeprecated').RKUIManager; var ReactIOSDOMIDOperations = require('ReactIOSDOMIDOperations'); var ReactIOSReconcileTransaction = require('ReactIOSReconcileTransaction'); -var ReactIOSTagHandles = require('ReactIOSTagHandles'); -var ReactPerf = require('ReactPerf'); var ReactIOSComponentEnvironment = { - /** - * Will need to supply something that implements this. - */ - BackendIDOperations: ReactIOSDOMIDOperations, + processChildrenUpdates: ReactIOSDOMIDOperations.dangerouslyProcessChildrenUpdates, + + replaceNodeWithMarkupByID: ReactIOSDOMIDOperations.dangerouslyReplaceNodeWithMarkupByID, /** * Nothing to do for UIKit bridge. @@ -34,34 +30,6 @@ var ReactIOSComponentEnvironment = { }, - /** - * @param {View} view View tree image. - * @param {number} containerViewID View to insert sub-view into. - */ - mountImageIntoNode: ReactPerf.measure( - // FIXME(frantic): #4441289 Hack to avoid modifying react-tools - 'ReactComponentBrowserEnvironment', - 'mountImageIntoNode', - function(mountImage, containerID) { - // Since we now know that the `mountImage` has been mounted, we can - // mark it as such. - ReactIOSTagHandles.associateRootNodeIDWithMountedNodeHandle( - mountImage.rootNodeID, - mountImage.tag - ); - var addChildTags = [mountImage.tag]; - var addAtIndices = [0]; - RKUIManager.manageChildren( - ReactIOSTagHandles.mostRecentMountedNodeHandleForRootNodeID(containerID), - null, // moveFromIndices - null, // moveToIndices - addChildTags, - addAtIndices, - null // removeAtIndices - ); - } - ), - ReactReconcileTransaction: ReactIOSReconcileTransaction, }; diff --git a/Libraries/ReactIOS/ReactIOSComponentMixin.js b/Libraries/ReactIOS/ReactIOSComponentMixin.js index 7abb213ee..f9ddde2c2 100644 --- a/Libraries/ReactIOS/ReactIOSComponentMixin.js +++ b/Libraries/ReactIOS/ReactIOSComponentMixin.js @@ -6,6 +6,7 @@ 'use strict'; var ReactIOSTagHandles = require('ReactIOSTagHandles'); +var ReactInstanceMap = require('ReactInstanceMap'); /** * ReactNative vs ReactWeb @@ -55,11 +56,17 @@ var ReactIOSComponentMixin = { * `getNodeHandle`. */ getNativeNode: function() { - return ReactIOSTagHandles.rootNodeIDToTag[this._rootNodeID]; + // TODO (balpert): Wrap iOS native components in a composite wrapper, then + // ReactInstanceMap.get here will always succeed + return ReactIOSTagHandles.rootNodeIDToTag[ + (ReactInstanceMap.get(this) || this)._rootNodeID + ]; }, getNodeHandle: function() { - return ReactIOSTagHandles.rootNodeIDToTag[this._rootNodeID]; + return ReactIOSTagHandles.rootNodeIDToTag[ + (ReactInstanceMap.get(this) || this)._rootNodeID + ]; } }; diff --git a/Libraries/ReactIOS/ReactIOSDOMIDOperations.js b/Libraries/ReactIOS/ReactIOSDOMIDOperations.js index 51a520155..dfb96f8cb 100644 --- a/Libraries/ReactIOS/ReactIOSDOMIDOperations.js +++ b/Libraries/ReactIOS/ReactIOSDOMIDOperations.js @@ -9,7 +9,7 @@ var ReactIOSTagHandles = require('ReactIOSTagHandles'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); -var RKUIManager = require('NativeModulesDeprecated').RKUIManager; +var RCTUIManager = require('NativeModules').UIManager; var ReactPerf = require('ReactPerf'); /** @@ -55,7 +55,7 @@ var dangerouslyProcessChildrenUpdates = function(childrenUpdates, markupList) { for (var updateParentTagString in byContainerTag) { var updateParentTagNumber = +updateParentTagString; var childUpdatesToSend = byContainerTag[updateParentTagNumber]; - RKUIManager.manageChildren( + RCTUIManager.manageChildren( updateParentTagNumber, childUpdatesToSend.moveFromIndices, childUpdatesToSend.moveToIndices, @@ -89,7 +89,7 @@ var ReactIOSDOMIDOperations = { 'dangerouslyReplaceNodeWithMarkupByID', function(id, mountImage) { var oldTag = ReactIOSTagHandles.mostRecentMountedNodeHandleForRootNodeID(id); - RKUIManager.replaceExistingNonRootView(oldTag, mountImage.tag); + RCTUIManager.replaceExistingNonRootView(oldTag, mountImage.tag); ReactIOSTagHandles.associateRootNodeIDWithMountedNodeHandle(id, mountImage.tag); } ), diff --git a/Libraries/ReactIOS/ReactIOSDefaultInjection.js b/Libraries/ReactIOS/ReactIOSDefaultInjection.js index 729c294e5..09dc9ea0a 100644 --- a/Libraries/ReactIOS/ReactIOSDefaultInjection.js +++ b/Libraries/ReactIOS/ReactIOSDefaultInjection.js @@ -15,20 +15,19 @@ var EventPluginUtils = require('EventPluginUtils'); var IOSDefaultEventPluginOrder = require('IOSDefaultEventPluginOrder'); var IOSNativeBridgeEventPlugin = require('IOSNativeBridgeEventPlugin'); var NodeHandle = require('NodeHandle'); -var ReactComponent = require('ReactComponent'); -var ReactCompositeComponent = require('ReactCompositeComponent'); +var ReactClass = require('ReactClass'); +var ReactComponentEnvironment = require('ReactComponentEnvironment'); var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); -var ReactElement = require('ReactElement'); var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactIOSComponentEnvironment = require('ReactIOSComponentEnvironment'); var ReactIOSComponentMixin = require('ReactIOSComponentMixin'); var ReactIOSGlobalInteractionHandler = require('ReactIOSGlobalInteractionHandler'); var ReactIOSGlobalResponderHandler = require('ReactIOSGlobalResponderHandler'); var ReactIOSMount = require('ReactIOSMount'); -var ReactTextComponent = require('ReactTextComponent'); +var ReactIOSTextComponent = require('ReactIOSTextComponent'); +var ReactNativeComponent = require('ReactNativeComponent'); var ReactUpdates = require('ReactUpdates'); var ResponderEventPlugin = require('ResponderEventPlugin'); -var RKRawText = require('RKRawText'); var UniversalWorkerNodeHandle = require('UniversalWorkerNodeHandle'); // Just to ensure this gets packaged, since its only caller is from Native. @@ -68,23 +67,17 @@ function inject() { ReactDefaultBatchingStrategy ); - ReactComponent.injection.injectEnvironment( + ReactComponentEnvironment.injection.injectEnvironment( ReactIOSComponentEnvironment ); EventPluginUtils.injection.injectMount(ReactIOSMount); - ReactCompositeComponent.injection.injectMixin(ReactIOSComponentMixin); + ReactClass.injection.injectMixin(ReactIOSComponentMixin); - ReactTextComponent.inject(function(initialText) { - // RKRawText is a class so we can't invoke it directly. Instead of using - // a factory, we use the internal fast path to create a descriptor. - // RKRawText is not quite a class yet, so we access the real class from - // the type property. TODO: Change this once factory wrappers are gone. - return new ReactElement(RKRawText.type, null, null, null, null, { - text: initialText - }); - }); + ReactNativeComponent.injection.injectTextComponentClass( + ReactIOSTextComponent + ); NodeHandle.injection.injectImplementation(UniversalWorkerNodeHandle); } diff --git a/Libraries/ReactIOS/ReactIOSGlobalResponderHandler.js b/Libraries/ReactIOS/ReactIOSGlobalResponderHandler.js index 4a9c94410..d22f98489 100644 --- a/Libraries/ReactIOS/ReactIOSGlobalResponderHandler.js +++ b/Libraries/ReactIOS/ReactIOSGlobalResponderHandler.js @@ -3,17 +3,17 @@ */ 'use strict'; -var RKUIManager = require('NativeModules').RKUIManager; +var RCTUIManager = require('NativeModules').UIManager; var ReactIOSTagHandles = require('ReactIOSTagHandles'); var ReactIOSGlobalResponderHandler = { onChange: function(from, to) { if (to !== null) { - RKUIManager.setJSResponder( + RCTUIManager.setJSResponder( ReactIOSTagHandles.mostRecentMountedNodeHandleForRootNodeID(to) ); } else { - RKUIManager.clearJSResponder(); + RCTUIManager.clearJSResponder(); } } }; diff --git a/Libraries/ReactIOS/ReactIOSMount.js b/Libraries/ReactIOS/ReactIOSMount.js index 4e29be92f..e26e9934e 100644 --- a/Libraries/ReactIOS/ReactIOSMount.js +++ b/Libraries/ReactIOS/ReactIOSMount.js @@ -5,11 +5,14 @@ */ 'use strict'; -var RKUIManager = require('NativeModulesDeprecated').RKUIManager; +var RCTUIManager = require('NativeModules').UIManager; var ReactIOSTagHandles = require('ReactIOSTagHandles'); var ReactPerf = require('ReactPerf'); +var ReactReconciler = require('ReactReconciler'); +var ReactUpdates = require('ReactUpdates'); +var emptyObject = require('emptyObject'); var instantiateReactComponent = require('instantiateReactComponent'); var invariant = require('invariant'); @@ -19,6 +22,49 @@ function instanceNumberToChildRootID(rootNodeID, instanceNumber) { return rootNodeID + '[' + instanceNumber + ']'; } +/** + * Mounts this component and inserts it into the DOM. + * + * @param {ReactComponent} componentInstance The instance to mount. + * @param {number} rootID ID of the root node. + * @param {number} container container element to mount into. + * @param {ReactReconcileTransaction} transaction + */ +function mountComponentIntoNode( + componentInstance, + rootID, + container, + transaction) { + var markup = ReactReconciler.mountComponent( + componentInstance, rootID, transaction, emptyObject + ); + componentInstance._isTopLevel = true; + ReactIOSMount._mountImageIntoNode(markup, container); +} + +/** + * Batched mount. + * + * @param {ReactComponent} componentInstance The instance to mount. + * @param {number} rootID ID of the root node. + * @param {number} container container element to mount into. + */ +function batchedMountComponentIntoNode( + componentInstance, + rootID, + container) { + var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(); + transaction.perform( + mountComponentIntoNode, + null, + componentInstance, + rootID, + container, + transaction + ); + ReactUpdates.ReactReconcileTransaction.release(transaction); +} + /** * As soon as `ReactMount` is refactored to not rely on the DOM, we can share * code between the two. For now, we'll hard code the ID logic. @@ -52,9 +98,47 @@ var ReactIOSMount = { ReactIOSMount.instanceCount++ ); ReactIOSMount._instancesByContainerID[topRootNodeID] = instance; - instance.mountComponentIntoNode(childRootNodeID, topRootNodeID); + + // The initial render is synchronous but any updates that happen during + // rendering, in componentWillMount or componentDidMount, will be batched + // according to the current batching strategy. + + ReactUpdates.batchedUpdates( + batchedMountComponentIntoNode, + instance, + childRootNodeID, + topRootNodeID + ); }, + /** + * @param {View} view View tree image. + * @param {number} containerViewID View to insert sub-view into. + */ + _mountImageIntoNode: ReactPerf.measure( + // FIXME(frantic): #4441289 Hack to avoid modifying react-tools + 'ReactComponentBrowserEnvironment', + 'mountImageIntoNode', + function(mountImage, containerID) { + // Since we now know that the `mountImage` has been mounted, we can + // mark it as such. + ReactIOSTagHandles.associateRootNodeIDWithMountedNodeHandle( + mountImage.rootNodeID, + mountImage.tag + ); + var addChildTags = [mountImage.tag]; + var addAtIndices = [0]; + RCTUIManager.manageChildren( + ReactIOSTagHandles.mostRecentMountedNodeHandleForRootNodeID(containerID), + null, // moveFromIndices + null, // moveToIndices + addChildTags, + addAtIndices, + null // removeAtIndices + ); + } + ), + /** * Standard unmounting of the component that is rendered into `containerID`, * but will also execute a command to remove the actual container view @@ -66,7 +150,7 @@ var ReactIOSMount = { unmountComponentAtNodeAndRemoveContainer: function(containerTag) { ReactIOSMount.unmountComponentAtNode(containerTag); // call back into native to remove all of the subviews from this container - RKUIManager.removeRootView(containerTag); + RCTUIManager.removeRootView(containerTag); }, /** @@ -106,7 +190,7 @@ var ReactIOSMount = { instance.unmountComponent(); var containerTag = ReactIOSTagHandles.mostRecentMountedNodeHandleForRootNodeID(containerID); - RKUIManager.removeSubviewsFromContainerWithID(containerTag); + RCTUIManager.removeSubviewsFromContainerWithID(containerTag); }, getNode: function(id) { diff --git a/Libraries/ReactIOS/ReactIOSNativeComponent.js b/Libraries/ReactIOS/ReactIOSNativeComponent.js index 7c8381855..3bbec560c 100644 --- a/Libraries/ReactIOS/ReactIOSNativeComponent.js +++ b/Libraries/ReactIOS/ReactIOSNativeComponent.js @@ -6,13 +6,12 @@ 'use strict'; var NativeMethodsMixin = require('NativeMethodsMixin'); -var ReactComponent = require('ReactComponent'); var ReactIOSComponentMixin = require('ReactIOSComponentMixin'); var ReactIOSEventEmitter = require('ReactIOSEventEmitter'); var ReactIOSStyleAttributes = require('ReactIOSStyleAttributes'); var ReactIOSTagHandles = require('ReactIOSTagHandles'); var ReactMultiChild = require('ReactMultiChild'); -var RKUIManager = require('NativeModulesDeprecated').RKUIManager; +var RCTUIManager = require('NativeModules').UIManager; var styleDiffer = require('styleDiffer'); var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); @@ -32,8 +31,6 @@ var deleteAllListeners = ReactIOSEventEmitter.deleteAllListeners; */ var ReactIOSNativeComponent = function(viewConfig) { this.viewConfig = viewConfig; - this.props = null; - this.previousFlattenedStyle = null; }; /** @@ -65,10 +62,19 @@ cachedIndexArray._cache = {}; * which is a `viewID` ... see the return value for `mountComponent` ! */ ReactIOSNativeComponent.Mixin = { + getPublicInstance: function() { + // TODO: This should probably use a composite wrapper + return this; + }, + + construct: function(element) { + this._currentElement = element; + }, + unmountComponent: function() { deleteAllListeners(this._rootNodeID); - ReactComponent.Mixin.unmountComponent.call(this); this.unmountChildren(); + this._rootNodeID = null; }, /** @@ -79,8 +85,8 @@ ReactIOSNativeComponent.Mixin = { * a child of a container can confidently record that in * `ReactIOSTagHandles`. */ - initializeChildren: function(children, containerTag, transaction) { - var mountImages = this.mountChildren(children, transaction); + initializeChildren: function(children, containerTag, transaction, context) { + var mountImages = this.mountChildren(children, transaction, context); // In a well balanced tree, half of the nodes are in the bottom row and have // no children - let's avoid calling out to the native bridge for a large // portion of the children. @@ -103,7 +109,7 @@ ReactIOSNativeComponent.Mixin = { ); createdTags[i] = mountImage.tag; } - RKUIManager + RCTUIManager .manageChildren(containerTag, null, null, createdTags, indexes, null); } }, @@ -158,26 +164,23 @@ ReactIOSNativeComponent.Mixin = { /** * Updates the component's currently mounted representation. * + * @param {object} nextElement * @param {ReactReconcileTransaction} transaction - * @param {object} prevDescriptor + * @param {object} context * @internal */ - updateComponent: function(transaction, prevDescriptor) { - ReactComponent.Mixin.updateComponent.call( - this, - transaction, - prevDescriptor - ); - var nextDescriptor = this._currentElement; + receiveComponent: function(nextElement, transaction, context) { + var prevElement = this._currentElement; + this._currentElement = nextElement; var updatePayload = this.computeUpdatedProperties( - prevDescriptor.props, - nextDescriptor.props, + prevElement.props, + nextElement.props, this.viewConfig.validAttributes ); if (updatePayload) { - RKUIManager.updateView( + RCTUIManager.updateView( ReactIOSTagHandles.mostRecentMountedNodeHandleForRootNodeID(this._rootNodeID), this.viewConfig.uiViewClassName, updatePayload @@ -185,10 +188,10 @@ ReactIOSNativeComponent.Mixin = { } this._reconcileListenersUponUpdate( - prevDescriptor.props, - nextDescriptor.props + prevElement.props, + nextElement.props ); - this.updateChildren(this.props.children, transaction); + this.updateChildren(nextElement.props.children, transaction, context); }, /** @@ -223,25 +226,26 @@ ReactIOSNativeComponent.Mixin = { * @param {Transaction} transaction For creating/updating. * @return {string} Unique iOS view tag. */ - mountComponent: function(rootID, transaction, mountDepth) { - ReactComponent.Mixin.mountComponent.call( - this, - rootID, - transaction, - mountDepth - ); + mountComponent: function(rootID, transaction, context) { + this._rootNodeID = rootID; + var tag = ReactIOSTagHandles.allocateTag(); this.previousFlattenedStyle = {}; var updatePayload = this.computeUpdatedProperties( {}, // previous props - this.props, // next props + this._currentElement.props, // next props this.viewConfig.validAttributes ); - RKUIManager.createView(tag, this.viewConfig.uiViewClassName, updatePayload); + RCTUIManager.createView(tag, this.viewConfig.uiViewClassName, updatePayload); - this._registerListenersUponCreation(this.props); - this.initializeChildren(this.props.children, tag, transaction); + this._registerListenersUponCreation(this._currentElement.props); + this.initializeChildren( + this._currentElement.props.children, + tag, + transaction, + context + ); return { rootNodeID: rootID, tag: tag @@ -255,7 +259,6 @@ ReactIOSNativeComponent.Mixin = { */ Object.assign( ReactIOSNativeComponent.prototype, - ReactComponent.Mixin, ReactMultiChild.Mixin, ReactIOSNativeComponent.Mixin, NativeMethodsMixin, diff --git a/Libraries/ReactIOS/ReactIOSTextComponent.js b/Libraries/ReactIOS/ReactIOSTextComponent.js new file mode 100644 index 000000000..ef53b2968 --- /dev/null +++ b/Libraries/ReactIOS/ReactIOSTextComponent.js @@ -0,0 +1,62 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule ReactIOSTextComponent + */ + +'use strict'; + +var ReactIOSTagHandles = require('ReactIOSTagHandles'); +var RCTUIManager = require('NativeModules').UIManager; + +var assign = require('Object.assign'); + +var ReactIOSTextComponent = function(props) { + // This constructor and its argument is currently used by mocks. +}; + +assign(ReactIOSTextComponent.prototype, { + + construct: function(text) { + // This is really a ReactText (ReactNode), not a ReactElement + this._currentElement = text; + this._stringText = '' + text; + this._rootNodeID = null; + }, + + mountComponent: function(rootID, transaction, context) { + this._rootNodeID = rootID; + var tag = ReactIOSTagHandles.allocateTag(); + RCTUIManager.createView(tag, 'RCTRawText', {text: this._stringText}); + return { + rootNodeID: rootID, + tag: tag, + }; + }, + + receiveComponent: function(nextText, transaction, context) { + if (nextText !== this._currentElement) { + this._currentElement = nextText; + var nextStringText = '' + nextText; + if (nextStringText !== this._stringText) { + this._stringText = nextStringText; + RCTUIManager.updateView( + ReactIOSTagHandles.mostRecentMountedNodeHandleForRootNodeID( + this._rootNodeID + ), + 'RCTRawText', + {text: this._stringText} + ); + } + } + }, + + unmountComponent: function() { + this._currentElement = null; + this._stringText = null; + this._rootNodeID = null; + } + +}); + +module.exports = ReactIOSTextComponent; diff --git a/Libraries/ReactIOS/ReactIOSViewAttributes.js b/Libraries/ReactIOS/ReactIOSViewAttributes.js index df5dfb249..9a04b315d 100644 --- a/Libraries/ReactIOS/ReactIOSViewAttributes.js +++ b/Libraries/ReactIOS/ReactIOSViewAttributes.js @@ -17,10 +17,10 @@ ReactIOSViewAttributes.UIView = { testID: true, }; -ReactIOSViewAttributes.RKView = merge( +ReactIOSViewAttributes.RCTView = merge( ReactIOSViewAttributes.UIView, { - // This is a special performance property exposed by RKView and useful for + // This is a special performance property exposed by RCTView and useful for // scrolling content when there are many subviews, most of which are offscreen. // For this property to be effective, it must be applied to a view that contains // many subviews that extend outside its bound. The subviews must also have diff --git a/Libraries/ReactIOS/ReactTextComponent.js b/Libraries/ReactIOS/ReactTextComponent.js deleted file mode 100644 index 7ecbed18a..000000000 --- a/Libraries/ReactIOS/ReactTextComponent.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @providesModule ReactTextComponent - * @typechecks static-only - */ - -"use strict"; - -var InjectedTextComponent = null; -var ReactTextComponent = function() { - return InjectedTextComponent.apply(this, arguments); -}; -ReactTextComponent.inject = function(textComponent) { - InjectedTextComponent = textComponent; -}; - -module.exports = ReactTextComponent; diff --git a/Libraries/ReactIOS/createReactIOSNativeComponentClass.js b/Libraries/ReactIOS/createReactIOSNativeComponentClass.js index 8df2f510b..465b26ba4 100644 --- a/Libraries/ReactIOS/createReactIOSNativeComponentClass.js +++ b/Libraries/ReactIOS/createReactIOSNativeComponentClass.js @@ -7,7 +7,6 @@ "use strict"; var ReactElement = require('ReactElement'); -var ReactLegacyElement = require('ReactLegacyElement'); var ReactIOSNativeComponent = require('ReactIOSNativeComponent'); /** @@ -15,15 +14,17 @@ var ReactIOSNativeComponent = require('ReactIOSNativeComponent'); * @private */ var createReactIOSNativeComponentClass = function(viewConfig) { - var Constructor = function(props) { + var Constructor = function(element) { + this._currentElement = element; + + this._rootNodeID = null; + this._renderedChildren = null; + this.previousFlattenedStyle = null; }; Constructor.displayName = viewConfig.uiViewClassName; Constructor.prototype = new ReactIOSNativeComponent(viewConfig); - Constructor.prototype.constructor = Constructor; - return ReactLegacyElement.wrapFactory( - ReactElement.createFactory(Constructor) - ); + return Constructor; }; module.exports = createReactIOSNativeComponentClass; diff --git a/Libraries/ReactIOS/nativePropType.js b/Libraries/ReactIOS/nativePropType.js deleted file mode 100644 index d61c7f191..000000000 --- a/Libraries/ReactIOS/nativePropType.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule nativePropType - */ -'use strict' - -/** - * A simple wrapper for prop types to mark them as native, which will allow them - * to be passed over the bridge to be applied to the native component if - * processed by `validAttributesFromPropTypes`. - */ -function nativePropType(propType) { - propType.isNative = true; - return propType; -} - -module.exports = nativePropType; diff --git a/Libraries/ReactIOS/renderApplication.ios.js b/Libraries/ReactIOS/renderApplication.js similarity index 81% rename from Libraries/ReactIOS/renderApplication.ios.js rename to Libraries/ReactIOS/renderApplication.js index 05d694c09..31ebf4678 100644 --- a/Libraries/ReactIOS/renderApplication.ios.js +++ b/Libraries/ReactIOS/renderApplication.js @@ -14,7 +14,12 @@ function renderApplication(RootComponent, initialProps, rootTag) { rootTag, 'Expect to have a valid rootTag, instead got ', rootTag ); - React.render(, rootTag); + React.render( + , + rootTag + ); } module.exports = renderApplication; diff --git a/Libraries/Storage/AsyncStorage.ios.js b/Libraries/Storage/AsyncStorage.ios.js new file mode 100644 index 000000000..b1a68c40b --- /dev/null +++ b/Libraries/Storage/AsyncStorage.ios.js @@ -0,0 +1,193 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AsyncStorage + * @flow-weak + */ +'use strict'; + +var NativeModules = require('NativeModules'); +var RCTAsyncLocalStorage = NativeModules.AsyncLocalStorage; +var RCTAsyncRocksDBStorage = NativeModules.AsyncRocksDBStorage; + +// We use RocksDB if available. +var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncLocalStorage; + +/** + * AsyncStorage is a simple, asynchronous, persistent, global, key-value storage + * system. It should be used instead of LocalStorage. + * + * It is recommended that you use an abstraction on top of AsyncStorage instead + * of AsyncStorage directly for anything more than light usage since it + * operates globally. + * + * This JS code is a simple facad over the native iOS implementation to provide + * a clear JS API, real Error objects, and simple non-multi functions. + */ +var AsyncStorage = { + /** + * Fetches `key` and passes the result to `callback`, along with an `Error` if + * there is any. + */ + getItem: function( + key: string, + callback: (error: ?Error, result: ?string) => void + ): void { + RCTAsyncStorage.multiGet([key], function(errors, result) { + // Unpack result to get value from [[key,value]] + var value = (result && result[0] && result[0][1]) ? result[0][1] : null; + callback((errors && convertError(errors[0])) || null, value); + }); + }, + + /** + * Sets `value` for `key` and calls `callback` on completion, along with an + * `Error` if there is any. + */ + setItem: function( + key: string, + value: string, + callback: ?(error: ?Error) => void + ): void { + RCTAsyncStorage.multiSet([[key,value]], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + }); + }, + + removeItem: function( + key: string, + callback: ?(error: ?Error) => void + ): void { + RCTAsyncStorage.multiRemove([key], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + }); + }, + + /** + * Merges existing value with input value, assuming they are stringified json. + * + * Not supported by all native implementations. + */ + mergeItem: function( + key: string, + value: string, + callback: ?(error: ?Error) => void + ): void { + RCTAsyncStorage.multiMerge([[key,value]], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + }); + }, + + /** + * Erases *all* AsyncStorage for all clients, libraries, etc. You probably + * don't want to call this - use removeItem or multiRemove to clear only your + * own keys instead. + */ + clear: function(callback: ?(error: ?Error) => void) { + RCTAsyncStorage.clear(function(error) { + callback && callback(convertError(error)); + }); + }, + + /** + * Gets *all* keys known to the system, for all callers, libraries, etc. + */ + getAllKeys: function(callback: (error: ?Error) => void) { + RCTAsyncStorage.getAllKeys(function(error, keys) { + callback(convertError(error), keys); + }); + }, + + /** + * The following batched functions are useful for executing a lot of + * operations at once, allowing for native optimizations and provide the + * convenience of a single callback after all operations are complete. + * + * These functions return arrays of errors, potentially one for every key. + * For key-specific errors, the Error object will have a key property to + * indicate which key caused the error. + */ + + /** + * multiGet invokes callback with an array of key-value pair arrays that + * matches the input format of multiSet. + * + * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) + */ + multiGet: function( + keys: Array, + callback: (errors: ?Array, result: ?Array>) => void + ): void { + RCTAsyncStorage.multiGet(keys, function(errors, result) { + callback( + (errors && errors.map((error) => convertError(error))) || null, + result + ); + }); + }, + + /** + * multiSet and multiMerge take arrays of key-value array pairs that match + * the output of multiGet, e.g. + * + * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); + */ + multiSet: function( + keyValuePairs: Array>, + callback: ?(errors: ?Array) => void + ): void { + RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { + callback && callback( + (errors && errors.map((error) => convertError(error))) || null + ); + }); + }, + + /** + * Delete all the keys in the `keys` array. + */ + multiRemove: function( + keys: Array, + callback: ?(errors: ?Array) => void + ): void { + RCTAsyncStorage.multiRemove(keys, function(errors) { + callback && callback( + (errors && errors.map((error) => convertError(error))) || null + ); + }); + }, + + /** + * Merges existing values with input values, assuming they are stringified + * json. + * + * Not supported by all native implementations. + */ + multiMerge: function( + keyValuePairs: Array>, + callback: ?(errors: ?Array) => void + ): void { + RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { + callback && callback( + (errors && errors.map((error) => convertError(error))) || null + ); + }); + }, +}; + +// Not all native implementations support merge. +if (!RCTAsyncStorage.multiMerge) { + delete AsyncStorage.mergeItem; + delete AsyncStorage.multiMerge; +} + +function convertError(error) { + if (!error) { + return null; + } + var out = new Error(error.message); + out.key = error.key; // flow doesn't like this :( + return out; +} + +module.exports = AsyncStorage; diff --git a/Libraries/StyleSheet/ArrayOfPropType.js b/Libraries/StyleSheet/ArrayOfPropType.js deleted file mode 100644 index 89d224d8b..000000000 --- a/Libraries/StyleSheet/ArrayOfPropType.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule ArrayOfPropType - */ -'use strict' - -var ReactPropTypes = require('ReactPropTypes'); - -var deepDiffer = require('deepDiffer'); - -var ArrayOfPropType = (type, differ) => { - var checker = ReactPropTypes.arrayOf(type); - checker.differ = differ ? differ : deepDiffer; - return checker; -}; - -module.exports = ArrayOfPropType; diff --git a/Libraries/StyleSheet/EdgeInsetsPropType.js b/Libraries/StyleSheet/EdgeInsetsPropType.js index dcf38de79..a57abcaf8 100644 --- a/Libraries/StyleSheet/EdgeInsetsPropType.js +++ b/Libraries/StyleSheet/EdgeInsetsPropType.js @@ -17,6 +17,4 @@ var EdgeInsetsPropType = createStrictShapeTypeChecker({ right: PropTypes.number, }); -EdgeInsetsPropType.differ = insetsDiffer; - module.exports = EdgeInsetsPropType; diff --git a/Libraries/StyleSheet/PointPropType.js b/Libraries/StyleSheet/PointPropType.js index b281b96e0..c409bd660 100644 --- a/Libraries/StyleSheet/PointPropType.js +++ b/Libraries/StyleSheet/PointPropType.js @@ -15,6 +15,4 @@ var PointPropType = createStrictShapeTypeChecker({ y: PropTypes.number, }); -PointPropType.differ = pointsDiffer; - module.exports = PointPropType; diff --git a/Libraries/StyleSheet/StyleSheet.js b/Libraries/StyleSheet/StyleSheet.js index cca0f87da..ec771e332 100644 --- a/Libraries/StyleSheet/StyleSheet.js +++ b/Libraries/StyleSheet/StyleSheet.js @@ -5,19 +5,15 @@ */ 'use strict'; -var ImageStylePropTypes = require('ImageStylePropTypes'); -var ReactPropTypeLocations = require('ReactPropTypeLocations'); var StyleSheetRegistry = require('StyleSheetRegistry'); -var TextStylePropTypes = require('TextStylePropTypes'); -var ViewStylePropTypes = require('ViewStylePropTypes'); - -var invariant = require('invariant'); +var StyleSheetValidation = require('StyleSheetValidation'); /** * A StyleSheet is an abstraction similar to CSS StyleSheets * * Create a new StyleSheet: * + * ``` * var styles = StyleSheet.create({ * container: { * borderRadius: 4, @@ -31,21 +27,26 @@ var invariant = require('invariant'); * activeTitle: { * color: 'red', * }, - * }) + * }); + * ``` * * Use a StyleSheet: * + * ``` * * * + * ``` * * Code quality: + * * - By moving styles away from the render function, you're making the code * code easier to understand. * - Naming the styles is a good way to add meaning to the low level components * in the render function. * * Performance: + * * - Making a stylesheet from a style object makes it possible to refer to it * by ID instead of creating a new style object every time. * - It also allows to send the style only once through the bridge. All @@ -55,66 +56,11 @@ class StyleSheet { static create(obj) { var result = {}; for (var key in obj) { - StyleSheet.validateStyle(key, obj); + StyleSheetValidation.validateStyle(key, obj); result[key] = StyleSheetRegistry.registerStyle(obj[key]); } return result; } - - static validateStyleProp(prop, style, caller) { - if (!__DEV__) { - return; - } - if (allStylePropTypes[prop] === undefined) { - var message1 = '"' + prop + '" is not a valid style property.'; - var message2 = '\nValid style props: ' + - JSON.stringify(Object.keys(allStylePropTypes), null, ' '); - styleError(message1, style, caller, message2); - } - var error = allStylePropTypes[prop]( - style, - prop, - caller, - ReactPropTypeLocations.prop - ); - if (error) { - styleError(error.message, style, caller); - } - } - - static validateStyle(name, styles) { - if (!__DEV__) { - return; - } - for (var prop in styles[name]) { - StyleSheet.validateStyleProp(prop, styles[name], 'StyleSheet ' + name); - } - } - - static addValidStylePropTypes(stylePropTypes) { - for (var key in stylePropTypes) { - invariant( - allStylePropTypes[key] === undefined || - allStylePropTypes[key] === stylePropTypes[key], - 'Attemped to redefine existing style prop type "' + key + '".' - ); - allStylePropTypes[key] = stylePropTypes[key]; - } - } } -var styleError = function(message1, style, caller, message2) { - invariant( - false, - message1 + '\n' + (caller || '<>') + ': ' + - JSON.stringify(style, null, ' ') + (message2 || '') - ); -}; - -var allStylePropTypes = {}; - -StyleSheet.addValidStylePropTypes(ImageStylePropTypes); -StyleSheet.addValidStylePropTypes(TextStylePropTypes); -StyleSheet.addValidStylePropTypes(ViewStylePropTypes); - module.exports = StyleSheet; diff --git a/Libraries/StyleSheet/StyleSheetValidation.js b/Libraries/StyleSheet/StyleSheetValidation.js new file mode 100644 index 000000000..694f11d5a --- /dev/null +++ b/Libraries/StyleSheet/StyleSheetValidation.js @@ -0,0 +1,72 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule StyleSheetValidation + */ +'use strict'; + +var ImageStylePropTypes = require('ImageStylePropTypes'); +var ReactPropTypeLocations = require('ReactPropTypeLocations'); +var TextStylePropTypes = require('TextStylePropTypes'); +var ViewStylePropTypes = require('ViewStylePropTypes'); + +var invariant = require('invariant'); + +class StyleSheetValidation { + static validateStyleProp(prop, style, caller) { + if (!__DEV__) { + return; + } + if (allStylePropTypes[prop] === undefined) { + var message1 = '"' + prop + '" is not a valid style property.'; + var message2 = '\nValid style props: ' + + JSON.stringify(Object.keys(allStylePropTypes), null, ' '); + styleError(message1, style, caller, message2); + } + var error = allStylePropTypes[prop]( + style, + prop, + caller, + ReactPropTypeLocations.prop + ); + if (error) { + styleError(error.message, style, caller); + } + } + + static validateStyle(name, styles) { + if (!__DEV__) { + return; + } + for (var prop in styles[name]) { + StyleSheetValidation.validateStyleProp(prop, styles[name], 'StyleSheet ' + name); + } + } + + static addValidStylePropTypes(stylePropTypes) { + for (var key in stylePropTypes) { + invariant( + allStylePropTypes[key] === undefined || + allStylePropTypes[key] === stylePropTypes[key], + 'Attemped to redefine existing style prop type "' + key + '".' + ); + allStylePropTypes[key] = stylePropTypes[key]; + } + } +} + +var styleError = function(message1, style, caller, message2) { + invariant( + false, + message1 + '\n' + (caller || '<>') + ': ' + + JSON.stringify(style, null, ' ') + (message2 || '') + ); +}; + +var allStylePropTypes = {}; + +StyleSheetValidation.addValidStylePropTypes(ImageStylePropTypes); +StyleSheetValidation.addValidStylePropTypes(TextStylePropTypes); +StyleSheetValidation.addValidStylePropTypes(ViewStylePropTypes); + +module.exports = StyleSheetValidation; diff --git a/ReactKit/Views/RCTRawTextManager.h b/Libraries/Text/RCTRawTextManager.h similarity index 100% rename from ReactKit/Views/RCTRawTextManager.h rename to Libraries/Text/RCTRawTextManager.h diff --git a/ReactKit/Views/RCTRawTextManager.m b/Libraries/Text/RCTRawTextManager.m similarity index 100% rename from ReactKit/Views/RCTRawTextManager.m rename to Libraries/Text/RCTRawTextManager.m diff --git a/ReactKit/Views/RCTShadowRawText.h b/Libraries/Text/RCTShadowRawText.h similarity index 100% rename from ReactKit/Views/RCTShadowRawText.h rename to Libraries/Text/RCTShadowRawText.h diff --git a/ReactKit/Views/RCTShadowRawText.m b/Libraries/Text/RCTShadowRawText.m similarity index 100% rename from ReactKit/Views/RCTShadowRawText.m rename to Libraries/Text/RCTShadowRawText.m diff --git a/ReactKit/Views/RCTShadowText.h b/Libraries/Text/RCTShadowText.h similarity index 100% rename from ReactKit/Views/RCTShadowText.h rename to Libraries/Text/RCTShadowText.h diff --git a/ReactKit/Views/RCTShadowText.m b/Libraries/Text/RCTShadowText.m similarity index 100% rename from ReactKit/Views/RCTShadowText.m rename to Libraries/Text/RCTShadowText.m diff --git a/ReactKit/Views/RCTText.h b/Libraries/Text/RCTText.h similarity index 100% rename from ReactKit/Views/RCTText.h rename to Libraries/Text/RCTText.h diff --git a/ReactKit/Views/RCTText.m b/Libraries/Text/RCTText.m similarity index 100% rename from ReactKit/Views/RCTText.m rename to Libraries/Text/RCTText.m diff --git a/Libraries/Text/RCTText.podspec b/Libraries/Text/RCTText.podspec new file mode 100644 index 000000000..656e0ee74 --- /dev/null +++ b/Libraries/Text/RCTText.podspec @@ -0,0 +1,28 @@ +Pod::Spec.new do |spec| + spec.name = 'RCTText' + spec.version = '0.0.1' + spec.summary = 'Provides basic Text capabilities in ReactNative apps.' + spec.description = <<-DESC + Text can be rendered in ReactNative apps with the component using this module. + DESC + spec.homepage = 'https://facebook.github.io/react-native/' + spec.license = { :type => 'BSD' } + spec.author = 'Facebook' + spec.platform = :ios, '7.0' + spec.requires_arc = true + spec.source_files = '**/*.{h,m,c}' + spec.dependency "ReactKit", "~> 0.0.1" + + # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # + # + # Link your library with frameworks, or libraries. Libraries do not include + # the lib prefix of their name. + # + + # s.framework = "SomeFramework" + # s.frameworks = "SomeFramework", "AnotherFramework" + + # s.library = "iconv" + #spec.libraries = "RCTText", "ReactKit" + +end diff --git a/Libraries/Text/RCTText.xcodeproj/project.pbxproj b/Libraries/Text/RCTText.xcodeproj/project.pbxproj new file mode 100644 index 000000000..22eafac25 --- /dev/null +++ b/Libraries/Text/RCTText.xcodeproj/project.pbxproj @@ -0,0 +1,272 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */; }; + 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511C91A9E6C5C00147676 /* RCTShadowRawText.m */; }; + 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; }; + 58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */; }; + 58B512161A9E6EFF00147676 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512141A9E6EFF00147676 /* RCTText.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 58B511991A9E6C1200147676 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 58B5119B1A9E6C1200147676 /* libRCTText.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTText.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRawTextManager.h; sourceTree = ""; }; + 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRawTextManager.m; sourceTree = ""; }; + 58B511C81A9E6C5C00147676 /* RCTShadowRawText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowRawText.h; sourceTree = ""; }; + 58B511C91A9E6C5C00147676 /* RCTShadowRawText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowRawText.m; sourceTree = ""; }; + 58B511CA1A9E6C5C00147676 /* RCTShadowText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowText.h; sourceTree = ""; }; + 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowText.m; sourceTree = ""; }; + 58B511CC1A9E6C5C00147676 /* RCTTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextManager.h; sourceTree = ""; }; + 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextManager.m; sourceTree = ""; }; + 58B512141A9E6EFF00147676 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = ""; }; + 58B512151A9E6EFF00147676 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 58B511981A9E6C1200147676 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 58B511921A9E6C1200147676 = { + isa = PBXGroup; + children = ( + 58B511C61A9E6C5C00147676 /* RCTRawTextManager.h */, + 58B511C71A9E6C5C00147676 /* RCTRawTextManager.m */, + 58B511C81A9E6C5C00147676 /* RCTShadowRawText.h */, + 58B511C91A9E6C5C00147676 /* RCTShadowRawText.m */, + 58B511CA1A9E6C5C00147676 /* RCTShadowText.h */, + 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */, + 58B512151A9E6EFF00147676 /* RCTText.h */, + 58B512141A9E6EFF00147676 /* RCTText.m */, + 58B511CC1A9E6C5C00147676 /* RCTTextManager.h */, + 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */, + 58B5119C1A9E6C1200147676 /* Products */, + ); + sourceTree = ""; + }; + 58B5119C1A9E6C1200147676 /* Products */ = { + isa = PBXGroup; + children = ( + 58B5119B1A9E6C1200147676 /* libRCTText.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 58B5119A1A9E6C1200147676 /* RCTText */ = { + isa = PBXNativeTarget; + buildConfigurationList = 58B511AF1A9E6C1300147676 /* Build configuration list for PBXNativeTarget "RCTText" */; + buildPhases = ( + 58B511971A9E6C1200147676 /* Sources */, + 58B511981A9E6C1200147676 /* Frameworks */, + 58B511991A9E6C1200147676 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTText; + productName = RCTText; + productReference = 58B5119B1A9E6C1200147676 /* libRCTText.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 58B511931A9E6C1200147676 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 58B5119A1A9E6C1200147676 = { + CreatedOnToolsVersion = 6.1.1; + }; + }; + }; + buildConfigurationList = 58B511961A9E6C1200147676 /* Build configuration list for PBXProject "RCTText" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 58B511921A9E6C1200147676; + productRefGroup = 58B5119C1A9E6C1200147676 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 58B5119A1A9E6C1200147676 /* RCTText */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 58B511971A9E6C1200147676 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */, + 58B511CE1A9E6C5C00147676 /* RCTRawTextManager.m in Sources */, + 58B512161A9E6EFF00147676 /* RCTText.m in Sources */, + 58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */, + 58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 58B511AD1A9E6C1300147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 58B511AE1A9E6C1300147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 58B511B01A9E6C1300147676 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 58B511B11A9E6C1300147676 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 58B511961A9E6C1200147676 /* Build configuration list for PBXProject "RCTText" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511AD1A9E6C1300147676 /* Debug */, + 58B511AE1A9E6C1300147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 58B511AF1A9E6C1300147676 /* Build configuration list for PBXNativeTarget "RCTText" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 58B511B01A9E6C1300147676 /* Debug */, + 58B511B11A9E6C1300147676 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 58B511931A9E6C1200147676 /* Project object */; +} diff --git a/ReactKit/Views/RCTTextManager.h b/Libraries/Text/RCTTextManager.h similarity index 100% rename from ReactKit/Views/RCTTextManager.h rename to Libraries/Text/RCTTextManager.h diff --git a/ReactKit/Views/RCTTextManager.m b/Libraries/Text/RCTTextManager.m similarity index 73% rename from ReactKit/Views/RCTTextManager.m rename to Libraries/Text/RCTTextManager.m index fe1ca6820..2a0ed3db7 100644 --- a/ReactKit/Views/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -23,18 +23,10 @@ return [[RCTShadowText alloc] init]; } +#pragma mark - View properties + RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor) - -- (void)set_textAlign:(id)json - forShadowView:(RCTShadowText *)shadowView - withDefaultView:(RCTShadowText *)defaultView -{ - shadowView.textAlign = json ? [RCTConvert NSTextAlignment:json] : defaultView.textAlign; -} - -- (void)set_numberOfLines:(id)json - forView:(RCTText *)view - withDefaultView:(RCTText *)defaultView +RCT_CUSTOM_VIEW_PROPERTY(numberOfLines, RCTText) { NSLineBreakMode truncationMode = NSLineBreakByClipping; view.numberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.numberOfLines; @@ -44,31 +36,35 @@ RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor) view.lineBreakMode = truncationMode; } -- (void)set_numberOfLines:(id)json - forShadowView:(RCTShadowText *)shadowView - withDefaultView:(RCTShadowText *)defaultView +#pragma mark - Shadow properties + +RCT_CUSTOM_SHADOW_PROPERTY(backgroundColor, RCTShadowText) +{ + view.textBackgroundColor = json ? [RCTConvert UIColor:json] : defaultView.textBackgroundColor; +} +RCT_CUSTOM_SHADOW_PROPERTY(containerBackgroundColor, RCTShadowText) +{ + view.backgroundColor = json ? [RCTConvert UIColor:json] : defaultView.backgroundColor; + view.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; +} +RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, RCTShadowText) { NSLineBreakMode truncationMode = NSLineBreakByClipping; - shadowView.maxNumberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.maxNumberOfLines; - if (shadowView.maxNumberOfLines > 0) { + view.maxNumberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.maxNumberOfLines; + if (view.maxNumberOfLines > 0) { truncationMode = NSLineBreakByTruncatingTail; } - shadowView.truncationMode = truncationMode; + view.truncationMode = truncationMode; +} +RCT_CUSTOM_SHADOW_PROPERTY(textAlign, RCTShadowText) +{ + view.textAlign = json ? [RCTConvert NSTextAlignment:json] : defaultView.textAlign; } -- (void)set_backgroundColor:(id)json - forShadowView:(RCTShadowText *)shadowView - withDefaultView:(RCTShadowText *)defaultView +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowView { - shadowView.textBackgroundColor = json ? [RCTConvert UIColor:json] : defaultView.textBackgroundColor; -} - -- (void)set_containerBackgroundColor:(id)json - forShadowView:(RCTShadowText *)shadowView - withDefaultView:(RCTShadowText *)defaultView -{ - shadowView.backgroundColor = json ? [RCTConvert UIColor:json] : defaultView.backgroundColor; - shadowView.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; + //TODO: This could be a cleaner replacement for uiBlockToAmendWithShadowViewRegistry + return nil; } // TODO: the purpose of this block is effectively just to copy properties from the shadow views diff --git a/Libraries/Components/Text/Text.js b/Libraries/Text/Text.js similarity index 86% rename from Libraries/Components/Text/Text.js rename to Libraries/Text/Text.js index 07160bc66..5d4efcd5c 100644 --- a/Libraries/Components/Text/Text.js +++ b/Libraries/Text/Text.js @@ -28,46 +28,42 @@ var viewConfig = { }; /** - * - A react component for displaying text which supports nesting, + * A react component for displaying text which supports nesting, * styling, and touch handling. In the following example, the nested title and * body text will inherit the `fontFamily` from `styles.baseText`, but the title * provides its own additional styles. The title and body will stack on top of * each other on account of the literal newlines: * - * renderText: function() { - * return ( - * - * - * {this.state.titleText + '\n\n'} - * - * - * {this.state.bodyText} - * + * ``` + * renderText: function() { + * return ( + * + * + * {this.state.titleText + '\n\n'} * - * ); + * + * {this.state.bodyText} + * + * + * ); + * }, + * ... + * var styles = StyleSheet.create({ + * baseText: { + * fontFamily: 'Cochin', * }, - * ... - * var styles = StyleSheet.create({ - * baseText: { - * fontFamily: 'Cochin', - * }, - * titleText: { - * fontSize: 20, - * fontWeight: 'bold', - * }, - * }; - * - * More example code in `TextExample.ios.js` + * titleText: { + * fontSize: 20, + * fontWeight: 'bold', + * }, + * }; + * ``` */ var Text = React.createClass({ mixins: [Touchable.Mixin, NativeMethodsMixin], - statics: { - stylePropType: stylePropType, - }, - propTypes: { /** * Used to truncate the text with an elipsis after computing the text @@ -87,6 +83,10 @@ var Text = React.createClass({ */ suppressHighlighting: React.PropTypes.bool, style: stylePropType, + /** + * Used to locate this view in end-to-end tests. + */ + testID: React.PropTypes.string, }, viewConfig: viewConfig, @@ -184,12 +184,12 @@ var Text = React.createClass({ props.onResponderMove = this.handleResponderMove; props.onResponderRelease = this.handleResponderRelease; props.onResponderTerminate = this.handleResponderTerminate; - return ; + return ; }, }); var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30}; -var RKText = createReactIOSNativeComponentClass(viewConfig); +var RCTText = createReactIOSNativeComponentClass(viewConfig); module.exports = Text; diff --git a/Libraries/Text/TextStylePropTypes.js b/Libraries/Text/TextStylePropTypes.js new file mode 100644 index 000000000..acc91f699 --- /dev/null +++ b/Libraries/Text/TextStylePropTypes.js @@ -0,0 +1,43 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule TextStylePropTypes + */ +'use strict'; + +var ReactPropTypes = require('ReactPropTypes'); +var ViewStylePropTypes = require('ViewStylePropTypes'); + +var TextStylePropTypes = { + ...ViewStylePropTypes, + fontFamily: ReactPropTypes.string, + fontSize: ReactPropTypes.number, + fontWeight: ReactPropTypes.oneOf(['normal' /*default*/, 'bold']), + fontStyle: ReactPropTypes.oneOf(['normal', 'italic']), + lineHeight: ReactPropTypes.number, + color: ReactPropTypes.string, + containerBackgroundColor: ReactPropTypes.string, + textAlign: ReactPropTypes.oneOf( + ['auto' /*default*/, 'left', 'right', 'center'] + ), + writingDirection: ReactPropTypes.oneOf( + ['auto' /*default*/, 'ltr', 'rtl'] + ), +}; + +// Text doesn't support padding correctly (#4841912) +var unsupportedProps = Object.keys({ + padding: null, + paddingTop: null, + paddingLeft: null, + paddingRight: null, + paddingBottom: null, + paddingVertical: null, + paddingHorizontal: null, +}); + +for (var key in unsupportedProps) { + delete TextStylePropTypes[key]; +} + +module.exports = TextStylePropTypes; diff --git a/Libraries/Utilities/AlertIOS.js b/Libraries/Utilities/AlertIOS.js new file mode 100644 index 000000000..3b2530f18 --- /dev/null +++ b/Libraries/Utilities/AlertIOS.js @@ -0,0 +1,75 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule AlertIOS + * @flow + */ +'use strict'; + +var RCTAlertManager = require('NativeModules').AlertManager; + +var DEFAULT_BUTTON_TEXT = 'OK'; +var DEFAULT_BUTTON = { + text: DEFAULT_BUTTON_TEXT, + onPress: null, +}; + +/** + * AlertIOS manages native iOS alerts, option sheets, and share dialogs + */ + +class AlertIOS { + + /** + * Launches an alert dialog with the specified title and message. + * + * Optionally provide a list of buttons. Tapping any button will fire the + * respective onPress callback and dismiss the alert. By default, the only + * button will be an 'OK' button + * + * The last button in the list will be considered the 'Primary' button and + * it will appear bold. + * + * ``` + * AlertIOS.alert( + * 'Foo Title', + * 'My Alert Msg', + * [ + * {text: 'Foo', onPress: () => console.log('Foo Pressed!')}, + * {text: 'Bar', onPress: () => console.log('Bar Pressed!')}, + * ] + * )} + * ``` + */ + static alert( + title: ?string, + message: ?string, + buttons?: Array<{ + text: ?string; + onPress: ?Function; + }> + ): void { + var callbacks = []; + var buttonsSpec = []; + title = title || ''; + message = message || ''; + buttons = buttons || [DEFAULT_BUTTON]; + buttons.forEach((btn, index) => { + callbacks[index] = btn.onPress; + var btnDef = {}; + btnDef[index] = btn.text || DEFAULT_BUTTON_TEXT; + buttonsSpec.push(btnDef); + }); + RCTAlertManager.alertWithArgs({ + title, + message, + buttons: buttonsSpec, + }, (id) => { + var cb = callbacks[id]; + cb && cb(); + }); + } + +} + +module.exports = AlertIOS; diff --git a/Libraries/Utilities/Dimensions.js b/Libraries/Utilities/Dimensions.js index 81621fd61..5cbf8cb87 100644 --- a/Libraries/Utilities/Dimensions.js +++ b/Libraries/Utilities/Dimensions.js @@ -8,9 +8,8 @@ var NativeModules = require('NativeModules'); var invariant = require('invariant'); -var mergeInto = require('mergeInto'); -var dimensions = NativeModules.RKUIManager.Dimensions; +var dimensions = NativeModules.UIManager.Dimensions; class Dimensions { /** @@ -19,7 +18,7 @@ class Dimensions { * @param {object} dims Simple string-keyed object of dimensions to set */ static set(dims) { - mergeInto(dimensions, dims); + Object.assign(dimensions, dims); return true; } diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index c5cba0525..1a487d56f 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -415,28 +415,6 @@ var MessageQueueMixin = { return ret; }, - callDeprecated: function(moduleName, methodName, params, cb, scope) { - invariant( - !cb || typeof cb === 'function', - 'Last argument (callback) must be function' - ); - // Store callback _before_ sending the request, just in case the MailBox - // returns the response in a blocking manner - if (cb) { - this._storeCallbacksInCurrentThread(null, cb, scope, this._POOLED_CBIDS); - params.push(this._POOLED_CBIDS.successCallbackID); - } - var moduleID = this._remoteModuleNameToModuleID[moduleName]; - if (moduleID === undefined || moduleID === null) { - throw new Error('Unrecognized module name:' + moduleName); - } - var methodID = this._remoteModuleNameToMethodNameToID[moduleName][methodName]; - if (methodID === undefined || moduleID === null) { - throw new Error('Unrecognized method name:' + methodName); - } - this._pushRequestToOutgoingItems(moduleID, methodID, params); - }, - call: function(moduleName, methodName, params, onFail, onSucc, scope) { invariant( (!onFail || typeof onFail === 'function') && @@ -445,9 +423,9 @@ var MessageQueueMixin = { ); // Store callback _before_ sending the request, just in case the MailBox // returns the response in a blocking manner. - if (onFail || onSucc) { + if (onSucc) { this._storeCallbacksInCurrentThread(onFail, onSucc, scope, this._POOLED_CBIDS); - params.push(this._POOLED_CBIDS.errorCallbackID); + onFail && params.push(this._POOLED_CBIDS.errorCallbackID); params.push(this._POOLED_CBIDS.successCallbackID); } var moduleID = this._remoteModuleNameToModuleID[moduleName]; diff --git a/Libraries/Utilities/PixelRatio.js b/Libraries/Utilities/PixelRatio.js index 0eeb074c6..f9b1ac13a 100644 --- a/Libraries/Utilities/PixelRatio.js +++ b/Libraries/Utilities/PixelRatio.js @@ -10,45 +10,49 @@ var Dimensions = require('Dimensions'); /** * PixelRatio class gives access to the device pixel density. * - * Some examples: - * - PixelRatio.get() === 2 - * - iPhone 4, 4S - * - iPhone 5, 5c, 5s - * - iPhone 6 - * - * - PixelRatio.get() === 3 - * - iPhone 6 plus - * * There are a few use cases for using PixelRatio: * - * == Displaying a line that's as thin as the device permits + * ### Displaying a line that's as thin as the device permits * * A width of 1 is actually pretty thick on an iPhone 4+, we can do one that's - * thinner using a width of 1 / PixelRatio.get(). It's a technique that works + * thinner using a width of `1 / PixelRatio.get()`. It's a technique that works * on all the devices independent of their pixel density. * - * style={{ borderWidth: 1 / PixelRatio.get() }} + * ``` + * style={{ borderWidth: 1 / PixelRatio.get() }} + * ``` * - * == Fetching a correctly sized image + * ### Fetching a correctly sized image * * You should get a higher resolution image if you are on a high pixel density * device. A good rule of thumb is to multiply the size of the image you display * by the pixel ratio. * - * var image = getImage({ - * width: 200 * PixelRatio.get(), - * height: 100 * PixelRatio.get() - * }); - * + * ``` + * var image = getImage({ + * width: 200 * PixelRatio.get(), + * height: 100 * PixelRatio.get() + * }); + * + * ``` */ class PixelRatio { + /** + * Returns the device pixel density. Some examples: + * + * - PixelRatio.get() === 2 + * - iPhone 4, 4S + * - iPhone 5, 5c, 5s + * - iPhone 6 + * - PixelRatio.get() === 3 + * - iPhone 6 plus + */ static get() { return Dimensions.get('window').scale; } +} - static startDetecting() { - // no-op for iOS, but this is useful for other platforms - } -}; +// No-op for iOS, but used on the web. Should not be documented. +PixelRatio.startDetecting = function() {}; module.exports = PixelRatio; diff --git a/Libraries/Utilities/PushNotificationIOS.js b/Libraries/Utilities/PushNotificationIOS.js new file mode 100644 index 000000000..cb40e6dec --- /dev/null +++ b/Libraries/Utilities/PushNotificationIOS.js @@ -0,0 +1,88 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule PushNotificationIOS + */ +'use strict'; + +var NativeModules = require('NativeModules'); +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); + +var RCTPushNotificationManager = NativeModules.PushNotificationManager; +if (RCTPushNotificationManager) { + var _initialNotification = RCTPushNotificationManager.initialNotification; +} + +var _notifHandlers = {}; + +var DEVICE_NOTIF_EVENT = 'remoteNotificationReceived'; + +class PushNotificationIOS { + + static addEventListener(type, handler) { + _notifHandlers[handler] = RCTDeviceEventEmitter.addListener( + DEVICE_NOTIF_EVENT, + (notifData) => { + handler(new PushNotificationIOS(notifData)); + } + ); + } + + static removeEventListener(type, handler) { + if (!_notifHandlers[handler]) { + return; + } + _notifHandlers[handler].remove(); + _notifHandlers[handler] = null; + } + + + static popInitialNotification() { + var initialNotification = _initialNotification && + new PushNotificationIOS(_initialNotification); + _initialNotification = null; + return initialNotification; + } + + constructor(nativeNotif) { + this._data = {}; + + // Extract data from Apple's `aps` dict as defined: + + // https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html + + Object.keys(nativeNotif).forEach((notifKey) => { + var notifVal = nativeNotif[notifKey]; + if (notifKey === 'aps') { + this._alert = notifVal.alert; + this._sound = notifVal.sound; + this._badgeCount = notifVal.badge; + } else { + this._data[notifKey] = notifVal; + } + }); + } + + getMessage() { + // alias because "alert" is an ambiguous name + return this._alert; + } + + getSound() { + return this._sound; + } + + getAlert() { + return this._alert; + } + + getBadgeCount() { + return this._badgeCount; + } + + getData() { + return this._data; + } +} + +module.exports = PushNotificationIOS; diff --git a/Libraries/Utilities/RCTRenderingPerf.js b/Libraries/Utilities/RCTRenderingPerf.js index cdc44aaa6..9df484bff 100644 --- a/Libraries/Utilities/RCTRenderingPerf.js +++ b/Libraries/Utilities/RCTRenderingPerf.js @@ -37,6 +37,19 @@ var RCTRenderingPerf = { ReactDefaultPerf.stop(); ReactDefaultPerf.printInclusive(); ReactDefaultPerf.printWasted(); + + var totalRender = 0; + var totalTime = 0; + var measurements = ReactDefaultPerf.getLastMeasurements(); + for (var ii = 0; ii < measurements.length; ii++) { + var render = measurements[ii].render; + for (var nodeName in render) { + totalRender += render[nodeName]; + } + totalTime += measurements[ii].totalTime; + } + console.log('Total time spent in render(): ' + totalRender + 'ms'); + perfModules.forEach((module) => module.stop()); }, diff --git a/Libraries/Utilities/differ/deepDiffer.js b/Libraries/Utilities/differ/deepDiffer.js index 97c3f516f..e118097ab 100644 --- a/Libraries/Utilities/differ/deepDiffer.js +++ b/Libraries/Utilities/differ/deepDiffer.js @@ -38,7 +38,7 @@ var deepDiffer = function(one: any, two: any): bool { for (var twoKey in two) { // The only case we haven't checked yet is keys that are in two but aren't // in one, which means they are different. - if (one[twoKey] === undefined) { + if (one[twoKey] === undefined && two[twoKey] !== undefined) { return true; } } diff --git a/Libraries/Utilities/groupByEveryN.js b/Libraries/Utilities/groupByEveryN.js new file mode 100644 index 000000000..e85e58ed0 --- /dev/null +++ b/Libraries/Utilities/groupByEveryN.js @@ -0,0 +1,46 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule groupByEveryN + */ + +/** + * Useful method to split an array into groups of the same number of elements. + * You can use it to generate grids, rows, pages... + * + * If the input length is not a multiple of the count, it'll fill the last + * array with null so you can display a placeholder. + * + * Example: + * groupByEveryN([1, 2, 3, 4, 5], 3) + * => [[1, 2, 3], [4, 5, null]] + * + * groupByEveryN([1, 2, 3], 2).map(elems => { + * return {elems.map(elem => {elem})}; + * }) + */ +'use strict'; + +function groupByEveryN(array, n) { + var result = []; + var temp = []; + + for (var i = 0; i < array.length; ++i) { + if (i > 0 && i % n === 0) { + result.push(temp); + temp = []; + } + temp.push(array[i]); + } + + if (temp.length > 0) { + while (temp.length !== n) { + temp.push(null); + } + result.push(temp); + } + + return result; +} + +module.exports = groupByEveryN; diff --git a/Libraries/Utilities/nativeModulePrefixNormalizer.js b/Libraries/Utilities/nativeModulePrefixNormalizer.js new file mode 100644 index 000000000..1c4807553 --- /dev/null +++ b/Libraries/Utilities/nativeModulePrefixNormalizer.js @@ -0,0 +1,24 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule nativeModulePrefixNormalizer + */ +'use strict'; + +// Dirty hack to support old (RK) and new (RCT) native module name conventions +function nativeModulePrefixNormalizer(modules) { + Object.keys(modules).forEach((moduleName) => { + var strippedName = moduleName.replace(/^(RCT|RK)/, ''); + if (modules['RCT' + strippedName] && modules['RK' + strippedName]) { + throw new Error( + 'Module cannot be registered as both RCT and RK: ' + moduleName + ); + } + if (strippedName !== moduleName) { + modules[strippedName] = modules[moduleName]; + delete modules[moduleName]; + } + }); +} + +module.exports = nativeModulePrefixNormalizer; diff --git a/Libraries/Utilities/validAttributesFromPropTypes.js b/Libraries/Utilities/validAttributesFromPropTypes.js deleted file mode 100644 index d08d17cfa..000000000 --- a/Libraries/Utilities/validAttributesFromPropTypes.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @providesModule validAttributesFromPropTypes - */ -'use strict' - -function validAttributesFromPropTypes(propTypes) { - var validAttributes = {}; - for (var key in propTypes) { - var propType = propTypes[key]; - if (propType && propType.isNative) { - var diff = propType.differ; - validAttributes[key] = diff ? {diff} : true; - } - } - return validAttributes; -} - -module.exports = validAttributesFromPropTypes; diff --git a/Libraries/Vibration/RCTVibration.h b/Libraries/Vibration/RCTVibration.h new file mode 100644 index 000000000..fe499acee --- /dev/null +++ b/Libraries/Vibration/RCTVibration.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTBridgeModule.h" + +@interface RCTVibration : NSObject + +@end diff --git a/Libraries/Vibration/RCTVibration.m b/Libraries/Vibration/RCTVibration.m new file mode 100644 index 000000000..e0e8e63be --- /dev/null +++ b/Libraries/Vibration/RCTVibration.m @@ -0,0 +1,15 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTVibration.h" + +#import + +@implementation RCTVibration + +- (void)vibrate +{ + RCT_EXPORT(); + AudioServicesPlaySystemSound(kSystemSoundID_Vibrate); +} + +@end diff --git a/Libraries/Vibration/RCTVibration.xcodeproj/project.pbxproj b/Libraries/Vibration/RCTVibration.xcodeproj/project.pbxproj new file mode 100644 index 000000000..8de930901 --- /dev/null +++ b/Libraries/Vibration/RCTVibration.xcodeproj/project.pbxproj @@ -0,0 +1,248 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 832C819C1AAF6E1A007FA2F7 /* RCTVibration.m in Sources */ = {isa = PBXBuildFile; fileRef = 832C819B1AAF6E1A007FA2F7 /* RCTVibration.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 832C817E1AAF6DEF007FA2F7 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 832C81801AAF6DEF007FA2F7 /* libRCTVibration.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVibration.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 832C819A1AAF6E1A007FA2F7 /* RCTVibration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTVibration.h; sourceTree = ""; }; + 832C819B1AAF6E1A007FA2F7 /* RCTVibration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTVibration.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 832C817D1AAF6DEF007FA2F7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 832C81771AAF6DEF007FA2F7 = { + isa = PBXGroup; + children = ( + 832C819A1AAF6E1A007FA2F7 /* RCTVibration.h */, + 832C819B1AAF6E1A007FA2F7 /* RCTVibration.m */, + 832C81811AAF6DEF007FA2F7 /* Products */, + ); + sourceTree = ""; + }; + 832C81811AAF6DEF007FA2F7 /* Products */ = { + isa = PBXGroup; + children = ( + 832C81801AAF6DEF007FA2F7 /* libRCTVibration.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 832C817F1AAF6DEF007FA2F7 /* RCTVibration */ = { + isa = PBXNativeTarget; + buildConfigurationList = 832C81941AAF6DF0007FA2F7 /* Build configuration list for PBXNativeTarget "RCTVibration" */; + buildPhases = ( + 832C817C1AAF6DEF007FA2F7 /* Sources */, + 832C817D1AAF6DEF007FA2F7 /* Frameworks */, + 832C817E1AAF6DEF007FA2F7 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RCTVibration; + productName = RCTVibration; + productReference = 832C81801AAF6DEF007FA2F7 /* libRCTVibration.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 832C81781AAF6DEF007FA2F7 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0620; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 832C817F1AAF6DEF007FA2F7 = { + CreatedOnToolsVersion = 6.2; + }; + }; + }; + buildConfigurationList = 832C817B1AAF6DEF007FA2F7 /* Build configuration list for PBXProject "RCTVibration" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 832C81771AAF6DEF007FA2F7; + productRefGroup = 832C81811AAF6DEF007FA2F7 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 832C817F1AAF6DEF007FA2F7 /* RCTVibration */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 832C817C1AAF6DEF007FA2F7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 832C819C1AAF6E1A007FA2F7 /* RCTVibration.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 832C81921AAF6DF0007FA2F7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 832C81931AAF6DF0007FA2F7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../../ReactKit/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 832C81951AAF6DF0007FA2F7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 832C81961AAF6DF0007FA2F7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 832C817B1AAF6DEF007FA2F7 /* Build configuration list for PBXProject "RCTVibration" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 832C81921AAF6DF0007FA2F7 /* Debug */, + 832C81931AAF6DF0007FA2F7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 832C81941AAF6DF0007FA2F7 /* Build configuration list for PBXNativeTarget "RCTVibration" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 832C81951AAF6DF0007FA2F7 /* Debug */, + 832C81961AAF6DF0007FA2F7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 832C81781AAF6DEF007FA2F7 /* Project object */; +} diff --git a/Libraries/Vibration/VibrationIOS.android.js b/Libraries/Vibration/VibrationIOS.android.js new file mode 100644 index 000000000..0828d35c1 --- /dev/null +++ b/Libraries/Vibration/VibrationIOS.android.js @@ -0,0 +1,18 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * Stub of VibrationIOS for Android. + * + * @providesModule VibrationIOS + */ +'use strict'; + +var warning = require('warning'); + +var VibrationIOS = { + vibrate: function() { + warning('VibrationIOS is not supported on this platform!'); + } +}; + +module.exports = VibrationIOS; diff --git a/Libraries/Vibration/VibrationIOS.ios.js b/Libraries/Vibration/VibrationIOS.ios.js new file mode 100644 index 000000000..11b09f989 --- /dev/null +++ b/Libraries/Vibration/VibrationIOS.ios.js @@ -0,0 +1,33 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule VibrationIOS + */ +'use strict'; + +var RCTVibration = require('NativeModules').Vibration; + +var invariant = require('invariant'); + +/** + * The Vibration API is exposed at `VibrationIOS.vibrate()`. On iOS, calling this + * function will trigger a one second vibration. The vibration is asynchronous + * so this method will return immediately. + * + * There will be no effect on devices that do not support Vibration, eg. the iOS + * simulator. + * + * Vibration patterns are currently unsupported. + */ + +var VibrationIOS = { + vibrate: function() { + invariant( + arguments[0] === undefined, + 'Vibration patterns not supported.' + ); + RCTVibration.vibrate(); + } +}; + +module.exports = VibrationIOS; diff --git a/Libraries/react-native/addons.js b/Libraries/react-native/addons.js deleted file mode 100644 index fcef285e2..000000000 --- a/Libraries/react-native/addons.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright 2004-present Facebook. All Rights Reserved. - * - * @flow - */ -'use strict'; - -var LinkedStateMixin = require('LinkedStateMixin'); -var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); -var ReactNative = require('react-native'); -var ReactUpdates = require('ReactUpdates'); - -var cloneWithProps = require('cloneWithProps'); -var update = require('update'); - -var addons = { - LinkedStateMixin: LinkedStateMixin, - PureRenderMixin: ReactComponentWithPureRenderMixin, - batchedUpdates: ReactUpdates.batchedUpdates, - cloneWithProps: cloneWithProps, - update: update, -}; - -if (__DEV__) { - addons.Perf = require('ReactDefaultPerf'); - addons.TestUtils = require('ReactTestUtils'); -} - -var ReactNativeWithAddons = { - ...ReactNative, - addons: addons, -}; - -module.exports = ReactNativeWithAddons; diff --git a/Libraries/react-native/package.json b/Libraries/react-native/package.json deleted file mode 100644 index e8306d485..000000000 --- a/Libraries/react-native/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "react-native", - "main": "react-native.js", - "directories": {".": ""} -} diff --git a/Libraries/react-native/react-native-interface.js b/Libraries/react-native/react-native-interface.js index c37bbe685..e62d5f576 100644 --- a/Libraries/react-native/react-native-interface.js +++ b/Libraries/react-native/react-native-interface.js @@ -1,36 +1,9 @@ -declare module "react-native" { - declare class ListViewDataSource { - constructor(params: Object): void; - } +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @flow + */ - declare var AppRegistry: ReactClass; - declare var ExpandingText: ReactClass; - declare var Image: ReactClass; - declare var ListView: ReactClass; - declare var NavigatorIOS: ReactClass; - declare var NavigatorItemIOS: ReactClass; - declare var PixelRatio: ReactClass; - declare var ScrollView: ReactClass; - declare var ActivityIndicatorIOS: ReactClass; - declare var StyleSheet: ReactClass; - declare var Text: ReactClass; - declare var TextInput: ReactClass; - declare var TimerMixin: ReactClass; - declare var TouchableHighlight: ReactClass; - declare var TouchableWithoutFeedback: ReactClass; - declare var View: ReactClass; - declare var invariant: Function; - declare var ix: Function; -} - -declare module "addons" { - declare var NavigatorIOS: ReactClass; - declare var NavigatorItemIOS: ReactClass; - declare var StyleSheet: ReactClass; -} +// see also react-native.js declare var __DEV__: boolean; - -declare module "fetch" { - declare function exports(url: string, options?: Object): Object; -} diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 78c93960b..4642c5b9b 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -5,29 +5,65 @@ */ 'use strict'; -var ReactNative = { - ...require('React'), - AppRegistry: require('AppRegistry'), - ExpandingText: require('ExpandingText'), - Image: require('Image'), - LayoutAnimation: require('LayoutAnimation'), - ListView: require('ListView'), - ListViewDataSource: require('ListViewDataSource'), - NavigatorIOS: require('NavigatorIOS'), - PixelRatio: require('PixelRatio'), - ScrollView: require('ScrollView'), +// Export React, plus some native additions. +// +// The use of Object.create/assign is to work around a Flow bug (#6560135). +// Once that is fixed, change this back to +// +// var ReactNative = {...require('React'), /* additions */} +// +var ReactNative = Object.assign(Object.create(require('React')), { + // Components ActivityIndicatorIOS: require('ActivityIndicatorIOS'), - StatusBarIOS: require('StatusBarIOS'), - StyleSheet: require('StyleSheet'), + DatePickerIOS: require('DatePickerIOS'), + Image: require('Image'), + ListView: require('ListView'), + MapView: require('MapView'), + NavigatorIOS: require('NavigatorIOS'), + PickerIOS: require('PickerIOS'), + ScrollView: require('ScrollView'), + SliderIOS: require('SliderIOS'), + SwitchIOS: require('SwitchIOS'), + TabBarIOS: require('TabBarIOS'), Text: require('Text'), TextInput: require('TextInput'), - TimerMixin: require('TimerMixin'), TouchableHighlight: require('TouchableHighlight'), TouchableOpacity: require('TouchableOpacity'), TouchableWithoutFeedback: require('TouchableWithoutFeedback'), View: require('View'), - invariant: require('invariant'), - ix: require('ix'), -}; + WebView: require('WebView'), + + // APIs + AlertIOS: require('AlertIOS'), + Animation: require('Animation'), + AppRegistry: require('AppRegistry'), + AppState: require('AppState'), + AppStateIOS: require('AppStateIOS'), + AsyncStorage: require('AsyncStorage'), + CameraRoll: require('CameraRoll'), + InteractionManager: require('InteractionManager'), + LayoutAnimation: require('LayoutAnimation'), + NetInfo: require('NetInfo'), + PixelRatio: require('PixelRatio'), + StatusBarIOS: require('StatusBarIOS'), + StyleSheet: require('StyleSheet'), + TimerMixin: require('TimerMixin'), + VibrationIOS: require('VibrationIOS'), + + addons: { + batchedUpdates: require('ReactUpdates').batchedUpdates, + LinkedStateMixin: require('LinkedStateMixin'), + Perf: undefined, + PureRenderMixin: require('ReactComponentWithPureRenderMixin'), + TestUtils: undefined, + cloneWithProps: require('cloneWithProps'), + update: require('update'), + }, +}); + +if (__DEV__) { + ReactNative.addons.Perf = require('ReactDefaultPerf'); + ReactNative.addons.TestUtils = require('ReactTestUtils'); +} module.exports = ReactNative; diff --git a/Libraries/vendor/core/clearImmediate.js b/Libraries/vendor/core/clearImmediate.js new file mode 100644 index 000000000..060147458 --- /dev/null +++ b/Libraries/vendor/core/clearImmediate.js @@ -0,0 +1,19 @@ +/** + * @generated SignedSource<<4595f3986407fd02332cf9f5fc12e70f>> + * + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * !! This file is a check-in of a static_upstream project! !! + * !! !! + * !! You should not modify this file directly. Instead: !! + * !! 1) Use `fjs use-upstream` to temporarily replace this with !! + * !! the latest version from upstream. !! + * !! 2) Make your changes, test them, etc. !! + * !! 3) Use `fjs push-upstream` to copy your changes back to !! + * !! static_upstream. !! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * + * @providesModule clearImmediate + */ + +module.exports = global.clearImmediate || + require('ImmediateImplementation').clearImmediate; diff --git a/Libraries/vendor/core/copyProperties.js b/Libraries/vendor/core/copyProperties.js new file mode 100644 index 000000000..e5a2638d9 --- /dev/null +++ b/Libraries/vendor/core/copyProperties.js @@ -0,0 +1,53 @@ +/** + * @generated SignedSource<> + * + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * !! This file is a check-in of a static_upstream project! !! + * !! !! + * !! You should not modify this file directly. Instead: !! + * !! 1) Use `fjs use-upstream` to temporarily replace this with !! + * !! the latest version from upstream. !! + * !! 2) Make your changes, test them, etc. !! + * !! 3) Use `fjs push-upstream` to copy your changes back to !! + * !! static_upstream. !! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * + * @providesModule copyProperties + */ + +/** + * Copy properties from one or more objects (up to 5) into the first object. + * This is a shallow copy. It mutates the first object and also returns it. + * + * NOTE: `arguments` has a very significant performance penalty, which is why + * we don't support unlimited arguments. + */ +function copyProperties(obj, a, b, c, d, e, f) { + obj = obj || {}; + + if (__DEV__) { + if (f) { + throw new Error('Too many arguments passed to copyProperties'); + } + } + + var args = [a, b, c, d, e]; + var ii = 0, v; + while (args[ii]) { + v = args[ii++]; + for (var k in v) { + obj[k] = v[k]; + } + + // IE ignores toString in object iteration.. See: + // webreflection.blogspot.com/2007/07/quick-fix-internet-explorer-and.html + if (v.hasOwnProperty && v.hasOwnProperty('toString') && + (typeof v.toString != 'undefined') && (obj.toString !== v.toString)) { + obj.toString = v.toString; + } + } + + return obj; +} + +module.exports = copyProperties; diff --git a/Libraries/vendor/core/merge.js b/Libraries/vendor/core/merge.js new file mode 100644 index 000000000..800c08c1b --- /dev/null +++ b/Libraries/vendor/core/merge.js @@ -0,0 +1,50 @@ +/** + * @generated SignedSource<<0e3063b19e14ed191102b1dffe45551f>> + * + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * !! This file is a check-in of a static_upstream project! !! + * !! !! + * !! You should not modify this file directly. Instead: !! + * !! 1) Use `fjs use-upstream` to temporarily replace this with !! + * !! the latest version from upstream. !! + * !! 2) Make your changes, test them, etc. !! + * !! 3) Use `fjs push-upstream` to copy your changes back to !! + * !! static_upstream. !! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * + * Copyright 2013-2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule merge + */ + +"use strict"; + +var mergeInto = require('mergeInto'); + +/** + * Shallow merges two structures into a return value, without mutating either. + * + * @param {?object} one Optional object with properties to merge from. + * @param {?object} two Optional object with properties to merge from. + * @return {object} The shallow extension of one by two. + */ +var merge = function(one, two) { + var result = {}; + mergeInto(result, one); + mergeInto(result, two); + return result; +}; + +module.exports = merge; diff --git a/Libraries/vendor/core/mergeHelpers.js b/Libraries/vendor/core/mergeHelpers.js new file mode 100644 index 000000000..58fa238d4 --- /dev/null +++ b/Libraries/vendor/core/mergeHelpers.js @@ -0,0 +1,160 @@ +/** + * @generated SignedSource<> + * + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * !! This file is a check-in of a static_upstream project! !! + * !! !! + * !! You should not modify this file directly. Instead: !! + * !! 1) Use `fjs use-upstream` to temporarily replace this with !! + * !! the latest version from upstream. !! + * !! 2) Make your changes, test them, etc. !! + * !! 3) Use `fjs push-upstream` to copy your changes back to !! + * !! static_upstream. !! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * + * Copyright 2013-2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule mergeHelpers + * + * requiresPolyfills: Array.isArray + */ + +"use strict"; + +var invariant = require('invariant'); +var keyMirror = require('keyMirror'); + +/** + * Maximum number of levels to traverse. Will catch circular structures. + * @const + */ +var MAX_MERGE_DEPTH = 36; + +/** + * We won't worry about edge cases like new String('x') or new Boolean(true). + * Functions are considered terminals, and arrays are not. + * @param {*} o The item/object/value to test. + * @return {boolean} true iff the argument is a terminal. + */ +var isTerminal = function(o) { + return typeof o !== 'object' || o === null; +}; + +var mergeHelpers = { + + MAX_MERGE_DEPTH: MAX_MERGE_DEPTH, + + isTerminal: isTerminal, + + /** + * Converts null/undefined values into empty object. + * + * @param {?Object=} arg Argument to be normalized (nullable optional) + * @return {!Object} + */ + normalizeMergeArg: function(arg) { + return arg === undefined || arg === null ? {} : arg; + }, + + /** + * If merging Arrays, a merge strategy *must* be supplied. If not, it is + * likely the caller's fault. If this function is ever called with anything + * but `one` and `two` being `Array`s, it is the fault of the merge utilities. + * + * @param {*} one Array to merge into. + * @param {*} two Array to merge from. + */ + checkMergeArrayArgs: function(one, two) { + invariant( + Array.isArray(one) && Array.isArray(two), + 'Tried to merge arrays, instead got %s and %s.', + one, + two + ); + }, + + /** + * @param {*} one Object to merge into. + * @param {*} two Object to merge from. + */ + checkMergeObjectArgs: function(one, two) { + mergeHelpers.checkMergeObjectArg(one); + mergeHelpers.checkMergeObjectArg(two); + }, + + /** + * @param {*} arg + */ + checkMergeObjectArg: function(arg) { + invariant( + !isTerminal(arg) && !Array.isArray(arg), + 'Tried to merge an object, instead got %s.', + arg + ); + }, + + /** + * @param {*} arg + */ + checkMergeIntoObjectArg: function(arg) { + invariant( + (!isTerminal(arg) || typeof arg === 'function') && !Array.isArray(arg), + 'Tried to merge into an object, instead got %s.', + arg + ); + }, + + /** + * Checks that a merge was not given a circular object or an object that had + * too great of depth. + * + * @param {number} Level of recursion to validate against maximum. + */ + checkMergeLevel: function(level) { + invariant( + level < MAX_MERGE_DEPTH, + 'Maximum deep merge depth exceeded. You may be attempting to merge ' + + 'circular structures in an unsupported way.' + ); + }, + + /** + * Checks that the supplied merge strategy is valid. + * + * @param {string} Array merge strategy. + */ + checkArrayStrategy: function(strategy) { + invariant( + strategy === undefined || strategy in mergeHelpers.ArrayStrategies, + 'You must provide an array strategy to deep merge functions to ' + + 'instruct the deep merge how to resolve merging two arrays.' + ); + }, + + /** + * Set of possible behaviors of merge algorithms when encountering two Arrays + * that must be merged together. + * - `clobber`: The left `Array` is ignored. + * - `indexByIndex`: The result is achieved by recursively deep merging at + * each index. (not yet supported.) + */ + ArrayStrategies: keyMirror({ + Clobber: true, + IndexByIndex: true + }) + +}; + +module.exports = mergeHelpers; diff --git a/Libraries/vendor/core/mergeInto.js b/Libraries/vendor/core/mergeInto.js new file mode 100644 index 000000000..0da86a50c --- /dev/null +++ b/Libraries/vendor/core/mergeInto.js @@ -0,0 +1,59 @@ +/** + * @generated SignedSource<> + * + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * !! This file is a check-in of a static_upstream project! !! + * !! !! + * !! You should not modify this file directly. Instead: !! + * !! 1) Use `fjs use-upstream` to temporarily replace this with !! + * !! the latest version from upstream. !! + * !! 2) Make your changes, test them, etc. !! + * !! 3) Use `fjs push-upstream` to copy your changes back to !! + * !! static_upstream. !! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * + * Copyright 2013-2014 Facebook, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @providesModule mergeInto + * @typechecks static-only + */ + +"use strict"; + +var mergeHelpers = require('mergeHelpers'); + +var checkMergeObjectArg = mergeHelpers.checkMergeObjectArg; +var checkMergeIntoObjectArg = mergeHelpers.checkMergeIntoObjectArg; + +/** + * Shallow merges two structures by mutating the first parameter. + * + * @param {object|function} one Object to be merged into. + * @param {?object} two Optional object with properties to merge from. + */ +function mergeInto(one, two) { + checkMergeIntoObjectArg(one); + if (two != null) { + checkMergeObjectArg(two); + for (var key in two) { + if (!two.hasOwnProperty(key)) { + continue; + } + one[key] = two[key]; + } + } +} + +module.exports = mergeInto; diff --git a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js index 5df94697e..020731fde 100644 --- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js +++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js @@ -636,7 +636,7 @@ var TouchableMixin = { } if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) { - var hasLongPressHandler = !!this.touchableHandleLongPress; + var hasLongPressHandler = !!this.props.onLongPress; var pressIsLongButStillCallOnPress = IsLongPressingIn[curState] && ( // We *are* long pressing.. !hasLongPressHandler || // But either has no long handler diff --git a/README.md b/README.md index 07e34b563..d2ff09e20 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ native implementation. We are committed to improving and expanding the capabilities of this project as fast as we can, and look forward to working with the community. -# React Native +# React Native [![Build Status](https://magnum.travis-ci.com/facebook/react-native.svg?token=L5Egb3B4dyQzH5wDijCB&branch=master)](https://magnum.travis-ci.com/facebook/react-native) Our first React Native implementation is `ReactKit`, targeting iOS. We are also working on an Android implementation which we will release later. `ReactKit` diff --git a/ReactKit/Base/RCTAssert.h b/ReactKit/Base/RCTAssert.h index 66f670430..0aaf176f0 100644 --- a/ReactKit/Base/RCTAssert.h +++ b/ReactKit/Base/RCTAssert.h @@ -4,8 +4,8 @@ #define RCTErrorDomain @"RCTErrorDomain" -#define RCTAssert(condition, message, ...) _RCTAssert(condition, message, ##__VA_ARGS__) -#define RCTCAssert(condition, message, ...) _RCTCAssert(condition, message, ##__VA_ARGS__) +#define RCTAssert(condition, message, ...) _RCTAssert((condition) != 0, message, ##__VA_ARGS__) +#define RCTCAssert(condition, message, ...) _RCTCAssert((condition) != 0, message, ##__VA_ARGS__) typedef void (^RCTAssertFunction)(BOOL condition, NSString *message, ...); diff --git a/ReactKit/Base/RCTBridge.h b/ReactKit/Base/RCTBridge.h index 9a890a07a..4dcaee8e2 100644 --- a/ReactKit/Base/RCTBridge.h +++ b/ReactKit/Base/RCTBridge.h @@ -6,32 +6,16 @@ @class RCTBridge; @class RCTEventDispatcher; -@class RCTRootView; - -/** - * Utilities for constructing common response objects. When sending a - * systemError back to JS, it's important to describe whether or not it was a - * system error, or API usage error. System errors should never happen and are - * therefore logged using `RCTLogError()`. API usage errors are expected if the - * API is misused and will therefore not be logged using `RCTLogError()`. The JS - * application code is expected to handle them. Regardless of type, each error - * should be logged at most once. - */ -static inline NSDictionary *RCTSystemErrorObject(NSString *msg) -{ - return @{@"systemError": msg ?: @""}; -} - -static inline NSDictionary *RCTAPIErrorObject(NSString *msg) -{ - return @{@"apiError": msg ?: @""}; -} /** * This block can be used to instantiate modules that require additional * init parameters, or additional configuration prior to being used. + * The bridge will call this block to instatiate the modules, and will + * be responsible for invalidating/releasing them when the bridge is destroyed. + * For this reason, the block should always return new module instances, and + * module instances should not be shared between bridges. */ -typedef NSArray *(^RCTBridgeModuleProviderBlock)(RCTBridge *bridge); +typedef NSArray *(^RCTBridgeModuleProviderBlock)(void); /** * Async batched bridge used to communicate with the JavaScript application. @@ -42,12 +26,12 @@ typedef NSArray *(^RCTBridgeModuleProviderBlock)(RCTBridge *bridge); * The designated initializer. This creates a new bridge on top of the specified * executor. The bridge should then be used for all subsequent communication * with the JavaScript code running in the executor. Modules will be automatically - * instantiated using the default contructor, but you can optionally pass in a - * module provider block to manually instantiate modules that require additional - * init parameters or configuration. + * instantiated using the default contructor, but you can optionally pass in an + * array of pre-initialized module instances if they require additional init + * parameters or configuration. */ -- (instancetype)initWithJavaScriptExecutor:(id)javaScriptExecutor - moduleProvider:(RCTBridgeModuleProviderBlock)block NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithExecutor:(id)executor + moduleProvider:(RCTBridgeModuleProviderBlock)block NS_DESIGNATED_INITIALIZER; /** * This method is used to call functions in the JavaScript application context. @@ -81,16 +65,6 @@ typedef NSArray *(^RCTBridgeModuleProviderBlock)(RCTBridge *bridge); */ @property (nonatomic, readonly) dispatch_queue_t shadowQueue; -// For use in implementing delegates, which may need to queue responses. -- (RCTResponseSenderBlock)createResponseSenderBlock:(NSInteger)callbackID; - -/** - * Register a root view with the bridge. Theorectically, a single bridge can - * support multiple root views, however this feature is not currently exposed - * and may eventually be removed. - */ -- (void)registerRootView:(RCTRootView *)rootView; - /** * Global logging function that will print to both xcode and JS debugger consoles. * diff --git a/ReactKit/Base/RCTBridge.m b/ReactKit/Base/RCTBridge.m index 97a5dc12e..1fd446b3f 100644 --- a/ReactKit/Base/RCTBridge.m +++ b/ReactKit/Base/RCTBridge.m @@ -3,11 +3,12 @@ #import "RCTBridge.h" #import -#import -#import #import #import +#import +#import + #import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" @@ -26,60 +27,6 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldFlushDateMillis }; -/** - * This private class is used as a container for exported method info - */ -@interface RCTModuleMethod : NSObject - -@property (readonly, nonatomic, assign) SEL selector; -@property (readonly, nonatomic, copy) NSString *JSMethodName; -@property (readonly, nonatomic, assign) NSUInteger arity; -@property (readonly, nonatomic, copy) NSIndexSet *blockArgumentIndexes; - -@end - -@implementation RCTModuleMethod - -- (instancetype)initWithSelector:(SEL)selector - JSMethodName:(NSString *)JSMethodName - arity:(NSUInteger)arity - blockArgumentIndexes:(NSIndexSet *)blockArgumentIndexes -{ - if ((self = [super init])) { - _selector = selector; - _JSMethodName = [JSMethodName copy]; - _arity = arity; - _blockArgumentIndexes = [blockArgumentIndexes copy]; - } - return self; -} - -- (NSString *)description -{ - NSString *blocks = @"no block args"; - if (self.blockArgumentIndexes.count > 0) { - NSMutableString *indexString = [NSMutableString string]; - [self.blockArgumentIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - [indexString appendFormat:@", %tu", idx]; - }]; - blocks = [NSString stringWithFormat:@"block args at %@", [indexString substringFromIndex:2]]; - } - - return [NSString stringWithFormat:@"<%@: %p; exports -%@ as %@; %@>", NSStringFromClass(self.class), self, NSStringFromSelector(self.selector), self.JSMethodName, blocks]; -} - -@end - -#ifdef __LP64__ -typedef uint64_t RCTExportValue; -typedef struct section_64 RCTExportSection; -#define RCTGetSectByNameFromHeader getsectbynamefromheader_64 -#else -typedef uint32_t RCTExportValue; -typedef struct section RCTExportSection; -#define RCTGetSectByNameFromHeader getsectbynamefromheader -#endif - /** * This function returns the module name for a given class. */ @@ -88,18 +35,6 @@ static NSString *RCTModuleNameForClass(Class cls) return [cls respondsToSelector:@selector(moduleName)] ? [cls moduleName] : NSStringFromClass(cls); } -/** - * This function instantiates a new module instance. - */ -static id RCTCreateModuleInstance(Class cls, RCTBridge *bridge) -{ - if ([cls instancesRespondToSelector:@selector(initWithBridge:)]) { - return [[cls alloc] initWithBridge:bridge]; - } else { - return [[cls alloc] init]; - } -} - /** * This function scans all classes available at runtime and returns an array * of all JSMethods registered. @@ -111,22 +46,11 @@ static NSArray *RCTJSMethods(void) dispatch_once(&onceToken, ^{ NSMutableSet *uniqueMethods = [NSMutableSet set]; - unsigned int classCount; - Class *classes = objc_copyClassList(&classCount); - for (unsigned int i = 0; i < classCount; i++) { - - Class cls = classes[i]; - - if (!class_getSuperclass(cls)) { - // Class has no superclass - it's probably something weird - continue; - } - + RCTEnumerateClasses(^(__unsafe_unretained Class cls) { if (RCTClassOverridesClassMethod(cls, @selector(JSMethods))) { [uniqueMethods addObjectsFromArray:[cls JSMethods]]; } - } - free(classes); + }); JSMethods = [uniqueMethods allObjects]; }); @@ -147,38 +71,216 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) modules = [NSMutableArray array]; RCTModuleNamesByID = [NSMutableArray array]; - unsigned int classCount; - Class *classes = objc_copyClassList(&classCount); - for (unsigned int i = 0; i < classCount; i++) { + RCTEnumerateClasses(^(__unsafe_unretained Class cls) { + if ([cls conformsToProtocol:@protocol(RCTBridgeModule)]) { - Class cls = classes[i]; + // Add module + [(NSMutableArray *)modules addObject:cls]; - if (!class_getSuperclass(cls)) { - // Class has no superclass - it's probably something weird - continue; + // Add module name + NSString *moduleName = RCTModuleNameForClass(cls); + [(NSMutableArray *)RCTModuleNamesByID addObject:moduleName]; } - - if (![cls conformsToProtocol:@protocol(RCTBridgeModule)]) { - // Not an RCTBridgeModule - continue; - } - - // Add module - [(NSMutableArray *)modules addObject:cls]; - - // Add module name - NSString *moduleName = RCTModuleNameForClass(cls); - [(NSMutableArray *)RCTModuleNamesByID addObject:moduleName]; - } - free(classes); + }); modules = [modules copy]; RCTModuleNamesByID = [RCTModuleNamesByID copy]; }); - + return modules; } +@interface RCTBridge () + +- (void)_invokeAndProcessModule:(NSString *)module + method:(NSString *)method + arguments:(NSArray *)args; + +@end + +/** + * This private class is used as a container for exported method info + */ +@interface RCTModuleMethod : NSObject + +@property (nonatomic, copy, readonly) NSString *moduleClassName; +@property (nonatomic, copy, readonly) NSString *JSMethodName; + +@end + +@implementation RCTModuleMethod +{ + BOOL _isClassMethod; + Class _moduleClass; + SEL _selector; + NSMethodSignature *_methodSignature; + NSArray *_argumentBlocks; + NSString *_methodName; +} + +- (instancetype)initWithMethodName:(NSString *)methodName + JSMethodName:(NSString *)JSMethodName +{ + if ((self = [super init])) { + + _methodName = methodName; + NSArray *parts = [[methodName substringWithRange:NSMakeRange(2, methodName.length - 3)] componentsSeparatedByString:@" "]; + + // Parse class and method + _moduleClassName = parts[0]; + NSRange categoryRange = [_moduleClassName rangeOfString:@"("]; + if (categoryRange.length) + { + _moduleClassName = [_moduleClassName substringToIndex:categoryRange.location]; + } + + // Extract class and method details + _isClassMethod = [methodName characterAtIndex:0] == '+'; + _moduleClass = NSClassFromString(_moduleClassName); + _selector = NSSelectorFromString(parts[1]); + _JSMethodName = JSMethodName ?: [NSStringFromSelector(_selector) componentsSeparatedByString:@":"][0]; + +#if DEBUG + + // Sanity check + RCTAssert([_moduleClass conformsToProtocol:@protocol(RCTBridgeModule)], + @"You are attempting to export the method %@, but %@ does not \ + conform to the RCTBridgeModule Protocol", methodName, _moduleClassName); +#endif + + // Get method signature + _methodSignature = _isClassMethod ? + [_moduleClass methodSignatureForSelector:_selector] : + [_moduleClass instanceMethodSignatureForSelector:_selector]; + + // Process arguments + NSUInteger numberOfArguments = _methodSignature.numberOfArguments; + NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2]; + for (NSUInteger i = 2; i < numberOfArguments; i++) { + const char *argumentType = [_methodSignature getArgumentTypeAtIndex:i]; + switch (argumentType[0]) { + +#define RCT_ARG_BLOCK(_logic) \ + [argumentBlocks addObject:^(RCTBridge *bridge, NSInvocation *invocation, NSUInteger index, id json) { \ + _logic \ + [invocation setArgument:&value atIndex:index]; \ + }]; \ + +#define RCT_CASE(_value, _class, _logic) \ + case _value: { \ + RCT_ARG_BLOCK( \ + if (json && ![json isKindOfClass:[_class class]]) { \ + RCTLogError(@"Argument %tu (%@) of %@.%@ should be of type %@", index, \ + json, RCTModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \ + return; \ + } \ + _logic \ + ) \ + break; \ + } + + RCT_CASE(':', NSString, SEL value = NSSelectorFromString(json); ); + RCT_CASE('*', NSString, const char *value = [json UTF8String]; ); + +#define RCT_SIMPLE_CASE(_value, _type, _selector) \ + case _value: { \ + RCT_ARG_BLOCK( \ + if (json && ![json respondsToSelector:@selector(_selector)]) { \ + RCTLogError(@"Argument %tu (%@) of %@.%@ does not respond to selector: %@", \ + index, json, RCTModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \ + return; \ + } \ + _type value = [json _selector]; \ + ) \ + break; \ + } + + RCT_SIMPLE_CASE('c', char, charValue) + RCT_SIMPLE_CASE('C', unsigned char, unsignedCharValue) + RCT_SIMPLE_CASE('s', short, shortValue) + RCT_SIMPLE_CASE('S', unsigned short, unsignedShortValue) + RCT_SIMPLE_CASE('i', int, intValue) + RCT_SIMPLE_CASE('I', unsigned int, unsignedIntValue) + RCT_SIMPLE_CASE('l', long, longValue) + RCT_SIMPLE_CASE('L', unsigned long, unsignedLongValue) + RCT_SIMPLE_CASE('q', long long, longLongValue) + RCT_SIMPLE_CASE('Q', unsigned long long, unsignedLongLongValue) + RCT_SIMPLE_CASE('f', float, floatValue) + RCT_SIMPLE_CASE('d', double, doubleValue) + RCT_SIMPLE_CASE('B', BOOL, boolValue) + + default: { + static const char *blockType = @encode(typeof(^{})); + if (!strcmp(argumentType, blockType)) { + RCT_ARG_BLOCK( + if (json && ![json isKindOfClass:[NSNumber class]]) { + RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index, + json, RCTModuleNameForClass(_moduleClass), _JSMethodName); + return; + } + // Marked as autoreleasing, because NSInvocation doesn't retain arguments + __autoreleasing id value = (json ? ^(NSArray *args) { + [bridge _invokeAndProcessModule:@"BatchedBridge" + method:@"invokeCallbackAndReturnFlushedQueue" + arguments:@[json, args]]; + } : ^(NSArray *unused) {}); + ) + } else { + RCT_ARG_BLOCK( id value = json; ) + } + break; + } + } + } + _argumentBlocks = [argumentBlocks copy]; + } + return self; +} + +- (void)invokeWithBridge:(RCTBridge *)bridge + module:(id)module + arguments:(NSArray *)arguments +{ + +#if DEBUG + + // Sanity check + RCTAssert([module class] == _moduleClass, @"Attempted to invoke method \ + %@ on a module of class %@", _methodName, [module class]); +#endif + + // Safety check + if (arguments.count != _argumentBlocks.count) { + RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", + RCTModuleNameForClass(_moduleClass), _JSMethodName, + arguments.count, _argumentBlocks.count); + return; + } + + // Create invocation (we can't re-use this as it wouldn't be thread-safe) + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:_methodSignature]; + [invocation setArgument:&_selector atIndex:1]; + + // Set arguments + NSUInteger index = 0; + for (id json in arguments) { + id arg = (json == [NSNull null]) ? nil : json; + void (^block)(RCTBridge *, NSInvocation *, NSUInteger, id) = _argumentBlocks[index]; + block(bridge, invocation, index + 2, arg); + index ++; + } + + // Invoke method + [invocation invokeWithTarget:_isClassMethod ? [module class] : module]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", NSStringFromClass(self.class), self, _methodName, _JSMethodName]; +} + +@end + /** * This function parses the exported methods inside RCTBridgeModules and * generates an array of arrays of RCTModuleMethod objects, keyed @@ -193,6 +295,16 @@ static RCTSparseArray *RCTExportedMethodsByModuleID(void) Dl_info info; dladdr(&RCTExportedMethodsByModuleID, &info); +#ifdef __LP64__ + typedef uint64_t RCTExportValue; + typedef struct section_64 RCTExportSection; +#define RCTGetSectByNameFromHeader getsectbynamefromheader_64 +#else + typedef uint32_t RCTExportValue; + typedef struct section RCTExportSection; +#define RCTGetSectByNameFromHeader getsectbynamefromheader +#endif + const RCTExportValue mach_header = (RCTExportValue)info.dli_fbase; const RCTExportSection *section = RCTGetSectByNameFromHeader((void *)mach_header, "__DATA", "RCTExport"); @@ -202,53 +314,23 @@ static RCTSparseArray *RCTExportedMethodsByModuleID(void) NSArray *classes = RCTBridgeModuleClassesByModuleID(); NSMutableDictionary *methodsByModuleClassName = [NSMutableDictionary dictionaryWithCapacity:[classes count]]; - NSCharacterSet *plusMinusCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"+-"]; for (RCTExportValue addr = section->offset; addr < section->offset + section->size; - addr += sizeof(id) * 2) { + addr += sizeof(const char **) * 2) { - const char **entry = (const char **)(mach_header + addr); - NSScanner *scanner = [NSScanner scannerWithString:@(entry[0])]; + // Get data entry + const char **entries = (const char **)(mach_header + addr); - NSString *plusMinus; - if (![scanner scanCharactersFromSet:plusMinusCharacterSet intoString:&plusMinus]) continue; - if (![scanner scanString:@"[" intoString:NULL]) continue; - - NSString *className; - if (![scanner scanUpToString:@" " intoString:&className]) continue; - [scanner scanString:@" " intoString:NULL]; - - NSString *selectorName; - if (![scanner scanUpToString:@"]" intoString:&selectorName]) continue; - - Class moduleClass = NSClassFromString(className); - if (moduleClass == Nil) continue; - - SEL selector = NSSelectorFromString(selectorName); - Method method = ([plusMinus characterAtIndex:0] == '+' ? class_getClassMethod : class_getInstanceMethod)(moduleClass, selector); - if (method == nil) continue; - - unsigned int argumentCount = method_getNumberOfArguments(method); - NSMutableIndexSet *blockArgumentIndexes = [NSMutableIndexSet indexSet]; - static const char *blockType = @encode(typeof(^{})); - for (unsigned int i = 2; i < argumentCount; i++) { - char *type = method_copyArgumentType(method, i); - if (!strcmp(type, blockType)) { - [blockArgumentIndexes addIndex:i - 2]; - } - free(type); - } - - NSString *JSMethodName = strlen(entry[1]) ? @(entry[1]) : [NSStringFromSelector(selector) componentsSeparatedByString:@":"][0]; + // Create method RCTModuleMethod *moduleMethod = - [[RCTModuleMethod alloc] initWithSelector:selector - JSMethodName:JSMethodName - arity:method_getNumberOfArguments(method) - 2 - blockArgumentIndexes:blockArgumentIndexes]; + [[RCTModuleMethod alloc] initWithMethodName:@(entries[0]) + JSMethodName:strlen(entries[1]) ? @(entries[1]) : nil]; - NSArray *methods = methodsByModuleClassName[className]; - methodsByModuleClassName[className] = methods ? [methods arrayByAddingObject:moduleMethod] : @[moduleMethod]; + // Cache method + NSArray *methods = methodsByModuleClassName[moduleMethod.moduleClassName]; + methodsByModuleClassName[moduleMethod.moduleClassName] = + methods ? [methods arrayByAddingObject:moduleMethod] : @[moduleMethod]; } methodsByModuleID = [[RCTSparseArray alloc] initWithCapacity:[classes count]]; @@ -256,7 +338,7 @@ static RCTSparseArray *RCTExportedMethodsByModuleID(void) methodsByModuleID[moduleID] = methodsByModuleClassName[NSStringFromClass(moduleClass)]; }]; }); - + return methodsByModuleID; } @@ -303,19 +385,12 @@ static NSDictionary *RCTRemoteModulesConfig(NSDictionary *modulesByName) @"type": @"remote", }; }]; - + NSDictionary *module = @{ @"moduleID": @(moduleID), @"methods": methodsByName }; - // Add static constants - if (RCTClassOverridesClassMethod(moduleClass, @selector(constantsToExport))) { - NSMutableDictionary *mutableModule = [module mutableCopy]; - mutableModule[@"constants"] = [moduleClass constantsToExport] ?: @{}; - module = [mutableModule copy]; - } - remoteModuleConfigByClassName[NSStringFromClass(moduleClass)] = module; }]; }); @@ -324,14 +399,15 @@ static NSDictionary *RCTRemoteModulesConfig(NSDictionary *modulesByName) NSMutableDictionary *moduleConfig = [[NSMutableDictionary alloc] init]; [modulesByName enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, id module, BOOL *stop) { - // Add "psuedo-constants" + // Add constants NSMutableDictionary *config = remoteModuleConfigByClassName[NSStringFromClass([module class])]; - if (RCTClassOverridesInstanceMethod([module class], @selector(constantsToExport))) { - NSMutableDictionary *mutableConfig = [NSMutableDictionary dictionaryWithDictionary:config]; - NSMutableDictionary *mutableConstants = [NSMutableDictionary dictionaryWithDictionary:config[@"constants"]]; - [mutableConstants addEntriesFromDictionary:[module constantsToExport]]; - mutableConfig[@"constants"] = mutableConstants; // There's no real need to copy this - config = mutableConfig; // Nor this - receiver is unlikely to mutate it + if ([module respondsToSelector:@selector(constantsToExport)]) { + NSDictionary *constants = [module constantsToExport]; + if (constants) { + NSMutableDictionary *mutableConfig = [NSMutableDictionary dictionaryWithDictionary:config]; + mutableConfig[@"constants"] = constants; // There's no real need to copy this + config = mutableConfig; // Nor this - receiver is unlikely to mutate it + } } moduleConfig[moduleName] = config; @@ -340,6 +416,7 @@ static NSDictionary *RCTRemoteModulesConfig(NSDictionary *modulesByName) return moduleConfig; } + /** * As above, but for local modules/methods, which represent JS classes * and methods that will be called by the native code via the bridge. @@ -417,21 +494,19 @@ static NSDictionary *RCTLocalModulesConfig() static id _latestJSExecutor; -- (instancetype)initWithJavaScriptExecutor:(id)javaScriptExecutor - moduleProvider:(RCTBridgeModuleProviderBlock)block +- (instancetype)initWithExecutor:(id)executor + moduleProvider:(RCTBridgeModuleProviderBlock)block { if ((self = [super init])) { - _javaScriptExecutor = javaScriptExecutor; + _javaScriptExecutor = executor; _latestJSExecutor = _javaScriptExecutor; _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; _shadowQueue = dispatch_queue_create("com.facebook.ReactKit.ShadowQueue", DISPATCH_QUEUE_SERIAL); // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; - if (block) { - for (id module in block(self)) { - preregisteredModules[RCTModuleNameForClass([module class])] = module; - } + for (id module in block ? block() : nil) { + preregisteredModules[RCTModuleNameForClass([module class])] = module; } // Instantiate modules @@ -444,14 +519,14 @@ static id _latestJSExecutor; // 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(RCTCreateModuleInstance(moduleClass, self) == nil, + RCTAssert([[moduleClass alloc] init] == nil, @"Attempted to register RCTBridgeModule class %@ for the name '%@', \ but name was already registered by class %@", moduleClass, moduleName, [modulesByName[moduleName] class]); } } else { // Module name hasn't been used before, so go ahead and instantiate - id module = RCTCreateModuleInstance(moduleClass, self); + id module = [[moduleClass alloc] init]; if (module) { _modulesByID[moduleID] = modulesByName[moduleName] = module; } @@ -461,6 +536,13 @@ static id _latestJSExecutor; // Store modules _modulesByName = [modulesByName copy]; + // Set bridge + for (id module in _modulesByName.allValues) { + if ([module respondsToSelector:@selector(setBridge:)]) { + module.bridge = self; + } + } + // Inject module data into JS context NSString *configJSON = RCTJSONStringify(@{ @"remoteModuleConfig": RCTRemoteModulesConfig(_modulesByName), @@ -470,12 +552,12 @@ static id _latestJSExecutor; [_javaScriptExecutor injectJSONText:configJSON asGlobalObjectNamed:@"__fbBatchedBridgeConfig" callback:^(id err) { dispatch_semaphore_signal(semaphore); }]; - + if (dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)) != 0) { - RCTLogMustFix(@"JavaScriptExecutor took too long to inject JSON object"); + RCTLogError(@"JavaScriptExecutor took too long to inject JSON object"); } } - + return self; } @@ -501,23 +583,29 @@ static id _latestJSExecutor; - (void)invalidate { + // Release executor if (_latestJSExecutor == _javaScriptExecutor) { _latestJSExecutor = nil; } [_javaScriptExecutor invalidate]; _javaScriptExecutor = nil; - - dispatch_sync(_shadowQueue, ^{ + + // Wait for queued methods to finish + dispatch_sync(self.shadowQueue, ^{ // Make sure all dispatchers have been executed before continuing // TODO: is this still needed? }); - + + // Invalidate modules for (id target in _modulesByID.allObjects) { if ([target respondsToSelector:@selector(invalidate)]) { [(id)target invalidate]; } } - [_modulesByID removeAllObjects]; + + // Release modules (breaks retain cycle if module has strong bridge reference) + _modulesByID = nil; + _modulesByName = nil; } /** @@ -537,10 +625,10 @@ static id _latestJSExecutor; NSNumber *moduleID = RCTLocalModuleIDs[moduleDotMethod]; RCTAssert(moduleID != nil, @"Module '%@' not registered.", [[moduleDotMethod componentsSeparatedByString:@"."] firstObject]); - + NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod]; RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod); - + [self _invokeAndProcessModule:@"BatchedBridge" method:@"callFunctionReturnFlushedQueue" arguments:@[moduleID, methodID, args ?: @[]]]; @@ -554,12 +642,12 @@ static id _latestJSExecutor; onComplete(scriptLoadError); return; } - + [_javaScriptExecutor executeJSCall:@"BatchedBridge" method:@"flushedQueue" arguments:@[] - callback:^(id objcValue, NSError *error) { - [self _handleBuffer:objcValue]; + callback:^(id json, NSError *error) { + [self _handleBuffer:json]; onComplete(error); }]; }]; @@ -570,35 +658,28 @@ static id _latestJSExecutor; - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args { NSTimeInterval startJS = RCTTGetAbsoluteTime(); - - RCTJavaScriptCallback processResponse = ^(id objcValue, NSError *error) { + + RCTJavaScriptCallback processResponse = ^(id json, NSError *error) { NSTimeInterval startNative = RCTTGetAbsoluteTime(); - [self _handleBuffer:objcValue]; - + [self _handleBuffer:json]; + NSTimeInterval end = RCTTGetAbsoluteTime(); NSTimeInterval timeJS = startNative - startJS; NSTimeInterval timeNative = end - startNative; - + // TODO: surface this performance information somewhere - [[NSNotificationCenter defaultCenter] postNotificationName:@"PERF" object:nil userInfo:@{@"JS": @(timeJS * 1000000), @"Native": @(timeNative * 1000000)}]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"PERF" object:nil userInfo:@{ + @"JS": @(timeJS * 1000000), + @"Native": @(timeNative * 1000000) + }]; }; - + [_javaScriptExecutor executeJSCall:module method:method arguments:args callback:processResponse]; } -/** - * TODO (#5906496): Have responses piggy backed on a round trip with ObjC->JS requests. - */ -- (void)_sendResponseToJavaScriptCallbackID:(NSInteger)cbID args:(NSArray *)args -{ - [self _invokeAndProcessModule:@"BatchedBridge" - method:@"invokeCallbackAndReturnFlushedQueue" - arguments:@[@(cbID), args]]; -} - #pragma mark - Payload Processing - (void)_handleBuffer:(id)buffer @@ -606,12 +687,12 @@ static id _latestJSExecutor; if (buffer == nil || buffer == (id)kCFNull) { return; } - + if (![buffer isKindOfClass:[NSArray class]]) { RCTLogError(@"Buffer must be an instance of NSArray, got %@", NSStringFromClass([buffer class])); return; } - + NSArray *requestsArray = (NSArray *)buffer; NSUInteger bufferRowCount = [requestsArray count]; NSUInteger expectedFieldsCount = RCTBridgeFieldResponseReturnValues + 1; @@ -619,7 +700,7 @@ static id _latestJSExecutor; RCTLogError(@"Must pass all fields to buffer - expected %zd, saw %zd", expectedFieldsCount, bufferRowCount); return; } - + for (NSUInteger fieldIndex = RCTBridgeFieldRequestModuleIDs; fieldIndex <= RCTBridgeFieldParamss; fieldIndex++) { id field = [requestsArray objectAtIndex:fieldIndex]; if (![field isKindOfClass:[NSArray class]]) { @@ -627,18 +708,18 @@ static id _latestJSExecutor; return; } } - + NSArray *moduleIDs = requestsArray[RCTBridgeFieldRequestModuleIDs]; NSArray *methodIDs = requestsArray[RCTBridgeFieldMethodIDs]; NSArray *paramsArrays = requestsArray[RCTBridgeFieldParamss]; - + NSUInteger numRequests = [moduleIDs count]; BOOL allSame = numRequests == [methodIDs count] && numRequests == [paramsArrays count]; if (!allSame) { RCTLogError(@"Invalid data message - all must be length: %zd", numRequests); return; } - + for (NSUInteger i = 0; i < numRequests; i++) { @autoreleasepool { [self _handleRequestNumber:i @@ -647,9 +728,9 @@ static id _latestJSExecutor; params:paramsArrays[i]]; } } - + // TODO: only used by RCTUIManager - can we eliminate this special case? - dispatch_async(_shadowQueue, ^{ + dispatch_async(self.shadowQueue, ^{ for (id module in _modulesByID.allObjects) { if ([module respondsToSelector:@selector(batchDidComplete)]) { [module batchDidComplete]; @@ -668,170 +749,42 @@ static id _latestJSExecutor; return NO; } + // Look up method NSArray *methods = RCTExportedMethodsByModuleID()[moduleID]; if (methodID >= methods.count) { RCTLogError(@"Unknown methodID: %zd for module: %zd (%@)", methodID, moduleID, RCTModuleNamesByID[moduleID]); return NO; } - RCTModuleMethod *method = methods[methodID]; - NSUInteger methodArity = method.arity; - if (params.count != methodArity) { - RCTLogError(@"Expected %tu arguments but got %tu invoking %@.%@", - methodArity, - params.count, - RCTModuleNamesByID[moduleID], - method.JSMethodName); - return NO; - } - + __weak RCTBridge *weakSelf = self; - dispatch_async(_shadowQueue, ^{ + dispatch_async(self.shadowQueue, ^{ __strong RCTBridge *strongSelf = weakSelf; - + if (!strongSelf.isValid) { // strongSelf has been invalidated since the dispatch_async call and this // invocation should not continue. return; } - - // TODO: we should just store module instances by index, since that's how we look them up anyway - id target = strongSelf->_modulesByID[moduleID]; - RCTAssert(target != nil, @"No module found for name '%@'", RCTModuleNamesByID[moduleID]); - - SEL selector = method.selector; - NSMethodSignature *methodSignature = [target methodSignatureForSelector:selector]; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; - [invocation setArgument:&target atIndex:0]; - [invocation setArgument:&selector atIndex:1]; - - // Retain used blocks until after invocation completes. - NS_VALID_UNTIL_END_OF_SCOPE NSMutableArray *blocks = [NSMutableArray array]; - - [params enumerateObjectsUsingBlock:^(id param, NSUInteger idx, BOOL *stop) { - if ([param isEqual:[NSNull null]]) { - param = nil; - } else if ([method.blockArgumentIndexes containsIndex:idx]) { - id block = [strongSelf createResponseSenderBlock:[param integerValue]]; - [blocks addObject:block]; - param = block; - } - - NSUInteger argIdx = idx + 2; - - // TODO: can we do this lookup in advance and cache the logic instead of - // recalculating it every time for every parameter? - BOOL shouldSet = YES; - const char *argumentType = [methodSignature getArgumentTypeAtIndex:argIdx]; - switch (argumentType[0]) { - case ':': - if ([param isKindOfClass:[NSString class]]) { - SEL sel = NSSelectorFromString(param); - [invocation setArgument:&sel atIndex:argIdx]; - shouldSet = NO; - } - break; - - case '*': - if ([param isKindOfClass:[NSString class]]) { - const char *string = [param UTF8String]; - [invocation setArgument:&string atIndex:argIdx]; - shouldSet = NO; - } - break; - - // TODO: it seems like an error if the param doesn't respond - // so we should probably surface that error rather than failing silently -#define CASE(_value, _type, _selector) \ - case _value: \ - if ([param respondsToSelector:@selector(_selector)]) { \ - _type value = [param _selector]; \ - [invocation setArgument:&value atIndex:argIdx]; \ - shouldSet = NO; \ - } \ - break; - - CASE('c', char, charValue) - CASE('C', unsigned char, unsignedCharValue) - CASE('s', short, shortValue) - CASE('S', unsigned short, unsignedShortValue) - CASE('i', int, intValue) - CASE('I', unsigned int, unsignedIntValue) - CASE('l', long, longValue) - CASE('L', unsigned long, unsignedLongValue) - CASE('q', long long, longLongValue) - CASE('Q', unsigned long long, unsignedLongLongValue) - CASE('f', float, floatValue) - CASE('d', double, doubleValue) - CASE('B', BOOL, boolValue) - - default: - break; - } - - if (shouldSet) { - [invocation setArgument:¶m atIndex:argIdx]; - } - }]; - + + // Look up module + id module = strongSelf->_modulesByID[moduleID]; + if (!module) { + RCTLogError(@"No module found for name '%@'", RCTModuleNamesByID[moduleID]); + return; + } + @try { - [invocation invoke]; + [method invokeWithBridge:strongSelf module:module arguments:params]; } @catch (NSException *exception) { - RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, target, params, exception); + RCTLogError(@"Exception thrown while invoking %@ on target %@ with params %@: %@", method.JSMethodName, module, params, exception); } }); - + return YES; } -/** - * Returns a callback that reports values back to the JS thread. - * TODO (#5906496): These responses should go into their own queue `MessageQueue.m` that - * mirrors the JS queue and protocol. For now, we speak the "language" of the JS - * queue by packing it into an array that matches the wire protocol. - */ -- (RCTResponseSenderBlock)createResponseSenderBlock:(NSInteger)cbID -{ - if (!cbID) { - return nil; - } - - return ^(NSArray *args) { - [self _sendResponseToJavaScriptCallbackID:cbID args:args]; - }; -} - -+ (NSInvocation *)invocationForAdditionalArguments:(NSUInteger)argCount -{ - static NSMutableDictionary *invocations; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - invocations = [NSMutableDictionary dictionary]; - }); - - id key = @(argCount); - NSInvocation *invocation = invocations[key]; - if (invocation == nil) { - NSString *objCTypes = [@"v@:" stringByPaddingToLength:3 + argCount withString:@"@" startingAtIndex:0]; - NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:objCTypes.UTF8String]; - invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; - invocations[key] = invocation; - } - - return invocation; -} - -- (void)registerRootView:(RCTRootView *)rootView -{ - // TODO: only used by RCTUIManager - can we eliminate this special case? - for (id module in _modulesByID.allObjects) { - if ([module respondsToSelector:@selector(registerRootView:)]) { - [module registerRootView:rootView]; - } - } -} - + (BOOL)hasValidJSExecutor { return (_latestJSExecutor != nil && [_latestJSExecutor isValid]); @@ -844,7 +797,7 @@ static id _latestJSExecutor; return; } NSMutableArray *args = [NSMutableArray arrayWithObject:level]; - + // TODO (#5906496): Find out and document why we skip the first object for (id ob in [objects subarrayWithRange:(NSRange){1, [objects count] - 1}]) { if ([NSJSONSerialization isValidJSONObject:@[ob]]) { @@ -853,11 +806,12 @@ static id _latestJSExecutor; [args addObject:[ob description]]; } } - // Note the js executor could get invalidated while we're trying to call this...need to watch out for that. + + // Note: the js executor could get invalidated while we're trying to call this...need to watch out for that. [_latestJSExecutor executeJSCall:@"RCTLog" method:@"logIfNoNativeHook" arguments:args - callback:^(id objcValue, NSError *error) {}]; + callback:^(id json, NSError *error) {}]; } @end diff --git a/ReactKit/Base/RCTBridgeModule.h b/ReactKit/Base/RCTBridgeModule.h index d9df70a22..2627a0b9f 100644 --- a/ReactKit/Base/RCTBridgeModule.h +++ b/ReactKit/Base/RCTBridgeModule.h @@ -19,10 +19,12 @@ typedef void (^RCTResponseSenderBlock)(NSArray *response); @optional /** - * Optional initializer for modules that require access - * to bridge features, such as sending events or making JS calls + * A reference to the RCTBridge. Useful for modules that require access + * to bridge features, such as sending events or making JS calls. This + * will be set automatically by the bridge when it initializes the module. +* To implement this in your module, just add @synthesize bridge = _bridge; */ -- (instancetype)initWithBridge:(RCTBridge *)bridge; +@property (nonatomic, strong) RCTBridge *bridge; /** * The module name exposed to JS. If omitted, this will be inferred @@ -42,17 +44,11 @@ typedef void (^RCTResponseSenderBlock)(NSArray *response); /** * Injects constants into JS. These constants are made accessible via - * NativeModules.moduleName.X. Note that this method is not inherited when you - * subclass a module, and you should not call [super constantsToExport] when - * implementing it. - */ -+ (NSDictionary *)constantsToExport; - -/** - * Some "constants" are not really constant, and need to be re-generated - * each time the bridge module is created. Support for this feature is - * deprecated and may be going away or changing, but for now you can use - * the -constantsToExport instance method to register these "pseudo-constants". + * NativeModules.ModuleName.X. This method is called when the module is + * registered by the bridge. It is only called once for the lifetime of the + * bridge, so it is not suitable for returning dynamic values, but may be + * used for long-lived values such as session keys, that are regenerated only + * as part of a reload of the entire React application. */ - (NSDictionary *)constantsToExport; diff --git a/ReactKit/Base/RCTConvert.h b/ReactKit/Base/RCTConvert.h index 51a6b76ce..3c1afaad7 100644 --- a/ReactKit/Base/RCTConvert.h +++ b/ReactKit/Base/RCTConvert.h @@ -3,9 +3,9 @@ #import #import -#import "Layout.h" -#import "RCTPointerEvents.h" -#import "RCTAnimationType.h" +#import "../Layout/Layout.h" +#import "../Views/RCTAnimationType.h" +#import "../Views/RCTPointerEvents.h" /** * This class provides a collection of conversion functions for mapping @@ -19,11 +19,14 @@ + (float)float:(id)json; + (int)int:(id)json; -+ (NSString *)NSString:(id)json; -+ (NSNumber *)NSNumber:(id)json; + (NSInteger)NSInteger:(id)json; + (NSUInteger)NSUInteger:(id)json; ++ (NSArray *)NSArray:(id)json; ++ (NSDictionary *)NSDictionary:(id)json; ++ (NSString *)NSString:(id)json; ++ (NSNumber *)NSNumber:(id)json; + + (NSURL *)NSURL:(id)json; + (NSURLRequest *)NSURLRequest:(id)json; @@ -42,13 +45,15 @@ + (CGRect)CGRect:(id)json; + (UIEdgeInsets)UIEdgeInsets:(id)json; ++ (CGLineCap)CGLineCap:(id)json; ++ (CGLineJoin)CGLineJoin:(id)json; + + (CATransform3D)CATransform3D:(id)json; + (CGAffineTransform)CGAffineTransform:(id)json; + (UIColor *)UIColor:(id)json; + (CGColorRef)CGColor:(id)json; -+ (CAKeyframeAnimation *)GIF:(id)json; + (UIImage *)UIImage:(id)json; + (CGImageRef)CGImage:(id)json; @@ -57,6 +62,11 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)json; + (UIFont *)UIFont:(UIFont *)font withFamily:(id)json size:(id)json weight:(id)json; ++ (NSArray *)NSStringArray:(id)json; ++ (NSArray *)NSNumberArray:(id)json; ++ (NSArray *)UIColorArray:(id)json; ++ (NSArray *)CGColorArray:(id)json; + + (BOOL)css_overflow:(id)json; + (css_flex_direction_t)css_flex_direction_t:(id)json; + (css_justify_t)css_justify_t:(id)json; @@ -69,6 +79,10 @@ @end +#ifdef __cplusplus +extern "C" { +#endif + /** * This function will attempt to set a property using a json value by first * inferring the correct type from all available information, and then @@ -83,3 +97,127 @@ BOOL RCTSetProperty(id target, NSString *keypath, id json); * be set, it will do nothing and return NO. */ BOOL RCTCopyProperty(id target, id source, NSString *keypath); + +/** + * This function attempts to convert a JSON value to an object that can be used + * in KVC with the specific target and key path. + */ +id RCTConvertValue(id target, NSString *keypath, id json); + +#ifdef __cplusplus +} +#endif + +/** + * This macro is used for creating converter functions with arbitrary logic. + */ +#define RCT_CONVERTER_CUSTOM(type, name, code) \ ++ (type)name:(id)json \ +{ \ + if (json == [NSNull null]) { \ + json = nil; \ + } \ + @try { \ + return code; \ + } \ + @catch (__unused NSException *e) { \ + RCTLogError(@"JSON value '%@' of type '%@' cannot be converted to '%s'", \ + json, [json class], #type); \ + json = nil; \ + return code; \ + } \ +} + +/** + * This macro is used for creating simple converter functions that just call + * the specified getter method on the json value. + */ +#define RCT_CONVERTER(type, name, getter) \ +RCT_CONVERTER_CUSTOM(type, name, [json getter]) + +/** + * This macro is used for creating converters for enum types. + */ +#define RCT_ENUM_CONVERTER(type, values, default, getter) \ ++ (type)type:(id)json \ +{ \ + static NSDictionary *mapping; \ + static dispatch_once_t onceToken; \ + dispatch_once(&onceToken, ^{ \ + mapping = values; \ + }); \ + if (!json || json == [NSNull null]) { \ + return default; \ + } \ + if ([json isKindOfClass:[NSNumber class]]) { \ + if ([[mapping allValues] containsObject:json] || [json getter] == default) { \ + return [json getter]; \ + } \ + RCTLogError(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allValues]); \ + return default; \ + } \ + if (![json isKindOfClass:[NSString class]]) { \ + RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", #type, [json class], json); \ + } \ + id value = mapping[json]; \ + if(!value && [json description].length > 0) { \ + RCTLogError(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allKeys]); \ + } \ + return value ? [value getter] : default; \ +} + +/** + * This macro is used for creating converter functions for structs that consist + * of a number of CGFloat properties, such as CGPoint, CGRect, etc. + */ +#define RCT_CGSTRUCT_CONVERTER(type, values) \ ++ (type)type:(id)json \ +{ \ + @try { \ + static NSArray *fields; \ + static NSUInteger count; \ + static dispatch_once_t onceToken; \ + dispatch_once(&onceToken, ^{ \ + fields = values; \ + count = [fields count]; \ + }); \ + type result; \ + if ([json isKindOfClass:[NSArray class]]) { \ + if ([json count] != count) { \ + RCTLogError(@"Expected array with count %zd, but count is %zd: %@", count, [json count], json); \ + } else { \ + for (NSUInteger i = 0; i < count; i++) { \ + ((CGFloat *)&result)[i] = [json[i] doubleValue]; \ + } \ + } \ + } else if ([json isKindOfClass:[NSDictionary class]]) { \ + for (NSUInteger i = 0; i < count; i++) { \ + ((CGFloat *)&result)[i] = [json[fields[i]] doubleValue]; \ + } \ + } else if (json && json != [NSNull null]) { \ + RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \ + } \ + return result; \ + } \ + @catch (__unused NSException *e) { \ + RCTLogError(@"JSON value '%@' cannot be converted to '%s'", json, #type); \ + type result; \ + return result; \ + } \ +} + +/** + * This macro is used for creating converter functions for typed arrays. + */ +#define RCT_ARRAY_CONVERTER(type) \ ++ (NSArray *)type##Array:(id)json \ +{ \ + NSMutableArray *values = [[NSMutableArray alloc] init]; \ + for (id jsonValue in [self NSArray:json]) { \ + id value = [self type:jsonValue]; \ + if (value) { \ + [values addObject:value]; \ + } \ + } \ + return values; \ +} diff --git a/ReactKit/Base/RCTConvert.m b/ReactKit/Base/RCTConvert.m index 752d03ced..e972ea26b 100644 --- a/ReactKit/Base/RCTConvert.m +++ b/ReactKit/Base/RCTConvert.m @@ -4,9 +4,6 @@ #import -#import -#import - #import "RCTLog.h" CGFloat const RCTDefaultFontSize = 14; @@ -14,87 +11,6 @@ NSString *const RCTDefaultFontName = @"HelveticaNeue"; NSString *const RCTDefaultFontWeight = @"normal"; NSString *const RCTBoldFontWeight = @"bold"; -#define RCT_CONVERTER_CUSTOM(type, name, code) \ -+ (type)name:(id)json \ -{ \ - @try { \ - return code; \ - } \ - @catch (__unused NSException *e) { \ - RCTLogError(@"JSON value '%@' of type '%@' cannot be converted to '%s'", \ - json, [json class], #type); \ - json = nil; \ - return code; \ - } \ -} - -#define RCT_CONVERTER(type, name, getter) \ -RCT_CONVERTER_CUSTOM(type, name, [json getter]) - -#define RCT_ENUM_CONVERTER(type, values, default, getter) \ -+ (type)type:(id)json \ -{ \ - static NSDictionary *mapping; \ - static dispatch_once_t onceToken; \ - dispatch_once(&onceToken, ^{ \ - mapping = values; \ - }); \ - if (!json) { \ - return default; \ - } \ - if ([json isKindOfClass:[NSNumber class]]) { \ - if ([[mapping allValues] containsObject:json] || [json getter] == default) { \ - return [json getter]; \ - } \ - RCTLogError(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allValues]); \ - return default; \ - } \ - if (![json isKindOfClass:[NSString class]]) { \ - RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", #type, [json class], json); \ - } \ - id value = mapping[json]; \ - if(!value && [json description].length > 0) { \ - RCTLogError(@"Invalid %s '%@'. should be one of: %@", #type, json, [mapping allKeys]); \ - } \ - return value ? [value getter] : default; \ -} - -#define RCT_STRUCT_CONVERTER(type, values) \ -+ (type)type:(id)json \ -{ \ - @try { \ - static NSArray *fields; \ - static NSUInteger count; \ - static dispatch_once_t onceToken; \ - dispatch_once(&onceToken, ^{ \ - fields = values; \ - count = [fields count]; \ - }); \ - type result; \ - if ([json isKindOfClass:[NSArray class]]) { \ - if ([json count] != count) { \ - RCTLogError(@"Expected array with count %zd, but count is %zd: %@", count, [json count], json); \ - } else { \ - for (NSUInteger i = 0; i < count; i++) { \ - ((CGFloat *)&result)[i] = [json[i] doubleValue]; \ - } \ - } \ - } else if ([json isKindOfClass:[NSDictionary class]]) { \ - for (NSUInteger i = 0; i < count; i++) { \ - ((CGFloat *)&result)[i] = [json[fields[i]] doubleValue]; \ - } \ - } else if (json) { \ - RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \ - } \ - return result; \ - } \ - @catch (__unused NSException *e) { \ - RCTLogError(@"JSON value '%@' cannot be converted to '%s'", json, #type); \ - type result; \ - return result; \ - } \ -} - @implementation RCTConvert RCT_CONVERTER(BOOL, BOOL, boolValue) @@ -102,11 +18,14 @@ RCT_CONVERTER(double, double, doubleValue) RCT_CONVERTER(float, float, floatValue) RCT_CONVERTER(int, int, intValue) -RCT_CONVERTER(NSString *, NSString, description) -RCT_CONVERTER_CUSTOM(NSNumber *, NSNumber, @([json doubleValue])) RCT_CONVERTER(NSInteger, NSInteger, integerValue) RCT_CONVERTER_CUSTOM(NSUInteger, NSUInteger, [json unsignedIntegerValue]) +RCT_CONVERTER_CUSTOM(NSArray *, NSArray, [NSArray arrayWithArray:json]) +RCT_CONVERTER_CUSTOM(NSDictionary *, NSDictionary, [NSDictionary dictionaryWithDictionary:json]) +RCT_CONVERTER(NSString *, NSString, description) +RCT_CONVERTER_CUSTOM(NSNumber *, NSNumber, @([json doubleValue])) + + (NSURL *)NSURL:(id)json { if (![json isKindOfClass:[NSString class]]) { @@ -131,21 +50,19 @@ RCT_CONVERTER_CUSTOM(NSUInteger, NSUInteger, [json unsignedIntegerValue]) return [NSURLRequest requestWithURL:[self NSURL:json]]; } -RCT_CONVERTER_CUSTOM(NSDate *, NSDate, [NSDate dateWithTimeIntervalSince1970:[json doubleValue]]) -RCT_CONVERTER_CUSTOM(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[json doubleValue]]) -RCT_CONVERTER(NSTimeInterval, NSTimeInterval, doubleValue) +// JS Standard for time is milliseconds +RCT_CONVERTER_CUSTOM(NSDate *, NSDate, [NSDate dateWithTimeIntervalSince1970:[json doubleValue] / 1000.0]) +RCT_CONVERTER_CUSTOM(NSTimeInterval, NSTimeInterval, [json doubleValue] / 1000.0) + +// JS standard for time zones is minutes. +RCT_CONVERTER_CUSTOM(NSTimeZone *, NSTimeZone, [NSTimeZone timeZoneForSecondsFromGMT:[json doubleValue] * 60.0]) -/** - * NOTE: We don't deliberately don't support NSTextAlignmentJustified in the - * X-platform RCTText implementation because it isn't available on Android. - * We may wish to support this for iOS-specific controls such as UILabel. - */ RCT_ENUM_CONVERTER(NSTextAlignment, (@{ @"auto": @(NSTextAlignmentNatural), @"left": @(NSTextAlignmentLeft), @"center": @(NSTextAlignmentCenter), @"right": @(NSTextAlignmentRight), - /* @"justify": @(NSTextAlignmentJustify), */ + @"justify": @(NSTextAlignmentJustified), }), NSTextAlignmentNatural, integerValue) RCT_ENUM_CONVERTER(NSWritingDirection, (@{ @@ -158,7 +75,7 @@ RCT_ENUM_CONVERTER(UITextAutocapitalizationType, (@{ @"none": @(UITextAutocapitalizationTypeNone), @"words": @(UITextAutocapitalizationTypeWords), @"sentences": @(UITextAutocapitalizationTypeSentences), - @"all": @(UITextAutocapitalizationTypeAllCharacters) + @"characters": @(UITextAutocapitalizationTypeAllCharacters) }), UITextAutocapitalizationTypeSentences, integerValue) RCT_ENUM_CONVERTER(UIKeyboardType, (@{ @@ -167,19 +84,31 @@ RCT_ENUM_CONVERTER(UIKeyboardType, (@{ }), UIKeyboardTypeDefault, integerValue) RCT_CONVERTER(CGFloat, CGFloat, doubleValue) -RCT_STRUCT_CONVERTER(CGPoint, (@[@"x", @"y"])) -RCT_STRUCT_CONVERTER(CGSize, (@[@"w", @"h"])) -RCT_STRUCT_CONVERTER(CGRect, (@[@"x", @"y", @"w", @"h"])) -RCT_STRUCT_CONVERTER(UIEdgeInsets, (@[@"top", @"left", @"bottom", @"right"])) +RCT_CGSTRUCT_CONVERTER(CGPoint, (@[@"x", @"y"])) +RCT_CGSTRUCT_CONVERTER(CGSize, (@[@"w", @"h"])) +RCT_CGSTRUCT_CONVERTER(CGRect, (@[@"x", @"y", @"w", @"h"])) +RCT_CGSTRUCT_CONVERTER(UIEdgeInsets, (@[@"top", @"left", @"bottom", @"right"])) -RCT_STRUCT_CONVERTER(CATransform3D, (@[ +RCT_ENUM_CONVERTER(CGLineJoin, (@{ + @"miter": @(kCGLineJoinMiter), + @"round": @(kCGLineJoinRound), + @"bevel": @(kCGLineJoinBevel), +}), kCGLineJoinMiter, intValue) + +RCT_ENUM_CONVERTER(CGLineCap, (@{ + @"butt": @(kCGLineCapButt), + @"round": @(kCGLineCapRound), + @"square": @(kCGLineCapSquare), +}), kCGLineCapButt, intValue) + +RCT_CGSTRUCT_CONVERTER(CATransform3D, (@[ @"m11", @"m12", @"m13", @"m14", @"m21", @"m22", @"m23", @"m24", @"m31", @"m32", @"m33", @"m34", @"m41", @"m42", @"m43", @"m44" ])) -RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"])) +RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"])) + (UIColor *)UIColor:(id)json { @@ -428,85 +357,11 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"] return [self UIColor:json].CGColor; } -+ (CAKeyframeAnimation *)GIF:(id)json -{ - CGImageSourceRef imageSource = NULL; - if ([json isKindOfClass:[NSString class]]) { - NSString *path = json; - if (path.length == 0) { - return nil; - } - - NSURL *fileURL = [path isAbsolutePath] ? [NSURL fileURLWithPath:path] : [[NSBundle mainBundle] URLForResource:path withExtension:nil]; - imageSource = CGImageSourceCreateWithURL((CFURLRef)fileURL, NULL); - } else if ([json isKindOfClass:[NSData class]]) { - NSData *data = json; - if (data.length == 0) { - return nil; - } - - imageSource = CGImageSourceCreateWithData((CFDataRef)data, NULL); - } - - if (!UTTypeConformsTo(CGImageSourceGetType(imageSource), kUTTypeGIF)) { - CFRelease(imageSource); - return nil; - } - - NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(imageSource, NULL); - NSUInteger loopCount = [properties[(id)kCGImagePropertyGIFDictionary][(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue]; - - size_t imageCount = CGImageSourceGetCount(imageSource); - NSTimeInterval duration = 0; - NSMutableArray *delays = [NSMutableArray arrayWithCapacity:imageCount]; - NSMutableArray *images = [NSMutableArray arrayWithCapacity:imageCount]; - for (size_t i = 0; i < imageCount; i++) { - CGImageRef image = CGImageSourceCreateImageAtIndex(imageSource, i, NULL); - NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, i, NULL); - NSDictionary *frameGIFProperties = frameProperties[(id)kCGImagePropertyGIFDictionary]; - - const NSTimeInterval kDelayTimeIntervalDefault = 0.1; - NSNumber *delayTime = frameGIFProperties[(id)kCGImagePropertyGIFUnclampedDelayTime] ?: frameGIFProperties[(id)kCGImagePropertyGIFDelayTime]; - if (delayTime == nil) { - if (i == 0) { - delayTime = @(kDelayTimeIntervalDefault); - } else { - delayTime = delays[i - 1]; - } - } - - const NSTimeInterval kDelayTimeIntervalMinimum = 0.02; - if (delayTime.floatValue < (float)kDelayTimeIntervalMinimum - FLT_EPSILON) { - delayTime = @(kDelayTimeIntervalDefault); - } - - duration += delayTime.doubleValue; - delays[i] = delayTime; - images[i] = (__bridge_transfer id)image; - } - - CFRelease(imageSource); - - NSMutableArray *keyTimes = [NSMutableArray arrayWithCapacity:delays.count]; - NSTimeInterval runningDuration = 0; - for (NSNumber *delayNumber in delays) { - [keyTimes addObject:@(runningDuration / duration)]; - runningDuration += delayNumber.doubleValue; - } - - [keyTimes addObject:@1.0]; - - CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"]; - animation.calculationMode = kCAAnimationDiscrete; - animation.repeatCount = loopCount == 0 ? HUGE_VALF : loopCount; - animation.keyTimes = keyTimes; - animation.values = images; - animation.duration = duration; - return animation; -} - + (UIImage *)UIImage:(id)json { + // 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 isKindOfClass:[NSString class]]) { RCTLogError(@"Expected NSString for UIImage, received %@: %@", [json class], json); return nil; @@ -526,9 +381,8 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"] image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:path ofType:nil]]; } } - if (!image) { - RCTLogWarn(@"No image was found at path %@", json); - } + // NOTE: we don't warn about nil images because there are legitimate + // case where we find out if a string is an image by using this method return image; } @@ -564,7 +418,7 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"] } // Get font family - NSString *familyName = [RCTConvert NSString:family]; + NSString *familyName = [self NSString:family]; if (familyName) { if ([UIFont fontNamesForFamilyName:familyName].count == 0) { font = [UIFont fontWithName:familyName size:fontDescriptor.pointSize]; @@ -584,7 +438,7 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"] } // Get font weight - NSString *fontWeight = [RCTConvert NSString:weight]; + NSString *fontWeight = [self NSString:weight]; if (fontWeight) { static NSSet *values; @@ -612,6 +466,20 @@ RCT_STRUCT_CONVERTER(CGAffineTransform, (@[@"a", @"b", @"c", @"d", @"tx", @"ty"] return [UIFont fontWithDescriptor:fontDescriptor size:fontDescriptor.pointSize]; } +RCT_ARRAY_CONVERTER(NSString) +RCT_ARRAY_CONVERTER(NSNumber) +RCT_ARRAY_CONVERTER(UIColor) + +// Can't use RCT_ARRAY_CONVERTER due to bridged cast ++ (NSArray *)CGColorArray:(id)json +{ + NSMutableArray *colors = [[NSMutableArray alloc] init]; + for (id value in [self NSArray:json]) { + [colors addObject:(__bridge id)[self CGColor:value]]; + } + return colors; +} + typedef BOOL css_overflow; RCT_ENUM_CONVERTER(css_overflow, (@{ @@ -652,8 +520,9 @@ RCT_ENUM_CONVERTER(css_wrap_type_t, (@{ RCT_ENUM_CONVERTER(RCTPointerEvents, (@{ @"none": @(RCTPointerEventsNone), - @"boxonly": @(RCTPointerEventsBoxOnly), - @"boxnone": @(RCTPointerEventsBoxNone) + @"box-only": @(RCTPointerEventsBoxOnly), + @"box-none": @(RCTPointerEventsBoxNone), + @"auto": @(RCTPointerEventsUnspecified) }), RCTPointerEventsUnspecified, integerValue) RCT_ENUM_CONVERTER(RCTAnimationType, (@{ @@ -668,19 +537,35 @@ RCT_ENUM_CONVERTER(RCTAnimationType, (@{ static NSString *RCTGuessTypeEncoding(id target, NSString *key, id value, NSString *encoding) { + /** + * NOTE: the property names below may seem weird, but it's + * because they are tested as case-sensitive suffixes, so + * "ffset" will match any of the following + * + * - offset + * - contentOffset + */ + // TODO (#5906496): handle more cases - if ([key rangeOfString:@"color" options:NSCaseInsensitiveSearch].location != NSNotFound) { + if ([key hasSuffix:@"olor"]) { if ([target isKindOfClass:[CALayer class]]) { return @(@encode(CGColorRef)); } else { return @"@\"UIColor\""; } + } else if ([key hasSuffix:@"Inset"] || [key hasSuffix:@"Insets"]) { + return @(@encode(UIEdgeInsets)); + } else if ([key hasSuffix:@"rame"] || [key hasSuffix:@"ounds"]) { + return @(@encode(CGRect)); + } else if ([key hasSuffix:@"ffset"] || [key hasSuffix:@"osition"]) { + return @(@encode(CGPoint)); + } else if ([key hasSuffix:@"ize"]) { + return @(@encode(CGSize)); } - return nil; } -static NSDictionary *RCTConvertValue(id value, NSString *encoding) +static id RCTConvertValueWithEncoding(id value, NSString *encoding) { static NSDictionary *converters = nil; static dispatch_once_t onceToken; @@ -762,18 +647,8 @@ static NSDictionary *RCTConvertValue(id value, NSString *encoding) return converter ? converter(value) : value; } -BOOL RCTSetProperty(id target, NSString *keypath, id value) +static NSString *RCTPropertyEncoding(id target, NSString *key, id value) { - // Split keypath - NSArray *parts = [keypath componentsSeparatedByString:@"."]; - NSString *key = [parts lastObject]; - for (NSUInteger i = 0; i < parts.count - 1; i++) { - target = [target valueForKey:parts[i]]; - if (!target) { - return NO; - } - } - // Check target class for property definition NSString *encoding = nil; objc_property_t property = class_getProperty([target class], [key UTF8String]); @@ -792,7 +667,7 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value) [key substringFromIndex:1]]); if (![target respondsToSelector:setter]) { - return NO; + return nil; } // Get type of first method argument @@ -802,17 +677,25 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value) encoding = @(typeEncoding); free(typeEncoding); } + + if (encoding.length == 0 || [encoding isEqualToString:@(@encode(id))]) { + // Not enough info about the type encoding to be useful, so + // try to guess the type from the value and property name + encoding = RCTGuessTypeEncoding(target, key, value, encoding); + } + } - if (encoding.length == 0 || [encoding isEqualToString:@(@encode(id))]) { - // Not enough info about the type encoding to be useful, so - // try to guess the type from the value and property name - encoding = RCTGuessTypeEncoding(target, key, value, encoding); - } + // id encoding means unknown, as opposed to nil which means no setter exists. + return encoding ?: @(@encode(id)); +} +static id RCTConvertValueWithExplicitEncoding(id target, NSString *key, id json, NSString *encoding) +{ // Special case for numeric encodings, which may be enums - if ([value isKindOfClass:[NSString class]] && - [@"iIsSlLqQ" rangeOfString:[encoding substringToIndex:1]].length) { + if ([json isKindOfClass:[NSString class]] && + ([encoding isEqualToString:@(@encode(id))] || + [@"iIsSlLqQ" rangeOfString:[encoding substringToIndex:1]].length)) { /** * NOTE: the property names below may seem weird, but it's @@ -839,6 +722,12 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value) @"extAlignment": ^(id val) { return [RCTConvert NSTextAlignment:val]; }, + @"Cap": ^(id val) { + return [RCTConvert CGLineCap:val]; + }, + @"Join": ^(id val) { + return [RCTConvert CGLineJoin:val]; + }, @"ointerEvents": ^(id val) { return [RCTConvert RCTPointerEvents:val]; }, @@ -847,12 +736,42 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value) for (NSString *subkey in converters) { if ([key hasSuffix:subkey]) { NSInteger (^converter)(NSString *) = converters[subkey]; - value = @(converter(value)); + json = @(converter(json)); break; } } } + return RCTConvertValueWithEncoding(json, encoding); +} + +id RCTConvertValue(id target, NSString *key, id json) +{ + NSString *encoding = RCTPropertyEncoding(target, key, json); + return RCTConvertValueWithExplicitEncoding(target, key, json, encoding); +} + +BOOL RCTSetProperty(id target, NSString *keypath, id value) +{ + // Split keypath + NSArray *parts = [keypath componentsSeparatedByString:@"."]; + NSString *key = [parts lastObject]; + for (NSUInteger i = 0; i < parts.count - 1; i++) { + target = [target valueForKey:parts[i]]; + if (!target) { + return NO; + } + } + + // Get encoding + NSString *encoding = RCTPropertyEncoding(target, key, value); + if (!encoding) { + return NO; + } + + // Convert value + value = RCTConvertValueWithExplicitEncoding(target, keypath, value, encoding); + // Another nasty special case if ([target isKindOfClass:[UITextField class]]) { static NSDictionary *specialCases = nil; @@ -870,15 +789,15 @@ BOOL RCTSetProperty(id target, NSString *keypath, id value) }); void (^block)(UITextField *f, NSInteger v) = specialCases[key]; - if (block) - { + if (block) { block(target, [value integerValue]); return YES; } } // Set converted value - [target setValue:RCTConvertValue(value, encoding) forKey:key]; + [target setValue:value forKey:key]; + return YES; } diff --git a/ReactKit/Base/RCTJavaScriptExecutor.h b/ReactKit/Base/RCTJavaScriptExecutor.h index 7062570a4..4d32f1c2f 100644 --- a/ReactKit/Base/RCTJavaScriptExecutor.h +++ b/ReactKit/Base/RCTJavaScriptExecutor.h @@ -5,7 +5,7 @@ #import "RCTInvalidating.h" typedef void (^RCTJavaScriptCompleteBlock)(NSError *error); -typedef void (^RCTJavaScriptCallback)(id objcValue, NSError *error); +typedef void (^RCTJavaScriptCallback)(id json, NSError *error); /** * Abstracts away a JavaScript execution context - we may be running code in a diff --git a/ReactKit/Base/RCTLog.h b/ReactKit/Base/RCTLog.h index a97d13458..ba72bc8ff 100644 --- a/ReactKit/Base/RCTLog.h +++ b/ReactKit/Base/RCTLog.h @@ -20,28 +20,22 @@ // If defined, only log messages that match this regex will fatal #define RCTLOG_FATAL_REGEX nil -#define _RCTLog(__RCTLog__level, ...) do { \ - NSString *__RCTLog__levelStr; \ - switch(__RCTLog__level) { \ - case RCTLOG_INFO: __RCTLog__levelStr = @"info"; break; \ - case RCTLOG_WARN: __RCTLog__levelStr = @"warn"; break; \ - case RCTLOG_ERROR: __RCTLog__levelStr = @"error"; break; \ - case RCTLOG_MUSTFIX: __RCTLog__levelStr = @"mustfix"; break; \ - } \ - NSString *__RCTLog__msg = _RCTLogObjects(RCTLogFormat(__VA_ARGS__), __RCTLog__levelStr); \ - if (__RCTLog__level >= RCTLOG_FATAL_LEVEL) { \ - BOOL __RCTLog__fail = YES; \ - if (RCTLOG_FATAL_REGEX) { \ - NSError *__RCTLog__e; \ - NSRegularExpression *__RCTLog__regex = [NSRegularExpression regularExpressionWithPattern:RCTLOG_FATAL_REGEX options:0 error:&__RCTLog__e]; \ - __RCTLog__fail = [__RCTLog__regex numberOfMatchesInString:__RCTLog__msg options:0 range:NSMakeRange(0, [__RCTLog__msg length])] > 0; \ - } \ - RCTCAssert(!__RCTLog__fail, @"RCTLOG_FATAL_LEVEL %@: %@", __RCTLog__levelStr, __RCTLog__msg); \ - } \ - if (__RCTLog__level >= RCTLOG_REDBOX_LEVEL) { \ - RCTRedBox *__RCTLog__redBox = [RCTRedBox sharedInstance]; \ - [__RCTLog__redBox showErrorMessage:__RCTLog__msg]; \ - } \ +extern __unsafe_unretained NSString *RCTLogLevels[]; + +#define _RCTLog(_level, ...) do { \ + NSString *__RCTLog__levelStr = RCTLogLevels[_level - 1]; \ + NSString *__RCTLog__msg = RCTLogObjects(RCTLogFormat(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__), __RCTLog__levelStr); \ + if (_level >= RCTLOG_FATAL_LEVEL) { \ + BOOL __RCTLog__fail = YES; \ + if (RCTLOG_FATAL_REGEX) { \ + NSRegularExpression *__RCTLog__regex = [NSRegularExpression regularExpressionWithPattern:RCTLOG_FATAL_REGEX options:0 error:NULL]; \ + __RCTLog__fail = [__RCTLog__regex numberOfMatchesInString:__RCTLog__msg options:0 range:NSMakeRange(0, [__RCTLog__msg length])] > 0; \ + } \ + RCTCAssert(!__RCTLog__fail, @"RCTLOG_FATAL_LEVEL %@: %@", __RCTLog__levelStr, __RCTLog__msg); \ + } \ + if (_level >= RCTLOG_REDBOX_LEVEL) { \ + [[RCTRedBox sharedInstance] showErrorMessage:__RCTLog__msg]; \ + } \ } while (0) #define RCTLog(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__) @@ -50,20 +44,15 @@ #define RCTLogError(...) _RCTLog(RCTLOG_ERROR, __VA_ARGS__) #define RCTLogMustFix(...) _RCTLog(RCTLOG_MUSTFIX, __VA_ARGS__) -#define RCTLogFormat(...) _RCTLogFormat(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__) -#define RCTLogFormatString(...) _RCTLogFormatString(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__) - #ifdef __cplusplus extern "C" { #endif -NSString *_RCTLogObjects(NSArray *objects, NSString *level); -NSArray *_RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) NS_FORMAT_FUNCTION(4,5); -NSString *_RCTLogFormatString(const char *file, int lineNumber, const char *funcName, NSString *format, ...) NS_FORMAT_FUNCTION(4,5); +NSString *RCTLogObjects(NSArray *objects, NSString *level); +NSArray *RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) NS_FORMAT_FUNCTION(4,5); + +void RCTInjectLogFunction(void (^logFunction)(NSString *msg)); #ifdef __cplusplus } #endif - -typedef void (^RCTLogFunction)(NSString *format, NSString *str); -void RCTInjectLogFunction(RCTLogFunction func); diff --git a/ReactKit/Base/RCTLog.m b/ReactKit/Base/RCTLog.m index dcf60084e..862eb5a26 100644 --- a/ReactKit/Base/RCTLog.m +++ b/ReactKit/Base/RCTLog.m @@ -4,10 +4,17 @@ #import "RCTBridge.h" -static RCTLogFunction injectedLogFunction; +__unsafe_unretained NSString *RCTLogLevels[] = { + @"info", + @"warn", + @"error", + @"mustfix" +}; -void RCTInjectLogFunction(RCTLogFunction func) { - injectedLogFunction = func; +static void (^RCTInjectedLogFunction)(NSString *msg); + +void RCTInjectLogFunction(void (^logFunction)(NSString *msg)) { + RCTInjectedLogFunction = logFunction; } static inline NSString *_RCTLogPreamble(const char *file, int lineNumber, const char *funcName) @@ -21,7 +28,7 @@ static inline NSString *_RCTLogPreamble(const char *file, int lineNumber, const } // TODO (#5906496): // kinda ugly that this is tied to RCTBridge -NSString *_RCTLogObjects(NSArray *objects, NSString *level) +NSString *RCTLogObjects(NSArray *objects, NSString *level) { NSString *str = objects[0]; #if TARGET_IPHONE_SIMULATOR @@ -33,8 +40,8 @@ NSString *_RCTLogObjects(NSArray *objects, NSString *level) { // Print normal errors with timestamps when not in simulator. // Non errors are already compiled out above, so log as error here. - if (injectedLogFunction) { - injectedLogFunction(@">\n %@", str); + if (RCTInjectedLogFunction) { + RCTInjectedLogFunction(str); } else { NSLog(@">\n %@", str); } @@ -42,14 +49,14 @@ NSString *_RCTLogObjects(NSArray *objects, NSString *level) return str; } -// Returns array of objects. First arg is a simple string to print, remaining args are objects to pass through to the debugger so they are -// inspectable in the console. -NSArray *_RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) +// Returns array of objects. First arg is a simple string to print, remaining args +// are objects to pass through to the debugger so they are inspectable in the console. +NSArray *RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) { va_list args; va_start(args, format); NSString *preamble = _RCTLogPreamble(file, lineNumber, funcName); - + // Pull out NSObjects so we can pass them through as inspectable objects to the js debugger NSArray *formatParts = [format componentsSeparatedByString:@"%"]; NSMutableArray *objects = [NSMutableArray arrayWithObject:preamble]; @@ -77,12 +84,3 @@ NSArray *_RCTLogFormat(const char *file, int lineNumber, const char *funcName, N [objectsOut addObjectsFromArray:objects]; return objectsOut; } - -NSString *_RCTLogFormatString(const char *file, int lineNumber, const char *funcName, NSString *format, ...) -{ - va_list args; - va_start (args, format); - NSString *body = [[NSString alloc] initWithFormat:format arguments:args]; - va_end (args); - return [NSString stringWithFormat:@"%@ %@", _RCTLogPreamble(file, lineNumber, funcName), body]; -} diff --git a/ReactKit/Base/RCTRedBox.h b/ReactKit/Base/RCTRedBox.h index 82137eb06..c13ac729b 100644 --- a/ReactKit/Base/RCTRedBox.h +++ b/ReactKit/Base/RCTRedBox.h @@ -11,6 +11,8 @@ - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack; - (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack; +- (NSString *)currentErrorMessage; + - (void)dismiss; @end diff --git a/ReactKit/Base/RCTRedBox.m b/ReactKit/Base/RCTRedBox.m index 917331f2b..d36202650 100644 --- a/ReactKit/Base/RCTRedBox.m +++ b/ReactKit/Base/RCTRedBox.m @@ -2,42 +2,42 @@ #import "RCTRedBox.h" -#import "RCTRootView.h" #import "RCTUtils.h" @interface RCTRedBoxWindow : UIWindow +@property (nonatomic, copy) NSString *lastErrorMessage; + @end @implementation RCTRedBoxWindow { UIView *_rootView; UITableView *_stackTraceTableView; - - NSString *_lastErrorMessage; + NSArray *_lastStackTrace; - + UITableViewCell *_cachedMessageCell; } - (id)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { - + self.windowLevel = UIWindowLevelStatusBar + 5; self.backgroundColor = [UIColor colorWithRed:0.8 green:0 blue:0 alpha:1]; self.hidden = YES; - + UIViewController *rootController = [[UIViewController alloc] init]; self.rootViewController = rootController; _rootView = rootController.view; _rootView.backgroundColor = [UIColor clearColor]; - + const CGFloat buttonHeight = 60; CGRect detailsFrame = _rootView.bounds; detailsFrame.size.height -= buttonHeight; - + _stackTraceTableView = [[UITableView alloc] initWithFrame:detailsFrame style:UITableViewStylePlain]; _stackTraceTableView.delegate = self; _stackTraceTableView.dataSource = self; @@ -45,21 +45,21 @@ _stackTraceTableView.separatorColor = [[UIColor whiteColor] colorWithAlphaComponent:0.3]; _stackTraceTableView.separatorStyle = UITableViewCellSeparatorStyleNone; [_rootView addSubview:_stackTraceTableView]; - + UIButton *dismissButton = [UIButton buttonWithType:UIButtonTypeCustom]; dismissButton.titleLabel.font = [UIFont systemFontOfSize:14]; [dismissButton setTitle:@"Dismiss (ESC)" forState:UIControlStateNormal]; [dismissButton setTitleColor:[[UIColor whiteColor] colorWithAlphaComponent:0.5] forState:UIControlStateNormal]; [dismissButton setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted]; [dismissButton addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside]; - + UIButton *reloadButton = [UIButton buttonWithType:UIButtonTypeCustom]; 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 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); @@ -85,18 +85,20 @@ - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow { - _lastStackTrace = stack; - _lastErrorMessage = message; - - if (self.hidden && shouldShow) { + if ((self.hidden && shouldShow) || (!self.hidden && [_lastErrorMessage isEqualToString:message])) { + _lastStackTrace = stack; + _lastErrorMessage = message; _cachedMessageCell = [self reuseCell:nil forErrorMessage:message]; [_stackTraceTableView reloadData]; [_stackTraceTableView setNeedsLayout]; - [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] - atScrollPosition:UITableViewScrollPositionTop - animated:NO]; + if (self.hidden) { + [_stackTraceTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] + atScrollPosition:UITableViewScrollPositionTop + animated:NO]; + } + [self makeKeyAndVisible]; [self becomeFirstResponder]; } @@ -105,11 +107,13 @@ - (void)dismiss { self.hidden = YES; + [self resignFirstResponder]; + [[[[UIApplication sharedApplication] delegate] window] makeKeyWindow]; } - (void)reload { - [RCTRootView reloadAll]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"RCTReloadNotification" object:nil userInfo:nil]; [self dismiss]; } @@ -149,9 +153,9 @@ cell.backgroundColor = [UIColor clearColor]; cell.selectionStyle = UITableViewCellSelectionStyleNone; } - + cell.textLabel.text = message; - + return cell; } @@ -168,7 +172,7 @@ cell.selectedBackgroundView = [[UIView alloc] init]; cell.selectedBackgroundView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.2]; } - + cell.textLabel.text = stackFrame[@"methodName"]; cell.detailTextLabel.text = cell.detailTextLabel.text = [NSString stringWithFormat:@"%@:%@", [stackFrame[@"file"] lastPathComponent], stackFrame[@"lineNumber"]]; return cell; @@ -179,7 +183,7 @@ if ([indexPath section] == 0) { NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; - + NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:16], NSParagraphStyleAttributeName: paragraphStyle}; CGRect boundingRect = [_lastErrorMessage boundingRectWithSize:CGSizeMake(tableView.frame.size.width - 30, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil]; @@ -206,14 +210,14 @@ // NOTE: We could use RCTKeyCommands for this, but since // we control this window, we can use the standard, non-hacky // mechanism instead - + return @[ - + // Dismiss red box [UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:0 action:@selector(dismiss)], - + // Reload [UIKeyCommand keyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand @@ -269,12 +273,12 @@ - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack showIfHidden:(BOOL)shouldShow { - + #if DEBUG - + dispatch_block_t block = ^{ if (!_window) { - _window = [[RCTRedBoxWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + _window = [[RCTRedBoxWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; } [_window showErrorMessage:message withStack:stack showIfHidden:shouldShow]; }; @@ -283,9 +287,18 @@ } else { dispatch_async(dispatch_get_main_queue(), block); } - + #endif - + +} + +- (NSString *)currentErrorMessage +{ + if (_window && !_window.hidden) { + return _window.lastErrorMessage; + } else { + return nil; + } } - (void)dismiss diff --git a/ReactKit/Base/RCTRootView.h b/ReactKit/Base/RCTRootView.h index f0c381638..3fc0be165 100644 --- a/ReactKit/Base/RCTRootView.h +++ b/ReactKit/Base/RCTRootView.h @@ -2,6 +2,8 @@ #import +#import "RCTBridge.h" + @interface RCTRootView : UIView /** @@ -19,13 +21,20 @@ */ @property (nonatomic, copy) NSString *moduleName; +/** + * A block that returns an array of pre-allocated modules. These + * modules will take precedence over any automatically registered + * modules of the same name. + */ +@property (nonatomic, copy) RCTBridgeModuleProviderBlock moduleProvider; + /** * The default properties to apply to the view when the script bundle * is first loaded. Defaults to nil/empty. */ @property (nonatomic, copy) NSDictionary *initialProperties; -/** +/** * The class of the RCTJavaScriptExecutor to use with this view. * If not specified, it will default to using RCTContextExecutor. * Changes will take effect next time the bundle is reloaded. @@ -38,4 +47,7 @@ - (void)reload; + (void)reloadAll; +- (void)startOrResetInteractionTiming; +- (NSDictionary *)endAndResetInteractionTiming; + @end diff --git a/ReactKit/Base/RCTRootView.m b/ReactKit/Base/RCTRootView.m index 5968357a9..bc9a0972a 100644 --- a/ReactKit/Base/RCTRootView.m +++ b/ReactKit/Base/RCTRootView.m @@ -8,19 +8,20 @@ #import "RCTKeyCommands.h" #import "RCTLog.h" #import "RCTRedBox.h" +#import "RCTSourceCode.h" #import "RCTTouchHandler.h" #import "RCTUIManager.h" #import "RCTUtils.h" #import "RCTWebViewExecutor.h" #import "UIView+ReactKit.h" -NSString *const RCTRootViewReloadNotification = @"RCTRootViewReloadNotification"; +NSString *const RCTReloadNotification = @"RCTReloadNotification"; @implementation RCTRootView { RCTBridge *_bridge; RCTTouchHandler *_touchHandler; - id _executor; + id _executor; } static Class _globalExecutorClass; @@ -41,7 +42,10 @@ static Class _globalExecutorClass; [[RCTKeyCommands sharedInstance] registerKeyCommandWithInput:@"d" modifierFlags:UIKeyModifierCommand action:^(UIKeyCommand *command) { - _globalExecutorClass = [RCTWebViewExecutor class]; + _globalExecutorClass = NSClassFromString(@"RCTWebSocketExecutor"); + if (!_globalExecutorClass) { + RCTLogWarn(@"WebSocket debugger is not available. Did you forget to include RCTWebSocketExecutor?"); + } [self reloadAll]; }]; @@ -77,7 +81,7 @@ static Class _globalExecutorClass; // Add reload observer [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reload) - name:RCTRootViewReloadNotification + name:RCTReloadNotification object:nil]; } @@ -95,6 +99,10 @@ static Class _globalExecutorClass; [_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer" args:@[self.reactTag]]; + + // TODO: eventually we'll want to be able to share the bridge between + // multiple rootviews, in which case we'll need to move this elsewhere + [_bridge invalidate]; } - (void)bundleFinishedLoading:(NSError *)error @@ -108,7 +116,7 @@ static Class _globalExecutorClass; } } else { - [_bridge registerRootView:self]; + [_bridge.uiManager registerRootView:self]; NSString *moduleName = _moduleName ?: @""; NSDictionary *appParameters = @{ @@ -131,12 +139,13 @@ static Class _globalExecutorClass; // Clean up [self removeGestureRecognizer:_touchHandler]; + [_touchHandler invalidate]; [_executor invalidate]; [_bridge invalidate]; // Choose local executor if specified, followed by global, followed by default _executor = [[_executorClass ?: _globalExecutorClass ?: [RCTContextExecutor class] alloc] init]; - _bridge = [[RCTBridge alloc] initWithJavaScriptExecutor:_executor moduleProvider:nil]; + _bridge = [[RCTBridge alloc] initWithExecutor:_executor moduleProvider:_moduleProvider]; _touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge]; [self addGestureRecognizer:_touchHandler]; @@ -195,6 +204,10 @@ static Class _globalExecutorClass; } // Success! + RCTSourceCode *sourceCodeModule = _bridge.modules[NSStringFromClass([RCTSourceCode class])]; + sourceCodeModule.scriptURL = _scriptURL; + sourceCodeModule.scriptText = rawText; + [_bridge enqueueApplicationScript:rawText url:_scriptURL onComplete:^(NSError *error) { dispatch_async(dispatch_get_main_queue(), ^{ [self bundleFinishedLoading:error]; @@ -228,7 +241,17 @@ static Class _globalExecutorClass; + (void)reloadAll { - [[NSNotificationCenter defaultCenter] postNotificationName:RCTRootViewReloadNotification object:nil]; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification object:nil]; +} + +- (void)startOrResetInteractionTiming +{ + [_touchHandler startOrResetInteractionTiming]; +} + +- (NSDictionary *)endAndResetInteractionTiming +{ + return [_touchHandler endAndResetInteractionTiming]; } @end diff --git a/ReactKit/Base/RCTTouchHandler.h b/ReactKit/Base/RCTTouchHandler.h index e75bb1531..46c81b9ef 100644 --- a/ReactKit/Base/RCTTouchHandler.h +++ b/ReactKit/Base/RCTTouchHandler.h @@ -2,10 +2,14 @@ #import +#import "RCTInvalidating.h" + @class RCTBridge; -@interface RCTTouchHandler : UIGestureRecognizer +@interface RCTTouchHandler : UIGestureRecognizer -- (instancetype)initWithBridge:(RCTBridge *)bridge; +- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; +- (void)startOrResetInteractionTiming; +- (NSDictionary *)endAndResetInteractionTiming; @end diff --git a/ReactKit/Base/RCTTouchHandler.m b/ReactKit/Base/RCTTouchHandler.m index c56f996cc..90e1e64c7 100644 --- a/ReactKit/Base/RCTTouchHandler.m +++ b/ReactKit/Base/RCTTouchHandler.m @@ -14,10 +14,42 @@ // TODO: this class behaves a lot like a module, and could be implemented as a // module if we were to assume that modules and RootViews had a 1:1 relationship +@interface RCTTouchEvent : NSObject + +@property (nonatomic, assign, readonly) NSUInteger id; +@property (nonatomic, copy, readonly) NSString *eventName; +@property (nonatomic, copy, readonly) NSArray *touches; +@property (nonatomic, copy, readonly) NSArray *changedIndexes; +@property (nonatomic, assign, readonly) CFTimeInterval originatingTime; + +@end + + +@implementation RCTTouchEvent + ++ (instancetype)touchWithEventName:(NSString *)eventName touches:(NSArray *)touches changedIndexes:(NSArray *)changedIndexes originatingTime:(CFTimeInterval)originatingTime +{ + RCTTouchEvent *touchEvent = [[self alloc] init]; + touchEvent->_id = [self newID]; + touchEvent->_eventName = [eventName copy]; + touchEvent->_touches = [touches copy]; + touchEvent->_changedIndexes = [changedIndexes copy]; + touchEvent->_originatingTime = originatingTime; + return touchEvent; +} + ++ (NSUInteger)newID +{ + static NSUInteger id = 0; + return ++id; +} + +@end + @implementation RCTTouchHandler { __weak RCTBridge *_bridge; - + /** * Arrays managed in parallel tracking native touch object along with the * native view that was touched, and the react touch data dictionary. @@ -27,25 +59,32 @@ NSMutableOrderedSet *_nativeTouches; NSMutableArray *_reactTouches; NSMutableArray *_touchViews; -} -- (instancetype)initWithTarget:(id)target action:(SEL)action -{ - RCT_NOT_DESIGNATED_INITIALIZER(); + BOOL _recordingInteractionTiming; + CFTimeInterval _mostRecentEnqueueJS; + CADisplayLink *_displayLink; + NSMutableArray *_pendingTouches; + NSMutableArray *_bridgeInteractionTiming; } - (instancetype)initWithBridge:(RCTBridge *)bridge { if ((self = [super initWithTarget:nil action:NULL])) { - + RCTAssert(bridge != nil, @"Expect an event dispatcher"); - + _bridge = bridge; - + _nativeTouches = [[NSMutableOrderedSet alloc] init]; _reactTouches = [[NSMutableArray alloc] init]; _touchViews = [[NSMutableArray alloc] init]; - + + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)]; + _pendingTouches = [[NSMutableArray alloc] init]; + _bridgeInteractionTiming = [[NSMutableArray alloc] init]; + + [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + // `cancelsTouchesInView` is needed in order to be used as a top level event delegated recognizer. Otherwise, lower // level components not build using RCT, will fail to recognize gestures. self.cancelsTouchesInView = NO; @@ -53,6 +92,17 @@ return self; } +- (BOOL)isValid +{ + return _displayLink != nil; +} + +- (void)invalidate +{ + [_displayLink invalidate]; + _displayLink = nil; +} + typedef NS_ENUM(NSInteger, RCTTouchEventType) { RCTTouchEventTypeStart, RCTTouchEventTypeMove, @@ -65,10 +115,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { - (void)_recordNewTouches:(NSSet *)touches { for (UITouch *touch in touches) { - + RCTAssert(![_nativeTouches containsObject:touch], @"Touch is already recorded. This is a critical bug."); - + // Find closest React-managed touchable view UIView *targetView = touch.view; while (targetView) { @@ -77,9 +127,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { } targetView = targetView.superview; } - - RCTAssert(targetView.reactTag && targetView.userInteractionEnabled, - @"No react view found for touch - something went wrong."); + + if (!targetView.reactTag || !targetView.userInteractionEnabled) { + return; + } // Get new, unique touch id const NSUInteger RCTMaxTouches = 11; // This is the maximum supported by iDevices @@ -94,14 +145,14 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { break; } } - + // Create touch NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:9]; reactTouch[@"target"] = [targetView reactTagAtPoint:[touch locationInView:targetView]]; reactTouch[@"identifier"] = @(touchID); reactTouch[@"touches"] = [NSNull null]; // We hijack this touchObj to serve both as an event reactTouch[@"changedTouches"] = [NSNull null]; // and as a Touch object, so making this JIT friendly. - + // Add to arrays [_touchViews addObject:targetView]; [_nativeTouches addObject:touch]; @@ -113,7 +164,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { { for (UITouch *touch in touches) { NSUInteger index = [_nativeTouches indexOfObject:touch]; - RCTAssert(index != NSNotFound, @"Touch is already removed. This is a critical bug."); + if(index == NSNotFound) { + continue; + } + [_touchViews removeObjectAtIndex:index]; [_nativeTouches removeObjectAtIndex:index]; [_reactTouches removeObjectAtIndex:index]; @@ -125,10 +179,10 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { UITouch *nativeTouch = _nativeTouches[touchIndex]; CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window]; CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:self.view]; - + UIView *touchView = _touchViews[touchIndex]; CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView]; - + NSMutableDictionary *reactTouch = _reactTouches[touchIndex]; reactTouch[@"pageX"] = @(rootViewLocation.x); reactTouch[@"pageY"] = @(rootViewLocation.y); @@ -153,16 +207,24 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { * (start/end/move/cancel) and the indices that represent "changed" `Touch`es * from that array. */ -- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName +- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName originatingTime:(CFTimeInterval)originatingTime { // Update touches + CFTimeInterval enqueueTime = CACurrentMediaTime(); NSMutableArray *changedIndexes = [[NSMutableArray alloc] init]; for (UITouch *touch in touches) { NSInteger index = [_nativeTouches indexOfObject:touch]; - RCTAssert(index != NSNotFound, @"Touch not found. This is a critical bug."); + if (index == NSNotFound) { + continue; + } + [self _updateReactTouchAtIndex:index]; [changedIndexes addObject:@(index)]; } + + if (changedIndexes.count == 0) { + return; + } // Deep copy the touches because they will be accessed from another thread // TODO: would it be safer to do this in the bridge or executor, rather than trusting caller? @@ -170,10 +232,72 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { for (NSDictionary *touch in _reactTouches) { [reactTouches addObject:[touch copy]]; } - + + RCTTouchEvent *touch = [RCTTouchEvent touchWithEventName:eventName + touches:reactTouches + changedIndexes:changedIndexes + originatingTime:originatingTime]; + [_pendingTouches addObject:touch]; + + if (_recordingInteractionTiming) { + [_bridgeInteractionTiming addObject:@{ + @"timeSeconds": @(touch.originatingTime), + @"operation": @"taskOriginated", + @"taskID": @(touch.id), + }]; + [_bridgeInteractionTiming addObject:@{ + @"timeSeconds": @(enqueueTime), + @"operation": @"taskEnqueuedPending", + @"taskID": @(touch.id), + }]; + } +} + +- (void)_update:(CADisplayLink *)sender +{ // Dispatch touch event - [_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches" - args:@[eventName, reactTouches, changedIndexes]]; + NSUInteger pendingCount = _pendingTouches.count; + for (RCTTouchEvent *touch in _pendingTouches) { + _mostRecentEnqueueJS = CACurrentMediaTime(); + [_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches" + args:@[touch.eventName, touch.touches, touch.changedIndexes]]; + } + + if (_recordingInteractionTiming) { + for (RCTTouchEvent *touch in _pendingTouches) { + [_bridgeInteractionTiming addObject:@{ + @"timeSeconds": @(sender.timestamp), + @"operation": @"frameAlignedDispatch", + @"taskID": @(touch.id), + }]; + } + + if (pendingCount > 0 || sender.timestamp - _mostRecentEnqueueJS < 0.1) { + [_bridgeInteractionTiming addObject:@{ + @"timeSeconds": @(sender.timestamp), + @"operation": @"mainThreadDisplayLink", + @"taskID": @([RCTTouchEvent newID]), + }]; + } + } + + [_pendingTouches removeAllObjects]; +} + +- (void)startOrResetInteractionTiming +{ + RCTAssertMainThread(); + [_bridgeInteractionTiming removeAllObjects]; + _recordingInteractionTiming = YES; +} + +- (NSDictionary *)endAndResetInteractionTiming +{ + RCTAssertMainThread(); + _recordingInteractionTiming = NO; + NSArray *_prevInteractionTimingData = _bridgeInteractionTiming; + _bridgeInteractionTiming = [[NSMutableArray alloc] init]; + return @{ @"interactionTiming": _prevInteractionTimingData }; } #pragma mark - Gesture Recognizer Delegate Callbacks @@ -182,11 +306,11 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { { [super touchesBegan:touches withEvent:event]; self.state = UIGestureRecognizerStateBegan; - + // "start" has to record new touches before extracting the event. // "end"/"cancel" needs to remove the touch *after* extracting the event. [self _recordNewTouches:touches]; - [self _updateAndDispatchTouches:touches eventName:@"topTouchStart"]; + [self _updateAndDispatchTouches:touches eventName:@"topTouchStart" originatingTime:event.timestamp]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event @@ -195,20 +319,20 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) { if (self.state == UIGestureRecognizerStateFailed) { return; } - [self _updateAndDispatchTouches:touches eventName:@"topTouchMove"]; + [self _updateAndDispatchTouches:touches eventName:@"topTouchMove" originatingTime:event.timestamp]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; - [self _updateAndDispatchTouches:touches eventName:@"topTouchEnd"]; + [self _updateAndDispatchTouches:touches eventName:@"topTouchEnd" originatingTime:event.timestamp]; [self _recordRemovedTouches:touches]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesCancelled:touches withEvent:event]; - [self _updateAndDispatchTouches:touches eventName:@"topTouchCancel"]; + [self _updateAndDispatchTouches:touches eventName:@"topTouchCancel" originatingTime:event.timestamp]; [self _recordRemovedTouches:touches]; } diff --git a/ReactKit/Base/RCTUtils.h b/ReactKit/Base/RCTUtils.h index 3612b1f27..adf35cb9b 100644 --- a/ReactKit/Base/RCTUtils.h +++ b/ReactKit/Base/RCTUtils.h @@ -1,17 +1,15 @@ // Copyright 2004-present Facebook. All Rights Reserved. +#import + #import #import -#import #import "RCTAssert.h" -// Macro to indicate when inherited initializer is not to be used -#define RCT_NOT_DESIGNATED_INITIALIZER() \ -do { \ - RCTAssert(NO, @"%@ is not the designated initializer for instances of %@.", NSStringFromSelector(_cmd), [self class]); \ - return nil; \ -} while (0) +#ifdef __cplusplus +extern "C" { +#endif // Utility functions for JSON object <-> string serialization/deserialization NSString *RCTJSONStringify(id jsonObject, NSError **error); @@ -39,3 +37,15 @@ void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement); // Module subclass support BOOL RCTClassOverridesClassMethod(Class cls, SEL selector); BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector); + +// Enumerate all classes that conform to NSObject protocol +void RCTEnumerateClasses(void (^block)(Class cls)); + +// Creates a standardized error object +// TODO(#6472857): create NSErrors and automatically convert them over the bridge. +NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData); +NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData); + +#ifdef __cplusplus +} +#endif diff --git a/ReactKit/Base/RCTUtils.m b/ReactKit/Base/RCTUtils.m index 615c5235e..217368e17 100644 --- a/ReactKit/Base/RCTUtils.m +++ b/ReactKit/Base/RCTUtils.m @@ -2,11 +2,15 @@ #import "RCTUtils.h" -#import #import #import + #import +#import + +#import "RCTLog.h" + NSString *RCTJSONStringify(id jsonObject, NSError **error) { NSData *jsonData = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:error]; @@ -15,7 +19,14 @@ NSString *RCTJSONStringify(id jsonObject, NSError **error) id RCTJSONParse(NSString *jsonString, NSError **error) { - NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + if (!jsonString) { + return nil; + } + NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:NO]; + if (!jsonData) { + RCTLog(@"RCTJSONParse received the following string, which could not be losslessly converted to UTF8 data: '%@'", jsonString); + jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES]; + } return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:error]; } @@ -63,7 +74,7 @@ CGSize RCTScreenSize() size = [UIScreen mainScreen].bounds.size; } }); - + return size; } @@ -93,7 +104,7 @@ NSTimeInterval RCTTGetAbsoluteTime(void) int ret = mach_timebase_info(&tb_info); assert(0 == ret); }); - + uint64_t timeInNanoseconds = (mach_absolute_time() * tb_info.numer) / tb_info.denom; return ((NSTimeInterval)timeInNanoseconds) / 1000000; } @@ -103,11 +114,11 @@ void RCTSwapClassMethods(Class cls, SEL original, SEL replacement) Method originalMethod = class_getClassMethod(cls, original); IMP originalImplementation = method_getImplementation(originalMethod); const char *originalArgTypes = method_getTypeEncoding(originalMethod); - + Method replacementMethod = class_getClassMethod(cls, replacement); IMP replacementImplementation = method_getImplementation(replacementMethod); const char *replacementArgTypes = method_getTypeEncoding(replacementMethod); - + if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes)) { class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes); @@ -123,11 +134,11 @@ void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement) Method originalMethod = class_getInstanceMethod(cls, original); IMP originalImplementation = method_getImplementation(originalMethod); const char *originalArgTypes = method_getTypeEncoding(originalMethod); - + Method replacementMethod = class_getInstanceMethod(cls, replacement); IMP replacementImplementation = method_getImplementation(replacementMethod); const char *replacementArgTypes = method_getTypeEncoding(replacementMethod); - + if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes)) { class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes); @@ -155,6 +166,50 @@ BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector) return YES; } } + free(methods); return NO; } +void RCTEnumerateClasses(void (^block)(Class cls)) +{ + static Class *classes; + static unsigned int classCount; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + classes = objc_copyClassList(&classCount); + }); + + for (unsigned int i = 0; i < classCount; i++) + { + Class cls = classes[i]; + Class superclass = cls; + while (superclass) + { + if (class_conformsToProtocol(superclass, @protocol(NSObject))) + { + block(cls); + break; + } + superclass = class_getSuperclass(superclass); + } + } +} + +NSDictionary *RCTMakeError(NSString *message, id toStringify, NSDictionary *extraData) +{ + if (toStringify) { + message = [NSString stringWithFormat:@"%@%@", message, toStringify]; + } + NSMutableDictionary *error = [@{@"message": message} mutableCopy]; + if (extraData) { + [error addEntriesFromDictionary:extraData]; + } + return error; +} + +NSDictionary *RCTMakeAndLogError(NSString *message, id toStringify, NSDictionary *extraData) +{ + id error = RCTMakeError(message, toStringify, extraData); + RCTLogError(@"\nError: %@", error); + return error; +} diff --git a/ReactKit/Executors/RCTContextExecutor.m b/ReactKit/Executors/RCTContextExecutor.m index e3eabc16f..ac27a9065 100644 --- a/ReactKit/Executors/RCTContextExecutor.m +++ b/ReactKit/Executors/RCTContextExecutor.m @@ -45,7 +45,7 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object, fprintf(stderr, "%s\n", [modifiedString UTF8String]); // don't print timestamps and other junk #else // Print normal errors with timestamps to files when not in simulator. - _RCTLogObjects(@[modifiedString], @"log"); + RCTLogObjects(@[modifiedString], @"log"); #endif JSStringRelease(string); } diff --git a/ReactKit/Modules/RCTAlertManager.m b/ReactKit/Modules/RCTAlertManager.m index ce5cabea6..97ca88f86 100644 --- a/ReactKit/Modules/RCTAlertManager.m +++ b/ReactKit/Modules/RCTAlertManager.m @@ -48,10 +48,10 @@ NSArray *buttons = args[@"buttons"]; if (!title && !message) { - RCTLogMustFix(@"Must specify either an alert title, or message, or both"); + RCTLogError(@"Must specify either an alert title, or message, or both"); return; } else if (buttons.count == 0) { - RCTLogMustFix(@"Must have at least one button."); + RCTLogError(@"Must have at least one button."); return; } @@ -68,7 +68,7 @@ NSInteger index = 0; for (NSDictionary *button in buttons) { if (button.count != 1) { - RCTLogMustFix(@"Button definitions should have exactly one key."); + RCTLogError(@"Button definitions should have exactly one key."); } NSString *buttonKey = [button.allKeys firstObject]; NSString *buttonTitle = [button[buttonKey] description]; diff --git a/ReactKit/Modules/RCTAnimationManager.h b/ReactKit/Modules/RCTAnimationManager.h new file mode 100644 index 000000000..9f2391514 --- /dev/null +++ b/ReactKit/Modules/RCTAnimationManager.h @@ -0,0 +1,9 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "RCTBridgeModule.h" + +@interface RCTAnimationManager : NSObject + +@end diff --git a/ReactKit/Modules/RCTAnimationManager.m b/ReactKit/Modules/RCTAnimationManager.m new file mode 100644 index 000000000..a5de9aa29 --- /dev/null +++ b/ReactKit/Modules/RCTAnimationManager.m @@ -0,0 +1,203 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTAnimationManager.h" + +#import +#import + +#import "RCTSparseArray.h" +#import "RCTUIManager.h" + +#if CGFLOAT_IS_DOUBLE + #define CG_APPEND(PREFIX, SUFFIX_F, SUFFIX_D) PREFIX##SUFFIX_D +#else + #define CG_APPEND(PREFIX, SUFFIX_F, SUFFIX_D) PREFIX##SUFFIX_F +#endif + +@implementation RCTAnimationManager +{ + RCTSparseArray *_animationRegistry; // Main thread only; animation tag -> view tag +} + +@synthesize bridge = _bridge; + +- (instancetype)init +{ + if ((self = [super init])) { + _animationRegistry = [[RCTSparseArray alloc] init]; + } + + return self; +} + +- (id (^)(CGFloat))interpolateFrom:(CGFloat[])fromArray to:(CGFloat[])toArray count:(NSUInteger)count typeName:(const char *)typeName +{ + if (count == 1) { + CGFloat from = *fromArray, to = *toArray, delta = to - from; + return ^(CGFloat t) { + return @(from + t * delta); + }; + } + + CG_APPEND(vDSP_vsub,,D)(fromArray, 1, toArray, 1, toArray, 1, count); + + const size_t size = count * sizeof(CGFloat); + NSData *deltaData = [NSData dataWithBytes:toArray length:size]; + NSData *fromData = [NSData dataWithBytes:fromArray length:size]; + + return ^(CGFloat t) { + const CGFloat *delta = deltaData.bytes; + const CGFloat *fromArray = fromData.bytes; + + CGFloat value[count]; + CG_APPEND(vDSP_vma,,D)(delta, 1, &t, 0, fromArray, 1, value, 1, count); + return [NSValue valueWithBytes:value objCType:typeName]; + }; +} + +- (void)startAnimationForTag:(NSNumber *)reactTag animationTag:(NSNumber *)animationTag duration:(double)duration delay:(double)delay easingSample:(NSArray *)easingSample properties:(NSDictionary *)properties +{ + RCT_EXPORT(startAnimation); + + __weak RCTAnimationManager *weakSelf = self; + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTAnimationManager *strongSelf = weakSelf; + + UIView *view = viewRegistry[reactTag]; + if (!view) { + RCTLogWarn(@"React tag %@ is not registered with the view registry", reactTag); + return; + } + + [properties enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { + NSValue *toValue = nil; + if ([key isEqualToString:@"scaleXY"]) { + key = @"transform.scale"; + toValue = obj[0]; + } else if ([obj respondsToSelector:@selector(count)]) { + switch ([obj count]) { + case 2: + if ([obj respondsToSelector:@selector(objectForKey:)] && [obj objectForKey:@"w"]) { + toValue = [NSValue valueWithCGSize:[RCTConvert CGSize:obj]]; + } else { + toValue = [NSValue valueWithCGPoint:[RCTConvert CGPoint:obj]]; + } + break; + case 4: + toValue = [NSValue valueWithCGRect:[RCTConvert CGRect:obj]]; + break; + case 16: + toValue = [NSValue valueWithCGAffineTransform:[RCTConvert CGAffineTransform:obj]]; + break; + } + } + + if (!toValue) toValue = obj; + + const char *typeName = toValue.objCType; + + size_t count; + switch (typeName[0]) { + case 'i': + case 'I': + case 's': + case 'S': + case 'l': + case 'L': + case 'q': + case 'Q': + count = 1; + break; + + default: { + NSUInteger size; + NSGetSizeAndAlignment(typeName, &size, NULL); + count = size / sizeof(CGFloat); + break; + } + } + + CGFloat toFields[count]; + + switch (typeName[0]) { +#define CASE(encoding, type) \ + case encoding: { \ + type value; \ + [toValue getValue:&value]; \ + toFields[0] = value; \ + break; \ + } + + CASE('i', int) + CASE('I', unsigned int) + CASE('s', short) + CASE('S', unsigned short) + CASE('l', long) + CASE('L', unsigned long) + CASE('q', long long) + CASE('Q', unsigned long long) + +#undef CASE + + default: + [toValue getValue:toFields]; + break; + } + + NSValue *fromValue = [view.layer.presentationLayer valueForKeyPath:key]; + CGFloat fromFields[count]; + [fromValue getValue:fromFields]; + + id (^interpolationBlock)(CGFloat t) = [strongSelf interpolateFrom:fromFields to:toFields count:count typeName:typeName]; + + NSMutableArray *sampledValues = [NSMutableArray arrayWithCapacity:easingSample.count]; + for (NSNumber *sample in easingSample) { + CGFloat t = sample.CG_APPEND(, floatValue, doubleValue); + [sampledValues addObject:interpolationBlock(t)]; + } + + CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:key]; + animation.beginTime = CACurrentMediaTime() + delay / 1000.0; + animation.duration = duration / 1000.0; + animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + animation.values = sampledValues; + + [view.layer setValue:toValue forKey:key]; + + NSString *animationKey = [NSString stringWithFormat:@"RCT.%@.%@", animationTag, key]; + [view.layer addAnimation:animation forKey:animationKey]; + }]; + + strongSelf->_animationRegistry[animationTag] = reactTag; + }]; +} + +- (void)stopAnimation:(NSNumber *)animationTag +{ + RCT_EXPORT(stopAnimation); + + __weak RCTAnimationManager *weakSelf = self; + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTAnimationManager *strongSelf = weakSelf; + + NSNumber *reactTag = strongSelf->_animationRegistry[animationTag]; + if (!reactTag) return; + + UIView *view = viewRegistry[reactTag]; + for (NSString *animationKey in view.layer.animationKeys) { + if ([animationKey hasPrefix:@"RCT"]) { + NSRange periodLocation = [animationKey rangeOfString:@"." options:0 range:NSMakeRange(3, animationKey.length - 3)]; + if (periodLocation.location != NSNotFound) { + NSInteger integerTag = [[animationKey substringWithRange:NSMakeRange(3, periodLocation.location)] integerValue]; + if (animationTag.integerValue == integerTag) { + [view.layer removeAnimationForKey:animationKey]; + } + } + } + } + + strongSelf->_animationRegistry[animationTag] = nil; + }]; +} + +@end diff --git a/ReactKit/Modules/RCTAppState.h b/ReactKit/Modules/RCTAppState.h new file mode 100644 index 000000000..027379492 --- /dev/null +++ b/ReactKit/Modules/RCTAppState.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTBridgeModule.h" + +@interface RCTAppState : NSObject + +@end diff --git a/ReactKit/Modules/RCTAppState.m b/ReactKit/Modules/RCTAppState.m new file mode 100644 index 000000000..c4a145c1e --- /dev/null +++ b/ReactKit/Modules/RCTAppState.m @@ -0,0 +1,105 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTAppState.h" + +#import "RCTAssert.h" +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" + +static NSString *RCTCurrentAppBackgroundState() +{ + static NSDictionary *states; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + states = @{ + @(UIApplicationStateActive): @"active", + @(UIApplicationStateBackground): @"background", + @(UIApplicationStateInactive): @"inactive" + }; + }); + + return states[@([[UIApplication sharedApplication] applicationState])] ?: @"unknown"; +} + +@implementation RCTAppState +{ + NSString *_lastKnownState; +} + +@synthesize bridge = _bridge; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if ((self = [super init])) { + + _lastKnownState = RCTCurrentAppBackgroundState(); + + for (NSString *name in @[UIApplicationDidBecomeActiveNotification, + UIApplicationDidEnterBackgroundNotification, + UIApplicationDidFinishLaunchingNotification]) { + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleAppStateDidChange) + name:name + object:nil]; + } + } + return self; +} + + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - App Notification Methods + +- (void)handleAppStateDidChange +{ + NSString *newState = RCTCurrentAppBackgroundState(); + if (![newState isEqualToString:_lastKnownState]) { + _lastKnownState = newState; + [_bridge.eventDispatcher sendDeviceEventWithName:@"appStateDidChange" + body:@{@"app_state": _lastKnownState}]; + } +} + +#pragma mark - Public API + +/** + * Get the current background/foreground state of the app + */ +- (void)getCurrentAppState:(RCTResponseSenderBlock)callback + error:(__unused RCTResponseSenderBlock)error +{ + RCT_EXPORT(); + + callback(@[@{@"app_state": _lastKnownState}]); +} + +/** + * Update the application icon badge number on the home screen + */ +- (void)setApplicationIconBadgeNumber:(NSInteger)number +{ + RCT_EXPORT(); + + [UIApplication sharedApplication].applicationIconBadgeNumber = number; +} + +/** + * Get the current application icon badge number on the home screen + */ +- (void)getApplicationIconBadgeNumber:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + callback(@[ + @([UIApplication sharedApplication].applicationIconBadgeNumber) + ]); +} + +@end diff --git a/ReactKit/Modules/RCTAsyncLocalStorage.h b/ReactKit/Modules/RCTAsyncLocalStorage.h new file mode 100644 index 000000000..54a320749 --- /dev/null +++ b/ReactKit/Modules/RCTAsyncLocalStorage.h @@ -0,0 +1,24 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTBridgeModule.h" + +/** + * A simple, asynchronous, persistent, key-value storage system designed as a + * backend to the AsyncStorage JS module, which is modeled after LocalStorage. + * + * Current implementation stores small values in serialized dictionary and + * larger values in separate files. Since we use a serial file queue + * `RKFileQueue`, reading/writing from multiple threads should be perceived as + * being atomic, unless someone bypasses the `RCTAsyncLocalStorage` API. + * + * Keys and values must always be strings or an error is returned. + */ +@interface RCTAsyncLocalStorage : NSObject + +- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; +- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; +- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; +- (void)clear:(RCTResponseSenderBlock)callback; +- (void)getAllKeys:(RCTResponseSenderBlock)callback; + +@end diff --git a/ReactKit/Modules/RCTAsyncLocalStorage.m b/ReactKit/Modules/RCTAsyncLocalStorage.m new file mode 100644 index 000000000..de8b7989e --- /dev/null +++ b/ReactKit/Modules/RCTAsyncLocalStorage.m @@ -0,0 +1,292 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTAsyncLocalStorage.h" + +#import + +#import +#import + +#import "RCTLog.h" +#import "RCTUtils.h" + +static NSString *const kStorageDir = @"RCTAsyncLocalStorage_V1"; +static NSString *const kManifestFilename = @"manifest.json"; +static const NSUInteger kInlineValueThreshold = 100; + +#pragma mark - Static helper functions + +static id RCTErrorForKey(NSString *key) +{ + if (![key isKindOfClass:[NSString class]]) { + return RCTMakeAndLogError(@"Invalid key - must be a string. Key: ", key, @{@"key": key}); + } else if (key.length < 1) { + return RCTMakeAndLogError(@"Invalid key - must be at least one character. Key: ", key, @{@"key": key}); + } else { + return nil; + } +} + +static void RCTAppendError(id error, NSMutableArray **errors) +{ + if (error && errors) { + if (!*errors) { + *errors = [NSMutableArray new]; + } + [*errors addObject:error]; + } +} + +static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut) +{ + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { + NSError *error; + NSStringEncoding encoding; + NSString *entryString = [NSString stringWithContentsOfFile:filePath usedEncoding:&encoding error:&error]; + if (error) { + *errorOut = RCTMakeError(@"Failed to read storage file.", error, @{@"key": key}); + } else if (encoding != NSUTF8StringEncoding) { + *errorOut = RCTMakeError(@"Incorrect encoding of storage file: ", @(encoding), @{@"key": key}); + } else { + return entryString; + } + } + return nil; +} + +static dispatch_queue_t RCTFileQueue(void) +{ + static dispatch_queue_t fileQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // All JS is single threaded, so a serial queue is our only option. + fileQueue = dispatch_queue_create("com.facebook.rkFile", DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(fileQueue, + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + }); + + return fileQueue; +} + +#pragma mark - RCTAsyncLocalStorage + +@implementation RCTAsyncLocalStorage +{ + BOOL _haveSetup; + // The manifest is a dictionary of all keys with small values inlined. Null values indicate values that are stored + // in separate files (as opposed to nil values which don't exist). The manifest is read off disk at startup, and + // written to disk after all mutations. + NSMutableDictionary *_manifest; + NSString *_manifestPath; + NSString *_storageDirectory; +} + +- (NSString *)_filePathForKey:(NSString *)key +{ + NSString *safeFileName = RCTMD5Hash(key); + return [_storageDirectory stringByAppendingPathComponent:safeFileName]; +} + +- (id)_ensureSetup +{ + if (_haveSetup) { + return nil; + } + NSString *documentDirectory = + [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSURL *homeURL = [NSURL fileURLWithPath:documentDirectory isDirectory:YES]; + _storageDirectory = [[homeURL URLByAppendingPathComponent:kStorageDir isDirectory:YES] path]; + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + return RCTMakeError(@"Failed to create storage directory.", error, nil); + } + _manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename]; + NSDictionary *errorOut; + NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut); + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [NSMutableDictionary new]; + if (error) { + RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); + _manifest = [NSMutableDictionary new]; + } + _haveSetup = YES; + return nil; +} + +- (id)_writeManifest:(NSMutableArray **)errors +{ + NSError *error; + NSString *serialized = RCTJSONStringify(_manifest, &error); + [serialized writeToFile:_manifestPath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + id errorOut; + if (error) { + errorOut = RCTMakeError(@"Failed to write manifest file.", error, nil); + RCTAppendError(errorOut, errors); + } + return errorOut; +} + +- (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result +{ + id errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. + if (value == [NSNull null]) { + NSString *filePath = [self _filePathForKey:key]; + value = RCTReadFile(filePath, key, &errorOut); + } + [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + return errorOut; +} + +- (id)_writeEntry:(NSArray *)entry +{ + if (![entry isKindOfClass:[NSArray class]] || entry.count != 2) { + return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); + } + if (![entry[1] isKindOfClass:[NSString class]]) { + return RCTMakeAndLogError(@"Values must be strings, got: ", entry[1], entry[0]); + } + NSString *key = entry[0]; + id errorOut = RCTErrorForKey(key); + if (errorOut) { + return errorOut; + } + NSString *value = entry[1]; + NSString *filePath = [self _filePathForKey:key]; + NSError *error; + if (value.length <= kInlineValueThreshold) { + if (_manifest[key] && _manifest[key] != [NSNull null]) { + // If the value already existed but wasn't inlined, remove the old file. + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + } + _manifest[key] = value; + return nil; + } + [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + if (error) { + errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); + } else { + _manifest[key] = [NSNull null]; // Mark existence of file with null, any other value is inline data. + } + return errorOut; +} + +#pragma mark - Exported JS Functions + +- (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + if (!callback) { + RCTLogError(@"Called getItem without a callback."); + return; + } + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut], [NSNull null]]); + return; + } + NSMutableArray *errors; + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; + for (NSString *key in keys) { + id keyError = [self _appendItemForKey:key toArray:result]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + callback(@[errors ?: [NSNull null], result]); + }); +} + +- (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSArray *entry in kvPairs) { + id keyError = [self _writeEntry:entry]; + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } + }); +} + +- (void)multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (NSString *key in keys) { + id keyError = RCTErrorForKey(key); + if (!keyError) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [_manifest removeObjectForKey:key]; + } + RCTAppendError(keyError, &errors); + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } + }); +} + +- (void)clear:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (!errorOut) { + NSError *error; + for (NSString *key in _manifest) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; + } + [_manifest removeAllObjects]; + errorOut = [self _writeManifest:nil]; + } + if (callback) { + callback(@[errorOut ?: [NSNull null]]); + } + }); +} + +- (void)getAllKeys:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + dispatch_async(RCTFileQueue(), ^{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[errorOut, [NSNull null]]); + } else { + callback(@[[NSNull null], [_manifest allKeys]]); + } + }); +} + +@end diff --git a/ReactKit/Modules/RCTExceptionsManager.h b/ReactKit/Modules/RCTExceptionsManager.h index 02ea33202..340c7878b 100644 --- a/ReactKit/Modules/RCTExceptionsManager.h +++ b/ReactKit/Modules/RCTExceptionsManager.h @@ -4,6 +4,14 @@ #import "RCTBridgeModule.h" -@interface RCTExceptionsManager : NSObject +@protocol RCTExceptionsManagerDelegate + +- (void)unhandledJSExceptionWithMessage:(NSString *)message stack:(NSArray *)stack; + +@end + +@interface RCTExceptionsManager : NSObject + +- (instancetype)initWithDelegate:(id)delegate NS_DESIGNATED_INITIALIZER; @end diff --git a/ReactKit/Modules/RCTExceptionsManager.m b/ReactKit/Modules/RCTExceptionsManager.m index 29cacf6c8..4d1b5c0ee 100644 --- a/ReactKit/Modules/RCTExceptionsManager.m +++ b/ReactKit/Modules/RCTExceptionsManager.m @@ -5,12 +5,32 @@ #import "RCTRedBox.h" @implementation RCTExceptionsManager +{ + __weak id _delegate; +} + +- (instancetype)initWithDelegate:(id)delegate +{ + if ((self = [super init])) { + _delegate = delegate; + } + return self; +} + +- (instancetype)init +{ + return [self initWithDelegate:nil]; +} - (void)reportUnhandledExceptionWithMessage:(NSString *)message stack:(NSArray *)stack { RCT_EXPORT(reportUnhandledException); - [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; + if (_delegate) { + [_delegate unhandledJSExceptionWithMessage:message stack:stack]; + } else { + [[RCTRedBox sharedInstance] showErrorMessage:message withStack:stack]; + } } - (void)updateExceptionMessage:(NSString *)message stack:(NSArray *)stack diff --git a/ReactKit/Modules/RCTPushNotificationManager.h b/ReactKit/Modules/RCTPushNotificationManager.h new file mode 100644 index 000000000..e0ba53a62 --- /dev/null +++ b/ReactKit/Modules/RCTPushNotificationManager.h @@ -0,0 +1,12 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +extern NSString *const RKRemoteNotificationReceived; +extern NSString *const RKOpenURLNotification; + +@interface RCTPushNotificationManager : NSObject + +- (instancetype)initWithInitialNotification:(NSDictionary *)initialNotification NS_DESIGNATED_INITIALIZER; + +@end diff --git a/ReactKit/Modules/RCTPushNotificationManager.m b/ReactKit/Modules/RCTPushNotificationManager.m new file mode 100644 index 000000000..b895f4d28 --- /dev/null +++ b/ReactKit/Modules/RCTPushNotificationManager.m @@ -0,0 +1,64 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTPushNotificationManager.h" + +#import "RCTAssert.h" +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" + +NSString *const RKRemoteNotificationReceived = @"RemoteNotificationReceived"; +NSString *const RKOpenURLNotification = @"RKOpenURLNotification"; + +@implementation RCTPushNotificationManager +{ + NSDictionary *_initialNotification; +} + +@synthesize bridge = _bridge; + +- (instancetype)init +{ + return [self initWithInitialNotification:nil]; +} + +- (instancetype)initWithInitialNotification:(NSDictionary *)initialNotification +{ + if ((self = [super init])) { + _initialNotification = [initialNotification copy]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleRemoteNotificationReceived:) + name:RKRemoteNotificationReceived + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleOpenURLNotification:) + name:RKOpenURLNotification + object:nil]; + } + return self; +} + +- (void)handleRemoteNotificationReceived:(NSNotification *)notification +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationReceived" + body:[notification userInfo]]; +} + +- (void)handleOpenURLNotification:(NSNotification *)notification +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"openURL" + body:[notification userInfo]]; +} + +- (NSDictionary *)constantsToExport +{ + return @{ + @"initialNotification": _initialNotification ?: [NSNull null] + }; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +@end diff --git a/ReactKit/Modules/RCTSourceCode.h b/ReactKit/Modules/RCTSourceCode.h new file mode 100644 index 000000000..3c9df24df --- /dev/null +++ b/ReactKit/Modules/RCTSourceCode.h @@ -0,0 +1,12 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +#import "RCTBridgeModule.h" + +@interface RCTSourceCode : NSObject + +@property (nonatomic, copy) NSString *scriptText; +@property (nonatomic, copy) NSURL *scriptURL; + +@end diff --git a/ReactKit/Modules/RCTSourceCode.m b/ReactKit/Modules/RCTSourceCode.m new file mode 100644 index 000000000..b8799378f --- /dev/null +++ b/ReactKit/Modules/RCTSourceCode.m @@ -0,0 +1,21 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTSourceCode.h" + +#import "RCTAssert.h" +#import "RCTUtils.h" + +@implementation RCTSourceCode + +- (void)getScriptText:(RCTResponseSenderBlock)successCallback failureCallback:(RCTResponseSenderBlock)failureCallback +{ + RCT_EXPORT(); + if (self.scriptText && self.scriptURL) { + successCallback(@[@{@"text": self.scriptText, @"url":[self.scriptURL absoluteString]}]); + } else { + failureCallback(@[RCTMakeError(@"Source code is not available", nil, nil)]); + } + +} + +@end diff --git a/ReactKit/Modules/RCTStatusBarManager.m b/ReactKit/Modules/RCTStatusBarManager.m index 9a49cdd62..c9639f070 100644 --- a/ReactKit/Modules/RCTStatusBarManager.m +++ b/ReactKit/Modules/RCTStatusBarManager.m @@ -11,18 +11,18 @@ static BOOL RCTViewControllerBasedStatusBarAppearance() static BOOL value; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - value = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"] boolValue]; + value = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"] ?: @YES boolValue]; }); - + return value; } - (void)setStyle:(UIStatusBarStyle)statusBarStyle animated:(BOOL)animated { RCT_EXPORT(); - + dispatch_async(dispatch_get_main_queue(), ^{ - + if (RCTViewControllerBasedStatusBarAppearance()) { RCTLogError(@"RCTStatusBarManager module requires that the \ UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); @@ -36,9 +36,9 @@ static BOOL RCTViewControllerBasedStatusBarAppearance() - (void)setHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation { RCT_EXPORT(); - + dispatch_async(dispatch_get_main_queue(), ^{ - + if (RCTViewControllerBasedStatusBarAppearance()) { RCTLogError(@"RCTStatusBarManager module requires that the \ UIViewControllerBasedStatusBarAppearance key in the Info.plist is set to NO"); @@ -49,7 +49,7 @@ static BOOL RCTViewControllerBasedStatusBarAppearance() }); } -+ (NSDictionary *)constantsToExport +- (NSDictionary *)constantsToExport { return @{ @"Style": @{ diff --git a/ReactKit/Modules/RCTTiming.m b/ReactKit/Modules/RCTTiming.m index e833f3d5f..c1d2ceeb5 100644 --- a/ReactKit/Modules/RCTTiming.m +++ b/ReactKit/Modules/RCTTiming.m @@ -51,35 +51,35 @@ @implementation RCTTiming { RCTSparseArray *_timers; - RCTBridge *_bridge; id _updateTimer; } +@synthesize bridge = _bridge; + + (NSArray *)JSMethods { return @[@"RCTJSTimers.callTimers"]; } -- (instancetype)initWithBridge:(RCTBridge *)bridge +- (instancetype)init { if ((self = [super init])) { - _bridge = bridge; + _timers = [[RCTSparseArray alloc] init]; - [self startTimers]; - + for (NSString *name in @[UIApplicationWillResignActiveNotification, UIApplicationDidEnterBackgroundNotification, UIApplicationWillTerminateNotification]) { - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(stopTimers) name:name object:nil]; } - + for (NSString *name in @[UIApplicationDidBecomeActiveNotification, UIApplicationWillEnterForegroundNotification]) { - + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startTimers) name:name @@ -114,14 +114,14 @@ - (void)startTimers { RCTAssertMainThread(); - - if (![self isValid] || _updateTimer != nil) { + + if (![self isValid] || _updateTimer != nil || _timers.count == 0) { return; } _updateTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)]; if (_updateTimer) { - [_updateTimer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + [_updateTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; } else { RCTLogWarn(@"Failed to create a display link (probably on buildbot) - using an NSTimer for AppEngine instead."); _updateTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60) @@ -135,7 +135,7 @@ - (void)update { RCTAssertMainThread(); - + NSMutableArray *timersToCall = [[NSMutableArray alloc] init]; for (RCTTimer *timer in _timers.allObjects) { if ([timer updateFoundNeedsJSUpdate]) { @@ -145,7 +145,7 @@ _timers[timer.callbackID] = nil; } } - + // call timers that need to be called if ([timersToCall count] > 0) { [_bridge enqueueJSCall:@"RCTJSTimers.callTimers" args:@[timersToCall]]; @@ -166,6 +166,12 @@ { RCT_EXPORT(); + if (jsDuration == 0 && repeats == NO) { + // For super fast, one-off timers, just enqueue them immediately rather than waiting a frame. + [_bridge enqueueJSCall:@"RCTJSTimers.callTimers" args:@[@[callbackID]]]; + return; + } + NSTimeInterval interval = jsDuration / 1000; NSTimeInterval jsCreationTimeSinceUnixEpoch = jsSchedulingTime / 1000; NSTimeInterval currentTimeSinceUnixEpoch = [[NSDate date] timeIntervalSince1970]; @@ -185,6 +191,7 @@ repeats:repeats]; dispatch_async(dispatch_get_main_queue(), ^{ _timers[callbackID] = timer; + [self startTimers]; }); } @@ -195,6 +202,9 @@ if (timerID) { dispatch_async(dispatch_get_main_queue(), ^{ _timers[timerID] = nil; + if (_timers.count == 0) { + [self stopTimers]; + } }); } else { RCTLogWarn(@"Called deleteTimer: with a nil timerID"); diff --git a/ReactKit/Modules/RCTUIManager.h b/ReactKit/Modules/RCTUIManager.h index 87e91118a..701c37f93 100644 --- a/ReactKit/Modules/RCTUIManager.h +++ b/ReactKit/Modules/RCTUIManager.h @@ -2,13 +2,18 @@ #import +#import "RCTBridge.h" #import "RCTBridgeModule.h" #import "RCTInvalidating.h" +#import "RCTViewManager.h" @class RCTRootView; @protocol RCTScrollableProtocol; +/** + * The RCTUIManager is the module responsible for updating the view hierarchy. + */ @interface RCTUIManager : NSObject @property (nonatomic, weak) id mainScrollView; @@ -19,8 +24,33 @@ */ @property (nonatomic, readwrite, weak) id nativeMainScrollDelegate; +/** + * Register a root view with the RCTUIManager. Theoretically, a single manager + * can support multiple root views, however this feature is not currently exposed + * and may eventually be removed. + */ - (void)registerRootView:(RCTRootView *)rootView; +/** + * Schedule a block to be executed on the UI thread. Useful if you need to execute + * view logic after all currently queued view updates have completed. + */ +- (void)addUIBlock:(RCTViewManagerUIBlock)block; + +/** + * The view that is currently first responder, according to the JS context. + */ + (UIView *)JSResponder; @end + +/** + * This category makes the current RCTUIManager instance available via the + * RCTBridge, which is useful for RCTBridgeModules or RCTViewManagers that + * need to access the RCTUIManager. + */ +@interface RCTBridge (RCTUIManager) + +@property (nonatomic, readonly) RCTUIManager *uiManager; + +@end diff --git a/ReactKit/Modules/RCTUIManager.m b/ReactKit/Modules/RCTUIManager.m index 3b1b56432..93090e2c1 100644 --- a/ReactKit/Modules/RCTUIManager.m +++ b/ReactKit/Modules/RCTUIManager.m @@ -2,24 +2,25 @@ #import "RCTUIManager.h" -#import #import +#import + #import "Layout.h" #import "RCTAnimationType.h" #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTConvert.h" -#import "RCTRootView.h" #import "RCTLog.h" #import "RCTNavigator.h" +#import "RCTRootView.h" #import "RCTScrollableProtocol.h" #import "RCTShadowView.h" #import "RCTSparseArray.h" #import "RCTUtils.h" #import "RCTView.h" -#import "RCTViewNodeProtocol.h" #import "RCTViewManager.h" +#import "RCTViewNodeProtocol.h" #import "UIView+ReactKit.h" typedef void (^react_view_node_block_t)(id); @@ -32,47 +33,6 @@ static void RCTTraverseViewNodes(id view, react_view_node_b } } -static NSDictionary *RCTViewModuleClasses(void) -{ - static NSMutableDictionary *modules; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - modules = [NSMutableDictionary dictionary]; - - unsigned int classCount; - Class *classes = objc_copyClassList(&classCount); - for (unsigned int i = 0; i < classCount; i++) { - - Class cls = classes[i]; - - if (!class_getSuperclass(cls)) { - // Class has no superclass - it's probably something weird - continue; - } - - if (![cls isSubclassOfClass:[RCTViewManager class]]) { - // Not a view module - continue; - } - - // Get module name - NSString *moduleName = [cls moduleName]; - - // Check module name is unique - id existingClass = modules[moduleName]; - RCTCAssert(existingClass == Nil, @"Attempted to register view module class %@ " - "for the name '%@', but name was already registered by class %@", cls, moduleName, existingClass); - - // Add to module list - modules[moduleName] = cls; - } - - free(classes); - }); - - return modules; -} - @interface RCTAnimation : NSObject @property (nonatomic, readonly) NSTimeInterval duration; @@ -115,8 +75,9 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t _property = [RCTConvert NSString:config[@"property"]]; // TODO: this should be provided in ms, not seconds - _duration = [RCTConvert NSTimeInterval:config[@"duration"]] ?: duration; - _delay = [RCTConvert NSTimeInterval:config[@"delay"]]; + // (this will require changing all call sites to ms as well) + _duration = [RCTConvert NSTimeInterval:config[@"duration"]] * 1000.0 ?: duration; + _delay = [RCTConvert NSTimeInterval:config[@"delay"]] * 1000.0; _animationType = [RCTConvert RCTAnimationType:config[@"type"]]; if (_animationType == RCTAnimationTypeSpring) { _springDamping = [RCTConvert CGFloat:config[@"springDamping"]]; @@ -176,7 +137,8 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t if ((self = [super init])) { // TODO: this should be provided in ms, not seconds - NSTimeInterval duration = [RCTConvert NSTimeInterval:config[@"duration"]]; + // (this will require changing all call sites to ms as well) + NSTimeInterval duration = [RCTConvert NSTimeInterval:config[@"duration"]] * 1000.0; _createAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"create"]]; _updateAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"update"]]; @@ -190,48 +152,70 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t @implementation RCTUIManager { + dispatch_queue_t _shadowQueue; + // Root views are only mutated on the shadow queue NSMutableSet *_rootViewTags; NSMutableArray *_pendingUIBlocks; NSLock *_pendingUIBlocksLock; - + // Animation RCTLayoutAnimation *_nextLayoutAnimation; // RCT thread only RCTLayoutAnimation *_layoutAnimation; // Main thread only - // Keyed by moduleName + // Keyed by viewName NSMutableDictionary *_defaultShadowViews; // RCT thread only NSMutableDictionary *_defaultViews; // Main thread only NSDictionary *_viewManagers; - + // Keyed by React tag RCTSparseArray *_viewManagerRegistry; // RCT thread only RCTSparseArray *_shadowViewRegistry; // RCT thread only RCTSparseArray *_viewRegistry; // Main thread only - - __weak RCTBridge *_bridge; } -- (instancetype)initWithBridge:(RCTBridge *)bridge +@synthesize bridge =_bridge; + +/** + * This function derives the view name automatically + * from the module name. + */ +static NSString *RCTViewNameForModuleName(NSString *moduleName) +{ + NSString *name = moduleName; + if ([name hasSuffix:@"Manager"]) { + name = [name substringToIndex:name.length - @"Manager".length]; + } + return name; +} + +/** + * This private constructor should only be called when creating + * isolated UIImanager instances for testing. Normal initialization + * is via -init:, which is called automatically by the bridge. + */ +- (instancetype)initWithShadowQueue:(dispatch_queue_t)shadowQueue +{ + if ((self = [self init])) { + _shadowQueue = shadowQueue; + _viewManagers = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (instancetype)init { if ((self = [super init])) { - - _bridge = bridge; + _pendingUIBlocksLock = [[NSLock alloc] init]; - - // Instantiate view managers - NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; - [RCTViewModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, Class moduleClass, BOOL *stop) { - viewManagers[moduleName] = [[moduleClass alloc] initWithEventDispatcher:_bridge.eventDispatcher]; - }]; - _viewManagers = viewManagers; + _defaultShadowViews = [[NSMutableDictionary alloc] init]; _defaultViews = [[NSMutableDictionary alloc] init]; - + _viewManagerRegistry = [[RCTSparseArray alloc] init]; _shadowViewRegistry = [[RCTSparseArray alloc] init]; _viewRegistry = [[RCTSparseArray alloc] init]; - + // Internal resources _pendingUIBlocks = [[NSMutableArray alloc] init]; _rootViewTags = [[NSMutableSet alloc] init]; @@ -239,16 +223,35 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t return self; } -- (instancetype)init -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - - (void)dealloc { RCTAssert(!self.valid, @"must call -invalidate before -dealloc"); } +- (void)setBridge:(RCTBridge *)bridge +{ + if (_bridge) { + + // Clear previous bridge data + [self invalidate]; + } + if (bridge) { + + _bridge = bridge; + _shadowQueue = _bridge.shadowQueue; + + // Get view managers from bridge + NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; + [_bridge.modules enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, RCTViewManager *manager, BOOL *stop) { + if ([manager isKindOfClass:[RCTViewManager class]]) { + viewManagers[RCTViewNameForModuleName(moduleName)] = manager; + } + }]; + + _viewManagers = [viewManagers copy]; + } +} + - (BOOL)isValid { return _viewRegistry != nil; @@ -260,6 +263,7 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t _viewRegistry = nil; _shadowViewRegistry = nil; + _bridge = nil; [_pendingUIBlocksLock lock]; _pendingUIBlocks = nil; @@ -269,22 +273,22 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t - (void)registerRootView:(RCTRootView *)rootView; { RCTAssertMainThread(); - + NSNumber *reactTag = rootView.reactTag; UIView *existingView = _viewRegistry[reactTag]; RCTCAssert(existingView == nil || existingView == rootView, @"Expect all root views to have unique tag. Added %@ twice", reactTag); - + // Register view _viewRegistry[reactTag] = rootView; CGRect frame = rootView.frame; - + // Register manager (TODO: should we do this, or leave it nil?) - _viewManagerRegistry[reactTag] = _viewManagers[[RCTViewManager moduleName]]; - + _viewManagerRegistry[reactTag] = _viewManagers[@"View"]; + // Register shadow view - dispatch_async(_bridge.shadowQueue, ^{ - + dispatch_async(_shadowQueue, ^{ + RCTShadowView *shadowView = [[RCTShadowView alloc] init]; shadowView.reactTag = reactTag; shadowView.frame = frame; @@ -315,7 +319,7 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t - (void)addUIBlock:(RCTViewManagerUIBlock)block { RCTAssert(![NSThread isMainThread], @"This method should only be called on the shadow thread"); - + __weak RCTUIManager *weakViewManager = self; __weak RCTSparseArray *weakViewRegistry = _viewRegistry; dispatch_block_t outerBlock = ^{ @@ -333,6 +337,8 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)rootShadowView { + RCTAssert(![NSThread isMainThread], @"This should never be executed on main thread."); + NSMutableSet *viewsWithNewFrames = [NSMutableSet setWithCapacity:1]; // This is nuanced. In the JS thread, we create a new update buffer @@ -340,7 +346,8 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t // these structures in the UI-thread block. `NSMutableArray` is not thread // safe so we rely on the fact that we never mutate it after it's passed to // the main thread. - [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}]; + [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames + parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}]; // Parallel arrays NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; @@ -354,12 +361,19 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t [areNew addObject:@(shadowView.isNewView)]; [parentsAreNew addObject:@(shadowView.superview.isNewView)]; } - + for (RCTShadowView *shadowView in viewsWithNewFrames) { // We have to do this after we build the parentsAreNew array. shadowView.newView = NO; } + NSMutableArray *updateBlocks = [[NSMutableArray alloc] init]; + for (RCTShadowView *shadowView in viewsWithNewFrames) { + RCTViewManager *manager = _viewManagerRegistry[shadowView.reactTag]; + RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView]; + if (block) [updateBlocks addObject:block]; + } + // Perform layout (possibly animated) NSNumber *rootViewTag = rootShadowView.reactTag; return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { @@ -373,15 +387,16 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t // Convert the frame so it works with anchorPoint = center. CGPoint position = {CGRectGetMidX(frame), CGRectGetMidY(frame)}; CGRect bounds = {0, 0, frame.size}; - + // Avoid crashes due to nan coords if (isnan(position.x) || isnan(position.y) || isnan(bounds.origin.x) || isnan(bounds.origin.y) || isnan(bounds.size.width) || isnan(bounds.size.height)) { - RCTLogError(@"Invalid layout for (%zd)%@. position: %@. bounds: %@", [view reactTag], self, NSStringFromCGPoint(position), NSStringFromCGRect(bounds)); + RCTLogError(@"Invalid layout for (%@)%@. position: %@. bounds: %@", + [view reactTag], self, NSStringFromCGPoint(position), NSStringFromCGRect(bounds)); continue; } - + void (^completion)(BOOL finished) = ^(BOOL finished) { if (self->_layoutAnimation.callback) { self->_layoutAnimation.callback(@[@(finished)]); @@ -395,14 +410,20 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t [updateAnimation performAnimations:^{ view.layer.position = position; view.layer.bounds = bounds; + for (RCTViewManagerUIBlock block in updateBlocks) { + block(self, _viewRegistry); + } } withCompletionBlock:completion]; } else { view.layer.position = position; view.layer.bounds = bounds; + for (RCTViewManagerUIBlock block in updateBlocks) { + block(self, _viewRegistry); + } completion(YES); } - - // Animate view creations + + // Animate view creation BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; RCTAnimation *createAnimation = _layoutAnimation.createAnimation; if (shouldAnimateCreation && createAnimation) { @@ -417,12 +438,22 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t } else if ([createAnimation.property isEqual:@"opacity"]) { view.layer.opacity = 1.0; } else { - RCTLogError(@"Unsupported layout animation createConfig property %@", createAnimation.property); + RCTLogError(@"Unsupported layout animation createConfig property %@", + createAnimation.property); + } + for (RCTViewManagerUIBlock block in updateBlocks) { + block(self, _viewRegistry); } } withCompletionBlock:nil]; } } + /** + * Enumerate all active (attached to a parent) views and call + * reactBridgeDidFinishTransaction on them if they implement it. + * TODO: this is quite inefficient. If this was handled via the + * ViewManager instead, it could be done more efficiently. + */ RCTRootView *rootView = _viewRegistry[rootViewTag]; RCTTraverseViewNodes(rootView, ^(id view) { if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) { @@ -436,7 +467,7 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t { NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1]; [topView collectUpdatedProperties:applierBlocks parentProperties:@{}]; - + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { for (RCTApplierBlock block in applierBlocks) { block(viewRegistry); @@ -454,7 +485,7 @@ UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimationType t id container = _viewRegistry[containerID]; RCTAssert(container != nil, @"container view (for ID %@) not found", containerID); - + NSUInteger subviewsCount = [[container reactSubviews] count]; NSMutableArray *indices = [[NSMutableArray alloc] initWithCapacity:subviewsCount]; for (NSInteger childIndex = 0; childIndex < subviewsCount; childIndex++) { @@ -616,11 +647,11 @@ static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, { // TODO: cache respondsToSelector tests if ([manager respondsToSelector:setter]) { - + if (value == [NSNull null]) { value = nil; } - + ((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView); return YES; } @@ -631,9 +662,9 @@ static void RCTSetViewProps(NSDictionary *props, UIView *view, UIView *defaultView, RCTViewManager *manager) { [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { - + SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forView:withDefaultView:", key]); - + // For regular views we don't attempt to set properties // unless the view property has been explicitly exported. RCTCallPropertySetter(setter, obj, view, defaultView, manager); @@ -644,13 +675,13 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView RCTShadowView *defaultView, RCTViewManager *manager) { [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { - + SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forShadowView:withDefaultView:", key]); - + // For shadow views we call any custom setter methods by default, // but if none is specified, we attempt to set property anyway. if (!RCTCallPropertySetter(setter, obj, shadowView, defaultView, manager)) { - + if (obj == [NSNull null]) { // Copy property from default view to current // Note: not just doing `[defaultView valueForKey:key]`, the @@ -661,20 +692,20 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView } } }]; - + // Update layout [shadowView updateShadowViewLayout]; } - (void)createAndRegisterViewWithReactTag:(NSNumber *)reactTag - moduleName:(NSString *)moduleName + viewName:(NSString *)viewName props:(NSDictionary *)props { RCT_EXPORT(createView); - RCTViewManager *manager = _viewManagers[moduleName]; + RCTViewManager *manager = _viewManagers[viewName]; if (manager == nil) { - RCTLogWarn(@"No manager class found for view with module name \"%@\"", moduleName); + RCTLogWarn(@"No manager class found for view with module name \"%@\"", viewName); manager = [[RCTViewManager alloc] init]; } @@ -682,57 +713,57 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView _viewManagerRegistry[reactTag] = manager; // Generate default view, used for resetting default props - if (!_defaultShadowViews[moduleName]) { - _defaultShadowViews[moduleName] = [manager shadowView]; + if (!_defaultShadowViews[viewName]) { + _defaultShadowViews[viewName] = [manager shadowView]; } - + RCTShadowView *shadowView = [manager shadowView]; - shadowView.moduleName = moduleName; + shadowView.viewName = viewName; shadowView.reactTag = reactTag; - RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], manager); + RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], manager); _shadowViewRegistry[shadowView.reactTag] = shadowView; - + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ RCTCAssertMainThread(); // Generate default view, used for resetting default props - if (!uiManager->_defaultViews[moduleName]) { + if (!uiManager->_defaultViews[viewName]) { // Note the default is setup after the props are read for the first time ever // for this className - this is ok because we only use the default for restoring // defaults, which never happens on first creation. - uiManager->_defaultViews[moduleName] = [manager view]; + uiManager->_defaultViews[viewName] = [manager view]; } - + UIView *view = [manager view]; if (view) { - + // Set required properties view.reactTag = reactTag; view.multipleTouchEnabled = YES; view.userInteractionEnabled = YES; // required for touch handling view.layer.allowsGroupOpacity = YES; // required for touch handling - + // Set custom properties - RCTSetViewProps(props, view, uiManager->_defaultViews[moduleName], manager); + RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], manager); } viewRegistry[view.reactTag] = view; }]; } -// TODO: remove moduleName param as it isn't needed -- (void)updateView:(NSNumber *)reactTag moduleName:(__unused NSString *)_ props:(NSDictionary *)props +// TODO: remove viewName param as it isn't needed +- (void)updateView:(NSNumber *)reactTag viewName:(__unused NSString *)_ props:(NSDictionary *)props { RCT_EXPORT(); RCTViewManager *viewManager = _viewManagerRegistry[reactTag]; - NSString *moduleName = [[viewManager class] moduleName]; - + NSString *viewName = RCTViewNameForModuleName([[viewManager class] moduleName]); + RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; - RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], viewManager); - + RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], viewManager); + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *view = uiManager->_viewRegistry[reactTag]; - RCTSetViewProps(props, view, uiManager->_defaultViews[moduleName], viewManager); + RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], viewManager); }]; } @@ -760,16 +791,15 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView - (void)batchDidComplete { - // First copy the previous blocks into a temporary variable, then reset the - // pending blocks to a new array. This guards against mutation while - // processing the pending blocks in another thread. + // 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 (RCTViewManager *manager in _viewManagers.allValues) { RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; if (uiBlock) { [self addUIBlock:uiBlock]; } } - + // Set up next layout animation if (_nextLayoutAnimation) { RCTLayoutAnimation *layoutAnimation = _nextLayoutAnimation; @@ -777,14 +807,14 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView uiManager->_layoutAnimation = layoutAnimation; }]; } - + // Perform layout for (NSNumber *reactTag in _rootViewTags) { RCTShadowView *rootView = _shadowViewRegistry[reactTag]; [self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]]; [self _amendPendingUIBlocksWithStylePropagationUpdateForRootView:rootView]; } - + // Clear layout animations if (_nextLayoutAnimation) { [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { @@ -792,12 +822,16 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }]; _nextLayoutAnimation = nil; } - + + // First copy the previous blocks into a temporary variable, then reset the + // pending blocks to a new array. This guards against mutation while + // processing the pending blocks in another thread. [_pendingUIBlocksLock lock]; NSArray *previousPendingUIBlocks = _pendingUIBlocks; _pendingUIBlocks = [[NSMutableArray alloc] init]; [_pendingUIBlocksLock unlock]; - + + // Execute the previously queued UI blocks dispatch_async(dispatch_get_main_queue(), ^{ for (dispatch_block_t block in previousPendingUIBlocks) { block(); @@ -828,7 +862,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView } RCTCAssert([rootView isReactRootView], @"React view not inside RCTRootView"); - + // By convention, all coordinates, whether they be touch coordinates, or // measurement coordinates are with respect to the root view. CGPoint pagePoint = [view.superview convertPoint:frame.origin toView:rootView]; @@ -844,37 +878,6 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }]; } -- (void)requestSchedulingJavaScriptNavigation:(NSNumber *)reactTag - errorCallback:(RCTResponseSenderBlock)errorCallback - callback:(RCTResponseSenderBlock)callback -{ - RCT_EXPORT(); - - if (!callback || !errorCallback) { - RCTLogError(@"Callback not provided for navigation scheduling."); - return; - } - [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ - if (reactTag) { - //TODO: This is nasty - why is RCTNavigator hard-coded? - id rkObject = viewRegistry[reactTag]; - if ([rkObject isKindOfClass:[RCTNavigator class]]) { - RCTNavigator *navigator = (RCTNavigator *)rkObject; - BOOL wasAcquired = [navigator requestSchedulingJavaScriptNavigation]; - callback(@[@(wasAcquired)]); - } else { - NSString *msg = - [NSString stringWithFormat: @"Cannot set lock: Tag %@ is not an RCTNavigator", reactTag]; - errorCallback(@[RCTAPIErrorObject(msg)]); - } - } else { - NSString *msg = [NSString stringWithFormat: @"Tag not specified for requestSchedulingJavaScriptNavigation"]; - errorCallback(@[RCTAPIErrorObject(msg)]); - } - }]; -} - - /** * TODO: This could be modified to accept any `RCTViewNodeProtocol`, if * appropriate changes were made to that protocol to support `superview` @@ -884,22 +887,20 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView + (void)measureLayoutOnNodes:(RCTShadowView *)view ancestor:(RCTShadowView *)ancestor errorCallback:(RCTResponseSenderBlock)errorCallback - callback:(RCTResponseSenderBlock)callback + callback:(__unused RCTResponseSenderBlock)callback { if (!view) { - NSString *msg = [NSString stringWithFormat: @"Attempting to measure view that does not exist %@", view]; - errorCallback(@[RCTAPIErrorObject(msg)]); + RCTLogError(@"Attempting to measure view that does not exist"); return; } if (!ancestor) { - NSString *msg = [NSString stringWithFormat: @"Attempting to measure relative to ancestor that does not exist %@", ancestor]; - errorCallback(@[RCTAPIErrorObject(msg)]); + RCTLogError(@"Attempting to measure relative to ancestor that does not exist"); return; } CGRect result = [RCTShadowView measureLayout:view relativeTo:ancestor]; if (CGRectIsNull(result)) { - NSString *msg = [NSString stringWithFormat: @"view %@ is not an decendant of %@", view, ancestor]; - errorCallback(@[RCTAPIErrorObject(msg)]); + RCTLogError(@"view %@ (tag #%@) is not a decendant of %@ (tag #%@)", + view, view.reactTag, ancestor, ancestor.reactTag); return; } CGFloat leftOffset = result.origin.x; @@ -907,7 +908,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView CGFloat width = result.size.width; CGFloat height = result.size.height; if (isnan(leftOffset) || isnan(topOffset) || isnan(width) || isnan(height)) { - errorCallback(@[RCTAPIErrorObject(@"Attempted to measure layout but offset or dimensions were NaN")]); + RCTLogError(@"Attempted to measure layout but offset or dimensions were NaN"); return; } callback(@[@(topOffset), @(leftOffset), @(width), @(height)]); @@ -965,20 +966,18 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; if (!shadowView) { - NSString *msg = [NSString stringWithFormat: @"Attempting to measure view that does not exist %@", shadowView]; - errorCallback(@[RCTAPIErrorObject(msg)]); + RCTLogError(@"Attempting to measure view that does not exist (tag #%@)", reactTag); return; } NSArray *childShadowViews = [shadowView reactSubviews]; NSMutableArray *results = [[NSMutableArray alloc] initWithCapacity:[childShadowViews count]]; CGRect layoutRect = [RCTConvert CGRect:rect]; - for (int ii = 0; ii < [childShadowViews count]; ii++) { - RCTShadowView *childShadowView = [childShadowViews objectAtIndex:ii]; + [childShadowViews enumerateObjectsUsingBlock:^(RCTShadowView *childShadowView, NSUInteger idx, BOOL *stop) { CGRect childLayout = [RCTShadowView measureLayout:childShadowView relativeTo:shadowView]; if (CGRectIsNull(childLayout)) { - NSString *msg = [NSString stringWithFormat: @"view %@ is not a decendant of %@", childShadowView, shadowView]; - errorCallback(@[RCTAPIErrorObject(msg)]); + RCTLogError(@"View %@ (tag #%@) is not a decendant of %@ (tag #%@)", + childShadowView, childShadowView.reactTag, shadowView, shadowView.reactTag); return; } @@ -992,7 +991,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView topOffset <= layoutRect.origin.y + layoutRect.size.height && topOffset + height >= layoutRect.origin.y) { // This view is within the layout rect - NSDictionary *result = @{@"index": @(ii), + NSDictionary *result = @{@"index": @(idx), @"left": @(leftOffset), @"top": @(topOffset), @"width": @(width), @@ -1000,7 +999,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView [results addObject:result]; } - } + }]; callback(@[results]); } @@ -1058,25 +1057,6 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }]; } -- (void)getScrollViewContentSize:(NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback failCallback:(RCTResponseSenderBlock)failCallback -{ - RCT_EXPORT(); - - [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - UIView *view = viewRegistry[reactTag]; - if (!view) { - NSString *error = [[NSString alloc] initWithFormat:@"cannot find view with tag %@", reactTag]; - RCTLogError(@"%@", error); - failCallback(@[@{@"error": error}]); - return; - } - - CGSize size = ((id)view).contentSize; - NSDictionary *dict = @{@"width" : @(size.width), @"height" : @(size.height)}; - callback(@[dict]); - }]; -} - /** * JS sets what *it* considers to be the responder. Later, scroll views can use * this in order to determine if scrolling is appropriate. @@ -1088,7 +1068,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { _jsResponder = viewRegistry[reactTag]; if (!_jsResponder) { - RCTLogMustFix(@"Invalid view set to be the JS responder - tag %zd", reactTag); + RCTLogError(@"Invalid view set to be the JS responder - tag %zd", reactTag); } }]; } @@ -1102,14 +1082,18 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }]; } -+ (NSDictionary *)allBubblingEventTypesConfigs +// TODO: these event types should be distributed among the modules +// that declare them. Also, events should be registerable by any class +// that can call event handlers, not just UIViewManagers. This code +// also seems highly redundant - every event has the same properties. +- (NSDictionary *)customBubblingEventTypes { NSMutableDictionary *customBubblingEventTypesConfigs = [@{ // Bubble dispatched events @"topTap": @{ @"phasedRegistrationNames": @{ - @"bubbled": @"notActuallyTapDontUseMe", - @"captured": @"notActuallyTapCaptureDontUseMe" + @"bubbled": @"onPress", + @"captured": @"onPressCapture" } }, @"topVisibleCellsChange": @{ @@ -1192,11 +1176,12 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }, } mutableCopy]; - [RCTViewModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { + [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { if (RCTClassOverridesClassMethod(cls, @selector(customBubblingEventTypes))) { NSDictionary *eventTypes = [cls customBubblingEventTypes]; for (NSString *eventName in eventTypes) { - RCTCAssert(!customBubblingEventTypesConfigs[eventName], @"Event '%@' registered multiple times.", eventName); + RCTCAssert(!customBubblingEventTypesConfigs[eventName], + @"Event '%@' registered multiple times.", eventName); } [customBubblingEventTypesConfigs addEntriesFromDictionary:eventTypes]; } @@ -1205,7 +1190,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView return customBubblingEventTypesConfigs; } -+ (NSDictionary *)allDirectEventTypesConfigs +- (NSDictionary *)customDirectEventTypes { NSMutableDictionary *customDirectEventTypes = [@{ @"topScrollBeginDrag": @{ @@ -1243,7 +1228,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }, } mutableCopy]; - [RCTViewModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { + [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { if (RCTClassOverridesClassMethod(cls, @selector(customDirectEventTypes))) { NSDictionary *eventTypes = [cls customDirectEventTypes]; for (NSString *eventName in eventTypes) { @@ -1256,11 +1241,11 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView return customDirectEventTypes; } -+ (NSDictionary *)constantsToExport +- (NSDictionary *)constantsToExport { NSMutableDictionary *allJSConstants = [@{ - @"customBubblingEventTypes": [self allBubblingEventTypesConfigs], - @"customDirectEventTypes": [self allDirectEventTypesConfigs], + @"customBubblingEventTypes": [self customBubblingEventTypes], + @"customDirectEventTypes": [self customDirectEventTypes], @"NSTextAlignment": @{ @"Left": @(NSTextAlignmentLeft), @"Center": @(NSTextAlignmentCenter), @@ -1280,17 +1265,25 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView @"StyleConstants": @{ @"PointerEventsValues": @{ @"none": @(RCTPointerEventsNone), - @"boxNone": @(RCTPointerEventsBoxNone), - @"boxOnly": @(RCTPointerEventsBoxOnly), - @"unspecified": @(RCTPointerEventsUnspecified), + @"box-none": @(RCTPointerEventsBoxNone), + @"box-only": @(RCTPointerEventsBoxOnly), + @"auto": @(RCTPointerEventsUnspecified), }, }, @"UIText": @{ @"AutocapitalizationType": @{ - @"AllCharacters": @(UITextAutocapitalizationTypeAllCharacters), - @"Sentences": @(UITextAutocapitalizationTypeSentences), - @"Words": @(UITextAutocapitalizationTypeWords), - @"None": @(UITextAutocapitalizationTypeNone), + @"characters": @(UITextAutocapitalizationTypeAllCharacters), + @"sentences": @(UITextAutocapitalizationTypeSentences), + @"words": @(UITextAutocapitalizationTypeWords), + @"none": @(UITextAutocapitalizationTypeNone), + }, + }, + @"UITextField": @{ + @"clearButtonMode": @{ + @"never": @(UITextFieldViewModeNever), + @"while-editing": @(UITextFieldViewModeWhileEditing), + @"unless-editing": @(UITextFieldViewModeUnlessEditing), + @"always": @(UITextFieldViewModeAlways), }, }, @"UIView": @{ @@ -1312,15 +1305,15 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView }, } mutableCopy]; - [RCTViewModuleClasses() enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { + [_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, Class cls, BOOL *stop) { // TODO: should these be inherited? NSDictionary *constants = RCTClassOverridesClassMethod(cls, @selector(constantsToExport)) ? [cls constantsToExport] : nil; if ([constants count]) { - NSMutableDictionary *namespace = [NSMutableDictionary dictionaryWithDictionary:allJSConstants[name]]; - RCTAssert(namespace[@"Constants"] == nil , @"Cannot redefine Constants in namespace: %@", name); + NSMutableDictionary *constantsNamespace = [NSMutableDictionary dictionaryWithDictionary:allJSConstants[name]]; + RCTAssert(constantsNamespace[@"Constants"] == nil , @"Cannot redefine Constants in namespace: %@", name); // add an additional 'Constants' namespace for each class - namespace[@"Constants"] = constants; - allJSConstants[name] = [namespace copy]; + constantsNamespace[@"Constants"] = constants; + allJSConstants[name] = [constantsNamespace copy]; } }]; @@ -1334,14 +1327,46 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView RCT_EXPORT(); if (_nextLayoutAnimation) { - RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimation, config); + RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", + _nextLayoutAnimation, config); } if (config[@"delete"] != nil) { - RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config); + RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config); } _nextLayoutAnimation = [[RCTLayoutAnimation alloc] initWithDictionary:config callback:callback]; } +- (void)startOrResetInteractionTiming +{ + RCT_EXPORT(); + + NSSet *rootViewTags = [_rootViewTags copy]; + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + for (NSNumber *reactTag in rootViewTags) { + RCTRootView *rootView = viewRegistry[reactTag]; + [rootView startOrResetInteractionTiming]; + } + }]; +} + +- (void)endAndResetInteractionTiming:(RCTResponseSenderBlock)onSuccess + onError:(RCTResponseSenderBlock)onError +{ + RCT_EXPORT(); + + NSSet *rootViewTags = [_rootViewTags copy]; + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + NSMutableDictionary *timingData = [[NSMutableDictionary alloc] init]; + for (NSNumber *reactTag in rootViewTags) { + RCTRootView *rootView = viewRegistry[reactTag]; + if (rootView) { + timingData[reactTag.stringValue] = [rootView endAndResetInteractionTiming]; + } + } + onSuccess(@[ timingData ]); + }]; +} + static UIView *_jsResponder; + (UIView *)JSResponder @@ -1350,3 +1375,12 @@ static UIView *_jsResponder; } @end + +@implementation RCTBridge (RCTUIManager) + +- (RCTUIManager *)uiManager +{ + return self.modules[NSStringFromClass([RCTUIManager class])]; +} + +@end diff --git a/ReactKit/ReactKit.podspec b/ReactKit/ReactKit.podspec new file mode 100644 index 000000000..3e1a8a6e2 --- /dev/null +++ b/ReactKit/ReactKit.podspec @@ -0,0 +1,16 @@ +Pod::Spec.new do |spec| + spec.name = 'ReactKit' + spec.version = '0.0.1' + spec.summary = 'An implementation of React that targets UIKit for iOS' + spec.description = <<-DESC + Our first React Native implementation is ReactKit, targeting iOS. We are also working on an Android implementation which we will release later. ReactKit apps are built using the React JS framework, and render directly to native UIKit elements using a fully asynchronous architecture. There is no browser and no HTML. We have picked what we think is the best set of features from these and other technologies to build what we hope to become the best product development framework available, with an emphasis on iteration speed, developer delight, continuity of technology, and absolutely beautiful and fast products with no compromises in quality or capability. + DESC + spec.homepage = 'https://facebook.github.io/react-native/' + spec.license = { :type => 'BSD' } + spec.author = 'Facebook' + spec.platform = :ios, '7.0' + spec.requires_arc = true + spec.source_files = '**/*.{h,m,c}' + spec.public_header_files = "**/*.h" + #spec.library = "ReactKit" +end diff --git a/ReactKit/ReactKit.xcodeproj/project.pbxproj b/ReactKit/ReactKit.xcodeproj/project.pbxproj index e342d22a3..7cd218891 100644 --- a/ReactKit/ReactKit.xcodeproj/project.pbxproj +++ b/ReactKit/ReactKit.xcodeproj/project.pbxproj @@ -7,25 +7,22 @@ objects = { /* Begin PBXBuildFile section */ - 1302F0FD1A78550100EBEF02 /* RCTStaticImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1302F0FA1A78550100EBEF02 /* RCTStaticImage.m */; }; - 1302F0FE1A78550100EBEF02 /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1302F0FC1A78550100EBEF02 /* RCTStaticImageManager.m */; }; + 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */ = {isa = PBXBuildFile; fileRef = 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */; }; 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */; }; - 134FCB371A6D4ED700051CC8 /* RCTRawTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137029581A6C197000575408 /* RCTRawTextManager.m */; }; 134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */; }; 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3C1A6E7F0800051CC8 /* RCTWebViewExecutor.m */; }; - 137029491A698FF000575408 /* RCTNetworkImageViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137029401A698FF000575408 /* RCTNetworkImageViewManager.m */; }; - 137029501A6990A100575408 /* RCTNetworkImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1370294F1A6990A100575408 /* RCTNetworkImageView.m */; }; - 137029531A69923600575408 /* RCTImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 137029521A69923600575408 /* RCTImageDownloader.m */; }; 13723B501A82FD3C00F88898 /* RCTStatusBarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */; }; + 1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7091AB030C200659ED6 /* RCTAppState.m */; }; + 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E01AA5CF210034F82E /* RCTTabBar.m */; }; + 137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E21AA5CF210034F82E /* RCTTabBarItem.m */; }; + 137327E91AA5CF210034F82E /* RCTTabBarItemManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E41AA5CF210034F82E /* RCTTabBarItemManager.m */; }; + 137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E61AA5CF210034F82E /* RCTTabBarManager.m */; }; 13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */; }; 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FE81A69327A00A75B9A /* RCTAlertManager.m */; }; 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FEA1A69327A00A75B9A /* RCTExceptionsManager.m */; }; 13B07FF21A69327A00A75B9A /* RCTTiming.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FEE1A69327A00A75B9A /* RCTTiming.m */; }; 13B080051A6947C200A75B9A /* RCTScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FF71A6947C200A75B9A /* RCTScrollView.m */; }; 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FF91A6947C200A75B9A /* RCTScrollViewManager.m */; }; - 13B080071A6947C200A75B9A /* RCTShadowRawText.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FFB1A6947C200A75B9A /* RCTShadowRawText.m */; }; - 13B080081A6947C200A75B9A /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FFD1A6947C200A75B9A /* RCTShadowText.m */; }; - 13B0800B1A6947C200A75B9A /* RCTTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080031A6947C200A75B9A /* RCTTextManager.m */; }; 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B0800D1A69489C00A75B9A /* RCTNavigator.m */; }; 13B0801B1A69489C00A75B9A /* RCTNavigatorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B0800F1A69489C00A75B9A /* RCTNavigatorManager.m */; }; 13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080111A69489C00A75B9A /* RCTNavItem.m */; }; @@ -34,16 +31,26 @@ 13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */; }; 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */; }; 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */; }; - 13B080291A694C4900A75B9A /* RCTDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B080281A694C4900A75B9A /* RCTDataManager.m */; }; + 13C156051AB1A2840079392D /* RCTWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156021AB1A2840079392D /* RCTWebView.m */; }; + 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13C156041AB1A2840079392D /* RCTWebViewManager.m */; }; 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067491A70F434002CDEE1 /* RCTUIManager.m */; }; 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */; }; 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067501A70F44B002CDEE1 /* RCTView.m */; }; 13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */; }; + 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE21AAC4AE100FC20F4 /* RCTMap.m */; }; + 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */; }; + 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F362081AABD06A001CE568 /* RCTSwitch.m */; }; + 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */; }; + 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */; }; + 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A131AAE854800E7D092 /* RCTPicker.m */; }; + 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A151AAE854800E7D092 /* RCTPickerManager.m */; }; + 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */; }; + 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */; }; 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = 830A229D1A66C68A008503DA /* RCTRootView.m */; }; 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 830BA4541A8E3BDA00D53203 /* RCTCache.m */; }; 832348161A77A5AA00B55238 /* Layout.c in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FC71A68125100A75B9A /* Layout.c */; }; - 835DD1321A7FDFB600D561F7 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 835DD1311A7FDFB600D561F7 /* RCTText.m */; }; + 83C911101AAE6521001323A3 /* RCTAnimationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 83C9110F1AAE6521001323A3 /* RCTAnimationManager.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 */; }; @@ -68,29 +75,31 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1302F0F91A78550100EBEF02 /* RCTStaticImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImage.h; sourceTree = ""; }; - 1302F0FA1A78550100EBEF02 /* RCTStaticImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImage.m; sourceTree = ""; }; - 1302F0FB1A78550100EBEF02 /* RCTStaticImageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStaticImageManager.h; sourceTree = ""; }; - 1302F0FC1A78550100EBEF02 /* RCTStaticImageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStaticImageManager.m; sourceTree = ""; }; + 000E6CE91AB0E97F000CDF4D /* RCTSourceCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSourceCode.h; sourceTree = ""; }; + 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSourceCode.m; sourceTree = ""; }; + 13442BF21AA90E0B0037E5B0 /* RCTAnimationType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationType.h; sourceTree = ""; }; + 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; }; + 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewControllerProtocol.h; sourceTree = ""; }; 134FCB391A6E7F0800051CC8 /* RCTContextExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTContextExecutor.h; sourceTree = ""; }; 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutor.m; sourceTree = ""; }; 134FCB3B1A6E7F0800051CC8 /* RCTWebViewExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebViewExecutor.h; sourceTree = ""; }; 134FCB3C1A6E7F0800051CC8 /* RCTWebViewExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebViewExecutor.m; sourceTree = ""; }; - 1370293F1A698FF000575408 /* RCTNetworkImageViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageViewManager.h; sourceTree = ""; }; - 137029401A698FF000575408 /* RCTNetworkImageViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageViewManager.m; sourceTree = ""; }; - 1370294E1A6990A100575408 /* RCTNetworkImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNetworkImageView.h; sourceTree = ""; }; - 1370294F1A6990A100575408 /* RCTNetworkImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNetworkImageView.m; sourceTree = ""; }; - 137029511A69923600575408 /* RCTImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageDownloader.h; sourceTree = ""; }; - 137029521A69923600575408 /* RCTImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageDownloader.m; sourceTree = ""; }; - 137029571A6C197000575408 /* RCTRawTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRawTextManager.h; sourceTree = ""; }; - 137029581A6C197000575408 /* RCTRawTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRawTextManager.m; sourceTree = ""; }; 13723B4E1A82FD3C00F88898 /* RCTStatusBarManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStatusBarManager.h; sourceTree = ""; }; 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStatusBarManager.m; sourceTree = ""; }; + 1372B7081AB030C200659ED6 /* RCTAppState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAppState.h; sourceTree = ""; }; + 1372B7091AB030C200659ED6 /* RCTAppState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAppState.m; sourceTree = ""; }; + 137327DF1AA5CF210034F82E /* RCTTabBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTabBar.h; sourceTree = ""; }; + 137327E01AA5CF210034F82E /* RCTTabBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTabBar.m; sourceTree = ""; }; + 137327E11AA5CF210034F82E /* RCTTabBarItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTabBarItem.h; sourceTree = ""; }; + 137327E21AA5CF210034F82E /* RCTTabBarItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTabBarItem.m; sourceTree = ""; }; + 137327E31AA5CF210034F82E /* RCTTabBarItemManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTabBarItemManager.h; sourceTree = ""; }; + 137327E41AA5CF210034F82E /* RCTTabBarItemManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTabBarItemManager.m; sourceTree = ""; }; + 137327E51AA5CF210034F82E /* RCTTabBarManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTabBarManager.h; sourceTree = ""; }; + 137327E61AA5CF210034F82E /* RCTTabBarManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTabBarManager.m; sourceTree = ""; }; 13A1F71C1A75392D00D3D453 /* RCTKeyCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTKeyCommands.h; sourceTree = ""; }; 13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTKeyCommands.m; sourceTree = ""; }; 13B07FC71A68125100A75B9A /* Layout.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = Layout.c; sourceTree = ""; }; 13B07FC81A68125100A75B9A /* Layout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Layout.h; sourceTree = ""; }; - 13B07FCD1A683B5F00A75B9A /* RCTScrollableProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTScrollableProtocol.h; sourceTree = ""; }; 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAlertManager.h; sourceTree = ""; }; 13B07FE81A69327A00A75B9A /* RCTAlertManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAlertManager.m; sourceTree = ""; }; 13B07FE91A69327A00A75B9A /* RCTExceptionsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTExceptionsManager.h; sourceTree = ""; }; @@ -101,12 +110,6 @@ 13B07FF71A6947C200A75B9A /* RCTScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTScrollView.m; sourceTree = ""; }; 13B07FF81A6947C200A75B9A /* RCTScrollViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTScrollViewManager.h; sourceTree = ""; }; 13B07FF91A6947C200A75B9A /* RCTScrollViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTScrollViewManager.m; sourceTree = ""; }; - 13B07FFA1A6947C200A75B9A /* RCTShadowRawText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowRawText.h; sourceTree = ""; }; - 13B07FFB1A6947C200A75B9A /* RCTShadowRawText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowRawText.m; sourceTree = ""; }; - 13B07FFC1A6947C200A75B9A /* RCTShadowText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowText.h; sourceTree = ""; }; - 13B07FFD1A6947C200A75B9A /* RCTShadowText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowText.m; sourceTree = ""; }; - 13B080021A6947C200A75B9A /* RCTTextManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTextManager.h; sourceTree = ""; }; - 13B080031A6947C200A75B9A /* RCTTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextManager.m; sourceTree = ""; }; 13B0800C1A69489C00A75B9A /* RCTNavigator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNavigator.h; sourceTree = ""; }; 13B0800D1A69489C00A75B9A /* RCTNavigator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTNavigator.m; sourceTree = ""; }; 13B0800E1A69489C00A75B9A /* RCTNavigatorManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTNavigatorManager.h; sourceTree = ""; }; @@ -123,9 +126,13 @@ 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIActivityIndicatorViewManager.m; sourceTree = ""; }; 13B080231A694A8400A75B9A /* RCTWrapperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWrapperViewController.h; sourceTree = ""; }; 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWrapperViewController.m; sourceTree = ""; }; - 13B080271A694C4900A75B9A /* RCTDataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDataManager.h; sourceTree = ""; }; - 13B080281A694C4900A75B9A /* RCTDataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDataManager.m; sourceTree = ""; }; - 13DB9D681A8CC58200429C20 /* RCTAnimationType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationType.h; sourceTree = ""; }; + 13C156011AB1A2840079392D /* RCTWebView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebView.h; sourceTree = ""; }; + 13C156021AB1A2840079392D /* RCTWebView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebView.m; sourceTree = ""; }; + 13C156031AB1A2840079392D /* RCTWebViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebViewManager.h; sourceTree = ""; }; + 13C156041AB1A2840079392D /* RCTWebViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebViewManager.m; sourceTree = ""; }; + 13C325261AA63B6A0048765F /* RCTAutoInsetsProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAutoInsetsProtocol.h; sourceTree = ""; }; + 13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTScrollableProtocol.h; sourceTree = ""; }; + 13C325281AA63B6A0048765F /* RCTViewNodeProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTViewNodeProtocol.h; sourceTree = ""; }; 13E067481A70F434002CDEE1 /* RCTUIManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUIManager.h; sourceTree = ""; }; 13E067491A70F434002CDEE1 /* RCTUIManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUIManager.m; sourceTree = ""; }; 13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowView.h; sourceTree = ""; }; @@ -136,17 +143,34 @@ 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = ""; }; 13E067531A70F44B002CDEE1 /* UIView+ReactKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+ReactKit.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+ReactKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+ReactKit.m"; sourceTree = ""; }; - 13ED13891A80C9D40050A8F9 /* RCTPointerEvents.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPointerEvents.h; sourceTree = ""; }; 13EFFCCF1A98E6FE002607DC /* RCTJSMethodRegistrar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJSMethodRegistrar.h; sourceTree = ""; }; + 14435CE11AAC4AE100FC20F4 /* RCTMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMap.h; sourceTree = ""; }; + 14435CE21AAC4AE100FC20F4 /* RCTMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMap.m; sourceTree = ""; }; + 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapManager.h; sourceTree = ""; }; + 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapManager.m; sourceTree = ""; }; + 14F362071AABD06A001CE568 /* RCTSwitch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSwitch.h; sourceTree = ""; }; + 14F362081AABD06A001CE568 /* RCTSwitch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSwitch.m; sourceTree = ""; }; + 14F362091AABD06A001CE568 /* RCTSwitchManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSwitchManager.h; sourceTree = ""; }; + 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSwitchManager.m; sourceTree = ""; }; + 14F484541AABFCE100FDF6B9 /* RCTSliderManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSliderManager.h; sourceTree = ""; }; + 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSliderManager.m; sourceTree = ""; }; + 58114A121AAE854800E7D092 /* RCTPicker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPicker.h; sourceTree = ""; }; + 58114A131AAE854800E7D092 /* RCTPicker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPicker.m; sourceTree = ""; }; + 58114A141AAE854800E7D092 /* RCTPickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPickerManager.h; sourceTree = ""; }; + 58114A151AAE854800E7D092 /* RCTPickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPickerManager.m; sourceTree = ""; }; + 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAsyncLocalStorage.m; sourceTree = ""; }; + 58114A4F1AAE93D500E7D092 /* RCTAsyncLocalStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAsyncLocalStorage.h; sourceTree = ""; }; + 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTDatePickerManager.m; sourceTree = ""; }; + 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTDatePickerManager.h; sourceTree = ""; }; 830213F31A654E0800B993E6 /* RCTBridgeModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTBridgeModule.h; sourceTree = ""; }; 830A229C1A66C68A008503DA /* RCTRootView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTRootView.h; sourceTree = ""; }; 830A229D1A66C68A008503DA /* RCTRootView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTRootView.m; sourceTree = ""; }; 830BA4531A8E3BDA00D53203 /* RCTCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCache.h; sourceTree = ""; }; 830BA4541A8E3BDA00D53203 /* RCTCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCache.m; sourceTree = ""; }; - 835DD1301A7FDFB600D561F7 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = ""; }; - 835DD1311A7FDFB600D561F7 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = ""; }; 83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSparseArray.h; sourceTree = ""; }; 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSparseArray.m; sourceTree = ""; }; + 83C9110E1AAE6521001323A3 /* RCTAnimationManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAnimationManager.h; sourceTree = ""; }; + 83C9110F1AAE6521001323A3 /* RCTAnimationManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAnimationManager.m; sourceTree = ""; }; 83CBBA2E1A601D0E00E9B192 /* libReactKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libReactKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; 83CBBA4A1A601E3B00E9B192 /* RCTAssert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAssert.h; sourceTree = ""; }; 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAssert.m; sourceTree = ""; }; @@ -159,8 +183,6 @@ 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 = ""; }; - 83CBBA611A601EB200E9B192 /* RCTAutoInsetsProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTAutoInsetsProtocol.h; sourceTree = ""; }; - 83CBBA621A601EB800E9B192 /* RCTViewNodeProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTViewNodeProtocol.h; sourceTree = ""; }; 83CBBA631A601ECA00E9B192 /* RCTJavaScriptExecutor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptExecutor.h; sourceTree = ""; }; 83CBBA651A601EF300E9B192 /* RCTEventDispatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTEventDispatcher.h; sourceTree = ""; }; 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTEventDispatcher.m; sourceTree = ""; }; @@ -204,12 +226,18 @@ 13B07FE01A69315300A75B9A /* Modules */ = { isa = PBXGroup; children = ( + 1372B7081AB030C200659ED6 /* RCTAppState.h */, + 1372B7091AB030C200659ED6 /* RCTAppState.m */, 13B07FE71A69327A00A75B9A /* RCTAlertManager.h */, 13B07FE81A69327A00A75B9A /* RCTAlertManager.m */, - 13B080271A694C4900A75B9A /* RCTDataManager.h */, - 13B080281A694C4900A75B9A /* RCTDataManager.m */, + 83C9110E1AAE6521001323A3 /* RCTAnimationManager.h */, + 83C9110F1AAE6521001323A3 /* RCTAnimationManager.m */, + 58114A4F1AAE93D500E7D092 /* RCTAsyncLocalStorage.h */, + 58114A4E1AAE93D500E7D092 /* RCTAsyncLocalStorage.m */, 13B07FE91A69327A00A75B9A /* RCTExceptionsManager.h */, 13B07FEA1A69327A00A75B9A /* RCTExceptionsManager.m */, + 000E6CE91AB0E97F000CDF4D /* RCTSourceCode.h */, + 000E6CEA1AB0E980000CDF4D /* RCTSourceCode.m */, 13723B4E1A82FD3C00F88898 /* RCTStatusBarManager.h */, 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */, 13B07FED1A69327A00A75B9A /* RCTTiming.h */, @@ -223,6 +251,14 @@ 13B07FF31A6947C200A75B9A /* Views */ = { isa = PBXGroup; children = ( + 13442BF21AA90E0B0037E5B0 /* RCTAnimationType.h */, + 13C325261AA63B6A0048765F /* RCTAutoInsetsProtocol.h */, + 58C571C01AA56C1900CDF9C8 /* RCTDatePickerManager.h */, + 58C571BF1AA56C1900CDF9C8 /* RCTDatePickerManager.m */, + 14435CE11AAC4AE100FC20F4 /* RCTMap.h */, + 14435CE21AAC4AE100FC20F4 /* RCTMap.m */, + 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */, + 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */, 13B0800C1A69489C00A75B9A /* RCTNavigator.h */, 13B0800D1A69489C00A75B9A /* RCTNavigator.m */, 13B0800E1A69489C00A75B9A /* RCTNavigatorManager.h */, @@ -231,40 +267,48 @@ 13B080111A69489C00A75B9A /* RCTNavItem.m */, 13B080121A69489C00A75B9A /* RCTNavItemManager.h */, 13B080131A69489C00A75B9A /* RCTNavItemManager.m */, - 1370294E1A6990A100575408 /* RCTNetworkImageView.h */, - 1370294F1A6990A100575408 /* RCTNetworkImageView.m */, - 1370293F1A698FF000575408 /* RCTNetworkImageViewManager.h */, - 137029401A698FF000575408 /* RCTNetworkImageViewManager.m */, - 137029571A6C197000575408 /* RCTRawTextManager.h */, - 137029581A6C197000575408 /* RCTRawTextManager.m */, + 58114A121AAE854800E7D092 /* RCTPicker.h */, + 58114A131AAE854800E7D092 /* RCTPicker.m */, + 58114A141AAE854800E7D092 /* RCTPickerManager.h */, + 58114A151AAE854800E7D092 /* RCTPickerManager.m */, + 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */, 13B07FF61A6947C200A75B9A /* RCTScrollView.h */, 13B07FF71A6947C200A75B9A /* RCTScrollView.m */, 13B07FF81A6947C200A75B9A /* RCTScrollViewManager.h */, 13B07FF91A6947C200A75B9A /* RCTScrollViewManager.m */, - 13B07FFA1A6947C200A75B9A /* RCTShadowRawText.h */, - 13B07FFB1A6947C200A75B9A /* RCTShadowRawText.m */, - 13B07FFC1A6947C200A75B9A /* RCTShadowText.h */, - 13B07FFD1A6947C200A75B9A /* RCTShadowText.m */, + 13C325271AA63B6A0048765F /* RCTScrollableProtocol.h */, 13E0674B1A70F44B002CDEE1 /* RCTShadowView.h */, 13E0674C1A70F44B002CDEE1 /* RCTShadowView.m */, - 1302F0F91A78550100EBEF02 /* RCTStaticImage.h */, - 1302F0FA1A78550100EBEF02 /* RCTStaticImage.m */, - 1302F0FB1A78550100EBEF02 /* RCTStaticImageManager.h */, - 1302F0FC1A78550100EBEF02 /* RCTStaticImageManager.m */, - 835DD1301A7FDFB600D561F7 /* RCTText.h */, - 835DD1311A7FDFB600D561F7 /* RCTText.m */, + 14F484541AABFCE100FDF6B9 /* RCTSliderManager.h */, + 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */, + 14F362071AABD06A001CE568 /* RCTSwitch.h */, + 14F362081AABD06A001CE568 /* RCTSwitch.m */, + 14F362091AABD06A001CE568 /* RCTSwitchManager.h */, + 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */, + 137327DF1AA5CF210034F82E /* RCTTabBar.h */, + 137327E01AA5CF210034F82E /* RCTTabBar.m */, + 137327E11AA5CF210034F82E /* RCTTabBarItem.h */, + 137327E21AA5CF210034F82E /* RCTTabBarItem.m */, + 137327E31AA5CF210034F82E /* RCTTabBarItemManager.h */, + 137327E41AA5CF210034F82E /* RCTTabBarItemManager.m */, + 137327E51AA5CF210034F82E /* RCTTabBarManager.h */, + 137327E61AA5CF210034F82E /* RCTTabBarManager.m */, 13B080141A69489C00A75B9A /* RCTTextField.h */, 13B080151A69489C00A75B9A /* RCTTextField.m */, 13B080161A69489C00A75B9A /* RCTTextFieldManager.h */, 13B080171A69489C00A75B9A /* RCTTextFieldManager.m */, - 13B080021A6947C200A75B9A /* RCTTextManager.h */, - 13B080031A6947C200A75B9A /* RCTTextManager.m */, 13B080181A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.h */, 13B080191A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m */, 13E0674F1A70F44B002CDEE1 /* RCTView.h */, 13E067501A70F44B002CDEE1 /* RCTView.m */, + 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */, 13E0674D1A70F44B002CDEE1 /* RCTViewManager.h */, 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */, + 13C325281AA63B6A0048765F /* RCTViewNodeProtocol.h */, + 13C156011AB1A2840079392D /* RCTWebView.h */, + 13C156021AB1A2840079392D /* RCTWebView.m */, + 13C156031AB1A2840079392D /* RCTWebViewManager.h */, + 13C156041AB1A2840079392D /* RCTWebViewManager.m */, 13B080231A694A8400A75B9A /* RCTWrapperViewController.h */, 13B080241A694A8400A75B9A /* RCTWrapperViewController.m */, 13E067531A70F44B002CDEE1 /* UIView+ReactKit.h */, @@ -304,10 +348,8 @@ 83CBBA491A601E3B00E9B192 /* Base */ = { isa = PBXGroup; children = ( - 13DB9D681A8CC58200429C20 /* RCTAnimationType.h */, 83CBBA4A1A601E3B00E9B192 /* RCTAssert.h */, 83CBBA4B1A601E3B00E9B192 /* RCTAssert.m */, - 83CBBA611A601EB200E9B192 /* RCTAutoInsetsProtocol.h */, 83CBBA5E1A601EAA00E9B192 /* RCTBridge.h */, 83CBBA5F1A601EAA00E9B192 /* RCTBridge.m */, 830213F31A654E0800B993E6 /* RCTBridgeModule.h */, @@ -317,8 +359,6 @@ 83CBBACB1A6023D300E9B192 /* RCTConvert.m */, 83CBBA651A601EF300E9B192 /* RCTEventDispatcher.h */, 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */, - 137029511A69923600575408 /* RCTImageDownloader.h */, - 137029521A69923600575408 /* RCTImageDownloader.m */, 83CBBA4C1A601E3B00E9B192 /* RCTInvalidating.h */, 83CBBA631A601ECA00E9B192 /* RCTJavaScriptExecutor.h */, 13EFFCCF1A98E6FE002607DC /* RCTJSMethodRegistrar.h */, @@ -326,19 +366,16 @@ 13A1F71D1A75392D00D3D453 /* RCTKeyCommands.m */, 83CBBA4D1A601E3B00E9B192 /* RCTLog.h */, 83CBBA4E1A601E3B00E9B192 /* RCTLog.m */, - 13ED13891A80C9D40050A8F9 /* RCTPointerEvents.h */, 83CBBA581A601E9000E9B192 /* RCTRedBox.h */, 83CBBA591A601E9000E9B192 /* RCTRedBox.m */, 830A229C1A66C68A008503DA /* RCTRootView.h */, 830A229D1A66C68A008503DA /* RCTRootView.m */, - 13B07FCD1A683B5F00A75B9A /* RCTScrollableProtocol.h */, 83BEE46C1A6D19BC00B5863B /* RCTSparseArray.h */, 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */, 83CBBA961A6020BB00E9B192 /* RCTTouchHandler.h */, 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */, 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */, 83CBBA501A601E3B00E9B192 /* RCTUtils.m */, - 83CBBA621A601EB800E9B192 /* RCTViewNodeProtocol.h */, ); path = Base; sourceTree = ""; @@ -418,50 +455,57 @@ buildActionMask = 2147483647; files = ( 13723B501A82FD3C00F88898 /* RCTStatusBarManager.m in Sources */, + 000E6CEB1AB0E980000CDF4D /* RCTSourceCode.m in Sources */, 13B0801E1A69489C00A75B9A /* RCTTextField.m in Sources */, 13B07FEF1A69327A00A75B9A /* RCTAlertManager.m in Sources */, 83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */, 830A229E1A66C68A008503DA /* RCTRootView.m in Sources */, - 1302F0FD1A78550100EBEF02 /* RCTStaticImage.m in Sources */, 13B07FF01A69327A00A75B9A /* RCTExceptionsManager.m in Sources */, 83CBBA5A1A601E9000E9B192 /* RCTRedBox.m in Sources */, 83CBBA511A601E3B00E9B192 /* RCTAssert.m in Sources */, + 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, 832348161A77A5AA00B55238 /* Layout.c in Sources */, + 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */, + 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */, 13B080201A69489C00A75B9A /* RCTUIActivityIndicatorViewManager.m in Sources */, 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */, + 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */, 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */, + 137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */, 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */, 13B080051A6947C200A75B9A /* RCTScrollView.m in Sources */, 13B07FF21A69327A00A75B9A /* RCTTiming.m in Sources */, + 1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */, 13B0801F1A69489C00A75B9A /* RCTTextFieldManager.m in Sources */, 134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */, 13E067591A70F44B002CDEE1 /* UIView+ReactKit.m in Sources */, - 835DD1321A7FDFB600D561F7 /* RCTText.m in Sources */, - 137029531A69923600575408 /* RCTImageDownloader.m in Sources */, + 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */, 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */, 83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */, - 13B080071A6947C200A75B9A /* RCTShadowRawText.m in Sources */, 13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */, - 137029501A6990A100575408 /* RCTNetworkImageView.m in Sources */, 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */, + 137327E91AA5CF210034F82E /* RCTTabBarItemManager.m in Sources */, 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */, - 137029491A698FF000575408 /* RCTNetworkImageViewManager.m in Sources */, 13A1F71E1A75392D00D3D453 /* RCTKeyCommands.m in Sources */, 83CBBA531A601E3B00E9B192 /* RCTUtils.m in Sources */, + 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */, + 83C911101AAE6521001323A3 /* RCTAnimationManager.m in Sources */, + 13C156051AB1A2840079392D /* RCTWebView.m in Sources */, 83CBBA601A601EAA00E9B192 /* RCTBridge.m in Sources */, - 13B080081A6947C200A75B9A /* RCTShadowText.m in Sources */, + 13C156061AB1A2840079392D /* RCTWebViewManager.m in Sources */, + 58114A161AAE854800E7D092 /* RCTPicker.m in Sources */, + 137327E81AA5CF210034F82E /* RCTTabBarItem.m in Sources */, 13E067551A70F44B002CDEE1 /* RCTShadowView.m in Sources */, + 58114A171AAE854800E7D092 /* RCTPickerManager.m in Sources */, 13B0801A1A69489C00A75B9A /* RCTNavigator.m in Sources */, 830BA4551A8E3BDA00D53203 /* RCTCache.m in Sources */, + 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */, + 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */, 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */, 13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */, 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */, - 134FCB371A6D4ED700051CC8 /* RCTRawTextManager.m in Sources */, - 13B0800B1A6947C200A75B9A /* RCTTextManager.m in Sources */, 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */, - 1302F0FE1A78550100EBEF02 /* RCTStaticImageManager.m in Sources */, 13B0801B1A69489C00A75B9A /* RCTNavigatorManager.m in Sources */, - 13B080291A694C4900A75B9A /* RCTDataManager.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -502,7 +546,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -537,7 +581,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.1; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; @@ -552,6 +596,10 @@ "$(inherited)", ); GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -562,6 +610,10 @@ isa = XCBuildConfiguration; buildSettings = { GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + ); OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/ReactKit/Base/RCTAnimationType.h b/ReactKit/Views/RCTAnimationType.h similarity index 100% rename from ReactKit/Base/RCTAnimationType.h rename to ReactKit/Views/RCTAnimationType.h diff --git a/ReactKit/Base/RCTAutoInsetsProtocol.h b/ReactKit/Views/RCTAutoInsetsProtocol.h similarity index 100% rename from ReactKit/Base/RCTAutoInsetsProtocol.h rename to ReactKit/Views/RCTAutoInsetsProtocol.h diff --git a/ReactKit/Views/RCTDatePickerManager.h b/ReactKit/Views/RCTDatePickerManager.h new file mode 100644 index 000000000..65459b80c --- /dev/null +++ b/ReactKit/Views/RCTDatePickerManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTViewManager.h" + +@interface RCTDatePickerManager : RCTViewManager + +@end diff --git a/ReactKit/Views/RCTDatePickerManager.m b/ReactKit/Views/RCTDatePickerManager.m new file mode 100644 index 000000000..39010214e --- /dev/null +++ b/ReactKit/Views/RCTDatePickerManager.m @@ -0,0 +1,51 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTDatePickerManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "UIView+ReactKit.h" + +@implementation RCTDatePickerManager + +- (UIView *)view +{ + UIDatePicker *picker = [[UIDatePicker alloc] init]; + [picker addTarget:self + action:@selector(onChange:) + forControlEvents:UIControlEventValueChanged]; + return picker; +} + +RCT_EXPORT_VIEW_PROPERTY(date) +RCT_EXPORT_VIEW_PROPERTY(minimumDate) +RCT_EXPORT_VIEW_PROPERTY(maximumDate) +RCT_EXPORT_VIEW_PROPERTY(minuteInterval) +RCT_REMAP_VIEW_PROPERTY(mode, datePickerMode) +RCT_REMAP_VIEW_PROPERTY(timeZoneOffsetInMinutes, timeZone) + +- (void)onChange:(UIDatePicker *)sender +{ + NSDictionary *event = @{ + @"target": sender.reactTag, + @"timestamp": @([sender.date timeIntervalSince1970] * 1000.0) + }; + [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; +} + +- (NSDictionary *)constantsToExport +{ + UIDatePicker *dp = [[UIDatePicker alloc] init]; + return @{ + @"ComponentHeight": @(CGRectGetHeight(dp.frame)), + @"ComponentWidth": @(CGRectGetWidth(dp.frame)), + @"DatePickerModes": @{ + @"time": @(UIDatePickerModeTime), + @"date": @(UIDatePickerModeDate), + @"datetime": @(UIDatePickerModeDateAndTime), + } + }; +} + +@end diff --git a/ReactKit/Views/RCTMap.h b/ReactKit/Views/RCTMap.h new file mode 100644 index 000000000..5ab56079b --- /dev/null +++ b/ReactKit/Views/RCTMap.h @@ -0,0 +1,24 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import +#import + +extern const CLLocationDegrees RCTMapDefaultSpan; +extern const NSTimeInterval RCTMapRegionChangeObserveInterval; +extern const CGFloat RCTMapZoomBoundBuffer; + +@class RCTEventDispatcher; + +@interface RCTMap: MKMapView + +@property (nonatomic, assign) BOOL followUserLocation; +@property (nonatomic, copy) NSDictionary *JSONRegion; +@property (nonatomic, assign) CGFloat minDelta; +@property (nonatomic, assign) CGFloat maxDelta; +@property (nonatomic, assign) UIEdgeInsets legalLabelInsets; +@property (nonatomic, strong) NSTimer *regionChangeObserveTimer; + +@end + +#define FLUSH_NAN(value) \ + (isnan(value) ? 0 : value) diff --git a/ReactKit/Views/RCTMap.m b/ReactKit/Views/RCTMap.m new file mode 100644 index 000000000..e0f4d9228 --- /dev/null +++ b/ReactKit/Views/RCTMap.m @@ -0,0 +1,122 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTMap.h" + +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "RCTLog.h" +#import "RCTUtils.h" + +const CLLocationDegrees RCTMapDefaultSpan = 0.005; +const NSTimeInterval RCTMapRegionChangeObserveInterval = 0.1; +const CGFloat RCTMapZoomBoundBuffer = 0.01; + +@interface RCTMap() + +@property (nonatomic, strong) UIView *legalLabel; +@property (nonatomic, strong) CLLocationManager *locationManager; + +@end + +@implementation RCTMap + +- (instancetype)init +{ + self = [super init]; + if (self) { + // Find Apple link label + for (UIView *subview in self.subviews) { + if ([NSStringFromClass(subview.class) isEqualToString:@"MKAttributionLabel"]) { + // This check is super hacky, but the whole premise of moving around Apple's internal subviews is super hacky + _legalLabel = subview; + break; + } + } + } + return self; +} + +- (void)dealloc +{ + [self.regionChangeObserveTimer invalidate]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + // Force resize subviews - only the layer is resized by default + CGRect mapFrame = self.frame; + self.frame = CGRectZero; + self.frame = mapFrame; + + if (_legalLabel) { + dispatch_async(dispatch_get_main_queue(), ^{ + CGRect frame = _legalLabel.frame; + if (_legalLabelInsets.left) { + frame.origin.x = _legalLabelInsets.left; + } else if (_legalLabelInsets.right) { + frame.origin.x = mapFrame.size.width - _legalLabelInsets.right - frame.size.width; + } + if (_legalLabelInsets.top) { + frame.origin.y = _legalLabelInsets.top; + } else if (_legalLabelInsets.bottom) { + frame.origin.y = mapFrame.size.height - _legalLabelInsets.bottom - frame.size.height; + } + _legalLabel.frame = frame; + }); + } +} + +#pragma mark Accessors + +- (void)setShowsUserLocation:(BOOL)showsUserLocation +{ + if (self.showsUserLocation != showsUserLocation) { + if (showsUserLocation && !_locationManager) { + _locationManager = [[CLLocationManager alloc] init]; + if ([_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) { + [_locationManager requestWhenInUseAuthorization]; + } + } + [super setShowsUserLocation:showsUserLocation]; + + // If it needs to show user location, force map view centered + // on user's current location on user location updates + self.followUserLocation = showsUserLocation; + } +} + +- (void)setJSONRegion:(NSDictionary *)region +{ + if (region) { + MKCoordinateRegion coordinateRegion = self.region; + coordinateRegion.center.latitude = [RCTConvert double:region[@"latitude"]]; + coordinateRegion.center.longitude = [RCTConvert double:region[@"longitude"]]; + + if ([region[@"latitudeDelta"] isKindOfClass:[NSNumber class]]) { + coordinateRegion.span.latitudeDelta = [region[@"latitudeDelta"] doubleValue]; + } + if ([region[@"longitudeDelta"] isKindOfClass:[NSNumber class]]) { + coordinateRegion.span.longitudeDelta = [region[@"longitudeDelta"] doubleValue]; + } + + [self setRegion:coordinateRegion animated:YES]; + } +} + +- (NSDictionary *)JSONRegion +{ + MKCoordinateRegion region = self.region; + if (!CLLocationCoordinate2DIsValid(region.center)) { + return nil; + } + return @{ + @"latitude": @(FLUSH_NAN(region.center.latitude)), + @"longitude": @(FLUSH_NAN(region.center.longitude)), + @"latitudeDelta": @(FLUSH_NAN(region.span.latitudeDelta)), + @"longitudeDelta": @(FLUSH_NAN(region.span.longitudeDelta)), + }; +} + +@end diff --git a/ReactKit/Views/RCTMapManager.h b/ReactKit/Views/RCTMapManager.h new file mode 100644 index 000000000..93b7049ca --- /dev/null +++ b/ReactKit/Views/RCTMapManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTViewManager.h" + +@interface RCTMapManager : RCTViewManager + +@end diff --git a/ReactKit/Views/RCTMapManager.m b/ReactKit/Views/RCTMapManager.m new file mode 100644 index 000000000..4e4cec164 --- /dev/null +++ b/ReactKit/Views/RCTMapManager.m @@ -0,0 +1,119 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTMapManager.h" + +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" +#import "RCTMap.h" +#import "UIView+ReactKit.h" + +@interface RCTMapManager() + +@end + +@implementation RCTMapManager + +- (UIView *)view +{ + RCTMap *map = [[RCTMap alloc] init]; + map.delegate = self; + return map; +} + +RCT_EXPORT_VIEW_PROPERTY(showsUserLocation); +RCT_EXPORT_VIEW_PROPERTY(zoomEnabled); +RCT_EXPORT_VIEW_PROPERTY(rotateEnabled); +RCT_EXPORT_VIEW_PROPERTY(pitchEnabled); +RCT_EXPORT_VIEW_PROPERTY(scrollEnabled); +RCT_EXPORT_VIEW_PROPERTY(maxDelta); +RCT_EXPORT_VIEW_PROPERTY(minDelta); +RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets); +RCT_REMAP_VIEW_PROPERTY(region, JSONRegion) + +#pragma mark MKMapViewDelegate + +- (void)mapView:(RCTMap *)mapView didUpdateUserLocation:(MKUserLocation *)location +{ + if (mapView.followUserLocation) { + MKCoordinateRegion region; + region.span.latitudeDelta = RCTMapDefaultSpan; + region.span.longitudeDelta = RCTMapDefaultSpan; + region.center = location.coordinate; + [mapView setRegion:region animated:YES]; + + // Move to user location only for the first time it loads up. + mapView.followUserLocation = NO; + } +} + +- (void)mapView:(RCTMap *)mapView regionWillChangeAnimated:(BOOL)animated +{ + [self _regionChanged:mapView]; + + mapView.regionChangeObserveTimer = [NSTimer timerWithTimeInterval:RCTMapRegionChangeObserveInterval + target:self + selector:@selector(_onTick:) + userInfo:@{ @"mapView": mapView } + repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:mapView.regionChangeObserveTimer forMode:NSRunLoopCommonModes]; +} + +- (void)mapView:(RCTMap *)mapView regionDidChangeAnimated:(BOOL)animated +{ + [mapView.regionChangeObserveTimer invalidate]; + mapView.regionChangeObserveTimer = nil; + + [self _regionChanged:mapView]; + [self _emitRegionChangeEvent:mapView continuous:NO]; +} + +#pragma mark Private + +- (void)_onTick:(NSTimer *)timer +{ + [self _regionChanged:timer.userInfo[@"mapView"]]; +} + +- (void)_regionChanged:(RCTMap *)mapView +{ + BOOL needZoom = NO; + CGFloat newLongitudeDelta = 0.0f; + MKCoordinateRegion region = mapView.region; + // On iOS 7, it's possible that we observe invalid locations during initialization of the map. + // Filter those out. + if (!CLLocationCoordinate2DIsValid(region.center)) { + return; + } + // Calculation on float is not 100% accurate. If user zoom to max/min and then move, it's likely the map will auto zoom to max/min from time to time. + // So let's try to make map zoom back to 99% max or 101% min so that there are some buffer that moving the map won't constantly hitting the max/min bound. + if (mapView.maxDelta > FLT_EPSILON && region.span.longitudeDelta > mapView.maxDelta) { + needZoom = YES; + newLongitudeDelta = mapView.maxDelta * (1 - RCTMapZoomBoundBuffer); + } else if (mapView.minDelta > FLT_EPSILON && region.span.longitudeDelta < mapView.minDelta) { + needZoom = YES; + newLongitudeDelta = mapView.minDelta * (1 + RCTMapZoomBoundBuffer); + } + if (needZoom) { + region.span.latitudeDelta = region.span.latitudeDelta / region.span.longitudeDelta * newLongitudeDelta; + region.span.longitudeDelta = newLongitudeDelta; + mapView.region = region; + } + + // Continously observe region changes + [self _emitRegionChangeEvent:mapView continuous:YES]; +} + +- (void)_emitRegionChangeEvent:(RCTMap *)mapView continuous:(BOOL)continuous +{ + NSDictionary *region = mapView.JSONRegion; + if (region) { + NSDictionary *event = @{ + @"target": [mapView reactTag], + @"continuous": @(continuous), + @"region": mapView.JSONRegion, + }; + [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; + } +} + +@end diff --git a/ReactKit/Views/RCTNavigator.h b/ReactKit/Views/RCTNavigator.h index 5d928efa7..c9051c753 100644 --- a/ReactKit/Views/RCTNavigator.h +++ b/ReactKit/Views/RCTNavigator.h @@ -11,7 +11,7 @@ @property (nonatomic, strong) UIView *reactNavSuperviewLink; @property (nonatomic, assign) NSInteger requestedTopOfStack; -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; /** * Schedules a JavaScript navigation and prevents `UIKit` from navigating until diff --git a/ReactKit/Views/RCTNavigator.m b/ReactKit/Views/RCTNavigator.m index 74e1621c2..05306e809 100644 --- a/ReactKit/Views/RCTNavigator.m +++ b/ReactKit/Views/RCTNavigator.m @@ -41,7 +41,6 @@ NSInteger kNeverProgressed = -10000; @end - /** * In general, `RCTNavigator` examines `_currentViews` (which are React child * views), and compares them to `_navigationController.viewControllers` (which @@ -126,21 +125,6 @@ NSInteger kNeverProgressed = -10000; */ @implementation RCTNavigationController -- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - -- (instancetype)initWithNavigationBarClass:(Class)navigationBarClass toolbarClass:(Class)toolbarClass -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - -- (instancetype)initWithRootViewController:(UIViewController *)rootViewController -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - /** * @param callback Callback that is invoked when a "scroll" interaction begins * so that `RCTNavigator` can notify `JavaScript`. @@ -153,7 +137,6 @@ NSInteger kNeverProgressed = -10000; return self; } - /** * Invoked when either a navigation item has been popped off, or when a * swipe-back gesture has began. The swipe-back gesture doesn't respect the @@ -199,7 +182,6 @@ NSInteger kNeverProgressed = -10000; @end - @interface RCTNavigator() { RCTEventDispatcher *_eventDispatcher; @@ -219,7 +201,7 @@ NSInteger kNeverProgressed = -10000; * * - The run loop retains the displayLink. * - `displayLink` retains its target. - * - We use `reactWillDestroy` to remove the `RCTNavigator`'s reference to the + * - We use `invalidate` to remove the `RCTNavigator`'s reference to the * `displayLink` and remove the `displayLink` from the run loop. * * @@ -227,7 +209,7 @@ NSInteger kNeverProgressed = -10000; * -------------- * * - Even though we could implement the `displayLink` cleanup without the - * `reactWillDestroy` hook by adding and removing it from the run loop at the + * `invalidate` hook by adding and removing it from the run loop at the * right times (begin/end animation), we need to account for the possibility * that the view itself is destroyed mid-interaction. So we always keep it * added to the run loop, but start/stop it with interactions/animations. We @@ -275,11 +257,6 @@ NSInteger kNeverProgressed = -10000; @implementation RCTNavigator -- (instancetype)initWithFrame:(CGRect)frame -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - - (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { if ((self = [super initWithFrame:CGRectZero])) { @@ -308,7 +285,6 @@ NSInteger kNeverProgressed = -10000; [self addSubview:_navigationController.view]; [_navigationController.view addSubview:_dummyView]; } - return self; } @@ -363,8 +339,8 @@ NSInteger kNeverProgressed = -10000; (RCTWrapperViewController *)[context viewControllerForKey:UITransitionContextToViewControllerKey]; NSUInteger indexOfFrom = [_currentViews indexOfObject:fromController.navItem]; NSUInteger indexOfTo = [_currentViews indexOfObject:toController.navItem]; - CGFloat destination = indexOfFrom < indexOfTo ? 1.0f : -1.0f; - _dummyView.frame = CGRectMake(destination, 0.0f, 0.0f, 0.0f); + CGFloat destination = indexOfFrom < indexOfTo ? 1.0 : -1.0; + _dummyView.frame = (CGRect){{destination}}; _currentlyTransitioningFrom = indexOfFrom; _currentlyTransitioningTo = indexOfTo; if (indexOfFrom != indexOfTo) { @@ -375,7 +351,7 @@ NSInteger kNeverProgressed = -10000; [weakSelf freeLock]; _currentlyTransitioningFrom = 0; _currentlyTransitioningTo = 0; - _dummyView.frame = CGRectMake(0.0f, 0.0f, 0.0f, 0.0f); + _dummyView.frame = CGRectZero; _displayLink.paused = YES; // Reset the parallel position tracker }]; @@ -462,7 +438,7 @@ NSInteger kNeverProgressed = -10000; } /** - * Must be overridden because UIKit destroys the views superview link when used + * Must be overridden because UIKit removes the view's superview when used * as a navigator - it's considered outside the view hierarchy. */ - (UIView *)reactSuperview @@ -471,26 +447,12 @@ NSInteger kNeverProgressed = -10000; return self.superview ? self.superview : self.reactNavSuperviewLink; } -- (void)addControllerToClosestParent:(UIViewController *)controller -{ - if (!controller.parentViewController) { - id responder = [self.superview nextResponder]; - while (responder && ![responder isKindOfClass:[UIViewController class]]) { - responder = [responder nextResponder]; - } - if (responder) { - [responder addChildViewController:controller]; - [controller didMoveToParentViewController:responder]; - } - } -} - - (void)reactBridgeDidFinishTransaction { - // we can't hook up the VC hierarchy in 'init' because the subviews aren't hooked up yet, - // so we do it on demand here + // we can't hook up the VC hierarchy in 'init' because the subviews aren't + // hooked up yet, so we do it on demand here [self addControllerToClosestParent:_navigationController]; - + NSInteger viewControllerCount = _navigationController.viewControllers.count; // The "react count" is the count of views that are visible on the navigation // stack. There may be more beyond this - that aren't visible, and may be @@ -563,7 +525,6 @@ NSInteger kNeverProgressed = -10000; _previousRequestedTopOfStack = _requestedTopOfStack; } - // TODO: This will likely fail when performing multiple pushes/pops. We must // free the lock only after the *last* push/pop. - (void)wrapperViewController:(RCTWrapperViewController *)wrapperViewController @@ -574,7 +535,7 @@ didMoveToNavigationController:(UINavigationController *)navigationController // while a push/pop is in progress. return; } - + RCTAssert( (navigationController == nil || [_navigationController.viewControllers containsObject:wrapperViewController]), @"if navigation controller is not nil, it should container the wrapper view controller" diff --git a/ReactKit/Views/RCTNavigatorManager.m b/ReactKit/Views/RCTNavigatorManager.m index 973d4958d..85004471b 100644 --- a/ReactKit/Views/RCTNavigatorManager.m +++ b/ReactKit/Views/RCTNavigatorManager.m @@ -2,15 +2,17 @@ #import "RCTNavigatorManager.h" +#import "RCTBridge.h" #import "RCTConvert.h" #import "RCTNavigator.h" -#import "RCTShadowView.h" +#import "RCTSparseArray.h" +#import "RCTUIManager.h" @implementation RCTNavigatorManager - (UIView *)view { - return [[RCTNavigator alloc] initWithEventDispatcher:self.eventDispatcher]; + return [[RCTNavigator alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; } RCT_EXPORT_VIEW_PROPERTY(requestedTopOfStack) @@ -24,5 +26,22 @@ RCT_EXPORT_VIEW_PROPERTY(requestedTopOfStack) }; } -@end +// TODO: remove error callbacks +- (void)requestSchedulingJavaScriptNavigation:(NSNumber *)reactTag + errorCallback:(RCTResponseSenderBlock)errorCallback + callback:(__unused RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ + RCTNavigator *navigator = viewRegistry[reactTag]; + if ([navigator isKindOfClass:[RCTNavigator class]]) { + BOOL wasAcquired = [navigator requestSchedulingJavaScriptNavigation]; + callback(@[@(wasAcquired)]); + } else { + RCTLogError(@"Cannot set lock: %@ (tag #%@) is not an RCTNavigator", navigator, reactTag); + } + }]; +} + +@end diff --git a/ReactKit/Views/RCTPicker.h b/ReactKit/Views/RCTPicker.h new file mode 100644 index 000000000..8b5b3c864 --- /dev/null +++ b/ReactKit/Views/RCTPicker.h @@ -0,0 +1,14 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@class RCTEventDispatcher; + +@interface RCTPicker : UIPickerView + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, copy) NSArray *items; +@property (nonatomic, assign) NSInteger selectedIndex; + +@end diff --git a/ReactKit/Views/RCTPicker.m b/ReactKit/Views/RCTPicker.m new file mode 100644 index 000000000..1fcb33c1c --- /dev/null +++ b/ReactKit/Views/RCTPicker.m @@ -0,0 +1,91 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTPicker.h" + +#import "RCTConvert.h" +#import "RCTEventDispatcher.h" +#import "RCTUtils.h" +#import "UIView+ReactKit.h" + +const NSInteger UNINITIALIZED_INDEX = -1; + +@interface RCTPicker() +{ + RCTEventDispatcher *_eventDispatcher; + NSArray *_items; + NSInteger _selectedIndex; +} +@end + +@implementation RCTPicker + +- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if (self = [super initWithFrame:CGRectZero]) { + _eventDispatcher = eventDispatcher; + _selectedIndex = UNINITIALIZED_INDEX; + self.delegate = self; + } + return self; +} + +- (void)setItems:(NSArray *)items +{ + if (_items != items) { + _items = [items copy]; + [self setNeedsLayout]; + } +} + +- (void)setSelectedIndex:(NSInteger)selectedIndex +{ + if (_selectedIndex != selectedIndex) { + BOOL animated = _selectedIndex != UNINITIALIZED_INDEX; // Don't animate the initial value + _selectedIndex = selectedIndex; + dispatch_async(dispatch_get_main_queue(), ^{ + [self selectRow:selectedIndex inComponent:0 animated:animated]; + }); + } +} + +#pragma mark - UIPickerViewDataSource protocol + +- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView +{ + return 1; +} + +- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component +{ + return [_items count]; +} + +#pragma mark - UIPickerViewDelegate methods + +- (NSDictionary *)itemForRow:(NSInteger)row +{ + return (NSDictionary*)[_items objectAtIndex:row]; +} + +- (id)valueForRow:(NSInteger)row +{ + return [self itemForRow:row][@"value"]; +} + +- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component +{ + return [self itemForRow:row][@"label"]; +} + +- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component +{ + _selectedIndex = row; + NSDictionary *event = @{ + @"target": self.reactTag, + @"newIndex": @(row), + @"newValue": [self valueForRow:row] + }; + + [_eventDispatcher sendInputEventWithName:@"topChange" body:event]; +} +@end diff --git a/ReactKit/Views/RCTPickerManager.h b/ReactKit/Views/RCTPickerManager.h new file mode 100644 index 000000000..49cd74cb7 --- /dev/null +++ b/ReactKit/Views/RCTPickerManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTViewManager.h" + +@interface RCTPickerManager : RCTViewManager + +@end diff --git a/ReactKit/Views/RCTPickerManager.m b/ReactKit/Views/RCTPickerManager.m new file mode 100644 index 000000000..320aae0d6 --- /dev/null +++ b/ReactKit/Views/RCTPickerManager.m @@ -0,0 +1,28 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTPickerManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTPicker.h" + +@implementation RCTPickerManager + +- (UIView *)view +{ + return [[RCTPicker alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_EXPORT_VIEW_PROPERTY(items) +RCT_EXPORT_VIEW_PROPERTY(selectedIndex) + +- (NSDictionary *)constantsToExport +{ + RCTPicker *pv = [[RCTPicker alloc] init]; + return @{ + @"ComponentHeight": @(CGRectGetHeight(pv.frame)), + @"ComponentWidth": @(CGRectGetWidth(pv.frame)) + }; +} + +@end diff --git a/ReactKit/Base/RCTPointerEvents.h b/ReactKit/Views/RCTPointerEvents.h similarity index 100% rename from ReactKit/Base/RCTPointerEvents.h rename to ReactKit/Views/RCTPointerEvents.h diff --git a/ReactKit/Views/RCTScrollView.h b/ReactKit/Views/RCTScrollView.h index 82667b205..204ddf494 100644 --- a/ReactKit/Views/RCTScrollView.h +++ b/ReactKit/Views/RCTScrollView.h @@ -12,6 +12,8 @@ @interface RCTScrollView : RCTView +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + /** * If the `contentSize` is not provided, then the `contentSize` will * automatically be determined by the size of the `RKScrollView` subview. @@ -32,6 +34,4 @@ @property (nonatomic, assign) BOOL centerContent; @property (nonatomic, copy) NSArray *stickyHeaderIndices; -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; - @end diff --git a/ReactKit/Views/RCTScrollView.m b/ReactKit/Views/RCTScrollView.m index d017c7edc..fa6dc0f81 100644 --- a/ReactKit/Views/RCTScrollView.m +++ b/ReactKit/Views/RCTScrollView.m @@ -31,8 +31,7 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; - (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { + if ((self = [super initWithFrame:frame])) { [self.panGestureRecognizer addTarget:self action:@selector(handleCustomPan:)]; } return self; @@ -257,22 +256,17 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; @synthesize nativeMainScrollDelegate = _nativeMainScrollDelegate; -- (instancetype)initWithFrame:(CGRect)frame -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { if ((self = [super initWithFrame:CGRectZero])) { - + _eventDispatcher = eventDispatcher; _scrollView = [[RCTCustomScrollView alloc] initWithFrame:CGRectZero]; _scrollView.delegate = self; _scrollView.delaysContentTouches = NO; _automaticallyAdjustContentInsets = YES; _contentInset = UIEdgeInsetsZero; - + _throttleScrollCallbackMS = 0; _lastScrollDispatchTime = CACurrentMediaTime(); _cachedChildFrames = [[NSMutableArray alloc] init]; @@ -343,14 +337,20 @@ CGFloat const ZINDEX_STICKY_HEADER = 50; [RCTView autoAdjustInsetsForView:self withScrollView:_scrollView updateOffset:YES]; + + [self updateClippedSubviews]; } - (void)setContentInset:(UIEdgeInsets)contentInset { + CGPoint contentOffset = _scrollView.contentOffset; + _contentInset = contentInset; [RCTView autoAdjustInsetsForView:self withScrollView:_scrollView updateOffset:NO]; + + _scrollView.contentOffset = contentOffset; } - (void)scrollToOffset:(CGPoint)offset @@ -393,9 +393,11 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) - (void)scrollViewDidScroll:(UIScrollView *)scrollView { + [self updateClippedSubviews]; + NSTimeInterval now = CACurrentMediaTime(); NSTimeInterval throttleScrollCallbackSeconds = _throttleScrollCallbackMS / 1000.0; - + /** * TODO: this logic looks wrong, and it may be because it is. Currently, if _throttleScrollCallbackMS * is set to zero (the default), the "didScroll" event is only sent once per scroll, instead of repeatedly @@ -404,11 +406,11 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) */ if (_allowNextScrollNoMatterWhat || (_throttleScrollCallbackMS != 0 && throttleScrollCallbackSeconds < (now - _lastScrollDispatchTime))) { - + // Calculate changed frames NSMutableArray *updatedChildFrames = [[NSMutableArray alloc] init]; [[_contentView reactSubviews] enumerateObjectsUsingBlock:^(UIView *subview, NSUInteger idx, BOOL *stop) { - + // Check if new or changed CGRect newFrame = subview.frame; BOOL frameChanged = NO; @@ -419,7 +421,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) frameChanged = YES; _cachedChildFrames[idx] = [NSValue valueWithCGRect:newFrame]; } - + // Create JS frame object if (frameChanged) { [updatedChildFrames addObject: @{ @@ -430,9 +432,9 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) @"height": @(newFrame.size.height), }]; } - + }]; - + // If there are new frames, add them to event data NSDictionary *userData = nil; if (updatedChildFrames.count > 0) { @@ -572,26 +574,51 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) } } +// Note: setting several properties of UIScrollView has the effect of +// resetting its contentOffset to {0, 0}. To prevent this, we generate +// setters here that will record the contentOffset beforehand, and +// restore it after the property has been set. + +#define RCT_SET_AND_PRESERVE_OFFSET(setter, type) \ +- (void)setter:(type)value \ +{ \ + CGPoint contentOffset = _scrollView.contentOffset; \ + [_scrollView setter:value]; \ + _scrollView.contentOffset = contentOffset; \ +} + +RCT_SET_AND_PRESERVE_OFFSET(setAlwaysBounceHorizontal, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setAlwaysBounceVertical, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setBounces, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setBouncesZoom, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setCanCancelContentTouches, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setDecelerationRate, CGFloat) +RCT_SET_AND_PRESERVE_OFFSET(setDirectionalLockEnabled, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setKeyboardDismissMode, UIScrollViewKeyboardDismissMode) +RCT_SET_AND_PRESERVE_OFFSET(setMaximumZoomScale, CGFloat) +RCT_SET_AND_PRESERVE_OFFSET(setMinimumZoomScale, CGFloat) +RCT_SET_AND_PRESERVE_OFFSET(setPagingEnabled, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setScrollEnabled, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setScrollsToTop, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setShowsHorizontalScrollIndicator, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setShowsVerticalScrollIndicator, BOOL) +RCT_SET_AND_PRESERVE_OFFSET(setZoomScale, CGFloat); +RCT_SET_AND_PRESERVE_OFFSET(setScrollIndicatorInsets, UIEdgeInsets); + +#pragma mark - Forward methods and properties to underlying UIScrollView + - (BOOL)respondsToSelector:(SEL)aSelector { - if ([super respondsToSelector:aSelector]) { - return YES; - } - if ([NSStringFromSelector(aSelector) hasPrefix:@"set"]) { - return [_scrollView respondsToSelector:aSelector]; - } - return NO; + return [super respondsToSelector:aSelector] || [_scrollView respondsToSelector:aSelector]; } - (void)setValue:(id)value forUndefinedKey:(NSString *)key { - // Pipe unrecognized properties to scrollview [_scrollView setValue:value forKey:key]; } - (id)valueForUndefinedKey:(NSString *)key { - // Pipe unrecognized properties from scrollview return [_scrollView valueForKey:key]; } diff --git a/ReactKit/Views/RCTScrollViewManager.m b/ReactKit/Views/RCTScrollViewManager.m index 5100d1186..620cfcaf4 100644 --- a/ReactKit/Views/RCTScrollViewManager.m +++ b/ReactKit/Views/RCTScrollViewManager.m @@ -2,14 +2,17 @@ #import "RCTScrollViewManager.h" +#import "RCTBridge.h" #import "RCTConvert.h" #import "RCTScrollView.h" +#import "RCTSparseArray.h" +#import "RCTUIManager.h" @implementation RCTScrollViewManager - (UIView *)view { - return [[RCTScrollView alloc] initWithEventDispatcher:self.eventDispatcher]; + return [[RCTScrollView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; } RCT_EXPORT_VIEW_PROPERTY(alwaysBounceHorizontal) @@ -34,22 +37,42 @@ RCT_EXPORT_VIEW_PROPERTY(throttleScrollCallbackMS); RCT_EXPORT_VIEW_PROPERTY(zoomScale); RCT_EXPORT_VIEW_PROPERTY(contentInset); RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets); -RCT_EXPORT_VIEW_PROPERTY(contentOffset); +RCT_REMAP_VIEW_PROPERTY(contentOffset, scrollView.contentOffset); -+ (NSDictionary *)constantsToExport +- (NSDictionary *)constantsToExport { - return - @{ + return @{ @"DecelerationRate": @{ - @"Normal": @(UIScrollViewDecelerationRateNormal), - @"Fast": @(UIScrollViewDecelerationRateFast), + @"Normal": @(UIScrollViewDecelerationRateNormal), + @"Fast": @(UIScrollViewDecelerationRateFast), }, @"KeyboardDismissMode": @{ - @"None": @(UIScrollViewKeyboardDismissModeNone), - @"Interactive": @(UIScrollViewKeyboardDismissModeInteractive), - @"OnDrag": @(UIScrollViewKeyboardDismissModeOnDrag), + @"None": @(UIScrollViewKeyboardDismissModeNone), + @"Interactive": @(UIScrollViewKeyboardDismissModeInteractive), + @"OnDrag": @(UIScrollViewKeyboardDismissModeOnDrag), }, }; } +- (void)getContentSize:(NSNumber *)reactTag + callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + + UIView *view = viewRegistry[reactTag]; + if (!view) { + RCTLogError(@"Cannot find view with tag %@", reactTag); + return; + } + + CGSize size = ((RCTScrollView *)view).scrollView.contentSize; + callback(@[@{ + @"width" : @(size.width), + @"height" : @(size.height) + }]); + }]; +} + @end diff --git a/ReactKit/Base/RCTScrollableProtocol.h b/ReactKit/Views/RCTScrollableProtocol.h similarity index 82% rename from ReactKit/Base/RCTScrollableProtocol.h rename to ReactKit/Views/RCTScrollableProtocol.h index a0ae7b611..8232449f1 100644 --- a/ReactKit/Base/RCTScrollableProtocol.h +++ b/ReactKit/Views/RCTScrollableProtocol.h @@ -8,7 +8,7 @@ */ @protocol RCTScrollableProtocol -@property (nonatomic, readwrite, weak) NSObject *nativeMainScrollDelegate; +@property (nonatomic, weak) NSObject *nativeMainScrollDelegate; @property (nonatomic, readonly) CGSize contentSize; - (void)scrollToOffset:(CGPoint)offset; diff --git a/ReactKit/Views/RCTShadowView.h b/ReactKit/Views/RCTShadowView.h index ec2b8c353..3b961fac6 100644 --- a/ReactKit/Views/RCTShadowView.h +++ b/ReactKit/Views/RCTShadowView.h @@ -2,30 +2,16 @@ #import -#import "Layout.h" +#import "../Layout/Layout.h" + #import "RCTViewNodeProtocol.h" @class RCTSparseArray; -// TODO: amalgamate these enums? -typedef NS_ENUM(NSUInteger, RCTLayoutLifecycle) { - RCTLayoutLifecycleUninitialized = 0, - RCTLayoutLifecycleComputed, - RCTLayoutLifecycleDirtied, -}; - -// TODO: is this still needed? -typedef NS_ENUM(NSUInteger, RCTPropagationLifecycle) { - RCTPropagationLifecycleUninitialized = 0, - RCTPropagationLifecycleComputed, - RCTPropagationLifecycleDirtied, -}; - -// TODO: move this to text node? -typedef NS_ENUM(NSUInteger, RCTTextLifecycle) { - RCTTextLifecycleUninitialized = 0, - RCTTextLifecycleComputed, - RCTTextLifecycleDirtied, +typedef NS_ENUM(NSUInteger, RCTUpdateLifecycle) { + RCTUpdateLifecycleUninitialized = 0, + RCTUpdateLifecycleComputed, + RCTUpdateLifecycleDirtied, }; // TODO: is this redundact now? @@ -45,10 +31,10 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); @property (nonatomic, weak, readonly) RCTShadowView *superview; @property (nonatomic, assign, readonly) css_node_t *cssNode; -@property (nonatomic, copy) NSString *moduleName; -@property (nonatomic, assign) BOOL isBGColorExplicitlySet; // Used to propogate to children -@property (nonatomic, strong) UIColor *backgroundColor; // Used to propogate to children -@property (nonatomic, assign) RCTLayoutLifecycle layoutLifecycle; +@property (nonatomic, copy) NSString *viewName; +@property (nonatomic, assign) BOOL isBGColorExplicitlySet; // Used to propagate to children +@property (nonatomic, strong) UIColor *backgroundColor; // Used to propagate to children +@property (nonatomic, assign) RCTUpdateLifecycle layoutLifecycle; /** * isNewView - Used to track the first time the view is introduced into the hierarchy. It is initialized YES, then is @@ -68,6 +54,9 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); */ @property (nonatomic, assign) CGFloat top; @property (nonatomic, assign) CGFloat left; +@property (nonatomic, assign) CGFloat bottom; +@property (nonatomic, assign) CGFloat right; + @property (nonatomic, assign) CGFloat width; @property (nonatomic, assign) CGFloat height; @property (nonatomic, assign) CGRect frame; @@ -80,8 +69,8 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); */ @property (nonatomic, assign) CGFloat borderTop; @property (nonatomic, assign) CGFloat borderLeft; -@property (nonatomic, assign) CGFloat borderWidth; -@property (nonatomic, assign) CGFloat borderHeight; +@property (nonatomic, assign) CGFloat borderBottom; +@property (nonatomic, assign) CGFloat borderRight; - (void)setBorderWidth:(CGFloat)value; @@ -122,11 +111,23 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *); @property (nonatomic, assign) css_wrap_type_t flexWrap; @property (nonatomic, assign) CGFloat flex; -- (void)collectUpdatedProperties:(NSMutableSet *)viewsWithNewProperties parentProperties:(NSDictionary *)parentProperties; -- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame parentConstraint:(CGSize)parentConstraint; -- (void)fillCSSNode:(css_node_t *)node; +/** + * Calculate property changes that need to be propagated to the view. + * The applierBlocks set contains RCTApplierBlock functions that must be applied + * on the main thread in order to update the view. + */ +- (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties; -// The following are implementation details exposed to subclasses. Do not call them directly +/** + * Calculate all views whose frame needs updating after layout has been calculated. + * The viewsWithNewFrame set contains the reactTags of the views that need updating. + */ +- (void)collectRootUpdatedFrames:(NSMutableSet *)viewsWithNewFrame parentConstraint:(CGSize)parentConstraint; + +/** + * The following are implementation details exposed to subclasses. Do not call them directly + */ +- (void)fillCSSNode:(css_node_t *)node; - (void)dirtyLayout; - (BOOL)isLayoutDirty; diff --git a/ReactKit/Views/RCTShadowView.m b/ReactKit/Views/RCTShadowView.m index 4877b0d67..d28495b4c 100644 --- a/ReactKit/Views/RCTShadowView.m +++ b/ReactKit/Views/RCTShadowView.m @@ -10,46 +10,6 @@ typedef void (^RCTActionBlock)(RCTShadowView *shadowViewSelf, id value); typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf); -@interface RCTLayoutAction : NSObject - -@property (nonatomic, readwrite, copy) RCTActionBlock block; -@property (nonatomic, readwrite, copy) RCTResetActionBlock resetBlock; -@property (nonatomic, readwrite, assign) NSInteger precedence; - -@end - -@implementation RCTLayoutAction @end - -#define ACTION_FOR_KEY_DEFAULT(name, default, blockIn) \ -do { \ - RCTLayoutAction *action = [[RCTLayoutAction alloc] init]; \ - action.block = blockIn; \ - action.resetBlock = ^(id idSelf1) { \ - blockIn(idSelf1, default); \ - }; \ - actions[@"" #name ""] = action; \ -} while(0) - -#define ACTION_FOR_KEY(name, blockIn) \ -ACTION_FOR_KEY_DEFAULT(name, @([defaultShadowView name]), (blockIn)) - -#define ACTION_FOR_FLOAT_KEY_DEFAULT(name, default, blockIn) \ -ACTION_FOR_KEY_DEFAULT(name, @(default), ^(id idSelf2, NSNumber *n) { \ - if (isnan([n floatValue])) { \ - RCTLogWarn(@"Got NaN for `"#name"` prop, ignoring"); \ - return; \ - } \ - blockIn(idSelf2, RCTNumberToFloat(n)); \ -}); - -#define ACTION_FOR_FLOAT_KEY(name, blockIn) \ -ACTION_FOR_FLOAT_KEY_DEFAULT(name, [defaultShadowView name], (blockIn)) - -#define ACTION_FOR_DEFAULT_UNDEFINED_KEY(name, blockIn) \ -ACTION_FOR_KEY_DEFAULT(name, nil, ^(id idSelf2, NSNumber *n) { \ - blockIn(idSelf2, n == nil ? CSS_UNDEFINED : [n floatValue]); \ -}); - #define MAX_TREE_DEPTH 30 const NSString *const RCTBackgroundColorProp = @"backgroundColor"; @@ -65,22 +25,16 @@ typedef enum { META_PROP_COUNT, } meta_prop_t; -@interface RCTShadowView() -{ - float _paddingMetaProps[META_PROP_COUNT]; - float _marginMetaProps[META_PROP_COUNT]; -} - -@end - @implementation RCTShadowView { - RCTPropagationLifecycle _propagationLifecycle; - RCTTextLifecycle _textLifecycle; + RCTUpdateLifecycle _propagationLifecycle; + RCTUpdateLifecycle _textLifecycle; NSDictionary *_lastParentProperties; NSMutableArray *_reactSubviews; BOOL _recomputePadding; BOOL _recomputeMargin; + float _paddingMetaProps[META_PROP_COUNT]; + float _marginMetaProps[META_PROP_COUNT]; } @synthesize reactTag = _reactTag; @@ -90,7 +44,7 @@ typedef enum { static void RCTPrint(void *context) { RCTShadowView *shadowView = (__bridge RCTShadowView *)context; - printf("%s(%zd), ", [[shadowView moduleName] UTF8String], [[shadowView reactTag] integerValue]); + printf("%s(%zd), ", shadowView.viewName.UTF8String, shadowView.reactTag.integerValue); } static css_node_t *RCTGetChild(void *context, int i) @@ -131,11 +85,6 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st node->children_count = (int)_reactSubviews.count; } -- (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame -{ - [self _applyLayoutNode:node viewsWithNewFrame:viewsWithNewFrame absolutePosition:CGPointZero]; -} - // The absolute stuff is so that we can take into account our absolute position when rounding in order to // snap to the pixel grid. For example, say you have the following structure: // @@ -165,30 +114,31 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st // width = 213.5 - 106.5 = 107 // You'll notice that this is the same width we calculated for the parent view because we've taken its position into account. -- (void)_applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition +- (void)applyLayoutNode:(css_node_t *)node viewsWithNewFrame:(NSMutableSet *)viewsWithNewFrame absolutePosition:(CGPoint)absolutePosition { if (!node->layout.should_update) { return; } node->layout.should_update = false; - _layoutLifecycle = RCTLayoutLifecycleComputed; + _layoutLifecycle = RCTUpdateLifecycleComputed; CGPoint absoluteTopLeft = { RCTRoundPixelValue(absolutePosition.x + node->layout.position[CSS_LEFT]), RCTRoundPixelValue(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]) }; - - CGRect frame = { + + CGRect frame = {{ RCTRoundPixelValue(node->layout.position[CSS_LEFT]), RCTRoundPixelValue(node->layout.position[CSS_TOP]), + }, { RCTRoundPixelValue(absoluteBottomRight.x - absoluteTopLeft.x), RCTRoundPixelValue(absoluteBottomRight.y - absoluteTopLeft.y) - }; + }}; if (!CGRectEqualToRect(frame, _frame)) { _frame = frame; @@ -205,7 +155,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st for (int i = 0; i < node->children_count; ++i) { RCTShadowView *child = (RCTShadowView *)_reactSubviews[i]; - [child _applyLayoutNode:node->get_child(node->context, i) viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; + [child applyLayoutNode:node->get_child(node->context, i) viewsWithNewFrame:viewsWithNewFrame absolutePosition:absolutePosition]; } } @@ -238,10 +188,10 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st - (void)collectUpdatedProperties:(NSMutableSet *)applierBlocks parentProperties:(NSDictionary *)parentProperties { - if (_propagationLifecycle == RCTPropagationLifecycleComputed && [parentProperties isEqualToDictionary:_lastParentProperties]) { + if (_propagationLifecycle == RCTUpdateLifecycleComputed && [parentProperties isEqualToDictionary:_lastParentProperties]) { return; } - _propagationLifecycle = RCTPropagationLifecycleComputed; + _propagationLifecycle = RCTUpdateLifecycleComputed; _lastParentProperties = parentProperties; NSDictionary *nextProps = [self processBackgroundColor:applierBlocks parentProperties:parentProperties]; for (RCTShadowView *child in _reactSubviews) { @@ -253,7 +203,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st { [self fillCSSNode:_cssNode]; layoutNode(_cssNode, CSS_UNDEFINED); - [self applyLayoutNode:_cssNode viewsWithNewFrame:viewsWithNewFrame]; + [self applyLayoutNode:_cssNode viewsWithNewFrame:viewsWithNewFrame absolutePosition:CGPointZero]; } + (CGRect)measureLayout:(RCTShadowView *)shadowView relativeTo:(RCTShadowView *)ancestor @@ -277,7 +227,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st - (instancetype)init { if ((self = [super init])) { - + _frame = CGRectMake(0, 0, CSS_UNDEFINED, CSS_UNDEFINED); for (int ii = 0; ii < META_PROP_COUNT; ii++) { @@ -286,9 +236,9 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st } _newView = YES; - _layoutLifecycle = RCTLayoutLifecycleUninitialized; - _propagationLifecycle = RCTPropagationLifecycleUninitialized; - _textLifecycle = RCTTextLifecycleUninitialized; + _layoutLifecycle = RCTUpdateLifecycleUninitialized; + _propagationLifecycle = RCTUpdateLifecycleUninitialized; + _textLifecycle = RCTUpdateLifecycleUninitialized; _reactSubviews = [NSMutableArray array]; @@ -309,46 +259,46 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st - (void)dirtyLayout { - if (_layoutLifecycle != RCTLayoutLifecycleDirtied) { - _layoutLifecycle = RCTLayoutLifecycleDirtied; + if (_layoutLifecycle != RCTUpdateLifecycleDirtied) { + _layoutLifecycle = RCTUpdateLifecycleDirtied; [_superview dirtyLayout]; } } - (BOOL)isLayoutDirty { - return _layoutLifecycle != RCTLayoutLifecycleComputed; + return _layoutLifecycle != RCTUpdateLifecycleComputed; } - (void)dirtyPropagation { - if (_propagationLifecycle != RCTPropagationLifecycleDirtied) { - _propagationLifecycle = RCTPropagationLifecycleDirtied; + if (_propagationLifecycle != RCTUpdateLifecycleDirtied) { + _propagationLifecycle = RCTUpdateLifecycleDirtied; [_superview dirtyPropagation]; } } - (BOOL)isPropagationDirty { - return _propagationLifecycle != RCTLayoutLifecycleComputed; + return _propagationLifecycle != RCTUpdateLifecycleComputed; } - (void)dirtyText { - if (_textLifecycle != RCTTextLifecycleDirtied) { - _textLifecycle = RCTTextLifecycleDirtied; + if (_textLifecycle != RCTUpdateLifecycleDirtied) { + _textLifecycle = RCTUpdateLifecycleDirtied; [_superview dirtyText]; } } - (BOOL)isTextDirty { - return _textLifecycle != RCTTextLifecycleComputed; + return _textLifecycle != RCTUpdateLifecycleComputed; } - (void)setTextComputed { - _textLifecycle = RCTTextLifecycleComputed; + _textLifecycle = RCTUpdateLifecycleComputed; } - (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex @@ -376,6 +326,11 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st return _reactSubviews; } +- (RCTShadowView *)reactSuperview +{ + return _superview; +} + - (NSNumber *)reactTagAtPoint:(CGPoint)point { for (RCTShadowView *shadowView in _reactSubviews) { @@ -387,7 +342,6 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st return [shadowView reactTagAtPoint:relativePoint]; } } - return self.reactTag; } diff --git a/ReactKit/Views/RCTSliderManager.h b/ReactKit/Views/RCTSliderManager.h new file mode 100644 index 000000000..1088ec569 --- /dev/null +++ b/ReactKit/Views/RCTSliderManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTViewManager.h" + +@interface RCTSliderManager : RCTViewManager + +@end diff --git a/ReactKit/Views/RCTSliderManager.m b/ReactKit/Views/RCTSliderManager.m new file mode 100644 index 000000000..8561c0a97 --- /dev/null +++ b/ReactKit/Views/RCTSliderManager.m @@ -0,0 +1,43 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTSliderManager.h" + +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" +#import "UIView+ReactKit.h" + +@implementation RCTSliderManager + +- (UIView *)view +{ + UISlider *slider = [[UISlider alloc] init]; + [slider addTarget:self action:@selector(sliderValueChanged:) forControlEvents:UIControlEventValueChanged]; + [slider addTarget:self action:@selector(sliderTouchEnd:) forControlEvents:UIControlEventTouchUpInside]; + return slider; +} + +- (void)sliderValueChanged:(UISlider *)sender +{ + NSDictionary *event = @{ + @"target": sender.reactTag, + @"value": @(sender.value), + @"continuous": @YES, + }; + + [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; +} + +- (void)sliderTouchEnd:(UISlider *)sender +{ + NSDictionary *event = @{ + @"target": sender.reactTag, + @"value": @(sender.value), + @"continuous": @NO, + }; + + [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:event]; +} + +RCT_EXPORT_VIEW_PROPERTY(value); + +@end diff --git a/ReactKit/Views/RCTSwitch.h b/ReactKit/Views/RCTSwitch.h new file mode 100644 index 000000000..7866eb866 --- /dev/null +++ b/ReactKit/Views/RCTSwitch.h @@ -0,0 +1,10 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + + +#import + +@interface RCTSwitch : UISwitch + +@property (nonatomic, assign) BOOL wasOn; + +@end diff --git a/ReactKit/Views/RCTSwitch.m b/ReactKit/Views/RCTSwitch.m new file mode 100644 index 000000000..70233fede --- /dev/null +++ b/ReactKit/Views/RCTSwitch.m @@ -0,0 +1,15 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTSwitch.h" + +#import "RCTEventDispatcher.h" +#import "UIView+ReactKit.h" + +@implementation RCTSwitch + +- (void)setOn:(BOOL)on animated:(BOOL)animated { + _wasOn = on; + [super setOn:on animated:animated]; +} + +@end diff --git a/ReactKit/Views/RCTSwitchManager.h b/ReactKit/Views/RCTSwitchManager.h new file mode 100644 index 000000000..f6833d106 --- /dev/null +++ b/ReactKit/Views/RCTSwitchManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTViewManager.h" + +@interface RCTSwitchManager : RCTViewManager + +@end diff --git a/ReactKit/Views/RCTSwitchManager.m b/ReactKit/Views/RCTSwitchManager.m new file mode 100644 index 000000000..61eab8199 --- /dev/null +++ b/ReactKit/Views/RCTSwitchManager.m @@ -0,0 +1,39 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTSwitchManager.h" + +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" +#import "RCTSwitch.h" +#import "UIView+ReactKit.h" + +@implementation RCTSwitchManager + +- (UIView *)view +{ + RCTSwitch *switcher = [[RCTSwitch alloc] init]; + [switcher addTarget:self + action:@selector(onChange:) + forControlEvents:UIControlEventValueChanged]; + return switcher; +} + +- (void)onChange:(RCTSwitch *)sender +{ + if (sender.wasOn != sender.on) { + [self.bridge.eventDispatcher sendInputEventWithName:@"topChange" body:@{ + @"target": sender.reactTag, + @"value": @(sender.on) + }]; + + sender.wasOn = sender.on; + } +} + +RCT_EXPORT_VIEW_PROPERTY(onTintColor); +RCT_EXPORT_VIEW_PROPERTY(tintColor); +RCT_EXPORT_VIEW_PROPERTY(thumbTintColor); +RCT_EXPORT_VIEW_PROPERTY(on); +RCT_EXPORT_VIEW_PROPERTY(enabled); + +@end diff --git a/ReactKit/Views/RCTTabBar.h b/ReactKit/Views/RCTTabBar.h new file mode 100644 index 000000000..58da056a3 --- /dev/null +++ b/ReactKit/Views/RCTTabBar.h @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@class RCTEventDispatcher; + +@interface RCTTabBar : UIView + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +@end diff --git a/ReactKit/Views/RCTTabBar.m b/ReactKit/Views/RCTTabBar.m new file mode 100644 index 000000000..d9a0ef39a --- /dev/null +++ b/ReactKit/Views/RCTTabBar.m @@ -0,0 +1,138 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTTabBar.h" + +#import "RCTEventDispatcher.h" +#import "RCTLog.h" +#import "RCTTabBarItem.h" +#import "RCTUtils.h" +#import "RCTView.h" +#import "RCTViewControllerProtocol.h" +#import "RCTWrapperViewController.h" +#import "UIView+ReactKit.h" + +@interface RKCustomTabBarController : UITabBarController + +@end + +@implementation RKCustomTabBarController + +@synthesize currentTopLayoutGuide = _currentTopLayoutGuide; +@synthesize currentBottomLayoutGuide = _currentBottomLayoutGuide; + +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + _currentTopLayoutGuide = self.topLayoutGuide; + _currentBottomLayoutGuide = self.bottomLayoutGuide; +} + +@end + +@interface RCTTabBar() + +@end + +@implementation RCTTabBar +{ + BOOL _tabsChanged; + RCTEventDispatcher *_eventDispatcher; + UITabBarController *_tabController; + NSMutableArray *_tabViews; +} + +- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { + _eventDispatcher = eventDispatcher; + _tabViews = [[NSMutableArray alloc] init]; + _tabController = [[RKCustomTabBarController alloc] init]; + _tabController.delegate = self; + [self addSubview:_tabController.view]; + } + return self; +} + +- (UIViewController *)backingViewController +{ + return _tabController; +} + +- (void)dealloc +{ + _tabController.delegate = nil; +} + +- (NSArray *)reactSubviews +{ + return _tabViews; +} + +- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex +{ + if (![view isKindOfClass:[RCTTabBarItem class]]) { + RCTLogError(@"subview should be of type RCTTabBarItem"); + return; + } + [_tabViews insertObject:view atIndex:atIndex]; + _tabsChanged = YES; +} + +- (void)removeReactSubview:(UIView *)subview +{ + if (_tabViews.count == 0) { + RCTLogError(@"should have at least one view to remove a subview"); + return; + } + [_tabViews removeObject:subview]; + _tabsChanged = YES; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + _tabController.view.frame = self.bounds; +} + +- (void)reactBridgeDidFinishTransaction +{ + // we can't hook up the VC hierarchy in 'init' because the subviews aren't + // hooked up yet, so we do it on demand here whenever a transaction has finished + [self addControllerToClosestParent:_tabController]; + + if (_tabsChanged) { + + NSMutableArray *viewControllers = [NSMutableArray array]; + for (RCTTabBarItem *tab in [self reactSubviews]) { + UIViewController *controller = tab.backingViewController; + if (!controller) { + controller = [[RCTWrapperViewController alloc] initWithContentView:tab + eventDispatcher:_eventDispatcher]; + } + [viewControllers addObject:controller]; + } + + _tabController.viewControllers = viewControllers; + _tabsChanged = NO; + } + + [[self reactSubviews] enumerateObjectsUsingBlock:^(RCTTabBarItem *tab, NSUInteger index, BOOL *stop) { + UIViewController *controller = _tabController.viewControllers[index]; + controller.tabBarItem = tab.barItem; + if (tab.selected) { + _tabController.selectedViewController = controller; + } + }]; +} + +#pragma mark - UITabBarControllerDelegate + +- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController +{ + NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController]; + RCTTabBarItem *tab = [self reactSubviews][index]; + [_eventDispatcher sendInputEventWithName:@"topTap" body:@{@"target": tab.reactTag}]; + return NO; +} + +@end diff --git a/ReactKit/Views/RCTTabBarItem.h b/ReactKit/Views/RCTTabBarItem.h new file mode 100644 index 000000000..18b03f151 --- /dev/null +++ b/ReactKit/Views/RCTTabBarItem.h @@ -0,0 +1,11 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import + +@interface RCTTabBarItem : UIView + +@property (nonatomic, copy) NSString *icon; +@property (nonatomic, assign, getter=isSelected) BOOL selected; +@property (nonatomic, readonly) UITabBarItem *barItem; + +@end diff --git a/ReactKit/Views/RCTTabBarItem.m b/ReactKit/Views/RCTTabBarItem.m new file mode 100644 index 000000000..cca0e5180 --- /dev/null +++ b/ReactKit/Views/RCTTabBarItem.m @@ -0,0 +1,83 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTTabBarItem.h" + +#import "RCTConvert.h" +#import "RCTLog.h" +#import "UIView+ReactKit.h" + +@implementation RCTTabBarItem + +@synthesize barItem = _barItem; + +- (UITabBarItem *)barItem +{ + if (!_barItem) { + _barItem = [[UITabBarItem alloc] init]; + } + return _barItem; +} + +- (void)setIcon:(NSString *)icon +{ + static NSDictionary *systemIcons; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + systemIcons = @{ + @"more": @(UITabBarSystemItemMore), + @"favorites": @(UITabBarSystemItemFavorites), + @"featured": @(UITabBarSystemItemFeatured), + @"topRated": @(UITabBarSystemItemTopRated), + @"recents": @(UITabBarSystemItemRecents), + @"contacts": @(UITabBarSystemItemContacts), + @"history": @(UITabBarSystemItemHistory), + @"bookmarks": @(UITabBarSystemItemBookmarks), + @"search": @(UITabBarSystemItemSearch), + @"downloads": @(UITabBarSystemItemDownloads), + @"mostRecent": @(UITabBarSystemItemMostRecent), + @"mostViewed": @(UITabBarSystemItemMostViewed), + }; + }); + + // Update icon + BOOL wasSystemIcon = (systemIcons[_icon] != nil); + _icon = [icon copy]; + + // Check if string matches any custom images first + UIImage *image = [RCTConvert UIImage:_icon]; + UITabBarItem *oldItem = _barItem; + if (image) { + + // Recreate barItem if previous item was a system icon + if (wasSystemIcon) { + _barItem = nil; + self.barItem.image = image; + } else { + self.barItem.image = image; + return; + } + + } else { + + // Not a custom image, may be a system item? + NSNumber *systemIcon = systemIcons[icon]; + if (!systemIcon) { + 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]; + } + + // Reapply previous properties + _barItem.title = oldItem.title; + _barItem.imageInsets = oldItem.imageInsets; + _barItem.selectedImage = oldItem.selectedImage; + _barItem.badgeValue = oldItem.badgeValue; +} + +- (UIViewController *)backingViewController +{ + return self.superview.backingViewController; +} + +@end diff --git a/ReactKit/Views/RCTTabBarItemManager.h b/ReactKit/Views/RCTTabBarItemManager.h new file mode 100644 index 000000000..623020769 --- /dev/null +++ b/ReactKit/Views/RCTTabBarItemManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTViewManager.h" + +@interface RCTTabBarItemManager : RCTViewManager + +@end diff --git a/ReactKit/Views/RCTTabBarItemManager.m b/ReactKit/Views/RCTTabBarItemManager.m new file mode 100644 index 000000000..a840dd6bf --- /dev/null +++ b/ReactKit/Views/RCTTabBarItemManager.m @@ -0,0 +1,25 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTTabBarItemManager.h" + +#import "RCTConvert.h" +#import "RCTTabBarItem.h" + +@implementation RCTTabBarItemManager + +- (UIView *)view +{ + return [[RCTTabBarItem alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(selected); +RCT_EXPORT_VIEW_PROPERTY(icon); +RCT_REMAP_VIEW_PROPERTY(selectedIcon, barItem.selectedImage); +RCT_REMAP_VIEW_PROPERTY(badgeValue, barItem.badgeValue); +RCT_CUSTOM_VIEW_PROPERTY(title, RCTTabBarItem) +{ + view.barItem.title = json ? [RCTConvert NSString:json] : defaultView.barItem.title; + view.barItem.imageInsets = [view.barItem.title length] ? UIEdgeInsetsZero : (UIEdgeInsets){6, 0, -6, 0}; +} + +@end diff --git a/ReactKit/Views/RCTTabBarManager.h b/ReactKit/Views/RCTTabBarManager.h new file mode 100644 index 000000000..34f745e1c --- /dev/null +++ b/ReactKit/Views/RCTTabBarManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTViewManager.h" + +@interface RCTTabBarManager : RCTViewManager + +@end diff --git a/ReactKit/Views/RCTTabBarManager.m b/ReactKit/Views/RCTTabBarManager.m new file mode 100644 index 000000000..70882bc16 --- /dev/null +++ b/ReactKit/Views/RCTTabBarManager.m @@ -0,0 +1,17 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTTabBarManager.h" + +#import "RCTBridge.h" +#import "RCTTabBar.h" + +@implementation RCTTabBarManager + +@synthesize bridge = _bridge; + +- (UIView *)view +{ + return [[RCTTabBar alloc] initWithEventDispatcher:_bridge.eventDispatcher]; +} + +@end diff --git a/ReactKit/Views/RCTTextField.h b/ReactKit/Views/RCTTextField.h index 2a0225f27..1688f8fdc 100644 --- a/ReactKit/Views/RCTTextField.h +++ b/ReactKit/Views/RCTTextField.h @@ -10,6 +10,6 @@ @property (nonatomic, assign) BOOL autoCorrect; @property (nonatomic, assign) UIEdgeInsets paddingEdgeInsets; // TODO: contentInset -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher; +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; @end diff --git a/ReactKit/Views/RCTTextField.m b/ReactKit/Views/RCTTextField.m index 4a70e0460..0dca73daa 100644 --- a/ReactKit/Views/RCTTextField.m +++ b/ReactKit/Views/RCTTextField.m @@ -14,15 +14,10 @@ BOOL _jsRequestingFirstResponder; } -- (instancetype)initWithFrame:(CGRect)frame -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} - - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher { if ((self = [super initWithFrame:CGRectZero])) { - + _eventDispatcher = eventDispatcher; [self addTarget:self action:@selector(_textFieldDidChange) forControlEvents:UIControlEventEditingChanged]; [self addTarget:self action:@selector(_textFieldBeginEditing) forControlEvents:UIControlEventEditingDidBegin]; @@ -44,16 +39,16 @@ - (void)removeReactSubview:(UIView *)subview { - // TODO: this is a bit broken - if the TextView inserts any of - // it's own views below or between React's, the indices won't match + // TODO: this is a bit broken - if the TextField inserts any of + // its own views below or between React's, the indices won't match [_reactSubviews removeObject:subview]; [subview removeFromSuperview]; } - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex { - // TODO: this is a bit broken - if the TextView inserts any of - // it's own views below or between React's, the indices won't match + // TODO: this is a bit broken - if the TextField inserts any of + // its own views below or between React's, the indices won't match [_reactSubviews insertObject:view atIndex:atIndex]; [super insertSubview:view atIndex:atIndex]; } @@ -79,7 +74,7 @@ - (void)setAutoCorrect:(BOOL)autoCorrect { - [super setAutocorrectionType:(autoCorrect ? UITextAutocorrectionTypeYes : UITextAutocorrectionTypeNo)]; + self.autocorrectionType = (autoCorrect ? UITextAutocorrectionTypeYes : UITextAutocorrectionTypeNo); } - (BOOL)autoCorrect @@ -122,7 +117,6 @@ RCT_TEXT_EVENT_HANDLER(_textFieldSubmitEditing, RCTTextEventTypeSubmit) return result; } -// Prevent native from becoming first responder (TODO: why?) - (BOOL)canBecomeFirstResponder { return _jsRequestingFirstResponder; diff --git a/ReactKit/Views/RCTTextFieldManager.m b/ReactKit/Views/RCTTextFieldManager.m index 339a7803e..9576935b9 100644 --- a/ReactKit/Views/RCTTextFieldManager.m +++ b/ReactKit/Views/RCTTextFieldManager.m @@ -2,15 +2,17 @@ #import "RCTTextFieldManager.h" +#import "RCTBridge.h" #import "RCTConvert.h" #import "RCTShadowView.h" +#import "RCTSparseArray.h" #import "RCTTextField.h" @implementation RCTTextFieldManager - (UIView *)view { - return [[RCTTextField alloc] initWithEventDispatcher:self.eventDispatcher]; + return [[RCTTextField alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; } RCT_EXPORT_VIEW_PROPERTY(caretHidden) @@ -18,34 +20,34 @@ RCT_EXPORT_VIEW_PROPERTY(autoCorrect) RCT_EXPORT_VIEW_PROPERTY(enabled) RCT_EXPORT_VIEW_PROPERTY(placeholder) RCT_EXPORT_VIEW_PROPERTY(text) -RCT_EXPORT_VIEW_PROPERTY(font) -RCT_REMAP_VIEW_PROPERTY(autoCapitalize, autocapitalizationType) +RCT_EXPORT_VIEW_PROPERTY(clearButtonMode) RCT_EXPORT_VIEW_PROPERTY(keyboardType) RCT_REMAP_VIEW_PROPERTY(color, textColor) - -- (void)set_fontSize:(id)json - forView:(RCTTextField *)view - withDefaultView:(RCTTextField *)defaultView +RCT_CUSTOM_VIEW_PROPERTY(autoCapitalize, RCTTextField) +{ + view.autocapitalizationType = json ? [RCTConvert UITextAutocapitalizationType:json] + : defaultView.autocapitalizationType; +} +RCT_CUSTOM_VIEW_PROPERTY(fontSize, RCTTextField) { view.font = [RCTConvert UIFont:view.font withSize:json ?: @(defaultView.font.pointSize)]; } - -- (void)set_FontWeight:(id)json - forView:(RCTTextField *)view - withDefaultView:(RCTTextField *)defaultView +RCT_CUSTOM_VIEW_PROPERTY(fontWeight, RCTTextField) { - view.font = [RCTConvert UIFont:view.font withWeight:json]; // TODO + view.font = [RCTConvert UIFont:view.font withWeight:json]; // TODO: default value } - -- (void)set_fontFamily:(id)json - forView:(RCTTextField *)view - withDefaultView:(RCTTextField *)defaultView +RCT_CUSTOM_VIEW_PROPERTY(fontFamily, RCTTextField) { view.font = [RCTConvert UIFont:view.font withFamily:json ?: defaultView.font.familyName]; } -// TODO: original code set view.paddingEdgeInsets from shadowView.paddingAsInsets -// could it be that this property is calculated asynchrously on shadow thread? +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView +{ + NSNumber *reactTag = shadowView.reactTag; + UIEdgeInsets padding = shadowView.paddingAsInsets; + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + ((RCTTextField *)viewRegistry[reactTag]).paddingEdgeInsets = padding; + }; +} @end - diff --git a/ReactKit/Views/RCTUIActivityIndicatorViewManager.m b/ReactKit/Views/RCTUIActivityIndicatorViewManager.m index b7e0971e7..2322e0d90 100644 --- a/ReactKit/Views/RCTUIActivityIndicatorViewManager.m +++ b/ReactKit/Views/RCTUIActivityIndicatorViewManager.m @@ -13,12 +13,9 @@ RCT_EXPORT_VIEW_PROPERTY(activityIndicatorViewStyle) RCT_EXPORT_VIEW_PROPERTY(color) - -- (void)set_animating:(NSNumber *)value - forView:(UIActivityIndicatorView *)view - withDefaultView:(UIActivityIndicatorView *)defaultView +RCT_CUSTOM_VIEW_PROPERTY(animating, UIActivityIndicatorView) { - BOOL animating = value ? [value boolValue] : [defaultView isAnimating]; + BOOL animating = json ? [json boolValue] : [defaultView isAnimating]; if (animating != [view isAnimating]) { if (animating) { [view startAnimating]; @@ -28,7 +25,7 @@ RCT_EXPORT_VIEW_PROPERTY(color) } } -+ (NSDictionary *)constantsToExport +- (NSDictionary *)constantsToExport { return @{ diff --git a/ReactKit/Views/RCTView.h b/ReactKit/Views/RCTView.h index 77dee1df8..379fa7160 100644 --- a/ReactKit/Views/RCTView.h +++ b/ReactKit/Views/RCTView.h @@ -10,15 +10,35 @@ @interface RCTView : UIView +/** + * Used to control how touch events are processed. + */ @property (nonatomic, assign) RCTPointerEvents pointerEvents; -@property (nonatomic, copy) NSString *overrideAccessibilityLabel; + (void)autoAdjustInsetsForView:(UIView *)parentView withScrollView:(UIScrollView *)scrollView updateOffset:(BOOL)updateOffset; -+ (UIViewController *)backingViewControllerForView:(UIView *)view; - +/** + * Find the first view controller whose view, or any subview is the specified view. + */ + (UIEdgeInsets)contentInsetsForView:(UIView *)curView; +/** + * This is an optimization used to improve performance + * for large scrolling views with many subviews, such as a + * list or table. If set to YES, any clipped subviews will + * be removed from the view hierarchy whenever -updateClippedSubviews + * is called. This would typically be triggered by a scroll event + */ +@property (nonatomic, assign) BOOL removeClippedSubviews; + +/** + * Hide subviews if they are outside the view bounds. + * This is an optimisation used predominantly with RKScrollViews + * but it is applied recursively to all subviews that have + * removeClippedSubviews set to YES + */ +- (void)updateClippedSubviews; + @end diff --git a/ReactKit/Views/RCTView.m b/ReactKit/Views/RCTView.m index abe6f00d9..609f26243 100644 --- a/ReactKit/Views/RCTView.m +++ b/ReactKit/Views/RCTView.m @@ -5,6 +5,66 @@ #import "RCTAutoInsetsProtocol.h" #import "RCTConvert.h" #import "RCTLog.h" +#import "UIView+ReactKit.h" + +@implementation UIView (RCTViewUnmounting) + +- (void)react_remountAllSubviews +{ + // Normal views don't support unmounting, so all + // this does is forward message to our subviews, + // in case any of those do support it + + for (UIView *subview in self.subviews) { + [subview react_remountAllSubviews]; + } +} + +- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView +{ + // Even though we don't support subview unmounting + // we do support clipsToBounds, so if that's enabled + // we'll update the clipping + + if (self.clipsToBounds && [self.subviews count] > 0) { + clipRect = [clipView convertRect:clipRect toView:self]; + clipRect = CGRectIntersection(clipRect, self.bounds); + clipView = self; + } + + // Normal views don't support unmounting, so all + // this does is forward message to our subviews, + // in case any of those do support it + + for (UIView *subview in self.subviews) { + [subview react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + } +} + +- (UIView *)react_findClipView +{ + UIView *testView = self; + UIView *clipView = nil; + CGRect clipRect = self.bounds; + while (testView) { + if (testView.clipsToBounds) { + if (clipView) { + CGRect testRect = [clipView convertRect:clipRect toView:testView]; + if (!CGRectContainsRect(testView.bounds, testRect)) { + clipView = testView; + clipRect = CGRectIntersection(testView.bounds, testRect); + } + } else { + clipView = testView; + clipRect = [self convertRect:self.bounds toView:clipView]; + } + } + testView = testView.superview; + } + return clipView ?: self.window; +} + +@end static NSString *RCTRecursiveAccessibilityLabel(UIView *view) { @@ -22,20 +82,14 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) } @implementation RCTView - -- (id)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - _pointerEvents = RCTPointerEventsUnspecified; - } - return self; + NSMutableArray *_reactSubviews; } - (NSString *)accessibilityLabel { - if (self.overrideAccessibilityLabel) { - return self.overrideAccessibilityLabel; + if (super.accessibilityLabel) { + return super.accessibilityLabel; } return RCTRecursiveAccessibilityLabel(self); } @@ -84,7 +138,7 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) UIEdgeInsets baseInset = parentView.contentInset; CGFloat previousInsetTop = scrollView.contentInset.top; CGPoint contentOffset = scrollView.contentOffset; - + if (parentView.automaticallyAdjustContentInsets) { UIEdgeInsets autoInset = [self contentInsetsForView:parentView]; baseInset.top += autoInset.top; @@ -94,7 +148,7 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) } [scrollView setContentInset:baseInset]; [scrollView setScrollIndicatorInsets:baseInset]; - + if (updateOffset) { // If we're adjusting the top inset, then let's also adjust the contentOffset so that the view // elements above the top guide do not cover the content. @@ -108,19 +162,10 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) } } -+ (UIViewController *)backingViewControllerForView:(UIView *)view -{ - id responder = [view nextResponder]; - if ([responder isKindOfClass:[UIViewController class]]) { - return responder; - } - return nil; -} - + (UIEdgeInsets)contentInsetsForView:(UIView *)view { while (view) { - UIViewController *controller = [self backingViewControllerForView:view]; + UIViewController *controller = view.backingViewController; if (controller) { return (UIEdgeInsets){ controller.topLayoutGuide.length, 0, @@ -132,4 +177,193 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) return UIEdgeInsetsZero; } +#pragma mark - View unmounting + +- (void)react_remountAllSubviews +{ + if (_reactSubviews) { + NSInteger index = 0; + for (UIView *view in _reactSubviews) { + if (view.superview != self) { + if (index < [self subviews].count) { + [self insertSubview:view atIndex:index]; + } else { + [self addSubview:view]; + } + [view react_remountAllSubviews]; + } + index++; + } + } else { + // If react_subviews is nil, we must already be showing all subviews + [super react_remountAllSubviews]; + } +} + +- (void)remountSubview:(UIView *)view +{ + // Calculate insertion index for view + NSInteger index = 0; + for (UIView *subview in _reactSubviews) { + if (subview == view) { + [self insertSubview:view atIndex:index]; + break; + } + if (subview.superview) { + // View is mounted, so bump the index + index++; + } + } +} + +- (void)mountOrUnmountSubview:(UIView *)view withClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView +{ + if (view.clipsToBounds) { + + // View has cliping enabled, so we can easily test if it is partially + // or completely within the clipRect, and mount or unmount it accordingly + + if (CGRectIntersectsRect(clipRect, view.frame)) { + + // View is at least partially visible, so remount it if unmounted + if (view.superview == nil) { + [self remountSubview:view]; + } + + // Then test its subviews + if (CGRectContainsRect(clipRect, view.frame)) { + [view react_remountAllSubviews]; + } else { + [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + } + + } else if (view.superview) { + + // View is completely outside the clipRect, so unmount it + [view removeFromSuperview]; + } + + } else { + + // View has clipping disabled, so there's no way to tell if it has + // any visible subviews without an expensive recursive test, so we'll + // just add it. + + if (view.superview == nil) { + [self remountSubview:view]; + } + + // Check if subviews need to be mounted/unmounted + [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + } +} + +- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView +{ + // TODO (#5906496): for scrollviews (the primary use-case) we could + // optimize this by only doing a range check along the scroll axis, + // instead of comparing the whole frame + + if (_reactSubviews == nil) { + // Use default behavior if unmounting is disabled + return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView]; + } + + if ([_reactSubviews count] == 0) { + // Do nothing if we have no subviews + return; + } + + if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) { + // Do nothing if layout hasn't happened yet + return; + } + + // Convert clipping rect to local coordinates + clipRect = [clipView convertRect:clipRect toView:self]; + clipView = self; + if (self.clipsToBounds) { + clipRect = CGRectIntersection(clipRect, self.bounds); + } + + // Mount / unmount views + for (UIView *view in _reactSubviews) { + [self mountOrUnmountSubview:view withClipRect:clipRect relativeToView:clipView]; + } +} + +- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews +{ + if (removeClippedSubviews && !_reactSubviews) { + _reactSubviews = [self.subviews mutableCopy]; + } else if (!removeClippedSubviews && _reactSubviews) { + [self react_remountAllSubviews]; + _reactSubviews = nil; + } +} + +- (BOOL)removeClippedSubviews +{ + return _reactSubviews != nil; +} + +- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex +{ + if (_reactSubviews == nil) { + [self insertSubview:view atIndex:atIndex]; + } else { + [_reactSubviews insertObject:view atIndex:atIndex]; + + // Find a suitable view to use for clipping + UIView *clipView = [self react_findClipView]; + if (clipView) { + + // If possible, don't add subviews if they are clipped + [self mountOrUnmountSubview:view withClipRect:clipView.bounds relativeToView:clipView]; + + } else { + + // Fallback if we can't find a suitable clipView + [self remountSubview:view]; + } + } +} + +- (void)removeReactSubview:(UIView *)subview +{ + [_reactSubviews removeObject:subview]; + [subview removeFromSuperview]; +} + +- (NSArray *)reactSubviews +{ + // The _reactSubviews array is only used when we have hidden + // offscreen views. If _reactSubviews is nil, we can assume + // that [self reactSubviews] and [self subviews] are the same + + return _reactSubviews ?: [self subviews]; +} + +- (void)updateClippedSubviews +{ + // Find a suitable view to use for clipping + UIView *clipView = [self react_findClipView]; + if (clipView) { + [self react_updateClippedSubviewsWithClipRect:clipView.bounds relativeToView:clipView]; + } +} + +- (void)layoutSubviews +{ + // TODO (#5906496): this a nasty performance drain, but necessary + // to prevent gaps appearing when the loading spinner disappears. + // We might be able to fix this another way by triggering a call + // to updateClippedSubviews manually after loading + + [super layoutSubviews]; + if (_reactSubviews) { + [self updateClippedSubviews]; + } +} + @end diff --git a/ReactKit/Views/RCTViewControllerProtocol.h b/ReactKit/Views/RCTViewControllerProtocol.h new file mode 100644 index 000000000..2c82572c3 --- /dev/null +++ b/ReactKit/Views/RCTViewControllerProtocol.h @@ -0,0 +1,13 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +/** + * A simple protocol that any React-managed ViewControllers should implement. + * We need all of our ViewControllers to cache layoutGuide changes so any View + * in our View hierarchy can access accurate layoutGuide info at any time. + */ +@protocol RCTViewControllerProtocol + +@property (nonatomic, readonly, strong) id currentTopLayoutGuide; +@property (nonatomic, readonly, strong) id currentBottomLayoutGuide; + +@end diff --git a/ReactKit/Views/RCTViewManager.h b/ReactKit/Views/RCTViewManager.h index e77be09c1..2240228b8 100644 --- a/ReactKit/Views/RCTViewManager.h +++ b/ReactKit/Views/RCTViewManager.h @@ -2,9 +2,11 @@ #import +#import "RCTBridgeModule.h" #import "RCTConvert.h" #import "RCTLog.h" +@class RCTBridge; @class RCTEventDispatcher; @class RCTShadowView; @class RCTSparseArray; @@ -12,19 +14,14 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *viewRegistry); -@interface RCTViewManager : NSObject +@interface RCTViewManager : NSObject /** - * Designated initializer for view modules. Override this when subclassing. + * The bridge can be used to access both the RCTUIIManager and the RCTEventDispatcher, + * allowing the manager (or the views that it manages) to manipulate the view + * hierarchy and send events back to the JS context. */ -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; - -/** - * The event dispatcher is used to send events back to the JavaScript application. - * It can either be used directly by the module, or passed on to instantiated - * view subclasses so that they can handle their own events. - */ -@property (nonatomic, readonly, weak) RCTEventDispatcher *eventDispatcher; +@property (nonatomic, strong) RCTBridge *bridge; /** * The module name exposed to React JS. If omitted, this will be inferred @@ -88,34 +85,19 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v + (NSDictionary *)customDirectEventTypes; /** - * Injects constants into JS. These constants are made accessible via - * NativeModules.moduleName.X. Note that this method is not inherited when you - * subclass a view module, and you should not call [super constantsToExport] - * when overriding it. + * Called to notify manager that layout has finished, in case any calculated + * properties need to be copied over from shadow view to view. */ -+ (NSDictionary *)constantsToExport; +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView; /** - * To deprecate, hopefully + * Called after view hierarchy manipulation has finished, and all shadow props + * have been set, but before layout has been performed. Useful for performing + * custo layout logic or tasks that involve walking the view hierarchy. + * To be deprecated, hopefully. */ - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry; -/** - * Informal protocol for setting view and shadowView properties. - * Implement methods matching these patterns to set any properties that - * require special treatment (e.g. where the type or name cannot be inferred). - * - * - (void)set_:(id)property - * forView:(UIView *)view - * withDefaultView:(UIView *)defaultView; - * - * - (void)set_:(id)property - * forShadowView:(RCTShadowView *)view - * withDefaultView:(RCTShadowView *)defaultView; - * - * For simple cases, use the macros below: - */ - /** * This handles the simple case, where JS and native property names match * And the type can be automatically inferred. @@ -131,10 +113,21 @@ RCT_REMAP_VIEW_PROPERTY(name, name) - (void)set_##name:(id)json forView:(id)view withDefaultView:(id)defaultView { \ if ((json && !RCTSetProperty(view, @#keypath, json)) || \ (!json && !RCTCopyProperty(view, defaultView, @#keypath))) { \ - RCTLogMustFix(@"%@ does not have setter for `%s` property", [view class], #name); \ + RCTLogError(@"%@ does not have setter for `%s` property", [view class], #name); \ } \ } +/** + * These macros can be used when you need to provide custom logic for setting + * view properties. The macro should be followed by a method body, which can + * refer to "json", "view" and "defaultView" to implement the required logic. + */ +#define RCT_CUSTOM_VIEW_PROPERTY(name, viewClass) \ +- (void)set_##name:(id)json forView:(viewClass *)view withDefaultView:(viewClass *)defaultView + +#define RCT_CUSTOM_SHADOW_PROPERTY(name, viewClass) \ +- (void)set_##name:(id)json forShadowView:(viewClass *)view withDefaultView:(viewClass *)defaultView + /** * These are useful in cases where the module's superclass handles a * property, but you wish to "unhandle" it, so it will be ignored. diff --git a/ReactKit/Views/RCTViewManager.m b/ReactKit/Views/RCTViewManager.m index 05ecc2198..151f72cfe 100644 --- a/ReactKit/Views/RCTViewManager.m +++ b/ReactKit/Views/RCTViewManager.m @@ -2,32 +2,28 @@ #import "RCTViewManager.h" +#import "RCTBridge.h" #import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTShadowView.h" +#import "RCTUtils.h" #import "RCTView.h" @implementation RCTViewManager -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher -{ - if ((self = [super init])) { - _eventDispatcher = eventDispatcher; - } - return self; -} +@synthesize bridge = _bridge; + (NSString *)moduleName { // Default implementation, works in most cases NSString *name = NSStringFromClass(self); + if ([name hasPrefix:@"RK"]) { + name = [name stringByReplacingCharactersInRange:(NSRange){0,@"RK".length} withString:@"RCT"]; + } if ([name hasPrefix:@"RCTUI"]) { name = [name substringFromIndex:@"RCT".length]; } - if ([name hasSuffix:@"Manager"]) { - name = [name substringToIndex:name.length - @"Manager".length]; - } return name; } @@ -51,7 +47,12 @@ return nil; } -+ (NSDictionary *)constantsToExport +- (NSDictionary *)constantsToExport +{ + return nil; +} + +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView { return nil; } @@ -61,7 +62,7 @@ return nil; } -// View properties +#pragma mark - View properties RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel) RCT_EXPORT_VIEW_PROPERTY(hidden) @@ -77,28 +78,22 @@ RCT_REMAP_VIEW_PROPERTY(borderColor, layer.borderColor); RCT_REMAP_VIEW_PROPERTY(borderRadius, layer.cornerRadius) RCT_REMAP_VIEW_PROPERTY(borderWidth, layer.borderWidth) RCT_REMAP_VIEW_PROPERTY(transformMatrix, layer.transform) - -- (void)set_overflow:(id)json - forView:(UIView *)view - withDefaultView:(UIView *)defaultView +RCT_CUSTOM_VIEW_PROPERTY(overflow, UIView) { view.clipsToBounds = json ? ![RCTConvert css_overflow:json] : defaultView.clipsToBounds; } - -- (void)set_pointerEvents:(id)json - forView:(RCTView *)view - withDefaultView:(RCTView *)defaultView +RCT_CUSTOM_VIEW_PROPERTY(pointerEvents, RCTView) { if ([view respondsToSelector:@selector(setPointerEvents:)]) { view.pointerEvents = json ? [RCTConvert RCTPointerEvents:json] : defaultView.pointerEvents; return; } - + if (!json) { view.userInteractionEnabled = defaultView.userInteractionEnabled; return; } - + switch ([RCTConvert NSInteger:json]) { case RCTPointerEventsUnspecified: // Pointer events "unspecified" acts as if a stylesheet had not specified, @@ -115,56 +110,36 @@ RCT_REMAP_VIEW_PROPERTY(transformMatrix, layer.transform) } } -// ShadowView properties +#pragma mark - ShadowView properties -- (void)set_backgroundColor:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView +RCT_CUSTOM_SHADOW_PROPERTY(backgroundColor, RCTShadowView) { - shadowView.backgroundColor = json ? [RCTConvert UIColor:json] : defaultView.backgroundColor; - shadowView.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; + view.backgroundColor = json ? [RCTConvert UIColor:json] : defaultView.backgroundColor; + view.isBGColorExplicitlySet = json ? YES : defaultView.isBGColorExplicitlySet; } - -- (void)set_flexDirection:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView +RCT_CUSTOM_SHADOW_PROPERTY(flexDirection, RCTShadowView) { - shadowView.flexDirection = json? [RCTConvert css_flex_direction_t:json] : defaultView.flexDirection; + view.flexDirection = json? [RCTConvert css_flex_direction_t:json] : defaultView.flexDirection; } - -- (void)set_flexWrap:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView +RCT_CUSTOM_SHADOW_PROPERTY(flexWrap, RCTShadowView) { - shadowView.flexWrap = json ? [RCTConvert css_wrap_type_t:json] : defaultView.flexWrap; + view.flexWrap = json ? [RCTConvert css_wrap_type_t:json] : defaultView.flexWrap; } - -- (void)set_justifyContent:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView +RCT_CUSTOM_SHADOW_PROPERTY(justifyContent, RCTShadowView) { - shadowView.justifyContent = json ? [RCTConvert css_justify_t:json] : defaultView.justifyContent; + view.justifyContent = json ? [RCTConvert css_justify_t:json] : defaultView.justifyContent; } - -- (void)set_alignItems:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView +RCT_CUSTOM_SHADOW_PROPERTY(alignItems, RCTShadowView) { - shadowView.alignItems = json ? [RCTConvert css_align_t:json] : defaultView.alignItems; + view.alignItems = json ? [RCTConvert css_align_t:json] : defaultView.alignItems; } - -- (void)set_alignSelf:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView +RCT_CUSTOM_SHADOW_PROPERTY(alignSelf, RCTShadowView) { - shadowView.alignSelf = json ? [RCTConvert css_align_t:json] : defaultView.alignSelf; + view.alignSelf = json ? [RCTConvert css_align_t:json] : defaultView.alignSelf; } - -- (void)set_position:(id)json - forShadowView:(RCTShadowView *)shadowView - withDefaultView:(RCTShadowView *)defaultView +RCT_CUSTOM_SHADOW_PROPERTY(position, RCTShadowView) { - shadowView.positionType = json ? [RCTConvert css_position_type_t:json] : defaultView.positionType; + view.positionType = json ? [RCTConvert css_position_type_t:json] : defaultView.positionType; } @end diff --git a/ReactKit/Base/RCTViewNodeProtocol.h b/ReactKit/Views/RCTViewNodeProtocol.h similarity index 59% rename from ReactKit/Base/RCTViewNodeProtocol.h rename to ReactKit/Views/RCTViewNodeProtocol.h index 1fa3e252b..71422b2c8 100644 --- a/ReactKit/Base/RCTViewNodeProtocol.h +++ b/ReactKit/Views/RCTViewNodeProtocol.h @@ -1,18 +1,18 @@ // Copyright 2004-present Facebook. All Rights Reserved. /** - * Logical node in a tree of application components. Both `ShadowView`s and * `UIView+ReactKit`s conform to this. Allows us to write utilities that * reason about trees generally. */ @protocol RCTViewNodeProtocol -@property (nonatomic, strong) NSNumber *reactTag; +@property (nonatomic, copy) NSNumber *reactTag; - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex; - (void)removeReactSubview:(id)subview; - (NSMutableArray *)reactSubviews; +- (id)reactSuperview; - (NSNumber *)reactTagAtPoint:(CGPoint)point; // View is an RCTRootView @@ -21,6 +21,14 @@ @optional // TODO: Deprecate this +// This method is called after layout has been performed for all views known +// to the RCTViewManager. It is only called on UIViews, not shadow views. - (void)reactBridgeDidFinishTransaction; @end + +// TODO: this is kinda dumb - let's come up with a +// better way of identifying root react views please! +static inline BOOL RCTIsReactRootView(NSNumber *reactTag) { + return reactTag.integerValue % 10 == 1; +} diff --git a/ReactKit/Views/RCTWebView.h b/ReactKit/Views/RCTWebView.h new file mode 100644 index 000000000..ec3c9d6eb --- /dev/null +++ b/ReactKit/Views/RCTWebView.h @@ -0,0 +1,20 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTView.h" + +@class RCTEventDispatcher; + +@interface RCTWebView : RCTView + +@property (nonatomic, strong) NSURL *URL; +@property (nonatomic, assign) UIEdgeInsets contentInset; +@property (nonatomic, assign) BOOL shouldInjectAJAXHandler; +@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; + +- (void)goForward; +- (void)goBack; +- (void)reload; + +@end diff --git a/ReactKit/Views/RCTWebView.m b/ReactKit/Views/RCTWebView.m new file mode 100644 index 000000000..cc5a07578 --- /dev/null +++ b/ReactKit/Views/RCTWebView.m @@ -0,0 +1,180 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTWebView.h" + +#import + +#import "RCTAutoInsetsProtocol.h" +#import "RCTEventDispatcher.h" +#import "RCTLog.h" +#import "RCTUtils.h" +#import "RCTView.h" +#import "UIView+ReactKit.h" + +@interface RCTWebView () + +@end + +@implementation RCTWebView +{ + RCTEventDispatcher *_eventDispatcher; + UIWebView *_webView; +} + +- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +{ + if ((self = [super initWithFrame:CGRectZero])) { + _automaticallyAdjustContentInsets = YES; + _contentInset = UIEdgeInsetsZero; + _eventDispatcher = eventDispatcher; + _webView = [[UIWebView alloc] initWithFrame:self.bounds]; + _webView.delegate = self; + [self addSubview:_webView]; + } + return self; +} + +- (void)goForward +{ + [_webView goForward]; +} + +- (void)goBack +{ + [_webView goBack]; +} + +- (void)reload +{ + [_webView reload]; +} + +- (void)setURL:(NSURL *)URL +{ + // Because of the way React works, as pages redirect, we actually end up + // passing the redirect urls back here, so we ignore them if trying to load + // the same url. We'll expose a call to 'reload' to allow a user to load + // the existing page. + if ([URL isEqual:_webView.request.URL]) { + return; + } + if (!URL) { + // Clear the webview + [_webView loadHTMLString:nil baseURL:nil]; + return; + } + [_webView loadRequest:[NSURLRequest requestWithURL:URL]]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + _webView.frame = self.bounds; + [RCTView autoAdjustInsetsForView:self + withScrollView:_webView.scrollView + updateOffset:YES]; +} + +- (void)setContentInset:(UIEdgeInsets)contentInset +{ + _contentInset = contentInset; + [RCTView autoAdjustInsetsForView:self + withScrollView:_webView.scrollView + updateOffset:NO]; +} + +- (NSMutableDictionary *)baseEvent +{ + NSURL *url = _webView.request.URL; + NSString *title = [_webView stringByEvaluatingJavaScriptFromString:@"document.title"]; + NSMutableDictionary *event = [[NSMutableDictionary alloc] initWithDictionary: @{ + @"target": self.reactTag, + @"url": url ? [url absoluteString] : @"", + @"loading" : @(_webView.loading), + @"title": title, + @"canGoBack": @([_webView canGoBack]), + @"canGoForward" : @([_webView canGoForward]), + }]; + + return event; +} + +#pragma mark - UIWebViewDelegate methods + +static NSString *const RCTJSAJAXScheme = @"react-ajax"; + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request + navigationType:(UIWebViewNavigationType)navigationType +{ + // We have this check to filter out iframe requests and whatnot + BOOL isTopFrame = [request.URL isEqual:request.mainDocumentURL]; + if (isTopFrame) { + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"url": [request.URL absoluteString], + @"navigationType": @(navigationType) + }]; + [_eventDispatcher sendInputEventWithName:@"topLoadingStart" body:event]; + } + + // AJAX handler + return ![request.URL.scheme isEqualToString:RCTJSAJAXScheme]; +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error +{ + if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) { + // NSURLErrorCancelled is reported when a page has a redirect OR if you load + // a new URL in the WebView before the previous one came back. We can just + // ignore these since they aren't real errors. + // http://stackoverflow.com/questions/1024748/how-do-i-fix-nsurlerrordomain-error-999-in-iphone-3-0-os + return; + } + + NSMutableDictionary *event = [self baseEvent]; + [event addEntriesFromDictionary: @{ + @"domain": error.domain, + @"code": @(error.code), + @"description": [error localizedDescription], + }]; + [_eventDispatcher sendInputEventWithName:@"topLoadingError" body:event]; +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView +{ + if (_shouldInjectAJAXHandler) { + + // From http://stackoverflow.com/questions/5353278/uiwebviewdelegate-not-monitoring-xmlhttprequest + + [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"\ + var s_ajaxListener = new Object(); \n\ + s_ajaxListener.tempOpen = XMLHttpRequest.prototype.open; \n\ + s_ajaxListener.tempSend = XMLHttpRequest.prototype.send; \n\ + s_ajaxListener.callback = function() { \n\ + window.location.href = '%@://' + this.url; \n\ + } \n\ + XMLHttpRequest.prototype.open = function(a,b) { \n\ + s_ajaxListener.tempOpen.apply(this, arguments); \n\ + s_ajaxListener.method = a; \n\ + s_ajaxListener.url = b; \n\ + if (a.toLowerCase() === 'get') { \n\ + s_ajaxListener.data = (b.split('?'))[1]; \n\ + } \n\ + } \n\ + XMLHttpRequest.prototype.send = function(a,b) { \n\ + s_ajaxListener.tempSend.apply(this, arguments); \n\ + if (s_ajaxListener.method.toLowerCase() === 'post') { \n\ + s_ajaxListener.data = a; \n\ + } \n\ + s_ajaxListener.callback(); \n\ + } \n\ + ", RCTJSAJAXScheme]]; + } + + // we only need the final 'finishLoad' call so only fire the event when we're actually done loading. + if (!webView.loading && ![webView.request.URL.absoluteString isEqualToString:@"about:blank"]) { + [_eventDispatcher sendInputEventWithName:@"topLoadingFinish" body:[self baseEvent]]; + } +} + +@end diff --git a/ReactKit/Views/RCTWebViewManager.h b/ReactKit/Views/RCTWebViewManager.h new file mode 100644 index 000000000..d375cbdab --- /dev/null +++ b/ReactKit/Views/RCTWebViewManager.h @@ -0,0 +1,7 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTViewManager.h" + +@interface RCTWebViewManager : RCTViewManager + +@end diff --git a/ReactKit/Views/RCTWebViewManager.m b/ReactKit/Views/RCTWebViewManager.m new file mode 100644 index 000000000..8e68d9eb8 --- /dev/null +++ b/ReactKit/Views/RCTWebViewManager.m @@ -0,0 +1,76 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#import "RCTWebViewManager.h" + +#import "RCTBridge.h" +#import "RCTSparseArray.h" +#import "RCTUIManager.h" +#import "RCTWebView.h" + +@implementation RCTWebViewManager + +- (UIView *)view +{ + return [[RCTWebView alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; +} + +RCT_REMAP_VIEW_PROPERTY(url, URL); +RCT_EXPORT_VIEW_PROPERTY(contentInset); +RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets); +RCT_EXPORT_VIEW_PROPERTY(shouldInjectAJAXHandler); + +- (NSDictionary *)constantsToExport +{ + return @{ + @"NavigationType": @{ + @"LinkClicked": @(UIWebViewNavigationTypeLinkClicked), + @"FormSubmitted": @(UIWebViewNavigationTypeFormSubmitted), + @"BackForward": @(UIWebViewNavigationTypeBackForward), + @"Reload": @(UIWebViewNavigationTypeReload), + @"FormResubmitted": @(UIWebViewNavigationTypeFormResubmitted), + @"Other": @(UIWebViewNavigationTypeOther) + }, + }; +} + +- (void)goBack:(NSNumber *)reactTag +{ + RCT_EXPORT(); + + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RKWebView, got: %@", view); + } + [view goBack]; + }]; +} + +- (void)goForward:(NSNumber *)reactTag +{ + RCT_EXPORT(); + + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWebView class]]) { + RCTLogError(@"Invalid view returned from registry, expecting RKWebView, got: %@", view); + } + [view goForward]; + }]; +} + + +- (void)reload:(NSNumber *)reactTag +{ + RCT_EXPORT(); + + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTWebView *view = viewRegistry[reactTag]; + if (![view isKindOfClass:[RCTWebView class]]) { + RCTLogMustFix(@"Invalid view returned from registry, expecting RKWebView, got: %@", view); + } + [view reload]; + }]; +} + +@end diff --git a/ReactKit/Views/RCTWrapperViewController.h b/ReactKit/Views/RCTWrapperViewController.h index d8f22270a..afe736430 100644 --- a/ReactKit/Views/RCTWrapperViewController.h +++ b/ReactKit/Views/RCTWrapperViewController.h @@ -2,6 +2,8 @@ #import +#import "RCTViewControllerProtocol.h" + @class RCTEventDispatcher; @class RCTNavItem; @class RCTWrapperViewController; @@ -13,12 +15,15 @@ didMoveToNavigationController:(UINavigationController *)navigationController; @end -@interface RCTWrapperViewController : UIViewController +@interface RCTWrapperViewController : UIViewController -- (instancetype)initWithContentView:(UIView *)contentView eventDispatcher:(RCTEventDispatcher *)eventDispatcher; -- (instancetype)initWithNavItem:(RCTNavItem *)navItem eventDispatcher:(RCTEventDispatcher *)eventDispatcher; +- (instancetype)initWithContentView:(UIView *)contentView + eventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; -@property (nonatomic, readwrite, weak) id navigationListener; -@property (nonatomic, strong, readwrite) RCTNavItem *navItem; +- (instancetype)initWithNavItem:(RCTNavItem *)navItem + eventDispatcher:(RCTEventDispatcher *)eventDispatcher; + +@property (nonatomic, weak) id navigationListener; +@property (nonatomic, strong) RCTNavItem *navItem; @end diff --git a/ReactKit/Views/RCTWrapperViewController.m b/ReactKit/Views/RCTWrapperViewController.m index d027dc2f2..f05a1f3c2 100644 --- a/ReactKit/Views/RCTWrapperViewController.m +++ b/ReactKit/Views/RCTWrapperViewController.m @@ -2,27 +2,30 @@ #import "RCTWrapperViewController.h" +#import + #import "RCTEventDispatcher.h" #import "RCTNavItem.h" #import "RCTUtils.h" +#import "RCTViewControllerProtocol.h" #import "UIView+ReactKit.h" @implementation RCTWrapperViewController { + UIView *_wrapperView; UIView *_contentView; RCTEventDispatcher *_eventDispatcher; CGFloat _previousTopLayout; CGFloat _previousBottomLayout; } -- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil -{ - RCT_NOT_DESIGNATED_INITIALIZER(); -} +@synthesize currentTopLayoutGuide = _currentTopLayoutGuide; +@synthesize currentBottomLayoutGuide = _currentBottomLayoutGuide; -- (instancetype)initWithContentView:(UIView *)contentView eventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithContentView:(UIView *)contentView + eventDispatcher:(RCTEventDispatcher *)eventDispatcher { - if ((self = [super initWithNibName:nil bundle:nil])) { + if (self = [super initWithNibName:nil bundle:nil]) { _contentView = contentView; _eventDispatcher = eventDispatcher; self.automaticallyAdjustsScrollViewInsets = NO; @@ -30,80 +33,91 @@ return self; } -- (instancetype)initWithNavItem:(RCTNavItem *)navItem eventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (instancetype)initWithNavItem:(RCTNavItem *)navItem + eventDispatcher:(RCTEventDispatcher *)eventDispatcher { - if ((self = [self initWithContentView:navItem eventDispatcher:eventDispatcher])) { + if (self = [self initWithContentView:navItem eventDispatcher:eventDispatcher]) { _navItem = navItem; } return self; } +- (void)viewWillLayoutSubviews +{ + [super viewWillLayoutSubviews]; + + _currentTopLayoutGuide = self.topLayoutGuide; + _currentBottomLayoutGuide = self.bottomLayoutGuide; +} + - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - [self.navigationController setNavigationBarHidden:!_navItem animated:animated]; - if (!_navItem) { - return; - } + // TODO: find a way to make this less-tightly coupled to navigation controller + if ([self.parentViewController isKindOfClass:[UINavigationController class]]) + { - self.navigationItem.title = _navItem.title; - - [self _configureNavBarStyle]; - - if (_navItem.rightButtonTitle.length > 0) { - self.navigationItem.rightBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.rightButtonTitle - style:UIBarButtonItemStyleDone - target:self - action:@selector(rightButtonTapped)]; - } - - if (_navItem.backButtonTitle.length > 0) { - self.navigationItem.backBarButtonItem = - [[UIBarButtonItem alloc] initWithTitle:_navItem.backButtonTitle - style:UIBarButtonItemStylePlain - target:nil - action:nil]; - } -} - -- (void)_configureNavBarStyle -{ - UINavigationBar *bar = self.navigationController.navigationBar; - if (_navItem.barTintColor) { - bar.barTintColor = _navItem.barTintColor; - } - if (_navItem.tintColor) { - BOOL canSetTintColor = _navItem.barTintColor == nil; - if (canSetTintColor) { - bar.tintColor = _navItem.tintColor; + [self.navigationController setNavigationBarHidden:!_navItem animated:animated]; + if (!_navItem) { + return; + } + + self.navigationItem.title = _navItem.title; + + UINavigationBar *bar = self.navigationController.navigationBar; + if (_navItem.barTintColor) { + bar.barTintColor = _navItem.barTintColor; + } + if (_navItem.tintColor) { + BOOL canSetTintColor = _navItem.barTintColor == nil; + if (canSetTintColor) { + bar.tintColor = _navItem.tintColor; + } + } + if (_navItem.titleTextColor) { + [bar setTitleTextAttributes:@{NSForegroundColorAttributeName : _navItem.titleTextColor}]; + } + + if (_navItem.rightButtonTitle.length > 0) { + self.navigationItem.rightBarButtonItem = + [[UIBarButtonItem alloc] initWithTitle:_navItem.rightButtonTitle + style:UIBarButtonItemStyleDone + target:self + action:@selector(handleNavRightButtonTapped)]; + } + + if (_navItem.backButtonTitle.length > 0) { + self.navigationItem.backBarButtonItem = + [[UIBarButtonItem alloc] initWithTitle:_navItem.backButtonTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; } - } - if (_navItem.titleTextColor) { - [bar setTitleTextAttributes:@{NSForegroundColorAttributeName : _navItem.titleTextColor}]; } } - (void)loadView { - // Add a wrapper so that UIViewControllerWrapperView (managed by the - // UINavigationController) doesn't end up resetting the frames for - // `contentView` which is a react-managed view. - self.view = [[UIView alloc] init]; - [self.view addSubview:_contentView]; + // Add a wrapper so that the wrapper view managed by the + // UINavigationController doesn't end up resetting the frames for + //`contentView` which is a react-managed view. + _wrapperView = [[UIView alloc] initWithFrame:_contentView.bounds]; + [_wrapperView addSubview:_contentView]; + self.view = _wrapperView; } -- (void)rightButtonTapped +- (void)handleNavRightButtonTapped { - [_eventDispatcher sendInputEventWithName:@"topNavRightButtonTap" body:@{@"target":_navItem.reactTag}]; + [_eventDispatcher sendInputEventWithName:@"topNavRightButtonTap" + body:@{@"target":_navItem.reactTag}]; } - (void)didMoveToParentViewController:(UIViewController *)parent { - // There's no clear setter for navigation controllers, but did move to parent view controller - // provides the desired effect. This is called after a pop finishes, be it a swipe to go back - // or a standard tap on the back button + // There's no clear setter for navigation controllers, but did move to parent + // view controller provides the desired effect. This is called after a pop + // finishes, be it a swipe to go back or a standard tap on the back button [super didMoveToParentViewController:parent]; if (parent == nil || [parent isKindOfClass:[UINavigationController class]]) { [self.navigationListener wrapperViewController:self didMoveToNavigationController:(UINavigationController *)parent]; diff --git a/ReactKit/Views/UIView+ReactKit.h b/ReactKit/Views/UIView+ReactKit.h index 3e45da8e7..9f0213a63 100644 --- a/ReactKit/Views/UIView+ReactKit.h +++ b/ReactKit/Views/UIView+ReactKit.h @@ -8,4 +8,17 @@ @interface UIView (ReactKit) +/** + * This method finds and returns the containing view controller for the view. + */ +- (UIViewController *)backingViewController; + +/** + * This method attaches the specified controller as a child of the + * the owning view controller of this view. Returns NO if no view + * controller is found (which may happen if the view is not currently + * attached to the view hierarchy). + */ +- (void)addControllerToClosestParent:(UIViewController *)controller; + @end diff --git a/ReactKit/Views/UIView+ReactKit.m b/ReactKit/Views/UIView+ReactKit.m index 39bca8ec6..a9feb8eed 100644 --- a/ReactKit/Views/UIView+ReactKit.m +++ b/ReactKit/Views/UIView+ReactKit.m @@ -5,6 +5,7 @@ #import #import "RCTAssert.h" +#import "RCTWrapperViewController.h" @implementation UIView (ReactKit) @@ -20,7 +21,7 @@ - (BOOL)isReactRootView { - return NO; + return RCTIsReactRootView(self.reactTag); } - (NSNumber *)reactTagAtPoint:(CGPoint)point @@ -39,7 +40,7 @@ - (void)removeReactSubview:(UIView *)subview { - RCTAssert(subview.superview == self, @""); + RCTAssert(subview.superview == self, @"%@ is a not a subview of %@", subview, self); [subview removeFromSuperview]; } @@ -48,4 +49,34 @@ return self.subviews; } +- (UIView *)reactSuperview +{ + return self.superview; +} + +- (UIViewController *)backingViewController +{ + id responder = [self nextResponder]; + if ([responder isKindOfClass:[RCTWrapperViewController class]]) { + return responder; + } + return nil; +} + +- (void)addControllerToClosestParent:(UIViewController *)controller +{ + if (!controller.parentViewController) { + UIView *parentView = (UIView *)self.reactSuperview; + while (parentView) { + if (parentView.backingViewController) { + [parentView.backingViewController addChildViewController:controller]; + [controller didMoveToParentViewController:parentView.backingViewController]; + break; + } + parentView = (UIView *)parentView.reactSuperview; + } + return; + } +} + @end diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..22b332a13 --- /dev/null +++ b/build.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +xctool \ + -project IntegrationTests/IntegrationTests.xcodeproj \ + -scheme IntegrationTests \ + -sdk iphonesimulator8.1 \ + -destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \ + build test + +xctool \ + -project Examples/UIExplorer/UIExplorer.xcodeproj \ + -scheme UIExplorer \ + -sdk iphonesimulator8.1 \ + -destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \ + build test diff --git a/cli.js b/cli.js new file mode 100644 index 000000000..99a3b24e0 --- /dev/null +++ b/cli.js @@ -0,0 +1,48 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ + +'use strict'; + +var spawn = require('child_process').spawn; +var path = require('path'); + +function printUsage() { + console.log([ + 'Usage: react-native ', + '', + 'Commands:', + ' start: starts the webserver', + ].join('\n')); + process.exit(1); +} + +function run() { + var args = process.argv.slice(2); + if (args.length === 0) { + printUsage(); + } + + switch (args[0]) { + case 'start': + spawn('sh', [ + path.resolve(__dirname, 'packager', 'packager.sh'), + '--projectRoots', + process.cwd(), + ], {stdio: 'inherit'}); + break; + default: + console.error('Command `%s` unrecognized', args[0]); + printUsage(); + } + // Here goes any cli commands we need to +} + +function init(root, projectName) { + spawn(path.resolve(__dirname, 'init.sh'), [projectName], {stdio:'inherit'}); +} + +module.exports = { + run: run, + init: init, +}; diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md index eda46461b..5ff207167 100644 --- a/docs/GettingStarted.md +++ b/docs/GettingStarted.md @@ -4,10 +4,9 @@ title: Getting Started layout: docs category: Quick Start permalink: docs/getting-started.html -next: navigatorios +next: videos --- - Our first React Native implementation is `ReactKit`, targeting iOS. We are also working on an Android implementation which we will release later. `ReactKit` apps are built using the [React JS](https://github.com/facebook/react) framework, and render directly to diff --git a/docs/Image.md b/docs/Image.md index 55b10bb40..2ccc915d1 100644 --- a/docs/Image.md +++ b/docs/Image.md @@ -8,7 +8,7 @@ In React Native, this behavior is intentionally not implemented. It is more work ## Background Image via Nesting -A common feature request from developers familiar with the web is `background-image`. It turns out that iOS has a very elegant solution to this: you can add elements as a children to an `` component. This simplifies the API and solves the use case. +A common feature request from developers familiar with the web is `background-image`. To handle this use case, simply create a normal `` component and add whatever children to it you would like to layer on top of it. ```javascript return ( @@ -24,20 +24,18 @@ Image decoding can take more than a frame-worth of time. This is one of the majo ## Static Assets -In the course of a project you add and remove images and in many instances, you end up shipping images you are not using anymore in the app. In order to fight this, we need to find a way to know statically which images are being used in the app. To do that, we introduced a marker called `ix`. The only allowed way to refer to an image in the bundle is to literally write `ix('name-of-the-asset')` in the source. +In the course of a project, it is not uncommon to add and remove many images and accidentally end up shipping images you are no longer using in the app. In order to fight this, we need to find a way to know statically which images are being used in the app. To do that, we introduced a marker on require. The only allowed way to refer to an image in the bundle is to literally write `require('image!name-of-the-asset')` in the source. ```javascript -var { ix } = React; - // GOOD - + // BAD var icon = this.props.active ? 'my-icon-active' : 'my-icon-inactive'; - + // GOOD -var icon = this.props.active ? ix('my-icon-active') : ix('my-icon-inactive'); +var icon = this.props.active ? require('image!my-icon-active') : require('image!my-icon-inactive'); ``` @@ -55,6 +53,6 @@ In React Native, one interesting decision is that the `src` attribute is named ` ``` -On the infrastructure side, the reason is that it allows to attach metadata to this object. For example if you are using `ix`, then we add a `isStored` attribute to flag it as a local file (don't rely on this fact, it's likely to change in the future!). This is also future proofing, for example we may want to support sprites at some point, instead of outputting `{uri: ...}`, we can output `{uri: ..., crop: {left: 10, top: 50, width: 20, height: 40}}` and transparently support spriting on all the existing call sites. +On the infrastructure side, the reason is that it allows to attach metadata to this object. For example if you are using `require('image!icon')`, then we add an `isStatic` attribute to flag it as a local file (don't rely on this fact, it's likely to change in the future!). This is also future proofing, for example we may want to support sprites at some point, instead of outputting `{uri: ...}`, we can output `{uri: ..., crop: {left: 10, top: 50, width: 20, height: 40}}` and transparently support spriting on all the existing call sites. On the user side, this lets you annotate the object with useful attributes such as the dimension of the image in order to compute the size it's going to be displayed in. Feel free to use it as your data structure to store more information about your image. diff --git a/docs/NativeModulesIOS.md b/docs/NativeModulesIOS.md new file mode 100644 index 000000000..d0b0fdf84 --- /dev/null +++ b/docs/NativeModulesIOS.md @@ -0,0 +1,200 @@ +--- +id: nativemodulesios +title: Native Modules (iOS) +layout: docs +category: Guides +permalink: docs/nativemodulesios.html +next: activityindicatorios +--- + +Sometimes an app needs access to platform API, and React Native doesn't have a corresponding wrapper yet. Maybe you want to reuse some existing Objective-C or C++ code without having to reimplement it in JavaScript. Or write some high performance, multi-threaded code such as image processing, network stack, database or rendering. + +We designed React Native such that it is possible for you to write real native code and have access to the full power of the platform. This is a more advanced feature and we don't expect it to be part of the usual development process, however it is essential that it exists. If React Native doesn't support a native feature that you need, you should be able to build it yourself. + +This is a more advanced guide that shows how to build a native module. It assumes the reader knows Objective-C (Swift is not supported yet) and core libraries (Foundation, UIKit). + +## iOS Calendar module example + +This guide will use [iOS Calendar API](https://developer.apple.com/library/mac/documentation/DataManagement/Conceptual/EventKitProgGuide/Introduction/Introduction.html) example. Let's say we would like to be able to access iOS calendar from JavaScript. + +Native module is just an Objectve-C class that implements `RCTBridgeModule` protocol. If you are wondering, RCT is a shorthand for ReaCT. + +```objective-c +// CalendarManager.h +#import "RCTBridgeModule.h" + +@interface CalendarManager : NSObject +@end +``` + +React Native will not expose any methods of `CalendarManager` to JavaScript unless explicitly asked. Fortunately this is pretty easy with `RCT_EXPORT`: + +```objective-c +// CalendarManager.m +@implementation CalendarManager + +- (void)addEventWithName:(NSString *)name location:(NSString *)location +{ + RCT_EXPORT(); + RCTLogInfo(@"Pretending to create an event %@ at %@", name, location); +} + +@end +``` + +Now from your JavaScript file you can call the method like this: + +```javascript +var CalendarManager = require('NativeModules').CalendarManager; +CalendarManager.addEventWithName('Birthday Party', '4 Privet Drive, Surrey'); +``` + +Notice that the exported method name was generated from first part of Objective-C selector. Sometimes it results in a non-idiomatic JavaScript name (like the one in our example). You can change the name by supplying an optional argument to `RCT_EXPORT`, e.g. `RCT_EXPORT(addEvent)`. + +The return type of the method should always be `void`. React Native bridge is asynchronous, so the only way to pass result to JavaScript is by using callbacks or emitting events (see below). + +## Argument types + +React Native supports several types of arguments that can be passed from JavaScript code to native module: + +- string (`NSString`) +- number (`NSInteger`, `float`, `double`, `CGFloat`, `NSNumber`) +- boolean (`BOOL`, `NSNumber`) +- array (`NSArray`) of any types from this list +- map (`NSDictionary`) with string keys and values of any type from this list +- function (`RCTResponseSenderBlock`) + +In our `CalendarManager` example, if we want to pass event date to native, we have to convert it to a string or a number: + +```objective-c +- (void)addEventWithName:(NSString *)name location:(NSString *)location date:(NSInteger)secondsSinceUnixEpoch +{ + RCT_EXPORT(addEvent); + NSDate *date = [NSDate dateWithTimeIntervalSince1970:secondsSinceUnixEpoch]; +} +``` + +As `CalendarManager.addEvent` method gets more and more complex, the number of arguments will grow. Some of them might be optional. In this case it's worth considering changing the API a little bit to accept a dictionary of event attributes, like this: + +```objective-c +- (void)addEventWithName:(NSString *)name details:(NSDictionary *)details +{ + RCT_EXPORT(addEvent); + NSString *location = [RCTConvert NSString:details[@"location"]]; // ensure location is a string + ... +} +``` + +and call it from JavaScript: + +```javascript +CalendarManager.addEvent('Birthday Party', { + location: '4 Privet Drive, Surrey', + time: date.toTime(), + description: '...' +}) +``` + +> **NOTE**: About array and map +> +> React Native doesn't provide any guarantees about the types of values in these structures. Your native module might expect array of strings, but if JavaScript calls your method with an array that contains number and string you'll get `NSArray` with `NSNumber` and `NSString`. It's developer's responsibility to check array/map values types (see [`RCTConvert`](https://github.com/facebook/react-native/blob/master/ReactKit/Base/RCTConvert.h) for helper methods). + +# Callbacks + +> **WARNING** +> +> This section is even more experimental than others, we don't have a set of best practices around callbacks yet. + +Native module also supports a special kind of argument - callback. In most cases it is used to provide function call result to JavaScript. + +```objective-c +- (void)findEvents:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(); + NSArray *events = ... + callback(@[[NSNull null], events]); +} +``` + +`RCTResponseSenderBlock` accepts only one argument - array of arguments to pass to JavaScript callback. In this case we use node's convention to set first argument to error and the rest - to the result of the function. + +```javascript +CalendarManager.findEvents((error, events) => { + if (error) { + console.error(error); + } else { + this.setState({events: events}); + } +}) +``` + +Native module is supposed to invoke callback only once. It can, however, store the callback as an ivar and invoke it later. This pattern is often used to wrap iOS APIs that require delegate. See [`RCTAlertManager`](https://github.com/facebook/react-native/blob/master/ReactKit/Modules/RCTAlertManager.m). + +If you want to pass error-like object to JavaScript, use `RCTMakeError` from [`RCTUtils.h`](https://github.com/facebook/react-native/blob/master/ReactKit/Base/RCTUtils.h). + +## Implementing native module + +The native module should not have any assumptions about what thread it is being called on. React Native invokes native modules methods on a separate serial GCD queue, but this is an implementation detail and might change. If the native module needs to call main-thread-only iOS API, it should schedule the operation on the main queue: + + +```objective-c +- (void)addEventWithName:(NSString *)name callback:(RCTResponseSenderBlock)callback +{ + RCT_EXPORT(addEvent); + dispatch_async(dispatch_get_main_queue(), ^{ + // Call iOS API on main thread + ... + // You can invoke callback from any thread/queue + callback(@[...]); + }); +} +``` + +The same way if the operation can take a long time to complete, the native module should not block. It is a good idea to use `dispatch_async` to schedule expensive work on background queue. + +## Exporting constants + +Native module can export constants that are instantly available to JavaScript at runtime. This is useful to export some initial data that would otherwise require a bridge round-trip. + +```objective-c +- (NSDictionary *)constantsToExport +{ + return @{ @"firstDayOfTheWeek": @"Monday" }; +} +``` + +JavaScript can use this value right away: + +```javascript +console.log(CalendarManager.firstDayOfTheWeek); +``` + +Note that the constants are exported only at initialization time, so if you change `constantsToExport` value at runtime it won't affect JavaScript environment. + + +## Sending events to JavaScript + +The native module can signal events to JavaScript without being invoked directly. The easiest way to do this is to use `eventDispatcher`: + +```objective-c +- (void)calendarEventReminderReceived:(NSNotification *)notification +{ + NSString *eventName = notification.userInfo[@"name"]; + [self.bridge.eventDispatcher sendAppEventWithName:@"EventReminder" + body:@{@"name": eventName}]; +} +``` + +JavaScript code can subscribe to these events: + +```javascript +var subscription = DeviceEventEmitter.addListener( + 'EventReminder', + (reminder) => console.log(reminder.name) +); +... +// Don't forget to unsubscribe +subscription.remove(); +``` +For more examples of sending events to JavaScript, see [`RCTLocationObserver`](https://github.com/facebook/react-native/blob/master/Libraries/Geolocation/RCTLocationObserver.m). + diff --git a/docs/Network.md b/docs/Network.md index 831adccd2..f2e8da23e 100644 --- a/docs/Network.md +++ b/docs/Network.md @@ -1,8 +1,17 @@ -One of React Native goal is to be a playground where we can experiment with different architectures and crazy ideas. Since browsers are not flexible enough, we had no choice but to reimplement the entire stack. In the places that we did not intend to change, we tried to be as faithful as possible to the browser APIs, the networking stack is a great example. +--- +id: network +title: Network +layout: docs +category: Polyfills +permalink: docs/network.html +next: timers +--- + +One of React Native goal is to be a playground where we can experiment with different architectures and crazy ideas. Since browsers are not flexible enough, we had no choice but to reimplement the entire stack. In the places that we did not intend to change, we tried to be as faithful as possible to the browser APIs. The networking stack is a great example. ## XMLHttpRequest -XMLHttpRequest API is implemented on-top of [iOS networking apis](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html). The notable difference from web is the security model: you can read from arbitrary websites on the internet, there isn't no concept of [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing). +XMLHttpRequest API is implemented on-top of [iOS networking apis](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html). The notable difference from web is the security model: you can read from arbitrary websites on the internet since there is no concept of [CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing). ```javascript var request = new XMLHttpRequest(); @@ -22,13 +31,13 @@ request.open('GET', 'https://mywebsite.com/endpoint.php'); request.send(); ``` -Please follow the [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) for a description of the API. +Please follow the [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) for a complete description of the API. -As a developer, you're probably not going to use XMLHttpRequest directly as its API is very tedious to work with. But the fact that it is implemented and compatible with the browser one gives you the ability to use third-party libraries such as [Parse JS SDK](https://parse.com/docs/js_guide) or [super-agent](https://github.com/visionmedia/superagent) directly from npm. +As a developer, you're probably not going to use XMLHttpRequest directly as its API is very tedious to work with. But the fact that it is implemented and compatible with the browser API gives you the ability to use third-party libraries such as [Parse]( https://parse.com/products/javascript) or [super-agent](https://github.com/visionmedia/superagent) directly from npm. ## Fetch -[fetch](https://fetch.spec.whatwg.org/) is a better API being worked on by the standard committee and already available in Chrome. It is available in React Native by default. +[fetch](https://fetch.spec.whatwg.org/) is a better networking API being worked on by the standard committee and is already available in Chrome. It is available in React Native by default. ```javascript fetch('https://mywebsite.com/endpoint.php') diff --git a/docs/Physical vs Logical Pixels.md b/docs/Physical vs Logical Pixels.md index 25ba6e884..0aac1e01c 100644 --- a/docs/Physical vs Logical Pixels.md +++ b/docs/Physical vs Logical Pixels.md @@ -1,8 +1,16 @@ +--- +id: pixels +title: Physical vs Logical Pixels +layout: docs +category: Guides +permalink: docs/pixels.html +next: style +--- ## Pixel Grid Snapping -In iOS, you can specify positions and dimensions for elements with arbitrary precision, for example 29.674825. But, ultimately the physical display only have a fixed number of pixels, for example 640×960 for iphone 4 or 750×1334 for iphone 6. iOS tries to be as faithful as possible to the user value by spreading one original pixel into multiple ones to be trick the eye. The downside of this technique is that it makes the resulting element look blurry. +In iOS, you can specify positions and dimensions for elements with arbitrary precision, for example 29.674825. But, ultimately the physical display only have a fixed number of pixels, for example 640×960 for iphone 4 or 750×1334 for iphone 6. iOS tries to be as faithful as possible to the user value by spreading one original pixel into multiple ones to trick the eye. The downside of this technique is that it makes the resulting element look blurry. In practice, we found out that developers do not want this feature and they have to work around it by doing manual rounding in order to avoid having blurry elements. In React Native, we are rounding all the pixels automatically. @@ -12,7 +20,7 @@ In React Native, everything in JS and within the layout engine work with arbitra ## Displaying a line that's as thin as the device permits -A width of 1 is actually 2 physical pixels thick on an iPhone 4 and ~3 physical pixels thick on an iphone 6+. If you want to display a line that's as thin as possible, you can use a width of `1 / PixelRatio.get()`. It's a technique that works on all the devices independent of their pixel density. +A width of 1 is actually 2 physical pixels thick on an iPhone 4 and 3 physical pixels thick on an iphone 6+. If you want to display a line that's as thin as possible, you can use a width of `1 / PixelRatio.get()`. It's a technique that works on all the devices independent of their pixel density. ```javascript style={{ borderWidth: 1 / PixelRatio.get() }} diff --git a/docs/Style.md b/docs/Style.md index b5738944b..f4bfdf286 100644 --- a/docs/Style.md +++ b/docs/Style.md @@ -1,3 +1,11 @@ +--- +id: style +title: Style +layout: docs +category: Guides +permalink: docs/style.html +next: nativemodulesios +--- ## Declaring Styles diff --git a/docs/Text.md b/docs/Text.md index f830582c9..a40f2d97b 100644 --- a/docs/Text.md +++ b/docs/Text.md @@ -13,7 +13,7 @@ In iOS, the way to display formatted text is by using `NSAttributedString`: you ``` -Behind the scenes, this is going to be converted to a flat NSAttributedString that contains the following information +Behind the scenes, this is going to be converted to a flat `NSAttributedString` that contains the following information ```javascript "I am bold and red" @@ -23,7 +23,7 @@ Behind the scenes, this is going to be converted to a flat NSAttributedString th ## Containers -The `` element is special relative to layout, everything inside is no longer using the flexbox layout but using text layout. This means that elements inside of a `` are no longer rectangles but wrap when they see the end of the line. +The `` element is special relative to layout: everything inside is no longer using the flexbox layout but using text layout. This means that elements inside of a `` are no longer rectangles, but wrap when they see the end of the line. ```javascript @@ -47,7 +47,7 @@ The `` element is special relative to layout, everything inside is no long ## Limited Style Inheritance -On the web, the usual way to set a font family and size for the entire document is to write +On the web, the usual way to set a font family and size for the entire document is to write: ```css /* CSS, *not* React Native */ @@ -58,9 +58,9 @@ html { } ``` -When the browser is trying to render a text node, it's going to go all the way up to the root element of the tree and find an element with a `font-size` attribute. An unexpected property with this system is that **any** node can have `font-size` attribute, including a `
`. The reason why it was designed this way is that it is convenient, even though not really semantically correct. +When the browser is trying to render a text node, it's going to go all the way up to the root element of the tree and find an element with a `font-size` attribute. An unexpected property of this system is that **any** node can have `font-size` attribute, including a `
`. This was designed for convenience, even though not really semantically correct. -In React Native, we are more strict about it. The first place where it'll show up is that you have to wrap all the text nodes inside of a `` component. It is not allowed to have a text node directly under a ``. +In React Native, we are more strict about it: **you must wrap all the text nodes inside of a `` component**; you cannot have a text node directly under a ``. ```javascript // BAD: will fatal, can't have a text node as child of a @@ -76,7 +76,7 @@ In React Native, we are more strict about it. The first place where it'll show u ``` -You also lose the ability to setup a default font for an entire subtree. The recommended way to use consistent fonts and sizes across your application is to create a component `MyAppText` that's going to set them and use this component all across your app. You can also make other components such as `MyAppHeaderText` for other kind of texts. +You also lose the ability to set up a default font for an entire subtree. The recommended way to use consistent fonts and sizes across your application is to create a component `MyAppText` that includes them and use this component across your app. You can also use this component to make more specific components like `MyAppHeaderText` for other kinds of text. ```javascript @@ -96,8 +96,8 @@ React Native still has the concept of style inheritance, but limited to text sub ``` -We believe that this more constrained way to style text will yield better apps. +We believe that this more constrained way to style text will yield better apps: -- (Developper) React components are designed with strong isolation properties in mind, you should be able to drop a component anywhere in your application and it will look and behave the same way, as long as the props are the same. Having text properties be inherited from outside of the props breaks isolation. +- (Developer) React components are designed with strong isolation in mind: You should be able to drop a component anywhere in your application, trusting that as long as the props are the same, it will look and behave the same way. Text properties that could inherit from outside of the props would break this isolation. -- (Implementor) The implementation of React Native is also simplified. We do not need to have a `fontFamily` field on every single element and we do not need to potentially traverse the tree up to the root every time we display a text node. The style inheritance is only encoded inside of the native Text component and doesn't leak to other components or the system itself. +- (Implementor) The implementation of React Native is also simplified. We do not need to have a `fontFamily` field on every single element, and we do not need to potentially traverse the tree up to the root every time we display a text node. The style inheritance is only encoded inside of the native Text component and doesn't leak to other components or the system itself. diff --git a/docs/Timers.md b/docs/Timers.md index 5cec2df9a..b7cfbb8b0 100644 --- a/docs/Timers.md +++ b/docs/Timers.md @@ -1,3 +1,11 @@ +--- +id: timers +title: Timers +layout: docs +category: Polyfills +permalink: docs/timers.html +--- + Timers are an important part of an application and React Native implements the [browser timers](https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Timers). ## Timers @@ -11,12 +19,12 @@ Timers are an important part of an application and React Native implements the [ `setImmediate` is executed at the end of the current JavaScript execution block, right before sending the batched response back to native. Note that if you call `setImmediate` within a `setImmediate` callback, it will be executed right away, it won't yield back to native in between. -The `Promise` implementation uses `setImmediate` its asynchronicity primitive. +The `Promise` implementation uses `setImmediate` as its asynchronicity primitive. ## InteractionManager -One reason why native apps feel so good performance wise is that barely any work is being done during an interaction/animation. In React Native, you can use `InteractionManager` that allows long-running work to be scheduled after any interactions/animations have completed. +One reason why well-built native apps feel so smooth is by avoiding expensive operations during interactions and animations. In React Native, we currently have a limitation that there is only a single JS execution thread, but you can use `InteractionManager` to make sure long-running work is scheduled to start after any interactions/animations have completed. Applications can schedule tasks to run after interactions with the following: @@ -50,7 +58,7 @@ InteractionManager.clearInteractionHandle(handle); We found out that the primary cause of fatals in apps created with React Native was due to timers firing after a component was unmounted. To solve this recurring issue, we introduced `TimerMixin`. If you include `TimerMixin`, then you can replace your calls to `setTimeout(fn, 500)` with `this.setTimeout(fn, 500)` (just prepend `this.`) and everything will be properly cleaned up for you when the component unmounts. ```javascript -var { TimerMixin } = React; +var TimerMixin = require('react-timer-mixin'); var Component = React.createClass({ mixins: [TimerMixin], diff --git a/docs/Videos.md b/docs/Videos.md new file mode 100644 index 000000000..b236b7daf --- /dev/null +++ b/docs/Videos.md @@ -0,0 +1,12 @@ +--- +id: videos +title: Videos +layout: docs +category: Community Resources +permalink: docs/videos.html +next: pixels +--- + + + + diff --git a/init.sh b/init.sh new file mode 100755 index 000000000..f74850d57 --- /dev/null +++ b/init.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env ruby + +def cp(src, dest, app_name) + if File.directory?(src) + Dir.mkdir(dest) unless Dir.exists?(dest) + else + content = File.read(src) + .gsub("SampleApp", app_name) + .gsub("Examples/#{app_name}/", "") + .gsub("../../Libraries/", "node_modules/react-native/Libraries/") + .gsub("../../ReactKit/", "node_modules/react-native/ReactKit/") + File.write(dest, content) + end +end + +def main(dest, app_name) + source = File.expand_path("../Examples/SampleApp", __FILE__) + files = Dir.chdir(source) { Dir["**/*"] } + .reject { |file| file["project.xcworkspace"] || file["xcuserdata"] } + .each { |file| + new_file = file.gsub("SampleApp", app_name) + cp File.join(source, file), File.join(dest, new_file), app_name + } +end + +if ARGV.count == 0 + puts "Usage: #{__FILE__} " + puts "" + puts "This script will bootstrap new React Native app in current folder" +else + app_name = ARGV.first + dest = Dir.pwd + puts "Setting up new React Native app in #{dest}" + puts "" + + main(dest, app_name) + + puts "Next steps:" + puts "" + puts " Open #{app_name}.xcproject in Xcode" + puts " Hit Run button" + puts "" +end + diff --git a/jestSupport/env.js b/jestSupport/env.js new file mode 100644 index 000000000..16f264feb --- /dev/null +++ b/jestSupport/env.js @@ -0,0 +1,5 @@ +'use strict'; + +global.setImmediate = global.setImmediate || function(fn) { + return setTimeout(fn, 0); +}; diff --git a/jestSupport/scriptPreprocess.js b/jestSupport/scriptPreprocess.js index 9231196c8..2764b35bd 100644 --- a/jestSupport/scriptPreprocess.js +++ b/jestSupport/scriptPreprocess.js @@ -2,16 +2,20 @@ var transformer = require('../packager/transformer.js'); -function transformSource(src) { - return transformer.transform(null, src).code; +function transformSource(src, filename) { + return transformer.transform(src, filename).code; } module.exports = { transformSource: transformSource, process: function(src, fileName) { + if (fileName.match(/node_modules/)) { + return src; + } + try { - return transformSource(src); + return transformSource(src, fileName); } catch(e) { throw new Error('\nError transforming file:\n js/' + (fileName.split('/js/')[1] || fileName) + ':' + e.lineNumber + ': \'' + diff --git a/lint/linterTransform.js b/lint/linterTransform.js new file mode 100644 index 000000000..6250db80f --- /dev/null +++ b/lint/linterTransform.js @@ -0,0 +1,76 @@ +'use strict'; + +var eslint = require('eslint'); + +var ignoredStylisticRules = { + 'key-spacing': false, + 'comma-spacing': true, + 'no-multi-spaces': true, + 'brace-style': true, + 'camelcase': true, + 'consistent-this': true, + 'eol-last': true, + 'func-names': true, + 'func-style': true, + 'new-cap': true, + 'new-parens': true, + 'no-nested-ternary': true, + 'no-array-constructor': true, + 'no-lonely-if': true, + 'no-new-object': true, + 'no-spaced-func': true, + 'no-space-before-semi': true, + 'no-ternary': true, + 'no-trailing-spaces': true, + 'no-underscore-dangle': true, + 'no-wrap-func': true, + 'no-mixed-spaces-and-tabs': true, + 'quotes': true, + 'quote-props': true, + 'semi': true, + 'sort-vars': true, + 'space-after-keywords': true, + 'space-in-brackets': true, + 'space-in-parens': true, + 'space-infix-ops': true, + 'space-return-throw-case': true, + 'space-unary-word-ops': true, + 'max-nested-callbacks': true, + 'one-var': true, + 'wrap-regex': true, + 'curly': true, + 'no-mixed-requires': true, +}; + +function setLinterTransform(transformSource) { + var originalVerify = eslint.linter.verify; + eslint.linter.verify = function(text, config, filename, saveState) { + var transformedText; + try { + transformedText = transformSource(text, filename); + } catch (e) { + return [{ + severity: 2, + line: e.lineNumber, + message: e.message, + source: text + }]; + } + var originalLines = text.split('\n'); + var transformedLines = transformedText.split('\n'); + var warnings = originalVerify.call(eslint.linter, transformedText, config, filename, saveState); + + // JSX and ES6 transforms usually generate pretty ugly code. Let's skip lint warnings + // about code style for lines that have been changed by transform step. + // Note that more important issues, like use of undefined vars, will still be reported. + return warnings.filter(function(error) { + var lineHasBeenTransformed = originalLines[error.line - 1] !== transformedLines[error.line - 1]; + var shouldIgnore = ignoredStylisticRules[error.ruleId] && lineHasBeenTransformed; + return !shouldIgnore; + }); + }; +} + +module.exports = { + setLinterTransform: setLinterTransform, +}; diff --git a/linter.js b/linter.js index ec7aa7c7a..205e757c1 100644 --- a/linter.js +++ b/linter.js @@ -1,83 +1,10 @@ // Copyright 2012-present Facebook. All Rights Reserved. 'use strict'; -var eslint = require('eslint'); var transformSource = require('./jestSupport/scriptPreprocess.js').transformSource; +var linterTransform = require('./lint/linterTransform'); -var ignoredStylisticRules = { - 'key-spacing': false, - 'comma-spacing': true, - 'no-multi-spaces': true, - 'brace-style': true, - 'camelcase': true, - 'consistent-this': true, - 'eol-last': true, - 'func-names': true, - 'func-style': true, - 'new-cap': true, - 'new-parens': true, - 'no-nested-ternary': true, - 'no-array-constructor': true, - 'no-lonely-if': true, - 'no-new-object': true, - 'no-spaced-func': true, - 'no-space-before-semi': true, - 'no-ternary': true, - 'no-trailing-spaces': true, - 'no-underscore-dangle': true, - 'no-wrap-func': true, - 'no-mixed-spaces-and-tabs': true, - 'quotes': true, - 'quote-props': true, - 'semi': true, - 'sort-vars': true, - 'space-after-keywords': true, - 'space-in-brackets': true, - 'space-in-parens': true, - 'space-infix-ops': true, - 'space-return-throw-case': true, - 'space-unary-word-ops': true, - 'max-nested-callbacks': true, - 'one-var': true, - 'wrap-regex': true, - 'curly': true, - 'no-mixed-requires': true, -}; - -/* - * Currently ESLint does not understand ES6+React-flavoured syntax. - * To make it work on our codebase, we monkey-patch `verify` function - * to do a transform before running lint rules. - * - * If future, as ESLint's support for ES6 expands, we can get rid of this - * hack - */ -var originalVerify = eslint.linter.verify; -eslint.linter.verify = function(text, config, filename, saveState) { - var transformedText; - try { - transformedText = transformSource(text); - } catch (e) { - return [{ - severity: 2, - line: e.lineNumber, - message: e.message, - source: text - }]; - } - var originalLines = text.split('\n'); - var transformedLines = transformedText.split('\n'); - var warnings = originalVerify.call(eslint.linter, transformedText, config, filename, saveState); - - // JSX and ES6 transforms usually generate pretty ugly code. Let's skip lint warnings - // about code style for lines that have been changed by transform step. - // Note that more important issues, like use of undefined vars, will still be reported. - return warnings.filter(function(error) { - var lineHasBeenTransformed = originalLines[error.line - 1] !== transformedLines[error.line - 1]; - var shouldIgnore = ignoredStylisticRules[error.ruleId] && lineHasBeenTransformed; - return !shouldIgnore; - }); -}; +linterTransform.setLinterTransform(transformSource); // Run the original CLI require('eslint/bin/eslint'); diff --git a/package.json b/package.json index 93a723ff5..38ac7fd4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native", - "version": "0.0.1", + "version": "0.1.0", "description": "Build native apps with React!", "repository": { "type": "git", @@ -8,49 +8,34 @@ }, "jest": { "scriptPreprocessor": "jestSupport/scriptPreprocess.js", + "setupEnvScriptFile": "jestSupport/env.js", "testPathIgnorePatterns": [ "/node_modules/", - "/packager/" + "packager/react-packager/src/Activity/" ], "testFileExtensions": [ "js" + ], + "unmockedModulePathPatterns": [ + "source-map" ] }, + "main": "Libraries/react-native/react-native.js", "scripts": { "test": "jest", "lint": "node linter.js Examples/", - "start": "./packager/packager.sh" + "start": "./packager/packager.sh", + "postinstall": "cd packager && npm install" }, "dependencies": { - "absolute-path": "0.0.0", - "base62": "0.1.1", "connect": "2.8.3", - "debug": "~2.1.0", - "esprima-fb": "7001.0001.0000-dev-harmony-fb", - "fs-extra": "0.15.0", - "jstransform": "8.2.0", - "mime": "1.2.11", - "module-deps": "3.5.6", - "node-haste": "1.2.6", - "node-static": "0.7.6", - "optimist": "0.6.1", - "path-is-inside": "1.0.1", - "punycode": "1.2.4", - "q": "1.0.1", - "qs": "0.6.5", - "react-tools": "0.12.2", - "rebound": "0.0.10", - "sane": "1.0.1", + "jstransform": "10.0.1", "source-map": "0.1.31", "stacktrace-parser": "0.1.1", - "through": "2.3.6", - "underscore": "1.7.0", - "wordwrap": "0.0.2", - "worker-farm": "1.1.0", - "yargs": "1.3.2", - "joi": "~5.1.0" + "react-tools": "0.13.0-rc2" }, "devDependencies": { + "ws": "0.4.31", "jest-cli": "0.2.1", "eslint": "0.9.2" } diff --git a/packager/blacklist.js b/packager/blacklist.js index 2b710af62..850a87244 100644 --- a/packager/blacklist.js +++ b/packager/blacklist.js @@ -6,13 +6,12 @@ // Don't forget to everything listed here to `testConfig.json` // modulePathIgnorePatterns. var sharedBlacklist = [ - 'node_modules/JSAppServer', - 'packager/react-packager', + __dirname, + 'website', 'node_modules/parse/node_modules/xmlhttprequest/lib/XMLHttpRequest.js', 'node_modules/react-tools/src/utils/ImmutableObject.js', 'node_modules/react-tools/src/core/ReactInstanceHandles.js', - 'node_modules/react-tools/src/event/EventPropagators.js', - 'node_modules/jest-cli', + 'node_modules/react-tools/src/event/EventPropagators.js' ]; var webBlacklist = [ @@ -22,7 +21,6 @@ var webBlacklist = [ var iosBlacklist = [ 'node_modules/react-tools/src/browser/ui/React.js', 'node_modules/react-tools/src/browser/eventPlugins/ResponderEventPlugin.js', - 'node_modules/react-tools/src/browser/ReactTextComponent.js', // 'node_modules/react-tools/src/vendor/core/ExecutionEnvironment.js', '.web.js', '.android.js', @@ -32,9 +30,9 @@ function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } -function blacklist(isWeb) { +function blacklist(isWeb, additionalBlacklist) { return new RegExp('(' + - sharedBlacklist + (additionalBlacklist || []).concat(sharedBlacklist) .concat(isWeb ? webBlacklist : iosBlacklist) .map(escapeRegExp) .join('|') + diff --git a/packager/debugger.html b/packager/debugger.html new file mode 100644 index 000000000..f7cabc5f8 --- /dev/null +++ b/packager/debugger.html @@ -0,0 +1,112 @@ + + + + + + +React Native Debugger + + + + +

+ 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

+ + diff --git a/packager/launchChromeDevTools.applescript b/packager/launchChromeDevTools.applescript new file mode 100755 index 000000000..4384b3ae0 --- /dev/null +++ b/packager/launchChromeDevTools.applescript @@ -0,0 +1,41 @@ +#!/usr/bin/env osascript + + +on run argv + set theURL to item 1 of argv + + tell application "Google Chrome" + activate + + if (count every window) = 0 then + make new window + end if + + -- Find a tab currently running the debugger + set found to false + set theTabIndex to -1 + repeat with theWindow in every window + set theTabIndex to 0 + repeat with theTab in every tab of theWindow + set theTabIndex to theTabIndex + 1 + if theTab's URL is theURL then + set found to true + exit repeat + end if + end repeat + + if found then + exit repeat + end if + end repeat + + if found then + set index of theWindow to 1 + set theWindow's active tab index to theTabIndex + else + tell window 1 + make new tab with properties {URL:theURL} + end tell + end if + end tell +end run diff --git a/packager/package.json b/packager/package.json new file mode 100644 index 000000000..0afcd3c3c --- /dev/null +++ b/packager/package.json @@ -0,0 +1,45 @@ +{ + "name": "react-native-cli", + "version": "0.1.1", + "description": "Build native apps with React!", + "repository": { + "type": "git", + "url": "git@github.com:facebook/react-native.git" + }, + "jest": { + "setupEnvScriptFile": "jestSupport/env.js", + "testPathIgnorePatterns": [ + "/node_modules/" + ], + "testFileExtensions": [ + "js" + ], + "unmockedModulePathPatterns": [ + "source-map" + ] + }, + "scripts": { + "test": "jest", + "lint": "node linter.js Examples/", + "start": "./packager/packager.sh" + }, + "dependencies": { + "absolute-path": "0.0.0", + "debug": "~2.1.0", + "joi": "~5.1.0", + "module-deps": "3.5.6", + "optimist": "0.6.1", + "q": "1.0.1", + "sane": "1.0.1", + "source-map": "0.1.31", + "stacktrace-parser": "0.1.1", + "uglify-js": "~2.4.16", + "underscore": "1.7.0", + "worker-farm": "1.1.0", + "yargs": "1.3.2" + }, + "devDependencies": { + "jest-cli": "0.2.1", + "eslint": "0.9.2" + } +} diff --git a/packager/packager.js b/packager/packager.js index 6d5336ef4..7214aaafd 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -16,12 +16,14 @@ if (!fs.existsSync(path.resolve(__dirname, '..', 'node_modules'))) { process.exit(); } +var exec = require('child_process').exec; var ReactPackager = require('./react-packager'); var blacklist = require('./blacklist.js'); var connect = require('connect'); var http = require('http'); var launchEditor = require('./launchEditor.js'); var parseCommandLine = require('./parseCommandLine.js'); +var webSocketProxy = require('./webSocketProxy.js'); var options = parseCommandLine([{ command: 'port', @@ -31,8 +33,17 @@ var options = parseCommandLine([{ description: 'add another root(s) to be used by the packager in this project', }]); -if (!options.projectRoots) { - options.projectRoots = [path.resolve(__dirname, '..')]; +if (options.projectRoots) { + if (!Array.isArray(options.projectRoots)) { + options.projectRoots = options.projectRoots.split(','); + } +} else { + if (__dirname.match(/node_modules\/react-native\/packager$/)) { + // packager is running from node_modules of another project + options.projectRoots = [path.resolve(__dirname, '../../..')]; + } else { + options.projectRoots = [path.resolve(__dirname, '..')]; + } } if (options.root) { @@ -45,6 +56,10 @@ if (options.root) { } } +if (!options.assetRoots) { + options.assetRoots = [path.resolve(__dirname, '..')]; +} + console.log('\n' + ' ===============================================================\n' + ' | Running packager on port ' + options.port + '. \n' + @@ -57,6 +72,8 @@ console.log('\n' + ' ===============================================================\n' ); +console.log('Looking for JS files in\n ', options.projectRoots.join('\n ')); + process.on('uncaughtException', function(e) { console.error(e); console.error(e.stack); @@ -64,10 +81,12 @@ process.on('uncaughtException', function(e) { 'any existing instances that are already running.\n\n'); }); -runServer(options, function() { +var server = runServer(options, function() { console.log('\nReact packager ready.\n'); }); +webSocketProxy.attachToServer(server, '/debugger-proxy'); + function loadRawBody(req, res, next) { req.rawBody = ''; req.setEncoding('utf8'); @@ -91,23 +110,48 @@ function openStackFrameInEditor(req, res, next) { } } +function getDevToolsLauncher(options) { + return function(req, res, next) { + if (req.url === '/debugger-ui') { + var debuggerPath = path.join(__dirname, 'debugger.html'); + res.writeHead(200, {'Content-Type': 'text/html'}); + fs.createReadStream(debuggerPath).pipe(res); + } else if (req.url === '/launch-chrome-devtools') { + var debuggerURL = 'http://localhost:' + options.port + '/debugger-ui'; + var script = 'launchChromeDevTools.applescript'; + console.log('Launching Dev Tools...'); + exec(path.join(__dirname, script) + ' ' + debuggerURL, function(err, stdout, stderr) { + if (err) { + console.log('Failed to run ' + script, err); + } + console.log(stdout); + console.warn(stderr); + }); + res.end('OK'); + } else { + next(); + } + }; +} + function getAppMiddleware(options) { return ReactPackager.middleware({ - dev: true, projectRoots: options.projectRoots, blacklistRE: blacklist(false), cacheVersion: '2', transformModulePath: require.resolve('./transformer.js'), + assetRoots: options.assetRoots, }); } function runServer( - options, /* {string projectRoot, bool web, bool dev} */ + options, /* {[]string projectRoot, bool web} */ readyCallback ) { var app = connect() .use(loadRawBody) .use(openStackFrameInEditor) + .use(getDevToolsLauncher(options)) .use(getAppMiddleware(options)); options.projectRoots.forEach(function(root) { diff --git a/packager/react-packager/, b/packager/react-packager/, deleted file mode 100644 index e69de29bb..000000000 diff --git a/packager/react-packager/.jshintrc b/packager/react-packager/.jshintrc deleted file mode 100644 index 7a3f79a72..000000000 --- a/packager/react-packager/.jshintrc +++ /dev/null @@ -1,86 +0,0 @@ -{ - "-W093": true, - "asi": false, - "bitwise": true, - "boss": false, - "browser": false, - "camelcase": true, - "couch": false, - "curly": true, - "debug": false, - "devel": true, - "dojo": false, - "eqeqeq": true, - "eqnull": true, - "esnext": true, - "evil": false, - "expr": true, - "forin": false, - "freeze": true, - "funcscope": true, - "gcl": false, - "globals": { - "Promise": true, - "React": true, - "XMLHttpRequest": true, - "document": true, - "location": true, - "window": true - }, - "globalstrict": true, - "immed": false, - "indent": 2, - "iterator": false, - "jquery": false, - "lastsemic": false, - "latedef": false, - "laxbreak": true, - "laxcomma": false, - "loopfunc": false, - "maxcomplexity": false, - "maxdepth": false, - "maxerr": 50, - "maxlen": 80, - "maxparams": false, - "maxstatements": false, - "mootools": false, - "moz": false, - "multistr": false, - "newcap": true, - "noarg": true, - "node": true, - "noempty": false, - "nonbsp": true, - "nonew": true, - "nonstandard": false, - "notypeof": false, - "noyield": false, - "phantom": false, - "plusplus": false, - "predef": [ - "afterEach", - "beforeEach", - "describe", - "expect", - "it", - "jest", - "pit" - ], - "proto": false, - "prototypejs": false, - "quotmark": true, - "rhino": false, - "scripturl": false, - "shadow": false, - "smarttabs": false, - "strict": false, - "sub": false, - "supernew": false, - "trailing": true, - "undef": true, - "unused": true, - "validthis": false, - "worker": false, - "wsh": false, - "yui": false -} diff --git a/packager/react-packager/package.json b/packager/react-packager/package.json deleted file mode 100644 index 0ac47c257..000000000 --- a/packager/react-packager/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "react-packager", - "version": "0.1.0", - "description": "", - "main": "index.js", - "jest": { - "unmockedModulePathPatterns": [ - "source-map" - ], - "testPathIgnorePatterns": [ - "JSAppServer/node_modules" - ] - } -} diff --git a/packager/react-packager/src/Activity/__tests__/Activity-test.js b/packager/react-packager/src/Activity/__tests__/Activity-test.js index 7a2bdf481..7fe316148 100644 --- a/packager/react-packager/src/Activity/__tests__/Activity-test.js +++ b/packager/react-packager/src/Activity/__tests__/Activity-test.js @@ -1,3 +1,5 @@ +'use strict'; + jest.autoMockOff(); describe('Activity', function() { @@ -8,6 +10,7 @@ describe('Activity', function() { beforeEach(function() { console.log = jest.genMockFn(); Activity = require('../'); + jest.runOnlyPendingTimers(); }); afterEach(function() { @@ -58,12 +61,15 @@ describe('Activity', function() { expect(function() { Activity.endEvent(eid); - }).toThrow('event(1) has already ended!'); + }).toThrow('event(3) has already ended!'); + + jest.runOnlyPendingTimers(); }); }); describe('signal', function() { it('writes a SIGNAL event out to the console', function() { + var EVENT_NAME = 'EVENT_NAME'; var DATA = {someData: 42}; diff --git a/packager/react-packager/src/Activity/index.js b/packager/react-packager/src/Activity/index.js index a60f87b08..611ccb0b1 100644 --- a/packager/react-packager/src/Activity/index.js +++ b/packager/react-packager/src/Activity/index.js @@ -1,3 +1,5 @@ +'use strict'; + var COLLECTION_PERIOD = 1000; var _endedEvents = Object.create(null); diff --git a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js b/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js index 0898767a8..df29e57c5 100644 --- a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js +++ b/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js @@ -1,3 +1,5 @@ +'use strict'; + function ModuleDescriptor(fields) { if (!fields.id) { throw new Error('Missing required fields id'); @@ -20,6 +22,8 @@ function ModuleDescriptor(fields) { this.isPolyfill = fields.isPolyfill || false; + this.isAsset = fields.isAsset || false; + this._fields = fields; } @@ -28,7 +32,7 @@ ModuleDescriptor.prototype.toJSON = function() { id: this.id, path: this.path, dependencies: this.dependencies - } + }; }; module.exports = ModuleDescriptor; diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js index fe8a18b61..6dee93ec6 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -5,12 +5,9 @@ jest .dontMock('q') .dontMock('path') .dontMock('absolute-path') - .dontMock('../../../../fb-path-utils') .dontMock('../docblock') .setMock('../../../ModuleDescriptor', function(data) {return data;}); -var q = require('q'); - describe('DependencyGraph', function() { var DependencyGraph; var fileWatcher; @@ -46,7 +43,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ @@ -56,6 +56,40 @@ describe('DependencyGraph', function() { }); }); + pit('should get dependencies', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("image!a")' + ].join('\n'), + 'imgs': { + 'a.png': '' + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetRoots: ['/root/imgs'] + }); + return dgraph.load().then(function() { + expect(dgraph.getOrderedDependencies('/root/index.js')) + .toEqual([ + {id: 'index', path: '/root/index.js', dependencies: ['image!a']}, + { id: 'image!a', + path: '/root/imgs/a.png', + dependencies: [], + isAsset: true + }, + ]); + }); + }); + pit('should get recursive dependencies', function() { var root = '/root'; fs.__setMockFilesystem({ @@ -75,7 +109,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ @@ -105,7 +142,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ @@ -135,7 +175,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ @@ -175,7 +218,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/somedir/somefile.js')) .toEqual([ @@ -216,7 +262,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ @@ -245,7 +294,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ @@ -280,7 +332,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ @@ -320,7 +375,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ @@ -360,7 +418,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { expect(dgraph.getOrderedDependencies('/root/index.js')) .toEqual([ @@ -386,7 +447,6 @@ describe('DependencyGraph', function() { }); describe('file watch updating', function() { - var fileWatcher; var triggerFileChange; beforeEach(function() { @@ -428,7 +488,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { filesystem.root['index.js'] = filesystem.root['index.js'].replace('require("foo")', ''); @@ -476,7 +539,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { filesystem.root['index.js'] = filesystem.root['index.js'].replace('require("foo")', ''); @@ -524,7 +590,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { delete filesystem.root.foo; triggerFileChange('delete', 'foo.js', root); @@ -571,7 +640,10 @@ describe('DependencyGraph', function() { } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { filesystem.root['bar.js'] = [ '/**', @@ -679,7 +751,7 @@ describe('DependencyGraph', function() { pit('should ignore directory updates', function() { var root = '/root'; - var filesystem = fs.__setMockFilesystem({ + fs.__setMockFilesystem({ 'root': { 'index.js': [ '/**', @@ -703,7 +775,10 @@ describe('DependencyGraph', function() { } } }); - var dgraph = new DependencyGraph({roots: [root], fileWatcher: fileWatcher}); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher + }); return dgraph.load().then(function() { triggerFileChange('change', 'aPackage', '/root', { isDirectory: function(){ return true; } diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/docblock.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/docblock.js index 52cac03ba..c2b6ac984 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/docblock.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/docblock.js @@ -14,6 +14,7 @@ * limitations under the License. */ +'use strict'; var docblockRe = /^\s*(\/\*\*(.|\r?\n)*?\*\/)/; @@ -35,7 +36,8 @@ var commentStartRe = /^\/\*\*?/; var commentEndRe = /\*\/$/; var wsRe = /[\t ]+/g; var stringStartRe = /(\r?\n|^) *\*/g; -var multilineRe = /(?:^|\r?\n) *(@[^\r\n]*?) *\r?\n *([^@\r\n\s][^@\r\n]+?) *\r?\n/g; +var multilineRe = + /(?:^|\r?\n) *(@[^\r\n]*?) *\r?\n *([^@\r\n\s][^@\r\n]+?) *\r?\n/g; var propertyRe = /(?:^|\r?\n) *@(\S+) *([^\r\n]*)/g; /** @@ -51,15 +53,15 @@ function parse(docblock) { // Normalize multi-line directives var prev = ''; - while (prev != docblock) { + while (prev !== docblock) { prev = docblock; - docblock = docblock.replace(multilineRe, "\n$1 $2\n"); + docblock = docblock.replace(multilineRe, '\n$1 $2\n'); } docblock = docblock.trim(); var result = []; var match; - while (match = propertyRe.exec(docblock)) { + while ((match = propertyRe.exec(docblock))) { result.push([match[1], match[2]]); } diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/example.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/example.js deleted file mode 100644 index 02e6c5928..000000000 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/example.js +++ /dev/null @@ -1,25 +0,0 @@ -var path = require('path'); -var DependecyGraph = require('./'); - -var example_project = path.resolve(__dirname, '../../../../example_project'); -var watcher = new (require('../../../FileWatcher'))({projectRoot: example_project}); -var graph = new DependecyGraph({ - fileWatcher: watcher, - root: example_project -}); - -graph.load().then(function() { - var index = path.join(example_project, 'index.js'); - console.log(graph.getOrderedDependencies(index)); -}).done(); - -watcher.getWatcher().then(function(watcher) { - watcher.on('all', function() { - setImmediate(function() { - graph.load().then(function() { - var index = path.join(example_project, 'index.js'); - console.log(graph.getOrderedDependencies(index)); - }); - }) - }); -}); diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index 6a7d8bbac..918b1e064 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -28,12 +28,22 @@ var validateOpts = declareOpts({ type: 'object', required: true, }, + assetRoots: { + type: 'array', + default: [], + }, + assetExts: { + type: 'array', + default: ['png'], + } }); function DependecyGraph(options) { var opts = validateOpts(options); this._roots = opts.roots; + this._assetRoots = opts.assetRoots; + this._assetExts = opts.assetExts; this._ignoreFilePath = opts.ignoreFilePath; this._fileWatcher = options.fileWatcher; @@ -50,7 +60,16 @@ function DependecyGraph(options) { } DependecyGraph.prototype.load = function() { - return this._loading || (this._loading = this._search()); + if (this._loading != null) { + return this._loading; + } + + this._loading = q.all([ + this._search(), + this._buildAssetMap(), + ]); + + return this._loading; }; /** @@ -115,6 +134,18 @@ DependecyGraph.prototype.resolveDependency = function( fromModule, depModuleId ) { + if (this._assetMap != null) { + // Process asset requires. + var assetMatch = depModuleId.match(/^image!(.+)/); + if (assetMatch && assetMatch[1]) { + if (!this._assetMap[assetMatch[1]]) { + debug('WARINING: Cannot find asset:', assetMatch[1]); + return null; + } + return this._assetMap[assetMatch[1]]; + } + } + var packageJson, modulePath, dep; // Package relative modules starts with '.' or '..'. @@ -139,7 +170,7 @@ DependecyGraph.prototype.resolveDependency = function( depModuleId, fromModule.id ); - return; + return null; } var main = packageJson.main || 'index'; @@ -147,7 +178,7 @@ DependecyGraph.prototype.resolveDependency = function( dep = this._graph[modulePath]; if (dep == null) { throw new Error( - 'Cannot find package main file for pacakge: ' + packageJson._root + 'Cannot find package main file for package: ' + packageJson._root ); } return dep; @@ -214,32 +245,13 @@ DependecyGraph.prototype._search = function() { // 2. Filter the files and queue up the directories. // 3. Process any package.json in the files // 4. recur. - return readDir(dir) - .then(function(files){ - return q.all(files.map(function(filePath) { - return realpath(path.join(dir, filePath)).catch(handleBrokenLink); - })); - }) - .then(function(filePaths) { - filePaths = filePaths.filter(function(filePath) { - if (filePath == null) { - return false - } - - return !self._ignoreFilePath(filePath); - }); - - var statsP = filePaths.map(function(filePath) { - return lstat(filePath).catch(handleBrokenLink); - }); - - return [ - filePaths, - q.all(statsP) - ]; - }) + return readAndStatDir(dir) .spread(function(files, stats) { var modulePaths = files.filter(function(filePath, i) { + if (self._ignoreFilePath(filePath)) { + return false; + } + if (stats[i].isDirectory()) { self._queue.push(filePath); return false; @@ -454,7 +466,8 @@ DependecyGraph.prototype._getAbsolutePath = function(filePath) { return filePath; } - for (var i = 0, root; root = this._roots[i]; i++) { + for (var i = 0; i < this._roots.length; i++) { + var root = this._roots[i]; var absPath = path.join(root, filePath); if (this._graph[absPath]) { return absPath; @@ -464,6 +477,19 @@ DependecyGraph.prototype._getAbsolutePath = function(filePath) { return null; }; +DependecyGraph.prototype._buildAssetMap = function() { + if (this._assetRoots == null || this._assetRoots.length === 0) { + return q(); + } + + var self = this; + return buildAssetMap(this._assetRoots, this._assetExts) + .then(function(map) { + self._assetMap = map; + return map; + }); +}; + /** * Extract all required modules from a `code` string. */ @@ -510,4 +536,71 @@ function handleBrokenLink(e) { return q(); } +function readAndStatDir(dir) { + return readDir(dir) + .then(function(files){ + return q.all(files.map(function(filePath) { + return realpath(path.join(dir, filePath)).catch(handleBrokenLink); + })); + }).then(function(files) { + files = files.filter(function(f) { + return !!f; + }); + + var stats = files.map(function(filePath) { + return lstat(filePath).catch(handleBrokenLink); + }); + + return [ + files, + q.all(stats), + ]; + }); +} + +/** + * Given a list of roots and list of extensions find all the files in + * the directory with that extension and build a map of those assets. + */ +function buildAssetMap(roots, exts) { + var queue = roots.slice(0); + var map = Object.create(null); + + function search() { + var root = queue.shift(); + + if (root == null) { + return q(map); + } + + return readAndStatDir(root).spread(function(files, stats) { + files.forEach(function(file, i) { + if (stats[i].isDirectory()) { + queue.push(file); + } else { + var ext = path.extname(file).replace(/^\./, ''); + if (exts.indexOf(ext) !== -1) { + var assetName = path.basename(file, '.' + ext) + .replace(/@[\d\.]+x/, ''); + if (map[assetName] != null) { + debug('Conflcting assets', assetName); + } + + map[assetName] = new ModuleDescriptor({ + id: 'image!' + assetName, + path: path.resolve(file), + isAsset: true, + dependencies: [], + }); + } + } + }); + + return search(); + }); + } + + return search(); +} + module.exports = DependecyGraph; diff --git a/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js b/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js index d3c4a7d9f..b25fd8211 100644 --- a/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js @@ -1,3 +1,4 @@ +'use strict'; jest.dontMock('../') .dontMock('q') @@ -7,7 +8,6 @@ var q = require('q'); describe('HasteDependencyResolver', function() { var HasteDependencyResolver; - var DependencyGraph; beforeEach(function() { // For the polyfillDeps @@ -15,7 +15,6 @@ describe('HasteDependencyResolver', function() { return b; }); HasteDependencyResolver = require('../'); - DependencyGraph = require('../DependencyGraph'); }); describe('getDependencies', function() { @@ -25,7 +24,6 @@ describe('HasteDependencyResolver', function() { var depResolver = new HasteDependencyResolver({ projectRoot: '/root', - dev: false, }); // Is there a better way? How can I mock the prototype instead? @@ -37,7 +35,7 @@ describe('HasteDependencyResolver', function() { return q(); }); - return depResolver.getDependencies('/root/index.js') + return depResolver.getDependencies('/root/index.js', { dev: false }) .then(function(result) { expect(result.mainModuleId).toEqual('index'); expect(result.dependencies).toEqual([ @@ -86,7 +84,6 @@ describe('HasteDependencyResolver', function() { var depResolver = new HasteDependencyResolver({ projectRoot: '/root', - dev: true, }); // Is there a better way? How can I mock the prototype instead? @@ -98,7 +95,7 @@ describe('HasteDependencyResolver', function() { return q(); }); - return depResolver.getDependencies('/root/index.js') + return depResolver.getDependencies('/root/index.js', { dev: true }) .then(function(result) { expect(result.mainModuleId).toEqual('index'); expect(result.dependencies).toEqual([ @@ -148,7 +145,6 @@ describe('HasteDependencyResolver', function() { var depResolver = new HasteDependencyResolver({ projectRoot: '/root', polyfillModuleNames: ['some module'], - dev: false, }); // Is there a better way? How can I mock the prototype instead? @@ -160,7 +156,7 @@ describe('HasteDependencyResolver', function() { return q(); }); - return depResolver.getDependencies('/root/index.js') + return depResolver.getDependencies('/root/index.js', { dev: false }) .then(function(result) { expect(result.mainModuleId).toEqual('index'); expect(result.dependencies).toEqual([ @@ -219,11 +215,10 @@ describe('HasteDependencyResolver', function() { it('should ', function() { var depResolver = new HasteDependencyResolver({ projectRoot: '/root', - dev: false, }); var depGraph = depResolver._depGraph; - var dependencies = ['x', 'y', 'z'] + var dependencies = ['x', 'y', 'z']; var code = [ 'require("x")', 'require("y")', @@ -248,10 +243,10 @@ describe('HasteDependencyResolver', function() { }, code); expect(processedCode).toEqual([ - "__d('test module',[\"changed\",\"y\"],function(global," + - " require, requireDynamic, requireLazy, module, exports) {" + - " require('changed')", - "require('y')", + '__d(\'test module\',["changed","y"],function(global,' + + ' require, requireDynamic, requireLazy, module, exports) {' + + ' require(\'changed\')', + 'require(\'y\')', 'require("z")});', ].join('\n')); }); diff --git a/packager/react-packager/src/DependencyResolver/haste/index.js b/packager/react-packager/src/DependencyResolver/haste/index.js index dc4976493..fdc779edc 100644 --- a/packager/react-packager/src/DependencyResolver/haste/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/index.js @@ -6,17 +6,17 @@ var DependencyGraph = require('./DependencyGraph'); var ModuleDescriptor = require('../ModuleDescriptor'); var declareOpts = require('../../lib/declareOpts'); -var DEFINE_MODULE_CODE = - '__d(' + - '\'_moduleName_\',' + - '_deps_,' + - 'function(global, require, requireDynamic, requireLazy, module, exports) {'+ - ' _code_' + - '}' + - ');'; +var DEFINE_MODULE_CODE = [ + '__d(', + '\'_moduleName_\',', + '_deps_,', + 'function(global, require, requireDynamic, requireLazy, module, exports) {', + ' _code_', + '}', + ');', +].join(''); var DEFINE_MODULE_REPLACE_RE = /_moduleName_|_code_|_deps_/g; - var REL_REQUIRE_STMT = /require\(['"]([\.\/0-9A-Z_$\-]*)['"]\)/gi; var validateOpts = declareOpts({ @@ -31,10 +31,6 @@ var validateOpts = declareOpts({ type: 'array', default: [], }, - dev: { - type: 'boolean', - default: true, - }, nonPersistent: { type: 'boolean', default: false, @@ -43,6 +39,10 @@ var validateOpts = declareOpts({ type: 'string', default: 'haste', }, + assetRoots: { + type: 'array', + default: [], + }, }); function HasteDependencyResolver(options) { @@ -54,27 +54,28 @@ function HasteDependencyResolver(options) { this._depGraph = new DependencyGraph({ roots: opts.projectRoots, + assetRoots: opts.assetRoots, ignoreFilePath: function(filepath) { return filepath.indexOf('__tests__') !== -1 || (opts.blacklistRE && opts.blacklistRE.test(filepath)); }, - fileWatcher: this._fileWatcher + fileWatcher: this._fileWatcher, }); - this._polyfillModuleNames = [ - opts.dev - ? path.join(__dirname, 'polyfills/prelude_dev.js') - : path.join(__dirname, 'polyfills/prelude.js'), - path.join(__dirname, 'polyfills/require.js'), - path.join(__dirname, 'polyfills/polyfills.js'), - path.join(__dirname, 'polyfills/console.js'), - path.join(__dirname, 'polyfills/error-guard.js'), - ].concat( - opts.polyfillModuleNames || [] - ); + + this._polyfillModuleNames = opts.polyfillModuleNames || []; } -HasteDependencyResolver.prototype.getDependencies = function(main) { +var getDependenciesValidateOpts = declareOpts({ + dev: { + type: 'boolean', + default: true, + }, +}); + +HasteDependencyResolver.prototype.getDependencies = function(main, options) { + var opts = getDependenciesValidateOpts(options); + var depGraph = this._depGraph; var self = this; @@ -83,7 +84,7 @@ HasteDependencyResolver.prototype.getDependencies = function(main) { var dependencies = depGraph.getOrderedDependencies(main); var mainModuleId = dependencies[0].id; - self._prependPolyfillDependencies(dependencies); + self._prependPolyfillDependencies(dependencies, opts.dev); return { mainModuleId: mainModuleId, @@ -93,22 +94,30 @@ HasteDependencyResolver.prototype.getDependencies = function(main) { }; HasteDependencyResolver.prototype._prependPolyfillDependencies = function( - dependencies + dependencies, + isDev ) { - var polyfillModuleNames = this._polyfillModuleNames; - if (polyfillModuleNames.length > 0) { - var polyfillModules = polyfillModuleNames.map( - function(polyfillModuleName, idx) { - return new ModuleDescriptor({ - path: polyfillModuleName, - id: polyfillModuleName, - dependencies: polyfillModuleNames.slice(0, idx), - isPolyfill: true - }); - } - ); - dependencies.unshift.apply(dependencies, polyfillModules); - } + var polyfillModuleNames = [ + isDev + ? path.join(__dirname, 'polyfills/prelude_dev.js') + : path.join(__dirname, 'polyfills/prelude.js'), + path.join(__dirname, 'polyfills/require.js'), + path.join(__dirname, 'polyfills/polyfills.js'), + path.join(__dirname, 'polyfills/console.js'), + path.join(__dirname, 'polyfills/error-guard.js'), + ].concat(this._polyfillModuleNames); + + var polyfillModules = polyfillModuleNames.map( + function(polyfillModuleName, idx) { + return new ModuleDescriptor({ + path: polyfillModuleName, + id: polyfillModuleName, + dependencies: polyfillModuleNames.slice(0, idx), + isPolyfill: true + }); + } + ); + dependencies.unshift.apply(dependencies, polyfillModules); }; HasteDependencyResolver.prototype.wrapModule = function(module, code) { @@ -116,7 +125,6 @@ HasteDependencyResolver.prototype.wrapModule = function(module, code) { return code; } - var depGraph = this._depGraph; var resolvedDeps = Object.create(null); var resolvedDepsArr = []; @@ -131,9 +139,9 @@ HasteDependencyResolver.prototype.wrapModule = function(module, code) { var relativizedCode = code.replace(REL_REQUIRE_STMT, function(codeMatch, depName) { - var dep = resolvedDeps[depName]; - if (dep != null) { - return 'require(\'' + dep + '\')'; + var depId = resolvedDeps[depName]; + if (depId != null) { + return 'require(\'' + depId + '\')'; } else { return codeMatch; } diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js index 4c9ddce1f..1b2604e3a 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js @@ -20,9 +20,18 @@ * @polyfill */ +/*eslint global-strict:0*/ (function(global) { + 'use strict'; var OBJECT_COLUMN_NAME = '(index)'; + var LOG_LEVELS = { + trace: 0, + log: 1, + info: 2, + warn: 3, + error: 4 + }; function setupConsole(global) { @@ -30,30 +39,32 @@ return; } - function doNativeLog() { - var str = Array.prototype.map.call(arguments, function(arg) { - if (arg == null) { - return arg === null ? 'null' : 'undefined'; - } else if (typeof arg === 'string') { - return '"' + arg + '"'; - } else { - // Perform a try catch, just in case the object has a circular - // reference or stringify throws for some other reason. - try { - return JSON.stringify(arg); - } catch (e) { - if (typeof arg.toString === 'function') { - try { - return arg.toString(); - } catch (e) { - return 'unknown'; + function getNativeLogFunction(level) { + return function() { + var str = Array.prototype.map.call(arguments, function(arg) { + if (arg == null) { + return arg === null ? 'null' : 'undefined'; + } else if (typeof arg === 'string') { + return '"' + arg + '"'; + } else { + // Perform a try catch, just in case the object has a circular + // reference or stringify throws for some other reason. + try { + return JSON.stringify(arg); + } catch (e) { + if (typeof arg.toString === 'function') { + try { + return arg.toString(); + } catch (E) { + return 'unknown'; + } } } } - } - }).join(', '); - global.nativeLoggingHook(str); - }; + }).join(', '); + global.nativeLoggingHook(str, level); + }; + } var repeat = function(element, n) { return Array.apply(null, Array(n)).map(function() { return element; }); @@ -73,7 +84,7 @@ } } if (rows.length === 0) { - global.nativeLoggingHook(''); + global.nativeLoggingHook('', LOG_LEVELS.log); return; } @@ -119,18 +130,19 @@ // Native logging hook adds "RCTLog >" at the front of every // logged string, which would shift the header and screw up // the table - global.nativeLoggingHook('\n' + table.join('\n')); - }; + global.nativeLoggingHook('\n' + table.join('\n'), LOG_LEVELS.log); + } global.console = { - error: doNativeLog, - info: doNativeLog, - log: doNativeLog, - warn: doNativeLog, + error: getNativeLogFunction(LOG_LEVELS.error), + info: getNativeLogFunction(LOG_LEVELS.info), + log: getNativeLogFunction(LOG_LEVELS.log), + warn: getNativeLogFunction(LOG_LEVELS.warn), + trace: getNativeLogFunction(LOG_LEVELS.trace), table: consoleTablePolyfill }; - }; + } if (typeof module !== 'undefined') { module.exports = setupConsole; diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js index 687a4a19c..745d650ea 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js @@ -39,7 +39,7 @@ return ErrorUtils._inGuard; }, guard: function(fun, name, context) { - if (typeof fun !== "function") { + if (typeof fun !== 'function') { console.warn('A function must be passed to ErrorUtils.guard, got ', fun); return null; } diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/polyfills.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/polyfills.js index 2fd322463..75f742790 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/polyfills.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/polyfills.js @@ -22,7 +22,7 @@ // WARNING: This is an optimized version that fails on hasOwnProperty checks // and non objects. It's not spec-compliant. It's a perf optimization. - +/* eslint global-strict:0 */ Object.assign = function(target, sources) { if (__DEV__) { if (target == null) { diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude.js index 95c66983b..9f4db44e2 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude.js @@ -1 +1,2 @@ +/* eslint global-strict:0 */ __DEV__ = false; diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude_dev.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude_dev.js index a5ca53b7a..26b26a076 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude_dev.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude_dev.js @@ -1 +1,2 @@ +/* eslint global-strict:0 */ __DEV__ = true; diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/require.js b/packager/react-packager/src/DependencyResolver/haste/polyfills/require.js index 3b5d6d87a..e7fdde250 100644 --- a/packager/react-packager/src/DependencyResolver/haste/polyfills/require.js +++ b/packager/react-packager/src/DependencyResolver/haste/polyfills/require.js @@ -1,3 +1,4 @@ +/* eslint global-strict:0,eqeqeq:0,no-bitwise:0,no-undef:0 */ (function(global) { // avoid redefining require() diff --git a/packager/react-packager/src/DependencyResolver/index.js b/packager/react-packager/src/DependencyResolver/index.js index 79eb48c11..f42ecb8a7 100644 --- a/packager/react-packager/src/DependencyResolver/index.js +++ b/packager/react-packager/src/DependencyResolver/index.js @@ -1,3 +1,5 @@ +'use strict'; + var HasteDependencyResolver = require('./haste'); var NodeDependencyResolver = require('./node'); diff --git a/packager/react-packager/src/DependencyResolver/node/index.js b/packager/react-packager/src/DependencyResolver/node/index.js index 0d3b807ef..da03cc7ea 100644 --- a/packager/react-packager/src/DependencyResolver/node/index.js +++ b/packager/react-packager/src/DependencyResolver/node/index.js @@ -1,17 +1,12 @@ +'use strict'; + var Promise = require('q').Promise; var ModuleDescriptor = require('../ModuleDescriptor'); var mdeps = require('module-deps'); var path = require('path'); -var fs = require('fs'); -// var REQUIRE_RUNTIME = fs.readFileSync( -// path.join(__dirname, 'require.js') -// ).toString(); - -exports.getRuntimeCode = function() { - return REQUIRE_RUNTIME; -}; +exports.getRuntimeCode = function() {}; exports.wrapModule = function(id, source) { return Promise.resolve( @@ -21,7 +16,7 @@ exports.wrapModule = function(id, source) { }; exports.getDependencies = function(root, fileEntryPath) { - return new Promise(function(resolve, reject) { + return new Promise(function(resolve) { fileEntryPath = path.join(process.cwd(), root, fileEntryPath); var md = mdeps(); diff --git a/packager/react-packager/src/FileWatcher/__mocks__/sane.js b/packager/react-packager/src/FileWatcher/__mocks__/sane.js new file mode 100644 index 000000000..20dda2a2b --- /dev/null +++ b/packager/react-packager/src/FileWatcher/__mocks__/sane.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + WatchmanWatcher: jest.genMockFromModule('sane/src/watchman_watcher') +}; diff --git a/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js b/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js index 8baae9e11..11b9f4a3c 100644 --- a/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js +++ b/packager/react-packager/src/FileWatcher/__tests__/FileWatcher-test.js @@ -1,8 +1,12 @@ 'use strict'; -jest.dontMock('../') - .dontMock('q') - .setMock('child_process', { exec: function(cmd, cb) { cb(null, '/usr/bin/watchman') } }); +jest + .dontMock('../') + .dontMock('q') + .setMock( + 'child_process', + { exec: function(cmd, cb) { cb(null, '/usr/bin/watchman'); } } + ); describe('FileWatcher', function() { var FileWatcher; @@ -16,7 +20,7 @@ describe('FileWatcher', function() { }); }); - it('it should get the watcher instance when ready', function() { + pit('it should get the watcher instance when ready', function() { var fileWatcher = new FileWatcher(['rootDir']); return fileWatcher._loading.then(function(watchers) { watchers.forEach(function(watcher) { diff --git a/packager/react-packager/src/JSTransformer/Cache.js b/packager/react-packager/src/JSTransformer/Cache.js index f04ffe9fe..bad0dadb3 100644 --- a/packager/react-packager/src/JSTransformer/Cache.js +++ b/packager/react-packager/src/JSTransformer/Cache.js @@ -1,13 +1,14 @@ 'use strict'; -var path = require('path'); -var version = require('../../package.json').version; -var tmpdir = require('os').tmpDir(); -var pathUtils = require('../fb-path-utils'); +var _ = require('underscore'); +var crypto = require('crypto'); var declareOpts = require('../lib/declareOpts'); var fs = require('fs'); -var _ = require('underscore'); +var isAbsolutePath = require('absolute-path'); +var path = require('path'); var q = require('q'); +var tmpdir = require('os').tmpDir(); +var version = require('../../../../package.json').version; var Promise = q.Promise; @@ -48,7 +49,7 @@ function Cache(options) { } Cache.prototype.get = function(filepath, loaderCb) { - if (!pathUtils.isAbsolutePath(filepath)) { + if (!isAbsolutePath(filepath)) { throw new Error('Use absolute paths'); } @@ -62,7 +63,7 @@ Cache.prototype.get = function(filepath, loaderCb) { }; Cache.prototype._set = function(filepath, loaderPromise) { - return this._data[filepath] = loaderPromise.then(function(data) { + this._data[filepath] = loaderPromise.then(function(data) { return [ data, q.nfbind(fs.stat)(filepath) @@ -74,10 +75,12 @@ Cache.prototype._set = function(filepath, loaderPromise) { mtime: stat.mtime.getTime(), }; }.bind(this)); + + return this._data[filepath]; }; Cache.prototype.invalidate = function(filepath){ - if(this._has(filepath)) { + if (this._has(filepath)) { delete this._data[filepath]; } }; @@ -94,7 +97,7 @@ Cache.prototype._persistCache = function() { var data = this._data; var cacheFilepath = this._cacheFilePath; - return this._persisting = q.all(_.values(data)) + this._persisting = q.all(_.values(data)) .then(function(values) { var json = Object.create(null); Object.keys(data).forEach(function(key, i) { @@ -106,15 +109,27 @@ Cache.prototype._persistCache = function() { this._persisting = null; return true; }.bind(this)); + + return this._persisting; }; -function loadCacheSync(cacheFilepath) { +function loadCacheSync(cachePath) { var ret = Object.create(null); - if (!fs.existsSync(cacheFilepath)) { + if (!fs.existsSync(cachePath)) { return ret; } - var cacheOnDisk = JSON.parse(fs.readFileSync(cacheFilepath)); + var cacheOnDisk; + try { + cacheOnDisk = JSON.parse(fs.readFileSync(cachePath)); + } catch (e) { + if (e instanceof SyntaxError) { + console.warn('Unable to parse cache file. Will clear and continue.'); + fs.unlinkSync(cachePath); + return ret; + } + throw e; + } // Filter outdated cache and convert to promises. Object.keys(cacheOnDisk).forEach(function(key) { @@ -132,15 +147,15 @@ function loadCacheSync(cacheFilepath) { } function cacheFilePath(options) { + var hash = crypto.createHash('md5'); + hash.update(version); + var roots = options.projectRoots.join(',').split(path.sep).join('-'); + hash.update(roots); + var cacheVersion = options.cacheVersion || '0'; - return path.join( - tmpdir, - [ - 'react-packager-cache', - version, - cacheVersion, - roots, - ].join('-') - ); + hash.update(cacheVersion); + + var name = 'react-packager-cache-' + hash.digest('hex'); + return path.join(tmpdir, name); } diff --git a/packager/react-packager/src/JSTransformer/__mocks__/q.js b/packager/react-packager/src/JSTransformer/__mocks__/q.js new file mode 100644 index 000000000..3d4d21f15 --- /dev/null +++ b/packager/react-packager/src/JSTransformer/__mocks__/q.js @@ -0,0 +1,6 @@ +'use strict'; + +// Bug with Jest because we're going to the node_modules that is a sibling +// of what jest thinks our root (the dir with the package.json) should be. + +module.exports = require.requireActual('q'); diff --git a/packager/react-packager/src/JSTransformer/__mocks__/underscore.js b/packager/react-packager/src/JSTransformer/__mocks__/underscore.js new file mode 100644 index 000000000..a985ab206 --- /dev/null +++ b/packager/react-packager/src/JSTransformer/__mocks__/underscore.js @@ -0,0 +1,5 @@ +'use strict'; + +// Bug with Jest because we're going to the node_modules that is a sibling +// of what jest thinks our root (the dir with the package.json) should be. +module.exports = require.requireActual('underscore'); diff --git a/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js b/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js index c77c63843..f5b55f056 100644 --- a/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js +++ b/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js @@ -3,9 +3,8 @@ jest .dontMock('underscore') .dontMock('path') - .dontMock('q') .dontMock('absolute-path') - .dontMock('../../fb-path-utils') + .dontMock('crypto') .dontMock('../Cache'); var q = require('q'); @@ -21,7 +20,7 @@ describe('JSTransformer Cache', function() { Cache = require('../Cache'); }); - describe('getting/settig', function() { + describe('getting/setting', function() { it('calls loader callback for uncached file', function() { var cache = new Cache({projectRoots: ['/rootDir']}); var loaderCb = jest.genMockFn().mockImpl(function() { @@ -195,7 +194,7 @@ describe('JSTransformer Cache', function() { return q('baz value'); }); - jest.runAllTimers(); + jest.runAllTicks(); expect(fs.writeFile).toBeCalled(); }); }); diff --git a/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js b/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js index 6c9c66446..36d81d8fa 100644 --- a/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js +++ b/packager/react-packager/src/JSTransformer/__tests__/Transformer-test.js @@ -2,7 +2,6 @@ jest .dontMock('worker-farm') - .dontMock('q') .dontMock('os') .dontMock('../index'); @@ -36,7 +35,7 @@ describe('Transformer', function() { callback(null, 'content'); }); - return new Transformer(OPTIONS).loadFileAndTransform([], 'file', {}) + return new Transformer(OPTIONS).loadFileAndTransform('file') .then(function(data) { expect(data).toEqual({ code: 'transformed', @@ -59,7 +58,7 @@ describe('Transformer', function() { callback(null, {error: esprimaError}); }); - return new Transformer(OPTIONS).loadFileAndTransform([], 'foo-file.js', {}) + return new Transformer(OPTIONS).loadFileAndTransform('foo-file.js') .catch(function(error) { expect(error.type).toEqual('TransformError'); expect(error.snippet).toEqual([ diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js index ade206a74..00e49d5d7 100644 --- a/packager/react-packager/src/JSTransformer/index.js +++ b/packager/react-packager/src/JSTransformer/index.js @@ -7,6 +7,7 @@ var Cache = require('./Cache'); var _ = require('underscore'); var workerFarm = require('worker-farm'); var declareOpts = require('../lib/declareOpts'); +var util = require('util'); var readFile = q.nfbind(fs.readFile); @@ -33,13 +34,9 @@ var validateOpts = declareOpts({ type: 'boolean', default: false, }, - dev: { - type: 'boolean', - default: true, - }, transformModulePath: { type:'string', - required: true, + required: false, }, nonPersistent: { type: 'boolean', @@ -62,7 +59,7 @@ function Transformer(options) { this._failedToStart = q.Promise.reject(new Error('No transfrom module')); } else { this._workers = workerFarm( - {autoStart: true}, + {autoStart: true, maxConcurrentCallsPerWorker: 1}, options.transformModulePath ); } @@ -75,15 +72,9 @@ Transformer.prototype.kill = function() { Transformer.prototype.invalidateFile = function(filePath) { this._cache.invalidate(filePath); - //TODO: We can read the file and put it into the cache right here - // This would simplify some caching logic as we can be sure that the cache is up to date -} +}; -Transformer.prototype.loadFileAndTransform = function( - transformSets, - filePath, - options -) { +Transformer.prototype.loadFileAndTransform = function(filePath) { if (this._failedToStart) { return this._failedToStart; } @@ -93,15 +84,14 @@ Transformer.prototype.loadFileAndTransform = function( return readFile(filePath) .then(function(buffer) { var sourceCode = buffer.toString(); - var opts = _.extend({}, options, {filename: filePath}); + return q.nfbind(workers)({ - transformSets: transformSets, sourceCode: sourceCode, - options: opts, + filename: filePath, }).then( function(res) { if (res.error) { - throw formatEsprimaError(res.error, filePath, sourceCode); + throw formatError(res.error, filePath, sourceCode); } return { @@ -116,13 +106,28 @@ Transformer.prototype.loadFileAndTransform = function( }; function TransformError() {} -TransformError.__proto__ = SyntaxError.prototype; +util.inherits(TransformError, SyntaxError); + +function formatError(err, filename, source) { + if (err.lineNumber && err.column) { + return formatEsprimaError(err, filename, source); + } else { + return formatGenericError(err, filename, source); + } +} + +function formatGenericError(err, filename) { + var msg = 'TransformError: ' + filename + ': ' + err.message; + var error = new TransformError(); + var stack = err.stack.split('\n').slice(0, -1); + stack.push(msg); + error.stack = stack.join('\n'); + error.message = msg; + error.type = 'TransformError'; + return error; +} function formatEsprimaError(err, filename, source) { - if (!(err.lineNumber && err.column)) { - return err; - } - var stack = err.stack.split('\n'); stack.shift(); diff --git a/packager/react-packager/src/Packager/Package.js b/packager/react-packager/src/Packager/Package.js index 787684bc2..5d9b201c7 100644 --- a/packager/react-packager/src/Packager/Package.js +++ b/packager/react-packager/src/Packager/Package.js @@ -1,12 +1,13 @@ 'use strict'; var _ = require('underscore'); -var SourceMapGenerator = require('source-map').SourceMapGenerator; var base64VLQ = require('./base64-vlq'); +var UglifyJS = require('uglify-js'); module.exports = Package; function Package(sourceMapUrl) { + this._finalized = false; this._modules = []; this._sourceMapUrl = sourceMapUrl; } @@ -28,6 +29,7 @@ Package.prototype.addModule = function( }; Package.prototype.finalize = function(options) { + options = options || {}; if (options.runMainModule) { var runCode = ';require("' + this._mainModuleId + '");'; this.addModule( @@ -39,17 +41,89 @@ Package.prototype.finalize = function(options) { Object.freeze(this._modules); Object.seal(this._modules); + this._finalized = true; }; -Package.prototype.getSource = function() { - return this._source || ( - this._source = _.pluck(this._modules, 'transformedCode').join('\n') + '\n' + - 'RAW_SOURCE_MAP = ' + JSON.stringify(this.getSourceMap({excludeSource: true})) + - ';\n' + '\/\/@ sourceMappingURL=' + this._sourceMapUrl - ); +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, 'transformedCode').join('\n'); + } + return this._source; +}; + +Package.prototype._getInlineSourceMap = function() { + if (this._inlineSourceMap == null) { + var sourceMap = this.getSourceMap({excludeSource: true}); + this._inlineSourceMap = '\nRAW_SOURCE_MAP = ' + + JSON.stringify(sourceMap) + ';'; + } + + 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 += this._getInlineSourceMap(); + } + + source += '\n\/\/@ sourceMappingURL=' + 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); + } }; Package.prototype.getSourceMap = function(options) { + this._assertFinalized(); + options = options || {}; var mappings = this._getMappings(); var map = { @@ -64,7 +138,6 @@ Package.prototype.getSourceMap = function(options) { return map; }; - Package.prototype._getMappings = function() { var modules = this._modules; @@ -102,7 +175,7 @@ Package.prototype._getMappings = function() { mappings += ';'; } } - if (i != modules.length - 1) { + if (i !== modules.length - 1) { mappings += ';'; } } diff --git a/packager/react-packager/src/Packager/__mocks__/source-map.js b/packager/react-packager/src/Packager/__mocks__/source-map.js deleted file mode 100644 index 08c127f6d..000000000 --- a/packager/react-packager/src/Packager/__mocks__/source-map.js +++ /dev/null @@ -1,5 +0,0 @@ -var SourceMapGenerator = jest.genMockFn(); -SourceMapGenerator.prototype.addMapping = jest.genMockFn(); -SourceMapGenerator.prototype.setSourceContent = jest.genMockFn(); -SourceMapGenerator.prototype.toJSON = jest.genMockFn(); -exports.SourceMapGenerator = SourceMapGenerator; diff --git a/packager/react-packager/src/Packager/__tests__/Package-test.js b/packager/react-packager/src/Packager/__tests__/Package-test.js index d18bb4d6c..41630fc47 100644 --- a/packager/react-packager/src/Packager/__tests__/Package-test.js +++ b/packager/react-packager/src/Packager/__tests__/Package-test.js @@ -1,10 +1,6 @@ 'use strict'; -jest - .dontMock('underscore') - .dontMock('../base64-vlq') - .dontMock('source-map') - .dontMock('../Package'); +jest.autoMockOff(); var SourceMapGenerator = require('source-map').SourceMapGenerator; @@ -25,7 +21,7 @@ describe('Package', function() { ppackage.addModule('transformed foo;', 'source foo', 'foo path'); ppackage.addModule('transformed bar;', 'source bar', 'bar path'); ppackage.finalize({}); - expect(ppackage.getSource()).toBe([ + expect(ppackage.getSource({inlineSourceMap: true})).toBe([ 'transformed foo;', 'transformed bar;', 'RAW_SOURCE_MAP = "test-source-map";', @@ -38,7 +34,7 @@ describe('Package', function() { ppackage.addModule('transformed bar;', 'source bar', 'bar path'); ppackage.setMainModuleId('foo'); ppackage.finalize({runMainModule: true}); - expect(ppackage.getSource()).toBe([ + expect(ppackage.getSource({inlineSourceMap: true})).toBe([ 'transformed foo;', 'transformed bar;', ';require("foo");', @@ -46,17 +42,32 @@ describe('Package', function() { '\/\/@ sourceMappingURL=test_url', ].join('\n')); }); + + it('should get minified source', function() { + var minified = { + code: 'minified', + map: 'map', + }; + + require('uglify-js').minify = function() { + return minified; + }; + + ppackage.addModule('transformed foo;', 'source foo', 'foo path'); + ppackage.finalize(); + expect(ppackage.getMinifiedSourceAndMap()).toBe(minified); + }); }); describe('sourcemap package', function() { it('should create sourcemap', function() { - var ppackage = new Package('test_url'); - ppackage.addModule('transformed foo;\n', 'source foo', 'foo path'); - ppackage.addModule('transformed bar;\n', 'source bar', 'bar path'); - ppackage.setMainModuleId('foo'); - ppackage.finalize({runMainModule: true}); - var s = ppackage.getSourceMap(); - expect(s).toEqual(genSourceMap(ppackage._modules)); + var p = new Package('test_url'); + p.addModule('transformed foo;\n', 'source foo', 'foo path'); + p.addModule('transformed bar;\n', 'source bar', 'bar path'); + p.setMainModuleId('foo'); + p.finalize({runMainModule: true}); + var s = p.getSourceMap(); + expect(s).toEqual(genSourceMap(p._modules)); }); }); }); @@ -92,4 +103,4 @@ describe('Package', function() { ); } return sourceMapGen.toJSON(); -}; + } diff --git a/packager/react-packager/src/Packager/__tests__/Packager-test.js b/packager/react-packager/src/Packager/__tests__/Packager-test.js index 21af12ca8..498faea3a 100644 --- a/packager/react-packager/src/Packager/__tests__/Packager-test.js +++ b/packager/react-packager/src/Packager/__tests__/Packager-test.js @@ -6,6 +6,7 @@ jest .dontMock('q') .dontMock('os') .dontMock('underscore') + .setMock('uglify-js') .dontMock('../'); var q = require('q'); @@ -39,6 +40,11 @@ describe('Packager', function() { var modules = [ {id: 'foo', path: '/root/foo.js', dependencies: []}, {id: 'bar', path: '/root/bar.js', dependencies: []}, + { id: 'image!img', + path: '/root/img/img.png', + isAsset: true, + dependencies: [], + } ]; getDependencies.mockImpl(function() { @@ -49,7 +55,7 @@ describe('Packager', function() { }); require('../../JSTransformer').prototype.loadFileAndTransform - .mockImpl(function(tsets, path) { + .mockImpl(function(path) { return q({ code: 'transformed ' + path, sourceCode: 'source ' + path, @@ -73,6 +79,15 @@ describe('Packager', function() { 'source /root/bar.js', '/root/bar.js' ]); + expect(p.addModule.mock.calls[2]).toEqual([ + 'lol module.exports = ' + + JSON.stringify({ uri: 'img', isStatic: true}) + + '; lol', + 'module.exports = ' + + JSON.stringify({ uri: 'img', isStatic: true}) + + ';', + '/root/img/img.png' + ]); expect(p.finalize.mock.calls[0]).toEqual([ {runMainModule: true} diff --git a/packager/react-packager/src/Packager/base64-vlq.js b/packager/react-packager/src/Packager/base64-vlq.js index 91d490b7d..4483a507a 100644 --- a/packager/react-packager/src/Packager/base64-vlq.js +++ b/packager/react-packager/src/Packager/base64-vlq.js @@ -35,9 +35,11 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +/*eslint no-bitwise:0,quotes:0,global-strict:0*/ + var charToIntMap = {}; var intToCharMap = {}; - + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' .split('') .forEach(function (ch, index) { @@ -55,7 +57,7 @@ base64.encode = function base64_encode(aNumber) { } throw new TypeError("Must be between 0 and 63: " + aNumber); }; - + /** * Decode a single base 64 digit to an integer. */ @@ -65,7 +67,7 @@ base64.decode = function base64_decode(aChar) { } throw new TypeError("Not a valid base 64 digit: " + aChar); }; - + // A single base 64 digit can contain 6 bits of data. For the base 64 variable @@ -165,4 +167,3 @@ exports.decode = function base64VLQ_decode(aStr, aOutParam) { aOutParam.value = fromVLQSigned(result); aOutParam.rest = aStr.slice(i); }; - diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js index ddcab6ee6..c21889b48 100644 --- a/packager/react-packager/src/Packager/index.js +++ b/packager/react-packager/src/Packager/index.js @@ -36,18 +36,18 @@ var validateOpts = declareOpts({ type: 'boolean', default: false, }, - dev: { - type: 'boolean', - default: true, - }, transformModulePath: { type:'string', - required: true, + required: false, }, nonPersistent: { type: 'boolean', default: false, }, + assetRoots: { + type: 'array', + required: false, + }, }); function Packager(options) { @@ -59,9 +59,9 @@ function Packager(options) { projectRoots: opts.projectRoots, blacklistRE: opts.blacklistRE, polyfillModuleNames: opts.polyfillModuleNames, - dev: opts.dev, nonPersistent: opts.nonPersistent, - moduleFormat: opts.moduleFormat + moduleFormat: opts.moduleFormat, + assetRoots: opts.assetRoots, }); this._transformer = new Transformer({ @@ -69,7 +69,6 @@ function Packager(options) { blacklistRE: opts.blacklistRE, cacheVersion: opts.cacheVersion, resetCache: opts.resetCache, - dev: opts.dev, transformModulePath: opts.transformModulePath, nonPersistent: opts.nonPersistent, }); @@ -82,14 +81,14 @@ Packager.prototype.kill = function() { ]); }; -Packager.prototype.package = function(main, runModule, sourceMapUrl) { +Packager.prototype.package = function(main, runModule, sourceMapUrl, isDev) { var transformModule = this._transformModule.bind(this); var ppackage = new Package(sourceMapUrl); var findEventId = Activity.startEvent('find dependencies'); var transformEventId; - return this.getDependencies(main) + return this.getDependencies(main, isDev) .then(function(result) { Activity.endEvent(findEventId); transformEventId = Activity.startEvent('transform'); @@ -119,17 +118,23 @@ Packager.prototype.invalidateFile = function(filePath) { this._transformer.invalidateFile(filePath); }; -Packager.prototype.getDependencies = function(main) { - return this._resolver.getDependencies(main); +Packager.prototype.getDependencies = function(main, isDev) { + return this._resolver.getDependencies(main, { dev: isDev }); }; Packager.prototype._transformModule = function(module) { + var transform; + + if (module.isAsset) { + transform = q(generateAssetModule(module)); + } else { + transform = this._transformer.loadFileAndTransform( + path.resolve(module.path) + ); + } + var resolver = this._resolver; - return this._transformer.loadFileAndTransform( - ['es6'], - path.resolve(module.path), - this._opts.transformer || {} - ).then(function(transformed) { + return transform.then(function(transformed) { return _.extend( {}, transformed, @@ -148,5 +153,17 @@ Packager.prototype.getGraphDebugInfo = function() { return this._resolver.getDebugInfo(); }; +function generateAssetModule(module) { + var code = 'module.exports = ' + JSON.stringify({ + uri: module.id.replace(/^[^!]+!/, ''), + isStatic: true, + }) + ';'; + + return { + code: code, + sourceCode: code, + sourcePath: module.path, + }; +} 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 690c7e068..f49058061 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -1,16 +1,22 @@ -jest.setMock('worker-farm', function(){ return function(){}; }) +'use strict'; + +jest.setMock('worker-farm', function() { return function() {}; }) .dontMock('q') .dontMock('os') - .dontMock('errno/custom') .dontMock('path') .dontMock('url') + .setMock('timers', { + setImmediate: function(fn) { + return setTimeout(fn, 0); + } + }) + .setMock('uglify-js') .dontMock('../'); var q = require('q'); -describe('processRequest', function(){ +describe('processRequest', function() { var server; - var Activity; var Packager; var FileWatcher; @@ -21,16 +27,16 @@ describe('processRequest', function(){ polyfillModuleNames: null }; - var makeRequest = function(requestHandler, requrl){ + var makeRequest = function(requestHandler, requrl) { var deferred = q.defer(); requestHandler({ url: requrl },{ - end: function(res){ + end: function(res) { deferred.resolve(res); } },{ - next: function(){} + next: function() {} } ); return deferred.promise; @@ -39,24 +45,32 @@ describe('processRequest', function(){ var invalidatorFunc = jest.genMockFunction(); var watcherFunc = jest.genMockFunction(); var requestHandler; + var triggerFileChange; - beforeEach(function(){ - Activity = require('../../Activity'); + beforeEach(function() { Packager = require('../../Packager'); FileWatcher = require('../../FileWatcher'); - Packager.prototype.package = function() { + Packager.prototype.package = jest.genMockFunction().mockImpl(function() { return q({ getSource: function() { return 'this is the source'; }, - getSourceMap: function(){ + getSourceMap: function() { return 'this is the source map'; }, }); - }; + }); - FileWatcher.prototype.on = watcherFunc; + + FileWatcher.prototype.on = function(eventType, callback) { + if (eventType !== 'all') { + throw new Error('Can only handle "all" event in watcher.'); + } + watcherFunc.apply(this, arguments); + triggerFileChange = callback; + return this; + }; Packager.prototype.invalidateFile = invalidatorFunc; @@ -65,44 +79,65 @@ describe('processRequest', function(){ requestHandler = server.processRequest.bind(server); }); - pit('returns JS bundle source on request of *.bundle',function(){ - result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle'); - return result.then(function(response){ - expect(response).toEqual("this is the source"); + pit('returns JS bundle source on request of *.bundle',function() { + return makeRequest( + requestHandler, + 'mybundle.bundle?runModule=true' + ).then(function(response) { + expect(response).toEqual('this is the source'); }); }); - pit('returns sourcemap on request of *.map', function(){ - result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle.map'); - return result.then(function(response){ + pit('returns JS bundle source on request of *.bundle (compat)',function() { + return makeRequest( + requestHandler, + 'mybundle.runModule.bundle' + ).then(function(response) { + expect(response).toEqual('this is the source'); + }); + }); + + pit('returns sourcemap on request of *.map', function() { + return makeRequest( + requestHandler, + 'mybundle.map?runModule=true' + ).then(function(response) { expect(response).toEqual('"this is the source map"'); }); }); - pit('watches all files in projectRoot', function(){ - result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle'); - return result.then(function(response){ + pit('works with .ios.js extension', function() { + return makeRequest( + requestHandler, + 'index.ios.includeRequire.bundle' + ).then(function(response) { + expect(response).toEqual('this is the source'); + expect(Packager.prototype.package).toBeCalledWith( + 'index.ios.js', + true, + 'index.ios.includeRequire.map', + true + ); + }); + }); + + pit('watches all files in projectRoot', function() { + return makeRequest( + requestHandler, + 'mybundle.bundle?runModule=true' + ).then(function() { expect(watcherFunc.mock.calls[0][0]).toEqual('all'); expect(watcherFunc.mock.calls[0][1]).not.toBe(null); - }) + }); }); describe('file changes', function() { - var triggerFileChange; - beforeEach(function() { - FileWatcher.prototype.on = function(eventType, callback) { - if (eventType !== 'all') { - throw new Error('Can only handle "all" event in watcher.'); - } - triggerFileChange = callback; - return this; - }; - }); - pit('invalides files in package when file is updated', function() { - result = makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle'); - return result.then(function(response){ + return makeRequest( + requestHandler, + 'mybundle.bundle?runModule=true' + ).then(function() { var 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'); @@ -114,43 +149,75 @@ describe('processRequest', function(){ packageFunc .mockReturnValueOnce( q({ - getSource: function(){ - return "this is the first source" + getSource: function() { + return 'this is the first source'; }, - getSourceMap: function(){}, + getSourceMap: function() {}, }) ) .mockReturnValue( q({ - getSource: function(){ - return "this is the rebuilt source" + getSource: function() { + return 'this is the rebuilt source'; }, - getSourceMap: function(){}, + getSourceMap: function() {}, }) ); Packager.prototype.package = packageFunc; var Server = require('../../Server'); - var server = new Server(options); + server = new Server(options); requestHandler = server.processRequest.bind(server); - return makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle') - .then(function(response){ - expect(response).toEqual("this is the first source"); + return makeRequest(requestHandler, 'mybundle.bundle?runModule=true') + .then(function(response) { + expect(response).toEqual('this is the first source'); expect(packageFunc.mock.calls.length).toBe(1); triggerFileChange('all','path/file.js', options.projectRoots[0]); jest.runAllTimers(); }) - .then(function(){ + .then(function() { expect(packageFunc.mock.calls.length).toBe(2); - return makeRequest(requestHandler,'mybundle.includeRequire.runModule.bundle') - .then(function(response){ - expect(response).toEqual("this is the rebuilt source"); + return makeRequest(requestHandler, 'mybundle.bundle?runModule=true') + .then(function(response) { + expect(response).toEqual('this is the rebuilt source'); }); }); }); }); + + describe('/onchange endpoint', function() { + var EventEmitter; + var req; + var res; + + beforeEach(function() { + EventEmitter = require.requireActual('events').EventEmitter; + req = new EventEmitter(); + req.url = '/onchange'; + res = { + writeHead: jest.genMockFn(), + end: jest.genMockFn() + }; + }); + + it('should hold on to request and inform on change', function() { + 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() { + server.processRequest(req, res); + req.emit('close'); + jest.runAllTimers(); + triggerFileChange('all', 'path/file.js', options.projectRoots[0]); + jest.runAllTimers(); + expect(res.end).not.toBeCalled(); + }); + }); }); diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 611d703e3..3043903dd 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -1,3 +1,5 @@ +'use strict'; + var url = require('url'); var path = require('path'); var declareOpts = require('../lib/declareOpts'); @@ -5,6 +7,7 @@ var FileWatcher = require('../FileWatcher'); var Packager = require('../Packager'); var Activity = require('../Activity'); var q = require('q'); +var _ = require('underscore'); module.exports = Server; @@ -32,18 +35,18 @@ var validateOpts = declareOpts({ type: 'boolean', default: false, }, - dev: { - type: 'boolean', - default: true, - }, transformModulePath: { type:'string', - required: true, + required: false, }, nonPersistent: { type: 'boolean', default: false, }, + assetRoots: { + type: 'array', + required: false, + }, }); function Server(options) { @@ -51,6 +54,7 @@ function Server(options) { this._projectRoots = opts.projectRoots; this._packages = Object.create(null); this._packager = new Packager(opts); + this._changeWatchers = []; this._fileWatcher = options.nonPersistent ? FileWatcher.createDummyWatcher() @@ -58,6 +62,12 @@ function Server(options) { 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, true); } Server.prototype._onFileChange = function(type, filepath, root) { @@ -65,22 +75,42 @@ Server.prototype._onFileChange = function(type, filepath, root) { this._packager.invalidateFile(absPath); // Make sure the file watcher event runs through the system before // we rebuild the packages. - setImmediate(this._rebuildPackages.bind(this, absPath)); + this._debouncedFileChangeHandler(absPath); }; Server.prototype._rebuildPackages = function() { var buildPackage = this._buildPackage.bind(this); var packages = this._packages; Object.keys(packages).forEach(function(key) { - var options = getOptionsFromPath(url.parse(key).pathname); - packages[key] = buildPackage(options).then(function(p) { - // Make a throwaway call to getSource to cache the source string. - p.getSource(); - return p; + var options = getOptionsFromUrl(key); + // Wait for a previous build (if exists) to finish. + packages[key] = (packages[key] || q()).then(function() { + return buildPackage(options).then(function(p) { + // Make a throwaway call to getSource to cache the source string. + p.getSource({ + inlineSourceMap: options.dev, + minify: options.minify, + }); + return p; + }); }); }); }; +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() { q.all([ this._fileWatcher.end(), @@ -92,12 +122,13 @@ Server.prototype._buildPackage = function(options) { return this._packager.package( options.main, options.runModule, - options.sourceMapUrl + options.sourceMapUrl, + options.dev ); }; Server.prototype.buildPackageFromUrl = function(reqUrl) { - var options = getOptionsFromPath(url.parse(reqUrl).pathname); + var options = getOptionsFromUrl(reqUrl); return this._buildPackage(options); }; @@ -139,27 +170,56 @@ Server.prototype._processDebugRequest = function(reqUrl, res) { } }; +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; + } + } + }); +}; + Server.prototype.processRequest = function(req, res, next) { + var urlObj = url.parse(req.url, true); + var pathname = urlObj.pathname; + var requestType; - if (req.url.match(/\.bundle$/)) { + if (pathname.match(/\.bundle$/)) { requestType = 'bundle'; - } else if (req.url.match(/\.map$/)) { + } else if (pathname.match(/\.map$/)) { requestType = 'map'; - } else if (req.url.match(/^\/debug/)) { + } else if (pathname.match(/^\/debug/)) { this._processDebugRequest(req.url, res); return; + } else if (pathname.match(/^\/onchange\/?$/)) { + this._processOnChangeRequest(req, res); + return; } else { - return next(); + next(); + return; } var startReqEventId = Activity.startEvent('request:' + req.url); - var options = getOptionsFromPath(url.parse(req.url).pathname); - var building = this._packages[req.url] || this._buildPackage(options) + var options = getOptionsFromUrl(req.url); + var building = this._packages[req.url] || this._buildPackage(options); + this._packages[req.url] = building; - building.then( + building.then( function(p) { if (requestType === 'bundle') { - res.end(p.getSource()); + res.end(p.getSource({ + inlineSourceMap: options.dev, + minify: options.minify, + })); Activity.endEvent(startReqEventId); } else if (requestType === 'map') { res.end(JSON.stringify(p.getSourceMap())); @@ -172,19 +232,38 @@ Server.prototype.processRequest = function(req, res, next) { ).done(); }; -function getOptionsFromPath(pathname) { - var parts = pathname.split('.'); - // Remove the leading slash. - var main = parts[0].slice(1) + '.js'; +function getOptionsFromUrl(reqUrl) { + // `true` to parse the query param as an object. + var urlObj = url.parse(reqUrl, true); + var pathname = 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 { - runModule: parts.slice(1).some(function(part) { - return part === 'runModule'; - }), - main: main, - sourceMapUrl: parts.slice(0, -1).join('.') + '.map' + sourceMapUrl: pathname.replace(/\.bundle$/, '.map'), + main: entryFile, + dev: getBoolOptionFromQuery(urlObj.query, 'dev', true), + minify: getBoolOptionFromQuery(urlObj.query, 'minify'), + runModule: getBoolOptionFromQuery(urlObj.query, 'runModule', true), }; } +function getBoolOptionFromQuery(query, opt, defaultVal) { + if (query[opt] == null && defaultVal != null) { + return defaultVal; + } + + return query[opt] === 'true' || query[opt] === '1'; +} + function handleError(res, error) { res.writeHead(500, { 'Content-Type': 'application/json; charset=UTF-8', diff --git a/packager/react-packager/src/fb-path-utils/index.js b/packager/react-packager/src/fb-path-utils/index.js deleted file mode 100644 index b4a1cb967..000000000 --- a/packager/react-packager/src/fb-path-utils/index.js +++ /dev/null @@ -1,14 +0,0 @@ -var absolutePath = require('absolute-path'); -var path = require('path'); -var pathIsInside = require('path-is-inside'); - -function isAbsolutePath(pathStr) { - return absolutePath(pathStr); -} - -function isChildPath(parentPath, childPath) { - return pathIsInside(parentPath, childPath); -} - -exports.isAbsolutePath = isAbsolutePath; -exports.isChildPath = isChildPath; diff --git a/packager/react-packager/src/lib/__mocks__/declareOpts.js b/packager/react-packager/src/lib/__mocks__/declareOpts.js index 2f7ae1f6b..1afe4e297 100644 --- a/packager/react-packager/src/lib/__mocks__/declareOpts.js +++ b/packager/react-packager/src/lib/__mocks__/declareOpts.js @@ -1,3 +1,5 @@ +'use strict'; + module.exports = function(declared) { return function(opts) { for (var p in declared) { diff --git a/packager/react-packager/src/lib/__tests__/declareOpts-test.js b/packager/react-packager/src/lib/__tests__/declareOpts-test.js index 044e3a1c6..66ae174fb 100644 --- a/packager/react-packager/src/lib/__tests__/declareOpts-test.js +++ b/packager/react-packager/src/lib/__tests__/declareOpts-test.js @@ -1,3 +1,5 @@ +'use strict'; + jest.autoMockOff(); var declareOpts = require('../declareOpts'); diff --git a/packager/react-packager/src/lib/declareOpts.js b/packager/react-packager/src/lib/declareOpts.js index 2bac59f33..3b80da519 100644 --- a/packager/react-packager/src/lib/declareOpts.js +++ b/packager/react-packager/src/lib/declareOpts.js @@ -10,6 +10,8 @@ * var myOptions = validate(someOptions); */ +'use strict'; + var Joi = require('joi'); module.exports = function(descriptor) { @@ -40,6 +42,8 @@ module.exports = function(descriptor) { var schema = Joi.object().keys(joiKeys); return function(opts) { + opts = opts || {}; + var res = Joi.validate(opts, schema, { abortEarly: true, allowUnknown: false, diff --git a/packager/transformer.js b/packager/transformer.js index ffcb80e2d..acb586d7f 100644 --- a/packager/transformer.js +++ b/packager/transformer.js @@ -16,10 +16,11 @@ var staticTypeSyntax = var visitorList = reactVisitors; -function transform(transformSets, srcTxt) { +function transform(srcTxt, filename) { var options = { es3: true, - sourceType: 'nonStrictModule' + sourceType: 'nonStrictModule', + filename: filename, }; // These tranforms mostly just erase type annotations and static typing @@ -42,8 +43,8 @@ module.exports = function(data, callback) { var result; try { result = transform( - data.transformSets, - data.sourceCode + data.sourceCode, + data.filename ); } catch (e) { return callback(null, { diff --git a/packager/webSocketProxy.js b/packager/webSocketProxy.js new file mode 100644 index 000000000..dada059a8 --- /dev/null +++ b/packager/webSocketProxy.js @@ -0,0 +1,41 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ + +'use strict'; + +var WebSocketServer = require('ws').Server; + +function attachToServer(server, path) { + var wss = new WebSocketServer({ + server: server, + path: path + }); + var clients = []; + + wss.on('connection', function(ws) { + clients.push(ws); + + var allClientsExcept = function(ws) { + return clients.filter(function(cn) { return cn !== ws; }); + }; + + ws.onerror = function() { + clients = allClientsExcept(ws); + }; + + ws.onclose = function() { + clients = allClientsExcept(ws); + }; + + ws.on('message', function(message) { + allClientsExcept(ws).forEach(function(cn) { + cn.send(message); + }); + }); + }); +} + +module.exports = { + attachToServer: attachToServer +}; diff --git a/react-native-cli/index.js b/react-native-cli/index.js new file mode 100755 index 000000000..49ee50013 --- /dev/null +++ b/react-native-cli/index.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +/** + * Copyright 2004-present Facebook. All Rights Reserved. + */ + +var fs = require('fs'); +var path = require('path'); +var spawn = require('child_process').spawn; + +var CLI_MODULE_PATH = function() { + return path.resolve( + process.cwd(), + 'node_modules', + 'react-native', + 'cli' + ); +}; + +var cli; +try { + cli = require(CLI_MODULE_PATH()); +} catch(e) {} + +if (cli) { + cli.run(); +} else { + var args = process.argv.slice(2); + if (args.length === 0) { + console.error( + 'You did not pass any commands, did you mean to run `react-native init`?' + ); + process.exit(1); + } + + if (args[0] === 'init') { + if (args[1]) { + init(args[1]); + } else { + console.error( + 'Usage: react-native init ' + ); + process.exit(1); + } + } else { + console.error( + 'Command `%s` unrecognized.' + + 'Did you mean to run this inside a react-native project?', + args[0] + ); + process.exit(1); + } +} + +function init(name) { + var root = path.resolve(name); + var projectName = path.basename(root); + + console.log( + 'This will walk you through creating a new React Native project in', + root + ); + + if (!fs.existsSync(root)) { + fs.mkdirSync(root); + } + + var packageJson = { + name: projectName, + version: '0.0.1', + private: true, + scripts: { + start: "react-native start" + } + }; + fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(packageJson)); + process.chdir(root); + + run('npm install --save react-native', function(e) { + if (e) { + console.error('`npm install --save react-native` failed'); + process.exit(1); + } + + var cli = require(CLI_MODULE_PATH()); + cli.init(root, projectName); + }); +} + +function run(command, cb) { + var parts = command.split(/\s+/); + var cmd = parts[0]; + var args = parts.slice(1); + var proc = spawn(cmd, args, {stdio: 'inherit'}); + proc.on('close', function(code) { + if (code !== 0) { + cb(new Error('Command exited with a non-zero status')); + } else { + cb(null); + } + }); +} diff --git a/react-native-cli/package.json b/react-native-cli/package.json new file mode 100644 index 000000000..e0e19dd72 --- /dev/null +++ b/react-native-cli/package.json @@ -0,0 +1,9 @@ +{ + "name": "react-native-cli", + "version": "0.0.0", + "description": "The ReactNative cli tools", + "main": "index.js", + "bin": { + "react-native": "index.js" + } +} diff --git a/website/core/DocsSidebar.js b/website/core/DocsSidebar.js index b730acb0d..7b8cbb98b 100644 --- a/website/core/DocsSidebar.js +++ b/website/core/DocsSidebar.js @@ -9,7 +9,7 @@ var Metadata = require('Metadata'); var DocsSidebar = React.createClass({ getCategories: function() { var metadatas = Metadata.files.filter(function(metadata) { - return metadata.layout === 'docs'; + return metadata.layout === 'docs' || metadata.layout === 'autodocs'; }); // Build a hashmap of article_id -> metadata diff --git a/website/core/Header.js b/website/core/Header.js index f49cae374..24e6784db 100644 --- a/website/core/Header.js +++ b/website/core/Header.js @@ -9,10 +9,9 @@ var slugify = require('slugify'); var Header = React.createClass({ render: function() { var slug = slugify(this.props.toSlug || this.props.children); - var H = React.DOM['h' + this.props.level]; - - return this.transferPropsTo( - + var H = 'h' + this.props.level; + return ( + {this.props.children} {' '}# diff --git a/website/core/Marked.js b/website/core/Marked.js index f9baeaf49..13eaab841 100644 --- a/website/core/Marked.js +++ b/website/core/Marked.js @@ -900,7 +900,7 @@ Parser.prototype.tok = function() { } case 'html': { return !this.token.pre && !this.options.pedantic - ? this.inline.output(this.token.text) + ? React.DOM.span({dangerouslySetInnerHTML: {__html: this.token.text}}) : this.token.text; } case 'paragraph': { @@ -1085,7 +1085,9 @@ marked.parse = marked; var Marked = React.createClass({ render: function() { - return React.DOM.div(null, marked(this.props.children, this.props)); + return this.props.children ? + React.DOM.div(null, marked(this.props.children, this.props)) : + null; } }); diff --git a/website/core/center.js b/website/core/center.js index 78fb127d5..fdc930965 100644 --- a/website/core/center.js +++ b/website/core/center.js @@ -7,8 +7,10 @@ var React = require('React'); var center = React.createClass({ render: function() { - return this.transferPropsTo( -
{this.props.children}
+ return ( +
+ {this.props.children} +
); } }); diff --git a/website/jsdocs/TypeExpressionParser.js b/website/jsdocs/TypeExpressionParser.js new file mode 100644 index 000000000..7efcb747a --- /dev/null +++ b/website/jsdocs/TypeExpressionParser.js @@ -0,0 +1,557 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +/*global exports:true*/ +"use strict"; + +var Syntax = require('esprima-fb').Syntax; + +function toObject(/*array*/ array) /*object*/ { + var object = {}; + for (var i = 0; i < array.length; i++) { + var value = array[i]; + object[value] = value; + } + return object; +} + +function reverseObject(/*object*/ object) /*object*/ { + var reversed = {}; + for (var key in object) { + if (object.hasOwnProperty(key)) { + reversed[object[key]] = key + } + } + return reversed; +} + +function getTagName(string) { + if (string === 'A') { + return 'Anchor'; + } + if (string === 'IMG') { + return 'Image'; + } + return string.charAt(0).toUpperCase() + string.substring(1).toLowerCase(); +} + +var TOKENS = { + STRING: 'string', + OPENGENERIC: '<', + CLOSEGENERIC: '>', + COMMA: ',', + OPENPAREN: '(', + CLOSEPAREN: ')', + COLON: ':', + BAR: '|', + NULLABLE: '?', + EOL: 'eol', + OPENSEGMENT: '{', + CLOSESEGMENT: '}' +}; +var TOKENMAP = reverseObject(TOKENS); + +var SYMBOLS = { + SIMPLE: 'simple', + UNION: 'union', + GENERIC: 'generic', + FUNCTION: 'function', + SEGMENT: 'segment' +}; + +var PARSERS = { + SIMPLE: 1, + UNION: 2, + GENERIC: 4, + FUNCTION: 8, + SEGMENT: 16 +}; + +/*----- tokenizer-----*/ + +function createTokenStream(source) { + var stream = [], string, pos = 0; + + do { + var character = source.charAt(pos); + if (character && /\w/.test(character)) { + string = string ? string + character : character; + } else { + if (string) { + stream.push({ type: TOKENS.STRING, value: string }); + string = null; + } + + if (character) { + if (character in TOKENMAP) { + stream.push({ type: character }); + } else { + throwError('Invalid character: ' + character + ' at pos: ' + pos); + } + } else { + stream.push({ type: TOKENS.EOL }); + break; + } + } + } while (++pos); + + return stream; +} + +/*----- parser-----*/ + +var SIMPLETYPES = toObject([ + 'string', + 'number', + 'regexp', + 'boolean', + 'object', + 'function', + 'array', + 'date', + 'blob', + 'file', + 'int8array', + 'uint8array', + 'int16array', + 'uint16array', + 'int32array', + 'uint32array', + 'float32array', + 'float64array', + 'filelist', + 'promise', + 'map', + 'set' +]); + +// types typically used in legacy docblock +var BLACKLISTED = toObject([ + 'Object', + 'Boolean', + 'bool', + 'Number', + 'String', + 'int', + 'Node', + 'Element', +]); + +function createAst(type, value, length) { + return { type: type, value: value, length: length }; +} + +function nullable(fn) { + return function(stream, pos) { + var nullable = stream[pos].type == '?' && ++pos; + var ast = fn(stream, pos); + if (ast && nullable) { + ast.nullable = true; + ast.length++; + } + return ast; + }; +} + +var parseSimpleType = nullable(function(stream, pos) { + if (stream[pos].type == TOKENS.STRING) { + var value = stream[pos].value; + if ((/^[a-z]/.test(value) && !(value in SIMPLETYPES)) + || value in BLACKLISTED) { + throwError('Invalid type ' + value + ' at pos: ' + pos); + } + return createAst(SYMBOLS.SIMPLE, stream[pos].value, 1); + } +}); + +var parseUnionType = nullable(function(stream, pos) { + var parsers = + PARSERS.SIMPLE | PARSERS.GENERIC | PARSERS.FUNCTION | PARSERS.SEGMENT; + var list = parseList(stream, pos, TOKENS.BAR, parsers); + + if (list.value.length > 1) { + return createAst(SYMBOLS.UNION, list.value, list.length); + } +}); + +var parseGenericType = nullable(function(stream, pos, ast) { + var genericAst, typeAst; + if ((genericAst = parseSimpleType(stream, pos)) && + stream[pos + genericAst.length].type == TOKENS.OPENGENERIC && + (typeAst = parseAnyType(stream, pos += genericAst.length + 1))) { + + if (stream[pos + typeAst.length].type != TOKENS.CLOSEGENERIC) { + throwError('Missing ' + TOKENS.CLOSEGENERIC + + ' at pos: ' + pos + typeAst.length); + } + + return createAst(SYMBOLS.GENERIC, [genericAst, typeAst], + genericAst.length + typeAst.length + 2); + } +}); + +var parseFunctionType = nullable(function(stream, pos) { + if (stream[pos].type == TOKENS.STRING && + stream[pos].value == 'function' && + stream[++pos].type == TOKENS.OPENPAREN) { + + var list = stream[pos + 1].type != TOKENS.CLOSEPAREN + ? parseList(stream, pos + 1, TOKENS.COMMA) + : {value: [], length: 0}; + + pos += list.length + 1; + + if (stream[pos].type == TOKENS.CLOSEPAREN) { + var length = list.length + 3, returnAst; + + if (stream[++pos].type == TOKENS.COLON) { + returnAst = parseAnyType(stream, ++pos); + if (!returnAst) { + throwError('Could not parse return type at pos: ' + pos); + } + length += returnAst.length + 1; + } + return createAst(SYMBOLS.FUNCTION, [list.value, returnAst || null], + length); + } + } +}); + +function parseSegmentType(stream, pos) { + var segmentAst; + if (stream[pos].type == TOKENS.OPENSEGMENT && + (segmentAst = parseAnyType(stream, ++pos))) { + pos += segmentAst.length + if (stream[pos].type == TOKENS.CLOSESEGMENT) { + return createAst(SYMBOLS.SEGMENT, segmentAst, segmentAst.length + 2); + } + } +} + +function parseAnyType(stream, pos, parsers) { + if (!parsers) parsers = + PARSERS.SEGMENT | PARSERS.SIMPLE | PARSERS.UNION | PARSERS.GENERIC + | PARSERS.FUNCTION; + + var ast = + (parsers & PARSERS.UNION && parseUnionType(stream, pos)) || + (parsers & PARSERS.SEGMENT && parseSegmentType(stream, pos)) || + (parsers & PARSERS.GENERIC && parseGenericType(stream, pos)) || + (parsers & PARSERS.FUNCTION && parseFunctionType(stream, pos)) || + (parsers & PARSERS.SIMPLE && parseSimpleType(stream, pos)); + if (!ast) { + throwError('Could not parse ' + stream[pos].type); + } + return ast; +} + +function parseList(stream, pos, separator, parsers) { + var symbols = [], childAst, length = 0, separators = 0; + while (true) { + if (childAst = parseAnyType(stream, pos, parsers)) { + symbols.push(childAst); + length += childAst.length; + pos += childAst.length; + + if (stream[pos].type == separator) { + length++; + pos++; + separators++; + continue; + } + } + break; + } + + if (symbols.length && symbols.length != separators + 1) { + throwError('Malformed list expression'); + } + + return { + value: symbols, + length: length + }; +} + +var _source; +function throwError(msg) { + throw new Error(msg + '\nSource: ' + _source); +} + + +function parse(source) { + _source = source; + var stream = createTokenStream(source); + var ast = parseAnyType(stream, 0); + if (ast) { + if (ast.length + 1 != stream.length) { + console.log(ast); + throwError('Could not parse ' + stream[ast.length].type + + ' at token pos:' + ast.length); + } + return ast; + } else { + throwError('Failed to parse the source'); + } +} + +exports.createTokenStream = createTokenStream; +exports.parse = parse; +exports.parseList = parseList; + +/*----- compiler -----*/ + +var compilers = {}; + +compilers[SYMBOLS.SIMPLE] = function(ast) { + switch (ast.value) { + case 'DOMElement': return 'HTMLElement'; + case 'FBID': return 'string'; + default: return ast.value; + } +}; + +compilers[SYMBOLS.UNION] = function(ast) { + return ast.value.map(function(symbol) { + return compile(symbol); + }).join(TOKENS.BAR); +}; + +compilers[SYMBOLS.GENERIC] = function(ast) { + var type = compile(ast.value[0]); + var parametricType = compile(ast.value[1]); + if (type === 'HTMLElement') { + return 'HTML' + getTagName(parametricType) + 'Element'; + } + return type + '<' + parametricType + '>'; +}; + +compilers[SYMBOLS.FUNCTION] = function(ast) { + return 'function(' + ast.value[0].map(function(symbol) { + return compile(symbol); + }).join(TOKENS.COMMA) + ')' + + (ast.value[1] ? ':' + compile(ast.value[1]) : ''); +}; + +function compile(ast) { + return (ast.nullable ? '?' : '') + compilers[ast.type](ast); +} + +exports.compile = compile; + +/*----- normalizer -----*/ + +function normalize(ast) { + if (ast.type === SYMBOLS.UNION) { + return ast.value.map(normalize).reduce(function(list, nodes) { + return list ? list.concat(nodes) : nodes; + }); + } + + var valueNodes = ast.type === SYMBOLS.GENERIC + ? normalize(ast.value[1]) + : [ast.value]; + + return valueNodes.map(function(valueNode) { + return createAst( + ast.type, + ast.type === SYMBOLS.GENERIC + ? [ast.value[0], valueNode] + : valueNode, + ast.length); + }); +} + +exports.normalize = function(ast) { + var normalized = normalize(ast); + normalized = normalized.length === 1 + ? normalized[0] + : createAst(SYMBOLS.UNION, normalized, normalized.length); + if (ast.nullable) { + normalized.nullable = true; + } + return normalized; +}; + +/*----- Tracking TypeAliases -----*/ + +function initTypeAliasTracking(state) { + state.g.typeAliasScopes = []; +} + +function pushTypeAliases(state, typeAliases) { + state.g.typeAliasScopes.unshift(typeAliases); +} + +function popTypeAliases(state) { + state.g.typeAliasScopes.shift(); +} + +function getTypeAlias(id, state) { + var typeAliasScopes = state.g.typeAliasScopes; + for (var ii = 0; ii < typeAliasScopes.length; ii++) { + var typeAliasAnnotation = typeAliasScopes[ii][id.name]; + if (typeAliasAnnotation) { + return typeAliasAnnotation; + } + } + return null; +} + +exports.initTypeAliasTracking = initTypeAliasTracking; +exports.pushTypeAliases = pushTypeAliases; +exports.popTypeAliases = popTypeAliases; + +/*----- Tracking which TypeVariables are in scope -----*/ +// Counts how many scopes deep each type variable is + +function initTypeVariableScopeTracking(state) { + state.g.typeVariableScopeDepth = {}; +} + +function pushTypeVariables(node, state) { + var parameterDeclaration = node.typeParameters, scopeHistory; + + if (parameterDeclaration != null + && parameterDeclaration.type === Syntax.TypeParameterDeclaration) { + parameterDeclaration.params.forEach(function (id) { + scopeHistory = state.g.typeVariableScopeDepth[id.name] || 0; + state.g.typeVariableScopeDepth[id.name] = scopeHistory + 1; + }); + } +} + +function popTypeVariables(node, state) { + var parameterDeclaration = node.typeParameters, scopeHistory; + + if (parameterDeclaration != null + && parameterDeclaration.type === Syntax.TypeParameterDeclaration) { + parameterDeclaration.params.forEach(function (id) { + scopeHistory = state.g.typeVariableScopeDepth[id.name]; + state.g.typeVariableScopeDepth[id.name] = scopeHistory - 1; + }); + } +} + +function isTypeVariableInScope(id, state) { + return state.g.typeVariableScopeDepth[id.name] > 0; +} + +exports.initTypeVariableScopeTracking = initTypeVariableScopeTracking; +exports.pushTypeVariables = pushTypeVariables; +exports.popTypeVariables = popTypeVariables; + +/*----- FromFlowToTypechecks -----*/ + +function fromFlowAnnotation(/*object*/ annotation, state) /*?object*/ { + var ast; + switch (annotation.type) { + case "NumberTypeAnnotation": + return createAst(SYMBOLS.SIMPLE, "number", 0); + case "StringTypeAnnotation": + return createAst(SYMBOLS.SIMPLE, "string", 0); + case "BooleanTypeAnnotation": + return createAst(SYMBOLS.SIMPLE, "boolean", 0); + case "AnyTypeAnnotation": // fallthrough + case "VoidTypeAnnotation": + return null; + case "NullableTypeAnnotation": + ast = fromFlowAnnotation(annotation.typeAnnotation, state); + if (ast) { + ast.nullable = true; + } + return ast; + case 'ObjectTypeAnnotation': + // ObjectTypeAnnotation is always converted to a simple object type, as we + // don't support records + return createAst(SYMBOLS.SIMPLE, 'object', 0); + case 'FunctionTypeAnnotation': + var params = annotation.params + .map(function(param) { + return fromFlowAnnotation(param.typeAnnotation, state); + }) + .filter(function(ast) { + return !!ast; + }); + + var returnType = fromFlowAnnotation(annotation.returnType, state); + + // If any of the params have a type that cannot be expressed, then we have + // to render a simple function instead of a detailed one + if ((params.length || returnType) + && params.length === annotation.params.length) { + return createAst(SYMBOLS.FUNCTION, [params, returnType], 0) + } + return createAst(SYMBOLS.SIMPLE, 'function', 0); + case "GenericTypeAnnotation": + var alias = getTypeAlias(annotation.id, state); + if (alias) { + return fromFlowAnnotation(alias, state); + } + + // Qualified type identifiers are not handled by runtime typechecker, + // so simply omit the annotation for now. + if (annotation.id.type === "QualifiedTypeIdentifier") { + return null; + } + + if (isTypeVariableInScope(annotation.id, state)) { + return null; + } + + var name = annotation.id.name; + var nameLowerCased = name.toLowerCase(); + if (name !== 'Object' && BLACKLISTED.hasOwnProperty(name)) { + return null; + } + if (SIMPLETYPES.hasOwnProperty(nameLowerCased)) { + name = nameLowerCased; + } + + var id = createAst( + SYMBOLS.SIMPLE, + name, + 0 + ); + + switch (name) { + case "mixed": // fallthrough + case "$Enum": + // Not supported + return null; + case "array": // fallthrough + case "promise": + if (annotation.typeParameters) { + var parametricAst = fromFlowAnnotation( + annotation.typeParameters.params[0], + state + ); + if (parametricAst) { + return createAst( + SYMBOLS.GENERIC, + [id, parametricAst], + 0 + ); + } + } + break; + case '$Either': + if (annotation.typeParameters) { + return createAst( + SYMBOLS.UNION, + annotation.typeParameters.params.map( + function (node) { return fromFlowAnnotation(node, state); } + ), + 0 + ); + } + return null; + } + return id; + } + return null; +} + +exports.fromFlow = function(/*object*/ annotation, state) /*?string*/ { + var ast = fromFlowAnnotation(annotation, state); + return ast ? compile(ast) : null; +}; diff --git a/website/jsdocs/findExportDefinition.js b/website/jsdocs/findExportDefinition.js new file mode 100644 index 000000000..07c38b5a7 --- /dev/null +++ b/website/jsdocs/findExportDefinition.js @@ -0,0 +1,276 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +/*jslint node: true */ +"use strict"; + +var esprima = require('esprima-fb'); +var Syntax = esprima.Syntax; +var traverseFlat = require('./traverseFlat'); + + +/** + * If the expression is an identifier, it is resolved in the scope chain. + * If it is an assignment expression, it resolves to the right hand side. + * + * In all other cases the expression itself is returned. + * + * Since the scope chain constructed by the traverse function is very simple + * (it doesn't take into account *changes* to the variable through assignment + * statements), this function doesn't return the correct value in every + * situation. But it's good enough for how it is used in the parser. + * + * @param {object} expr + * @param {array} scopeChain + * + * @return {object} + */ +function resolveToValue(expr, scopeChain) { + switch (expr.type) { + case Syntax.AssignmentExpression: + if (expr.operator === '=') { + return resolveToValue(expr.right, scopeChain); + } + break; + case Syntax.Identifier: + var value; + scopeChain.some(function(scope, i) { + if (hasOwnProperty.call(scope, expr.name) && scope[expr.name]) { + value = resolveToValue(scope[expr.name], scopeChain.slice(i)); + return true; + } + }); + return value; + } + return expr; +} + +/** + * Returns true if the statement is of form `foo = bar;`. + * + * @param {object} node + * @return {bool} + */ +function isAssignmentStatement(node) { + return node.type === Syntax.ExpressionStatement && + node.expression.type === Syntax.AssignmentExpression && + node.expression.operator === '='; +} + +/** + * Splits a member or call expression into parts. E.g. foo.bar.baz becomes + * ['foo', 'bar', 'baz'] + * + * @param {object} expr + * @return {array} + */ +function expressionToArray(expr) { + var parts = []; + switch(expr.type) { + case Syntax.CallExpression: + parts = expressionToArray(expr.callee); + break; + case Syntax.MemberExpression: + parts = expressionToArray(expr.object); + if (expr.computed) { + parts.push('...'); + } else { + parts.push(expr.property.name || expr.property.value); + } + break; + case Syntax.Identifier: + parts = [expr.name]; + break; + case Syntax.Literal: + parts = [expr.raw]; + break; + case Syntax.ThisExpression: + parts = ['this']; + break; + case Syntax.ObjectExpression: + var properties = expr.properties.map(function(property) { + return expressionToString(property.key) + + ': ' + + expressionToString(property.value); + }); + parts = ['{' + properties.join(', ') + '}']; + break; + case Syntax.ArrayExpression: + parts = ['[' + expr.elements.map(expressionToString).join(', ') + ']']; + break; + } + return parts; +} + +/** + * Creates a string representation of a member expression. + * + * @param {object} expr + * @return {array} + */ +function expressionToString(expr) { + return expressionToArray(expr).join('.'); +} + +/** + * Returns true if the expression is of form `exports.foo = bar;` or + * `modules.exports = foo;`. + * + * @param {object} node + * @return {bool} + */ +function isExportsOrModuleExpression(expr) { + if (expr.left.type !== Syntax.MemberExpression) { + return false; + } + var exprArr = expressionToArray(expr.left); + return (exprArr[0] === 'module' && exprArr[1] === 'exports') || + exprArr[0] == 'exports'; +} + + +/** + * Finds module.exports / exports.X statements inside an assignment expression. + */ +function handleAssignmentExpression(expr, scopeChain, multipleExports) { + while (!isExportsOrModuleExpression(expr)) { + if (expr.type === Syntax.AssignmentExpression && + expr.right.type === Syntax.AssignmentExpression) { + expr = expr.right; + } else { + return; + } + } + + var definition = resolveToValue( + expr.right, + scopeChain + ); + + if (!definition) { + // handle empty var declaration, e.g. "var x; ... module.exports = x" + if (expr.right.type === Syntax.Identifier) { + var found = false; + scopeChain.some(function(scope) { + if (scope[expr.right.name] === null) { + return found = true; + } + }); + if (found) { + // fake definition so we still return something at least + return { + definition: { + type: Syntax.VariableDeclaration, + loc: expr.loc, + isSynthesized: true + }, + scopeChain: scopeChain + }; + } + } + return; + } + + var leftExpression = expr.left; + var leftExpressions = expressionToArray(leftExpression); + if (leftExpressions[0] === 'exports') { + // exports.A = A + if (leftExpressions.length === 2 && leftExpression.property) { + // The 2nd element is the field name + multipleExports.push({ + key: leftExpression.property, + value: definition + }); + } + } else if (definition) { + // module.exports = A + return { + definition: definition, + scopeChain: scopeChain + }; + } +} + +/** + * Given an AST, this function tries to find the object expression that is the + * module's exported value. + * + * @param {object} ast + * @return {?object} + */ +function findExportDefinition(ast) { + var multipleExports = []; + var singleExport; + traverseFlat(ast, function(node, scopeChain) { + if (singleExport) { + return false; + } + if (node.type === Syntax.VariableDeclaration) { + node.declarations.forEach(function (decl) { + if (!singleExport && decl.init && + decl.init.type === Syntax.AssignmentExpression) { + singleExport = handleAssignmentExpression( + decl.init, + scopeChain, + multipleExports + ); + } + }); + return false; + } + if (!isAssignmentStatement(node)) { + return false; + } + if (node.expression) { + singleExport = handleAssignmentExpression( + node.expression, + scopeChain, + multipleExports + ); + } + }); + + // NOT going to handle the f**ked up case where in the same file we have + // module.exports = A; exports.b = b; + if (singleExport) { + return singleExport; + } + + if (multipleExports.length === 1) { + return { + scopeChain: [], + definition: multipleExports[0].value + }; + } + + if (multipleExports.length > 0) { + // Synthesize an ObjectExpression union all exports + var properties = multipleExports.map(function(element) { + var key = element.key; + var value = element.value; + return { + type: Syntax.Property, + key: key, + value: value, + loc: { + start: { line: key.loc.start.line, column: key.loc.start.column }, + end: { line: value.loc.end.line, column: value.loc.end.column } + }, + range: [ key.range[0], value.range[1] ] + }; + }); + return { + scopeChain: [], + definition: { + isSynthesized: true, + type: Syntax.ObjectExpression, + properties: properties, + // Use the first export statement location + loc: properties[0].loc + } + }; + } + + return null; +}; + +module.exports = findExportDefinition; diff --git a/website/jsdocs/generic-function-visitor.js b/website/jsdocs/generic-function-visitor.js new file mode 100644 index 000000000..5262d3fb8 --- /dev/null +++ b/website/jsdocs/generic-function-visitor.js @@ -0,0 +1,534 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +/*global exports:true*/ +/*jslint node:true*/ +"use strict"; + +var util = require('util'); + +var Syntax = require('esprima-fb').Syntax; +var utils = require('jstransform/src/utils'); + +// Transforms +var meta = require('./meta'); +var type = require('./type'); + +var typeHintExp = /^\??[\w<>|:(),?]+$/; +var paramRe = /\*\s+@param\s+{?([^\s*{}.]+)}?(\s+([\w\$]+))?/g; +var returnRe = /\*\s+@return(s?)\s+{?([^\s*{}.]+)}?/; + +var nameToTransforms = { + 'sourcemeta': meta, + 'typechecks': type, +}; + +var excludes = []; + +function getTypeHintsFromDocBlock(node, docBlocksByLine) { + var comments = docBlocksByLine[node.loc.start.line - 1]; + if (!comments) { + return { + params: null, + returns: null + }; + } + + var params = []; + if (node.params) { + var paramNames = node.params.reduce(function(map, param) { + map[param.name] = true; + return map; + }, {}); + + var param; + while(param = paramRe.exec(comments.value)) { + + if (!param[1]) { + continue; + } + + var functionName = node.id + ? '`' + node.id.name + '\'' + : ''; + + if (!param[3]) { + throw new Error(util.format('Lines: %s-%s: Your @param declaration in' + + ' function %s is missing the parameter\'s name,' + + ' i.e. "@param {string} name"', + comments.loc.start.line, comments.loc.end.line, functionName)); + } + + // TODO(ostrulovich) if we're really nice, we should probably check edit + // distance and suggest the right name the user meant + if (!(param[3] in paramNames)) { + throw new Error(util.format('Lines: %s-%s: `%s\' is not a valid ' + + 'formal parameter of function %s. Must be one of: %s', + comments.loc.start.line, comments.loc.end.line, param[3], + functionName, Object.keys(paramNames).join(', '))); + } + + params.push([param[3], param[1]]); + } + } + var returnType = returnRe.exec(comments.value); + if (returnType && returnType[1]) { + throw new Error(util.format('Lines: %s-%s: Your @return declaration in' + + ' function %s is incorrectly written as @returns. Remove the trailing'+ + ' \'s\'.', + comments.loc.start.line, comments.loc.end.line, functionName)); + } + return { + params: params.length ? params : null, + returns: returnType ? returnType[2] : null + }; +} + +function getTypeHintFromInline(node, commentsByLine) { + var key = node.loc.start.column - 1; + var comments = commentsByLine[node.loc.start.line]; + if (!comments || !(key in comments)) { + return null; + } + // annotate the node + node.typeHint = comments[key].value; + return node.typeHint; +} + +/** + * Parses out comments from AST + * and populates commentsByLine and docBlocksByLine + */ +function parseComments(programNode, state) { + programNode.comments.forEach(function(c) { + if (c.type !== 'Block') return; + + var comments; + if (c.loc.start.line === c.loc.end.line && + typeHintExp.test(c.value)) { + // inline comments + comments = state.commentsByLine[c.loc.start.line] || + (state.commentsByLine[c.loc.start.line] = {}); + comments[c.loc.end.column] = c; + + comments = state.commentsByLine[c.loc.end.line] || + (state.commentsByLine[c.loc.start.line] = {}); + comments[c.loc.end.column] = c; + } else { + // docblocks + state.docBlocksByLine[c.loc.end.line] = c; + } + }); +} + +function getTypeHintParams(node, state) { + // First look for typehints in the docblock. + var typeHints = getTypeHintsFromDocBlock(node, state.docBlocksByLine); + + // If not found, look inline. + if (!typeHints.params && node.params) { + typeHints.params = node.params.map(function(param, index) { + return [param.name, getTypeHintFromInline(param, state.commentsByLine)]; + }).filter(function(param) { + return param[1]; + }); + } + if (!typeHints.returns) { + typeHints.returns = getTypeHintFromInline(node.body, state.commentsByLine); + } + + return typeHints; +} + +/** + * Get parameters needed for the dynamic typehint checks. + */ +function normalizeTypeHintParams(node, state, typeHints) { + var preCond = []; + if (typeHints.params.length > 0) { + typeHints.params.forEach(function(typeHint) { + if (typeHint[1]) { + preCond.push([ + typeHint[0], + '\''+ type.parseAndNormalize(typeHint[1], typeHint[0], node) +'\'', + '\''+ typeHint[0] +'\'' + ]); + } + }); + } + + var postCond = null; + if (typeHints.returns) { + postCond = type.parseAndNormalize(typeHints.returns, 'return', node); + } + + // If static-only, then we don't need to pass the type hint + // params since we're not going to do any dynamic checking. + var pragmas = utils.getDocblock(state); + if ('static-only' in pragmas) { + return null; + } + + var typeHintParams = {}; + if (preCond.length > 0) { + typeHintParams.params = preCond; + } + if (postCond) { + typeHintParams.returns = postCond; + } + return (preCond.length || postCond) ? typeHintParams : null; +} + +/** + * Takes in all the various params on the function in the docblock or inline + * comments and converts them into the format the bodyWrapper transform is + * expecting. If there are no params needed, returns null. + * + * For example, for a docblock like so + * @param {string} x + * @param {number} y + * @return {number} + * the resulting params object would contain + * { + * params: [ [ 'x', 'number' ], [ 'y', 'number' ] ], + * returns: 'number' + * } + * + * However the bodyWrapper transform expects input like + * { + * params: + * [ [ 'x', '\'number\'', '\'x\'' ], + * [ 'y', '\'number\'', '\'y\'' ] ], + * returns: 'number' + * } + */ +function formatBodyParams(node, state, params) /*?object*/ { + return normalizeTypeHintParams(node, state, params); +} + +/** + * Takes in all the various params on the function in the docblock or inline + * comments and converts them into the format the annotator transform is + * expecting. If there are no params needed, returns null. + * + * For example, for a docblock like so + * @param {string} x + * @param {number} y + * @return {number} + * the resulting params object would contain + * { + * params: [ [ 'x', 'number' ], [ 'y', 'number' ] ], + * returns: 'number' + * } + * + * However the bodyWrapper transform expects input like + * { + * params: [ 'number', 'number' ], + * returns: 'number' + * } + */ +function formatAnnotatorParams(params) /*?object*/ { + if ((!params.params || params.params.length === 0) && !params.returns) { + return null; + } + var annotatorParams = {}; + if (params.params && params.params.length > 0) { + var paramTypes = []; + params.params.forEach(function(paramArray) { + paramTypes.push(paramArray[1]); + }); + annotatorParams.params = paramTypes; + } + + if (params.returns) { + annotatorParams.returns = params.returns; + } + + return annotatorParams; +} + +/** + * Function used for determining how the params will be inlined + * into the function transform. We can't just use utils.format + * with %j because the way the data is stored in params vs + * formatted is different. + */ +function renderParams(/*?object*/ params) /*string*/ { + if (params == null) { + return null; + } + + var formattedParams = []; + if (params.params && params.params.length > 0) { + var preCond = params.params; + var joined = preCond.map(function(cond) { + return '[' + cond.join(', ') + ']'; + }).join(', '); + var paramString = '\"params\":' + '[' + joined + ']'; + formattedParams.push(paramString); + } + + if (params.returns) { + var returnParam = '\"returns\":' + '\'' + params.returns + '\''; + formattedParams.push(returnParam); + } + return "{" + formattedParams.join(',') + "}"; +} + +function getModuleName(state) { + var docblock = utils.getDocblock(state); + return docblock.providesModule || docblock.providesLegacy; +} + +function getFunctionMetadata(node, state) { + var funcMeta = { + module: getModuleName(state), + line: node.loc.start.line, + column: node.loc.start.column, + name: node.id && node.id.name + }; + if (!funcMeta.name) { + delete funcMeta.name; + } + return funcMeta; +} + +function getNameToTransforms() { + var filtered = {}; + Object.keys(nameToTransforms).forEach(function(name) { + if (excludes.indexOf(name) == -1) { + filtered[name] = nameToTransforms[name]; + } + }); + return filtered; +} + +/** + * Returns true if there are any transforms that would want to modify the + * current source. Usually we can rule out some transforms because the top + * pragma may say @nosourcemeta or there isn't a @typechecks. This function is + * used to rule out sources where no transform applies. + * + * @param {object} state + * @param {object} pragmas + * @return {bool} + */ +function shouldTraverseFile(state, pragmas) { + var t = false; + var nameToTransforms = getNameToTransforms(); + Object.keys(nameToTransforms).forEach(function(value) { + var transform = nameToTransforms[value]; + t = t || transform.shouldTraverseFile(state, pragmas); + }); + return t; +} + +/** + * Collects all the necessary information from the docblock and inline comments + * that may be useful to a transform. Currently only the type transform has + * information like @param and @return or the inline comments. + */ +function getAllParams(node, state) { + if (type.shouldTransformFile(state, utils.getDocblock(state))) { + return getTypeHintParams(node, state); + } + return {}; +} + +/** + * Returns an array of transforms that return true when shouldTransformFile is + * called. + */ +function getTransformsForFile(state, pragmas) { + var transforms = []; + var nameToTransforms = getNameToTransforms(); + Object.keys(nameToTransforms).forEach(function(value) { + var transform = nameToTransforms[value]; + if (transform.shouldTransformFile(state, pragmas)) { + transforms.push(transform); + } + }); + return transforms; +} + +/** + * Returns an array of trasnforms that return true when + * shouldTransformFunction is called. + */ +function getTransformsForFunction(transformsForFile, node, state, pragmas, + params) { + var transforms = []; + transformsForFile.forEach(function(transform) { + if (transform.shouldTransformFunction(node, state, pragmas, params)) { + transforms.push(transform); + } + }); + return transforms; +} + +/** + * This function will perform any checks over the JS source that doesn't + * require injecting in source code. For example the typechecks transform + * has a mode called static-only that does not add any extra code. + */ +function processStaticOnly(node, state) { + var pragmas = utils.getDocblock(state); + if (pragmas.typechecks === 'static-only') { + var params = getTypeHintParams(node, state); + normalizeTypeHintParams(node, state, params); + } +} + +function shouldWrapBody(transformsForFile) { + var t = false; + transformsForFile.forEach(function(transform) { + t = t || transform.wrapsBody(); + }); + return t; +} + +function shouldAnnotate(transformsForFile) { + var t = false; + transformsForFile.forEach(function(transform) { + t = t || transform.annotates(); + }); + return t; +} + +/** + * Gets the trailing arguments string that should be appended to + * __annotator(foo, + * and does not include a semicolon. + */ +function getTrailingAnnotatorArguments(funcMeta, annotatorParams) { + if (annotatorParams === null) { + return util.format(', %j)', funcMeta); + } + return util.format(', %j, %j)', funcMeta, annotatorParams); +} + +/** + * This is the main entry point into the generic function transforming. + */ +function genericFunctionTransformer(traverse, node, path, state) { + // The typechecks transform has a static-only mode that doesn't actually + // perform a transform but validates the types. + processStaticOnly(node, state, params); + + var params = getAllParams(node, state); + var transformsForFile = getTransformsForFile(state, utils.getDocblock(state)); + var transformsForFunction = + getTransformsForFunction( + transformsForFile, + node, + state, + utils.getDocblock(state), + params + ); + + if (transformsForFunction.length === 0) { + traverse(node.body, path, state); + return; + } + + var wrapBody = shouldWrapBody(transformsForFunction); + var annotate = shouldAnnotate(transformsForFunction); + + // There are two different objects containing the params for the wrapper + // vs annotator because the type param information only makes sense inside + // the body wrapper like [x, 'number', 'x']. During execution the body wrapper + // will be passed the correct values whereas during the annotator the + // arguments don't exist yet. + var bodyParams = wrapBody ? formatBodyParams(node, state, params) : null; + var annotatorParams = annotate ? formatAnnotatorParams(params) : null; + var funcMeta = getFunctionMetadata(node, state); + + // If there are no params to pass to the body, then don't wrap the + // body function. + wrapBody = wrapBody && bodyParams !== null; + var renderedBodyParams = renderParams(bodyParams); + + if (node.type === Syntax.FunctionExpression && annotate) { + utils.append('__annotator(', state); + } + + // Enter function body. + utils.catchup(node.body.range[0] + 1, state); + + // Insert a function that wraps the function body. + if (wrapBody) { + utils.append( + 'return __bodyWrapper(this, arguments, function() {', + state + ); + } + + // Recurse down into the child. + traverse(node.body, path, state); + // Move the cursor to the end of the function body. + utils.catchup(node.body.range[1] - 1, state); + + // Close the inserted function. + if (wrapBody) { + utils.append(util.format('}, %s);', renderedBodyParams), state); + } + + // Write the closing } of the function. + utils.catchup(node.range[1], state); + + if (!annotate) { + return; + } + + if (node.type === Syntax.FunctionExpression) { + utils.append( + getTrailingAnnotatorArguments(funcMeta, annotatorParams), + state + ); + } else if (node.type === Syntax.FunctionDeclaration) { + utils.append( + util.format( + '__annotator(%s', + node.id.name + ) + getTrailingAnnotatorArguments(funcMeta, annotatorParams) + ';', + state + ); + } +} + +function visitFunction(traverse, node, path, state) { + if (node.type === Syntax.Program) { + state.docBlocksByLine = {}; + state.commentsByLine = {}; + parseComments(node, state); + return; + } + + genericFunctionTransformer(traverse, node, path, state); + return false; +} +visitFunction.test = function(node, path, state) { + var pragmas = utils.getDocblock(state); + if (!shouldTraverseFile(state, pragmas)) { + return false; + } + + switch (node.type) { + case Syntax.Program: + case Syntax.FunctionExpression: + case Syntax.FunctionDeclaration: + return true; + default: + return false; + } +}; + +function setExcludes(excl) { + excludes = excl; +} + +exports.visitorList = [visitFunction]; +exports.setExcludes = setExcludes; +exports.formatAnnotatorParams = formatAnnotatorParams; +exports.getTrailingAnnotatorArguments = getTrailingAnnotatorArguments; +exports.getTypeHintsFromDocBlock = getTypeHintsFromDocBlock; +exports.getTypeHintFromInline = getTypeHintFromInline; diff --git a/website/jsdocs/jsdocs.js b/website/jsdocs/jsdocs.js new file mode 100644 index 000000000..678ab83de --- /dev/null +++ b/website/jsdocs/jsdocs.js @@ -0,0 +1,536 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +/*jslint node: true */ +"use strict"; + +var esprima = require('esprima-fb'); +var fs = require('fs'); +var Syntax = esprima.Syntax; + +var findExportDefinition = require('./findExportDefinition'); +var genericTransform = require('./generic-function-visitor'); +var genericVisitor = genericTransform.visitorList[0]; +var traverseFlat = require('./traverseFlat') +var parseTypehint = require('./TypeExpressionParser').parse; + +// Don't save object properties source code that is longer than this +var MAX_PROPERTY_SOURCE_LENGTH = 1000; + +function invariant(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +/** + * If the expression is an identifier, it is resolved in the scope chain. + * If it is an assignment expression, it resolves to the right hand side. + * + * In all other cases the expression itself is returned. + * + * Since the scope chain constructed by the traverse function is very simple + * (it doesn't take into account *changes* to the variable through assignment + * statements), this function doesn't return the correct value in every + * situation. But it's good enough for how it is used in the parser. + * + * @param {object} expr + * @param {array} scopeChain + * + * @return {object} + */ +function resolveToValue(expr, scopeChain) { + switch (expr.type) { + case Syntax.AssignmentExpression: + if (expr.operator === '=') { + return resolveToValue(expr.right, scopeChain); + } + break; + case Syntax.Identifier: + var value; + scopeChain.some(function(scope, i) { + if (hasOwnProperty.call(scope, expr.name) && scope[expr.name]) { + value = resolveToValue(scope[expr.name], scopeChain.slice(i)); + return true; + } + }); + return value; + } + return expr; +} + +/** + * Strips the "static upstream" warning from the docblock. + * + * @param {?string} docblock + * @return {?string} + */ +function stripStaticUpstreamWarning(docblock) { + if (!docblock) { + return docblock; + } + // Esprima strips out the starting and ending tokens, so add them back + docblock = "/*" + docblock + "*/\n"; + return docblock; +} + +/** + * Parse a typehint into the 'nice' form, if possible. + */ +function safeParseTypehint(typehint) { + if (!typehint) { + return null; + } + try { + return JSON.stringify(parseTypehint(typehint)); + } catch (e) { + return typehint; + } +} + +/** + * Gets the docblock for the file + * + * @param {array} commentsForFile + * @return {?string} + */ +function getFileDocBlock(commentsForFile) { + var docblock; + commentsForFile.some(function(comment, i) { + if (comment.loc.start.line === 1) { + var lines = comment.value.split("\n"); + var filteredLines = lines.filter(function(line) { + var hasCopyright = !!line.match(/^\s*\*\s+Copyright/); + var hasProvides = !!line.match(/^\s*\*\s+@provides/); + return !hasCopyright && !hasProvides; + }); + docblock = filteredLines.join("\n"); + return true; + } + }); + return stripStaticUpstreamWarning(docblock); +} + +/** + * Gets the docblock for a given node. + * + * @param {object} Node to get docblock for + * @param {array} commentsForFile + * @param {array} linesForFile + * @return {?string} + */ +function getDocBlock(node, commentsForFile, linesForFile) { + if (node.isSynthesized === true) { + return ''; + } + var docblock; + var prevLine = node.loc.start.line - 1; + // skip blank lines + while (linesForFile[prevLine - 1].trim() === '') { + prevLine--; + } + + commentsForFile.some(function(comment, i) { + if (comment.loc.end.line === prevLine) { + if (comment.type === 'Line') { + // Don't accept line comments that are separated + if (prevLine !== node.loc.start.line - 1) { + return true; + } + var line = prevLine; + docblock = ''; + for (var ii = i; ii >= 0; ii--) { + var lineComment = commentsForFile[ii]; + if (lineComment.loc.end.line === line) { + docblock = '//' + lineComment.value + + (docblock ? "\n" + docblock : ""); + line--; + } else { + break; + } + } + } else { + docblock = stripStaticUpstreamWarning(comment.value); + } + return true; + } + }); + return docblock; +} + +/** + * Given the comments for a file, return the module name (by looking for + * @providesModule). + * + * @param {array} + * @return {?string} + */ +function getModuleName(commentsForFile) { + var moduleName; + commentsForFile.forEach(function(comment) { + if (comment.type === 'Block') { + var matches = comment.value.match(/@providesModule\s+(\S*)/); + if (matches && matches[1]) { + moduleName = matches[1]; + } + } + }); + return moduleName; +} + +/** + * Esprima includes the leading colon (and possibly spaces) as part of the + * typehint, so we have to strip those out. + */ +function sanitizeTypehint(string) { + for (var i = 0; i < string.length; i++) { + if (string[i] != ' ' && string[i] != ':') { + return string.substring(i); + } + } + return null; +} + +/** + * @param {object} node + * @param {object} state + * @param {string} source + * @param {array} commentsForFile + * @param {array} linesForFile + * @return {object} + */ +function getFunctionData(node, state, source, commentsForFile, linesForFile) { + var params = []; + var typechecks = commentsForFile.typechecks; + var typehintsFromBlock = null; + if (typechecks) { + // esprima has trouble with some params so ignore them (e.g. $__0) + if (!node.params.some(function(param) { return !param.name; })) { + try { + typehintsFromBlock = genericTransform.getTypeHintsFromDocBlock( + node, + state.docBlocksByLine + ); + } catch (e) { + } + } + } + node.params.forEach(function(param) { + // TODO: Handle other things like Syntax.ObjectPattern + if (param.type === Syntax.Identifier) { + var typehint; + if (param.typeAnnotation) { + typehint = sanitizeTypehint(source.substring( + param.typeAnnotation.range[0], + param.typeAnnotation.range[1] + )); + } else if (typehintsFromBlock && typehintsFromBlock.params) { + typehintsFromBlock.params.some(function(paramTypehint) { + if (paramTypehint[0] === param.name) { + typehint = paramTypehint[1]; + return true; + } + }); + } + if (!typehint && typechecks) { + try { + typehint = genericTransform.getTypeHintFromInline( + param, + state.commentsByLine + ); + } catch (e) { + } + } + params.push({ + typehint: safeParseTypehint(typehint), + name: param.name + (param.optional ? '?' : ''), + }); + } else if (param.type === Syntax.TypeAnnotatedIdentifier) { + params.push({ + typehint: sanitizeTypehint(source.substring( + param.annotation.range[0], + param.annotation.range[1] + )), + name: param.id.name + }); + } + }); + var returnTypehint = null; + if (node.returnType) { + returnTypehint = sanitizeTypehint(source.substring( + node.returnType.range[0], + node.returnType.range[1] + )); + } else if (typehintsFromBlock) { + returnTypehint = typehintsFromBlock.returns; + } + var tparams = null; + if (node.typeParameters) { + tparams = node.typeParameters.params.map(function(x) { + return x.name; + }); + } + return { + line: node.loc.start.line, + source: source.substring.apply(source, node.range), + docblock: getDocBlock(node, commentsForFile, linesForFile), + modifiers: [], + params: params, + tparams: tparams, + returntypehint: safeParseTypehint(returnTypehint) + }; +} + +/** + * @param {object} node + * @param {object} state + * @param {string} source + * @param {array} commentsForFile + * @param {array} linesForFile + * @return {object} + */ +function getObjectData(node, state, source, scopeChain, + commentsForFile, linesForFile) { + var methods = []; + var properties = []; + var superClass = null; + node.properties.forEach(function(property) { + if (property.type === Syntax.SpreadProperty) { + if (property.argument.type === Syntax.Identifier) { + superClass = property.argument.name; + } + return; + } + + switch (property.value.type) { + case Syntax.FunctionExpression: + var methodData = getFunctionData(property.value, state, source, + commentsForFile, linesForFile); + methodData.name = property.key.name || property.key.value; + methodData.source = source.substring.apply(source, property.range); + methodData.modifiers.push('static'); + methods.push(methodData); + break; + case Syntax.Identifier: + var expr = resolveToValue( + property.value, + scopeChain + ); + if (expr) { + if (expr.type === Syntax.FunctionDeclaration) { + var functionData = + getFunctionData(expr, state, source, commentsForFile, linesForFile); + functionData.name = property.key.name || property.key.value; + functionData.modifiers.push('static'); + methods.push(functionData); + break; + } else { + property.value = expr; + } + } + /* falls through */ + default: + var propertySource = ''; + var valueRange = property.value.range; + if (valueRange[1] - valueRange[0] < MAX_PROPERTY_SOURCE_LENGTH) { + propertySource = source.substring.apply(source, valueRange); + } + var docBlock = getDocBlock(property, commentsForFile, linesForFile); + /* CodexVarDef: modifiers, type, name, default, docblock */ + var propertyData = [ + ['static'], + '', + // Cast to String because this can be a Number + // Could also be a String literal (e.g. "key") hence the value + String(property.key.name || property.key.value), + propertySource, + docBlock || '', + property.loc.start.line + ]; + properties.push(propertyData); + break; + } + }); + return { + methods: methods, + properties: properties, + superClass: superClass + }; +} + +/** + * @param {object} node + * @param {object} state + * @param {string} source + * @param {array} commentsForFile + * @param {array} linesForFile + * @return {object} + */ +function getClassData(node, state, source, commentsForFile, linesForFile) { + var methods = []; + invariant(node.body.type === Syntax.ClassBody, 'Expected ClassBody'); + node.body.body.forEach(function(bodyItem) { + if (bodyItem.type === Syntax.MethodDefinition) { + if (bodyItem.value.type === Syntax.FunctionExpression) { + var methodData = + getFunctionData(bodyItem.value, state, source, + commentsForFile, linesForFile); + methodData.name = bodyItem.key.name; + methodData.source = source.substring.apply(source, bodyItem.range); + if (bodyItem.static) { + methodData.modifiers.push('static'); + } + methods.push(methodData); + } + } + }); + var data = { + methods: methods + }; + if (node.superClass && node.superClass.type === Syntax.Identifier) { + data.superClass = node.superClass.name; + } + if (node.typeParameters) { + data.tparams = node.typeParameters.params.map(function(x) { + return x.name; + }); + } + return data; +} + + +/** + * Finds all the requires + * + * @param {object} ast + * @return {array} + */ +function findRequires(ast) { + var requires = []; + traverseFlat(ast, function(node, scopeChain) { + var requireData = getRequireData(node); + if (requireData) { + requires.push(requireData); + } + return !requireData; + }); + return requires; +} + +/** + * If the given node is a 'require' statement, returns a list of following data + * { + * name: string + * } + * + * @return ?object + */ +function getRequireData(node) { + if (!node || node.type !== Syntax.CallExpression) { + return null; + } + + var callee = node.callee; + if (callee.type !== Syntax.Identifier + || (callee.name !== 'require')) { + return null; + } + var args = node['arguments']; + if (args.length === 0) { + return null; + } + var firstArgument = args[0]; + if (firstArgument.type !== Syntax.Literal) { + return null; + } + + return { + name: firstArgument.value + }; +} + +/** + * Given the source of a file, this returns the data about the module's exported + * value. + * + * @param {string} source + * @return {?object} data + */ +function parseSource(source) { + var lines = source.split("\n"); + var ast = esprima.parse(source, { + loc: true, + comment: true, + range: true, + sourceType: 'nonStrictModule', + }); + + /** + * This sets up genericTransform so that it can be queried above. + */ + var _state = { + g: { + source: source + } + }; + if (genericVisitor.test(ast, [], _state)) { + // HACK: Mark that this file has typechecks on the comments object. + ast.comments.typechecks = true; + // This fills out the data for genericTransform. + genericVisitor(function() {}, ast, [], _state); + } + var result = findExportDefinition(ast.body); + if (result) { + var definition = result.definition; + var scopeChain = result.scopeChain; + var data; + var moduleName = getModuleName(ast.comments); + if (!moduleName) { + return null; + } + if (definition.type === Syntax.NewExpression && + definition.callee.type === Syntax.Identifier) { + var name = definition.callee.name; + // If the class is defined in the scopeChain, export that instead. + scopeChain.some(function(scope) { + if (hasOwnProperty.call(scope, name) && + scope[name].type === Syntax.ClassDeclaration) { + definition = scope[name]; + return true; + } + }); + } + + switch (definition.type) { + case Syntax.ClassDeclaration: + data = getClassData(definition, _state, source, ast.comments, lines); + data.type = 'class'; + break; + case Syntax.ObjectExpression: + data = getObjectData(definition, _state, source, scopeChain, + ast.comments, lines); + data.type = 'object'; + break; + case Syntax.FunctionDeclaration: + case Syntax.FunctionExpression: + data = getFunctionData(definition, _state, source, ast.comments, lines); + data.type = 'function'; + break; + default: + data = {type: 'module'}; + break; + } + if (data) { + data.line = definition.loc.start.line; + data.name = moduleName; + data.docblock = + getDocBlock(definition, ast.comments, lines) || + getFileDocBlock(ast.comments); + data.requires = findRequires(ast.body); + return data; + } + } + return null; +} + + +module.exports = parseSource; diff --git a/website/jsdocs/meta.js b/website/jsdocs/meta.js new file mode 100644 index 000000000..0cc92da3e --- /dev/null +++ b/website/jsdocs/meta.js @@ -0,0 +1,54 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +/*global exports:true*/ +/*jslint node:true*/ +"use strict"; + +var util = require('util'); + +var Syntax = require('esprima-fb').Syntax; +var utils = require('jstransform/src/utils'); + +// Top level file pragmas that must not exist for the meta transform to +// be applied. +var mustNotHave = [ + 'nosourcemeta', +]; + +function shouldTraverseFile(state, pragmas) { + if (state.g.sourcemeta === undefined) { + var notHaves = true; + mustNotHave.forEach(function (value) { + notHaves = notHaves && !(value in pragmas); + }); + state.g.sourcemeta = notHaves; + } + return state.g.sourcemeta; +} + +var shouldTransformFile = shouldTraverseFile; + +function shouldTransformFunction(node, state, pragmas, params) /*bool*/ { + if (!shouldTransformFile(state, pragmas)) { + throw new Error( + 'shouldTransformFunction should not be called if shouldTransformFile ' + + 'fails' + ); + } + return true; +} + +function wrapsBody() { + return false; +} + +function annotates() { + return true; +} + +exports.shouldTransformFile = shouldTransformFile; +exports.shouldTraverseFile = shouldTraverseFile; +exports.shouldTransformFunction = shouldTransformFunction; +exports.wrapsBody = wrapsBody; +exports.annotates = annotates; +exports.name = 'sourcemeta'; diff --git a/website/jsdocs/traverseFlat.js b/website/jsdocs/traverseFlat.js new file mode 100644 index 000000000..5d2390dca --- /dev/null +++ b/website/jsdocs/traverseFlat.js @@ -0,0 +1,97 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +/*global exports:true*/ +/*jslint node:true*/ +"use strict"; + +var Syntax = require('esprima-fb').Syntax; + +/** + * Executes visitor on the object and its children (recursively). + * While traversing the tree, a scope chain is built and passed to the visitor. + * + * If the visitor returns false, the object's children are not traversed. + * + * @param {object} object + * @param {function} visitor + * @param {?array} scopeChain + */ +function traverse(object, visitor, scopeChain) { + scopeChain = scopeChain || [{}]; + + var scope = scopeChain[0]; + + switch (object.type) { + case Syntax.VariableDeclaration: + object.declarations.forEach(function(decl) { + scope[decl.id.name] = decl.init; + }); + break; + case Syntax.ClassDeclaration: + scope[object.id.name] = object; + break; + case Syntax.FunctionDeclaration: + // A function declaration creates a symbol in the current scope + scope[object.id.name] = object; + /* falls through */ + case Syntax.FunctionExpression: + case Syntax.Program: + scopeChain = [{}].concat(scopeChain); + break; + } + + if (object.type === Syntax.FunctionExpression || + object.type === Syntax.FunctionDeclaration) { + // add parameters to the new scope + object.params.forEach(function(param) { + // since the value of the parameters are unknown during parsing time + // we set the value to `undefined`. + scopeChain[0][param.name] = undefined; + }); + } + + if (object.type) { + if (visitor.call(null, object, scopeChain) === false) { + return; + } + } + + for (var key in object) { + if (object.hasOwnProperty(key)) { + var child = object[key]; + if (typeof child === 'object' && child !== null) { + traverse(child, visitor, scopeChain); + } + } + } +} + +/** + * Executes visitor on the object and its children, but only traverses into + * children which can be statically analyzed and don't depend on runtime + * information. + * + * @param {object} object + * @param {function} visitor + * @param {?array} scopeChain + */ +function traverseFlat(object, visitor, scopeChain) { + traverse(object, function(node, scopeChain) { + switch (node.type) { + case Syntax.FunctionDeclaration: + case Syntax.FunctionExpression: + case Syntax.IfStatement: + case Syntax.WithStatement: + case Syntax.SwitchStatement: + case Syntax.TryStatement: + case Syntax.WhileStatement: + case Syntax.DoWhileStatement: + case Syntax.ForStatement: + case Syntax.ForInStatement: + return false; + } + return visitor(node, scopeChain); + }, scopeChain); +} + +module.exports = traverseFlat; diff --git a/website/jsdocs/type.js b/website/jsdocs/type.js new file mode 100644 index 000000000..bcfe0b531 --- /dev/null +++ b/website/jsdocs/type.js @@ -0,0 +1,79 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +/*global exports:true*/ +"use strict"; + +var util = require('util'); + +var Syntax = require('esprima-fb').Syntax; +var utils = require('jstransform/src/utils'); + +var parse = require('./TypeExpressionParser').parse; +var compile = require('./TypeExpressionParser').compile; +var normalize = require('./TypeExpressionParser').normalize; + +function parseAndNormalize(source, name, object) { + if (/\?$/.test(source)) { + source = '?' + source.substring(0, source.length - 1); + } + try { + var ast = parse(source); + return compile(normalize(ast)); + } catch (e) { + var functionName = object.id + ? '`' + object.id.name + '\'' + : ''; + throw new Error(util.format('The type `%s\' specified for %s for ' + + 'the function %s, on line %s, could not be parsed. The error given was: %s', + source, name, functionName, object.loc.start.line, e.message + )); + } +} + +function initializeSettings(state, pragmas) { + state.g.typechecks = 'typechecks' in pragmas; + state.g.staticOnly = pragmas.typechecks === 'static-only'; +} + +function shouldTraverseFile(state, pragmas) { + if (state.g.typechecks === undefined) { + initializeSettings(state, pragmas); + } + return state.g.typechecks; +} + +function shouldTransformFile(state, pragmas) { + if (state.g.typechecks === undefined) { + initializeSettings(state, pragmas); + } + return !state.g.staticOnly && state.g.typechecks; +} + +function shouldTransformFunction(node, state, pragmas, params) { + if (!shouldTransformFile(state, pragmas)) { + throw new Error( + 'shouldTransformFunction should not be called if shouldTransformFile ' + + 'fails' + ); + } + + return (params.params && params.params.length > 0) || + params.returns || + (node.id && /^[A-Z]/.test(node.id.name)); +} + +function wrapsBody() { + return true; +} + +function annotates() { + return true; +} + +exports.parseAndNormalize = parseAndNormalize; +exports.shouldTransformFile = shouldTransformFile; +exports.shouldTraverseFile = shouldTraverseFile; +exports.shouldTransformFunction = shouldTransformFunction; +exports.wrapsBody = wrapsBody; +exports.annotates = annotates; +exports.name = 'typechecks'; diff --git a/website/layout/AutodocsLayout.js b/website/layout/AutodocsLayout.js new file mode 100644 index 000000000..79d992c4e --- /dev/null +++ b/website/layout/AutodocsLayout.js @@ -0,0 +1,219 @@ +/** + * @providesModule AutodocsLayout + * @jsx React.DOM + */ + +var DocsSidebar = require('DocsSidebar'); +var H = require('Header'); +var Header = require('Header'); +var Marked = require('Marked'); +var React = require('React'); +var Site = require('Site'); +var slugify = require('slugify'); + + +var ComponentDoc = React.createClass({ + renderType: function(type) { + if (type.name === 'enum') { + if (typeof type.value === 'string') { + return type.value; + } + return 'enum(' + type.value.map((v => v.value)).join(', ') + ')'; + } + + if (type.name === 'shape') { + return '{' + Object.keys(type.value).map((key => key + ': ' + this.renderType(type.value[key]))).join(', ') + '}'; + } + + if (type.name === 'arrayOf') { + return '[' + this.renderType(type.value) + ']'; + } + + if (type.name === 'instanceOf') { + return type.value; + } + + if (type.name === 'custom') { + if (type.raw === 'EdgeInsetsPropType') { + return '{top: number, left: number, bottom: number, right: number}'; + } + return type.raw; + } + + if (type.name === 'func') { + return 'function'; + } + + return type.name; + }, + + renderProp: function(name, prop) { + return ( +
+
+ {name} + {' '} + {prop.type && + {this.renderType(prop.type)} + } +
+ {prop.description && {prop.description}} +
+ ); + }, + + renderCompose: function(name) { + return ( +
+
+ {name} props... +
+
+ ); + }, + + renderProps: function(props, composes) { + return ( +
+ {(composes || []).map((name) => + this.renderCompose(name) + )} + {Object.keys(props).sort().map((name) => + this.renderProp(name, props[name]) + )} +
+ ); + }, + + render: function() { + var content = this.props.content; + return ( +
+ + {content.description} + + Props + {this.renderProps(content.props, content.composes)} +
+ ); + } +}); + +var APIDoc = React.createClass({ + removeCommentsFromDocblock: function(docblock) { + return docblock + .trim('\n ') + .replace(/^\/\*+/, '') + .replace(/\*\/$/, '') + .split('\n') + .map(function(line) { + return line.trim().replace(/^\* ?/, ''); + }) + .join('\n'); + }, + + renderTypehintRec: function(typehint) { + if (typehint.type === 'simple') { + return typehint.value; + } + + if (typehint.type === 'generic') { + return this.renderTypehintRec(typehint.value[0]) + '<' + this.renderTypehintRec(typehint.value[1]) + '>'; + } + + return JSON.stringify(typehint); + + }, + + renderTypehint: function(typehint) { + try { + var typehint = JSON.parse(typehint); + } catch(e) { + return typehint; + } + + return this.renderTypehintRec(typehint); + }, + + renderMethod: function(method) { + return ( +
+
+ {method.modifiers.length && + {method.modifiers.join(' ') + ' '} + } + {method.name} + + ({method.params + .map((param) => { + var res = param.name; + if (param.typehint) { + res += ': ' + this.renderTypehint(param.typehint); + } + return res; + }) + .join(', ')}) + +
+ {method.docblock && + {this.removeCommentsFromDocblock(method.docblock)} + } +
+ ); + }, + + + renderMethods: function(methods) { + return ( +
+ {methods.map(this.renderMethod)} +
+ ); + }, + + render: function() { + var content = this.props.content; + if (!content.methods) { + return
Error
; + } + return ( +
+ + {this.removeCommentsFromDocblock(content.docblock)} + + Methods + {this.renderMethods(content.methods)} +
+ ); + } +}); + +var Autodocs = React.createClass({ + render: function() { + var metadata = this.props.metadata; + var content = JSON.parse(this.props.children); + return ( + +
+ + +
+
+ ); + } +}); + +module.exports = Autodocs; diff --git a/website/package.json b/website/package.json index b5580871c..1f2cd7159 100644 --- a/website/package.json +++ b/website/package.json @@ -3,13 +3,16 @@ "start": "node server/server.js" }, "dependencies": { - "React": "~0.12.0", - "optimist": "0.6.0", - "react-page-middleware": "git://github.com/facebook/react-page-middleware.git", "connect": "2.8.3", + "esprima-fb": "*", + "fs.extra": "*", "glob": "*", + "jstransform": "*", "mkdirp": "*", - "request": "*", - "fs.extra": "*" + "optimist": "0.6.0", + "react": "~0.12.0", + "react-docgen": "^1.0.0", + "react-page-middleware": "git://github.com/facebook/react-page-middleware.git", + "request": "*" } } diff --git a/website/react-docgen/.gitignore b/website/react-docgen/.gitignore deleted file mode 100644 index 849ddff3b..000000000 --- a/website/react-docgen/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ diff --git a/website/react-docgen/LICENSE b/website/react-docgen/LICENSE deleted file mode 100644 index 17e428880..000000000 --- a/website/react-docgen/LICENSE +++ /dev/null @@ -1,30 +0,0 @@ -BSD License - -For React docs generator software - -Copyright (c) 2015, Facebook, Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name Facebook nor the names of its contributors may be used to - endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/website/react-docgen/PATENTS b/website/react-docgen/PATENTS deleted file mode 100644 index f8ef30d1d..000000000 --- a/website/react-docgen/PATENTS +++ /dev/null @@ -1,23 +0,0 @@ -Additional Grant of Patent Rights - -"Software" means the React docs generator software distributed by Facebook, Inc. - -Facebook hereby grants you a perpetual, worldwide, royalty-free, non-exclusive, -irrevocable (subject to the termination provision below) license under any -rights in any patent claims owned by Facebook, to make, have made, use, sell, -offer to sell, import, and otherwise transfer the Software. For avoidance of -doubt, no license is granted under Facebook’s rights in any patent claims that -are infringed by (i) modifications to the Software made by you or a third party, -or (ii) the Software in combination with any software or other technology -provided by you or a third party. - -The license granted hereunder will terminate, automatically and without notice, -for anyone that makes any claim (including by filing any lawsuit, assertion or -other action) alleging (a) direct, indirect, or contributory infringement or -inducement to infringe any patent: (i) by Facebook or any of its subsidiaries or -affiliates, whether or not such claim is related to the Software, (ii) by any -party if such claim arises in whole or in part from any software, product or -service of Facebook or any of its subsidiaries or affiliates, whether or not -such claim is related to the Software, or (iii) by any party relating to the -Software; or (b) that any right in any patent claim of Facebook is invalid or -unenforceable. diff --git a/website/react-docgen/README.md b/website/react-docgen/README.md deleted file mode 100644 index e670a286e..000000000 --- a/website/react-docgen/README.md +++ /dev/null @@ -1,178 +0,0 @@ -# react-docgen - -`react-docgen` extracts information from React components with which -you can generate documentation for those components. - -It uses [recast][] to parse the provided files into an AST, looks for React -component definitions, and inspects the `propTypes` and `getDefaultProps` -declarations. The output is a JSON blob with the extracted information. - -Note that component definitions must follow certain guidelines in order to be -analyzable by this tool. We will work towards less strict guidelines, but there -is a limit to what is statically analyzable. - -## Install - -Install the module directly from npm: - -``` -npm install -g react-docgen -``` - -## CLI - -Installing the module adds a `react-docgen` executable which allows you do convert -a single file, multiple files or an input stream. We are trying to make the -executable as versatile as possible so that it can be integrated into many -workflows. - -``` -Usage: react-docgen [path]... [options] - -path A component file or directory. If no path is provided it reads from stdin. - -Options: - -o FILE, --out FILE store extracted information in FILE - --pretty pretty print JSON - -x, --extension File extensions to consider. Repeat to define multiple extensions. Default: [js,jsx] - -i, --ignore Folders to ignore. Default: [node_modules,__tests__] - -Extract meta information from React components. -If a directory is passed, it is recursively traversed. -``` - -## API - -The tool can also be used programmatically to extract component information: - -```js -var reactDocs = require('react-docgen'); -var componentInfo reactDocs.parseSource(src); -``` - -## Guidelines - -- Modules have to export a single component, and only that component is - analyzed. -- `propTypes` must be an object literal or resolve to an object literal in the - same file. -- The `return` statement in `getDefaultProps` must consist of an object literal. - -## Example - -For the following component - -```js -var React = require('react'); - -/** - * General component description. - */ -var Component = React.createClass({ - propTypes: { - /** - * Description of prop "foo". - */ - foo: React.PropTypes.number, - /** - * Description of prop "bar" (a custom validation function). - */ - bar: function(props, propName, componentName) { - // ... - }, - baz: React.PropTypes.oneOfType([ - React.PropTypes.number, - React.PropTypes.string - ]), - }, - - getDefaultProps: function() { - return { - foo: 42, - bar: 21 - }; - }, - - render: function() { - // ... - } -}); - -module.exports = Component; -``` - -we are getting this output: - -``` -{ - "props": { - "foo": { - "type": { - "name": "number" - }, - "required": false, - "description": "Description of prop \"foo\".", - "defaultValue": { - "value": "42", - "computed": false - } - }, - "bar": { - "type": { - "name": "custom" - }, - "required": false, - "description": "Description of prop \"bar\" (a custom validation function).", - "defaultValue": { - "value": "21", - "computed": false - } - }, - "baz": { - "type": { - "name": "union", - "value": [ - { - "name": "number" - }, - { - "name": "string" - } - ] - }, - "required": false, - "description": "" - } - }, - "description": "General component description." -} -``` - -## Result data structure - -The structure of the JSON blob / JavaScript object is as follows: - -``` -{ - "description": string - "props": { - "": { - "type": { - "name": "", - ["value": ] - ["raw": string] - }, - "required": boolean, - "description": string, - ["defaultValue": { - "value": number | string, - "computed": boolean - }] - }, - ... - }, - ["composes": ] -} -``` - -[recast]: https://github.com/benjamn/recast diff --git a/website/react-docgen/bin/react-docgen.js b/website/react-docgen/bin/react-docgen.js deleted file mode 100755 index 4921f67ad..000000000 --- a/website/react-docgen/bin/react-docgen.js +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env node -/* - * Copyright (c) 2015, 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 argv = require('nomnom') - .script('react-docgen') - .help( - 'Extract meta information from React components.\n' + - 'If a directory is passed, it is recursively traversed.' - ) - .options({ - path: { - position: 0, - help: 'A component file or directory. If no path is provided it reads from stdin.', - metavar: 'PATH', - list: true - }, - out: { - abbr: 'o', - help: 'store extracted information in FILE', - metavar: 'FILE' - }, - pretty: { - help: 'pretty print JSON', - flag: true - }, - extension: { - abbr: 'x', - help: 'File extensions to consider. Repeat to define multiple extensions. Default:', - list: true, - default: ['js', 'jsx'] - }, - ignoreDir: { - abbr: 'i', - full: 'ignore', - help: 'Folders to ignore. Default:', - list: true, - default: ['node_modules', '__tests__'] - } - }) - .parse(); - -var async = require('async'); -var dir = require('node-dir'); -var fs = require('fs'); -var parser = require('../dist/main.js'); - -var output = argv.o; -var paths = argv.path; -var extensions = new RegExp('\\.(?:' + argv.extension.join('|') + ')$'); -var ignoreDir = argv.ignoreDir; - -function writeError(msg, path) { - if (path) { - process.stderr.write('Error with path "' + path + '": '); - } - process.stderr.write(msg + '\n'); -} - -function exitWithError(error) { - writeError(error); - process.exit(1); -} - -function exitWithResult(result) { - result = argv.pretty ? - JSON.stringify(result, null, 2) : - JSON.stringify(result); - if (argv.o) { - fs.writeFileSync(argv.o, result); - } else { - process.stdout.write(result + '\n'); - } - process.exit(0); -} - -/** - * 1. No files passed, consume input stream - */ -if (paths.length === 0) { - var source = ''; - process.stdin.setEncoding('utf8'); - process.stdin.resume(); - var timer = setTimeout(function() { - process.stderr.write('Still waiting for std input...'); - }, 5000); - process.stdin.on('data', function (chunk) { - clearTimeout(timer); - source += chunk; - }); - process.stdin.on('end', function () { - exitWithResult(parser.parseSource(source)); - }); -} - -function traverseDir(path, result, done) { - dir.readFiles( - path, - { - match: extensions, - excludeDir: ignoreDir - }, - function(error, content, filename, next) { - if (error) { - exitWithError(error); - } - try { - result[filename] = parser.parseSource(content); - } catch(error) { - writeError(error, path); - } - next(); - }, - function(error) { - if (error) { - writeError(error); - } - done(); - } - ); -} - -/** - * 2. Paths are passed. - */ -var result = Object.create(null); -async.eachSeries(paths, function(path, done) { - fs.stat(path, function(error, stats) { - if (error) { - writeError(error, path); - done(); - return; - } - if (stats.isDirectory()) { - traverseDir(path, result, done); - } - else { - try { - result[path] = parser.parseSource(fs.readFileSync(path)); - } catch(error) { - writeError(error, path); - } - finally { - done(); - } - } - }); -}, function() { - var resultsPaths = Object.keys(result); - if (resultsPaths.length === 0) { - // we must have gotten an error - process.exit(1); - } - if (paths.length === 1) { // a single path? - fs.stat(paths[0], function(error, stats) { - exitWithResult(stats.isDirectory() ? result : result[resultsPaths[0]]); - }); - } else { - exitWithResult(result); - } -}); diff --git a/website/react-docgen/flow/recast.js b/website/react-docgen/flow/recast.js deleted file mode 100644 index 8d87bb65c..000000000 --- a/website/react-docgen/flow/recast.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * A minimal set of declarations to make flow work with the recast API. - */ - -type ASTNode = Object; - -declare class Scope { - lookup(name: string): ?Scope; - getBindings(): Object>; -} - -declare class NodePath { - node: ASTNode; - parent: NodePath; - scope: Scope; - - get(...x: (string|number)): NodePath; - each(f: (p: NodePath) => void): void; - map(f: (p: NodePath) => T): Array; -} diff --git a/website/react-docgen/lib/Documentation.js b/website/react-docgen/lib/Documentation.js deleted file mode 100644 index d23d28e40..000000000 --- a/website/react-docgen/lib/Documentation.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -type PropDescriptor = { - type?: { - name: string; - value?: any; - raw?: string; - }; - required?: boolean; - defaultValue?: any; - description?: string; -}; - -class Documentation { - _props: Object; - _description: string; - _composes: Array; - - constructor() { - this._props = {}; - this._description = ''; - this._composes = []; - } - - addComposes(moduleName: string) { - if (this._composes.indexOf(moduleName) === -1) { - this._composes.push(moduleName); - } - } - - getDescription(): string { - return this._description; - } - - setDescription(description: string): void { - this._description = description; - } - - getPropDescriptor(propName: string): PropDescriptor { - var propDescriptor = this._props[propName]; - if (!propDescriptor) { - propDescriptor = this._props[propName] = {}; - } - return propDescriptor; - } - - toObject(): Object { - var obj = { - description: this._description, - props: this._props - }; - - if (this._composes.length) { - obj.composes = this._composes; - } - return obj; - } -} - -module.exports = Documentation; diff --git a/website/react-docgen/lib/ReactDocumentationParser.js b/website/react-docgen/lib/ReactDocumentationParser.js deleted file mode 100644 index 49914d60f..000000000 --- a/website/react-docgen/lib/ReactDocumentationParser.js +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -type Handler = (documentation: Documentation, path: NodePath) => void; - -var Documentation = require('./Documentation'); - -var findExportedReactCreateClass = - require('./strategies/findExportedReactCreateClassCall'); -var getPropertyName = require('./utils/getPropertyName'); -var recast = require('recast'); -var resolveToValue = require('./utils/resolveToValue'); -var n = recast.types.namedTypes; - -class ReactDocumentationParser { - _componentHandlers: Array; - _apiHandlers: Object; - - constructor() { - this._componentHandlers = []; - this._apiHandlers = Object.create(null); - } - - /** - * Handlers to extract information from the component definition. - * - * If "property" is not provided, the handler is passed the whole component - * definition. - * - * NOTE: The component definition is currently expected to be represented as - * an ObjectExpression (an object literal). This will likely change in the - * future. - */ - addHandler(handler: Handler, property?: string): void { - if (!property) { - this._componentHandlers.push(handler); - } else { - if (!this._apiHandlers[property]) { - this._apiHandlers[property] = []; - } - this._apiHandlers[property].push(handler); - } - } - - /** - * Takes JavaScript source code and returns an object with the information - * extract from it. - * - * The second argument is strategy to find the AST node(s) of the component - * definition(s) inside `source`. - * It is a function that gets passed the program AST node of - * the source as first argument, and a reference to recast as second argument. - * - * This allows you define your own strategy for finding component definitions. - * By default it will look for the exported component created by - * React.createClass. An error is thrown if multiple components are exported. - * - * NOTE: The component definition is currently expected to be represented as - * an ObjectExpression (an object literal), no matter which strategy is - * chosen. This will likely change in the future. - */ - parseSource( - source: string, - componentDefinitionStrategy?: - (program: ASTNode, recast: Object) => (Array|NodePath) - ): (Array|Object) { - if (!componentDefinitionStrategy) { - componentDefinitionStrategy = findExportedReactCreateClass; - } - var ast = recast.parse(source); - // Find the component definitions first. The return value should be - // an ObjectExpression. - var componentDefinition = componentDefinitionStrategy(ast.program, recast); - var isArray = Array.isArray(componentDefinition); - if (!componentDefinition || (isArray && componentDefinition.length === 0)) { - throw new Error(ReactDocumentationParser.ERROR_MISSING_DEFINITION); - } - - return isArray ? - this._executeHandlers(componentDefinition).map( - documentation => documentation.toObject() - ) : - this._executeHandlers([componentDefinition])[0].toObject(); - } - - _executeHandlers(componentDefinitions: Array): Array { - return componentDefinitions.map(componentDefinition => { - var documentation = new Documentation(); - componentDefinition.get('properties').each(propertyPath => { - var name = getPropertyName(propertyPath); - if (!this._apiHandlers[name]) { - return; - } - var propertyValuePath = propertyPath.get('value'); - this._apiHandlers[name].forEach( - handler => handler(documentation, propertyValuePath) - ); - }); - - this._componentHandlers.forEach( - handler => handler(documentation, componentDefinition) - ); - return documentation; - }); - } - -} - -ReactDocumentationParser.ERROR_MISSING_DEFINITION = - 'No suitable component definition found.'; - -module.exports = ReactDocumentationParser; diff --git a/website/react-docgen/lib/__tests__/ReactDocumentationParser-test.js b/website/react-docgen/lib/__tests__/ReactDocumentationParser-test.js deleted file mode 100644 index f0a865d1c..000000000 --- a/website/react-docgen/lib/__tests__/ReactDocumentationParser-test.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2015, 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.autoMockOff(); - -describe('React documentation parser', function() { - var ReactDocumentationParser; - var parser; - var recast; - - beforeEach(function() { - recast = require('recast'); - ReactDocumentationParser = require('../ReactDocumentationParser'); - parser = new ReactDocumentationParser(); - }); - - function pathFromSource(source) { - return new recast.types.NodePath( - recast.parse(source).program.body[0].expression - ); - } - - describe('parseSource', function() { - - it('allows custom component definition resolvers', function() { - var path = pathFromSource('({foo: "bar"})'); - var resolver = jest.genMockFunction().mockReturnValue(path); - var handler = jest.genMockFunction(); - parser.addHandler(handler); - parser.parseSource('', resolver); - - expect(resolver).toBeCalled(); - expect(handler.mock.calls[0][1]).toBe(path); - }); - - it('errors if component definition is not found', function() { - var handler = jest.genMockFunction(); - expect(function() { - parser.parseSource('', handler); - }).toThrow(ReactDocumentationParser.ERROR_MISSING_DEFINITION); - expect(handler).toBeCalled(); - - handler = jest.genMockFunction().mockReturnValue([]); - expect(function() { - parser.parseSource('', handler); - }).toThrow(ReactDocumentationParser.ERROR_MISSING_DEFINITION); - expect(handler).toBeCalled(); - }); - - }); - -}); diff --git a/website/react-docgen/lib/handlers/__tests__/componentDocblockHandler-test.js b/website/react-docgen/lib/handlers/__tests__/componentDocblockHandler-test.js deleted file mode 100644 index fd0dfd1d6..000000000 --- a/website/react-docgen/lib/handlers/__tests__/componentDocblockHandler-test.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2015, 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.autoMockOff(); - -describe('React documentation parser', function() { - var parser; - - beforeEach(function() { - parser = new (require('../../ReactDocumentationParser')); - parser.addHandler(require('../componentDocblockHandler')); - }); - - it('finds docblocks for component definitions', function() { - var source = [ - 'var React = require("React");', - '/**', - ' * Component description', - ' */', - 'var Component = React.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - var expectedResult = { - props: {}, - description: 'Component description' - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('ignores other types of comments', function() { - var source = [ - 'var React = require("React");', - '/*', - ' * This is not a docblock', - ' */', - 'var Component = React.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - var expectedResult = { - props: {}, - description: '' - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - - - source = [ - 'var React = require("React");', - '// Inline comment', - 'var Component = React.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - expectedResult = { - props: {}, - description: '' - }; - - result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('only considers the docblock directly above the definition', function() { - var source = [ - 'var React = require("React");', - '/**', - ' * This is the wrong docblock', - ' */', - 'var something_else = "foo";', - 'var Component = React.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - var expectedResult = { - props: {}, - description: '' - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); -}); diff --git a/website/react-docgen/lib/handlers/__tests__/defaultValueHandler-test.js b/website/react-docgen/lib/handlers/__tests__/defaultValueHandler-test.js deleted file mode 100644 index 7902d3ee7..000000000 --- a/website/react-docgen/lib/handlers/__tests__/defaultValueHandler-test.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2015, 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.autoMockOff(); - -var module_template = [ - 'var React = require("React");', - 'var PropTypes = React.PropTypes;', - 'var Component = React.createClass(%s);', - 'module.exports = Component;' -].join('\n'); - -function getSource(definition) { - return module_template.replace('%s', definition); -} - -describe('React documentation parser', function() { - var parser; - - beforeEach(function() { - parser = new (require('../../ReactDocumentationParser')); - parser.addHandler(require('../defaultValueHandler'), 'getDefaultProps'); - }); - - it ('should find prop default values that are literals', function() { - var source = getSource([ - '{', - ' getDefaultProps: function() {', - ' return {', - ' foo: "bar",', - ' bar: 42,', - ' baz: ["foo", "bar"],', - ' abc: {xyz: abc.def, 123: 42}', - ' };', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props: { - foo: { - defaultValue: { - value: '"bar"', - computed: false - } - }, - bar: { - defaultValue: { - value: '42', - computed: false - } - }, - baz: { - defaultValue: { - value: '["foo", "bar"]', - computed: false - } - }, - abc: { - defaultValue: { - value: '{xyz: abc.def, 123: 42}', - computed: false - } - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); -}); diff --git a/website/react-docgen/lib/handlers/__tests__/propDocblockHandler-test.js b/website/react-docgen/lib/handlers/__tests__/propDocblockHandler-test.js deleted file mode 100644 index 90f035b53..000000000 --- a/website/react-docgen/lib/handlers/__tests__/propDocblockHandler-test.js +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (c) 2015, 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.autoMockOff(); - -var module_template = [ - 'var React = require("React");', - 'var PropTypes = React.PropTypes;', - 'var Component = React.createClass(%s);', - 'module.exports = Component;' -].join('\n'); - -function getSource(definition) { - return module_template.replace('%s', definition); -} - -describe('React documentation parser', function() { - var parser; - - beforeEach(function() { - parser = new (require('../../ReactDocumentationParser')); - parser.addHandler(require('../propDocblockHandler'), 'propTypes'); - }); - - it('finds docblocks for prop types', function() { - var source = getSource([ - '{', - ' propTypes: {', - ' /**', - ' * Foo comment', - ' */', - ' foo: Prop.bool,', - '', - ' /**', - ' * Bar comment', - ' */', - ' bar: Prop.bool,', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props: { - foo: { - description: 'Foo comment' - }, - bar: { - description: 'Bar comment' - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('can handle multline comments', function() { - var source = getSource([ - '{', - ' propTypes: {', - ' /**', - ' * Foo comment with', - ' * many lines!', - ' *', - ' * even with empty lines in between', - ' */', - ' foo: Prop.bool', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props: { - foo: { - description: - 'Foo comment with\nmany lines!\n\neven with empty lines in between' - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('ignores non-docblock comments', function() { - var source = getSource([ - '{', - ' propTypes: {', - ' /**', - ' * Foo comment', - ' */', - ' // TODO: remove this comment', - ' foo: Prop.bool,', - '', - ' /**', - ' * Bar comment', - ' */', - ' /* This is not a doc comment */', - ' bar: Prop.bool,', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props: { - foo: { - description: 'Foo comment' - }, - bar: { - description: 'Bar comment' - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('only considers the comment with the property below it', function() { - var source = getSource([ - '{', - ' propTypes: {', - ' /**', - ' * Foo comment', - ' */', - ' foo: Prop.bool,', - ' bar: Prop.bool,', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props: { - foo: { - description: 'Foo comment' - }, - bar: { - description: '' - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('understands and ignores the spread operator', function() { - var source = getSource([ - '{', - ' propTypes: {', - ' ...Foo.propTypes,', - ' /**', - ' * Foo comment', - ' */', - ' foo: Prop.bool,', - ' bar: Prop.bool,', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props: { - foo: { - description: 'Foo comment' - }, - bar: { - description: '' - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); -}); diff --git a/website/react-docgen/lib/handlers/__tests__/propTypeHandler-test.js b/website/react-docgen/lib/handlers/__tests__/propTypeHandler-test.js deleted file mode 100644 index 7555ab0e6..000000000 --- a/website/react-docgen/lib/handlers/__tests__/propTypeHandler-test.js +++ /dev/null @@ -1,474 +0,0 @@ -/* - * Copyright (c) 2015, 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.autoMockOff(); - -var module_template = [ - 'var React = require("React");', - 'var PropTypes = React.PropTypes;', - 'var Component = React.createClass(%s);', - 'module.exports = Component;' -].join('\n'); - -function getSource(definition) { - return module_template.replace('%s', definition); -} - -describe('React documentation parser', function() { - var parser; - - beforeEach(function() { - parser = new (require('../../ReactDocumentationParser')); - parser.addHandler(require('../propTypeHandler'), 'propTypes'); - }); - - it('finds definitions via React.PropTypes', function() { - var source = [ - 'var React = require("React");', - 'var Prop = React.PropTypes;', - 'var Prop1 = require("React").PropTypes;', - 'var Component = React.createClass({', - ' propTypes: {', - ' foo: Prop.bool,', - ' bar: Prop1.bool,', - ' }', - '});', - 'module.exports = Component;' - ].join('\n'); - - var expectedResult = { - description: '', - props: { - foo: { - type: {name: 'bool'}, - required: false - }, - bar: { - type: {name: 'bool'}, - required: false - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('finds definitions via the ReactPropTypes module', function() { - var source = [ - 'var React = require("React");', - 'var Prop = require("ReactPropTypes");', - 'var Component = React.createClass({', - ' propTypes: {', - ' foo: Prop.bool,', - ' }', - '});', - 'module.exports = Component;' - ].join('\n'); - - var expectedResult = { - description: '', - props: { - foo: { - type: {name: 'bool'}, - required: false - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('detects simple prop types', function() { - var source = getSource([ - '{', - ' propTypes: {', - ' array_prop: PropTypes.array,', - ' bool_prop: PropTypes.bool,', - ' func_prop: PropTypes.func,', - ' number_prop: PropTypes.number,', - ' object_prop: PropTypes.object,', - ' string_prop: PropTypes.string,', - ' element_prop: PropTypes.element,', - ' any_prop: PropTypes.any,', - ' node_prop: PropTypes.node', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props:{ - array_prop: { - type: {name: 'array'}, - required: false - }, - bool_prop: { - type: {name: 'bool'}, - required: false - }, - func_prop: { - type: {name: 'func'}, - required: false - }, - number_prop: { - type: {name: 'number'}, - required: false - }, - object_prop: { - type: {name: 'object'}, - required: false - }, - string_prop: { - type: {name: 'string'}, - required: false - }, - element_prop: { - type: {name: 'element'}, - required: false - }, - any_prop: { - type: {name: 'any'}, - required: false - }, - node_prop: { - type: {name: 'node'}, - required: false - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('detects complex prop types', function() { - var source = getSource([ - '{', - ' propTypes: {', - ' oneOf_prop: PropTypes.oneOf(["foo", "bar"]),', - ' oneOfType_prop:', - ' PropTypes.oneOfType([PropTypes.number, PropTypes.bool]),', - ' oneOfType_custom_prop:', - ' PropTypes.oneOfType([xyz]),', - ' instanceOf_prop: PropTypes.instanceOf(Foo),', - ' arrayOf_prop: PropTypes.arrayOf(PropTypes.string),', - ' shape_prop:', - ' PropTypes.shape({foo: PropTypes.string, bar: PropTypes.bool}),', - ' shape_custom_prop:', - ' PropTypes.shape({foo: xyz})', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props:{ - oneOf_prop: { - type: { - name: 'enum', - value: [ - {value: '"foo"', computed: false}, - {value: '"bar"', computed: false} - ] - }, - required: false - }, - oneOfType_prop: { - type: { - name:'union', - value: [ - {name: 'number'}, - {name: 'bool'} - ] - }, - required: false - }, - oneOfType_custom_prop: { - type: { - name:'union', - value: [{ - name: 'custom', - raw: 'xyz' - }] - }, - required: false - }, - instanceOf_prop: { - type: { - name: 'instance', - value: 'Foo' - }, - required: false - }, - arrayOf_prop: { - type: { - name: 'arrayof', - value: {name: 'string'} - }, - required: false - }, - shape_prop: { - type: { - name: 'shape', - value: { - foo: {name: 'string'}, - bar: {name: 'bool'} - } - }, - required: false - }, - shape_custom_prop: { - type: { - name: 'shape', - value: { - foo: { - name: 'custom', - raw: 'xyz' - }, - } - }, - required: false - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('resolves variables to their values', function() { - var source = [ - 'var React = require("React");', - 'var PropTypes = React.PropTypes;', - 'var shape = {bar: PropTypes.string};', - 'var Component = React.createClass({', - ' propTypes: {', - ' foo: PropTypes.shape(shape)', - ' }', - '});', - 'module.exports = Component;' - ].join('\n'); - - var expectedResult = { - description: '', - props: { - foo: { - type: { - name: 'shape', - value: { - bar: {name: 'string'} - } - }, - required: false - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('detects whether a prop is required', function() { - var source = getSource([ - '{', - ' propTypes: {', - ' array_prop: PropTypes.array.isRequired,', - ' bool_prop: PropTypes.bool.isRequired,', - ' func_prop: PropTypes.func.isRequired,', - ' number_prop: PropTypes.number.isRequired,', - ' object_prop: PropTypes.object.isRequired,', - ' string_prop: PropTypes.string.isRequired,', - ' element_prop: PropTypes.element.isRequired,', - ' any_prop: PropTypes.any.isRequired,', - ' oneOf_prop: PropTypes.oneOf(["foo", "bar"]).isRequired,', - ' oneOfType_prop: ', - ' PropTypes.oneOfType([PropTypes.number, PropTypes.bool]).isRequired,', - ' instanceOf_prop: PropTypes.instanceOf(Foo).isRequired', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props:{ - array_prop: { - type: {name: 'array'}, - required: true - }, - bool_prop: { - type: {name: 'bool'}, - required: true - }, - func_prop: { - type: {name: 'func'}, - required: true - }, - number_prop: { - type: {name: 'number'}, - required: true - }, - object_prop: { - type: {name: 'object'}, - required: true - }, - string_prop: { - type: {name: 'string'}, - required: true - }, - element_prop: { - type: {name: 'element'}, - required: true - }, - any_prop: { - type: {name: 'any'}, - required: true - }, - oneOf_prop: { - type: { - name: 'enum', - value: [ - {value: '"foo"', computed: false}, - {value: '"bar"', computed: false} - ] - }, - required: true - }, - oneOfType_prop: { - type: { - name: 'union', - value: [ - {name: 'number'}, - {name: 'bool'} - ] - }, - required: true - }, - instanceOf_prop: { - type: { - name: 'instance', - value: 'Foo' - }, - required: true - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('detects custom validation functions', function() { - var source = getSource([ - '{', - ' propTypes: {', - ' custom_prop: function() {},', - ' custom_prop2: () => {}', - ' }', - '}' - ].join('\n')); - - var expectedResult = { - description: '', - props: { - custom_prop: { - type: { - name: 'custom', - raw: 'function() {}' - }, - required: false - }, - custom_prop2: { - type: { - name: 'custom', - raw: '() => {}' - }, - required: false - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('only considers definitions from React or ReactPropTypes', function() { - var source = [ - 'var React = require("React");', - 'var PropTypes = React.PropTypes;', - 'var Prop = require("Foo");', - 'var Component = React.createClass({', - ' propTypes: {', - ' custom_propA: PropTypes.bool,', - ' custom_propB: Prop.bool.isRequired', - ' }', - '});', - 'module.exports = Component;' - ].join('\n'); - - var expectedResult = { - description: '', - props: { - custom_propA: { - type: {name: 'bool'}, - required: false - }, - custom_propB: { - type: { - name: 'custom', - raw: 'Prop.bool.isRequired' - }, - required: false - } - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); - - it('understands the spread operator', function() { - var source = [ - 'var React = require("React");', - 'var PropTypes = React.PropTypes;', - 'var Foo = require("Foo.react");', - 'var props = {bar: PropTypes.bool};', - 'var Component = React.createClass({', - ' propTypes: {', - ' ...Foo.propTypes,', - ' ...props,', - ' foo: PropTypes.number', - ' }', - '});', - 'module.exports = Component;' - ].join('\n'); - - var expectedResult = { - description: '', - composes: ['Foo.react'], - props:{ - foo: { - type: {name: 'number'}, - required: false - }, - bar: { - type: {name: 'bool'}, - required: false - }, - } - }; - - var result = parser.parseSource(source); - expect(result).toEqual(expectedResult); - }); -}); diff --git a/website/react-docgen/lib/handlers/componentDocblockHandler.js b/website/react-docgen/lib/handlers/componentDocblockHandler.js deleted file mode 100644 index 8cb876a06..000000000 --- a/website/react-docgen/lib/handlers/componentDocblockHandler.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var Documentation = require('../Documentation'); - -var n = require('recast').types.namedTypes; -var getDocblock = require('../utils/docblock').getDocblock; - -/** - * Finds the nearest block comment before the component definition. - */ -function componentDocblockHandler( - documentation: Documentation, - path: NodePath -) { - var description = ''; - // Find parent statement (e.g. var Component = React.createClass(path);) - while (path && !n.Statement.check(path.node)) { - path = path.parent; - } - if (path) { - description = getDocblock(path) || ''; - } - documentation.setDescription(description); -} - -module.exports = componentDocblockHandler; diff --git a/website/react-docgen/lib/handlers/defaultValueHandler.js b/website/react-docgen/lib/handlers/defaultValueHandler.js deleted file mode 100644 index dd51d58d7..000000000 --- a/website/react-docgen/lib/handlers/defaultValueHandler.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var Documentation = require('../Documentation'); - -var expressionTo = require('../utils/expressionTo'); -var getPropertyName = require('../utils/getPropertyName'); -var recast = require('recast'); -var resolveToValue = require('../utils/resolveToValue'); -var types = recast.types.namedTypes; -var visit = recast.types.visit; - -function getDefaultValue(path) { - var node = path.node; - var defaultValue; - if (types.Literal.check(node)) { - defaultValue = node.raw; - } else { - path = resolveToValue(path); - node = path.node; - defaultValue = recast.print(path).code; - } - if (typeof defaultValue !== 'undefined') { - return { - value: defaultValue, - computed: types.CallExpression.check(node) || - types.MemberExpression.check(node) || - types.Identifier.check(node) - }; - } -} - -function defaultValueHandler(documentation: Documentation, path: NodePath) { - if (!types.FunctionExpression.check(path.node)) { - return; - } - - // Find the value that is returned from the function and process it if it is - // an object literal. - var objectExpressionPath; - visit(path.get('body'), { - visitFunction: () => false, - visitReturnStatement: function(path) { - var resolvedPath = resolveToValue(path.get('argument')); - if (types.ObjectExpression.check(resolvedPath.node)) { - objectExpressionPath = resolvedPath; - } - return false; - } - }); - - if (objectExpressionPath) { - objectExpressionPath.get('properties').each(function(propertyPath) { - var propDescriptor = documentation.getPropDescriptor( - getPropertyName(propertyPath) - ); - var defaultValue = getDefaultValue(propertyPath.get('value')); - if (defaultValue) { - propDescriptor.defaultValue = defaultValue; - } - }); - } -} - -module.exports = defaultValueHandler; diff --git a/website/react-docgen/lib/handlers/propDocBlockHandler.js b/website/react-docgen/lib/handlers/propDocBlockHandler.js deleted file mode 100644 index d1642e975..000000000 --- a/website/react-docgen/lib/handlers/propDocBlockHandler.js +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var Documentation = require('../Documentation'); - -var types = require('recast').types.namedTypes; -var getDocblock = require('../utils/docblock').getDocblock; -var getPropertyName = require('../utils/getPropertyName'); - -function propDocBlockHandler(documentation: Documentation, path: NodePath) { - if (!types.ObjectExpression.check(path.node)) { - return; - } - - path.get('properties').each(function(propertyPath) { - // we only support documentation of actual properties, not spread - if (types.Property.check(propertyPath.node)) { - var propDescriptor = documentation.getPropDescriptor( - getPropertyName(propertyPath) - ); - propDescriptor.description = getDocblock(propertyPath) || ''; - } - }); -} - -module.exports = propDocBlockHandler; diff --git a/website/react-docgen/lib/handlers/propTypeHandler.js b/website/react-docgen/lib/handlers/propTypeHandler.js deleted file mode 100644 index bd28a415a..000000000 --- a/website/react-docgen/lib/handlers/propTypeHandler.js +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var Documentation = require('../Documentation'); - -var expressionTo = require('../utils/expressionTo'); -var getNameOrValue = require('../utils/getNameOrValue'); -var getPropertyName = require('../utils/getPropertyName'); -var isReactModuleName = require('../utils/isReactModuleName'); -var recast = require('recast'); -var resolveToModule = require('../utils/resolveToModule'); -var resolveToValue = require('../utils/resolveToValue'); -var types = recast.types.namedTypes; - -var simplePropTypes = { - array: 1, - bool: 1, - func: 1, - number: 1, - object: 1, - string: 1, - any: 1, - element: 1, - node: 1 -}; - -function isPropTypesExpression(path) { - var moduleName = resolveToModule(path); - if (moduleName) { - return isReactModuleName(moduleName) || moduleName === 'ReactPropTypes'; - } - return false; -} - -function getEnumValues(path) { - return path.get('elements').map(function(elementPath) { - return { - value: expressionTo.String(elementPath), - computed: !types.Literal.check(elementPath.node) - }; - }); -} - -function getPropTypeOneOf(path) { - types.CallExpression.assert(path.node); - - var argumentPath = path.get('arguments', 0); - var type = {name: 'enum'}; - if (!types.ArrayExpression.check(argumentPath.node)) { - type.computed = true; - type.value = expressionTo.String(argumentPath); - } else { - type.value = getEnumValues(argumentPath); - } - return type; -} - -function getPropTypeOneOfType(path) { - types.CallExpression.assert(path.node); - - var argumentPath = path.get('arguments', 0); - var type = {name: 'union'}; - if (!types.ArrayExpression.check(argumentPath.node)) { - type.computed = true; - type.value = expressionTo.String(argumentPath); - } else { - type.value = argumentPath.get('elements').map(getPropType); - } - return type; -} - -function getPropTypeArrayOf(path) { - types.CallExpression.assert(path.node); - - var argumentPath = path.get('arguments', 0); - var type = {name: 'arrayof'}; - var subType = getPropType(argumentPath); - - if (subType.name === 'unknown') { - type.value = expressionTo.String(argumentPath); - type.computed = true; - } else { - type.value = subType; - } - return type; -} - -function getPropTypeShape(path) { - types.CallExpression.assert(path.node); - - var valuePath = path.get('arguments', 0); - var type: {name: string; value: any;} = {name: 'shape', value: 'unkown'}; - if (!types.ObjectExpression.check(valuePath.node)) { - valuePath = resolveToValue(valuePath); - } - - if (types.ObjectExpression.check(valuePath.node)) { - type.value = {}; - valuePath.get('properties').each(function(propertyPath) { - type.value[getPropertyName(propertyPath)] = - getPropType(propertyPath.get('value')); - }); - } - - return type; -} - -function getPropTypeInstanceOf(path) { - types.CallExpression.assert(path.node); - - return { - name: 'instance', - value: expressionTo.String(path.get('arguments', 0)) - }; -} - -var propTypes = { - oneOf: getPropTypeOneOf, - oneOfType: getPropTypeOneOfType, - instanceOf: getPropTypeInstanceOf, - arrayOf: getPropTypeArrayOf, - shape: getPropTypeShape -}; - -/** - * Tries to identify the prop type by the following rules: - * - * Member expressions which resolve to the `React` or `ReactPropTypes` module - * are inspected to see whether their properties are prop types. Strictly - * speaking we'd have to test whether the Member expression resolves to - * require('React').PropTypes, but we are not doing this right now for - * simplicity. - * - * Everything else is treated as custom validator - */ -function getPropType(path) { - var node = path.node; - if (types.Function.check(node) || !isPropTypesExpression(path)) { - return { - name: 'custom', - raw: recast.print(path).code - }; - } - - var expressionParts = []; - - if (types.MemberExpression.check(node)) { - // React.PropTypes.something.isRequired - if (isRequired(path)) { - path = path.get('object'); - node = path.node; - } - // React.PropTypes.something - expressionParts = expressionTo.Array(path); - } - if (types.CallExpression.check(node)) { - // React.PropTypes.something() - expressionParts = expressionTo.Array(path.get('callee')); - } - - // React.PropTypes.something -> something - var propType = expressionParts.pop(); - var type; - if (propType in propTypes) { - type = propTypes[propType](path); - } else { - type = {name: (propType in simplePropTypes) ? propType : 'unknown'}; - } - return type; -} - -/** - * Returns true of the prop is required, according to its type defintion - */ -function isRequired(path) { - if (types.MemberExpression.check(path.node)) { - var expressionParts = expressionTo.Array(path); - if (expressionParts[expressionParts.length - 1] === 'isRequired') { - return true; - } - } - return false; -} - -/** - * Handles member expressions of the form - * - * ComponentA.propTypes - * - * it resolves ComponentA to its module name and adds it to the "composes" entry - * in the documentation. - */ -function amendComposes(documentation, path) { - var node = path.node; - if (!types.MemberExpression.check(node) || - getNameOrValue(path.get('property')) !== 'propTypes' || - !types.Identifier.check(node.object)) { - return; - } - - var moduleName = resolveToModule(path.get('object')); - if (moduleName) { - documentation.addComposes(moduleName); - } -} - -function amendPropTypes(documentation, path) { - path.get('properties').each(function(propertyPath) { - switch (propertyPath.node.type) { - case types.Property.name: - var type = getPropType(propertyPath.get('value')); - if (type) { - var propDescriptor = documentation.getPropDescriptor( - getPropertyName(propertyPath) - ); - propDescriptor.type = type; - propDescriptor.required = type.name !== 'custom' && - isRequired(propertyPath.get('value')); - } - break; - case types.SpreadProperty.name: - var resolvedValuePath = resolveToValue(propertyPath.get('argument')); - switch (resolvedValuePath.node.type) { - case types.ObjectExpression.name: // normal object literal - amendPropTypes(documentation, resolvedValuePath); - break; - case types.MemberExpression.name: - amendComposes(documentation, resolvedValuePath); - break; - } - break; - } - }); -} - -function propTypeHandler(documentation: Documentation, path: NodePath) { - path = resolveToValue(path); - switch (path.node.type) { - case types.ObjectExpression.name: - amendPropTypes(documentation, path); - break; - case types.MemberExpression.name: - amendComposes(documentation, path); - } -} - -module.exports = propTypeHandler; diff --git a/website/react-docgen/lib/main.js b/website/react-docgen/lib/main.js deleted file mode 100644 index d2b184c8b..000000000 --- a/website/react-docgen/lib/main.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -/** - * Extractor for React documentation in JavaScript. - */ -var ReactDocumentationParser = require('./ReactDocumentationParser'); -var parser = new ReactDocumentationParser(); - -parser.addHandler( - require('./handlers/propTypeHandler'), - 'propTypes' -); -parser.addHandler( - require('./handlers/propDocBlockHandler'), - 'propTypes' -); -parser.addHandler( - require('./handlers/defaultValueHandler'), - 'getDefaultProps' -); - -parser.addHandler( - require('./handlers/componentDocblockHandler') -); - -module.exports = parser; diff --git a/website/react-docgen/lib/strategies/__tests__/findAllReactCreateClassCalls-test.js b/website/react-docgen/lib/strategies/__tests__/findAllReactCreateClassCalls-test.js deleted file mode 100644 index a78e6a77d..000000000 --- a/website/react-docgen/lib/strategies/__tests__/findAllReactCreateClassCalls-test.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2015, 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.autoMockOff(); - -describe('React documentation parser', function() { - var findAllReactCreateClassCalls; - var recast; - - function parse(source) { - return findAllReactCreateClassCalls( - recast.parse(source).program, - recast - ); - } - - beforeEach(function() { - findAllReactCreateClassCalls = require('../findAllReactCreateClassCalls'); - recast = require('recast'); - }); - - - it('finds React.createClass', function() { - var source = [ - 'var React = require("React");', - 'var Component = React.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - var result = parse(source); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(1); - expect(result[0] instanceof recast.types.NodePath).toBe(true); - expect(result[0].node.type).toBe('ObjectExpression'); - }); - - it('finds React.createClass, independent of the var name', function() { - var source = [ - 'var R = require("React");', - 'var Component = R.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - var result = parse(source); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(1); - }); - - it('does not process X.createClass of other modules', function() { - var source = [ - 'var R = require("NoReact");', - 'var Component = R.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - var result = parse(source); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(0); - }); - - it('finds assignments to exports', function() { - var source = [ - 'var R = require("React");', - 'var Component = R.createClass({});', - 'exports.foo = 42;', - 'exports.Component = Component;' - ].join('\n'); - - var result = parse(source); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(1); - }); - - it('accepts multiple definitions', function() { - var source = [ - 'var R = require("React");', - 'var ComponentA = R.createClass({});', - 'var ComponentB = R.createClass({});', - 'exports.ComponentB = ComponentB;' - ].join('\n'); - - var result = parse(source); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(2); - - source = [ - 'var R = require("React");', - 'var ComponentA = R.createClass({});', - 'var ComponentB = R.createClass({});', - 'module.exports = ComponentB;' - ].join('\n'); - - result = parse(source); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBe(2); - }); -}); diff --git a/website/react-docgen/lib/strategies/__tests__/findExportedReactCreateClassCall-test.js b/website/react-docgen/lib/strategies/__tests__/findExportedReactCreateClassCall-test.js deleted file mode 100644 index b0e4cba92..000000000 --- a/website/react-docgen/lib/strategies/__tests__/findExportedReactCreateClassCall-test.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2015, 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.autoMockOff(); - -describe('React documentation parser', function() { - var findExportedReactCreateClass; - var recast; - - function parse(source) { - return findExportedReactCreateClass( - recast.parse(source).program, - recast - ); - } - - beforeEach(function() { - findExportedReactCreateClass = - require('../findExportedReactCreateClassCall'); - recast = require('recast'); - }); - - it('finds React.createClass', function() { - var source = [ - 'var React = require("React");', - 'var Component = React.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - expect(parse(source)).toBeDefined(); - }); - - it('finds React.createClass, independent of the var name', function() { - var source = [ - 'var R = require("React");', - 'var Component = R.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - expect(parse(source)).toBeDefined(); - }); - - it('does not process X.createClass of other modules', function() { - var source = [ - 'var R = require("NoReact");', - 'var Component = R.createClass({});', - 'module.exports = Component;' - ].join('\n'); - - expect(parse(source)).toBeUndefined(); - }); - - it('finds assignments to exports', function() { - var source = [ - 'var R = require("React");', - 'var Component = R.createClass({});', - 'exports.foo = 42;', - 'exports.Component = Component;' - ].join('\n'); - - expect(parse(source)).toBeDefined(); - }); - - it('errors if multiple components are exported', function() { - var source = [ - 'var R = require("React");', - 'var ComponentA = R.createClass({});', - 'var ComponentB = R.createClass({});', - 'exports.ComponentA = ComponentA;', - 'exports.ComponentB = ComponentB;' - ].join('\n'); - - expect(function() { - parse(source) - }).toThrow(); - }); - - it('accepts multiple definitions if only one is exported', function() { - var source = [ - 'var R = require("React");', - 'var ComponentA = R.createClass({});', - 'var ComponentB = R.createClass({});', - 'exports.ComponentB = ComponentB;' - ].join('\n'); - - expect(parse(source)).toBeDefined(); - - source = [ - 'var R = require("React");', - 'var ComponentA = R.createClass({});', - 'var ComponentB = R.createClass({});', - 'module.exports = ComponentB;' - ].join('\n'); - - expect(parse(source)).toBeDefined(); - }); -}); diff --git a/website/react-docgen/lib/strategies/findAllReactCreateClassCalls.js b/website/react-docgen/lib/strategies/findAllReactCreateClassCalls.js deleted file mode 100644 index 7971a1d26..000000000 --- a/website/react-docgen/lib/strategies/findAllReactCreateClassCalls.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var isReactCreateClassCall = require('../utils/isReactCreateClassCall'); -var resolveToValue = require('../utils/resolveToValue'); - -/** - * Given an AST, this function tries to find all object expressions that are - * passed to `React.createClass` calls, by resolving all references properly. - */ -function findAllReactCreateClassCalls( - ast: ASTNode, - recast: Object -): Array { - var types = recast.types.namedTypes; - var definitions = []; - - recast.visit(ast, { - visitCallExpression: function(path) { - if (!isReactCreateClassCall(path)) { - return false; - } - // We found React.createClass. Lets get cracking! - var resolvedPath = resolveToValue(path.get('arguments', 0)); - if (types.ObjectExpression.check(resolvedPath.node)) { - definitions.push(resolvedPath); - } - return false; - } - }); - - return definitions; -} - -module.exports = findAllReactCreateClassCalls; diff --git a/website/react-docgen/lib/strategies/findExportedReactCreateClassCall.js b/website/react-docgen/lib/strategies/findExportedReactCreateClassCall.js deleted file mode 100644 index ccb8a3907..000000000 --- a/website/react-docgen/lib/strategies/findExportedReactCreateClassCall.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var isExportsOrModuleAssignment = - require('../utils/isExportsOrModuleAssignment'); -var isReactCreateClassCall = require('../utils/isReactCreateClassCall'); -var resolveToValue = require('../utils/resolveToValue'); - -var ERROR_MULTIPLE_DEFINITIONS = - 'Multiple exported component definitions found.'; - -function ignore() { - return false; -} - -/** - * Given an AST, this function tries to find the object expression that is - * passed to `React.createClass`, by resolving all references properly. - */ -function findExportedReactCreateClass( - ast: ASTNode, - recast: Object -): ?NodePath { - var types = recast.types.namedTypes; - var definition; - - recast.visit(ast, { - visitFunctionDeclaration: ignore, - visitFunctionExpression: ignore, - visitIfStatement: ignore, - visitWithStatement: ignore, - visitSwitchStatement: ignore, - visitCatchCause: ignore, - visitWhileStatement: ignore, - visitDoWhileStatement: ignore, - visitForStatement: ignore, - visitForInStatement: ignore, - visitAssignmentExpression: function(path) { - // Ignore anything that is not `exports.X = ...;` or - // `module.exports = ...;` - if (!isExportsOrModuleAssignment(path)) { - return false; - } - // Resolve the value of the right hand side. It should resolve to a call - // expression, something like React.createClass - path = resolveToValue(path.get('right')); - if (!isReactCreateClassCall(path)) { - return false; - } - if (definition) { - // If a file exports multiple components, ... complain! - throw new Error(ERROR_MULTIPLE_DEFINITIONS); - } - // We found React.createClass. Lets get cracking! - var resolvedPath = resolveToValue(path.get('arguments', 0)); - if (types.ObjectExpression.check(resolvedPath.node)) { - definition = resolvedPath; - } - return false; - } - }); - - return definition; -} - -module.exports = findExportedReactCreateClass; diff --git a/website/react-docgen/lib/utils/__tests__/docblock-test.js b/website/react-docgen/lib/utils/__tests__/docblock-test.js deleted file mode 100644 index 23e3d1eb1..000000000 --- a/website/react-docgen/lib/utils/__tests__/docblock-test.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2015, 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.autoMockOff(); - -describe('docblock', function() { - - describe('getDoclets', function() { - var getDoclets; - - beforeEach(function() { - getDoclets = require('../docblock').getDoclets; - }); - - it('extacts single line doclets', function() { - expect(getDoclets('@foo bar\n@bar baz')) - .toEqual({foo: 'bar', bar: 'baz'}); - }); - - it('extacts multi line doclets', function() { - expect(getDoclets('@foo bar\nbaz\n@bar baz')) - .toEqual({foo: 'bar\nbaz', bar: 'baz'}); - }); - - it('extacts boolean doclets', function() { - expect(getDoclets('@foo bar\nbaz\n@abc\n@bar baz')) - .toEqual({foo: 'bar\nbaz', abc: true, bar: 'baz'}); - }); - }); - -}); diff --git a/website/react-docgen/lib/utils/__tests__/isExportsOrModuleAssignment-test.js b/website/react-docgen/lib/utils/__tests__/isExportsOrModuleAssignment-test.js deleted file mode 100644 index 11c049c38..000000000 --- a/website/react-docgen/lib/utils/__tests__/isExportsOrModuleAssignment-test.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict"; - -jest.autoMockOff(); - -describe('isExportsOrModuleAssignment', function() { - var recast; - var isExportsOrModuleAssignment; - - function parse(src) { - return new recast.types.NodePath( - recast.parse(src).program.body[0] - ); - } - - beforeEach(function() { - isExportsOrModuleAssignment = require('../isExportsOrModuleAssignment'); - recast = require('recast'); - }); - - it('detects "module.exports = ...;"', function() { - expect(isExportsOrModuleAssignment(parse('module.exports = foo;'))) - .toBe(true); - }); - - it('detects "exports.foo = ..."', function() { - expect(isExportsOrModuleAssignment(parse('exports.foo = foo;'))) - .toBe(true); - }); - - it('does not accept "exports = foo;"', function() { - // That doesn't actually export anything - expect(isExportsOrModuleAssignment(parse('exports = foo;'))) - .toBe(false); - }); - -}); diff --git a/website/react-docgen/lib/utils/docblock.js b/website/react-docgen/lib/utils/docblock.js deleted file mode 100644 index 09f888a88..000000000 --- a/website/react-docgen/lib/utils/docblock.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * Helper functions to work with docblock comments. - * @flow - */ -"use strict"; - -var types = require('recast').types.namedTypes; -var docletPattern = /^@(\w+)(?:$|\s((?:[^](?!^@\w))*))/gmi; - -function parseDocblock(str) { - var lines = str.split('\n'); - for (var i = 0, l = lines.length; i < l; i++) { - lines[i] = lines[i].replace(/^\s*\*\s?/, ''); - } - return lines.join('\n').trim(); -} - -/** - * Given a path, this function returns the closest preceding docblock if it - * exists. - */ -function getDocblock(path: NodePath): ?string { - if (path.node.comments) { - var comments = path.node.comments.leading.filter(function(comment) { - return comment.type === 'Block' && comment.value.indexOf('*\n') === 0; - }); - if (comments.length > 0) { - return parseDocblock(comments[comments.length - 1].value); - } - } - return null; -} - -/** - * Given a string, this functions returns an object with doclet names as keys - * and their "content" as values. - */ -function getDoclets(str: string): Object { - var doclets = Object.create(null); - var match = docletPattern.exec(str); - - for (; match; match = docletPattern.exec(str)) { - doclets[match[1]] = match[2] || true; - } - - return doclets; -} - -exports.getDocblock = getDocblock; -exports.getDoclets = getDoclets; diff --git a/website/react-docgen/lib/utils/expressionTo.js b/website/react-docgen/lib/utils/expressionTo.js deleted file mode 100644 index aa713a81b..000000000 --- a/website/react-docgen/lib/utils/expressionTo.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var resolveToValue = require('./resolveToValue'); -var types = require('recast').types.namedTypes; - -/** - * Splits a MemberExpression or CallExpression into parts. - * E.g. foo.bar.baz becomes ['foo', 'bar', 'baz'] - */ -function toArray(path: NodePath): Array { - var parts = [path]; - var result = []; - - while (parts.length > 0) { - path = parts.shift(); - var node = path.node; - if (types.CallExpression.check(node)) { - parts.push(path.get('callee')); - continue; - } else if (types.MemberExpression.check(node)) { - parts.push(path.get('object')); - if (node.computed) { - var resolvedPath = resolveToValue(path.get('property')); - if (resolvedPath !== undefined) { - result = result.concat(toArray(resolvedPath)); - } else { - result.push(''); - } - } else { - result.push(node.property.name); - } - continue; - } else if (types.Identifier.check(node)) { - result.push(node.name); - continue; - } else if (types.Literal.check(node)) { - result.push(node.raw); - continue; - } else if (types.ThisExpression.check(node)) { - result.push('this'); - continue; - } else if (types.ObjectExpression.check(node)) { - var properties = path.get('properties').map(function(property) { - return toString(property.get('key')) + - ': ' + - toString(property.get('value')); - }); - result.push('{' + properties.join(', ') + '}'); - continue; - } else if(types.ArrayExpression.check(node)) { - result.push('[' + path.get('elements').map(toString).join(', ') + ']'); - continue; - } - } - - return result.reverse(); -} - -/** - * Creates a string representation of a member expression. - */ -function toString(path: NodePath): string { - return toArray(path).join('.'); -} - -exports.String = toString; -exports.Array = toArray; diff --git a/website/react-docgen/lib/utils/getNameOrValue.js b/website/react-docgen/lib/utils/getNameOrValue.js deleted file mode 100644 index 9c63d2fe6..000000000 --- a/website/react-docgen/lib/utils/getNameOrValue.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var types = require('recast').types.namedTypes; - -/** - * If node is an Identifier, it returns its name. If it is a literal, it returns - * its value. - */ -function getNameOrValue(path: NodePath, raw?: boolean): string { - var node = path.node; - switch (node.type) { - case types.Identifier.name: - return node.name; - case types.Literal.name: - return raw ? node.raw : node.value; - default: - throw new TypeError('Argument must be an Identifier or a Literal'); - } -} - -module.exports = getNameOrValue; diff --git a/website/react-docgen/lib/utils/getPropertyName.js b/website/react-docgen/lib/utils/getPropertyName.js deleted file mode 100644 index ae70396f4..000000000 --- a/website/react-docgen/lib/utils/getPropertyName.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var getNameOrValue = require('./getNameOrValue'); -var types = require('recast').types.namedTypes; - -/** - * In an ObjectExpression, the name of a property can either be an identifier - * or a literal (or dynamic, but we don't support those). This function simply - * returns the value of the literal or name of the identifier. - */ -function getPropertyName(propertyPath: NodePath): string { - if (propertyPath.node.computed) { - throw new TypeError('Propery name must be an Identifier or a Literal'); - } - - return getNameOrValue(propertyPath.get('key'), false); -} - -module.exports = getPropertyName; diff --git a/website/react-docgen/lib/utils/isExportsOrModuleAssignment.js b/website/react-docgen/lib/utils/isExportsOrModuleAssignment.js deleted file mode 100644 index f6d7f1a9c..000000000 --- a/website/react-docgen/lib/utils/isExportsOrModuleAssignment.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var expressionTo = require('./expressionTo'); -var types = require('recast').types.namedTypes; - -/** - * Returns true if the expression is of form `exports.foo = ...;` or - * `modules.exports = ...;`. - */ -function isExportsOrModuleAssignment(path: NodePath): boolean { - if (types.ExpressionStatement.check(path.node)) { - path = path.get('expression'); - } - if (!types.AssignmentExpression.check(path.node) || - !types.MemberExpression.check(path.node.left)) { - return false; - } - - var exprArr = expressionTo.Array(path.get('left')); - return (exprArr[0] === 'module' && exprArr[1] === 'exports') || - exprArr[0] == 'exports'; -} - -module.exports = isExportsOrModuleAssignment; diff --git a/website/react-docgen/lib/utils/isReactCreateClassCall.js b/website/react-docgen/lib/utils/isReactCreateClassCall.js deleted file mode 100644 index 5231f3da8..000000000 --- a/website/react-docgen/lib/utils/isReactCreateClassCall.js +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var isReactModuleName = require('./isReactModuleName'); -var match = require('./match'); -var resolveToModule = require('./resolveToModule'); -var types = require('recast').types.namedTypes; - -/** - * Returns true if the expression is a function call of the form - * `React.createClass(...)`. - */ -function isReactCreateClassCall(path: NodePath): boolean { - if (types.ExpressionStatement.check(path.node)) { - path = path.get('expression'); - } - - if (!match(path.node, {callee: {property: {name: 'createClass'}}})) { - return false; - } - var module = resolveToModule(path.get('callee', 'object')); - return module && isReactModuleName(module); -} - -module.exports = isReactCreateClassCall; diff --git a/website/react-docgen/lib/utils/isReactModuleName.js b/website/react-docgen/lib/utils/isReactModuleName.js deleted file mode 100644 index 1b9f8878b..000000000 --- a/website/react-docgen/lib/utils/isReactModuleName.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var reactModules = ['react', 'react/addons']; - -/** - * Takes a module name (string) and returns true if it refers to a root react - * module name. - */ -function isReactModuleName(moduleName: string): boolean { - return reactModules.some(function(reactModuleName) { - return reactModuleName === moduleName.toLowerCase(); - }); -} - -module.exports = isReactModuleName; diff --git a/website/react-docgen/lib/utils/match.js b/website/react-docgen/lib/utils/match.js deleted file mode 100644 index 9365d0ff8..000000000 --- a/website/react-docgen/lib/utils/match.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -/** - * This function takes an AST node and matches it against "pattern". Pattern - * is simply a (nested) object literal and it is traversed to see whether node - * contains those (nested) properties with the provided values. - */ -function match(node: ASTNOde, pattern: Object): boolean { - if (!node) { - return false; - } - for (var prop in pattern) { - if (!node[prop]) { - return false; - } - if (pattern[prop] && typeof pattern[prop] === 'object') { - if (!match(node[prop], pattern[prop])) { - return false; - } - } else if (pattern[prop] !== pattern[prop]) { - return false; - } - } - return true; -} - -module.exports = match; diff --git a/website/react-docgen/lib/utils/resolveToModule.js b/website/react-docgen/lib/utils/resolveToModule.js deleted file mode 100644 index d60769b33..000000000 --- a/website/react-docgen/lib/utils/resolveToModule.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var match = require('./match'); -var resolveToValue = require('./resolveToValue'); -var types = require('recast').types.namedTypes; - -/** - * Given a path (e.g. call expression, member expression or identifier), - * this function tries to find the name of module from which the "root value" - * was imported. - */ -function resolveToModule(path: NodePath): ?string { - var node = path.node; - switch (node.type) { - case types.VariableDeclarator.name: - if (node.init) { - return resolveToModule(path.get('init')); - } - break; - case types.CallExpression.name: - if (match(node.callee, {type: types.Identifier.name, name: 'require'})) { - return node['arguments'][0].value; - } - return resolveToModule(path.get('callee')); - case types.Identifier.name: - var valuePath = resolveToValue(path); - if (valuePath !== path) { - return resolveToModule(valuePath); - } - break; - case types.MemberExpression.name: - while (path && types.MemberExpression.check(path.node)) { - path = path.get('object'); - } - if (path) { - return resolveToModule(path); - } - } -} - -module.exports = resolveToModule; diff --git a/website/react-docgen/lib/utils/resolveToValue.js b/website/react-docgen/lib/utils/resolveToValue.js deleted file mode 100644 index 4e352d75f..000000000 --- a/website/react-docgen/lib/utils/resolveToValue.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2015, 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. - * - */ - -/** - * @flow - */ -"use strict"; - -var types = require('recast').types.namedTypes; - -/** - * If the path is an identifier, it is resolved in the scope chain. - * If it is an assignment expression, it resolves to the right hand side. - * - * Else the path itself is returned. - */ -function resolveToValue(path: NodePath): NodePath { - var node = path.node; - if (types.AssignmentExpression.check(node)) { - if (node.operator === '=') { - return resolveToValue(node.get('right')); - } - } else if (types.Identifier.check(node)) { - var scope = path.scope.lookup(node.name); - if (scope) { - var bindings = scope.getBindings()[node.name]; - if (bindings.length > 0) { - var parentPath = scope.getBindings()[node.name][0].parent; - if (types.VariableDeclarator.check(parentPath.node)) { - parentPath = parentPath.get('init'); - } - return resolveToValue(parentPath); - } - } - } - return path; -} - -module.exports = resolveToValue; diff --git a/website/react-docgen/package.json b/website/react-docgen/package.json deleted file mode 100644 index c0f85e0f7..000000000 --- a/website/react-docgen/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "react-docgen", - "version": "1.0.0", - "description": "Extract information from React components for documentation generation", - "bin": { - "react-docgen": "bin/react-docgen.js" - }, - "main": "dist/main.js", - "scripts": { - "watch": "jsx lib/ dist/ --harmony --strip-types -w", - "build": "rm -rf dist/ && jsx lib/ dist/ --harmony --strip-types --no-cache-dir", - "prepublish": "npm run build", - "test": "jest" - }, - "keywords": [ - "react", - "documentation" - ], - "author": "Felix Kling", - "license": "BSD-3-Clause", - "dependencies": { - "async": "^0.9.0", - "node-dir": "^0.1.6", - "nomnom": "^1.8.1", - "recast": "^0.9.17" - }, - "devDependencies": { - "jest-cli": "^0.3.0", - "react-tools": "^0.12.2" - }, - "jest": { - "scriptPreprocessor": "./preprocessor", - "testPathDirs": ["lib"] - } -} diff --git a/website/react-docgen/preprocessor.js b/website/react-docgen/preprocessor.js deleted file mode 100644 index f827426d4..000000000 --- a/website/react-docgen/preprocessor.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -var reactTools = require('react-tools'); - -function process(source) { - return reactTools.transform(source, {harmony: true, stripTypes: true}); -} - -exports.process = process; diff --git a/website/server/convert.js b/website/server/convert.js index 1a02efca7..7d239313d 100644 --- a/website/server/convert.js +++ b/website/server/convert.js @@ -30,7 +30,7 @@ function backtickify(str) { function execute() { var MD_DIR = '../docs/'; - var files = glob.sync('src/react-native/docs/*.*') + var files = glob.sync('src/react-native/docs/*.*'); files.forEach(function(file) { try { fs.unlinkSync(file); diff --git a/website/server/extractDocs.js b/website/server/extractDocs.js index d07395a39..1c7f19063 100644 --- a/website/server/extractDocs.js +++ b/website/server/extractDocs.js @@ -1,13 +1,8 @@ -var docs = require('../react-docgen'); -var findExportedReactCreateClassCall = require( - '../react-docgen/dist/strategies/findExportedReactCreateClassCall' -); -var findAllReactCreateClassCalls = require( - '../react-docgen/dist/strategies/findAllReactCreateClassCalls' -); +var docs = require('react-docgen'); var fs = require('fs'); var path = require('path'); var slugify = require('../core/slugify'); +var jsDocs = require('../jsdocs/jsdocs.js'); function getNameFromPath(filepath) { var ext = null; @@ -17,50 +12,92 @@ function getNameFromPath(filepath) { return filepath; } -function docsToMarkdown(filepath, i) { - var json = docs.parseSource( - fs.readFileSync(filepath), - function(node, recast) { - return findExportedReactCreateClassCall(node, recast) || - findAllReactCreateClassCalls(node, recast)[0]; - } - ) - +function componentsToMarkdown(type, json, filepath, i) { var componentName = getNameFromPath(filepath); + var docFilePath = '../docs/' + componentName + '.md'; + if (fs.existsSync(docFilePath)) { + json.fullDescription = fs.readFileSync(docFilePath).toString(); + } + json.type = type; + var res = [ '---', 'id: ' + slugify(componentName), 'title: ' + componentName, - 'layout: docs', - 'category: Components', + 'layout: autodocs', + 'category: ' + type + 's', 'permalink: docs/' + slugify(componentName) + '.html', - components[i + 1] && ('next: ' + slugify(getNameFromPath(components[i + 1]))), + 'next: ' + (all[i + 1] ? + slugify(getNameFromPath(all[i + 1])) : + 'network'), '---', - ' ', - json.description, - ' ', - '# Props', - '```', - JSON.stringify(json.props, null, 2), - '```', + JSON.stringify(json, null, 2), ].filter(function(line) { return line; }).join('\n'); return res; } var components = [ + '../Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js', + '../Libraries/Components/DatePicker/DatePickerIOS.ios.js', + '../Libraries/Image/Image.ios.js', + '../Libraries/CustomComponents/ListView/ListView.js', + '../Libraries/Components/MapView/MapView.js', '../Libraries/Components/Navigation/NavigatorIOS.ios.js', - '../Libraries/Components/Image/Image.ios.js', - '../Libraries/Components/ListView/ListView.js', - '../Libraries/Components/Navigation/NavigatorIOS.ios.js', - '../Libraries/Components/ScrollView/ScrollView.ios.js', - '../Libraries/Components/Text/Text.js', + '../Libraries/Picker/PickerIOS.ios.js', + '../Libraries/Components/ScrollView/ScrollView.js', + '../Libraries/Components/SliderIOS/SliderIOS.js', + '../Libraries/Components/SwitchIOS/SwitchIOS.ios.js', + '../Libraries/Components/TabBarIOS/TabBarIOS.ios.js', + '../Libraries/Text/Text.js', '../Libraries/Components/TextInput/TextInput.ios.js', '../Libraries/Components/Touchable/TouchableHighlight.js', + '../Libraries/Components/Touchable/TouchableOpacity.js', '../Libraries/Components/Touchable/TouchableWithoutFeedback.js', '../Libraries/Components/View/View.js', + '../Libraries/Components/WebView/WebView.ios.js', ]; +var apis = [ + '../Libraries/Utilities/AlertIOS.js', + '../Libraries/Animation/Animation.js', + '../Libraries/AppRegistry/AppRegistry.js', + '../Libraries/AppState/AppState.js', + '../Libraries/AppStateIOS/AppStateIOS.ios.js', + '../Libraries/Storage/AsyncStorage.ios.js', + '../Libraries/CameraRoll/CameraRoll.js', + '../Libraries/Interaction/InteractionManager.js', + '../Libraries/Animation/LayoutAnimation.js', + '../Libraries/Network/NetInfo.js', + '../Libraries/Utilities/PixelRatio.js', + '../Libraries/Components/StatusBar/StatusBarIOS.ios.js', + '../Libraries/StyleSheet/StyleSheet.js', + '../Libraries/Vibration/VibrationIOS.ios.js', +]; + +var all = components.concat(apis); + module.exports = function() { - return components.map(docsToMarkdown); + var i = 0; + return [].concat( + components.map(function(filepath) { + var json = docs.parse( + fs.readFileSync(filepath), + function(node, recast) { + return docs.resolver.findExportedReactCreateClassCall(node, recast) || + docs.resolver.findAllReactCreateClassCalls(node, recast)[0]; + } + ); + return componentsToMarkdown('component', json, filepath, i++); + }), + apis.map(function(filepath) { + try { + var json = jsDocs(fs.readFileSync(filepath).toString()); + } catch(e) { + console.error('Cannot parse file', filepath); + var json = {}; + } + return componentsToMarkdown('api', json, filepath, i++); + }) + ); }; diff --git a/website/server/generate.js b/website/server/generate.js index 0ec921036..3191ef992 100644 --- a/website/server/generate.js +++ b/website/server/generate.js @@ -5,6 +5,9 @@ var fs = require('fs.extra'); var mkdirp = require('mkdirp'); var server = require('./server.js'); +require('./convert.js')(); +server.noconvert = true; + // Sadly, our setup fatals when doing multiple concurrent requests // I don't have the time to dig into why, it's easier to just serialize // requests. diff --git a/website/server/server.js b/website/server/server.js index e74d95b87..57a11e3b0 100644 --- a/website/server/server.js +++ b/website/server/server.js @@ -36,7 +36,9 @@ var app = connect() .use(function(req, res, next) { // convert all the md files on every request. This is not optimal // but fast enough that we don't really need to care right now. - convert(); + if (!server.noconvert) { + convert(); + } next(); }) .use(reactMiddleware.provide(buildOptions)) @@ -49,5 +51,5 @@ var app = connect() var portToUse = port || 8080; var server = http.createServer(app); server.listen(portToUse); -console.log('Open http://localhost:' + portToUse + '/react-native/index.html'); +console.log('Open http://localhost:' + portToUse + '/react-native/_index.html'); module.exports = server; diff --git a/website/setup.sh b/website/setup.sh new file mode 100755 index 000000000..69b5bddf4 --- /dev/null +++ b/website/setup.sh @@ -0,0 +1,7 @@ +cd ../../ +git clone git@github.com:facebook/react-native.git react-native-gh-pages +cd react-native-gh-pages +git checkout origin/gh-pages +git checkout -b gh-pages +git push --set-upstream origin gh-pages +cd ../react-native/website diff --git a/website/src/react-native/index.js b/website/src/react-native/_index.js similarity index 100% rename from website/src/react-native/index.js rename to website/src/react-native/_index.js diff --git a/website/src/react-native/css/react-native.css b/website/src/react-native/css/react-native.css index ace8010f0..35906a652 100644 --- a/website/src/react-native/css/react-native.css +++ b/website/src/react-native/css/react-native.css @@ -345,15 +345,16 @@ h1:hover .hash-link, h2:hover .hash-link, h3:hover .hash-link, h4:hover .hash-li } .nav-docs { - color: #2d2d2d; font-size: 14px; float: left; width: 210px; + margin-top: 5px; } .nav-docs ul { list-style: none; margin: 0; + margin-left: 10px; } .nav-docs ul ul { @@ -383,10 +384,10 @@ h1:hover .hash-link, h2:hover .hash-link, h3:hover .hash-link, h4:hover .hash-li color: #0485A9; } -.nav-docs .nav-docs-section { - border-bottom: 1px solid #ccc; - border-top: 1px solid #eee; - padding: 12px 0; +.nav-docs h3 { + margin: 0; + line-height: 25px; + margin-top: 5px; } .nav-docs .nav-docs-section:first-child { @@ -895,6 +896,30 @@ div[data-twttr-id] iframe { text-align: center; } + +.props { + background-color: hsl(198, 100%, 96%); +} + +.prop:nth-child(2n) { + background-color: hsl(198, 100%, 94%); +} + +.propTitle { + font-weight: bold; + font-size: 16px; +} + +.prop { + padding: 5px 10px; +} + +.propType { + font-weight: normal; + font-size: 15px; +} + + #content { display: none; } @@ -965,3 +990,4 @@ div[data-twttr-id] iframe { margin: 0; } } +