Skip to content

Commit

Permalink
feat[devtools/extension]: show disclaimer when page doesnt run react …
Browse files Browse the repository at this point in the history
…and refactor react polling logic
  • Loading branch information
hoxyq committed Sep 14, 2023
1 parent 54c2f2a commit 040b4ff
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 69 deletions.
35 changes: 34 additions & 1 deletion packages/react-devtools-extensions/panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,44 @@
right: 0;
bottom: 0;
}
.no-react-disclaimer {
margin: 16px;
font-family: Courier, monospace, serif;
font-size: 16px;
animation: fadeIn .5s ease-in-out forwards;
}

@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}

@supports (-moz-appearance:none) {
:root {
background: black;
}

body {
color: white;
}
}
}
</style>
</head>
<body>
<!-- main react mount point -->
<div id="container">Unable to find React on the page.</div>
<div id="container">
<h1 class="no-react-disclaimer">Looks like this page doesn't have React, or it hasn't been loaded yet.</h1>
</div>
<script src="./build/panel.js"></script>
</body>
</html>
12 changes: 11 additions & 1 deletion packages/react-devtools-extensions/popups/shared.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ body {
}

@media (prefers-color-scheme: dark) {
html {
:root {
color-scheme: dark;
}

@supports (-moz-appearance:none) {
:root {
background: black;
}

body {
color: white;
}
}
}
4 changes: 2 additions & 2 deletions packages/react-devtools-extensions/src/background/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ chrome.runtime.onConnect.addListener(port => {
const tabId = port.sender.tab.id;

if (ports[tabId]?.proxy) {
port.disconnect();
return;
ports[tabId].disconnectPipe?.();
ports[tabId].proxy.disconnect();
}

registerTab(tabId);
Expand Down
99 changes: 34 additions & 65 deletions packages/react-devtools-extensions/src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
setBrowserSelectionFromReact,
setReactSelectionFromBrowser,
} from './elementSelection';
import {startReactPolling} from './reactPolling';
import cloneStyleTags from './cloneStyleTags';
import injectBackendManager from './injectBackendManager';
import syncSavedPreferences from './syncSavedPreferences';
Expand All @@ -30,60 +31,6 @@ import getProfilingFlags from './getProfilingFlags';
import debounce from './debounce';
import './requestAnimationFramePolyfill';

// Try polling for at least 5 seconds, in case if it takes too long to load react
const REACT_POLLING_TICK_COOLDOWN = 250;
const REACT_POLLING_ATTEMPTS_THRESHOLD = 20;

let reactPollingTimeoutId = null;
export function clearReactPollingTimeout() {
clearTimeout(reactPollingTimeoutId);
reactPollingTimeoutId = null;
}

export function executeIfReactHasLoaded(callback, attempt = 1) {
clearReactPollingTimeout();

if (attempt > REACT_POLLING_ATTEMPTS_THRESHOLD) {
return;
}

chrome.devtools.inspectedWindow.eval(
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
(pageHasReact, exceptionInfo) => {
if (exceptionInfo) {
const {code, description, isError, isException, value} = exceptionInfo;

if (isException) {
console.error(
`Received error while checking if react has loaded: ${value}`,
);
return;
}

if (isError) {
console.error(
`Received error with code ${code} while checking if react has loaded: ${description}`,
);
return;
}
}

if (pageHasReact) {
callback();
} else {
reactPollingTimeoutId = setTimeout(
executeIfReactHasLoaded,
REACT_POLLING_TICK_COOLDOWN,
callback,
attempt + 1,
);
}
},
);
}

let lastSubscribedBridgeListener = null;

function createBridge() {
bridge = new Bridge({
listen(fn) {
Expand Down Expand Up @@ -370,6 +317,7 @@ function ensureInitialHTMLIsCleared(container) {
function createComponentsPanel() {
if (componentsPortalContainer) {
// Panel is created and user opened it at least once
ensureInitialHTMLIsCleared(componentsPortalContainer);
render('components');

return;
Expand All @@ -389,7 +337,7 @@ function createComponentsPanel() {

createdPanel.onShown.addListener(portal => {
componentsPortalContainer = portal.container;
if (componentsPortalContainer != null) {
if (componentsPortalContainer != null && render) {
ensureInitialHTMLIsCleared(componentsPortalContainer);

render('components');
Expand All @@ -408,6 +356,7 @@ function createComponentsPanel() {
function createProfilerPanel() {
if (profilerPortalContainer) {
// Panel is created and user opened it at least once
ensureInitialHTMLIsCleared(profilerPortalContainer);
render('profiler');

return;
Expand All @@ -427,7 +376,7 @@ function createProfilerPanel() {

createdPanel.onShown.addListener(portal => {
profilerPortalContainer = portal.container;
if (profilerPortalContainer != null) {
if (profilerPortalContainer != null && render) {
ensureInitialHTMLIsCleared(profilerPortalContainer);

render('profiler');
Expand All @@ -442,7 +391,7 @@ function createProfilerPanel() {

function performInTabNavigationCleanup() {
// Potentially, if react hasn't loaded yet and user performs in-tab navigation
clearReactPollingTimeout();
clearReactPollingInstance();

if (store !== null) {
// Store profiling data, so it can be used later
Expand Down Expand Up @@ -479,7 +428,7 @@ function performInTabNavigationCleanup() {

function performFullCleanup() {
// Potentially, if react hasn't loaded yet and user closed the browser DevTools
clearReactPollingTimeout();
clearReactPollingInstance();

if ((componentsPortalContainer || profilerPortalContainer) && root) {
// This should also emit bridge.shutdown, but only if this root was mounted
Expand Down Expand Up @@ -531,6 +480,8 @@ function connectExtensionPort() {
}

function mountReactDevTools() {
reactPollingInstance = null;

registerEventsLogger();

createBridgeAndStore();
Expand All @@ -541,18 +492,36 @@ function mountReactDevTools() {
createProfilerPanel();
}

// TODO: display some disclaimer if user performs in-tab navigation to non-react application
// when React DevTools panels are already opened, currently we will display just blank white block
function mountReactDevToolsWhenReactHasLoaded() {
function onReactReady() {
clearReactPollingTimeout();
mountReactDevTools();
let reactPollingInstance = null;
function clearReactPollingInstance() {
reactPollingInstance?.abort();
reactPollingInstance = null;
}

function showNoReactDisclaimer() {
if (componentsPortalContainer) {
componentsPortalContainer.innerHTML =
'<h1 class="no-react-disclaimer">Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.</h1>';
delete componentsPortalContainer._hasInitialHTMLBeenCleared;
}

if (profilerPortalContainer) {
profilerPortalContainer.innerHTML =
'<h1 class="no-react-disclaimer">Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.</h1>';
delete profilerPortalContainer._hasInitialHTMLBeenCleared;
}
}

executeIfReactHasLoaded(onReactReady, 1);
function mountReactDevToolsWhenReactHasLoaded() {
reactPollingInstance = startReactPolling(
mountReactDevTools,
5, // ~5 seconds
showNoReactDisclaimer,
);
}

let bridge = null;
let lastSubscribedBridgeListener = null;
let store = null;

let profilingData = null;
Expand Down
103 changes: 103 additions & 0 deletions packages/react-devtools-extensions/src/main/reactPolling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/* global chrome */

class CouldNotFindReactOnThePageError extends Error {
constructor() {
super("Could not find React, or it hasn't been loaded yet");

// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CouldNotFindReactOnThePageError);
}

this.name = 'CouldNotFindReactOnThePageError';
}
}

export function startReactPolling(
onReactFound,
attemptsThreshold,
onCouldNotFindReactAfterReachingAttemptsThreshold,
) {
let status = 'idle';

function abort() {
status = 'aborted';
}

// 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(
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
(pageHasReact, exceptionInfo) => {
if (status === 'aborted') {
onError(
'Polling was aborted, user probably navigated to the other page',
);
return;
}

if (exceptionInfo) {
const {code, description, isError, isException, value} =
exceptionInfo;

if (isException) {
onError(
`Received error while checking if react has loaded: ${value}`,
);
return;
}

if (isError) {
onError(
`Received error with code ${code} while checking if react has loaded: "${description}"`,
);
return;
}
}

if (pageHasReact) {
onSuccess();
return;
}

onError(new CouldNotFindReactOnThePageError());
},
);
}

// Just a Promise wrapper around `checkIfReactPresentInInspectedWindow`
// returns a Promise, which will resolve only if React has been found on the page
function poll(attempt) {
return new Promise((resolve, reject) => {
checkIfReactPresentInInspectedWindow(resolve, reject);
}).catch(error => {
if (error instanceof CouldNotFindReactOnThePageError) {
if (attempt === attemptsThreshold) {
onCouldNotFindReactAfterReachingAttemptsThreshold();
}

// Start next attempt in 0.5s
return new Promise(r => setTimeout(r, 500)).then(() =>
poll(attempt + 1),
);
}

// Propagating every other Error
throw error;
});
}

poll(1)
.then(onReactFound)
.catch(error => {
// Log propagated errors only if polling was not aborted
// Some errors are expected when user performs in-tab navigation and `.eval()` is still being executed
if (status === 'aborted') {
return;
}

console.error(error);
});

return {abort};
}

0 comments on commit 040b4ff

Please sign in to comment.