Skip to content

Commit

Permalink
Split React DevTools into individual panels
Browse files Browse the repository at this point in the history
  • Loading branch information
huntie committed Aug 29, 2024
1 parent c869824 commit d13cee8
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 148 deletions.
7 changes: 5 additions & 2 deletions config/gni/devtools_grd_files.gni
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,8 @@ grd_files_release_sources = [
"front_end/panels/protocol_monitor/components/components.js",
"front_end/panels/protocol_monitor/protocol_monitor-meta.js",
"front_end/panels/protocol_monitor/protocol_monitor.js",
"front_end/panels/react_devtools/react_devtools-meta.js",
"front_end/panels/react_devtools/react_devtools_components-meta.js",
"front_end/panels/react_devtools/react_devtools_profiler-meta.js",
"front_end/panels/react_devtools/react_devtools.js",
"front_end/panels/recorder/components/components.js",
"front_end/panels/recorder/controllers/controllers.js",
Expand Down Expand Up @@ -1464,8 +1465,10 @@ grd_files_debug_sources = [
"front_end/panels/protocol_monitor/components/Toolbar.js",
"front_end/panels/protocol_monitor/components/toolbar.css.js",
"front_end/panels/protocol_monitor/protocolMonitor.css.js",
"front_end/panels/react_devtools/ReactDevToolsComponentsView.js",
"front_end/panels/react_devtools/ReactDevToolsModel.js",
"front_end/panels/react_devtools/ReactDevToolsView.js",
"front_end/panels/react_devtools/ReactDevToolsProfilerView.js",
"front_end/panels/react_devtools/ReactDevToolsViewBase.js",
"front_end/panels/recorder/RecorderController.js",
"front_end/panels/recorder/RecorderEvents.js",
"front_end/panels/recorder/RecorderPanel.js",
Expand Down
3 changes: 2 additions & 1 deletion front_end/entrypoints/rn_fusebox/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ devtools_entrypoint("entrypoint") {
"../../panels/network:meta",
"../../panels/performance_monitor:meta",
"../../panels/recorder:meta",
"../../panels/react_devtools:meta",
"../../panels/react_devtools:components_meta",
"../../panels/react_devtools:profiler_meta",
"../../panels/rn_welcome:meta",
"../../panels/security:meta",
"../../panels/sensors:meta",
Expand Down
3 changes: 2 additions & 1 deletion front_end/entrypoints/rn_fusebox/rn_fusebox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import '../inspector_main/inspector_main-meta.js';
import '../../panels/issues/issues-meta.js';
import '../../panels/mobile_throttling/mobile_throttling-meta.js';
import '../../panels/network/network-meta.js';
import '../../panels/react_devtools/react_devtools-meta.js';
import '../../panels/react_devtools/react_devtools_components-meta.js';
import '../../panels/react_devtools/react_devtools_profiler-meta.js';
import '../../panels/rn_welcome/rn_welcome-meta.js';
import '../../panels/timeline/timeline-meta.js';

Expand Down
19 changes: 16 additions & 3 deletions front_end/panels/react_devtools/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import("../../../scripts/build/ninja/generate_css.gni")
import("../visibility.gni")

devtools_module("react_devtools") {
sources = [ "ReactDevToolsView.ts", "ReactDevToolsModel.ts" ]
sources = [
"ReactDevToolsComponentsView.ts",
"ReactDevToolsProfilerView.ts",
"ReactDevToolsModel.ts",
"ReactDevToolsViewBase.ts",
]

deps = [
"../../core/host:bundle",
Expand Down Expand Up @@ -38,8 +43,16 @@ devtools_entrypoint("bundle") {
visibility += devtools_panels_visibility
}

devtools_entrypoint("meta") {
entrypoint = "react_devtools-meta.ts"
devtools_entrypoint("components_meta") {
entrypoint = "react_devtools_components-meta.ts"

deps = [ ":bundle" ]

visibility = [ "../../entrypoints/*" ]
}

devtools_entrypoint("profiler_meta") {
entrypoint = "react_devtools_profiler-meta.ts"

deps = [ ":bundle" ]

Expand Down
23 changes: 23 additions & 0 deletions front_end/panels/react_devtools/ReactDevToolsComponentsView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as i18n from '../../core/i18n/i18n.js';

import { ReactDevToolsViewBase } from './ReactDevToolsViewBase.js';

const UIStrings = {
/**
*@description Title of the React DevTools view
*/
title: '⚛️ Components (React DevTools)',
};
const str_ = i18n.i18n.registerUIStrings('panels/react_devtools/ReactDevToolsComponentsView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class ReactDevToolsComponentsViewImpl extends ReactDevToolsViewBase {
constructor() {
super('components', i18nString(UIStrings.title));
}
}
141 changes: 106 additions & 35 deletions front_end/panels/react_devtools/ReactDevToolsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

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

import type * as ReactDevToolsTypes from '../../third_party/react-devtools/react-devtools.js';
import type * as Common from '../../core/common/common.js';
Expand All @@ -13,14 +14,12 @@ export const enum Events {
InitializationCompleted = 'InitializationCompleted',
InitializationFailed = 'InitializationFailed',
Destroyed = 'Destroyed',
MessageReceived = 'MessageReceived',
}

export type EventTypes = {
[Events.InitializationCompleted]: void,
[Events.InitializationFailed]: string,
[Events.Destroyed]: void,
[Events.MessageReceived]: ReactDevToolsTypes.Message,
};

type ReactDevToolsBindingsBackendExecutionContextUnavailableEvent = Common.EventTarget.EventTargetEvent<
Expand All @@ -31,91 +30,163 @@ type ReactDevToolsBindingsBackendExecutionContextUnavailableEvent = Common.Event

export class ReactDevToolsModel extends SDK.SDKModel.SDKModel<EventTypes> {
private static readonly FUSEBOX_BINDING_NAMESPACE = 'react-devtools';
private readonly rdtBindingsModel: ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel | null;

readonly #wall: ReactDevToolsTypes.Wall;
readonly #bindingsModel: ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel;
readonly #listeners: Set<ReactDevToolsTypes.WallListener> = new Set();
#initialized: boolean = false;
#bridge: ReactDevToolsTypes.Bridge | null;
#store: ReactDevToolsTypes.Store | null;

constructor(target: SDK.Target.Target) {
super(target);

const rdtBindingsModel = target.model(ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel);
if (!rdtBindingsModel) {
this.#wall = {
listen: (listener): Function => {
this.#listeners.add(listener);

return (): void => {
this.#listeners.delete(listener);
};
},
send: (event, payload): void => void this.#sendMessage({event, payload}),
};
this.#bridge = ReactDevTools.createBridge(this.#wall);
this.#store = ReactDevTools.createStore(this.#bridge);

const bindingsModel = target.model(ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel);
if (bindingsModel == null) {
throw new Error('Failed to construct ReactDevToolsModel: ReactDevToolsBindingsModel was null');
}

this.rdtBindingsModel = rdtBindingsModel;
this.#bindingsModel = bindingsModel;

bindingsModel.addEventListener(
ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextCreated,
this.#handleBackendExecutionContextCreated,
this,
);
bindingsModel.addEventListener(
ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextUnavailable,
this.#handleBackendExecutionContextUnavailable,
this,
);
bindingsModel.addEventListener(
ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextDestroyed,
this.#handleBackendExecutionContextDestroyed,
this,
);

// Notify backend if Chrome DevTools was closed, marking frontend as disconnected
window.addEventListener('beforeunload', () => this.#bridge?.shutdown());
}

rdtBindingsModel.addEventListener(ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextCreated, this.onBackendExecutionContextCreated, this);
rdtBindingsModel.addEventListener(ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextUnavailable, this.onBackendExecutionContextUnavailable, this);
rdtBindingsModel.addEventListener(ReactNativeModels.ReactDevToolsBindingsModel.Events.BackendExecutionContextDestroyed, this.onBackendExecutionContextDestroyed, this);
async ensureInitialized(): Promise<void> {
if (this.#initialized) {
return;
}

void this.initialize(rdtBindingsModel);
await this.#bindingsModel.enable()
.then(() => this.#handleBindingsModelInitializationCompleted())
.catch((error: Error) => this.#handleBindingsModelInitializationFailed(error));
}

private async initialize(rdtBindingsModel: ReactNativeModels.ReactDevToolsBindingsModel.ReactDevToolsBindingsModel): Promise<void> {
return rdtBindingsModel.enable()
.then(() => this.onBindingsModelInitializationCompleted())
.catch((error: Error) => this.onBindingsModelInitializationFailed(error));
isInitialized(): boolean {
return this.#initialized;
}

private onBindingsModelInitializationCompleted(): void {
const rdtBindingsModel = this.rdtBindingsModel;
if (!rdtBindingsModel) {
getBridge(): ReactDevToolsTypes.Bridge {
if (this.#bridge == null) {
throw new Error('Failed to get bridge from ReactDevToolsModel: bridge was null');
}

return this.#bridge;
}

getStore(): ReactDevToolsTypes.Store {
if (this.#store == null) {
throw new Error('Failed to get store from ReactDevToolsModel: store was null');
}

return this.#store;
}

#handleBindingsModelInitializationCompleted(): void {
const bindingsModel = this.#bindingsModel;
if (!bindingsModel) {
throw new Error('Failed to initialize ReactDevToolsModel: ReactDevToolsBindingsModel was null');
}

rdtBindingsModel.subscribeToDomainMessages(
bindingsModel.subscribeToDomainMessages(
ReactDevToolsModel.FUSEBOX_BINDING_NAMESPACE,
message => this.onMessage(message as ReactDevToolsTypes.Message),
message => this.#handleMessage(message as ReactDevToolsTypes.Message),
);

void rdtBindingsModel.initializeDomain(ReactDevToolsModel.FUSEBOX_BINDING_NAMESPACE)
.then(() => this.onDomainInitializationCompleted())
.catch((error: Error) => this.onDomainInitializationFailed(error));
void bindingsModel.initializeDomain(ReactDevToolsModel.FUSEBOX_BINDING_NAMESPACE)
.then(() => this.#handleDomainInitializationCompleted())
.catch((error: Error) => this.#handleDomainInitializationFailed(error));

this.#initialized = true;
}

private onBindingsModelInitializationFailed(error: Error): void {
#handleBindingsModelInitializationFailed(error: Error): void {
this.dispatchEventToListeners(Events.InitializationFailed, error.message);
}

private onDomainInitializationCompleted(): void {
#handleDomainInitializationCompleted(): void {
this.dispatchEventToListeners(Events.InitializationCompleted);
}

private onDomainInitializationFailed(error: Error): void {
#handleDomainInitializationFailed(error: Error): void {
this.dispatchEventToListeners(Events.InitializationFailed, error.message);
}

private onMessage(message: ReactDevToolsTypes.Message): void {
this.dispatchEventToListeners(Events.MessageReceived, message);
#handleMessage(message: ReactDevToolsTypes.Message): void {
if (!message) {
return;
}

for (const listener of this.#listeners) {
listener(message);
}
}

async sendMessage(message: ReactDevToolsTypes.Message): Promise<void> {
const rdtBindingsModel = this.rdtBindingsModel;
async #sendMessage(message: ReactDevToolsTypes.Message): Promise<void> {
const rdtBindingsModel = this.#bindingsModel;
if (!rdtBindingsModel) {
throw new Error('Failed to send message from ReactDevToolsModel: ReactDevToolsBindingsModel was null');
}

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

private onBackendExecutionContextCreated(): void {
const rdtBindingsModel = this.rdtBindingsModel;
#handleBackendExecutionContextCreated(): void {
const rdtBindingsModel = this.#bindingsModel;
if (!rdtBindingsModel) {
throw new Error('ReactDevToolsModel failed to handle BackendExecutionContextCreated event: ReactDevToolsBindingsModel was null');
}

// This could happen if the app was reloaded while ReactDevToolsBindingsModel was initialing
this.#bridge = ReactDevTools.createBridge(this.#wall);
this.#store = ReactDevTools.createStore(this.#bridge);

// This could happen if the app was reloaded while ReactDevToolsBindingsModel was initializing
if (!rdtBindingsModel.isEnabled()) {
void this.initialize(rdtBindingsModel);
void this.ensureInitialized();
} else {
this.dispatchEventToListeners(Events.InitializationCompleted);
}
}

private onBackendExecutionContextUnavailable({data: errorMessage}: ReactDevToolsBindingsBackendExecutionContextUnavailableEvent): void {
#handleBackendExecutionContextUnavailable({data: errorMessage}: ReactDevToolsBindingsBackendExecutionContextUnavailableEvent): void {
this.dispatchEventToListeners(Events.InitializationFailed, errorMessage);
}

private onBackendExecutionContextDestroyed(): void {
#handleBackendExecutionContextDestroyed(): void {
this.#bridge?.shutdown();
this.#bridge = null;
this.#store = null;
this.#listeners.clear();

this.dispatchEventToListeners(Events.Destroyed);
}
}
Expand Down
23 changes: 23 additions & 0 deletions front_end/panels/react_devtools/ReactDevToolsProfilerView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as i18n from '../../core/i18n/i18n.js';

import { ReactDevToolsViewBase } from './ReactDevToolsViewBase.js';

const UIStrings = {
/**
*@description Title of the React DevTools view
*/
title: '⚛️ Profiler (React DevTools)',
};
const str_ = i18n.i18n.registerUIStrings('panels/react_devtools/ReactDevToolsProfilerView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export class ReactDevToolsProfilerViewImpl extends ReactDevToolsViewBase {
constructor() {
super('profiler', i18nString(UIStrings.title));
}
}
Loading

0 comments on commit d13cee8

Please sign in to comment.