Skip to content

Commit 59c63cb

Browse files
authored
Add conditional disabling of features incompatible with multiple hosts (#218)
1 parent d37d426 commit 59c63cb

12 files changed

Lines changed: 296 additions & 106 deletions

File tree

‎config/gni/devtools_grd_files.gni‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ grd_files_debug_sources = [
995995
"front_end/entrypoints/node_app/NodeMain.js",
996996
"front_end/entrypoints/node_app/nodeConnectionsPanel.css.js",
997997
"front_end/entrypoints/rn_fusebox/FuseboxAppMetadataObserver.js",
998-
"front_end/entrypoints/rn_fusebox/FuseboxExperimentsObserver.js",
998+
"front_end/entrypoints/rn_fusebox/FuseboxFeatureObserver.js",
999999
"front_end/entrypoints/rn_fusebox/FuseboxReconnectDeviceButton.js",
10001000
"front_end/entrypoints/rn_fusebox/FuseboxWindowTitleManager.js",
10011001
"front_end/entrypoints/shell/browser_compatibility_guard.js",

‎front_end/core/sdk/ReactNativeApplicationModel.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export class ReactNativeApplicationModel extends SDKModel<EventTypes> implements
5252
this.dispatchEventToListeners(Events.METADATA_UPDATED,metadata);
5353
}
5454

55+
systemStateChanged(params: Protocol.ReactNativeApplication.SystemStateChangedEvent): void{
56+
this.dispatchEventToListeners(Events.SYSTEM_STATE_CHANGED,params);
57+
}
58+
5559
traceRequested(): void{
5660
Host.rnPerfMetrics.traceRequested();
5761
this.dispatchEventToListeners(Events.TRACE_REQUESTED);
@@ -60,10 +64,12 @@ export class ReactNativeApplicationModel extends SDKModel<EventTypes> implements
6064

6165
exportconstenumEvents{
6266
METADATA_UPDATED='MetadataUpdated',
67+
SYSTEM_STATE_CHANGED='SystemStateChanged',
6368
TRACE_REQUESTED='TraceRequested',
6469
}
6570

6671
exportinterfaceEventTypes{
6772
[Events.METADATA_UPDATED]: Protocol.ReactNativeApplication.MetadataUpdatedEvent;
73+
[Events.SYSTEM_STATE_CHANGED]: Protocol.ReactNativeApplication.SystemStateChangedEvent;
6874
[Events.TRACE_REQUESTED]: void;
6975
}

‎front_end/entrypoints/rn_fusebox/BUILD.gn‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ devtools_module("rn_fusebox") {
1111
sources= [
1212
"FuseboxAppMetadataObserver.ts",
1313
"FuseboxReconnectDeviceButton.ts",
14-
"FuseboxExperimentsObserver.ts",
14+
"FuseboxFeatureObserver.ts",
1515
"FuseboxWindowTitleManager.ts",
1616
]
1717

‎front_end/entrypoints/rn_fusebox/FuseboxExperimentsObserver.ts‎

Lines changed: 0 additions & 102 deletions
This file was deleted.
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
// Copyright 2024 The Chromium Authors. All rights reserved.
3+
// Use of this source code is governed by a BSD-style license that can be
4+
// found in the LICENSE file.
5+
6+
importtype*asCommonfrom'../../core/common/common.js';
7+
import*asi18nfrom'../../core/i18n/i18n.js';
8+
import*asRootfrom'../../core/root/root.js';
9+
import*asSDKfrom'../../core/sdk/sdk.js';
10+
importtype*asProtocolfrom'../../generated/protocol.js';
11+
import*asUIfrom'../../ui/legacy/legacy.js';
12+
import*asLitfrom'../../ui/lit/lit.js';
13+
import*asVisualLoggingfrom'../../ui/visual_logging/visual_logging.js';
14+
15+
import{FuseboxWindowTitleManager}from'./FuseboxWindowTitleManager.js';
16+
17+
const{html, render}=Lit;
18+
19+
constUIStrings={
20+
/**
21+
* @description Message for the "settings changed" banner shown when a reload is required for the Network panel.
22+
*/
23+
reloadRequiredForNetworkPanelMessage: 'The Network panel is now available for dogfooding. Please reload to access it.',
24+
/**
25+
* @description Title shown when Network inspection is disabled due to multiple React Native hosts.
26+
*/
27+
networkInspectionUnavailable: 'Network inspection is unavailable',
28+
/**
29+
* @description Title shown when Performance profiling is disabled due to multiple React Native hosts.
30+
*/
31+
performanceProfilingUnavailable: 'Performance profiling is unavailable',
32+
/**
33+
* @description Title shown when a feature is unavailable due to multiple React Native hosts.
34+
*/
35+
multiHostFeatureUnavailableTitle: 'Feature is unavailable',
36+
/**
37+
* @description Detail message shown when a feature is disabled due to multiple React Native hosts.
38+
*/
39+
multiHostFeatureDisabledDetail: 'This feature is disabled as the app or framework has registered multiple React Native hosts, which is not currently supported.',
40+
}asconst;
41+
42+
conststr_=i18n.i18n.registerUIStrings('entrypoints/rn_fusebox/FuseboxFeatureObserver.ts',UIStrings);
43+
consti18nString=i18n.i18n.getLocalizedString.bind(undefined,str_);
44+
45+
/**
46+
* The set of features that are not guaranteed to behave safely with multiple
47+
* React Native hosts.
48+
*/
49+
constUNSAFE_MULTI_HOST_FEATURES=newSet([
50+
'network',
51+
'timeline',
52+
]);
53+
54+
/**
55+
* [RN] Model observer which configures available DevTools features and
56+
* experiments based on the target's capabilities.
57+
*/
58+
exportclassFuseboxFeatureObserverimplements
59+
SDK.TargetManager.SDKModelObserver<SDK.ReactNativeApplicationModel.ReactNativeApplicationModel>{
60+
#singleHostFeaturesDisabled =false;
61+
62+
constructor(targetManager: SDK.TargetManager.TargetManager){
63+
targetManager.observeModels(SDK.ReactNativeApplicationModel.ReactNativeApplicationModel,this);
64+
}
65+
66+
modelAdded(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void{
67+
model.ensureEnabled();
68+
model.addEventListener(SDK.ReactNativeApplicationModel.Events.METADATA_UPDATED,this.#handleMetadataUpdated,this);
69+
model.addEventListener(SDK.ReactNativeApplicationModel.Events.SYSTEM_STATE_CHANGED,this.#handleSystemStateChanged,this);
70+
}
71+
72+
modelRemoved(model: SDK.ReactNativeApplicationModel.ReactNativeApplicationModel): void{
73+
model.removeEventListener(
74+
SDK.ReactNativeApplicationModel.Events.METADATA_UPDATED,this.#handleMetadataUpdated,this);
75+
model.removeEventListener(
76+
SDK.ReactNativeApplicationModel.Events.SYSTEM_STATE_CHANGED,this.#handleSystemStateChanged,this);
77+
}
78+
79+
#handleMetadataUpdated(
80+
event: Common.EventTarget.EventTargetEvent<Protocol.ReactNativeApplication.MetadataUpdatedEvent>): void{
81+
// eslint-disable-next-line @typescript-eslint/naming-convention
82+
const{unstable_isProfilingBuild, unstable_networkInspectionEnabled}=event.data;
83+
84+
if(unstable_isProfilingBuild){
85+
FuseboxWindowTitleManager.instance().setSuffix('[PROFILING]');
86+
this.#hideUnsupportedFeaturesForProfilingBuilds();
87+
}
88+
89+
if(unstable_networkInspectionEnabled){
90+
this.#ensureNetworkPanelEnabled();
91+
}
92+
}
93+
94+
#handleSystemStateChanged(
95+
event: Common.EventTarget.EventTargetEvent<Protocol.ReactNativeApplication.SystemStateChangedEvent>): void{
96+
const{isSingleHost}=event.data;
97+
if(!isSingleHost){
98+
this.#disableSingleHostOnlyFeatures();
99+
}
100+
}
101+
102+
#hideUnsupportedFeaturesForProfilingBuilds(): void{
103+
UI.InspectorView.InspectorView.instance().closeDrawer();
104+
105+
constviewManager=UI.ViewManager.ViewManager.instance();
106+
constpanelLocationPromise=viewManager.resolveLocation(UI.ViewManager.ViewLocationValues.PANEL);
107+
constdrawerLocationPromise=viewManager.resolveLocation(UI.ViewManager.ViewLocationValues.DRAWER_VIEW);
108+
voidPromise.all([panelLocationPromise,drawerLocationPromise])
109+
.then(([panelLocation,drawerLocation])=>{
110+
UI.ViewManager.getRegisteredViewExtensions().forEach(view=>{
111+
if(view.location()===UI.ViewManager.ViewLocationValues.DRAWER_VIEW){
112+
drawerLocation?.removeView(view);
113+
}else{
114+
switch(view.viewId()){
115+
case'console':
116+
case'heap-profiler':
117+
case'live-heap-profile':
118+
case'sources':
119+
case'network':
120+
case'react-devtools-components':
121+
case'react-devtools-profiler':
122+
panelLocation?.removeView(view);
123+
break;
124+
}
125+
}
126+
});
127+
});
128+
}
129+
130+
#ensureNetworkPanelEnabled(): void{
131+
if(Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.ENABLE_NETWORK_PANEL)){
132+
return;
133+
}
134+
135+
Root.Runtime.experiments.setEnabled(
136+
Root.Runtime.ExperimentName.ENABLE_NETWORK_PANEL,
137+
true,
138+
);
139+
140+
UI.InspectorView?.InspectorView?.instance()?.displayReloadRequiredWarning(
141+
i18nString(UIStrings.reloadRequiredForNetworkPanelMessage),
142+
);
143+
}
144+
145+
#disableSingleHostOnlyFeatures(): void{
146+
if(this.#singleHostFeaturesDisabled){
147+
return;
148+
}
149+
150+
// Disable relevant CDP domains
151+
consttargetManager=SDK.TargetManager.TargetManager.instance();
152+
for(consttargetoftargetManager.targets()){
153+
voidtarget.networkAgent().invoke_disable();
154+
}
155+
156+
// Stop network recording if active
157+
voidthis.#disableNetworkRecording();
158+
159+
// Show in-panel overlay when disabled panels are selected
160+
constinspectorView=UI.InspectorView.InspectorView.instance();
161+
constoverlaidPanels=newSet<string>();
162+
163+
constshowPanelOverlay=(panel: UI.Panel.Panel,panelId: string): void=>{
164+
consttitleText=
165+
panelId==='network'
166+
? i18nString(UIStrings.networkInspectionUnavailable)
167+
: panelId==='timeline'
168+
? i18nString(UIStrings.performanceProfilingUnavailable)
169+
: i18nString(UIStrings.multiHostFeatureUnavailableTitle);
170+
171+
// Dim the existing panel content and disable interaction
172+
for(constchildofpanel.element.children){
173+
constelement=childasHTMLElement;
174+
element.style.opacity='0.5';
175+
element.style.pointerEvents='none';
176+
element.setAttribute('inert','');
177+
element.setAttribute('aria-hidden','true');
178+
}
179+
180+
constalertBar=document.createElement('div');
181+
render(html`
182+
<style>
183+
.alert-bar {
184+
background:var(--sys-color-tonal-container);
185+
color:var(--sys-color-on-tonal-container);
186+
padding:var(--sys-size-6) var(--sys-size-8);
187+
border-bottom:1px solid var(--sys-color-tonal-outline);
188+
}
189+
.alert-title {
190+
font:var(--sys-typescale-body2-medium);
191+
margin-bottom:var(--sys-size-3);
192+
}
193+
.alert-detail {
194+
font:var(--sys-typescale-body4-regular);
195+
}
196+
</style>
197+
<divclass="alert-bar">
198+
<divclass="alert-title">${titleText}</div>
199+
<divclass="alert-detail">
200+
${i18nString(UIStrings.multiHostFeatureDisabledDetail)}
201+
See <x-linkhref="https://github.com/react-native-community/discussions-and-proposals/discussions/954" class="devtools-link" jslog=${VisualLogging.link().track({click: true,keydown:'Enter|Space'}).context('multi-host-learn-more')}>discussions/954</x-link>.
202+
</div>
203+
</div>
204+
`,alertBar,{host: this});
205+
206+
panel.element.insertBefore(alertBar,panel.element.firstChild);
207+
};
208+
209+
inspectorView.tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected,event=>{
210+
consttabId=event.data.tabId;
211+
if(UNSAFE_MULTI_HOST_FEATURES.has(tabId)&&!overlaidPanels.has(tabId)){
212+
overlaidPanels.add(tabId);
213+
voidinspectorView.panel(tabId).then(panel=>{
214+
if(panel){
215+
showPanelOverlay(panel,tabId);
216+
}
217+
});
218+
}
219+
});
220+
221+
// Show overlay if a disabled panel is currently selected
222+
constcurrentTabId=inspectorView.tabbedPane.selectedTabId;
223+
if(currentTabId&&UNSAFE_MULTI_HOST_FEATURES.has(currentTabId)){
224+
overlaidPanels.add(currentTabId);
225+
voidinspectorView.panel(currentTabId).then(panel=>{
226+
if(panel){
227+
showPanelOverlay(panel,currentTabId);
228+
}
229+
});
230+
}
231+
232+
this.#singleHostFeaturesDisabled =true;
233+
}
234+
235+
async #disableNetworkRecording(): Promise<void>{
236+
constinspectorView=UI.InspectorView.InspectorView.instance();
237+
try{
238+
constnetworkPanel=awaitinspectorView.panel('network');
239+
if(networkPanel&&'toggleRecord'innetworkPanel){
240+
(networkPanelasUI.Panel.Panel&{toggleRecord: (toggled: boolean)=>void}).toggleRecord(false);
241+
}
242+
}catch{
243+
}
244+
}
245+
}

‎front_end/entrypoints/rn_fusebox/rn_fusebox.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import * as UI from '../../ui/legacy/legacy.js';
2727
import*asMainfrom'../main/main.js';
2828

2929
import*asFuseboxAppMetadataObserverModulefrom'./FuseboxAppMetadataObserver.js';
30-
import*asFuseboxFeatureObserverModulefrom'./FuseboxExperimentsObserver.js';
30+
import*asFuseboxFeatureObserverModulefrom'./FuseboxFeatureObserver.js';
3131
import*asFuseboxReconnectDeviceButtonModulefrom'./FuseboxReconnectDeviceButton.js';
3232

3333
// To ensure accurate timing measurements, please make sure these perf metrics

0 commit comments

Comments
 (0)