mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
c7d34527c9
Summary: The Facebook app can have both bridge and bridgeless surface. Each type of surface has its own DevMenu. It's important to specify which DevMenu is being rendered, because not doing so can make for some confusing/frustrating devx (e.g: the bridgeless dev menu shows up for bridge surfaces: https://pxl.cl/1L1m7). Changelog: [Internal] Reviewed By: PeteTheHeat Differential Revision: D29439980 fbshipit-source-id: 24de1e6b44e5315c0cee0684d0db5df31514d3e2
567 lines
21 KiB
Plaintext
567 lines
21 KiB
Plaintext
/*
|
|
* 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 <React/RCTDevMenu.h>
|
|
|
|
#import <FBReactNativeSpec/FBReactNativeSpec.h>
|
|
#import <React/RCTBridge+Private.h>
|
|
#import <React/RCTBundleURLProvider.h>
|
|
#import <React/RCTDefines.h>
|
|
#import <React/RCTDevSettings.h>
|
|
#import <React/RCTKeyCommands.h>
|
|
#import <React/RCTLog.h>
|
|
#import <React/RCTReloadCommand.h>
|
|
#import <React/RCTUtils.h>
|
|
#import "CoreModulesPlugins.h"
|
|
|
|
#if RCT_DEV_MENU
|
|
#if RCT_ENABLE_INSPECTOR
|
|
#import <React/RCTInspectorDevServerHelper.h>
|
|
#endif
|
|
|
|
NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
|
|
|
|
@implementation UIWindow (RCTDevMenu)
|
|
|
|
- (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event
|
|
{
|
|
if (event.subtype == UIEventSubtypeMotionShake) {
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTDevMenuItem {
|
|
RCTDevMenuItemTitleBlock _titleBlock;
|
|
dispatch_block_t _handler;
|
|
}
|
|
|
|
- (instancetype)initWithTitleBlock:(RCTDevMenuItemTitleBlock)titleBlock handler:(dispatch_block_t)handler
|
|
{
|
|
if ((self = [super init])) {
|
|
_titleBlock = [titleBlock copy];
|
|
_handler = [handler copy];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(-(instancetype)init)
|
|
|
|
+ (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(dispatch_block_t)handler
|
|
{
|
|
return [[self alloc] initWithTitleBlock:titleBlock handler:handler];
|
|
}
|
|
|
|
+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(dispatch_block_t)handler
|
|
{
|
|
return [[self alloc]
|
|
initWithTitleBlock:^NSString * {
|
|
return title;
|
|
}
|
|
handler:handler];
|
|
}
|
|
|
|
- (void)callHandler
|
|
{
|
|
if (_handler) {
|
|
_handler();
|
|
}
|
|
}
|
|
|
|
- (NSString *)title
|
|
{
|
|
if (_titleBlock) {
|
|
return _titleBlock();
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
@end
|
|
|
|
typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action);
|
|
|
|
@interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
|
|
|
|
@end
|
|
|
|
@implementation RCTDevMenu {
|
|
UIAlertController *_actionSheet;
|
|
NSMutableArray<RCTDevMenuItem *> *_extraMenuItems;
|
|
}
|
|
|
|
@synthesize bridge = _bridge;
|
|
@synthesize moduleRegistry = _moduleRegistry;
|
|
@synthesize callableJSModules = _callableJSModules;
|
|
@synthesize bundleManager = _bundleManager;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
+ (void)initialize
|
|
{
|
|
// We're swizzling here because it's poor form to override methods in a category,
|
|
// however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
|
|
// no need to call the original implementation.
|
|
RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
|
|
}
|
|
|
|
+ (BOOL)requiresMainQueueSetup
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(showOnShake)
|
|
name:RCTShowDevMenuNotification
|
|
object:nil];
|
|
_extraMenuItems = [NSMutableArray new];
|
|
|
|
#if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
|
|
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
|
|
__weak __typeof(self) weakSelf = self;
|
|
|
|
// Toggle debug menu
|
|
[commands registerKeyCommandWithInput:@"d"
|
|
modifierFlags:UIKeyModifierCommand
|
|
action:^(__unused UIKeyCommand *command) {
|
|
[weakSelf toggle];
|
|
}];
|
|
|
|
// Toggle element inspector
|
|
[commands registerKeyCommandWithInput:@"i"
|
|
modifierFlags:UIKeyModifierCommand
|
|
action:^(__unused UIKeyCommand *command) {
|
|
[(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"]
|
|
toggleElementInspector];
|
|
}];
|
|
|
|
// Reload in normal mode
|
|
[commands registerKeyCommandWithInput:@"n"
|
|
modifierFlags:UIKeyModifierCommand
|
|
action:^(__unused UIKeyCommand *command) {
|
|
[(RCTDevSettings *)[weakSelf.moduleRegistry moduleForName:"DevSettings"]
|
|
setIsDebuggingRemotely:NO];
|
|
}];
|
|
#endif
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (dispatch_queue_t)methodQueue
|
|
{
|
|
return dispatch_get_main_queue();
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
_presentedItems = nil;
|
|
[_actionSheet dismissViewControllerAnimated:YES
|
|
completion:^(void){
|
|
}];
|
|
}
|
|
|
|
- (void)showOnShake
|
|
{
|
|
if ([((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]) isShakeToShowDevMenuEnabled]) {
|
|
for (UIWindow *window in [RCTSharedApplication() windows]) {
|
|
NSString *recursiveDescription = [window valueForKey:@"recursiveDescription"];
|
|
if ([recursiveDescription containsString:@"RCTView"]) {
|
|
[self show];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)toggle
|
|
{
|
|
if (_actionSheet) {
|
|
[_actionSheet dismissViewControllerAnimated:YES
|
|
completion:^(void){
|
|
}];
|
|
_actionSheet = nil;
|
|
} else {
|
|
[self show];
|
|
}
|
|
}
|
|
|
|
- (BOOL)isActionSheetShown
|
|
{
|
|
return _actionSheet != nil;
|
|
}
|
|
|
|
- (void)addItem:(NSString *)title handler:(void (^)(void))handler
|
|
{
|
|
[self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]];
|
|
}
|
|
|
|
- (void)addItem:(RCTDevMenuItem *)item
|
|
{
|
|
[_extraMenuItems addObject:item];
|
|
}
|
|
|
|
- (void)setDefaultJSBundle
|
|
{
|
|
[[RCTBundleURLProvider sharedSettings] resetToDefaults];
|
|
self->_bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings] jsBundleURLForFallbackResource:nil
|
|
fallbackExtension:nil];
|
|
RCTTriggerReloadCommandListeners(@"Dev menu - reset to default");
|
|
}
|
|
|
|
- (NSArray<RCTDevMenuItem *> *)_menuItemsToPresent
|
|
{
|
|
NSMutableArray<RCTDevMenuItem *> *items = [NSMutableArray new];
|
|
|
|
// Add built-in items
|
|
__weak RCTDevSettings *devSettings = [_moduleRegistry moduleForName:"DevSettings"];
|
|
__weak RCTDevMenu *weakSelf = self;
|
|
__weak RCTBundleManager *bundleManager = _bundleManager;
|
|
|
|
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload"
|
|
handler:^{
|
|
RCTTriggerReloadCommandListeners(@"Dev menu - reload");
|
|
}]];
|
|
|
|
if (!devSettings.isProfilingEnabled) {
|
|
#if RCT_ENABLE_INSPECTOR
|
|
if (devSettings.isDeviceDebuggingAvailable) {
|
|
// For on-device debugging we link out to Flipper.
|
|
// Since we're assuming Flipper is available, also include the DevTools.
|
|
// Note: For parity with the Android code.
|
|
[items addObject:[RCTDevMenuItem
|
|
buttonItemWithTitleBlock:^NSString * {
|
|
return @"Open Debugger";
|
|
}
|
|
handler:^{
|
|
[RCTInspectorDevServerHelper
|
|
openURL:@"flipper://null/Hermesdebuggerrn?device=React%20Native"
|
|
withBundleURL:bundleManager.bundleURL
|
|
withErrorMessage:@"Failed to open Flipper. Please check that Metro is runnning."];
|
|
}]];
|
|
|
|
[items addObject:[RCTDevMenuItem
|
|
buttonItemWithTitleBlock:^NSString * {
|
|
return @"Open React DevTools";
|
|
}
|
|
handler:^{
|
|
[RCTInspectorDevServerHelper
|
|
openURL:@"flipper://null/React?device=React%20Native"
|
|
withBundleURL:bundleManager.bundleURL
|
|
withErrorMessage:@"Failed to open Flipper. Please check that Metro is runnning."];
|
|
}]];
|
|
} else if (devSettings.isRemoteDebuggingAvailable) {
|
|
#else
|
|
if (devSettings.isRemoteDebuggingAvailable) {
|
|
#endif
|
|
// For remote debugging, we open up Chrome running the app in a web worker.
|
|
// Note that this requires async communication, which will not work for Turbo Modules.
|
|
[items addObject:[RCTDevMenuItem
|
|
buttonItemWithTitleBlock:^NSString * {
|
|
return devSettings.isDebuggingRemotely ? @"Stop Debugging" : @"Debug with Chrome";
|
|
}
|
|
handler:^{
|
|
devSettings.isDebuggingRemotely = !devSettings.isDebuggingRemotely;
|
|
}]];
|
|
} else {
|
|
// If neither are available, we're defaulting to a message that tells you about remote debugging.
|
|
[items
|
|
addObject:[RCTDevMenuItem
|
|
buttonItemWithTitle:@"Debugger Unavailable"
|
|
handler:^{
|
|
NSString *message = RCTTurboModuleEnabled()
|
|
? @"Debugging with Chrome is not supported when TurboModules are enabled."
|
|
: @"Include the RCTWebSocket library to enable JavaScript debugging.";
|
|
UIAlertController *alertController =
|
|
[UIAlertController alertControllerWithTitle:@"Debugger Unavailable"
|
|
message:message
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
__weak __typeof__(alertController) weakAlertController = alertController;
|
|
[alertController
|
|
addAction:[UIAlertAction actionWithTitle:@"OK"
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(__unused UIAlertAction *action) {
|
|
[weakAlertController
|
|
dismissViewControllerAnimated:YES
|
|
completion:nil];
|
|
}]];
|
|
[RCTPresentedViewController() presentViewController:alertController
|
|
animated:YES
|
|
completion:NULL];
|
|
}]];
|
|
}
|
|
}
|
|
|
|
[items addObject:[RCTDevMenuItem
|
|
buttonItemWithTitleBlock:^NSString * {
|
|
return devSettings.isElementInspectorShown ? @"Hide Inspector" : @"Show Inspector";
|
|
}
|
|
handler:^{
|
|
[devSettings toggleElementInspector];
|
|
}]];
|
|
|
|
if (devSettings.isHotLoadingAvailable) {
|
|
[items addObject:[RCTDevMenuItem
|
|
buttonItemWithTitleBlock:^NSString * {
|
|
// Previously known as "Hot Reloading". We won't use this term anymore.
|
|
return devSettings.isHotLoadingEnabled ? @"Disable Fast Refresh" : @"Enable Fast Refresh";
|
|
}
|
|
handler:^{
|
|
devSettings.isHotLoadingEnabled = !devSettings.isHotLoadingEnabled;
|
|
}]];
|
|
}
|
|
|
|
[items
|
|
addObject:[RCTDevMenuItem
|
|
buttonItemWithTitleBlock:^NSString * {
|
|
return @"Configure Bundler";
|
|
}
|
|
handler:^{
|
|
UIAlertController *alertController = [UIAlertController
|
|
alertControllerWithTitle:@"Configure Bundler"
|
|
message:@"Provide a custom bundler address, port, and entrypoint."
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
|
textField.placeholder = @"0.0.0.0";
|
|
}];
|
|
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
|
textField.placeholder = @"8081";
|
|
}];
|
|
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
|
|
textField.placeholder = @"index";
|
|
}];
|
|
[alertController
|
|
addAction:[UIAlertAction
|
|
actionWithTitle:@"Apply Changes"
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(__unused UIAlertAction *action) {
|
|
NSArray *textfields = alertController.textFields;
|
|
UITextField *ipTextField = textfields[0];
|
|
UITextField *portTextField = textfields[1];
|
|
UITextField *bundleRootTextField = textfields[2];
|
|
NSString *bundleRoot = bundleRootTextField.text;
|
|
if (ipTextField.text.length == 0 && portTextField.text.length == 0) {
|
|
[weakSelf setDefaultJSBundle];
|
|
return;
|
|
}
|
|
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
|
|
formatter.numberStyle = NSNumberFormatterDecimalStyle;
|
|
NSNumber *portNumber =
|
|
[formatter numberFromString:portTextField.text];
|
|
if (portNumber == nil) {
|
|
portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
|
|
}
|
|
[RCTBundleURLProvider sharedSettings].jsLocation = [NSString
|
|
stringWithFormat:@"%@:%d", ipTextField.text, portNumber.intValue];
|
|
if (bundleRoot.length == 0) {
|
|
[bundleManager resetBundleURL];
|
|
} else {
|
|
bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings]
|
|
jsBundleURLForBundleRoot:bundleRoot
|
|
fallbackResource:nil];
|
|
}
|
|
|
|
RCTTriggerReloadCommandListeners(@"Dev menu - apply changes");
|
|
}]];
|
|
[alertController addAction:[UIAlertAction actionWithTitle:@"Reset to Default"
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(__unused UIAlertAction *action) {
|
|
[weakSelf setDefaultJSBundle];
|
|
}]];
|
|
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel"
|
|
style:UIAlertActionStyleCancel
|
|
handler:^(__unused UIAlertAction *action) {
|
|
return;
|
|
}]];
|
|
[RCTPresentedViewController() presentViewController:alertController animated:YES completion:NULL];
|
|
}]];
|
|
|
|
[items addObjectsFromArray:_extraMenuItems];
|
|
return items;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(show)
|
|
{
|
|
if (_actionSheet || RCTRunningInAppExtension()) {
|
|
return;
|
|
}
|
|
|
|
NSString *bridgeDescription = _bridge.bridgeDescription;
|
|
NSString *description =
|
|
bridgeDescription.length > 0 ? [NSString stringWithFormat:@"Running %@", bridgeDescription] : nil;
|
|
|
|
// On larger devices we don't have an anchor point for the action sheet
|
|
UIAlertControllerStyle style = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone
|
|
? UIAlertControllerStyleActionSheet
|
|
: UIAlertControllerStyleAlert;
|
|
|
|
NSString *debugMenuType = self.bridge ? @"Bridge" : @"Bridgeless";
|
|
NSString *debugMenuTitle = [NSString stringWithFormat:@"React Native Debug Menu (%@)", debugMenuType];
|
|
|
|
_actionSheet = [UIAlertController alertControllerWithTitle:debugMenuTitle message:description preferredStyle:style];
|
|
|
|
NSArray<RCTDevMenuItem *> *items = [self _menuItemsToPresent];
|
|
for (RCTDevMenuItem *item in items) {
|
|
[_actionSheet addAction:[UIAlertAction actionWithTitle:item.title
|
|
style:UIAlertActionStyleDefault
|
|
handler:[self alertActionHandlerForDevItem:item]]];
|
|
}
|
|
|
|
[_actionSheet addAction:[UIAlertAction actionWithTitle:@"Cancel"
|
|
style:UIAlertActionStyleCancel
|
|
handler:[self alertActionHandlerForDevItem:nil]]];
|
|
|
|
_presentedItems = items;
|
|
[RCTPresentedViewController() presentViewController:_actionSheet animated:YES completion:nil];
|
|
|
|
[_callableJSModules invokeModule:@"RCTNativeAppEventEmitter" method:@"emit" withArgs:@[ @"RCTDevMenuShown" ]];
|
|
}
|
|
|
|
- (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__nullable)item
|
|
{
|
|
return ^(__unused UIAlertAction *action) {
|
|
if (item) {
|
|
[item callHandler];
|
|
}
|
|
|
|
self->_actionSheet = nil;
|
|
};
|
|
}
|
|
|
|
#pragma mark - deprecated methods and properties
|
|
|
|
#define WARN_DEPRECATED_DEV_MENU_EXPORT() \
|
|
RCTLogWarn(@"Using deprecated method %s, use RCTDevSettings instead", __func__)
|
|
|
|
- (void)setShakeToShow:(BOOL)shakeToShow
|
|
{
|
|
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isShakeToShowDevMenuEnabled = shakeToShow;
|
|
}
|
|
|
|
- (BOOL)shakeToShow
|
|
{
|
|
return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isShakeToShowDevMenuEnabled;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(reload)
|
|
{
|
|
WARN_DEPRECATED_DEV_MENU_EXPORT();
|
|
RCTTriggerReloadCommandListeners(@"Unknown from JS");
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(debugRemotely : (BOOL)enableDebug)
|
|
{
|
|
WARN_DEPRECATED_DEV_MENU_EXPORT();
|
|
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isDebuggingRemotely = enableDebug;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setProfilingEnabled : (BOOL)enabled)
|
|
{
|
|
WARN_DEPRECATED_DEV_MENU_EXPORT();
|
|
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isProfilingEnabled = enabled;
|
|
}
|
|
|
|
- (BOOL)profilingEnabled
|
|
{
|
|
return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isProfilingEnabled;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled)
|
|
{
|
|
WARN_DEPRECATED_DEV_MENU_EXPORT();
|
|
((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isHotLoadingEnabled = enabled;
|
|
}
|
|
|
|
- (BOOL)hotLoadingEnabled
|
|
{
|
|
return ((RCTDevSettings *)[_moduleRegistry moduleForName:"DevSettings"]).isHotLoadingEnabled;
|
|
}
|
|
|
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
{
|
|
return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(params);
|
|
}
|
|
|
|
@end
|
|
|
|
#else // Unavailable when not in dev mode
|
|
|
|
@interface RCTDevMenu () <NativeDevMenuSpec>
|
|
@end
|
|
|
|
@implementation RCTDevMenu
|
|
|
|
- (void)show
|
|
{
|
|
}
|
|
- (void)reload
|
|
{
|
|
}
|
|
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler
|
|
{
|
|
}
|
|
- (void)addItem:(RCTDevMenu *)item
|
|
{
|
|
}
|
|
|
|
- (void)debugRemotely:(BOOL)enableDebug
|
|
{
|
|
}
|
|
|
|
- (BOOL)isActionSheetShown
|
|
{
|
|
return NO;
|
|
}
|
|
+ (NSString *)moduleName
|
|
{
|
|
return @"DevMenu";
|
|
}
|
|
|
|
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
{
|
|
return std::make_shared<facebook::react::NativeDevMenuSpecJSI>(params);
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTDevMenuItem
|
|
|
|
+ (instancetype)buttonItemWithTitle:(NSString *)title handler:(void (^)(void))handler
|
|
{
|
|
return nil;
|
|
}
|
|
+ (instancetype)buttonItemWithTitleBlock:(NSString * (^)(void))titleBlock handler:(void (^)(void))handler
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
@end
|
|
|
|
#endif
|
|
|
|
@implementation RCTBridge (RCTDevMenu)
|
|
|
|
- (RCTDevMenu *)devMenu
|
|
{
|
|
#if RCT_DEV_MENU
|
|
return [self moduleForClass:[RCTDevMenu class]];
|
|
#else
|
|
return nil;
|
|
#endif
|
|
}
|
|
|
|
@end
|
|
|
|
Class RCTDevMenuCls(void)
|
|
{
|
|
return RCTDevMenu.class;
|
|
}
|