Skip to content

Commit d494510

Browse files
sbuggaymeta-codesync[bot]
authored andcommitted
External inspection support (facebook#55408)
Summary: Pull Request resolved: facebook#55408 Changelog: [Internal] Reviewed By: hoxyq Differential Revision: D91919421 fbshipit-source-id: 7c7bb0ccefa758745600202bcaf216c019eae615
1 parent f714658 commit d494510

File tree

6 files changed

+190
-20
lines changed

6 files changed

+190
-20
lines changed

packages/react-native/Libraries/ReactNative/AppContainer-dev.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
* @format
99
*/
1010

11+
import type {TouchedViewDataAtPoint} from '../Renderer/shims/ReactNativeTypes';
1112
import type {
1213
ReactDevToolsAgent,
1314
ReactDevToolsGlobalHook,
1415
} from '../Types/ReactDevToolsTypes';
1516
import type {Props} from './AppContainer';
1617

18+
import useExternalInspection from '../../src/private/devsupport/devmenu/elementinspector/useExternalInspection';
1719
import ReactNativeStyleAttributes from '../Components/View/ReactNativeStyleAttributes';
1820
import View from '../Components/View/View';
1921
import DebuggingOverlay from '../Debugging/DebuggingOverlay';
@@ -40,16 +42,25 @@ if (reactDevToolsHook) {
4042
);
4143
}
4244

45+
type ExternalInspection = {
46+
externalInspectingEnabled: boolean,
47+
+reportToExternalInspection: (viewData: TouchedViewDataAtPoint) => void,
48+
};
49+
4350
type InspectorDeferredProps = {
4451
inspectedViewRef: InspectedViewRef,
4552
onInspectedViewRerenderRequest: () => void,
4653
reactDevToolsAgent?: ReactDevToolsAgent,
54+
devMenuInspectorOpen: boolean,
55+
externalInspection: ExternalInspection,
4756
};
4857

4958
const InspectorDeferred = ({
5059
inspectedViewRef,
5160
onInspectedViewRerenderRequest,
5261
reactDevToolsAgent,
62+
devMenuInspectorOpen,
63+
externalInspection,
5364
}: InspectorDeferredProps) => {
5465
// D39382967 adds a require cycle: InitializeCore -> AppContainer -> Inspector -> InspectorPanel -> ScrollView -> InitializeCore
5566
// We can't remove it yet, fallback to dynamic require for now. This is the only reason why this logic is in a separate function.
@@ -61,6 +72,8 @@ const InspectorDeferred = ({
6172
inspectedViewRef={inspectedViewRef}
6273
onRequestRerenderApp={onInspectedViewRerenderRequest}
6374
reactDevToolsAgent={reactDevToolsAgent}
75+
devMenuInspectorOpen={devMenuInspectorOpen}
76+
externalInspection={externalInspection}
6477
/>
6578
);
6679
};
@@ -108,6 +121,7 @@ const AppContainer = ({
108121
const [shouldRenderInspector, setShouldRenderInspector] = useState(false);
109122
const [reactDevToolsAgent, setReactDevToolsAgent] =
110123
useState<ReactDevToolsAgent | void>(reactDevToolsHook?.reactDevtoolsAgent);
124+
const externalInspection = useExternalInspection();
111125

112126
useEffect(() => {
113127
let inspectorSubscription = null;
@@ -179,11 +193,14 @@ const AppContainer = ({
179193
/>
180194
)}
181195

182-
{shouldRenderInspector && (
196+
{(shouldRenderInspector ||
197+
externalInspection.externalInspectingEnabled) && (
183198
<InspectorDeferred
184199
inspectedViewRef={innerViewRef}
185200
onInspectedViewRerenderRequest={onInspectedViewRerenderRequest}
186201
reactDevToolsAgent={reactDevToolsAgent}
202+
devMenuInspectorOpen={shouldRenderInspector}
203+
externalInspection={externalInspection}
187204
/>
188205
)}
189206

packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,6 +936,17 @@ const definitions: FeatureFlagDefinitions = {
936936
},
937937
ossReleaseStage: 'none',
938938
},
939+
externalElementInspectionEnabled: {
940+
defaultValue: false,
941+
metadata: {
942+
dateAdded: '2026-02-04',
943+
description:
944+
'Enable the external inspection API for DevTools to communicate with the Inspector overlay.',
945+
expectedReleaseValue: true,
946+
purpose: 'experimentation',
947+
},
948+
ossReleaseStage: 'none',
949+
},
939950
fixVirtualizeListCollapseWindowSize: {
940951
defaultValue: false,
941952
metadata: {

packages/react-native/src/private/devsupport/devmenu/elementinspector/Inspector.js

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,29 @@ export type InspectedElement = Readonly<{
4747
}>;
4848
export type ElementsHierarchy = InspectorData['hierarchy'];
4949

50+
type ExternalInspection = {
51+
externalInspectingEnabled: boolean,
52+
+reportToExternalInspection: (viewData: TouchedViewDataAtPoint) => void,
53+
};
54+
5055
type Props = {
5156
inspectedViewRef: InspectedViewRef,
5257
onRequestRerenderApp: () => void,
5358
reactDevToolsAgent?: ReactDevToolsAgent,
59+
devMenuInspectorOpen: boolean,
60+
externalInspection: ExternalInspection,
5461
};
5562

5663
function Inspector({
5764
inspectedViewRef,
5865
onRequestRerenderApp,
5966
reactDevToolsAgent,
67+
devMenuInspectorOpen,
68+
externalInspection,
6069
}: Props): React.Node {
61-
const [inspecting, setInspecting] = useState<boolean>(true);
70+
const [inspectingEnabled, setInspectingEnabled] = useState<boolean>(true);
71+
const {externalInspectingEnabled, reportToExternalInspection} =
72+
externalInspection;
6273

6374
const [panelPosition, setPanelPosition] = useState<PanelPosition>('bottom');
6475
const [inspectedElement, setInspectedElement] =
@@ -67,6 +78,9 @@ function Inspector({
6778
const [elementsHierarchy, setElementsHierarchy] =
6879
useState<?ElementsHierarchy>(null);
6980

81+
// Derive inspecting state: external inspection forces it on, otherwise use local state
82+
const isInspecting = externalInspectingEnabled || inspectingEnabled;
83+
7084
const setSelection = (i: number) => {
7185
const hierarchyItem = elementsHierarchy?.[i];
7286
if (hierarchyItem == null) {
@@ -99,6 +113,11 @@ function Inspector({
99113
closestInstance,
100114
} = viewData;
101115

116+
// Report to external inspection if in external inspection mode
117+
if (externalInspectingEnabled) {
118+
reportToExternalInspection(viewData);
119+
}
120+
102121
// Sync the touched view with React DevTools.
103122
// Note: This is Paper only. To support Fabric,
104123
// DevTools needs to be updated to not rely on view tags.
@@ -138,7 +157,7 @@ function Inspector({
138157
};
139158

140159
const handleSetInspecting = (enabled: boolean) => {
141-
setInspecting(enabled);
160+
setInspectingEnabled(enabled);
142161
setInspectedElement(null);
143162
};
144163

@@ -154,26 +173,29 @@ function Inspector({
154173

155174
return (
156175
<View style={styles.container} pointerEvents="box-none">
157-
{inspecting && (
176+
{isInspecting && (
158177
<InspectorOverlay
159178
inspected={inspectedElement}
160179
onTouchPoint={onTouchPoint}
180+
externalInspectionActive={externalInspectingEnabled}
161181
/>
162182
)}
163183

164-
<SafeAreaView style={[styles.panelContainer, panelContainerStyle]}>
165-
<InspectorPanel
166-
devtoolsIsOpen={!!reactDevToolsAgent}
167-
inspecting={inspecting}
168-
setInspecting={handleSetInspecting}
169-
inspected={inspectedElement}
170-
hierarchy={elementsHierarchy}
171-
selection={selectionIndex}
172-
setSelection={setSelection}
173-
touchTargeting={PressabilityDebug.isEnabled()}
174-
setTouchTargeting={setTouchTargeting}
175-
/>
176-
</SafeAreaView>
184+
{!externalInspectingEnabled && (
185+
<SafeAreaView style={[styles.panelContainer, panelContainerStyle]}>
186+
<InspectorPanel
187+
devtoolsIsOpen={!!reactDevToolsAgent}
188+
inspecting={inspectingEnabled}
189+
setInspecting={handleSetInspecting}
190+
inspected={inspectedElement}
191+
hierarchy={elementsHierarchy}
192+
selection={selectionIndex}
193+
setSelection={setSelection}
194+
touchTargeting={PressabilityDebug.isEnabled()}
195+
setTouchTargeting={setTouchTargeting}
196+
/>
197+
</SafeAreaView>
198+
)}
177199
</View>
178200
);
179201
}

packages/react-native/src/private/devsupport/devmenu/elementinspector/InspectorOverlay.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ const ElementBox = require('./ElementBox').default;
2323
type Props = Readonly<{
2424
inspected?: ?InspectedElement,
2525
onTouchPoint: (locationX: number, locationY: number) => void,
26+
externalInspectionActive?: boolean,
2627
}>;
2728

28-
function InspectorOverlay({inspected, onTouchPoint}: Props): React.Node {
29+
function InspectorOverlay({
30+
inspected,
31+
onTouchPoint,
32+
externalInspectionActive,
33+
}: Props): React.Node {
2934
const findViewForTouchEvent = (e: GestureResponderEvent) => {
3035
const {locationX, locationY} = e.nativeEvent.touches[0];
3136

@@ -47,7 +52,10 @@ function InspectorOverlay({inspected, onTouchPoint}: Props): React.Node {
4752
onStartShouldSetResponder={handleStartShouldSetResponder}
4853
onResponderMove={findViewForTouchEvent}
4954
nativeID="inspectorOverlay" /* TODO: T68258846. */
50-
style={styles.inspector}>
55+
style={[
56+
styles.inspector,
57+
externalInspectionActive === true && styles.externalInspectionIndicator,
58+
]}>
5159
{content}
5260
</View>
5361
);
@@ -62,6 +70,10 @@ const styles = StyleSheet.create({
6270
right: 0,
6371
bottom: 0,
6472
},
73+
externalInspectionIndicator: {
74+
borderWidth: 4,
75+
borderColor: 'rgba(255, 0, 0, 0.8)',
76+
},
6577
});
6678

6779
export default InspectorOverlay;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and 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+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
'use strict';
12+
13+
import type {TouchedViewDataAtPoint} from '../../../../../Libraries/Renderer/shims/ReactNativeTypes';
14+
15+
import * as ReactNativeFeatureFlags from '../../../../../src/private/featureflags/ReactNativeFeatureFlags';
16+
import {useCallback, useEffect, useState} from 'react';
17+
18+
type ExternalInspectionAPI = {
19+
enable: () => void,
20+
disable: () => void,
21+
};
22+
23+
declare var __EXTERNAL_INSPECTION__: ?ExternalInspectionAPI;
24+
declare var __EXTERNAL_INSPECTION_SELECT__: ?(payload: string) => void;
25+
26+
type UseExternalInspectionResult = {
27+
externalInspectingEnabled: boolean,
28+
reportToExternalInspection: (viewData: TouchedViewDataAtPoint) => void,
29+
};
30+
31+
/**
32+
* Initializes the __EXTERNAL_INSPECTION__ API on the device.
33+
*/
34+
function ensureExternalInspectionAPI(
35+
onEnable: () => void,
36+
onDisable: () => void,
37+
): ExternalInspectionAPI {
38+
if (
39+
typeof __EXTERNAL_INSPECTION__ === 'undefined' ||
40+
__EXTERNAL_INSPECTION__ == null
41+
) {
42+
const api: ExternalInspectionAPI = {
43+
enable: onEnable,
44+
disable: onDisable,
45+
};
46+
// $FlowFixMe[prop-missing] Initializing global API for DevTools communication
47+
(global: $FlowFixMe).__EXTERNAL_INSPECTION__ = api;
48+
return api;
49+
}
50+
__EXTERNAL_INSPECTION__.enable = onEnable;
51+
__EXTERNAL_INSPECTION__.disable = onDisable;
52+
return __EXTERNAL_INSPECTION__;
53+
}
54+
55+
export default function useExternalInspection(): UseExternalInspectionResult {
56+
const [externalInspectingEnabled, setExternalInspectingEnabled] =
57+
useState<boolean>(false);
58+
59+
useEffect(() => {
60+
if (!ReactNativeFeatureFlags.externalElementInspectionEnabled()) {
61+
return;
62+
}
63+
64+
const handleEnable = () => {
65+
setExternalInspectingEnabled(true);
66+
};
67+
68+
const handleDisable = () => {
69+
setExternalInspectingEnabled(false);
70+
};
71+
72+
ensureExternalInspectionAPI(handleEnable, handleDisable);
73+
}, []);
74+
75+
const reportToExternalInspection = useCallback(
76+
(viewData: TouchedViewDataAtPoint) => {
77+
if (
78+
typeof __EXTERNAL_INSPECTION_SELECT__ === 'undefined' ||
79+
__EXTERNAL_INSPECTION_SELECT__ === null ||
80+
!externalInspectingEnabled
81+
) {
82+
return;
83+
}
84+
85+
__EXTERNAL_INSPECTION_SELECT__(
86+
JSON.stringify({
87+
frame: viewData.frame,
88+
hierarchy: viewData.hierarchy.map(item => ({
89+
name: item.name,
90+
})),
91+
touchedViewTag: viewData.touchedViewTag,
92+
}),
93+
);
94+
},
95+
[externalInspectingEnabled],
96+
);
97+
98+
return {
99+
externalInspectingEnabled,
100+
reportToExternalInspection,
101+
};
102+
}

packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<8b51e19257e928d50c473288cd1a01df>>
7+
* @generated SignedSource<<c3eeb5a098c4f8f0738ee377e81679cb>>
88
* @flow strict
99
* @noformat
1010
*/
@@ -33,6 +33,7 @@ export type ReactNativeFeatureFlagsJsOnly = $ReadOnly<{
3333
animatedShouldUseSingleOp: Getter<boolean>,
3434
deferFlatListFocusChangeRenderUpdate: Getter<boolean>,
3535
disableMaintainVisibleContentPosition: Getter<boolean>,
36+
externalElementInspectionEnabled: Getter<boolean>,
3637
fixVirtualizeListCollapseWindowSize: Getter<boolean>,
3738
isLayoutAnimationEnabled: Getter<boolean>,
3839
shouldUseAnimatedObjectForTransform: Getter<boolean>,
@@ -151,6 +152,11 @@ export const deferFlatListFocusChangeRenderUpdate: Getter<boolean> = createJavaS
151152
*/
152153
export const disableMaintainVisibleContentPosition: Getter<boolean> = createJavaScriptFlagGetter('disableMaintainVisibleContentPosition', false);
153154

155+
/**
156+
* Enable the external inspection API for DevTools to communicate with the Inspector overlay.
157+
*/
158+
export const externalElementInspectionEnabled: Getter<boolean> = createJavaScriptFlagGetter('externalElementInspectionEnabled', false);
159+
154160
/**
155161
* Fixing an edge case where the current window size is not properly calculated with fast scrolling. Window size collapsed to 1 element even if windowSize more than the current amount of elements
156162
*/

0 commit comments

Comments
 (0)