Skip to content

Commit 1c755ec

Browse files
committed
Split React DevTools into individual panels
1 parent c869824 commit 1c755ec

File tree

12 files changed

+304
-156
lines changed

12 files changed

+304
-156
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,8 @@ grd_files_release_sources = [
538538
"front_end/panels/protocol_monitor/components/components.js",
539539
"front_end/panels/protocol_monitor/protocol_monitor-meta.js",
540540
"front_end/panels/protocol_monitor/protocol_monitor.js",
541-
"front_end/panels/react_devtools/react_devtools-meta.js",
541+
"front_end/panels/react_devtools/react_devtools_components-meta.js",
542+
"front_end/panels/react_devtools/react_devtools_profiler-meta.js",
542543
"front_end/panels/react_devtools/react_devtools.js",
543544
"front_end/panels/recorder/components/components.js",
544545
"front_end/panels/recorder/controllers/controllers.js",
@@ -1464,8 +1465,10 @@ grd_files_debug_sources = [
14641465
"front_end/panels/protocol_monitor/components/Toolbar.js",
14651466
"front_end/panels/protocol_monitor/components/toolbar.css.js",
14661467
"front_end/panels/protocol_monitor/protocolMonitor.css.js",
1468+
"front_end/panels/react_devtools/ReactDevToolsComponentsView.js",
14671469
"front_end/panels/react_devtools/ReactDevToolsModel.js",
1468-
"front_end/panels/react_devtools/ReactDevToolsView.js",
1470+
"front_end/panels/react_devtools/ReactDevToolsProfilerView.js",
1471+
"front_end/panels/react_devtools/ReactDevToolsViewBase.js",
14691472
"front_end/panels/recorder/RecorderController.js",
14701473
"front_end/panels/recorder/RecorderEvents.js",
14711474
"front_end/panels/recorder/RecorderPanel.js",

front_end/entrypoints/rn_fusebox/BUILD.gn

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ devtools_entrypoint("entrypoint") {
2929
"../../panels/network:meta",
3030
"../../panels/performance_monitor:meta",
3131
"../../panels/recorder:meta",
32-
"../../panels/react_devtools:meta",
32+
"../../panels/react_devtools:components_meta",
33+
"../../panels/react_devtools:profiler_meta",
3334
"../../panels/rn_welcome:meta",
3435
"../../panels/security:meta",
3536
"../../panels/sensors:meta",

front_end/entrypoints/rn_fusebox/rn_fusebox.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import '../inspector_main/inspector_main-meta.js';
1111
import '../../panels/issues/issues-meta.js';
1212
import '../../panels/mobile_throttling/mobile_throttling-meta.js';
1313
import '../../panels/network/network-meta.js';
14-
import '../../panels/react_devtools/react_devtools-meta.js';
14+
import '../../panels/react_devtools/react_devtools_components-meta.js';
15+
import '../../panels/react_devtools/react_devtools_profiler-meta.js';
1516
import '../../panels/rn_welcome/rn_welcome-meta.js';
1617
import '../../panels/timeline/timeline-meta.js';
1718

front_end/panels/react_devtools/BUILD.gn

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import("../../../scripts/build/ninja/generate_css.gni")
99
import("../visibility.gni")
1010

1111
devtools_module("react_devtools") {
12-
sources = [ "ReactDevToolsView.ts", "ReactDevToolsModel.ts" ]
12+
sources = [
13+
"ReactDevToolsComponentsView.ts",
14+
"ReactDevToolsProfilerView.ts",
15+
"ReactDevToolsModel.ts",
16+
"ReactDevToolsViewBase.ts",
17+
]
1318

1419
deps = [
1520
"../../core/host:bundle",
@@ -38,8 +43,16 @@ devtools_entrypoint("bundle") {
3843
visibility += devtools_panels_visibility
3944
}
4045

41-
devtools_entrypoint("meta") {
42-
entrypoint = "react_devtools-meta.ts"
46+
devtools_entrypoint("components_meta") {
47+
entrypoint = "react_devtools_components-meta.ts"
48+
49+
deps = [ ":bundle" ]
50+
51+
visibility = [ "../../entrypoints/*" ]
52+
}
53+
54+
devtools_entrypoint("profiler_meta") {
55+
entrypoint = "react_devtools_profiler-meta.ts"
4356

4457
deps = [ ":bundle" ]
4558

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
import * as i18n from '../../core/i18n/i18n.js';
7+
8+
import { ReactDevToolsViewBase } from './ReactDevToolsViewBase.js';
9+
10+
const UIStrings = {
11+
/**
12+
*@description Title of the React DevTools view
13+
*/
14+
title: '⚛️ Components (React DevTools)',
15+
};
16+
const str_ = i18n.i18n.registerUIStrings('panels/react_devtools/ReactDevToolsComponentsView.ts', UIStrings);
17+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
18+
19+
export class ReactDevToolsComponentsViewImpl extends ReactDevToolsViewBase {
20+
constructor() {
21+
super('components', i18nString(UIStrings.title));
22+
}
23+
}

front_end/panels/react_devtools/ReactDevToolsModel.ts

Lines changed: 100 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as SDK from '../../core/sdk/sdk.js';
77
import * as ReactNativeModels from '../../models/react_native/react_native.js';
8+
import * as ReactDevTools from '../../third_party/react-devtools/react-devtools.js';
89

910
import type * as ReactDevToolsTypes from '../../third_party/react-devtools/react-devtools.js';
1011
import type * as Common from '../../core/common/common.js';
@@ -13,14 +14,12 @@ export const enum Events {
1314
InitializationCompleted = 'InitializationCompleted',
1415
InitializationFailed = 'InitializationFailed',
1516
Destroyed = 'Destroyed',
16-
MessageReceived = 'MessageReceived',
1717
}
1818

1919
export type EventTypes = {
2020
[Events.InitializationCompleted]: void,
2121
[Events.InitializationFailed]: string,
2222
[Events.Destroyed]: void,
23-
[Events.MessageReceived]: ReactDevToolsTypes.Message,
2423
};
2524

2625
type ReactDevToolsBindingsBackendExecutionContextUnavailableEvent = Common.EventTarget.EventTargetEvent<
@@ -31,91 +30,149 @@ type ReactDevToolsBindingsBackendExecutionContextUnavailableEvent = Common.Event
3130

3231
export class ReactDevToolsModel extends SDK.SDKModel.SDKModel<EventTypes> {
3332
private static readonly FUSEBOX_BINDING_NAMESPACE = 'react-devtools';
34-
private readonly rdtBindingsModel: ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel | null;
33+
34+
readonly #wall: ReactDevToolsTypes.Wall;
35+
readonly #bindingsModel: ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel;
36+
readonly #listeners: Set<ReactDevToolsTypes.WallListener> = new Set();
37+
#initializeCalled: boolean = false;
38+
#initialized: boolean = false;
39+
#bridge: ReactDevToolsTypes.Bridge | null;
40+
#store: ReactDevToolsTypes.Store | null;
3541

3642
constructor(target: SDK.Target.Target) {
3743
super(target);
3844

39-
const rdtBindingsModel = target.model(ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel);
40-
if (!rdtBindingsModel) {
45+
this.#wall = {
46+
listen: (listener): Function => {
47+
this.#listeners.add(listener);
48+
49+
return (): void => {
50+
this.#listeners.delete(listener);
51+
};
52+
},
53+
send: (event, payload): void => void this.#sendMessage({event, payload}),
54+
};
55+
this.#bridge = ReactDevTools.createBridge(this.#wall);
56+
this.#store = ReactDevTools.createStore(this.#bridge);
57+
58+
const bindingsModel = target.model(ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel);
59+
if (bindingsModel == null) {
4160
throw new Error('Failed to construct ReactDevToolsModel: ReactDevToolsBindingsModel was null');
4261
}
4362

44-
this.rdtBindingsModel = rdtBindingsModel;
45-
46-
rdtBindingsModel.addEventListener(ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextCreated, this.onBackendExecutionContextCreated, this);
47-
rdtBindingsModel.addEventListener(ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextUnavailable, this.onBackendExecutionContextUnavailable, this);
48-
rdtBindingsModel.addEventListener(ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextDestroyed, this.onBackendExecutionContextDestroyed, this);
63+
this.#bindingsModel = bindingsModel;
4964

50-
void this.initialize(rdtBindingsModel);
51-
}
65+
bindingsModel.addEventListener(
66+
ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextCreated,
67+
this.#handleBackendExecutionContextCreated,
68+
this,
69+
);
70+
bindingsModel.addEventListener(
71+
ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextUnavailable,
72+
this.#handleBackendExecutionContextUnavailable,
73+
this,
74+
);
75+
bindingsModel.addEventListener(
76+
ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextDestroyed,
77+
this.#handleBackendExecutionContextDestroyed,
78+
this,
79+
);
5280

53-
private async initialize(rdtBindingsModel: ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel): Promise<void> {
54-
return rdtBindingsModel.enable()
55-
.then(() => this.onBindingsModelInitializationCompleted())
56-
.catch((error: Error) => this.onBindingsModelInitializationFailed(error));
81+
// Notify backend if Chrome DevTools was closed, marking frontend as disconnected
82+
window.addEventListener('beforeunload', () => this.#bridge?.shutdown());
5783
}
5884

59-
private onBindingsModelInitializationCompleted(): void {
60-
const rdtBindingsModel = this.rdtBindingsModel;
61-
if (!rdtBindingsModel) {
62-
throw new Error('Failed to initialize ReactDevToolsModel: ReactDevToolsBindingsModel was null');
85+
async ensureInitialized(): Promise<void> {
86+
if (this.#initializeCalled) {
87+
return;
6388
}
6489

65-
rdtBindingsModel.subscribeToDomainMessages(
66-
ReactDevToolsModel.FUSEBOX_BINDING_NAMESPACE,
67-
message => this.onMessage(message as ReactDevToolsTypes.Message),
68-
);
90+
this.#initializeCalled = true;
91+
92+
try {
93+
const bindingsModel = this.#bindingsModel;
94+
await bindingsModel.enable();
95+
96+
bindingsModel.subscribeToDomainMessages(
97+
ReactDevToolsModel.FUSEBOX_BINDING_NAMESPACE,
98+
message => this.#handleMessage(message as ReactDevToolsTypes.Message),
99+
);
100+
101+
await bindingsModel.initializeDomain(ReactDevToolsModel.FUSEBOX_BINDING_NAMESPACE);
69102

70-
void rdtBindingsModel.initializeDomain(ReactDevToolsModel.FUSEBOX_BINDING_NAMESPACE)
71-
.then(() => this.onDomainInitializationCompleted())
72-
.catch((error: Error) => this.onDomainInitializationFailed(error));
103+
this.#initialized = true;
104+
this.dispatchEventToListeners(Events.InitializationCompleted);
105+
} catch (e) {
106+
this.dispatchEventToListeners(Events.InitializationFailed, e.message);
107+
}
73108
}
74109

75-
private onBindingsModelInitializationFailed(error: Error): void {
76-
this.dispatchEventToListeners(Events.InitializationFailed, error.message);
110+
isInitialized(): boolean {
111+
return this.#initialized;
77112
}
78113

79-
private onDomainInitializationCompleted(): void {
80-
this.dispatchEventToListeners(Events.InitializationCompleted);
114+
getBridgeOrThrow(): ReactDevToolsTypes.Bridge {
115+
if (this.#bridge == null) {
116+
throw new Error('Failed to get bridge from ReactDevToolsModel: bridge was null');
117+
}
118+
119+
return this.#bridge;
81120
}
82121

83-
private onDomainInitializationFailed(error: Error): void {
84-
this.dispatchEventToListeners(Events.InitializationFailed, error.message);
122+
getStoreOrThrow(): ReactDevToolsTypes.Store {
123+
if (this.#store == null) {
124+
throw new Error('Failed to get store from ReactDevToolsModel: store was null');
125+
}
126+
127+
return this.#store;
85128
}
86129

87-
private onMessage(message: ReactDevToolsTypes.Message): void {
88-
this.dispatchEventToListeners(Events.MessageReceived, message);
130+
#handleMessage(message: ReactDevToolsTypes.Message): void {
131+
if (!message) {
132+
return;
133+
}
134+
135+
for (const listener of this.#listeners) {
136+
listener(message);
137+
}
89138
}
90139

91-
async sendMessage(message: ReactDevToolsTypes.Message): Promise<void> {
92-
const rdtBindingsModel = this.rdtBindingsModel;
140+
async #sendMessage(message: ReactDevToolsTypes.Message): Promise<void> {
141+
const rdtBindingsModel = this.#bindingsModel;
93142
if (!rdtBindingsModel) {
94143
throw new Error('Failed to send message from ReactDevToolsModel: ReactDevToolsBindingsModel was null');
95144
}
96145

97146
return rdtBindingsModel.sendMessage(ReactDevToolsModel.FUSEBOX_BINDING_NAMESPACE, message);
98147
}
99148

100-
private onBackendExecutionContextCreated(): void {
101-
const rdtBindingsModel = this.rdtBindingsModel;
149+
#handleBackendExecutionContextCreated(): void {
150+
const rdtBindingsModel = this.#bindingsModel;
102151
if (!rdtBindingsModel) {
103152
throw new Error('ReactDevToolsModel failed to handle BackendExecutionContextCreated event: ReactDevToolsBindingsModel was null');
104153
}
105154

106-
// This could happen if the app was reloaded while ReactDevToolsBindingsModel was initialing
155+
this.#bridge = ReactDevTools.createBridge(this.#wall);
156+
this.#store = ReactDevTools.createStore(this.#bridge);
157+
158+
// This could happen if the app was reloaded while ReactDevToolsBindingsModel was initializing
107159
if (!rdtBindingsModel.isEnabled()) {
108-
void this.initialize(rdtBindingsModel);
160+
void this.ensureInitialized();
109161
} else {
110162
this.dispatchEventToListeners(Events.InitializationCompleted);
111163
}
112164
}
113165

114-
private onBackendExecutionContextUnavailable({data: errorMessage}: ReactDevToolsBindingsBackendExecutionContextUnavailableEvent): void {
166+
#handleBackendExecutionContextUnavailable({data: errorMessage}: ReactDevToolsBindingsBackendExecutionContextUnavailableEvent): void {
115167
this.dispatchEventToListeners(Events.InitializationFailed, errorMessage);
116168
}
117169

118-
private onBackendExecutionContextDestroyed(): void {
170+
#handleBackendExecutionContextDestroyed(): void {
171+
this.#bridge?.shutdown();
172+
this.#bridge = null;
173+
this.#store = null;
174+
this.#listeners.clear();
175+
119176
this.dispatchEventToListeners(Events.Destroyed);
120177
}
121178
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
import * as i18n from '../../core/i18n/i18n.js';
7+
8+
import { ReactDevToolsViewBase } from './ReactDevToolsViewBase.js';
9+
10+
const UIStrings = {
11+
/**
12+
*@description Title of the React DevTools view
13+
*/
14+
title: '⚛️ Profiler (React DevTools)',
15+
};
16+
const str_ = i18n.i18n.registerUIStrings('panels/react_devtools/ReactDevToolsProfilerView.ts', UIStrings);
17+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
18+
19+
export class ReactDevToolsProfilerViewImpl extends ReactDevToolsViewBase {
20+
constructor() {
21+
super('profiler', i18nString(UIStrings.title));
22+
}
23+
}

0 commit comments

Comments
 (0)