From 7bf157c92c368e7b87a34ff681e2c545f171f742 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 26 Aug 2015 09:28:14 -0700 Subject: [PATCH 01/25] Fix assertion when modules are accessed early on in bridge startup --- React/Base/RCTBatchedBridge.m | 7 ++++--- React/Base/RCTLog.m | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index ad0cc39ee..5ef4c6502 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -407,9 +407,10 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithBundleURL:(__unused NSURL *)bundleUR - (NSDictionary *)modules { - RCTAssert(!self.isValid || _modulesByName != nil, @"Bridge modules have not yet been initialized. " - "You may be trying to access a module too early in the startup procedure."); - + if (RCT_DEBUG && self.isValid && _modulesByName == nil) { + RCTLogError(@"Bridge modules have not yet been initialized. You may be " + "trying to access a module too early in the startup procedure."); + } return _modulesByName; } diff --git a/React/Base/RCTLog.m b/React/Base/RCTLog.m index a8679da9e..ebbf6a1b8 100644 --- a/React/Base/RCTLog.m +++ b/React/Base/RCTLog.m @@ -226,7 +226,11 @@ void _RCTLogFormat( } } }]; - [[RCTBridge currentBridge].redBox showErrorMessage:message withStack:stack]; + dispatch_async(dispatch_get_main_queue(), ^{ + // red box is thread safe, but by deferring to main queue we avoid a startup + // race condition that causes the module to be accessed before it has loaded + [[RCTBridge currentBridge].redBox showErrorMessage:message withStack:stack]; + }); } // Log to JS executor From a9115bcbb34dd851b46317043576f1601fe2416d Mon Sep 17 00:00:00 2001 From: Andy Street Date: Wed, 26 Aug 2015 15:59:25 -0700 Subject: [PATCH 02/25] [ReactNative][SyncDiff] Add parameter to allow turning on/off off screen rendering for alpha compositing --- Libraries/Components/View/View.js | 23 +++++++++++++++++++ .../ReactNative/ReactNativeViewAttributes.js | 1 + 2 files changed, 24 insertions(+) diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 8594cfe8b..fdb2ff6ed 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -274,6 +274,29 @@ var View = React.createClass({ * @platform android */ collapsable: PropTypes.bool, + + /** + * Whether this view needs to rendered offscreen and composited with an alpha + * in order to preserve 100% correct colors and blending behavior. The default + * (false) falls back to drawing the component and its children with an alpha + * applied to the paint used to draw each element instead of rendering the full + * component offscreen and compositing it back with an alpha value. This default + * may be noticeable and undesired in the case where the View you are setting + * an opacity on has multiple overlapping elements (e.g. multiple overlapping + * Views, or text and a background). + * + * Rendering offscreen to preserve correct alpha behavior is extremely + * expensive and hard to debug for non-native developers, which is why it is + * not turned on by default. If you do need to enable this property for an + * animation, consider combining it with renderToHardwareTextureAndroid if the + * view **contents** are static (i.e. it doesn't need to be redrawn each frame). + * If that property is enabled, this View will be rendered off-screen once, + * saved in a hardware texture, and then composited onto the screen with an alpha + * each frame without having to switch rendering targets on the GPU. + * + * @platform android + */ + needsOffscreenAlphaCompositing: PropTypes.bool, }, render: function() { diff --git a/Libraries/ReactNative/ReactNativeViewAttributes.js b/Libraries/ReactNative/ReactNativeViewAttributes.js index 028284db5..0447c78ab 100644 --- a/Libraries/ReactNative/ReactNativeViewAttributes.js +++ b/Libraries/ReactNative/ReactNativeViewAttributes.js @@ -30,6 +30,7 @@ ReactNativeViewAttributes.UIView = { onAccessibilityTap: true, onMagicTap: true, collapsable: true, + needsOffscreenAlphaCompositing: true, }; ReactNativeViewAttributes.RCTView = merge( From ac5c1e9cc48dd71cce80ba21278fb6e13b03e02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Wed, 26 Aug 2015 16:30:54 -0700 Subject: [PATCH 03/25] [react-packager] Make sure server is listening on socket --- .../__tests__/SocketInterface-test.js | 12 ++ .../src/SocketInterface/index.js | 115 ++++++++++-------- 2 files changed, 75 insertions(+), 52 deletions(-) diff --git a/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js b/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js index c03f39992..d43d35861 100644 --- a/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js +++ b/packager/react-packager/src/SocketInterface/__tests__/SocketInterface-test.js @@ -26,6 +26,17 @@ describe('SocketInterface', () => { pit('creates socket path by hashing options', () => { const fs = require('fs'); fs.existsSync = jest.genMockFn().mockImpl(() => true); + fs.unlinkSync = jest.genMockFn(); + let callback; + + require('child_process').spawn.mockImpl(() => ({ + on: (event, cb) => callback = cb, + send: (message) => { + setImmediate(() => callback({ type: 'createdServer' })); + }, + unref: () => undefined, + disconnect: () => undefined, + })); // Check that given two equivelant server options, we end up with the same // socket path. @@ -49,6 +60,7 @@ describe('SocketInterface', () => { pit('should fork a server', () => { const fs = require('fs'); fs.existsSync = jest.genMockFn().mockImpl(() => false); + fs.unlinkSync = jest.genMockFn(); let sockPath; let callback; diff --git a/packager/react-packager/src/SocketInterface/index.js b/packager/react-packager/src/SocketInterface/index.js index 470d0fc24..36d8bc66c 100644 --- a/packager/react-packager/src/SocketInterface/index.js +++ b/packager/react-packager/src/SocketInterface/index.js @@ -14,6 +14,7 @@ const SocketServer = require('./SocketServer'); const _ = require('underscore'); const crypto = require('crypto'); const fs = require('fs'); +const net = require('net'); const path = require('path'); const tmpdir = require('os').tmpdir(); const {spawn} = require('child_process'); @@ -38,66 +39,76 @@ const SocketInterface = { ); if (fs.existsSync(sockPath)) { - resolve(SocketClient.create(sockPath)); - return; - } - - const logPath = path.join(tmpdir, 'react-packager.log'); - - const timeout = setTimeout( - () => reject( - new Error( - 'Took too long to start server. Server logs: \n' + - fs.readFileSync(logPath, 'utf8') - ) - ), - CREATE_SERVER_TIMEOUT, - ); - - const log = fs.openSync(logPath, 'a'); - - // Enable server debugging by default since it's going to a log file. - const env = _.clone(process.env); - env.DEBUG = 'ReactPackager:SocketServer'; - - // We have to go through the main entry point to make sure - // we go through the babel require hook. - const child = spawn( - process.execPath, - [path.join(__dirname, '..', '..', 'index.js')], - { - detached: true, - env: env, - stdio: ['ipc', log, log] - } - ); - - child.unref(); - - child.on('message', m => { - if (m && m.type && m.type === 'createdServer') { - clearTimeout(timeout); - child.disconnect(); + var sock = net.connect(sockPath); + sock.on('connect', () => { + sock.end(); resolve(SocketClient.create(sockPath)); - } - }); - - - if (options.blacklistRE) { - options.blacklistRE = { source: options.blacklistRE.source }; + }); + sock.on('error', (e) => { + fs.unlinkSync(sockPath); + createServer(resolve, reject, options, sockPath); + }); + } else { + createServer(resolve, reject, options, sockPath); } - - child.send({ - type: 'createSocketServer', - data: { sockPath, options } - }); }); }, listenOnServerMessages() { return SocketServer.listenOnServerIPCMessages(); } - }; +function createServer(resolve, reject, options, sockPath) { + const logPath = path.join(tmpdir, 'react-packager.log'); + + const timeout = setTimeout( + () => reject( + new Error( + 'Took too long to start server. Server logs: \n' + + fs.readFileSync(logPath, 'utf8') + ) + ), + CREATE_SERVER_TIMEOUT, + ); + + const log = fs.openSync(logPath, 'a'); + + // Enable server debugging by default since it's going to a log file. + const env = _.clone(process.env); + env.DEBUG = 'ReactPackager:SocketServer'; + + // We have to go through the main entry point to make sure + // we go through the babel require hook. + const child = spawn( + process.execPath, + [path.join(__dirname, '..', '..', 'index.js')], + { + detached: true, + env: env, + stdio: ['ipc', log, log] + } + ); + + child.unref(); + + child.on('message', m => { + if (m && m.type && m.type === 'createdServer') { + clearTimeout(timeout); + child.disconnect(); + + resolve(SocketClient.create(sockPath)); + } + }); + + if (options.blacklistRE) { + options.blacklistRE = { source: options.blacklistRE.source }; + } + + child.send({ + type: 'createSocketServer', + data: { sockPath, options } + }); +} + module.exports = SocketInterface; From 5b25f208c5ee025c7e727b880db9ad84ff9ea03c Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 26 Aug 2015 18:30:46 -0700 Subject: [PATCH 04/25] [ReactNative] Teach flow how to deal with images --- .flowconfig | 4 +- Examples/SampleApp/_flowconfig | 3 ++ Examples/UIExplorer/ImageMocks.js | 56 ---------------------------- Libraries/Image/AssetRegistry.js | 20 ++++++++-- Libraries/Image/GlobalImageStub.js | 25 +++++++++++++ Libraries/Image/RelativeImageStub.js | 29 ++++++++++++++ 6 files changed, 77 insertions(+), 60 deletions(-) delete mode 100644 Examples/UIExplorer/ImageMocks.js create mode 100644 Libraries/Image/GlobalImageStub.js create mode 100644 Libraries/Image/RelativeImageStub.js diff --git a/.flowconfig b/.flowconfig index a5a80524a..65df8bc75 100644 --- a/.flowconfig +++ b/.flowconfig @@ -30,13 +30,15 @@ [libs] Libraries/react-native/react-native-interface.js -Examples/UIExplorer/ImageMocks.js [options] module.system=haste munge_underscores=true +module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' +module.name_mapper='^[./a-zA-Z0-9$_-]+\.png$' -> 'RelativeImageStub' + suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe diff --git a/Examples/SampleApp/_flowconfig b/Examples/SampleApp/_flowconfig index 438e495d4..9ca5deb8f 100644 --- a/Examples/SampleApp/_flowconfig +++ b/Examples/SampleApp/_flowconfig @@ -34,6 +34,9 @@ module.system=haste munge_underscores=true +module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' +module.name_mapper='^[./a-zA-Z0-9$_-]+\.png$' -> 'RelativeImageStub' + suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FixMe diff --git a/Examples/UIExplorer/ImageMocks.js b/Examples/UIExplorer/ImageMocks.js deleted file mode 100644 index 670346373..000000000 --- a/Examples/UIExplorer/ImageMocks.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * The examples provided by Facebook are for non-commercial testing and - * evaluation purposes only. - * - * Facebook reserves all rights not expressly granted. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL - * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * @flow - */ -'use strict'; - -/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ -declare module 'image!story-background' { - declare var uri: string; - declare var isStatic: boolean; -} - -/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ -declare module 'image!uie_comment_highlighted' { - declare var uri: string; - declare var isStatic: boolean; -} - -/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ -declare module 'image!uie_comment_normal' { - declare var uri: string; - declare var isStatic: boolean; -} - -/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ -declare module 'image!uie_thumb_normal' { - declare var uri: string; - declare var isStatic: boolean; -} - -/* $FlowIssue #7387208 - There's a flow bug preventing this type from flowing - * into a proptype shape */ -declare module 'image!uie_thumb_selected' { - declare var uri: string; - declare var isStatic: boolean; -} - -declare module 'image!NavBarButtonPlus' { - declare var uri: string; - declare var isStatic: boolean; -} diff --git a/Libraries/Image/AssetRegistry.js b/Libraries/Image/AssetRegistry.js index df4173e78..440f16587 100644 --- a/Libraries/Image/AssetRegistry.js +++ b/Libraries/Image/AssetRegistry.js @@ -2,18 +2,32 @@ * Copyright 2004-present Facebook. All Rights Reserved. * * @providesModule AssetRegistry + * @flow */ 'use strict'; -var assets = []; +export type PackagerAsset = { + __packager_asset: boolean, + fileSystemLocation: string, + httpServerLocation: string, + width: number, + height: number, + scales: Array, + hash: string, + name: string, + type: string, +}; -function registerAsset(asset) { + +var assets: Array = []; + +function registerAsset(asset: PackagerAsset): number { // `push` returns new array length, so the first asset will // get id 1 (not 0) to make the value truthy return assets.push(asset); } -function getAssetByID(assetId) { +function getAssetByID(assetId: number): PackagerAsset { return assets[assetId - 1]; } diff --git a/Libraries/Image/GlobalImageStub.js b/Libraries/Image/GlobalImageStub.js new file mode 100644 index 000000000..5f1eb5194 --- /dev/null +++ b/Libraries/Image/GlobalImageStub.js @@ -0,0 +1,25 @@ +/** + * 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 GlobalImageStub + * @flow + */ +'use strict'; + +// This is a stub for flow to make it understand require('image!icon') +// See packager/react-packager/src/Bundler/index.js + +module.exports = { + __packager_asset: true, + isStatic: true, + path: '/full/path/to/something.png', + uri: 'icon', + width: 100, + height: 100, + deprecated: true, +}; diff --git a/Libraries/Image/RelativeImageStub.js b/Libraries/Image/RelativeImageStub.js new file mode 100644 index 000000000..40c30d3c8 --- /dev/null +++ b/Libraries/Image/RelativeImageStub.js @@ -0,0 +1,29 @@ +/** + * 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 RelativeImageStub + * @flow + */ +'use strict'; + +// This is a stub for flow to make it understand require('./icon.png') +// See packager/react-packager/src/Bundler/index.js + +var AssetRegistry = require('AssetRegistry'); + +module.exports = AssetRegistry.registerAsset({ + __packager_asset: true, + fileSystemLocation: '/full/path/to/directory', + httpServerLocation: '/assets/full/path/to/directory', + width: 100, + height: 100, + scales: [1, 2, 3], + hash: 'nonsense', + name: 'icon', + type: 'png', +}); From c7b1509615412bf781ed3c8d56b168da49720def Mon Sep 17 00:00:00 2001 From: Alex Kotliarskyi Date: Wed, 26 Aug 2015 19:57:21 -0700 Subject: [PATCH 05/25] [react-native] Fix tests and re-enable CI --- .../Initialization/InitializeJavaScriptAppEngine.js | 5 ++++- packager/react-packager/src/JSTransformer/index.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 54b954dc0..ce1e56cfa 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -138,7 +138,10 @@ function setupProfile() { function setUpProcessEnv() { GLOBAL.process = GLOBAL.process || {}; - GLOBAL.process.env = {NODE_ENV: __DEV__ ? 'development' : 'production'}; + GLOBAL.process.env = GLOBAL.process.env || {}; + if (!GLOBAL.process.env.NODE_ENV) { + GLOBAL.process.env.NODE_ENV = __DEV__ ? 'development' : 'production'; + } } setUpRedBoxErrorHandler(); diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js index 47019846d..93dd5d01d 100644 --- a/packager/react-packager/src/JSTransformer/index.js +++ b/packager/react-packager/src/JSTransformer/index.js @@ -23,7 +23,7 @@ const readFile = Promise.denodeify(fs.readFile); const MAX_CALLS_PER_WORKER = 600; // Worker will timeout if one of the callers timeout. -const DEFAULT_MAX_CALL_TIME = 60000; +const DEFAULT_MAX_CALL_TIME = 120000; const validateOpts = declareOpts({ projectRoots: { From 1598cc69799fbdbd66fe15bbc7615986a90688ed Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Wed, 26 Aug 2015 23:18:41 -0700 Subject: [PATCH 06/25] [react-packager] bump create server timeout to 60s --- packager/react-packager/src/SocketInterface/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packager/react-packager/src/SocketInterface/index.js b/packager/react-packager/src/SocketInterface/index.js index 36d8bc66c..aca355407 100644 --- a/packager/react-packager/src/SocketInterface/index.js +++ b/packager/react-packager/src/SocketInterface/index.js @@ -19,7 +19,7 @@ const path = require('path'); const tmpdir = require('os').tmpdir(); const {spawn} = require('child_process'); -const CREATE_SERVER_TIMEOUT = 30000; +const CREATE_SERVER_TIMEOUT = 60000; const SocketInterface = { getOrCreateSocketFor(options) { From 34b5aa2a57ca66d3f879c0fe9389da1d0fc7080d Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Thu, 27 Aug 2015 01:52:56 -0700 Subject: [PATCH 07/25] [react-packager] Fix races Summary: A few potential races to fix: 1. Multiple clients maybe racing to delete a zombie socket 2. Servers who should die because other servers are already listening are taking the socket with them (move `process.on('exit'` code to after the server is listening 3. Servers which are redundant should immediatly die --- .../react-packager/src/SocketInterface/SocketServer.js | 10 +++++++--- packager/react-packager/src/SocketInterface/index.js | 8 +++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packager/react-packager/src/SocketInterface/SocketServer.js b/packager/react-packager/src/SocketInterface/SocketServer.js index f6faf8368..2e91aaba4 100644 --- a/packager/react-packager/src/SocketInterface/SocketServer.js +++ b/packager/react-packager/src/SocketInterface/SocketServer.js @@ -32,6 +32,7 @@ class SocketServer { options ); resolve(this); + process.on('exit', () => fs.unlinkSync(sockPath)); }); }); this._server.on('connection', (sock) => this._handleConnection(sock)); @@ -41,8 +42,6 @@ class SocketServer { this._packagerServer = new Server(options); this._jobs = 0; this._dieEventually(); - - process.on('exit', () => fs.unlinkSync(sockPath)); } onReady() { @@ -138,12 +137,17 @@ class SocketServer { process.send({ type: 'createdServer' }); }, error => { - debug('error creating server', error.code); if (error.code === 'EADDRINUSE') { // Server already listening, this may happen if multiple // clients where started in quick succussion (buck). process.send({ type: 'createdServer' }); + + // Kill this server because some other server with the same + // config and socket already started. + debug('server already started'); + setImmediate(() => process.exit()); } else { + debug('error creating server', error.code); throw error; } } diff --git a/packager/react-packager/src/SocketInterface/index.js b/packager/react-packager/src/SocketInterface/index.js index aca355407..19b39bf09 100644 --- a/packager/react-packager/src/SocketInterface/index.js +++ b/packager/react-packager/src/SocketInterface/index.js @@ -13,6 +13,7 @@ const SocketClient = require('./SocketClient'); const SocketServer = require('./SocketServer'); const _ = require('underscore'); const crypto = require('crypto'); +const debug = require('debug')('ReactPackager:SocketInterface'); const fs = require('fs'); const net = require('net'); const path = require('path'); @@ -45,7 +46,12 @@ const SocketInterface = { resolve(SocketClient.create(sockPath)); }); sock.on('error', (e) => { - fs.unlinkSync(sockPath); + try { + debug('deleting socket for not responding', sockPath); + fs.unlinkSync(sockPath); + } catch (err) { + // Another client might have deleted it first. + } createServer(resolve, reject, options, sockPath); }); } else { From 8ec1a180df0bf8707570eb2340ff8b4d6a2425d5 Mon Sep 17 00:00:00 2001 From: Andrei Coman Date: Thu, 27 Aug 2015 06:32:35 -0700 Subject: [PATCH 08/25] [ReactNative][SyncDiff] Add more items to react-native target --- Libraries/react-native/react-native.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 7d005069f..f0b492980 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -21,6 +21,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { // Components ActivityIndicatorIOS: require('ActivityIndicatorIOS'), DatePickerIOS: require('DatePickerIOS'), + DrawerLayoutAndroid: require('DrawerLayoutAndroid'), Image: require('Image'), ListView: require('ListView'), MapView: require('MapView'), @@ -28,15 +29,19 @@ var ReactNative = Object.assign(Object.create(require('React')), { Navigator: require('Navigator'), NavigatorIOS: require('NavigatorIOS'), PickerIOS: require('PickerIOS'), + ProgressBarAndroid: require('ProgressBarAndroid'), ProgressViewIOS: require('ProgressViewIOS'), ScrollView: require('ScrollView'), SegmentedControlIOS: require('SegmentedControlIOS'), SliderIOS: require('SliderIOS'), + SwitchAndroid: require('SwitchAndroid'), SwitchIOS: require('SwitchIOS'), TabBarIOS: require('TabBarIOS'), Text: require('Text'), TextInput: require('TextInput'), + ToolbarAndroid: require('ToolbarAndroid'), TouchableHighlight: require('TouchableHighlight'), + TouchableNativeFeedback: require('TouchableNativeFeedback'), TouchableOpacity: require('TouchableOpacity'), TouchableWithoutFeedback: require('TouchableWithoutFeedback'), View: require('View'), @@ -50,6 +55,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { AppRegistry: require('AppRegistry'), AppStateIOS: require('AppStateIOS'), AsyncStorage: require('AsyncStorage'), + BackAndroid: require('BackAndroid'), CameraRoll: require('CameraRoll'), Dimensions: require('Dimensions'), Easing: require('Easing'), From 5dbde93ec6246c161b2b49d22bb20bbbc0dc12f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Thu, 27 Aug 2015 09:33:59 -0700 Subject: [PATCH 09/25] [react-packager] `System.import` transform --- .../babel-plugin-system-import-test.js | 71 +++++++++++++++++++ .../babel-plugin-system-import/index.js | 65 +++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 packager/react-packager/src/transforms/babel-plugin-system-import/__tests__/babel-plugin-system-import-test.js create mode 100644 packager/react-packager/src/transforms/babel-plugin-system-import/index.js diff --git a/packager/react-packager/src/transforms/babel-plugin-system-import/__tests__/babel-plugin-system-import-test.js b/packager/react-packager/src/transforms/babel-plugin-system-import/__tests__/babel-plugin-system-import-test.js new file mode 100644 index 000000000..ddf8f2403 --- /dev/null +++ b/packager/react-packager/src/transforms/babel-plugin-system-import/__tests__/babel-plugin-system-import-test.js @@ -0,0 +1,71 @@ +/** + * Copyright 2004-present Facebook. All Rights Reserved. + * + * @emails oncall+jsinfra + */ + +'use strict'; + +jest.autoMockOff(); +jest.mock('../../../BundlesLayout'); + +const babel = require('babel-core'); +const BundlesLayout = require('../../../BundlesLayout'); + +const testData = { + isolated: { + input: 'System.import("moduleA");', + output: 'loadBundles(["bundle.0"]);' + }, + single: { + input: 'System.import("moduleA").then(function (bundleA) {});', + output: 'loadBundles(["bundle.0"]).then(function (bundleA) {});' + }, + multiple: { + input: [ + 'Promise.all([', + 'System.import("moduleA"), System.import("moduleB"),', + ']).then(function (bundlesA, bundlesB) {});', + ].join('\n'), + output: [ + 'Promise.all([', + 'loadBundles(["bundle.0"]), loadBundles(["bundle.1"])', + ']).then(function (bundlesA, bundlesB) {});', + ].join(''), + }, +}; + +describe('System.import', () => { + let layout = new BundlesLayout(); + BundlesLayout.prototype.getBundleIDForModule.mockImpl(module => { + switch (module) { + case 'moduleA': return 'bundle.0'; + case 'moduleB': return 'bundle.1'; + } + }); + + function transform(source) { + return babel.transform(source, { + plugins: [require('../')], + blacklist: ['strict'], + extra: { bundlesLayout: layout }, + }).code; + } + + function test(data) { + // transform and remove new lines + expect(transform(data.input).replace(/(\r\n|\n|\r)/gm,'')).toEqual(data.output); + } + + it('should transform isolated `System.import`', () => { + test(testData.isolated); + }); + + it('should transform single `System.import`', () => { + test(testData.single); + }); + + it('should transform multiple `System.import`s', () => { + test(testData.multiple); + }); +}); diff --git a/packager/react-packager/src/transforms/babel-plugin-system-import/index.js b/packager/react-packager/src/transforms/babel-plugin-system-import/index.js new file mode 100644 index 000000000..21ef64c0c --- /dev/null +++ b/packager/react-packager/src/transforms/babel-plugin-system-import/index.js @@ -0,0 +1,65 @@ + /** + * 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. + * + */ +/*jslint node: true */ +'use strict'; + +var t = require('babel-core').types; + +/** + * Transforms asynchronous module importing into a function call + * that includes which bundle needs to be loaded + * + * Transforms: + * + * System.import('moduleA') + * + * to: + * + * loadBundles('bundleA') + */ +module.exports = function systemImportTransform(babel) { + return new babel.Transformer('system-import', { + CallExpression: function(node, parent, scope, state) { + if (!isAppropriateSystemImportCall(node, parent)) { + return node; + } + + var bundlesLayout = state.opts.extra.bundlesLayout; + var bundleID = bundlesLayout.getBundleIDForModule( + node.arguments[0].value + ); + + var bundles = bundleID.split('.'); + bundles.splice(0, 1); + bundles = bundles.map(function(id) { + return t.literal('bundle.' + id); + }); + + return t.callExpression( + t.identifier('loadBundles'), + [t.arrayExpression(bundles)] + ); + }, + + metadata: { + group: 'fb' + } + }); +}; + +function isAppropriateSystemImportCall(node) { + return ( + node.callee.type === 'MemberExpression' && + node.callee.object.name === 'System' && + node.callee.property.name === 'import' && + node.arguments.length === 1 && + node.arguments[0].type === 'Literal' + ); +} From 99bd57aef2a1b5bc130c77feb714cb73526ce931 Mon Sep 17 00:00:00 2001 From: Eric Vicenti Date: Thu, 27 Aug 2015 11:02:42 -0700 Subject: [PATCH 10/25] [ReactNative] Improve error handling with missing bridge callback Summary: This will throw an error message with the problematic callback module/method. Previously we would get an invariant in this case when we try to access `callback.apply` later in the method. --- Libraries/Utilities/MessageQueue.js | 32 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/Libraries/Utilities/MessageQueue.js b/Libraries/Utilities/MessageQueue.js index ffca2bca5..007b0dfca 100644 --- a/Libraries/Utilities/MessageQueue.js +++ b/Libraries/Utilities/MessageQueue.js @@ -65,13 +65,11 @@ class MessageQueue { localModules && this._genLookupTables( localModules, this._moduleTable, this._methodTable); - if (__DEV__) { - this._debugInfo = {}; - this._remoteModuleTable = {}; - this._remoteMethodTable = {}; - this._genLookupTables( - remoteModules, this._remoteModuleTable, this._remoteMethodTable); - } + this._debugInfo = {}; + this._remoteModuleTable = {}; + this._remoteMethodTable = {}; + this._genLookupTables( + remoteModules, this._remoteModuleTable, this._remoteMethodTable); } /** @@ -116,13 +114,11 @@ class MessageQueue { */ __nativeCall(module, method, params, onFail, onSucc) { if (onFail || onSucc) { - if (__DEV__) { - // eventually delete old debug info - (this._callbackID > (1 << 5)) && - (this._debugInfo[this._callbackID >> 5] = null); + // eventually delete old debug info + (this._callbackID > (1 << 5)) && + (this._debugInfo[this._callbackID >> 5] = null); - this._debugInfo[this._callbackID >> 1] = [module, method]; - } + this._debugInfo[this._callbackID >> 1] = [module, method]; onFail && params.push(this._callbackID); this._callbacks[this._callbackID++] = onFail; onSucc && params.push(this._callbackID); @@ -155,13 +151,15 @@ class MessageQueue { BridgeProfiling.profile( () => `MessageQueue.invokeCallback(${cbID}, ${stringifySafe(args)})`); let callback = this._callbacks[cbID]; - if (__DEV__) { + if (!callback || __DEV__) { let debug = this._debugInfo[cbID >> 1]; let module = debug && this._remoteModuleTable[debug[0]]; let method = debug && this._remoteMethodTable[debug[0]][debug[1]]; - if (!callback) { - console.error(`Callback with id ${cbID}: ${module}.${method}() not found`); - } else if (SPY_MODE) { + invariant( + callback, + `Callback with id ${cbID}: ${module}.${method}() not found` + ); + if (callback && SPY_MODE) { console.log('N->JS : (' + JSON.stringify(args) + ')'); } } From fa6bc1c3cd37f7e1a3c3fca46fe8dd093452544e Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Thu, 27 Aug 2015 14:04:59 -0700 Subject: [PATCH 11/25] Fixed WebView example --- Examples/UIExplorer/WebViewExample.js | 2 +- Libraries/Components/WebView/WebView.ios.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/UIExplorer/WebViewExample.js b/Examples/UIExplorer/WebViewExample.js index 478c2a995..41f6b4d1a 100644 --- a/Examples/UIExplorer/WebViewExample.js +++ b/Examples/UIExplorer/WebViewExample.js @@ -75,7 +75,7 @@ var WebViewExample = React.createClass({ Date: Fri, 28 Aug 2015 09:57:51 -0100 Subject: [PATCH 12/25] [ReactNative][SyncDiff] Add Movies app --- Examples/Movies/MovieCell.js | 10 ++- .../Movies/{MoviesApp.js => MoviesApp.ios.js} | 0 Examples/Movies/SearchBar.ios.js | 66 +++++++++++++++++ Examples/Movies/SearchScreen.js | 72 ++++++++----------- 4 files changed, 103 insertions(+), 45 deletions(-) rename Examples/Movies/{MoviesApp.js => MoviesApp.ios.js} (100%) create mode 100644 Examples/Movies/SearchBar.ios.js diff --git a/Examples/Movies/MovieCell.js b/Examples/Movies/MovieCell.js index 341460e58..62062cb2a 100644 --- a/Examples/Movies/MovieCell.js +++ b/Examples/Movies/MovieCell.js @@ -19,9 +19,11 @@ var React = require('react-native'); var { Image, PixelRatio, + Platform, StyleSheet, Text, TouchableHighlight, + TouchableNativeFeedback, View } = React; @@ -32,9 +34,13 @@ var getTextFromScore = require('./getTextFromScore'); var MovieCell = React.createClass({ render: function() { var criticsScore = this.props.movie.ratings.critics_score; + var TouchableElement = TouchableHighlight; + if (Platform.OS === 'android') { + TouchableElement = TouchableNativeFeedback; + } return ( - @@ -59,7 +65,7 @@ var MovieCell = React.createClass({ - + ); } diff --git a/Examples/Movies/MoviesApp.js b/Examples/Movies/MoviesApp.ios.js similarity index 100% rename from Examples/Movies/MoviesApp.js rename to Examples/Movies/MoviesApp.ios.js diff --git a/Examples/Movies/SearchBar.ios.js b/Examples/Movies/SearchBar.ios.js new file mode 100644 index 000000000..f4a2354ef --- /dev/null +++ b/Examples/Movies/SearchBar.ios.js @@ -0,0 +1,66 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @providesModule SearchBar + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + ActivityIndicatorIOS, + TextInput, + StyleSheet, + View, +} = React; + +var SearchBar = React.createClass({ + render: function() { + return ( + + + + + ); + } +}); + +var styles = StyleSheet.create({ + searchBar: { + marginTop: 64, + padding: 3, + paddingLeft: 8, + flexDirection: 'row', + alignItems: 'center', + }, + searchBarInput: { + fontSize: 15, + flex: 1, + height: 30, + }, + spinner: { + width: 30, + }, +}); + +module.exports = SearchBar; diff --git a/Examples/Movies/SearchScreen.js b/Examples/Movies/SearchScreen.js index 477ed6a50..4fd36790b 100644 --- a/Examples/Movies/SearchScreen.js +++ b/Examples/Movies/SearchScreen.js @@ -19,6 +19,8 @@ var React = require('react-native'); var { ActivityIndicatorIOS, ListView, + Platform, + ProgressBarAndroid, StyleSheet, Text, TextInput, @@ -27,9 +29,11 @@ var { var TimerMixin = require('react-timer-mixin'); var invariant = require('invariant'); +var dismissKeyboard = require('dismissKeyboard'); var MovieCell = require('./MovieCell'); var MovieScreen = require('./MovieScreen'); +var SearchBar = require('SearchBar'); /** * This is for demo purposes only, and rate limited. @@ -219,11 +223,20 @@ var SearchScreen = React.createClass({ }, selectMovie: function(movie: Object) { - this.props.navigator.push({ - title: movie.title, - component: MovieScreen, - passProps: {movie}, - }); + if (Platform.OS === 'ios') { + this.props.navigator.push({ + title: movie.title, + component: MovieScreen, + passProps: {movie}, + }); + } else { + dismissKeyboard(); + this.props.navigator.push({ + title: movie.title, + name: 'movie', + movie: movie, + }); + } }, onSearchChange: function(event: Object) { @@ -237,7 +250,15 @@ var SearchScreen = React.createClass({ if (!this.hasMore() || !this.state.isLoadingTail) { return ; } - return ; + if (Platform.OS === 'ios') { + return ; + } else { + return ( + + + + ); + } }, renderSeparator: function( @@ -295,7 +316,8 @@ var SearchScreen = React.createClass({ this.refs.listview.getScrollResponder().scrollTo(0, 0)} + onFocus={() => + this.refs.listview && this.refs.listview.getScrollResponder().scrollTo(0, 0)} /> {content} @@ -323,27 +345,6 @@ var NoMovies = React.createClass({ } }); -var SearchBar = React.createClass({ - render: function() { - return ( - - - - - ); - } -}); - var styles = StyleSheet.create({ container: { flex: 1, @@ -356,25 +357,10 @@ var styles = StyleSheet.create({ marginTop: 80, color: '#888888', }, - searchBar: { - marginTop: 64, - padding: 3, - paddingLeft: 8, - flexDirection: 'row', - alignItems: 'center', - }, - searchBarInput: { - fontSize: 15, - flex: 1, - height: 30, - }, separator: { height: 1, backgroundColor: '#eeeeee', }, - spinner: { - width: 30, - }, scrollSpinner: { marginVertical: 20, }, From e15f584a3d153d923a25ae3f40fceb0b30951277 Mon Sep 17 00:00:00 2001 From: Pieter De Baets Date: Fri, 28 Aug 2015 05:45:26 -0700 Subject: [PATCH 14/25] Slim RCTSourceCode in production --- React/Modules/RCTSourceCode.m | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/React/Modules/RCTSourceCode.m b/React/Modules/RCTSourceCode.m index 08b033781..05860e1dd 100644 --- a/React/Modules/RCTSourceCode.m +++ b/React/Modules/RCTSourceCode.m @@ -9,6 +9,7 @@ #import "RCTSourceCode.h" +#import "RCTDefines.h" #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTUtils.h" @@ -19,20 +20,18 @@ RCT_EXPORT_MODULE() @synthesize bridge = _bridge; +#if !RCT_DEV +- (void)setScriptText:(NSString *)scriptText {} +#endif + RCT_EXPORT_METHOD(getScriptText:(RCTResponseSenderBlock)successCallback failureCallback:(RCTResponseErrorBlock)failureCallback) { - if (self.scriptText && self.scriptURL) { + if (RCT_DEV && self.scriptText && self.scriptURL) { successCallback(@[@{@"text": self.scriptText, @"url": self.scriptURL.absoluteString}]); } else { failureCallback(RCTErrorWithMessage(@"Source code is not available")); } } -- (NSDictionary *)constantsToExport -{ - NSString *URL = self.bridge.bundleURL.absoluteString ?: @""; - return @{@"scriptURL": URL}; -} - @end From aee74efde786566fc137e1364f02537ae1f98b39 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Fri, 28 Aug 2015 10:11:02 -0700 Subject: [PATCH 15/25] [ReactNative] Add JSC profiler to Dev Menu Summary: Add JSC profiler to the dev menu and rename the pre-existent one to systrace. For now it just outputs to the console, but a better workflow is on the way. --- JSCLegacyProfiler/JSCLegacyProfiler.h | 17 ++-------- JSCLegacyProfiler/JSCLegacyProfiler.mm | 43 +++++--------------------- JSCLegacyProfiler/Makefile | 2 -- React/Executors/RCTContextExecutor.m | 17 +++++++--- React/Modules/RCTDevMenu.m | 2 +- 5 files changed, 23 insertions(+), 58 deletions(-) diff --git a/JSCLegacyProfiler/JSCLegacyProfiler.h b/JSCLegacyProfiler/JSCLegacyProfiler.h index 826e39f21..6705a1de4 100644 --- a/JSCLegacyProfiler/JSCLegacyProfiler.h +++ b/JSCLegacyProfiler/JSCLegacyProfiler.h @@ -6,20 +6,7 @@ extern "C" { -JSValueRef nativeProfilerStart( - JSContextRef ctx, - JSObjectRef function, - JSObjectRef thisObject, - size_t argumentCount, - const JSValueRef arguments[], - JSValueRef *exception); - -JSValueRef nativeProfilerEnd( - JSContextRef ctx, - JSObjectRef function, - JSObjectRef thisObject, - size_t argumentCount, - const JSValueRef arguments[], - JSValueRef *exception); +void nativeProfilerStart(JSContextRef ctx, const char *title); +const char *nativeProfilerEnd(JSContextRef ctx, const char *title); } diff --git a/JSCLegacyProfiler/JSCLegacyProfiler.mm b/JSCLegacyProfiler/JSCLegacyProfiler.mm index b946c8fd1..dd2f01f8a 100644 --- a/JSCLegacyProfiler/JSCLegacyProfiler.mm +++ b/JSCLegacyProfiler/JSCLegacyProfiler.mm @@ -7,6 +7,7 @@ #include "OpaqueJSString.h" #include "JSProfilerPrivate.h" #include "JSStringRef.h" +#include "String.h" #include @@ -114,48 +115,18 @@ static char *convert_to_json(const JSC::Profile *profile) { return json_copy; } -static char *JSEndProfilingAndRender(JSContextRef ctx, JSStringRef title) +static const char *JSEndProfilingAndRender(JSContextRef ctx, const char *title) { JSC::ExecState *exec = toJS(ctx); JSC::LegacyProfiler *profiler = JSC::LegacyProfiler::profiler(); - RefPtr rawProfile = profiler->stopProfiling(exec, title->string()); + RefPtr rawProfile = profiler->stopProfiling(exec, WTF::String(title)); return convert_to_json(rawProfile.get()); } -JSValueRef nativeProfilerStart( - JSContextRef ctx, - JSObjectRef function, - JSObjectRef thisObject, - size_t argumentCount, - const JSValueRef arguments[], - JSValueRef *exception) { - if (argumentCount < 1) { - // Could raise an exception here. - return JSValueMakeUndefined(ctx); - } - - JSStringRef title = JSValueToStringCopy(ctx, arguments[0], NULL); - JSStartProfiling(ctx, title); - JSStringRelease(title); - return JSValueMakeUndefined(ctx); +void nativeProfilerStart(JSContextRef ctx, const char *title) { + JSStartProfiling(ctx, JSStringCreateWithUTF8CString(title)); } -JSValueRef nativeProfilerEnd( - JSContextRef ctx, - JSObjectRef function, - JSObjectRef thisObject, - size_t argumentCount, - const JSValueRef arguments[], - JSValueRef *exception) { - if (argumentCount < 1) { - // Could raise an exception here. - return JSValueMakeUndefined(ctx); - } - - JSStringRef title = JSValueToStringCopy(ctx, arguments[0], NULL); - char *rendered = JSEndProfilingAndRender(ctx, title); - JSStringRelease(title); - JSStringRef profile = JSStringCreateWithUTF8CString(rendered); - free(rendered); - return JSValueMakeString(ctx, profile); +const char *nativeProfilerEnd( JSContextRef ctx, const char *title) { + return JSEndProfilingAndRender(ctx, title); } diff --git a/JSCLegacyProfiler/Makefile b/JSCLegacyProfiler/Makefile index 0cf8f6afe..a1f704d98 100644 --- a/JSCLegacyProfiler/Makefile +++ b/JSCLegacyProfiler/Makefile @@ -110,5 +110,3 @@ armv7: ${HEADER_PATHS} \ -undefined dynamic_lookup \ ./JSCLegacyProfiler.mm ./tmp/yajl.a - -.PHONY: ios8 diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index 8b051fa67..be12934f0 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -16,6 +16,7 @@ #import "RCTAssert.h" #import "RCTDefines.h" +#import "RCTDevMenu.h" #import "RCTLog.h" #import "RCTProfile.h" #import "RCTPerformanceLogger.h" @@ -89,6 +90,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)init) } @synthesize valid = _valid; +@synthesize bridge = _bridge; RCT_EXPORT_MODULE() @@ -285,11 +287,18 @@ static JSValueRef RCTNativeTraceEndSection(JSContextRef context, __unused JSObje #if RCT_JSC_PROFILER void *JSCProfiler = dlopen(RCT_JSC_PROFILER_DYLIB, RTLD_NOW); if (JSCProfiler != NULL) { - JSObjectCallAsFunctionCallback nativeProfilerStart = dlsym(JSCProfiler, "nativeProfilerStart"); - JSObjectCallAsFunctionCallback nativeProfilerEnd = dlsym(JSCProfiler, "nativeProfilerEnd"); + void (*nativeProfilerStart)(JSContextRef, const char *) = (void (*)(JSContextRef, const char *))dlsym(JSCProfiler, "nativeProfilerStart"); + const char *(*nativeProfilerEnd)(JSContextRef, const char *) = (const char *(*)(JSContextRef, const char *))dlsym(JSCProfiler, "nativeProfilerEnd"); if (nativeProfilerStart != NULL && nativeProfilerEnd != NULL) { - [strongSelf _addNativeHook:nativeProfilerStart withName:"nativeProfilerStart"]; - [strongSelf _addNativeHook:nativeProfilerEnd withName:"nativeProfilerStop"]; + __block BOOL isProfiling = NO; + [_bridge.devMenu addItem:@"Profile" handler:^{ + if (isProfiling) { + RCTLogInfo(@"%s", nativeProfilerEnd(strongSelf->_context.ctx, "profile")); + } else { + nativeProfilerStart(strongSelf->_context.ctx, "profile"); + } + isProfiling = !isProfiling; + }]; } } #endif diff --git a/React/Modules/RCTDevMenu.m b/React/Modules/RCTDevMenu.m index 56ef7dfd4..b99bd5554 100644 --- a/React/Modules/RCTDevMenu.m +++ b/React/Modules/RCTDevMenu.m @@ -314,7 +314,7 @@ RCT_EXPORT_MODULE() self.liveReloadEnabled = !_liveReloadEnabled; }]]; - NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Profiling" : @"Start Profiling"; + NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Systrace" : @"Start Systrace"; [items addObject:[[RCTDevMenuItem alloc] initWithTitle:profilingTitle handler:^{ self.profilingEnabled = !_profilingEnabled; }]]; From 53fc5624e42cafcd267266ef45a48ddb90fdab43 Mon Sep 17 00:00:00 2001 From: Felix Oghina Date: Sat, 29 Aug 2015 06:37:43 -0700 Subject: [PATCH 16/25] [reactnative] send platform arg with all packager requests --- Examples/2048/2048/AppDelegate.m | 4 ++-- Examples/TicTacToe/TicTacToe/AppDelegate.m | 4 ++-- Examples/UIExplorer/UIExplorer/AppDelegate.m | 4 ++-- Libraries/RCTTest/RCTTestRunner.m | 2 +- React/Executors/RCTContextExecutor.m | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Examples/2048/2048/AppDelegate.m b/Examples/2048/2048/AppDelegate.m index 393ce83cb..2413b3552 100644 --- a/Examples/2048/2048/AppDelegate.m +++ b/Examples/2048/2048/AppDelegate.m @@ -36,14 +36,14 @@ * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle"]; + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/2048/Game2048.bundle?platform=ios"]; /** * OPTION 2 * Load from pre-bundled file on disk. To re-generate the static bundle, `cd` * to your Xcode project folder in the terminal, and run * - * $ curl 'http://localhost:8081/Examples/2048/Game2048.includeRequire.runModule.bundle' -o main.jsbundle + * $ curl 'http://localhost:8081/Examples/2048/Game2048.bundle?platform=ios' -o main.jsbundle * * then add the `main.jsbundle` file to your project and uncomment this line: */ diff --git a/Examples/TicTacToe/TicTacToe/AppDelegate.m b/Examples/TicTacToe/TicTacToe/AppDelegate.m index 7fa214fab..aa746ba69 100644 --- a/Examples/TicTacToe/TicTacToe/AppDelegate.m +++ b/Examples/TicTacToe/TicTacToe/AppDelegate.m @@ -36,14 +36,14 @@ * on the same Wi-Fi network. */ - jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle"]; + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/Examples/TicTacToe/TicTacToeApp.bundle?platform=ios"]; /** * OPTION 2 * Load from pre-bundled file on disk. To re-generate the static bundle, `cd` * to your Xcode project folder in the terminal, and run * - * $ curl 'http://localhost:8081/Examples/TicTacToe/TicTacToeApp.includeRequire.runModule.bundle' -o main.jsbundle + * $ curl 'http://localhost:8081/Examples/TicTacToe/TicTacToeApp.bundle?platform=ios' -o main.jsbundle * * then add the `main.jsbundle` file to your project and uncomment this line: */ diff --git a/Examples/UIExplorer/UIExplorer/AppDelegate.m b/Examples/UIExplorer/UIExplorer/AppDelegate.m index f394c9903..24ada2b56 100644 --- a/Examples/UIExplorer/UIExplorer/AppDelegate.m +++ b/Examples/UIExplorer/UIExplorer/AppDelegate.m @@ -59,14 +59,14 @@ * on the same Wi-Fi network. */ - sourceURL = [NSURL URLWithString:@"http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.includeRequire.runModule.bundle?dev=true"]; + sourceURL = [NSURL URLWithString:@"http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.bundle?dev=true"]; /** * OPTION 2 * Load from pre-bundled file on disk. To re-generate the static bundle, `cd` * to your Xcode project folder and run * - * $ curl 'http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.includeRequire.runModule.bundle' -o main.jsbundle + * $ curl 'http://localhost:8081/Examples/UIExplorer/UIExplorerApp.ios.bundle' -o main.jsbundle * * then add the `main.jsbundle` file to your project and uncomment this line: */ diff --git a/Libraries/RCTTest/RCTTestRunner.m b/Libraries/RCTTest/RCTTestRunner.m index 372858ac9..b0c932bc1 100644 --- a/Libraries/RCTTest/RCTTestRunner.m +++ b/Libraries/RCTTest/RCTTestRunner.m @@ -49,7 +49,7 @@ _scriptURL = [[NSBundle bundleForClass:[RCTBridge class]] URLForResource:@"main" withExtension:@"jsbundle"]; RCTAssert(_scriptURL != nil, @"Could not locate main.jsBundle"); #else - _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.includeRequire.runModule.bundle?dev=true", app]]; + _scriptURL = [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:8081/%@.bundle?dev=true&platform=ios", app]]; #endif } return self; diff --git a/React/Executors/RCTContextExecutor.m b/React/Executors/RCTContextExecutor.m index be12934f0..2dcf3d7b0 100644 --- a/React/Executors/RCTContextExecutor.m +++ b/React/Executors/RCTContextExecutor.m @@ -112,7 +112,7 @@ static JSValueRef RCTNativeLoggingHook(JSContextRef context, __unused JSObjectRe NSString *message = (__bridge_transfer NSString *)JSStringCopyCFString(kCFAllocatorDefault, messageRef); JSStringRelease(messageRef); NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern: - @"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).includeRequire.runModule.bundle(:[0-9]+:[0-9]+)" + @"( stack: )?([_a-z0-9]*)@?(http://|file:///)[a-z.0-9:/_-]+/([a-z0-9_]+).bundle(:[0-9]+:[0-9]+)" options:NSRegularExpressionCaseInsensitive error:NULL]; message = [regex stringByReplacingMatchesInString:message From 7fed66884174fc952e99688f931729bbe2e04e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Bigio?= Date: Mon, 31 Aug 2015 07:25:28 -0700 Subject: [PATCH 17/25] [react-packager] Cache BundlesLayout --- packager/react-packager/src/Bundler/index.js | 12 + .../__tests__/BundlesLayout-test.js | 479 ++++++++++-------- .../BundlesLayoutIntegration-test.js | 34 +- .../react-packager/src/BundlesLayout/index.js | 106 +++- .../src/Cache/__tests__/Cache-test.js | 4 +- packager/react-packager/src/Cache/index.js | 57 +-- .../src/lib/getCacheFilePath.js | 25 + .../react-packager/src/lib/loadCacheSync.js | 30 ++ 8 files changed, 485 insertions(+), 262 deletions(-) create mode 100644 packager/react-packager/src/lib/getCacheFilePath.js create mode 100644 packager/react-packager/src/lib/loadCacheSync.js diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index d29ab0f89..8f76ce103 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -13,6 +13,7 @@ const fs = require('fs'); const path = require('path'); const Promise = require('promise'); const ProgressBar = require('progress'); +const BundlesLayout = require('../BundlesLayout'); const Cache = require('../Cache'); const Transformer = require('../JSTransformer'); const DependencyResolver = require('../DependencyResolver'); @@ -104,6 +105,13 @@ class Bundler { cache: this._cache, }); + this._bundlesLayout = new BundlesLayout({ + dependencyResolver: this._resolver, + resetCache: opts.resetCache, + cacheVersion: opts.cacheVersion, + projectRoots: opts.projectRoots, + }); + this._transformer = new Transformer({ projectRoots: opts.projectRoots, blacklistRE: opts.blacklistRE, @@ -120,6 +128,10 @@ class Bundler { return this._cache.end(); } + getLayout(main, isDev) { + return this._bundlesLayout.generateLayout(main, isDev); + } + bundle(main, runModule, sourceMapUrl, isDev, platform) { const bundle = new Bundle(sourceMapUrl); const findEventId = Activity.startEvent('find dependencies'); diff --git a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js index cca9c8a7e..2f66d6ff6 100644 --- a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js +++ b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayout-test.js @@ -8,249 +8,320 @@ */ 'use strict'; -jest - .dontMock('../index'); +jest.dontMock('../index'); +jest.mock('fs'); const Promise = require('promise'); describe('BundlesLayout', () => { - var BundlesLayout; - var DependencyResolver; + let BundlesLayout; + let DependencyResolver; + let loadCacheSync; beforeEach(() => { BundlesLayout = require('../index'); DependencyResolver = require('../../DependencyResolver'); + loadCacheSync = require('../../lib/loadCacheSync'); }); - describe('generate', () => { - function newBundlesLayout() { - return new BundlesLayout({ - dependencyResolver: new DependencyResolver(), - }); - } + function newBundlesLayout(options) { + return new BundlesLayout(Object.assign({ + projectRoots: ['/root'], + dependencyResolver: new DependencyResolver(), + }, options)); + } + describe('layout', () => { function isPolyfill() { return false; } - function dep(path) { - return { - path: path, - isPolyfill: isPolyfill, - }; - } + describe('getLayout', () => { + function dep(path) { + return { + path: path, + isPolyfill: isPolyfill, + }; + } - pit('should bundle sync dependencies', () => { - DependencyResolver.prototype.getDependencies.mockImpl((path) => { - switch (path) { - case '/root/index.js': - return Promise.resolve({ - dependencies: [dep('/root/index.js'), dep('/root/a.js')], - asyncDependencies: [], - }); - case '/root/a.js': - return Promise.resolve({ - dependencies: [dep('/root/a.js')], - asyncDependencies: [], - }); - default: - throw 'Undefined path: ' + path; - } + pit('should bundle sync dependencies', () => { + DependencyResolver.prototype.getDependencies.mockImpl((path) => { + switch (path) { + case '/root/index.js': + return Promise.resolve({ + dependencies: [dep('/root/index.js'), dep('/root/a.js')], + asyncDependencies: [], + }); + case '/root/a.js': + return Promise.resolve({ + dependencies: [dep('/root/a.js')], + asyncDependencies: [], + }); + default: + throw 'Undefined path: ' + path; + } + }); + + return newBundlesLayout({resetCache: true}) + .getLayout('/root/index.js') + .then(bundles => + expect(bundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js', '/root/a.js'], + children: [], + }) + ); }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => - expect(bundles).toEqual({ - id: 'bundle.0', - modules: ['/root/index.js', '/root/a.js'], - children: [], - }) - ); - }); + pit('should separate async dependencies into different bundle', () => { + DependencyResolver.prototype.getDependencies.mockImpl((path) => { + switch (path) { + case '/root/index.js': + return Promise.resolve({ + dependencies: [dep('/root/index.js')], + asyncDependencies: [['/root/a.js']], + }); + case '/root/a.js': + return Promise.resolve({ + dependencies: [dep('/root/a.js')], + asyncDependencies: [], + }); + default: + throw 'Undefined path: ' + path; + } + }); - pit('should separate async dependencies into different bundle', () => { - DependencyResolver.prototype.getDependencies.mockImpl((path) => { - switch (path) { - case '/root/index.js': - return Promise.resolve({ - dependencies: [dep('/root/index.js')], - asyncDependencies: [['/root/a.js']], - }); - case '/root/a.js': - return Promise.resolve({ - dependencies: [dep('/root/a.js')], - asyncDependencies: [], - }); - default: - throw 'Undefined path: ' + path; - } - }); - - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => - expect(bundles).toEqual({ - id: 'bundle.0', - modules: ['/root/index.js'], - children: [{ - id:'bundle.0.1', - modules: ['/root/a.js'], - children: [], - }], - }) - ); - }); - - pit('separate async dependencies of async dependencies', () => { - DependencyResolver.prototype.getDependencies.mockImpl((path) => { - switch (path) { - case '/root/index.js': - return Promise.resolve({ - dependencies: [dep('/root/index.js')], - asyncDependencies: [['/root/a.js']], - }); - case '/root/a.js': - return Promise.resolve({ - dependencies: [dep('/root/a.js')], - asyncDependencies: [['/root/b.js']], - }); - case '/root/b.js': - return Promise.resolve({ - dependencies: [dep('/root/b.js')], - asyncDependencies: [], - }); - default: - throw 'Undefined path: ' + path; - } - }); - - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => - expect(bundles).toEqual({ - id: 'bundle.0', - modules: ['/root/index.js'], - children: [{ - id: 'bundle.0.1', - modules: ['/root/a.js'], + return newBundlesLayout({resetCache: true}) + .getLayout('/root/index.js') + .then(bundles => + expect(bundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], children: [{ - id: 'bundle.0.1.2', + id:'bundle.0.1', + modules: ['/root/a.js'], + children: [], + }], + }) + ); + }); + + pit('separate async dependencies of async dependencies', () => { + DependencyResolver.prototype.getDependencies.mockImpl((path) => { + switch (path) { + case '/root/index.js': + return Promise.resolve({ + dependencies: [dep('/root/index.js')], + asyncDependencies: [['/root/a.js']], + }); + case '/root/a.js': + return Promise.resolve({ + dependencies: [dep('/root/a.js')], + asyncDependencies: [['/root/b.js']], + }); + case '/root/b.js': + return Promise.resolve({ + dependencies: [dep('/root/b.js')], + asyncDependencies: [], + }); + default: + throw 'Undefined path: ' + path; + } + }); + + return newBundlesLayout({resetCache: true}) + .getLayout('/root/index.js') + .then(bundles => + expect(bundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/a.js'], + children: [{ + id: 'bundle.0.1.2', + modules: ['/root/b.js'], + children: [], + }], + }], + }) + ); + }); + + pit('separate bundle sync dependencies of async ones on same bundle', () => { + DependencyResolver.prototype.getDependencies.mockImpl((path) => { + switch (path) { + case '/root/index.js': + return Promise.resolve({ + dependencies: [dep('/root/index.js')], + asyncDependencies: [['/root/a.js']], + }); + case '/root/a.js': + return Promise.resolve({ + dependencies: [dep('/root/a.js'), dep('/root/b.js')], + asyncDependencies: [], + }); + case '/root/b.js': + return Promise.resolve({ + dependencies: [dep('/root/b.js')], + asyncDependencies: [], + }); + default: + throw 'Undefined path: ' + path; + } + }); + + return newBundlesLayout({resetCache: true}) + .getLayout('/root/index.js') + .then(bundles => + expect(bundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/a.js', '/root/b.js'], + children: [], + }], + }) + ); + }); + + pit('separate cache in which bundle is each dependency', () => { + DependencyResolver.prototype.getDependencies.mockImpl((path) => { + switch (path) { + case '/root/index.js': + return Promise.resolve({ + dependencies: [dep('/root/index.js'), dep('/root/a.js')], + asyncDependencies: [], + }); + case '/root/a.js': + return Promise.resolve({ + dependencies: [dep('/root/a.js')], + asyncDependencies: [['/root/b.js']], + }); + case '/root/b.js': + return Promise.resolve({ + dependencies: [dep('/root/b.js')], + asyncDependencies: [], + }); + default: + throw 'Undefined path: ' + path; + } + }); + + return newBundlesLayout({resetCache: true}) + .getLayout('/root/index.js') + .then(bundles => + expect(bundles).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js', '/root/a.js'], + children: [{ + id: 'bundle.0.1', modules: ['/root/b.js'], children: [], }], - }], - }) - ); - }); - - pit('separate bundle sync dependencies of async ones on same bundle', () => { - DependencyResolver.prototype.getDependencies.mockImpl((path) => { - switch (path) { - case '/root/index.js': - return Promise.resolve({ - dependencies: [dep('/root/index.js')], - asyncDependencies: [['/root/a.js']], - }); - case '/root/a.js': - return Promise.resolve({ - dependencies: [dep('/root/a.js'), dep('/root/b.js')], - asyncDependencies: [], - }); - case '/root/b.js': - return Promise.resolve({ - dependencies: [dep('/root/b.js')], - asyncDependencies: [], - }); - default: - throw 'Undefined path: ' + path; - } + }) + ); }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => - expect(bundles).toEqual({ + pit('separate cache in which bundle is each dependency', () => { + DependencyResolver.prototype.getDependencies.mockImpl((path) => { + switch (path) { + case '/root/index.js': + return Promise.resolve({ + dependencies: [dep('/root/index.js'), dep('/root/a.js')], + asyncDependencies: [['/root/b.js'], ['/root/c.js']], + }); + case '/root/a.js': + return Promise.resolve({ + dependencies: [dep('/root/a.js')], + asyncDependencies: [], + }); + case '/root/b.js': + return Promise.resolve({ + dependencies: [dep('/root/b.js')], + asyncDependencies: [['/root/d.js']], + }); + case '/root/c.js': + return Promise.resolve({ + dependencies: [dep('/root/c.js')], + asyncDependencies: [], + }); + case '/root/d.js': + return Promise.resolve({ + dependencies: [dep('/root/d.js')], + asyncDependencies: [], + }); + default: + throw 'Undefined path: ' + path; + } + }); + + var layout = newBundlesLayout({resetCache: true}); + return layout.getLayout('/root/index.js').then(() => { + expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0'); + expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0'); + expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.0.1'); + expect(layout.getBundleIDForModule('/root/c.js')).toBe('bundle.0.2'); + expect(layout.getBundleIDForModule('/root/d.js')).toBe('bundle.0.1.3'); + }); + }); + }); + }); + + describe('cache', () => { + beforeEach(() => { + loadCacheSync.mockReturnValue({ + '/root/index.js': { id: 'bundle.0', modules: ['/root/index.js'], children: [{ id: 'bundle.0.1', - modules: ['/root/a.js', '/root/b.js'], + modules: ['/root/a.js'], children: [], }], - }) - ); + }, + '/root/b.js': { + id: 'bundle.2', + modules: ['/root/b.js'], + children: [], + }, + }); }); - pit('separate cache in which bundle is each dependency', () => { - DependencyResolver.prototype.getDependencies.mockImpl((path) => { - switch (path) { - case '/root/index.js': - return Promise.resolve({ - dependencies: [dep('/root/index.js'), dep('/root/a.js')], - asyncDependencies: [], - }); - case '/root/a.js': - return Promise.resolve({ - dependencies: [dep('/root/a.js')], - asyncDependencies: [['/root/b.js']], - }); - case '/root/b.js': - return Promise.resolve({ - dependencies: [dep('/root/b.js')], - asyncDependencies: [], - }); - default: - throw 'Undefined path: ' + path; - } - }); + pit('should load layouts', () => { + const layout = newBundlesLayout({ resetCache: false }); - return newBundlesLayout().generateLayout(['/root/index.js']).then( - bundles => expect(bundles).toEqual({ - id: 'bundle.0', - modules: ['/root/index.js', '/root/a.js'], - children: [{ - id: 'bundle.0.1', + return Promise + .all([ + layout.getLayout('/root/index.js'), + layout.getLayout('/root/b.js'), + ]) + .then(([layoutIndex, layoutB]) => { + expect(layoutIndex).toEqual({ + id: 'bundle.0', + modules: ['/root/index.js'], + children: [{ + id: 'bundle.0.1', + modules: ['/root/a.js'], + children: [], + }], + }); + + expect(layoutB).toEqual({ + id: 'bundle.2', modules: ['/root/b.js'], children: [], - }], - }) - ); + }); + }); }); - pit('separate cache in which bundle is each dependency', () => { - DependencyResolver.prototype.getDependencies.mockImpl((path) => { - switch (path) { - case '/root/index.js': - return Promise.resolve({ - dependencies: [dep('/root/index.js'), dep('/root/a.js')], - asyncDependencies: [['/root/b.js'], ['/root/c.js']], - }); - case '/root/a.js': - return Promise.resolve({ - dependencies: [dep('/root/a.js')], - asyncDependencies: [], - }); - case '/root/b.js': - return Promise.resolve({ - dependencies: [dep('/root/b.js')], - asyncDependencies: [['/root/d.js']], - }); - case '/root/c.js': - return Promise.resolve({ - dependencies: [dep('/root/c.js')], - asyncDependencies: [], - }); - case '/root/d.js': - return Promise.resolve({ - dependencies: [dep('/root/d.js')], - asyncDependencies: [], - }); - default: - throw 'Undefined path: ' + path; - } - }); + it('should load moduleToBundle map', () => { + const layout = newBundlesLayout({ resetCache: false }); - var layout = newBundlesLayout(); - return layout.generateLayout(['/root/index.js']).then(() => { - expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0'); - expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0'); - expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.0.1'); - expect(layout.getBundleIDForModule('/root/c.js')).toBe('bundle.0.2'); - expect(layout.getBundleIDForModule('/root/d.js')).toBe('bundle.0.1.3'); - }); + expect(layout.getBundleIDForModule('/root/index.js')).toBe('bundle.0'); + expect(layout.getBundleIDForModule('/root/a.js')).toBe('bundle.0.1'); + expect(layout.getBundleIDForModule('/root/b.js')).toBe('bundle.2'); }); }); }); diff --git a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js index f834ccf57..a54ee2164 100644 --- a/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js +++ b/packager/react-packager/src/BundlesLayout/__tests__/BundlesLayoutIntegration-test.js @@ -75,7 +75,11 @@ describe('BundlesLayout', () => { assetRoots: ['/root'], }); - return new BundlesLayout({dependencyResolver: resolver}); + return new BundlesLayout({ + dependencyResolver: resolver, + resetCache: true, + projectRoots: ['/root', '/' + __dirname.split('/')[1]], + }); } function stripPolyfills(bundle) { @@ -114,7 +118,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -140,7 +144,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -166,7 +170,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -201,7 +205,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -242,7 +246,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -282,7 +286,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -323,7 +327,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -370,7 +374,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -408,7 +412,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -446,7 +450,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -480,7 +484,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -512,7 +516,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -539,7 +543,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', @@ -576,7 +580,7 @@ describe('BundlesLayout', () => { } }); - return newBundlesLayout().generateLayout(['/root/index.js']).then(bundles => + return newBundlesLayout().getLayout('/root/index.js').then(bundles => stripPolyfills(bundles).then(resolvedBundles => expect(resolvedBundles).toEqual({ id: 'bundle.0', diff --git a/packager/react-packager/src/BundlesLayout/index.js b/packager/react-packager/src/BundlesLayout/index.js index c6da31573..d946cefe1 100644 --- a/packager/react-packager/src/BundlesLayout/index.js +++ b/packager/react-packager/src/BundlesLayout/index.js @@ -8,14 +8,33 @@ */ 'use strict'; +const Activity = require('../Activity'); + const _ = require('underscore'); const declareOpts = require('../lib/declareOpts'); +const fs = require('fs'); +const getCacheFilePath = require('../lib/getCacheFilePath'); +const loadCacheSync = require('../lib/loadCacheSync'); +const version = require('../../../../package.json').version; +const path = require('path'); const validateOpts = declareOpts({ dependencyResolver: { type: 'object', required: true, }, + resetCache: { + type: 'boolean', + default: false, + }, + cacheVersion: { + type: 'string', + default: '1.0', + }, + projectRoots: { + type: 'array', + required: true, + }, }); const BUNDLE_PREFIX = 'bundle'; @@ -29,19 +48,37 @@ class BundlesLayout { const opts = validateOpts(options); this._resolver = opts.dependencyResolver; + // Cache in which bundle is each module. this._moduleToBundle = Object.create(null); + + // Cache the bundles layouts for each entry point. This entries + // are not evicted unless the user explicitly specifies so as + // computing them is pretty expensive + this._layouts = Object.create(null); + + // TODO: watch for file creations and removals to update this caches + + this._cacheFilePath = this._getCacheFilePath(opts); + if (!opts.resetCache) { + this._loadCacheSync(this._cacheFilePath); + } else { + this._persistCacheEventually(); + } } - generateLayout(entryPaths, isDev) { + getLayout(entryPath, isDev) { + if (this._layouts[entryPath]) { + return this._layouts[entryPath]; + } var currentBundleID = 0; const rootBundle = { id: BUNDLE_PREFIX + '.' + currentBundleID++, modules: [], children: [], }; - var pending = [{paths: entryPaths, bundle: rootBundle}]; + var pending = [{paths: [entryPath], bundle: rootBundle}]; - return promiseWhile( + this._layouts[entryPath] = promiseWhile( () => pending.length > 0, () => rootBundle, () => { @@ -62,6 +99,9 @@ class BundlesLayout { if (dependencies.length > 0) { bundle.modules = dependencies; } + + // persist changes to layouts + this._persistCacheEventually(); }, index => { const pendingSyncDep = pendingSyncDeps.shift(); @@ -90,11 +130,71 @@ class BundlesLayout { ); }, ); + + return this._layouts[entryPath]; } getBundleIDForModule(path) { return this._moduleToBundle[path]; } + + _loadCacheSync(cachePath) { + const loadCacheId = Activity.startEvent('Loading bundles layout'); + const cacheOnDisk = loadCacheSync(cachePath); + + // TODO: create single-module bundles for unexistent modules + // TODO: remove modules that no longer exist + Object.keys(cacheOnDisk).forEach(entryPath => { + this._layouts[entryPath] = Promise.resolve(cacheOnDisk[entryPath]); + this._fillModuleToBundleMap(cacheOnDisk[entryPath]); + }); + + Activity.endEvent(loadCacheId); + } + + _fillModuleToBundleMap(bundle) { + bundle.modules.forEach(module => this._moduleToBundle[module] = bundle.id); + bundle.children.forEach(child => this._fillModuleToBundleMap(child)); + } + + _persistCacheEventually() { + _.debounce( + this._persistCache.bind(this), + 2000, + ); + } + + _persistCache() { + if (this._persisting !== null) { + return this._persisting; + } + + this._persisting = Promise + .all(_.values(this._layouts)) + .then(bundlesLayout => { + var json = Object.create(null); + Object.keys(this._layouts).forEach((p, i) => + json[p] = bundlesLayout[i] + ); + + return Promise.denodeify(fs.writeFile)( + this._cacheFilepath, + JSON.stringify(json), + ); + }) + .then(() => this._persisting = null); + + return this._persisting; + } + + _getCacheFilePath(options) { + return getCacheFilePath( + 'react-packager-bundles-cache-', + version, + options.projectRoots.join(',').split(path.sep).join('-'), + options.cacheVersion || '0', + ); + } } // Runs the body Promise meanwhile the condition callback is satisfied. diff --git a/packager/react-packager/src/Cache/__tests__/Cache-test.js b/packager/react-packager/src/Cache/__tests__/Cache-test.js index f4aef9147..8040e9e66 100644 --- a/packager/react-packager/src/Cache/__tests__/Cache-test.js +++ b/packager/react-packager/src/Cache/__tests__/Cache-test.js @@ -11,7 +11,9 @@ jest .dontMock('underscore') .dontMock('absolute-path') - .dontMock('../'); + .dontMock('../') + .dontMock('../../lib/loadCacheSync') + .dontMock('../../lib/getCacheFilePath'); jest .mock('os') diff --git a/packager/react-packager/src/Cache/index.js b/packager/react-packager/src/Cache/index.js index ae6d1aa35..708f8cbd7 100644 --- a/packager/react-packager/src/Cache/index.js +++ b/packager/react-packager/src/Cache/index.js @@ -8,17 +8,17 @@ */ 'use strict'; -var _ = require('underscore'); -var crypto = require('crypto'); -var declareOpts = require('../lib/declareOpts'); -var fs = require('fs'); -var isAbsolutePath = require('absolute-path'); -var path = require('path'); -var Promise = require('promise'); -var tmpdir = require('os').tmpDir(); -var version = require('../../../../package.json').version; +const Promise = require('promise'); +const _ = require('underscore'); +const declareOpts = require('../lib/declareOpts'); +const fs = require('fs'); +const getCacheFilePath = require('../lib/getCacheFilePath'); +const isAbsolutePath = require('absolute-path'); +const loadCacheSync = require('../lib/loadCacheSync'); +const path = require('path'); +const version = require('../../../../package.json').version; -var validateOpts = declareOpts({ +const validateOpts = declareOpts({ resetCache: { type: 'boolean', default: false, @@ -164,21 +164,7 @@ class Cache { _loadCacheSync(cachePath) { var ret = Object.create(null); - if (!fs.existsSync(cachePath)) { - return ret; - } - - var cacheOnDisk; - try { - cacheOnDisk = JSON.parse(fs.readFileSync(cachePath)); - } catch (e) { - if (e instanceof SyntaxError) { - console.warn('Unable to parse cache file. Will clear and continue.'); - fs.unlinkSync(cachePath); - return ret; - } - throw e; - } + var cacheOnDisk = loadCacheSync(cachePath); // Filter outdated cache and convert to promises. Object.keys(cacheOnDisk).forEach(key => { @@ -203,20 +189,13 @@ class Cache { } _getCacheFilePath(options) { - var hash = crypto.createHash('md5'); - hash.update(version); - - var roots = options.projectRoots.join(',').split(path.sep).join('-'); - hash.update(roots); - - var cacheVersion = options.cacheVersion || '0'; - hash.update(cacheVersion); - - hash.update(options.transformModulePath); - - var name = 'react-packager-cache-' + hash.digest('hex'); - - return path.join(tmpdir, name); + return getCacheFilePath( + 'react-packager-cache-', + version, + options.projectRoots.join(',').split(path.sep).join('-'), + options.cacheVersion || '0', + options.transformModulePath, + ); } } diff --git a/packager/react-packager/src/lib/getCacheFilePath.js b/packager/react-packager/src/lib/getCacheFilePath.js new file mode 100644 index 000000000..d5ef19b31 --- /dev/null +++ b/packager/react-packager/src/lib/getCacheFilePath.js @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const crypto = require('crypto'); +const path = require('path'); +const tmpdir = require('os').tmpDir(); + +function getCacheFilePath(args) { + args = Array.prototype.slice.call(args); + const prefix = args.shift(); + + let hash = crypto.createHash('md5'); + args.forEach(arg => hash.update(arg)); + + return path.join(tmpdir, prefix + hash.digest('hex')); +} + +module.exports = getCacheFilePath; diff --git a/packager/react-packager/src/lib/loadCacheSync.js b/packager/react-packager/src/lib/loadCacheSync.js new file mode 100644 index 000000000..64188365c --- /dev/null +++ b/packager/react-packager/src/lib/loadCacheSync.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const fs = require('fs'); + +function loadCacheSync(cachePath) { + if (!fs.existsSync(cachePath)) { + return Object.create(null); + } + + try { + return JSON.parse(fs.readFileSync(cachePath)); + } catch (e) { + if (e instanceof SyntaxError) { + console.warn('Unable to parse cache file. Will clear and continue.'); + fs.unlinkSync(cachePath); + return Object.create(null); + } + throw e; + } +} + +module.exports = loadCacheSync; From 250494acd34b023d9c0e901374c10b86e67a70c1 Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Mon, 31 Aug 2015 09:40:46 -0700 Subject: [PATCH 18/25] [react-packager] Fail loudly with errors from the transformer --- packager/react-packager/src/JSTransformer/index.js | 11 +++++++++++ .../src/SocketInterface/SocketClient.js | 8 +++++++- .../src/SocketInterface/SocketServer.js | 5 +++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js index 93dd5d01d..9bf191390 100644 --- a/packager/react-packager/src/JSTransformer/index.js +++ b/packager/react-packager/src/JSTransformer/index.js @@ -25,6 +25,9 @@ const MAX_CALLS_PER_WORKER = 600; // Worker will timeout if one of the callers timeout. const DEFAULT_MAX_CALL_TIME = 120000; +// How may times can we tolerate failures from the worker. +const MAX_RETRIES = 3; + const validateOpts = declareOpts({ projectRoots: { type: 'array', @@ -63,6 +66,7 @@ class Transformer { maxConcurrentCallsPerWorker: 1, maxCallsPerWorker: MAX_CALLS_PER_WORKER, maxCallTime: opts.transformTimeoutInterval, + maxRetries: MAX_RETRIES, }, opts.transformModulePath); this._transform = Promise.denodeify(this._workers); @@ -118,6 +122,13 @@ class Transformer { ); timeoutErr.type = 'TimeoutError'; throw timeoutErr; + } else if (err.type === 'ProcessTerminatedError') { + const uncaughtError = new Error( + 'Uncaught error in the transformer worker: ' + + this._opts.transformModulePath + ); + uncaughtError.type = 'ProcessTerminatedError'; + throw uncaughtError; } throw formatError(err, filePath); diff --git a/packager/react-packager/src/SocketInterface/SocketClient.js b/packager/react-packager/src/SocketInterface/SocketClient.js index 30b599b0f..e20e5b896 100644 --- a/packager/react-packager/src/SocketInterface/SocketClient.js +++ b/packager/react-packager/src/SocketInterface/SocketClient.js @@ -13,6 +13,10 @@ const Promise = require('promise'); const bser = require('bser'); const debug = require('debug')('ReactPackager:SocketClient'); const net = require('net'); +const path = require('path'); +const tmpdir = require('os').tmpdir(); + +const LOG_PATH = path.join(tmpdir, 'react-packager.log'); class SocketClient { static create(sockPath) { @@ -81,7 +85,9 @@ class SocketClient { delete this._resolvers[message.id]; if (message.type === 'error') { - resolver.reject(new Error(message.data)); + resolver.reject(new Error( + message.data + '\n' + 'See logs ' + LOG_PATH + )); } else { resolver.resolve(message.data); } diff --git a/packager/react-packager/src/SocketInterface/SocketServer.js b/packager/react-packager/src/SocketInterface/SocketServer.js index 2e91aaba4..d3cb752d1 100644 --- a/packager/react-packager/src/SocketInterface/SocketServer.js +++ b/packager/react-packager/src/SocketInterface/SocketServer.js @@ -71,6 +71,11 @@ class SocketServer { debug('request error', error); this._jobs--; this._reply(sock, m.id, 'error', error.stack); + + // Fatal error from JSTransformer transform workers. + if (error.type === 'ProcessTerminatedError') { + setImmediate(() => process.exit(1)); + } }; switch (m.type) { From 6f3849ea7bc3e79d02640ad7a3657806d525d601 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 31 Aug 2015 10:35:25 -0700 Subject: [PATCH 19/25] [RN] Disable TimersTest - sporadic failures on Travis Summary: Can't get any local failures to trigger. Travis failure: https://travis-ci.org/facebook/react-native/jobs/78064148 --- .../UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m index 267ed1409..ccf5e7f74 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/IntegrationTests.m @@ -57,7 +57,7 @@ expectErrorRegex:@"because shouldThrow"]; } -- (void)testTimers +- (void)DISABLED_testTimers // #8192477 { [_runner runTest:_cmd module:@"TimersTest"]; } From d080c8dc2bc7d333560284244b18fe3df58faac6 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 31 Aug 2015 10:00:07 -0700 Subject: [PATCH 20/25] [RN] fix TimerExample Summary: Pressing clear would crash if timer was unmounted. Fixed by not rendering clear button when timer unmounted. --- Examples/UIExplorer/TimerExample.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Examples/UIExplorer/TimerExample.js b/Examples/UIExplorer/TimerExample.js index 0bfd5d05a..b5923b350 100644 --- a/Examples/UIExplorer/TimerExample.js +++ b/Examples/UIExplorer/TimerExample.js @@ -181,8 +181,12 @@ exports.examples = [ render: function() { if (this.state.showTimer) { - var timer = - ; + var timer = [ + , + + ]; var toggleText = 'Unmount timer'; } else { var timer = null; @@ -191,9 +195,6 @@ exports.examples = [ return ( {timer} - From 81c2216d82633c9cd76927f7b7ae5be3b555332d Mon Sep 17 00:00:00 2001 From: James Ide Date: Mon, 31 Aug 2015 11:18:23 -0700 Subject: [PATCH 21/25] [RN Debugger] Don't try to handle messages without a method Summary: Some messages are special and are intended for the devtools, like `{$open: id}` and `{$error: id}`. The main debugger-ui page can't handle these and thinks something is wrong when `object.method` is undefined. This diff handles messages only if they specify a method. Fixes #2377 Closes https://github.com/facebook/react-native/pull/2405 Github Author: James Ide --- packager/debugger.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packager/debugger.html b/packager/debugger.html index 24f8aea55..3b905efe3 100644 --- a/packager/debugger.html +++ b/packager/debugger.html @@ -85,6 +85,10 @@ function connectToDebuggerProxy() { ws.onmessage = function(message) { var object = JSON.parse(message.data); + if (!object.method) { + return; + } + var sendReply = function(result) { ws.send(JSON.stringify({replyID: object.id, result: result})); }; From 94ae886060e0b30199ea3a499cfc68d34a919fbf Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 31 Aug 2015 11:17:53 -0700 Subject: [PATCH 22/25] [RN] add clarifying Android-only comment to Portal --- Libraries/Portal/Portal.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Libraries/Portal/Portal.js b/Libraries/Portal/Portal.js index 652e9ed6f..ac80e0410 100644 --- a/Libraries/Portal/Portal.js +++ b/Libraries/Portal/Portal.js @@ -18,6 +18,9 @@ var _portalRef: any; var lastUsedTag = 0; /* + * Note: Only intended for Android at the moment. Just use Modal in your iOS + * code. + * * A container that renders all the modals on top of everything else in the application. * * Portal makes it possible for application code to pass modal views all the way up to From d87480e9ac2d4ce1950a33a0d8bc16fe411422ca Mon Sep 17 00:00:00 2001 From: Amjad Masad Date: Mon, 31 Aug 2015 13:19:05 -0700 Subject: [PATCH 23/25] [react-pacakger] Fix failing test Summary: Fix failing test that matches the exact error string to match using `contains`. I was under the impression that jest tests were running in CI -- turns out not yet. --- .../src/SocketInterface/__tests__/SocketClient-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js b/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js index c898e9272..59477fcf1 100644 --- a/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js +++ b/packager/react-packager/src/SocketInterface/__tests__/SocketClient-test.js @@ -107,6 +107,6 @@ describe('SocketClient', () => { data: 'some error' }); - return promise.catch(m => expect(m.message).toBe('some error')); + return promise.catch(m => expect(m.message).toContain('some error')); }); }); From 9ad2c322c0affd0454c087d4e64edaad79f01f63 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 31 Aug 2015 13:50:24 -0700 Subject: [PATCH 24/25] [RN] improve elastic easing Summary: 1) Makes params more intuitive (only one now, bounciness, which maps intuitively to number of oscillations). 2) Satisfies boundary conditions (f(0) = 0, f(1) = 1) so animation actually goes where you tell it (before it would finish at a random location depending on the input params). 3) Simple test to verify boundary conditions. --- Libraries/Animated/Easing.js | 29 +++++++++++---------- Libraries/Animated/__tests__/Easing-test.js | 8 ++++++ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Libraries/Animated/Easing.js b/Libraries/Animated/Easing.js index ae90136d1..58969278b 100644 --- a/Libraries/Animated/Easing.js +++ b/Libraries/Animated/Easing.js @@ -59,21 +59,22 @@ class Easing { return Math.pow(2, 10 * (t - 1)); } - static elastic(a: number, p: number): (t: number) => number { - var tau = Math.PI * 2; - // flow isn't smart enough to figure out that s is always assigned to a - // number before being used in the returned function - var s: any; - if (arguments.length < 2) { - p = 0.45; + /** + * A simple elastic interaction, similar to a spring. Default bounciness + * is 1, which overshoots a little bit once. 0 bounciness doesn't overshoot + * at all, and bounciness of N > 1 will overshoot about N times. + * + * Wolfram Plots: + * + * http://tiny.cc/elastic_b_1 (default bounciness = 1) + * http://tiny.cc/elastic_b_3 (bounciness = 3) + */ + static elastic(bounciness: number): (t: number) => number { + if (arguments.length === 0) { + bounciness = 1; } - if (arguments.length) { - s = p / tau * Math.asin(1 / a); - } else { - a = 1; - s = p / 4; - } - return (t) => 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * tau / p); + var p = bounciness * Math.PI; + return (t) => 1 - Math.pow(Math.cos(t * Math.PI / 2), 3) * Math.cos(t * p); }; static back(s: number): (t: number) => number { diff --git a/Libraries/Animated/__tests__/Easing-test.js b/Libraries/Animated/__tests__/Easing-test.js index bee894d6f..b865250d7 100644 --- a/Libraries/Animated/__tests__/Easing-test.js +++ b/Libraries/Animated/__tests__/Easing-test.js @@ -71,6 +71,14 @@ describe('Easing', () => { } }); + it('should satisfy boundary conditions with elastic', () => { + for (var b = 0; b < 4; b += 0.3) { + var easing = Easing.elastic(b); + expect(easing(0)).toBe(0); + expect(easing(1)).toBe(1); + } + }); + function sampleEasingFunction(easing) { var DURATION = 300; var tickCount = Math.round(DURATION * 60 / 1000); From f1c9c5c2aff73fdeb9f01d0af1a75e391010b340 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Mon, 31 Aug 2015 14:31:43 -0700 Subject: [PATCH 25/25] [RN] Disable flaky testJavaScriptExecutorIsDeallocated --- Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m index dd50708e3..cd44b7229 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m @@ -141,7 +141,7 @@ RCT_EXPORT_METHOD(test:(__unused NSString *)a XCTAssertNil(weakMethod, @"RCTModuleMethod should have been deallocated"); } -- (void)testJavaScriptExecutorIsDeallocated +- (void)DISABLED_testJavaScriptExecutorIsDeallocated // flaky: #8195866 { __weak id weakExecutor; @autoreleasepool {