@samitouri / QOS-React-1 / commits / 583e200332

[DevTools] Enable minimal support in pages with `sandbox` Content-Security-Policy (#35208)

Yukimasa Funaoka committed Jan 14, 2026 at 01:49 UTC 583e20033220bc17a590a25624f6251c6b101ff4
11 files changed +390 -54
packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js
+8
@@ -17,6 +17,14 @@ const contentScriptsToInject = [
17 runAt: 'document_end',
18 world: chrome.scripting.ExecutionWorld.ISOLATED,
19 },
20 + {
21 + id: '@react-devtools/fallback-eval-context',
22 + js: ['build/fallbackEvalContext.js'],
23 + matches: ['<all_urls>'],
24 + persistAcrossSessions: true,
25 + runAt: 'document_start',
26 + world: chrome.scripting.ExecutionWorld.MAIN,
27 + },
28 {
29 id: '@react-devtools/hook',
30 js: ['build/installHook.js'],
packages/react-devtools-extensions/src/background/messageHandlers.js
+52
@@ -97,6 +97,58 @@ export function handleDevToolsPageMessage(message) {
97
98 break;
99 }
100 +
101 + case 'eval-in-inspected-window': {
102 + const {
103 + payload: {tabId, requestId, scriptId, args},
104 + } = message;
105 +
106 + chrome.tabs
107 + .sendMessage(tabId, {
108 + source: 'devtools-page-eval',
109 + payload: {
110 + scriptId,
111 + args,
112 + },
113 + })
114 + .then(response => {
115 + if (!response) {
116 + chrome.runtime.sendMessage({
117 + source: 'react-devtools-background',
118 + payload: {
119 + type: 'eval-in-inspected-window-response',
120 + requestId,
121 + result: null,
122 + error: 'No response from content script',
123 + },
124 + });
125 + return;
126 + }
127 + const {result, error} = response;
128 + chrome.runtime.sendMessage({
129 + source: 'react-devtools-background',
130 + payload: {
131 + type: 'eval-in-inspected-window-response',
132 + requestId,
133 + result,
134 + error,
135 + },
136 + });
137 + })
138 + .catch(error => {
139 + chrome.runtime.sendMessage({
140 + source: 'react-devtools-background',
141 + payload: {
142 + type: 'eval-in-inspected-window-response',
143 + requestId,
144 + result: null,
145 + error: error?.message || String(error),
146 + },
147 + });
148 + });
149 +
150 + break;
151 + }
152 }
153 }
154
packages/react-devtools-extensions/src/contentScripts/fallbackEvalContext.js new
+35
@@ -0,0 +1,35 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {evalScripts} from '../evalScripts';
11 +
12 +window.addEventListener('message', event => {
13 + if (event.data?.source === 'react-devtools-content-script-eval') {
14 + const {scriptId, args, requestId} = event.data.payload;
15 + const response = {result: null, error: null};
16 + try {
17 + if (!evalScripts[scriptId]) {
18 + throw new Error(`No eval script with id "${scriptId}" exists.`);
19 + }
20 + response.result = evalScripts[scriptId].fn.apply(null, args);
21 + } catch (err) {
22 + response.error = err.message;
23 + }
24 + window.postMessage(
25 + {
26 + source: 'react-devtools-content-script-eval-response',
27 + payload: {
28 + requestId,
29 + response,
30 + },
31 + },
32 + '*',
33 + );
34 + }
35 +});
packages/react-devtools-extensions/src/contentScripts/proxy.js
+46
@@ -117,3 +117,49 @@ function connectPort() {
117 // $FlowFixMe[incompatible-use]
118 port.onDisconnect.addListener(handleDisconnect);
119 }
120 +
121 +let evalRequestId = 0;
122 +const evalRequestCallbacks = new Map<number, Function>();
123 +
124 +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
125 + switch (msg?.source) {
126 + case 'devtools-page-eval': {
127 + const {scriptId, args} = msg.payload;
128 + const requestId = evalRequestId++;
129 + window.postMessage(
130 + {
131 + source: 'react-devtools-content-script-eval',
132 + payload: {
133 + requestId,
134 + scriptId,
135 + args,
136 + },
137 + },
138 + '*',
139 + );
140 + evalRequestCallbacks.set(requestId, sendResponse);
141 + return true; // Indicate we will respond asynchronously
142 + }
143 + }
144 +});
145 +
146 +window.addEventListener('message', event => {
147 + if (event.data?.source === 'react-devtools-content-script-eval-response') {
148 + const {requestId, response} = event.data.payload;
149 + const callback = evalRequestCallbacks.get(requestId);
150 + try {
151 + if (!callback)
152 + throw new Error(
153 + `No eval request callback for id "${requestId}" exists.`,
154 + );
155 + callback(response);
156 + } catch (e) {
157 + console.warn(
158 + 'React DevTools Content Script eval response error occurred:',
159 + e,
160 + );
161 + } finally {
162 + evalRequestCallbacks.delete(requestId);
163 + }
164 + }
165 +});
packages/react-devtools-extensions/src/evalScripts.js new
+112
@@ -0,0 +1,112 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export type EvalScriptIds =
11 + | 'checkIfReactPresentInInspectedWindow'
12 + | 'reload'
13 + | 'setBrowserSelectionFromReact'
14 + | 'setReactSelectionFromBrowser'
15 + | 'viewAttributeSource'
16 + | 'viewElementSource';
17 +
18 +/*
19 + .fn for fallback in Content Script context
20 + .code for chrome.devtools.inspectedWindow.eval()
21 +*/
22 +type EvalScriptEntry = {
23 + fn: (...args: any[]) => any,
24 + code: (...args: any[]) => string,
25 +};
26 +
27 +/*
28 + Can not access `Developer Tools Console API` (e.g., inspect(), $0) in this context.
29 + So some fallback functions are no-op or throw error.
30 +*/
31 +export const evalScripts: {[key: EvalScriptIds]: EvalScriptEntry} = {
32 + checkIfReactPresentInInspectedWindow: {
33 + fn: () =>
34 + window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&
35 + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0,
36 + code: () =>
37 + 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ &&' +
38 + 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
39 + },
40 + reload: {
41 + fn: () => window.location.reload(),
42 + code: () => 'window.location.reload();',
43 + },
44 + setBrowserSelectionFromReact: {
45 + fn: () => {
46 + throw new Error('Not supported in fallback eval context');
47 + },
48 + code: () =>
49 + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
50 + '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
51 + 'false',
52 + },
53 + setReactSelectionFromBrowser: {
54 + fn: () => {
55 + throw new Error('Not supported in fallback eval context');
56 + },
57 + code: () =>
58 + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
59 + '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
60 + 'false',
61 + },
62 + viewAttributeSource: {
63 + fn: ({rendererID, elementID, path}) => {
64 + return false; // Not supported in fallback eval context
65 + },
66 + code: ({rendererID, elementID, path}) =>
67 + '{' + // The outer block is important because it means we can declare local variables.
68 + 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
69 + JSON.stringify(rendererID) +
70 + ');' +
71 + 'if (renderer) {' +
72 + ' const value = renderer.getElementAttributeByPath(' +
73 + JSON.stringify(elementID) +
74 + ',' +
75 + JSON.stringify(path) +
76 + ');' +
77 + ' if (value) {' +
78 + ' inspect(value);' +
79 + ' true;' +
80 + ' } else {' +
81 + ' false;' +
82 + ' }' +
83 + '} else {' +
84 + ' false;' +
85 + '}' +
86 + '}',
87 + },
88 + viewElementSource: {
89 + fn: ({rendererID, elementID}) => {
90 + return false; // Not supported in fallback eval context
91 + },
92 + code: ({rendererID, elementID}) =>
93 + '{' + // The outer block is important because it means we can declare local variables.
94 + 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
95 + JSON.stringify(rendererID) +
96 + ');' +
97 + 'if (renderer) {' +
98 + ' const value = renderer.getElementSourceFunctionById(' +
99 + JSON.stringify(elementID) +
100 + ');' +
101 + ' if (value) {' +
102 + ' inspect(value);' +
103 + ' true;' +
104 + ' } else {' +
105 + ' false;' +
106 + ' }' +
107 + '} else {' +
108 + ' false;' +
109 + '}' +
110 + '}',
111 + },
112 +};
packages/react-devtools-extensions/src/main/elementSelection.js
+7 -9
@@ -1,13 +1,12 @@
1 -/* global chrome */
1 +import {evalInInspectedWindow} from './evalInInspectedWindow';
2
3 export function setBrowserSelectionFromReact() {
4 // This is currently only called on demand when you press "view DOM".
5 // In the future, if Chrome adds an inspect() that doesn't switch tabs,
6 // we could make this happen automatically when you select another component.
7 - chrome.devtools.inspectedWindow.eval(
8 - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
9 - '(inspect(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0), true) :' +
10 - 'false',
7 + evalInInspectedWindow(
8 + 'setBrowserSelectionFromReact',
9 + [],
10 (didSelectionChange, evalError) => {
11 if (evalError) {
12 console.error(evalError);
@@ -19,10 +18,9 @@ export function setBrowserSelectionFromReact() {
18 export function setReactSelectionFromBrowser(bridge) {
19 // When the user chooses a different node in the browser Elements tab,
20 // copy it over to the hook object so that we can sync the selection.
22 - chrome.devtools.inspectedWindow.eval(
23 - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 !== $0) ?' +
24 - '(window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = $0, true) :' +
25 - 'false',
21 + evalInInspectedWindow(
22 + 'setReactSelectionFromBrowser',
23 + [],
24 (didSelectionChange, evalError) => {
25 if (evalError) {
26 console.error(evalError);
packages/react-devtools-extensions/src/main/evalInInspectedWindow.js new
+116
@@ -0,0 +1,116 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {EvalScriptIds} from '../evalScripts';
11 +
12 +import {evalScripts} from '../evalScripts';
13 +
14 +type ExceptionInfo = {
15 + code: ?string,
16 + description: ?string,
17 + isError: boolean,
18 + isException: boolean,
19 + value: any,
20 +};
21 +
22 +const EVAL_TIMEOUT = 1000 * 10;
23 +
24 +let evalRequestId = 0;
25 +const evalRequestCallbacks = new Map<
26 + number,
27 + (value: {result: any, error: any}) => void,
28 +>();
29 +
30 +function fallbackEvalInInspectedWindow(
31 + scriptId: EvalScriptIds,
32 + args: any[],
33 + callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
34 +) {
35 + if (!evalScripts[scriptId]) {
36 + throw new Error(`No eval script with id "${scriptId}" exists.`);
37 + }
38 + const code = evalScripts[scriptId].code.apply(null, args);
39 + const tabId = chrome.devtools.inspectedWindow.tabId;
40 + const requestId = evalRequestId++;
41 + chrome.runtime.sendMessage({
42 + source: 'devtools-page',
43 + payload: {
44 + type: 'eval-in-inspected-window',
45 + tabId,
46 + requestId,
47 + scriptId,
48 + args,
49 + },
50 + });
51 + const timeout = setTimeout(() => {
52 + evalRequestCallbacks.delete(requestId);
53 + if (callback) {
54 + callback(null, {
55 + code,
56 + description:
57 + 'Timed out while waiting for eval response from the inspected window.',
58 + isError: true,
59 + isException: false,
60 + value: undefined,
61 + });
62 + }
63 + }, EVAL_TIMEOUT);
64 + evalRequestCallbacks.set(requestId, ({result, error}) => {
65 + clearTimeout(timeout);
66 + evalRequestCallbacks.delete(requestId);
67 + if (callback) {
68 + if (error) {
69 + callback(null, {
70 + code,
71 + description: undefined,
72 + isError: false,
73 + isException: true,
74 + value: error,
75 + });
76 + return;
77 + }
78 + callback(result, null);
79 + }
80 + });
81 +}
82 +
83 +export function evalInInspectedWindow(
84 + scriptId: EvalScriptIds,
85 + args: any[],
86 + callback: (value: any, exceptionInfo: ?ExceptionInfo) => void,
87 +) {
88 + if (!evalScripts[scriptId]) {
89 + throw new Error(`No eval script with id "${scriptId}" exists.`);
90 + }
91 + const code = evalScripts[scriptId].code.apply(null, args);
92 + chrome.devtools.inspectedWindow.eval(code, (result, exceptionInfo) => {
93 + if (!exceptionInfo) {
94 + callback(result, exceptionInfo);
95 + return;
96 + }
97 + // If an exception (e.g. CSP Blocked) occurred,
98 + // fallback to the content script eval context
99 + fallbackEvalInInspectedWindow(scriptId, args, callback);
100 + });
101 +}
102 +
103 +chrome.runtime.onMessage.addListener(({payload, source}) => {
104 + if (source === 'react-devtools-background') {
105 + switch (payload?.type) {
106 + case 'eval-in-inspected-window-response': {
107 + const {requestId, result, error} = payload;
108 + const callback = evalRequestCallbacks.get(requestId);
109 + if (callback) {
110 + callback({result, error});
111 + }
112 + break;
113 + }
114 + }
115 + }
116 +});
packages/react-devtools-extensions/src/main/index.js
+2 -1
@@ -32,6 +32,7 @@ import {
32 } from './elementSelection';
33 import {viewAttributeSource} from './sourceSelection';
34
35 +import {evalInInspectedWindow} from './evalInInspectedWindow';
36 import {startReactPolling} from './reactPolling';
37 import {cloneStyleTags} from './cloneStyleTags';
38 import fetchFileWithCaching from './fetchFileWithCaching';
@@ -70,7 +71,7 @@ function createBridge() {
71
72 bridge.addListener('reloadAppForProfiling', () => {
73 localStorageSetItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
73 - chrome.devtools.inspectedWindow.eval('window.location.reload();');
74 + evalInInspectedWindow('reload', []);
75 });
76
77 bridge.addListener(
packages/react-devtools-extensions/src/main/reactPolling.js
+4 -3
@@ -1,4 +1,4 @@
1 -/* global chrome */
1 +import {evalInInspectedWindow} from './evalInInspectedWindow';
2
3 class CouldNotFindReactOnThePageError extends Error {
4 constructor() {
@@ -26,8 +26,9 @@ export function startReactPolling(
26
27 // This function will call onSuccess only if React was found and polling is not aborted, onError will be called for every other case
28 function checkIfReactPresentInInspectedWindow(onSuccess, onError) {
29 - chrome.devtools.inspectedWindow.eval(
30 - 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
29 + evalInInspectedWindow(
30 + 'checkIfReactPresentInInspectedWindow',
31 + [],
32 (pageHasReact, exceptionInfo) => {
33 if (status === 'aborted') {
34 onError(
packages/react-devtools-extensions/src/main/sourceSelection.js
+7 -41
@@ -1,27 +1,9 @@
1 -/* global chrome */
1 +import {evalInInspectedWindow} from './evalInInspectedWindow';
2
3 export function viewAttributeSource(rendererID, elementID, path) {
4 - chrome.devtools.inspectedWindow.eval(
5 - '{' + // The outer block is important because it means we can declare local variables.
6 - 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
7 - JSON.stringify(rendererID) +
8 - ');' +
9 - 'if (renderer) {' +
10 - ' const value = renderer.getElementAttributeByPath(' +
11 - JSON.stringify(elementID) +
12 - ',' +
13 - JSON.stringify(path) +
14 - ');' +
15 - ' if (value) {' +
16 - ' inspect(value);' +
17 - ' true;' +
18 - ' } else {' +
19 - ' false;' +
20 - ' }' +
21 - '} else {' +
22 - ' false;' +
23 - '}' +
24 - '}',
4 + evalInInspectedWindow(
5 + 'viewAttributeSource',
6 + [{rendererID, elementID, path}],
7 (didInspect, evalError) => {
8 if (evalError) {
9 console.error(evalError);
@@ -31,25 +13,9 @@ export function viewAttributeSource(rendererID, elementID, path) {
13 }
14
15 export function viewElementSource(rendererID, elementID) {
34 - chrome.devtools.inspectedWindow.eval(
35 - '{' + // The outer block is important because it means we can declare local variables.
36 - 'const renderer = window.__REACT_DEVTOOLS_GLOBAL_HOOK__.rendererInterfaces.get(' +
37 - JSON.stringify(rendererID) +
38 - ');' +
39 - 'if (renderer) {' +
40 - ' const value = renderer.getElementSourceFunctionById(' +
41 - JSON.stringify(elementID) +
42 - ');' +
43 - ' if (value) {' +
44 - ' inspect(value);' +
45 - ' true;' +
46 - ' } else {' +
47 - ' false;' +
48 - ' }' +
49 - '} else {' +
50 - ' false;' +
51 - '}' +
52 - '}',
16 + evalInInspectedWindow(
17 + 'viewElementSource',
18 + [{rendererID, elementID}],
19 (didInspect, evalError) => {
20 if (evalError) {
21 console.error(evalError);
packages/react-devtools-extensions/webpack.config.js
+1
@@ -69,6 +69,7 @@ module.exports = {
69 backend: './src/backend.js',
70 background: './src/background/index.js',
71 backendManager: './src/contentScripts/backendManager.js',
72 + fallbackEvalContext: './src/contentScripts/fallbackEvalContext.js',
73 fileFetcher: './src/contentScripts/fileFetcher.js',
74 main: './src/main/index.js',
75 panel: './src/panel.js',