Skip to content

Commit

Permalink
Export the DevSettings module, add addMenuItem method (#25848)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
janicduplessis authored and facebook-github-bot committed Sep 17, 2019
1 parent 1534386 commit cc068b0
Show file tree
Hide file tree
Showing 15 changed files with 190 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,10 @@ + (RCTManagedPointer *)JS_NativeAsyncStorage_SpecGetAllKeysCallbackError:(id)jso
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "toggleElementInspector", @selector(toggleElementInspector), args, count);
}

static facebook::jsi::Value __hostFunction_NativeDevSettingsSpecJSI_addMenuItem(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, VoidKind, "addMenuItem", @selector(addMenuItem:), args, count);
}

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


methodMap_["addMenuItem"] = MethodMetadata {1, __hostFunction_NativeDevSettingsSpecJSI_addMenuItem};


methodMap_["setIsShakeToShowDevMenuEnabled"] = MethodMetadata {1, __hostFunction_NativeDevSettingsSpecJSI_setIsShakeToShowDevMenuEnabled};


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,7 @@ namespace facebook {
- (void)setIsDebuggingRemotely:(BOOL)isDebuggingRemotelyEnabled;
- (void)setProfilingEnabled:(BOOL)isProfilingEnabled;
- (void)toggleElementInspector;
- (void)addMenuItem:(NSString *)title;
- (void)setIsShakeToShowDevMenuEnabled:(BOOL)enabled;

@end
Expand Down
1 change: 1 addition & 0 deletions Libraries/NativeModules/specs/NativeDevSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface Spec extends TurboModule {
+setIsDebuggingRemotely: (isDebuggingRemotelyEnabled: boolean) => void;
+setProfilingEnabled: (isProfilingEnabled: boolean) => void;
+toggleElementInspector: () => void;
+addMenuItem: (title: string) => void;

// iOS only.
+setIsShakeToShowDevMenuEnabled: (enabled: boolean) => void;
Expand Down
52 changes: 52 additions & 0 deletions Libraries/Utilities/DevSettings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* @format
*/

import NativeDevSettings from '../NativeModules/specs/NativeDevSettings';
import NativeEventEmitter from '../EventEmitter/NativeEventEmitter';

class DevSettings extends NativeEventEmitter {
_menuItems: Map<string, () => mixed>;

constructor() {
super(NativeDevSettings);

this._menuItems = new Map();
}

addMenuItem(title: string, handler: () => mixed) {
// Make sure items are not added multiple times. This can
// happen when hot reloading the module that registers the
// menu items. The title is used as the id which means we
// don't support multiple items with the same name.
const oldHandler = this._menuItems.get(title);
if (oldHandler != null) {
this.removeListener('didPressMenuItem', oldHandler);
} else {
NativeDevSettings.addMenuItem(title);
}

this._menuItems.set(title, handler);
this.addListener('didPressMenuItem', event => {
if (event.title === title) {
handler();
}
});
}

reload() {
NativeDevSettings.reload();
}

// TODO: Add other dev setting methods exposed by the native module.
}

// Avoid including the full `NativeDevSettings` class in prod.
class NoopDevSettings {
addMenuItem(title: string, handler: () => mixed) {}
reload() {}
}

module.exports = __DEV__ ? new DevSettings() : new NoopDevSettings();
47 changes: 47 additions & 0 deletions RNTester/js/examples/DevSettings/DevSettingsExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* 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.
*
* @format
* @flow
*/

'use strict';

import * as React from 'react';
import {Alert, Button, DevSettings} from 'react-native';

exports.title = 'DevSettings';
exports.description = 'Customize the development settings';
exports.examples = [
{
title: 'Add dev menu item',
render(): React.Element<any> {
return (
<Button
title="Add"
onPress={() => {
DevSettings.addMenuItem('Show Secret Dev Screen', () => {
Alert.alert('Showing secret dev screen!');
});
}}
/>
);
},
},
{
title: 'Reload the app',
render(): React.Element<any> {
return (
<Button
title="Reload"
onPress={() => {
DevSettings.reload();
}}
/>
);
},
},
];
4 changes: 4 additions & 0 deletions RNTester/js/utils/RNTesterList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ const APIExamples: Array<RNTesterExample> = [
key: 'DatePickerAndroidExample',
module: require('../examples/DatePicker/DatePickerAndroidExample'),
},
{
key: 'DevSettings',
module: require('../examples/DevSettings/DevSettingsExample'),
},
{
key: 'Dimensions',
module: require('../examples/Dimensions/DimensionsExample'),
Expand Down
4 changes: 4 additions & 0 deletions RNTester/js/utils/RNTesterList.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@ const APIExamples: Array<RNTesterExample> = [
module: require('../examples/Crash/CrashExample'),
supportsTVOS: false,
},
{
key: 'DevSettings',
module: require('../examples/DevSettings/DevSettingsExample'),
},
{
key: 'Dimensions',
module: require('../examples/Dimensions/DimensionsExample'),
Expand Down
3 changes: 2 additions & 1 deletion React/Modules/RCTDevSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#import <React/RCTBridge.h>
#import <React/RCTDefines.h>
#import <React/RCTEventEmitter.h>

@protocol RCTPackagerClientMethod;

Expand All @@ -29,7 +30,7 @@

@end

@interface RCTDevSettings : NSObject
@interface RCTDevSettings : RCTEventEmitter

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

Expand Down
46 changes: 29 additions & 17 deletions React/Modules/RCTDevSettings.mm
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
#import "RCTProfile.h"
#import "RCTUtils.h"

#import <React/RCTDevMenu.h>

static NSString *const kRCTDevSettingProfilingEnabled = @"profilingEnabled";
static NSString *const kRCTDevSettingHotLoadingEnabled = @"hotLoadingEnabled";
static NSString *const kRCTDevSettingIsInspectorShown = @"showInspector";
Expand Down Expand Up @@ -111,8 +113,6 @@ @interface RCTDevSettings () <RCTBridgeModule, RCTInvalidating> {

@implementation RCTDevSettings

@synthesize bridge = _bridge;

RCT_EXPORT_MODULE()

+ (BOOL)requiresMainQueueSetup
Expand Down Expand Up @@ -152,8 +152,7 @@ - (instancetype)initWithDataSource:(id<RCTDevSettingsDataSource>)dataSource

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

#if ENABLE_PACKAGER_CONNECTION
RCTBridge *__weak weakBridge = bridge;
Expand Down Expand Up @@ -197,6 +196,11 @@ - (void)invalidate
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (NSArray<NSString *> *)supportedEvents
{
return @[@"didPressMenuItem"];
}

- (void)_updateSettingWithValue:(id)value forKey:(NSString *)key
{
[_dataSource updateSettingWithValue:value forKey:key];
Expand All @@ -210,7 +214,7 @@ - (id)settingForKey:(NSString *)key
- (BOOL)isNuclideDebuggingAvailable
{
#if RCT_ENABLE_INSPECTOR
return _bridge.isInspectable;
return self.bridge.isInspectable;
#else
return false;
#endif // RCT_ENABLE_INSPECTOR
Expand All @@ -227,12 +231,12 @@ - (BOOL)isRemoteDebuggingAvailable

- (BOOL)isHotLoadingAvailable
{
return _bridge.bundleURL && !_bridge.bundleURL.fileURL; // Only works when running from server
return self.bridge.bundleURL && !self.bridge.bundleURL.fileURL; // Only works when running from server
}

RCT_EXPORT_METHOD(reload)
{
[_bridge reload];
[self.bridge reload];
}

RCT_EXPORT_METHOD(setIsShakeToShowDevMenuEnabled : (BOOL)enabled)
Expand Down Expand Up @@ -285,10 +289,10 @@ - (void)_profilingSettingDidChange
BOOL enabled = self.isProfilingEnabled;
if (self.isHotLoadingAvailable && enabled != RCTProfileIsProfiling()) {
if (enabled) {
[_bridge startProfiling];
[self.bridge startProfiling];
} else {
[_bridge stopProfiling:^(NSData *logData) {
RCTProfileSendResult(self->_bridge, @"systrace", logData);
[self.bridge stopProfiling:^(NSData *logData) {
RCTProfileSendResult(self.bridge, @"systrace", logData);
}];
}
}
Expand All @@ -302,9 +306,9 @@ - (void)_profilingSettingDidChange
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
if (enabled) {
[_bridge enqueueJSCall:@"HMRClient" method:@"enable" args:@[] completion:NULL];
[self.bridge enqueueJSCall:@"HMRClient" method:@"enable" args:@[] completion:NULL];
} else {
[_bridge enqueueJSCall:@"HMRClient" method:@"disable" args:@[] completion:NULL];
[self.bridge enqueueJSCall:@"HMRClient" method:@"disable" args:@[] completion:NULL];
}
#pragma clang diagnostic pop
}
Expand All @@ -329,6 +333,14 @@ - (BOOL)isHotLoadingEnabled
}
}

RCT_EXPORT_METHOD(addMenuItem:(NSString *)title)
{
__weak __typeof(self) weakSelf = self;
[self.bridge.devMenu addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:^{
[weakSelf sendEventWithName:@"didPressMenuItem" body:@{@"title": title}];
}]];
}

- (BOOL)isElementInspectorShown
{
return [[self settingForKey:kRCTDevSettingIsInspectorShown] boolValue];
Expand All @@ -347,17 +359,17 @@ - (BOOL)isPerfMonitorShown
- (void)setExecutorClass:(Class)executorClass
{
_executorClass = executorClass;
if (_bridge.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 && _bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) {
if (executorClass == Nil && self.bridge.executorClass != objc_lookUpClass("RCTWebSocketExecutor")) {
return;
}

_bridge.executorClass = executorClass;
[_bridge reload];
self.bridge.executorClass = executorClass;
[self.bridge reload];
}
}

Expand Down Expand Up @@ -386,7 +398,7 @@ - (void)_synchronizeAllSettings

- (void)jsLoaded:(NSNotification *)notification
{
if (notification.userInfo[@"bridge"] != _bridge) {
if (notification.userInfo[@"bridge"] != self.bridge) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,7 @@ public void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled) {}
public boolean isStartSamplingProfilerOnInit() {
return false;
}

@Override
public void addMenuItem(String title) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ public NativeModule getModule(String name, ReactApplicationContext reactContext)
case DeviceEventManagerModule.NAME:
return new DeviceEventManagerModule(reactContext, mHardwareBackBtnHandler);
case DevSettingsModule.NAME:
return new DevSettingsModule(mReactInstanceManager.getDevSupportManager());
return new DevSettingsModule(reactContext, mReactInstanceManager.getDevSupportManager());
case ExceptionsManagerModule.NAME:
return new ExceptionsManagerModule(mReactInstanceManager.getDevSupportManager());
case HeadlessJsTaskSupportModule.NAME:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ public boolean isStartSamplingProfilerOnInit() {
return mPreferences.getBoolean(PREFS_START_SAMPLING_PROFILER_ON_INIT, false);
}

@Override
public void addMenuItem(String title) {
// Not supported.
}

public interface Listener {
void onInternalSettingsChanged();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@
*/
package com.facebook.react.modules.debug;

import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.devsupport.interfaces.DevOptionHandler;
import com.facebook.react.devsupport.interfaces.DevSupportManager;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;

/**
* Module that exposes the URL to the source code map (used for exception stack trace parsing) to JS
*/
@ReactModule(name = DevSettingsModule.NAME)
public class DevSettingsModule extends BaseJavaModule {
public class DevSettingsModule extends ReactContextBaseJavaModule {

public static final String NAME = "DevSettings";

private final DevSupportManager mDevSupportManager;

public DevSettingsModule(DevSupportManager devSupportManager) {
public DevSettingsModule(
ReactApplicationContext reactContext, DevSupportManager devSupportManager) {
super(reactContext);

mDevSupportManager = devSupportManager;
}

Expand Down Expand Up @@ -63,4 +71,20 @@ public void setProfilingEnabled(boolean isProfilingEnabled) {
public void toggleElementInspector() {
mDevSupportManager.toggleElementInspector();
}

@ReactMethod
public void addMenuItem(final String title) {
mDevSupportManager.addCustomDevOption(
title,
new DevOptionHandler() {
@Override
public void onOptionSelected() {
WritableMap data = Arguments.createMap();
data.putString("title", title);
getReactApplicationContext()
.getJSModule(RCTDeviceEventEmitter.class)
.emit("didPressMenuItem", data);
}
});
}
}
Loading

0 comments on commit cc068b0

Please sign in to comment.