Skip to content

Commit 6dbccb9

Browse files
authored
[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`
1 parent 973b90b commit 6dbccb9

File tree

14 files changed

+268
-152
lines changed

14 files changed

+268
-152
lines changed

packages/react-devtools-extensions/chrome/manifest.json

+27-16
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
{
2-
"manifest_version": 2,
2+
"manifest_version": 3,
33
"name": "React Developer Tools",
44
"description": "Adds React debugging tools to the Chrome Developer Tools.",
55
"version": "4.26.1",
66
"version_name": "4.26.1",
7-
"minimum_chrome_version": "60",
7+
"minimum_chrome_version": "88",
88
"icons": {
99
"16": "icons/16-production.png",
1010
"32": "icons/32-production.png",
1111
"48": "icons/48-production.png",
1212
"128": "icons/128-production.png"
1313
},
14-
"browser_action": {
14+
"action": {
1515
"default_icon": {
1616
"16": "icons/16-disabled.png",
1717
"32": "icons/32-disabled.png",
@@ -21,31 +21,42 @@
2121
"default_popup": "popups/disabled.html"
2222
},
2323
"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+
},
2527
"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+
}
3042
],
3143
"background": {
32-
"scripts": [
33-
"build/background.js"
34-
],
35-
"persistent": false
44+
"service_worker": "build/background.js"
3645
},
3746
"permissions": [
38-
"file:///*",
39-
"http://*/*",
40-
"https://*/*"
47+
"storage",
48+
"scripting"
49+
],
50+
"host_permissions": [
51+
"<all_urls>"
4152
],
4253
"content_scripts": [
4354
{
4455
"matches": [
4556
"<all_urls>"
4657
],
4758
"js": [
48-
"build/injectGlobalHook.js"
59+
"build/prepareInjection.js"
4960
],
5061
"run_at": "document_start"
5162
}

packages/react-devtools-extensions/edge/manifest.json

+27-16
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
{
2-
"manifest_version": 2,
2+
"manifest_version": 3,
33
"name": "React Developer Tools",
44
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
55
"version": "4.26.1",
66
"version_name": "4.26.1",
7-
"minimum_chrome_version": "60",
7+
"minimum_chrome_version": "88",
88
"icons": {
99
"16": "icons/16-production.png",
1010
"32": "icons/32-production.png",
1111
"48": "icons/48-production.png",
1212
"128": "icons/128-production.png"
1313
},
14-
"browser_action": {
14+
"action": {
1515
"default_icon": {
1616
"16": "icons/16-disabled.png",
1717
"32": "icons/32-disabled.png",
@@ -21,31 +21,42 @@
2121
"default_popup": "popups/disabled.html"
2222
},
2323
"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+
},
2527
"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+
}
3042
],
3143
"background": {
32-
"scripts": [
33-
"build/background.js"
34-
],
35-
"persistent": false
44+
"service_worker": "build/background.js"
3645
},
3746
"permissions": [
38-
"file:///*",
39-
"http://*/*",
40-
"https://*/*"
47+
"storage",
48+
"scripting"
49+
],
50+
"host_permissions": [
51+
"<all_urls>"
4152
],
4253
"content_scripts": [
4354
{
4455
"matches": [
4556
"<all_urls>"
4657
],
4758
"js": [
48-
"build/injectGlobalHook.js"
59+
"build/prepareInjection.js"
4960
],
5061
"run_at": "document_start"
5162
}

packages/react-devtools-extensions/firefox/manifest.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
"main.html",
3232
"panel.html",
3333
"build/react_devtools_backend.js",
34-
"build/renderer.js"
34+
"build/proxy.js",
35+
"build/renderer.js",
36+
"build/installHook.js"
3537
],
3638
"background": {
3739
"scripts": [
@@ -50,7 +52,7 @@
5052
"<all_urls>"
5153
],
5254
"js": [
53-
"build/injectGlobalHook.js"
55+
"build/prepareInjection.js"
5456
],
5557
"run_at": "document_start"
5658
}

packages/react-devtools-extensions/src/background.js

+43-18
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,41 @@
22

33
'use strict';
44

5+
import {IS_FIREFOX} from './utils';
6+
57
const ports = {};
68

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+
}
832

933
chrome.runtime.onConnect.addListener(function(port) {
1034
let tab = null;
1135
let name = null;
1236
if (isNumeric(port.name)) {
1337
tab = port.name;
1438
name = 'devtools';
15-
installContentScript(+port.name);
39+
installProxy(+port.name);
1640
} else {
1741
tab = port.sender.tab.id;
1842
name = 'content-script';
@@ -35,12 +59,15 @@ function isNumeric(str: string): boolean {
3559
return +str + '' === str;
3660
}
3761

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+
}
4471
}
4572

4673
function doublePipe(one, two) {
@@ -63,18 +90,19 @@ function doublePipe(one, two) {
6390
}
6491

6592
function setIconAndPopup(reactBuildType, tabId) {
66-
chrome.browserAction.setIcon({
93+
const action = IS_FIREFOX ? chrome.browserAction : chrome.action;
94+
action.setIcon({
6795
tabId: tabId,
6896
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`),
73101
},
74102
});
75-
chrome.browserAction.setPopup({
103+
action.setPopup({
76104
tabId: tabId,
77-
popup: 'popups/' + reactBuildType + '.html',
105+
popup: chrome.runtime.getURL(`popups/${reactBuildType}.html`),
78106
});
79107
}
80108

@@ -123,9 +151,6 @@ chrome.runtime.onMessage.addListener((request, sender) => {
123151
// This is sent from the hook content script.
124152
// It tells us a renderer has attached.
125153
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.
129154
setIconAndPopup(request.reactBuildType, id);
130155
} else {
131156
switch (request.payload?.type) {
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)