From e018aa3100d93abf0e222cd70bbcbb6ab248eced Mon Sep 17 00:00:00 2001 From: Adam Miskiewicz Date: Fri, 12 Feb 2016 08:09:43 -0800 Subject: [PATCH] Enable HMR Reviewed By: svcscm Differential Revision: D2932137 fb-gh-sync-id: 8bfab09aaac22ae498ac4fa896eee495111abc0d shipit-source-id: 8bfab09aaac22ae498ac4fa896eee495111abc0d --- Libraries/Utilities/HMRClient.js | 45 ++++++++++++------- React/Base/RCTBatchedBridge.m | 4 +- React/Executors/RCTJSCExecutor.m | 29 ++++++------ .../facebook/react/CoreModulesPackage.java | 2 + .../react/devsupport/DevSupportManager.java | 1 + .../devsupport/DevSupportManagerImpl.java | 23 +++++++++- .../devsupport/DisabledDevSupportManager.java | 5 +++ .../facebook/react/devsupport/HMRClient.java | 32 +++++++++++++ .../modules/core/ExceptionsManagerModule.java | 7 +++ .../src/main/jni/react/JSCExecutor.cpp | 22 ++++++++- 10 files changed, 137 insertions(+), 33 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/devsupport/HMRClient.java diff --git a/Libraries/Utilities/HMRClient.js b/Libraries/Utilities/HMRClient.js index ed55ee6b9..d9d8b79bd 100644 --- a/Libraries/Utilities/HMRClient.js +++ b/Libraries/Utilities/HMRClient.js @@ -10,6 +10,7 @@ */ 'use strict'; +const Platform = require('Platform'); const invariant = require('invariant'); const processColor = require('processColor'); @@ -18,23 +19,26 @@ const processColor = require('processColor'); * runtime to reflects those changes. */ const HMRClient = { - enable(platform, bundleEntry) { + enable(platform, bundleEntry, host, port) { invariant(platform, 'Missing required parameter `platform`'); invariant(bundleEntry, 'Missing required paramenter `bundleEntry`'); - - // TODO(martinb) receive host and port as parameters - const host = 'localhost'; - const port = '8081'; + invariant(host, 'Missing required paramenter `host`'); // need to require WebSocket inside of `enable` function because // this module is defined as a `polyfillGlobal`. // See `InitializeJavascriptAppEngine.js` const WebSocket = require('WebSocket'); - const activeWS = new WebSocket( - `ws://${host}:${port}/hot?platform=${platform}&` + - `bundleEntry=${bundleEntry.replace('.bundle', '.js')}` - ); + const wsHostPort = port !== null && port !== '' + ? `${host}:${port}` + : host; + + // Build the websocket url + const wsUrl = `ws://${wsHostPort}/hot?` + + `platform=${platform}&` + + `bundleEntry=${bundleEntry.replace('.bundle', '.js')}`; + + const activeWS = new WebSocket(wsUrl); activeWS.onerror = (e) => { throw new Error( `Hot loading isn't working because it cannot connect to the development server. @@ -50,10 +54,16 @@ Error: ${e.message}` ); }; activeWS.onmessage = ({data}) => { - const DevLoadingView = require('NativeModules').DevLoadingView; + let DevLoadingView = require('NativeModules').DevLoadingView; + if (!DevLoadingView) { + DevLoadingView = { + showMessage() {}, + hide() {}, + }; + } data = JSON.parse(data); - switch(data.type) { + switch (data.type) { case 'update-start': { DevLoadingView.showMessage( 'Hot Loading...', @@ -67,8 +77,13 @@ Error: ${e.message}` const sourceMappingURLs = data.body.sourceMappingURLs; const sourceURLs = data.body.sourceURLs; - const RCTRedBox = require('NativeModules').RedBox; - RCTRedBox && RCTRedBox.dismiss && RCTRedBox.dismiss(); + if (Platform.OS === 'ios') { + const RCTRedBox = require('NativeModules').RedBox; + RCTRedBox && RCTRedBox.dismiss && RCTRedBox.dismiss(); + } else { + const RCTExceptionsManager = require('NativeModules').ExceptionsManager; + RCTExceptionsManager && RCTExceptionsManager.dismissRedbox && RCTExceptionsManager.dismissRedbox(); + } modules.forEach((code, i) => { code = code + '\n\n' + sourceMappingURLs[i]; @@ -82,8 +97,8 @@ Error: ${e.message}` // on JSC we need to inject from native for sourcemaps to work // (Safari doesn't support `sourceMappingURL` nor any variant when // evaluating code) but on Chrome we can simply use eval - const injectFunction = typeof __injectHMRUpdate === 'function' - ? __injectHMRUpdate + const injectFunction = typeof global.nativeInjectHMRUpdate === 'function' + ? global.nativeInjectHMRUpdate : eval; injectFunction(code, sourceURLs[i]); diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index debc00d9a..49c4dcb7c 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -461,7 +461,9 @@ RCT_EXTERN NSArray *RCTGetModuleClasses(void); if (RCTGetURLQueryParam(self.bundleURL, @"hot")) { NSString *path = [self.bundleURL.path substringFromIndex:1]; // strip initial slash - [self enqueueJSCall:@"HMRClient.enable" args:@[@"ios", path]]; + NSString *host = self.bundleURL.host; + NSNumber *port = self.bundleURL.port; + [self enqueueJSCall:@"HMRClient.enable" args:@[@"ios", path, host, RCTNullIfNil(port)]]; } #endif diff --git a/React/Executors/RCTJSCExecutor.m b/React/Executors/RCTJSCExecutor.m index 176bffe87..db08f806e 100644 --- a/React/Executors/RCTJSCExecutor.m +++ b/React/Executors/RCTJSCExecutor.m @@ -341,6 +341,20 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) name:event object:nil]; } + + // Inject handler used by HMR + [self addSynchronousHookWithName:@"nativeInjectHMRUpdate" usingBlock:^(NSString *sourceCode, NSString *sourceCodeURL) { + RCTJSCExecutor *strongSelf = weakSelf; + if (!strongSelf.valid) { + return; + } + + JSStringRef execJSString = JSStringCreateWithUTF8CString(sourceCode.UTF8String); + JSStringRef jsURL = JSStringCreateWithUTF8CString(sourceCodeURL.UTF8String); + JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, NULL); + JSStringRelease(jsURL); + JSStringRelease(execJSString); + }]; #endif } @@ -513,21 +527,6 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context) RCTAssertParam(sourceURL); __weak RCTJSCExecutor *weakSelf = self; -#if RCT_DEV - _context.context[@"__injectHMRUpdate"] = ^(NSString *sourceCode, NSString *sourceCodeURL) { - RCTJSCExecutor *strongSelf = weakSelf; - - if (!strongSelf) { - return; - } - - JSStringRef execJSString = JSStringCreateWithUTF8CString(sourceCode.UTF8String); - JSStringRef jsURL = JSStringCreateWithUTF8CString(sourceCodeURL.UTF8String); - JSEvaluateScript(strongSelf->_context.ctx, execJSString, NULL, jsURL, 0, NULL); - JSStringRelease(jsURL); - JSStringRelease(execJSString); - }; -#endif [self executeBlockOnJavaScriptQueue:RCTProfileBlock((^{ RCTJSCExecutor *strongSelf = weakSelf; diff --git a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java index 07b18b066..d7b34e270 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java @@ -19,6 +19,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.ExceptionsManagerModule; +import com.facebook.react.devsupport.HMRClient; import com.facebook.react.modules.core.JSTimersExecution; import com.facebook.react.modules.core.RCTNativeAppEventEmitter; import com.facebook.react.modules.core.Timing; @@ -95,6 +96,7 @@ import com.facebook.systrace.Systrace; RCTNativeAppEventEmitter.class, AppRegistry.class, com.facebook.react.bridge.Systrace.class, + HMRClient.class, DebugComponentOwnershipModule.RCTDebugComponentOwnership.class); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java index 4f114ded6..4b27023c0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManager.java @@ -25,6 +25,7 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler { void addCustomDevOption(String optionName, DevOptionHandler optionHandler); void showNewJSError(String message, ReadableArray details, int errorCookie); void updateJSError(final String message, final ReadableArray details, final int errorCookie); + void hideRedboxDialog(); void showDevOptionsDialog(); void setDevSupportEnabled(boolean isDevSupportEnabled); boolean getDevSupportEnabled(); diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java index 120443eca..75d7282a7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java @@ -13,6 +13,8 @@ import javax.annotation.Nullable; import java.io.File; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.LinkedHashMap; import java.util.Locale; import java.util.concurrent.ExecutionException; @@ -40,7 +42,6 @@ import com.facebook.react.R; import com.facebook.react.bridge.CatalystInstance; import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler; import com.facebook.react.bridge.JavaJSExecutor; -import com.facebook.react.bridge.NativeModuleCallExceptionHandler; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.UiThreadUtil; @@ -216,6 +217,14 @@ public class DevSupportManagerImpl implements DevSupportManager { }); } + @Override + public void hideRedboxDialog() { + // dismiss redbox if exists + if (mRedBoxDialog != null) { + mRedBoxDialog.dismiss(); + } + } + private void showNewError( final String message, final StackFrame[] stack, @@ -522,6 +531,18 @@ public class DevSupportManagerImpl implements DevSupportManager { mDebugOverlayController = new DebugOverlayController(reactContext); } + if (mDevSettings.isHotModuleReplacementEnabled() && mCurrentContext != null) { + try { + URL sourceUrl = new URL(getSourceUrl()); + String path = sourceUrl.getPath().substring(1); // strip initial slash in path + String host = sourceUrl.getHost(); + int port = sourceUrl.getPort(); + mCurrentContext.getJSModule(HMRClient.class).enable("android", path, host, port); + } catch (MalformedURLException e) { + showNewJavaError(e.getMessage(), e); + } + } + reloadSettings(); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java index 54f5335cd..5388f98b7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/DisabledDevSupportManager.java @@ -46,6 +46,11 @@ public class DisabledDevSupportManager implements DevSupportManager { } + @Override + public void hideRedboxDialog() { + + } + @Override public void showDevOptionsDialog() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/devsupport/HMRClient.java b/ReactAndroid/src/main/java/com/facebook/react/devsupport/HMRClient.java new file mode 100644 index 000000000..8ef0e5e35 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/devsupport/HMRClient.java @@ -0,0 +1,32 @@ +/** + * 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. + */ + +package com.facebook.react.devsupport; + +import com.facebook.react.bridge.JavaScriptModule; + +/** + * JS module interface for HMRClient + * + * The HMR(Hot Module Replacement)Client allows for the application to receive updates + * from the packager server (over a web socket), allowing for injection of JavaScript to + * the running application (without a refresh). + */ +public interface HMRClient extends JavaScriptModule { + + /** + * Enable the HMRClient so that the client will receive updates + * from the packager server. + * @param platform The platform in which HMR updates will be enabled. Should be "android". + * @param bundleEntry The path to the bundle entry file (e.g. index.ios.bundle). + * @param host The host that the HMRClient should communicate with. + * @param port The port that the HMRClient should communicate with on the host. + */ + void enable(String platform, String bundleEntry, String host, int port); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java index ca12dd6c5..5b185d64b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/core/ExceptionsManagerModule.java @@ -77,4 +77,11 @@ public class ExceptionsManagerModule extends BaseJavaModule { mDevSupportManager.updateJSError(title, details, exceptionId); } } + + @ReactMethod + public void dismissRedbox() { + if (mDevSupportManager.getDevSupportEnabled()) { + mDevSupportManager.hideRedboxDialog(); + } + } } diff --git a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp index a951c5d8f..03d28562c 100644 --- a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp +++ b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp @@ -59,6 +59,13 @@ static JSValueRef nativePerformanceNow( size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception); +static JSValueRef nativeInjectHMRUpdate( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); static std::string executeJSCallWithJSC( JSGlobalContextRef ctx, @@ -93,6 +100,7 @@ JSCExecutor::JSCExecutor(FlushImmediateCallback cb, const std::string& cacheDir) installGlobalFunction(m_context, "nativeStartWorker", nativeStartWorker); installGlobalFunction(m_context, "nativePostMessageToWorker", nativePostMessageToWorker); installGlobalFunction(m_context, "nativeTerminateWorker", nativeTerminateWorker); + installGlobalFunction(m_context, "nativeInjectHMRUpdate", nativeInjectHMRUpdate); installGlobalFunction(m_context, "nativeLoggingHook", JSLogging::nativeHook); @@ -140,7 +148,7 @@ void JSCExecutor::executeApplicationScript( if (!jsSourceURL) { evaluateScript(m_context, jsScript, jsSourceURL); } else { - // If we're evaluating a script, get the device's cache dir + // If we're evaluating a script, get the device's cache dir // in which a cache file for that script will be stored. evaluateScript(m_context, jsScript, jsSourceURL, m_deviceCacheDir.c_str()); } @@ -471,4 +479,16 @@ static JSValueRef nativePerformanceNow( return JSValueMakeNumber(ctx, (nano / (double)NANOSECONDS_IN_MILLISECOND)); } +static JSValueRef nativeInjectHMRUpdate( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], JSValueRef *exception) { + String execJSString = Value(ctx, arguments[0]).toString(); + String jsURL = Value(ctx, arguments[1]).toString(); + evaluateScript(ctx, execJSString, jsURL); + return JSValueMakeUndefined(ctx); +} + } }