Permalink
Browse files

Hot Loading E2E basic flow

Summary:
public

Implement all the necessary glue code for several diffs submitted before to get Hot Loading work end to end:

- Simplify `HMRClient`: we don't need to make it stateful allowing to enable and disable it because both when we enable and disable the interface we need to reload the bundle.
- On the native side we introduced a singleton to process the bundle URL. This new class might alter the url to include the `hot` attribute. I'm not 100% sure this is the best way to implement this but we cannot use `CTLSettings` for this as it's are not available on oss and I didn't want to contaminate `RCTBridge` with something specific to hot loading. Also, we could potentially use this processor for other things in the future. Please let me know if you don't like this approach or you have a better idea :).
- Use this processor to alter the default bundle URL and request a `hot` bundle when hot loading is enabled. Also make sure to enable the HMR interface when the client activates it on the dev menu.
- Add packager `hot` option.
- Include gaeron's `react-transform` on Facebook's JS transformer.

The current implementation couples a bit React Native to this feature because `react-transform-hmr` is required on `InitializeJavaScriptAppEngine`. Ideally, the packager should accept an additional list of requires and include them on the bundle among all their dependencies. Note this is not the same as the option `runBeforeMainModule` as that one only adds a require to the provided module but doesn't include all the dependencies that module amy have that the entry point doesn't. I'll address this in a follow up task to enable asap hot loading (9536142)

I had to remove 2 `.babelrc` files from `react-proxy` and `react-deep-force-update`. There's an internal task for fixing the underlaying issue to avoid doing this horrible hack (t9515889).

Reviewed By: vjeux

Differential Revision: D2790806

fb-gh-sync-id: d4b78a2acfa071d6b3accc2e6716ef5611ad4fda
  • Loading branch information...
martinbigio authored and facebook-github-bot-6 committed Dec 29, 2015
1 parent 24c908e commit 4ffb24164712c0f0cafc7c097a0cdbc80e2c04d1
@@ -26,8 +26,7 @@ BatchedBridge.registerCallableModule('Systrace', Systrace);
BatchedBridge.registerCallableModule('JSTimersExecution', JSTimersExecution);
if (__DEV__) {
const HMRClient = require('HMRClient');
BatchedBridge.registerCallableModule('HMRClient', HMRClient);
BatchedBridge.registerCallableModule('HMRClient', require('HMRClient'));
}
// Wire up the batched bridge on the global object so that we can call into it.
@@ -216,3 +216,8 @@ if (__DEV__) {
}
require('RCTDeviceEventEmitter');
require('PerformanceLogger');
if (__DEV__) {
// include this transform and it's dependencies on the bundle on dev mode
require('react-transform-hmr');
}
@@ -10,30 +10,26 @@
*/
'use strict';
let _activeWS;
/**
* HMR Client that receives from the server HMR updates and propagates them
* runtime to reflects those changes.
*/
const HMRClient = {
setEnabled(enabled) {
if (_activeWS && _activeWS) {
_activeWS.close();
_activeWS = null;
}
enable() {
// need to require WebSocket inside of `enable` function because the
// this module is defined as a `polyfillGlobal`.
// See `InitializeJavascriptAppEngine.js`
const WebSocket = require('WebSocket');
if (enabled) {
// TODO(martinb): parametrize the url and receive entryFile to minimize
// the number of updates we want to receive from the server.
_activeWS = new WebSocket('ws://localhost:8081/hot');
_activeWS.onerror = (e) => {
console.error('[Hot Module Replacement] Unexpected error', e);
};
_activeWS.onmessage = (m) => {
// TODO(martinb): inject HMR update
};
}
// TODO(martinb): parametrize the url and receive entryFile to minimize
// the number of updates we want to receive from the server.
const activeWS = new WebSocket('ws://localhost:8081/hot');
activeWS.onerror = (e) => {
console.error('[Hot Module Replacement] Unexpected error', e);
};
activeWS.onmessage = (m) => {
eval(m.data); // eslint-disable-line no-eval
};
},
};
View
@@ -17,6 +17,7 @@
#import "RCTLog.h"
#import "RCTPerformanceLogger.h"
#import "RCTUtils.h"
#import "RCTBundleURLProcessor.h"
NSString *const RCTReloadNotification = @"RCTReloadNotification";
NSString *const RCTJavaScriptWillStartLoadingNotification = @"RCTJavaScriptWillStartLoadingNotification";
@@ -257,6 +258,7 @@ - (void)setUp
RCTAssertMainThread();
_bundleURL = [self.delegate sourceURLForBridge:self] ?: _bundleURL;
_bundleURL = [[RCTBundleURLProcessor sharedProcessor] process: _bundleURL];
// Sanitize the bundle URL
_bundleURL = [RCTConvert NSURL:_bundleURL.absoluteString];
@@ -0,0 +1,18 @@
/**
* 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.
*/
@interface RCTBundleURLProcessor : NSObject
+ (id)sharedProcessor;
- (NSString *)getQueryStringValue:(NSString *)attribute;
- (void)setQueryStringValue:(NSString *)value forAttribute:(NSString *)attribute;
- (NSURL *)process:(NSURL *)url;
@end
@@ -0,0 +1,73 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import <Foundation/Foundation.h>
#import "RCTBundleURLProcessor.h"
@implementation RCTBundleURLProcessor
NSDictionary *_qsAttributes;
+ (id)sharedProcessor
{
static RCTBundleURLProcessor *sharedProcessor = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedProcessor = [self new];
});
return sharedProcessor;
}
- (instancetype)init
{
// dictionary with additional query string attributes that will get appended
// to the bundle URL
_qsAttributes = [NSMutableDictionary new];
return self;
}
- (NSString *)getQueryStringValue:(NSString *)attribute
{
return [_qsAttributes valueForKey:attribute];
}
- (void)setQueryStringValue:(NSString *)value forAttribute:(NSString *)attribute
{
[_qsAttributes setValue:value forKey:attribute];
}
- (NSURL *)process:(NSURL *)url
{
if (url.isFileURL || [_qsAttributes count] == 0) {
return url;
}
// append either `?` or `&` depending on whether there are query string
// attibutes or not.
NSString *urlString = url.absoluteString;
if ([urlString rangeOfString:@"?"].location == NSNotFound) {
urlString = [urlString stringByAppendingString:@"?"];
} else {
urlString = [urlString stringByAppendingString:@"&"];
}
// array with new query string attributes
NSMutableArray *parts = [NSMutableArray new];
for (id attribute in _qsAttributes) {
if ([urlString rangeOfString:[NSString stringWithFormat:@"%@=", attribute]].location != NSNotFound) {
[NSException raise:@"Cannot override attribute" format:@"Attribute %@ is already present in url: %@", attribute, url.absoluteString];
}
[parts addObject:[NSString stringWithFormat:@"%@=%@", attribute, _qsAttributes[attribute]]];
}
return [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", urlString, [parts componentsJoinedByString:@"&"]]];
}
@end
@@ -23,6 +23,7 @@
#import "RCTPerformanceLogger.h"
#import "RCTUtils.h"
#import "RCTJSCProfiler.h"
#import "RCTBundleURLProcessor.h"
static NSString *const RCTJSCProfilerEnabledDefaultsKey = @"RCTJSCProfilerEnabled";
static NSString *const RCTHotLoadingEnabledDefaultsKey = @"RCTHotLoadingEnabled";
@@ -146,9 +147,20 @@ static void RCTInstallJSCProfiler(RCTBridge *bridge, JSContextRef context)
static void RCTInstallHotLoading(RCTBridge *bridge, RCTJSCExecutor *executor)
{
[bridge.devMenu addItem:[RCTDevMenuItem toggleItemWithKey:RCTHotLoadingEnabledDefaultsKey title:@"Enable Hot Loading" selectedTitle:@"Disable Hot Loading" handler:^(BOOL enabled) {
[bridge.devMenu addItem:[RCTDevMenuItem toggleItemWithKey:RCTHotLoadingEnabledDefaultsKey title:@"Enable Hot Loading" selectedTitle:@"Disable Hot Loading" handler:^(BOOL enabledOnCurrentBundle) {
[executor executeBlockOnJavaScriptQueue:^{
[bridge enqueueJSCall:@"HMRClient.setEnabled" args:@[enabled ? @YES : @NO]];
NSString *enabledQS = [[RCTBundleURLProcessor sharedProcessor] getQueryStringValue:@"hot"];
BOOL enabledOnConfig = (enabledQS != nil && [enabledQS isEqualToString:@"true"]) ? YES : NO;
// reload bundle when user change Hot Loading setting
if (enabledOnConfig != enabledOnCurrentBundle) {
[[RCTBundleURLProcessor sharedProcessor] setQueryStringValue:enabledOnCurrentBundle ? @"true" : @"false" forAttribute:@"hot"];
[bridge reload];
}
if (enabledOnCurrentBundle) {
[bridge enqueueJSCall:@"HMRClient.enable" args:@[@YES]];
}
}];
}]];
}
@@ -14,6 +14,11 @@
*/
function attachHMRServer({httpServer, path, packagerServer}) {
let activeWS;
function disconnect() {
activeWS = null;
}
packagerServer.addFileChangeListener(filename => {
if (!activeWS) {
return;
@@ -40,12 +45,10 @@ function attachHMRServer({httpServer, path, packagerServer}) {
ws.on('error', e => {
console.error('[Hot Module Replacement] Unexpected error', e);
disconnect();
});
ws.on('close', () => {
console.log('[Hot Module Replacement] Client disconnected');
activeWS = null;
});
ws.on('close', () => disconnect());
});
}
@@ -137,7 +137,7 @@ class Bundler {
this._assetServer = opts.assetServer;
if (opts.getTransformOptionsModulePath) {
this._getTransformOptions = require(opts.getTransformOptionsModulePath);
this._getTransformOptionsModule = require(opts.getTransformOptionsModulePath);
}
}
@@ -158,6 +158,7 @@ class Bundler {
dev: isDev,
platform,
unbundle: isUnbundle,
hot: hot,
}) {
// Const cannot have the same name as the method (babel/babel#2834)
const bbundle = new Bundle(sourceMapUrl);
@@ -194,7 +195,8 @@ class Bundler {
bbundle,
response,
module,
platform
platform,
hot,
).then(transformed => {
if (bar) {
bar.tick();
@@ -286,12 +288,16 @@ class Bundler {
return Promise.all([
module.getName(),
this._transformer.loadFileAndTransform(path.resolve(entryFile)),
this._transformer.loadFileAndTransform(
path.resolve(entryFile),
// TODO(martinb): pass non null main (t9527509)
this._getTransformOptions({main: null}, {hot: true}),
),
]).then(([moduleName, transformedSource]) => {
return (`
__accept(
'${moduleName}',
function(global, require, module, exports) {
'${moduleName}',
function(global, require, module, exports) {
${transformedSource.code}
}
);
@@ -340,7 +346,7 @@ class Bundler {
);
}
_transformModule(bundle, response, module, platform = null) {
_transformModule(bundle, response, module, platform = null, hot = false) {
if (module.isAsset_DEPRECATED()) {
return this.generateAssetModule_DEPRECATED(bundle, module);
} else if (module.isAsset()) {
@@ -350,8 +356,10 @@ class Bundler {
} else {
return this._transformer.loadFileAndTransform(
path.resolve(module.path),
this._getTransformOptions ?
this._getTransformOptions({bundle, module, platform}) : {}
this._getTransformOptions(
{bundleEntry: bundle.getMainModuleId(), modulePath: module.path},
{hot: hot},
),
);
}
}
@@ -445,6 +453,14 @@ class Bundler {
});
});
}
_getTransformOptions(config, options) {
const transformerOptions = this._getTransformOptionsModule
? this._getTransformOptionsModule(config)
: null;
return {...options, ...transformerOptions};
}
}
function generateJSONModule(module) {
@@ -89,6 +89,7 @@ class Resolver {
// should work after this release and we can
// remove it from here.
'parse',
'react-transform-hmr',
],
platforms: ['ios', 'android'],
fileWatcher: opts.fileWatcher,
@@ -16,7 +16,7 @@
hot: {
acceptCallback: null,
accept: function(callback) {
this.acceptCallback = callback;
modules[id].module.hot.acceptCallback = callback;
}
}
});
@@ -97,12 +97,28 @@
if (__DEV__) { // HMR
function accept(id, factory) {
var mod = modules[id];
if (!mod) {
console.error(
'Cannot accept unknown module `' + id + '`. Make sure you\'re not ' +
'trying to modify something else other than a module ' +
'(i.e.: a polyfill).'
);
}
if (!mod.module.hot) {
console.error(
'Cannot accept module because Hot Module Replacement ' +
'API was not installed.'
);
}
if (mod.module.hot.acceptCallback) {
mod.factory = factory;
mod.isInitialized = false;
require(id);
mod.hot.acceptCallback();
mod.module.hot.acceptCallback();
} else {
console.log(
'[HMR] Module `' + id + '` cannot be accepted. ' +
@@ -109,7 +109,11 @@ const bundleOpts = declareOpts({
unbundle: {
type: 'boolean',
default: false,
}
},
hot: {
type: 'boolean',
default: false,
},
});
const hmrBundleOpts = declareOpts({
@@ -501,6 +505,7 @@ class Server {
entryFile: entryFile,
dev: this._getBoolOptionFromQuery(urlObj.query, 'dev', true),
minify: this._getBoolOptionFromQuery(urlObj.query, 'minify'),
hot: this._getBoolOptionFromQuery(urlObj.query, 'hot', false),
runModule: this._getBoolOptionFromQuery(urlObj.query, 'runModule', true),
inlineSourceMap: this._getBoolOptionFromQuery(
urlObj.query,

8 comments on commit 4ffb241

@satya164

This comment has been minimized.

Show comment
Hide comment
@satya164

satya164 Dec 29, 2015

Collaborator

What about Android though?

Collaborator

satya164 replied Dec 29, 2015

What about Android though?

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Dec 29, 2015

Contributor

I'm working on iOS first. The amount of native code is minimum. We only need the code to show the option on the menu and the one to make the URL include hot=true when Hot Loading is enabled. Wanna send a PR for that? :)

Contributor

martinbigio replied Dec 29, 2015

I'm working on iOS first. The amount of native code is minimum. We only need the code to show the option on the menu and the one to make the URL include hot=true when Hot Loading is enabled. Wanna send a PR for that? :)

@satya164

This comment has been minimized.

Show comment
Hide comment
@satya164

satya164 Dec 29, 2015

Collaborator

@martinbigio Sure. Will try to send a PR asap. This is really exciting :)

Collaborator

satya164 replied Dec 29, 2015

@martinbigio Sure. Will try to send a PR asap. This is really exciting :)

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Dec 29, 2015

Contributor

@satya164 that's awesome!

I'm planning to land the packager internal transform pipeline to move Hot Loading into the packager instead of the transformer. Hot Loading won't be available until we land that (the PR is already accepted but I to rebase and take another look as it's been sitting there for a month or so). I'm currently rushing on fixing lots of edge cases to make this more usable internally. I'll try do everything we need so that we can use this on open source so that everyone can contribute to it asap :)

Contributor

martinbigio replied Dec 29, 2015

@satya164 that's awesome!

I'm planning to land the packager internal transform pipeline to move Hot Loading into the packager instead of the transformer. Hot Loading won't be available until we land that (the PR is already accepted but I to rebase and take another look as it's been sitting there for a month or so). I'm currently rushing on fixing lots of edge cases to make this more usable internally. I'll try do everything we need so that we can use this on open source so that everyone can contribute to it asap :)

@satya164

This comment has been minimized.

Show comment
Hide comment
@satya164

satya164 Jan 3, 2016

Collaborator

@martinbigio Sent a PR for Android. I don't know Objective-C, so no iOS :(

Collaborator

satya164 replied Jan 3, 2016

@martinbigio Sent a PR for Android. I don't know Objective-C, so no iOS :(

@datapimp

This comment has been minimized.

Show comment
Hide comment
@datapimp

datapimp Jan 15, 2016

@martinbigio please let me know if you need help testing, QA'ing, if you need me to do your laundry or run errands for you. Whatever you need sir. This would be such a great feature to have.

datapimp replied Jan 15, 2016

@martinbigio please let me know if you need help testing, QA'ing, if you need me to do your laundry or run errands for you. Whatever you need sir. This would be such a great feature to have.

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Jan 15, 2016

Contributor

@datapimp great timing to comment on this! Last night I landed one last commit to enable this on open-source. There're still a few high level things that need to be done and a few KPs but it's mostly working.

I'd love to get your feedback and help to get this to the finish line! Here're are some steps to give it a try:

  1. $ react-native init test; cd test
  2. Edit RCTDevMenu.m and return YES on the method hotLoadingAvailable.
  3. Open Xcode and run the app, the option to enable/disable Hot Loading should be on the dev menu.
  4. Have fun

I'll create issues for the high level tasks and KPs and cc you guys. Thanks a lot, and let's make React Native even more awesome!

Contributor

martinbigio replied Jan 15, 2016

@datapimp great timing to comment on this! Last night I landed one last commit to enable this on open-source. There're still a few high level things that need to be done and a few KPs but it's mostly working.

I'd love to get your feedback and help to get this to the finish line! Here're are some steps to give it a try:

  1. $ react-native init test; cd test
  2. Edit RCTDevMenu.m and return YES on the method hotLoadingAvailable.
  3. Open Xcode and run the app, the option to enable/disable Hot Loading should be on the dev menu.
  4. Have fun

I'll create issues for the high level tasks and KPs and cc you guys. Thanks a lot, and let's make React Native even more awesome!

@martinbigio

This comment has been minimized.

Show comment
Hide comment
@martinbigio

martinbigio Jan 15, 2016

Contributor

@datapimp I just opened all this issues: 5338, 5339, 5340, 5342, 5343 and 5344. At this point I feel like we need more people to try it to get a better sense of how far we're from making this fully available. If you want to give it a try and give some feedback that would be great!. Also, of course, we'll all be super happy if you want to contribute ;)

Contributor

martinbigio replied Jan 15, 2016

@datapimp I just opened all this issues: 5338, 5339, 5340, 5342, 5343 and 5344. At this point I feel like we need more people to try it to get a better sense of how far we're from making this fully available. If you want to give it a try and give some feedback that would be great!. Also, of course, we'll all be super happy if you want to contribute ;)

Please sign in to comment.