From 99a1740259df9a1f1757c9b75ed034a625d25035 Mon Sep 17 00:00:00 2001 From: Yukimasa Funaoka Date: Tue, 25 Nov 2025 21:21:12 +0900 Subject: [PATCH 1/3] [DevTools] Fix developer tools not working in tabs restricted by CSP --- .../dynamicallyInjectContentScripts.js | 8 ++ .../src/background/messageHandlers.js | 52 +++++++++++++ .../src/contentScripts/fallbackEvalContext.js | 44 +++++++++++ .../src/contentScripts/proxy.js | 36 +++++++++ .../src/main/elementSelection.js | 10 ++- .../src/main/evalInInspectedWindow.js | 78 +++++++++++++++++++ .../src/main/index.js | 3 +- .../src/main/reactPolling.js | 6 +- .../src/main/sourceSelection.js | 10 ++- .../webpack.config.js | 1 + 10 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js create mode 100644 packages/react-devtools-extensions/src/main/evalInInspectedWindow.js diff --git a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js index 3e2f6b7c1ea83..84624b021bc68 100644 --- a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js +++ b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js @@ -17,6 +17,14 @@ const contentScriptsToInject = [ runAt: 'document_end', world: chrome.scripting.ExecutionWorld.ISOLATED, }, + { + id: '@react-devtools/fallback-eval-context', + js: ['build/fallbackEvalContext.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_start', + world: chrome.scripting.ExecutionWorld.MAIN, + }, { id: '@react-devtools/hook', js: ['build/installHook.js'], diff --git a/packages/react-devtools-extensions/src/background/messageHandlers.js b/packages/react-devtools-extensions/src/background/messageHandlers.js index 0152418633fbb..f762d6464cab1 100644 --- a/packages/react-devtools-extensions/src/background/messageHandlers.js +++ b/packages/react-devtools-extensions/src/background/messageHandlers.js @@ -97,6 +97,58 @@ export function handleDevToolsPageMessage(message) { break; } + + case 'eval-in-inspected-window': { + const { + payload: {tabId, requestId, scriptId, args}, + } = message; + + chrome.tabs + .sendMessage(tabId, { + source: 'devtools-page-eval', + payload: { + scriptId, + args, + }, + }) + .then(response => { + if (!response) { + chrome.runtime.sendMessage({ + source: 'react-devtools-background', + payload: { + type: 'eval-in-inspected-window-response', + requestId, + result: null, + error: 'No response from content script', + }, + }); + return; + } + const {result, error} = response; + chrome.runtime.sendMessage({ + source: 'react-devtools-background', + payload: { + type: 'eval-in-inspected-window-response', + requestId, + result, + error, + }, + }); + }) + .catch(error => { + chrome.runtime.sendMessage({ + source: 'react-devtools-background', + payload: { + type: 'eval-in-inspected-window-response', + requestId, + result: null, + error: error?.message || String(error), + }, + }); + }); + + break; + } } } diff --git a/packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js b/packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js new file mode 100644 index 0000000000000..1e9037edf3ec6 --- /dev/null +++ b/packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js @@ -0,0 +1,44 @@ +/* + Can not access `Developer Tools Console API` (e.g., inspect(), $0) in this context. + So some functions are no-op or throw error. +*/ +const evalScripts = { + checkIfReactPresentInInspectedWindow: () => + window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0, + reload: () => window.location.reload(), + setBrowserSelectionFromReact: () => { + throw new Error('Not supported in fallback eval context'); + }, + setReactSelectionFromBrowser: () => { + throw new Error('Not supported in fallback eval context'); + }, + viewAttributeSource: ({rendererID, elementID, path}) => { + return false; // Not supported in fallback eval context + }, + viewElementSource: ({rendererID, elementID}) => { + return false; // Not supported in fallback eval context + }, +}; + +window.addEventListener('message', event => { + if (event.data?.source === 'react-devtools-content-script-eval') { + const {scriptId, args, requestId} = event.data.payload; + const response = {result: null, error: null}; + try { + response.result = evalScripts[scriptId].apply(null, args); + } catch (err) { + response.error = err.message; + } + window.postMessage( + { + source: 'react-devtools-content-script-eval-response', + payload: { + requestId, + response, + }, + }, + '*', + ); + } +}); diff --git a/packages/react-devtools-extensions/src/contentScripts/proxy.js b/packages/react-devtools-extensions/src/contentScripts/proxy.js index 02253f65d4a83..330bcbdb2a84b 100644 --- a/packages/react-devtools-extensions/src/contentScripts/proxy.js +++ b/packages/react-devtools-extensions/src/contentScripts/proxy.js @@ -117,3 +117,39 @@ function connectPort() { // $FlowFixMe[incompatible-use] port.onDisconnect.addListener(handleDisconnect); } + +let evalRequestId = 0; +const evalRequestCallbacks = new Map(); + +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + switch (msg?.source) { + case 'devtools-page-eval': { + const {scriptId, args} = msg.payload; + const requestId = evalRequestId++; + window.postMessage( + { + source: 'react-devtools-content-script-eval', + payload: { + requestId, + scriptId, + args, + }, + }, + '*', + ); + evalRequestCallbacks.set(requestId, sendResponse); + return true; // Indicate we will respond asynchronously + } + } +}); + +window.addEventListener('message', event => { + if (event.data?.source === 'react-devtools-content-script-eval-response') { + const {requestId, response} = event.data.payload; + const callback = evalRequestCallbacks.get(requestId); + if (callback) { + callback(response); + evalRequestCallbacks.delete(requestId); + } + } +}); diff --git a/packages/react-devtools-extensions/src/main/elementSelection.js b/packages/react-devtools-extensions/src/main/elementSelection.js index 7f9b8ed208527..92bead0340d15 100644 --- a/packages/react-devtools-extensions/src/main/elementSelection.js +++ b/packages/react-devtools-extensions/src/main/elementSelection.js @@ -1,10 +1,12 @@ -/* global chrome */ +import {evalInInspectedWindow} from './evalInInspectedWindow'; export function setBrowserSelectionFromReact() { // This is currently only called on demand when you press "view DOM". // In the future, if Chrome adds an inspect() that doesn't switch tabs, // we could make this happen automatically when you select another component. - chrome.devtools.inspectedWindow.eval( + evalInInspectedWindow( + 'setBrowserSelectionFromReact', + [], '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' + 'false', @@ -19,7 +21,9 @@ export function setBrowserSelectionFromReact() { export function setReactSelectionFromBrowser(bridge) { // When the user chooses a different node in the browser Elements tab, // copy it over to the hook object so that we can sync the selection. - chrome.devtools.inspectedWindow.eval( + evalInInspectedWindow( + 'setReactSelectionFromBrowser', + [], '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' + 'false', diff --git a/packages/react-devtools-extensions/src/main/evalInInspectedWindow.js b/packages/react-devtools-extensions/src/main/evalInInspectedWindow.js new file mode 100644 index 0000000000000..8ef131284c387 --- /dev/null +++ b/packages/react-devtools-extensions/src/main/evalInInspectedWindow.js @@ -0,0 +1,78 @@ +const EVAL_TIMEOUT = 1000 * 10; + +let evalRequestId = 0; +const evalRequestCallbacks = new Map(); + +function fallbackEvalInInspectedWindow(scriptId, args, code, callback) { + const tabId = chrome.devtools.inspectedWindow.tabId; + const requestId = evalRequestId++; + chrome.runtime.sendMessage({ + source: 'devtools-page', + payload: { + type: 'eval-in-inspected-window', + tabId, + requestId, + scriptId, + args, + }, + }); + const timeout = setTimeout(() => { + evalRequestCallbacks.delete(requestId); + if (callback) { + callback(null, { + code, + description: + 'Timed out while waiting for eval response from the inspected window.', + isError: true, + isException: false, + value: undefined, + }); + } + }, EVAL_TIMEOUT); + evalRequestCallbacks.set(requestId, ({result, error}) => { + clearTimeout(timeout); + evalRequestCallbacks.delete(requestId); + if (callback) { + if (error) { + callback(null, { + code, + description: undefined, + isError: false, + isException: true, + value: error, + }); + return; + } + callback(result, null); + } + }); +} + +export function evalInInspectedWindow(scriptId, args, code, callback) { + chrome.devtools.inspectedWindow.eval(code, (result, exceptionInfo) => { + if (!exceptionInfo) { + callback(result, exceptionInfo); + return; + } + // If an exception (e.g. CSP Blocked) occurred, + // fallback to the content script eval context + fallbackEvalInInspectedWindow(scriptId, args, code, callback); + }); +} + +function handleEvalInInspectedWindow({payload, source}) { + if (source === 'react-devtools-background') { + switch (payload?.type) { + case 'eval-in-inspected-window-response': { + const {requestId, result, error} = payload; + const callback = evalRequestCallbacks.get(requestId); + if (callback) { + callback({result, error}); + } + break; + } + } + } +} + +chrome.runtime.onMessage.addListener(handleEvalInInspectedWindow); diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index bc948d1e6d6c9..3659736fc0190 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -32,6 +32,7 @@ import { } from './elementSelection'; import {viewAttributeSource} from './sourceSelection'; +import {evalInInspectedWindow} from './evalInInspectedWindow'; import {startReactPolling} from './reactPolling'; import {cloneStyleTags} from './cloneStyleTags'; import fetchFileWithCaching from './fetchFileWithCaching'; @@ -70,7 +71,7 @@ function createBridge() { bridge.addListener('reloadAppForProfiling', () => { localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); - chrome.devtools.inspectedWindow.eval('window.location.reload();'); + evalInInspectedWindow('reload', [], 'window.location.reload();'); }); bridge.addListener( diff --git a/packages/react-devtools-extensions/src/main/reactPolling.js b/packages/react-devtools-extensions/src/main/reactPolling.js index 9bb034c6a1091..7aec21aed1013 100644 --- a/packages/react-devtools-extensions/src/main/reactPolling.js +++ b/packages/react-devtools-extensions/src/main/reactPolling.js @@ -1,4 +1,4 @@ -/* global chrome */ +import {evalInInspectedWindow} from './evalInInspectedWindow'; class CouldNotFindReactOnThePageError extends Error { constructor() { @@ -26,7 +26,9 @@ export function startReactPolling( // This function will call onSuccess only if React was found and polling is not aborted, onError will be called for every other case function checkIfReactPresentInInspectedWindow(onSuccess, onError) { - chrome.devtools.inspectedWindow.eval( + evalInInspectedWindow( + 'checkIfReactPresentInInspectedWindow', + [], 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0', (pageHasReact, exceptionInfo) => { if (status === 'aborted') { diff --git a/packages/react-devtools-extensions/src/main/sourceSelection.js b/packages/react-devtools-extensions/src/main/sourceSelection.js index 0534a921af05e..cbb48e97323da 100644 --- a/packages/react-devtools-extensions/src/main/sourceSelection.js +++ b/packages/react-devtools-extensions/src/main/sourceSelection.js @@ -1,7 +1,9 @@ -/* global chrome */ +import {evalInInspectedWindow} from './evalInInspectedWindow'; export function viewAttributeSource(rendererID, elementID, path) { - chrome.devtools.inspectedWindow.eval( + evalInInspectedWindow( + 'viewAttributeSource', + [{rendererID, elementID, path}], '{' + // The outer block is important because it means we can declare local variables. 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' + JSON.stringify(rendererID) + @@ -31,7 +33,9 @@ export function viewAttributeSource(rendererID, elementID, path) { } export function viewElementSource(rendererID, elementID) { - chrome.devtools.inspectedWindow.eval( + evalInInspectedWindow( + 'viewElementSource', + [{rendererID, elementID}], '{' + // The outer block is important because it means we can declare local variables. 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' + JSON.stringify(rendererID) + diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index 191eabc47cbf9..7b5acca6cc288 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -69,6 +69,7 @@ module.exports = { backend: './src/backend.js', background: './src/background/index.js', backendManager: './src/contentScripts/backendManager.js', + fallbackEvalContext: './src/contentScripts/fallbackEvalContext.js', fileFetcher: './src/contentScripts/fileFetcher.js', main: './src/main/index.js', panel: './src/panel.js', From 82984e5ea9cec4ef45b180261703eba40025a34a Mon Sep 17 00:00:00 2001 From: Yukimasa Funaoka Date: Thu, 27 Nov 2025 00:23:24 +0900 Subject: [PATCH 2/3] Refactor and improve error handling --- .../src/contentScripts/fallbackEvalContext.js | 37 +++--- .../src/contentScripts/proxy.js | 9 +- .../src/evalScripts.js | 112 ++++++++++++++++++ .../src/main/elementSelection.js | 6 - .../src/main/evalInInspectedWindow.js | 54 +++++++-- .../src/main/index.js | 2 +- .../src/main/reactPolling.js | 1 - .../src/main/sourceSelection.js | 38 ------ 8 files changed, 179 insertions(+), 80 deletions(-) create mode 100644 packages/react-devtools-extensions/src/evalScripts.js diff --git a/packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js b/packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js index 1e9037edf3ec6..e274f886ee56e 100644 --- a/packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js +++ b/packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js @@ -1,32 +1,23 @@ -/* - Can not access `Developer Tools Console API` (e.g., inspect(), $0) in this context. - So some functions are no-op or throw error. -*/ -const evalScripts = { - checkIfReactPresentInInspectedWindow: () => - window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && - window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0, - reload: () => window.location.reload(), - setBrowserSelectionFromReact: () => { - throw new Error('Not supported in fallback eval context'); - }, - setReactSelectionFromBrowser: () => { - throw new Error('Not supported in fallback eval context'); - }, - viewAttributeSource: ({rendererID, elementID, path}) => { - return false; // Not supported in fallback eval context - }, - viewElementSource: ({rendererID, elementID}) => { - return false; // Not supported in fallback eval context - }, -}; +/** + * 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 + */ + +import {evalScripts} from '../evalScripts'; window.addEventListener('message', event => { if (event.data?.source === 'react-devtools-content-script-eval') { const {scriptId, args, requestId} = event.data.payload; const response = {result: null, error: null}; try { - response.result = evalScripts[scriptId].apply(null, args); + if (!evalScripts[scriptId]) { + throw new Error(`No eval script with id "${scriptId}" exists.`); + } + response.result = evalScripts[scriptId].fn.apply(null, args); } catch (err) { response.error = err.message; } diff --git a/packages/react-devtools-extensions/src/contentScripts/proxy.js b/packages/react-devtools-extensions/src/contentScripts/proxy.js index 330bcbdb2a84b..f79392c852e5f 100644 --- a/packages/react-devtools-extensions/src/contentScripts/proxy.js +++ b/packages/react-devtools-extensions/src/contentScripts/proxy.js @@ -119,7 +119,7 @@ function connectPort() { } let evalRequestId = 0; -const evalRequestCallbacks = new Map(); +const evalRequestCallbacks = new Map(); chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { switch (msg?.source) { @@ -148,8 +148,11 @@ window.addEventListener('message', event => { const {requestId, response} = event.data.payload; const callback = evalRequestCallbacks.get(requestId); if (callback) { - callback(response); - evalRequestCallbacks.delete(requestId); + try { + callback(response); + } finally { + evalRequestCallbacks.delete(requestId); + } } } }); diff --git a/packages/react-devtools-extensions/src/evalScripts.js b/packages/react-devtools-extensions/src/evalScripts.js new file mode 100644 index 0000000000000..60757caaaa29a --- /dev/null +++ b/packages/react-devtools-extensions/src/evalScripts.js @@ -0,0 +1,112 @@ +/** + * 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 + */ + +export type EvalScriptIds = + | 'checkIfReactPresentInInspectedWindow' + | 'reload' + | 'setBrowserSelectionFromReact' + | 'setReactSelectionFromBrowser' + | 'viewAttributeSource' + | 'viewElementSource'; + +/* + .fn for fallback in Content Script context + .code for chrome.devtools.inspectedWindow.eval() +*/ +type EvalScriptEntry = { + fn: (...args: any[]) => any, + code: (...args: any[]) => string, +}; + +/* + Can not access `Developer Tools Console API` (e.g., inspect(), $0) in this context. + So some fallback functions are no-op or throw error. +*/ +export const evalScripts: {[key: EvalScriptIds]: EvalScriptEntry} = { + checkIfReactPresentInInspectedWindow: { + fn: () => + window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0, + code: () => + 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&' + + 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0', + }, + reload: { + fn: () => window.location.reload(), + code: () => 'window.location.reload();', + }, + setBrowserSelectionFromReact: { + fn: () => { + throw new Error('Not supported in fallback eval context'); + }, + code: () => + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + + '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' + + 'false', + }, + setReactSelectionFromBrowser: { + fn: () => { + throw new Error('Not supported in fallback eval context'); + }, + code: () => + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' + + 'false', + }, + viewAttributeSource: { + fn: ({rendererID, elementID, path}) => { + return false; // Not supported in fallback eval context + }, + code: ({rendererID, elementID, path}) => + '{' + // The outer block is important because it means we can declare local variables. + 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' + + JSON.stringify(rendererID) + + ');' + + 'if (renderer) {' + + ' const value = renderer.getElementAttributeByPath(' + + JSON.stringify(elementID) + + ',' + + JSON.stringify(path) + + ');' + + ' if (value) {' + + ' inspect(value);' + + ' true;' + + ' } else {' + + ' false;' + + ' }' + + '} else {' + + ' false;' + + '}' + + '}', + }, + viewElementSource: { + fn: ({rendererID, elementID}) => { + return false; // Not supported in fallback eval context + }, + code: ({rendererID, elementID}) => + '{' + // The outer block is important because it means we can declare local variables. + 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' + + JSON.stringify(rendererID) + + ');' + + 'if (renderer) {' + + ' const value = renderer.getElementSourceFunctionById(' + + JSON.stringify(elementID) + + ');' + + ' if (value) {' + + ' inspect(value);' + + ' true;' + + ' } else {' + + ' false;' + + ' }' + + '} else {' + + ' false;' + + '}' + + '}', + }, +}; diff --git a/packages/react-devtools-extensions/src/main/elementSelection.js b/packages/react-devtools-extensions/src/main/elementSelection.js index 92bead0340d15..315482e8eec54 100644 --- a/packages/react-devtools-extensions/src/main/elementSelection.js +++ b/packages/react-devtools-extensions/src/main/elementSelection.js @@ -7,9 +7,6 @@ export function setBrowserSelectionFromReact() { evalInInspectedWindow( 'setBrowserSelectionFromReact', [], - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + - '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' + - 'false', (didSelectionChange, evalError) => { if (evalError) { console.error(evalError); @@ -24,9 +21,6 @@ export function setReactSelectionFromBrowser(bridge) { evalInInspectedWindow( 'setReactSelectionFromBrowser', [], - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' + - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' + - 'false', (didSelectionChange, evalError) => { if (evalError) { console.error(evalError); diff --git a/packages/react-devtools-extensions/src/main/evalInInspectedWindow.js b/packages/react-devtools-extensions/src/main/evalInInspectedWindow.js index 8ef131284c387..27a339f4e8aee 100644 --- a/packages/react-devtools-extensions/src/main/evalInInspectedWindow.js +++ b/packages/react-devtools-extensions/src/main/evalInInspectedWindow.js @@ -1,9 +1,41 @@ +/** + * 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 + */ + +import type {EvalScriptIds} from '../evalScripts'; + +import {evalScripts} from '../evalScripts'; + +type ExceptionInfo = { + code: ?string, + description: ?string, + isError: boolean, + isException: boolean, + value: any, +}; + const EVAL_TIMEOUT = 1000 * 10; let evalRequestId = 0; -const evalRequestCallbacks = new Map(); +const evalRequestCallbacks = new Map< + number, + (value: {result: any, error: any}) => void, +>(); -function fallbackEvalInInspectedWindow(scriptId, args, code, callback) { +function fallbackEvalInInspectedWindow( + scriptId: EvalScriptIds, + args: any[], + callback: (value: any, exceptionInfo: ?ExceptionInfo) => void, +) { + if (!evalScripts[scriptId]) { + throw new Error(`No eval script with id "${scriptId}" exists.`); + } + const code = evalScripts[scriptId].code.apply(null, args); const tabId = chrome.devtools.inspectedWindow.tabId; const requestId = evalRequestId++; chrome.runtime.sendMessage({ @@ -48,7 +80,15 @@ function fallbackEvalInInspectedWindow(scriptId, args, code, callback) { }); } -export function evalInInspectedWindow(scriptId, args, code, callback) { +export function evalInInspectedWindow( + scriptId: EvalScriptIds, + args: any[], + callback: (value: any, exceptionInfo: ?ExceptionInfo) => void, +) { + if (!evalScripts[scriptId]) { + throw new Error(`No eval script with id "${scriptId}" exists.`); + } + const code = evalScripts[scriptId].code.apply(null, args); chrome.devtools.inspectedWindow.eval(code, (result, exceptionInfo) => { if (!exceptionInfo) { callback(result, exceptionInfo); @@ -56,11 +96,11 @@ export function evalInInspectedWindow(scriptId, args, code, callback) { } // If an exception (e.g. CSP Blocked) occurred, // fallback to the content script eval context - fallbackEvalInInspectedWindow(scriptId, args, code, callback); + fallbackEvalInInspectedWindow(scriptId, args, callback); }); } -function handleEvalInInspectedWindow({payload, source}) { +chrome.runtime.onMessage.addListener(({payload, source}) => { if (source === 'react-devtools-background') { switch (payload?.type) { case 'eval-in-inspected-window-response': { @@ -73,6 +113,4 @@ function handleEvalInInspectedWindow({payload, source}) { } } } -} - -chrome.runtime.onMessage.addListener(handleEvalInInspectedWindow); +}); diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 3659736fc0190..03f9c2b77331e 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -71,7 +71,7 @@ function createBridge() { bridge.addListener('reloadAppForProfiling', () => { localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true'); - evalInInspectedWindow('reload', [], 'window.location.reload();'); + evalInInspectedWindow('reload', []); }); bridge.addListener( diff --git a/packages/react-devtools-extensions/src/main/reactPolling.js b/packages/react-devtools-extensions/src/main/reactPolling.js index 7aec21aed1013..d4b3c22e66e62 100644 --- a/packages/react-devtools-extensions/src/main/reactPolling.js +++ b/packages/react-devtools-extensions/src/main/reactPolling.js @@ -29,7 +29,6 @@ export function startReactPolling( evalInInspectedWindow( 'checkIfReactPresentInInspectedWindow', [], - 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0', (pageHasReact, exceptionInfo) => { if (status === 'aborted') { onError( diff --git a/packages/react-devtools-extensions/src/main/sourceSelection.js b/packages/react-devtools-extensions/src/main/sourceSelection.js index cbb48e97323da..49ee99eaa4a37 100644 --- a/packages/react-devtools-extensions/src/main/sourceSelection.js +++ b/packages/react-devtools-extensions/src/main/sourceSelection.js @@ -4,26 +4,6 @@ export function viewAttributeSource(rendererID, elementID, path) { evalInInspectedWindow( 'viewAttributeSource', [{rendererID, elementID, path}], - '{' + // The outer block is important because it means we can declare local variables. - 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' + - JSON.stringify(rendererID) + - ');' + - 'if (renderer) {' + - ' const value = renderer.getElementAttributeByPath(' + - JSON.stringify(elementID) + - ',' + - JSON.stringify(path) + - ');' + - ' if (value) {' + - ' inspect(value);' + - ' true;' + - ' } else {' + - ' false;' + - ' }' + - '} else {' + - ' false;' + - '}' + - '}', (didInspect, evalError) => { if (evalError) { console.error(evalError); @@ -36,24 +16,6 @@ export function viewElementSource(rendererID, elementID) { evalInInspectedWindow( 'viewElementSource', [{rendererID, elementID}], - '{' + // The outer block is important because it means we can declare local variables. - 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' + - JSON.stringify(rendererID) + - ');' + - 'if (renderer) {' + - ' const value = renderer.getElementSourceFunctionById(' + - JSON.stringify(elementID) + - ');' + - ' if (value) {' + - ' inspect(value);' + - ' true;' + - ' } else {' + - ' false;' + - ' }' + - '} else {' + - ' false;' + - '}' + - '}', (didInspect, evalError) => { if (evalError) { console.error(evalError); From 0a383b7c2d93364f72a32cc5f5a7870f14fabec8 Mon Sep 17 00:00:00 2001 From: Yukimasa Funaoka Date: Thu, 27 Nov 2025 05:49:51 +0900 Subject: [PATCH 3/3] Add error handling for eval callback in DevTools content script --- .../src/contentScripts/proxy.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/react-devtools-extensions/src/contentScripts/proxy.js b/packages/react-devtools-extensions/src/contentScripts/proxy.js index f79392c852e5f..a4e7dd68241c5 100644 --- a/packages/react-devtools-extensions/src/contentScripts/proxy.js +++ b/packages/react-devtools-extensions/src/contentScripts/proxy.js @@ -147,12 +147,19 @@ window.addEventListener('message', event => { if (event.data?.source === 'react-devtools-content-script-eval-response') { const {requestId, response} = event.data.payload; const callback = evalRequestCallbacks.get(requestId); - if (callback) { - try { - callback(response); - } finally { - evalRequestCallbacks.delete(requestId); - } + try { + if (!callback) + throw new Error( + `No eval request callback for id "${requestId}" exists.`, + ); + callback(response); + } catch (e) { + console.warn( + 'React DevTools Content Script eval response error occurred:', + e, + ); + } finally { + evalRequestCallbacks.delete(requestId); } } });