@samitouri / QOS-React / commits / 6dbccb9249

[DevTools] upgrade to Manifest V3 (#25145)

## Summary resolves #24522 To upgrade to Manifest V3, one of the biggest issue is that we are no longer allowed to add a script element with code in textContent so that it would run synchronously. It's necessary for us because we need to inject a global hook for react reconciler to detect whether devtools exist. To do that, we'll leverage a new API `chrome.scripting.registerContentScripts` in V3. Particularly, we rely on the "world" option (added in Chrome v102 [commit](https://chromium.googlesource.com/chromium/src/+/e5ad3451c17b21341b0b9019b074801c44c92c9f)) to run it in the "main world" on the page. This PR also renames a few content script files so that it's easier to tell them apart from other extension scripts and understand the purpose of each of them. Manifest V3 is not yet ready for Firefox, so we need to keep some code for compatibility. ## How did you test this change? `yarn build:chrome && yarn test:chrome` `yarn build:edge && yarn test:edge` `yarn build:firefox && yarn test:firefox`

Mengdi Chen committed Oct 21, 2022 at 22:52 UTC 6dbccb92493c3a4fea14b6832ceb4ac42e6330c1
14 files changed +268 -152
packages/react-devtools-extensions/chrome/manifest.json
+27 -16
@@ -1,17 +1,17 @@
1 {
2 - "manifest_version": 2,
2 + "manifest_version": 3,
3 "name": "React Developer Tools",
4 "description": "Adds React debugging tools to the Chrome Developer Tools.",
5 "version": "4.26.1",
6 "version_name": "4.26.1",
7 - "minimum_chrome_version": "60",
7 + "minimum_chrome_version": "88",
8 "icons": {
9 "16": "icons/16-production.png",
10 "32": "icons/32-production.png",
11 "48": "icons/48-production.png",
12 "128": "icons/128-production.png"
13 },
14 - "browser_action": {
14 + "action": {
15 "default_icon": {
16 "16": "icons/16-disabled.png",
17 "32": "icons/32-disabled.png",
@@ -21,23 +21,34 @@
21 "default_popup": "popups/disabled.html"
22 },
23 "devtools_page": "main.html",
24 - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
24 + "content_security_policy": {
25 + "extension_pages": "script-src 'self'; object-src 'self'"
26 + },
27 "web_accessible_resources": [
26 - "main.html",
27 - "panel.html",
28 - "build/react_devtools_backend.js",
29 - "build/renderer.js"
28 + {
29 + "resources": [
30 + "main.html",
31 + "panel.html",
32 + "build/react_devtools_backend.js",
33 + "build/proxy.js",
34 + "build/renderer.js",
35 + "build/installHook.js"
36 + ],
37 + "matches": [
38 + "<all_urls>"
39 + ],
40 + "extension_ids": []
41 + }
42 ],
43 "background": {
32 - "scripts": [
33 - "build/background.js"
34 - ],
35 - "persistent": false
44 + "service_worker": "build/background.js"
45 },
46 "permissions": [
38 - "file:///*",
39 - "http://*/*",
40 - "https://*/*"
47 + "storage",
48 + "scripting"
49 + ],
50 + "host_permissions": [
51 + "<all_urls>"
52 ],
53 "content_scripts": [
54 {
@@ -45,7 +56,7 @@
56 "<all_urls>"
57 ],
58 "js": [
48 - "build/injectGlobalHook.js"
59 + "build/prepareInjection.js"
60 ],
61 "run_at": "document_start"
62 }
packages/react-devtools-extensions/edge/manifest.json
+27 -16
@@ -1,17 +1,17 @@
1 {
2 - "manifest_version": 2,
2 + "manifest_version": 3,
3 "name": "React Developer Tools",
4 "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
5 "version": "4.26.1",
6 "version_name": "4.26.1",
7 - "minimum_chrome_version": "60",
7 + "minimum_chrome_version": "88",
8 "icons": {
9 "16": "icons/16-production.png",
10 "32": "icons/32-production.png",
11 "48": "icons/48-production.png",
12 "128": "icons/128-production.png"
13 },
14 - "browser_action": {
14 + "action": {
15 "default_icon": {
16 "16": "icons/16-disabled.png",
17 "32": "icons/32-disabled.png",
@@ -21,23 +21,34 @@
21 "default_popup": "popups/disabled.html"
22 },
23 "devtools_page": "main.html",
24 - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
24 + "content_security_policy": {
25 + "extension_pages": "script-src 'self'; object-src 'self'"
26 + },
27 "web_accessible_resources": [
26 - "main.html",
27 - "panel.html",
28 - "build/react_devtools_backend.js",
29 - "build/renderer.js"
28 + {
29 + "resources": [
30 + "main.html",
31 + "panel.html",
32 + "build/react_devtools_backend.js",
33 + "build/proxy.js",
34 + "build/renderer.js",
35 + "build/installHook.js"
36 + ],
37 + "matches": [
38 + "<all_urls>"
39 + ],
40 + "extension_ids": []
41 + }
42 ],
43 "background": {
32 - "scripts": [
33 - "build/background.js"
34 - ],
35 - "persistent": false
44 + "service_worker": "build/background.js"
45 },
46 "permissions": [
38 - "file:///*",
39 - "http://*/*",
40 - "https://*/*"
47 + "storage",
48 + "scripting"
49 + ],
50 + "host_permissions": [
51 + "<all_urls>"
52 ],
53 "content_scripts": [
54 {
@@ -45,7 +56,7 @@
56 "<all_urls>"
57 ],
58 "js": [
48 - "build/injectGlobalHook.js"
59 + "build/prepareInjection.js"
60 ],
61 "run_at": "document_start"
62 }
packages/react-devtools-extensions/firefox/manifest.json
+4 -2
@@ -31,7 +31,9 @@
31 "main.html",
32 "panel.html",
33 "build/react_devtools_backend.js",
34 - "build/renderer.js"
34 + "build/proxy.js",
35 + "build/renderer.js",
36 + "build/installHook.js"
37 ],
38 "background": {
39 "scripts": [
@@ -50,7 +52,7 @@
52 "<all_urls>"
53 ],
54 "js": [
53 - "build/injectGlobalHook.js"
55 + "build/prepareInjection.js"
56 ],
57 "run_at": "document_start"
58 }
packages/react-devtools-extensions/src/background.js
+43 -18
@@ -2,9 +2,33 @@
2
3 'use strict';
4
5 +import {IS_FIREFOX} from './utils';
6 +
7 const ports = {};
8
7 -const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0;
9 +if (!IS_FIREFOX) {
10 + // Manifest V3 method of injecting content scripts (not yet supported in Firefox)
11 + // Note: the "world" option in registerContentScripts is only available in Chrome v102+
12 + // It's critical since it allows us to directly run scripts on the "main" world on the page
13 + // "document_start" allows it to run before the page's scripts
14 + // so the hook can be detected by react reconciler
15 + chrome.scripting.registerContentScripts([
16 + {
17 + id: 'hook',
18 + matches: ['<all_urls>'],
19 + js: ['build/installHook.js'],
20 + runAt: 'document_start',
21 + world: chrome.scripting.ExecutionWorld.MAIN,
22 + },
23 + {
24 + id: 'renderer',
25 + matches: ['<all_urls>'],
26 + js: ['build/renderer.js'],
27 + runAt: 'document_start',
28 + world: chrome.scripting.ExecutionWorld.MAIN,
29 + },
30 + ]);
31 +}
32
33 chrome.runtime.onConnect.addListener(function(port) {
34 let tab = null;
@@ -12,7 +36,7 @@ chrome.runtime.onConnect.addListener(function(port) {
36 if (isNumeric(port.name)) {
37 tab = port.name;
38 name = 'devtools';
15 - installContentScript(+port.name);
39 + installProxy(+port.name);
40 } else {
41 tab = port.sender.tab.id;
42 name = 'content-script';
@@ -35,12 +59,15 @@ function isNumeric(str: string): boolean {
59 return +str + '' === str;
60 }
61
38 -function installContentScript(tabId: number) {
39 - chrome.tabs.executeScript(
40 - tabId,
41 - {file: '/build/contentScript.js'},
42 - function() {},
43 - );
62 +function installProxy(tabId: number) {
63 + if (IS_FIREFOX) {
64 + chrome.tabs.executeScript(tabId, {file: '/build/proxy.js'}, function() {});
65 + } else {
66 + chrome.scripting.executeScript({
67 + target: {tabId: tabId},
68 + files: ['/build/proxy.js'],
69 + });
70 + }
71 }
72
73 function doublePipe(one, two) {
@@ -63,18 +90,19 @@ function doublePipe(one, two) {
90 }
91
92 function setIconAndPopup(reactBuildType, tabId) {
66 - chrome.browserAction.setIcon({
93 + const action = IS_FIREFOX ? chrome.browserAction : chrome.action;
94 + action.setIcon({
95 tabId: tabId,
96 path: {
69 - '16': 'icons/16-' + reactBuildType + '.png',
70 - '32': 'icons/32-' + reactBuildType + '.png',
71 - '48': 'icons/48-' + reactBuildType + '.png',
72 - '128': 'icons/128-' + reactBuildType + '.png',
97 + '16': chrome.runtime.getURL(`icons/16-${reactBuildType}.png`),
98 + '32': chrome.runtime.getURL(`icons/32-${reactBuildType}.png`),
99 + '48': chrome.runtime.getURL(`icons/48-${reactBuildType}.png`),
100 + '128': chrome.runtime.getURL(`icons/128-${reactBuildType}.png`),
101 },
102 });
75 - chrome.browserAction.setPopup({
103 + action.setPopup({
104 tabId: tabId,
77 - popup: 'popups/' + reactBuildType + '.html',
105 + popup: chrome.runtime.getURL(`popups/${reactBuildType}.html`),
106 });
107 }
108
@@ -123,9 +151,6 @@ chrome.runtime.onMessage.addListener((request, sender) => {
151 // This is sent from the hook content script.
152 // It tells us a renderer has attached.
153 if (request.hasDetectedReact) {
126 - // We use browserAction instead of pageAction because this lets us
127 - // display a custom default popup when React is *not* detected.
128 - // It is specified in the manifest.
154 setIconAndPopup(request.reactBuildType, id);
155 } else {
156 switch (request.payload?.type) {
packages/react-devtools-extensions/src/contentScripts/installHook.js new
+25
@@ -0,0 +1,25 @@
1 +import {installHook} from 'react-devtools-shared/src/hook';
2 +
3 +// avoid double execution
4 +if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
5 + installHook(window);
6 +
7 + // detect react
8 + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on('renderer', function({
9 + reactBuildType,
10 + }) {
11 + window.postMessage(
12 + {
13 + source: 'react-devtools-detector',
14 + reactBuildType,
15 + },
16 + '*',
17 + );
18 + });
19 +
20 + // save native values
21 + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeObjectCreate = Object.create;
22 + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeMap = Map;
23 + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeWeakMap = WeakMap;
24 + window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeSet = Set;
25 +}
packages/react-devtools-extensions/src/contentScripts/prepareInjection.js renamed
+32 -42
@@ -1,11 +1,19 @@
1 /* global chrome */
2
3 import nullthrows from 'nullthrows';
4 -import {installHook} from 'react-devtools-shared/src/hook';
4 import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants';
5 import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
6 +import {IS_FIREFOX} from '../utils';
7 +
8 +function injectScriptSync(src) {
9 + let code = '';
10 + const request = new XMLHttpRequest();
11 + request.addEventListener('load', function() {
12 + code = this.responseText;
13 + });
14 + request.open('GET', src, false);
15 + request.send();
16
8 -function injectCode(code) {
17 const script = document.createElement('script');
18 script.textContent = code;
19
@@ -15,6 +23,15 @@ function injectCode(code) {
23 nullthrows(script.parentNode).removeChild(script);
24 }
25
26 +function injectScriptAsync(src) {
27 + const script = document.createElement('script');
28 + script.src = src;
29 + script.onload = function() {
30 + script.remove();
31 + };
32 + nullthrows(document.documentElement).appendChild(script);
33 +}
34 +
35 let lastDetectionResult;
36
37 // We want to detect when a renderer attaches, and notify the "background page"
@@ -77,10 +94,9 @@ window.addEventListener('message', function onMessage({data, source}) {
94 }
95 break;
96 case 'react-devtools-inject-backend':
80 - const script = document.createElement('script');
81 - script.src = chrome.runtime.getURL('build/react_devtools_backend.js');
82 - document.documentElement.appendChild(script);
83 - script.parentNode.removeChild(script);
97 + injectScriptAsync(
98 + chrome.runtime.getURL('build/react_devtools_backend.js'),
99 + );
100 break;
101 }
102 });
@@ -96,38 +112,18 @@ window.addEventListener('pageshow', function({target}) {
112 chrome.runtime.sendMessage(lastDetectionResult);
113 });
114
99 -const detectReact = `
100 -window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on('renderer', function({reactBuildType}) {
101 - window.postMessage({
102 - source: 'react-devtools-detector',
103 - reactBuildType,
104 - }, '*');
105 -});
106 -`;
107 -const saveNativeValues = `
108 -window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeObjectCreate = Object.create;
109 -window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeMap = Map;
110 -window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeWeakMap = WeakMap;
111 -window.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeSet = Set;
112 -`;
115 +// We create a "sync" script tag to page to inject the global hook on Manifest V2 extensions.
116 +// To comply with the new security policy in V3, we use chrome.scripting.registerContentScripts instead (see background.js).
117 +// However, the new API only works for Chrome v102+.
118 +// We insert a "async" script tag as a fallback for older versions.
119 +// It has known issues if JS on the page is faster than the extension.
120 +// Users will see a notice in components tab when that happens (see <Tree>).
121 +// For Firefox, V3 is not ready, so sync injection is still the best approach.
122 +const injectScript = IS_FIREFOX ? injectScriptSync : injectScriptAsync;
123
124 // If we have just reloaded to profile, we need to inject the renderer interface before the app loads.
125 if (sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true') {
116 - const rendererURL = chrome.runtime.getURL('build/renderer.js');
117 - let rendererCode;
118 -
119 - // We need to inject in time to catch the initial mount.
120 - // This means we need to synchronously read the renderer code itself,
121 - // and synchronously inject it into the page.
122 - // There are very few ways to actually do this.
123 - // This seems to be the best approach.
124 - const request = new XMLHttpRequest();
125 - request.addEventListener('load', function() {
126 - rendererCode = this.responseText;
127 - });
128 - request.open('GET', rendererURL, false);
129 - request.send();
130 - injectCode(rendererCode);
126 + injectScript(chrome.runtime.getURL('build/renderer.js'));
127 }
128
129 // Inject a __REACT_DEVTOOLS_GLOBAL_HOOK__ global for React to interact with.
@@ -138,13 +134,7 @@ if (sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true') {
134 switch (document.contentType) {
135 case 'text/html':
136 case 'application/xhtml+xml': {
141 - injectCode(
142 - ';(' +
143 - installHook.toString() +
144 - '(window))' +
145 - saveNativeValues +
146 - detectReact,
147 - );
137 + injectScript(chrome.runtime.getURL('build/installHook.js'));
138 break;
139 }
140 }
packages/react-devtools-extensions/src/contentScripts/proxy.js renamed
packages/react-devtools-extensions/src/contentScripts/renderer.js new
+33
@@ -0,0 +1,33 @@
1 +/**
2 + * In order to support reload-and-profile functionality, the renderer needs to be injected before any other scripts.
3 + * Since it is a complex file (with imports) we can't just toString() it like we do with the hook itself,
4 + * So this entry point (one of the web_accessible_resources) provides a way to eagerly inject it.
5 + * The hook will look for the presence of a global __REACT_DEVTOOLS_ATTACH__ and attach an injected renderer early.
6 + * The normal case (not a reload-and-profile) will not make use of this entry point though.
7 + *
8 + * @flow
9 + */
10 +
11 +import {attach} from 'react-devtools-shared/src/backend/renderer';
12 +import {SESSION_STORAGE_RELOAD_AND_PROFILE_KEY} from 'react-devtools-shared/src/constants';
13 +import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
14 +
15 +if (
16 + sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true' &&
17 + !window.hasOwnProperty('__REACT_DEVTOOLS_ATTACH__')
18 +) {
19 + Object.defineProperty(
20 + window,
21 + '__REACT_DEVTOOLS_ATTACH__',
22 + ({
23 + enumerable: false,
24 + // This property needs to be configurable to allow third-party integrations
25 + // to attach their own renderer. Note that using third-party integrations
26 + // is not officially supported. Use at your own risk.
27 + configurable: true,
28 + get() {
29 + return attach;
30 + },
31 + }: Object),
32 + );
33 +}
packages/react-devtools-extensions/src/renderer.js deleted
-26
@@ -1,26 +0,0 @@
1 -/**
2 - * In order to support reload-and-profile functionality, the renderer needs to be injected before any other scripts.
3 - * Since it is a complex file (with imports) we can't just toString() it like we do with the hook itself,
4 - * So this entry point (one of the web_accessible_resources) provides a way to eagerly inject it.
5 - * The hook will look for the presence of a global __REACT_DEVTOOLS_ATTACH__ and attach an injected renderer early.
6 - * The normal case (not a reload-and-profile) will not make use of this entry point though.
7 - *
8 - * @flow
9 - */
10 -
11 -import {attach} from 'react-devtools-shared/src/backend/renderer';
12 -
13 -Object.defineProperty(
14 - window,
15 - '__REACT_DEVTOOLS_ATTACH__',
16 - ({
17 - enumerable: false,
18 - // This property needs to be configurable to allow third-party integrations
19 - // to attach their own renderer. Note that using third-party integrations
20 - // is not officially supported. Use at your own risk.
21 - configurable: true,
22 - get() {
23 - return attach;
24 - },
25 - }: Object),
26 -);
packages/react-devtools-extensions/src/utils.js
+3 -3
@@ -2,9 +2,9 @@
2
3 import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
4
5 -const IS_EDGE = navigator.userAgent.indexOf('Edg') >= 0;
6 -const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0;
7 -const IS_CHROME = IS_EDGE === false && IS_FIREFOX === false;
5 +export const IS_EDGE = navigator.userAgent.indexOf('Edg') >= 0;
6 +export const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0;
7 +export const IS_CHROME = IS_EDGE === false && IS_FIREFOX === false;
8
9 export type BrowserName = 'Chrome' | 'Firefox' | 'Edge';
10
packages/react-devtools-extensions/webpack.config.js
+4 -3
@@ -51,11 +51,12 @@ module.exports = {
51 devtool: __DEV__ ? 'cheap-module-eval-source-map' : false,
52 entry: {
53 background: './src/background.js',
54 - contentScript: './src/contentScript.js',
55 - injectGlobalHook: './src/injectGlobalHook.js',
54 main: './src/main.js',
55 panel: './src/panel.js',
58 - renderer: './src/renderer.js',
56 + proxy: './src/contentScripts/proxy.js',
57 + prepareInjection: './src/contentScripts/prepareInjection.js',
58 + renderer: './src/contentScripts/renderer.js',
59 + installHook: './src/contentScripts/installHook.js',
60 },
61 output: {
62 path: __dirname + '/build',
packages/react-devtools-shared/src/devtools/views/Components/Tree.css
+19
@@ -79,3 +79,22 @@
79 .WarningIcon {
80 color: var(--color-console-warning-icon);
81 }
82 +
83 +.ZeroElementsNotice {
84 + padding-left: 1em;
85 + opacity: 0;
86 + animation: fadeIn 0.5s forwards;
87 + animation-delay: 2s;
88 +}
89 +@keyframes fadeIn {
90 + 0% {
91 + opacity: 0;
92 + }
93 + 100% {
94 + opacity: 1;
95 + }
96 +}
97 +
98 +.Link {
99 + color: var(--color-button-active);
100 +}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Components/Tree.js
+46 -26
@@ -344,6 +344,22 @@ export default function Tree(props: Props): React.Node {
344 clearErrorsAndWarningsAPI({bridge, store});
345 };
346
347 + const zeroElementsNotice = (
348 + <div className={styles.ZeroElementsNotice}>
349 + <p>Loading React Element Tree...</p>
350 + <p>
351 + If this seems stuck, please follow the{' '}
352 + <a
353 + className={styles.Link}
354 + href="https://github.com/facebook/react/tree/main/packages/react-devtools#the-issue-with-chrome-v101-and-earlier-versions"
355 + target="_blank">
356 + troubleshooting instructions
357 + </a>
358 + .
359 + </p>
360 + </div>
361 + );
362 +
363 return (
364 <TreeFocusedContext.Provider value={treeFocused}>
365 <div className={styles.Tree} ref={treeRef}>
@@ -398,32 +414,36 @@ export default function Tree(props: Props): React.Node {
414 </Fragment>
415 )}
416 </div>
401 - <div
402 - className={styles.AutoSizerWrapper}
403 - onBlur={handleBlur}
404 - onFocus={handleFocus}
405 - onKeyPress={handleKeyPress}
406 - onMouseMove={handleMouseMove}
407 - onMouseLeave={handleMouseLeave}
408 - ref={focusTargetRef}
409 - tabIndex={0}>
410 - <AutoSizer>
411 - {({height, width}) => (
412 - <FixedSizeList
413 - className={styles.List}
414 - height={height}
415 - innerElementType={InnerElementType}
416 - itemCount={numElements}
417 - itemData={itemData}
418 - itemKey={itemKey}
419 - itemSize={lineHeight}
420 - ref={listCallbackRef}
421 - width={width}>
422 - {Element}
423 - </FixedSizeList>
424 - )}
425 - </AutoSizer>
426 - </div>
417 + {numElements === 0 ? (
418 + zeroElementsNotice
419 + ) : (
420 + <div
421 + className={styles.AutoSizerWrapper}
422 + onBlur={handleBlur}
423 + onFocus={handleFocus}
424 + onKeyPress={handleKeyPress}
425 + onMouseMove={handleMouseMove}
426 + onMouseLeave={handleMouseLeave}
427 + ref={focusTargetRef}
428 + tabIndex={0}>
429 + <AutoSizer>
430 + {({height, width}) => (
431 + <FixedSizeList
432 + className={styles.List}
433 + height={height}
434 + innerElementType={InnerElementType}
435 + itemCount={numElements}
436 + itemData={itemData}
437 + itemKey={itemKey}
438 + itemSize={lineHeight}
439 + ref={listCallbackRef}
440 + width={width}>
441 + {Element}
442 + </FixedSizeList>
443 + )}
444 + </AutoSizer>
445 + </div>
446 + )}
447 </div>
448 </TreeFocusedContext.Provider>
449 );
packages/react-devtools/README.md
+5
@@ -105,8 +105,13 @@ Or you could develop with a local HTTP server like [`serve`](https://www.npmjs.c
105
106 **If your app is inside an iframe, a Chrome extension, React Native, or in another unusual environment**, try [the standalone version instead](https://github.com/facebook/react/tree/main/packages/react-devtools). Chrome apps are currently not inspectable.
107
108 +**If your Components tab is empty, refer to "Chrome v101 and earlier" section below**, please read the "the issue with Chrome v101 and earlier versions" part below.
109 +
110 **If you still have issues** please [report them](https://github.com/facebook/react/issues/new?labels=Component:%20Developer%20Tools). Don't forget to specify your OS, browser version, extension version, and the exact instructions to reproduce the issue with a screenshot.
111
112 +### The Issue with Chrome v101 and earlier
113 +As we migrate to a Chrome Extension Manifest V3, we start to use a new method to hook the DevTools with the inspected page. This new method is more secure, but relies on a new API that's only supported in Chrome v102+. For Chrome v101 or earlier, we use a fallback method, which can cause malfunctions (e.g. empty React component tab) if the JS resources on your page is loaded from cache. Please upgrade to Chrome v102+ to avoid this issue.
114 +
115 ## Local development
116 The standalone DevTools app can be built and tested from source following the instructions below.
117