Repository: facebook/react
Pull request: Typecheck React DevTools extension main script

Notably we ignore the passed theme in `onThemeChanged` and just do a plain re-render (introduced in https://github.com/facebook/react/pull/33992).

Remove emission of unused `extensionEditorPaneShown` and `extensionEditorPaneHidden` events (added in https://github.com/facebook/react/issues/33968).

Changed files:
filename: packages/react-devtools-extensions/src/main/index.js
patch: @@ -1,6 +1,14 @@
 /* global chrome */
-
+/** @flow */
+
+import type {RootType} from 'react-dom/src/client/ReactDOMRoot';
+import type {FrontendBridge, Message} from 'react-devtools-shared/src/bridge';
+import type {
+  TabID,
+  ViewElementSource,
+} from 'react-devtools-shared/src/devtools/views/DevTools';
 import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane';
+import type {Element} from 'react-devtools-shared/src/frontend/types';
 
 import {createElement} from 'react';
 import {flushSync} from 'react-dom';
@@ -51,9 +59,9 @@ const hookNamesModuleLoaderFunction = () => resolvedParseHookNames;
 function createBridge() {
   bridge = new Bridge({
     listen(fn) {
-      const bridgeListener = message => fn(message);
+      const bridgeListener = (message: Message) => fn(message);
       // Store the reference so that we unsubscribe from the same object.
-      const portOnMessage = port.onMessage;
+      const portOnMessage = ((port: any): ExtensionPort).onMessage;
       portOnMessage.addListener(bridgeListener);
 
       lastSubscribedBridgeListener = bridgeListener;
@@ -71,7 +79,7 @@ function createBridge() {
 
   bridge.addListener('reloadAppForProfiling', () => {
     localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
-    evalInInspectedWindow('reload', []);
+    evalInInspectedWindow('reload', [], () => {});
   });
 
   bridge.addListener(
@@ -176,14 +184,20 @@ function createBridgeAndStore() {
   // Otherwise, the Store may miss important initial tree op codes.
   injectBackendManager(chrome.devtools.inspectedWindow.tabId);
 
-  const viewAttributeSourceFunction = (id, path) => {
+  const viewAttributeSourceFunction = (
+    id: Element['id'],
+    path: Array<string | number>,
+  ) => {
     const rendererID = store.getRendererIDForElement(id);
     if (rendererID != null) {
       viewAttributeSource(rendererID, id, path);
     }
   };
 
-  const viewElementSourceFunction = (source, symbolicatedSource) => {
+  const viewElementSourceFunction: ViewElementSource = (
+    source,
+    symbolicatedSource,
+  ) => {
     const [, sourceURL, line, column] = symbolicatedSource
       ? symbolicatedSource
       : source;
@@ -198,7 +212,7 @@ function createBridgeAndStore() {
 
   root = createRoot(document.createElement('div'));
 
-  render = (overrideTab = mostRecentOverrideTab) => {
+  render = (overrideTab: TabID | null = mostRecentOverrideTab) => {
     mostRecentOverrideTab = overrideTab;
 
     root.render(
@@ -227,7 +241,9 @@ function createBridgeAndStore() {
   };
 }
 
-function ensureInitialHTMLIsCleared(container) {
+function ensureInitialHTMLIsCleared(
+  container: HTMLElement & {_hasInitialHTMLBeenCleared?: boolean},
+) {
   if (container._hasInitialHTMLBeenCleared) {
     return;
   }
@@ -397,13 +413,6 @@ function createSourcesEditorPanel() {
         logEvent({event_name: 'selected-editor-pane'});
       }
     });
-
-    createdPane.onShown.addListener(() => {
-      bridge.emit('extensionEditorPaneShown');
-    });
-    createdPane.onHidden.addListener(() => {
-      bridge.emit('extensionEditorPaneHidden');
-    });
   });
 }
 
@@ -479,10 +488,10 @@ function performInTabNavigationCleanup() {
   // Do not clean mostRecentOverrideTab on purpose, so we remember last opened
   // React DevTools tab, when user does in-tab navigation
 
-  store = null;
-  bridge = null;
-  render = null;
-  root = null;
+  store = (null: $FlowFixMe);
+  bridge = (null: $FlowFixMe);
+  render = (null: $FlowFixMe);
+  root = (null: $FlowFixMe);
 }
 
 function performFullCleanup() {
@@ -504,18 +513,18 @@ function performFullCleanup() {
   componentsPortalContainer = null;
   profilerPortalContainer = null;
   suspensePortalContainer = null;
-  root = null;
+  root = (null: $FlowFixMe);
 
   mostRecentOverrideTab = null;
-  store = null;
-  bridge = null;
-  render = null;
+  store = (null: $FlowFixMe);
+  bridge = (null: $FlowFixMe);
+  render = (null: $FlowFixMe);
 
   port?.disconnect();
-  port = null;
+  port = (null: $FlowFixMe);
 }
 
-function connectExtensionPort() {
+function connectExtensionPort(): void {
   if (port) {
     throw new Error('DevTools port was already connected');
   }
@@ -539,7 +548,7 @@ function connectExtensionPort() {
   // so, when we call `port.disconnect()` from this script,
   // this should not trigger this callback and port reconnection
   port.onDisconnect.addListener(() => {
-    port = null;
+    port = (null: $FlowFixMe);
     connectExtensionPort();
   });
 }
@@ -593,9 +602,9 @@ function mountReactDevToolsWhenReactHasLoaded() {
   );
 }
 
-let bridge = null;
+let bridge: FrontendBridge = (null: $FlowFixMe);
 let lastSubscribedBridgeListener = null;
-let store = null;
+let store: Store = (null: $FlowFixMe);
 
 let profilingData = null;
 
@@ -610,13 +619,28 @@ let suspensePortalContainer = null;
 let editorPortalContainer = null;
 let inspectedElementPortalContainer = null;
 
-let mostRecentOverrideTab = null;
-let render = null;
-let root = null;
+let mostRecentOverrideTab: null | TabID = null;
+let render: (overrideTab?: TabID) => void = (null: $FlowFixMe);
+let root: RootType = (null: $FlowFixMe);
 
 let currentSelectedSource: null | SourceSelection = null;
 
-let port = null;
+type ExtensionEvent = {
+  addListener(callback: (message: Message, port: ExtensionPort) => void): void,
+  removeListener(
+    callback: (message: Message, port: ExtensionPort) => void,
+  ): void,
+};
+
+/** https://developer.chrome.com/docs/extensions/reference/api/runtime#type-Port */
+type ExtensionPort = {
+  onDisconnect: ExtensionEvent,
+  onMessage: ExtensionEvent,
+  postMessage(message: mixed, transferable?: Array<mixed>): void,
+  disconnect(): void,
+};
+
+let port: ExtensionPort = (null: $FlowFixMe);
 
 // In case when multiple navigation events emitted in a short period of time
 // This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
@@ -649,7 +673,7 @@ connectExtensionPort();
 
 mountReactDevToolsWhenReactHasLoaded();
 
-function onThemeChanged(themeName) {
+function onThemeChanged() {
   // Rerender with the new theme
   render();
 }

filename: packages/react-devtools-shared/src/Logger.js
patch: @@ -63,7 +63,11 @@ export type LoggerEvent =
         +value: any,
         ...
       },
-    };
+    }
+  | {
+      +event_name: 'selected-editor-pane',
+    }
+  | {+event_name: 'selected-inspected-element-pane'};
 
 export type LogFunction = LoggerEvent => void | Promise<void>;
 

filename: packages/react-devtools-shared/src/bridge.js
patch: @@ -74,7 +74,7 @@ export const currentBridgeProtocol: BridgeProtocol =
 
 type ElementAndRendererID = {id: number, rendererID: RendererID};
 
-type Message = {
+export type Message = {
   event: string,
   payload: any,
 };
@@ -239,7 +239,7 @@ export type BackendEvents = {
 type StartProfilingParams = ProfilingSettings;
 type ReloadAndProfilingParams = ProfilingSettings;
 
-type FrontendEvents = {
+export type FrontendEvents = {
   clearErrorsAndWarnings: [{rendererID: RendererID}],
   clearErrorsForElementID: [ElementAndRendererID],
   clearHostInstanceHighlight: [],