diff --git a/front_end/models/react_native/BUILD.gn b/front_end/models/react_native/BUILD.gn new file mode 100644 index 00000000000..06084e402a8 --- /dev/null +++ b/front_end/models/react_native/BUILD.gn @@ -0,0 +1,33 @@ +# 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("../../../scripts/build/ninja/devtools_entrypoint.gni") +import("../../../scripts/build/ninja/devtools_module.gni") +import("../visibility.gni") + +devtools_module("react-native") { + sources = [ + "ReactDevToolsBindingsModel.ts", + ] + + deps = [ + "../../core/common:bundle", + "../../core/sdk:bundle", + ] +} + +devtools_entrypoint("bundle") { + entrypoint = "react_native.ts" + + deps = [ + ":react-native", + ] + + visibility = [ + ":*", + ] + + visibility += devtools_models_visibility +} diff --git a/front_end/models/react_native/ReactDevToolsBindingsModel.ts b/front_end/models/react_native/ReactDevToolsBindingsModel.ts new file mode 100644 index 00000000000..6579d918944 --- /dev/null +++ b/front_end/models/react_native/ReactDevToolsBindingsModel.ts @@ -0,0 +1,121 @@ +// 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 SDK from '../../core/sdk/sdk.js'; +import type * as Common from '../../core/common/common.js'; + +type JSONValue = null | string | number | boolean | {[key: string]: JSONValue} | JSONValue[]; +type DomainName = 'react-devtools'; +type DomainMessageListener = (message: JSONValue) => void; +type BindingCalledEventTargetEvent = Common.EventTarget.EventTargetEvent; + +const RUNTIME_GLOBAL = '__FUSEBOX_REACT_DEVTOOLS_DISPATCHER__'; + +export class ReactDevToolsBindingsModel extends SDK.SDKModel.SDKModel { + private readonly domainToListeners: Map> = new Map(); + + constructor(target: SDK.Target.Target) { + super(target); + + this.initialize(target); + } + + private initialize(target: SDK.Target.Target): void { + const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); + if (!runtimeModel) { + return; + } + + runtimeModel.addEventListener(SDK.RuntimeModel.Events.BindingCalled, this.bindingCalled, this); + void this.enable(target); + } + + private bindingCalled(event: BindingCalledEventTargetEvent): void { + const serializedMessage = event.data.payload; + + try { + const {domain, message} = JSON.parse(serializedMessage); + this.dispatchMessageToDomainEventListeners(domain, message); + } catch (err) { + throw new Error('Failed to parse bindingCalled event payload:', err); + } + } + + subscribeToDomainMessages(domainName: DomainName, listener: DomainMessageListener): void { + let listeners = this.domainToListeners.get(domainName); + if (!listeners) { + listeners = new Set(); + this.domainToListeners.set(domainName, listeners); + } + + listeners.add(listener); + } + + unsubscribeFromDomainMessages(domainName: DomainName, listener: DomainMessageListener): void { + const listeners = this.domainToListeners.get(domainName); + if (!listeners) { + return; + } + + listeners.delete(listener); + } + + private dispatchMessageToDomainEventListeners(domainName: DomainName, message: JSONValue): void { + const listeners = this.domainToListeners.get(domainName); + if (!listeners) { + // No subscriptions, no need to throw, just don't notify. + return; + } + + const errors = []; + for (const listener of listeners) { + try { + listener(message); + } catch (e) { + errors.push(e); + } + } + + if (errors.length > 0) { + throw new AggregateError( + errors, + `[ReactDevToolsBindingsModel] Error occurred when calling event listeners for domain: ${domainName}`, + ); + } + } + + async initializeDomain(domainName: DomainName): Promise { + const runtimeModel = this.target().model(SDK.RuntimeModel.RuntimeModel); + if (!runtimeModel) { + throw new Error(`[ReactDevToolsBindingsModel] Failed to initialize domain ${domainName}: runtime model is not available`); + } + + await runtimeModel.agent.invoke_evaluate({expression: `void ${RUNTIME_GLOBAL}.initializeDomain('${domainName}')`}); + } + + async sendMessage(domainName: DomainName, message: JSONValue): Promise { + const runtimeModel = this.target().model(SDK.RuntimeModel.RuntimeModel); + if (!runtimeModel) { + throw new Error(`[ReactDevToolsBindingsModel] Failed to send message for domain ${domainName}: runtime model is not available`); + } + + const serializedMessage = JSON.stringify(message); + + await runtimeModel.agent.invoke_evaluate({expression: `${RUNTIME_GLOBAL}.sendMessage('${domainName}', '${serializedMessage}')`}); + } + + async enable(target: SDK.Target.Target): Promise { + const runtimeModel = target.model(SDK.RuntimeModel.RuntimeModel); + if (!runtimeModel) { + throw new Error('[ReactDevToolsBindingsModel] Failed to enable model: runtime model is not available'); + } + + await runtimeModel.agent.invoke_evaluate({expression: `${RUNTIME_GLOBAL}.BINDING_NAME`}) + .then(response => response.result.value) + .then(bindingName => runtimeModel.agent.invoke_addBinding({name: bindingName})); + } +} + +SDK.SDKModel.SDKModel.register(ReactDevToolsBindingsModel, {capabilities: SDK.Target.Capability.JS, autostart: false}); diff --git a/front_end/models/react_native/react_native.ts b/front_end/models/react_native/react_native.ts new file mode 100644 index 00000000000..b0f37d0fdac --- /dev/null +++ b/front_end/models/react_native/react_native.ts @@ -0,0 +1,8 @@ +// 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 ReactDevToolsBindingsModel from './ReactDevToolsBindingsModel.js'; + +export {ReactDevToolsBindingsModel}; diff --git a/scripts/eslint_rules/lib/check_license_header.js b/scripts/eslint_rules/lib/check_license_header.js index f1e6059c930..07747081faa 100644 --- a/scripts/eslint_rules/lib/check_license_header.js +++ b/scripts/eslint_rules/lib/check_license_header.js @@ -67,11 +67,12 @@ const EXCLUDED_FILES = [ ]; const META_CODE_PATHS = [ + 'core/host/RNPerfMetrics.ts', 'entrypoints/rn_inspector', + 'global_typings/react_native.d.ts', + 'models/react_native', 'panels/react_devtools_placeholder', 'panels/rn_welcome', - 'core/host/RNPerfMetrics.ts', - 'global_typings/react_native.d.ts', ]; const OTHER_LICENSE_HEADERS = [