From b546603bcb309a52343fd6a7b8751145205f8ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 21 Jan 2026 08:33:35 -0500 Subject: [PATCH 1/2] [Fiber] getNearestMountedFiber should consider fibers with alternates as mounted (#35578) --- .../src/ReactFiberTreeReflection.js | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberTreeReflection.js b/packages/react-reconciler/src/ReactFiberTreeReflection.js index c23c3665cf8..530b65d6995 100644 --- a/packages/react-reconciler/src/ReactFiberTreeReflection.js +++ b/packages/react-reconciler/src/ReactFiberTreeReflection.js @@ -33,25 +33,24 @@ import {NoFlags, Placement, Hydrating} from './ReactFiberFlags'; export function getNearestMountedFiber(fiber: Fiber): null | Fiber { let node = fiber; let nearestMounted: null | Fiber = fiber; - if (!fiber.alternate) { - // If there is no alternate, this might be a new tree that isn't inserted - // yet. If it is, then it will have a pending insertion effect on it. - let nextNode: Fiber = node; - do { - node = nextNode; - if ((node.flags & (Placement | Hydrating)) !== NoFlags) { - // This is an insertion or in-progress hydration. The nearest possible - // mounted fiber is the parent but we need to continue to figure out - // if that one is still mounted. - nearestMounted = node.return; - } - // $FlowFixMe[incompatible-type] we bail out when we get a null - nextNode = node.return; - } while (nextNode); - } else { - while (node.return) { - node = node.return; + // If there is no alternate, this might be a new tree that isn't inserted + // yet. If it is, then it will have a pending insertion effect on it. + let nextNode: Fiber = node; + while (nextNode && !nextNode.alternate) { + node = nextNode; + if ((node.flags & (Placement | Hydrating)) !== NoFlags) { + // This is an insertion or in-progress hydration. The nearest possible + // mounted fiber is the parent but we need to continue to figure out + // if that one is still mounted. + nearestMounted = node.return; } + // $FlowFixMe[incompatible-type] we bail out when we get a null + nextNode = node.return; + } + // After we've reached an alternate, go the rest of the way to see if the + // tree is still mounted. If it's not, its return pointer will be disconnected. + while (node.return) { + node = node.return; } if (node.tag === HostRoot) { // TODO: Check if this was a nested HostRoot when used with From cdbd55f44004cf77a61c178952e34d4514414519 Mon Sep 17 00:00:00 2001 From: "Sebastian \"Sebbie\" Silbermann" Date: Wed, 21 Jan 2026 19:13:24 +0100 Subject: [PATCH 2/2] Type `react-devtools-hook-installer` and `react-devtools-hook-settings-injector` messages (#35586) --- .eslintrc.js | 1 + flow-typed/environments/bom.js | 16 +++--- flow-typed/environments/dom.js | 6 +- flow-typed/environments/html.js | 4 +- packages/react-devtools-core/src/backend.js | 2 +- .../contentScripts/hookSettingsInjector.js | 56 +++++++++++-------- .../src/contentScripts/installHook.js | 23 +++++--- .../src/contentScripts/messages.js | 42 ++++++++++++++ 8 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 packages/react-devtools-extensions/src/contentScripts/messages.js diff --git a/.eslintrc.js b/.eslintrc.js index dec7cd8f304..952149f30ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -593,6 +593,7 @@ module.exports = { mixin$Animatable: 'readonly', MouseEventHandler: 'readonly', NavigateEvent: 'readonly', + Partial: 'readonly', PerformanceMeasureOptions: 'readonly', PropagationPhases: 'readonly', PropertyDescriptor: 'readonly', diff --git a/flow-typed/environments/bom.js b/flow-typed/environments/bom.js index 06412856009..7c082ea5dae 100644 --- a/flow-typed/environments/bom.js +++ b/flow-typed/environments/bom.js @@ -826,7 +826,7 @@ declare class WebSocket extends EventTarget { bufferedAmount: number; extensions: string; onopen: (ev: any) => mixed; - onmessage: (ev: MessageEvent) => mixed; + onmessage: (ev: MessageEvent<>) => mixed; onclose: (ev: CloseEvent) => mixed; onerror: (ev: any) => mixed; binaryType: 'blob' | 'arraybuffer'; @@ -855,8 +855,8 @@ declare class Worker extends EventTarget { workerOptions?: WorkerOptions ): void; onerror: null | ((ev: any) => mixed); - onmessage: null | ((ev: MessageEvent) => mixed); - onmessageerror: null | ((ev: MessageEvent) => mixed); + onmessage: null | ((ev: MessageEvent<>) => mixed); + onmessageerror: null | ((ev: MessageEvent<>) => mixed); postMessage(message: any, ports?: any): void; terminate(): void; } @@ -888,14 +888,14 @@ declare class WorkerGlobalScope extends EventTarget { } declare class DedicatedWorkerGlobalScope extends WorkerGlobalScope { - onmessage: (ev: MessageEvent) => mixed; - onmessageerror: (ev: MessageEvent) => mixed; + onmessage: (ev: MessageEvent<>) => mixed; + onmessageerror: (ev: MessageEvent<>) => mixed; postMessage(message: any, transfer?: Iterable): void; } declare class SharedWorkerGlobalScope extends WorkerGlobalScope { name: string; - onconnect: (ev: MessageEvent) => mixed; + onconnect: (ev: MessageEvent<>) => mixed; } declare class WorkerLocation { @@ -2056,8 +2056,8 @@ declare class MessagePort extends EventTarget { start(): void; close(): void; - onmessage: null | ((ev: MessageEvent) => mixed); - onmessageerror: null | ((ev: MessageEvent) => mixed); + onmessage: null | ((ev: MessageEvent<>) => mixed); + onmessageerror: null | ((ev: MessageEvent<>) => mixed); } declare class MessageChannel { diff --git a/flow-typed/environments/dom.js b/flow-typed/environments/dom.js index 8e3e7570731..331e73f8914 100644 --- a/flow-typed/environments/dom.js +++ b/flow-typed/environments/dom.js @@ -151,7 +151,7 @@ type TransitionEventHandler = (event: TransitionEvent) => mixed; type TransitionEventListener = | {handleEvent: TransitionEventHandler, ...} | TransitionEventHandler; -type MessageEventHandler = (event: MessageEvent) => mixed; +type MessageEventHandler = (event: MessageEvent<>) => mixed; type MessageEventListener = | {handleEvent: MessageEventHandler, ...} | MessageEventHandler; @@ -845,8 +845,8 @@ declare class PageTransitionEvent extends Event { // https://www.w3.org/TR/2008/WD-html5-20080610/comms.html // and // https://html.spec.whatwg.org/multipage/comms.html#the-messageevent-interfaces -declare class MessageEvent extends Event { - data: mixed; +declare class MessageEvent extends Event { + data: Data; origin: string; lastEventId: string; source: WindowProxy; diff --git a/flow-typed/environments/html.js b/flow-typed/environments/html.js index 54e1e48f739..ffbf9ac9405 100644 --- a/flow-typed/environments/html.js +++ b/flow-typed/environments/html.js @@ -109,8 +109,8 @@ declare class ErrorEvent extends Event { // https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts declare class BroadcastChannel extends EventTarget { name: string; - onmessage: ?(event: MessageEvent) => void; - onmessageerror: ?(event: MessageEvent) => void; + onmessage: ?(event: MessageEvent<>) => void; + onmessageerror: ?(event: MessageEvent<>) => void; constructor(name: string): void; postMessage(msg: mixed): void; diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 9305155ab0e..4cca0887c57 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -293,7 +293,7 @@ export function connectToDevTools(options: ?ConnectOptions) { scheduleRetry(); } - function handleMessage(event: MessageEvent) { + function handleMessage(event: MessageEvent<>) { let data; try { if (typeof event.data === 'string') { diff --git a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js index da809f65cac..196369e71d9 100644 --- a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js +++ b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js @@ -1,38 +1,50 @@ /* global chrome */ +/** @flow */ // We can't use chrome.storage domain from scripts which are injected in ExecutionWorld.MAIN // This is the only purpose of this script - to send persisted settings to installHook.js content script -async function messageListener(event: MessageEvent) { +import type {UnknownMessageEvent} from './messages'; +import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; +import {postMessage} from './messages'; + +async function messageListener(event: UnknownMessageEvent) { if (event.source !== window) { return; } if (event.data.source === 'react-devtools-hook-installer') { if (event.data.payload.handshake) { - const settings = await chrome.storage.local.get(); + const settings: Partial = + await chrome.storage.local.get(); // If storage was empty (first installation), define default settings - if (typeof settings.appendComponentStack !== 'boolean') { - settings.appendComponentStack = true; - } - if (typeof settings.breakOnConsoleErrors !== 'boolean') { - settings.breakOnConsoleErrors = false; - } - if (typeof settings.showInlineWarningsAndErrors !== 'boolean') { - settings.showInlineWarningsAndErrors = true; - } - if (typeof settings.hideConsoleLogsInStrictMode !== 'boolean') { - settings.hideConsoleLogsInStrictMode = false; - } - if ( - typeof settings.disableSecondConsoleLogDimmingInStrictMode !== 'boolean' - ) { - settings.disableSecondConsoleLogDimmingInStrictMode = false; - } + const hookSettings: DevToolsHookSettings = { + appendComponentStack: + typeof settings.appendComponentStack === 'boolean' + ? settings.appendComponentStack + : true, + breakOnConsoleErrors: + typeof settings.breakOnConsoleErrors === 'boolean' + ? settings.breakOnConsoleErrors + : false, + showInlineWarningsAndErrors: + typeof settings.showInlineWarningsAndErrors === 'boolean' + ? settings.showInlineWarningsAndErrors + : true, + hideConsoleLogsInStrictMode: + typeof settings.hideConsoleLogsInStrictMode === 'boolean' + ? settings.hideConsoleLogsInStrictMode + : false, + disableSecondConsoleLogDimmingInStrictMode: + typeof settings.disableSecondConsoleLogDimmingInStrictMode === + 'boolean' + ? settings.disableSecondConsoleLogDimmingInStrictMode + : false, + }; - window.postMessage({ + postMessage({ source: 'react-devtools-hook-settings-injector', - payload: {settings}, + payload: {settings: hookSettings}, }); window.removeEventListener('message', messageListener); @@ -41,7 +53,7 @@ async function messageListener(event: MessageEvent) { } window.addEventListener('message', messageListener); -window.postMessage({ +postMessage({ source: 'react-devtools-hook-settings-injector', payload: {handshake: true}, }); diff --git a/packages/react-devtools-extensions/src/contentScripts/installHook.js b/packages/react-devtools-extensions/src/contentScripts/installHook.js index e70e97b2857..8f30d6f0892 100644 --- a/packages/react-devtools-extensions/src/contentScripts/installHook.js +++ b/packages/react-devtools-extensions/src/contentScripts/installHook.js @@ -1,39 +1,46 @@ +/** @flow */ + +import type {UnknownMessageEvent} from './messages'; +import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; + import {installHook} from 'react-devtools-shared/src/hook'; import { getIfReloadedAndProfiling, getProfilingSettings, } from 'react-devtools-shared/src/utils'; +import {postMessage} from './messages'; -let resolveHookSettingsInjection; +let resolveHookSettingsInjection: (settings: DevToolsHookSettings) => void; -function messageListener(event: MessageEvent) { +function messageListener(event: UnknownMessageEvent) { if (event.source !== window) { return; } if (event.data.source === 'react-devtools-hook-settings-injector') { + const payload = event.data.payload; // In case handshake message was sent prior to hookSettingsInjector execution // We can't guarantee order - if (event.data.payload.handshake) { - window.postMessage({ + if (payload.handshake) { + postMessage({ source: 'react-devtools-hook-installer', payload: {handshake: true}, }); - } else if (event.data.payload.settings) { + } else if (payload.settings) { window.removeEventListener('message', messageListener); - resolveHookSettingsInjection(event.data.payload.settings); + resolveHookSettingsInjection(payload.settings); } } } // Avoid double execution if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { - const hookSettingsPromise = new Promise(resolve => { + const hookSettingsPromise = new Promise(resolve => { resolveHookSettingsInjection = resolve; }); window.addEventListener('message', messageListener); - window.postMessage({ + postMessage({ source: 'react-devtools-hook-installer', payload: {handshake: true}, }); diff --git a/packages/react-devtools-extensions/src/contentScripts/messages.js b/packages/react-devtools-extensions/src/contentScripts/messages.js new file mode 100644 index 00000000000..e65d46b4b26 --- /dev/null +++ b/packages/react-devtools-extensions/src/contentScripts/messages.js @@ -0,0 +1,42 @@ +/** @flow */ + +import type {DevToolsHookSettings} from 'react-devtools-shared/src/backend/types'; + +export function postMessage(event: UnknownMessageEventData): void { + window.postMessage(event); +} + +export interface UnknownMessageEvent + extends MessageEvent {} + +export type UnknownMessageEventData = + | HookSettingsInjectorEventData + | HookInstallerEventData; + +export type HookInstallerEventData = { + source: 'react-devtools-hook-installer', + payload: HookInstallerEventPayload, +}; + +export type HookInstallerEventPayload = HookInstallerEventPayloadHandshake; + +export type HookInstallerEventPayloadHandshake = { + handshake: true, +}; + +export type HookSettingsInjectorEventData = { + source: 'react-devtools-hook-settings-injector', + payload: HookSettingsInjectorEventPayload, +}; + +export type HookSettingsInjectorEventPayload = + | HookSettingsInjectorEventPayloadHandshake + | HookSettingsInjectorEventPayloadSettings; + +export type HookSettingsInjectorEventPayloadHandshake = { + handshake: true, +}; + +export type HookSettingsInjectorEventPayloadSettings = { + settings: DevToolsHookSettings, +};