From 040b4ff535eaaba7a1b837ad450b4c7d72215917 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Thu, 14 Sep 2023 15:23:30 +0100 Subject: [PATCH] feat[devtools/extension]: show disclaimer when page doesnt run react and refactor react polling logic --- packages/react-devtools-extensions/panel.html | 35 +++++- .../popups/shared.css | 12 +- .../src/background/index.js | 4 +- .../src/main/index.js | 99 ++++++----------- .../src/main/reactPolling.js | 103 ++++++++++++++++++ 5 files changed, 184 insertions(+), 69 deletions(-) create mode 100644 packages/react-devtools-extensions/src/main/reactPolling.js diff --git a/packages/react-devtools-extensions/panel.html b/packages/react-devtools-extensions/panel.html index 60fd1bdf13ff2..ccbb16ad25cae 100644 --- a/packages/react-devtools-extensions/panel.html +++ b/packages/react-devtools-extensions/panel.html @@ -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; + } + } + } -
Unable to find React on the page.
+
+

Looks like this page doesn't have React, or it hasn't been loaded yet.

+
diff --git a/packages/react-devtools-extensions/popups/shared.css b/packages/react-devtools-extensions/popups/shared.css index 9c9c1ecd96cbd..731a2036c374b 100644 --- a/packages/react-devtools-extensions/popups/shared.css +++ b/packages/react-devtools-extensions/popups/shared.css @@ -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; + } + } } diff --git a/packages/react-devtools-extensions/src/background/index.js b/packages/react-devtools-extensions/src/background/index.js index 89a0f74edf748..9340733b29ef6 100644 --- a/packages/react-devtools-extensions/src/background/index.js +++ b/packages/react-devtools-extensions/src/background/index.js @@ -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); diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 19a086f1ea7a4..da5b211f715d6 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -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'; @@ -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) { @@ -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; @@ -389,7 +337,7 @@ function createComponentsPanel() { createdPanel.onShown.addListener(portal => { componentsPortalContainer = portal.container; - if (componentsPortalContainer != null) { + if (componentsPortalContainer != null && render) { ensureInitialHTMLIsCleared(componentsPortalContainer); render('components'); @@ -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; @@ -427,7 +376,7 @@ function createProfilerPanel() { createdPanel.onShown.addListener(portal => { profilerPortalContainer = portal.container; - if (profilerPortalContainer != null) { + if (profilerPortalContainer != null && render) { ensureInitialHTMLIsCleared(profilerPortalContainer); render('profiler'); @@ -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 @@ -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 @@ -531,6 +480,8 @@ function connectExtensionPort() { } function mountReactDevTools() { + reactPollingInstance = null; + registerEventsLogger(); createBridgeAndStore(); @@ -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 = + '

Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.

'; + delete componentsPortalContainer._hasInitialHTMLBeenCleared; + } + + if (profilerPortalContainer) { + profilerPortalContainer.innerHTML = + '

Looks like this page doesn\'t have React, or it hasn\'t been loaded yet.

'; + 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; diff --git a/packages/react-devtools-extensions/src/main/reactPolling.js b/packages/react-devtools-extensions/src/main/reactPolling.js new file mode 100644 index 0000000000000..9bb034c6a1091 --- /dev/null +++ b/packages/react-devtools-extensions/src/main/reactPolling.js @@ -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}; +}