diff --git a/.flowconfig b/.flowconfig index bf06f66d3f8545..56d38f30838ddc 100644 --- a/.flowconfig +++ b/.flowconfig @@ -40,8 +40,9 @@ suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe -suppress_comment=\\(.\\|\n\\)*\\$FlowIssue #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-2]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-2]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy [version] 0.12.0 diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index e7eb7f2b4a299d..21819df4665f97 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -77,12 +77,14 @@ var SearchScreen = React.createClass({ var apiKey = API_KEYS[this.state.queryNumber % API_KEYS.length]; if (query) { return ( + // $FlowFixMe(>=0.13.0) - pageNumber may be null or undefined API_URL + 'movies.json?apikey=' + apiKey + '&q=' + encodeURIComponent(query) + '&page_limit=20&page=' + pageNumber ); } else { // With no query, load latest movies return ( + // $FlowFixMe(>=0.13.0) - pageNumber may be null or undefined API_URL + 'lists/movies/in_theaters.json?apikey=' + apiKey + '&page_limit=20&page=' + pageNumber ); diff --git a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj index 05480738f35f33..e714ca03812afc 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -600,7 +600,7 @@ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", ); - INFOPLIST_FILE = "$(SRCROOT)/iOS/Info.plist"; + INFOPLIST_FILE = "iOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = SampleApp; @@ -616,7 +616,7 @@ /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, "$(SRCROOT)/../../React/**", ); - INFOPLIST_FILE = "$(SRCROOT)/iOS/Info.plist"; + INFOPLIST_FILE = "iOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = SampleApp; diff --git a/Examples/UIExplorer/DatePickerIOSExample.js b/Examples/UIExplorer/DatePickerIOSExample.js index 36ff9cd52bbbb2..fc7686880adff5 100644 --- a/Examples/UIExplorer/DatePickerIOSExample.js +++ b/Examples/UIExplorer/DatePickerIOSExample.js @@ -125,6 +125,7 @@ var Heading = React.createClass({ } }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Select dates and times using the native UIDatePicker.'; exports.examples = [ diff --git a/Examples/UIExplorer/ImageExample.js b/Examples/UIExplorer/ImageExample.js index b4c54f997d158f..60a4a5ab1ab24a 100644 --- a/Examples/UIExplorer/ImageExample.js +++ b/Examples/UIExplorer/ImageExample.js @@ -25,6 +25,7 @@ var { var ImageCapInsetsExample = require('./ImageCapInsetsExample'); +exports.displayName = (undefined: ?string); exports.framework = 'React'; exports.title = ''; exports.description = 'Base component for displaying different types of images.'; diff --git a/Examples/UIExplorer/MapViewExample.js b/Examples/UIExplorer/MapViewExample.js index 23789c7f1346b8..0d1061acef49b8 100644 --- a/Examples/UIExplorer/MapViewExample.js +++ b/Examples/UIExplorer/MapViewExample.js @@ -236,6 +236,7 @@ var styles = StyleSheet.create({ }, }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Base component to display maps'; exports.examples = [ diff --git a/Examples/UIExplorer/Navigator/NavigationBarSample.js b/Examples/UIExplorer/Navigator/NavigationBarSample.js index 2b3f8e2503c0f9..545f76b828ad6e 100644 --- a/Examples/UIExplorer/Navigator/NavigationBarSample.js +++ b/Examples/UIExplorer/Navigator/NavigationBarSample.js @@ -92,6 +92,31 @@ function newRandomRoute() { var NavigationBarSample = React.createClass({ + componentWillMount: function() { + var navigator = this.props.navigator; + + var callback = (event) => { + console.log( + `NavigationBarSample : event ${event.type}`, + { + route: JSON.stringify(event.data.route), + target: event.target, + type: event.type, + } + ); + }; + + // Observe focus change events from this component. + this._listeners = [ + navigator.navigationContext.addListener('willfocus', callback), + navigator.navigationContext.addListener('didfocus', callback), + ]; + }, + + componentWillUnmount: function() { + this._listeners && this._listeners.forEach(listener => listener.remove()); + }, + render: function() { return ( listener.remove()); + }, + + _setNavigatorRef: function(navigator) { + if (navigator !== this._navigator) { + this._navigator = navigator; + + if (navigator) { + var callback = (event) => { + console.log( + `TabBarExample: event ${event.type}`, + { + route: JSON.stringify(event.data.route), + target: event.target, + type: event.type, + } + ); + }; + // Observe focus change events from the owner. + this._listeners = [ + navigator.navigationContext.addListener('willfocus', callback), + navigator.navigationContext.addListener('didfocus', callback), + ]; + } + } + }, }); var styles = StyleSheet.create({ diff --git a/Examples/UIExplorer/PickerIOSExample.js b/Examples/UIExplorer/PickerIOSExample.js index 14361e7608165d..31c81ccccdad5c 100644 --- a/Examples/UIExplorer/PickerIOSExample.js +++ b/Examples/UIExplorer/PickerIOSExample.js @@ -112,6 +112,7 @@ var PickerExample = React.createClass({ }, }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Render lists of selectable options with UIPickerView.'; exports.examples = [ diff --git a/Examples/UIExplorer/ProgressViewIOSExample.js b/Examples/UIExplorer/ProgressViewIOSExample.js index f0a17a7c6e78f2..e294a33708122a 100644 --- a/Examples/UIExplorer/ProgressViewIOSExample.js +++ b/Examples/UIExplorer/ProgressViewIOSExample.js @@ -60,6 +60,7 @@ var ProgressViewExample = React.createClass({ }, }); +exports.displayName = (undefined: ?string); exports.framework = 'React'; exports.title = 'ProgressViewIOS'; exports.description = 'ProgressViewIOS'; diff --git a/Examples/UIExplorer/ScrollViewExample.js b/Examples/UIExplorer/ScrollViewExample.js index 69f3ac9c72d254..1ca8baf9a8530c 100644 --- a/Examples/UIExplorer/ScrollViewExample.js +++ b/Examples/UIExplorer/ScrollViewExample.js @@ -23,6 +23,7 @@ var { Image } = React; +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Component that enables scrolling through child components'; exports.examples = [ diff --git a/Examples/UIExplorer/TextExample.ios.js b/Examples/UIExplorer/TextExample.ios.js index ebc67b672068ab..5abfae323f6964 100644 --- a/Examples/UIExplorer/TextExample.ios.js +++ b/Examples/UIExplorer/TextExample.ios.js @@ -25,7 +25,7 @@ var { var Entity = React.createClass({ render: function() { return ( - + {this.props.children} ); @@ -34,7 +34,12 @@ var Entity = React.createClass({ var AttributeToggler = React.createClass({ getInitialState: function() { - return {fontWeight: '500', fontSize: 15}; + return {fontWeight: 'bold', fontSize: 15}; + }, + toggleWeight: function() { + this.setState({ + fontWeight: this.state.fontWeight === 'bold' ? 'normal' : 'bold' + }); }, increaseSize: function() { this.setState({ @@ -42,22 +47,26 @@ var AttributeToggler = React.createClass({ }); }, render: function() { - var curStyle = {fontSize: this.state.fontSize}; + var curStyle = {fontWeight: this.state.fontWeight, fontSize: this.state.fontSize}; return ( - + Tap the controls below to change attributes. - See how it will even work on{' '} - - this nested text - - - {'>> Increase Size <<'} - + See how it will even work on this nested text - + + Toggle Weight + + + Increase Size + + ); } }); @@ -206,6 +215,12 @@ exports.examples = [ render: function() { return ( + + auto (default) - english LTR + + + أحب اللغة العربية auto (default) - arabic RTL + left left left left left left left left left left left left left left left @@ -282,43 +297,21 @@ exports.examples = [ description: 'backgroundColor is inherited from all types of views.', render: function() { return ( - - - Yellow background inherited from View parent, - - {' '}red background, - - {' '}blue background, - - {' '}inherited blue background, - - {' '}nested green background. - + + Yellow container background, + + {' '}red background, + + {' '}blue background, + + {' '}inherited blue background, + + {' '}nested green background. - - ); - }, -}, { - title: 'containerBackgroundColor attribute', - render: function() { - return ( - - - - - - - Default containerBackgroundColor (inherited) + backgroundColor wash - - - {"containerBackgroundColor: 'transparent' + backgroundColor wash"} - - + ); }, }, { @@ -346,8 +339,4 @@ var styles = StyleSheet.create({ marginBottom: 0, backgroundColor: 'rgba(100, 100, 100, 0.3)' }, - entity: { - fontWeight: '500', - color: '#527fe4', - }, }); diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index 10064491d6e660..06cc12ee383266 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -133,6 +133,7 @@ var styles = StyleSheet.create({ }, }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Single and multi-line text inputs.'; exports.examples = [ diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index acbba362921ea0..494d7771d8b383 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -26,6 +26,7 @@ var { View, } = React; +exports.displayName = (undefined: ?string); exports.title = ' and onPress'; exports.examples = [ { diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme index c3dcde8eb494fe..e2f84182e74e9d 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/xcshareddata/xcschemes/UIExplorer.xcscheme @@ -1,7 +1,7 @@ + version = "1.8"> @@ -22,10 +22,10 @@ + buildForAnalyzing = "NO"> + buildForAnalyzing = "NO"> 0 && !foundElement && !redboxError) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - - redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; - foundElement = [self findSubviewInView:vc.view matching:^(UIView *view) { - if ([view.accessibilityLabel isEqualToString:@""]) { - return YES; - } - return NO; - }]; - } - - XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); - XCTAssertTrue(foundElement, @"Couldn't find element with '' text in %d seconds", TIMEOUT_SECONDS); } #define RCT_SNAPSHOT_TEST(name, reRecord) \ @@ -102,10 +54,4 @@ - (void)test##name##Snapshot \ RCT_SNAPSHOT_TEST(SliderExample, NO) RCT_SNAPSHOT_TEST(TabBarExample, NO) -// Make sure this test runs last -- (void)testZZZ_NotInRecordMode -{ - RCTAssert(_runner.recordMode == NO, @"Don't forget to turn record mode back to NO before commit."); -} - @end diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/AsyncStorageTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/AsyncStorageTest.js index 911887d3e2dd89..c440d10cd23e9d 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/AsyncStorageTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/AsyncStorageTest.js @@ -53,15 +53,15 @@ function expectEqual(lhs, rhs, testname) { ); } -function expectAsyncNoError(err) { - expectTrue(err === null, 'Unexpected Async error: ' + JSON.stringify(err)); +function expectAsyncNoError(place, err) { + expectTrue(err === null, 'Unexpected error in ' + place + ': ' + JSON.stringify(err)); } function testSetAndGet() { AsyncStorage.setItem(KEY_1, VAL_1, (err1) => { - expectAsyncNoError(err1); + expectAsyncNoError('testSetAndGet/setItem', err1); AsyncStorage.getItem(KEY_1, (err2, result) => { - expectAsyncNoError(err2); + expectAsyncNoError('testSetAndGet/getItem', err2); expectEqual(result, VAL_1, 'testSetAndGet setItem'); updateMessage('get(key_1) correctly returned ' + result); runTestCase('should get null for missing key', testMissingGet); @@ -71,7 +71,7 @@ function testSetAndGet() { function testMissingGet() { AsyncStorage.getItem(KEY_2, (err, result) => { - expectAsyncNoError(err); + expectAsyncNoError('testMissingGet/setItem', err); expectEqual(result, null, 'testMissingGet'); updateMessage('missing get(key_2) correctly returned ' + result); runTestCase('check set twice results in a single key', testSetTwice); @@ -82,7 +82,7 @@ function testSetTwice() { AsyncStorage.setItem(KEY_1, VAL_1, ()=>{ AsyncStorage.setItem(KEY_1, VAL_1, ()=>{ AsyncStorage.getItem(KEY_1, (err, result) => { - expectAsyncNoError(err); + expectAsyncNoError('testSetTwice/setItem', err); expectEqual(result, VAL_1, 'testSetTwice'); updateMessage('setTwice worked as expected'); runTestCase('test removeItem', testRemoveItem); @@ -95,17 +95,17 @@ function testRemoveItem() { AsyncStorage.setItem(KEY_1, VAL_1, ()=>{ AsyncStorage.setItem(KEY_2, VAL_2, ()=>{ AsyncStorage.getAllKeys((err, result) => { - expectAsyncNoError(err); + expectAsyncNoError('testRemoveItem/getAllKeys', 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, (err2) => { - expectAsyncNoError(err2); + expectAsyncNoError('testRemoveItem/removeItem', err2); updateMessage('delete successful '); AsyncStorage.getItem(KEY_1, (err3, result2) => { - expectAsyncNoError(err3); + expectAsyncNoError('testRemoveItem/getItem', err3); expectEqual( result2, null, @@ -113,7 +113,7 @@ function testRemoveItem() { ); updateMessage('key properly removed '); AsyncStorage.getAllKeys((err4, result3) => { - expectAsyncNoError(err4); + expectAsyncNoError('testRemoveItem/getAllKeys', err4); expectTrue( result3.indexOf(KEY_1) === -1, 'Unexpected: KEY_1 present in ' + result3 @@ -130,11 +130,11 @@ function testRemoveItem() { function testMerge() { AsyncStorage.setItem(KEY_MERGE, JSON.stringify(VAL_MERGE_1), (err1) => { - expectAsyncNoError(err1); + expectAsyncNoError('testMerge/setItem', err1); AsyncStorage.mergeItem(KEY_MERGE, JSON.stringify(VAL_MERGE_2), (err2) => { - expectAsyncNoError(err2); + expectAsyncNoError('testMerge/mergeItem', err2); AsyncStorage.getItem(KEY_MERGE, (err3, result) => { - expectAsyncNoError(err3); + expectAsyncNoError('testMerge/setItem', err3); expectEqual(JSON.parse(result), VAL_MERGE_EXPECT, 'testMerge'); updateMessage('objects deeply merged\nDone!'); done(); diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/PromiseTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/PromiseTest.js index 38660d3d8b575a..3bcc129941327f 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/PromiseTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/PromiseTest.js @@ -22,7 +22,7 @@ var PromiseTest = React.createClass({ Promise.all([ this.testShouldResolve(), this.testShouldReject(), - ]).then(() => RCTTestModule.finish( + ]).then(() => RCTTestModule.markTestPassed( this.shouldResolve && this.shouldReject )); }, @@ -42,7 +42,7 @@ var PromiseTest = React.createClass({ }, render() { - return ; + return ; } }); diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/js/SimpleSnapshotTest.js b/Examples/UIExplorer/UIExplorerIntegrationTests/js/SimpleSnapshotTest.js index 1715f093f8d7dd..c57700a0c954d4 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/js/SimpleSnapshotTest.js +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/js/SimpleSnapshotTest.js @@ -24,8 +24,8 @@ var SimpleSnapshotTest = React.createClass({ requestAnimationFrame(() => TestModule.verifySnapshot(this.done)); }, - done() { - TestModule.markTestCompleted(); + done(success) { + TestModule.markTestPassed(success); }, render() { diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index a8050b33b92442..5af5a32c648f92 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -115,7 +115,7 @@ COMPONENTS.concat(APIS).forEach((Example) => { // View is still blank after first RAF :\ global.requestAnimationFrame(() => global.requestAnimationFrame(() => TestModule.verifySnapshot( - TestModule.markTestCompleted + TestModule.markTestPassed ) )); }, diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m index ea6af0805410c9..13bcdd7b426c28 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTBridgeTests.m @@ -79,25 +79,19 @@ @interface RCTBridgeTests : XCTestCase { RCTBridge *_bridge; BOOL _testMethodCalled; - dispatch_queue_t _queue; } @end @implementation RCTBridgeTests -RCT_EXPORT_MODULE(TestModule) +@synthesize methodQueue = _methodQueue; -- (dispatch_queue_t)methodQueue -{ - return _queue; -} +RCT_EXPORT_MODULE(TestModule) - (void)setUp { [super setUp]; - _queue = dispatch_queue_create("com.facebook.React.TestQueue", DISPATCH_QUEUE_SERIAL); - _bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[self]; } launchOptions:nil]; @@ -151,7 +145,7 @@ - (void)testCallNativeMethod [_bridge.batchedBridge _handleBuffer:buffer context:RCTGetExecutorID(executor)]; - dispatch_sync(_queue, ^{ + dispatch_sync(_methodQueue, ^{ // clear the queue XCTAssertTrue(_testMethodCalled); }); diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index d1e990cb4b4995..fe3cbef6f7d590 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -43,6 +43,7 @@ var WebViewExample = React.createClass({ backButtonEnabled: false, forwardButtonEnabled: false, loading: true, + scalesPageToFit: true, }; }, @@ -97,6 +98,7 @@ var WebViewExample = React.createClass({ javaScriptEnabledAndroid={true} onNavigationStateChange={this.onNavigationStateChange} startInLoadingState={true} + scalesPageToFit={this.state.scalesPageToFit} /> {this.state.status} @@ -124,6 +126,7 @@ var WebViewExample = React.createClass({ url: navState.url, status: navState.title, loading: navState.loading, + scalesPageToFit: true }); }, @@ -217,6 +220,7 @@ var styles = StyleSheet.create({ }, }); +exports.displayName = (undefined: ?string); exports.title = ''; exports.description = 'Base component to display web content'; exports.examples = [ diff --git a/Libraries/Animation/RCTAnimationExperimentalManager.m b/Libraries/Animation/RCTAnimationExperimentalManager.m index 6bcda39ae30d83..13c3f079a8c756 100644 --- a/Libraries/Animation/RCTAnimationExperimentalManager.m +++ b/Libraries/Animation/RCTAnimationExperimentalManager.m @@ -235,7 +235,6 @@ static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NS @try { [view.layer setValue:toValue forKey:keypath]; NSString *animationKey = [@"RCT" stringByAppendingString:RCTJSONStringify(@{@"tag": animationTag, @"key": keypath}, nil)]; - [view.layer addAnimation:animation forKey:animationKey]; if (!completionBlockSet) { strongSelf->_callbackRegistry[animationTag] = callback; [CATransaction setCompletionBlock:^{ @@ -247,6 +246,7 @@ static void RCTInvalidAnimationProp(RCTSparseArray *callbacks, NSNumber *tag, NS }]; completionBlockSet = YES; } + [view.layer addAnimation:animation forKey:animationKey]; } @catch (NSException *exception) { return RCTInvalidAnimationProp(strongSelf->_callbackRegistry, animationTag, keypath, toValue); diff --git a/Libraries/AppRegistry/AppRegistry.js b/Libraries/AppRegistry/AppRegistry.js index 157cbaa379b134..7465d85b925d32 100644 --- a/Libraries/AppRegistry/AppRegistry.js +++ b/Libraries/AppRegistry/AppRegistry.js @@ -68,7 +68,7 @@ var AppRegistry = { console.log( 'Running application "' + appKey + '" with appParams: ' + JSON.stringify(appParameters) + '. ' + - '__DEV__ === ' + __DEV__ + + '__DEV__ === ' + String(__DEV__) + ', development-level warning are ' + (__DEV__ ? 'ON' : 'OFF') + ', performance optimizations are ' + (__DEV__ ? 'OFF' : 'ON') ); diff --git a/Libraries/BatchedBridge/BatchedBridge.js b/Libraries/BatchedBridge/BatchedBridge.js new file mode 100644 index 00000000000000..ea49b202fc01a2 --- /dev/null +++ b/Libraries/BatchedBridge/BatchedBridge.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule BatchedBridge + */ +'use strict'; + +let MessageQueue = require('MessageQueue'); + +let BatchedBridge = new MessageQueue( + __fbBatchedBridgeConfig.remoteModuleConfig, + __fbBatchedBridgeConfig.localModulesConfig, +); + +module.exports = BatchedBridge; diff --git a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridge.js b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridge.js deleted file mode 100644 index 249e27e76a285d..00000000000000 --- a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridge.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule BatchedBridge - */ -'use strict'; - -var BatchedBridgeFactory = require('BatchedBridgeFactory'); -var MessageQueue = require('MessageQueue'); - -/** - * Signature that matches the native IOS modules/methods that are exposed. We - * indicate which ones accept a callback. The order of modules and methods - * within them implicitly define their numerical *ID* that will be used to - * describe method calls across the wire. This is so that memory is used - * efficiently and we do not need to copy strings in native land - or across any - * wire. - */ - -var remoteModulesConfig = __fbBatchedBridgeConfig.remoteModuleConfig; -var localModulesConfig = __fbBatchedBridgeConfig.localModulesConfig; - - -var BatchedBridge = BatchedBridgeFactory.create( - MessageQueue, - remoteModulesConfig, - localModulesConfig -); - -BatchedBridge._config = remoteModulesConfig; - -module.exports = BatchedBridge; diff --git a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js b/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js deleted file mode 100644 index 3243fb1456b697..00000000000000 --- a/Libraries/BatchedBridge/BatchingImplementation/BatchedBridgeFactory.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule BatchedBridgeFactory - */ -'use strict'; - -var invariant = require('invariant'); -var keyMirror = require('keyMirror'); -var mapObject = require('mapObject'); -var warning = require('warning'); - -var slice = Array.prototype.slice; - -var MethodTypes = keyMirror({ - remote: null, - remoteAsync: null, - local: null, -}); - -type ErrorData = { - message: string; - domain: string; - code: number; - nativeStackIOS?: string; -}; - -/** - * Creates remotely invokable modules. - */ -var BatchedBridgeFactory = { - MethodTypes: MethodTypes, - /** - * @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. - */ - _createBridgedModule: function(messageQueue, moduleConfig, moduleName) { - var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) { - switch (methodConfig.type) { - case MethodTypes.remoteAsync: - return function(...args) { - return new Promise((resolve, reject) => { - messageQueue.call(moduleName, memberName, args, resolve, (errorData) => { - var error = _createErrorFromErrorData(errorData); - reject(error); - }); - }); - }; - - case MethodTypes.local: - return null; - - default: - return function() { - var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null; - var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null; - var hasSuccCB = typeof lastArg === 'function'; - var hasErrorCB = typeof secondLastArg === 'function'; - hasErrorCB && invariant( - hasSuccCB, - 'Cannot have a non-function arg after a function arg.' - ); - 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); - }; - } - }); - for (var constName in moduleConfig.constants) { - warning(!remoteModule[constName], 'saw constant and method named %s', constName); - remoteModule[constName] = moduleConfig.constants[constName]; - } - return remoteModule; - }, - - create: function(MessageQueue, modulesConfig, localModulesConfig) { - var messageQueue = new MessageQueue(modulesConfig, localModulesConfig); - return { - callFunction: messageQueue.callFunction.bind(messageQueue), - callFunctionReturnFlushedQueue: - messageQueue.callFunctionReturnFlushedQueue.bind(messageQueue), - invokeCallback: messageQueue.invokeCallback.bind(messageQueue), - invokeCallbackAndReturnFlushedQueue: - messageQueue.invokeCallbackAndReturnFlushedQueue.bind(messageQueue), - flushedQueue: messageQueue.flushedQueue.bind(messageQueue), - RemoteModules: mapObject(modulesConfig, this._createBridgedModule.bind(this, messageQueue)), - setLoggingEnabled: messageQueue.setLoggingEnabled.bind(messageQueue), - getLoggedOutgoingItems: messageQueue.getLoggedOutgoingItems.bind(messageQueue), - getLoggedIncomingItems: messageQueue.getLoggedIncomingItems.bind(messageQueue), - replayPreviousLog: messageQueue.replayPreviousLog.bind(messageQueue), - processBatch: messageQueue.processBatch.bind(messageQueue), - }; - } -}; - -function _createErrorFromErrorData(errorData: ErrorData): Error { - var { - message, - ...extraErrorInfo, - } = errorData; - var error = new Error(message); - error.framesToPop = 1; - return Object.assign(error, extraErrorInfo); -} - -module.exports = BatchedBridgeFactory; diff --git a/Libraries/CameraRoll/CameraRoll.js b/Libraries/CameraRoll/CameraRoll.js index 67fa5083048dfd..1f5c6c22a1582f 100644 --- a/Libraries/CameraRoll/CameraRoll.js +++ b/Libraries/CameraRoll/CameraRoll.js @@ -71,6 +71,11 @@ var getPhotosParamChecker = createStrictShapeTypeChecker({ * Specifies filter on asset type */ assetType: ReactPropTypes.oneOf(ASSET_TYPE_OPTIONS), + + /** + * Filter by mimetype (e.g. image/jpeg). + */ + mimeTypes: ReactPropTypes.arrayOf(ReactPropTypes.string), }); /** diff --git a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js index c229b60c321540..53390cabed3e6a 100644 --- a/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js +++ b/Libraries/Components/ActivityIndicatorIOS/ActivityIndicatorIOS.ios.js @@ -88,9 +88,11 @@ var styles = StyleSheet.create({ justifyContent: 'center', }, sizeSmall: { + width: 20, height: 20, }, sizeLarge: { + width: 36, height: 36, } }); diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 8952fb0d1e624a..512d8dbd6a4192 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -42,6 +42,14 @@ var INNERVIEW = 'InnerScrollView'; * Component that wraps platform ScrollView while providing * integration with touch locking "responder" system. * + * Keep in mind that ScrollViews must have a bounded height in order to work, + * since they contain unbounded-height children into a bounded container (via + * a scroll interaction). In order to bound the height of a ScrollView, either + * set the height of the view directly (discouraged) or make sure all parent + * views have bounded height. Forgetting to transfer `{flex: 1}` down the + * view stack can lead to errors here, which the element inspector makes + * easy to debug. + * * Doesn't yet support other contained responders from blocking this scroll * view from becoming the responder. */ diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 00a27e3c1d4901..a6949125406f54 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -44,12 +44,15 @@ var AndroidTextInputAttributes = { autoCapitalize: true, autoCorrect: true, autoFocus: true, + textAlign: true, + textAlignVertical: true, keyboardType: true, multiline: true, password: true, placeholder: true, text: true, testID: true, + underlineColorAndroid: true, }; var viewConfigAndroid = { @@ -68,8 +71,8 @@ type Event = Object; /** * 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 + * 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 @@ -123,6 +126,19 @@ var TextInput = React.createClass({ * If true, focuses the input on componentDidMount. Default value is false. */ autoFocus: PropTypes.bool, + /** + * Set the position of the cursor from where editing will begin. + */ + textAlign: PropTypes.oneOf([ + 'start', + 'center', + 'end', + ]), + textAlignVertical: PropTypes.oneOf([ + 'top', + 'center', + 'bottom', + ]), /** * If false, text is not editable. Default value is true. */ @@ -260,6 +276,10 @@ var TextInput = React.createClass({ * Used to locate this view in end-to-end tests. */ testID: PropTypes.string, + /** + * The color of the textInput underline. Is only supported on Android. + */ + underlineColorAndroid: PropTypes.string, }, /** @@ -461,6 +481,10 @@ var TextInput = React.createClass({ _renderAndroid: function() { var autoCapitalize = RCTUIManager.UIText.AutocapitalizationType[this.props.autoCapitalize]; + var textAlign = + RCTUIManager.AndroidTextInput.Constants.TextAlign[this.props.textAlign]; + var textAlignVertical = + RCTUIManager.AndroidTextInput.Constants.TextAlignVertical[this.props.textAlignVertical]; var children = this.props.children; var childCount = 0; ReactChildren.forEach(children, () => ++childCount); @@ -477,6 +501,8 @@ var TextInput = React.createClass({ style={[this.props.style]} autoCapitalize={autoCapitalize} autoCorrect={this.props.autoCorrect} + textAlign={textAlign} + textAlignVertical={textAlignVertical} keyboardType={this.props.keyboardType} multiline={this.props.multiline} onFocus={this._onFocus} @@ -489,6 +515,7 @@ var TextInput = React.createClass({ password={this.props.password || this.props.secureTextEntry} placeholder={this.props.placeholder} text={this.state.bufferedValue} + underlineColorAndroid={this.props.underlineColorAndroid} children={children} />; diff --git a/Libraries/Components/WebView/WebView.ios.js b/Libraries/Components/WebView/WebView.ios.js index 83af4a8adc3910..15ab9e676dec38 100644 --- a/Libraries/Components/WebView/WebView.ios.js +++ b/Libraries/Components/WebView/WebView.ios.js @@ -95,6 +95,11 @@ var WebView = React.createClass({ * Used for android only, JS is enabled by default for WebView on iOS */ javaScriptEnabledAndroid: PropTypes.bool, + /** + * Used for iOS only, sets whether the webpage scales to fit the view and the + * user can change the scale + */ + scalesPageToFit: PropTypes.bool, }, getInitialState: function() { @@ -155,6 +160,7 @@ var WebView = React.createClass({ onLoadingStart={this.onLoadingStart} onLoadingFinish={this.onLoadingFinish} onLoadingError={this.onLoadingError} + scalesPageToFit={this.props.scalesPageToFit} />; return ( diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 857f476c12f87d..dda32340c22fab 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -275,6 +275,10 @@ var ListView = React.createClass({ } }, + componentDidUpdate: function() { + this._measureAndUpdateScrollProps(); + }, + onRowHighlighted: function(sectionID, rowID) { this.setState({highlightedRow: {sectionID, rowID}}); }, @@ -368,7 +372,6 @@ var ListView = React.createClass({ if (!props.scrollEventThrottle) { props.scrollEventThrottle = DEFAULT_SCROLL_CALLBACK_THROTTLE; } - return ( diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js new file mode 100644 index 00000000000000..35ed24e3ad7aab --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationContext.js @@ -0,0 +1,96 @@ +/** + * 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 CustomComponents 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. + * + * @providesModule NavigationContext + */ +'use strict'; + +var NavigationEventEmitter = require('NavigationEventEmitter'); + +var emptyFunction = require('emptyFunction'); +var invariant = require('invariant'); + +import type * as NavigationEvent from 'NavigationEvent'; +import type * as EventSubscription from 'EventSubscription'; + +/** + * Class that contains the info and methods for app navigation. + */ +class NavigationContext { + _eventEmitter: ?NavigationEventEmitter; + _currentRoute: any; + + constructor() { + this._eventEmitter = new NavigationEventEmitter(this); + this._currentRoute = null; + this.addListener('willfocus', this._onFocus, this); + this.addListener('didfocus', this._onFocus, this); + } + + // TODO: @flow does not like this getter. Will add @flow check back once + // getter/setter is supported. + get currentRoute(): any { + return this._currentRoute; + } + + addListener( + eventType: string, + listener: Function, + context: ?Object + ): EventSubscription { + var emitter = this._eventEmitter; + if (emitter) { + return emitter.addListener(eventType, listener, context); + } else { + return {remove: emptyFunction}; + } + } + + emit(eventType: String, data: any): void { + var emitter = this._eventEmitter; + if (emitter) { + emitter.emit(eventType, data); + } + } + + dispose(): void { + var emitter = this._eventEmitter; + if (emitter) { + // clean up everything. + emitter.removeAllListeners(); + this._eventEmitter = null; + this._currentRoute = null; + } + } + + _onFocus(event: NavigationEvent): void { + invariant( + event.data && event.data.hasOwnProperty('route'), + 'didfocus event should provide route' + ); + this._currentRoute = event.data.route; + } +} + +module.exports = NavigationContext; diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js new file mode 100644 index 00000000000000..b6923b4f2b0549 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js @@ -0,0 +1,21 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule NavigationEvent + * @flow + */ +'use strict'; + +class NavigationEvent { + type: String; + target: Object; + data: any; + + constructor(type: String, target: Object, data: any) { + this.type = type; + this.target = target; + this.data = data; + } +} + +module.exports = NavigationEvent; diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js new file mode 100644 index 00000000000000..db9e785540f164 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationEventEmitter.js @@ -0,0 +1,70 @@ +/** + * 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 CustomComponents 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. + * + * @providesModule NavigationEventEmitter + * @flow + */ +'use strict'; + +var EventEmitter = require('EventEmitter'); +var NavigationEvent = require('NavigationEvent'); + +type EventParams = { + eventType: String; + data: any; +}; + +class NavigationEventEmitter extends EventEmitter { + _emitQueue: Array; + _emitting: boolean; + _target: Object; + + constructor(target: Object) { + super(); + this._emitting = false; + this._emitQueue = []; + this._target = target; + } + + emit(eventType: String, data: any): void { + if (this._emitting) { + // An event cycle that was previously created hasn't finished yet. + // Put this event cycle into the queue and will finish them later. + this._emitQueue.push({eventType, data}); + return; + } + + this._emitting = true; + var event = new NavigationEvent(eventType, this._target, data); + super.emit(eventType, event); + this._emitting = false; + + while (this._emitQueue.length) { + var arg = this._emitQueue.shift(); + this.emit(arg.eventType, arg.data); + } + } +} + +module.exports = NavigationEventEmitter; diff --git a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js new file mode 100644 index 00000000000000..796d5633e64bea --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationContext-test.js @@ -0,0 +1,51 @@ +/** + * 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 CustomComponents 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. + */ +'use strict'; + +jest + .dontMock('EmitterSubscription') + .dontMock('EventEmitter') + .dontMock('EventSubscriptionVendor') + .dontMock('NavigationContext') + .dontMock('NavigationEvent') + .dontMock('NavigationEventEmitter') + .dontMock('invariant'); + +var NavigationContext = require('NavigationContext'); + +describe('NavigationContext', () => { + it('defaults `currentRoute` to null', () => { + var context = new NavigationContext(); + expect(context.currentRoute).toEqual(null); + }); + + it('updates `currentRoute`', () => { + var context = new NavigationContext(); + context.emit('didfocus', {route: {name: 'a'}}); + expect(context.currentRoute.name).toEqual('a'); + }); +}); + + diff --git a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js new file mode 100644 index 00000000000000..2a8d7d82a73cf0 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEventEmitter-test.js @@ -0,0 +1,80 @@ +/** + * 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 CustomComponents 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. + */ +'use strict'; + +jest + .dontMock('EmitterSubscription') + .dontMock('EventEmitter') + .dontMock('EventSubscriptionVendor') + .dontMock('NavigationEvent') + .dontMock('NavigationEventEmitter'); + +var NavigationEventEmitter = require('NavigationEventEmitter'); + +describe('NavigationEventEmitter', () => { + it('emit event', () => { + var target = {}; + var emitter = new NavigationEventEmitter(target); + var focusCounter = 0; + var focusTarget; + + emitter.addListener('focus', (event) => { + focusCounter++; + focusTarget = event.target; + }); + + emitter.emit('focus'); + emitter.emit('blur'); + + expect(focusCounter).toBe(1); + expect(focusTarget).toBe(target); + }); + + it('put nested emit call in queue', () => { + var target = {}; + var emitter = new NavigationEventEmitter(target); + var logs = []; + + emitter.addListener('one', () => { + logs.push(1); + emitter.emit('two'); + logs.push(2); + }); + + emitter.addListener('two', () => { + logs.push(3); + emitter.emit('three'); + logs.push(4); + }); + + emitter.addListener('three', () => { + logs.push(5); + }); + + emitter.emit('one'); + + expect(logs).toEqual([1, 2, 3, 4, 5]); + }); +}); diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index af10348fcac51e..d9e452d22d9db1 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -30,11 +30,11 @@ var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule; var Dimensions = require('Dimensions'); var InteractionMixin = require('InteractionMixin'); +var NavigationContext = require('NavigationContext'); var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar'); var NavigatorNavigationBar = require('NavigatorNavigationBar'); var NavigatorSceneConfigs = require('NavigatorSceneConfigs'); var PanResponder = require('PanResponder'); -var Platform = require('Platform'); var React = require('React'); var StaticContainer = require('StaticContainer.react'); var StyleSheet = require('StyleSheet'); @@ -203,11 +203,17 @@ var Navigator = React.createClass({ initialRouteStack: PropTypes.arrayOf(PropTypes.object), /** + * @deprecated + * Use `navigationContext.addListener('willfocus', callback)` instead. + * * Will emit the target route upon mounting and before each nav transition */ onWillFocus: PropTypes.func, /** + * @deprecated + * Use `navigationContext.addListener('didfocus', callback)` instead. + * * Will be called with the new route of each scene after the transition is * complete or after the initial mounting */ @@ -283,6 +289,9 @@ var Navigator = React.createClass({ }, componentWillMount: function() { + // TODO(t7489503): Don't need this once ES6 Class landed. + this.__defineGetter__('navigationContext', this._getNavigationContext); + this._subRouteFocus = []; this.parentNavigator = this.props.navigator; this._handlers = {}; @@ -321,7 +330,10 @@ var Navigator = React.createClass({ }, componentWillUnmount: function() { - + if (this._navigationContext) { + this._navigationContext.dispose(); + this._navigationContext = null; + } }, /** @@ -400,13 +412,11 @@ var Navigator = React.createClass({ ); } else if (this.state.activeGesture != null) { var presentedToIndex = this.state.presentedIndex + this._deltaForGestureAction(this.state.activeGesture); - if (presentedToIndex > -1) { - this._transitionBetween( - this.state.presentedIndex, - presentedToIndex, - this.spring.getCurrentValue() - ); - } + this._transitionBetween( + this.state.presentedIndex, + presentedToIndex, + this.spring.getCurrentValue() + ); } }, @@ -461,12 +471,16 @@ var Navigator = React.createClass({ }, _emitDidFocus: function(route) { + this.navigationContext.emit('didfocus', {route: route}); + if (this.props.onDidFocus) { this.props.onDidFocus(route); } }, _emitWillFocus: function(route) { + this.navigationContext.emit('willfocus', {route: route}); + var navBar = this._navBar; if (navBar && navBar.handleWillFocus) { navBar.handleWillFocus(route); @@ -806,7 +820,7 @@ var Navigator = React.createClass({ this._transitionSceneStyle(fromIndex, toIndex, progress, fromIndex); this._transitionSceneStyle(fromIndex, toIndex, progress, toIndex); var navBar = this._navBar; - if (navBar && navBar.updateProgress) { + if (navBar && navBar.updateProgress && toIndex >= 0 && fromIndex >= 0) { navBar.updateProgress(progress, fromIndex, toIndex); } }, @@ -1139,6 +1153,13 @@ var Navigator = React.createClass({ ); }, + + _getNavigationContext: function() { + if (!this._navigationContext) { + this._navigationContext = new NavigationContext(); + } + return this._navigationContext; + } }); module.exports = Navigator; diff --git a/Libraries/Image/Image.ios.js b/Libraries/Image/Image.ios.js index 23e2c7a6372bc5..7a629ce9a60c48 100644 --- a/Libraries/Image/Image.ios.js +++ b/Libraries/Image/Image.ios.js @@ -178,7 +178,6 @@ var nativeOnlyProps = { src: true, defaultImageSrc: true, imageTag: true, - resizeMode: true, }; if (__DEV__) { verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps); diff --git a/Libraries/Image/ImagePickerIOS.js b/Libraries/Image/ImagePickerIOS.js new file mode 100644 index 00000000000000..9b2f75e5b92d9a --- /dev/null +++ b/Libraries/Image/ImagePickerIOS.js @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ImagePickerIOS + * @flow + */ +'use strict'; + +var RCTImagePicker = require('NativeModules').ImagePickerIOS; + +var ImagePickerIOS = { + canRecordVideos: function(callback: Function) { + return RCTImagePicker.canRecordVideos(callback); + }, + canUseCamera: function(callback: Function) { + return RCTImagePicker.canUseCamera(callback); + }, + openCameraDialog: function(config: Object, successCallback: Function, cancelCallback: Function) { + config = { + videoMode: false, + ...config, + } + return RCTImagePicker.openCameraDialog(config, successCallback, cancelCallback); + }, + openSelectDialog: function(config: Object, successCallback: Function, cancelCallback: Function) { + config = { + showImages: true, + showVideos: false, + ...config, + } + return RCTImagePicker.openSelectDialog(config, successCallback, cancelCallback); + }, +}; + +module.exports = ImagePickerIOS; diff --git a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj index 9e5427bf30a4a8..1e3cf75c894e05 100644 --- a/Libraries/Image/RCTImage.xcodeproj/project.pbxproj +++ b/Libraries/Image/RCTImage.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5AA1AA8C4A30002E2BE /* RCTStaticImageManager.m */; }; 1304D5B21AA8C50D0002E2BE /* RCTGIFImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */; }; 1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1345A8381B26592900583190 /* RCTImageRequestHandler.m */; }; + 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 137620341B31C53500677FF0 /* RCTImagePickerManager.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 */; }; @@ -39,6 +40,8 @@ 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTGIFImage.m; sourceTree = ""; }; 1345A8371B26592900583190 /* RCTImageRequestHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImageRequestHandler.h; sourceTree = ""; }; 1345A8381B26592900583190 /* RCTImageRequestHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImageRequestHandler.m; sourceTree = ""; }; + 137620331B31C53500677FF0 /* RCTImagePickerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTImagePickerManager.h; sourceTree = ""; }; + 137620341B31C53500677FF0 /* RCTImagePickerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTImagePickerManager.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 = ""; }; @@ -74,6 +77,8 @@ 1304D5B11AA8C50D0002E2BE /* RCTGIFImage.m */, 58B511891A9E6BD600147676 /* RCTImageDownloader.h */, 58B5118A1A9E6BD600147676 /* RCTImageDownloader.m */, + 137620331B31C53500677FF0 /* RCTImagePickerManager.h */, + 137620341B31C53500677FF0 /* RCTImagePickerManager.m */, 1345A8371B26592900583190 /* RCTImageRequestHandler.h */, 1345A8381B26592900583190 /* RCTImageRequestHandler.m */, 58B5118B1A9E6BD600147676 /* RCTNetworkImageView.h */, @@ -155,6 +160,7 @@ buildActionMask = 2147483647; files = ( 58B5118F1A9E6BD600147676 /* RCTImageDownloader.m in Sources */, + 137620351B31C53500677FF0 /* RCTImagePickerManager.m in Sources */, 58B511911A9E6BD600147676 /* RCTNetworkImageViewManager.m in Sources */, 1304D5AC1AA8C4A30002E2BE /* RCTStaticImageManager.m in Sources */, 1345A8391B26592900583190 /* RCTImageRequestHandler.m in Sources */, diff --git a/Libraries/Image/RCTImagePickerManager.h b/Libraries/Image/RCTImagePickerManager.h new file mode 100644 index 00000000000000..a008c46f37a022 --- /dev/null +++ b/Libraries/Image/RCTImagePickerManager.h @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2013, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "RCTBridgeModule.h" + +@interface RCTImagePickerManager : NSObject + +@end diff --git a/Libraries/Image/RCTImagePickerManager.m b/Libraries/Image/RCTImagePickerManager.m new file mode 100644 index 00000000000000..7fad953b0d6a5a --- /dev/null +++ b/Libraries/Image/RCTImagePickerManager.m @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2013, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "RCTImagePickerManager.h" +#import "RCTRootView.h" + +#import + +#import + +@interface RCTImagePickerManager () + +@end + +@implementation RCTImagePickerManager +{ + NSMutableArray *_pickers; + NSMutableArray *_pickerCallbacks; + NSMutableArray *_pickerCancelCallbacks; +} + +RCT_EXPORT_MODULE(ImagePickerIOS); + +- (instancetype)init +{ + if ((self = [super init])) { + _pickers = [[NSMutableArray alloc] init]; + _pickerCallbacks = [[NSMutableArray alloc] init]; + _pickerCancelCallbacks = [[NSMutableArray alloc] init]; + } + return self; +} + +RCT_EXPORT_METHOD(canRecordVideos:(RCTResponseSenderBlock)callback) +{ + NSArray *availableMediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; + callback(@[@([availableMediaTypes containsObject:(NSString *)kUTTypeMovie])]); +} + +RCT_EXPORT_METHOD(canUseCamera:(RCTResponseSenderBlock)callback) +{ + callback(@[@([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])]); +} + +RCT_EXPORT_METHOD(openCameraDialog:(NSDictionary *)config + successCallback:(RCTResponseSenderBlock)callback + cancelCallback:(RCTResponseSenderBlock)cancelCallback) +{ + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIViewController *rootViewController = keyWindow.rootViewController; + + UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; + imagePicker.delegate = self; + imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera; + + if ([config[@"videoMode"] boolValue]) { + imagePicker.cameraCaptureMode = UIImagePickerControllerCameraCaptureModeVideo; + } + + [_pickers addObject:imagePicker]; + [_pickerCallbacks addObject:callback]; + [_pickerCancelCallbacks addObject:cancelCallback]; + + [rootViewController presentViewController:imagePicker animated:YES completion:nil]; +} + +RCT_EXPORT_METHOD(openSelectDialog:(NSDictionary *)config + successCallback:(RCTResponseSenderBlock)callback + cancelCallback:(RCTResponseSenderBlock)cancelCallback) +{ + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIViewController *rootViewController = keyWindow.rootViewController; + + UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init]; + imagePicker.delegate = self; + imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; + + NSMutableArray *allowedTypes = [[NSMutableArray alloc] init]; + if ([config[@"showImages"] boolValue]) { + [allowedTypes addObject:(NSString *)kUTTypeImage]; + } + if ([config[@"showVideos"] boolValue]) { + [allowedTypes addObject:(NSString *)kUTTypeMovie]; + } + + imagePicker.mediaTypes = allowedTypes; + + [_pickers addObject:imagePicker]; + [_pickerCallbacks addObject:callback]; + [_pickerCancelCallbacks addObject:cancelCallback]; + + [rootViewController presentViewController:imagePicker animated:YES completion:nil]; +} + +- (void)imagePickerController:(UIImagePickerController *)picker +didFinishPickingMediaWithInfo:(NSDictionary *)info +{ + NSUInteger index = [_pickers indexOfObject:picker]; + RCTResponseSenderBlock callback = _pickerCallbacks[index]; + + [_pickers removeObjectAtIndex:index]; + [_pickerCallbacks removeObjectAtIndex:index]; + [_pickerCancelCallbacks removeObjectAtIndex:index]; + + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIViewController *rootViewController = keyWindow.rootViewController; + [rootViewController dismissViewControllerAnimated:YES completion:nil]; + + callback(@[[info[UIImagePickerControllerReferenceURL] absoluteString]]); +} + +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker +{ + NSUInteger index = [_pickers indexOfObject:picker]; + RCTResponseSenderBlock callback = _pickerCancelCallbacks[index]; + + [_pickers removeObjectAtIndex:index]; + [_pickerCallbacks removeObjectAtIndex:index]; + [_pickerCancelCallbacks removeObjectAtIndex:index]; + + UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow]; + UIViewController *rootViewController = keyWindow.rootViewController; + [rootViewController dismissViewControllerAnimated:YES completion:nil]; + + callback(@[]); +} + +@end diff --git a/Libraries/Inspector/Inspector.js b/Libraries/Inspector/Inspector.js index 46615d967b1902..fa6e20e08eb15e 100644 --- a/Libraries/Inspector/Inspector.js +++ b/Libraries/Inspector/Inspector.js @@ -66,14 +66,9 @@ class Inspector extends React.Component { } render() { - var panelPosition; - if (this.state.panelPos === 'bottom') { - panelPosition = {bottom: -Dimensions.get('window').height}; - } else { - panelPosition = {top: 0}; - } + var panelContainerStyle = (this.state.panelPos === 'bottom') ? {bottom: 0} : {top: 0}; return ( - + {this.state.inspecting && } - + +@interface RCTNetworking : NSObject @end diff --git a/Libraries/Network/RCTDataManager.m b/Libraries/Network/RCTNetworking.m similarity index 97% rename from Libraries/Network/RCTDataManager.m rename to Libraries/Network/RCTNetworking.m index 49ce6d754ec7bd..b4ae07c1a3fa70 100644 --- a/Libraries/Network/RCTDataManager.m +++ b/Libraries/Network/RCTNetworking.m @@ -7,7 +7,7 @@ * of patent rights can be found in the PATENTS file in the same directory. */ -#import "RCTDataManager.h" +#import "RCTNetworking.h" #import "RCTAssert.h" #import "RCTConvert.h" @@ -19,7 +19,7 @@ typedef void (^RCTHTTPQueryResult)(NSError *error, NSDictionary *result); -@interface RCTDataManager () +@interface RCTNetworking () - (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError *error, NSDictionary *result))callback; @@ -30,7 +30,7 @@ - (void)processDataForHTTPQuery:(NSDictionary *)data callback:(void (^)(NSError */ @interface RCTHTTPFormDataHelper : NSObject -@property (nonatomic, weak) RCTDataManager *dataManager; +@property (nonatomic, weak) RCTNetworking *dataManager; @end @@ -207,14 +207,14 @@ - (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error /** * Bridge module that provides the JS interface to the network stack. */ -@implementation RCTDataManager +@implementation RCTNetworking { NSInteger _currentRequestID; NSMapTable *_activeRequests; - dispatch_queue_t _methodQueue; } @synthesize bridge = _bridge; +@synthesize methodQueue = _methodQueue; RCT_EXPORT_MODULE() @@ -222,7 +222,6 @@ - (instancetype)init { if ((self = [super init])) { _currentRequestID = 0; - _methodQueue = dispatch_queue_create("com.facebook.React.RCTDataManager", DISPATCH_QUEUE_SERIAL); _activeRequests = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsStrongMemory capacity:0]; @@ -230,11 +229,6 @@ - (instancetype)init return self; } -- (dispatch_queue_t)methodQueue -{ - return _methodQueue; -} - - (void)buildRequest:(NSDictionary *)query responseSender:(RCTResponseSenderBlock)responseSender { diff --git a/Libraries/Network/XMLHttpRequest.ios.js b/Libraries/Network/XMLHttpRequest.ios.js index 151781c91591dd..6eb586c2692499 100644 --- a/Libraries/Network/XMLHttpRequest.ios.js +++ b/Libraries/Network/XMLHttpRequest.ios.js @@ -12,7 +12,7 @@ 'use strict'; var FormData = require('FormData'); -var RCTDataManager = require('NativeModules').DataManager; +var RCTNetworking = require('NativeModules').Networking; var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); var XMLHttpRequestBase = require('XMLHttpRequestBase'); @@ -89,7 +89,7 @@ class XMLHttpRequest extends XMLHttpRequestBase { if (data instanceof FormData) { data = {formData: data.getParts()}; } - RCTDataManager.sendRequest( + RCTNetworking.sendRequest( { method, url, @@ -103,7 +103,7 @@ class XMLHttpRequest extends XMLHttpRequestBase { abortImpl(): void { if (this._requestId) { - RCTDataManager.cancelRequest(this._requestId); + RCTNetworking.cancelRequest(this._requestId); this._clearSubscriptions(); this._requestId = null; } diff --git a/Libraries/Network/XMLHttpRequestBase.js b/Libraries/Network/XMLHttpRequestBase.js index 9d06f486a6143e..f4d106b72aa45e 100644 --- a/Libraries/Network/XMLHttpRequestBase.js +++ b/Libraries/Network/XMLHttpRequestBase.js @@ -143,7 +143,7 @@ class XMLHttpRequestBase { return; } this.status = status; - this.setResponseHeaders(responseHeaders); + this.setResponseHeaders(responseHeaders || {}); this.responseText = responseText; this.setReadyState(this.DONE); } diff --git a/Libraries/RCTTest/RCTTestModule.h b/Libraries/RCTTest/RCTTestModule.h index f248cbfca40818..5ea69dcb6ffc5e 100644 --- a/Libraries/RCTTest/RCTTestModule.h +++ b/Libraries/RCTTest/RCTTestModule.h @@ -12,6 +12,12 @@ #import "RCTBridgeModule.h" #import "RCTDefines.h" +typedef NS_ENUM(NSInteger, RCTTestStatus) { + RCTTestStatusPending = 0, + RCTTestStatusPassed, + RCTTestStatusFailed +}; + @class FBSnapshotTestController; @interface RCTTestModule : NSObject @@ -32,8 +38,8 @@ @property (nonatomic, assign) SEL testSelector; /** - * This is typically polled while running the runloop until true. + * This is polled while running the runloop until true. */ -@property (nonatomic, readonly, getter=isDone) BOOL done; +@property (nonatomic, readonly) RCTTestStatus status; @end diff --git a/Libraries/RCTTest/RCTTestModule.m b/Libraries/RCTTest/RCTTestModule.m index f7d504b06103ad..54c44513e3206e 100644 --- a/Libraries/RCTTest/RCTTestModule.m +++ b/Libraries/RCTTest/RCTTestModule.m @@ -51,16 +51,7 @@ - (instancetype)init selector:_testSelector identifier:_snapshotCounter[testName] error:&error]; - - RCTAssert(success, @"Snapshot comparison failed: %@", error); - callback(@[]); - }]; -} - -RCT_EXPORT_METHOD(markTestCompleted) -{ - [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - _done = YES; + callback(@[@(success)]); }]; } @@ -79,11 +70,16 @@ - (instancetype)init reject(nil); } -RCT_EXPORT_METHOD(finish:(BOOL)success) +RCT_EXPORT_METHOD(markTestCompleted) { - RCTAssert(success, @"RCTTestModule finished without success"); - [self markTestCompleted]; + [self markTestPassed:YES]; } +RCT_EXPORT_METHOD(markTestPassed:(BOOL)success) +{ + [_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + _status = success ? RCTTestStatusPassed : RCTTestStatusFailed; + }]; +} @end diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 11c57e0babd19f..0ab8c0555d7a53 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -16,7 +16,7 @@ #import "RCTTestModule.h" #import "RCTUtils.h" -#define TIMEOUT_SECONDS 240 +#define TIMEOUT_SECONDS 60 @interface RCTBridge (RCTTestRunner) @@ -93,7 +93,7 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictiona NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; NSString *error = [[RCTRedBox sharedInstance] currentErrorMessage]; - while ([date timeIntervalSinceNow] > 0 && ![testModule isDone] && error == nil) { + while ([date timeIntervalSinceNow] > 0 && testModule.status == RCTTestStatusPending && error == nil) { [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; error = [[RCTRedBox sharedInstance] currentErrorMessage]; @@ -104,11 +104,12 @@ - (void)runTest:(SEL)test module:(NSString *)moduleName initialProps:(NSDictiona [[RCTRedBox sharedInstance] dismiss]; if (expectErrorBlock) { RCTAssert(expectErrorBlock(error), @"Expected an error but nothing matched."); - } else if (error) { - RCTAssert(error == nil, @"RedBox error: %@", error); } else { - RCTAssert([testModule isDone], @"Test didn't finish within %d seconds", TIMEOUT_SECONDS); + RCTAssert(error == nil, @"RedBox error: %@", error); + RCTAssert(testModule.status != RCTTestStatusPending, @"Test didn't finish within %d seconds", TIMEOUT_SECONDS); + RCTAssert(testModule.status == RCTTestStatusPassed, @"Test failed"); } + RCTAssert(self.recordMode == NO, @"Don't forget to turn record mode back to NO before commit."); } @end diff --git a/Libraries/ReactNative/ReactNativeBaseComponent.js b/Libraries/ReactNative/ReactNativeBaseComponent.js index 95af2902353007..84baf67535ea12 100644 --- a/Libraries/ReactNative/ReactNativeBaseComponent.js +++ b/Libraries/ReactNative/ReactNativeBaseComponent.js @@ -251,7 +251,14 @@ ReactNativeBaseComponent.Mixin = { this._currentElement.props, // next props this.viewConfig.validAttributes ); - RCTUIManager.createView(tag, this.viewConfig.uiViewClassName, updatePayload); + + var nativeTopRootID = ReactNativeTagHandles.getNativeTopRootIDFromNodeID(rootID); + RCTUIManager.createView( + tag, + this.viewConfig.uiViewClassName, + nativeTopRootID ? ReactNativeTagHandles.rootNodeIDToTag[nativeTopRootID] : null, + updatePayload + ); this._registerListenersUponCreation(this._currentElement.props); this.initializeChildren( diff --git a/Libraries/ReactNative/ReactNativeGlobalResponderHandler.js b/Libraries/ReactNative/ReactNativeGlobalResponderHandler.js index 3ba933e8ce30f4..1f548c3ebcc2e0 100644 --- a/Libraries/ReactNative/ReactNativeGlobalResponderHandler.js +++ b/Libraries/ReactNative/ReactNativeGlobalResponderHandler.js @@ -15,10 +15,11 @@ var RCTUIManager = require('NativeModules').UIManager; var ReactNativeTagHandles = require('ReactNativeTagHandles'); var ReactNativeGlobalResponderHandler = { - onChange: function(from: string, to: string) { + onChange: function(from: string, to: string, blockNativeResponder: boolean) { if (to !== null) { RCTUIManager.setJSResponder( - ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID(to) + ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID(to), + blockNativeResponder ); } else { RCTUIManager.clearJSResponder(); diff --git a/Libraries/ReactNative/ReactNativeTagHandles.js b/Libraries/ReactNative/ReactNativeTagHandles.js index bf1dc59f25736a..ab350817c6e90c 100644 --- a/Libraries/ReactNative/ReactNativeTagHandles.js +++ b/Libraries/ReactNative/ReactNativeTagHandles.js @@ -28,6 +28,7 @@ var warning = require('warning'); * unmount a component with a `rootNodeID`, then mount a new one in its place, */ var INITIAL_TAG_COUNT = 1; +var NATIVE_TOP_ROOT_ID_SEPARATOR = '{TOP_LEVEL}'; var ReactNativeTagHandles = { tagsStartAt: INITIAL_TAG_COUNT, tagCount: INITIAL_TAG_COUNT, @@ -67,7 +68,7 @@ var ReactNativeTagHandles = { this.reactTagIsNativeTopRootID(tag), 'Expect a native root tag, instead got ', tag ); - return '.r[' + tag + ']{TOP_LEVEL}'; + return '.r[' + tag + ']' + NATIVE_TOP_ROOT_ID_SEPARATOR; }, reactTagIsNativeTopRootID: function(reactTag: number): bool { @@ -75,6 +76,17 @@ var ReactNativeTagHandles = { return reactTag % 10 === 1; }, + getNativeTopRootIDFromNodeID: function(nodeID: ?string): ?string { + if (!nodeID) { + return null; + } + var index = nodeID.indexOf(NATIVE_TOP_ROOT_ID_SEPARATOR); + if (index === -1) { + return null; + } + return nodeID.substr(0, index + NATIVE_TOP_ROOT_ID_SEPARATOR.length); + }, + /** * Returns the native `nodeHandle` (`tag`) that was most recently *natively* * mounted at the `rootNodeID`. Just because a React component has been diff --git a/Libraries/ReactNative/ReactNativeTextComponent.js b/Libraries/ReactNative/ReactNativeTextComponent.js index bdca141c06424f..bb93b9c34f0ba8 100644 --- a/Libraries/ReactNative/ReactNativeTextComponent.js +++ b/Libraries/ReactNative/ReactNativeTextComponent.js @@ -32,7 +32,13 @@ assign(ReactNativeTextComponent.prototype, { mountComponent: function(rootID, transaction, context) { this._rootNodeID = rootID; var tag = ReactNativeTagHandles.allocateTag(); - RCTUIManager.createView(tag, 'RCTRawText', {text: this._stringText}); + var nativeTopRootID = ReactNativeTagHandles.getNativeTopRootIDFromNodeID(rootID); + RCTUIManager.createView( + tag, + 'RCTRawText', + nativeTopRootID ? ReactNativeTagHandles.rootNodeIDToTag[nativeTopRootID] : null, + {text: this._stringText} + ); return { rootNodeID: rootID, tag: tag, diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h index d156bb4d60a197..4343110bf5e41f 100644 --- a/Libraries/Text/RCTShadowText.h +++ b/Libraries/Text/RCTShadowText.h @@ -25,7 +25,6 @@ extern NSString *const RCTReactTagAttributeName; @property (nonatomic, assign) NSUInteger numberOfLines; @property (nonatomic, assign) CGSize shadowOffset; @property (nonatomic, assign) NSTextAlignment textAlign; -@property (nonatomic, strong) UIColor *textBackgroundColor; @property (nonatomic, assign) NSWritingDirection writingDirection; - (void)recomputeText; diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index 65bee774e06e65..e970249287982d 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -131,7 +131,8 @@ - (NSAttributedString *)attributedString fontSize:nil fontWeight:nil fontStyle:nil - letterSpacing:nil]; + letterSpacing:nil + useBackgroundColor:NO]; } - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily @@ -139,6 +140,7 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily fontWeight:(NSString *)fontWeight fontStyle:(NSString *)fontStyle letterSpacing:(NSNumber *)letterSpacing + useBackgroundColor:(BOOL)useBackgroundColor { if (![self isTextDirty] && _cachedAttributedString) { return _cachedAttributedString; @@ -166,7 +168,7 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily for (RCTShadowView *child in [self reactSubviews]) { if ([child isKindOfClass:[RCTShadowText class]]) { RCTShadowText *shadowText = (RCTShadowText *)child; - [attributedString appendAttributedString:[shadowText _attributedStringWithFontFamily:fontFamily fontSize:fontSize fontWeight:fontWeight fontStyle:fontStyle letterSpacing:letterSpacing]]; + [attributedString appendAttributedString:[shadowText _attributedStringWithFontFamily:fontFamily fontSize:fontSize fontWeight:fontWeight fontStyle:fontStyle letterSpacing:letterSpacing useBackgroundColor:YES]]; } else if ([child isKindOfClass:[RCTShadowRawText class]]) { RCTShadowRawText *shadowRawText = (RCTShadowRawText *)child; [attributedString appendAttributedString:[[NSAttributedString alloc] initWithString:[shadowRawText text] ?: @""]]; @@ -183,8 +185,8 @@ - (NSAttributedString *)_attributedStringWithFontFamily:(NSString *)fontFamily if (_isHighlighted) { [self _addAttribute:RCTIsHighlightedAttributeName withValue:@YES toAttributedString:attributedString]; } - if (_textBackgroundColor) { - [self _addAttribute:NSBackgroundColorAttributeName withValue:_textBackgroundColor toAttributedString:attributedString]; + if (useBackgroundColor && self.backgroundColor) { + [self _addAttribute:NSBackgroundColorAttributeName withValue:self.backgroundColor toAttributedString:attributedString]; } UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily size:fontSize weight:fontWeight style:fontStyle]; @@ -271,6 +273,12 @@ - (void)removeReactSubview:(RCTShadowView *)subview [self cssNode]->children_count = 0; } +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + super.backgroundColor = backgroundColor; + [self dirtyText]; +} + #define RCT_TEXT_PROPERTY(setProp, ivar, type) \ - (void)set##setProp:(type)value; \ { \ @@ -289,7 +297,6 @@ - (void)set##setProp:(type)value; \ RCT_TEXT_PROPERTY(NumberOfLines, _numberOfLines, NSUInteger) RCT_TEXT_PROPERTY(ShadowOffset, _shadowOffset, CGSize) RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment) -RCT_TEXT_PROPERTY(TextBackgroundColor, _textBackgroundColor, UIColor *) RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection) @end diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 1ae432d90e4c99..9dc8e42000eb11 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -34,6 +34,14 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (NSString *)description +{ + NSString *superDescription = super.description; + NSRange semicolonRange = [superDescription rangeOfString:@";"]; + NSString *replacement = [NSString stringWithFormat:@"; reactTag: %@; text: %@", self.reactTag, self.textStorage.string]; + return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement]; +} + - (void)reactSetFrame:(CGRect)frame { // Text looks super weird if its frame is animated. diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 26c6329e2313ec..3d2c737598de17 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -32,11 +32,6 @@ - (RCTShadowView *)shadowView return [[RCTShadowText alloc] init]; } -#pragma mark - View properties - -RCT_IGNORE_VIEW_PROPERTY(backgroundColor); -RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor, UIColor) - #pragma mark - Shadow properties RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection) @@ -50,8 +45,6 @@ - (RCTShadowView *)shadowView RCT_EXPORT_SHADOW_PROPERTY(lineHeight, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(shadowOffset, CGSize) RCT_EXPORT_SHADOW_PROPERTY(textAlign, NSTextAlignment) -RCT_REMAP_SHADOW_PROPERTY(backgroundColor, textBackgroundColor, UIColor) -RCT_REMAP_SHADOW_PROPERTY(containerBackgroundColor, backgroundColor, UIColor) RCT_EXPORT_SHADOW_PROPERTY(numberOfLines, NSUInteger) - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js index 7ff16c5d9ec270..d02733749bebd7 100644 --- a/Libraries/Text/Text.js +++ b/Libraries/Text/Text.js @@ -97,7 +97,7 @@ var Text = React.createClass({ /** * Invoked on mount and layout changes with * - * {nativeEvent: { layout: {x, y, width, height}}}. + * {nativeEvent: {layout: {x, y, width, height}}}. */ onLayout: React.PropTypes.func, }, diff --git a/Libraries/Text/TextStylePropTypes.js b/Libraries/Text/TextStylePropTypes.js index 450d26f3378fa3..b903675093c89b 100644 --- a/Libraries/Text/TextStylePropTypes.js +++ b/Libraries/Text/TextStylePropTypes.js @@ -25,8 +25,7 @@ var TextStylePropTypes = Object.assign(Object.create(ViewStylePropTypes), { fontStyle: ReactPropTypes.oneOf(['normal', 'italic']), lineHeight: ReactPropTypes.number, color: ReactPropTypes.string, - containerBackgroundColor: ReactPropTypes.string, - // NOTE: "justify" is supported only on iOS + // NOTE: 'justify is supported only on iOS textAlign: ReactPropTypes.oneOf( ['auto' /*default*/, 'left', 'right', 'center', 'justify'] ), diff --git a/Libraries/Utilities/BridgeProfiling.js b/Libraries/Utilities/BridgeProfiling.js index 02685e01cb8c76..1b800901fc36ef 100644 --- a/Libraries/Utilities/BridgeProfiling.js +++ b/Libraries/Utilities/BridgeProfiling.js @@ -14,7 +14,7 @@ var GLOBAL = GLOBAL || this; var BridgeProfiling = { - profile(profileName?: string, args?: any) { + profile(profileName?: any, args?: any) { if (GLOBAL.__BridgeProfilingIsProfiling) { if (args) { try { @@ -23,6 +23,8 @@ var BridgeProfiling = { args = err.message; } } + profileName = typeof profileName === 'function' ? + profileName() : profileName; console.profile(profileName, args); } }, diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index cac48a8e53901f..589ee5e9ada077 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -7,541 +7,241 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule MessageQueue - * @flow */ -'use strict'; - -var ErrorUtils = require('ErrorUtils'); -var ReactUpdates = require('ReactUpdates'); - -var invariant = require('invariant'); -var warning = require('warning'); - -var BridgeProfiling = require('BridgeProfiling'); -var JSTimersExecution = require('JSTimersExecution'); -var INTERNAL_ERROR = 'Error in MessageQueue implementation'; +/*eslint no-bitwise: 0*/ -// Prints all bridge traffic to console.log -var DEBUG_SPY_MODE = false; +'use strict'; -type ModulesConfig = { - [key:string]: { - moduleID: number; - methods: {[key:string]: { - methodID: number; - }}; - } -} +let BridgeProfiling = require('BridgeProfiling'); +let ErrorUtils = require('ErrorUtils'); +let JSTimersExecution = require('JSTimersExecution'); +let ReactUpdates = require('ReactUpdates'); -type NameToID = {[key:string]: number} -type IDToName = {[key:number]: string} +let invariant = require('invariant'); +let keyMirror = require('keyMirror'); +let stringifySafe = require('stringifySafe'); -/** - * So as not to confuse static build system. - */ -var requireFunc = require; +let MODULE_IDS = 0; +let METHOD_IDS = 1; +let PARAMS = 2; -/** - * @param {Object!} module Module instance, must be loaded. - * @param {string} methodName Name of method in `module`. - * @param {array<*>} params Arguments to method. - * @returns {*} Return value of method invocation. - */ -var jsCall = function(module, methodName, params) { - return module[methodName].apply(module, params); -}; +let MethodTypes = keyMirror({ + local: null, + remote: null, + remoteAsync: null, +}); -/** - * A utility for aggregating "work" to be done, and potentially transferring - * that work to another thread. Each instance of `MessageQueue` has the notion - * of a "target" thread - the thread that the work will be sent to. - * - * TODO: Long running callback results, and streaming callback results (ability - * for a callback to be invoked multiple times). - * - * @param {object} moduleNameToID Used to translate module/method names into - * efficient numeric IDs. - * @class MessageQueue - */ -var MessageQueue = function( - remoteModulesConfig: ModulesConfig, - localModulesConfig: ModulesConfig, - customRequire: (id: string) => any -) { - this._requireFunc = customRequire || requireFunc; - this._initBookeeping(); - this._initNamingMap(remoteModulesConfig, localModulesConfig); -}; - -// REQUEST: Parallell arrays: -var REQUEST_MODULE_IDS = 0; -var REQUEST_METHOD_IDS = 1; -var REQUEST_PARAMSS = 2; -// RESPONSE: Parallell arrays: -var RESPONSE_CBIDS = 3; -var RESPONSE_RETURN_VALUES = 4; - -var applyWithErrorReporter = function(fun: Function, context: ?any, args: ?any) { +var guard = (fn) => { try { - return fun.apply(context, args); - } catch (e) { - ErrorUtils.reportFatalError(e); + fn(); + } catch (error) { + ErrorUtils.reportFatalError(error); } }; -/** - * Utility to catch errors and prevent having to bind, or execute a bound - * function, while catching errors in a process and returning a resulting - * return value. This ensures that even if a process fails, we can still return - * *some* values (from `_flushedQueueUnguarded` for example). Glorified - * try/catch/finally that invokes the global `onerror`. - * - * @param {function} operation Function to execute, likely populates the - * message buffer. - * @param {Array<*>} operationArguments Arguments passed to `operation`. - * @param {function} getReturnValue Returns a return value - will be invoked - * even if the `operation` fails half way through completing its task. - * @return {object} Return value returned from `getReturnValue`. - */ -var guardReturn = function(operation, operationArguments, getReturnValue, context) { - if (operation) { - applyWithErrorReporter(operation, context, operationArguments); - } - if (getReturnValue) { - return applyWithErrorReporter(getReturnValue, context, null); - } - return null; -}; - -/** - * Bookkeeping logic for callbackIDs. We ensure that success and error - * callbacks are numerically adjacent. - * - * We could have also stored the association between success cbID and errorCBID - * in a map without relying on this adjacency, but the bookkeeping here avoids - * an additional two maps to associate in each direction, and avoids growing - * dictionaries (new fields). Instead, we compute pairs of callback IDs, by - * populating the `res` argument to `allocateCallbackIDs` (in conjunction with - * pooling). Behind this bookeeping API, we ensure that error and success - * callback IDs are always adjacent so that when one is invoked, we always know - * how to free the memory of the other. By using this API, it is impossible to - * create malformed callbackIDs that are not adjacent. - */ -var createBookkeeping = function() { - return { - /** - * Incrementing callback ID. Must start at 1 - otherwise converted null - * values which become zero are not distinguishable from a GUID of zero. - */ - GUID: 1, - errorCallbackIDForSuccessCallbackID: function(successID) { - return successID + 1; - }, - successCallbackIDForErrorCallbackID: function(errorID) { - return errorID - 1; - }, - allocateCallbackIDs: function(res) { - res.successCallbackID = this.GUID++; - res.errorCallbackID = this.GUID++; - }, - isSuccessCallback: function(id) { - return id % 2 === 1; +class MessageQueue { + + constructor(remoteModules, localModules, customRequire) { + this.RemoteModules = {}; + + this._require = customRequire || require; + this._queue = [[],[],[]]; + this._moduleTable = {}; + this._methodTable = {}; + this._callbacks = []; + this._callbackID = 0; + + [ + 'processBatch', + 'invokeCallbackAndReturnFlushedQueue', + 'callFunctionReturnFlushedQueue', + 'flushedQueue', + ].forEach((fn) => this[fn] = this[fn].bind(this)); + + this._genModules(remoteModules); + localModules && this._genLookupTables( + localModules, this._moduleTable, this._methodTable); + + if (__DEV__) { + this._debugInfo = {}; + this._remoteModuleTable = {}; + this._remoteMethodTable = {}; + this._genLookupTables( + remoteModules, this._remoteModuleTable, this._remoteMethodTable); } - }; -}; + } -var MessageQueueMixin = { /** - * Creates an efficient wire protocol for communicating across a bridge. - * Avoids allocating strings. - * - * @param {object} remoteModulesConfig Configuration of modules and their - * methods. + * Public APIs */ - _initNamingMap: function( - remoteModulesConfig: ModulesConfig, - localModulesConfig: ModulesConfig - ) { - this._remoteModuleNameToModuleID = {}; - this._remoteModuleIDToModuleName = {}; // Reverse - - this._remoteModuleNameToMethodNameToID = {}; - this._remoteModuleNameToMethodIDToName = {}; // Reverse - - this._localModuleNameToModuleID = {}; - this._localModuleIDToModuleName = {}; // Reverse - - this._localModuleNameToMethodNameToID = {}; - this._localModuleNameToMethodIDToName = {}; // Reverse - - function fillMappings( - modulesConfig: ModulesConfig, - moduleNameToModuleID: NameToID, - moduleIDToModuleName: IDToName, - moduleNameToMethodNameToID: {[key:string]: NameToID}, - moduleNameToMethodIDToName: {[key:string]: IDToName} - ) { - for (var moduleName in modulesConfig) { - var moduleConfig = modulesConfig[moduleName]; - var moduleID = moduleConfig.moduleID; - moduleNameToModuleID[moduleName] = moduleID; - moduleIDToModuleName[moduleID] = moduleName; // Reverse - - moduleNameToMethodNameToID[moduleName] = {}; - moduleNameToMethodIDToName[moduleName] = {}; // Reverse - var methods = moduleConfig.methods; - for (var methodName in methods) { - var methodID = methods[methodName].methodID; - moduleNameToMethodNameToID[moduleName][methodName] = - methodID; - moduleNameToMethodIDToName[moduleName][methodID] = - methodName; // Reverse - } - } - } - fillMappings( - remoteModulesConfig, - this._remoteModuleNameToModuleID, - this._remoteModuleIDToModuleName, - this._remoteModuleNameToMethodNameToID, - this._remoteModuleNameToMethodIDToName - ); - - fillMappings( - localModulesConfig, - this._localModuleNameToModuleID, - this._localModuleIDToModuleName, - this._localModuleNameToMethodNameToID, - this._localModuleNameToMethodIDToName - ); - - }, - - _initBookeeping: function() { - this._POOLED_CBIDS = {errorCallbackID: null, successCallbackID: null}; - this._bookkeeping = createBookkeeping(); - - /** - * Stores callbacks so that we may simulate asynchronous return values from - * other threads. Remote invocations in other threads can pass return values - * back asynchronously to the requesting thread. - */ - this._threadLocalCallbacksByID = []; - this._threadLocalScopesByID = []; + processBatch(batch) { + ReactUpdates.batchedUpdates(() => { + batch.forEach((call) => { + let method = call.method === 'callFunctionReturnFlushedQueue' ? + '__callFunction' : '__invokeCallback'; + guard(() => this[method].apply(this, call.args)); + }); + BridgeProfiling.profile('ReactUpdates.batchedUpdates()'); + }); + BridgeProfiling.profileEnd(); + return this.flushedQueue(); + } - /** - * Memory efficient parallel arrays. Each index cuts through the three - * arrays and forms a remote invocation of methodName(params) whos return - * value will be reported back to the other thread by way of the - * corresponding id in cbIDs. Each entry (A-D in the graphic below), - * represents a work item of the following form: - * - moduleID: ID of module to invoke method from. - * - methodID: ID of method in module to invoke. - * - params: List of params to pass to method. - * - cbID: ID to respond back to originating thread with. - * - * TODO: We can make this even more efficient (memory) by creating a single - * array, that is always pushed `n` elements as a time. - */ - this._outgoingItems = [ - /*REQUEST_MODULE_IDS: */ [/* +-+ +-+ +-+ +-+ */], - /*REQUEST_METHOD_IDS: */ [/* |A| |B| |C| |D| */], - /*REQUEST_PARAMSS: */ [/* |-| |-| |-| |-| */], + callFunctionReturnFlushedQueue(module, method, args) { + guard(() => this.__callFunction(module, method, args)); + return this.flushedQueue(); + } - /*RESPONSE_CBIDS: */ [/* +-+ +-+ +-+ +-+ */], - /* |E| |F| |G| |H| */ - /*RESPONSE_RETURN_VALUES: */ [/* +-+ +-+ +-+ +-+ */] - ]; + invokeCallbackAndReturnFlushedQueue(cbID, args) { + guard(() => this.__invokeCallback(cbID, args)); + return this.flushedQueue(); + } - /** - * Used to allow returning the buffer, while at the same time clearing it in - * a memory efficient manner. - */ - this._outgoingItemsSwap = [[], [], [], [], []]; - }, + flushedQueue() { + BridgeProfiling.profile('JSTimersExecution.callImmediates()'); + guard(() => JSTimersExecution.callImmediates()); + BridgeProfiling.profileEnd(); + let queue = this._queue; + this._queue = [[],[],[]]; + return queue[0].length ? queue : null; + } - invokeCallback: function(cbID, args) { - return guardReturn(this._invokeCallback, [cbID, args], null, this); - }, + /** + * "Private" methods + */ + __nativeCall(module, method, params, onFail, onSucc) { + if (onFail || onSucc) { + if (__DEV__) { + // eventually delete old debug info + (this._callbackID > (1 << 5)) && + (this._debugInfo[this._callbackID >> 5] = null); - _invokeCallback: function(cbID, args) { - try { - var cb = this._threadLocalCallbacksByID[cbID]; - var scope = this._threadLocalScopesByID[cbID]; - warning( - cb, - 'Cannot find callback with CBID %s. Native module may have invoked ' + - 'both the success callback and the error callback.', - cbID - ); - if (DEBUG_SPY_MODE) { - console.log('N->JS: Callback#' + cbID + '(' + JSON.stringify(args) + ')'); + this._debugInfo[this._callbackID >> 1] = [module, method]; } - BridgeProfiling.profile('Callback#' + cbID + '(' + JSON.stringify(args) + ')'); - cb.apply(scope, args); - BridgeProfiling.profileEnd(); - } catch(ie_requires_catch) { - throw ie_requires_catch; - } finally { - // Clear out the memory regardless of success or failure. - this._freeResourcesForCallbackID(cbID); - } - }, - - invokeCallbackAndReturnFlushedQueue: function(cbID, args) { - if (this._enableLogging) { - this._loggedIncomingItems.push([new Date().getTime(), cbID, args]); - } - return guardReturn( - this._invokeCallback, - [cbID, args], - this._flushedQueueUnguarded, - this - ); - }, - - callFunction: function(moduleID, methodID, params) { - return guardReturn(this._callFunction, [moduleID, methodID, params], null, this); - }, - - _callFunction: function(moduleName, methodName, params) { - if (isFinite(moduleName)) { - moduleName = this._localModuleIDToModuleName[moduleName]; - methodName = this._localModuleNameToMethodIDToName[moduleName][methodName]; + onFail && params.push(this._callbackID); + this._callbacks[this._callbackID++] = onFail; + onSucc && params.push(this._callbackID); + this._callbacks[this._callbackID++] = onSucc; } + this._queue[MODULE_IDS].push(module); + this._queue[METHOD_IDS].push(method); + this._queue[PARAMS].push(params); + } - if (DEBUG_SPY_MODE) { - console.log( - 'N->JS: ' + moduleName + '.' + methodName + - '(' + JSON.stringify(params) + ')'); + __callFunction(module, method, args) { + BridgeProfiling.profile(() => `${module}.${method}(${stringifySafe(args)})`); + if (isFinite(module)) { + method = this._methodTable[module][method]; + module = this._moduleTable[module]; } - BridgeProfiling.profile(moduleName + '.' + methodName + '(' + JSON.stringify(params) + ')'); - var ret = jsCall(this._requireFunc(moduleName), methodName, params); + module = this._require(module); + module[method].apply(module, args); BridgeProfiling.profileEnd(); + } - return ret; - }, - - callFunctionReturnFlushedQueue: function(moduleID, methodID, params) { - if (this._enableLogging) { - this._loggedIncomingItems.push([new Date().getTime(), moduleID, methodID, params]); + __invokeCallback(cbID, args) { + BridgeProfiling.profile( + () => `MessageQueue.invokeCallback(${cbID}, ${stringifySafe(args)})`); + let callback = this._callbacks[cbID]; + if (__DEV__ && !callback) { + let debug = this._debugInfo[cbID >> 1]; + let module = this._remoteModuleTable[debug[0]]; + let method = this._remoteMethodTable[debug[0]][debug[1]]; + console.error(`Callback with id ${cbID}: ${module}.${method}() not found`); } - return guardReturn( - this._callFunction, - [moduleID, methodID, params], - this._flushedQueueUnguarded, - this - ); - }, - - processBatch: function(batch) { - var self = this; - BridgeProfiling.profile('MessageQueue.processBatch()'); - var flushedQueue = guardReturn(function () { - ReactUpdates.batchedUpdates(function() { - batch.forEach(function(call) { - invariant( - call.module === 'BatchedBridge', - 'All the calls should pass through the BatchedBridge module' - ); - if (call.method === 'callFunctionReturnFlushedQueue') { - self._callFunction.apply(self, call.args); - } else if (call.method === 'invokeCallbackAndReturnFlushedQueue') { - self._invokeCallback.apply(self, call.args); - } else { - throw new Error( - 'Unrecognized method called on BatchedBridge: ' + call.method); - } - }); - BridgeProfiling.profile('React.batchedUpdates()'); - }); - BridgeProfiling.profileEnd(); - }, null, this._flushedQueueUnguarded, this); + this._callbacks[cbID & ~1] = null; + this._callbacks[cbID | 1] = null; + callback.apply(null, args); BridgeProfiling.profileEnd(); - return flushedQueue; - }, - - setLoggingEnabled: function(enabled) { - this._enableLogging = enabled; - this._loggedIncomingItems = []; - this._loggedOutgoingItems = [[], [], [], [], []]; - }, - - getLoggedIncomingItems: function() { - return this._loggedIncomingItems; - }, - - getLoggedOutgoingItems: function() { - return this._loggedOutgoingItems; - }, - - replayPreviousLog: function(previousLog) { - this._outgoingItems = previousLog; - }, - - /** - * Simple helpers for clearing the queues. This doesn't handle the fact that - * memory in the current buffer is leaked until the next frame or update - but - * that will typically be on the order of < 500ms. - */ - _swapAndReinitializeBuffer: function() { - // Outgoing requests - var currentOutgoingItems = this._outgoingItems; - var nextOutgoingItems = this._outgoingItemsSwap; - - nextOutgoingItems[REQUEST_MODULE_IDS].length = 0; - nextOutgoingItems[REQUEST_METHOD_IDS].length = 0; - nextOutgoingItems[REQUEST_PARAMSS].length = 0; - - // Outgoing responses - nextOutgoingItems[RESPONSE_CBIDS].length = 0; - nextOutgoingItems[RESPONSE_RETURN_VALUES].length = 0; - - this._outgoingItemsSwap = currentOutgoingItems; - this._outgoingItems = nextOutgoingItems; - }, + } /** - * @param {string} moduleID JS module name. - * @param {methodName} methodName Method in module to invoke. - * @param {array<*>?} params Array representing arguments to method. - * @param {string} cbID Unique ID to pass back in potential response. + * Private helper methods */ - _pushRequestToOutgoingItems: function(moduleID, methodName, params) { - this._outgoingItems[REQUEST_MODULE_IDS].push(moduleID); - this._outgoingItems[REQUEST_METHOD_IDS].push(methodName); - this._outgoingItems[REQUEST_PARAMSS].push(params); - - if (this._enableLogging) { - this._loggedOutgoingItems[REQUEST_MODULE_IDS].push(moduleID); - this._loggedOutgoingItems[REQUEST_METHOD_IDS].push(methodName); - this._loggedOutgoingItems[REQUEST_PARAMSS].push(params); + _genLookupTables(localModules, moduleTable, methodTable) { + let moduleNames = Object.keys(localModules); + for (var i = 0, l = moduleNames.length; i < l; i++) { + let moduleName = moduleNames[i]; + let methods = localModules[moduleName].methods; + let moduleID = localModules[moduleName].moduleID; + moduleTable[moduleID] = moduleName; + methodTable[moduleID] = {}; + + let methodNames = Object.keys(methods); + for (var j = 0, k = methodNames.length; j < k; j++) { + let methodName = methodNames[j]; + let methodConfig = methods[methodName]; + methodTable[moduleID][methodConfig.methodID] = methodName; + } } - }, - - /** - * @param {string} cbID Unique ID that other side of bridge has remembered. - * @param {*} returnValue Return value to pass to callback on other side of - * bridge. - */ - _pushResponseToOutgoingItems: function(cbID, returnValue) { - this._outgoingItems[RESPONSE_CBIDS].push(cbID); - this._outgoingItems[RESPONSE_RETURN_VALUES].push(returnValue); - }, + } - _freeResourcesForCallbackID: function(cbID) { - var correspondingCBID = this._bookkeeping.isSuccessCallback(cbID) ? - this._bookkeeping.errorCallbackIDForSuccessCallbackID(cbID) : - this._bookkeeping.successCallbackIDForErrorCallbackID(cbID); - this._threadLocalCallbacksByID[cbID] = null; - this._threadLocalScopesByID[cbID] = null; - if (this._threadLocalCallbacksByID[correspondingCBID]) { - this._threadLocalCallbacksByID[correspondingCBID] = null; - this._threadLocalScopesByID[correspondingCBID] = null; + _genModules(remoteModules) { + let moduleNames = Object.keys(remoteModules); + for (var i = 0, l = moduleNames.length; i < l; i++) { + let moduleName = moduleNames[i]; + let moduleConfig = remoteModules[moduleName]; + this.RemoteModules[moduleName] = this._genModule({}, moduleConfig); } - }, - - /** - * @param {Function} onFail Function to store in current thread for later - * lookup, when request fails. - * @param {Function} onSucc Function to store in current thread for later - * lookup, when request succeeds. - * @param {Object?=} scope Scope to invoke `cb` with. - * @param {Object?=} res Resulting callback ids. Use `this._POOLED_CBIDS`. - */ - _storeCallbacksInCurrentThread: function(onFail, onSucc, scope) { - invariant(onFail || onSucc, INTERNAL_ERROR); - this._bookkeeping.allocateCallbackIDs(this._POOLED_CBIDS); - var succCBID = this._POOLED_CBIDS.successCallbackID; - var errorCBID = this._POOLED_CBIDS.errorCallbackID; - this._threadLocalCallbacksByID[errorCBID] = onFail; - this._threadLocalCallbacksByID[succCBID] = onSucc; - this._threadLocalScopesByID[errorCBID] = scope; - this._threadLocalScopesByID[succCBID] = scope; - }, - - - /** - * IMPORTANT: There is possibly a timing issue with this form of flushing. We - * are currently not seeing any problems but the potential issue to look out - * for is: - * - While flushing this._outgoingItems contains the work for the other thread - * to perform. - * - To mitigate this, we never allow enqueueing messages if the queue is - * already reserved - as long as it is reserved, it could be in the midst of - * a flush. - * - * If this ever occurs we can easily eliminate the race condition. We can - * completely solve any ambiguity by sending messages such that we'll never - * try to reserve the queue when already reserved. Here's the pseudocode: - * - * var defensiveCopy = efficientDefensiveCopy(this._outgoingItems); - * this._swapAndReinitializeBuffer(); - */ - flushedQueue: function() { - return guardReturn(null, null, this._flushedQueueUnguarded, this); - }, - - _flushedQueueUnguarded: function() { - BridgeProfiling.profile('JSTimersExecution.callImmediates()'); - // Call the functions registered via setImmediate - JSTimersExecution.callImmediates(); - BridgeProfiling.profileEnd(); - - var currentOutgoingItems = this._outgoingItems; - this._swapAndReinitializeBuffer(); - var ret = currentOutgoingItems[REQUEST_MODULE_IDS].length || - currentOutgoingItems[RESPONSE_RETURN_VALUES].length ? currentOutgoingItems : null; + } - if (DEBUG_SPY_MODE && ret) { - for (var i = 0; i < currentOutgoingItems[0].length; i++) { - var moduleName = this._remoteModuleIDToModuleName[currentOutgoingItems[0][i]]; - var methodName = - this._remoteModuleNameToMethodIDToName[moduleName][currentOutgoingItems[1][i]]; - console.log( - 'JS->N: ' + moduleName + '.' + methodName + - '(' + JSON.stringify(currentOutgoingItems[2][i]) + ')'); - } + _genModule(module, moduleConfig) { + let methodNames = Object.keys(moduleConfig.methods); + for (var i = 0, l = methodNames.length; i < l; i++) { + let methodName = methodNames[i]; + let methodConfig = moduleConfig.methods[methodName]; + module[methodName] = this._genMethod( + moduleConfig.moduleID, methodConfig.methodID, methodConfig.type); } + Object.assign(module, moduleConfig.constants); + return module; + } - return ret; - }, - - call: function(moduleName, methodName, params, onFail, onSucc, scope) { - invariant( - (!onFail || typeof onFail === 'function') && - (!onSucc || typeof onSucc === 'function'), - 'Callbacks must be functions' - ); - // Store callback _before_ sending the request, just in case the MailBox - // returns the response in a blocking manner. - if (onFail || onSucc) { - this._storeCallbacksInCurrentThread(onFail, onSucc, scope, this._POOLED_CBIDS); - onFail && params.push(this._POOLED_CBIDS.errorCallbackID); - onSucc && params.push(this._POOLED_CBIDS.successCallbackID); - } - var moduleID = this._remoteModuleNameToModuleID[moduleName]; - if (moduleID === undefined || moduleID === null) { - throw new Error('Unrecognized module name:' + moduleName); + _genMethod(module, method, type) { + if (type === MethodTypes.local) { + return null; } - var methodID = this._remoteModuleNameToMethodNameToID[moduleName][methodName]; - if (methodID === undefined || moduleID === null) { - throw new Error('Unrecognized method name:' + methodName); - } - this._pushRequestToOutgoingItems(moduleID, methodID, params); - }, - __numPendingCallbacksOnlyUseMeInTestCases: function() { - var callbacks = this._threadLocalCallbacksByID; - var total = 0; - for (var i = 0; i < callbacks.length; i++) { - if (callbacks[i]) { - total++; - } + + let self = this; + if (type === MethodTypes.remoteAsync) { + return function(...args) { + return new Promise((resolve, reject) => { + self.__nativeCall(module, method, args, resolve, (errorData) => { + var error = createErrorFromErrorData(errorData); + reject(error); + }); + }); + }; + } else { + return function(...args) { + let lastArg = args.length > 0 ? args[args.length - 1] : null; + let secondLastArg = args.length > 1 ? args[args.length - 2] : null; + let hasSuccCB = typeof lastArg === 'function'; + let hasErrorCB = typeof secondLastArg === 'function'; + hasErrorCB && invariant( + hasSuccCB, + 'Cannot have a non-function arg after a function arg.' + ); + let numCBs = hasSuccCB + hasErrorCB; + let onSucc = hasSuccCB ? lastArg : null; + let onFail = hasErrorCB ? secondLastArg : null; + args = args.slice(0, args.length - numCBs); + return self.__nativeCall(module, method, args, onFail, onSucc); + }; } - return total; } -}; -Object.assign(MessageQueue.prototype, MessageQueueMixin); +} + +function createErrorFromErrorData(errorData: ErrorData): Error { + var { + message, + ...extraErrorInfo, + } = errorData; + var error = new Error(message); + error.framesToPop = 1; + return Object.assign(error, extraErrorInfo); +} + module.exports = MessageQueue; diff --git a/Libraries/Utilities/PerformanceLogger.js b/Libraries/Utilities/PerformanceLogger.js new file mode 100644 index 00000000000000..c05c4b7c2e1bb6 --- /dev/null +++ b/Libraries/Utilities/PerformanceLogger.js @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule PerformanceLogger + */ +'use strict'; + + +var performanceNow = require('performanceNow'); + +var timespans = {}; + +/** + * This is meant to collect and log performance data in production, which means + * it needs to have minimal overhead. + */ +var PerformanceLogger = { + addTimespan(key, lengthInMs, description) { + if (timespans[key]) { + if (__DEV__) { + console.log( + 'PerformanceLogger: Attempting to add a timespan that already exists' + ); + } + return; + } + + timespans[key] = { + description: description, + totalTime: lengthInMs, + }; + }, + + startTimespan(key, description) { + if (timespans[key]) { + if (__DEV__) { + console.log( + 'PerformanceLogger: Attempting to start a timespan that already exists' + ); + } + return; + } + + timespans[key] = { + description: description, + startTime: performanceNow(), + }; + }, + + stopTimespan(key) { + if (!timespans[key] || !timespans[key].startTime) { + if (__DEV__) { + console.log( + 'PerformanceLogger: Attempting to end a timespan that has not started' + ); + } + return; + } + + timespans[key].endTime = performanceNow(); + timespans[key].totalTime = + timespans[key].endTime - timespans[key].startTime; + }, + + clearTimespans() { + timespans = {}; + }, + + getTimespans() { + return timespans; + }, + + logTimespans() { + for (var key in timespans) { + console.log(key + ': ' + timespans[key].totalTime + 'ms'); + } + }, + + addTimespans(newTimespans, labels) { + for (var i = 0, l = newTimespans.length; i < l; i += 2) { + var label = labels[i / 2]; + PerformanceLogger.addTimespan( + label, + (newTimespans[i + 1] - newTimespans[i]), + label + ); + } + } +}; + +module.exports = PerformanceLogger; diff --git a/Libraries/Utilities/__tests__/MessageQueue-test.js b/Libraries/Utilities/__tests__/MessageQueue-test.js new file mode 100644 index 00000000000000..60fae4c6b9c5c3 --- /dev/null +++ b/Libraries/Utilities/__tests__/MessageQueue-test.js @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest.setMock('ReactUpdates', { + batchedUpdates: fn => fn() +}); + +jest.dontMock('MessageQueue'); +jest.dontMock('keyMirror'); +var MessageQueue = require('MessageQueue'); + +let MODULE_IDS = 0; +let METHOD_IDS = 1; +let PARAMS = 2; + +let TestModule = { + testHook1(){}, testHook2(){}, +}; + +let customRequire = (moduleName) => TestModule; + +let assertQueue = (flushedQueue, index, moduleID, methodID, params) => { + expect(flushedQueue[MODULE_IDS][index]).toEqual(moduleID); + expect(flushedQueue[METHOD_IDS][index]).toEqual(methodID); + expect(flushedQueue[PARAMS][index]).toEqual(params); +}; + +var queue; + +describe('MessageQueue', () => { + + beforeEach(() => { + queue = new MessageQueue( + remoteModulesConfig, + localModulesConfig, + customRequire, + ); + + TestModule.testHook1 = jasmine.createSpy(); + TestModule.testHook2 = jasmine.createSpy(); + }); + + it('should enqueue native calls', () => { + queue.__nativeCall(0, 1, [2]); + let flushedQueue = queue.flushedQueue(); + assertQueue(flushedQueue, 0, 0, 1, [2]); + }); + + it('should call a local function with id', () => { + expect(TestModule.testHook1.callCount).toEqual(0); + queue.__callFunction(0, 0, [1]); + expect(TestModule.testHook1.callCount).toEqual(1); + }); + + it('should call a local function with the function name', () => { + expect(TestModule.testHook2.callCount).toEqual(0); + queue.__callFunction('one', 'testHook2', [2]); + expect(TestModule.testHook2.callCount).toEqual(1); + }); + + it('should generate native modules', () => { + queue.RemoteModules.one.remoteMethod1('foo'); + let flushedQueue = queue.flushedQueue(); + assertQueue(flushedQueue, 0, 0, 0, ['foo']); + }); + + it('should store callbacks', () => { + queue.RemoteModules.one.remoteMethod2('foo', () => {}, () => {}); + let flushedQueue = queue.flushedQueue(); + assertQueue(flushedQueue, 0, 0, 1, ['foo', 0, 1]); + }); + + it('should call the stored callback', (done) => { + var done = false; + queue.RemoteModules.one.remoteMethod1(() => { done = true; }); + queue.__invokeCallback(1); + expect(done).toEqual(true); + }); + + it('should throw when calling the same callback twice', () => { + queue.RemoteModules.one.remoteMethod1(() => {}); + queue.__invokeCallback(1); + expect(() => queue.__invokeCallback(1)).toThrow(); + }); + + it('should throw when calling both success and failure callback', () => { + queue.RemoteModules.one.remoteMethod1(() => {}, () => {}); + queue.__invokeCallback(1); + expect(() => queue.__invokeCallback(0)).toThrow(); + }); + + describe('processBatch', () => { + + it('should call __invokeCallback for invokeCallbackAndReturnFlushedQueue', () => { + queue.__invokeCallback = jasmine.createSpy(); + queue.processBatch([{ + method: 'invokeCallbackAndReturnFlushedQueue', + args: [], + }]); + expect(queue.__invokeCallback.callCount).toEqual(1); + }); + + it('should call __callFunction for callFunctionReturnFlushedQueue', () => { + queue.__callFunction = jasmine.createSpy(); + queue.processBatch([{ + method: 'callFunctionReturnFlushedQueue', + args: [], + }]); + expect(queue.__callFunction.callCount).toEqual(1); + }); + + }); + +}); + +var remoteModulesConfig = { + 'one': { + 'moduleID':0, + 'methods': { + 'remoteMethod1':{ 'type': 'remote', 'methodID': 0 }, + 'remoteMethod2':{ 'type': 'remote', 'methodID': 1 }, + } + }, +}; + +var localModulesConfig = { + 'one': { + 'moduleID': 0, + 'methods': { + 'testHook1':{ 'type': 'local', 'methodID': 0 }, + 'testHook2':{ 'type': 'local', 'methodID': 1 }, + } + }, +}; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 479476cbe0875d..36727656739638 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -47,6 +47,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { AppStateIOS: require('AppStateIOS'), AsyncStorage: require('AsyncStorage'), CameraRoll: require('CameraRoll'), + ImagePickerIOS: require('ImagePickerIOS'), InteractionManager: require('InteractionManager'), LayoutAnimation: require('LayoutAnimation'), LinkingIOS: require('LinkingIOS'), diff --git a/Libraries/vendor/react/browser/eventPlugins/PanResponder.js b/Libraries/vendor/react/browser/eventPlugins/PanResponder.js index 721da744a79dbb..b6f0a765add160 100644 --- a/Libraries/vendor/react/browser/eventPlugins/PanResponder.js +++ b/Libraries/vendor/react/browser/eventPlugins/PanResponder.js @@ -73,6 +73,11 @@ var currentCentroidY = TouchHistoryMath.currentCentroidY; * // Another component has become the responder, so this gesture * // should be cancelled * }, + * onShouldBlockNativeResponder: (evt, gestureState) => { + * // Returns whether this component should block native components from becoming the JS + * // responder. Returns true by default. Is currently only supported on android. + * return true; + * }, * }); * }, * @@ -241,6 +246,7 @@ var PanResponder = { * - `onPanResponderMove: (e, gestureState) => {...}` * - `onPanResponderTerminate: (e, gestureState) => {...}` * - `onPanResponderTerminationRequest: (e, gestureState) => {...}` + * - 'onShouldBlockNativeResponder: (e, gestureState) => {...}' * * In general, for events that have capture equivalents, we update the * gestureState once in the capture phase and can use it in the bubble phase @@ -298,6 +304,9 @@ var PanResponder = { gestureState.dx = 0; gestureState.dy = 0; config.onPanResponderGrant && config.onPanResponderGrant(e, gestureState); + // TODO: t7467124 investigate if this can be removed + return config.onShouldBlockNativeResponder === undefined ? true : + config.onShouldBlockNativeResponder(); }, onResponderReject: function(e) { diff --git a/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js b/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js index 141b0a9700cb66..2a4df8c8d2f990 100644 --- a/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js +++ b/Libraries/vendor/react/browser/eventPlugins/ResponderEventPlugin.js @@ -55,13 +55,14 @@ var trackedTouchCount = 0; */ var previousActiveTouches = 0; -var changeResponder = function(nextResponderID) { +var changeResponder = function(nextResponderID, blockNativeResponder) { var oldResponderID = responderID; responderID = nextResponderID; if (ResponderEventPlugin.GlobalResponderHandler !== null) { ResponderEventPlugin.GlobalResponderHandler.onChange( oldResponderID, - nextResponderID + nextResponderID, + blockNativeResponder ); } }; @@ -379,6 +380,7 @@ function setResponderAndExtractTransfer( grantEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; EventPropagators.accumulateDirectDispatches(grantEvent); + var blockNativeResponder = executeDirectDispatch(grantEvent) === true; if (responderID) { var terminationRequestEvent = ResponderSyntheticEvent.getPooled( @@ -404,7 +406,7 @@ function setResponderAndExtractTransfer( terminateEvent.touchHistory = ResponderTouchHistoryStore.touchHistory; EventPropagators.accumulateDirectDispatches(terminateEvent); extracted = accumulate(extracted, [grantEvent, terminateEvent]); - changeResponder(wantsResponderID); + changeResponder(wantsResponderID, blockNativeResponder); } else { var rejectEvent = ResponderSyntheticEvent.getPooled( eventTypes.responderReject, @@ -417,7 +419,7 @@ function setResponderAndExtractTransfer( } } else { extracted = accumulate(extracted, grantEvent); - changeResponder(wantsResponderID); + changeResponder(wantsResponderID, blockNativeResponder); } return extracted; } diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index f8e9eeab711d3b..e844cd8b1971e3 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -20,6 +20,7 @@ #import "RCTKeyCommands.h" #import "RCTLog.h" #import "RCTPerfStats.h" +#import "RCTPerformanceLogger.h" #import "RCTProfile.h" #import "RCTRedBox.h" #import "RCTRootView.h" @@ -31,8 +32,6 @@ NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; NSString *const RCTJavaScriptDidFailToLoadNotification = @"RCTJavaScriptDidFailToLoadNotification"; -dispatch_queue_t const RCTJSThread = nil; - /** * Must be kept in sync with `MessageQueue.js`. */ @@ -40,9 +39,6 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { RCTBridgeFieldRequestModuleIDs = 0, RCTBridgeFieldMethodIDs, RCTBridgeFieldParamss, - RCTBridgeFieldResponseCBIDs, - RCTBridgeFieldResponseReturnValues, - RCTBridgeFieldFlushDateMillis }; typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { @@ -166,7 +162,6 @@ @implementation RCTModuleMethod SEL _selector; NSMethodSignature *_methodSignature; NSArray *_argumentBlocks; - dispatch_block_t _methodQueue; } - (instancetype)initWithObjCMethodName:(NSString *)objCMethodName @@ -570,14 +565,19 @@ - (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink @implementation RCTBridge static id _latestJSExecutor; - -#if RCT_DEBUG +dispatch_queue_t RCTJSThread; + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ + // Set up JS thread + RCTJSThread = (id)kCFNull; + +#if RCT_DEBUG + + // Set up module classes static unsigned int classCount; Class *classes = objc_copyClassList(&classCount); @@ -590,7 +590,8 @@ + (void)initialize if (class_conformsToProtocol(superclass, @protocol(RCTBridgeModule))) { if (![RCTModuleClassesByID containsObject:cls]) { - RCTLogError(@"Class %@ was not exported. Did you forget to use RCT_EXPORT_MODULE()?", NSStringFromClass(cls)); + RCTLogError(@"Class %@ was not exported. Did you forget to use " + "RCT_EXPORT_MODULE()?", NSStringFromClass(cls)); } break; } @@ -600,11 +601,11 @@ + (void)initialize free(classes); +#endif + }); } -#endif - - (instancetype)initWithBundleURL:(NSURL *)bundleURL moduleProvider:(RCTBridgeModuleProviderBlock)block launchOptions:(NSDictionary *)launchOptions @@ -612,6 +613,8 @@ - (instancetype)initWithBundleURL:(NSURL *)bundleURL RCTAssertMainThread(); if ((self = [super init])) { + RCTPerformanceLoggerStart(RCTPLTTI); + _bundleURL = bundleURL; _moduleProvider = block; _launchOptions = [launchOptions copy]; @@ -744,7 +747,6 @@ @implementation RCTBatchedBridge __weak id _javaScriptExecutor; RCTSparseArray *_modulesByID; RCTSparseArray *_queuesByID; - dispatch_queue_t _methodQueue; NSDictionary *_modulesByName; CADisplayLink *_mainDisplayLink; CADisplayLink *_jsDisplayLink; @@ -786,7 +788,6 @@ - (instancetype)initWithParentBridge:(RCTBridge *)bridge * Initialize and register bridge modules *before* adding the display link * so we don't have threading issues */ - _methodQueue = dispatch_queue_create("com.facebook.React.BridgeMethodQueue", DISPATCH_QUEUE_SERIAL); [self registerModules]; /** @@ -899,21 +900,49 @@ - (void)registerModules } } - // Get method queues - [_modulesByID enumerateObjectsUsingBlock: - ^(id module, NSNumber *moduleID, __unused BOOL *stop) { - if ([module respondsToSelector:@selector(methodQueue)]) { - dispatch_queue_t queue = [module methodQueue]; - if (queue) { - _queuesByID[moduleID] = queue; - } else { - _queuesByID[moduleID] = (id)kCFNull; + // Set/get method queues + [_modulesByID enumerateObjectsUsingBlock:^(id module, NSNumber *moduleID, __unused BOOL *stop) { + + dispatch_queue_t queue = nil; + BOOL implementsMethodQueue = [module respondsToSelector:@selector(methodQueue)]; + if (implementsMethodQueue) { + queue = [module methodQueue]; + } + if (!queue) { + + // Need to cache queueNames because they aren't retained by dispatch_queue + static NSMutableDictionary *queueNames; + if (!queueNames) { + queueNames = [[NSMutableDictionary alloc] init]; + } + NSString *moduleName = RCTBridgeModuleNameForClass([module class]); + NSString *queueName = queueNames[moduleName]; + if (!queueName) { + queueName = [NSString stringWithFormat:@"com.facebook.React.%@Queue", moduleName]; + queueNames[moduleName] = queueName; + } + + // Create new queue + queue = dispatch_queue_create(queueName.UTF8String, DISPATCH_QUEUE_SERIAL); + + // assign it to the module + if (implementsMethodQueue) { + @try { + [(id)module setValue:queue forKey:@"methodQueue"]; + } + @catch (NSException *exception) { + RCTLogError(@"%@ is returning nil for it's methodQueue, which is not " + "permitted. You must either return a pre-initialized " + "queue, or @synthesize the methodQueue to let the bridge " + "create a queue for you.", moduleName); + } } } + _queuesByID[moduleID] = queue; if ([module conformsToProtocol:@protocol(RCTFrameUpdateObserver)]) { [_frameUpdateObservers addObject:module]; - } + } }]; } @@ -954,14 +983,20 @@ - (void)initJS }); } else { + RCTProfileBeginEvent(); + RCTPerformanceLoggerStart(RCTPLScriptDownload); RCTJavaScriptLoader *loader = [[RCTJavaScriptLoader alloc] initWithBridge:self]; [loader loadBundleAtURL:bundleURL onComplete:^(NSError *error, NSString *script) { + RCTPerformanceLoggerEnd(RCTPLScriptDownload); + RCTProfileEndEvent(@"JavaScript dowload", @"init,download", @[]); _loading = NO; if (!self.isValid) { return; } + [[RCTRedBox sharedInstance] dismiss]; + RCTSourceCode *sourceCodeModule = self.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])]; sourceCodeModule.scriptURL = bundleURL; sourceCodeModule.scriptText = script; @@ -1009,11 +1044,7 @@ - (void)initJS - (NSDictionary *)modules { - if (!self.isValid) { - return nil; - } - - RCTAssert(_modulesByName != nil, @"Bridge modules have not yet been initialized. " + RCTAssert(!self.isValid || _modulesByName != nil, @"Bridge modules have not yet been initialized. " "You may be trying to access a module too early in the startup procedure."); return _modulesByName; @@ -1041,19 +1072,21 @@ - (void)invalidate _mainDisplayLink = nil; // Invalidate modules + dispatch_group_t group = dispatch_group_create(); for (id target in _modulesByID.allObjects) { if ([target respondsToSelector:@selector(invalidate)]) { [self dispatchBlock:^{ [(id)target invalidate]; - } forModule:target]; + } forModule:target dispatchGroup:group]; } + _queuesByID[RCTModuleIDsByName[RCTBridgeModuleNameForClass([target class])]] = nil; } - - // Release modules (breaks retain cycle if module has strong bridge reference) - _frameUpdateObservers = nil; - _modulesByID = nil; - _queuesByID = nil; - _modulesByName = nil; + dispatch_group_notify(group, dispatch_get_main_queue(), ^{ + _queuesByID = nil; + _modulesByID = nil; + _modulesByName = nil; + _frameUpdateObservers = nil; + }); }; if (!_javaScriptExecutor) { @@ -1122,12 +1155,11 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete: { RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil"); - RCTProfileBeginEvent(); - + RCTProfileBeginFlowEvent(); [_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) { + RCTProfileEndFlowEvent(); RCTAssertJSThread(); - RCTProfileEndEvent(@"ApplicationScript", @"js_call,init", scriptLoadError); if (scriptLoadError) { onComplete(scriptLoadError); return; @@ -1153,13 +1185,30 @@ - (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete: } #pragma mark - Payload Generation +- (void)dispatchBlock:(dispatch_block_t)block + forModule:(id)module +{ + [self dispatchBlock:block forModule:module dispatchGroup:NULL]; +} -- (void)dispatchBlock:(dispatch_block_t)block forModule:(id)module +- (void)dispatchBlock:(dispatch_block_t)block + forModule:(id)module + dispatchGroup:(dispatch_group_t)group { - [self dispatchBlock:block forModuleID:RCTModuleIDsByName[RCTBridgeModuleNameForClass([module class])]]; + [self dispatchBlock:block + forModuleID:RCTModuleIDsByName[RCTBridgeModuleNameForClass([module class])] + dispatchGroup:group]; } -- (void)dispatchBlock:(dispatch_block_t)block forModuleID:(NSNumber *)moduleID +- (void)dispatchBlock:(dispatch_block_t)block + forModuleID:(NSNumber *)moduleID +{ + [self dispatchBlock:block forModuleID:moduleID dispatchGroup:NULL]; +} + +- (void)dispatchBlock:(dispatch_block_t)block + forModuleID:(NSNumber *)moduleID + dispatchGroup:(dispatch_group_t)group { RCTAssertJSThread(); @@ -1168,10 +1217,14 @@ - (void)dispatchBlock:(dispatch_block_t)block forModuleID:(NSNumber *)moduleID queue = _queuesByID[moduleID]; } - if (queue == (id)kCFNull) { + if (queue == RCTJSThread) { [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; - } else { - dispatch_async(queue ?: _methodQueue, block); + } else if (queue) { + if (group != NULL) { + dispatch_group_async(group, queue, block); + } else { + dispatch_async(queue, block); + } } } @@ -1258,14 +1311,6 @@ - (void)_handleBuffer:(id)buffer context:(NSNumber *)context return; } - NSUInteger bufferRowCount = [requestsArray count]; - NSUInteger expectedFieldsCount = RCTBridgeFieldResponseReturnValues + 1; - - if (bufferRowCount != expectedFieldsCount) { - 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]]) { @@ -1319,9 +1364,9 @@ - (void)_handleBuffer:(id)buffer context:(NSNumber *)context RCTProfileEndEvent(RCTCurrentThreadName(), @"objc_call,dispatch_async", @{ @"calls": @(calls.count) }); }; - if (queue == (id)kCFNull) { + if (queue == RCTJSThread) { [_javaScriptExecutor executeBlockOnJavaScriptQueue:block]; - } else { + } else if (queue) { dispatch_async(queue, block); } } @@ -1484,6 +1529,14 @@ - (void)stopProfiling ^(__unused NSData *data, __unused NSURLResponse *response, NSError *error) { if (error) { RCTLogError(@"%@", error.localizedDescription); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [[[UIAlertView alloc] initWithTitle:@"Profile" + message:@"The profile has been generated, check the dev server log for instructions." + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil] show]; + }); } }]; diff --git a/React/Base/RCTBridgeModule.h b/React/Base/RCTBridgeModule.h index 90090e8471ccdf..4715f9df72d3a0 100644 --- a/React/Base/RCTBridgeModule.h +++ b/React/Base/RCTBridgeModule.h @@ -41,7 +41,7 @@ typedef void (^RCTPromiseRejectBlock)(NSError *error); * * NOTE: RCTJSThread is not a real libdispatch queue */ -extern const dispatch_queue_t RCTJSThread; +extern dispatch_queue_t RCTJSThread; /** * Provides the interface needed to register a bridge module. @@ -53,10 +53,33 @@ extern const dispatch_queue_t RCTJSThread; * 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; + * To implement this in your module, just add `@synthesize bridge = _bridge;` */ @property (nonatomic, weak) RCTBridge *bridge; +/** + * The queue that will be used to call all exported methods. If omitted, this + * will call on a default background queue, which is avoids blocking the main + * thread. + * + * If the methods in your module need to interact with UIKit methods, they will + * probably need to call those on the main thread, as most of UIKit is main- + * thread-only. You can tell React Native to call your module methods on the + * main thread by returning a reference to the main queue, like this: + * + * - (dispatch_queue_t)methodQueue + * { + * return dispatch_get_main_queue(); + * } + * + * If you don't want to specify the queue yourself, but you need to use it + * inside your class (e.g. if you have internal methods that need to disaptch + * onto that queue), you can just add `@synthesize methodQueue = _methodQueue;` + * and the bridge will populate the methodQueue property for you automatically + * when it initializes the module. + */ +@property (nonatomic, weak, readonly) dispatch_queue_t methodQueue; + /** * Place this macro in your class implementation to automatically register * your module with the bridge when it loads. The optional js_name argument @@ -180,38 +203,6 @@ extern const dispatch_queue_t RCTJSThread; return @[@#js_name, @#method]; \ } \ - -/** - * The queue that will be used to call all exported methods. If omitted, this - * will call on the default background queue, which is avoids blocking the main - * thread. - * - * If the methods in your module need to interact with UIKit methods, they will - * probably need to call those on the main thread, as most of UIKit is main- - * thread-only. You can tell React Native to call your module methods on the - * main thread by returning a reference to the main queue, like this: - * - * - (dispatch_queue_t)methodQueue - * { - * return dispatch_get_main_queue(); - * } - * - * If your methods perform heavy work such as synchronous filesystem or network - * access, you probably don't want to block the default background queue, as - * this will stall other methods. Instead, you should return a custom serial - * queue, like this: - * - * - (dispatch_queue_t)methodQueue - * { - * return dispatch_queue_create("com.mydomain.FileQueue", DISPATCH_QUEUE_SERIAL); - * } - * - * Alternatively, if only some methods of the module should be executed on a - * particular queue you can leave this method unimplemented, and simply - * dispatch_async() to the required queue within the method itself. - */ -- (dispatch_queue_t)methodQueue; - /** * Injects constants into JS. These constants are made accessible via * NativeModules.ModuleName.X. This method is called when the module is diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 145e88b21bcbf4..e96e4562d3cc61 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -46,6 +46,9 @@ + (NSURL *)NSURL:(id)json; + (NSURLRequest *)NSURLRequest:(id)json; +typedef NSURL RCTFileURL; ++ (RCTFileURL *)RCTFileURL:(id)json; + + (NSDate *)NSDate:(id)json; + (NSTimeZone *)NSTimeZone:(id)json; + (NSTimeInterval)NSTimeInterval:(id)json; @@ -95,6 +98,9 @@ typedef NSArray NSDictionaryArray; typedef NSArray NSURLArray; + (NSURLArray *)NSURLArray:(id)json; +typedef NSArray RCTFileURLArray; ++ (RCTFileURLArray *)RCTFileURLArray:(id)json; + typedef NSArray NSNumberArray; + (NSNumberArray *)NSNumberArray:(id)json; diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 804faf87ece82b..201b783cbd0d00 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -122,6 +122,20 @@ + (NSURLRequest *)NSURLRequest:(id)json return URL ? [NSURLRequest requestWithURL:URL] : nil; } ++ (RCTFileURL *)RCTFileURL:(id)json +{ + NSURL *fileURL = [self NSURL:json]; + if (![fileURL isFileURL]) { + RCTLogError(@"URI must be a local file, '%@' isn't.", fileURL); + return nil; + } + if (![[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]) { + RCTLogError(@"File '%@' could not be found.", fileURL); + return nil; + } + return fileURL; +} + + (NSDate *)NSDate:(id)json { if ([json isKindOfClass:[NSNumber class]]) { @@ -854,6 +868,7 @@ + (UIFont *)UIFont:(UIFont *)font withFamily:(id)family RCT_ARRAY_CONVERTER(NSString) RCT_ARRAY_CONVERTER(NSDictionary) RCT_ARRAY_CONVERTER(NSURL) +RCT_ARRAY_CONVERTER(RCTFileURL) RCT_ARRAY_CONVERTER(NSNumber) RCT_ARRAY_CONVERTER(UIColor) diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index bce19e8a9f81bf..a375629121c9ae 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -119,6 +119,11 @@ - (instancetype)init name:RCTJavaScriptDidLoadNotification object:nil]; + [notificationCenter addObserver:self + selector:@selector(jsLoaded:) + name:RCTJavaScriptDidFailToLoadNotification + object:nil]; + _defaults = [NSUserDefaults standardUserDefaults]; _settings = [[NSMutableDictionary alloc] init]; _extraMenuItems = [NSMutableArray array]; @@ -142,7 +147,7 @@ - (instancetype)init [commands registerKeyCommandWithInput:@"i" modifierFlags:UIKeyModifierCommand action:^(__unused UIKeyCommand *command) { - [_bridge.eventDispatcher + [weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; }]; diff --git a/React/Base/RCTKeyCommands.m b/React/Base/RCTKeyCommands.m index 52d8c30dddd229..a9d358482eb904 100644 --- a/React/Base/RCTKeyCommands.m +++ b/React/Base/RCTKeyCommands.m @@ -11,28 +11,112 @@ #import +#import "RCTDefines.h" #import "RCTUtils.h" -@interface RCTKeyCommands () +#if RCT_DEV -@property (nonatomic, strong) NSMutableDictionary *commandBindings; +static BOOL RCTIsIOS8OrEarlier() +{ + return [UIDevice currentDevice].systemVersion.floatValue < 9; +} -- (void)RCT_handleKeyCommand:(UIKeyCommand *)key; +@interface RCTKeyCommand : NSObject + +@property (nonatomic, strong) UIKeyCommand *keyCommand; +@property (nonatomic, copy) void (^block)(UIKeyCommand *); @end -@implementation UIApplication (RCTKeyCommands) +@implementation RCTKeyCommand + +- (instancetype)initWithKeyCommand:(UIKeyCommand *)keyCommand + block:(void (^)(UIKeyCommand *))block +{ + if ((self = [super init])) { + _keyCommand = keyCommand; + _block = block; + } + return self; +} + +RCT_NOT_IMPLEMENTED(-init) + +- (id)copyWithZone:(__unused NSZone *)zone +{ + return self; +} + +- (NSUInteger)hash +{ + return _keyCommand.input.hash ^ _keyCommand.modifierFlags; +} + +- (BOOL)isEqual:(RCTKeyCommand *)object +{ + if (![object isKindOfClass:[RCTKeyCommand class]]) { + return NO; + } + return [self matchesInput:object.keyCommand.input + flags:object.keyCommand.modifierFlags]; +} + +- (BOOL)matchesInput:(NSString *)input flags:(UIKeyModifierFlags)flags +{ + return [_keyCommand.input isEqual:input] && _keyCommand.modifierFlags == flags; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@:%p input=\"%@\" flags=%zd hasBlock=%@>", + [self class], self, _keyCommand.input, _keyCommand.modifierFlags, + _block ? @"YES" : @"NO"]; +} + +@end + +@interface RCTKeyCommands () + +@property (nonatomic, strong) NSMutableSet *commands; + +@end + +@implementation UIResponder (RCTKeyCommands) - (NSArray *)RCT_keyCommands { - NSDictionary *commandBindings = [RCTKeyCommands sharedInstance].commandBindings; - return [[self RCT_keyCommands] arrayByAddingObjectsFromArray:[commandBindings allKeys]]; + NSSet *commands = [RCTKeyCommands sharedInstance].commands; + return [[commands valueForKeyPath:@"keyCommand"] allObjects]; +} + +- (void)RCT_handleKeyCommand:(UIKeyCommand *)key +{ + // NOTE: throttle the key handler because on iOS 9 the handleKeyCommand: + // method gets called repeatedly if the command key is held down. + + static NSTimeInterval lastCommand = 0; + if (RCTIsIOS8OrEarlier() || CACurrentMediaTime() - lastCommand > 0.5) { + for (RCTKeyCommand *command in [RCTKeyCommands sharedInstance].commands) { + if ([command.keyCommand.input isEqualToString:key.input] && + command.keyCommand.modifierFlags == key.modifierFlags) { + if (command.block) { + command.block(key); + lastCommand = CACurrentMediaTime(); + } + } + } + } } +@end + +@implementation UIApplication (RCTKeyCommands) + +// Required for iOS 8.x - (BOOL)RCT_sendAction:(SEL)action to:(id)target from:(id)sender forEvent:(UIEvent *)event { if (action == @selector(RCT_handleKeyCommand:)) { - [[RCTKeyCommands sharedInstance] RCT_handleKeyCommand:sender]; + [self RCT_handleKeyCommand:sender]; return YES; } return [self RCT_sendAction:action to:target from:sender forEvent:event]; @@ -44,13 +128,25 @@ @implementation RCTKeyCommands + (void)initialize { - //swizzle UIApplication - RCTSwapInstanceMethods([UIApplication class], @selector(keyCommands), @selector(RCT_keyCommands)); - RCTSwapInstanceMethods([UIApplication class], @selector(sendAction:to:from:forEvent:), @selector(RCT_sendAction:to:from:forEvent:)); + if (RCTIsIOS8OrEarlier()) { + + //swizzle UIApplication + RCTSwapInstanceMethods([UIApplication class], + @selector(keyCommands), + @selector(RCT_keyCommands)); + + RCTSwapInstanceMethods([UIApplication class], + @selector(sendAction:to:from:forEvent:), + @selector(RCT_sendAction:to:from:forEvent:)); + } else { + + //swizzle UIResponder + RCTSwapInstanceMethods([UIResponder class], + @selector(keyCommands), + @selector(RCT_keyCommands)); + } } -static RCTKeyCommands *RKKeyCommandsSharedInstance = nil; - + (instancetype)sharedInstance { static RCTKeyCommands *sharedInstance; @@ -65,36 +161,22 @@ + (instancetype)sharedInstance - (instancetype)init { if ((self = [super init])) { - _commandBindings = [[NSMutableDictionary alloc] init]; + _commands = [[NSMutableSet alloc] init]; } return self; } -- (void)RCT_handleKeyCommand:(UIKeyCommand *)key -{ - // NOTE: We should just be able to do commandBindings[key] here, but curiously, the - // lookup seems to return nil sometimes, even if the key is found in the dictionary. - // To fix this, we use a linear search, since there won't be many keys anyway - - [_commandBindings enumerateKeysAndObjectsUsingBlock: - ^(UIKeyCommand *k, void (^block)(UIKeyCommand *), __unused BOOL *stop) { - if ([key.input isEqualToString:k.input] && key.modifierFlags == k.modifierFlags) { - block(key); - } - }]; -} - - (void)registerKeyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)flags action:(void (^)(UIKeyCommand *))block { RCTAssertMainThread(); - if (input.length && flags) { + if (input.length && flags && RCTIsIOS8OrEarlier()) { // Workaround around the first cmd not working: http://openradar.appspot.com/19613391 // You can register just the cmd key and do nothing. This ensures that - // command-key modified commands will work first time. + // command-key modified commands will work first time. Fixed in iOS 9. [self registerKeyCommandWithInput:@"" modifierFlags:flags @@ -105,7 +187,9 @@ - (void)registerKeyCommandWithInput:(NSString *)input modifierFlags:flags action:@selector(RCT_handleKeyCommand:)]; - _commandBindings[command] = block ?: ^(__unused UIKeyCommand *cmd) {}; + RCTKeyCommand *keyCommand = [[RCTKeyCommand alloc] initWithKeyCommand:command block:block]; + [_commands removeObject:keyCommand]; + [_commands addObject:keyCommand]; } - (void)unregisterKeyCommandWithInput:(NSString *)input @@ -113,9 +197,9 @@ - (void)unregisterKeyCommandWithInput:(NSString *)input { RCTAssertMainThread(); - for (UIKeyCommand *key in [_commandBindings allKeys]) { - if ([key.input isEqualToString:input] && key.modifierFlags == flags) { - [_commandBindings removeObjectForKey:key]; + for (RCTKeyCommand *command in _commands.allObjects) { + if ([command matchesInput:input flags:flags]) { + [_commands removeObject:command]; break; } } @@ -126,8 +210,8 @@ - (BOOL)isKeyCommandRegisteredForInput:(NSString *)input { RCTAssertMainThread(); - for (UIKeyCommand *key in [_commandBindings allKeys]) { - if ([key.input isEqualToString:input] && key.modifierFlags == flags) { + for (RCTKeyCommand *command in _commands) { + if ([command matchesInput:input flags:flags]) { return YES; } } @@ -135,3 +219,29 @@ - (BOOL)isKeyCommandRegisteredForInput:(NSString *)input } @end + +#else + +@implementation RCTKeyCommands + ++ (instancetype)sharedInstance +{ + return nil; +} + +- (void)registerKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags + action:(void (^)(UIKeyCommand *))block {} + +- (void)unregisterKeyCommandWithInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags {} + +- (BOOL)isKeyCommandRegisteredForInput:(NSString *)input + modifierFlags:(UIKeyModifierFlags)flags +{ + return NO; +} + +@end + +#endif diff --git a/React/Base/RCTPerfStats.m b/React/Base/RCTPerfStats.m index 2e5ec8d142c7e8..39f72928f03ed6 100644 --- a/React/Base/RCTPerfStats.m +++ b/React/Base/RCTPerfStats.m @@ -41,7 +41,7 @@ - (UIView *)container - (RCTFPSGraph *)jsGraph { - if (!_jsGraph) { + if (!_jsGraph && _container) { UIColor *jsColor = [UIColor colorWithRed:0 green:1 blue:0 alpha:1]; _jsGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(2, 2, 124, 34) graphPosition:RCTFPSGraphPositionRight @@ -54,7 +54,7 @@ - (RCTFPSGraph *)jsGraph - (RCTFPSGraph *)uiGraph { - if (!_uiGraph) { + if (!_uiGraph && _container) { UIColor *uiColor = [UIColor colorWithRed:0 green:1 blue:1 alpha:1]; _uiGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(2, 2, 124, 34) graphPosition:RCTFPSGraphPositionLeft diff --git a/React/Base/RCTPerformanceLogger.h b/React/Base/RCTPerformanceLogger.h new file mode 100644 index 00000000000000..4c220d170fa976 --- /dev/null +++ b/React/Base/RCTPerformanceLogger.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "RCTDefines.h" +#import "RCTBridgeModule.h" + +typedef NS_ENUM(NSUInteger, RCTPLTag) { + RCTPLScriptDownload = 0, + RCTPLAppScriptExecution, + RCTPLTTI, + RCTPLSize +}; + +void RCTPerformanceLoggerStart(RCTPLTag tag); +void RCTPerformanceLoggerEnd(RCTPLTag tag); +NSArray *RCTPerformanceLoggerOutput(void); diff --git a/React/Base/RCTPerformanceLogger.m b/React/Base/RCTPerformanceLogger.m new file mode 100644 index 00000000000000..b43ab22529563c --- /dev/null +++ b/React/Base/RCTPerformanceLogger.m @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +#import "RCTPerformanceLogger.h" +#import "RCTRootView.h" + +static int64_t RCTPLData[RCTPLSize][2] = {}; + +void RCTPerformanceLoggerStart(RCTPLTag tag) +{ + RCTPLData[tag][0] = CACurrentMediaTime() * 1000; +} + +void RCTPerformanceLoggerEnd(RCTPLTag tag) +{ + RCTPLData[tag][1] = CACurrentMediaTime() * 1000; +} + +NSArray *RCTPerformanceLoggerOutput(void) +{ + return @[ + @(RCTPLData[0][0]), + @(RCTPLData[0][1]), + @(RCTPLData[1][0]), + @(RCTPLData[1][1]), + @(RCTPLData[2][0]), + @(RCTPLData[2][1]), + ]; +} + +@interface RCTPerformanceLogger : NSObject + +@end + +@implementation RCTPerformanceLogger + +RCT_EXPORT_MODULE() + +@synthesize bridge = _bridge; + +- (instancetype)init +{ + if ((self = [super init])) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(sendTimespans) + name:RCTContentDidAppearNotification + object:nil]; + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)sendTimespans +{ + [_bridge enqueueJSCall:@"PerformanceLogger.addTimespans" args:@[ + RCTPerformanceLoggerOutput(), + @[ + @"ScriptDownload", + @"ScriptExecution", + @"TTI", + ], + ]]; +} + +@end diff --git a/React/Base/RCTProfile.h b/React/Base/RCTProfile.h index 2718871d25e86f..469a8155291b45 100644 --- a/React/Base/RCTProfile.h +++ b/React/Base/RCTProfile.h @@ -20,8 +20,8 @@ * before before using it. */ -NSString *const RCTProfileDidStartProfiling; -NSString *const RCTProfileDidEndProfiling; +RCT_EXTERN NSString *const RCTProfileDidStartProfiling; +RCT_EXTERN NSString *const RCTProfileDidEndProfiling; #if RCT_DEV diff --git a/React/Base/RCTProfile.m b/React/Base/RCTProfile.m index a2b3d710627e02..d81d545f2a85fb 100644 --- a/React/Base/RCTProfile.m +++ b/React/Base/RCTProfile.m @@ -172,6 +172,8 @@ static void RCTProfileHookModules(RCTBridge *bridge) } free(methods); + class_replaceMethod(object_getClass(proxyClass), @selector(initialize), imp_implementationWithBlock(^{}), "v@:"); + for (Class cls in @[proxyClass, object_getClass(proxyClass)]) { Method oldImp = class_getInstanceMethod(cls, @selector(class)); class_replaceMethod(cls, @selector(class), imp_implementationWithBlock(^{ return moduleClass; }), method_getTypeEncoding(oldImp)); diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 094e88840e70ae..6f9e7c71c681fd 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -17,6 +17,7 @@ #import "RCTEventDispatcher.h" #import "RCTKeyCommands.h" #import "RCTLog.h" +#import "RCTPerformanceLogger.h" #import "RCTSourceCode.h" #import "RCTTouchHandler.h" #import "RCTUIManager.h" @@ -247,6 +248,7 @@ - (instancetype)initWithFrame:(CGRect)frame - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex { [super insertReactSubview:subview atIndex:atIndex]; + RCTPerformanceLoggerEnd(RCTPLTTI); dispatch_async(dispatch_get_main_queue(), ^{ if (!_contentHasAppeared) { _contentHasAppeared = YES; diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 54736613e957b2..200a3f64566fdb 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -17,6 +17,7 @@ #import "RCTDefines.h" #import "RCTLog.h" #import "RCTProfile.h" +#import "RCTPerformanceLogger.h" #import "RCTUtils.h" @interface RCTJavaScriptContext : NSObject @@ -446,12 +447,14 @@ - (void)executeApplicationScript:(NSString *)script if (!strongSelf || !strongSelf.isValid) { return; } + RCTPerformanceLoggerStart(RCTPLAppScriptExecution); JSValueRef jsError = NULL; JSStringRef execJSString = JSStringCreateWithCFString((__bridge CFStringRef)script); JSStringRef jsURL = JSStringCreateWithCFString((__bridge CFStringRef)sourceURL.absoluteString); JSValueRef result = JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, &jsError); JSStringRelease(jsURL); JSStringRelease(execJSString); + RCTPerformanceLoggerEnd(RCTPLAppScriptExecution); if (onComplete) { NSError *error; diff --git a/React/Modules/RCTAsyncLocalStorage.h b/React/Modules/RCTAsyncLocalStorage.h index 31ff98c6a88383..4fd1064ad1c2d5 100644 --- a/React/Modules/RCTAsyncLocalStorage.h +++ b/React/Modules/RCTAsyncLocalStorage.h @@ -8,6 +8,7 @@ */ #import "RCTBridgeModule.h" +#import "RCTInvalidating.h" /** * A simple, asynchronous, persistent, key-value storage system designed as a @@ -20,7 +21,9 @@ * * Keys and values must always be strings or an error is returned. */ -@interface RCTAsyncLocalStorage : NSObject +@interface RCTAsyncLocalStorage : NSObject + +@property (nonatomic, assign) BOOL clearOnInvalidate; - (void)multiGet:(NSArray *)keys callback:(RCTResponseSenderBlock)callback; - (void)multiSet:(NSArray *)kvPairs callback:(RCTResponseSenderBlock)callback; @@ -28,4 +31,7 @@ - (void)clear:(RCTResponseSenderBlock)callback; - (void)getAllKeys:(RCTResponseSenderBlock)callback; +// For clearing data when the bridge may not exist, e.g. when logging out. ++ (void)clearAllData; + @end diff --git a/React/Modules/RCTAsyncLocalStorage.m b/React/Modules/RCTAsyncLocalStorage.m index a7f3892826a949..50b5312f9962a0 100644 --- a/React/Modules/RCTAsyncLocalStorage.m +++ b/React/Modules/RCTAsyncLocalStorage.m @@ -61,6 +61,13 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut return nil; } +static NSString *RCTGetStorageDir() +{ + NSString *documentDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + NSURL *homeURL = [NSURL fileURLWithPath:documentDirectory isDirectory:YES]; + return [[homeURL URLByAppendingPathComponent:kStorageDir isDirectory:YES] path]; +} + // Only merges objects - all other types are just clobbered (including arrays) static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) { @@ -89,6 +96,26 @@ static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *so } } +static dispatch_queue_t RCTGetMethodQueue() +{ + // We want all instances to share the same queue since they will be reading/writing the same files. + static dispatch_queue_t queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); + }); + return queue; +} + +static BOOL RCTHasCreatedStorageDirectory = NO; +static NSError *RCTDeleteStorageDirectory() +{ + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:RCTGetStorageDir() error:&error]; + RCTHasCreatedStorageDirectory = NO; + return error; +} + #pragma mark - RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage @@ -106,7 +133,33 @@ @implementation RCTAsyncLocalStorage - (dispatch_queue_t)methodQueue { - return dispatch_queue_create("com.facebook.React.AsyncLocalStorageQueue", DISPATCH_QUEUE_SERIAL); + return RCTGetMethodQueue(); +} + ++ (void)clearAllData +{ + dispatch_async(RCTGetMethodQueue(), ^{ + RCTDeleteStorageDirectory(); + }); +} + +- (void)invalidate +{ + if (_clearOnInvalidate) { + RCTDeleteStorageDirectory(); + } + _clearOnInvalidate = NO; + _manifest = [[NSMutableDictionary alloc] init]; + _haveSetup = NO; +} +- (BOOL)isValid +{ + return _haveSetup; +} + +- (void)dealloc +{ + [self invalidate]; } - (NSString *)_filePathForKey:(NSString *)key @@ -117,30 +170,31 @@ - (NSString *)_filePathForKey:(NSString *)key - (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); + RCTAssertThread(RCTGetMethodQueue(), @"Must be executed on storage thread"); + + NSError *error = nil; + if (!RCTHasCreatedStorageDirectory) { + _storageDirectory = RCTGetStorageDir(); + [[NSFileManager defaultManager] createDirectoryAtPath:_storageDirectory + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + return RCTMakeError(@"Failed to create storage directory.", error, nil); + } + RCTHasCreatedStorageDirectory = YES; } - _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]; + if (!_haveSetup) { + _manifestPath = [_storageDirectory stringByAppendingPathComponent:kManifestFilename]; + NSDictionary *errorOut; + NSString *serialized = RCTReadFile(_manifestPath, nil, &errorOut); + _manifest = serialized ? [RCTJSONParse(serialized, &error) mutableCopy] : [[NSMutableDictionary alloc] init]; + if (error) { + RCTLogWarn(@"Failed to parse manifest - creating new one.\n\n%@", error); + _manifest = [[NSMutableDictionary alloc] init]; + } + _haveSetup = YES; } - _haveSetup = YES; return nil; } @@ -312,18 +366,10 @@ - (id)_writeEntry:(NSArray *)entry RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - 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]; - } + _manifest = [[NSMutableDictionary alloc] init]; + NSError *error = RCTDeleteStorageDirectory(); if (callback) { - callback(@[RCTNullIfNil(errorOut)]); + callback(@[RCTNullIfNil(error)]); } } diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 0ffbaf0c39efd8..839682f26d3b90 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -804,6 +804,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView RCT_EXPORT_METHOD(createView:(NSNumber *)reactTag viewName:(NSString *)viewName + rootTag:(NSNumber *)rootTag props:(NSDictionary *)props) { RCTViewManager *manager = _viewManagers[viewName]; @@ -979,7 +980,9 @@ - (void)flushUIBlocks [_pendingUIBlocksLock unlock]; // Execute the previously queued UI blocks + RCTProfileBeginFlowEvent(); dispatch_async(dispatch_get_main_queue(), ^{ + RCTProfileEndFlowEvent(); RCTProfileBeginEvent(); for (dispatch_block_t block in previousPendingUIBlocks) { block(); @@ -1212,7 +1215,8 @@ static void RCTMeasureLayout(RCTShadowView *view, * JS sets what *it* considers to be the responder. Later, scroll views can use * this in order to determine if scrolling is appropriate. */ -RCT_EXPORT_METHOD(setJSResponder:(NSNumber *)reactTag) +RCT_EXPORT_METHOD(setJSResponder:(NSNumber *)reactTag + blockNativeResponder:(BOOL)blockNativeResponder) { [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { _jsResponder = viewRegistry[reactTag]; @@ -1329,7 +1333,7 @@ - (NSDictionary *)customBubblingEventTypes }, } mutableCopy]; - for (RCTViewManager *manager in _viewManagers) { + for (RCTViewManager *manager in _viewManagers.allValues) { if (RCTClassOverridesInstanceMethod([manager class], @selector(customBubblingEventTypes))) { NSDictionary *eventTypes = [manager customBubblingEventTypes]; for (NSString *eventName in eventTypes) { @@ -1390,7 +1394,7 @@ - (NSDictionary *)customDirectEventTypes }, } mutableCopy]; - for (RCTViewManager *manager in _viewManagers) { + for (RCTViewManager *manager in _viewManagers.allValues) { if (RCTClassOverridesInstanceMethod([manager class], @selector(customDirectEventTypes))) { NSDictionary *eventTypes = [manager customDirectEventTypes]; for (NSString *eventName in eventTypes) { diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 81f65d39ac2084..6d10e297896750 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+React.m */; }; 1403F2B31B0AE60700C2A9A4 /* RCTPerfStats.m in Sources */ = {isa = PBXBuildFile; fileRef = 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */; }; 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */; }; + 142014191B32094000CC17BA /* RCTPerformanceLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 142014171B32094000CC17BA /* RCTPerformanceLogger.m */; }; 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE21AAC4AE100FC20F4 /* RCTMap.m */; }; 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */; }; 146459261B06C49500B389AA /* RCTFPSGraph.m in Sources */ = {isa = PBXBuildFile; fileRef = 146459251B06C49500B389AA /* RCTFPSGraph.m */; }; @@ -177,6 +178,8 @@ 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPerfStats.m; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJavaScriptLoader.m; sourceTree = ""; }; + 142014171B32094000CC17BA /* RCTPerformanceLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPerformanceLogger.m; sourceTree = ""; }; + 142014181B32094000CC17BA /* RCTPerformanceLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPerformanceLogger.h; sourceTree = ""; }; 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTFrameUpdate.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 = ""; }; @@ -439,6 +442,8 @@ 146459251B06C49500B389AA /* RCTFPSGraph.m */, 1403F2B11B0AE60700C2A9A4 /* RCTPerfStats.h */, 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */, + 142014171B32094000CC17BA /* RCTPerformanceLogger.m */, + 142014181B32094000CC17BA /* RCTPerformanceLogger.h */, ); path = Base; sourceTree = ""; @@ -551,6 +556,7 @@ 134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */, 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */, 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */, + 142014191B32094000CC17BA /* RCTPerformanceLogger.m in Sources */, 83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */, 83CBBA521A601E3B00E9B192 /* RCTLog.m in Sources */, 13B0801D1A69489C00A75B9A /* RCTNavItemManager.m in Sources */, diff --git a/React/Views/RCTTabBarManager.m b/React/Views/RCTTabBarManager.m index 0f038c4d741942..2290c78c741feb 100644 --- a/React/Views/RCTTabBarManager.m +++ b/React/Views/RCTTabBarManager.m @@ -14,13 +14,11 @@ @implementation RCTTabBarManager -@synthesize bridge = _bridge; - RCT_EXPORT_MODULE() - (UIView *)view { - return [[RCTTabBar alloc] initWithEventDispatcher:_bridge.eventDispatcher]; + return [[RCTTabBar alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; } RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index a151efb5c2c059..1acb1b2d69e8e7 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -494,12 +494,17 @@ - (void)displayLayer:(CALayer *)layer !RCTRunningInTestEnvironment() && RCTCornerRadiiAreEqual(cornerRadii) && RCTBorderInsetsAreEqual(borderInsets) && - RCTBorderColorsAreEqual(borderColors); + RCTBorderColorsAreEqual(borderColors) && - // TODO: A problem with this is that iOS draws borders in front of the content - // whereas CSS draws them behind the content. Also iOS clips to the outside of - // the border, but CSS clips to the inside. To solve this, we'll need to add - // a container view inside the main view to correctly clip the subviews. + // iOS draws borders in front of the content whereas CSS draws them behind + // the content. For this reason, only use iOS border drawing when clipping + // or when the border is hidden. + + (borderInsets.top == 0 || CGColorGetAlpha(borderColors.top) == 0 || self.clipsToBounds); + + // iOS clips to the outside of the border, but CSS clips to the inside. To + // solve this, we'll need to add a container view inside the main view to + // correctly clip the subviews. if (useIOSBorderRendering) { layer.cornerRadius = cornerRadii.topLeft; diff --git a/React/Views/RCTWebViewManager.m b/React/Views/RCTWebViewManager.m index 50659a5629d8e4..a5de572bd57aa8 100644 --- a/React/Views/RCTWebViewManager.m +++ b/React/Views/RCTWebViewManager.m @@ -30,6 +30,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets); RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL); RCT_EXPORT_VIEW_PROPERTY(shouldInjectAJAXHandler, BOOL); +RCT_REMAP_VIEW_PROPERTY(scalesPageToFit, _webView.scalesPageToFit, BOOL); - (NSDictionary *)constantsToExport { diff --git a/jestSupport/scriptPreprocess.js b/jestSupport/scriptPreprocess.js index fe675a577f142e..ac219c97dd9f7e 100644 --- a/jestSupport/scriptPreprocess.js +++ b/jestSupport/scriptPreprocess.js @@ -18,16 +18,13 @@ module.exports = { transformSource: transformSource, process: function(src, fileName) { - if (fileName.match(/node_modules/)) { - return src; - } - try { return transformSource(src, fileName); } catch(e) { - throw new Error('\nError transforming file:\n js/' + + console.error('\nError transforming file:\n js/' + (fileName.split('/js/')[1] || fileName) + ':' + e.lineNumber + ': \'' + e.message + '\'\n'); + return src; } } }; diff --git a/package.json b/package.json index 606c9bf59ce073..b215d6ec421ba8 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "jstransform": "11.0.1", "module-deps": "3.5.6", "optimist": "0.6.1", - "promise": "^7.0.0", + "promise": "^7.0.3", "react-timer-mixin": "^0.13.1", "react-tools": "0.13.2", "rebound": "^0.0.12", diff --git a/packager/blacklist.js b/packager/blacklist.js index 9bfeda509b8d87..a2ba71673af5a2 100644 --- a/packager/blacklist.js +++ b/packager/blacklist.js @@ -11,7 +11,6 @@ // Don't forget to everything listed here to `testConfig.json` // modulePathIgnorePatterns. var sharedBlacklist = [ - __dirname, 'website', 'node_modules/react-tools/src/utils/ImmutableObject.js', 'node_modules/react-tools/src/core/ReactInstanceHandles.js', diff --git a/packager/debugger.html b/packager/debugger.html index d72e40ead23ba4..0d0084559b0f49 100644 --- a/packager/debugger.html +++ b/packager/debugger.html @@ -48,10 +48,11 @@ loadScript(message.url, sendReply.bind(null, null)); }, 'executeJSCall': function(message, sendReply) { - var returnValue = [[], [], [], [], []]; + var returnValue = null; try { if (window && window.require) { - returnValue = window.require(message.moduleName)[message.moduleMethod].apply(null, message.arguments); + var module = window.require(message.moduleName); + returnValue = module[message.moduleMethod].apply(module, message.arguments); } } finally { sendReply(JSON.stringify(returnValue)); diff --git a/packager/launchChromeDevTools.applescript b/packager/launchChromeDevTools.applescript index 4b718f5bd48c78..a839079053f25d 100755 --- a/packager/launchChromeDevTools.applescript +++ b/packager/launchChromeDevTools.applescript @@ -11,7 +11,6 @@ on run argv set theURL to item 1 of argv tell application "Chrome" - activate if (count every window) = 0 then make new window @@ -40,6 +39,7 @@ on run argv set theWindow's active tab index to theTabIndex else tell window 1 + activate make new tab with properties {URL:theURL} end tell end if diff --git a/packager/packager.js b/packager/packager.js index 7dd013c67c7d01..f2d526e06bcbb7 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -66,6 +66,9 @@ if (options.projectRoots) { if (__dirname.match(/node_modules\/react-native\/packager$/)) { // packager is running from node_modules of another project options.projectRoots = [path.resolve(__dirname, '../../..')]; + } else if (__dirname.match(/Pods\/React\/packager$/)) { + // packager is running from node_modules of another project + options.projectRoots = [path.resolve(__dirname, '../../..')]; } else { options.projectRoots = [path.resolve(__dirname, '..')]; } @@ -83,11 +86,15 @@ if (options.root) { if (options.assetRoots) { if (!Array.isArray(options.assetRoots)) { - options.assetRoots = options.assetRoots.split(','); + options.assetRoots = options.assetRoots.split(',').map(function (dir) { + return path.resolve(process.cwd(), dir); + }); } } else { if (__dirname.match(/node_modules\/react-native\/packager$/)) { options.assetRoots = [path.resolve(__dirname, '../../..')]; + } else if (__dirname.match(/Pods\/React\/packager$/)) { + options.assetRoots = [path.resolve(__dirname, '../../..')]; } else { options.assetRoots = [path.resolve(__dirname, '..')]; } diff --git a/packager/react-packager/.babelrc b/packager/react-packager/.babelrc new file mode 100644 index 00000000000000..c1b12d8164af16 --- /dev/null +++ b/packager/react-packager/.babelrc @@ -0,0 +1,24 @@ +// Keep in sync with packager/transformer.js +{ + "retainLines": true, + "compact": true, + "comments": false, + "whitelist": [ + "es6.arrowFunctions", + "es6.blockScoping", + // This is the only place where we differ from transformer.js + "es6.constants", + "es6.classes", + "es6.destructuring", + "es6.parameters.rest", + "es6.properties.computed", + "es6.properties.shorthand", + "es6.spread", + "es6.templateLiterals", + "es7.trailingFunctionCommas", + "es7.objectRestSpread", + "flow", + "react" + ], + "sourceMaps": false +} diff --git a/packager/react-packager/__mocks__/bluebird.js b/packager/react-packager/__mocks__/bluebird.js deleted file mode 100644 index 9ac6e14b62fe2e..00000000000000 --- a/packager/react-packager/__mocks__/bluebird.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -jest.autoMockOff(); -module.exports = require.requireActual('bluebird'); -jest.autoMockOn(); diff --git a/packager/react-packager/index.js b/packager/react-packager/index.js index 6be111997cca48..d4ea0dd391fbce 100644 --- a/packager/react-packager/index.js +++ b/packager/react-packager/index.js @@ -8,6 +8,10 @@ */ 'use strict'; +require('babel/register')({ + only: /react-packager\/src/ +}); + useGracefulFs(); var Activity = require('./src/Activity'); diff --git a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js index c6acc6a84c6226..95916c9eaf63bc 100644 --- a/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js +++ b/packager/react-packager/src/AssetServer/__tests__/AssetServer-test.js @@ -8,7 +8,7 @@ jest .mock('crypto') .mock('fs'); -var Promise = require('bluebird'); +var Promise = require('promise'); describe('AssetServer', function() { var AssetServer; diff --git a/packager/react-packager/src/AssetServer/index.js b/packager/react-packager/src/AssetServer/index.js index 9bae26823a539d..2cd365fdc315b3 100644 --- a/packager/react-packager/src/AssetServer/index.js +++ b/packager/react-packager/src/AssetServer/index.js @@ -11,13 +11,13 @@ var declareOpts = require('../lib/declareOpts'); var getAssetDataFromName = require('../lib/getAssetDataFromName'); var path = require('path'); -var Promise = require('bluebird'); +var Promise = require('promise'); var fs = require('fs'); var crypto = require('crypto'); -var lstat = Promise.promisify(fs.lstat); -var readDir = Promise.promisify(fs.readdir); -var readFile = Promise.promisify(fs.readFile); +var stat = Promise.denodeify(fs.stat); +var readDir = Promise.denodeify(fs.readdir); +var readFile = Promise.denodeify(fs.readFile); module.exports = AssetServer; @@ -56,12 +56,15 @@ AssetServer.prototype._getAssetRecord = function(assetPath) { this._roots, path.dirname(assetPath) ).then(function(dir) { - return [ + return Promise.all([ dir, readDir(dir), - ]; - }).spread(function(dir, files) { + ]); + }).then(function(res) { + var dir = res[0]; + var files = res[1]; var assetData = getAssetDataFromName(filename); + var map = buildAssetMap(dir, files); var record = map[assetData.assetName]; @@ -98,14 +101,14 @@ AssetServer.prototype.getAssetData = function(assetPath) { return Promise.all( record.files.map(function(file) { - return lstat(file); + return stat(file); }) ); }).then(function(stats) { var hash = crypto.createHash('md5'); - stats.forEach(function(stat) { - hash.update(stat.mtime.getTime().toString()); + stats.forEach(function(fstat) { + hash.update(fstat.mtime.getTime().toString()); }); data.hash = hash.digest('hex'); @@ -114,21 +117,23 @@ AssetServer.prototype.getAssetData = function(assetPath) { }; function findRoot(roots, dir) { - return Promise.some( + return Promise.all( roots.map(function(root) { var absPath = path.join(root, dir); - return lstat(absPath).then(function(stat) { - if (!stat.isDirectory()) { - throw new Error('Looking for dirs'); - } - stat._path = absPath; - return stat; + return stat(absPath).then(function(fstat) { + return {path: absPath, isDirectory: fstat.isDirectory()}; + }, function (err) { + return {path: absPath, isDirectory: false}; }); - }), - 1 - ).spread( - function(stat) { - return stat._path; + }) + ).then( + function(stats) { + for (var i = 0; i < stats.length; i++) { + if (stats[i].isDirectory) { + return stats[i].path; + } + } + throw new Error('Could not find any directories'); } ); } diff --git a/packager/react-packager/src/DependencyResolver/AssetModule.js b/packager/react-packager/src/DependencyResolver/AssetModule.js new file mode 100644 index 00000000000000..bfe4b6f8894d69 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/AssetModule.js @@ -0,0 +1,46 @@ +'use strict'; + +const Module = require('./Module'); +const Promise = require('promise'); +const getAssetDataFromName = require('../lib/getAssetDataFromName'); + +class AssetModule extends Module { + + isHaste() { + return Promise.resolve(false); + } + + getDependencies() { + return Promise.resolve([]); + } + + _read() { + return Promise.resolve({}); + } + + getName() { + return super.getName().then(id => { + const {name, type} = getAssetDataFromName(this.path); + return id.replace(/\/[^\/]+$/, `/${name}.${type}`); + }); + } + + getPlainObject() { + return this.getName().then(name => this.addReference({ + path: this.path, + isJSON: false, + isAsset: true, + isAsset_DEPRECATED: false, + isPolyfill: false, + resolution: getAssetDataFromName(this.path).resolution, + id: name, + dependencies: [], + })); + } + + hash() { + return `AssetModule : ${this.path}`; + } +} + +module.exports = AssetModule; diff --git a/packager/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js b/packager/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js new file mode 100644 index 00000000000000..fd4cb70818a434 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/AssetModule_DEPRECATED.js @@ -0,0 +1,40 @@ +'use strict'; + +const Module = require('./Module'); +const Promise = require('promise'); +const getAssetDataFromName = require('../lib/getAssetDataFromName'); + +class AssetModule_DEPRECATED extends Module { + isHaste() { + return Promise.resolve(false); + } + + getName() { + return Promise.resolve(this.name); + } + + getDependencies() { + return Promise.resolve([]); + } + + getPlainObject() { + const {name, resolution} = getAssetDataFromName(this.path); + + return Promise.resolve(this.addReference({ + path: this.path, + id: `image!${name}`, + resolution, + isAsset_DEPRECATED: true, + dependencies: [], + isJSON: false, + isPolyfill: false, + isAsset: false, + })); + } + + hash() { + return `AssetModule_DEPRECATED : ${this.path}`; + } +} + +module.exports = AssetModule_DEPRECATED; diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js new file mode 100644 index 00000000000000..dbe7dc3aa46fc9 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/__tests__/DependencyGraph-test.js @@ -0,0 +1,3241 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest + .dontMock('../index') + .dontMock('crypto') + .dontMock('absolute-path') + .dontMock('../docblock') + .dontMock('../../replacePatterns') + .dontMock('../../../lib/getAssetDataFromName') + .dontMock('../../fastfs') + .dontMock('../../AssetModule_DEPRECATED') + .dontMock('../../AssetModule') + .dontMock('../../Module') + .dontMock('../../Package') + .dontMock('../../ModuleCache'); + +jest.mock('fs'); + +describe('DependencyGraph', function() { + var DependencyGraph; + var fileWatcher; + var fs; + + beforeEach(function() { + fs = require('fs'); + DependencyGraph = require('../index'); + + fileWatcher = { + on: function() { + return this; + } + }; + }); + + describe('getOrderedDependencies', function() { + pit('should get dependencies', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")' + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined + }, + { + id: 'a', + path: '/root/a.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined + }, + ]); + }); + }); + + pit('should get dependencies with the correct extensions', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")' + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + 'a.js.orig': [ + '/**', + ' * @providesModule a', + ' */', + ].join('\n'), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a', + path: '/root/a.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should get json dependencies', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'package.json': JSON.stringify({ + name: 'package' + }), + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./a.json")', + 'require("./b")' + ].join('\n'), + 'a.json': JSON.stringify({}), + 'b.json': JSON.stringify({}), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['./a.json', './b'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'package/a.json', + isJSON: true, + path: '/root/a.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'package/b.json', + isJSON: true, + path: '/root/b.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should get dependencies with deprecated assets', 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, + assetExts: ['png', 'jpg'], + assetRoots_DEPRECATED: ['/root/imgs'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['image!a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'image!a', + path: '/root/imgs/a.png', + dependencies: [], + isAsset_DEPRECATED: true, + resolution: 1, + isAsset: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + + pit('should get dependencies with relative assets', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./imgs/a.png")' + ].join('\n'), + 'imgs': { + 'a.png': '' + }, + 'package.json': JSON.stringify({ + name: 'rootPackage' + }), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['./imgs/a.png'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'rootPackage/imgs/a.png', + path: '/root/imgs/a.png', + dependencies: [], + isAsset: true, + resolution: 1, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + + pit('should get dependencies with assets and resolution', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./imgs/a.png");', + 'require("./imgs/b.png");', + 'require("./imgs/c.png");', + ].join('\n'), + 'imgs': { + 'a@1.5x.png': '', + 'b@.7x.png': '', + 'c.png': '', + 'c@2x.png': '', + }, + 'package.json': JSON.stringify({ + name: 'rootPackage' + }), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: [ + './imgs/a.png', + './imgs/b.png', + './imgs/c.png', + ], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'rootPackage/imgs/a.png', + path: '/root/imgs/a@1.5x.png', + resolution: 1.5, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'rootPackage/imgs/b.png', + path: '/root/imgs/b@.7x.png', + resolution: 0.7, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'rootPackage/imgs/c.png', + path: '/root/imgs/c.png', + resolution: 1, + dependencies: [], + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + + pit('Deprecated and relative assets can live together', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./imgs/a.png")', + 'require("image!a")', + ].join('\n'), + 'imgs': { + 'a.png': '' + }, + 'package.json': JSON.stringify({ + name: 'rootPackage' + }), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + assetRoots_DEPRECATED: ['/root/imgs'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['./imgs/a.png', 'image!a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'rootPackage/imgs/a.png', + path: '/root/imgs/a.png', + dependencies: [], + isAsset: true, + resolution: 1, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + }, + { + id: 'image!a', + path: '/root/imgs/a.png', + dependencies: [], + isAsset_DEPRECATED: true, + resolution: 1, + isAsset: false, + isJSON: false, + isPolyfill: false, + }, + ]); + }); + }); + + pit('should get recursive dependencies', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("a")', + ].join('\n'), + 'a.js': [ + '/**', + ' * @providesModule a', + ' */', + 'require("index")', + ].join('\n'), + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'a', + path: '/root/a.js', + dependencies: ['index'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'lol' + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage/")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'lol' + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with packages with a dot in the name', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("sha.js")', + 'require("x.y.z")', + ].join('\n'), + 'sha.js': { + 'package.json': JSON.stringify({ + name: 'sha.js', + main: 'main.js' + }), + 'main.js': 'lol' + }, + 'x.y.z': { + 'package.json': JSON.stringify({ + name: 'x.y.z', + main: 'main.js' + }), + 'main.js': 'lol' + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['sha.js', 'x.y.z'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'sha.js/main.js', + path: '/root/sha.js/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'x.y.z/main.js', + path: '/root/x.y.z/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should default main package to index.js', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': 'require("aPackage")', + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + }), + 'index.js': 'lol', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should resolve using alternative ids', () => { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': 'require("aPackage")', + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + }), + 'index.js': [ + '/**', + ' * @providesModule EpicModule', + ' */', + ].join('\n'), + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'EpicModule', + path: '/root/aPackage/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should default use index.js if main is a dir', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': 'require("aPackage")', + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'lib', + }), + lib: { + 'index.js': 'lol', + }, + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/lib/index.js', + path: '/root/aPackage/lib/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should resolve require to index if it is a dir', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'package.json': JSON.stringify({ + name: 'test', + }), + 'index.js': 'require("./lib/")', + lib: { + 'index.js': 'lol', + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'test/index.js', + path: '/root/index.js', + dependencies: ['./lib/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'test/lib/index.js', + path: '/root/lib/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should resolve require to main if it is a dir w/ a package.json', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'package.json': JSON.stringify({ + name: 'test', + }), + 'index.js': 'require("./lib/")', + lib: { + 'package.json': JSON.stringify({ + 'main': 'main.js', + }), + 'index.js': 'lol', + 'main.js': 'lol', + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'test/index.js', + path: '/root/index.js', + dependencies: ['./lib/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: '/root/lib/main.js', + path: '/root/lib/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should ignore malformed packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + ].join('\n'), + 'aPackage': { + 'package.json': 'lol', + 'main.js': 'lol' + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('can have multiple modules with the same name', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("b")', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule b', + ' */', + ].join('\n'), + 'c.js': [ + '/**', + ' * @providesModule c', + ' */', + ].join('\n'), + 'somedir': { + 'somefile.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("c")', + ].join('\n') + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/somedir/somefile.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/somedir/somefile.js', + dependencies: ['c'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'c', + path: '/root/c.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('providesModule wins when conflict with package', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'b.js': [ + '/**', + ' * @providesModule aPackage', + ' */', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'lol' + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage', + path: '/root/b.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should be forgiving with missing requires', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("lolomg")', + ].join('\n') + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['lolomg'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + } + ]); + }); + }); + + pit('should work with packages with subdirs', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage/subdir/lolynot")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'lol', + 'subdir': { + 'lolynot.js': 'lolynot' + } + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage/subdir/lolynot'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/subdir/lolynot.js', + path: '/root/aPackage/subdir/lolynot.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should work with packages with symlinked subdirs', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'symlinkedPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'lol', + 'subdir': { + 'lolynot.js': 'lolynot' + } + }, + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage/subdir/lolynot")', + ].join('\n'), + 'aPackage': { SYMLINK: '/symlinkedPackage' }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage/subdir/lolynot'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/subdir/lolynot.js', + path: '/root/aPackage/subdir/lolynot.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should work with relative modules in packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'require("./subdir/lolynot")', + 'subdir': { + 'lolynot.js': 'require("../other")' + }, + 'other.js': 'some code' + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: ['./subdir/lolynot'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/subdir/lolynot.js', + path: '/root/aPackage/subdir/lolynot.js', + dependencies: ['../other'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/other.js', + path: '/root/aPackage/other.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should support simple browser field in packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + browser: 'client.js', + }), + 'main.js': 'some other code', + 'client.js': 'some code', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should support browser field in packages w/o .js ext', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js', + browser: 'client', + }), + 'main.js': 'some other code', + 'client.js': 'some code', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should support mapping main in browser field json', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: './main.js', + browser: { + './main.js': './client.js', + }, + }), + 'main.js': 'some other code', + 'client.js': 'some code', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should work do correct browser mapping w/o js ext', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: './main.js', + browser: { + './main': './client.js', + }, + }), + 'main.js': 'some other code', + 'client.js': 'some code', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + + pit('should support browser mapping of files', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: './main.js', + browser: { + './main': './client.js', + './node.js': './not-node.js', + './not-browser': './browser.js', + './dir/server.js': './dir/client', + './hello.js': './bye.js', + }, + }), + 'main.js': 'some other code', + 'client.js': 'require("./node")\nrequire("./dir/server.js")', + 'not-node.js': 'require("./not-browser")', + 'not-browser.js': 'require("./dir/server")', + 'browser.js': 'some browser code', + 'dir': { + 'server.js': 'some node code', + 'client.js': 'require("../hello")', + }, + 'hello.js': 'hello', + 'bye.js': 'bye', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/client.js', + path: '/root/aPackage/client.js', + dependencies: ['./node', './dir/server.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/not-node.js', + path: '/root/aPackage/not-node.js', + dependencies: ['./not-browser'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/browser.js', + path: '/root/aPackage/browser.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/dir/client.js', + path: '/root/aPackage/dir/client.js', + dependencies: ['../hello'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/bye.js', + path: '/root/aPackage/bye.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should support browser mapping for packages', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + browser: { + 'node-package': 'browser-package', + } + }), + 'index.js': 'require("node-package")', + 'node-package': { + 'package.json': JSON.stringify({ + 'name': 'node-package', + }), + 'index.js': 'some node code', + }, + 'browser-package': { + 'package.json': JSON.stringify({ + 'name': 'browser-package', + }), + 'index.js': 'some browser code', + }, + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'aPackage/index.js', + path: '/root/aPackage/index.js', + dependencies: ['node-package'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { id: 'browser-package/index.js', + path: '/root/aPackage/browser-package/index.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + describe('node_modules', function() { + pit('should work with nested node_modules', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 1 module', + }, + } + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 2 module', + }, + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: '/root/node_modules/foo/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: '/root/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('nested node_modules with specific paths', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar/");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar/lol");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 1 module', + 'lol.js': '', + }, + } + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 2 module', + }, + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo', 'bar/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: ['bar/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/lol.js', + path: '/root/node_modules/foo/node_modules/bar/lol.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main.js', + path: '/root/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('nested node_modules with browser field', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar/lol");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + browser: { + './lol': './wow' + } + }), + 'main.js': 'bar 1 module', + 'lol.js': '', + 'wow.js': '', + }, + } + }, + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + browser: './main2', + }), + 'main2.js': 'bar 2 module', + }, + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: ['bar/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/lol.js', + path: '/root/node_modules/foo/node_modules/bar/lol.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar/main2.js', + path: '/root/node_modules/bar/main2.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('node_modules should support multi level', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("bar");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': '', + }, + }, + 'path': { + 'to': { + 'bar.js': [ + '/**', + ' * @providesModule bar', + ' */', + 'require("foo")', + ].join('\n'), + }, + 'node_modules': {}, + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar', + path: '/root/path/to/bar.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should selectively ignore providesModule in node_modules', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("shouldWork");', + 'require("dontWork");', + 'require("wontWork");', + ].join('\n'), + 'node_modules': { + 'react-tools': { + 'package.json': JSON.stringify({ + name: 'react-tools', + main: 'main.js', + }), + 'main.js': [ + '/**', + ' * @providesModule shouldWork', + ' */', + 'require("submodule");', + ].join('\n'), + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js':[ + '/**', + ' * @providesModule dontWork', + ' */', + 'hi();', + ].join('\n'), + }, + 'submodule': { + 'package.json': JSON.stringify({ + name: 'submodule', + main: 'main.js', + }), + 'main.js': 'log()', + }, + } + }, + 'ember': { + 'package.json': JSON.stringify({ + name: 'ember', + main: 'main.js', + }), + 'main.js':[ + '/**', + ' * @providesModule wontWork', + ' */', + 'hi();', + ].join('\n'), + }, + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['shouldWork', 'dontWork', 'wontWork'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'shouldWork', + path: '/root/node_modules/react-tools/main.js', + dependencies: ['submodule'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'submodule/main.js', + path: '/root/node_modules/react-tools/node_modules/submodule/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should ignore modules it cant find (assumes own require system)', function() { + // For example SourceMap.js implements it's own require system. + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo/lol");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'foo module', + }, + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + + pit('should work with node packages with a .js in the name', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("sha.js")', + ].join('\n'), + 'node_modules': { + 'sha.js': { + 'package.json': JSON.stringify({ + name: 'sha.js', + main: 'main.js' + }), + 'main.js': 'lol' + } + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['sha.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'sha.js/main.js', + path: '/root/node_modules/sha.js/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + describe('file watch updating', function() { + var triggerFileChange; + var mockStat = { + isDirectory: () => false + }; + + beforeEach(function() { + var callbacks = []; + triggerFileChange = (...args) => + callbacks.map(callback => callback(...args)); + + fileWatcher = { + on: function(eventType, callback) { + if (eventType !== 'all') { + throw new Error('Can only handle "all" event in watcher.'); + } + callbacks.push(callback); + return this; + } + }; + }); + + pit('updates module dependencies', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function() { + filesystem.root['index.js'] = + filesystem.root['index.js'].replace('require("foo")', ''); + triggerFileChange('change', 'index.js', root, mockStat); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file change', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function() { + filesystem.root['index.js'] = + filesystem.root['index.js'].replace('require("foo")', ''); + triggerFileChange('change', 'index.js', root, mockStat); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file delete', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function() { + delete filesystem.root.foo; + triggerFileChange('delete', 'foo.js', root); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage', 'foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file add', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function() { + filesystem.root['bar.js'] = [ + '/**', + ' * @providesModule bar', + ' */', + 'require("foo")' + ].join('\n'); + triggerFileChange('add', 'bar.js', root, mockStat); + + filesystem.root.aPackage['main.js'] = 'require("bar")'; + triggerFileChange('change', 'aPackage/main.js', root, mockStat); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage', 'foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + }, + { + id: 'bar', + path: '/root/bar.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo', + path: '/root/foo.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on deprecated asset add', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("image!foo")' + ].join('\n'), + }, + }); + + var dgraph = new DependencyGraph({ + roots: [root], + assetRoots_DEPRECATED: [root], + assetExts: ['png'], + fileWatcher: fileWatcher, + }); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['image!foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + } + ]); + + filesystem.root['foo.png'] = ''; + triggerFileChange('add', 'foo.png', root, mockStat); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + expect(deps2) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['image!foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'image!foo', + path: '/root/foo.png', + dependencies: [], + isAsset_DEPRECATED: true, + resolution: 1, + isAsset: false, + isJSON: false, + isPolyfill: false, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on relative asset add', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("./foo.png")' + ].join('\n'), + 'package.json': JSON.stringify({ + name: 'aPackage' + }), + }, + }); + + var dgraph = new DependencyGraph({ + roots: [root], + assetExts: ['png'], + fileWatcher: fileWatcher, + }); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { id: 'index', + path: '/root/index.js', + dependencies: ['./foo.png'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + } + ]); + + filesystem.root['foo.png'] = ''; + triggerFileChange('add', 'foo.png', root, mockStat); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + expect(deps2) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['./foo.png'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/foo.png', + path: '/root/foo.png', + dependencies: [], + isAsset: true, + resolution: 1, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('runs changes through ignore filter', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + ignoreFilePath: function(filePath) { + if (filePath === '/root/bar.js') { + return true; + } + return false; + } + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function() { + filesystem.root['bar.js'] = [ + '/**', + ' * @providesModule bar', + ' */', + 'require("foo")' + ].join('\n'); + triggerFileChange('add', 'bar.js', root, mockStat); + + filesystem.root.aPackage['main.js'] = 'require("bar")'; + triggerFileChange('change', 'aPackage/main.js', root, mockStat); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage', 'foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo', + path: '/root/foo.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('should ignore directory updates', function() { + var root = '/root'; + fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + 'require("foo")' + ].join('\n'), + 'foo.js': [ + '/**', + ' * @providesModule foo', + ' */', + 'require("aPackage")' + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function() { + triggerFileChange('change', 'aPackage', '/root', { + isDirectory: function(){ return true; } + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage', 'foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo', + path: '/root/foo.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('updates package.json', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function() { + filesystem.root['index.js'] = filesystem.root['index.js'].replace(/aPackage/, 'bPackage'); + triggerFileChange('change', 'index.js', root, mockStat); + + filesystem.root.aPackage['package.json'] = JSON.stringify({ + name: 'bPackage', + main: 'main.js', + }); + triggerFileChange('change', 'package.json', '/root/aPackage', mockStat); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['bPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bPackage/main.js', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('changes to browser field', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + 'browser.js': 'browser', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function() { + filesystem.root.aPackage['package.json'] = JSON.stringify({ + name: 'aPackage', + main: 'main.js', + browser: 'browser.js', + }); + triggerFileChange('change', 'package.json', '/root/aPackage', mockStat); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/browser.js', + path: '/root/aPackage/browser.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('removes old package from cache', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("aPackage")', + ].join('\n'), + 'aPackage': { + 'package.json': JSON.stringify({ + name: 'aPackage', + main: 'main.js' + }), + 'main.js': 'main', + 'browser.js': 'browser', + } + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function() { + filesystem.root.aPackage['package.json'] = JSON.stringify({ + name: 'bPackage', + main: 'main.js', + }); + triggerFileChange('change', 'package.json', '/root/aPackage', mockStat); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('should update node package changes', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'require("bar");\nfoo module', + 'node_modules': { + 'bar': { + 'package.json': JSON.stringify({ + name: 'bar', + main: 'main.js', + }), + 'main.js': 'bar 1 module', + }, + } + }, + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(deps) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar/main.js', + path: '/root/node_modules/foo/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + + filesystem.root.node_modules.foo['main.js'] = 'lol'; + triggerFileChange('change', 'main.js', '/root/node_modules/foo', mockStat); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + expect(deps2) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/main.js', + path: '/root/node_modules/foo/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('should update node package main changes', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ + 'root': { + 'index.js': [ + '/**', + ' * @providesModule index', + ' */', + 'require("foo");', + ].join('\n'), + 'node_modules': { + 'foo': { + 'package.json': JSON.stringify({ + name: 'foo', + main: 'main.js', + }), + 'main.js': 'foo module', + 'browser.js': 'foo module', + }, + }, + } + }); + + var dgraph = new DependencyGraph({ + roots: [root], + fileWatcher: fileWatcher, + assetExts: ['png', 'jpg'], + }); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + filesystem.root.node_modules.foo['package.json'] = JSON.stringify({ + name: 'foo', + main: 'main.js', + browser: 'browser.js', + }); + triggerFileChange('change', 'package.json', '/root/node_modules/foo', mockStat); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + expect(deps2) + .toEqual([ + { + id: 'index', + path: '/root/index.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/browser.js', + path: '/root/node_modules/foo/browser.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + }); +}); diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/docblock.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/docblock.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/DependencyGraph/docblock.js rename to packager/react-packager/src/DependencyResolver/DependencyGraph/docblock.js diff --git a/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js new file mode 100644 index 00000000000000..63ba7875e02789 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/DependencyGraph/index.js @@ -0,0 +1,556 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const path = require('path'); +const Fastfs = require('../fastfs'); +const ModuleCache = require('../ModuleCache'); +const AssetModule_DEPRECATED = require('../AssetModule_DEPRECATED'); +const declareOpts = require('../../lib/declareOpts'); +const isAbsolutePath = require('absolute-path'); +const debug = require('debug')('DependencyGraph'); +const getAssetDataFromName = require('../../lib/getAssetDataFromName'); +const util = require('util'); +const Promise = require('promise'); +const _ = require('underscore'); + +const validateOpts = declareOpts({ + roots: { + type: 'array', + required: true, + }, + ignoreFilePath: { + type: 'function', + default: function(){} + }, + fileWatcher: { + type: 'object', + required: true, + }, + assetRoots_DEPRECATED: { + type: 'array', + default: [], + }, + assetExts: { + type: 'array', + required: true, + }, + providesModuleNodeModules: { + type: 'array', + default: [ + 'react-tools', + 'react-native', + // Parse requires AsyncStorage. They will + // change that to require('react-native') which + // should work after this release and we can + // remove it from here. + 'parse', + ], + }, +}); + +class DependencyGraph { + constructor(options) { + this._opts = validateOpts(options); + this._hasteMap = Object.create(null); + this._immediateResolutionCache = Object.create(null); + this.load(); + } + + load() { + if (this._loading) { + return this._loading; + } + + const modulePattern = new RegExp( + '\.(' + ['js', 'json'].concat(this._assetExts).join('|') + ')$' + ); + + this._fastfs = new Fastfs(this._opts.roots,this._opts.fileWatcher, { + pattern: modulePattern, + ignore: this._opts.ignoreFilePath, + }); + + this._fastfs.on('change', this._processFileChange.bind(this)); + + this._moduleCache = new ModuleCache(this._fastfs); + + this._loading = Promise.all([ + this._fastfs.build().then(() => this._buildHasteMap()), + this._buildAssetMap_DEPRECATED(), + ]); + + return this._loading; + } + + resolveDependency(fromModule, toModuleName) { + if (fromModule._ref) { + fromModule = fromModule._ref; + } + + const resHash = resolutionHash(fromModule.path, toModuleName); + + if (this._immediateResolutionCache[resHash]) { + return Promise.resolve(this._immediateResolutionCache[resHash]); + } + + const asset_DEPRECATED = this._resolveAsset_DEPRECATED( + fromModule, + toModuleName + ); + if (asset_DEPRECATED) { + return Promise.resolve(asset_DEPRECATED); + } + + const cacheResult = (result) => { + this._immediateResolutionCache[resHash] = result; + return result; + }; + + const forgive = () => { + console.warn( + 'Unable to resolve module %s from %s', + toModuleName, + fromModule.path + ); + return null; + }; + + if (!this._isNodeModulesDir(fromModule.path) + && toModuleName[0] !== '.' && + toModuleName[0] !== '/') { + return this._resolveHasteDependency(fromModule, toModuleName).catch( + () => this._resolveNodeDependency(fromModule, toModuleName) + ).then( + cacheResult, + forgive + ); + } + + return this._resolveNodeDependency(fromModule, toModuleName) + .then( + cacheResult, + forgive + ); + } + + getOrderedDependencies(entryPath) { + return this.load().then(() => { + const absolutePath = path.resolve(this._getAbsolutePath(entryPath)); + + if (absolutePath == null) { + throw new NotFoundError( + 'Cannot find entry file %s in any of the roots: %j', + entryPath, + this._opts.roots + ); + } + + const entry = this._moduleCache.getModule(absolutePath); + const deps = []; + const visited = Object.create(null); + visited[entry.hash()] = true; + + const collect = (mod) => { + deps.push(mod); + return mod.getDependencies().then( + depNames => Promise.all( + depNames.map(name => this.resolveDependency(mod, name)) + ).then((dependencies) => [depNames, dependencies]) + ).then(([depNames, dependencies]) => { + let p = Promise.resolve(); + dependencies.forEach((modDep, i) => { + if (modDep == null) { + debug( + 'WARNING: Cannot find required module `%s` from module `%s`', + depNames[i], + mod.path + ); + return; + } + + p = p.then(() => { + if (!visited[modDep.hash()]) { + visited[modDep.hash()] = true; + return collect(modDep); + } + return null; + }); + }); + + return p; + }); + }; + + return collect(entry) + .then(() => Promise.all(deps.map(dep => dep.getPlainObject()))); + }); + } + + _getAbsolutePath(filePath) { + if (isAbsolutePath(filePath)) { + return filePath; + } + + for (let i = 0; i < this._opts.roots.length; i++) { + const root = this._opts.roots[i]; + const absPath = path.join(root, filePath); + if (this._fastfs.fileExists(absPath)) { + return absPath; + } + } + + return null; + } + + _resolveHasteDependency(fromModule, toModuleName) { + toModuleName = normalizePath(toModuleName); + + let p = fromModule.getPackage(); + if (p) { + p = p.redirectRequire(toModuleName); + } else { + p = Promise.resolve(toModuleName); + } + + return p.then((realModuleName) => { + let dep = this._hasteMap[realModuleName]; + + if (dep && dep.type === 'Module') { + return dep; + } + + let packageName = realModuleName; + + while (packageName && packageName !== '.') { + dep = this._hasteMap[packageName]; + if (dep && dep.type === 'Package') { + break; + } + packageName = path.dirname(packageName); + } + + if (dep && dep.type === 'Package') { + const potentialModulePath = path.join( + dep.root, + path.relative(packageName, realModuleName) + ); + return this._loadAsFile(potentialModulePath) + .catch(() => this._loadAsDir(potentialModulePath)); + } + + throw new Error('Unable to resolve dependency'); + }); + } + + _redirectRequire(fromModule, modulePath) { + return Promise.resolve(fromModule.getPackage()).then(p => { + if (p) { + return p.redirectRequire(modulePath); + } + return modulePath; + }); + } + + _resolveNodeDependency(fromModule, toModuleName) { + if (toModuleName[0] === '.' || toModuleName[1] === '/') { + const potentialModulePath = isAbsolutePath(toModuleName) ? + toModuleName : + path.join(path.dirname(fromModule.path), toModuleName); + return this._redirectRequire(fromModule, potentialModulePath).then( + realModuleName => this._loadAsFile(realModuleName) + .catch(() => this._loadAsDir(realModuleName)) + ); + } else { + return this._redirectRequire(fromModule, toModuleName).then( + realModuleName => { + const searchQueue = []; + for (let currDir = path.dirname(fromModule.path); + currDir !== '/'; + currDir = path.dirname(currDir)) { + searchQueue.push( + path.join(currDir, 'node_modules', realModuleName) + ); + } + + let p = Promise.reject(new Error('Node module not found')); + searchQueue.forEach(potentialModulePath => { + p = p.catch( + () => this._loadAsFile(potentialModulePath) + ).catch( + () => this._loadAsDir(potentialModulePath) + ); + }); + + return p; + }); + } + } + + _resolveAsset_DEPRECATED(fromModule, toModuleName) { + if (this._assetMap_DEPRECATED != null) { + const assetMatch = toModuleName.match(/^image!(.+)/); + // Process DEPRECATED global asset requires. + if (assetMatch && assetMatch[1]) { + if (!this._assetMap_DEPRECATED[assetMatch[1]]) { + debug('WARINING: Cannot find asset:', assetMatch[1]); + return null; + } + return this._assetMap_DEPRECATED[assetMatch[1]]; + } + } + return null; + } + + _isAssetFile(file) { + return this._opts.assetExts.indexOf(extname(file)) !== -1; + } + + _loadAsFile(potentialModulePath) { + return Promise.resolve().then(() => { + if (this._isAssetFile(potentialModulePath)) { + const {name, type} = getAssetDataFromName(potentialModulePath); + const pattern = new RegExp('^' + name + '(@[\\d\\.]+x)?\\.' + type); + // We arbitrarly grab the first one, because scale selection + // will happen somewhere + const [assetFile] = this._fastfs.matches( + path.dirname(potentialModulePath), + pattern + ); + + if (assetFile) { + return this._moduleCache.getAssetModule(assetFile); + } + } + + let file; + if (this._fastfs.fileExists(potentialModulePath)) { + file = potentialModulePath; + } else if (this._fastfs.fileExists(potentialModulePath + '.js')) { + file = potentialModulePath + '.js'; + } else if (this._fastfs.fileExists(potentialModulePath + '.json')) { + file = potentialModulePath + '.json'; + } else { + throw new Error(`File ${potentialModulePath} doesnt exist`); + } + + return this._moduleCache.getModule(file); + }); + } + + _loadAsDir(potentialDirPath) { + return Promise.resolve().then(() => { + if (!this._fastfs.dirExists(potentialDirPath)) { + throw new Error(`Invalid directory ${potentialDirPath}`); + } + + const packageJsonPath = path.join(potentialDirPath, 'package.json'); + if (this._fastfs.fileExists(packageJsonPath)) { + return this._moduleCache.getPackage(packageJsonPath) + .getMain().then( + (main) => this._loadAsFile(main).catch( + () => this._loadAsDir(main) + ) + ); + } + + return this._loadAsFile(path.join(potentialDirPath, 'index')); + }); + } + + _buildHasteMap() { + let promises = this._fastfs.findFilesByExt('js', { + ignore: (file) => this._isNodeModulesDir(file) + }).map(file => this._processHasteModule(file)); + + promises = promises.concat( + this._fastfs.findFilesByName('package.json', { + ignore: (file) => this._isNodeModulesDir(file) + }).map(file => this._processHastePackage(file)) + ); + + return Promise.all(promises); + } + + _processHasteModule(file) { + const module = this._moduleCache.getModule(file); + return module.isHaste().then( + isHaste => isHaste && module.getName() + .then(name => this._updateHasteMap(name, module)) + ); + } + + _processHastePackage(file) { + file = path.resolve(file); + const p = this._moduleCache.getPackage(file, this._fastfs); + return p.isHaste() + .then(isHaste => isHaste && p.getName() + .then(name => this._updateHasteMap(name, p))) + .catch(e => { + if (e instanceof SyntaxError) { + // Malformed package.json. + return; + } + throw e; + }); + } + + _updateHasteMap(name, mod) { + if (this._hasteMap[name]) { + debug('WARNING: conflicting haste modules: ' + name); + if (mod.type === 'Package' && + this._hasteMap[name].type === 'Module') { + // Modules takes precendence over packages. + return; + } + } + this._hasteMap[name] = mod; + } + + _isNodeModulesDir(file) { + const inNodeModules = file.indexOf('/node_modules/') !== -1; + + if (!inNodeModules) { + return false; + } + + const dirs = this._opts.providesModuleNodeModules; + + for (let i = 0; i < dirs.length; i++) { + const index = file.indexOf(dirs[i]); + if (index !== -1) { + return file.slice(index).indexOf('/node_modules/') !== -1; + } + } + + return true; + } + + _processAsset_DEPRECATED(file) { + let ext = extname(file); + if (this._opts.assetExts.indexOf(ext) !== -1) { + let name = assetName(file, ext); + if (this._assetMap_DEPRECATED[name] != null) { + debug('Conflcting assets', name); + } + + this._assetMap_DEPRECATED[name] = new AssetModule_DEPRECATED(file); + } + } + + _buildAssetMap_DEPRECATED() { + if (this._opts.assetRoots_DEPRECATED == null || + this._opts.assetRoots_DEPRECATED.length === 0) { + return Promise.resolve(); + } + + this._assetMap_DEPRECATED = Object.create(null); + + const pattern = new RegExp( + '\.(' + this._opts.assetExts.join('|') + ')$' + ); + + const fastfs = new Fastfs( + this._opts.assetRoots_DEPRECATED, + this._opts.fileWatcher, + { pattern, ignore: this._opts.ignoreFilePath } + ); + + fastfs.on('change', this._processAssetChange_DEPRECATED.bind(this)); + + return fastfs.build().then( + () => fastfs.findFilesByExts(this._opts.assetExts).map( + file => this._processAsset_DEPRECATED(file) + ) + ); + } + + _processAssetChange_DEPRECATED(type, filePath, root, fstat) { + const name = assetName(filePath); + if (type === 'change' || type === 'delete') { + delete this._assetMap_DEPRECATED[name]; + } + + if (type === 'change' || type === 'add') { + this._loading = this._loading.then( + () => this._processAsset_DEPRECATED(path.join(root, filePath)) + ); + } + } + + _processFileChange(type, filePath, root, fstat) { + // It's really hard to invalidate the right module resolution cache + // so we just blow it up with every file change. + this._immediateResolutionCache = Object.create(null); + + const absPath = path.join(root, filePath); + if ((fstat && fstat.isDirectory()) || + this._opts.ignoreFilePath(absPath) || + this._isNodeModulesDir(absPath)) { + return; + } + + if (type === 'delete' || type === 'change') { + _.each(this._hasteMap, (mod, name) => { + if (mod.path === absPath) { + delete this._hasteMap[name]; + } + }); + + if (type === 'delete') { + return; + } + } + + if (extname(absPath) === 'js' || extname(absPath) === 'json') { + this._loading = this._loading.then(() => { + if (path.basename(filePath) === 'package.json') { + return this._processHastePackage(absPath); + } else { + return this._processHasteModule(absPath); + } + }); + } + } +} + +function assetName(file, ext) { + return path.basename(file, '.' + ext).replace(/@[\d\.]+x/, ''); +} + +function extname(name) { + return path.extname(name).replace(/^\./, ''); +} + +function resolutionHash(modulePath, depName) { + return `${path.resolve(modulePath)}:${depName}`; +} + +function NotFoundError() { + Error.call(this); + Error.captureStackTrace(this, this.constructor); + var msg = util.format.apply(util, arguments); + this.message = msg; + this.type = this.name = 'NotFoundError'; + this.status = 404; +} + +function normalizePath(modulePath) { + if (path.sep === '/') { + modulePath = path.normalize(modulePath); + } else if (path.posix) { + modulePath = path.posix.normalize(modulePath); + } + + return modulePath.replace(/\/$/, ''); +} + +util.inherits(NotFoundError, Error); + +module.exports = DependencyGraph; diff --git a/packager/react-packager/src/DependencyResolver/Module.js b/packager/react-packager/src/DependencyResolver/Module.js new file mode 100644 index 00000000000000..3ae9354d340eb4 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/Module.js @@ -0,0 +1,133 @@ +'use strict'; + +const Promise = require('promise'); +const docblock = require('./DependencyGraph/docblock'); +const isAbsolutePath = require('absolute-path'); +const path = require('path'); +const replacePatterns = require('./replacePatterns'); + +class Module { + + constructor(file, fastfs, moduleCache) { + if (!isAbsolutePath(file)) { + throw new Error('Expected file to be absolute path but got ' + file); + } + + this.path = path.resolve(file); + this.type = 'Module'; + + this._fastfs = fastfs; + this._moduleCache = moduleCache; + } + + isHaste() { + return this._read().then(data => !!data.id); + } + + getName() { + return this._read().then(data => { + if (data.id) { + return data.id; + } + + const p = this.getPackage(); + + if (!p) { + // Name is full path + return this.path; + } + + return p.getName() + .then(name => { + if (!name) { + return this.path; + } + + return path.join(name, path.relative(p.root, this.path)); + }); + }); + } + + getPackage() { + return this._moduleCache.getPackageForModule(this); + } + + getDependencies() { + return this._read().then(data => data.dependencies); + } + + _read() { + if (!this._reading) { + this._reading = this._fastfs.readFile(this.path).then(content => { + const data = {}; + const moduleDocBlock = docblock.parseAsObject(content); + if (moduleDocBlock.providesModule || moduleDocBlock.provides) { + data.id = /^(\S*)/.exec( + moduleDocBlock.providesModule || moduleDocBlock.provides + )[1]; + } + + // Ignore requires in generated code. An example of this is prebuilt + // files like the SourceMap library. + if ('extern' in moduleDocBlock) { + data.dependencies = []; + } else { + data.dependencies = extractRequires(content); + } + + return data; + }); + } + + return this._reading; + } + + getPlainObject() { + return Promise.all([ + this.getName(), + this.getDependencies(), + ]).then(([name, dependencies]) => this.addReference({ + path: this.path, + isJSON: path.extname(this.path) === '.json', + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + resolution: undefined, + id: name, + dependencies + })); + } + + hash() { + return `Module : ${this.path}`; + } + + addReference(obj) { + Object.defineProperty(obj, '_ref', { value: this }); + return obj; + } +} + +/** + * Extract all required modules from a `code` string. + */ +var blockCommentRe = /\/\*(.|\n)*?\*\//g; +var lineCommentRe = /\/\/.+(\n|$)/g; +function extractRequires(code /*: string*/) /*: Array*/ { + var deps = []; + + code + .replace(blockCommentRe, '') + .replace(lineCommentRe, '') + .replace(replacePatterns.IMPORT_RE, (match, pre, quot, dep, post) => { + deps.push(dep); + return match; + }) + .replace(replacePatterns.REQUIRE_RE, function(match, pre, quot, dep, post) { + deps.push(dep); + }); + + return deps; +} + +module.exports = Module; diff --git a/packager/react-packager/src/DependencyResolver/ModuleCache.js b/packager/react-packager/src/DependencyResolver/ModuleCache.js new file mode 100644 index 00000000000000..3fa02cc17a7a46 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/ModuleCache.js @@ -0,0 +1,72 @@ +'use strict'; + +const AssetModule = require('./AssetModule'); +const Package = require('./Package'); +const Module = require('./Module'); +const path = require('path'); + +class ModuleCache { + + constructor(fastfs) { + this._moduleCache = Object.create(null); + this._packageCache = Object.create(null); + this._fastfs = fastfs; + fastfs.on('change', this._processFileChange.bind(this)); + } + + getModule(filePath) { + filePath = path.resolve(filePath); + if (!this._moduleCache[filePath]) { + this._moduleCache[filePath] = new Module(filePath, this._fastfs, this); + } + return this._moduleCache[filePath]; + } + + getAssetModule(filePath) { + filePath = path.resolve(filePath); + if (!this._moduleCache[filePath]) { + this._moduleCache[filePath] = new AssetModule( + filePath, + this._fastfs, + this + ); + } + return this._moduleCache[filePath]; + } + + getPackage(filePath) { + filePath = path.resolve(filePath); + if (!this._packageCache[filePath]){ + this._packageCache[filePath] = new Package(filePath, this._fastfs); + } + return this._packageCache[filePath]; + } + + getPackageForModule(module) { + // TODO(amasad): use ES6 Map. + if (module.__package) { + if (this._packageCache[module.__package]) { + return this._packageCache[module.__package]; + } else { + delete module.__package; + } + } + + const packagePath = this._fastfs.closest(module.path, 'package.json'); + + if (!packagePath) { + return null; + } + + module.__package = packagePath; + return this.getPackage(packagePath); + } + + _processFileChange(type, filePath, root) { + const absPath = path.join(root, filePath); + delete this._moduleCache[absPath]; + delete this._packageCache[absPath]; + } +} + +module.exports = ModuleCache; diff --git a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js b/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js deleted file mode 100644 index 90db1c4ade5f54..00000000000000 --- a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -function ModuleDescriptor(fields) { - if (!fields.id) { - throw new Error('Missing required fields id'); - } - this.id = fields.id; - - if (!fields.path) { - throw new Error('Missing required fields path'); - } - this.path = fields.path; - - if (!fields.dependencies) { - throw new Error('Missing required fields dependencies'); - } - this.dependencies = fields.dependencies; - - this.resolveDependency = fields.resolveDependency; - - this.entry = fields.entry || false; - - this.isPolyfill = fields.isPolyfill || false; - - this.isAsset_DEPRECATED = fields.isAsset_DEPRECATED || false; - this.isAsset = fields.isAsset || false; - - if (this.isAsset_DEPRECATED && this.isAsset) { - throw new Error('Cannot be an asset and a deprecated asset'); - } - - this.resolution = fields.resolution; - - if (this.isAsset && isNaN(this.resolution)) { - throw new Error('Expected resolution to be a number for asset modules'); - } - - this.altId = fields.altId; - - this.isJSON = fields.isJSON; - - this._fields = fields; -} - -ModuleDescriptor.prototype.toJSON = function() { - return { - id: this.id, - path: this.path, - dependencies: this.dependencies - }; -}; - -module.exports = ModuleDescriptor; diff --git a/packager/react-packager/src/DependencyResolver/Package.js b/packager/react-packager/src/DependencyResolver/Package.js new file mode 100644 index 00000000000000..f52ff42b898c45 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/Package.js @@ -0,0 +1,84 @@ +'use strict'; + +const isAbsolutePath = require('absolute-path'); +const path = require('path'); + +class Package { + + constructor(file, fastfs) { + this.path = path.resolve(file); + this.root = path.dirname(this.path); + this._fastfs = fastfs; + this.type = 'Package'; + } + + getMain() { + return this._read().then(json => { + if (typeof json.browser === 'string') { + return path.join(this.root, json.browser); + } + + let main = json.main || 'index'; + + if (json.browser && typeof json.browser === 'object') { + main = json.browser[main] || + json.browser[main + '.js'] || + json.browser[main + '.json'] || + json.browser[main.replace(/(\.js|\.json)$/, '')] || + main; + } + + return path.join(this.root, main); + }); + } + + isHaste() { + return this._read().then(json => !!json.name); + } + + getName() { + return this._read().then(json => json.name); + } + + redirectRequire(name) { + return this._read().then(json => { + const {browser} = json; + + if (!browser || typeof browser !== 'object') { + return name; + } + + if (name[0] !== '/') { + return browser[name] || name; + } + + if (!isAbsolutePath(name)) { + throw new Error(`Expected ${name} to be absolute path`); + } + + const relPath = './' + path.relative(this.root, name); + const redirect = browser[relPath] || + browser[relPath + '.js'] || + browser[relPath + '.json']; + if (redirect) { + return path.join( + this.root, + redirect + ); + } + + return name; + }); + } + + _read() { + if (!this._reading) { + this._reading = this._fastfs.readFile(this.path) + .then(jsonStr => JSON.parse(jsonStr)); + } + + return this._reading; + } +} + +module.exports = Package; diff --git a/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js b/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js similarity index 65% rename from packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js rename to packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js index 9bc8b8b95c3766..da159b5e9b80f4 100644 --- a/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js +++ b/packager/react-packager/src/DependencyResolver/__tests__/HasteDependencyResolver-test.js @@ -9,17 +9,21 @@ 'use strict'; jest.dontMock('../') - .dontMock('q') - .dontMock('../replacePatterns') - .setMock('../../ModuleDescriptor', function(data) {return data;}); + .dontMock('q') + .dontMock('../replacePatterns'); jest.mock('path'); -var Promise = require('bluebird'); +var Promise = require('promise'); describe('HasteDependencyResolver', function() { var HasteDependencyResolver; + function createModule(o) { + o.getPlainObject = () => Promise.resolve(o); + return o; + } + beforeEach(function() { // For the polyfillDeps require('path').join.mockImpl(function(a, b) { @@ -30,7 +34,10 @@ describe('HasteDependencyResolver', function() { describe('getDependencies', function() { pit('should get dependencies with polyfills', function() { - var module = {id: 'index', path: '/root/index.js', dependencies: ['a']}; + var module = createModule({ + id: 'index', + path: '/root/index.js', dependencies: ['a'] + }); var deps = [module]; var depResolver = new HasteDependencyResolver({ @@ -40,7 +47,7 @@ describe('HasteDependencyResolver', function() { // Is there a better way? How can I mock the prototype instead? var depGraph = depResolver._depGraph; depGraph.getOrderedDependencies.mockImpl(function() { - return deps; + return Promise.resolve(deps); }); depGraph.load.mockImpl(function() { return Promise.resolve(); @@ -113,7 +120,12 @@ describe('HasteDependencyResolver', function() { }); pit('should get dependencies with polyfills', function() { - var module = {id: 'index', path: '/root/index.js', dependencies: ['a']}; + var module = createModule({ + id: 'index', + path: '/root/index.js', + dependencies: ['a'], + }); + var deps = [module]; var depResolver = new HasteDependencyResolver({ @@ -123,7 +135,7 @@ describe('HasteDependencyResolver', function() { // Is there a better way? How can I mock the prototype instead? var depGraph = depResolver._depGraph; depGraph.getOrderedDependencies.mockImpl(function() { - return deps; + return Promise.resolve(deps); }); depGraph.load.mockImpl(function() { return Promise.resolve(); @@ -196,7 +208,11 @@ describe('HasteDependencyResolver', function() { }); pit('should pass in more polyfills', function() { - var module = {id: 'index', path: '/root/index.js', dependencies: ['a']}; + var module = createModule({ + id: 'index', + path: '/root/index.js', + dependencies: ['a'] + }); var deps = [module]; var depResolver = new HasteDependencyResolver({ @@ -207,7 +223,7 @@ describe('HasteDependencyResolver', function() { // Is there a better way? How can I mock the prototype instead? var depGraph = depResolver._depGraph; depGraph.getOrderedDependencies.mockImpl(function() { - return deps; + return Promise.resolve(deps); }); depGraph.load.mockImpl(function() { return Promise.resolve(); @@ -294,7 +310,7 @@ describe('HasteDependencyResolver', function() { }); describe('wrapModule', function() { - it('should resolve modules', function() { + pit('should resolve modules', function() { var depResolver = new HasteDependencyResolver({ projectRoot: '/root', }); @@ -446,162 +462,165 @@ describe('HasteDependencyResolver', function() { depGraph.resolveDependency.mockImpl(function(fromModule, toModuleName) { if (toModuleName === 'x') { - return { + return Promise.resolve(createModule({ id: 'changed' - }; + })); } else if (toModuleName === 'y') { - return { id: 'Y' }; + return Promise.resolve(createModule({ id: 'Y' })); } - return null; + + return Promise.resolve(null); }); - var processedCode = depResolver.wrapModule({ + return depResolver.wrapModule({ id: 'test module', path: '/root/test.js', dependencies: dependencies - }, code); + }, code).then(processedCode => { - expect(processedCode).toEqual([ - '__d(\'test module\',["changed","Y"],function(global,' + - ' require, requireDynamic, requireLazy, module, exports) { ' + - "import'x';", - "import 'changed';", - "import 'changed' ;", - "import Default from 'changed';", - "import * as All from 'changed';", - "import {} from 'changed';", - "import { } from 'changed';", - "import {Foo} from 'changed';", - "import { Foo } from 'changed';", - "import { Foo, } from 'changed';", - "import {Foo as Bar} from 'changed';", - "import { Foo as Bar } from 'changed';", - "import { Foo as Bar, } from 'changed';", - "import { Foo, Bar } from 'changed';", - "import { Foo, Bar, } from 'changed';", - "import { Foo as Bar, Baz } from 'changed';", - "import { Foo as Bar, Baz, } from 'changed';", - "import { Foo, Bar as Baz } from 'changed';", - "import { Foo, Bar as Baz, } from 'changed';", - "import { Foo as Bar, Baz as Qux } from 'changed';", - "import { Foo as Bar, Baz as Qux, } from 'changed';", - "import { Foo, Bar, Baz } from 'changed';", - "import { Foo, Bar, Baz, } from 'changed';", - "import { Foo as Bar, Baz, Qux } from 'changed';", - "import { Foo as Bar, Baz, Qux, } from 'changed';", - "import { Foo, Bar as Baz, Qux } from 'changed';", - "import { Foo, Bar as Baz, Qux, } from 'changed';", - "import { Foo, Bar, Baz as Qux } from 'changed';", - "import { Foo, Bar, Baz as Qux, } from 'changed';", - "import { Foo as Bar, Baz as Qux, Norf } from 'changed';", - "import { Foo as Bar, Baz as Qux, Norf, } from 'changed';", - "import { Foo as Bar, Baz, Qux as Norf } from 'changed';", - "import { Foo as Bar, Baz, Qux as Norf, } from 'changed';", - "import { Foo, Bar as Baz, Qux as Norf } from 'changed';", - "import { Foo, Bar as Baz, Qux as Norf, } from 'changed';", - "import { Foo as Bar, Baz as Qux, Norf as Enuf } from 'changed';", - "import { Foo as Bar, Baz as Qux, Norf as Enuf, } from 'changed';", - "import Default, * as All from 'changed';", - "import Default, { } from 'changed';", - "import Default, { Foo } from 'changed';", - "import Default, { Foo, } from 'changed';", - "import Default, { Foo as Bar } from 'changed';", - "import Default, { Foo as Bar, } from 'changed';", - "import Default, { Foo, Bar } from 'changed';", - "import Default, { Foo, Bar, } from 'changed';", - "import Default, { Foo as Bar, Baz } from 'changed';", - "import Default, { Foo as Bar, Baz, } from 'changed';", - "import Default, { Foo, Bar as Baz } from 'changed';", - "import Default, { Foo, Bar as Baz, } from 'changed';", - "import Default, { Foo as Bar, Baz as Qux } from 'changed';", - "import Default, { Foo as Bar, Baz as Qux, } from 'changed';", - "import Default, { Foo, Bar, Baz } from 'changed';", - "import Default, { Foo, Bar, Baz, } from 'changed';", - "import Default, { Foo as Bar, Baz, Qux } from 'changed';", - "import Default, { Foo as Bar, Baz, Qux, } from 'changed';", - "import Default, { Foo, Bar as Baz, Qux } from 'changed';", - "import Default, { Foo, Bar as Baz, Qux, } from 'changed';", - "import Default, { Foo, Bar, Baz as Qux } from 'changed';", - "import Default, { Foo, Bar, Baz as Qux, } from 'changed';", - "import Default, { Foo as Bar, Baz as Qux, Norf } from 'changed';", - "import Default, { Foo as Bar, Baz as Qux, Norf, } from 'changed';", - "import Default, { Foo as Bar, Baz, Qux as Norf } from 'changed';", - "import Default, { Foo as Bar, Baz, Qux as Norf, } from 'changed';", - "import Default, { Foo, Bar as Baz, Qux as Norf } from 'changed';", - "import Default, { Foo, Bar as Baz, Qux as Norf, } from 'changed';", - "import Default, { Foo as Bar, Baz as Qux, Norf as NoMore } from 'changed';", - "import Default, { Foo as Bar, Baz as Qux, Norf as NoMore, } from 'changed';", - "import Default , { } from 'changed';", - 'import "changed";', - 'import Default from "changed";', - 'import * as All from "changed";', - 'import { } from "changed";', - 'import { Foo } from "changed";', - 'import { Foo, } from "changed";', - 'import { Foo as Bar } from "changed";', - 'import { Foo as Bar, } from "changed";', - 'import { Foo, Bar } from "changed";', - 'import { Foo, Bar, } from "changed";', - 'import { Foo as Bar, Baz } from "changed";', - 'import { Foo as Bar, Baz, } from "changed";', - 'import { Foo, Bar as Baz } from "changed";', - 'import { Foo, Bar as Baz, } from "changed";', - 'import { Foo as Bar, Baz as Qux } from "changed";', - 'import { Foo as Bar, Baz as Qux, } from "changed";', - 'import { Foo, Bar, Baz } from "changed";', - 'import { Foo, Bar, Baz, } from "changed";', - 'import { Foo as Bar, Baz, Qux } from "changed";', - 'import { Foo as Bar, Baz, Qux, } from "changed";', - 'import { Foo, Bar as Baz, Qux } from "changed";', - 'import { Foo, Bar as Baz, Qux, } from "changed";', - 'import { Foo, Bar, Baz as Qux } from "changed";', - 'import { Foo, Bar, Baz as Qux, } from "changed";', - 'import { Foo as Bar, Baz as Qux, Norf } from "changed";', - 'import { Foo as Bar, Baz as Qux, Norf, } from "changed";', - 'import { Foo as Bar, Baz, Qux as Norf } from "changed";', - 'import { Foo as Bar, Baz, Qux as Norf, } from "changed";', - 'import { Foo, Bar as Baz, Qux as Norf } from "changed";', - 'import { Foo, Bar as Baz, Qux as Norf, } from "changed";', - 'import { Foo as Bar, Baz as Qux, Norf as NoMore } from "changed";', - 'import { Foo as Bar, Baz as Qux, Norf as NoMore, } from "changed";', - 'import Default, * as All from "changed";', - 'import Default, { } from "changed";', - 'import Default, { Foo } from "changed";', - 'import Default, { Foo, } from "changed";', - 'import Default, { Foo as Bar } from "changed";', - 'import Default, { Foo as Bar, } from "changed";', - 'import Default, { Foo, Bar } from "changed";', - 'import Default, { Foo, Bar, } from "changed";', - 'import Default, { Foo as Bar, Baz } from "changed";', - 'import Default, { Foo as Bar, Baz, } from "changed";', - 'import Default, { Foo, Bar as Baz } from "changed";', - 'import Default, { Foo, Bar as Baz, } from "changed";', - 'import Default, { Foo as Bar, Baz as Qux } from "changed";', - 'import Default, { Foo as Bar, Baz as Qux, } from "changed";', - 'import Default, { Foo, Bar, Baz } from "changed";', - 'import Default, { Foo, Bar, Baz, } from "changed";', - 'import Default, { Foo as Bar, Baz, Qux } from "changed";', - 'import Default, { Foo as Bar, Baz, Qux, } from "changed";', - 'import Default, { Foo, Bar as Baz, Qux } from "changed";', - 'import Default, { Foo, Bar as Baz, Qux, } from "changed";', - 'import Default, { Foo, Bar, Baz as Qux } from "changed";', - 'import Default, { Foo, Bar, Baz as Qux, } from "changed";', - 'import Default, { Foo as Bar, Baz as Qux, Norf } from "changed";', - 'import Default, { Foo as Bar, Baz as Qux, Norf, } from "changed";', - 'import Default, { Foo as Bar, Baz, Qux as Norf } from "changed";', - 'import Default, { Foo as Bar, Baz, Qux as Norf, } from "changed";', - 'import Default, { Foo, Bar as Baz, Qux as Norf } from "changed";', - 'import Default, { Foo, Bar as Baz, Qux as Norf, } from "changed";', - 'import Default, { Foo as Bar, Baz as Qux, Norf as Enuf } from "changed";', - 'import Default, { Foo as Bar, Baz as Qux, Norf as Enuf, } from "changed";', - 'import Default from "Y";', - 'import * as All from \'z\';', - 'require("changed")', - 'require("Y")', - 'require( \'z\' )', - 'require( "a")', - 'require("b" )});', - ].join('\n')); + expect(processedCode).toEqual([ + '__d(\'test module\',["changed","Y"],function(global,' + + ' require, requireDynamic, requireLazy, module, exports) { ' + + "import'x';", + "import 'changed';", + "import 'changed' ;", + "import Default from 'changed';", + "import * as All from 'changed';", + "import {} from 'changed';", + "import { } from 'changed';", + "import {Foo} from 'changed';", + "import { Foo } from 'changed';", + "import { Foo, } from 'changed';", + "import {Foo as Bar} from 'changed';", + "import { Foo as Bar } from 'changed';", + "import { Foo as Bar, } from 'changed';", + "import { Foo, Bar } from 'changed';", + "import { Foo, Bar, } from 'changed';", + "import { Foo as Bar, Baz } from 'changed';", + "import { Foo as Bar, Baz, } from 'changed';", + "import { Foo, Bar as Baz } from 'changed';", + "import { Foo, Bar as Baz, } from 'changed';", + "import { Foo as Bar, Baz as Qux } from 'changed';", + "import { Foo as Bar, Baz as Qux, } from 'changed';", + "import { Foo, Bar, Baz } from 'changed';", + "import { Foo, Bar, Baz, } from 'changed';", + "import { Foo as Bar, Baz, Qux } from 'changed';", + "import { Foo as Bar, Baz, Qux, } from 'changed';", + "import { Foo, Bar as Baz, Qux } from 'changed';", + "import { Foo, Bar as Baz, Qux, } from 'changed';", + "import { Foo, Bar, Baz as Qux } from 'changed';", + "import { Foo, Bar, Baz as Qux, } from 'changed';", + "import { Foo as Bar, Baz as Qux, Norf } from 'changed';", + "import { Foo as Bar, Baz as Qux, Norf, } from 'changed';", + "import { Foo as Bar, Baz, Qux as Norf } from 'changed';", + "import { Foo as Bar, Baz, Qux as Norf, } from 'changed';", + "import { Foo, Bar as Baz, Qux as Norf } from 'changed';", + "import { Foo, Bar as Baz, Qux as Norf, } from 'changed';", + "import { Foo as Bar, Baz as Qux, Norf as Enuf } from 'changed';", + "import { Foo as Bar, Baz as Qux, Norf as Enuf, } from 'changed';", + "import Default, * as All from 'changed';", + "import Default, { } from 'changed';", + "import Default, { Foo } from 'changed';", + "import Default, { Foo, } from 'changed';", + "import Default, { Foo as Bar } from 'changed';", + "import Default, { Foo as Bar, } from 'changed';", + "import Default, { Foo, Bar } from 'changed';", + "import Default, { Foo, Bar, } from 'changed';", + "import Default, { Foo as Bar, Baz } from 'changed';", + "import Default, { Foo as Bar, Baz, } from 'changed';", + "import Default, { Foo, Bar as Baz } from 'changed';", + "import Default, { Foo, Bar as Baz, } from 'changed';", + "import Default, { Foo as Bar, Baz as Qux } from 'changed';", + "import Default, { Foo as Bar, Baz as Qux, } from 'changed';", + "import Default, { Foo, Bar, Baz } from 'changed';", + "import Default, { Foo, Bar, Baz, } from 'changed';", + "import Default, { Foo as Bar, Baz, Qux } from 'changed';", + "import Default, { Foo as Bar, Baz, Qux, } from 'changed';", + "import Default, { Foo, Bar as Baz, Qux } from 'changed';", + "import Default, { Foo, Bar as Baz, Qux, } from 'changed';", + "import Default, { Foo, Bar, Baz as Qux } from 'changed';", + "import Default, { Foo, Bar, Baz as Qux, } from 'changed';", + "import Default, { Foo as Bar, Baz as Qux, Norf } from 'changed';", + "import Default, { Foo as Bar, Baz as Qux, Norf, } from 'changed';", + "import Default, { Foo as Bar, Baz, Qux as Norf } from 'changed';", + "import Default, { Foo as Bar, Baz, Qux as Norf, } from 'changed';", + "import Default, { Foo, Bar as Baz, Qux as Norf } from 'changed';", + "import Default, { Foo, Bar as Baz, Qux as Norf, } from 'changed';", + "import Default, { Foo as Bar, Baz as Qux, Norf as NoMore } from 'changed';", + "import Default, { Foo as Bar, Baz as Qux, Norf as NoMore, } from 'changed';", + "import Default , { } from 'changed';", + 'import "changed";', + 'import Default from "changed";', + 'import * as All from "changed";', + 'import { } from "changed";', + 'import { Foo } from "changed";', + 'import { Foo, } from "changed";', + 'import { Foo as Bar } from "changed";', + 'import { Foo as Bar, } from "changed";', + 'import { Foo, Bar } from "changed";', + 'import { Foo, Bar, } from "changed";', + 'import { Foo as Bar, Baz } from "changed";', + 'import { Foo as Bar, Baz, } from "changed";', + 'import { Foo, Bar as Baz } from "changed";', + 'import { Foo, Bar as Baz, } from "changed";', + 'import { Foo as Bar, Baz as Qux } from "changed";', + 'import { Foo as Bar, Baz as Qux, } from "changed";', + 'import { Foo, Bar, Baz } from "changed";', + 'import { Foo, Bar, Baz, } from "changed";', + 'import { Foo as Bar, Baz, Qux } from "changed";', + 'import { Foo as Bar, Baz, Qux, } from "changed";', + 'import { Foo, Bar as Baz, Qux } from "changed";', + 'import { Foo, Bar as Baz, Qux, } from "changed";', + 'import { Foo, Bar, Baz as Qux } from "changed";', + 'import { Foo, Bar, Baz as Qux, } from "changed";', + 'import { Foo as Bar, Baz as Qux, Norf } from "changed";', + 'import { Foo as Bar, Baz as Qux, Norf, } from "changed";', + 'import { Foo as Bar, Baz, Qux as Norf } from "changed";', + 'import { Foo as Bar, Baz, Qux as Norf, } from "changed";', + 'import { Foo, Bar as Baz, Qux as Norf } from "changed";', + 'import { Foo, Bar as Baz, Qux as Norf, } from "changed";', + 'import { Foo as Bar, Baz as Qux, Norf as NoMore } from "changed";', + 'import { Foo as Bar, Baz as Qux, Norf as NoMore, } from "changed";', + 'import Default, * as All from "changed";', + 'import Default, { } from "changed";', + 'import Default, { Foo } from "changed";', + 'import Default, { Foo, } from "changed";', + 'import Default, { Foo as Bar } from "changed";', + 'import Default, { Foo as Bar, } from "changed";', + 'import Default, { Foo, Bar } from "changed";', + 'import Default, { Foo, Bar, } from "changed";', + 'import Default, { Foo as Bar, Baz } from "changed";', + 'import Default, { Foo as Bar, Baz, } from "changed";', + 'import Default, { Foo, Bar as Baz } from "changed";', + 'import Default, { Foo, Bar as Baz, } from "changed";', + 'import Default, { Foo as Bar, Baz as Qux } from "changed";', + 'import Default, { Foo as Bar, Baz as Qux, } from "changed";', + 'import Default, { Foo, Bar, Baz } from "changed";', + 'import Default, { Foo, Bar, Baz, } from "changed";', + 'import Default, { Foo as Bar, Baz, Qux } from "changed";', + 'import Default, { Foo as Bar, Baz, Qux, } from "changed";', + 'import Default, { Foo, Bar as Baz, Qux } from "changed";', + 'import Default, { Foo, Bar as Baz, Qux, } from "changed";', + 'import Default, { Foo, Bar, Baz as Qux } from "changed";', + 'import Default, { Foo, Bar, Baz as Qux, } from "changed";', + 'import Default, { Foo as Bar, Baz as Qux, Norf } from "changed";', + 'import Default, { Foo as Bar, Baz as Qux, Norf, } from "changed";', + 'import Default, { Foo as Bar, Baz, Qux as Norf } from "changed";', + 'import Default, { Foo as Bar, Baz, Qux as Norf, } from "changed";', + 'import Default, { Foo, Bar as Baz, Qux as Norf } from "changed";', + 'import Default, { Foo, Bar as Baz, Qux as Norf, } from "changed";', + 'import Default, { Foo as Bar, Baz as Qux, Norf as Enuf } from "changed";', + 'import Default, { Foo as Bar, Baz as Qux, Norf as Enuf, } from "changed";', + 'import Default from "Y";', + 'import * as All from \'z\';', + 'require("changed")', + 'require("Y")', + 'require( \'z\' )', + 'require( "a")', + 'require("b" )', + '});', + ].join('\n')); + }); }); }); }); diff --git a/packager/react-packager/src/DependencyResolver/fastfs.js b/packager/react-packager/src/DependencyResolver/fastfs.js new file mode 100644 index 00000000000000..0053b14e3103a1 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/fastfs.js @@ -0,0 +1,310 @@ +'use strict'; + +const Promise = require('promise'); +const {EventEmitter} = require('events'); + +const _ = require('underscore'); +const debug = require('debug')('DependencyGraph'); +const fs = require('fs'); +const path = require('path'); + +const readDir = Promise.denodeify(fs.readdir); +const readFile = Promise.denodeify(fs.readFile); +const stat = Promise.denodeify(fs.stat); +const hasOwn = Object.prototype.hasOwnProperty; + +class Fastfs extends EventEmitter { + constructor(roots, fileWatcher, {ignore, pattern}) { + super(); + this._fileWatcher = fileWatcher; + this._ignore = ignore; + this._pattern = pattern; + this._roots = roots.map(root => new File(root, { isDir: true })); + this._fastPaths = Object.create(null); + } + + build() { + const queue = this._roots.slice(); + return this._search(queue).then(() => { + this._fileWatcher.on('all', this._processFileChange.bind(this)); + }); + } + + stat(filePath) { + return Promise.resolve().then(() => { + const file = this._getFile(filePath); + return file.stat(); + }); + } + + getAllFiles() { + return _.chain(this._roots) + .map(root => root.getFiles()) + .flatten() + .value(); + } + + findFilesByExt(ext, { ignore }) { + return this.getAllFiles() + .filter( + file => file.ext() === ext && (!ignore || !ignore(file.path)) + ) + .map(file => file.path); + } + + findFilesByExts(exts) { + return this.getAllFiles() + .filter(file => exts.indexOf(file.ext()) !== -1) + .map(file => file.path); + } + + findFilesByName(name, { ignore }) { + return this.getAllFiles() + .filter( + file => path.basename(file.path) === name && + (!ignore || !ignore(file.path)) + ) + .map(file => file.path); + } + + readFile(filePath) { + return this._getFile(filePath).read(); + } + + closest(filePath, name) { + for (let file = this._getFile(filePath).parent; + file; + file = file.parent) { + if (file.children[name]) { + return file.children[name].path; + } + } + return null; + } + + fileExists(filePath) { + const file = this._getFile(filePath); + return file && !file.isDir; + } + + dirExists(filePath) { + const file = this._getFile(filePath); + return file && file.isDir; + } + + matches(dir, pattern) { + let dirFile = this._getFile(dir); + if (!dirFile.isDir) { + throw new Error(`Expected file ${dirFile.path} to be a directory`); + } + + return Object.keys(dirFile.children) + .filter(name => name.match(pattern)) + .map(name => path.join(dirFile.path, name)); + } + + _getRoot(filePath) { + for (let i = 0; i < this._roots.length; i++) { + let possibleRoot = this._roots[i]; + if (isDescendant(possibleRoot.path, filePath)) { + return possibleRoot; + } + } + return null; + } + + _getAndAssertRoot(filePath) { + const root = this._getRoot(filePath); + if (!root) { + throw new Error(`File ${filePath} not found in any of the roots`); + } + return root; + } + + _getFile(filePath) { + filePath = path.normalize(filePath); + if (!hasOwn.call(this._fastPaths, filePath)) { + this._fastPaths[filePath] = this._getAndAssertRoot(filePath).getFileFromPath(filePath); + } + + return this._fastPaths[filePath]; + } + + _add(file) { + this._getAndAssertRoot(file.path).addChild(file); + } + + _search(queue) { + const dir = queue.shift(); + if (!dir) { + return Promise.resolve(); + } + + return readAndStatDir(dir.path).then(([filePaths, stats]) => { + filePaths.forEach((filePath, i) => { + if (this._ignore(filePath)) { + return; + } + + if (stats[i].isDirectory()) { + queue.push( + new File(filePath, { isDir: true, fstat: stats[i] }) + ); + return; + } + + if (filePath.match(this._pattern)) { + this._add(new File(filePath, { fstat: stats[i] })); + } + }); + return this._search(queue); + }); + } + + _processFileChange(type, filePath, root, fstat) { + const absPath = path.join(root, filePath); + if (this._ignore(absPath) || (fstat && fstat.isDirectory())) { + return; + } + + // Make sure this event belongs to one of our roots. + if (!this._getRoot(absPath)) { + return; + } + + if (type === 'delete' || type === 'change') { + const file = this._getFile(absPath); + if (file) { + file.remove(); + } + } + + delete this._fastPaths[path.normalize(absPath)]; + + if (type !== 'delete') { + this._add(new File(absPath, { + isDir: false, + fstat + })); + } + + this.emit('change', type, filePath, root, fstat); + } +} + +class File { + constructor(filePath, {isDir, fstat}) { + this.path = filePath; + this.isDir = Boolean(isDir); + if (this.isDir) { + this.children = Object.create(null); + } + + if (fstat) { + this._stat = Promise.resolve(fstat); + } + } + + read() { + if (!this._read) { + this._read = readFile(this.path, 'utf8'); + } + return this._read; + } + + stat() { + if (!this._stat) { + this._stat = stat(this.path); + } + + return this._stat; + } + + addChild(file) { + const parts = path.relative(this.path, file.path).split(path.sep); + + if (parts.length === 0) { + return; + } + + if (parts.length === 1) { + this.children[parts[0]] = file; + file.parent = this; + } else if (this.children[parts[0]]) { + this.children[parts[0]].addChild(file); + } else { + const dir = new File(path.join(this.path, parts[0]), { isDir: true }); + dir.parent = this; + this.children[parts[0]] = dir; + dir.addChild(file); + } + } + + getFileFromPath(filePath) { + const parts = path.relative(this.path, filePath) + .split(path.sep); + + /*eslint consistent-this:0*/ + let file = this; + for (let i = 0; i < parts.length; i++) { + let fileName = parts[i]; + if (!fileName) { + continue; + } + + if (!file || !file.isDir) { + // File not found. + return null; + } + + file = file.children[fileName]; + } + + return file; + } + + getFiles() { + return _.flatten(_.values(this.children).map(file => { + if (file.isDir) { + return file.getFiles(); + } else { + return file; + } + })); + } + + ext() { + return path.extname(this.path).replace(/^\./, ''); + } + + remove() { + if (!this.parent) { + throw new Error(`No parent to delete ${this.path} from`); + } + + delete this.parent.children[path.basename(this.path)]; + } +} + +function isDescendant(root, child) { + return path.relative(root, child).indexOf('..') !== 0; +} + +function readAndStatDir(dir) { + return readDir(dir) + .then(files => Promise.all(files.map(f => path.join(dir, f)))) + .then(files => Promise.all( + files.map(f => stat(f).catch(handleBrokenLink)) + ).then(stats => [ + // Remove broken links. + files.filter((file, i ) => !!stats[i]), + stats.filter(Boolean), + ])); +} + +function handleBrokenLink(e) { + debug('WARNING: error stating, possibly broken symlink', e.message); + return Promise.resolve(); +} + +module.exports = Fastfs; 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 deleted file mode 100644 index c247e59d37a32f..00000000000000 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ /dev/null @@ -1,1612 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -jest - .dontMock('../index') - .dontMock('absolute-path') - .dontMock('../docblock') - .dontMock('../../replacePatterns') - .dontMock('../../../../lib/getAssetDataFromName') - .setMock('../../../ModuleDescriptor', function(data) {return data;}); - -jest.mock('fs'); - -describe('DependencyGraph', function() { - var DependencyGraph; - var fileWatcher; - var fs; - - beforeEach(function() { - fs = require('fs'); - DependencyGraph = require('../index'); - - fileWatcher = { - on: function() { - return this; - } - }; - }); - - describe('getOrderedDependencies', function() { - pit('should get dependencies', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("a")' - ].join('\n'), - 'a.js': [ - '/**', - ' * @providesModule a', - ' */', - ].join('\n'), - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['a']}, - {id: 'a', altId: '/root/a.js', path: '/root/a.js', dependencies: []}, - ]); - }); - }); - - pit('should get dependencies with the correct extensions', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("a")' - ].join('\n'), - 'a.js': [ - '/**', - ' * @providesModule a', - ' */', - ].join('\n'), - 'a.js.orig': [ - '/**', - ' * @providesModule a', - ' */', - ].join('\n'), - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['a']}, - {id: 'a', altId: '/root/a.js', path: '/root/a.js', dependencies: []}, - ]); - }); - }); - - pit('should get json dependencies', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'package.json': JSON.stringify({ - name: 'package' - }), - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("./a.json")' - ].join('\n'), - 'a.json': JSON.stringify({}), - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { - id: 'index', - altId: 'package/index', - path: '/root/index.js', - dependencies: ['./a.json'] - }, - { - id: 'package/a.json', - isJSON: true, - path: '/root/a.json', - dependencies: [] - }, - ]); - }); - }); - - pit('should get dependencies with deprecated assets', 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, - assetExts: ['png', 'jpg'], - assetRoots_DEPRECATED: ['/root/imgs'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['image!a']}, - { id: 'image!a', - path: '/root/imgs/a.png', - dependencies: [], - isAsset_DEPRECATED: true, - resolution: 1, - }, - ]); - }); - }); - - pit('should get dependencies with relative assets', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("./imgs/a.png")' - ].join('\n'), - 'imgs': { - 'a.png': '' - }, - 'package.json': JSON.stringify({ - name: 'rootPackage' - }), - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { - id: 'index', - altId: 'rootPackage/index', - path: '/root/index.js', - dependencies: ['./imgs/a.png'] - }, - { id: 'rootPackage/imgs/a.png', - path: '/root/imgs/a.png', - dependencies: [], - isAsset: true, - resolution: 1, - }, - ]); - }); - }); - - pit('should get dependencies with assets and resolution', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("./imgs/a.png");', - 'require("./imgs/b.png");', - 'require("./imgs/c.png");', - ].join('\n'), - 'imgs': { - 'a@1.5x.png': '', - 'b@.7x.png': '', - 'c.png': '', - 'c@2x.png': '', - }, - 'package.json': JSON.stringify({ - name: 'rootPackage' - }), - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { - id: 'index', - altId: 'rootPackage/index', - path: '/root/index.js', - dependencies: [ - './imgs/a.png', - './imgs/b.png', - './imgs/c.png', - ] - }, - { - id: 'rootPackage/imgs/a.png', - path: '/root/imgs/a@1.5x.png', - resolution: 1.5, - dependencies: [], - isAsset: true, - }, - { - id: 'rootPackage/imgs/b.png', - path: '/root/imgs/b@.7x.png', - resolution: 0.7, - dependencies: [], - isAsset: true - }, - { - id: 'rootPackage/imgs/c.png', - path: '/root/imgs/c.png', - resolution: 1, - dependencies: [], - isAsset: true - }, - ]); - }); - }); - - pit('Deprecated and relative assets can live together', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("./imgs/a.png")', - 'require("image!a")', - ].join('\n'), - 'imgs': { - 'a.png': '' - }, - 'package.json': JSON.stringify({ - name: 'rootPackage' - }), - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - assetRoots_DEPRECATED: ['/root/imgs'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { - id: 'index', - altId: 'rootPackage/index', - path: '/root/index.js', - dependencies: ['./imgs/a.png', 'image!a'] - }, - { - id: 'rootPackage/imgs/a.png', - path: '/root/imgs/a.png', - dependencies: [], - isAsset: true, - resolution: 1, - }, - { - id: 'image!a', - path: '/root/imgs/a.png', - dependencies: [], - isAsset_DEPRECATED: true, - resolution: 1, - }, - ]); - }); - }); - - pit('should get recursive dependencies', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("a")', - ].join('\n'), - 'a.js': [ - '/**', - ' * @providesModule a', - ' */', - 'require("index")', - ].join('\n'), - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['a']}, - {id: 'a', altId: '/root/a.js', path: '/root/a.js', dependencies: ['index']}, - ]); - }); - }); - - pit('should work with packages', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'lol' - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should default main package to index.js', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': 'require("aPackage")', - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - }), - 'index.js': 'lol', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, - { id: 'aPackage/index', - path: '/root/aPackage/index.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should have altId for a package with providesModule', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': 'require("aPackage")', - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - }), - 'index.js': [ - '/**', - ' * @providesModule EpicModule', - ' */', - ].join('\n'), - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, - { id: 'EpicModule', - altId: 'aPackage/index', - path: '/root/aPackage/index.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should default use index.js if main is a dir', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': 'require("aPackage")', - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'lib', - }), - lib: { - 'index.js': 'lol', - }, - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, - { id: 'aPackage/lib/index', - path: '/root/aPackage/lib/index.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should resolve require to index if it is a dir', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'package.json': JSON.stringify({ - name: 'test', - }), - 'index.js': 'require("./lib/")', - lib: { - 'index.js': 'lol', - }, - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: 'test/index', path: '/root/index.js', dependencies: ['./lib/']}, - { id: 'test/lib/index', - path: '/root/lib/index.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should ignore malformed packages', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'aPackage': { - 'package.json': 'lol', - 'main.js': 'lol' - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, - ]); - }); - }); - - pit('can have multiple modules with the same name', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("b")', - ].join('\n'), - 'b.js': [ - '/**', - ' * @providesModule b', - ' */', - ].join('\n'), - 'c.js': [ - '/**', - ' * @providesModule c', - ' */', - ].join('\n'), - 'somedir': { - 'somefile.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("c")', - ].join('\n') - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/somedir/somefile.js')) - .toEqual([ - { id: 'index', - altId: '/root/somedir/somefile.js', - path: '/root/somedir/somefile.js', - dependencies: ['c'] - }, - { id: 'c', - altId: '/root/c.js', - path: '/root/c.js', - dependencies: [] - }, - ]); - }); - }); - - pit('providesModule wins when conflict with package', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'b.js': [ - '/**', - ' * @providesModule aPackage', - ' */', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'lol' - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage', - altId: '/root/b.js', - path: '/root/b.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should be forgiving with missing requires', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("lolomg")', - ].join('\n') - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['lolomg'] - } - ]); - }); - }); - - pit('should work with packages with subdirs', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage/subdir/lolynot")', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'lol', - 'subdir': { - 'lolynot.js': 'lolynot' - } - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage/subdir/lolynot'] - }, - { id: 'aPackage/subdir/lolynot', - path: '/root/aPackage/subdir/lolynot.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should work with packages with symlinked subdirs', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'symlinkedPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'lol', - 'subdir': { - 'lolynot.js': 'lolynot' - } - }, - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage/subdir/lolynot")', - ].join('\n'), - 'aPackage': { SYMLINK: '/symlinkedPackage' }, - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage/subdir/lolynot'] - }, - { id: 'aPackage/subdir/lolynot', - path: '/symlinkedPackage/subdir/lolynot.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should work with relative modules in packages', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'require("./subdir/lolynot")', - 'subdir': { - 'lolynot.js': 'require("../other")' - }, - 'other.js': 'some code' - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: ['./subdir/lolynot'] - }, - { id: 'aPackage/subdir/lolynot', - path: '/root/aPackage/subdir/lolynot.js', - dependencies: ['../other'] - }, - { id: 'aPackage/other', - path: '/root/aPackage/other.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should support simple browser field in packages', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js', - browser: 'client.js', - }), - 'main.js': 'some other code', - 'client.js': 'some code', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/client', - path: '/root/aPackage/client.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should supportbrowser field in packages w/o .js ext', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js', - browser: 'client', - }), - 'main.js': 'some other code', - 'client.js': 'some code', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/client', - path: '/root/aPackage/client.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should support mapping main in browser field json', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: './main.js', - browser: { - './main.js': './client.js', - }, - }), - 'main.js': 'some other code', - 'client.js': 'some code', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/client', - path: '/root/aPackage/client.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should work do correct browser mapping w/o js ext', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: './main.js', - browser: { - './main': './client.js', - }, - }), - 'main.js': 'some other code', - 'client.js': 'some code', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/client', - path: '/root/aPackage/client.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should support browser mapping of files', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: './main.js', - browser: { - './main': './client.js', - './node.js': './not-node.js', - './not-browser': './browser.js', - './dir/server.js': './dir/client', - }, - }), - 'main.js': 'some other code', - 'client.js': 'require("./node")\nrequire("./dir/server.js")', - 'not-node.js': 'require("./not-browser")', - 'not-browser.js': 'require("./dir/server")', - 'browser.js': 'some browser code', - 'dir': { - 'server.js': 'some node code', - 'client.js': 'some browser code', - } - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/client', - path: '/root/aPackage/client.js', - dependencies: ['./node', './dir/server.js'] - }, - { id: 'aPackage/not-node', - path: '/root/aPackage/not-node.js', - dependencies: ['./not-browser'] - }, - { id: 'aPackage/browser', - path: '/root/aPackage/browser.js', - dependencies: [] - }, - { id: 'aPackage/dir/client', - path: '/root/aPackage/dir/client.js', - dependencies: [] - }, - ]); - }); - }); - - pit('should support browser mapping for packages', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - browser: { - 'node-package': 'browser-package', - } - }), - 'index.js': 'require("node-package")', - 'node-package': { - 'package.json': JSON.stringify({ - 'name': 'node-package', - }), - 'index.js': 'some node code', - }, - 'browser-package': { - 'package.json': JSON.stringify({ - 'name': 'browser-package', - }), - 'index.js': 'some browser code', - }, - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/index', - path: '/root/aPackage/index.js', - dependencies: ['node-package'] - }, - { id: 'browser-package/index', - path: '/root/aPackage/browser-package/index.js', - dependencies: [] - }, - ]); - }); - }); - }); - - describe('file watch updating', function() { - var triggerFileChange; - - beforeEach(function() { - fileWatcher = { - on: function(eventType, callback) { - if (eventType !== 'all') { - throw new Error('Can only handle "all" event in watcher.'); - } - triggerFileChange = callback; - return this; - } - }; - }); - - pit('updates module dependencies', function() { - var root = '/root'; - var filesystem = fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - 'require("foo")' - ].join('\n'), - 'foo': [ - '/**', - ' * @providesModule foo', - ' */', - 'require("aPackage")' - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'main', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - filesystem.root['index.js'] = - filesystem.root['index.js'].replace('require("foo")', ''); - triggerFileChange('change', 'index.js', root); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: [] - }, - ]); - }); - }); - }); - - pit('updates module dependencies on file change', function() { - var root = '/root'; - var filesystem = fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - 'require("foo")' - ].join('\n'), - 'foo.js': [ - '/**', - ' * @providesModule foo', - ' */', - 'require("aPackage")' - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'main', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - filesystem.root['index.js'] = - filesystem.root['index.js'].replace('require("foo")', ''); - triggerFileChange('change', 'index.js', root); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: [] - }, - ]); - }); - }); - }); - - pit('updates module dependencies on file delete', function() { - var root = '/root'; - var filesystem = fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - 'require("foo")' - ].join('\n'), - 'foo.js': [ - '/**', - ' * @providesModule foo', - ' */', - 'require("aPackage")' - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'main', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - delete filesystem.root.foo; - triggerFileChange('delete', 'foo.js', root); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage', 'foo'] - }, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: [] - }, - ]); - }); - }); - }); - - pit('updates module dependencies on file add', function() { - var root = '/root'; - var filesystem = fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - 'require("foo")' - ].join('\n'), - 'foo.js': [ - '/**', - ' * @providesModule foo', - ' */', - 'require("aPackage")' - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'main', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - filesystem.root['bar.js'] = [ - '/**', - ' * @providesModule bar', - ' */', - 'require("foo")' - ].join('\n'); - triggerFileChange('add', 'bar.js', root); - - filesystem.root.aPackage['main.js'] = 'require("bar")'; - triggerFileChange('change', 'aPackage/main.js', root); - - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage', 'foo'] - }, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: ['bar'] - }, - { id: 'bar', - altId: '/root/bar.js', - path: '/root/bar.js', - dependencies: ['foo'] - }, - { id: 'foo', - altId: '/root/foo.js', - path: '/root/foo.js', - dependencies: ['aPackage'] - }, - ]); - }); - }); - }); - - pit('updates module dependencies on deprecated asset add', function() { - var root = '/root'; - var filesystem = fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("image!foo")' - ].join('\n'), - }, - }); - - var dgraph = new DependencyGraph({ - roots: [root], - assetRoots_DEPRECATED: [root], - assetExts: ['png'], - fileWatcher: fileWatcher, - }); - - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['image!foo'] - } - ]); - - filesystem.root['foo.png'] = ''; - triggerFileChange('add', 'foo.png', root); - - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['image!foo'] - }, - { id: 'image!foo', - path: '/root/foo.png', - dependencies: [], - isAsset_DEPRECATED: true, - resolution: 1, - }, - ]); - }); - }); - }); - - pit('updates module dependencies on relative asset add', function() { - var root = '/root'; - var filesystem = fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("./foo.png")' - ].join('\n'), - 'package.json': JSON.stringify({ - name: 'aPackage' - }), - }, - }); - - var dgraph = new DependencyGraph({ - roots: [root], - assetExts: ['png'], - fileWatcher: fileWatcher, - }); - - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: 'aPackage/index', - path: '/root/index.js', - dependencies: ['./foo.png'] - } - ]); - - filesystem.root['foo.png'] = ''; - triggerFileChange('add', 'foo.png', root); - - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: 'aPackage/index', - path: '/root/index.js', - dependencies: ['./foo.png'] - }, - { id: 'aPackage/foo.png', - path: '/root/foo.png', - dependencies: [], - isAsset: true, - resolution: 1, - }, - ]); - }); - }); - }); - - pit('runs changes through ignore filter', function() { - var root = '/root'; - var filesystem = fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - 'require("foo")' - ].join('\n'), - 'foo.js': [ - '/**', - ' * @providesModule foo', - ' */', - 'require("aPackage")' - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'main', - } - } - }); - - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - ignoreFilePath: function(filePath) { - if (filePath === '/root/bar.js') { - return true; - } - return false; - } - }); - return dgraph.load().then(function() { - filesystem.root['bar.js'] = [ - '/**', - ' * @providesModule bar', - ' */', - 'require("foo")' - ].join('\n'); - triggerFileChange('add', 'bar.js', root); - - filesystem.root.aPackage['main.js'] = 'require("bar")'; - triggerFileChange('change', 'aPackage/main.js', root); - - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage', 'foo'] - }, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: ['bar'] - }, - { id: 'foo', - altId: '/root/foo.js', - path: '/root/foo.js', - dependencies: ['aPackage'] - }, - ]); - }); - }); - }); - - pit('should ignore directory updates', function() { - var root = '/root'; - fs.__setMockFilesystem({ - 'root': { - 'index.js': [ - '/**', - ' * @providesModule index', - ' */', - 'require("aPackage")', - 'require("foo")' - ].join('\n'), - 'foo.js': [ - '/**', - ' * @providesModule foo', - ' */', - 'require("aPackage")' - ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'main', - } - } - }); - var dgraph = new DependencyGraph({ - roots: [root], - fileWatcher: fileWatcher, - assetExts: ['png', 'jpg'], - }); - return dgraph.load().then(function() { - triggerFileChange('change', 'aPackage', '/root', { - isDirectory: function(){ return true; } - }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) - .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage', 'foo'] - }, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: [] - }, - { id: 'foo', - altId: '/root/foo.js', - path: '/root/foo.js', - dependencies: ['aPackage'] - }, - ]); - }); - }); - }); - }); -}); diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js deleted file mode 100644 index 0881e5dc77dea0..00000000000000 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ /dev/null @@ -1,798 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -var ModuleDescriptor = require('../../ModuleDescriptor'); -var Promise = require('bluebird'); -var fs = require('fs'); -var docblock = require('./docblock'); -var replacePatterns = require('../replacePatterns'); -var path = require('path'); -var isAbsolutePath = require('absolute-path'); -var debug = require('debug')('DependecyGraph'); -var util = require('util'); -var declareOpts = require('../../../lib/declareOpts'); -var getAssetDataFromName = require('../../../lib/getAssetDataFromName'); - -var readFile = Promise.promisify(fs.readFile); -var readDir = Promise.promisify(fs.readdir); -var lstat = Promise.promisify(fs.lstat); -var realpath = Promise.promisify(fs.realpath); - -var validateOpts = declareOpts({ - roots: { - type: 'array', - required: true, - }, - ignoreFilePath: { - type: 'function', - default: function(){} - }, - fileWatcher: { - type: 'object', - required: true, - }, - assetRoots_DEPRECATED: { - type: 'array', - default: [], - }, - assetExts: { - type: 'array', - required: true, - } -}); - -function DependecyGraph(options) { - var opts = validateOpts(options); - - this._roots = opts.roots; - this._assetRoots_DEPRECATED = opts.assetRoots_DEPRECATED; - this._assetExts = opts.assetExts; - this._ignoreFilePath = opts.ignoreFilePath; - this._fileWatcher = options.fileWatcher; - - this._loaded = false; - this._queue = this._roots.slice(); - this._graph = Object.create(null); - this._packageByRoot = Object.create(null); - this._packagesById = Object.create(null); - this._moduleById = Object.create(null); - this._debugUpdateEvents = []; - - this._moduleExtPattern = new RegExp( - '\.(' + ['js', 'json'].concat(this._assetExts).join('|') + ')$' - ); - - // Kick off the search process to precompute the dependency graph. - this._init(); -} - -DependecyGraph.prototype.load = function() { - if (this._loading != null) { - return this._loading; - } - - this._loading = Promise.all([ - this._search(), - this._buildAssetMap_DEPRECATED(), - ]); - - return this._loading; -}; - -/** - * Given an entry file return an array of all the dependent module descriptors. - */ -DependecyGraph.prototype.getOrderedDependencies = function(entryPath) { - var absolutePath = this._getAbsolutePath(entryPath); - if (absolutePath == null) { - throw new NotFoundError( - 'Cannot find entry file %s in any of the roots: %j', - entryPath, - this._roots - ); - } - - var module = this._graph[absolutePath]; - if (module == null) { - throw new Error('Module with path "' + entryPath + '" is not in graph'); - } - - var self = this; - var deps = []; - var visited = Object.create(null); - - // Node haste sucks. Id's aren't unique. So to make sure our entry point - // is the thing that ends up in our dependency list. - var graphMap = Object.create(this._moduleById); - graphMap[module.id] = module; - - // Recursively collect the dependency list. - function collect(module) { - deps.push(module); - - module.dependencies.forEach(function(name) { - var id = sansExtJs(name); - var dep = self.resolveDependency(module, id); - - if (dep == null) { - debug( - 'WARNING: Cannot find required module `%s` from module `%s`.', - name, - module.id - ); - return; - } - - if (!visited[dep.id]) { - visited[dep.id] = true; - collect(dep); - } - }); - } - - visited[module.id] = true; - collect(module); - - return deps; -}; - -/** - * Given a module descriptor `fromModule` return the module descriptor for - * the required module `depModuleId`. It could be top-level or relative, - * or both. - */ -DependecyGraph.prototype.resolveDependency = function( - fromModule, - depModuleId -) { - if (this._assetMap_DEPRECATED != null) { - var assetMatch = depModuleId.match(/^image!(.+)/); - // Process DEPRECATED global asset requires. - if (assetMatch && assetMatch[1]) { - if (!this._assetMap_DEPRECATED[assetMatch[1]]) { - debug('WARINING: Cannot find asset:', assetMatch[1]); - return null; - } - return this._assetMap_DEPRECATED[assetMatch[1]]; - } - } - - var packageJson, modulePath, dep; - - // Package relative modules starts with '.' or '..'. - if (depModuleId[0] !== '.') { - - // Check if we need to map the dependency to something else via the - // `browser` field in package.json - var fromPackageJson = this._lookupPackage(fromModule.path); - if (fromPackageJson && fromPackageJson.browser && - fromPackageJson.browser[depModuleId]) { - depModuleId = fromPackageJson.browser[depModuleId]; - } - - // `depModuleId` is simply a top-level `providesModule`. - // `depModuleId` is a package module but given the full path from the - // package, i.e. package_name/module_name - if (this._moduleById[sansExtJs(depModuleId)]) { - return this._moduleById[sansExtJs(depModuleId)]; - } - - // `depModuleId` is a package and it's depending on the "main" resolution. - packageJson = this._packagesById[depModuleId]; - - // We are being forgiving here and raising an error because we could be - // processing a file that uses it's own require system. - if (packageJson == null) { - debug( - 'WARNING: Cannot find required module `%s` from module `%s`.', - depModuleId, - fromModule.id - ); - return null; - } - - var main; - - // We prioritize the `browser` field if it's a module path. - if (typeof packageJson.browser === 'string') { - main = packageJson.browser; - } else { - main = packageJson.main || 'index'; - } - - // If there is a mapping for main in the `browser` field. - if (packageJson.browser && typeof packageJson.browser === 'object') { - var tmpMain = packageJson.browser[main] || - packageJson.browser[withExtJs(main)] || - packageJson.browser[sansExtJs(main)]; - if (tmpMain) { - main = tmpMain; - } - } - - modulePath = withExtJs(path.join(packageJson._root, main)); - dep = this._graph[modulePath]; - - // Some packages use just a dir and rely on an index.js inside that dir. - if (dep == null) { - dep = this._graph[path.join(packageJson._root, main, 'index.js')]; - } - - if (dep == null) { - throw new Error( - 'Cannot find package main file for package: ' + packageJson._root - ); - } - return dep; - } else { - - // `depModuleId` is a module defined in a package relative to `fromModule`. - packageJson = this._lookupPackage(fromModule.path); - - if (packageJson == null) { - throw new Error( - 'Expected relative module lookup from ' + fromModule.id + ' to ' + - depModuleId + ' to be within a package but no package.json found.' - ); - } - - // Example: depModuleId: ../a/b - // fromModule.path: /x/y/z - // modulePath: /x/y/a/b - var dir = path.dirname(fromModule.path); - modulePath = path.join(dir, depModuleId); - - if (packageJson.browser && typeof packageJson.browser === 'object') { - var relPath = './' + path.relative(packageJson._root, modulePath); - var tmpModulePath = packageJson.browser[withExtJs(relPath)] || - packageJson.browser[sansExtJs(relPath)]; - if (tmpModulePath) { - modulePath = path.join(packageJson._root, tmpModulePath); - } - } - - // JS modules can be required without extensios. - if (!this._isFileAsset(modulePath) && !modulePath.match(/\.json$/)) { - modulePath = withExtJs(modulePath); - } - - dep = this._graph[modulePath]; - - // Maybe the dependency is a directory and there is an index.js inside it. - if (dep == null) { - dep = this._graph[path.join(dir, depModuleId, 'index.js')]; - } - - // Maybe it's an asset with @n.nx resolution and the path doesn't map - // to the id - if (dep == null && this._isFileAsset(modulePath)) { - dep = this._moduleById[this._lookupName(modulePath)]; - } - - if (dep == null) { - debug( - 'WARNING: Cannot find required module `%s` from module `%s`.' + - ' Inferred required module path is %s', - depModuleId, - fromModule.id, - modulePath - ); - return null; - } - - return dep; - } -}; - -/** - * Intiates the filewatcher and kicks off the search process. - */ -DependecyGraph.prototype._init = function() { - var processChange = this._processFileChange.bind(this); - var watcher = this._fileWatcher; - - this._loading = this.load().then(function() { - watcher.on('all', processChange); - }); -}; - -/** - * Implements a DFS over the file system looking for modules and packages. - */ -DependecyGraph.prototype._search = function() { - var self = this; - var dir = this._queue.shift(); - - if (dir == null) { - return Promise.resolve(this._graph); - } - - // Steps: - // 1. Read a dir and stat all the entries. - // 2. Filter the files and queue up the directories. - // 3. Process any package.json in the files - // 4. recur. - 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; - } - - if (stats[i].isSymbolicLink()) { - return false; - } - - return filePath.match(self._moduleExtPattern); - }); - - var processing = self._findAndProcessPackage(files, dir) - .then(function() { - return Promise.all(modulePaths.map(self._processModule.bind(self))); - }); - - return Promise.all([ - processing, - self._search() - ]); - }) - .then(function() { - return self; - }); -}; - -/** - * Given a list of files find a `package.json` file, and if found parse it - * and update indices. - */ -DependecyGraph.prototype._findAndProcessPackage = function(files, root) { - var self = this; - - var packagePath; - for (var i = 0; i < files.length ; i++) { - var file = files[i]; - if (path.basename(file) === 'package.json') { - packagePath = file; - break; - } - } - - if (packagePath != null) { - return this._processPackage(packagePath); - } else { - return Promise.resolve(); - } -}; - -DependecyGraph.prototype._processPackage = function(packagePath) { - var packageRoot = path.dirname(packagePath); - var self = this; - return readFile(packagePath, 'utf8') - .then(function(content) { - var packageJson; - try { - packageJson = JSON.parse(content); - } catch (e) { - debug('WARNING: malformed package.json: ', packagePath); - return Promise.resolve(); - } - - if (packageJson.name == null) { - debug( - 'WARNING: package.json `%s` is missing a name field', - packagePath - ); - return Promise.resolve(); - } - - packageJson._root = packageRoot; - self._addPackageToIndices(packageJson); - - return packageJson; - }); -}; - -DependecyGraph.prototype._addPackageToIndices = function(packageJson) { - this._packageByRoot[packageJson._root] = packageJson; - this._packagesById[packageJson.name] = packageJson; -}; - -DependecyGraph.prototype._removePackageFromIndices = function(packageJson) { - delete this._packageByRoot[packageJson._root]; - delete this._packagesById[packageJson.name]; -}; - -/** - * Parse a module and update indices. - */ -DependecyGraph.prototype._processModule = function(modulePath) { - var moduleData = { path: path.resolve(modulePath) }; - var module; - - if (this._assetExts.indexOf(extname(modulePath)) > -1) { - var assetData = getAssetDataFromName(this._lookupName(modulePath)); - moduleData.id = assetData.assetName; - moduleData.resolution = assetData.resolution; - moduleData.isAsset = true; - moduleData.dependencies = []; - module = new ModuleDescriptor(moduleData); - this._updateGraphWithModule(module); - return Promise.resolve(module); - } - - if (extname(modulePath) === 'json') { - moduleData.id = this._lookupName(modulePath); - moduleData.isJSON = true; - moduleData.dependencies = []; - module = new ModuleDescriptor(moduleData); - this._updateGraphWithModule(module); - return Promise.resolve(module); - } - - var self = this; - return readFile(modulePath, 'utf8') - .then(function(content) { - var moduleDocBlock = docblock.parseAsObject(content); - if (moduleDocBlock.providesModule || moduleDocBlock.provides) { - moduleData.id = /^(\S*)/.exec( - moduleDocBlock.providesModule || moduleDocBlock.provides - )[1]; - - // Incase someone wants to require this module via - // packageName/path/to/module - moduleData.altId = self._lookupName(modulePath); - } else { - moduleData.id = self._lookupName(modulePath); - } - moduleData.dependencies = extractRequires(content); - - module = new ModuleDescriptor(moduleData); - self._updateGraphWithModule(module); - return module; - }); -}; - -/** - * Compute the name of module relative to a package it may belong to. - */ -DependecyGraph.prototype._lookupName = function(modulePath) { - var packageJson = this._lookupPackage(modulePath); - if (packageJson == null) { - return path.resolve(modulePath); - } else { - var relativePath = - sansExtJs(path.relative(packageJson._root, modulePath)); - return path.join(packageJson.name, relativePath); - } -}; - -DependecyGraph.prototype._deleteModule = function(module) { - delete this._graph[module.path]; - - // Others may keep a reference so we mark it as deleted. - module.deleted = true; - - // Haste allows different module to have the same id. - if (this._moduleById[module.id] === module) { - delete this._moduleById[module.id]; - } - - if (module.altId && this._moduleById[module.altId] === module) { - delete this._moduleById[module.altId]; - } -}; - -/** - * Update the graph and indices with the module. - */ -DependecyGraph.prototype._updateGraphWithModule = function(module) { - if (this._graph[module.path]) { - this._deleteModule(this._graph[module.path]); - } - - this._graph[module.path] = module; - - if (this._moduleById[module.id]) { - debug( - 'WARNING: Top-level module name conflict `%s`.\n' + - 'module with path `%s` will replace `%s`', - module.id, - module.path, - this._moduleById[module.id].path - ); - } - - this._moduleById[module.id] = module; - - // Some module maybe refrenced by both @providesModule and - // require(package/moduleName). - if (module.altId != null && this._moduleById[module.altId] == null) { - this._moduleById[module.altId] = module; - } -}; - -/** - * Find the nearest package to a module. - */ -DependecyGraph.prototype._lookupPackage = function(modulePath) { - var packageByRoot = this._packageByRoot; - - /** - * Auxiliary function to recursively lookup a package. - */ - function lookupPackage(currDir) { - // ideally we stop once we're outside root and this can be a simple child - // dir check. However, we have to support modules that was symlinked inside - // our project root. - if (currDir === '/') { - return null; - } else { - var packageJson = packageByRoot[currDir]; - if (packageJson) { - return packageJson; - } else { - return lookupPackage(path.dirname(currDir)); - } - } - } - - return lookupPackage(path.dirname(modulePath)); -}; - -/** - * Process a filewatcher change event. - */ -DependecyGraph.prototype._processFileChange = function( - eventType, - filePath, - root, - stat -) { - var absPath = path.join(root, filePath); - if (this._ignoreFilePath(absPath)) { - return; - } - - this._debugUpdateEvents.push({event: eventType, path: filePath}); - - if (this._assetExts.indexOf(extname(filePath)) > -1) { - this._processAssetChange_DEPRECATED(eventType, absPath); - // Fall through because new-style assets are actually modules. - } - - var isPackage = path.basename(filePath) === 'package.json'; - if (eventType === 'delete') { - if (isPackage) { - var packageJson = this._packageByRoot[path.dirname(absPath)]; - if (packageJson) { - this._removePackageFromIndices(packageJson); - } - } else { - var module = this._graph[absPath]; - if (module == null) { - return; - } - - this._deleteModule(module); - } - } else if (!(stat && stat.isDirectory())) { - var self = this; - this._loading = this._loading.then(function() { - if (isPackage) { - return self._processPackage(absPath); - } - return self._processModule(absPath); - }); - } -}; - -DependecyGraph.prototype.getDebugInfo = function() { - return '

FileWatcher Update Events

' + - '
' + util.inspect(this._debugUpdateEvents) + '
' + - '

Graph dump

' + - '
' + util.inspect(this._graph) + '
'; -}; - -/** - * Searches all roots for the file and returns the first one that has file of - * the same path. - */ -DependecyGraph.prototype._getAbsolutePath = function(filePath) { - if (isAbsolutePath(filePath)) { - return filePath; - } - - 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; - } - } - - return null; -}; - -DependecyGraph.prototype._buildAssetMap_DEPRECATED = function() { - if (this._assetRoots_DEPRECATED == null || - this._assetRoots_DEPRECATED.length === 0) { - return Promise.resolve(); - } - - this._assetMap_DEPRECATED = Object.create(null); - return buildAssetMap_DEPRECATED( - this._assetRoots_DEPRECATED, - this._processAsset_DEPRECATED.bind(this) - ); -}; - -DependecyGraph.prototype._processAsset_DEPRECATED = function(file) { - var ext = extname(file); - if (this._assetExts.indexOf(ext) !== -1) { - var name = assetName(file, ext); - if (this._assetMap_DEPRECATED[name] != null) { - debug('Conflcting assets', name); - } - - this._assetMap_DEPRECATED[name] = new ModuleDescriptor({ - id: 'image!' + name, - path: path.resolve(file), - isAsset_DEPRECATED: true, - dependencies: [], - resolution: getAssetDataFromName(file).resolution, - }); - } -}; - -DependecyGraph.prototype._processAssetChange_DEPRECATED = function(eventType, file) { - if (this._assetMap_DEPRECATED == null) { - return; - } - - var name = assetName(file, extname(file)); - if (eventType === 'change' || eventType === 'delete') { - delete this._assetMap_DEPRECATED[name]; - } - - if (eventType === 'change' || eventType === 'add') { - this._processAsset_DEPRECATED(file); - } -}; - -DependecyGraph.prototype._isFileAsset = function(file) { - return this._assetExts.indexOf(extname(file)) !== -1; -}; - -/** - * Extract all required modules from a `code` string. - */ -var blockCommentRe = /\/\*(.|\n)*?\*\//g; -var lineCommentRe = /\/\/.+(\n|$)/g; -function extractRequires(code) { - var deps = []; - - code - .replace(blockCommentRe, '') - .replace(lineCommentRe, '') - .replace(replacePatterns.IMPORT_RE, function(match, pre, quot, dep, post) { - deps.push(dep); - return match; - }) - .replace(replacePatterns.REQUIRE_RE, function(match, pre, quot, dep, post) { - deps.push(dep); - }); - - return deps; -} - -/** - * `file` without the .js extension. - */ -function sansExtJs(file) { - if (file.match(/\.js$/)) { - return file.slice(0, -3); - } else { - return file; - } -} - -/** - * `file` with the .js extension. - */ -function withExtJs(file) { - if (file.match(/\.js$/)) { - return file; - } else { - return file + '.js'; - } -} - -function handleBrokenLink(e) { - debug('WARNING: error stating, possibly broken symlink', e.message); - return Promise.resolve(); -} - -function readAndStatDir(dir) { - return readDir(dir) - .then(function(files){ - return Promise.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, - Promise.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_DEPRECATED(roots, processAsset) { - var queue = roots.slice(0); - - function search() { - var root = queue.shift(); - - if (root == null) { - return Promise.resolve(); - } - - return readAndStatDir(root).spread(function(files, stats) { - files.forEach(function(file, i) { - if (stats[i].isDirectory()) { - queue.push(file); - } else { - processAsset(file); - } - }); - - return search(); - }); - } - - return search(); -} - -function assetName(file, ext) { - return path.basename(file, '.' + ext).replace(/@[\d\.]+x/, ''); -} - -function extname(name) { - return path.extname(name).replace(/^\./, ''); -} - -function NotFoundError() { - Error.call(this); - Error.captureStackTrace(this, this.constructor); - var msg = util.format.apply(util, arguments); - this.message = msg; - this.type = this.name = 'NotFoundError'; - this.status = 404; -} - -util.inherits(NotFoundError, Error); - -module.exports = DependecyGraph; diff --git a/packager/react-packager/src/DependencyResolver/haste/index.js b/packager/react-packager/src/DependencyResolver/haste/index.js deleted file mode 100644 index da68785eacd520..00000000000000 --- a/packager/react-packager/src/DependencyResolver/haste/index.js +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -var path = require('path'); -var DependencyGraph = require('./DependencyGraph'); -var replacePatterns = require('./replacePatterns'); -var ModuleDescriptor = require('../ModuleDescriptor'); -var declareOpts = require('../../lib/declareOpts'); - -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 validateOpts = declareOpts({ - projectRoots: { - type: 'array', - required: true, - }, - blacklistRE: { - type: 'object', // typeof regex is object - }, - polyfillModuleNames: { - type: 'array', - default: [], - }, - nonPersistent: { - type: 'boolean', - default: false, - }, - moduleFormat: { - type: 'string', - default: 'haste', - }, - assetRoots: { - type: 'array', - default: [], - }, - fileWatcher: { - type: 'object', - required: true, - }, - assetExts: { - type: 'array', - required: true, - } -}); - -function HasteDependencyResolver(options) { - var opts = validateOpts(options); - - this._depGraph = new DependencyGraph({ - roots: opts.projectRoots, - assetRoots_DEPRECATED: opts.assetRoots, - assetExts: opts.assetExts, - ignoreFilePath: function(filepath) { - return filepath.indexOf('__tests__') !== -1 || - (opts.blacklistRE && opts.blacklistRE.test(filepath)); - }, - fileWatcher: opts.fileWatcher, - }); - - - this._polyfillModuleNames = opts.polyfillModuleNames || []; -} - -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; - - return depGraph.load() - .then(function() { - var dependencies = depGraph.getOrderedDependencies(main); - var mainModuleId = dependencies[0].id; - - self._prependPolyfillDependencies(dependencies, opts.dev); - - return { - mainModuleId: mainModuleId, - dependencies: dependencies - }; - }); -}; - -HasteDependencyResolver.prototype._prependPolyfillDependencies = function( - dependencies, - isDev -) { - 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'), - path.join(__dirname, 'polyfills/String.prototype.es6.js'), - path.join(__dirname, 'polyfills/Array.prototype.es6.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) { - if (module.isPolyfill) { - return code; - } - - var resolvedDeps = Object.create(null); - var resolvedDepsArr = []; - - for (var i = 0; i < module.dependencies.length; i++) { - var depName = module.dependencies[i]; - var dep = this._depGraph.resolveDependency(module, depName); - if (dep) { - resolvedDeps[depName] = dep.id; - resolvedDepsArr.push(dep.id); - } - } - - var relativizeCode = function(codeMatch, pre, quot, depName, post) { - var depId = resolvedDeps[depName]; - if (depId) { - return pre + quot + depId + post; - } else { - return codeMatch; - } - }; - - return DEFINE_MODULE_CODE.replace(DEFINE_MODULE_REPLACE_RE, function(key) { - return { - '_moduleName_': module.id, - '_code_': code.replace(replacePatterns.IMPORT_RE, relativizeCode) - .replace(replacePatterns.REQUIRE_RE, relativizeCode), - '_deps_': JSON.stringify(resolvedDepsArr), - }[key]; - }); -}; - -HasteDependencyResolver.prototype.getDebugInfo = function() { - return this._depGraph.getDebugInfo(); -}; - -module.exports = HasteDependencyResolver; diff --git a/packager/react-packager/src/DependencyResolver/index.js b/packager/react-packager/src/DependencyResolver/index.js index ca80ab0b89da80..0ddf5c3cc047f3 100644 --- a/packager/react-packager/src/DependencyResolver/index.js +++ b/packager/react-packager/src/DependencyResolver/index.js @@ -8,15 +8,174 @@ */ 'use strict'; -var HasteDependencyResolver = require('./haste'); -var NodeDependencyResolver = require('./node'); - -module.exports = function createDependencyResolver(options) { - if (options.moduleFormat === 'haste') { - return new HasteDependencyResolver(options); - } else if (options.moduleFormat === 'node') { - return new NodeDependencyResolver(options); - } else { - throw new Error('unsupported'); +var path = require('path'); +var DependencyGraph = require('./DependencyGraph'); +var replacePatterns = require('./replacePatterns'); +var declareOpts = require('../lib/declareOpts'); +var Promise = require('promise'); + +var validateOpts = declareOpts({ + projectRoots: { + type: 'array', + required: true, + }, + blacklistRE: { + type: 'object', // typeof regex is object + }, + polyfillModuleNames: { + type: 'array', + default: [], + }, + nonPersistent: { + type: 'boolean', + default: false, + }, + moduleFormat: { + type: 'string', + default: 'haste', + }, + assetRoots: { + type: 'array', + default: [], + }, + fileWatcher: { + type: 'object', + required: true, + }, + assetExts: { + type: 'array', + required: true, } +}); + +function HasteDependencyResolver(options) { + var opts = validateOpts(options); + + this._depGraph = new DependencyGraph({ + roots: opts.projectRoots, + assetRoots_DEPRECATED: opts.assetRoots, + assetExts: opts.assetExts, + ignoreFilePath: function(filepath) { + return filepath.indexOf('__tests__') !== -1 || + (opts.blacklistRE && opts.blacklistRE.test(filepath)); + }, + fileWatcher: opts.fileWatcher, + }); + + + this._polyfillModuleNames = opts.polyfillModuleNames || []; +} + +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; + return depGraph.load().then( + () => depGraph.getOrderedDependencies(main).then( + dependencies => { + const mainModuleId = dependencies[0].id; + self._prependPolyfillDependencies( + dependencies, + opts.dev + ); + + return { + mainModuleId: mainModuleId, + dependencies: dependencies + }; + } + ) + ); }; + +HasteDependencyResolver.prototype._prependPolyfillDependencies = function( + dependencies, + isDev +) { + 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'), + path.join(__dirname, 'polyfills/String.prototype.es6.js'), + path.join(__dirname, 'polyfills/Array.prototype.es6.js'), + ].concat(this._polyfillModuleNames); + + var polyfillModules = polyfillModuleNames.map( + (polyfillModuleName, idx) => ({ + path: polyfillModuleName, + id: polyfillModuleName, + dependencies: polyfillModuleNames.slice(0, idx), + isPolyfill: true, + }) + ); + + dependencies.unshift.apply(dependencies, polyfillModules); +}; + +HasteDependencyResolver.prototype.wrapModule = function(module, code) { + if (module.isPolyfill) { + return Promise.resolve(code); + } + + const resolvedDeps = Object.create(null); + const resolvedDepsArr = []; + + return Promise.all( + module.dependencies.map(depName => { + return this._depGraph.resolveDependency(module, depName) + .then((dep) => dep && dep.getPlainObject().then(mod => { + if (mod) { + resolvedDeps[depName] = mod.id; + resolvedDepsArr.push(mod.id); + } + })); + }) + ).then(() => { + const relativizeCode = (codeMatch, pre, quot, depName, post) => { + const depId = resolvedDeps[depName]; + if (depId) { + return pre + quot + depId + post; + } else { + return codeMatch; + } + }; + + return defineModuleCode({ + code: code + .replace(replacePatterns.IMPORT_RE, relativizeCode) + .replace(replacePatterns.REQUIRE_RE, relativizeCode), + deps: JSON.stringify(resolvedDepsArr), + moduleName: module.id, + }); + }); +}; + +HasteDependencyResolver.prototype.getDebugInfo = function() { + return this._depGraph.getDebugInfo(); +}; + +function defineModuleCode({moduleName, code, deps}) { + return [ + `__d(`, + `'${moduleName}',`, + `${deps},`, + 'function(global, require, ', + 'requireDynamic, requireLazy, module, exports) {', + ` ${code}`, + '\n});', + ].join(''); +} + +module.exports = HasteDependencyResolver; diff --git a/packager/react-packager/src/DependencyResolver/node/index.js b/packager/react-packager/src/DependencyResolver/node/index.js deleted file mode 100644 index 573885646e97ce..00000000000000 --- a/packager/react-packager/src/DependencyResolver/node/index.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -var Promise = require('bluebird'); -var ModuleDescriptor = require('../ModuleDescriptor'); - -var mdeps = require('module-deps'); -var path = require('path'); - -exports.getRuntimeCode = function() {}; - -exports.wrapModule = function(id, source) { - return Promise.resolve( - 'define(' + JSON.stringify(id) + ',' + ' function(exports, module) {\n' - + source + '\n});' - ); -}; - -exports.getDependencies = function(root, fileEntryPath) { - return new Promise(function(resolve) { - fileEntryPath = path.join(process.cwd(), root, fileEntryPath); - - var md = mdeps(); - - md.end({file: fileEntryPath}); - - var deps = []; - - md.on('data', function(data) { - deps.push( - new ModuleDescriptor({ - id: data.id, - deps: data.deps, - path: data.file, - entry: data.entry - }) - ); - }); - - md.on('end', function() { - resolve(deps); - }); - }); -}; diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js b/packager/react-packager/src/DependencyResolver/polyfills/Array.prototype.es6.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/polyfills/Array.prototype.es6.js rename to packager/react-packager/src/DependencyResolver/polyfills/Array.prototype.es6.js diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/String.prototype.es6.js b/packager/react-packager/src/DependencyResolver/polyfills/String.prototype.es6.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/polyfills/String.prototype.es6.js rename to packager/react-packager/src/DependencyResolver/polyfills/String.prototype.es6.js diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/console.js b/packager/react-packager/src/DependencyResolver/polyfills/console.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/polyfills/console.js rename to packager/react-packager/src/DependencyResolver/polyfills/console.js diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js b/packager/react-packager/src/DependencyResolver/polyfills/error-guard.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/polyfills/error-guard.js rename to packager/react-packager/src/DependencyResolver/polyfills/error-guard.js diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/polyfills.js b/packager/react-packager/src/DependencyResolver/polyfills/polyfills.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/polyfills/polyfills.js rename to packager/react-packager/src/DependencyResolver/polyfills/polyfills.js diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude.js b/packager/react-packager/src/DependencyResolver/polyfills/prelude.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/polyfills/prelude.js rename to packager/react-packager/src/DependencyResolver/polyfills/prelude.js diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/prelude_dev.js b/packager/react-packager/src/DependencyResolver/polyfills/prelude_dev.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/polyfills/prelude_dev.js rename to packager/react-packager/src/DependencyResolver/polyfills/prelude_dev.js diff --git a/packager/react-packager/src/DependencyResolver/haste/polyfills/require.js b/packager/react-packager/src/DependencyResolver/polyfills/require.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/polyfills/require.js rename to packager/react-packager/src/DependencyResolver/polyfills/require.js diff --git a/packager/react-packager/src/DependencyResolver/haste/replacePatterns.js b/packager/react-packager/src/DependencyResolver/replacePatterns.js similarity index 100% rename from packager/react-packager/src/DependencyResolver/haste/replacePatterns.js rename to packager/react-packager/src/DependencyResolver/replacePatterns.js diff --git a/packager/react-packager/src/FileWatcher/index.js b/packager/react-packager/src/FileWatcher/index.js index cd1a28e558cdbb..aac211ad203196 100644 --- a/packager/react-packager/src/FileWatcher/index.js +++ b/packager/react-packager/src/FileWatcher/index.js @@ -10,7 +10,7 @@ var EventEmitter = require('events').EventEmitter; var sane = require('sane'); -var Promise = require('bluebird'); +var Promise = require('promise'); var util = require('util'); var exec = require('child_process').exec; @@ -57,7 +57,7 @@ util.inherits(FileWatcher, EventEmitter); FileWatcher.prototype.end = function() { return this._loading.then(function(watchers) { watchers.forEach(function(watcher) { - return Promise.promisify(watcher.close, watcher)(); + return Promise.denodeify(watcher.close).call(watcher); }); }); }; diff --git a/packager/react-packager/src/JSTransformer/Cache.js b/packager/react-packager/src/JSTransformer/Cache.js index 584077b6c56762..aee8d4f219bb9a 100644 --- a/packager/react-packager/src/JSTransformer/Cache.js +++ b/packager/react-packager/src/JSTransformer/Cache.js @@ -14,7 +14,7 @@ var declareOpts = require('../lib/declareOpts'); var fs = require('fs'); var isAbsolutePath = require('absolute-path'); var path = require('path'); -var Promise = require('bluebird'); +var Promise = require('promise'); var tmpdir = require('os').tmpDir(); var version = require('../../../../package.json').version; @@ -74,11 +74,13 @@ Cache.prototype.get = function(filepath, loaderCb) { Cache.prototype._set = function(filepath, loaderPromise) { this._data[filepath] = loaderPromise.then(function(data) { - return [ + return Promise.all([ data, - Promise.promisify(fs.stat)(filepath) - ]; - }).spread(function(data, stat) { + Promise.denodeify(fs.stat)(filepath) + ]); + }).then(function(ref) { + var data = ref[0]; + var stat = ref[1]; this._persistEventually(); return { data: data, @@ -113,7 +115,7 @@ Cache.prototype._persistCache = function() { Object.keys(data).forEach(function(key, i) { json[key] = values[i]; }); - return Promise.promisify(fs.writeFile)(cacheFilepath, JSON.stringify(json)); + return Promise.denodeify(fs.writeFile)(cacheFilepath, JSON.stringify(json)); }) .then(function() { this._persisting = null; diff --git a/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js b/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js index f91490ba0f3927..3877b3dd55dbd0 100644 --- a/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js +++ b/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js @@ -17,7 +17,7 @@ jest .mock('os') .mock('fs'); -var Promise = require('bluebird'); +var Promise = require('promise'); describe('JSTransformer Cache', function() { var Cache; diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js index 513d4394e17f4e..7c20e10edf20b8 100644 --- a/packager/react-packager/src/JSTransformer/index.js +++ b/packager/react-packager/src/JSTransformer/index.js @@ -9,14 +9,14 @@ 'use strict'; var fs = require('fs'); -var Promise = require('bluebird'); +var Promise = require('promise'); var Cache = require('./Cache'); var workerFarm = require('worker-farm'); var declareOpts = require('../lib/declareOpts'); var util = require('util'); var ModuleTransport = require('../lib/ModuleTransport'); -var readFile = Promise.promisify(fs.readFile); +var readFile = Promise.denodeify(fs.readFile); module.exports = Transformer; Transformer.TransformError = TransformError; @@ -69,7 +69,7 @@ function Transformer(options) { options.transformModulePath ); - this._transform = Promise.promisify(this._workers); + this._transform = Promise.denodeify(this._workers); } } diff --git a/packager/react-packager/src/Packager/__tests__/Packager-test.js b/packager/react-packager/src/Packager/__tests__/Packager-test.js index 901c467bac414a..216e9009fb71f8 100644 --- a/packager/react-packager/src/Packager/__tests__/Packager-test.js +++ b/packager/react-packager/src/Packager/__tests__/Packager-test.js @@ -17,12 +17,15 @@ jest jest.mock('fs'); -var Promise = require('bluebird'); +var Promise = require('promise'); describe('Packager', function() { var getDependencies; var wrapModule; var Packager; + var packager; + var assetServer; + var modules; beforeEach(function() { getDependencies = jest.genMockFn(); @@ -35,30 +38,27 @@ describe('Packager', function() { }); Packager = require('../'); - }); - pit('create a package', function() { require('fs').statSync.mockImpl(function() { return { - isDirectory: function() {return true;} + isDirectory: () => true }; }); - require('fs').readFile.mockImpl(function(file, callback) { callback(null, '{"json":true}'); }); - var assetServer = { + assetServer = { getAssetData: jest.genMockFn(), }; - var packager = new Packager({ + packager = new Packager({ projectRoots: ['/root'], assetServer: assetServer, }); - var modules = [ + modules = [ {id: 'foo', path: '/root/foo.js', dependencies: []}, {id: 'bar', path: '/root/bar.js', dependencies: []}, { @@ -101,7 +101,7 @@ describe('Packager', function() { }); wrapModule.mockImpl(function(module, code) { - return 'lol ' + code + ' lol'; + return Promise.resolve('lol ' + code + ' lol'); }); require('image-size').mockImpl(function(path, cb) { @@ -116,7 +116,9 @@ describe('Packager', function() { type: 'png', }; }); + }); + pit('create a package', function() { return packager.package('/root/foo.js', true, 'source_map_url') .then(function(p) { expect(p.addModule.mock.calls[0][0]).toEqual({ @@ -200,4 +202,42 @@ describe('Packager', function() { ]); }); }); + + pit('gets the list of dependencies', function() { + return packager.getDependencies('/root/foo.js', true) + .then(({dependencies}) => { + expect(dependencies).toEqual([ + { + dependencies: [], + id: 'foo', + path: '/root/foo.js', + }, + { + dependencies: [], + id: 'bar', + path: '/root/bar.js', + }, + { + dependencies: [], + id: 'image!img', + isAsset_DEPRECATED: true, + path: '/root/img/img.png', + resolution: 2, + }, + { + dependencies: [], + id: 'new_image.png', + isAsset: true, + path: '/root/img/new_image.png', + resolution: 2, + }, + { + dependencies: [], + id: 'package/file.json', + isJSON: true, + path: '/root/file.json', + }, + ]); + }); + }); }); diff --git a/packager/react-packager/src/Packager/index.js b/packager/react-packager/src/Packager/index.js index a85281d2a49ad7..3c6d1a2fc82c9a 100644 --- a/packager/react-packager/src/Packager/index.js +++ b/packager/react-packager/src/Packager/index.js @@ -11,7 +11,7 @@ var assert = require('assert'); var fs = require('fs'); var path = require('path'); -var Promise = require('bluebird'); +var Promise = require('promise'); var Transformer = require('../JSTransformer'); var DependencyResolver = require('../DependencyResolver'); var Package = require('./Package'); @@ -20,8 +20,8 @@ var ModuleTransport = require('../lib/ModuleTransport'); var declareOpts = require('../lib/declareOpts'); var imageSize = require('image-size'); -var sizeOf = Promise.promisify(imageSize); -var readFile = Promise.promisify(fs.readFile); +var sizeOf = Promise.denodeify(imageSize); +var readFile = Promise.denodeify(fs.readFile); var validateOpts = declareOpts({ projectRoots: { @@ -159,16 +159,17 @@ Packager.prototype._transformModule = function(ppackage, module) { } var resolver = this._resolver; - return transform.then(function(transformed) { - var code = resolver.wrapModule(module, transformed.code); - return new ModuleTransport({ - code: code, - map: transformed.map, - sourceCode: transformed.sourceCode, - sourcePath: transformed.sourcePath, - virtual: transformed.virtual, - }); - }); + return transform.then( + transformed => resolver.wrapModule(module, transformed.code).then( + code => new ModuleTransport({ + code: code, + map: transformed.map, + sourceCode: transformed.sourceCode, + sourcePath: transformed.sourcePath, + virtual: transformed.virtual, + }) + ) + ); }; Packager.prototype.getGraphDebugInfo = function() { @@ -206,7 +207,9 @@ Packager.prototype.generateAssetModule = function(ppackage, module) { return Promise.all([ sizeOf(module.path), this._assetServer.getAssetData(relPath), - ]).spread(function(dimensions, assetData) { + ]).then(function(res) { + var dimensions = res[0]; + var assetData = res[1]; var img = { __packager_asset: true, fileSystemLocation: path.dirname(module.path), diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index e4e7b5088767a0..32c9060a4ecf25 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -20,7 +20,7 @@ jest.setMock('worker-farm', function() { return function() {}; }) .setMock('uglify-js') .dontMock('../'); -var Promise = require('bluebird'); +var Promise = require('promise'); describe('processRequest', function() { var server; diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 0ce5c584715bd3..1d2140ef508c8e 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -15,7 +15,7 @@ var FileWatcher = require('../FileWatcher'); var Packager = require('../Packager'); var Activity = require('../Activity'); var AssetServer = require('../AssetServer'); -var Promise = require('bluebird'); +var Promise = require('promise'); var _ = require('underscore'); var exec = require('child_process').exec; var fs = require('fs'); @@ -131,13 +131,14 @@ Server.prototype._onFileChange = function(type, filepath, root) { Server.prototype._rebuildPackages = function() { var buildPackage = this.buildPackage.bind(this); var packages = this._packages; - Object.keys(packages).forEach(function(key) { - var options = getOptionsFromUrl(key); + + Object.keys(packages).forEach(function(optionsJson) { + var options = JSON.parse(optionsJson); // Wait for a previous build (if exists) to finish. - packages[key] = (packages[key] || Promise.resolve()).finally(function() { + packages[optionsJson] = (packages[optionsJson] || Promise.resolve()).finally(function() { // With finally promise callback we can't change the state of the promise // so we need to reassign the promise. - packages[key] = buildPackage(options).then(function(p) { + packages[optionsJson] = buildPackage(options).then(function(p) { // Make a throwaway call to getSource to cache the source string. p.getSource({ inlineSourceMap: options.inlineSourceMap, @@ -146,7 +147,7 @@ Server.prototype._rebuildPackages = function() { return p; }); }); - return packages[key]; + return packages[optionsJson]; }); }; @@ -228,15 +229,15 @@ Server.prototype._processDebugRequest = function(reqUrl, res) { res.end(ret); } else if (parts[1] === 'packages') { ret += '

Cached Packages

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

' + url + '

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

' + optionsJson + '

'; ret += p.getDebugInfo(); }); }, this)).then( function() { res.end(ret); }, function(e) { - res.wrteHead(500); + res.writeHead(500); res.end('Internal Error'); console.log(e.stack); } @@ -350,10 +351,11 @@ Server.prototype.processRequest = function(req, res, next) { var startReqEventId = Activity.startEvent('request:' + req.url); var options = getOptionsFromUrl(req.url); - var building = this._packages[req.url] || this.buildPackage(options); + var optionsJson = JSON.stringify(options); + var building = this._packages[optionsJson] || this.buildPackage(options); - this._packages[req.url] = building; - building.then( + this._packages[optionsJson] = building; + building.then( function(p) { if (requestType === 'bundle') { res.end(p.getSource({ diff --git a/packager/react-packager/src/__mocks__/fs.js b/packager/react-packager/src/__mocks__/fs.js index d0e08a2f4baacf..b5251a447c8295 100644 --- a/packager/react-packager/src/__mocks__/fs.js +++ b/packager/react-packager/src/__mocks__/fs.js @@ -51,7 +51,7 @@ fs.readFile.mockImpl(function(filepath, encoding, callback) { var node = getToNode(filepath); // dir check if (node && typeof node === 'object' && node.SYMLINK == null) { - callback(new Error('Trying to read a dir, ESIDR, or whatever')); + callback(new Error('Error readFile a dir: ' + filepath)); } return callback(null, node); } catch (e) { @@ -59,12 +59,13 @@ fs.readFile.mockImpl(function(filepath, encoding, callback) { } }); -fs.lstat.mockImpl(function(filepath, callback) { +fs.stat.mockImpl(function(filepath, callback) { var node; try { node = getToNode(filepath); } catch (e) { - return callback(e); + callback(e); + return; } var mtime = { @@ -73,7 +74,12 @@ fs.lstat.mockImpl(function(filepath, callback) { } }; - if (node && typeof node === 'object' && node.SYMLINK == null) { + if (node.SYMLINK) { + fs.stat(node.SYMLINK, callback); + return; + } + + if (node && typeof node === 'object') { callback(null, { isDirectory: function() { return true; @@ -89,9 +95,6 @@ fs.lstat.mockImpl(function(filepath, callback) { return false; }, isSymbolicLink: function() { - if (typeof node === 'object' && node.SYMLINK) { - return true; - } return false; }, mtime: mtime, @@ -113,6 +116,9 @@ function getToNode(filepath) { } var node = filesystem; parts.slice(1).forEach(function(part) { + if (node && node.SYMLINK) { + node = getToNode(node.SYMLINK); + } node = node[part]; });