/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "RCTDevSettings.h" #import #import #import #import #import #import #import #import #import #import #import #import #import "CoreModulesPlugins.h" static NSString *const kRCTDevSettingProfilingEnabled = @"profilingEnabled"; static NSString *const kRCTDevSettingHotLoadingEnabled = @"hotLoadingEnabled"; static NSString *const kRCTDevSettingIsInspectorShown = @"showInspector"; static NSString *const kRCTDevSettingIsDebuggingRemotely = @"isDebuggingRemotely"; static NSString *const kRCTDevSettingExecutorOverrideClass = @"executor-override"; static NSString *const kRCTDevSettingShakeToShowDevMenu = @"shakeToShow"; static NSString *const kRCTDevSettingIsPerfMonitorShown = @"RCTPerfMonitorKey"; static NSString *const kRCTDevSettingsUserDefaultsKey = @"RCTDevMenu"; #if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION #import #import #endif #if RCT_ENABLE_INSPECTOR #import #endif #if RCT_DEV static BOOL devSettingsMenuEnabled = YES; #else static BOOL devSettingsMenuEnabled = NO; #endif void RCTDevSettingsSetEnabled(BOOL enabled) { devSettingsMenuEnabled = enabled; } #if RCT_DEV_MENU || RCT_REMOTE_PROFILE @interface RCTDevSettingsUserDefaultsDataSource : NSObject @end @implementation RCTDevSettingsUserDefaultsDataSource { NSMutableDictionary *_settings; NSUserDefaults *_userDefaults; } - (instancetype)init { return [self initWithDefaultValues:nil]; } - (instancetype)initWithDefaultValues:(NSDictionary *)defaultValues { if (self = [super init]) { _userDefaults = [NSUserDefaults standardUserDefaults]; if (defaultValues) { [self _reloadWithDefaults:defaultValues]; } } return self; } - (void)updateSettingWithValue:(id)value forKey:(NSString *)key { RCTAssert((key != nil), @"%@", [NSString stringWithFormat:@"%@: Tried to update nil key", [self class]]); id currentValue = [self settingForKey:key]; if (currentValue == value || [currentValue isEqual:value]) { return; } if (value) { _settings[key] = value; } else { [_settings removeObjectForKey:key]; } [_userDefaults setObject:_settings forKey:kRCTDevSettingsUserDefaultsKey]; } - (id)settingForKey:(NSString *)key { return _settings[key]; } - (void)_reloadWithDefaults:(NSDictionary *)defaultValues { NSDictionary *existingSettings = [_userDefaults objectForKey:kRCTDevSettingsUserDefaultsKey]; _settings = existingSettings ? [existingSettings mutableCopy] : [NSMutableDictionary dictionary]; for (NSString *key in [defaultValues keyEnumerator]) { if (!_settings[key]) { _settings[key] = defaultValues[key]; } } [_userDefaults setObject:_settings forKey:kRCTDevSettingsUserDefaultsKey]; } @end #if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION static RCTHandlerToken reloadToken; static RCTHandlerToken devMenuToken; static std::atomic numInitializedModules{0}; #endif @interface RCTDevSettings () { BOOL _isJSLoaded; #if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION RCTHandlerToken _bridgeExecutorOverrideToken; #endif } @property (nonatomic, strong) Class executorClass; @property (nonatomic, readwrite, strong) id dataSource; @end @implementation RCTDevSettings @synthesize isInspectable = _isInspectable; @synthesize bundleManager = _bundleManager; RCT_EXPORT_MODULE() - (instancetype)init { // Default behavior is to use NSUserDefaults with shake and hot loading enabled. NSDictionary *defaultValues = @{ kRCTDevSettingShakeToShowDevMenu : @YES, kRCTDevSettingHotLoadingEnabled : @YES, }; RCTDevSettingsUserDefaultsDataSource *dataSource = [[RCTDevSettingsUserDefaultsDataSource alloc] initWithDefaultValues:defaultValues]; return [self initWithDataSource:dataSource]; } + (BOOL)requiresMainQueueSetup { return NO; } - (instancetype)initWithDataSource:(id)dataSource { if (self = [super init]) { _dataSource = dataSource; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jsLoaded:) name:RCTJavaScriptDidLoadNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jsLoaded:) name:@"RCTInstanceDidLoadBundle" object:nil]; } return self; } - (void)initialize { #if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION if (self.bridge) { RCTBridge *__weak weakBridge = self.bridge; _bridgeExecutorOverrideToken = [[RCTPackagerConnection sharedPackagerConnection] addNotificationHandler:^(id params) { if (params != (id)kCFNull && [params[@"debug"] boolValue]) { weakBridge.executorClass = objc_lookUpClass("RCTWebSocketExecutor"); } } queue:dispatch_get_main_queue() forMethod:@"reload"]; } if (numInitializedModules++ == 0) { reloadToken = [[RCTPackagerConnection sharedPackagerConnection] addNotificationHandler:^(id params) { RCTTriggerReloadCommandListeners(@"Global hotkey"); } queue:dispatch_get_main_queue() forMethod:@"reload"]; #if RCT_DEV_MENU devMenuToken = [[RCTPackagerConnection sharedPackagerConnection] addNotificationHandler:^(id params) { [self.bridge.devMenu show]; } queue:dispatch_get_main_queue() forMethod:@"devMenu"]; #endif } #endif #if RCT_ENABLE_INSPECTOR if (self.bridge) { // We need this dispatch to the main thread because the bridge is not yet // finished with its initialisation. By the time it relinquishes control of // the main thread, this operation can be performed. __weak __typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ __typeof(self) strongSelf = weakSelf; if (!strongSelf) { return; } id dispatchBlock = ^{ __typeof(self) strongSelf2 = weakSelf; if (!strongSelf2) { return; } NSURL *url = strongSelf2.bundleManager.bundleURL; [RCTInspectorDevServerHelper connectWithBundleURL:url]; }; [strongSelf.bridge dispatchBlock:dispatchBlock queue:RCTJSThread]; }); } else { NSURL *url = self.bundleManager.bundleURL; [RCTInspectorDevServerHelper connectWithBundleURL:url]; } #endif __weak __typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [weakSelf _synchronizeAllSettings]; }); } - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - (void)invalidate { [super invalidate]; #if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION if (self.bridge) { [[RCTPackagerConnection sharedPackagerConnection] removeHandler:_bridgeExecutorOverrideToken]; } if (--numInitializedModules == 0) { [[RCTPackagerConnection sharedPackagerConnection] removeHandler:reloadToken]; #if RCT_DEV_MENU [[RCTPackagerConnection sharedPackagerConnection] removeHandler:devMenuToken]; #endif } #endif } - (NSArray *)supportedEvents { return @[ @"didPressMenuItem" ]; } - (void)_updateSettingWithValue:(id)value forKey:(NSString *)key { [_dataSource updateSettingWithValue:value forKey:key]; } - (id)settingForKey:(NSString *)key { return [_dataSource settingForKey:key]; } - (BOOL)isDeviceDebuggingAvailable { #if RCT_ENABLE_INSPECTOR if (self.bridge) { return self.bridge.isInspectable; } else { return self.isInspectable; } #else return false; #endif // RCT_ENABLE_INSPECTOR } - (BOOL)isRemoteDebuggingAvailable { if (RCTTurboModuleEnabled()) { return NO; } Class jsDebuggingExecutorClass = objc_lookUpClass("RCTWebSocketExecutor"); return (jsDebuggingExecutorClass != nil); } - (BOOL)isHotLoadingAvailable { if (self.bundleManager.bundleURL) { return !self.bundleManager.bundleURL.fileURL; } return NO; } RCT_EXPORT_METHOD(reload) { RCTTriggerReloadCommandListeners(@"Unknown From JS"); } RCT_EXPORT_METHOD(reloadWithReason : (NSString *)reason) { RCTTriggerReloadCommandListeners(reason); } RCT_EXPORT_METHOD(onFastRefresh) { [self.bridge onFastRefresh]; } RCT_EXPORT_METHOD(setIsShakeToShowDevMenuEnabled : (BOOL)enabled) { [self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingShakeToShowDevMenu]; } - (BOOL)isShakeToShowDevMenuEnabled { return [[self settingForKey:kRCTDevSettingShakeToShowDevMenu] boolValue]; } RCT_EXPORT_METHOD(setIsDebuggingRemotely : (BOOL)enabled) { [self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingIsDebuggingRemotely]; [self _remoteDebugSettingDidChange]; } - (BOOL)isDebuggingRemotely { return [[self settingForKey:kRCTDevSettingIsDebuggingRemotely] boolValue]; } - (void)_remoteDebugSettingDidChange { // This value is passed as a command-line argument, so fall back to reading from NSUserDefaults directly NSString *executorOverride = [[NSUserDefaults standardUserDefaults] stringForKey:kRCTDevSettingExecutorOverrideClass]; Class executorOverrideClass = executorOverride ? NSClassFromString(executorOverride) : nil; if (executorOverrideClass) { self.executorClass = executorOverrideClass; } else { BOOL enabled = self.isRemoteDebuggingAvailable && self.isDebuggingRemotely; self.executorClass = enabled ? objc_getClass("RCTWebSocketExecutor") : nil; } } RCT_EXPORT_METHOD(setProfilingEnabled : (BOOL)enabled) { [self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingProfilingEnabled]; [self _profilingSettingDidChange]; } - (BOOL)isProfilingEnabled { return [[self settingForKey:kRCTDevSettingProfilingEnabled] boolValue]; } - (void)_profilingSettingDidChange { BOOL enabled = self.isProfilingEnabled; if (self.isHotLoadingAvailable && enabled != RCTProfileIsProfiling()) { if (enabled) { [self.bridge startProfiling]; } else { __weak __typeof(self) weakSelf = self; [self.bridge stopProfiling:^(NSData *logData) { __typeof(self) strongSelf = weakSelf; if (!strongSelf) { return; } RCTProfileSendResult(strongSelf.bridge, @"systrace", logData); }]; } } } RCT_EXPORT_METHOD(setHotLoadingEnabled : (BOOL)enabled) { if (self.isHotLoadingEnabled != enabled) { [self _updateSettingWithValue:@(enabled) forKey:kRCTDevSettingHotLoadingEnabled]; if (_isJSLoaded) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (enabled) { if (self.callableJSModules) { [self.callableJSModules invokeModule:@"HMRClient" method:@"enable" withArgs:@[]]; } } else { if (self.callableJSModules) { [self.callableJSModules invokeModule:@"HMRClient" method:@"disable" withArgs:@[]]; } } #pragma clang diagnostic pop } } } - (BOOL)isHotLoadingEnabled { return [[self settingForKey:kRCTDevSettingHotLoadingEnabled] boolValue]; } RCT_EXPORT_METHOD(toggleElementInspector) { BOOL value = [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue]; [self _updateSettingWithValue:@(!value) forKey:kRCTDevSettingIsInspectorShown]; if (_isJSLoaded) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [[self.moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"toggleElementInspector" body:nil]; #pragma clang diagnostic pop } } RCT_EXPORT_METHOD(addMenuItem : (NSString *)title) { __weak __typeof(self) weakSelf = self; [(RCTDevMenu *)[self.moduleRegistry moduleForName:"DevMenu"] addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:^{ [weakSelf sendEventWithName:@"didPressMenuItem" body:@{@"title" : title}]; }]]; } - (BOOL)isElementInspectorShown { return [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue]; } - (void)setIsPerfMonitorShown:(BOOL)isPerfMonitorShown { [self _updateSettingWithValue:@(isPerfMonitorShown) forKey:kRCTDevSettingIsPerfMonitorShown]; } - (BOOL)isPerfMonitorShown { return [[self settingForKey:kRCTDevSettingIsPerfMonitorShown] boolValue]; } - (void)setExecutorClass:(Class)executorClass { _executorClass = executorClass; if (self.bridge.executorClass != executorClass) { // TODO (6929129): we can remove this special case test once we have better // support for custom executors in the dev menu. But right now this is // needed to prevent overriding a custom executor with the default if a // custom executor has been set directly on the bridge if (executorClass == Nil && self.bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) { return; } self.bridge.executorClass = executorClass; RCTTriggerReloadCommandListeners(@"Custom executor class reset"); } } - (void)addHandler:(id)handler forPackagerMethod:(NSString *)name { #if RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION [[RCTPackagerConnection sharedPackagerConnection] addHandler:handler forMethod:name]; #endif } - (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL { if (bundleURL && !bundleURL.fileURL) { NSURLComponents *urlComponents = [[NSURLComponents alloc] initWithURL:bundleURL resolvingAgainstBaseURL:NO]; NSString *const path = [urlComponents.path substringFromIndex:1]; // Strip initial slash. NSString *const host = urlComponents.host; NSNumber *const port = urlComponents.port; NSString *const scheme = urlComponents.scheme; BOOL isHotLoadingEnabled = self.isHotLoadingEnabled; if (self.callableJSModules) { [self.callableJSModules invokeModule:@"HMRClient" method:@"setup" withArgs:@[ @"ios", path, host, RCTNullIfNil(port), @(isHotLoadingEnabled), scheme ]]; } } } - (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL { if (bundleURL && !bundleURL.fileURL) { // isHotLoadingAvailable check if (self.callableJSModules) { [self.callableJSModules invokeModule:@"HMRClient" method:@"registerBundle" withArgs:@[ [bundleURL absoluteString] ]]; } } } #pragma mark - Internal /** * Query the data source for all possible settings and make sure we're doing the right * thing for the state of each setting. */ - (void)_synchronizeAllSettings { [self _remoteDebugSettingDidChange]; [self _profilingSettingDidChange]; } - (void)jsLoaded:(NSNotification *)notification { // In bridge mode, the bridge that sent the notif must be the same as the one stored in this module. // In bridgless mode, we don't care about this. if ([notification.name isEqualToString:RCTJavaScriptDidLoadNotification] && notification.userInfo[@"bridge"] != self.bridge) { return; } _isJSLoaded = YES; __weak __typeof(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ __typeof(self) strongSelf = weakSelf; if (!strongSelf) { return; } // update state again after the bridge has finished loading [strongSelf _synchronizeAllSettings]; // Inspector can only be shown after JS has loaded if ([strongSelf isElementInspectorShown]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [[strongSelf.moduleRegistry moduleForName:"EventDispatcher"] sendDeviceEventWithName:@"toggleElementInspector" body:nil]; #pragma clang diagnostic pop } }); } - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @end #else // #if RCT_DEV_MENU @interface RCTDevSettings () @end @implementation RCTDevSettings - (instancetype)initWithDataSource:(id)dataSource { return [super init]; } - (void)initialize { } - (BOOL)isHotLoadingAvailable { return NO; } - (BOOL)isRemoteDebuggingAvailable { return NO; } + (BOOL)requiresMainQueueSetup { return NO; } - (id)settingForKey:(NSString *)key { return nil; } - (void)reload { } - (void)reloadWithReason:(NSString *)reason { } - (void)onFastRefresh { } - (void)setHotLoadingEnabled:(BOOL)isHotLoadingEnabled { } - (void)setIsDebuggingRemotely:(BOOL)isDebuggingRemotelyEnabled { } - (void)setProfilingEnabled:(BOOL)isProfilingEnabled { } - (void)toggleElementInspector { } - (void)setupHMRClientWithBundleURL:(NSURL *)bundleURL { } - (void)setupHMRClientWithAdditionalBundleURL:(NSURL *)bundleURL { } - (void)addMenuItem:(NSString *)title { } - (void)setIsShakeToShowDevMenuEnabled:(BOOL)enabled { } - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); } @end #endif // #if RCT_DEV_MENU @implementation RCTBridge (RCTDevSettings) - (RCTDevSettings *)devSettings { #if RCT_REMOTE_PROFILE return [self moduleForClass:[RCTDevSettings class]]; #elif RCT_DEV_MENU return devSettingsMenuEnabled ? [self moduleForClass:[RCTDevSettings class]] : nil; #else return nil; #endif } @end Class RCTDevSettingsCls(void) { return RCTDevSettings.class; }