diff --git a/Examples/UIExplorer/ProgressViewIOSExample.js b/Examples/UIExplorer/ProgressViewIOSExample.js new file mode 100644 index 00000000000000..f0a17a7c6e78f2 --- /dev/null +++ b/Examples/UIExplorer/ProgressViewIOSExample.js @@ -0,0 +1,83 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + ProgressViewIOS, + StyleSheet, + View, +} = React; +var TimerMixin = require('react-timer-mixin'); + +var ProgressViewExample = React.createClass({ + mixins: [TimerMixin], + + getInitialState() { + return { + progress: 0, + }; + }, + + componentDidMount() { + this.updateProgress(); + }, + + updateProgress() { + var progress = this.state.progress + 0.01; + this.setState({ progress }); + this.requestAnimationFrame(() => this.updateProgress()); + }, + + getProgress(offset) { + var progress = this.state.progress + offset; + return Math.sin(progress % Math.PI) % 1; + }, + + render() { + return ( + + + + + + + + ); + }, +}); + +exports.framework = 'React'; +exports.title = 'ProgressViewIOS'; +exports.description = 'ProgressViewIOS'; +exports.examples = [{ + title: 'ProgressViewIOS', + render() { + return ( + + ); + } +}]; + +var styles = StyleSheet.create({ + container: { + marginTop: -20, + backgroundColor: 'transparent', + }, + progressView: { + marginTop: 20, + } +}); diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index 45a679b315a9a5..acbba362921ea0 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -75,6 +75,14 @@ exports.examples = [ render: function(): ReactElement { return ; }, +}, { + title: 'Touchable delay for events', + description: ' components also accept delayPressIn, ' + + 'delayPressOut, and delayLongPress as props. These props impact the ' + + 'timing of feedback events.', + render: function(): ReactElement { + return ; + }, }]; var TextOnPressBox = React.createClass({ @@ -148,6 +156,44 @@ var TouchableFeedbackEvents = React.createClass({ }, }); +var TouchableDelayEvents = React.createClass({ + getInitialState: function() { + return { + eventLog: [], + }; + }, + render: function() { + return ( + + + this._appendEvent('press')} + delayPressIn={400} + onPressIn={() => this._appendEvent('pressIn - 400ms delay')} + delayPressOut={1000} + onPressOut={() => this._appendEvent('pressOut - 1000ms delay')} + delayLongPress={800} + onLongPress={() => this._appendEvent('longPress - 800ms delay')}> + + Press Me + + + + + {this.state.eventLog.map((e, ii) => {e})} + + + ); + }, + _appendEvent: function(eventName) { + var limit = 6; + var eventLog = this.state.eventLog.slice(0, limit - 1); + eventLog.unshift(eventName); + this.setState({eventLog}); + }, +}); + var heartImage = {uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small'}; var styles = StyleSheet.create({ diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index a030220cabb1af..df9a3b12322f30 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -45,6 +45,7 @@ var COMPONENTS = [ require('./NavigatorIOSColorsExample'), require('./NavigatorIOSExample'), require('./PickerIOSExample'), + require('./ProgressViewIOSExample'), require('./ScrollViewExample'), require('./SegmentedControlIOSExample'), require('./SliderIOSExample'), diff --git a/IntegrationTests/AsyncStorageTest.js b/IntegrationTests/AsyncStorageTest.js index 6d13bb6e9fe22a..911887d3e2dd89 100644 --- a/IntegrationTests/AsyncStorageTest.js +++ b/IntegrationTests/AsyncStorageTest.js @@ -16,12 +16,19 @@ var { View, } = React; +var deepDiffer = require('deepDiffer'); + var DEBUG = false; var KEY_1 = 'key_1'; var VAL_1 = 'val_1'; var KEY_2 = 'key_2'; var VAL_2 = 'val_2'; +var KEY_MERGE = 'key_merge'; +var VAL_MERGE_1 = {'foo': 1, 'bar': {'hoo': 1, 'boo': 1}, 'moo': {'a': 3}}; +var VAL_MERGE_2 = {'bar': {'hoo': 2}, 'baz': 2, 'moo': {'a': 3}}; +var VAL_MERGE_EXPECT = + {'foo': 1, 'bar': {'hoo': 2, 'boo': 1}, 'baz': 2, 'moo': {'a': 3}}; // setup in componentDidMount var done; @@ -40,8 +47,9 @@ function expectTrue(condition, message) { function expectEqual(lhs, rhs, testname) { expectTrue( - lhs === rhs, - 'Error in test ' + testname + ': expected ' + rhs + ', got ' + lhs + !deepDiffer(lhs, rhs), + 'Error in test ' + testname + ': expected\n' + JSON.stringify(rhs) + + '\ngot\n' + JSON.stringify(lhs) ); } @@ -93,25 +101,25 @@ function testRemoveItem() { 'Missing KEY_1 or KEY_2 in ' + '(' + result + ')' ); updateMessage('testRemoveItem - add two items'); - AsyncStorage.removeItem(KEY_1, (err) => { - expectAsyncNoError(err); + AsyncStorage.removeItem(KEY_1, (err2) => { + expectAsyncNoError(err2); updateMessage('delete successful '); - AsyncStorage.getItem(KEY_1, (err, result) => { - expectAsyncNoError(err); + AsyncStorage.getItem(KEY_1, (err3, result2) => { + expectAsyncNoError(err3); expectEqual( - result, + result2, null, 'testRemoveItem: key_1 present after delete' ); updateMessage('key properly removed '); - AsyncStorage.getAllKeys((err, result2) => { - expectAsyncNoError(err); + AsyncStorage.getAllKeys((err4, result3) => { + expectAsyncNoError(err4); expectTrue( - result2.indexOf(KEY_1) === -1, - 'Unexpected: KEY_1 present in ' + result2 + result3.indexOf(KEY_1) === -1, + 'Unexpected: KEY_1 present in ' + result3 ); - updateMessage('proper length returned.\nDone!'); - done(); + updateMessage('proper length returned.'); + runTestCase('should merge values', testMerge); }); }); }); @@ -120,6 +128,21 @@ function testRemoveItem() { }); } +function testMerge() { + AsyncStorage.setItem(KEY_MERGE, JSON.stringify(VAL_MERGE_1), (err1) => { + expectAsyncNoError(err1); + AsyncStorage.mergeItem(KEY_MERGE, JSON.stringify(VAL_MERGE_2), (err2) => { + expectAsyncNoError(err2); + AsyncStorage.getItem(KEY_MERGE, (err3, result) => { + expectAsyncNoError(err3); + expectEqual(JSON.parse(result), VAL_MERGE_EXPECT, 'testMerge'); + updateMessage('objects deeply merged\nDone!'); + done(); + }); + }); + }); +} + var AsyncStorageTest = React.createClass({ getInitialState() { return { diff --git a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js new file mode 100644 index 00000000000000..abda1c368cab31 --- /dev/null +++ b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.android.js @@ -0,0 +1,49 @@ + +/** + * 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 ProgressViewIOS + */ + +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); + +var DummyProgressViewIOS = React.createClass({ + render: function() { + return ( + + + ProgressViewIOS is not supported on this platform! + + + ); + }, +}); + +var styles = StyleSheet.create({ + dummy: { + width: 120, + height: 20, + backgroundColor: '#ffbcbc', + borderWidth: 1, + borderColor: 'red', + alignItems: 'center', + justifyContent: 'center', + }, + text: { + color: '#333333', + margin: 5, + fontSize: 10, + } +}); + +module.exports = DummyProgressViewIOS; diff --git a/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js new file mode 100644 index 00000000000000..b4fb3e76875195 --- /dev/null +++ b/Libraries/Components/ProgressViewIOS/ProgressViewIOS.ios.js @@ -0,0 +1,89 @@ +/** + * 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 ProgressViewIOS + * @flow + */ +'use strict'; + +var Image = require('Image'); +var NativeMethodsMixin = require('NativeMethodsMixin'); +var NativeModules = require('NativeModules'); +var PropTypes = require('ReactPropTypes'); +var React = require('React'); +var StyleSheet = require('StyleSheet'); + +var requireNativeComponent = require('requireNativeComponent'); +var verifyPropTypes = require('verifyPropTypes'); + +/** + * Use `ProgressViewIOS` to render a UIProgressView on iOS. + */ +var ProgressViewIOS = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + /** + * The progress bar style. + */ + progressViewStyle: PropTypes.oneOf(['default', 'bar']), + + /** + * The progress value (between 0 and 1). + */ + progress: PropTypes.number, + + /** + * The tint color of the progress bar itself. + */ + progressTintColor: PropTypes.string, + + /** + * The tint color of the progress bar track. + */ + trackTintColor: PropTypes.string, + + /** + * A stretchable image to display as the progress bar. + */ + progressImage: Image.propTypes.source, + + /** + * A stretchable image to display behind the progress bar. + */ + trackImage: Image.propTypes.source, + }, + + render: function() { + return ( + + ); + } +}); + +var styles = StyleSheet.create({ + progressView: { + height: NativeModules.ProgressViewManager.ComponentHeight + }, +}); + +var RCTProgressView = requireNativeComponent( + 'RCTProgressView', + null +); +if (__DEV__) { + verifyPropTypes( + RCTProgressView, + RCTProgressView.viewConfig + ); +} + +module.exports = ProgressViewIOS; diff --git a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js index 28fbea0271dc11..848144bff7f039 100644 --- a/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js +++ b/Libraries/Components/SegmentedControlIOS/SegmentedControlIOS.android.js @@ -17,7 +17,7 @@ var StyleSheet = require('StyleSheet'); var Text = require('Text'); var View = require('View'); -var Dummy = React.createClass({ +var DummySegmentedControlIOS = React.createClass({ render: function() { return ( @@ -46,4 +46,4 @@ var styles = StyleSheet.create({ } }); -module.exports = Dummy; +module.exports = DummySegmentedControlIOS; diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index 533652f6504c91..dcbfbeee19d91e 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -23,6 +23,7 @@ var View = require('View'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var keyOf = require('keyOf'); var merge = require('merge'); var onlyChild = require('onlyChild'); @@ -111,6 +112,7 @@ var TouchableHighlight = React.createClass({ }, componentDidMount: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, @@ -119,6 +121,7 @@ var TouchableHighlight = React.createClass({ }, componentWillReceiveProps: function(nextProps) { + ensurePositiveDelayProps(nextProps); if (nextProps.activeOpacity !== this.props.activeOpacity || nextProps.underlayColor !== this.props.underlayColor || nextProps.style !== this.props.style) { @@ -152,7 +155,8 @@ var TouchableHighlight = React.createClass({ touchableHandlePress: function() { this.clearTimeout(this._hideTimeout); this._showUnderlay(); - this._hideTimeout = this.setTimeout(this._hideUnderlay, 100); + this._hideTimeout = this.setTimeout(this._hideUnderlay, + this.props.delayPressOut || 100); this.props.onPress && this.props.onPress(); }, @@ -164,6 +168,18 @@ var TouchableHighlight = React.createClass({ return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant! }, + touchableGetHighlightDelayMS: function() { + return this.props.delayPressIn; + }, + + touchableGetLongPressDelayMS: function() { + return this.props.delayLongPress; + }, + + touchableGetPressOutDelayMS: function() { + return this.props.delayPressOut; + }, + _showUnderlay: function() { this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps); this.refs[CHILD_REF].setNativeProps(this.state.activeProps); diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index d99bf7380af4ae..a0891714f26766 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -15,11 +15,13 @@ var NativeMethodsMixin = require('NativeMethodsMixin'); var POPAnimationMixin = require('POPAnimationMixin'); var React = require('React'); +var TimerMixin = require('react-timer-mixin'); var Touchable = require('Touchable'); var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); var cloneWithProps = require('cloneWithProps'); var ensureComponentIsNative = require('ensureComponentIsNative'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var flattenStyle = require('flattenStyle'); var keyOf = require('keyOf'); var onlyChild = require('onlyChild'); @@ -50,7 +52,7 @@ var onlyChild = require('onlyChild'); */ var TouchableOpacity = React.createClass({ - mixins: [Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin], + mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin], propTypes: { ...TouchableWithoutFeedback.propTypes, @@ -72,6 +74,7 @@ var TouchableOpacity = React.createClass({ }, componentDidMount: function() { + ensurePositiveDelayProps(this.props); ensureComponentIsNative(this.refs[CHILD_REF]); }, @@ -79,6 +82,10 @@ var TouchableOpacity = React.createClass({ ensureComponentIsNative(this.refs[CHILD_REF]); }, + componentWillReceiveProps: function(nextProps) { + ensurePositiveDelayProps(nextProps); + }, + setOpacityTo: function(value) { if (POPAnimationMixin) { // Reset with animation if POP is available @@ -86,6 +93,7 @@ var TouchableOpacity = React.createClass({ var anim = { type: this.AnimationTypes.linear, property: this.AnimationProperties.opacity, + duration: 0.15, toValue: value, }; this.startAnimation(CHILD_REF, anim); @@ -102,20 +110,26 @@ var TouchableOpacity = React.createClass({ * defined on your component. */ touchableHandleActivePressIn: function() { - this.refs[CHILD_REF].setNativeProps({ - opacity: this.props.activeOpacity - }); + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + this._opacityActive(); this.props.onPressIn && this.props.onPressIn(); }, touchableHandleActivePressOut: function() { - var child = onlyChild(this.props.children); - var childStyle = flattenStyle(child.props.style) || {}; - this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity); + if (!this._hideTimeout) { + this._opacityInactive(); + } this.props.onPressOut && this.props.onPressOut(); }, touchableHandlePress: function() { + this.clearTimeout(this._hideTimeout); + this._opacityActive(); + this._hideTimeout = this.setTimeout( + this._opacityInactive, + this.props.delayPressOut || 100 + ); this.props.onPress && this.props.onPress(); }, @@ -128,7 +142,30 @@ var TouchableOpacity = React.createClass({ }, touchableGetHighlightDelayMS: function() { - return 0; + return this.props.delayPressIn || 0; + }, + + touchableGetLongPressDelayMS: function() { + return this.props.delayLongPress === 0 ? 0 : + this.props.delayLongPress || 500; + }, + + touchableGetPressOutDelayMS: function() { + return this.props.delayPressOut; + }, + + _opacityActive: function() { + this.setOpacityTo(this.props.activeOpacity); + }, + + _opacityInactive: function() { + this.clearTimeout(this._hideTimeout); + this._hideTimeout = null; + var child = onlyChild(this.props.children); + var childStyle = flattenStyle(child.props.style) || {}; + this.setOpacityTo( + childStyle.opacity === undefined ? 1 : childStyle.opacity + ); }, render: function() { diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index cd9ea02fdf1517..227cbeae282782 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -12,7 +12,9 @@ 'use strict'; var React = require('React'); +var TimerMixin = require('react-timer-mixin'); var Touchable = require('Touchable'); +var ensurePositiveDelayProps = require('ensurePositiveDelayProps'); var onlyChild = require('onlyChild'); /** @@ -31,23 +33,44 @@ type Event = Object; * one of the primary reason a "web" app doesn't feel "native". */ var TouchableWithoutFeedback = React.createClass({ - mixins: [Touchable.Mixin], + mixins: [TimerMixin, Touchable.Mixin], propTypes: { /** * Called when the touch is released, but not if cancelled (e.g. by a scroll * that steals the responder lock). */ + accessible: React.PropTypes.bool, onPress: React.PropTypes.func, onPressIn: React.PropTypes.func, onPressOut: React.PropTypes.func, onLongPress: React.PropTypes.func, + /** + * Delay in ms, from the start of the touch, before onPressIn is called. + */ + delayPressIn: React.PropTypes.number, + /** + * Delay in ms, from the release of the touch, before onPressOut is called. + */ + delayPressOut: React.PropTypes.number, + /** + * Delay in ms, from onPressIn, before onLongPress is called. + */ + delayLongPress: React.PropTypes.number, }, getInitialState: function() { return this.touchableGetInitialState(); }, + componentDidMount: function() { + ensurePositiveDelayProps(this.props); + }, + + componentWillReceiveProps: function(nextProps: Object) { + ensurePositiveDelayProps(nextProps); + }, + /** * `Touchable.Mixin` self callbacks. The mixin will invoke these if they are * defined on your component. @@ -73,13 +96,22 @@ var TouchableWithoutFeedback = React.createClass({ }, touchableGetHighlightDelayMS: function(): number { - return 0; + return this.props.delayPressIn || 0; + }, + + touchableGetLongPressDelayMS: function(): number { + return this.props.delayLongPress === 0 ? 0 : + this.props.delayLongPress || 500; + }, + + touchableGetPressOutDelayMS: function(): number { + return this.props.delayPressOut || 0; }, render: function(): ReactElement { // Note(avik): remove dynamic typecast once Flow has been upgraded return (React: any).cloneElement(onlyChild(this.props.children), { - accessible: true, + accessible: this.props.accessible !== false, testID: this.props.testID, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest, diff --git a/Libraries/Components/Touchable/ensurePositiveDelayProps.js b/Libraries/Components/Touchable/ensurePositiveDelayProps.js new file mode 100644 index 00000000000000..4c6525a54130ca --- /dev/null +++ b/Libraries/Components/Touchable/ensurePositiveDelayProps.js @@ -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. + * + * @providesModule ensurePositiveDelayProps + * @flow + */ +'use strict'; + +var invariant = require('invariant'); + +var ensurePositiveDelayProps = function(props: any) { + invariant( + !(props.delayPressIn < 0 || props.delayPressOut < 0 || + props.delayLongPress < 0), + 'Touchable components cannot have negative delay properties' + ); +}; + +module.exports = ensurePositiveDelayProps; diff --git a/Libraries/Inspector.js b/Libraries/Inspector.js index 5c70be2e8686a2..e0017c3cf32888 100644 --- a/Libraries/Inspector.js +++ b/Libraries/Inspector.js @@ -50,6 +50,9 @@ function findInstanceByNativeTag(rootTag, nativeTag) { var containerID = ReactNativeTagHandles.tagToRootNodeID[rootTag]; var rootInstance = ReactNativeMount._instancesByContainerID[containerID]; var targetID = ReactNativeTagHandles.tagToRootNodeID[nativeTag]; + if (!targetID) { + return undefined; + } return findInstance(rootInstance, targetID); } diff --git a/Libraries/Picker/PickerIOS.ios.js b/Libraries/Picker/PickerIOS.ios.js index b2c3c3927717a5..b1b1d06f466189 100644 --- a/Libraries/Picker/PickerIOS.ios.js +++ b/Libraries/Picker/PickerIOS.ios.js @@ -20,8 +20,7 @@ var RCTPickerIOSConsts = require('NativeModules').UIManager.RCTPicker.Constants; var StyleSheet = require('StyleSheet'); var View = require('View'); -var createReactNativeComponentClass = - require('createReactNativeComponentClass'); +var requireNativeComponent = require('requireNativeComponent'); var merge = require('merge'); var PICKER = 'picker'; @@ -112,14 +111,6 @@ var styles = StyleSheet.create({ }, }); -var rkPickerIOSAttributes = merge(ReactNativeViewAttributes.UIView, { - items: true, - selectedIndex: true, -}); - -var RCTPickerIOS = createReactNativeComponentClass({ - validAttributes: rkPickerIOSAttributes, - uiViewClassName: 'RCTPicker', -}); +var RCTPickerIOS = requireNativeComponent('RCTPicker', null); module.exports = PickerIOS; diff --git a/Libraries/PushNotificationIOS/PushNotificationIOS.js b/Libraries/PushNotificationIOS/PushNotificationIOS.js index 4d03c6641535f7..732d4c0294c9af 100644 --- a/Libraries/PushNotificationIOS/PushNotificationIOS.js +++ b/Libraries/PushNotificationIOS/PushNotificationIOS.js @@ -20,6 +20,7 @@ var _initialNotification = RCTPushNotificationManager && RCTPushNotificationManager.initialNotification; var DEVICE_NOTIF_EVENT = 'remoteNotificationReceived'; +var NOTIF_REGISTER_EVENT = 'remoteNotificationsRegistered'; /** * Handle push notifications for your app, including permission handling and @@ -49,30 +50,72 @@ class PushNotificationIOS { } /** - * Attaches a listener to remote notifications while the app is running in the - * foreground or the background. + * Attaches a listener to remote notification events while the app is running + * in the foreground or the background. * - * The handler will get be invoked with an instance of `PushNotificationIOS` + * Valid events are: + * + * - `notification` : Fired when a remote notification is received. The + * handler will be invoked with an instance of `PushNotificationIOS`. + * - `register`: Fired when the user registers for remote notifications. The + * handler will be invoked with a hex string representing the deviceToken. */ static addEventListener(type: string, handler: Function) { invariant( - type === 'notification', - 'PushNotificationIOS only supports `notification` events' - ); - _notifHandlers[handler] = RCTDeviceEventEmitter.addListener( - DEVICE_NOTIF_EVENT, - (notifData) => { - handler(new PushNotificationIOS(notifData)); - } + type === 'notification' || type === 'register', + 'PushNotificationIOS only supports `notification` and `register` events' ); + if (type === 'notification') { + _notifHandlers[handler] = RCTDeviceEventEmitter.addListener( + DEVICE_NOTIF_EVENT, + (notifData) => { + handler(new PushNotificationIOS(notifData)); + } + ); + } else if (type === 'register') { + _notifHandlers[handler] = RCTDeviceEventEmitter.addListener( + NOTIF_REGISTER_EVENT, + (registrationInfo) => { + handler(registrationInfo.deviceToken); + } + ); + } } /** - * Requests all notification permissions from iOS, prompting the user's - * dialog box. + * Requests notification permissions from iOS, prompting the user's + * dialog box. By default, it will request all notification permissions, but + * a subset of these can be requested by passing a map of requested + * permissions. + * The following permissions are supported: + * + * - `alert` + * - `badge` + * - `sound` + * + * If a map is provided to the method, only the permissions with truthy values + * will be requested. */ - static requestPermissions() { - RCTPushNotificationManager.requestPermissions(); + static requestPermissions(permissions?: { + alert?: boolean, + badge?: boolean, + sound?: boolean + }) { + var requestedPermissions = {}; + if (permissions) { + requestedPermissions = { + alert: !!permissions.alert, + badge: !!permissions.badge, + sound: !!permissions.sound + }; + } else { + requestedPermissions = { + alert: true, + badge: true, + sound: true + }; + } + RCTPushNotificationManager.requestPermissions(requestedPermissions); } /** @@ -97,8 +140,8 @@ class PushNotificationIOS { */ static removeEventListener(type: string, handler: Function) { invariant( - type === 'notification', - 'PushNotificationIOS only supports `notification` events' + type === 'notification' || type === 'register', + 'PushNotificationIOS only supports `notification` and `register` events' ); if (!_notifHandlers[handler]) { return; diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h index ef1ba1496e8c62..194bbc5ddeeda4 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.h +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.h @@ -14,6 +14,7 @@ @interface RCTPushNotificationManager : NSObject + (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; ++ (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; + (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification; @end diff --git a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m index 4846c885e302e8..1966c60452835c 100644 --- a/Libraries/PushNotificationIOS/RCTPushNotificationManager.m +++ b/Libraries/PushNotificationIOS/RCTPushNotificationManager.m @@ -12,7 +12,18 @@ #import "RCTBridge.h" #import "RCTEventDispatcher.h" +#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 + +#define UIUserNotificationTypeAlert UIRemoteNotificationTypeAlert +#define UIUserNotificationTypeBadge UIRemoteNotificationTypeBadge +#define UIUserNotificationTypeSound UIRemoteNotificationTypeSound +#define UIUserNotificationTypeNone UIRemoteNotificationTypeNone +#define UIUserNotificationType UIRemoteNotificationType + +#endif + NSString *const RCTRemoteNotificationReceived = @"RemoteNotificationReceived"; +NSString *const RCTRemoteNotificationsRegistered = @"RemoteNotificationsRegistered"; @implementation RCTPushNotificationManager { @@ -30,6 +41,10 @@ - (instancetype)init selector:@selector(handleRemoteNotificationReceived:) name:RCTRemoteNotificationReceived object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleRemoteNotificationsRegistered:) + name:RCTRemoteNotificationsRegistered + object:nil]; } return self; } @@ -52,6 +67,21 @@ + (void)application:(UIApplication *)application didRegisterUserNotificationSett } } ++ (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + NSMutableString *hexString = [NSMutableString string]; + const unsigned char *bytes = [deviceToken bytes]; + for (int i = 0; i < [deviceToken length]; i++) { + [hexString appendFormat:@"%02x", bytes[i]]; + } + NSDictionary *userInfo = @{ + @"deviceToken" : [hexString copy] + }; + [[NSNotificationCenter defaultCenter] postNotificationName:RCTRemoteNotificationsRegistered + object:self + userInfo:userInfo]; +} + + (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification { [[NSNotificationCenter defaultCenter] postNotificationName:RCTRemoteNotificationReceived @@ -65,6 +95,12 @@ - (void)handleRemoteNotificationReceived:(NSNotification *)notification body:[notification userInfo]]; } +- (void)handleRemoteNotificationsRegistered:(NSNotification *)notification +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"remoteNotificationsRegistered" + body:[notification userInfo]]; +} + /** * Update the application icon badge number on the home screen */ @@ -83,36 +119,35 @@ - (void)handleRemoteNotificationReceived:(NSNotification *)notification ]); } -RCT_EXPORT_METHOD(requestPermissions) +RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)permissions) { - Class _UIUserNotificationSettings; - if ((_UIUserNotificationSettings = NSClassFromString(@"UIUserNotificationSettings"))) { - UIUserNotificationType types = UIUserNotificationTypeSound | UIUserNotificationTypeBadge | UIUserNotificationTypeAlert; - UIUserNotificationSettings *notificationSettings = [_UIUserNotificationSettings settingsForTypes:types categories:nil]; - [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; + UIUserNotificationType types = UIRemoteNotificationTypeNone; + if (permissions) { + if ([permissions[@"alert"] boolValue]) { + types |= UIUserNotificationTypeAlert; + } + if ([permissions[@"badge"] boolValue]) { + types |= UIUserNotificationTypeBadge; + } + if ([permissions[@"sound"] boolValue]) { + types |= UIUserNotificationTypeSound; + } } else { + types = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound; + } -#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 - - [[UIApplication sharedApplication] registerForRemoteNotificationTypes: - UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert]; - +#if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0 + id notificationSettings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; + [[UIApplication sharedApplication] registerUserNotificationSettings:notificationSettings]; + [[UIApplication sharedApplication] registerForRemoteNotifications]; +#else + [[UIApplication sharedApplication] registerForRemoteNotificationTypes:types]; #endif - } } RCT_EXPORT_METHOD(checkPermissions:(RCTResponseSenderBlock)callback) { - -#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0 - -#define UIUserNotificationTypeAlert UIRemoteNotificationTypeAlert -#define UIUserNotificationTypeBadge UIRemoteNotificationTypeBadge -#define UIUserNotificationTypeSound UIRemoteNotificationTypeSound - -#endif - NSUInteger types = 0; if ([UIApplication instancesRespondToSelector:@selector(currentUserNotificationSettings)]) { types = [[[UIApplication sharedApplication] currentUserNotificationSettings] types]; diff --git a/Libraries/ReactIOS/InspectorOverlay/BorderBox.js b/Libraries/ReactIOS/InspectorOverlay/BorderBox.js new file mode 100644 index 00000000000000..caee8ccd6b3334 --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/BorderBox.js @@ -0,0 +1,38 @@ +/** + * 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 BorderBox + * @flow + */ +'use strict'; + +var React = require('React'); +var View = require('View'); + +class BorderBox extends React.Component { + render() { + var box = this.props.box; + if (!box) { + return this.props.children; + } + var style = { + borderTopWidth: box.top, + borderBottomWidth: box.bottom, + borderLeftWidth: box.left, + borderRightWidth: box.right, + }; + return ( + + {this.props.children} + + ); + } +} + +module.exports = BorderBox; + diff --git a/Libraries/ReactIOS/InspectorOverlay/BoxInspector.js b/Libraries/ReactIOS/InspectorOverlay/BoxInspector.js new file mode 100644 index 00000000000000..e50d9869aac7ce --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/BoxInspector.js @@ -0,0 +1,113 @@ +/** + * 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 BoxInspector + * @flow + */ +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); +var resolveBoxStyle = require('resolveBoxStyle'); + +var blank = { + top: 0, + left: 0, + right: 0, + bottom: 0, +}; + +class BoxInspector extends React.Component { + render() { + var frame = this.props.frame; + var style = this.props.style; + var margin = style && resolveBoxStyle('margin', style) || blank; + var padding = style && resolveBoxStyle('padding', style) || blank; + return ( + + + + + ({frame.left}, {frame.top}) + + + {frame.width} × {frame.height} + + + + + ); + } +} + +class BoxContainer extends React.Component { + render() { + var box = this.props.box; + return ( + + + {this.props.title} + {box.top} + + + {box.left} + {this.props.children} + {box.right} + + {box.bottom} + + ); + } +} + +var styles = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + }, + marginLabel: { + width: 60, + }, + label: { + fontSize: 10, + color: 'rgb(255,100,0)', + marginLeft: 5, + flex: 1, + textAlign: 'left', + top: -3, + }, + buffer: { + fontSize: 10, + color: 'yellow', + flex: 1, + textAlign: 'center', + }, + innerText: { + color: 'yellow', + fontSize: 12, + textAlign: 'center', + width: 70, + }, + box: { + borderWidth: 1, + borderColor: 'grey', + }, + boxText: { + color: 'white', + fontSize: 12, + marginHorizontal: 3, + marginVertical: 2, + textAlign: 'center', + }, +}); + +module.exports = BoxInspector; + diff --git a/Libraries/ReactIOS/InspectorOverlay/ElementBox.js b/Libraries/ReactIOS/InspectorOverlay/ElementBox.js new file mode 100644 index 00000000000000..a3851001c2ee45 --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/ElementBox.js @@ -0,0 +1,74 @@ +/** + * 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 ElementBox + * @flow + */ +'use strict'; + +var React = require('React'); +var View = require('View'); +var StyleSheet = require('StyleSheet'); +var BorderBox = require('BorderBox'); +var resolveBoxStyle = require('resolveBoxStyle'); + +var flattenStyle = require('flattenStyle'); + +class ElementBox extends React.Component { + render() { + var style = flattenStyle(this.props.style) || {}; + var margin = resolveBoxStyle('margin', style); + var padding = resolveBoxStyle('padding', style); + var frameStyle = this.props.frame; + if (margin) { + frameStyle = { + top: frameStyle.top - margin.top, + left: frameStyle.left - margin.left, + height: frameStyle.height + margin.top + margin.bottom, + width: frameStyle.width + margin.left + margin.right, + }; + } + var contentStyle = { + width: this.props.frame.width, + height: this.props.frame.height, + }; + if (padding) { + contentStyle = { + width: contentStyle.width - padding.left - padding.right, + height: contentStyle.height - padding.top - padding.bottom, + }; + } + return ( + + + + + + + + ); + } +} + +var styles = StyleSheet.create({ + frame: { + position: 'absolute', + }, + content: { + backgroundColor: 'rgba(200, 230, 255, 0.8)', + }, + padding: { + borderColor: 'rgba(77, 255, 0, 0.3)', + }, + margin: { + borderColor: 'rgba(255, 132, 0, 0.3)', + }, +}); + +module.exports = ElementBox; + diff --git a/Libraries/ReactIOS/InspectorOverlay/ElementProperties.js b/Libraries/ReactIOS/InspectorOverlay/ElementProperties.js new file mode 100644 index 00000000000000..310374fb148f75 --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/ElementProperties.js @@ -0,0 +1,105 @@ +/** + * 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 ElementProperties + * @flow + */ +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); +var PropTypes = require('ReactPropTypes'); +var BoxInspector = require('BoxInspector'); +var StyleInspector = require('StyleInspector'); +var TouchableHighlight = require('TouchableHighlight'); +var TouchableWithoutFeedback = require('TouchableWithoutFeedback'); + +var flattenStyle = require('flattenStyle'); +var mapWithSeparator = require('mapWithSeparator'); + +var ElementProperties = React.createClass({ + propTypes: { + hierarchy: PropTypes.array.isRequired, + style: PropTypes.array.isRequired, + }, + + render: function() { + var style = flattenStyle(this.props.style); + var selection = this.props.selection; + // Without the `TouchableWithoutFeedback`, taps on this inspector pane + // would change the inspected element to whatever is under the inspector + return ( + + + + {mapWithSeparator( + this.props.hierarchy, + (item, i) => ( + this.props.setSelection(i)}> + + {item.getName ? item.getName() : 'Unknown'} + + + ), + () => + )} + + + + + + + + ); + } +}); + +var styles = StyleSheet.create({ + breadSep: { + fontSize: 8, + color: 'white', + }, + breadcrumb: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 5, + }, + selected: { + borderColor: 'white', + borderRadius: 5, + }, + breadItem: { + borderWidth: 1, + borderColor: 'transparent', + marginHorizontal: 2, + }, + breadItemText: { + fontSize: 10, + color: 'white', + marginHorizontal: 5, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + info: { + backgroundColor: 'rgba(0, 0, 0, 0.7)', + padding: 10, + }, + path: { + color: 'white', + fontSize: 9, + }, +}); + +module.exports = ElementProperties; diff --git a/Libraries/ReactIOS/InspectorOverlay.js b/Libraries/ReactIOS/InspectorOverlay/InspectorOverlay.js similarity index 53% rename from Libraries/ReactIOS/InspectorOverlay.js rename to Libraries/ReactIOS/InspectorOverlay/InspectorOverlay.js index eeb6e7965728f9..3b391502ffa4fc 100644 --- a/Libraries/ReactIOS/InspectorOverlay.js +++ b/Libraries/ReactIOS/InspectorOverlay/InspectorOverlay.js @@ -17,12 +17,16 @@ var StyleSheet = require('StyleSheet'); var Text = require('Text'); var UIManager = require('NativeModules').UIManager; var View = require('View'); +var ElementBox = require('ElementBox'); +var ElementProperties = require('ElementProperties'); var InspectorOverlay = React.createClass({ getInitialState: function() { return { frame: null, + pointerY: 0, hierarchy: [], + selection: -1, }; }, @@ -33,15 +37,34 @@ var InspectorOverlay = React.createClass({ [locationX, locationY], (nativeViewTag, left, top, width, height) => { var instance = Inspector.findInstanceByNativeTag(this.props.rootTag, nativeViewTag); + if (!instance) { + return; + } var hierarchy = Inspector.getOwnerHierarchy(instance); + var publicInstance = instance.getPublicInstance(); this.setState({ hierarchy, - frame: {left, top, width, height} + pointerY: locationY, + selection: hierarchy.length - 1, + frame: {left, top, width, height}, + style: publicInstance.props ? publicInstance.props.style : {}, }); } ); }, + setSelection(i) { + var instance = this.state.hierarchy[i]; + var publicInstance = instance.getPublicInstance(); + UIManager.measure(React.findNodeHandle(instance), (x, y, width, height, left, top) => { + this.setState({ + frame: {left, top, width, height}, + style: publicInstance.props ? publicInstance.props.style : {}, + selection: i, + }); + }); + }, + shouldSetResponser: function(e) { this.findViewForTouchEvent(e); return true; @@ -49,18 +72,32 @@ var InspectorOverlay = React.createClass({ render: function() { var content = []; + var justifyContent = 'flex-end'; if (this.state.frame) { - var distanceToTop = this.state.frame.top; - var distanceToBottom = Dimensions.get('window').height - - (this.state.frame.top + this.state.frame.height); + var distanceToTop = this.state.pointerY; + var distanceToBottom = Dimensions.get('window').height - distanceToTop; - var justifyContent = distanceToTop > distanceToBottom + justifyContent = distanceToTop > distanceToBottom ? 'flex-start' : 'flex-end'; - content.push(); - content.push(); + content.push(); + content.push( + + ); + } else { + content.push( + + Welcome to the inspector! Tap something to inspect it. + + ); } return ( { - return instance.getName ? instance.getName() : 'Unknown'; - }).join(' > '); - return ( - - - {path} - - - ); - } -}); - var styles = StyleSheet.create({ + welcomeMessage: { + backgroundColor: 'rgba(0, 0, 0, 0.7)', + padding: 10, + paddingVertical: 50, + }, + welcomeText: { + color: 'white', + }, inspector: { - backgroundColor: 'rgba(255,255,255,0.8)', + backgroundColor: 'rgba(255,255,255,0.0)', position: 'absolute', left: 0, top: 0, right: 0, bottom: 0, }, - frame: { - position: 'absolute', - backgroundColor: 'rgba(155,155,255,0.3)', - }, - info: { - backgroundColor: 'rgba(0, 0, 0, 0.7)', - padding: 10, - }, - path: { - color: 'white', - fontSize: 9, - } }); module.exports = InspectorOverlay; diff --git a/Libraries/ReactIOS/InspectorOverlay/StyleInspector.js b/Libraries/ReactIOS/InspectorOverlay/StyleInspector.js new file mode 100644 index 00000000000000..702d01e1da35a8 --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/StyleInspector.js @@ -0,0 +1,63 @@ +/** + * 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 StyleInspector + * @flow + */ +'use strict'; + +var React = require('React'); +var StyleSheet = require('StyleSheet'); +var Text = require('Text'); +var View = require('View'); + +class StyleInspector extends React.Component { + render() { + if (!this.props.style) { + return No style; + } + var names = Object.keys(this.props.style); + return ( + + + {names.map(name => {name}:)} + + + {names.map(name => {this.props.style[name]})} + + + ); + } +} + +var styles = StyleSheet.create({ + container: { + flexDirection: 'row', + }, + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + }, + attr: { + fontSize: 10, + color: '#ccc', + }, + value: { + fontSize: 10, + color: 'white', + marginLeft: 10, + }, + noStyle: { + color: 'white', + fontSize: 10, + }, +}); + +module.exports = StyleInspector; + diff --git a/Libraries/ReactIOS/InspectorOverlay/resolveBoxStyle.js b/Libraries/ReactIOS/InspectorOverlay/resolveBoxStyle.js new file mode 100644 index 00000000000000..e0bfb601c129a3 --- /dev/null +++ b/Libraries/ReactIOS/InspectorOverlay/resolveBoxStyle.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule resolveBoxStyle + * @flow + */ +'use strict'; + +/** + * Resolve a style property into it's component parts, e.g. + * + * resolveProperties('margin', {margin: 5, marginBottom: 10}) + * -> + * {top: 5, left: 5, right: 5, bottom: 10} + * + * If none are set, returns false. + */ +function resolveBoxStyle(prefix: String, style: Object): ?Object { + var res = {}; + var subs = ['top', 'left', 'bottom', 'right']; + var set = false; + subs.forEach(sub => { + res[sub] = style[prefix] || 0; + }); + if (style[prefix]) { + set = true; + } + if (style[prefix + 'Vertical']) { + res.top = res.bottom = style[prefix + 'Vertical']; + set = true; + } + if (style[prefix + 'Horizontal']) { + res.left = res.right = style[prefix + 'Horizontal']; + set = true; + } + subs.forEach(sub => { + var val = style[prefix + capFirst(sub)]; + if (val) { + res[sub] = val; + set = true; + } + }); + if (!set) { + return; + } + return res; +} + +function capFirst(text) { + return text[0].toUpperCase() + text.slice(1); +} + +module.exports = resolveBoxStyle; + diff --git a/Libraries/Utilities/mapWithSeparator.js b/Libraries/Utilities/mapWithSeparator.js new file mode 100644 index 00000000000000..4aa8665c3520b1 --- /dev/null +++ b/Libraries/Utilities/mapWithSeparator.js @@ -0,0 +1,19 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @providesModule mapWithSeparator + */ +'use strict'; + +function mapWithSeparator(array, valueFunction, separatorFunction) { + var results = []; + for (var i = 0; i < array.length; i++) { + results.push(valueFunction(array[i], i, array)); + if (i !== array.length - 1) { + results.push(separatorFunction(i)); + } + } + return results; +} + +module.exports = mapWithSeparator; diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index c81183b5b2f3e0..6b1c992ccedaa0 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -24,11 +24,12 @@ var ReactNative = Object.assign(Object.create(require('React')), { Image: require('Image'), ListView: require('ListView'), MapView: require('MapView'), + Navigator: require('Navigator'), NavigatorIOS: require('NavigatorIOS'), PickerIOS: require('PickerIOS'), - Navigator: require('Navigator'), - SegmentedControlIOS: require('SegmentedControlIOS'), + ProgressViewIOS: require('ProgressViewIOS'), ScrollView: require('ScrollView'), + SegmentedControlIOS: require('SegmentedControlIOS'), SliderIOS: require('SliderIOS'), SwitchIOS: require('SwitchIOS'), TabBarIOS: require('TabBarIOS'), @@ -47,12 +48,12 @@ var ReactNative = Object.assign(Object.create(require('React')), { AsyncStorage: require('AsyncStorage'), CameraRoll: require('CameraRoll'), InteractionManager: require('InteractionManager'), - LinkingIOS: require('LinkingIOS'), LayoutAnimation: require('LayoutAnimation'), + LinkingIOS: require('LinkingIOS'), NetInfo: require('NetInfo'), + PanResponder: require('PanResponder'), PixelRatio: require('PixelRatio'), PushNotificationIOS: require('PushNotificationIOS'), - PanResponder: require('PanResponder'), StatusBarIOS: require('StatusBarIOS'), StyleSheet: require('StyleSheet'), VibrationIOS: require('VibrationIOS'), diff --git a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js index 37c42382712627..2a0dd1813aec0b 100644 --- a/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js +++ b/Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js @@ -232,6 +232,8 @@ var PRESS_EXPAND_PX = 20; var LONG_PRESS_THRESHOLD = 500; +var LONG_PRESS_DELAY_MS = LONG_PRESS_THRESHOLD - HIGHLIGHT_DELAY_MS; + var LONG_PRESS_ALLOWED_MOVEMENT = 10; // Default amount "active" region protrudes beyond box @@ -276,7 +278,7 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10; * + * | RESPONDER_GRANT (HitRect) * v - * +---------------------------+ DELAY +-------------------------+ T - DELAY +------------------------------+ + * +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+ * |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN| * +---------------------------+ +-------------------------+ +------------------------------+ * + ^ + ^ + ^ @@ -288,7 +290,7 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10; * |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT| * +----------------------------+ +--------------------------+ +-------------------------------+ * - * T - DELAY => LONG_PRESS_THRESHOLD - DELAY + * T + DELAY => LONG_PRESS_DELAY_MS + DELAY * * Not drawn are the side effects of each transition. The most important side * effect is the `touchableHandlePress` abstract method invocation that occurs @@ -348,12 +350,16 @@ var TouchableMixin = { // event to make sure it doesn't get reused in the event object pool. e.persist(); + this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout); + this.pressOutDelayTimeout = null; + this.state.touchable.touchState = States.NOT_RESPONDER; this.state.touchable.responderID = dispatchID; this._receiveSignal(Signals.RESPONDER_GRANT, e); var delayMS = this.touchableGetHighlightDelayMS !== undefined ? - this.touchableGetHighlightDelayMS() : HIGHLIGHT_DELAY_MS; + Math.max(this.touchableGetHighlightDelayMS(), 0) : HIGHLIGHT_DELAY_MS; + delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS; if (delayMS !== 0) { this.touchableDelayTimeout = setTimeout( this._handleDelay.bind(this, e), @@ -363,9 +369,13 @@ var TouchableMixin = { this._handleDelay(e); } + var longDelayMS = + this.touchableGetLongPressDelayMS !== undefined ? + Math.max(this.touchableGetLongPressDelayMS(), 10) : LONG_PRESS_DELAY_MS; + longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS; this.longPressDelayTimeout = setTimeout( this._handleLongDelay.bind(this, e), - LONG_PRESS_THRESHOLD - delayMS + longDelayMS + delayMS ); }, @@ -632,8 +642,14 @@ var TouchableMixin = { if (newIsHighlight && !curIsHighlight) { this._savePressInLocation(e); this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(); - } else if (!newIsHighlight && curIsHighlight) { - this.touchableHandleActivePressOut && this.touchableHandleActivePressOut(); + } else if (!newIsHighlight && curIsHighlight && this.touchableHandleActivePressOut) { + if (this.touchableGetPressOutDelayMS && this.touchableGetPressOutDelayMS()) { + this.pressOutDelayTimeout = this.setTimeout(function() { + this.touchableHandleActivePressOut(); + }, this.touchableGetPressOutDelayMS()); + } else { + this.touchableHandleActivePressOut(); + } } if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) { diff --git a/React/Base/RCTDevMenu.h b/React/Base/RCTDevMenu.h index b260fca4a4c6cc..ed1ff90b8cb098 100644 --- a/React/Base/RCTDevMenu.h +++ b/React/Base/RCTDevMenu.h @@ -51,6 +51,12 @@ */ - (void)reload; +/** + * Add custom item to the development menu. The handler will be called + * when user selects the item. + */ +- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler; + @end /** diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 2416e974c1757f..8a5f23f9d2f256 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -43,6 +43,28 @@ - (void)RCT_motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event @end +@interface RCTDevMenuItem : NSObject + +@property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) dispatch_block_t handler; + +- (instancetype)initWithTitle:(NSString *)title handler:(dispatch_block_t)handler; + +@end + +@implementation RCTDevMenuItem + +- (instancetype)initWithTitle:(NSString *)title handler:(dispatch_block_t)handler +{ + if (self = [super init]) { + self.title = title; + self.handler = handler; + } + return self; +} + +@end + @interface RCTDevMenu () @property (nonatomic, strong) Class executorClass; @@ -57,6 +79,8 @@ @implementation RCTDevMenu NSURLSessionDataTask *_updateTask; NSURL *_liveReloadURL; BOOL _jsLoaded; + NSArray *_presentedItems; + NSMutableArray *_extraMenuItems; } @synthesize bridge = _bridge; @@ -94,6 +118,7 @@ - (instancetype)init _defaults = [NSUserDefaults standardUserDefaults]; _settings = [[NSMutableDictionary alloc] init]; + _extraMenuItems = [NSMutableArray array]; // Delay setup until after Bridge init [self settingsDidChange]; @@ -110,6 +135,13 @@ - (instancetype)init [weakSelf toggle]; }]; + // Toggle element inspector + [commands registerKeyCommandWithInput:@"i" + modifierFlags:UIKeyModifierCommand + action:^(UIKeyCommand *command) { + [_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; + }]; + // Reload in normal mode [commands registerKeyCommandWithInput:@"n" modifierFlags:UIKeyModifierCommand @@ -225,32 +257,82 @@ - (void)toggle } } -RCT_EXPORT_METHOD(show) +- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler { - if (_actionSheet || !_bridge) { - return; + [_extraMenuItems addObject:[[RCTDevMenuItem alloc] initWithTitle:title handler:handler]]; +} + +- (NSArray *)menuItems +{ + NSMutableArray *items = [NSMutableArray array]; + + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Reload" handler:^{ + [self reload]; + }]]; + + Class chromeExecutorClass = NSClassFromString(@"RCTWebSocketExecutor"); + if (!chromeExecutorClass) { + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Chrome Debugger Unavailable" handler:^{ + [[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable" + message:@"You need to include the RCTWebSocket library to enable Chrome debugging" + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil] show]; + }]]; + } else { + BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass; + NSString *debugTitleChrome = isDebuggingInChrome ? @"Disable Chrome Debugging" : @"Debug in Chrome"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleChrome handler:^{ + self.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass; + }]]; } - NSString *debugTitleChrome = _executorClass && _executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Debug in Chrome"; - NSString *debugTitleSafari = _executorClass && _executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Debug in Safari"; - NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor"; + Class safariExecutorClass = NSClassFromString(@"RCTWebViewExecutor"); + BOOL isDebuggingInSafari = _executorClass && _executorClass == safariExecutorClass; + NSString *debugTitleSafari = isDebuggingInSafari ? @"Disable Safari Debugging" : @"Debug in Safari"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:debugTitleSafari handler:^{ + self.executorClass = isDebuggingInSafari ? Nil : safariExecutorClass; + }]]; - UIActionSheet *actionSheet = - [[UIActionSheet alloc] initWithTitle:@"React Native: Development" - delegate:self - cancelButtonTitle:nil - destructiveButtonTitle:nil - otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, fpsMonitor, nil]; + NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:fpsMonitor handler:^{ + self.showFPS = !_showFPS; + }]]; - [actionSheet addButtonWithTitle:@"Inspect Element"]; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:@"Inspect Element" handler:^{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; + }]]; if (_liveReloadURL) { - NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:liveReloadTitle handler:^{ + self.liveReloadEnabled = !_liveReloadEnabled; + }]]; + NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Profiling" : @"Start Profiling"; + [items addObject:[[RCTDevMenuItem alloc] initWithTitle:profilingTitle handler:^{ + self.profilingEnabled = !_profilingEnabled; + }]]; + } + + [items addObjectsFromArray:_extraMenuItems]; - [actionSheet addButtonWithTitle:liveReloadTitle]; - [actionSheet addButtonWithTitle:profilingTitle]; + return items; +} + +RCT_EXPORT_METHOD(show) +{ + if (_actionSheet || !_bridge) { + return; + } + + UIActionSheet *actionSheet = [[UIActionSheet alloc] init]; + actionSheet.title = @"React Native: Development"; + actionSheet.delegate = self; + + NSArray *items = [self menuItems]; + for (RCTDevMenuItem *item in items) { + [actionSheet addButtonWithTitle:item.title]; } [actionSheet addButtonWithTitle:@"Cancel"]; @@ -259,13 +341,7 @@ - (void)toggle actionSheet.actionSheetStyle = UIBarStyleBlack; [actionSheet showInView:[UIApplication sharedApplication].keyWindow.rootViewController.view]; _actionSheet = actionSheet; -} - -RCT_EXPORT_METHOD(reload) -{ - _jsLoaded = NO; - _liveReloadURL = nil; - [_bridge reload]; + _presentedItems = items; } - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex @@ -275,48 +351,16 @@ - (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger return; } - switch (buttonIndex) { - case 0: { - [self reload]; - break; - } - case 1: { - Class cls = NSClassFromString(@"RCTWebSocketExecutor"); - if (!cls) { - [[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable" - message:@"You need to include the RCTWebSocket library to enable Chrome debugging" - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil] show]; - return; - } - self.executorClass = (_executorClass == cls) ? Nil : cls; - break; - } - case 2: { - Class cls = NSClassFromString(@"RCTWebViewExecutor"); - self.executorClass = (_executorClass == cls) ? Nil : cls; - break; - } - case 3: { - self.showFPS = !_showFPS; - break; - } - case 4: { - [_bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil]; - break; - } - case 5: { - self.liveReloadEnabled = !_liveReloadEnabled; - break; - } - case 6: { - self.profilingEnabled = !_profilingEnabled; - break; - } - default: - break; - } + RCTDevMenuItem *item = _presentedItems[buttonIndex]; + item.handler(); + return; +} + +RCT_EXPORT_METHOD(reload) +{ + _jsLoaded = NO; + _liveReloadURL = nil; + [_bridge reload]; } - (void)setShakeToShow:(BOOL)shakeToShow @@ -438,6 +482,7 @@ @implementation RCTDevMenu - (void)show {} - (void)reload {} +- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler {} @end diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index f95f134c34f16b..4ffef7ee752cad 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -36,8 +36,6 @@ @implementation RCTTouchHandler BOOL _recordingInteractionTiming; CFTimeInterval _mostRecentEnqueueJS; - NSMutableArray *_pendingTouches; - NSMutableArray *_bridgeInteractionTiming; } - (instancetype)initWithBridge:(RCTBridge *)bridge @@ -52,9 +50,6 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge _reactTouches = [[NSMutableArray alloc] init]; _touchViews = [[NSMutableArray alloc] init]; - _pendingTouches = [[NSMutableArray alloc] init]; - _bridgeInteractionTiming = [[NSMutableArray alloc] init]; - // `cancelsTouchesInView` is needed in order to be used as a top level // event delegated recognizer. Otherwise, lower-level components not built // using RCT, will fail to recognize gestures. diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 5c34d0e0a582db..641500b3884976 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -18,6 +18,8 @@ // Utility functions for JSON object <-> string serialization/deserialization RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error); RCT_EXTERN id RCTJSONParse(NSString *jsonString, NSError **error); +RCT_EXTERN id RCTJSONParseMutable(NSString *jsonString, NSError **error); +RCT_EXTERN id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options); // Strip non JSON-safe values from an object graph RCT_EXTERN id RCTJSONClean(id object); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 712e9724e3cd5c..613b13163446d5 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -24,7 +24,7 @@ return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil; } -id RCTJSONParse(NSString *jsonString, NSError **error) +id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJSONReadingOptions options) { if (!jsonString) { return nil; @@ -39,7 +39,15 @@ id RCTJSONParse(NSString *jsonString, NSError **error) return nil; } } - return [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:error]; + return [NSJSONSerialization JSONObjectWithData:jsonData options:options error:error]; +} + +id RCTJSONParse(NSString *jsonString, NSError **error) { + return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingAllowFragments); +} + +id RCTJSONParseMutable(NSString *jsonString, NSError **error) { + return RCTJSONParseWithOptions(jsonString, error, NSJSONReadingMutableContainers|NSJSONReadingMutableLeaves); } id RCTJSONClean(id object) diff --git a/React/Modules/RCTAsyncLocalStorage.m b/React/Modules/RCTAsyncLocalStorage.m index 2c01161d48fa93..76f7fa885556e9 100644 --- a/React/Modules/RCTAsyncLocalStorage.m +++ b/React/Modules/RCTAsyncLocalStorage.m @@ -61,6 +61,34 @@ static id RCTReadFile(NSString *filePath, NSString *key, NSDictionary **errorOut return nil; } +// Only merges objects - all other types are just clobbered (including arrays) +static void RCTMergeRecursive(NSMutableDictionary *destination, NSDictionary *source) +{ + for (NSString *key in source) { + id sourceValue = source[key]; + if ([sourceValue isKindOfClass:[NSDictionary class]]) { + id destinationValue = destination[key]; + NSMutableDictionary *nestedDestination; + if ([destinationValue classForCoder] == [NSMutableDictionary class]) { + nestedDestination = destinationValue; + } else { + if ([destinationValue isKindOfClass:[NSDictionary class]]) { + // Ideally we wouldn't eagerly copy here... + nestedDestination = [destinationValue mutableCopy]; + } else { + destination[key] = [sourceValue copy]; + } + } + if (nestedDestination) { + RCTMergeRecursive(nestedDestination, sourceValue); + destination[key] = nestedDestination; + } + } else { + destination[key] = sourceValue; + } + } +} + #pragma mark - RCTAsyncLocalStorage @implementation RCTAsyncLocalStorage @@ -135,13 +163,19 @@ - (id)_appendItemForKey:(NSString *)key toArray:(NSMutableArray *)result if (errorOut) { return errorOut; } + id value = [self _getValueForKey:key errorOut:&errorOut]; + [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. + return errorOut; +} + +- (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut +{ id value = _manifest[key]; // nil means missing, null means there is a data file, anything else is an inline value. if (value == [NSNull null]) { NSString *filePath = [self _filePathForKey:key]; - value = RCTReadFile(filePath, key, &errorOut); + value = RCTReadFile(filePath, key, errorOut); } - [result addObject:@[key, value ?: [NSNull null]]]; // Insert null if missing or failure. - return errorOut; + return value; } - (id)_writeEntry:(NSArray *)entry @@ -198,7 +232,6 @@ - (id)_writeEntry:(NSArray *)entry id keyError = [self _appendItemForKey:key toArray:result]; RCTAppendError(keyError, &errors); } - [self _writeManifest:&errors]; callback(@[errors ?: [NSNull null], result]); } @@ -221,6 +254,38 @@ - (id)_writeEntry:(NSArray *)entry } } +RCT_EXPORT_METHOD(multiMerge:(NSArray *)kvPairs + callback:(RCTResponseSenderBlock)callback) +{ + id errorOut = [self _ensureSetup]; + if (errorOut) { + callback(@[@[errorOut]]); + return; + } + NSMutableArray *errors; + for (__strong NSArray *entry in kvPairs) { + id keyError; + NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; + if (keyError) { + RCTAppendError(keyError, &errors); + } else { + if (value) { + NSMutableDictionary *mergedVal = [RCTJSONParseMutable(value, &keyError) mutableCopy]; + RCTMergeRecursive(mergedVal, RCTJSONParse(entry[1], &keyError)); + entry = @[entry[0], RCTJSONStringify(mergedVal, &keyError)]; + } + if (!keyError) { + keyError = [self _writeEntry:entry]; + } + RCTAppendError(keyError, &errors); + } + } + [self _writeManifest:&errors]; + if (callback) { + callback(@[errors ?: [NSNull null]]); + } +} + RCT_EXPORT_METHOD(multiRemove:(NSArray *)keys callback:(RCTResponseSenderBlock)callback) { diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index c7309989b4ca88..3c0cfe72284851 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 134FCB361A6D42D900051CC8 /* RCTSparseArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 83BEE46D1A6D19BC00B5863B /* RCTSparseArray.m */; }; 134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */; }; 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 134FCB3C1A6E7F0800051CC8 /* RCTWebViewExecutor.m */; }; + 13513F3C1B1F43F400FCE529 /* RCTProgressViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13513F3B1B1F43F400FCE529 /* RCTProgressViewManager.m */; }; 13723B501A82FD3C00F88898 /* RCTStatusBarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */; }; 1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */ = {isa = PBXBuildFile; fileRef = 1372B7091AB030C200659ED6 /* RCTAppState.m */; }; 137327E71AA5CF210034F82E /* RCTTabBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 137327E01AA5CF210034F82E /* RCTTabBar.m */; }; @@ -104,6 +105,8 @@ 134FCB3A1A6E7F0800051CC8 /* RCTContextExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTContextExecutor.m; sourceTree = ""; }; 134FCB3B1A6E7F0800051CC8 /* RCTWebViewExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebViewExecutor.h; sourceTree = ""; }; 134FCB3C1A6E7F0800051CC8 /* RCTWebViewExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebViewExecutor.m; sourceTree = ""; }; + 13513F3A1B1F43F400FCE529 /* RCTProgressViewManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTProgressViewManager.h; sourceTree = ""; }; + 13513F3B1B1F43F400FCE529 /* RCTProgressViewManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTProgressViewManager.m; sourceTree = ""; }; 13723B4E1A82FD3C00F88898 /* RCTStatusBarManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTStatusBarManager.h; sourceTree = ""; }; 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTStatusBarManager.m; sourceTree = ""; }; 1372B7081AB030C200659ED6 /* RCTAppState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAppState.h; sourceTree = ""; }; @@ -307,6 +310,8 @@ 58114A141AAE854800E7D092 /* RCTPickerManager.h */, 58114A151AAE854800E7D092 /* RCTPickerManager.m */, 13442BF31AA90E0B0037E5B0 /* RCTPointerEvents.h */, + 13513F3A1B1F43F400FCE529 /* RCTProgressViewManager.h */, + 13513F3B1B1F43F400FCE529 /* RCTProgressViewManager.m */, 131B6AF01AF1093D00FFC3E0 /* RCTSegmentedControl.h */, 131B6AF11AF1093D00FFC3E0 /* RCTSegmentedControl.m */, 131B6AF21AF1093D00FFC3E0 /* RCTSegmentedControlManager.h */, @@ -524,6 +529,7 @@ 58114A501AAE93D500E7D092 /* RCTAsyncLocalStorage.m in Sources */, 832348161A77A5AA00B55238 /* Layout.c in Sources */, 14F4D38B1AE1B7E40049C042 /* RCTProfile.m in Sources */, + 13513F3C1B1F43F400FCE529 /* RCTProgressViewManager.m in Sources */, 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */, 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */, 13B080201A69489C00A75B9A /* RCTActivityIndicatorViewManager.m in Sources */, diff --git a/React/Views/RCTProgressViewManager.h b/React/Views/RCTProgressViewManager.h new file mode 100644 index 00000000000000..ae8a6a38805b3d --- /dev/null +++ b/React/Views/RCTProgressViewManager.h @@ -0,0 +1,14 @@ +/** + * 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 "RCTViewManager.h" + +@interface RCTProgressViewManager : RCTViewManager + +@end diff --git a/React/Views/RCTProgressViewManager.m b/React/Views/RCTProgressViewManager.m new file mode 100644 index 00000000000000..deb6285a68c6ec --- /dev/null +++ b/React/Views/RCTProgressViewManager.m @@ -0,0 +1,47 @@ +/** + * 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 "RCTProgressViewManager.h" + +#import "RCTConvert.h" + +@implementation RCTConvert (RCTProgressViewManager) + +RCT_ENUM_CONVERTER(UIProgressViewStyle, (@{ + @"default": @(UIProgressViewStyleDefault), + @"bar": @(UIProgressViewStyleBar), +}), UIProgressViewStyleDefault, integerValue) + +@end + +@implementation RCTProgressViewManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [[UIProgressView alloc] init]; +} + +RCT_EXPORT_VIEW_PROPERTY(progressViewStyle, UIProgressViewStyle) +RCT_EXPORT_VIEW_PROPERTY(progress, float) +RCT_EXPORT_VIEW_PROPERTY(progressTintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(trackTintColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(progressImage, UIImage) +RCT_EXPORT_VIEW_PROPERTY(trackImage, UIImage) + +- (NSDictionary *)constantsToExport +{ + UIProgressView *view = [[UIProgressView alloc] init]; + return @{ + @"ComponentHeight": @(view.intrinsicContentSize.height), + }; +} + +@end diff --git a/packager/README.md b/packager/README.md index 8f9f649bf5ac17..c3fae48064e801 100644 --- a/packager/README.md +++ b/packager/README.md @@ -62,7 +62,7 @@ if the option is boolean `1/0` or `true/false` is accepted. Here are the current options the packager accepts: * `dev` boolean, defaults to true: sets a global `__DEV__` variable - which will effect how the React Nativeg core libraries behave. + which will effect how the React Native core libraries behave. * `minify` boolean, defaults to false: whether to minify the bundle. * `runModule` boolean, defaults to true: whether to require your entry point module. So if you requested `moduleName`, this option will add diff --git a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js b/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js index 90db1c4ade5f54..3c1a1d26dc54c1 100644 --- a/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js +++ b/packager/react-packager/src/DependencyResolver/ModuleDescriptor.js @@ -8,6 +8,9 @@ */ 'use strict'; +var Promise = require('bluebird'); +var isAbsolutePath = require('absolute-path'); + function ModuleDescriptor(fields) { if (!fields.id) { throw new Error('Missing required fields id'); @@ -17,17 +20,13 @@ function ModuleDescriptor(fields) { if (!fields.path) { throw new Error('Missing required fields path'); } + if (!isAbsolutePath(fields.path)) { + throw new Error('Expected absolute path but found: ' + 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; @@ -50,12 +49,30 @@ function ModuleDescriptor(fields) { this._fields = fields; } +ModuleDescriptor.prototype.loadDependencies = function(loader) { + if (!this.dependencies) { + if (this._loadingDependencies) { + return this._loadingDependencies; + } + + var self = this; + this._loadingDependencies = loader(this).then(function(dependencies) { + self.dependencies = dependencies; + }); + return this._loadingDependencies; + } + + return Promise.resolve(this.dependencies); +}; + ModuleDescriptor.prototype.toJSON = function() { - return { - id: this.id, - path: this.path, - dependencies: this.dependencies - }; + var ret = {}; + Object.keys(this).forEach(function(prop) { + if (prop[0] !== '_' && typeof this[prop] !== 'function') { + ret[prop] = this[prop]; + } + }, this); + return ret; }; module.exports = ModuleDescriptor; diff --git a/packager/react-packager/src/DependencyResolver/__tests__/ModuleDescriptor-test.js b/packager/react-packager/src/DependencyResolver/__tests__/ModuleDescriptor-test.js new file mode 100644 index 00000000000000..99bed5df70eab0 --- /dev/null +++ b/packager/react-packager/src/DependencyResolver/__tests__/ModuleDescriptor-test.js @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +jest + .dontMock('absolute-path') + .dontMock('../ModuleDescriptor'); + + +describe('ModuleDescriptor', function() { + var ModuleDescriptor; + var Promise; + + beforeEach(function() { + ModuleDescriptor = require('../ModuleDescriptor'); + Promise = require('bluebird'); + }); + + describe('constructor', function() { + it('should validate fields', function() { + /* eslint no-new:0*/ + expect(function() { + new ModuleDescriptor({}); + }).toThrow(); + + expect(function() { + new ModuleDescriptor({ + id: 'foo', + }); + }).toThrow(); + + expect(function() { + new ModuleDescriptor({ + id: 'foo', + path: 'foo', + }); + }).toThrow(); + + expect(function() { + new ModuleDescriptor({ + id: 'foo', + path: '/foo', + isAsset: true, + }); + }).toThrow(); + + var m = new ModuleDescriptor({ + id: 'foo', + path: '/foo', + isAsset: true, + resolution: 1, + }); + + expect(m.toJSON()).toEqual({ + altId:undefined, + dependencies: undefined, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + id: 'foo', + path: '/foo', + isAsset: true, + resolution: 1, + }); + }); + }); + + describe('loadDependencies', function() { + pit('should load dependencies', function() { + var mod = new ModuleDescriptor({ + id: 'foo', + path: '/foo', + isAsset: true, + resolution: 1, + }); + + return mod.loadDependencies(function() { + return Promise.resolve([1, 2]); + }).then(function() { + expect(mod.dependencies).toEqual([1, 2]); + }); + }); + + pit('should load cached dependencies', function() { + var mod = new ModuleDescriptor({ + id: 'foo', + path: '/foo', + isAsset: true, + resolution: 1, + }); + + return mod.loadDependencies(function() { + return Promise.resolve([1, 2]); + }).then(function() { + return mod.loadDependencies(function() { + throw new Error('no!'); + }); + }).then(function() { + expect(mod.dependencies).toEqual([1, 2]); + }); + }); + }); +}); diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js index c247e59d37a32f..935c4e3084bf86 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/__tests__/DependencyGraph-test.js @@ -10,11 +10,12 @@ jest .dontMock('../index') + .dontMock('crypto') .dontMock('absolute-path') .dontMock('../docblock') .dontMock('../../replacePatterns') .dontMock('../../../../lib/getAssetDataFromName') - .setMock('../../../ModuleDescriptor', function(data) {return data;}); + .dontMock('../../../ModuleDescriptor'); jest.mock('fs'); @@ -34,6 +35,14 @@ describe('DependencyGraph', function() { }; }); + // There are a lot of crap in ModuleDescriptors, this maps an array + // to get the relevant data. + function getDataFromModules(modules) { + return modules.map(function(module) { + return module.toJSON(); + }); + } + describe('getOrderedDependencies', function() { pit('should get dependencies', function() { var root = '/root'; @@ -58,11 +67,33 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['a']}, - {id: 'a', altId: '/root/a.js', path: '/root/a.js', dependencies: []}, + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined + }, + { + id: 'a', + altId: '/root/a.js', + path: '/root/a.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined + }, ]); }); }); @@ -95,11 +126,33 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['a']}, - {id: 'a', altId: '/root/a.js', path: '/root/a.js', dependencies: []}, + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined + }, + { + id: 'a', + altId: '/root/a.js', + path: '/root/a.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined + }, ]); }); }); @@ -115,9 +168,11 @@ describe('DependencyGraph', function() { '/**', ' * @providesModule index', ' */', - 'require("./a.json")' + 'require("./a.json")', + 'require("./b")' ].join('\n'), 'a.json': JSON.stringify({}), + 'b.json': JSON.stringify({}), } }); @@ -126,20 +181,42 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ { id: 'index', altId: 'package/index', path: '/root/index.js', - dependencies: ['./a.json'] + dependencies: ['./a.json', './b'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'package/a.json', isJSON: true, path: '/root/a.json', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'package/b.json', + isJSON: true, + path: '/root/b.json', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -167,15 +244,31 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], assetRoots_DEPRECATED: ['/root/imgs'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .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, + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['image!a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'image!a', + path: '/root/imgs/a.png', + dependencies: [], + isAsset_DEPRECATED: true, + resolution: 1, + isAsset: false, + isJSON: undefined, + isPolyfill: false, + resolveDependency: undefined, }, ]); }); @@ -205,20 +298,31 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ { id: 'index', altId: 'rootPackage/index', path: '/root/index.js', - dependencies: ['./imgs/a.png'] + dependencies: ['./imgs/a.png'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'rootPackage/imgs/a.png', - path: '/root/imgs/a.png', - dependencies: [], - isAsset: true, - resolution: 1, + { + id: 'rootPackage/imgs/a.png', + path: '/root/imgs/a.png', + dependencies: [], + isAsset: true, + resolution: 1, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolveDependency: undefined, }, ]); }); @@ -253,8 +357,8 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ { id: 'index', @@ -264,7 +368,13 @@ describe('DependencyGraph', function() { './imgs/a.png', './imgs/b.png', './imgs/c.png', - ] + ], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'rootPackage/imgs/a.png', @@ -272,20 +382,32 @@ describe('DependencyGraph', function() { resolution: 1.5, dependencies: [], isAsset: true, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolveDependency: undefined, }, { id: 'rootPackage/imgs/b.png', path: '/root/imgs/b@.7x.png', resolution: 0.7, dependencies: [], - isAsset: true + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolveDependency: undefined, }, { id: 'rootPackage/imgs/c.png', path: '/root/imgs/c.png', resolution: 1, dependencies: [], - isAsset: true + isAsset: true, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolveDependency: undefined, }, ]); }); @@ -317,14 +439,20 @@ describe('DependencyGraph', function() { assetExts: ['png', 'jpg'], assetRoots_DEPRECATED: ['/root/imgs'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ { id: 'index', altId: 'rootPackage/index', path: '/root/index.js', - dependencies: ['./imgs/a.png', 'image!a'] + dependencies: ['./imgs/a.png', 'image!a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'rootPackage/imgs/a.png', @@ -332,6 +460,10 @@ describe('DependencyGraph', function() { dependencies: [], isAsset: true, resolution: 1, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolveDependency: undefined, }, { id: 'image!a', @@ -339,6 +471,10 @@ describe('DependencyGraph', function() { dependencies: [], isAsset_DEPRECATED: true, resolution: 1, + isAsset: false, + isJSON: undefined, + isPolyfill: false, + resolveDependency: undefined, }, ]); }); @@ -368,11 +504,33 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .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']}, + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['a'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'a', + altId: '/root/a.js', + path: '/root/a.js', + dependencies: ['index'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, ]); }); }); @@ -402,13 +560,105 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, - { id: 'aPackage/main', + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/main', path: '/root/aPackage/main.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: 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(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['sha.js', 'x.y.z'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'sha.js/main', + path: '/root/sha.js/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'x.y.z/main', + path: '/root/x.y.z/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -433,13 +683,30 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - {id: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, - { id: 'aPackage/index', + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/index', path: '/root/aPackage/index.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -468,14 +735,31 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - {id: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, - { id: 'EpicModule', + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'EpicModule', altId: 'aPackage/index', path: '/root/aPackage/index.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -503,13 +787,30 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - {id: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, - { id: 'aPackage/lib/index', + { + id: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/lib/index', path: '/root/aPackage/lib/index.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -534,13 +835,82 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - {id: 'test/index', path: '/root/index.js', dependencies: ['./lib/']}, - { id: 'test/lib/index', + { + id: 'test/index', + path: '/root/index.js', + dependencies: ['./lib/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'test/lib/index', path: '/root/lib/index.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: 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(getDataFromModules(deps)) + .toEqual([ + { + id: 'test/index', + path: '/root/index.js', + dependencies: ['./lib/'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: '/root/lib/main.js', + path: '/root/lib/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -568,10 +938,21 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - {id: 'index', altId: '/root/index.js', path: '/root/index.js', dependencies: ['aPackage']}, + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, ]); }); }); @@ -612,18 +993,32 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/somedir/somefile.js')) + return dgraph.getOrderedDependencies('/root/somedir/somefile.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', + { + id: 'index', altId: '/root/somedir/somefile.js', path: '/root/somedir/somefile.js', - dependencies: ['c'] + dependencies: ['c'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'c', + { + id: 'c', altId: '/root/c.js', path: '/root/c.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -659,17 +1054,31 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'aPackage', + { + id: 'aPackage', altId: '/root/b.js', path: '/root/b.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -693,12 +1102,19 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['lolomg'] + dependencies: ['lolomg'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, } ]); }); @@ -732,16 +1148,29 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage/subdir/lolynot'] + dependencies: ['aPackage/subdir/lolynot'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'aPackage/subdir/lolynot', path: '/root/aPackage/subdir/lolynot.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -776,16 +1205,30 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage/subdir/lolynot'] + dependencies: ['aPackage/subdir/lolynot'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'aPackage/subdir/lolynot', + { + id: 'aPackage/subdir/lolynot', path: '/symlinkedPackage/subdir/lolynot.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -820,24 +1263,56 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', + altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'aPackage/main', + { + id: 'aPackage/main', + altId: undefined, path: '/root/aPackage/main.js', - dependencies: ['./subdir/lolynot'] + dependencies: ['./subdir/lolynot'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'aPackage/subdir/lolynot', + { + id: 'aPackage/subdir/lolynot', + altId: undefined, path: '/root/aPackage/subdir/lolynot.js', - dependencies: ['../other'] + dependencies: ['../other'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'aPackage/other', + { + id: 'aPackage/other', + altId: undefined, path: '/root/aPackage/other.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -870,16 +1345,32 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', + altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'aPackage/client', + { + id: 'aPackage/client', + altId: undefined, path: '/root/aPackage/client.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -912,16 +1403,30 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'aPackage/client', + altId: undefined, path: '/root/aPackage/client.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -956,16 +1461,29 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'aPackage/client', path: '/root/aPackage/client.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -1000,16 +1518,29 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'aPackage/client', path: '/root/aPackage/client.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -1054,28 +1585,58 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ { id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'aPackage/client', path: '/root/aPackage/client.js', - dependencies: ['./node', './dir/server.js'] + dependencies: ['./node', './dir/server.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'aPackage/not-node', path: '/root/aPackage/not-node.js', - dependencies: ['./not-browser'] + dependencies: ['./not-browser'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'aPackage/browser', path: '/root/aPackage/browser.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'aPackage/dir/client', path: '/root/aPackage/dir/client.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -1120,65 +1681,81 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ { id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'aPackage/index', path: '/root/aPackage/index.js', - dependencies: ['node-package'] + dependencies: ['node-package'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, { id: 'browser-package/index', path: '/root/aPackage/browser-package/index.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); }); }); - 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() { + describe('node_modules', function() { + pit('should work with nested node_modules', function() { var root = '/root'; - var filesystem = fs.__setMockFilesystem({ + fs.__setMockFilesystem({ 'root': { 'index.js': [ '/**', ' * @providesModule index', ' */', - 'require("aPackage")', - 'require("foo")' - ].join('\n'), - 'foo': [ - '/**', - ' * @providesModule foo', - ' */', - 'require("aPackage")' + 'require("foo");', + 'require("bar");', ].join('\n'), - 'aPackage': { - 'package.json': JSON.stringify({ - name: 'aPackage', - main: 'main.js' - }), - 'main.js': 'main', - } + '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', + }, + }, } }); @@ -1187,29 +1764,640 @@ describe('DependencyGraph', function() { 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', + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage'] + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: [] + { + id: 'foo/main', + altId: undefined, + path: '/root/node_modules/foo/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar/main', + altId: undefined, + path: '/root/node_modules/foo/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar/main', + altId: undefined, + path: '/root/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); - }); }); }); - pit('updates module dependencies on file change', function() { + pit('nested node_modules with specific paths', function() { var root = '/root'; - var filesystem = fs.__setMockFilesystem({ + 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(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/main', + altId: undefined, + path: '/root/node_modules/foo/main.js', + dependencies: ['bar/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar/lol', + altId: undefined, + path: '/root/node_modules/foo/node_modules/bar/lol.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar/main', + altId: undefined, + path: '/root/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: 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(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['foo', 'bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/main', + path: '/root/node_modules/foo/main.js', + dependencies: ['bar/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar/wow', + path: '/root/node_modules/foo/node_modules/bar/wow.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar/main2', + path: '/root/node_modules/bar/main2.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: 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(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar', + path: '/root/path/to/bar.js', + altId: '/root/path/to/bar.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/main', + path: '/root/node_modules/foo/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: 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(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['shouldWork', 'dontWork', 'wontWork'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'shouldWork', + path: '/root/node_modules/react-tools/main.js', + altId:'react-tools/main', + dependencies: ['submodule'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'submodule/main', + path: '/root/node_modules/react-tools/node_modules/submodule/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: 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(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['foo/lol'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: 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(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['sha.js'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'sha.js/main', + path: '/root/node_modules/sha.js/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + 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.getOrderedDependencies('/root/index.js').then(function() { + filesystem.root['index.js'] = + filesystem.root['index.js'].replace('require("foo")', ''); + triggerFileChange('change', 'index.js', root); + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + }); + }); + }); + + pit('updates module dependencies on file change', function() { + var root = '/root'; + var filesystem = fs.__setMockFilesystem({ 'root': { 'index.js': [ '/**', @@ -1239,22 +2427,36 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { + return dgraph.getOrderedDependencies('/root/index.js').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')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage'] - }, - { id: 'aPackage/main', - path: '/root/aPackage/main.js', - dependencies: [] - }, - ]); + { + id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); }); }); }); @@ -1291,19 +2493,31 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { + return dgraph.getOrderedDependencies('/root/index.js').then(function() { delete filesystem.root.foo; triggerFileChange('delete', 'foo.js', root); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ { id: 'index', altId: '/root/index.js', - path: '/root/index.js', - dependencies: ['aPackage', 'foo'] - }, + path: '/root/index.js', + dependencies: ['aPackage', 'foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, { id: 'aPackage/main', path: '/root/aPackage/main.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -1342,7 +2556,7 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { + return dgraph.getOrderedDependencies('/root/index.js').then(function() { filesystem.root['bar.js'] = [ '/**', ' * @providesModule bar', @@ -1354,27 +2568,54 @@ describe('DependencyGraph', function() { filesystem.root.aPackage['main.js'] = 'require("bar")'; triggerFileChange('change', 'aPackage/main.js', root); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .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'] - }, + dependencies: ['aPackage', 'foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/main', + path: '/root/aPackage/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar', + altId: '/root/bar.js', + path: '/root/bar.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo', + altId: '/root/foo.js', + path: '/root/foo.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, ]); }); }); @@ -1400,32 +2641,50 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ { id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['image!foo'] + dependencies: ['image!foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, } ]); filesystem.root['foo.png'] = ''; triggerFileChange('add', 'foo.png', root); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + expect(getDataFromModules(deps2)) .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, - }, - ]); + { + id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['image!foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'image!foo', + path: '/root/foo.png', + dependencies: [], + isAsset_DEPRECATED: true, + resolution: 1, + isAsset: false, + isJSON: undefined, + isPolyfill: false, + resolveDependency: undefined, + }, + ]); }); }); }); @@ -1452,30 +2711,49 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ { id: 'index', altId: 'aPackage/index', path: '/root/index.js', - dependencies: ['./foo.png'] + dependencies: ['./foo.png'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, } ]); filesystem.root['foo.png'] = ''; triggerFileChange('add', 'foo.png', root); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + expect(getDataFromModules(deps2)) .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, + { + id: 'index', + altId: 'aPackage/index', + path: '/root/index.js', + dependencies: ['./foo.png'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/foo.png', + path: '/root/foo.png', + dependencies: [], + isAsset: true, + resolution: 1, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolveDependency: undefined, }, ]); }); @@ -1520,7 +2798,7 @@ describe('DependencyGraph', function() { return false; } }); - return dgraph.load().then(function() { + return dgraph.getOrderedDependencies('/root/index.js').then(function() { filesystem.root['bar.js'] = [ '/**', ' * @providesModule bar', @@ -1532,21 +2810,42 @@ describe('DependencyGraph', function() { filesystem.root.aPackage['main.js'] = 'require("bar")'; triggerFileChange('change', 'aPackage/main.js', root); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage', 'foo'] + dependencies: ['aPackage', 'foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'aPackage/main', + { + id: 'aPackage/main', path: '/root/aPackage/main.js', - dependencies: ['bar'] + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'foo', + { + id: 'foo', altId: '/root/foo.js', path: '/root/foo.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); @@ -1584,25 +2883,407 @@ describe('DependencyGraph', function() { fileWatcher: fileWatcher, assetExts: ['png', 'jpg'], }); - return dgraph.load().then(function() { + return dgraph.getOrderedDependencies('/root/index.js').then(function() { triggerFileChange('change', 'aPackage', '/root', { isDirectory: function(){ return true; } }); - return dgraph.load().then(function() { - expect(dgraph.getOrderedDependencies('/root/index.js')) + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) .toEqual([ - { id: 'index', altId: '/root/index.js', + { + id: 'index', altId: '/root/index.js', path: '/root/index.js', - dependencies: ['aPackage', 'foo'] + dependencies: ['aPackage', 'foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'aPackage/main', + { + id: 'aPackage/main', path: '/root/aPackage/main.js', - dependencies: [] + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, - { id: 'foo', + { + id: 'foo', altId: '/root/foo.js', path: '/root/foo.js', - dependencies: ['aPackage'] + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + 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); + + filesystem.root.aPackage['package.json'] = JSON.stringify({ + name: 'bPackage', + main: 'main.js', + }); + triggerFileChange('change', 'package.json', '/root/aPackage'); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['bPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bPackage/main', + path: '/root/aPackage/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + 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'); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'aPackage/browser', + path: '/root/aPackage/browser.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + 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'); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps) { + expect(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['aPackage'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + 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(getDataFromModules(deps)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/main', + altId: undefined, + path: '/root/node_modules/foo/main.js', + dependencies: ['bar'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'bar/main', + altId: undefined, + path: '/root/node_modules/foo/node_modules/bar/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + ]); + + filesystem.root.node_modules.foo['main.js'] = 'lol'; + triggerFileChange('change', 'main.js', '/root/node_modules/foo'); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + expect(getDataFromModules(deps2)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/main', + altId: undefined, + path: '/root/node_modules/foo/main.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + 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'); + + return dgraph.getOrderedDependencies('/root/index.js').then(function(deps2) { + expect(getDataFromModules(deps2)) + .toEqual([ + { + id: 'index', + altId: '/root/index.js', + path: '/root/index.js', + dependencies: ['foo'], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, + }, + { + id: 'foo/browser', + altId: undefined, + path: '/root/node_modules/foo/browser.js', + dependencies: [], + isAsset: false, + isAsset_DEPRECATED: false, + isJSON: undefined, + isPolyfill: false, + resolution: undefined, + resolveDependency: undefined, }, ]); }); diff --git a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js index 0881e5dc77dea0..1aa8b12902aceb 100644 --- a/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/DependencyGraph/index.js @@ -19,6 +19,7 @@ var debug = require('debug')('DependecyGraph'); var util = require('util'); var declareOpts = require('../../../lib/declareOpts'); var getAssetDataFromName = require('../../../lib/getAssetDataFromName'); +var crypto = require('crypto'); var readFile = Promise.promisify(fs.readFile); var readDir = Promise.promisify(fs.readdir); @@ -45,7 +46,11 @@ var validateOpts = declareOpts({ assetExts: { type: 'array', required: true, - } + }, + _providesModuleNodeModules: { + type: 'array', + default: ['react-tools', 'react-native'], + }, }); function DependecyGraph(options) { @@ -69,6 +74,8 @@ function DependecyGraph(options) { '\.(' + ['js', 'json'].concat(this._assetExts).join('|') + ')$' ); + this._providesModuleNodeModules = opts._providesModuleNodeModules; + // Kick off the search process to precompute the dependency graph. this._init(); } @@ -90,59 +97,100 @@ DependecyGraph.prototype.load = function() { * 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 - ); - } + return this.load().then(function() { + 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 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; + 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(mod) { + deps.push(mod); + + if (mod.dependencies == null) { + return mod.loadDependencies(function() { + return readFile(mod.path, 'utf8').then(function(content) { + return extractRequires(content); + }); + }).then(function() { + return iter(mod); + }); } - if (!visited[dep.id]) { - visited[dep.id] = true; - collect(dep); - } - }); - } + return iter(mod); + } - visited[module.id] = true; - collect(module); + function iter(mod) { + var p = Promise.resolve(); + mod.dependencies.forEach(function(name) { + var dep = self.resolveDependency(mod, name); + + if (dep == null) { + debug( + 'WARNING: Cannot find required module `%s` from module `%s`.', + name, + mod.id + ); + return; + } - return deps; + p = p.then(function() { + if (!visited[realId(dep)]) { + visited[realId(dep)] = true; + return collect(dep); + } + return null; + }); + }); + return p; + } + + visited[realId(module)] = true; + return collect(module).then(function() { + return deps; + }); + }.bind(this)); }; +function browserFieldRedirect(packageJson, modulePath, isMain) { + if (packageJson.browser && typeof packageJson.browser === 'object') { + if (isMain) { + var tmpMain = packageJson.browser[modulePath] || + packageJson.browser[sansExtJs(modulePath)] || + packageJson.browser[withExtJs(modulePath)]; + if (tmpMain) { + return tmpMain; + } + } else { + var relPath = './' + path.relative(packageJson._root, modulePath); + var tmpModulePath = packageJson.browser[withExtJs(relPath)] || + packageJson.browser[sansExtJs(relPath)]; + if (tmpModulePath) { + return path.join(packageJson._root, tmpModulePath); + } + } + } + return modulePath; +} + /** * Given a module descriptor `fromModule` return the module descriptor for * the required module `depModuleId`. It could be top-level or relative, @@ -157,7 +205,7 @@ DependecyGraph.prototype.resolveDependency = function( // Process DEPRECATED global asset requires. if (assetMatch && assetMatch[1]) { if (!this._assetMap_DEPRECATED[assetMatch[1]]) { - debug('WARINING: Cannot find asset:', assetMatch[1]); + debug('WARNING: Cannot find asset:', assetMatch[1]); return null; } return this._assetMap_DEPRECATED[assetMatch[1]]; @@ -177,19 +225,39 @@ DependecyGraph.prototype.resolveDependency = function( depModuleId = fromPackageJson.browser[depModuleId]; } + + var packageName = depModuleId.replace(/\/.+/, ''); + packageJson = this._lookupNodePackage(fromModule.path, packageName); + + if (packageJson != null && packageName !== depModuleId) { + modulePath = path.join( + packageJson._root, + path.relative(packageName, depModuleId) + ); + + modulePath = browserFieldRedirect(packageJson, modulePath); + + dep = this._graph[withExtJs(modulePath)]; + if (dep != null) { + return dep; + } + } + // `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)]) { + if (packageJson == null && 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]; + if (packageJson == null) { + // `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 + // We are being forgiving here and not raising an error because we could be // processing a file that uses it's own require system. - if (packageJson == null) { + if (packageJson == null || packageName !== depModuleId) { debug( 'WARNING: Cannot find required module `%s` from module `%s`.', depModuleId, @@ -198,33 +266,8 @@ DependecyGraph.prototype.resolveDependency = function( 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')]; - } - + // We are requiring node or a haste package via it's main file. + dep = this._resolvePackageMain(packageJson); if (dep == null) { throw new Error( 'Cannot find package main file for package: ' + packageJson._root @@ -248,26 +291,20 @@ DependecyGraph.prototype.resolveDependency = function( // modulePath: /x/y/a/b var dir = path.dirname(fromModule.path); modulePath = path.join(dir, depModuleId); + modulePath = browserFieldRedirect(packageJson, modulePath); - 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]; + dep = this._graph[modulePath] || + this._graph[modulePath + '.js'] || + this._graph[modulePath + '.json']; - // Maybe the dependency is a directory and there is an index.js inside it. + // Maybe the dependency is a directory and there is a packageJson and/or index.js inside it. if (dep == null) { - dep = this._graph[path.join(dir, depModuleId, 'index.js')]; + var dirPackageJson = this._packageByRoot[path.join(dir, depModuleId).replace(/\/$/, '')]; + if (dirPackageJson) { + dep = this._resolvePackageMain(dirPackageJson); + } else { + dep = this._graph[path.join(dir, depModuleId, 'index.js')]; + } } // Maybe it's an asset with @n.nx resolution and the path doesn't map @@ -291,6 +328,25 @@ DependecyGraph.prototype.resolveDependency = function( } }; +DependecyGraph.prototype._resolvePackageMain = function(packageJson) { + 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. + main = browserFieldRedirect(packageJson, main, true); + + var modulePath = withExtJs(path.join(packageJson._root, main)); + + return this._graph[modulePath] || + // Some packages use just a dir and rely on an index.js inside that dir. + this._graph[path.join(packageJson._root, main, 'index.js')]; +}; + /** * Intiates the filewatcher and kicks off the search process. */ @@ -339,9 +395,9 @@ DependecyGraph.prototype._search = function() { }); var processing = self._findAndProcessPackage(files, dir) - .then(function() { - return Promise.all(modulePaths.map(self._processModule.bind(self))); - }); + .then(function() { + return Promise.all(modulePaths.map(self._processModule.bind(self))); + }); return Promise.all([ processing, @@ -358,10 +414,8 @@ DependecyGraph.prototype._search = function() { * and update indices. */ DependecyGraph.prototype._findAndProcessPackage = function(files, root) { - var self = this; - var packagePath; - for (var i = 0; i < files.length ; i++) { + for (var i = 0; i < files.length; i++) { var file = files[i]; if (path.basename(file) === 'package.json') { packagePath = file; @@ -389,14 +443,6 @@ DependecyGraph.prototype._processPackage = function(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); @@ -406,12 +452,16 @@ DependecyGraph.prototype._processPackage = function(packagePath) { DependecyGraph.prototype._addPackageToIndices = function(packageJson) { this._packageByRoot[packageJson._root] = packageJson; - this._packagesById[packageJson.name] = packageJson; + if (!this._isInNodeModules(packageJson._root) && packageJson.name != null) { + this._packagesById[packageJson.name] = packageJson; + } }; DependecyGraph.prototype._removePackageFromIndices = function(packageJson) { delete this._packageByRoot[packageJson._root]; - delete this._packagesById[packageJson.name]; + if (!this._isInNodeModules(packageJson._root) && packageJson.name != null) { + delete this._packagesById[packageJson.name]; + } }; /** @@ -441,6 +491,14 @@ DependecyGraph.prototype._processModule = function(modulePath) { return Promise.resolve(module); } + if (this._isInNodeModules(modulePath)) { + moduleData.id = this._lookupName(modulePath); + moduleData.dependencies = null; + module = new ModuleDescriptor(moduleData); + this._updateGraphWithModule(module); + return Promise.resolve(module); + } + var self = this; return readFile(modulePath, 'utf8') .then(function(content) { @@ -469,7 +527,7 @@ DependecyGraph.prototype._processModule = function(modulePath) { */ DependecyGraph.prototype._lookupName = function(modulePath) { var packageJson = this._lookupPackage(modulePath); - if (packageJson == null) { + if (packageJson == null || packageJson.name == null) { return path.resolve(modulePath); } else { var relativePath = @@ -484,6 +542,10 @@ DependecyGraph.prototype._deleteModule = function(module) { // Others may keep a reference so we mark it as deleted. module.deleted = true; + if (this._isInNodeModules(module.path)) { + return; + } + // Haste allows different module to have the same id. if (this._moduleById[module.id] === module) { delete this._moduleById[module.id]; @@ -504,6 +566,10 @@ DependecyGraph.prototype._updateGraphWithModule = function(module) { this._graph[module.path] = module; + if (this._isInNodeModules(module.path)) { + return; + } + if (this._moduleById[module.id]) { debug( 'WARNING: Top-level module name conflict `%s`.\n' + @@ -527,28 +593,14 @@ DependecyGraph.prototype._updateGraphWithModule = function(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), this._packageByRoot); +}; - return lookupPackage(path.dirname(modulePath)); +/** + * Find the nearest node package to a module. + */ +DependecyGraph.prototype._lookupNodePackage = function(startPath, packageName) { + return lookupNodePackage(path.dirname(startPath), this._packageByRoot, packageName); }; /** @@ -573,12 +625,14 @@ DependecyGraph.prototype._processFileChange = function( } var isPackage = path.basename(filePath) === 'package.json'; + var packageJson; + if (isPackage) { + packageJson = this._packageByRoot[path.dirname(absPath)]; + } + if (eventType === 'delete') { - if (isPackage) { - var packageJson = this._packageByRoot[path.dirname(absPath)]; - if (packageJson) { - this._removePackageFromIndices(packageJson); - } + if (isPackage && packageJson) { + this._removePackageFromIndices(packageJson); } else { var module = this._graph[absPath]; if (module == null) { @@ -591,7 +645,14 @@ DependecyGraph.prototype._processFileChange = function( var self = this; this._loading = this._loading.then(function() { if (isPackage) { - return self._processPackage(absPath); + self._removePackageFromIndices(packageJson); + return self._processPackage(absPath) + .then(function(p) { + return self._resolvePackageMain(p); + }) + .then(function(mainModule) { + return self._processModule(mainModule.path); + }); } return self._processModule(absPath); }); @@ -675,6 +736,25 @@ DependecyGraph.prototype._isFileAsset = function(file) { return this._assetExts.indexOf(extname(file)) !== -1; }; +DependecyGraph.prototype._isInNodeModules = function(file) { + var inNodeModules = file.indexOf('/node_modules/') !== -1; + + if (!inNodeModules) { + return false; + } + + var dirs = this._providesModuleNodeModules; + + for (var i = 0; i < dirs.length; i++) { + var index = file.indexOf(dirs[i]); + if (index !== -1) { + return file.slice(index).indexOf('/node_modules/') !== -1; + } + } + + return true; +}; + /** * Extract all required modules from a `code` string. */ @@ -784,6 +864,54 @@ function extname(name) { return path.extname(name).replace(/^\./, ''); } +function realId(module) { + if (module._realId) { + return module._realId; + } + + var hash = crypto.createHash('md5'); + hash.update(module.id); + hash.update(module.path); + Object.defineProperty(module, '_realId', { value: hash.digest('hex') }); + return module._realId; +} + +/** + * Auxiliary function to recursively lookup a package. + */ +function lookupPackage(currDir, packageByRoot) { + // 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), packageByRoot); + } + } +} + +/** + * Auxiliary function to recursively lookup a package. + */ +function lookupNodePackage(currDir, packageByRoot, packageName) { + if (currDir === '/') { + return null; + } + var packageRoot = path.join(currDir, 'node_modules', packageName); + + var packageJson = packageByRoot[packageRoot]; + if (packageJson) { + return packageJson; + } else { + return lookupNodePackage(path.dirname(currDir), packageByRoot, packageName); + } +} + function NotFoundError() { Error.call(this); Error.captureStackTrace(this, this.constructor); diff --git a/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js b/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js index 9bc8b8b95c3766..1f8c95479bb37e 100644 --- a/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js +++ b/packager/react-packager/src/DependencyResolver/haste/__tests__/HasteDependencyResolver-test.js @@ -40,7 +40,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(); @@ -123,7 +123,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(); @@ -207,7 +207,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(); diff --git a/packager/react-packager/src/DependencyResolver/haste/index.js b/packager/react-packager/src/DependencyResolver/haste/index.js index da68785eacd520..d7a8c0eb1b338c 100644 --- a/packager/react-packager/src/DependencyResolver/haste/index.js +++ b/packager/react-packager/src/DependencyResolver/haste/index.js @@ -91,9 +91,8 @@ HasteDependencyResolver.prototype.getDependencies = function(main, options) { var depGraph = this._depGraph; var self = this; - return depGraph.load() - .then(function() { - var dependencies = depGraph.getOrderedDependencies(main); + return depGraph.getOrderedDependencies(main) + .then(function(dependencies) { var mainModuleId = dependencies[0].id; self._prependPolyfillDependencies(dependencies, opts.dev);