Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export the DevSettings module, add addMenuItem method #25848

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Libraries/NativeModules/specs/NativeDevSettings.js
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
46 changes: 46 additions & 0 deletions Libraries/Utilities/DevSettings.js
@@ -0,0 +1,46 @@
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel good about using the string as identifier to connect them. Shall we use a numeric ID to keep track of them instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for that is android already stores them by their name (https://github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/devsupport/DevSupportManagerImpl.java#L309) so it would break later anyway if 2 items have the same name. It also makes it easier to not add the same item multiple times when hot reloading (see this comment https://github.com/facebook/react-native/pull/25848/files#diff-bd79be22516654291770eb407afd366fR14).

Not ideal but in practice for a dev only module I thought this was fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I hate it!

Do you mind rebasing and making the flow type change?

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
@@ -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
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
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
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
Expand Up @@ -16,6 +16,8 @@
#import "RCTProfile.h"
#import "RCTUtils.h"

#import <React/RCTDevMenu.h>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this okay? This header is from the DevSupport target. Otherwise I could check if bridge.devMenu exists at runtime.


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
Expand Up @@ -52,4 +52,7 @@ public void setRemoteJSDebugEnabled(boolean remoteJSDebugEnabled) {}
public boolean isStartSamplingProfilerOnInit() {
return false;
}

@Override
public void addMenuItem(String title) {}
}
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
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
Expand Up @@ -6,23 +6,30 @@
*/
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.DevSupportManager;
import com.facebook.react.devsupport.interfaces.DevOptionHandler;
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 +70,18 @@ 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);
}
});
}
}
Expand Up @@ -35,4 +35,7 @@ public interface DeveloperSettings {

/** @return Whether Start Sampling Profiler on App Start is enabled. */
boolean isStartSamplingProfilerOnInit();

/** Add an item to the dev menu. */
void addMenuItem(String title);
}
4 changes: 4 additions & 0 deletions index.js
Expand Up @@ -56,6 +56,7 @@ import typeof BackHandler from './Libraries/Utilities/BackHandler';
import typeof Clipboard from './Libraries/Components/Clipboard/Clipboard';
import typeof DatePickerAndroid from './Libraries/Components/DatePickerAndroid/DatePickerAndroid';
import typeof DeviceInfo from './Libraries/Utilities/DeviceInfo';
import typeof DevSettings from './Libraries/Utilities/DevSettings';
import typeof Dimensions from './Libraries/Utilities/Dimensions';
import typeof Easing from './Libraries/Animated/src/Easing';
import typeof ReactNative from './Libraries/Renderer/shims/ReactNative';
Expand Down Expand Up @@ -291,6 +292,9 @@ module.exports = {
get DeviceInfo(): DeviceInfo {
return require('./Libraries/Utilities/DeviceInfo');
},
get DevSettings(): DevSettings {
return require('./Libraries/Utilities/DevSettings');
},
get Dimensions(): Dimensions {
return require('./Libraries/Utilities/Dimensions');
},
Expand Down