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:08:57 -07:00
committed by Facebook GitHub Bot
parent 2e2c881147
commit fca3a39da5
15 changed files with 272 additions and 46 deletions
@@ -35,6 +35,7 @@ rn_android_library(
react_native_target("java/com/facebook/react/module/model:model"),
react_native_target("java/com/facebook/react/modules/appregistry:appregistry"),
react_native_target("java/com/facebook/react/modules/appearance:appearance"),
react_native_target("java/com/facebook/react/modules/bundleloader:bundleloader"),
react_native_target("java/com/facebook/react/modules/debug:debug"),
react_native_target("java/com/facebook/react/modules/fabric:fabric"),
react_native_target("java/com/facebook/react/modules/debug:interfaces"),
@@ -21,6 +21,7 @@ import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.module.annotations.ReactModuleList;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.modules.bundleloader.NativeDevSplitBundleLoaderModule;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.modules.core.ExceptionsManagerModule;
@@ -56,6 +57,7 @@ import java.util.Map;
SourceCodeModule.class,
TimingModule.class,
UIManagerModule.class,
NativeDevSplitBundleLoaderModule.class,
})
public class CoreModulesPackage extends TurboReactPackage implements ReactPackageLogger {
@@ -101,7 +103,8 @@ public class CoreModulesPackage extends TurboReactPackage implements ReactPackag
HeadlessJsTaskSupportModule.class,
SourceCodeModule.class,
TimingModule.class,
UIManagerModule.class
UIManagerModule.class,
NativeDevSplitBundleLoaderModule.class,
};
final Map<String, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
@@ -158,6 +161,9 @@ public class CoreModulesPackage extends TurboReactPackage implements ReactPackag
return createUIManager(reactContext);
case DeviceInfoModule.NAME:
return new DeviceInfoModule(reactContext);
case NativeDevSplitBundleLoaderModule.NAME:
return new NativeDevSplitBundleLoaderModule(
reactContext, mReactInstanceManager.getDevSupportManager());
default:
throw new IllegalArgumentException(
"In CoreModulesPackage, could not find Native module for " + name);
@@ -240,6 +240,11 @@ public class CatalystInstanceImpl implements CatalystInstance {
jniLoadScriptFromFile(fileName, sourceURL, loadSynchronously);
}
@Override
public void loadSplitBundleFromFile(String fileName, String sourceURL) {
jniLoadScriptFromFile(fileName, sourceURL, false);
}
private native void jniSetSourceURL(String sourceURL);
private native void jniRegisterSegment(int segmentId, String path);
@@ -73,6 +73,25 @@ public abstract class JSBundleLoader {
};
}
/**
* Same as {{@link JSBundleLoader#createCachedBundleFromNetworkLoader(String, String)}}, but for
* split bundles in development.
*/
public static JSBundleLoader createCachedSplitBundleFromNetworkLoader(
final String sourceURL, final String cachedFileLocation) {
return new JSBundleLoader() {
@Override
public String loadScript(JSBundleLoaderDelegate delegate) {
try {
delegate.loadSplitBundleFromFile(cachedFileLocation, sourceURL);
return sourceURL;
} catch (Exception e) {
throw DebugServerException.makeGeneric(sourceURL, e.getMessage(), e);
}
}
};
}
/**
* This loader is used when proxy debugging is enabled. In that case there is no point in fetching
* the bundle from device as remote executor will have to do it anyway.
@@ -33,6 +33,12 @@ public interface JSBundleLoaderDelegate {
*/
void loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously);
/**
* Load a split JS bundle from the filesystem. See {@link
* JSBundleLoader#createCachedSplitBundleFromNetworkLoader(String, String)}.
*/
void loadSplitBundleFromFile(String fileName, String sourceURL);
/**
* This API is used in situations where the JS bundle is being executed not on the device, but on
* a host machine. In that case, we must provide two source URLs for the JS bundle: One to be used
@@ -40,16 +40,25 @@ public class DebugServerException extends RuntimeException {
return new DebugServerException(reason + message + extra, t);
}
private final String mOriginalMessage;
private DebugServerException(String description, String fileName, int lineNumber, int column) {
super(description + "\n at " + fileName + ":" + lineNumber + ":" + column);
mOriginalMessage = description;
}
public DebugServerException(String description) {
super(description);
mOriginalMessage = description;
}
public DebugServerException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
mOriginalMessage = detailMessage;
}
public String getOriginalMessage() {
return mOriginalMessage;
}
/**
@@ -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);
@@ -0,0 +1,21 @@
load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "react_native_target", "rn_android_library")
rn_android_library(
name = "bundleloader",
srcs = glob(["*.java"]),
is_androidx = True,
labels = ["supermodule:xplat/default/public.react_native.infra"],
provided_deps = [
react_native_dep("third-party/android/androidx:annotation"),
],
visibility = [
"PUBLIC",
],
deps = [
react_native_target("java/com/facebook/react/bridge:bridge"),
react_native_target("java/com/facebook/react/common:common"),
react_native_target("java/com/facebook/react/devsupport:interfaces"),
react_native_target("java/com/facebook/react/module/annotations:annotations"),
],
exported_deps = [react_native_target("java/com/facebook/fbreact/specs:FBReactNativeSpec")],
)
@@ -0,0 +1,58 @@
/*
* 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.modules.bundleloader;
import androidx.annotation.NonNull;
import com.facebook.fbreact.specs.NativeDevSplitBundleLoaderSpec;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.common.DebugServerException;
import com.facebook.react.devsupport.interfaces.DevSplitBundleCallback;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.module.annotations.ReactModule;
@ReactModule(name = NativeDevSplitBundleLoaderModule.NAME)
public class NativeDevSplitBundleLoaderModule extends NativeDevSplitBundleLoaderSpec {
public static final String NAME = "DevSplitBundleLoader";
private static final String REJECTION_CODE = "E_BUNDLE_LOAD_ERROR";
private final DevSupportManager mDevSupportManager;
public NativeDevSplitBundleLoaderModule(
ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
super(reactContext);
mDevSupportManager = devSupportManager;
}
@Override
public void loadBundle(String bundlePath, final Promise promise) {
mDevSupportManager.loadSplitBundleFromServer(
bundlePath,
new DevSplitBundleCallback() {
@Override
public void onSuccess() {
promise.resolve(true);
}
@Override
public void onError(String url, Throwable cause) {
String message =
cause instanceof DebugServerException
? ((DebugServerException) cause).getOriginalMessage()
: "Unknown error fetching '" + url + "'.";
promise.reject(REJECTION_CODE, message, cause);
}
});
}
@NonNull
@Override
public String getName() {
return NAME;
}
}