Permalink
Browse files

Enable HMR

Reviewed By: svcscm

Differential Revision: D2932137

fb-gh-sync-id: 8bfab09aaac22ae498ac4fa896eee495111abc0d
shipit-source-id: 8bfab09aaac22ae498ac4fa896eee495111abc0d
  • Loading branch information...
skevy authored and facebook-github-bot-4 committed Feb 12, 2016
1 parent 8eddead commit e018aa3100d93abf0e222cd70bbcbb6ab248eced
@@ -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]);
@@ -461,7 +461,9 @@ - (void)executeSourceCode:(NSData *)sourceCode
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
@@ -341,6 +341,20 @@ - (void)setUp
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 @@ - (void)executeApplicationScript:(NSData *)script
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;
@@ -19,6 +19,7 @@
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 @@
RCTNativeAppEventEmitter.class,
AppRegistry.class,
com.facebook.react.bridge.Systrace.class,
+ HMRClient.class,
DebugComponentOwnershipModule.RCTDebugComponentOwnership.class);
}
@@ -25,6 +25,7 @@
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();
@@ -13,6 +13,8 @@
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.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 void run() {
});
}
+ @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 @@ private void resetCurrentContext(@Nullable ReactContext reactContext) {
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();
}
@@ -46,6 +46,11 @@ public void updateJSError(String message, ReadableArray details, int errorCookie
}
+ @Override
+ public void hideRedboxDialog() {
+
+ }
+
@Override
public void showDevOptionsDialog() {
@@ -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);
+}
@@ -77,4 +77,11 @@ public void updateExceptionMessage(String title, ReadableArray details, int exce
mDevSupportManager.updateJSError(title, details, exceptionId);
}
}
+
+ @ReactMethod
+ public void dismissRedbox() {
+ if (mDevSupportManager.getDevSupportEnabled()) {
+ mDevSupportManager.hideRedboxDialog();
+ }
+ }
}
@@ -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);
+}
+
} }

21 comments on commit e018aa3

@cpunion

This comment has been minimized.

Show comment
Hide comment
@cpunion

cpunion Feb 22, 2016

Contributor

@skevy

How to use HMR? I just added &hot=true to bundle url, it's no effects.

Then I forced to enable it in RCTBatchedBridge.m, it generated a warning can't be hot reloaded because it doesn't provide accept callback hook. Reload the app to get the updates.

Then I added a module.hot.accept(() => {}) to module, no warning, but no HMR effects.

Is there an example for HMR?

Contributor

cpunion replied Feb 22, 2016

@skevy

How to use HMR? I just added &hot=true to bundle url, it's no effects.

Then I forced to enable it in RCTBatchedBridge.m, it generated a warning can't be hot reloaded because it doesn't provide accept callback hook. Reload the app to get the updates.

Then I added a module.hot.accept(() => {}) to module, no warning, but no HMR effects.

Is there an example for HMR?

@Ranatchai

This comment has been minimized.

Show comment
Hide comment
@maluramichael

This comment has been minimized.

Show comment
Hide comment
@maluramichael

maluramichael Feb 22, 2016

@cpunion +1 i would like to know this too

@cpunion +1 i would like to know this too

@tomauty

This comment has been minimized.

Show comment
Hide comment
@tomauty

tomauty Feb 22, 2016

Contributor

Same :)

Contributor

tomauty replied Feb 22, 2016

Same :)

@milasevicius

This comment has been minimized.

Show comment
Hide comment
@milasevicius

milasevicius Feb 22, 2016

+1 for example how to use this

+1 for example how to use this

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Feb 23, 2016

Contributor

The feature is not enabled yet on master nor any RC or released version because we want to add support for redux before releasing it. Support for that is coming very soon, stay tuned.
If you wanna play with it just revert f514409 for Android support or tweak hotLoadingAvailable on RCTDevMenu.m for iOS support. We'll add docs and blog when it's officially ready :)

Contributor

martinbigio replied Feb 23, 2016

The feature is not enabled yet on master nor any RC or released version because we want to add support for redux before releasing it. Support for that is coming very soon, stay tuned.
If you wanna play with it just revert f514409 for Android support or tweak hotLoadingAvailable on RCTDevMenu.m for iOS support. We'll add docs and blog when it's officially ready :)

@cpunion

This comment has been minimized.

Show comment
Hide comment
@cpunion

cpunion Feb 23, 2016

Contributor

@martinbigio Great! THANK YOU!

Contributor

cpunion replied Feb 23, 2016

@martinbigio Great! THANK YOU!

@tomauty

This comment has been minimized.

Show comment
Hide comment
@tomauty

tomauty Feb 23, 2016

Contributor

"Support for redux" is exciting and scary. Already a few releases into my current redux app :o

Contributor

tomauty replied Feb 23, 2016

"Support for redux" is exciting and scary. Already a few releases into my current redux app :o

@JohnyDays

This comment has been minimized.

Show comment
Hide comment
@JohnyDays

JohnyDays Feb 23, 2016

Contributor

Enabling it on iOS was a breeze 👍 🆒 🆒, on android I can't seem to figure out how to do it without building from source. Is that needed? Never worked with gradle before 😢.

Contributor

JohnyDays replied Feb 23, 2016

Enabling it on iOS was a breeze 👍 🆒 🆒, on android I can't seem to figure out how to do it without building from source. Is that needed? Never worked with gradle before 😢.

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Feb 23, 2016

Contributor

@JohnyDays reverting f514409 should just work on Android

Contributor

martinbigio replied Feb 23, 2016

@JohnyDays reverting f514409 should just work on Android

@milasevicius

This comment has been minimized.

Show comment
Hide comment
@milasevicius

milasevicius Feb 23, 2016

O have turned hot reload on IOS developer menu, but it doesnt work. What am I missing?

O have turned hot reload on IOS developer menu, but it doesnt work. What am I missing?

@JohnyDays

This comment has been minimized.

Show comment
Hide comment
@JohnyDays

JohnyDays Feb 23, 2016

Contributor

I have my android project, but the react-native libraries as imported as binaries (as far as I can see). There is no reference to those files in the file-tree (the react-native library java files)

Contributor

JohnyDays replied Feb 23, 2016

I have my android project, but the react-native libraries as imported as binaries (as far as I can see). There is no reference to those files in the file-tree (the react-native library java files)

@JohnyDays

This comment has been minimized.

Show comment
Hide comment
@JohnyDays

JohnyDays Feb 23, 2016

Contributor

BTW: Ios is working like a charm 👍 been using it this afternoon, only getting some minor warnings.
Although, if I have HMR enabled in my iOS app, the android "Live Reload" option seems to stop working (for the same packager instance)

Contributor

JohnyDays replied Feb 23, 2016

BTW: Ios is working like a charm 👍 been using it this afternoon, only getting some minor warnings.
Although, if I have HMR enabled in my iOS app, the android "Live Reload" option seems to stop working (for the same packager instance)

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Feb 24, 2016

Contributor

if I have HMR enabled in my iOS app, the android "Live Reload" option seems to stop working

That's expected. When HMR is enabled we don't do live reload to optimize the HMR code path. Do you develop both iOS and Android at the same time? Maybe we should envision supporting Hot Loading enabled on more than a single client at the same time.

Contributor

martinbigio replied Feb 24, 2016

if I have HMR enabled in my iOS app, the android "Live Reload" option seems to stop working

That's expected. When HMR is enabled we don't do live reload to optimize the HMR code path. Do you develop both iOS and Android at the same time? Maybe we should envision supporting Hot Loading enabled on more than a single client at the same time.

@JohnyDays

This comment has been minimized.

Show comment
Hide comment
@JohnyDays

JohnyDays Feb 24, 2016

Contributor

I do, I develop with 2 different emulators (genymotion, ios emulator) at the same time, one with hot reloading (iOS) and the other (android) i was attempting to use live reload (because i haven't been able to setup hot reload yet 😢 ). I also sometimes connect more devices and use those, to test simultaneously on all platforms.
Is it a big change to support multiple outputs? I think it'd be a delightful use experience to work like that, cheers 🍺

Contributor

JohnyDays replied Feb 24, 2016

I do, I develop with 2 different emulators (genymotion, ios emulator) at the same time, one with hot reloading (iOS) and the other (android) i was attempting to use live reload (because i haven't been able to setup hot reload yet 😢 ). I also sometimes connect more devices and use those, to test simultaneously on all platforms.
Is it a big change to support multiple outputs? I think it'd be a delightful use experience to work like that, cheers 🍺

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Feb 24, 2016

Contributor

No, it's not a big change, just need to refactor attachHRMServer.js to keep track of multiple clients. Wanna submit a PR for that? :).

Contributor

martinbigio replied Feb 24, 2016

No, it's not a big change, just need to refactor attachHRMServer.js to keep track of multiple clients. Wanna submit a PR for that? :).

@JohnyDays

This comment has been minimized.

Show comment
Hide comment
@JohnyDays

JohnyDays Feb 25, 2016

Contributor

Thanks for pointing me in the right direction, I'll check it out

Contributor

JohnyDays replied Feb 25, 2016

Thanks for pointing me in the right direction, I'll check it out

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Feb 25, 2016

Contributor

I'm happy to help if you have any question :)

Contributor

martinbigio replied Feb 25, 2016

I'm happy to help if you have any question :)

@JohnyDays

This comment has been minimized.

Show comment
Hide comment
@JohnyDays

JohnyDays Feb 25, 2016

Contributor

It's happening 👍 https://gist.github.com/JohnyDays/fdbab1f031f0218aee8d
I do have a question, is it safe to assume the same bundleURL for each platform?
That would probably simplify / make it faster. I'm leaning towards the safer, less perfomant option right now

Contributor

JohnyDays replied Feb 25, 2016

It's happening 👍 https://gist.github.com/JohnyDays/fdbab1f031f0218aee8d
I do have a question, is it safe to assume the same bundleURL for each platform?
That would probably simplify / make it faster. I'm leaning towards the safer, less perfomant option right now

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Feb 25, 2016

Contributor

Awesome, thanks for looking into it!.

I don't think you want to assume that. Ideally the packager should be able to support HMR even if you're developing different apps (even on different platforms if you want). I think the code should actually be easier if you remove that assumption :).

Each client, identified by the bundleURL (entryFile + platform) would cache all it's dependencies information (dependenciesCache, dependenciesModulesCache, shallowDependencies) and when a file is changed you'd simply have to iterate over all the clients and do what we're currently doing. Does this makes sense?

Contributor

martinbigio replied Feb 25, 2016

Awesome, thanks for looking into it!.

I don't think you want to assume that. Ideally the packager should be able to support HMR even if you're developing different apps (even on different platforms if you want). I think the code should actually be easier if you remove that assumption :).

Each client, identified by the bundleURL (entryFile + platform) would cache all it's dependencies information (dependenciesCache, dependenciesModulesCache, shallowDependencies) and when a file is changed you'd simply have to iterate over all the clients and do what we're currently doing. Does this makes sense?

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Feb 25, 2016

Contributor

Also, maybe we should move the discussion to a separate issue to avoid spamming people here :D.

Contributor

martinbigio replied Feb 25, 2016

Also, maybe we should move the discussion to a separate issue to avoid spamming people here :D.

Please sign in to comment.