Skip to content

Commit cc068b0

Browse files
janicduplessisfacebook-github-bot
authored andcommitted
Export the DevSettings module, add addMenuItem method (#25848)
Summary: I wanted to configure the RN dev menu without having to write native code. This is pretty useful in a greenfield app since it avoids having to write a custom native module for both platforms (and might enable the feature for expo too). This ended up a bit more involved than planned since callbacks can only be called once. I needed to convert the `DevSettings` module to a `NativeEventEmitter` and use events when buttons are clicked. This means creating a JS wrapper for it. Currently it does not export all methods, they can be added in follow ups as needed. ## Changelog [General] [Added] - Export the DevSettings module, add `addMenuItem` method Pull Request resolved: #25848 Test Plan: Tested in an app using the following code. ```js if (__DEV__) { DevSettings.addMenuItem('Show Dev Screen', () => { dispatchNavigationAction( NavigationActions.navigate({ routeName: 'dev', }), ); }); } ``` Added an example in RN tester ![devmenu](https://user-images.githubusercontent.com/2677334/62000297-71624680-b0a1-11e9-8403-bc95c4747f0c.gif) Differential Revision: D17394916 Pulled By: cpojer fbshipit-source-id: f9d2c548b09821c594189d1436a27b97cf5a5737
1 parent 1534386 commit cc068b0

File tree

15 files changed

+190
-22
lines changed

15 files changed

+190
-22
lines changed

Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec-generated.mm

+7
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,10 @@ + (RCTManagedPointer *)JS_NativeAsyncStorage_SpecGetAllKeysCallbackError:(id)jso
801801
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "toggleElementInspector", @selector(toggleElementInspector), args, count);
802802
}
803803

804+
static facebook::jsi::Value __hostFunction_NativeDevSettingsSpecJSI_addMenuItem(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
805+
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "addMenuItem", @selector(addMenuItem:), args, count);
806+
}
807+
804808
static facebook::jsi::Value __hostFunction_NativeDevSettingsSpecJSI_setIsShakeToShowDevMenuEnabled(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
805809
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "setIsShakeToShowDevMenuEnabled", @selector(setIsShakeToShowDevMenuEnabled:), args, count);
806810
}
@@ -824,6 +828,9 @@ + (RCTManagedPointer *)JS_NativeAsyncStorage_SpecGetAllKeysCallbackError:(id)jso
824828
methodMap_["toggleElementInspector"] = MethodMetadata {0, __hostFunction_NativeDevSettingsSpecJSI_toggleElementInspector};
825829

826830

831+
methodMap_["addMenuItem"] = MethodMetadata {1, __hostFunction_NativeDevSettingsSpecJSI_addMenuItem};
832+
833+
827834
methodMap_["setIsShakeToShowDevMenuEnabled"] = MethodMetadata {1, __hostFunction_NativeDevSettingsSpecJSI_setIsShakeToShowDevMenuEnabled};
828835

829836

Libraries/FBReactNativeSpec/FBReactNativeSpec/FBReactNativeSpec.h

+1
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ namespace facebook {
743743
- (void)setIsDebuggingRemotely:(BOOL)isDebuggingRemotelyEnabled;
744744
- (void)setProfilingEnabled:(BOOL)isProfilingEnabled;
745745
- (void)toggleElementInspector;
746+
- (void)addMenuItem:(NSString *)title;
746747
- (void)setIsShakeToShowDevMenuEnabled:(BOOL)enabled;
747748

748749
@end

Libraries/NativeModules/specs/NativeDevSettings.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface Spec extends TurboModule {
1919
+setIsDebuggingRemotely: (isDebuggingRemotelyEnabled: boolean) => void;
2020
+setProfilingEnabled: (isProfilingEnabled: boolean) => void;
2121
+toggleElementInspector: () => void;
22+
+addMenuItem: (title: string) => void;
2223

2324
// iOS only.
2425
+setIsShakeToShowDevMenuEnabled: (enabled: boolean) => void;

Libraries/Utilities/DevSettings.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* @format
5+
*/
6+
7+
import NativeDevSettings from '../NativeModules/specs/NativeDevSettings';
8+
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';
9+
10+
class DevSettings extends NativeEventEmitter {
11+
_menuItems: Map<string, () => mixed>;
12+
13+
constructor() {
14+
super(NativeDevSettings);
15+
16+
this._menuItems = new Map();
17+
}
18+
19+
addMenuItem(title: string, handler: () => mixed) {
20+
// Make sure items are not added multiple times. This can
21+
// happen when hot reloading the module that registers the
22+
// menu items. The title is used as the id which means we
23+
// don't support multiple items with the same name.
24+
const oldHandler = this._menuItems.get(title);
25+
if (oldHandler != null) {
26+
this.removeListener('didPressMenuItem', oldHandler);
27+
} else {
28+
NativeDevSettings.addMenuItem(title);
29+
}
30+
31+
this._menuItems.set(title, handler);
32+
this.addListener('didPressMenuItem', event => {
33+
if (event.title === title) {
34+
handler();
35+
}
36+
});
37+
}
38+
39+
reload() {
40+
NativeDevSettings.reload();
41+
}
42+
43+
// TODO: Add other dev setting methods exposed by the native module.
44+
}
45+
46+
// Avoid including the full `NativeDevSettings` class in prod.
47+
class NoopDevSettings {
48+
addMenuItem(title: string, handler: () => mixed) {}
49+
reload() {}
50+
}
51+
52+
module.exports = __DEV__ ? new DevSettings() : new NoopDevSettings();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @flow
9+
*/
10+
11+
'use strict';
12+
13+
import * as React from 'react';
14+
import {Alert, Button, DevSettings} from 'react-native';
15+
16+
exports.title = 'DevSettings';
17+
exports.description = 'Customize the development settings';
18+
exports.examples = [
19+
{
20+
title: 'Add dev menu item',
21+
render(): React.Element<any> {
22+
return (
23+
<Button
24+
title="Add"
25+
onPress={() => {
26+
DevSettings.addMenuItem('Show Secret Dev Screen', () => {
27+
Alert.alert('Showing secret dev screen!');
28+
});
29+
}}
30+
/>
31+
);
32+
},
33+
},
34+
{
35+
title: 'Reload the app',
36+
render(): React.Element<any> {
37+
return (
38+
<Button
39+
title="Reload"
40+
onPress={() => {
41+
DevSettings.reload();
42+
}}
43+
/>
44+
);
45+
},
46+
},
47+
];

RNTester/js/utils/RNTesterList.android.js

+4
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ const APIExamples: Array<RNTesterExample> = [
156156
key: 'DatePickerAndroidExample',
157157
module: require('../examples/DatePicker/DatePickerAndroidExample'),
158158
},
159+
{
160+
key: 'DevSettings',
161+
module: require('../examples/DevSettings/DevSettingsExample'),
162+
},
159163
{
160164
key: 'Dimensions',
161165
module: require('../examples/Dimensions/DimensionsExample'),

RNTester/js/utils/RNTesterList.ios.js

+4
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ const APIExamples: Array<RNTesterExample> = [
235235
module: require('../examples/Crash/CrashExample'),
236236
supportsTVOS: false,
237237
},
238+
{
239+
key: 'DevSettings',
240+
module: require('../examples/DevSettings/DevSettingsExample'),
241+
},
238242
{
239243
key: 'Dimensions',
240244
module: require('../examples/Dimensions/DimensionsExample'),

React/Modules/RCTDevSettings.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#import <React/RCTBridge.h>
99
#import <React/RCTDefines.h>
10+
#import <React/RCTEventEmitter.h>
1011

1112
@protocol RCTPackagerClientMethod;
1213

@@ -29,7 +30,7 @@
2930

3031
@end
3132

32-
@interface RCTDevSettings : NSObject
33+
@interface RCTDevSettings : RCTEventEmitter
3334

3435
- (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource;
3536

React/Modules/RCTDevSettings.mm

+29-17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
#import "RCTProfile.h"
1717
#import "RCTUtils.h"
1818

19+
#import <React/RCTDevMenu.h>
20+
1921
static NSString *const kRCTDevSettingProfilingEnabled = @"profilingEnabled";
2022
static NSString *const kRCTDevSettingHotLoadingEnabled = @"hotLoadingEnabled";
2123
static NSString *const kRCTDevSettingIsInspectorShown = @"showInspector";
@@ -111,8 +113,6 @@ @interface RCTDevSettings () <RCTBridgeModule, RCTInvalidating> {
111113

112114
@implementation RCTDevSettings
113115

114-
@synthesize bridge = _bridge;
115-
116116
RCT_EXPORT_MODULE()
117117

118118
+ (BOOL)requiresMainQueueSetup
@@ -152,8 +152,7 @@ - (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource
152152

153153
- (void)setBridge:(RCTBridge *)bridge
154154
{
155-
RCTAssert(_bridge == nil, @"RCTDevSettings module should not be reused");
156-
_bridge = bridge;
155+
[super setBridge:bridge];
157156

158157
#if ENABLE_PACKAGER_CONNECTION
159158
RCTBridge *__weak weakBridge = bridge;
@@ -197,6 +196,11 @@ - (void)invalidate
197196
[[NSNotificationCenter defaultCenter] removeObserver:self];
198197
}
199198

199+
- (NSArray<NSString *> *)supportedEvents
200+
{
201+
return @[@"didPressMenuItem"];
202+
}
203+
200204
- (void)_updateSettingWithValue:(id)value forKey:(NSString *)key
201205
{
202206
[_dataSource updateSettingWithValue:value forKey:key];
@@ -210,7 +214,7 @@ - (id)settingForKey:(NSString *)key
210214
- (BOOL)isNuclideDebuggingAvailable
211215
{
212216
#if RCT_ENABLE_INSPECTOR
213-
return _bridge.isInspectable;
217+
return self.bridge.isInspectable;
214218
#else
215219
return false;
216220
#endif // RCT_ENABLE_INSPECTOR
@@ -227,12 +231,12 @@ - (BOOL)isRemoteDebuggingAvailable
227231

228232
- (BOOL)isHotLoadingAvailable
229233
{
230-
return _bridge.bundleURL && !_bridge.bundleURL.fileURL; // Only works when running from server
234+
return self.bridge.bundleURL && !self.bridge.bundleURL.fileURL; // Only works when running from server
231235
}
232236

233237
RCT_EXPORT_METHOD(reload)
234238
{
235-
[_bridge reload];
239+
[self.bridge reload];
236240
}
237241

238242
RCT_EXPORT_METHOD(setIsShakeToShowDevMenuEnabled : (BOOL)enabled)
@@ -285,10 +289,10 @@ - (void)_profilingSettingDidChange
285289
BOOL enabled = self.isProfilingEnabled;
286290
if (self.isHotLoadingAvailable && enabled != RCTProfileIsProfiling()) {
287291
if (enabled) {
288-
[_bridge startProfiling];
292+
[self.bridge startProfiling];
289293
} else {
290-
[_bridge stopProfiling:^(NSData *logData) {
291-
RCTProfileSendResult(self->_bridge, @"systrace", logData);
294+
[self.bridge stopProfiling:^(NSData *logData) {
295+
RCTProfileSendResult(self.bridge, @"systrace", logData);
292296
}];
293297
}
294298
}
@@ -302,9 +306,9 @@ - (void)_profilingSettingDidChange
302306
#pragma clang diagnostic push
303307
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
304308
if (enabled) {
305-
[_bridge enqueueJSCall:@"HMRClient" method:@"enable" args:@[] completion:NULL];
309+
[self.bridge enqueueJSCall:@"HMRClient" method:@"enable" args:@[] completion:NULL];
306310
} else {
307-
[_bridge enqueueJSCall:@"HMRClient" method:@"disable" args:@[] completion:NULL];
311+
[self.bridge enqueueJSCall:@"HMRClient" method:@"disable" args:@[] completion:NULL];
308312
}
309313
#pragma clang diagnostic pop
310314
}
@@ -329,6 +333,14 @@ - (BOOL)isHotLoadingEnabled
329333
}
330334
}
331335

336+
RCT_EXPORT_METHOD(addMenuItem:(NSString *)title)
337+
{
338+
__weak __typeof(self) weakSelf = self;
339+
[self.bridge.devMenu addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:^{
340+
[weakSelf sendEventWithName:@"didPressMenuItem" body:@{@"title": title}];
341+
}]];
342+
}
343+
332344
- (BOOL)isElementInspectorShown
333345
{
334346
return [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue];
@@ -347,17 +359,17 @@ - (BOOL)isPerfMonitorShown
347359
- (void)setExecutorClass:(Class)executorClass
348360
{
349361
_executorClass = executorClass;
350-
if (_bridge.executorClass != executorClass) {
362+
if (self.bridge.executorClass != executorClass) {
351363
// TODO (6929129): we can remove this special case test once we have better
352364
// support for custom executors in the dev menu. But right now this is
353365
// needed to prevent overriding a custom executor with the default if a
354366
// custom executor has been set directly on the bridge
355-
if (executorClass == Nil && _bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) {
367+
if (executorClass == Nil && self.bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) {
356368
return;
357369
}
358370

359-
_bridge.executorClass = executorClass;
360-
[_bridge reload];
371+
self.bridge.executorClass = executorClass;
372+
[self.bridge reload];
361373
}
362374
}
363375

@@ -386,7 +398,7 @@ - (void)_synchronizeAllSettings
386398

387399
- (void)jsLoaded:(NSNotification *)notification
388400
{
389-
if (notification.userInfo[@"bridge"] != _bridge) {
401+
if (notification.userInfo[@"bridge"] != self.bridge) {
390402
return;
391403
}
392404

ReactAndroid/src/androidTest/java/com/facebook/react/testing/ReactSettingsForTests.java

+3
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@ public void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled) {}
5252
public boolean isStartSamplingProfilerOnInit() {
5353
return false;
5454
}
55+
56+
@Override
57+
public void addMenuItem(String title) {}
5558
}

ReactAndroid/src/main/java/com/facebook/react/CoreModulesPackage.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public NativeModule getModule(String name, ReactApplicationContext reactContext)
139139
case DeviceEventManagerModule.NAME:
140140
return new DeviceEventManagerModule(reactContext, mHardwareBackBtnHandler);
141141
case DevSettingsModule.NAME:
142-
return new DevSettingsModule(mReactInstanceManager.getDevSupportManager());
142+
return new DevSettingsModule(reactContext, mReactInstanceManager.getDevSupportManager());
143143
case ExceptionsManagerModule.NAME:
144144
return new ExceptionsManagerModule(mReactInstanceManager.getDevSupportManager());
145145
case HeadlessJsTaskSupportModule.NAME:

ReactAndroid/src/main/java/com/facebook/react/devsupport/DevInternalSettings.java

+5
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ public boolean isStartSamplingProfilerOnInit() {
123123
return mPreferences.getBoolean(PREFS_START_SAMPLING_PROFILER_ON_INIT, false);
124124
}
125125

126+
@Override
127+
public void addMenuItem(String title) {
128+
// Not supported.
129+
}
130+
126131
public interface Listener {
127132
void onInternalSettingsChanged();
128133
}

ReactAndroid/src/main/java/com/facebook/react/modules/debug/DevSettingsModule.java

+27-3
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,31 @@
66
*/
77
package com.facebook.react.modules.debug;
88

9-
import com.facebook.react.bridge.BaseJavaModule;
9+
import com.facebook.react.bridge.Arguments;
10+
import com.facebook.react.bridge.ReactApplicationContext;
11+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
1012
import com.facebook.react.bridge.ReactMethod;
1113
import com.facebook.react.bridge.UiThreadUtil;
14+
import com.facebook.react.bridge.WritableMap;
15+
import com.facebook.react.devsupport.interfaces.DevOptionHandler;
1216
import com.facebook.react.devsupport.interfaces.DevSupportManager;
1317
import com.facebook.react.module.annotations.ReactModule;
18+
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
1419

1520
/**
1621
* Module that exposes the URL to the source code map (used for exception stack trace parsing) to JS
1722
*/
1823
@ReactModule(name = DevSettingsModule.NAME)
19-
public class DevSettingsModule extends BaseJavaModule {
24+
public class DevSettingsModule extends ReactContextBaseJavaModule {
2025

2126
public static final String NAME = "DevSettings";
2227

2328
private final DevSupportManager mDevSupportManager;
2429

25-
public DevSettingsModule(DevSupportManager devSupportManager) {
30+
public DevSettingsModule(
31+
ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
32+
super(reactContext);
33+
2634
mDevSupportManager = devSupportManager;
2735
}
2836

@@ -63,4 +71,20 @@ public void setProfilingEnabled(boolean isProfilingEnabled) {
6371
public void toggleElementInspector() {
6472
mDevSupportManager.toggleElementInspector();
6573
}
74+
75+
@ReactMethod
76+
public void addMenuItem(final String title) {
77+
mDevSupportManager.addCustomDevOption(
78+
title,
79+
new DevOptionHandler() {
80+
@Override
81+
public void onOptionSelected() {
82+
WritableMap data = Arguments.createMap();
83+
data.putString("title", title);
84+
getReactApplicationContext()
85+
.getJSModule(RCTDeviceEventEmitter.class)
86+
.emit("didPressMenuItem", data);
87+
}
88+
});
89+
}
6690
}

0 commit comments

Comments
 (0)