Skip to content

Commit 5685d3e

Browse files
author
Juan Tejada
committed
Prefer internal version + improve handling of local dev builds
This commit does the following: - Ensures that the Chrome Web Store version is disabled instead of the internal version when duplicates are present. To do this we also rely on the stable ID of the internal version - It improves handling of local dev builds: - For dev builds, we enable the "management" permissions, so dev builds can easily detect duplicate extensions. - When a duplicate extension is detected, we disable the dev build and show an error so that other extensions are disabled or uninstalled. Ideally in this case we would disable any other extensions except the development one. However, since we don't have a stable extension ID for dev builds, doing so would require for other installations to wait for a message from this extension, which would unnecessarily delay initialization of those extensions.
1 parent aec8557 commit 5685d3e

File tree

7 files changed

+163
-71
lines changed

7 files changed

+163
-71
lines changed

packages/react-devtools-extensions/build.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ const build = async (tempPath, manifestPath) => {
102102
}
103103
manifest.description += `\n\nCreated from revision ${commit} on ${dateString}.`;
104104

105+
if (process.env.NODE_ENV === 'development') {
106+
if (Array.isArray(manifest.permissions)) {
107+
manifest.permissions.push('management');
108+
}
109+
}
110+
105111
writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2));
106112

107113
// Pack the extension

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ const ports = {};
77
const IS_FIREFOX = navigator.userAgent.indexOf('Firefox') >= 0;
88

99
import {
10-
IS_CHROME_WEBSTORE_EXTENSION,
1110
EXTENSION_INSTALL_CHECK_MESSAGE,
11+
EXTENSION_INSTALLATION_TYPE,
1212
} from './constants';
1313

1414
chrome.runtime.onConnect.addListener(function(port) {
@@ -121,7 +121,7 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
121121
}
122122
});
123123

124-
if (IS_CHROME_WEBSTORE_EXTENSION) {
124+
if (EXTENSION_INSTALLATION_TYPE === 'internal') {
125125
chrome.runtime.onMessageExternal.addListener(
126126
(request, sender, sendResponse) => {
127127
if (request === EXTENSION_INSTALL_CHECK_MESSAGE) {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its 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 strict-local
8+
*/
9+
10+
declare var chrome: any;
11+
12+
import {__DEBUG__} from 'react-devtools-shared/src/constants';
13+
import {
14+
EXTENSION_INSTALL_CHECK_MESSAGE,
15+
EXTENSION_INSTALLATION_TYPE,
16+
INTERNAL_EXTENSION_ID,
17+
EXTENSION_NAME,
18+
} from './constants';
19+
20+
export function checkForDuplicateInstallations(callback: boolean => void) {
21+
switch (EXTENSION_INSTALLATION_TYPE) {
22+
case 'chrome-web-store': {
23+
// If this is the Chrome Web Store extension, check if an internal build of the
24+
// extension is also installed, and if so, disable this extension.
25+
chrome.runtime.sendMessage(
26+
INTERNAL_EXTENSION_ID,
27+
EXTENSION_INSTALL_CHECK_MESSAGE,
28+
response => {
29+
if (__DEBUG__) {
30+
console.log(
31+
'[main] checkForDuplicateInstallations: Duplicate installation check responded with',
32+
{
33+
response,
34+
error: chrome.runtime.lastError?.message,
35+
currentExtension: EXTENSION_INSTALLATION_TYPE,
36+
},
37+
);
38+
}
39+
if (chrome.runtime.lastError != null) {
40+
callback(false);
41+
} else {
42+
callback(response === true);
43+
}
44+
},
45+
);
46+
break;
47+
}
48+
case 'internal': {
49+
// If this is the internal extension, keep this one enabled.
50+
// Other installations disable themselves if they detect this installation.
51+
// TODO show warning if other installations are present.
52+
callback(false);
53+
break;
54+
}
55+
case 'unknown': {
56+
if (__DEV__) {
57+
// If this extension was built locally during development, then we check for other
58+
// installations of the extension via the `chrome.management` API (which is only
59+
// enabled in local development builds).
60+
// If we detect other installations, we disable this one and show a warning
61+
// for the developer to disable the other installations.
62+
// NOTE: Ideally in this case we would disable any other extensions except the
63+
// development one. However, since we don't have a stable extension ID for dev builds,
64+
// doing so would require for other installations to wait for a message from this extension,
65+
// which could unnecessarily delay initialization of those extensions.
66+
chrome.management.getAll(extensions => {
67+
if (chrome.runtime.lastError != null) {
68+
const errorMessage =
69+
'React Developer Tools: Unable to access `chrome.management` to check for duplicate extensions. This extension will be disabled.' +
70+
'If you are developing this extension locally, make sure to build the extension using the `yarn build:<browser>:dev` command.';
71+
console.error(errorMessage);
72+
chrome.devtools.inspectedWindow.eval(
73+
`console.error("${errorMessage}")`,
74+
);
75+
callback(true);
76+
return;
77+
}
78+
const devToolsExtensions = extensions.filter(
79+
extension => extension.name === EXTENSION_NAME && extension.enabled,
80+
);
81+
if (devToolsExtensions.length > 1) {
82+
// TODO: Show warning in UI of extension that remains enabled
83+
const errorMessage =
84+
'React Developer Tools: You are running multiple installations of the React Developer Tools extension, which will conflict with this development build of the extension.' +
85+
'In order to prevent conflicts, this development build of the extension will be disabled. In order to continue local development, please disable or uninstall ' +
86+
'any other installations of the extension in your browser.';
87+
chrome.devtools.inspectedWindow.eval(
88+
`console.error("${errorMessage}")`,
89+
);
90+
console.error(errorMessage);
91+
callback(true);
92+
} else {
93+
callback(false);
94+
}
95+
});
96+
break;
97+
}
98+
99+
// If this extension wasn't built locally during development, we can't reliably
100+
// detect if there are other installations of DevTools present.
101+
// In this case, assume there are no duplicate exensions and show a warning about
102+
// potential conflicts.
103+
const warnMessage =
104+
'React Developer Tools: You are running an unrecognized installation of the React Developer Tools extension, which might conflict with other versions of the extension installed in your browser.' +
105+
'Please make sure you only have a single version of the extension installed or enabled.' +
106+
'If you are developing this extension locally, make sure to build the extension using the `yarn build:<browser>:dev` command.';
107+
console.warn(warnMessage);
108+
chrome.devtools.inspectedWindow.eval(`console.warn("${warnMessage}")`);
109+
callback(false);
110+
break;
111+
}
112+
default: {
113+
(EXTENSION_INSTALLATION_TYPE: empty);
114+
}
115+
}
116+
}

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,20 @@
99

1010
declare var chrome: any;
1111

12-
export const CHROME_WEBSTORE_EXTENSION_ID = 'fmkadmapgofadopljbjfkapdkoienihi';
1312
export const CURRENT_EXTENSION_ID = chrome.runtime.id;
14-
export const IS_CHROME_WEBSTORE_EXTENSION =
15-
CURRENT_EXTENSION_ID === CHROME_WEBSTORE_EXTENSION_ID;
13+
14+
export const EXTENSION_NAME = 'React Developer Tools';
1615
export const EXTENSION_INSTALL_CHECK_MESSAGE = 'extension-install-check';
16+
17+
export const CHROME_WEBSTORE_EXTENSION_ID = 'fmkadmapgofadopljbjfkapdkoienihi';
18+
export const INTERNAL_EXTENSION_ID = 'dnjnjgbfilfphmojnmhliehogmojhclc';
19+
20+
export const EXTENSION_INSTALLATION_TYPE:
21+
| 'chrome-web-store'
22+
| 'internal'
23+
| 'unknown' =
24+
CURRENT_EXTENSION_ID === CHROME_WEBSTORE_EXTENSION_ID
25+
? 'chrome-web-store'
26+
: CURRENT_EXTENSION_ID === INTERNAL_EXTENSION_ID
27+
? 'internal'
28+
: 'unknown';

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
__DEBUG__,
77
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
88
} from 'react-devtools-shared/src/constants';
9-
import {CURRENT_EXTENSION_ID, IS_CHROME_WEBSTORE_EXTENSION} from './constants';
9+
import {CURRENT_EXTENSION_ID, EXTENSION_INSTALLATION_TYPE} from './constants';
1010
import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
1111

1212
function injectCode(code) {
@@ -36,7 +36,7 @@ window.addEventListener('message', function onMessage({data, source}) {
3636
console.log(
3737
`[injectGlobalHook] Received message '${data.source}' from different extension instance. Skipping message.`,
3838
{
39-
currentIsChromeWebstoreExtension: IS_CHROME_WEBSTORE_EXTENSION,
39+
currentExtension: EXTENSION_INSTALLATION_TYPE,
4040
},
4141
);
4242
}

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

Lines changed: 21 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,8 @@ import {
2222
import DevTools from 'react-devtools-shared/src/devtools/views/DevTools';
2323
import {__DEBUG__} from 'react-devtools-shared/src/constants';
2424
import {logEvent} from 'react-devtools-shared/src/Logger';
25-
import {
26-
IS_CHROME_WEBSTORE_EXTENSION,
27-
CHROME_WEBSTORE_EXTENSION_ID,
28-
CURRENT_EXTENSION_ID,
29-
EXTENSION_INSTALL_CHECK_MESSAGE,
30-
} from './constants';
25+
import {CURRENT_EXTENSION_ID, EXTENSION_INSTALLATION_TYPE} from './constants';
26+
import {checkForDuplicateInstallations} from './checkForDuplicateInstallations';
3127

3228
const LOCAL_STORAGE_SUPPORTS_PROFILING_KEY =
3329
'React::DevTools::supportsProfiling';
@@ -36,44 +32,6 @@ const isChrome = getBrowserName() === 'Chrome';
3632

3733
let panelCreated = false;
3834

39-
function checkForDuplicateInstallation(callback) {
40-
if (IS_CHROME_WEBSTORE_EXTENSION) {
41-
if (__DEBUG__) {
42-
console.log(
43-
'[main] checkForDuplicateExtension: Skipping duplicate extension check from current webstore extension.\n' +
44-
'We only check for duplicate extension installations from extension instances that are not the Chrome Web Store instance.',
45-
{
46-
currentIsChromeWebstoreExtension: IS_CHROME_WEBSTORE_EXTENSION,
47-
},
48-
);
49-
}
50-
callback(false);
51-
return;
52-
}
53-
54-
chrome.runtime.sendMessage(
55-
CHROME_WEBSTORE_EXTENSION_ID,
56-
EXTENSION_INSTALL_CHECK_MESSAGE,
57-
response => {
58-
if (__DEBUG__) {
59-
console.log(
60-
'[main] checkForDuplicateInstallation: Duplicate installation check responded with',
61-
{
62-
response,
63-
error: chrome.runtime.lastError?.message,
64-
currentIsChromeWebstoreExtension: IS_CHROME_WEBSTORE_EXTENSION,
65-
},
66-
);
67-
}
68-
if (chrome.runtime.lastError != null) {
69-
callback(false);
70-
} else {
71-
callback(response === true);
72-
}
73-
},
74-
);
75-
}
76-
7735
// The renderer interface can't read saved component filters directly,
7836
// because they are stored in localStorage within the context of the extension.
7937
// Instead it relies on the extension to pass filters through.
@@ -107,23 +65,23 @@ function createPanelIfReactLoaded() {
10765
return;
10866
}
10967

110-
checkForDuplicateInstallation(hasDuplicateInstallation => {
111-
if (hasDuplicateInstallation) {
112-
if (__DEBUG__) {
113-
console.log(
114-
'[main] createPanelIfReactLoaded: Duplicate installation detected, skipping initialization of extension.',
115-
{currentIsChromeWebstoreExtension: IS_CHROME_WEBSTORE_EXTENSION},
116-
);
68+
chrome.devtools.inspectedWindow.eval(
69+
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
70+
function(pageHasReact, error) {
71+
if (!pageHasReact || panelCreated) {
72+
return;
11773
}
118-
panelCreated = true;
119-
clearInterval(loadCheckInterval);
120-
return;
121-
}
122-
123-
chrome.devtools.inspectedWindow.eval(
124-
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
125-
function(pageHasReact, error) {
126-
if (!pageHasReact || panelCreated) {
74+
75+
checkForDuplicateInstallations(hasDuplicateInstallation => {
76+
if (hasDuplicateInstallation) {
77+
if (__DEBUG__) {
78+
console.log(
79+
'[main] createPanelIfReactLoaded: Duplicate installation detected, skipping initialization of extension.',
80+
{currentExtension: EXTENSION_INSTALLATION_TYPE},
81+
);
82+
}
83+
panelCreated = true;
84+
clearInterval(loadCheckInterval);
12785
return;
12886
}
12987

@@ -560,9 +518,9 @@ function createPanelIfReactLoaded() {
560518

561519
initBridgeAndStore();
562520
});
563-
},
564-
);
565-
});
521+
});
522+
},
523+
);
566524
}
567525

568526
// Load (or reload) the DevTools extension when the user navigates to a new page.

packages/react-devtools/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Some changes requiring testing in the browser extension (e.g. like "named hooks"
5757
```sh
5858
cd <react-repo>
5959
cd packages/react-devtools-extensions
60-
yarn build:chrome && yarn test:chrome
60+
yarn build:chrome:dev && yarn test:chrome
6161
```
6262
This will launch a standalone version of Chrome with the locally built React DevTools pre-installed. If you are testing a specific URL, you can make your testing even faster by passing the `--url` argument to the test script:
6363
```sh

0 commit comments

Comments
 (0)