From 248ddba18608e1bb5ef14c823085a7ff9d7a54a3 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 7 Aug 2024 09:09:27 +0100 Subject: [PATCH] feat[react-devtools/extension]: use chrome.storage to persist settings across sessions --- .../chrome/manifest.json | 1 + .../edge/manifest.json | 1 + .../firefox/manifest.json | 1 + .../dynamicallyInjectContentScripts.js | 7 ++++ .../contentScripts/hookSettingsInjector.js | 42 +++++++++++++++++++ .../src/contentScripts/installHook.js | 39 +++++++++++++++-- .../src/main/index.js | 10 ++--- .../src/main/syncSavedPreferences.js | 34 --------------- .../webpack.config.js | 1 + .../src/backend/agent.js | 9 +--- .../src/backend/types.js | 2 +- .../src/devtools/store.js | 3 ++ 12 files changed, 99 insertions(+), 51 deletions(-) create mode 100644 packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js delete mode 100644 packages/react-devtools-extensions/src/main/syncSavedPreferences.js diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index e61ebd1e57ed0..1ab43194f2620 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -42,6 +42,7 @@ }, "permissions": [ "scripting", + "storage", "tabs" ], "host_permissions": [ diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 48a56c7400ce4..bd03dea08efb3 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -42,6 +42,7 @@ }, "permissions": [ "scripting", + "storage", "tabs" ], "host_permissions": [ diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 930c1ab11083e..8a5a272fb4500 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -49,6 +49,7 @@ }, "permissions": [ "scripting", + "storage", "tabs" ], "host_permissions": [ diff --git a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js index 9398d71a54e7c..f1a3598a519ca 100644 --- a/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js +++ b/packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js @@ -25,6 +25,13 @@ const contentScriptsToInject = [ runAt: 'document_start', world: chrome.scripting.ExecutionWorld.MAIN, }, + { + id: '@react-devtools/hook-settings-injector', + js: ['build/hookSettingsInjector.js'], + matches: [''], + persistAcrossSessions: true, + runAt: 'document_start', + }, ]; async function dynamicallyInjectContentScripts() { diff --git a/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js new file mode 100644 index 0000000000000..e26108edac7e6 --- /dev/null +++ b/packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js @@ -0,0 +1,42 @@ +/* global chrome */ + +// We can't use chrome.storage domain from scripts which are injected in ExecutionWorld.MAIN +// This is the only purpose of this script - to send persisted settings to installHook.js content script + +async function messageListener(event: MessageEvent) { + if (event.source !== window) { + return; + } + + if (event.data.source === 'react-devtools-hook-installer') { + if (event.data.payload.handshake) { + const settings = await chrome.storage.local.get(); + // If storage was empty (first installation), define default settings + if (typeof settings.appendComponentStack !== 'boolean') { + settings.appendComponentStack = true; + } + if (typeof settings.breakOnConsoleErrors !== 'boolean') { + settings.breakOnConsoleErrors = false; + } + if (typeof settings.showInlineWarningsAndErrors !== 'boolean') { + settings.showInlineWarningsAndErrors = true; + } + if (typeof settings.hideConsoleLogsInStrictMode !== 'boolean') { + settings.hideConsoleLogsInStrictMode = false; + } + + window.postMessage({ + source: 'react-devtools-hook-settings-injector', + payload: {settings}, + }); + + window.removeEventListener('message', messageListener); + } + } +} + +window.addEventListener('message', messageListener); +window.postMessage({ + source: 'react-devtools-hook-settings-injector', + payload: {handshake: true}, +}); diff --git a/packages/react-devtools-extensions/src/contentScripts/installHook.js b/packages/react-devtools-extensions/src/contentScripts/installHook.js index ff7e041627f0e..b7b96ed24714b 100644 --- a/packages/react-devtools-extensions/src/contentScripts/installHook.js +++ b/packages/react-devtools-extensions/src/contentScripts/installHook.js @@ -1,10 +1,43 @@ import {installHook} from 'react-devtools-shared/src/hook'; -// avoid double execution +let resolveHookSettingsInjection; + +function messageListener(event: MessageEvent) { + if (event.source !== window) { + return; + } + + if (event.data.source === 'react-devtools-hook-settings-injector') { + // In case handshake message was sent prior to hookSettingsInjector execution + // We can't guarantee order + if (event.data.payload.handshake) { + window.postMessage({ + source: 'react-devtools-hook-installer', + payload: {handshake: true}, + }); + } else if (event.data.payload.settings) { + window.removeEventListener('message', messageListener); + resolveHookSettingsInjection(event.data.payload.settings); + } + } +} + +// Avoid double execution if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { - installHook(window); + const hookSettingsPromise = new Promise(resolve => { + resolveHookSettingsInjection = resolve; + }); + + window.addEventListener('message', messageListener); + window.postMessage({ + source: 'react-devtools-hook-installer', + payload: {handshake: true}, + }); + + // Can't delay hook installation, inject settings lazily + installHook(window, hookSettingsPromise); - // detect react + // Detect React window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on( 'renderer', function ({reactBuildType}) { diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index 36931e42194a4..a2758567138c8 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -27,7 +27,6 @@ import {startReactPolling} from './reactPolling'; import cloneStyleTags from './cloneStyleTags'; import fetchFileWithCaching from './fetchFileWithCaching'; import injectBackendManager from './injectBackendManager'; -import syncSavedPreferences from './syncSavedPreferences'; import registerEventsLogger from './registerEventsLogger'; import getProfilingFlags from './getProfilingFlags'; import debounce from './debounce'; @@ -103,6 +102,10 @@ function createBridgeAndStore() { supportsClickToInspect: true, }); + store.addListener('settingsUpdated', settings => { + chrome.storage.local.set(settings); + }); + if (!isProfiling) { // We previously stored this in performCleanup function store.profilerStore.profilingData = profilingData; @@ -393,10 +396,6 @@ let root = null; let port = null; -// Re-initialize saved filters on navigation, -// since global values stored on window get reset in this case. -chrome.devtools.network.onNavigated.addListener(syncSavedPreferences); - // In case when multiple navigation events emitted in a short period of time // This debounced callback primarily used to avoid mounting React DevTools multiple times, which results // into subscribing to the same events from Bridge and window multiple times @@ -426,5 +425,4 @@ if (__IS_FIREFOX__) { connectExtensionPort(); -syncSavedPreferences(); mountReactDevToolsWhenReactHasLoaded(); diff --git a/packages/react-devtools-extensions/src/main/syncSavedPreferences.js b/packages/react-devtools-extensions/src/main/syncSavedPreferences.js deleted file mode 100644 index f22d41eb7c904..0000000000000 --- a/packages/react-devtools-extensions/src/main/syncSavedPreferences.js +++ /dev/null @@ -1,34 +0,0 @@ -/* global chrome */ - -import { - getAppendComponentStack, - getBreakOnConsoleErrors, - getSavedComponentFilters, - getShowInlineWarningsAndErrors, - getHideConsoleLogsInStrictMode, -} from 'react-devtools-shared/src/utils'; - -// The renderer interface can't read saved component filters directly, -// because they are stored in localStorage within the context of the extension. -// Instead it relies on the extension to pass filters through. -function syncSavedPreferences() { - chrome.devtools.inspectedWindow.eval( - `window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify( - getAppendComponentStack(), - )}; - window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify( - getBreakOnConsoleErrors(), - )}; - window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify( - getSavedComponentFilters(), - )}; - window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify( - getShowInlineWarningsAndErrors(), - )}; - window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify( - getHideConsoleLogsInStrictMode(), - )};`, - ); -} - -export default syncSavedPreferences; diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index ddbb4356f658c..e6d40a1f20ad2 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -56,6 +56,7 @@ module.exports = { proxy: './src/contentScripts/proxy.js', prepareInjection: './src/contentScripts/prepareInjection.js', installHook: './src/contentScripts/installHook.js', + hookSettingsInjector: './src/contentScripts/hookSettingsInjector.js', }, output: { path: __dirname + '/build', diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index 277b743f3f6a4..e55e8a6d41e2b 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -149,7 +149,7 @@ export default class Agent extends EventEmitter<{ drawTraceUpdates: [Array], disableTraceUpdates: [], getIfHasUnsupportedRendererVersion: [], - updateHookSettings: [DevToolsHookSettings], + updateHookSettings: [$ReadOnly], getHookSettings: [], }> { _bridge: BackendBridge; @@ -806,12 +806,7 @@ export default class Agent extends EventEmitter<{ updateHookSettings: (settings: $ReadOnly) => void = settings => { // Propagate the settings, so Backend can subscribe to it and modify hook - this.emit('updateHookSettings', { - appendComponentStack: settings.appendComponentStack, - breakOnConsoleErrors: settings.breakOnConsoleErrors, - showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors, - hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode, - }); + this.emit('updateHookSettings', settings); }; getHookSettings: () => void = () => { diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index 3e1fe75ee4684..836725c528ecb 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -524,7 +524,7 @@ export type DevToolsHook = { // Testing dangerous_setTargetConsoleForTesting?: (fakeConsole: Object) => void, - settings?: DevToolsHookSettings, + settings?: $ReadOnly, ... }; diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 1798e0952b8c2..b54907338b372 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -96,6 +96,7 @@ export default class Store extends EventEmitter<{ componentFilters: [], error: [Error], hookSettings: [$ReadOnly], + settingsUpdated: [$ReadOnly], mutated: [[Array, Map]], recordChangeDescriptions: [], roots: [], @@ -1519,7 +1520,9 @@ export default class Store extends EventEmitter<{ updateHookSettings: (settings: $ReadOnly) => void = settings => { this._hookSettings = settings; + this._bridge.send('updateHookSettings', settings); + this.emit('settingsUpdated', settings); }; onHookSettings: (settings: $ReadOnly) => void =