diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm index c3d0f565300d5f..790d8fb1f86e04 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm @@ -981,6 +981,26 @@ + (RCTManagedPointer *)JS_NativeCameraRollManager_PhotoIdentifiersPage:(id)json + } + + } // namespace react +} // namespace facebook +namespace facebook { + namespace react { + + + static facebook::jsi::Value __hostFunction_NativeDevSplitBundleLoaderSpecJSI_loadBundle(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeObjCMethod(rt, PromiseKind, "loadBundle", @selector(loadBundle:resolve:reject:), args, count); + } + + + NativeDevSplitBundleLoaderSpecJSI::NativeDevSplitBundleLoaderSpecJSI(const ObjCTurboModule::InitParams ¶ms) + : ObjCTurboModule(params) { + + methodMap_["loadBundle"] = MethodMetadata {1, __hostFunction_NativeDevSplitBundleLoaderSpecJSI_loadBundle}; + + + } } // namespace react diff --git a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h index ae5df71d3c9300..cd228b2f43a53c 100644 --- a/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h +++ b/Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h @@ -918,6 +918,26 @@ namespace facebook { }; } // namespace react } // namespace facebook +@protocol NativeDevSplitBundleLoaderSpec + +- (void)loadBundle:(NSString *)bundlePath + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject; + +@end +namespace facebook { + namespace react { + /** + * ObjC++ class for module 'DevSplitBundleLoader' + */ + + class JSI_EXPORT NativeDevSplitBundleLoaderSpecJSI : public ObjCTurboModule { + public: + NativeDevSplitBundleLoaderSpecJSI(const ObjCTurboModule::InitParams ¶ms); + + }; + } // namespace react +} // namespace facebook @protocol NativeDeviceEventManagerSpec - (void)invokeDefaultBackPressHandler; diff --git a/Libraries/Utilities/NativeDevSplitBundleLoader.js b/Libraries/Utilities/NativeDevSplitBundleLoader.js new file mode 100644 index 00000000000000..505ae1c21081e5 --- /dev/null +++ b/Libraries/Utilities/NativeDevSplitBundleLoader.js @@ -0,0 +1,20 @@ +/** + * 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. + * + * @flow + * @format + */ + +'use strict'; + +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; + +export interface Spec extends TurboModule { + +loadBundle: (bundlePath: string) => Promise; +} + +export default (TurboModuleRegistry.get('DevSplitBundleLoader'): ?Spec); diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 63a3b34d5115f1..ecf2959240a587 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -33,6 +33,11 @@ RCT_EXTERN NSString *const RCTJavaScriptWillStartExecutingNotification; */ RCT_EXTERN NSString *const RCTJavaScriptDidLoadNotification; +/** + * This notification fires every time the bridge has finished loading an additional JS bundle. + */ +RCT_EXTERN NSString *const RCTAdditionalJavaScriptDidLoadNotification; + /** * This notification fires when the bridge failed to load the JS bundle. The * `error` key can be used to determine the error that occurred. @@ -135,6 +140,12 @@ RCT_EXTERN NSString *const RCTBridgeDidDownloadScriptNotificationBridgeDescripti */ typedef NSArray> * (^RCTBridgeModuleListProvider)(void); +/** + * These blocks are used to report whether an additional bundle + * fails or succeeds loading. + */ +typedef void (^RCTLoadAndExecuteErrorBlock)(NSError *error); + /** * This function returns the module name for a given class. */ @@ -290,8 +301,15 @@ RCT_EXTERN void RCTEnableTurboModule(BOOL enabled); - (void)requestReload __deprecated_msg("Use RCTReloadCommand instead"); /** - * Says whether bridge has started receiving calls from javascript. + * Says whether bridge has started receiving calls from JavaScript. */ - (BOOL)isBatchActive; +/** + * Loads and executes additional bundles in the VM for development. + */ +- (void)loadAndExecuteSplitBundleURL:(NSURL *)bundleURL + onError:(RCTLoadAndExecuteErrorBlock)onError + onComplete:(dispatch_block_t)onComplete; + @end diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 81314b3d3b3fcd..ecbed768b5b771 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -15,6 +15,7 @@ #if RCT_ENABLE_INSPECTOR #import "RCTInspectorDevServerHelper.h" #endif +#import "RCTDevLoadingViewProtocol.h" #import "RCTLog.h" #import "RCTModuleData.h" #import "RCTPerformanceLogger.h" @@ -22,10 +23,11 @@ #import "RCTReloadCommand.h" #import "RCTUtils.h" -NSString *const RCTJavaScriptWillStartLoadingNotification = @"RCTJavaScriptWillStartLoadingNotification"; -NSString *const RCTJavaScriptWillStartExecutingNotification = @"RCTJavaScriptWillStartExecutingNotification"; -NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; +NSString *const RCTAdditionalJavaScriptDidLoadNotification = @"RCTAdditionalJavaScriptDidLoadNotification"; NSString *const RCTJavaScriptDidFailToLoadNotification = @"RCTJavaScriptDidFailToLoadNotification"; +NSString *const RCTJavaScriptDidLoadNotification = @"RCTJavaScriptDidLoadNotification"; +NSString *const RCTJavaScriptWillStartExecutingNotification = @"RCTJavaScriptWillStartExecutingNotification"; +NSString *const RCTJavaScriptWillStartLoadingNotification = @"RCTJavaScriptWillStartLoadingNotification"; NSString *const RCTDidInitializeModuleNotification = @"RCTDidInitializeModuleNotification"; NSString *const RCTDidSetupModuleNotification = @"RCTDidSetupModuleNotification"; NSString *const RCTDidSetupModuleNotificationModuleNameKey = @"moduleName"; @@ -388,4 +390,11 @@ - (void)registerSegmentWithId:(NSUInteger)segmentId path:(NSString *)path [self.batchedBridge registerSegmentWithId:segmentId path:path]; } +- (void)loadAndExecuteSplitBundleURL:(NSURL *)bundleURL + onError:(RCTLoadAndExecuteErrorBlock)onError + onComplete:(dispatch_block_t)onComplete +{ + [self.batchedBridge loadAndExecuteSplitBundleURL:bundleURL onError:onError onComplete:onComplete]; +} + @end diff --git a/React/Base/RCTJavaScriptLoader.mm b/React/Base/RCTJavaScriptLoader.mm index d15738c549eabe..a2ab5a22807a14 100755 --- a/React/Base/RCTJavaScriptLoader.mm +++ b/React/Base/RCTJavaScriptLoader.mm @@ -301,9 +301,23 @@ static void attemptAsynchronousLoadOfBundleAtURL( NSString *contentType = headers[@"Content-Type"]; NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject]; if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"]) { - NSString *description = [NSString - stringWithFormat:@"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.", - mimeType]; + NSString *description; + if ([mimeType isEqualToString:@"application/json"]) { + NSError *parseError; + NSDictionary *jsonError = [NSJSONSerialization JSONObjectWithData:data options:0 error:&parseError]; + if (!parseError && [jsonError isKindOfClass:[NSDictionary class]] && + [[jsonError objectForKey:@"message"] isKindOfClass:[NSString class]] && + [[jsonError objectForKey:@"message"] length]) { + description = [jsonError objectForKey:@"message"]; + } else { + description = [NSString stringWithFormat:@"Unknown error fetching '%@'.", scriptURL.absoluteString]; + } + } else { + description = [NSString + stringWithFormat: + @"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.", mimeType]; + } + error = [NSError errorWithDomain:@"JSServer" code:NSURLErrorCannotParseResponse diff --git a/React/CoreModules/BUCK b/React/CoreModules/BUCK index 274a89b115fdd6..2f9902478139d5 100644 --- a/React/CoreModules/BUCK +++ b/React/CoreModules/BUCK @@ -116,6 +116,9 @@ rn_apple_library( ) + react_module_plugin_providers( name = "DevLoadingView", native_class_func = "RCTDevLoadingViewCls", + ) + react_module_plugin_providers( + name = "DevSplitBundleLoader", + native_class_func = "RCTDevSplitBundleLoaderCls", ), plugins_header = "FBCoreModulesPlugins.h", preprocessor_flags = OBJC_ARC_PREPROCESSOR_FLAGS + get_preprocessor_flags_for_build_mode() + rn_extra_build_flags() + [ diff --git a/React/CoreModules/CoreModulesPlugins.h b/React/CoreModules/CoreModulesPlugins.h index 478c7473a512ba..509eb5af2f8dbc 100644 --- a/React/CoreModules/CoreModulesPlugins.h +++ b/React/CoreModules/CoreModulesPlugins.h @@ -53,6 +53,7 @@ Class RCTTVNavigationEventEmitterCls(void) __attribute__((used)); Class RCTWebSocketExecutorCls(void) __attribute__((used)); Class RCTWebSocketModuleCls(void) __attribute__((used)); Class RCTDevLoadingViewCls(void) __attribute__((used)); +Class RCTDevSplitBundleLoaderCls(void) __attribute__((used)); #ifdef __cplusplus } diff --git a/React/CoreModules/CoreModulesPlugins.mm b/React/CoreModules/CoreModulesPlugins.mm index 40c36eb59508fa..ced0cbd0797bbe 100644 --- a/React/CoreModules/CoreModulesPlugins.mm +++ b/React/CoreModules/CoreModulesPlugins.mm @@ -42,6 +42,7 @@ Class RCTCoreModulesClassProvider(const char *name) { {"WebSocketExecutor", RCTWebSocketExecutorCls}, {"WebSocketModule", RCTWebSocketModuleCls}, {"DevLoadingView", RCTDevLoadingViewCls}, + {"DevSplitBundleLoader", RCTDevSplitBundleLoaderCls}, }; auto p = sCoreModuleClassMap.find(name); diff --git a/React/CoreModules/RCTDevSettings.h b/React/CoreModules/RCTDevSettings.h index b1ea0e75315dfb..c6393d0630e2bd 100644 --- a/React/CoreModules/RCTDevSettings.h +++ b/React/CoreModules/RCTDevSettings.h @@ -82,9 +82,14 @@ - (void)toggleElementInspector; /** - * If loading bundle from metro, sets up HMRClient. + * Set up the HMRClient if loading the bundle from Metro. */ -- (void)setupHotModuleReloadClientIfApplicableForURL:(NSURL *)bundleURL; +- (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL; + +/** + * Register additional bundles with the HMRClient. + */ +- (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL; #if RCT_DEV_MENU - (void)addHandler:(id)handler diff --git a/React/CoreModules/RCTDevSettings.mm b/React/CoreModules/RCTDevSettings.mm index 172573be3d6972..b84d4e3114233e 100644 --- a/React/CoreModules/RCTDevSettings.mm +++ b/React/CoreModules/RCTDevSettings.mm @@ -403,7 +403,7 @@ - (void)addHandler:(id)handler forPackagerMethod:(NSStr #endif } -- (void)setupHotModuleReloadClientIfApplicableForURL:(NSURL *)bundleURL +- (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL { if (bundleURL && !bundleURL.fileURL) { // isHotLoadingAvailable check NSString *const path = [bundleURL.path substringFromIndex:1]; // Strip initial slash. @@ -420,6 +420,20 @@ - (void)setupHotModuleReloadClientIfApplicableForURL:(NSURL *)bundleURL } } +- (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL +{ + if (bundleURL && !bundleURL.fileURL) { // isHotLoadingAvailable check + if (self.bridge) { + [self.bridge enqueueJSCall:@"HMRClient" + method:@"registerBundle" + args:@[ [bundleURL absoluteString] ] + completion:NULL]; + } else { + self.invokeJS(@"HMRClient", @"registerBundle", @[ [bundleURL absoluteString] ]); + } + } +} + #pragma mark - Internal /** @@ -509,7 +523,10 @@ - (void)setProfilingEnabled:(BOOL)isProfilingEnabled - (void)toggleElementInspector { } -- (void)setupHotModuleReloadClientIfApplicableForURL:(NSURL *)bundleURL +- (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL +{ +} +- (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL { } - (void)addMenuItem:(NSString *)title diff --git a/React/CoreModules/RCTDevSplitBundleLoader.h b/React/CoreModules/RCTDevSplitBundleLoader.h new file mode 100644 index 00000000000000..c32f60d9750a5e --- /dev/null +++ b/React/CoreModules/RCTDevSplitBundleLoader.h @@ -0,0 +1,12 @@ +/* + * 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. + */ + +#import +#import + +@interface RCTDevSplitBundleLoader : NSObject +@end diff --git a/React/CoreModules/RCTDevSplitBundleLoader.mm b/React/CoreModules/RCTDevSplitBundleLoader.mm new file mode 100644 index 00000000000000..73d0c98e692dff --- /dev/null +++ b/React/CoreModules/RCTDevSplitBundleLoader.mm @@ -0,0 +1,88 @@ +/* + * 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. + */ + +#import + +#import +#import +#import +#import +#import +#import + +#import "CoreModulesPlugins.h" + +using namespace facebook::react; + +@interface RCTDevSplitBundleLoader () +@end + +#if RCT_DEV_MENU + +@implementation RCTDevSplitBundleLoader { +} + +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +- (void)setBridge:(RCTBridge *)bridge +{ + _bridge = bridge; +} + +RCT_EXPORT_METHOD(loadBundle + : (NSString *)bundlePath resolve + : (RCTPromiseResolveBlock)resolve reject + : (RCTPromiseRejectBlock)reject) +{ + NSURL *sourceURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForSplitBundleRoot:bundlePath]; + [_bridge loadAndExecuteSplitBundleURL:sourceURL + onError:^(NSError *error) { + reject(@"E_BUNDLE_LOAD_ERROR", [error localizedDescription], error); + } + onComplete:^() { + resolve(@YES); + }]; +} + +- (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +@end + +#else + +@implementation RCTDevSplitBundleLoader + ++ (NSString *)moduleName +{ + return nil; +} +- (void)loadBundle:(NSString *)bundlePath resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +{ +} +- (std::shared_ptr)getTurboModule:(const ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} + +@end + +#endif + +Class RCTDevSplitBundleLoaderCls(void) +{ + return RCTDevSplitBundleLoader.class; +} diff --git a/React/CxxBridge/RCTCxxBridge.mm b/React/CxxBridge/RCTCxxBridge.mm index 7b2fc3808bce16..aa2a816d0a04a7 100644 --- a/React/CxxBridge/RCTCxxBridge.mm +++ b/React/CxxBridge/RCTCxxBridge.mm @@ -966,9 +966,49 @@ - (void)executeSourceCode:(NSData *)sourceCode sync:(BOOL)sync [self enqueueApplicationScript:sourceCode url:self.bundleURL onComplete:completion]; } - [self.devSettings setupHotModuleReloadClientIfApplicableForURL:self.bundleURL]; + [self.devSettings setupHMRClientWithBundleURL:self.bundleURL]; } +#if RCT_DEV_MENU +- (void)loadAndExecuteSplitBundleURL:(NSURL *)bundleURL + onError:(RCTLoadAndExecuteErrorBlock)onError + onComplete:(dispatch_block_t)onComplete +{ + __weak __typeof(self) weakSelf = self; + [RCTJavaScriptLoader loadBundleAtURL:bundleURL + onProgress:^(RCTLoadingProgress *progressData) { +#if (RCT_DEV_MENU | RCT_ENABLE_LOADING_VIEW) && __has_include() + id loadingView = [weakSelf moduleForName:@"DevLoadingView" + lazilyLoadIfNecessary:YES]; + [loadingView updateProgress:progressData]; +#endif + } + onComplete:^(NSError *error, RCTSource *source) { + if (error) { + onError(error); + return; + } + + [self enqueueApplicationScript:source.data + url:source.url + onComplete:^{ + [[NSNotificationCenter defaultCenter] + postNotificationName:RCTAdditionalJavaScriptDidLoadNotification + object:self->_parentBridge + userInfo:@{@"bridge" : self}]; + [self.devSettings setupHMRClientWithAdditionalBundleURL:source.url]; + onComplete(); + }]; + }]; +} +#else +- (void)loadAndExecuteSplitBundleURL:(NSURL *)bundleURL + onError:(RCTLoadAndExecuteErrorBlock)onError + onComplete:(dispatch_block_t)onComplete +{ +} +#endif + - (void)handleError:(NSError *)error { // This is generally called when the infrastructure throws an diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeDevSplitBundleLoaderSpec.java b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeDevSplitBundleLoaderSpec.java new file mode 100644 index 00000000000000..979b134c908d27 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/NativeDevSplitBundleLoaderSpec.java @@ -0,0 +1,29 @@ +/** + * 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. + * + *

Generated by an internal genrule from Flow types. + * + * @generated + * @nolint + */ + +package com.facebook.fbreact.specs; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReactModuleWithSpec; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; + +public abstract class NativeDevSplitBundleLoaderSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule { + public NativeDevSplitBundleLoaderSpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @ReactMethod + public abstract void loadBundle(String bundlePath, Promise promise); +} diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp index 8de44bae7bf431..2283e38b311476 100644 --- a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec-generated.cpp @@ -819,6 +819,26 @@ namespace facebook { + } + + } // namespace react +} // namespace facebook +namespace facebook { + namespace react { + + + static facebook::jsi::Value __hostFunction_NativeDevSplitBundleLoaderSpecJSI_loadBundle(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) { + return static_cast(turboModule).invokeJavaMethod(rt, PromiseKind, "loadBundle", "(Ljava/lang/String;Lcom/facebook/react/bridge/Promise;)V", args, count); + } + + + NativeDevSplitBundleLoaderSpecJSI::NativeDevSplitBundleLoaderSpecJSI(const JavaTurboModule::InitParams ¶ms) + : JavaTurboModule(params) { + + methodMap_["loadBundle"] = MethodMetadata {1, __hostFunction_NativeDevSplitBundleLoaderSpecJSI_loadBundle}; + + + } } // namespace react diff --git a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec.h b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec.h index bfe63a1859561f..3ce071a5f41fcd 100644 --- a/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec.h +++ b/ReactAndroid/src/main/java/com/facebook/fbreact/specs/jni/FBReactNativeSpec.h @@ -255,6 +255,20 @@ namespace facebook { } // namespace react } // namespace facebook +namespace facebook { + namespace react { + /** + * C++ class for module 'DevSplitBundleLoader' + */ + + class JSI_EXPORT NativeDevSplitBundleLoaderSpecJSI : public JavaTurboModule { + public: + NativeDevSplitBundleLoaderSpecJSI(const JavaTurboModule::InitParams ¶ms); + + }; + } // namespace react +} // namespace facebook + namespace facebook { namespace react { /**