Add native module for loading split JS bundles in development

Reviewed By: mdvacca, cpojer

Differential Revision: D22001709

fbshipit-source-id: 4e378fd6ae90268e7db9092a71628205b9f7c37d
This commit is contained in:
Oleksandr Melnykov
2020-06-17 09:10:51 -07:00
committed by Facebook GitHub Bot
parent 2e2c881147
commit fca3a39da5
15 changed files with 272 additions and 46 deletions
@@ -107,10 +107,7 @@ public class BundleDownloader {
Request.Builder requestBuilder) {
final Request request =
requestBuilder
.url(formatBundleUrl(bundleURL))
.addHeader("Accept", "multipart/mixed")
.build();
requestBuilder.url(bundleURL).addHeader("Accept", "multipart/mixed").build();
mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
mDownloadBundleFromURLCall.enqueue(
new Callback() {
@@ -165,10 +162,6 @@ public class BundleDownloader {
});
}
private String formatBundleUrl(String bundleURL) {
return bundleURL;
}
private void processMultipartResponse(
final String url,
final Response response,
@@ -419,15 +419,26 @@ public class DevServerHelper {
}
private String createBundleURL(String mainModuleID, BundleType type, String host) {
return createBundleURL(mainModuleID, type, host, false, true);
}
private String createSplitBundleURL(String mainModuleID, String host) {
return createBundleURL(mainModuleID, BundleType.BUNDLE, host, true, false);
}
private String createBundleURL(
String mainModuleID, BundleType type, String host, boolean modulesOnly, boolean runModule) {
return String.format(
Locale.US,
"http://%s/%s.%s?platform=android&dev=%s&minify=%s&app=%s",
"http://%s/%s.%s?platform=android&dev=%s&minify=%s&app=%s&modulesOnly=%s&runModule=%s",
host,
mainModuleID,
type.typeID(),
getDevMode(),
getJSMinifyMode(),
mPackageName);
mPackageName,
modulesOnly ? "true" : "false",
runModule ? "true" : "false");
}
private String createBundleURL(String mainModuleID, BundleType type) {
@@ -454,6 +465,11 @@ public class DevServerHelper {
mSettings.getPackagerConnectionSettings().getDebugServerHost());
}
public String getDevServerSplitBundleURL(String jsModulePath) {
return createSplitBundleURL(
jsModulePath, mSettings.getPackagerConnectionSettings().getDebugServerHost());
}
public void isPackagerRunning(final PackagerStatusCallback callback) {
String statusURL =
createPackagerStatusURL(mSettings.getPackagerConnectionSettings().getDebugServerHost());
@@ -23,12 +23,14 @@ import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.facebook.common.logging.FLog;
import com.facebook.debug.holder.PrinterHolder;
import com.facebook.debug.tags.ReactDebugOverlayTags;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.R;
import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler;
import com.facebook.react.bridge.JSBundleLoader;
import com.facebook.react.bridge.JavaJSExecutor;
import com.facebook.react.bridge.JavaScriptExecutorFactory;
import com.facebook.react.bridge.ReactContext;
@@ -43,6 +45,7 @@ import com.facebook.react.common.futures.SimpleSettableFuture;
import com.facebook.react.devsupport.DevServerHelper.PackagerCommandListener;
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener;
import com.facebook.react.devsupport.interfaces.DevOptionHandler;
import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.devsupport.interfaces.ErrorCustomizer;
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
@@ -70,6 +73,7 @@ public abstract class DevSupportManagerBase
private static final int JAVA_ERROR_COOKIE = -1;
private static final int JSEXCEPTION_ERROR_COOKIE = -1;
private static final String JS_BUNDLE_FILE_NAME = "ReactNativeDevBundle.js";
private static final String JS_SPLIT_BUNDLES_DIR_NAME = "dev_js_split_bundles";
private static final String RELOAD_APP_ACTION_SUFFIX = ".RELOAD_APP_ACTION";
private static final String FLIPPER_DEBUGGER_URL =
"flipper://null/Hermesdebuggerrn?device=React%20Native";
@@ -97,6 +101,7 @@ public abstract class DevSupportManagerBase
private final ReactInstanceManagerDevHelper mReactInstanceManagerHelper;
private final @Nullable String mJSAppBundleName;
private final File mJSBundleTempFile;
private final File mJSSplitBundlesDir;
private final DefaultNativeModuleCallExceptionHandler mDefaultNativeModuleCallExceptionHandler;
private final DevLoadingViewController mDevLoadingViewController;
@@ -104,6 +109,7 @@ public abstract class DevSupportManagerBase
private @Nullable AlertDialog mDevOptionsDialog;
private @Nullable DebugOverlayController mDebugOverlayController;
private boolean mDevLoadingViewVisible = false;
private int mPendingJSSplitBundleRequests = 0;
private @Nullable ReactContext mCurrentContext;
private DevInternalSettings mDevSettings;
private boolean mIsReceiverRegistered = false;
@@ -113,7 +119,6 @@ public abstract class DevSupportManagerBase
private @Nullable String mLastErrorTitle;
private @Nullable StackFrame[] mLastErrorStack;
private int mLastErrorCookie = 0;
private @Nullable ErrorType mLastErrorType;
private @Nullable DevBundleDownloadListener mBundleDownloadListener;
private @Nullable List<ErrorCustomizer> mErrorCustomizers;
private @Nullable PackagerLocationCustomizer mPackagerLocationCustomizer;
@@ -204,6 +209,9 @@ public abstract class DevSupportManagerBase
// TODO(6418010): Fix readers-writers problem in debug reload from HTTP server
mJSBundleTempFile = new File(applicationContext.getFilesDir(), JS_BUNDLE_FILE_NAME);
mJSSplitBundlesDir =
mApplicationContext.getDir(JS_SPLIT_BUNDLES_DIR_NAME, Context.MODE_PRIVATE);
mDefaultNativeModuleCallExceptionHandler = new DefaultNativeModuleCallExceptionHandler();
setDevSupportEnabled(enableOnCreate);
@@ -776,26 +784,6 @@ public abstract class DevSupportManagerBase
return false;
}
/**
* @return {@code true} if JS bundle {@param bundleAssetName} exists, in that case {@link
* com.facebook.react.ReactInstanceManager} should use that file from assets instead of
* downloading bundle from dev server
*/
public boolean hasBundleInAssets(String bundleAssetName) {
try {
String[] assets = mApplicationContext.getAssets().list("");
for (int i = 0; i < assets.length; i++) {
if (assets[i].equals(bundleAssetName)) {
return true;
}
}
} catch (IOException e) {
// Ignore this error and just fallback to downloading JS from devserver
FLog.e(ReactConstants.TAG, "Error while loading assets list");
}
return false;
}
private void resetCurrentContext(@Nullable ReactContext reactContext) {
if (mCurrentContext == reactContext) {
// new context is the same as the old one - do nothing
@@ -875,6 +863,82 @@ public abstract class DevSupportManagerBase
}
}
@Override
public void loadSplitBundleFromServer(String bundlePath, final DevSplitBundleCallback callback) {
final String bundleUrl = mDevServerHelper.getDevServerSplitBundleURL(bundlePath);
// The bundle path may contain the '/' character, which is not allowed in file names.
final File bundleFile =
new File(mJSSplitBundlesDir, bundlePath.replaceAll("/", "_") + ".jsbundle");
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
showSplitBundleDevLoadingView(bundleUrl);
mDevServerHelper.downloadBundleFromURL(
new DevBundleDownloadListener() {
@Override
public void onSuccess() {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
hideSplitBundleDevLoadingView();
}
});
@Nullable ReactContext context = mCurrentContext;
if (context == null || !context.hasActiveCatalystInstance()) {
return;
}
JSBundleLoader.createCachedSplitBundleFromNetworkLoader(
bundleUrl, bundleFile.getAbsolutePath())
.loadScript(context.getCatalystInstance());
context.getJSModule(HMRClient.class).registerBundle(bundleUrl);
callback.onSuccess();
}
@Override
public void onProgress(
@Nullable String status, @Nullable Integer done, @Nullable Integer total) {
mDevLoadingViewController.updateProgress(status, done, total);
}
@Override
public void onFailure(Exception cause) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
hideSplitBundleDevLoadingView();
}
});
callback.onError(bundleUrl, cause);
}
},
bundleFile,
bundleUrl,
null);
}
});
}
@UiThread
private void showSplitBundleDevLoadingView(String bundleUrl) {
mDevLoadingViewController.showForUrl(bundleUrl);
mDevLoadingViewVisible = true;
mPendingJSSplitBundleRequests++;
}
@UiThread
private void hideSplitBundleDevLoadingView() {
if (--mPendingJSSplitBundleRequests == 0) {
mDevLoadingViewController.hide();
mDevLoadingViewVisible = false;
}
}
@Override
public void isPackagerRunning(final PackagerStatusCallback callback) {
Runnable checkPackagerRunning =
@@ -988,7 +1052,6 @@ public abstract class DevSupportManagerBase
mLastErrorTitle = message;
mLastErrorStack = stack;
mLastErrorCookie = errorCookie;
mLastErrorType = errorType;
}
private void reloadJSInProxyMode() {
@@ -1107,19 +1170,7 @@ public abstract class DevSupportManagerBase
mBundleDownloadListener.onFailure(cause);
}
FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause);
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (cause instanceof DebugServerException) {
DebugServerException debugServerException = (DebugServerException) cause;
showNewJavaError(debugServerException.getMessage(), cause);
} else {
showNewJavaError(
mApplicationContext.getString(R.string.catalyst_reload_error), cause);
}
}
});
reportBundleLoadingFailure(cause);
}
},
mJSBundleTempFile,
@@ -1127,6 +1178,22 @@ public abstract class DevSupportManagerBase
bundleInfo);
}
private void reportBundleLoadingFailure(final Exception cause) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (cause instanceof DebugServerException) {
DebugServerException debugServerException = (DebugServerException) cause;
showNewJavaError(debugServerException.getMessage(), cause);
} else {
showNewJavaError(
mApplicationContext.getString(R.string.catalyst_reload_error), cause);
}
}
});
}
@Override
public void startInspector() {
if (mIsDevSupportEnabled) {
@@ -13,6 +13,7 @@ import com.facebook.react.bridge.DefaultNativeModuleCallExceptionHandler;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.devsupport.interfaces.DevOptionHandler;
import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.devsupport.interfaces.ErrorCustomizer;
import com.facebook.react.devsupport.interfaces.PackagerStatusCallback;
@@ -129,6 +130,9 @@ public class DisabledDevSupportManager implements DevSupportManager {
@Override
public void reloadJSFromServer(String bundleURL) {}
@Override
public void loadSplitBundleFromServer(String bundlePath, DevSplitBundleCallback callback) {}
@Override
public void isPackagerRunning(final PackagerStatusCallback callback) {}
@@ -29,6 +29,9 @@ public interface HMRClient extends JavaScriptModule {
*/
void setup(String platform, String bundleEntry, String host, int port, boolean isEnabled);
/** Registers an additional JS bundle with HMRClient. */
void registerBundle(String bundleUrl);
/**
* Sets up a connection to the packager when called the first time. Ensures code updates received
* from the packager are applied.
@@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.devsupport.interfaces;
/** Callback class for loading split JS bundles from Metro in development. */
public interface DevSplitBundleCallback {
/** Called when the split JS bundle has been downloaded and evaluated. */
void onSuccess();
/** Called when the split JS bundle failed to load. */
void onError(String url, Throwable cause);
}
@@ -69,6 +69,8 @@ public interface DevSupportManager extends NativeModuleCallExceptionHandler {
void reloadJSFromServer(final String bundleURL);
void loadSplitBundleFromServer(String bundlePath, DevSplitBundleCallback callback);
void isPackagerRunning(PackagerStatusCallback callback);
void setHotModuleReplacementEnabled(final boolean isHotModuleReplacementEnabled);