From 3f6943c9abc90592c7f2e36abda0a98cb435aaf0 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Tue, 7 Apr 2015 18:26:09 -0700 Subject: [PATCH] Updates from Tue 7 Apr - [AdsManager] Correct back button functionality | Eric Vicenti - [ReactNative] Replace Backstack with BackAndroid | Eric Vicenti - [ReactNative] Better error message for EADDRINUSE | Alex Kotliarskyi - [ReactNative] npm install --save chalk | Alex Kotliarskyi - Removed redundant views and shadow views | Nick Lockwood - [ReactNative] Fix variable shadowing in RCTText | Tadeu Zagallo - [react-packager] check in image-size module | Amjad Masad - Refactored RCTLog and added facility to prepend extra data to the log message | Nick Lockwood - [ReactNative] Fix crash on image download | Tadeu Zagallo - [React Native] #WIP Modify RCTShadowText measure function to honor maxNumberOfLines property | Alex Akers - Add promise support to AsyncStorage | Spencer Ahrens - [ReactNative] Revert high-level Subscribable | Eric Vicenti - [ReactNative] wrong deprecated prop check in RCTConvert | Kevin Gozali - [ReactNative][MAdMan] Add type for image source, flowify AdsManagerObjectiveTypes | Philipp von Weitershausen --- Examples/UIExplorer/AsyncStorageExample.js | 43 ++- .../story-background.imageset/Contents.json | 4 + .../RCTAnimationExperimentalManager.m | 2 +- Libraries/Components/Subscribable.js | 309 +----------------- .../CustomComponents/Navigator/Navigator.js | 259 +++++++-------- .../Navigator/NavigatorInterceptor.js | 95 ++++++ .../NavigatorStaticContextContainer.js | 53 +++ .../Navigator/getNavigatorContext.js | 37 +++ Libraries/Image/ImageSource.js | 17 + Libraries/Image/RCTImageDownloader.m | 15 +- .../RCTWebSocketExecutor.m | 4 +- Libraries/Storage/AsyncStorage.ios.js | 172 +++++++--- Libraries/Text/RCTRawTextManager.m | 3 +- Libraries/Text/RCTShadowText.h | 6 +- Libraries/Text/RCTShadowText.m | 51 ++- Libraries/Text/RCTText.h | 5 +- Libraries/Text/RCTText.m | 38 +-- Libraries/Text/RCTTextManager.m | 21 +- Libraries/Utilities/BackAndroid.ios.js | 21 ++ Libraries/Utilities/Backstack.ios.js | 16 - React/Base/RCTBridge.h | 13 +- React/Base/RCTBridge.m | 37 +-- React/Base/RCTConvert.h | 5 +- React/Base/RCTConvert.m | 17 +- React/Base/RCTLog.h | 170 +++++++--- React/Base/RCTLog.m | 265 +++++++++++---- React/Base/RCTRootView.h | 7 + React/Base/RCTRootView.m | 1 - React/Executors/RCTContextExecutor.m | 21 +- React/Modules/RCTAlertManager.m | 1 + React/Modules/RCTUIManager.m | 96 ++++-- React/Views/RCTScrollViewManager.m | 2 +- package.json | 18 +- packager/packager.js | 37 ++- 34 files changed, 1058 insertions(+), 803 deletions(-) create mode 100644 Libraries/CustomComponents/Navigator/NavigatorInterceptor.js create mode 100644 Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js create mode 100644 Libraries/CustomComponents/Navigator/getNavigatorContext.js create mode 100644 Libraries/Image/ImageSource.js create mode 100644 Libraries/Utilities/BackAndroid.ios.js delete mode 100644 Libraries/Utilities/Backstack.ios.js diff --git a/Examples/UIExplorer/AsyncStorageExample.js b/Examples/UIExplorer/AsyncStorageExample.js index 323cedd15..8bc1631cd 100644 --- a/Examples/UIExplorer/AsyncStorageExample.js +++ b/Examples/UIExplorer/AsyncStorageExample.js @@ -29,16 +29,17 @@ var COLORS = ['red', 'orange', 'yellow', 'green', 'blue']; var BasicStorageExample = React.createClass({ componentDidMount() { - AsyncStorage.getItem(STORAGE_KEY, (error, value) => { - if (error) { - this._appendMessage('AsyncStorage error: ' + error.message); - } else if (value !== null) { - this.setState({selectedValue: value}); - this._appendMessage('Recovered selection from disk: ' + value); - } else { - this._appendMessage('Initialized with no selection on disk.'); - } - }); + AsyncStorage.getItem(STORAGE_KEY) + .then((value) => { + if (value !== null){ + this.setState({selectedValue: value}); + this._appendMessage('Recovered selection from disk: ' + value); + } else { + this._appendMessage('Initialized with no selection on disk.'); + } + }) + .catch((error) => this._appendMessage('AsyncStorage error: ' + error.message)) + .done(); }, getInitialState() { return { @@ -81,23 +82,17 @@ var BasicStorageExample = React.createClass({ _onValueChange(selectedValue) { this.setState({selectedValue}); - AsyncStorage.setItem(STORAGE_KEY, selectedValue, (error) => { - if (error) { - this._appendMessage('AsyncStorage error: ' + error.message); - } else { - this._appendMessage('Saved selection to disk: ' + selectedValue); - } - }); + AsyncStorage.setItem(STORAGE_KEY, selectedValue) + .then(() => this._appendMessage('Saved selection to disk: ' + selectedValue)) + .catch((error) => this._appendMessage('AsyncStorage error: ' + error.message)) + .done(); }, _removeStorage() { - AsyncStorage.removeItem(STORAGE_KEY, (error) => { - if (error) { - this._appendMessage('AsyncStorage error: ' + error.message); - } else { - this._appendMessage('Selection removed from disk.'); - } - }); + AsyncStorage.removeItem(STORAGE_KEY) + .then(() => this._appendMessage('Selection removed from disk.')) + .catch((error) => { this._appendMessage('AsyncStorage error: ' + error.message) }) + .done(); }, _appendMessage(message) { diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json index 9c8120dff..e1e9cd56b 100644 --- a/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json +++ b/Examples/UIExplorer/UIExplorer/Images.xcassets/story-background.imageset/Contents.json @@ -1,5 +1,9 @@ { "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, { "idiom" : "universal", "scale" : "2x", diff --git a/Libraries/Animation/RCTAnimationExperimentalManager.m b/Libraries/Animation/RCTAnimationExperimentalManager.m index 88bb5fe28..b7c76c9f5 100644 --- a/Libraries/Animation/RCTAnimationExperimentalManager.m +++ b/Libraries/Animation/RCTAnimationExperimentalManager.m @@ -71,7 +71,7 @@ UIView *view = viewRegistry[reactTag]; if (!view) { - RCTLogWarn(@"React tag %@ is not registered with the view registry", reactTag); + RCTLogWarn(@"React tag #%@ is not registered with the view registry", reactTag); return; } diff --git a/Libraries/Components/Subscribable.js b/Libraries/Components/Subscribable.js index 06b7e0ce4..cf9bc773a 100644 --- a/Libraries/Components/Subscribable.js +++ b/Libraries/Components/Subscribable.js @@ -11,308 +11,25 @@ */ 'use strict'; -/** - * Subscribable wraps EventEmitter in a clean interface, and provides a mixin - * so components can easily subscribe to events and not worry about cleanup on - * unmount. - * - * Also acts as a basic store because it records the last data that it emitted, - * and provides a way to populate the initial data. The most recent data can be - * fetched from the Subscribable by calling `get()` - * - * Advantages over EventEmitter + Subscibable.Mixin.addListenerOn: - * - Cleaner usage: no strings to identify the event - * - Lifespan pattern enforces cleanup - * - More logical: Subscribable.Mixin now uses a Subscribable class - * - Subscribable saves the last data and makes it available with `.get()` - * - * Legacy Subscribable.Mixin.addListenerOn allowed automatic subscription to - * EventEmitters. Now we should avoid EventEmitters and wrap with Subscribable - * instead: - * - * ``` - * AppState.networkReachability = new Subscribable( - * RCTDeviceEventEmitter, - * 'reachabilityDidChange', - * (resp) => resp.network_reachability, - * RCTReachability.getCurrentReachability - * ); - * - * var myComponent = React.createClass({ - * mixins: [Subscribable.Mixin], - * getInitialState: function() { - * return { - * isConnected: AppState.networkReachability.get() !== 'none' - * }; - * }, - * componentDidMount: function() { - * this._reachSubscription = this.subscribeTo( - * AppState.networkReachability, - * (reachability) => { - * this.setState({ isConnected: reachability !== 'none' }) - * } - * ); - * }, - * render: function() { - * return ( - * - * {this.state.isConnected ? 'Network Connected' : 'No network'} - * - * this._reachSubscription.remove()}> - * End reachability subscription - * - * ); - * } - * }); - * ``` - */ - var EventEmitter = require('EventEmitter'); -var invariant = require('invariant'); -var logError = require('logError'); - -var SUBSCRIBABLE_INTERNAL_EVENT = 'subscriptionEvent'; - -type Data = Object; -type EventMapping = (_: Data) => Data; - -class Subscribable { - _eventMapping: EventMapping; - _lastData: Data; - - /** - * Creates a new Subscribable object - * - * @param {EventEmitter} eventEmitter Emitter to trigger subscription events. - * @param {string} eventName Name of emitted event that triggers subscription - * events. - * @param {function} eventMapping (optional) Function to convert the output - * of the eventEmitter to the subscription output. - * @param {function} getInitData (optional) Async function to grab the initial - * data to publish. Signature `function(successCallback, errorCallback)`. - * The resolved data will be transformed with the eventMapping before it - * gets emitted. - */ - constructor(eventEmitter: EventEmitter, eventName: string, eventMapping?: EventMapping, getInitData?: Function) { - - this._internalEmitter = new EventEmitter(); - this._eventMapping = eventMapping || (data => data); - - this._upstreamSubscription = eventEmitter.addListener( - eventName, - this._handleEmit, - this - ); - - // Asyncronously get the initial data, if provided - getInitData && getInitData(this._handleInitData.bind(this), logError); - } - - /** - * Returns the last data emitted from the Subscribable, or undefined - */ - get(): Data { - return this._lastData; - } - - /** - * Unsubscribe from the upstream EventEmitter - */ - cleanup() { - this._upstreamSubscription && this._upstreamSubscription.remove(); - } - - /** - * Add a new listener to the subscribable. This should almost never be used - * directly, and instead through Subscribable.Mixin.subscribeTo - * - * @param {object} lifespan Object with `addUnmountCallback` that accepts - * a handler to be called when the component unmounts. This is required and - * desirable because it enforces cleanup. There is no easy way to leave the - * subsciption hanging - * { - * addUnmountCallback: function(newUnmountHanlder) {...}, - * } - * @param {function} callback Handler to call when Subscribable has data - * updates - * @param {object} context Object to bind the handler on, as "this" - * - * @return {object} the subscription object: - * { - * remove: function() {...}, - * } - * Call `remove` to terminate the subscription before unmounting - */ - subscribe(lifespan: { addUnmountCallback: Function }, callback: Function, context: Object) { - invariant( - typeof lifespan.addUnmountCallback === 'function', - 'Must provide a valid lifespan, which provides a way to add a ' + - 'callback for when subscription can be cleaned up. This is used ' + - 'automatically by Subscribable.Mixin' - ); - invariant( - typeof callback === 'function', - 'Must provide a valid subscription handler.' - ); - - // Add a listener to the internal EventEmitter - var subscription = this._internalEmitter.addListener( - SUBSCRIBABLE_INTERNAL_EVENT, - callback, - context - ); - - // Clean up subscription upon the lifespan unmount callback - lifespan.addUnmountCallback(() => { - subscription.remove(); - }); - - return subscription; - } - - /** - * Callback for the initial data resolution. Currently behaves the same as - * `_handleEmit`, but we may eventually want to keep track of the difference - */ - _handleInitData(dataInput: Data) { - var emitData = this._eventMapping(dataInput); - this._lastData = emitData; - this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData); - } - - /** - * Handle new data emissions. Pass the data through our eventMapping - * transformation, store it for later `get()`ing, and emit it for subscribers - */ - _handleEmit(dataInput: Data) { - var emitData = this._eventMapping(dataInput); - this._lastData = emitData; - this._internalEmitter.emit(SUBSCRIBABLE_INTERNAL_EVENT, emitData); - } -} +/** + * Subscribable provides a mixin for safely subscribing a component to an + * eventEmitter + * + * This will be replaced with the observe interface that will be coming soon to + * React Core + */ +var Subscribable = {}; Subscribable.Mixin = { - /** - * @return {object} lifespan Object with `addUnmountCallback` that accepts - * a handler to be called when the component unmounts - * { - * addUnmountCallback: function(newUnmountHanlder) {...}, - * } - */ - _getSubscribableLifespan: function() { - if (!this._subscribableLifespan) { - this._subscribableLifespan = { - addUnmountCallback: (cb) => { - this._endSubscribableLifespanCallbacks.push(cb); - }, - }; - } - return this._subscribableLifespan; - }, - - _endSubscribableLifespan: function() { - this._endSubscribableLifespanCallbacks.forEach(cb => cb()); - }, - - /** - * Components use `subscribeTo` for listening to Subscribable stores. Cleanup - * is automatic on component unmount. - * - * To stop listening to the subscribable and end the subscription early, - * components should store the returned subscription object and invoke the - * `remove()` function on it - * - * @param {Subscribable} subscription to subscribe to. - * @param {function} listener Function to invoke when event occurs. - * @param {object} context Object to bind the handler on, as "this" - * - * @return {object} the subscription object: - * { - * remove: function() {...}, - * } - * Call `remove` to terminate the subscription before unmounting - */ - subscribeTo: function(subscribable, handler, context) { - invariant( - subscribable instanceof Subscribable, - 'Must provide a Subscribable' - ); - return subscribable.subscribe( - this._getSubscribableLifespan(), - handler, - context - ); - }, - - /** - * Gets a Subscribable store, scoped to the component, that can be passed to - * children. The component will automatically clean up the subscribable's - * subscription to the eventEmitter when unmounting. - * - * `provideSubscribable` will always return the same Subscribable for any - * particular emitter/eventName combo, so it can be called directly from - * render, and it will never create duplicate Subscribables. - * - * @param {EventEmitter} eventEmitter Emitter to trigger subscription events. - * @param {string} eventName Name of emitted event that triggers subscription - * events. - * @param {function} eventMapping (optional) Function to convert the output - * of the eventEmitter to the subscription output. - * @param {function} getInitData (optional) Async function to grab the initial - * data to publish. Signature `function(successCallback, errorCallback)`. - * The resolved data will be transformed with the eventMapping before it - * gets emitted. - */ - provideSubscribable: function(eventEmitter, eventName, eventMapping, getInitData) { - this._localSubscribables = this._localSubscribables || {}; - this._localSubscribables[eventEmitter] = - this._localSubscribables[eventEmitter] || {}; - if (!this._localSubscribables[eventEmitter][eventName]) { - this._localSubscribables[eventEmitter][eventName] = - new Subscribable(eventEmitter, eventName, eventMapping, getInitData); - } - return this._localSubscribables[eventEmitter][eventName]; - }, - - /** - * Removes any local Subscribables created with `provideSubscribable`, so the - * component can unmount without leaving any dangling listeners on - * eventEmitters - */ - _cleanupLocalSubscribables: function() { - if (!this._localSubscribables) { - return; - } - Object.keys(this._localSubscribables).forEach((eventEmitter) => { - var emitterSubscribables = this._localSubscribables[eventEmitter]; - if (emitterSubscribables) { - Object.keys(emitterSubscribables).forEach((eventName) => { - emitterSubscribables[eventName].cleanup(); - }); - } - }); - this._localSubscribables = null; - }, - componentWillMount: function() { - this._endSubscribableLifespanCallbacks = []; - - // DEPRECATED addListenerOn* usage: this._subscribableSubscriptions = []; }, componentWillUnmount: function() { - // Resolve the lifespan, which will cause Subscribable to clean any - // remaining subscriptions - this._endSubscribableLifespan && this._endSubscribableLifespan(); - - this._cleanupLocalSubscribables(); - - // DEPRECATED addListenerOn* usage uses _subscribableSubscriptions array - // instead of lifespan this._subscribableSubscriptions.forEach( (subscription) => subscription.remove() ); @@ -320,9 +37,6 @@ Subscribable.Mixin = { }, /** - * DEPRECATED - Use `Subscribable` and `Mixin.subscribeTo` instead. - * `addListenerOn` subscribes the component to an `EventEmitter`. - * * Special form of calling `addListener` that *guarantees* that a * subscription *must* be tied to a component instance, and therefore will * be cleaned up when the component is unmounted. It is impossible to create @@ -335,7 +49,12 @@ Subscribable.Mixin = { * @param {function} listener Function to invoke when event occurs. * @param {object} context Object to use as listener context. */ - addListenerOn: function(eventEmitter, eventType, listener, context) { + addListenerOn: function( + eventEmitter: EventEmitter, + eventType: string, + listener: Function, + context: Object + ) { this._subscribableSubscriptions.push( eventEmitter.addListener(eventType, listener, context) ); diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 89f3b49c2..0ad3c4269 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -27,12 +27,14 @@ 'use strict'; var AnimationsDebugModule = require('NativeModules').AnimationsDebugModule; -var Backstack = require('Backstack'); +var BackAndroid = require('BackAndroid'); var Dimensions = require('Dimensions'); var InteractionMixin = require('InteractionMixin'); -var NavigatorSceneConfigs = require('NavigatorSceneConfigs'); -var NavigatorNavigationBar = require('NavigatorNavigationBar'); var NavigatorBreadcrumbNavigationBar = require('NavigatorBreadcrumbNavigationBar'); +var NavigatorInterceptor = require('NavigatorInterceptor'); +var NavigatorNavigationBar = require('NavigatorNavigationBar'); +var NavigatorSceneConfigs = require('NavigatorSceneConfigs'); +var NavigatorStaticContextContainer = require('NavigatorStaticContextContainer'); var PanResponder = require('PanResponder'); var React = require('React'); var StaticContainer = require('StaticContainer.react'); @@ -41,6 +43,7 @@ var Subscribable = require('Subscribable'); var TimerMixin = require('react-timer-mixin'); var View = require('View'); +var getNavigatorContext = require('getNavigatorContext'); var clamp = require('clamp'); var invariant = require('invariant'); var keyMirror = require('keyMirror'); @@ -59,8 +62,6 @@ function getuid() { return __uid++; } -var nextComponentUid = 0; - // styles moved to the top of the file so getDefaultProps can refer to it var styles = StyleSheet.create({ container: { @@ -185,12 +186,11 @@ var Navigator = React.createClass({ /** * Required function which renders the scene for a given route. Will be - * invoked with the route, the navigator object, and a ref handler that - * will allow a ref to your scene to be provided by props.onItemRef + * invoked with the route and the navigator object * * ``` - * (route, navigator, onRef) => - * + * (route, navigator) => + * * ``` */ renderScene: PropTypes.func.isRequired, @@ -242,19 +242,18 @@ var Navigator = React.createClass({ * Styles to apply to the container of each scene */ sceneStyle: View.propTypes.style, + }, - /** - * Should the backstack back button "jump" back instead of pop? Set to true - * if a jump forward might happen after the android back button is pressed, - * so the scenes will remain mounted - */ - shouldJumpOnBackstackPop: PropTypes.bool, + contextTypes: { + navigator: PropTypes.object, }, statics: { BreadcrumbNavigationBar: NavigatorBreadcrumbNavigationBar, NavigationBar: NavigatorNavigationBar, SceneConfigs: NavigatorSceneConfigs, + Interceptor: NavigatorInterceptor, + getContext: getNavigatorContext, }, mixins: [TimerMixin, InteractionMixin, Subscribable.Mixin], @@ -303,7 +302,20 @@ var Navigator = React.createClass({ }, componentWillMount: function() { - this.navigatorActions = { + this.parentNavigator = getNavigatorContext(this) || this.props.navigator; + this.navigatorContext = { + setHandlerForRoute: this.setHandlerForRoute, + request: this.request, + + parentNavigator: this.parentNavigator, + getCurrentRoutes: this.getCurrentRoutes, + // We want to bubble focused routes to the top navigation stack. If we + // are a child navigator, this allows us to call props.navigator.on*Focus + // of the topmost Navigator + onWillFocus: this.props.onWillFocus, + onDidFocus: this.props.onDidFocus, + + // Legacy, imperitive nav actions. Use request when possible. jumpBack: this.jumpBack, jumpForward: this.jumpForward, jumpTo: this.jumpTo, @@ -317,14 +329,8 @@ var Navigator = React.createClass({ resetTo: this.resetTo, popToRoute: this.popToRoute, popToTop: this.popToTop, - parentNavigator: this.props.navigator, - getCurrentRoutes: this.getCurrentRoutes, - // We want to bubble focused routes to the top navigation stack. If we - // are a child navigator, this allows us to call props.navigator.on*Focus - // of the topmost Navigator - onWillFocus: this.props.onWillFocus, - onDidFocus: this.props.onDidFocus, }; + this._handlers = {}; this.panGesture = PanResponder.create({ onStartShouldSetPanResponderCapture: this._handleStartShouldSetPanResponderCapture, @@ -336,17 +342,50 @@ var Navigator = React.createClass({ }); this._itemRefs = {}; this._interactionHandle = null; - this._backstackComponentKey = 'jsnavstack' + nextComponentUid; - nextComponentUid++; - - Backstack.eventEmitter && this.addListenerOn( - Backstack.eventEmitter, - 'popNavigation', - this._onBackstackPopState); this._emitWillFocus(this.state.presentedIndex); }, + request: function(action, arg1, arg2) { + if (this.parentNavigator) { + return this.parentNavigator.request.apply(null, arguments); + } + return this._handleRequest.apply(null, arguments); + }, + + _handleRequest: function(action, arg1, arg2) { + var childHandler = this._handlers[this.state.presentedIndex]; + if (childHandler && childHandler(action, arg1, arg2)) { + return true; + } + switch (action) { + case 'pop': + return this._handlePop(); + case 'push': + return this._handlePush(arg1); + default: + invariant(false, 'Unsupported request type ' + action); + return false; + } + }, + + _handlePop: function() { + if (this.state.presentedIndex === 0) { + return false; + } + this.pop(); + return true; + }, + + _handlePush: function(route) { + this.push(route); + return true; + }, + + setHandlerForRoute: function(route, handler) { + this._handlers[this.state.routeStack.indexOf(route)] = handler; + }, + _configureSpring: function(animationConfig) { var config = this.spring.getSpringConfig(); config.friction = animationConfig.springFriction; @@ -361,30 +400,28 @@ var Navigator = React.createClass({ animationConfig && this._configureSpring(animationConfig); this.spring.addListener(this); this.onSpringUpdate(); - - // Fill up the Backstack with routes that have been provided in - // initialRouteStack - this._fillBackstackRange(0, this.state.routeStack.indexOf(this.props.initialRoute)); this._emitDidFocus(this.state.presentedIndex); + if (this.parentNavigator) { + this.parentNavigator.setHandler(this._handleRequest); + } else { + // There is no navigator in our props or context, so this is the + // top-level navigator. We will handle back button presses here + BackAndroid.addEventListener('hardwareBackPress', this._handleBackPress); + } }, componentWillUnmount: function() { - Backstack.removeComponentHistory(this._backstackComponentKey); + if (this.parentNavigator) { + this.parentNavigator.setHandler(null); + } else { + BackAndroid.removeEventListener('hardwareBackPress', this._handleBackPress); + } }, - _onBackstackPopState: function(componentKey, stateKey, state) { - if (componentKey !== this._backstackComponentKey) { - return; - } - if (!this._canNavigate()) { - // A bit hacky: if we can't actually handle the pop, just push it back on the stack - Backstack.pushNavigation(componentKey, stateKey, state); - } else { - if (this.props.shouldJumpOnBackstackPop) { - this._jumpToWithoutBackstack(state.fromRoute); - } else { - this._popToRouteWithoutBackstack(state.fromRoute); - } + _handleBackPress: function() { + var didPop = this.request('pop'); + if (!didPop) { + BackAndroid.exitApp(); } }, @@ -737,41 +774,6 @@ var Navigator = React.createClass({ return !this.state.isAnimating; }, - _jumpNWithoutBackstack: function(n) { - var destIndex = this._getDestIndexWithinBounds(n); - if (!this._canNavigate()) { - return; // It's busy animating or transitioning. - } - var requestTransitionAndResetUpdatingRange = () => { - this._requestTransitionTo(destIndex); - this._resetUpdatingRange(); - }; - this.setState({ - updatingRangeStart: destIndex, - updatingRangeLength: 1, - toIndex: destIndex, - }, requestTransitionAndResetUpdatingRange); - }, - - _fillBackstackRange: function(start, end) { - invariant( - start <= end, - 'Can only fill the backstack forward. Provide end index greater than start' - ); - for (var i = 0; i < (end - start); i++) { - var fromIndex = start + i; - var toIndex = start + i + 1; - Backstack.pushNavigation( - this._backstackComponentKey, - toIndex, - { - fromRoute: this.state.routeStack[fromIndex], - toRoute: this.state.routeStack[toIndex], - } - ); - } - }, - _getDestIndexWithinBounds: function(n) { var currentIndex = this.state.presentedIndex; var destIndex = currentIndex + n; @@ -788,20 +790,19 @@ var Navigator = React.createClass({ }, _jumpN: function(n) { - var currentIndex = this.state.presentedIndex; + var destIndex = this._getDestIndexWithinBounds(n); if (!this._canNavigate()) { return; // It's busy animating or transitioning. } - if (n > 0) { - this._fillBackstackRange(currentIndex, currentIndex + n); - } else { - var landingBeforeIndex = currentIndex + n + 1; - Backstack.resetToBefore( - this._backstackComponentKey, - landingBeforeIndex - ); - } - this._jumpNWithoutBackstack(n); + var requestTransitionAndResetUpdatingRange = () => { + this._requestTransitionTo(destIndex); + this._resetUpdatingRange(); + }; + this.setState({ + updatingRangeStart: destIndex, + updatingRangeLength: 1, + toIndex: destIndex, + }, requestTransitionAndResetUpdatingRange); }, jumpTo: function(route) { @@ -813,15 +814,6 @@ var Navigator = React.createClass({ this._jumpN(destIndex - this.state.presentedIndex); }, - _jumpToWithoutBackstack: function(route) { - var destIndex = this.state.routeStack.indexOf(route); - invariant( - destIndex !== -1, - 'Cannot jump to route that is not in the route stack' - ); - this._jumpNWithoutBackstack(destIndex - this.state.presentedIndex); - }, - jumpForward: function() { this._jumpN(1); }, @@ -852,11 +844,6 @@ var Navigator = React.createClass({ toRoute: route, fromRoute: this.state.routeStack[this.state.routeStack.length - 1], }; - Backstack.pushNavigation( - this._backstackComponentKey, - this.state.routeStack.length, - navigationState); - this.setState({ idStack: nextIDStack, routeStack: nextStack, @@ -867,14 +854,7 @@ var Navigator = React.createClass({ }, requestTransitionAndResetUpdatingRange); }, - _manuallyPopBackstack: function(n) { - Backstack.resetToBefore(this._backstackComponentKey, this.state.routeStack.length - n); - }, - - /** - * Like popN, but doesn't also update the Backstack. - */ - _popNWithoutBackstack: function(n) { + popN: function(n) { if (n === 0 || !this._canNavigate()) { return; } @@ -888,17 +868,10 @@ var Navigator = React.createClass({ ); }, - popN: function(n) { - if (n === 0 || !this._canNavigate()) { - return; - } - this._popNWithoutBackstack(n); - this._manuallyPopBackstack(n); - }, - pop: function() { - if (this.props.navigator && this.state.routeStack.length === 1) { - return this.props.navigator.pop(); + // TODO (t6707686): remove this parentNavigator call after transitioning call sites to `.request('pop')` + if (this.parentNavigator && this.state.routeStack.length === 1) { + return this.parentNavigator.pop(); } this.popN(1); }, @@ -970,14 +943,6 @@ var Navigator = React.createClass({ return this.state.routeStack.length - indexOfRoute - 1; }, - /** - * Like popToRoute, but doesn't update the Backstack, presumably because it's already up to date. - */ - _popToRouteWithoutBackstack: function(route) { - var numToPop = this._getNumToPopForRoute(route); - this._popNWithoutBackstack(numToPop); - }, - popToRoute: function(route) { var numToPop = this._getNumToPopForRoute(route); this.popN(numToPop); @@ -1003,7 +968,7 @@ var Navigator = React.createClass({ return this.state.routeStack; }, - _onItemRef: function(itemId, ref) { + _handleItemRef: function(itemId, ref) { this._itemRefs[itemId] = ref; var itemIndex = this.state.idStack.indexOf(itemId); if (itemIndex === -1) { @@ -1036,23 +1001,33 @@ var Navigator = React.createClass({ this.state.updatingRangeLength !== 0 && i >= this.state.updatingRangeStart && i <= this.state.updatingRangeStart + this.state.updatingRangeLength; + var sceneNavigatorContext = { + ...this.navigatorContext, + route, + setHandler: (handler) => { + this.navigatorContext.setHandlerForRoute(route, handler); + }, + }; var child = this.props.renderScene( route, - this.navigatorActions, - this._onItemRef.bind(null, this.state.idStack[i]) + sceneNavigatorContext ); - var initialSceneStyle = i === this.state.presentedIndex ? styles.presentNavItem : styles.futureNavItem; return ( - + - {child} + {React.cloneElement(child, { + ref: this._handleItemRef.bind(null, this.state.idStack[i]), + })} - + ); }, @@ -1081,7 +1056,7 @@ var Navigator = React.createClass({ } return React.cloneElement(this.props.navigationBar, { ref: (navBar) => { this._navBar = navBar; }, - navigator: this.navigatorActions, + navigator: this.navigatorContext, navState: this.state, }); }, diff --git a/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js new file mode 100644 index 000000000..dcc5d43ef --- /dev/null +++ b/Libraries/CustomComponents/Navigator/NavigatorInterceptor.js @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule NavigatorInterceptor + */ +'use strict'; + +var React = require('React'); + +var getNavigatorContext = require('getNavigatorContext'); + +var NavigatorInterceptor = React.createClass({ + + contextTypes: { + navigator: React.PropTypes.object, + }, + + componentWillMount: function() { + this.navigator = getNavigatorContext(this); + }, + + componentDidMount: function() { + this.navigator.setHandler(this._navigatorHandleRequest); + }, + + childContextTypes: { + navigator: React.PropTypes.object, + }, + + getChildContext: function() { + return { + navigator: { + ...this.navigator, + setHandler: (handler) => { + this._childNavigationHandler = handler; + }, + } + }; + }, + + componentWillUnmount: function() { + this.navigator.setHandler(null); + }, + + _navigatorHandleRequest: function(action, arg1, arg2) { + if (this._interceptorHandle(action, arg1, arg2)) { + return true; + } + if (this._childNavigationHandler && this._childNavigationHandler(action, arg1, arg2)) { + return true; + } + }, + + _interceptorHandle: function(action, arg1, arg2) { + if (this.props.onRequest && this.props.onRequest(action, arg1, arg2)) { + return true; + } + switch (action) { + case 'pop': + return this.props.onPopRequest && this.props.onPopRequest(action, arg1, arg2); + case 'push': + return this.props.onPushRequest && this.props.onPushRequest(action, arg1, arg2); + default: + return false; + } + }, + + render: function() { + return this.props.children; + }, + +}); + +module.exports = NavigatorInterceptor; diff --git a/Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js b/Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js new file mode 100644 index 000000000..ded8048e4 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/NavigatorStaticContextContainer.js @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule NavigatorStaticContextContainer + */ +'use strict'; + +var React = require('React'); +var StaticContainer = require('StaticContainer.react'); + +var PropTypes = React.PropTypes; + +var NavigatorStaticContextContainer = React.createClass({ + + childContextTypes: { + navigator: PropTypes.object, + }, + + getChildContext: function() { + return { + navigator: this.props.navigatorContext, + }; + }, + + render: function() { + return ( + + ); + }, +}); + +module.exports = NavigatorStaticContextContainer; diff --git a/Libraries/CustomComponents/Navigator/getNavigatorContext.js b/Libraries/CustomComponents/Navigator/getNavigatorContext.js new file mode 100644 index 000000000..0c169fe0f --- /dev/null +++ b/Libraries/CustomComponents/Navigator/getNavigatorContext.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @providesModule getNavigatorContext + */ +'use strict'; + + +var ReactInstanceMap = require('ReactInstanceMap'); + +function getNavigatorContext(el) { + // TODO (t6707746): replace with `el.context.navigator` when parent context is supported + return ReactInstanceMap.get(el)._context.navigator; +} + +module.exports = getNavigatorContext; diff --git a/Libraries/Image/ImageSource.js b/Libraries/Image/ImageSource.js new file mode 100644 index 000000000..c9b3dcac4 --- /dev/null +++ b/Libraries/Image/ImageSource.js @@ -0,0 +1,17 @@ +/** + * 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 ImageSource + * @flow + */ +'use strict'; + +type ImageSource = { + uri: string; + isStatic: boolean; +}; diff --git a/Libraries/Image/RCTImageDownloader.m b/Libraries/Image/RCTImageDownloader.m index 3eb5c1b78..9577ce4e5 100644 --- a/Libraries/Image/RCTImageDownloader.m +++ b/Libraries/Image/RCTImageDownloader.m @@ -77,13 +77,14 @@ static NSString *RCTCacheKeyForURL(NSURL *url) __weak RCTImageDownloader *weakSelf = self; RCTCachedDataDownloadBlock runBlocks = ^(BOOL cached, NSData *data, NSError *error) { - - RCTImageDownloader *strongSelf = weakSelf; - NSArray *blocks = strongSelf->_pendingBlocks[cacheKey]; - [strongSelf->_pendingBlocks removeObjectForKey:cacheKey]; - for (RCTCachedDataDownloadBlock block in blocks) { - block(cached, data, error); - } + dispatch_async(_processingQueue, ^{ + RCTImageDownloader *strongSelf = weakSelf; + NSArray *blocks = strongSelf->_pendingBlocks[cacheKey]; + [strongSelf->_pendingBlocks removeObjectForKey:cacheKey]; + for (RCTCachedDataDownloadBlock block in blocks) { + block(cached, data, error); + } + }); }; if ([_cache hasDataForKey:cacheKey]) { diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m index ddf973b24..4d6aba5cc 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m +++ b/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m @@ -119,7 +119,9 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); [_jsQueue addOperationWithBlock:^{ if (!self.valid) { - NSError *error = [NSError errorWithDomain:@"WS" code:1 userInfo:@{NSLocalizedDescriptionKey:@"socket closed"}]; + NSError *error = [NSError errorWithDomain:@"WS" code:1 userInfo:@{ + NSLocalizedDescriptionKey: @"socket closed" + }]; callback(error, nil); return; } diff --git a/Libraries/Storage/AsyncStorage.ios.js b/Libraries/Storage/AsyncStorage.ios.js index aa48e40de..5ee2dc5e3 100644 --- a/Libraries/Storage/AsyncStorage.ios.js +++ b/Libraries/Storage/AsyncStorage.ios.js @@ -27,49 +27,73 @@ var RCTAsyncStorage = RCTAsyncRocksDBStorage || RCTAsyncLocalStorage; * operates globally. * * This JS code is a simple facad over the native iOS implementation to provide - * a clear JS API, real Error objects, and simple non-multi functions. + * a clear JS API, real Error objects, and simple non-multi functions. Each + * method returns a `Promise` object. */ var AsyncStorage = { /** * Fetches `key` and passes the result to `callback`, along with an `Error` if - * there is any. + * there is any. Returns a `Promise` object. */ getItem: function( key: string, callback: (error: ?Error, result: ?string) => void - ): void { - RCTAsyncStorage.multiGet([key], function(errors, result) { - // Unpack result to get value from [[key,value]] - var value = (result && result[0] && result[0][1]) ? result[0][1] : null; - callback((errors && convertError(errors[0])) || null, value); + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiGet([key], function(errors, result) { + // Unpack result to get value from [[key,value]] + var value = (result && result[0] && result[0][1]) ? result[0][1] : null; + callback && callback((errors && convertError(errors[0])) || null, value); + if (errors) { + reject(convertError(errors[0])); + } else { + resolve(value); + } + }); }); }, /** * Sets `value` for `key` and calls `callback` on completion, along with an - * `Error` if there is any. + * `Error` if there is any. Returns a `Promise` object. */ setItem: function( key: string, value: string, callback: ?(error: ?Error) => void - ): void { - RCTAsyncStorage.multiSet([[key,value]], function(errors) { - callback && callback((errors && convertError(errors[0])) || null); + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiSet([[key,value]], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + if (errors) { + reject(convertError(errors[0])); + } else { + resolve(null); + } + }); }); }, - + /** + * Returns a `Promise` object. + */ removeItem: function( key: string, callback: ?(error: ?Error) => void - ): void { - RCTAsyncStorage.multiRemove([key], function(errors) { - callback && callback((errors && convertError(errors[0])) || null); + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiRemove([key], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + if (errors) { + reject(convertError(errors[0])); + } else { + resolve(null); + } + }); }); }, /** - * Merges existing value with input value, assuming they are stringified json. + * Merges existing value with input value, assuming they are stringified json. Returns a `Promise` object. * * Not supported by all native implementations. */ @@ -77,29 +101,50 @@ var AsyncStorage = { key: string, value: string, callback: ?(error: ?Error) => void - ): void { - RCTAsyncStorage.multiMerge([[key,value]], function(errors) { - callback && callback((errors && convertError(errors[0])) || null); + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiMerge([[key,value]], function(errors) { + callback && callback((errors && convertError(errors[0])) || null); + if (errors) { + reject(convertError(errors[0])); + } else { + resolve(null); + } + }); }); }, /** * Erases *all* AsyncStorage for all clients, libraries, etc. You probably * don't want to call this - use removeItem or multiRemove to clear only your - * own keys instead. + * own keys instead. Returns a `Promise` object. */ - clear: function(callback: ?(error: ?Error) => void) { - RCTAsyncStorage.clear(function(error) { - callback && callback(convertError(error)); + clear: function(callback: ?(error: ?Error) => void): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.clear(function(error) { + callback && callback(convertError(error)); + if (error && convertError(error)){ + reject(convertError(error)); + } else { + resolve(null); + } + }); }); }, /** - * Gets *all* keys known to the system, for all callers, libraries, etc. + * Gets *all* keys known to the system, for all callers, libraries, etc. Returns a `Promise` object. */ - getAllKeys: function(callback: (error: ?Error) => void) { - RCTAsyncStorage.getAllKeys(function(error, keys) { - callback(convertError(error), keys); + getAllKeys: function(callback: (error: ?Error) => void): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.getAllKeys(function(error, keys) { + callback && callback(convertError(error), keys); + if (error) { + reject(convertError(error)); + } else { + resolve(keys); + } + }); }); }, @@ -115,67 +160,90 @@ var AsyncStorage = { /** * multiGet invokes callback with an array of key-value pair arrays that - * matches the input format of multiSet. + * matches the input format of multiSet. Returns a `Promise` object. * * multiGet(['k1', 'k2'], cb) -> cb([['k1', 'val1'], ['k2', 'val2']]) */ multiGet: function( keys: Array, callback: (errors: ?Array, result: ?Array>) => void - ): void { - RCTAsyncStorage.multiGet(keys, function(errors, result) { - callback( - (errors && errors.map((error) => convertError(error))) || null, - result - ); + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiGet(keys, function(errors, result) { + var error = (errors && errors.map((error) => convertError(error))) || null; + callback && callback(error, result); + if (errors) { + reject(error); + } else { + resolve(result); + } + }); }); }, /** * multiSet and multiMerge take arrays of key-value array pairs that match - * the output of multiGet, e.g. + * the output of multiGet, e.g. Returns a `Promise` object. * * multiSet([['k1', 'val1'], ['k2', 'val2']], cb); */ multiSet: function( keyValuePairs: Array>, callback: ?(errors: ?Array) => void - ): void { - RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { - callback && callback( - (errors && errors.map((error) => convertError(error))) || null - ); + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiSet(keyValuePairs, function(errors) { + var error = (errors && errors.map((error) => convertError(error))) || null; + callback && callback(error); + if (errors) { + reject(error); + } else { + resolve(null); + } + }); }); }, /** - * Delete all the keys in the `keys` array. + * Delete all the keys in the `keys` array. Returns a `Promise` object. */ multiRemove: function( keys: Array, callback: ?(errors: ?Array) => void - ): void { - RCTAsyncStorage.multiRemove(keys, function(errors) { - callback && callback( - (errors && errors.map((error) => convertError(error))) || null - ); + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiRemove(keys, function(errors) { + var error = (errors && errors.map((error) => convertError(error))) || null; + callback && callback(error); + if (errors) { + reject(error); + } else { + resolve(null); + } + }); }); }, /** * Merges existing values with input values, assuming they are stringified - * json. + * json. Returns a `Promise` object. * * Not supported by all native implementations. */ multiMerge: function( keyValuePairs: Array>, callback: ?(errors: ?Array) => void - ): void { - RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { - callback && callback( - (errors && errors.map((error) => convertError(error))) || null - ); + ): Promise { + return new Promise((resolve, reject) => { + RCTAsyncStorage.multiMerge(keyValuePairs, function(errors) { + var error = (errors && errors.map((error) => convertError(error))) || null; + callback && callback(error); + if (errors) { + reject(error); + } else { + resolve(null); + } + }); }); }, }; diff --git a/Libraries/Text/RCTRawTextManager.m b/Libraries/Text/RCTRawTextManager.m index 3215b36c1..ab856d049 100644 --- a/Libraries/Text/RCTRawTextManager.m +++ b/Libraries/Text/RCTRawTextManager.m @@ -15,7 +15,7 @@ - (UIView *)view { - return [[UIView alloc] init]; + return nil; } - (RCTShadowView *)shadowView @@ -26,4 +26,3 @@ RCT_EXPORT_SHADOW_PROPERTY(text, NSString) @end - diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h index 82ea2b632..286edb53a 100644 --- a/Libraries/Text/RCTShadowText.h +++ b/Libraries/Text/RCTShadowText.h @@ -23,7 +23,7 @@ extern NSString *const RCTReactTagAttributeName; @property (nonatomic, copy) NSString *fontStyle; @property (nonatomic, assign) BOOL isHighlighted; @property (nonatomic, assign) CGFloat lineHeight; -@property (nonatomic, assign) NSInteger maxNumberOfLines; +@property (nonatomic, assign) NSUInteger maximumNumberOfLines; @property (nonatomic, assign) CGSize shadowOffset; @property (nonatomic, assign) NSTextAlignment textAlign; @@ -31,6 +31,8 @@ extern NSString *const RCTReactTagAttributeName; @property (nonatomic, strong) UIFont *font; @property (nonatomic, assign) NSLineBreakMode truncationMode; -- (NSAttributedString *)attributedString; +@property (nonatomic, copy, readonly) NSAttributedString *attributedString; +@property (nonatomic, strong, readonly) NSLayoutManager *layoutManager; +@property (nonatomic, strong, readonly) NSTextContainer *textContainer; @end diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index da4b90c06..4201b1b4e 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -20,9 +20,17 @@ NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName"; static css_dim_t RCTMeasure(void *context, float width) { RCTShadowText *shadowText = (__bridge RCTShadowText *)context; - CGSize computedSize = [[shadowText attributedString] boundingRectWithSize:(CGSize){isnan(width) ? CGFLOAT_MAX : width, CGFLOAT_MAX} - options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading - context:nil].size; + + NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[shadowText attributedString]]; + [textStorage addLayoutManager:shadowText.layoutManager]; + + shadowText.textContainer.size = CGSizeMake(isnan(width) ? CGFLOAT_MAX : width, CGFLOAT_MAX); + shadowText.layoutManager.textStorage = textStorage; + [shadowText.layoutManager ensureLayoutForTextContainer:shadowText.textContainer]; + + CGSize computedSize = [shadowText.layoutManager usedRectForTextContainer:shadowText.textContainer].size; + + [textStorage removeLayoutManager:shadowText.layoutManager]; css_dim_t result; result.dimensions[CSS_WIDTH] = RCTCeilPixelValue(computedSize.width); @@ -30,8 +38,9 @@ static css_dim_t RCTMeasure(void *context, float width) return result; } -@implementation RCTShadowText -{ +@implementation RCTShadowText { + NSLayoutManager *_layoutManager; + NSTextContainer *_textContainer; NSAttributedString *_cachedAttributedString; UIFont *_font; } @@ -41,7 +50,15 @@ static css_dim_t RCTMeasure(void *context, float width) if ((self = [super init])) { _fontSize = NAN; _isHighlighted = NO; + + _textContainer = [[NSTextContainer alloc] init]; + _textContainer.lineBreakMode = NSLineBreakByTruncatingTail; + _textContainer.lineFragmentPadding = 0.0; + + _layoutManager = [[NSLayoutManager alloc] init]; + [_layoutManager addTextContainer:_textContainer]; } + return self; } @@ -201,11 +218,31 @@ RCT_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *); RCT_TEXT_PROPERTY(FontSize, _fontSize, CGFloat); RCT_TEXT_PROPERTY(FontWeight, _fontWeight, NSString *); RCT_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat); -RCT_TEXT_PROPERTY(MaxNumberOfLines, _maxNumberOfLines, NSInteger); RCT_TEXT_PROPERTY(ShadowOffset, _shadowOffset, CGSize); RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment); -RCT_TEXT_PROPERTY(TruncationMode, _truncationMode, NSLineBreakMode); RCT_TEXT_PROPERTY(IsHighlighted, _isHighlighted, BOOL); RCT_TEXT_PROPERTY(Font, _font, UIFont *); +- (NSLineBreakMode)truncationMode +{ + return _textContainer.lineBreakMode; +} + +- (void)setTruncationMode:(NSLineBreakMode)truncationMode +{ + _textContainer.lineBreakMode = truncationMode; + [self dirtyText]; +} + +- (NSUInteger)maximumNumberOfLines +{ + return _textContainer.maximumNumberOfLines; +} + +- (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines +{ + _textContainer.maximumNumberOfLines = maximumNumberOfLines; + [self dirtyText]; +} + @end diff --git a/Libraries/Text/RCTText.h b/Libraries/Text/RCTText.h index 59b15668a..24b98e991 100644 --- a/Libraries/Text/RCTText.h +++ b/Libraries/Text/RCTText.h @@ -11,9 +11,10 @@ @interface RCTText : UIView +@property (nonatomic, strong) NSLayoutManager *layoutManager; +@property (nonatomic, strong) NSTextContainer *textContainer; @property (nonatomic, copy) NSAttributedString *attributedText; -@property (nonatomic, assign) NSLineBreakMode lineBreakMode; -@property (nonatomic, assign) NSUInteger numberOfLines; + @property (nonatomic, assign) UIEdgeInsets contentInset; @end diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index a2f7f11cc..d6e00f1b6 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -23,15 +23,7 @@ - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { - _textContainer = [[NSTextContainer alloc] init]; - _textContainer.lineBreakMode = NSLineBreakByTruncatingTail; - _textContainer.lineFragmentPadding = 0.0; - - _layoutManager = [[NSLayoutManager alloc] init]; - [_layoutManager addTextContainer:_textContainer]; - _textStorage = [[NSTextStorage alloc] init]; - [_textStorage addLayoutManager:_layoutManager]; self.contentMode = UIViewContentModeRedraw; } @@ -50,25 +42,31 @@ [self setNeedsDisplay]; } -- (NSUInteger)numberOfLines +- (void)setTextContainer:(NSTextContainer *)textContainer { - return _textContainer.maximumNumberOfLines; -} + if ([_textContainer isEqual:textContainer]) return; + + _textContainer = textContainer; + + for (NSInteger i = _layoutManager.textContainers.count - 1; i >= 0; i--) { + [_layoutManager removeTextContainerAtIndex:i]; + } + [_layoutManager addTextContainer:_textContainer]; -- (void)setNumberOfLines:(NSUInteger)numberOfLines -{ - _textContainer.maximumNumberOfLines = numberOfLines; [self setNeedsDisplay]; } -- (NSLineBreakMode)lineBreakMode +- (void)setLayoutManager:(NSLayoutManager *)layoutManager { - return _textContainer.lineBreakMode; -} + if ([_layoutManager isEqual:layoutManager]) return; + + _layoutManager = layoutManager; + + for (NSLayoutManager *existingLayoutManager in _textStorage.layoutManagers) { + [_textStorage removeLayoutManager:existingLayoutManager]; + } + [_textStorage addLayoutManager:_layoutManager]; -- (void)setLineBreakMode:(NSLineBreakMode)lineBreakMode -{ - _textContainer.lineBreakMode = lineBreakMode; [self setNeedsDisplay]; } diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 8df67e59e..d19dfa71e 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -33,15 +33,6 @@ #pragma mark - View properties RCT_REMAP_VIEW_PROPERTY(containerBackgroundColor, backgroundColor, UIColor) -RCT_CUSTOM_VIEW_PROPERTY(numberOfLines, NSInteger, RCTText) -{ - NSLineBreakMode truncationMode = NSLineBreakByClipping; - view.numberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.numberOfLines; - if (view.numberOfLines > 0) { - truncationMode = NSLineBreakByTruncatingTail; - } - view.lineBreakMode = truncationMode; -} #pragma mark - Shadow properties @@ -65,8 +56,8 @@ RCT_CUSTOM_SHADOW_PROPERTY(containerBackgroundColor, UIColor, RCTShadowText) RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText) { NSLineBreakMode truncationMode = NSLineBreakByClipping; - view.maxNumberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.maxNumberOfLines; - if (view.maxNumberOfLines > 0) { + view.maximumNumberOfLines = json ? [RCTConvert NSInteger:json] : defaultView.maximumNumberOfLines; + if (view.maximumNumberOfLines > 0) { truncationMode = NSLineBreakByTruncatingTail; } view.truncationMode = truncationMode; @@ -124,12 +115,16 @@ RCT_CUSTOM_SHADOW_PROPERTY(numberOfLines, NSInteger, RCTShadowText) }; } -- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView +- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowText *)shadowView { NSNumber *reactTag = shadowView.reactTag; UIEdgeInsets padding = shadowView.paddingAsInsets; + return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - ((RCTText *)viewRegistry[reactTag]).contentInset = padding; + RCTText *text = (RCTText *)viewRegistry[reactTag]; + text.contentInset = padding; + text.layoutManager = shadowView.layoutManager; + text.textContainer = shadowView.textContainer; }; } diff --git a/Libraries/Utilities/BackAndroid.ios.js b/Libraries/Utilities/BackAndroid.ios.js new file mode 100644 index 000000000..c5a56f40e --- /dev/null +++ b/Libraries/Utilities/BackAndroid.ios.js @@ -0,0 +1,21 @@ +/** + * iOS stub for BackAndroid.android.js + * + * @providesModule BackAndroid + */ + +'use strict'; + +var warning = require('warning'); + +function platformWarn() { + warning(false, 'BackAndroid is not supported on this platform.'); +} + +var BackAndroid = { + exitApp: platformWarn, + addEventListener: platformWarn, + removeEventListener: platformWarn, +}; + +module.exports = BackAndroid; diff --git a/Libraries/Utilities/Backstack.ios.js b/Libraries/Utilities/Backstack.ios.js deleted file mode 100644 index 00a538668..000000000 --- a/Libraries/Utilities/Backstack.ios.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * To lower the risk of breaking things on iOS, we are stubbing out the - * BackStack for now. See Backstack.android.js - * - * @providesModule Backstack - */ - -'use strict'; - -var Backstack = { - pushNavigation: () => {}, - resetToBefore: () => {}, - removeComponentHistory: () => {}, -}; - -module.exports = Backstack; diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 05767533d..3f0ad735e 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -28,6 +28,11 @@ typedef NSArray *(^RCTBridgeModuleProviderBlock)(void); extern NSString *const RCTReloadBridge; +/** + * This function returns the module name for a given class. + */ +extern NSString *RCTBridgeModuleNameForClass(Class bridgeModuleClass); + /** * Async batched bridge used to communicate with the JavaScript application. */ @@ -81,14 +86,6 @@ extern NSString *const RCTReloadBridge; */ @property (nonatomic, readonly) dispatch_queue_t shadowQueue; -/** - * Global logging function that will print to both xcode and JS debugger consoles. - * - * NOTE: Use via RCTLog* macros defined in RCTLog.h - * TODO (#5906496): should log function be exposed here, or could it be a module? - */ -+ (void)log:(NSArray *)objects level:(NSString *)level; - @property (nonatomic, copy, readonly) NSDictionary *launchOptions; diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 1ad3d58b7..3bf23a5d3 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -22,6 +22,7 @@ #import "RCTJavaScriptLoader.h" #import "RCTKeyCommands.h" #import "RCTLog.h" +#import "RCTRedBox.h" #import "RCTRootView.h" #import "RCTSparseArray.h" #import "RCTUtils.h" @@ -41,10 +42,7 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) { NSString *const RCTReloadBridge = @"RCTReloadBridge"; -/** - * This function returns the module name for a given class. - */ -static NSString *RCTModuleNameForClass(Class cls) +NSString *RCTBridgeModuleNameForClass(Class cls) { return [cls respondsToSelector:@selector(moduleName)] ? [cls moduleName] : NSStringFromClass(cls); } @@ -92,7 +90,7 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) [(NSMutableArray *)modules addObject:cls]; // Add module name - NSString *moduleName = RCTModuleNameForClass(cls); + NSString *moduleName = RCTBridgeModuleNameForClass(cls); [(NSMutableArray *)RCTModuleNamesByID addObject:moduleName]; } }); @@ -187,7 +185,7 @@ static Class _globalExecutorClass; RCT_ARG_BLOCK( \ if (json && ![json isKindOfClass:[_class class]]) { \ RCTLogError(@"Argument %tu (%@) of %@.%@ should be of type %@", index, \ - json, RCTModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \ + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, [_class class]); \ return; \ } \ _logic \ @@ -203,7 +201,7 @@ static Class _globalExecutorClass; RCT_ARG_BLOCK( \ if (json && ![json respondsToSelector:@selector(_selector)]) { \ RCTLogError(@"Argument %tu (%@) of %@.%@ does not respond to selector: %@", \ - index, json, RCTModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \ + index, json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, @#_selector); \ return; \ } \ _type value = [json _selector]; \ @@ -231,7 +229,7 @@ static Class _globalExecutorClass; RCT_ARG_BLOCK( if (json && ![json isKindOfClass:[NSNumber class]]) { RCTLogError(@"Argument %tu (%@) of %@.%@ should be a number", index, - json, RCTModuleNameForClass(_moduleClass), _JSMethodName); + json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName); return; } // Marked as autoreleasing, because NSInvocation doesn't retain arguments @@ -268,7 +266,7 @@ static Class _globalExecutorClass; // Safety check if (arguments.count != _argumentBlocks.count) { RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd", - RCTModuleNameForClass(_moduleClass), _JSMethodName, + RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName, arguments.count, _argumentBlocks.count); return; } @@ -544,7 +542,7 @@ static id _latestJSExecutor; // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; for (id module in _moduleProvider ? _moduleProvider() : nil) { - preregisteredModules[RCTModuleNameForClass([module class])] = module; + preregisteredModules[RCTBridgeModuleNameForClass([module class])] = module; } // Instantiate modules @@ -895,27 +893,18 @@ static id _latestJSExecutor; return (_latestJSExecutor != nil && [_latestJSExecutor isValid]); } -+ (void)log:(NSArray *)objects level:(NSString *)level ++ (void)logMessage:(NSString *)message level:(NSString *)level { if (!_latestJSExecutor || ![_latestJSExecutor isValid]) { - RCTLogError(@"ERROR: No valid JS executor to log %@.", objects); + RCTLogError(@"ERROR: No valid JS executor to log '%@'.", message); return; } - NSMutableArray *args = [NSMutableArray arrayWithObject:level]; - // TODO (#5906496): Find out and document why we skip the first object - for (id ob in [objects subarrayWithRange:(NSRange){1, [objects count] - 1}]) { - if ([NSJSONSerialization isValidJSONObject:@[ob]]) { - [args addObject:ob]; - } else { - [args addObject:[ob description]]; - } - } - - // Note: the js executor could get invalidated while we're trying to call this...need to watch out for that. + // Note: the js executor could get invalidated while we're trying to call + // this...need to watch out for that. [_latestJSExecutor executeJSCall:@"RCTLog" method:@"logIfNoNativeHook" - arguments:args + arguments:@[level, message] callback:^(id json, NSError *error) {}]; } diff --git a/React/Base/RCTConvert.h b/React/Base/RCTConvert.h index 666ce013e..7e573370e 100644 --- a/React/Base/RCTConvert.h +++ b/React/Base/RCTConvert.h @@ -145,7 +145,7 @@ RCT_CUSTOM_CONVERTER(type, name, [json getter]) } \ @catch (__unused NSException *e) { \ RCTLogError(@"JSON value '%@' of type '%@' cannot be converted to '%s'", \ - json, [json class], #type); \ + json, [json classForCoder], #type); \ json = nil; \ return code; \ } \ @@ -181,7 +181,8 @@ RCT_CUSTOM_CONVERTER(type, type, [[self NSNumber:json] getter]) return default; \ } \ if (![json isKindOfClass:[NSString class]]) { \ - RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", #type, [json class], json); \ + RCTLogError(@"Expected NSNumber or NSString for %s, received %@: %@", \ + #type, [json classForCoder], json); \ } \ id value = mapping[json]; \ if(!value && [json description].length > 0) { \ diff --git a/React/Base/RCTConvert.m b/React/Base/RCTConvert.m index 5fbc2d527..1ddc9884c 100644 --- a/React/Base/RCTConvert.m +++ b/React/Base/RCTConvert.m @@ -45,7 +45,7 @@ RCT_CONVERTER(NSString *, NSString, description) } return number; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json class]); + RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a number", json, [json classForCoder]); } return nil; } @@ -53,7 +53,7 @@ RCT_CONVERTER(NSString *, NSString, description) + (NSURL *)NSURL:(id)json { if (![json isKindOfClass:[NSString class]]) { - RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json class], json); + RCTLogError(@"Expected NSString for NSURL, received %@: %@", [json classForCoder], json); return nil; } @@ -98,7 +98,7 @@ RCT_CONVERTER(NSString *, NSString, description) } return date; } else if (json && json != [NSNull null]) { - RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json class]); + RCTLogError(@"JSON value '%@' of class %@ could not be interpreted as a date", json, [json classForCoder]); } return nil; } @@ -223,7 +223,7 @@ RCT_ENUM_CONVERTER(UIBarStyle, (@{ json = [json mutableCopy]; \ for (NSString *alias in aliases) { \ NSString *key = aliases[alias]; \ - NSNumber *number = json[key]; \ + NSNumber *number = json[alias]; \ if (number) { \ RCTLogWarn(@"Using deprecated '%@' property for '%s'. Use '%@' instead.", alias, #type, key); \ ((NSMutableDictionary *)json)[key] = number; \ @@ -234,7 +234,8 @@ RCT_ENUM_CONVERTER(UIBarStyle, (@{ ((CGFloat *)&result)[i] = [self CGFloat:json[fields[i]]]; \ } \ } else if (json && json != [NSNull null]) { \ - RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", #type, [json class], json); \ + RCTLogError(@"Expected NSArray or NSDictionary for %s, received %@: %@", \ + #type, [json classForCoder], json); \ } \ return result; \ } \ @@ -511,8 +512,8 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ } else if (json && ![json isKindOfClass:[NSNull class]]) { - RCTLogError(@"Expected NSArray, NSDictionary or NSString for UIColor, \ - received %@: %@", [json class], json); + RCTLogError(@"Expected NSArray, NSDictionary or NSString for UIColor, received %@: %@", + [json classForCoder], json); } // Default color @@ -538,7 +539,7 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[ // image itself) so as to reduce overhead on subsequent checks of the same input if (![json isKindOfClass:[NSString class]]) { - RCTLogError(@"Expected NSString for UIImage, received %@: %@", [json class], json); + RCTLogError(@"Expected NSString for UIImage, received %@: %@", [json classForCoder], json); return nil; } diff --git a/React/Base/RCTLog.h b/React/Base/RCTLog.h index 9698e6476..c30da141b 100644 --- a/React/Base/RCTLog.h +++ b/React/Base/RCTLog.h @@ -7,58 +7,138 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import "RCTAssert.h" -#import "RCTRedBox.h" - -#define RCTLOG_INFO 1 -#define RCTLOG_WARN 2 -#define RCTLOG_ERROR 3 -#define RCTLOG_MUSTFIX 4 - -// If set to e.g. `RCTLOG_ERROR`, will assert after logging the first error. -#if DEBUG -#define RCTLOG_FATAL_LEVEL RCTLOG_MUSTFIX -#define RCTLOG_REDBOX_LEVEL RCTLOG_ERROR -#else -#define RCTLOG_FATAL_LEVEL (RCTLOG_MUSTFIX + 1) -#define RCTLOG_REDBOX_LEVEL (RCTLOG_MUSTFIX + 1) -#endif - -// If defined, only log messages that match this regex will fatal -#define RCTLOG_FATAL_REGEX nil - -extern __unsafe_unretained NSString *RCTLogLevels[]; - -#define _RCTLog(_level, ...) do { \ - NSString *__RCTLog__levelStr = RCTLogLevels[_level - 1]; \ - NSString *__RCTLog__msg = RCTLogObjects(RCTLogFormat(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__), __RCTLog__levelStr); \ - if (_level >= RCTLOG_FATAL_LEVEL) { \ - BOOL __RCTLog__fail = YES; \ - if (RCTLOG_FATAL_REGEX) { \ - NSRegularExpression *__RCTLog__regex = [NSRegularExpression regularExpressionWithPattern:RCTLOG_FATAL_REGEX options:0 error:NULL]; \ - __RCTLog__fail = [__RCTLog__regex numberOfMatchesInString:__RCTLog__msg options:0 range:NSMakeRange(0, [__RCTLog__msg length])] > 0; \ - } \ - RCTCAssert(!__RCTLog__fail, @"RCTLOG_FATAL_LEVEL %@: %@", __RCTLog__levelStr, __RCTLog__msg); \ - } \ - if (_level >= RCTLOG_REDBOX_LEVEL) { \ - [[RCTRedBox sharedInstance] showErrorMessage:__RCTLog__msg]; \ - } \ -} while (0) - -#define RCTLog(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__) -#define RCTLogInfo(...) _RCTLog(RCTLOG_INFO, __VA_ARGS__) -#define RCTLogWarn(...) _RCTLog(RCTLOG_WARN, __VA_ARGS__) -#define RCTLogError(...) _RCTLog(RCTLOG_ERROR, __VA_ARGS__) -#define RCTLogMustFix(...) _RCTLog(RCTLOG_MUSTFIX, __VA_ARGS__) #ifdef __cplusplus extern "C" { #endif -NSString *RCTLogObjects(NSArray *objects, NSString *level); -NSArray *RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) NS_FORMAT_FUNCTION(4,5); +/** + * Thresholds for logs to raise an assertion, or display redbox, respectively. + * You can override these values when debugging in order to tweak the default + * logging behavior. + */ +#define RCTLOG_FATAL_LEVEL RCTLogLevelMustFix +#define RCTLOG_REDBOX_LEVEL RCTLogLevelError -void RCTInjectLogFunction(void (^logFunction)(NSString *msg)); +/** + * A regular expression that can be used to selectively limit the throwing of + * a exception to specific log contents. + */ +#define RCTLOG_FATAL_REGEX nil + +/** + * An enum representing the severity of the log message. + */ +typedef NS_ENUM(NSInteger, RCTLogLevel) { + RCTLogLevelInfo = 1, + RCTLogLevelWarning = 2, + RCTLogLevelError = 3, + RCTLogLevelMustFix = 4 +}; + +/** + * A block signature to be used for custom logging functions. In most cases you + * will want to pass these arguments to the RCTFormatLog function in order to + * generate a string, or use the RCTSimpleLogFunction() constructor to register + * a simple function that does not use all of the arguments. + */ +typedef void (^RCTLogFunction)( + RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message +); + +/** + * A method to generate a string from a collection of log data. To omit any + * particular data from the log, just pass nil or zero for the argument. + */ +NSString *RCTFormatLog( + NSDate *timestamp, + NSThread *thread, + RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message +); + +/** + * A method to generate a log function from a block with a much simpler + * template. The message passed to the simpler block is equivalent to the + * output of the RCTFormatLog() function. + */ +RCTLogFunction RCTSimpleLogFunction(void (^logFunction)(RCTLogLevel level, NSString *message)); + +/** + * The default logging function used by RCTLogXX. + */ +extern RCTLogFunction RCTDefaultLogFunction; + +/** + * These methods get and set the current logging threshold. This is the level + * below which logs will be ignored. Default is RCTLogLevelInfo for debug and + * RCTLogLevelError for production. + */ +void RCTSetLogThreshold(RCTLogLevel threshold); +RCTLogLevel RCTGetLogThreshold(void); + +/** + * These methods get and set the current logging function called by the RCTLogXX + * macros. You can use these to replace the standard behavior with custom log + * functionality. + */ +void RCTSetLogFunction(RCTLogFunction logFunction); +RCTLogFunction RCTGetLogFunction(void); + +/** + * This appends additional code to the existing log function, without replacing + * the existing functionality. Useful if you just want to forward logs to an + * extra service without changing the default behavior. + */ +void RCTAddLogFunction(RCTLogFunction logFunction); + +/** + * This method adds a conditional prefix to any messages logged within the scope + * of the passed block. This is useful for adding additional context to log + * messages. The block will be performed synchronously on the current thread. + */ +void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix); + +/** + * Private logging functions - ignore these. + */ +void _RCTLogFormat(RCTLogLevel, const char *, int, NSString *, ...) NS_FORMAT_FUNCTION(4,5); +#define _RCTLog(lvl, ...) do { \ + NSString *msg = [NSString stringWithFormat:__VA_ARGS__]; \ + if (lvl >= RCTLOG_FATAL_LEVEL) { \ + BOOL fail = YES; \ + if (RCTLOG_FATAL_REGEX) { \ + if ([msg rangeOfString:RCTLOG_FATAL_REGEX options:NSRegularExpressionSearch].length) { \ + fail = NO; \ + } \ + } \ + RCTCAssert(!fail, @"FATAL ERROR: %@", msg); \ + }\ + _RCTLogFormat(lvl, __FILE__, __LINE__, __VA_ARGS__); \ +} while (0) + +/** + * Legacy injection function - don't use this. + */ +void RCTInjectLogFunction(void (^)(NSString *msg)); + +/** + * Logging macros. Use these to log information, warnings and errors in your + * own code. + */ +#define RCTLog(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__) +#define RCTLogInfo(...) _RCTLog(RCTLogLevelInfo, __VA_ARGS__) +#define RCTLogWarn(...) _RCTLog(RCTLogLevelWarning, __VA_ARGS__) +#define RCTLogError(...) _RCTLog(RCTLogLevelError, __VA_ARGS__) +#define RCTLogMustFix(...) _RCTLog(RCTLogLevelMustFix, __VA_ARGS__) #ifdef __cplusplus } diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index cd035eda2..d2de897f1 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -9,85 +9,218 @@ #import "RCTLog.h" +#import "RCTAssert.h" #import "RCTBridge.h" +#import "RCTRedBox.h" -__unsafe_unretained NSString *RCTLogLevels[] = { - @"info", - @"warn", - @"error", - @"mustfix" +@interface RCTBridge (Logging) + ++ (void)logMessage:(NSString *)message level:(NSString *)level; + +@end + +static NSString *const RCTLogPrefixStack = @"RCTLogPrefixStack"; + +const char *RCTLogLevels[] = { + "info", + "warn", + "error", + "mustfix" }; -static void (^RCTInjectedLogFunction)(NSString *msg); +static RCTLogFunction RCTCurrentLogFunction; +static RCTLogLevel RCTCurrentLogThreshold; -void RCTInjectLogFunction(void (^logFunction)(NSString *msg)) { - RCTInjectedLogFunction = logFunction; -} - -static inline NSString *_RCTLogPreamble(const char *file, int lineNumber, const char *funcName) +void RCTLogSetup(void) __attribute__((constructor)); +void RCTLogSetup() { - NSString *threadName = [[NSThread currentThread] name]; - NSString *fileName=[[NSString stringWithUTF8String:file] lastPathComponent]; - if (!threadName || threadName.length <= 0) { - threadName = [NSString stringWithFormat:@"%p", [NSThread currentThread]]; - } - return [NSString stringWithFormat:@"[RCTLog][tid:%@][%@:%d]>", threadName, fileName, lineNumber]; -} + RCTCurrentLogFunction = RCTDefaultLogFunction; -// TODO (#5906496): Does this need to be tied to RCTBridge? -NSString *RCTLogObjects(NSArray *objects, NSString *level) -{ - NSString *str = objects[0]; -#if TARGET_IPHONE_SIMULATOR - if ([RCTBridge hasValidJSExecutor]) { - fprintf(stderr, "%s\n", [str UTF8String]); // don't print timestamps and other junk - [RCTBridge log:objects level:level]; - } else +#if DEBUG + RCTCurrentLogThreshold = RCTLogLevelInfo - 1; +#else + RCTCurrentLogThreshold = RCTLogLevelError; #endif - { - // Print normal errors with timestamps when not in simulator. - // Non errors are already compiled out above, so log as error here. - if (RCTInjectedLogFunction) { - RCTInjectedLogFunction(str); - } else { - NSLog(@">\n %@", str); - } - } - return str; + } -// Returns array of objects. First arg is a simple string to print, remaining args -// are objects to pass through to the debugger so they are inspectable in the console. -NSArray *RCTLogFormat(const char *file, int lineNumber, const char *funcName, NSString *format, ...) +RCTLogFunction RCTDefaultLogFunction = ^( + RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message +) { - va_list args; - va_start(args, format); - NSString *preamble = _RCTLogPreamble(file, lineNumber, funcName); + NSString *log = RCTFormatLog( + [NSDate date], [NSThread currentThread], level, fileName, lineNumber, message + ); + fprintf(stderr, "%s\n", log.UTF8String); +}; - // Pull out NSObjects so we can pass them through as inspectable objects to the js debugger - NSArray *formatParts = [format componentsSeparatedByString:@"%"]; - NSMutableArray *objects = [NSMutableArray arrayWithObject:preamble]; - BOOL valid = YES; - for (int i = 0; i < formatParts.count; i++) { - if (i == 0) { // first part is always a string - [objects addObject:formatParts[i]]; +void RCTSetLogFunction(RCTLogFunction logFunction) +{ + RCTCurrentLogFunction = logFunction; +} + +RCTLogFunction RCTGetLogFunction() +{ + return RCTCurrentLogFunction; +} + +void RCTAddLogFunction(RCTLogFunction logFunction) +{ + RCTLogFunction existing = RCTCurrentLogFunction; + if (existing) { + RCTCurrentLogFunction = ^(RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message) { + + existing(level, fileName, lineNumber, message); + logFunction(level, fileName, lineNumber, message); + }; + } else { + RCTCurrentLogFunction = logFunction; + } +} + +void RCTPerformBlockWithLogPrefix(void (^block)(void), NSString *prefix) +{ + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + NSMutableArray *prefixStack = threadDictionary[RCTLogPrefixStack]; + if (!prefixStack) { + prefixStack = [[NSMutableArray alloc] init]; + threadDictionary[RCTLogPrefixStack] = prefixStack; + } + [prefixStack addObject:prefix]; + block(); + [prefixStack removeLastObject]; +} + +NSString *RCTFormatLog( + NSDate *timestamp, + NSThread *thread, + RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message +) +{ + NSMutableString *log = [[NSMutableString alloc] init]; + if (timestamp) { + static NSDateFormatter *formatter; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS "; + }); + [log appendString:[formatter stringFromDate:timestamp]]; + } + [log appendString:@"[react]"]; + if (level) { + [log appendFormat:@"[%s]", RCTLogLevels[level - 1]]; + } + if (thread) { + NSString *threadName = thread.name; + if (threadName.length == 0) { +#if DEBUG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + threadName = @(dispatch_queue_get_label(dispatch_get_current_queue())); +#pragma clang diagnostic pop +#else + threadName = [NSString stringWithFormat:@"%p", thread]; +#endif + } + [log appendFormat:@"[tid:%@]", threadName]; + } + if (fileName) { + fileName = [fileName lastPathComponent]; + if (lineNumber) { + [log appendFormat:@"[%@:%@]", fileName, lineNumber]; } else { - if (valid && [formatParts[i] length] && [formatParts[i] characterAtIndex:0] == '@') { - id obj = va_arg(args, id); - [objects addObject:obj ?: @"null"]; - [objects addObject:[formatParts[i] substringFromIndex:1]]; // remove formatting char - } else { - // We could determine the type (double, int?) of the va_arg by parsing the formatPart, but for now we just bail. - valid = NO; - [objects addObject:[NSString stringWithFormat:@"unknown object for %%%@", formatParts[i]]]; - } + [log appendFormat:@"[%@]", fileName]; } } - va_end(args); - va_start(args, format); - NSString *strOut = [preamble stringByAppendingString:[[NSString alloc] initWithFormat:format arguments:args]]; - va_end(args); - NSMutableArray *objectsOut = [NSMutableArray arrayWithObject:strOut]; - [objectsOut addObjectsFromArray:objects]; - return objectsOut; + if (message) { + [log appendString:@" "]; + [log appendString:message]; + } + return log; +} + +RCTLogFunction RCTSimpleLogFunction(void (^logFunction)(RCTLogLevel level, NSString *message)) +{ + return ^(RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message) { + + logFunction(level, RCTFormatLog( + [NSDate date], [NSThread currentThread], level, fileName, lineNumber, message + )); + }; +} + +void _RCTLogFormat(RCTLogLevel level, const char *fileName, int lineNumber, NSString *format, ...) +{ + if (RCTCurrentLogFunction && level >= RCTCurrentLogThreshold) { + + // Get message + va_list args; + va_start(args, format); + __block NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + + // Add prefix + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + NSArray *prefixStack = threadDictionary[RCTLogPrefixStack]; + NSString *prefix = [prefixStack lastObject]; + if (prefix) { + message = [prefix stringByAppendingString:message]; + } + + // Call log function + RCTCurrentLogFunction( + level, fileName ? @(fileName) : nil, (lineNumber >= 0) ? @(lineNumber) : nil, message + ); + +#if DEBUG + + // Log to red box + if (level >= RCTLOG_REDBOX_LEVEL) { + [[RCTRedBox sharedInstance] showErrorMessage:message]; + } + + // Log to JS executor + if ([RCTBridge hasValidJSExecutor]) { + [RCTBridge logMessage:message level:level ? @(RCTLogLevels[level - 1]) : @"info"]; + } + +#endif + + } +} + +#pragma mark - Deprecated + +void RCTInjectLogFunction(void (^logFunction)(NSString *msg)) +{ + RCTSetLogFunction(^(RCTLogLevel level, + NSString *fileName, + NSNumber *lineNumber, + NSString *message) { + + if (level > RCTLogLevelError) { + + // Use custom log function + NSString *loc = fileName ? [NSString stringWithFormat:@"[%@:%@] ", fileName, lineNumber] : @""; + logFunction([loc stringByAppendingString:message]); + + } else if (RCTDefaultLogFunction && level >= RCTCurrentLogThreshold) { + + // Use default logger + RCTDefaultLogFunction(level, fileName, lineNumber, message); + } + }); } diff --git a/React/Base/RCTRootView.h b/React/Base/RCTRootView.h index b9e91b7a0..f85bb7ecb 100644 --- a/React/Base/RCTRootView.h +++ b/React/Base/RCTRootView.h @@ -17,6 +17,9 @@ extern NSString *const RCTReloadViewsNotification; @interface RCTRootView : UIView +/** + * - Designated initializer - + */ - (instancetype)initWithBridge:(RCTBridge *)bridge moduleName:(NSString *)moduleName NS_DESIGNATED_INITIALIZER; @@ -39,6 +42,10 @@ extern NSString *const RCTReloadViewsNotification; */ @property (nonatomic, copy, readonly) NSString *moduleName; +/** + * The bridge used by the root view. Bridges can be shared between multiple + * root views, so you can use this property to initialize another RCTRootView. + */ @property (nonatomic, strong, readonly) RCTBridge *bridge; /** diff --git a/React/Base/RCTRootView.m b/React/Base/RCTRootView.m index 9d913c048..6c15e509c 100644 --- a/React/Base/RCTRootView.m +++ b/React/Base/RCTRootView.m @@ -17,7 +17,6 @@ #import "RCTEventDispatcher.h" #import "RCTKeyCommands.h" #import "RCTLog.h" -#import "RCTRedBox.h" #import "RCTSourceCode.h" #import "RCTTouchHandler.h" #import "RCTUIManager.h" diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index e2b289f80..6c424ec7f 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -38,23 +38,18 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, JSObjectRef object, if (!string) { return JSValueMakeUndefined(context); } - - NSString *str = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string); - NSError *error = nil; + NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, string); + JSStringRelease(string); NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: @"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).includeRequire.runModule.bundle(:[0-9]+:[0-9]+)" options:NSRegularExpressionCaseInsensitive - error:&error]; - NSString *modifiedString = [regex stringByReplacingMatchesInString:str options:0 range:NSMakeRange(0, [str length]) withTemplate:@"[$4$5] \t$2"]; + error:NULL]; + message = [regex stringByReplacingMatchesInString:message + options:0 + range:(NSRange){0, message.length} + withTemplate:@"[$4$5] \t$2"]; - modifiedString = [@"RCTJSLog> " stringByAppendingString:modifiedString]; -#if TARGET_IPHONE_SIMULATOR - fprintf(stderr, "%s\n", [modifiedString UTF8String]); // don't print timestamps and other junk -#else - // Print normal errors with timestamps to files when not in simulator. - RCTLogObjects(@[modifiedString], @"log"); -#endif - JSStringRelease(string); + _RCTLogFormat(0, NULL, -1, @"%@", message); } return JSValueMakeUndefined(context); diff --git a/React/Modules/RCTAlertManager.m b/React/Modules/RCTAlertManager.m index 11da8e7e7..bda7c357e 100644 --- a/React/Modules/RCTAlertManager.m +++ b/React/Modules/RCTAlertManager.m @@ -9,6 +9,7 @@ #import "RCTAlertManager.h" +#import "RCTAssert.h" #import "RCTLog.h" @interface RCTAlertManager() diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 185982fdc..c21adc1d0 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -197,6 +197,11 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio @synthesize bridge = _bridge; +/** + * Declared in RCTBridge. + */ +extern NSString *RCTBridgeModuleNameForClass(Class cls); + /** * This function derives the view name automatically * from the module name. @@ -334,7 +339,7 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName) dispatch_async(_bridge.shadowQueue, ^{ RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag]; - RCTAssert(rootShadowView != nil, @"Could not locate root view with tag %@", reactTag); + RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag); rootShadowView.frame = frame; [rootShadowView updateLayout]; @@ -672,7 +677,7 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName) } } -static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, RCTViewManager *manager) +static BOOL RCTCallPropertySetter(NSString *key, SEL setter, id value, id view, id defaultView, RCTViewManager *manager) { // TODO: cache respondsToSelector tests if ([manager respondsToSelector:setter]) { @@ -681,7 +686,25 @@ static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, value = nil; } - ((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView); + void (^block)() = ^{ + ((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView); + }; + +#if DEBUG + + NSString *viewName = RCTViewNameForModuleName(RCTBridgeModuleNameForClass([manager class])); + NSString *logPrefix = [NSString stringWithFormat: + @"Error setting property '%@' of %@ with tag #%@: ", + key, viewName, [view reactTag]]; + + RCTPerformBlockWithLogPrefix(block, logPrefix); + +#else + + block(); + +#endif + return YES; } return NO; @@ -693,7 +716,7 @@ static void RCTSetViewProps(NSDictionary *props, UIView *view, [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forView:withDefaultView:", key]); - RCTCallPropertySetter(setter, obj, view, defaultView, manager); + RCTCallPropertySetter(key, setter, obj, view, defaultView, manager); }]; } @@ -704,7 +727,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forShadowView:withDefaultView:", key]); - RCTCallPropertySetter(setter, obj, shadowView, defaultView, manager); + RCTCallPropertySetter(key, setter, obj, shadowView, defaultView, manager); }]; @@ -727,44 +750,47 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView // Register manager _viewManagerRegistry[reactTag] = manager; - // Generate default view, used for resetting default props - if (!_defaultShadowViews[viewName]) { - _defaultShadowViews[viewName] = [manager shadowView]; - } - RCTShadowView *shadowView = [manager shadowView]; - shadowView.viewName = viewName; - shadowView.reactTag = reactTag; - RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], manager); - _shadowViewRegistry[shadowView.reactTag] = shadowView; + if (shadowView) { + + // Generate default view, used for resetting default props + if (!_defaultShadowViews[viewName]) { + _defaultShadowViews[viewName] = [manager shadowView]; + } + + // Set properties + shadowView.viewName = viewName; + shadowView.reactTag = reactTag; + RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], manager); + } + _shadowViewRegistry[reactTag] = shadowView; [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ RCTCAssertMainThread(); - // Generate default view, used for resetting default props - if (!uiManager->_defaultViews[viewName]) { - // Note the default is setup after the props are read for the first time ever - // for this className - this is ok because we only use the default for restoring - // defaults, which never happens on first creation. - uiManager->_defaultViews[viewName] = [manager view]; - } - UIView *view = [manager view]; if (view) { - // Set required properties - view.reactTag = reactTag; - view.multipleTouchEnabled = YES; - view.userInteractionEnabled = YES; // required for touch handling - view.layer.allowsGroupOpacity = YES; // required for touch handling + // Generate default view, used for resetting default props + if (!uiManager->_defaultViews[viewName]) { + // Note the default is setup after the props are read for the first time ever + // for this className - this is ok because we only use the default for restoring + // defaults, which never happens on first creation. + uiManager->_defaultViews[viewName] = [manager view]; + } - // Set custom properties + // Set properties + view.reactTag = reactTag; + if ([view isKindOfClass:[UIView class]]) { + view.multipleTouchEnabled = YES; + view.userInteractionEnabled = YES; // required for touch handling + view.layer.allowsGroupOpacity = YES; // required for touch handling + } RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], manager); } - viewRegistry[view.reactTag] = view; + viewRegistry[reactTag] = view; }]; } - // TODO: remove viewName param as it isn't needed - (void)updateView:(NSNumber *)reactTag viewName:(__unused NSString *)_ props:(NSDictionary *)props { @@ -875,7 +901,7 @@ static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *view = viewRegistry[reactTag]; if (!view) { - RCTLogError(@"measure cannot find view with tag %@", reactTag); + RCTLogError(@"measure cannot find view with tag #%@", reactTag); return; } CGRect frame = view.frame; @@ -1039,7 +1065,7 @@ static void RCTMeasureLayout(RCTShadowView *view, uiManager.mainScrollView = (id)rkObject; ((id)rkObject).nativeMainScrollDelegate = uiManager.nativeMainScrollDelegate; } else { - RCTCAssert(NO, @"Tag %@ does not conform to RCTScrollableProtocol", reactTag); + RCTCAssert(NO, @"Tag #%@ does not conform to RCTScrollableProtocol", reactTag); } } else { uiManager.mainScrollView = nil; @@ -1056,7 +1082,7 @@ static void RCTMeasureLayout(RCTShadowView *view, if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue]) animated:YES]; } else { - RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag); + RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); } }]; } @@ -1070,7 +1096,7 @@ static void RCTMeasureLayout(RCTShadowView *view, if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue]) animated:NO]; } else { - RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag); + RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); } }]; } @@ -1084,7 +1110,7 @@ static void RCTMeasureLayout(RCTShadowView *view, if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view zoomToRect:[RCTConvert CGRect:rectDict] animated:YES]; } else { - RCTLogError(@"tried to zoomToRect: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag); + RCTLogError(@"tried to zoomToRect: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); } }]; } diff --git a/React/Views/RCTScrollViewManager.m b/React/Views/RCTScrollViewManager.m index 9c0c56f04..066d28adc 100644 --- a/React/Views/RCTScrollViewManager.m +++ b/React/Views/RCTScrollViewManager.m @@ -72,7 +72,7 @@ RCT_DEPRECATED_VIEW_PROPERTY(throttleScrollCallbackMS, scrollEventThrottle) UIView *view = viewRegistry[reactTag]; if (!view) { - RCTLogError(@"Cannot find view with tag %@", reactTag); + RCTLogError(@"Cannot find view with tag #%@", reactTag); return; } diff --git a/package.json b/package.json index eef77648d..300537564 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "Examples/SampleApp", "Libraries", "packager", - "local-cli", "cli.js", "init.sh", "LICENSE", @@ -42,25 +41,26 @@ "react-native-start": "packager/packager.sh" }, "dependencies": { + "absolute-path": "0.0.0", + "bluebird": "^2.9.21", + "chalk": "^1.0.0", "connect": "2.8.3", + "debug": "~2.1.0", + "joi": "~5.1.0", "jstransform": "10.1.0", + "module-deps": "3.5.6", + "optimist": "0.6.1", "react-timer-mixin": "^0.13.1", "react-tools": "0.13.1", "rebound": "^0.0.12", + "sane": "1.0.1", "source-map": "0.1.31", "stacktrace-parser": "0.1.1", - "absolute-path": "0.0.0", - "debug": "~2.1.0", - "joi": "~5.1.0", - "module-deps": "3.5.6", - "optimist": "0.6.1", - "sane": "1.0.1", "uglify-js": "~2.4.16", "underscore": "1.7.0", "worker-farm": "1.1.0", - "yargs": "1.3.2", "ws": "0.4.31", - "bluebird": "^2.9.21" + "yargs": "1.3.2" }, "devDependencies": { "jest-cli": "0.2.1", diff --git a/packager/packager.js b/packager/packager.js index e2a32f2ca..55004b7cf 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -23,6 +23,7 @@ if (!fs.existsSync(path.resolve(__dirname, '..', 'node_modules'))) { process.exit(); } +var chalk = require('chalk'); var connect = require('connect'); var ReactPackager = require('./react-packager'); var blacklist = require('./blacklist.js'); @@ -88,13 +89,35 @@ console.log('\n' + ' ===============================================================\n' ); -console.log('Looking for JS files in\n ', options.projectRoots.join('\n ')); +console.log( + 'Looking for JS files in\n ', + chalk.dim(options.projectRoots.join('\n ')), + '\n' +); process.on('uncaughtException', function(e) { - console.error(e); - console.error(e.stack); - console.error('\n >>> ERROR: could not create packager - please shut down ' + - 'any existing instances that are already running.\n\n'); + if (e.code === 'EADDRINUSE') { + console.log( + chalk.bgRed.bold(' ERROR '), + chalk.red('Packager can\'t listen on port', chalk.bold(options.port)) + ); + console.log('Most likely another process is already using this port'); + console.log('Run the following command to find out which process:'); + console.log('\n ', chalk.bold('lsof -n -i4TCP:' + options.port), '\n'); + console.log('You can either shut down the other process:'); + console.log('\n ', chalk.bold('kill -9 '), '\n'); + console.log('or run packager on different port.'); + } else { + console.log(chalk.bgRed.bold(' ERROR '), chalk.red(e.message)); + var errorAttributes = JSON.stringify(e); + if (errorAttributes !== '{}') { + console.error(chalk.red(errorAttributes)); + } + console.error(chalk.red(e.stack)); + } + console.log('\nSee', chalk.underline('http://facebook.github.io/react-native/docs/troubleshooting.html')); + console.log('for common problems and solutions.'); + process.exit(1); }); var server = runServer(options, function() { @@ -151,13 +174,13 @@ function getDevToolsLauncher(options) { } // A status page so the React/project.pbxproj build script -// can verify that packager is running on 8081 and not +// can verify that packager is running on 8081 and not // another program / service. function statusPageMiddleware(req, res, next) { if (req.url === '/status') { res.end('packager-status:running'); } else { - next(); + next(); } }