diff --git a/packages/react-native/src/private/fusebox/.npmignore b/packages/react-native/src/private/fusebox/.npmignore new file mode 100644 index 00000000000000..7e2f179b52bfdb --- /dev/null +++ b/packages/react-native/src/private/fusebox/.npmignore @@ -0,0 +1 @@ +assets diff --git a/packages/react-native/src/private/fusebox/README.md b/packages/react-native/src/private/fusebox/README.md new file mode 100644 index 00000000000000..64d2b5e9a50f4d --- /dev/null +++ b/packages/react-native/src/private/fusebox/README.md @@ -0,0 +1,12 @@ +# Fusebox Runtime + +## What is Fusebox? +"Fusebox" is the internal codename for the new React Native debugger stack based on Chrome DevTools. + +## Architecture for React DevTools communication + +### Frontend to Backend communication +![Frontend to backend communication diagram](./assets/frontend-to-backend.excalidraw-embedded.png) + +### Backend to Frontend communication +![Backend to frontend communication diagram](./assets/backend-to-frontend.excalidraw-embedded.png) diff --git a/packages/react-native/src/private/fusebox/assets/backend-to-frontend.excalidraw-embedded.png b/packages/react-native/src/private/fusebox/assets/backend-to-frontend.excalidraw-embedded.png new file mode 100644 index 00000000000000..ac8afed14cec97 Binary files /dev/null and b/packages/react-native/src/private/fusebox/assets/backend-to-frontend.excalidraw-embedded.png differ diff --git a/packages/react-native/src/private/fusebox/assets/frontend-to-backend.excalidraw-embedded.png b/packages/react-native/src/private/fusebox/assets/frontend-to-backend.excalidraw-embedded.png new file mode 100644 index 00000000000000..2f6d7ed1fe835c Binary files /dev/null and b/packages/react-native/src/private/fusebox/assets/frontend-to-backend.excalidraw-embedded.png differ diff --git a/packages/react-native/src/private/fusebox/setUpFuseboxReactDevToolsDispatcher.js b/packages/react-native/src/private/fusebox/setUpFuseboxReactDevToolsDispatcher.js new file mode 100644 index 00000000000000..e5638d8e6a7b16 --- /dev/null +++ b/packages/react-native/src/private/fusebox/setUpFuseboxReactDevToolsDispatcher.js @@ -0,0 +1,108 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +type JSONValue = + | string + | number + | boolean + | null + | {[key: string]: JSONValue} + | Array; +type DomainName = 'react-devtools'; + +class EventScope { + #listeners: Set<(T) => void> = new Set(); + + addEventListener(listener: T => void): void { + this.#listeners.add(listener); + } + + removeEventListener(listener: T => void): void { + this.#listeners.delete(listener); + } + + emit(value: T): void { + // Assuming that listeners won't throw. + for (const listener of this.#listeners) { + listener(value); + } + } +} + +class Domain { + name: DomainName; + onMessage: EventScope; + + constructor(name: DomainName) { + if (global[FuseboxReactDevToolsDispatcher.BINDING_NAME] == null) { + throw new Error( + `Could not create domain ${name}: receiving end doesn't exist`, + ); + } + + this.name = name; + this.onMessage = new EventScope(); + } + + sendMessage(message: JSONValue) { + const messageWithDomain = {domain: this.name, message}; + const serializedMessageWithDomain = JSON.stringify(messageWithDomain); + + global[FuseboxReactDevToolsDispatcher.BINDING_NAME]( + serializedMessageWithDomain, + ); + } +} + +class FuseboxReactDevToolsDispatcher { + static #domainNameToDomainMap: Map = new Map(); + + // Referenced and initialized from Chrome DevTools frontend. + static BINDING_NAME: string = '__CHROME_DEVTOOLS_FRONTEND_BINDING__'; + static onDomainInitialization: EventScope = new EventScope(); + + // Should be private, referenced from Chrome DevTools frontend only. + static initializeDomain(domainName: DomainName): Domain { + const domain = new Domain(domainName); + + this.#domainNameToDomainMap.set(domainName, domain); + this.onDomainInitialization.emit(domain); + + return domain; + } + + // Should be private, referenced from Chrome DevTools frontend only. + static sendMessage(domainName: DomainName, message: string): void { + const domain = this.#domainNameToDomainMap.get(domainName); + if (domain == null) { + throw new Error( + `Could not send message to ${domainName}: domain doesn't exist`, + ); + } + + try { + const parsedMessage = JSON.parse(message); + domain.onMessage.emit(parsedMessage); + } catch (err) { + console.error( + `Error while trying to send a message to domain ${domainName}:`, + err, + ); + } + } +} + +Object.defineProperty(global, '__FUSEBOX_REACT_DEVTOOLS_DISPATCHER__', { + value: FuseboxReactDevToolsDispatcher, + configurable: false, + enumerable: false, + writable: false, +});