@samitouri / QOS-React-1 / commits / f37c7bc653

feat[react-devtools/extension]: use chrome.storage to persist settings across sessions (#30636)

Stacked on https://github.com/facebook/react/pull/30610 and whats under it. See [last commit](https://github.com/facebook/react/pull/30636/commits/248ddba18608e1bb5ef14c823085a7ff9d7a54a3). Now, we are using [`chrome.storage`](https://developer.chrome.com/docs/extensions/reference/api/storage) to persist settings for the browser extension across different sessions. Once settings are updated from the UI, the `Store` will emit `settingsUpdated` event, and we are going to persist them via `chrome.storage.local.set` in `main/index.js`. When hook is being injected, we are going to pass a `Promise`, which is going to be resolved after the settings are read from the storage via `chrome.storage.local.get` in `hookSettingsInjector.js`.

Ruslan Lesiutin committed Sep 18, 2024 at 18:26 UTC f37c7bc6539b4da38f7080b5486eb00bdb2c3237
12 files changed +99 -51
packages/react-devtools-extensions/chrome/manifest.json
+1
@@ -42,6 +42,7 @@
42 },
43 "permissions": [
44 "scripting",
45 + "storage",
46 "tabs"
47 ],
48 "host_permissions": [
packages/react-devtools-extensions/edge/manifest.json
+1
@@ -42,6 +42,7 @@
42 },
43 "permissions": [
44 "scripting",
45 + "storage",
46 "tabs"
47 ],
48 "host_permissions": [
packages/react-devtools-extensions/firefox/manifest.json
+1
@@ -49,6 +49,7 @@
49 },
50 "permissions": [
51 "scripting",
52 + "storage",
53 "tabs"
54 ],
55 "host_permissions": [
packages/react-devtools-extensions/src/background/dynamicallyInjectContentScripts.js
+7
@@ -25,6 +25,13 @@ const contentScriptsToInject = [
25 runAt: 'document_start',
26 world: chrome.scripting.ExecutionWorld.MAIN,
27 },
28 + {
29 + id: '@react-devtools/hook-settings-injector',
30 + js: ['build/hookSettingsInjector.js'],
31 + matches: ['<all_urls>'],
32 + persistAcrossSessions: true,
33 + runAt: 'document_start',
34 + },
35 ];
36
37 async function dynamicallyInjectContentScripts() {
packages/react-devtools-extensions/src/contentScripts/hookSettingsInjector.js new
+42
@@ -0,0 +1,42 @@
1 +/* global chrome */
2 +
3 +// We can't use chrome.storage domain from scripts which are injected in ExecutionWorld.MAIN
4 +// This is the only purpose of this script - to send persisted settings to installHook.js content script
5 +
6 +async function messageListener(event: MessageEvent) {
7 + if (event.source !== window) {
8 + return;
9 + }
10 +
11 + if (event.data.source === 'react-devtools-hook-installer') {
12 + if (event.data.payload.handshake) {
13 + const settings = await chrome.storage.local.get();
14 + // If storage was empty (first installation), define default settings
15 + if (typeof settings.appendComponentStack !== 'boolean') {
16 + settings.appendComponentStack = true;
17 + }
18 + if (typeof settings.breakOnConsoleErrors !== 'boolean') {
19 + settings.breakOnConsoleErrors = false;
20 + }
21 + if (typeof settings.showInlineWarningsAndErrors !== 'boolean') {
22 + settings.showInlineWarningsAndErrors = true;
23 + }
24 + if (typeof settings.hideConsoleLogsInStrictMode !== 'boolean') {
25 + settings.hideConsoleLogsInStrictMode = false;
26 + }
27 +
28 + window.postMessage({
29 + source: 'react-devtools-hook-settings-injector',
30 + payload: {settings},
31 + });
32 +
33 + window.removeEventListener('message', messageListener);
34 + }
35 + }
36 +}
37 +
38 +window.addEventListener('message', messageListener);
39 +window.postMessage({
40 + source: 'react-devtools-hook-settings-injector',
41 + payload: {handshake: true},
42 +});
packages/react-devtools-extensions/src/contentScripts/installHook.js
+36 -3
@@ -1,10 +1,43 @@
1 import {installHook} from 'react-devtools-shared/src/hook';
2
3 -// avoid double execution
3 +let resolveHookSettingsInjection;
4 +
5 +function messageListener(event: MessageEvent) {
6 + if (event.source !== window) {
7 + return;
8 + }
9 +
10 + if (event.data.source === 'react-devtools-hook-settings-injector') {
11 + // In case handshake message was sent prior to hookSettingsInjector execution
12 + // We can't guarantee order
13 + if (event.data.payload.handshake) {
14 + window.postMessage({
15 + source: 'react-devtools-hook-installer',
16 + payload: {handshake: true},
17 + });
18 + } else if (event.data.payload.settings) {
19 + window.removeEventListener('message', messageListener);
20 + resolveHookSettingsInjection(event.data.payload.settings);
21 + }
22 + }
23 +}
24 +
25 +// Avoid double execution
26 if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
5 - installHook(window);
27 + const hookSettingsPromise = new Promise(resolve => {
28 + resolveHookSettingsInjection = resolve;
29 + });
30 +
31 + window.addEventListener('message', messageListener);
32 + window.postMessage({
33 + source: 'react-devtools-hook-installer',
34 + payload: {handshake: true},
35 + });
36 +
37 + // Can't delay hook installation, inject settings lazily
38 + installHook(window, hookSettingsPromise);
39
7 - // detect react
40 + // Detect React
41 window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on(
42 'renderer',
43 function ({reactBuildType}) {
packages/react-devtools-extensions/src/main/index.js
+4 -6
@@ -27,7 +27,6 @@ import {startReactPolling} from './reactPolling';
27 import cloneStyleTags from './cloneStyleTags';
28 import fetchFileWithCaching from './fetchFileWithCaching';
29 import injectBackendManager from './injectBackendManager';
30 -import syncSavedPreferences from './syncSavedPreferences';
30 import registerEventsLogger from './registerEventsLogger';
31 import getProfilingFlags from './getProfilingFlags';
32 import debounce from './debounce';
@@ -103,6 +102,10 @@ function createBridgeAndStore() {
102 supportsClickToInspect: true,
103 });
104
105 + store.addListener('settingsUpdated', settings => {
106 + chrome.storage.local.set(settings);
107 + });
108 +
109 if (!isProfiling) {
110 // We previously stored this in performCleanup function
111 store.profilerStore.profilingData = profilingData;
@@ -393,10 +396,6 @@ let root = null;
396
397 let port = null;
398
396 -// Re-initialize saved filters on navigation,
397 -// since global values stored on window get reset in this case.
398 -chrome.devtools.network.onNavigated.addListener(syncSavedPreferences);
399 -
399 // In case when multiple navigation events emitted in a short period of time
400 // This debounced callback primarily used to avoid mounting React DevTools multiple times, which results
401 // into subscribing to the same events from Bridge and window multiple times
@@ -426,5 +425,4 @@ if (__IS_FIREFOX__) {
425
426 connectExtensionPort();
427
429 -syncSavedPreferences();
428 mountReactDevToolsWhenReactHasLoaded();
packages/react-devtools-extensions/src/main/syncSavedPreferences.js deleted
-34
@@ -1,34 +0,0 @@
1 -/* global chrome */
2 -
3 -import {
4 - getAppendComponentStack,
5 - getBreakOnConsoleErrors,
6 - getSavedComponentFilters,
7 - getShowInlineWarningsAndErrors,
8 - getHideConsoleLogsInStrictMode,
9 -} from 'react-devtools-shared/src/utils';
10 -
11 -// The renderer interface can't read saved component filters directly,
12 -// because they are stored in localStorage within the context of the extension.
13 -// Instead it relies on the extension to pass filters through.
14 -function syncSavedPreferences() {
15 - chrome.devtools.inspectedWindow.eval(
16 - `window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ = ${JSON.stringify(
17 - getAppendComponentStack(),
18 - )};
19 - window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ = ${JSON.stringify(
20 - getBreakOnConsoleErrors(),
21 - )};
22 - window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = ${JSON.stringify(
23 - getSavedComponentFilters(),
24 - )};
25 - window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ = ${JSON.stringify(
26 - getShowInlineWarningsAndErrors(),
27 - )};
28 - window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ = ${JSON.stringify(
29 - getHideConsoleLogsInStrictMode(),
30 - )};`,
31 - );
32 -}
33 -
34 -export default syncSavedPreferences;
packages/react-devtools-extensions/webpack.config.js
+1
@@ -56,6 +56,7 @@ module.exports = {
56 proxy: './src/contentScripts/proxy.js',
57 prepareInjection: './src/contentScripts/prepareInjection.js',
58 installHook: './src/contentScripts/installHook.js',
59 + hookSettingsInjector: './src/contentScripts/hookSettingsInjector.js',
60 },
61 output: {
62 path: __dirname + '/build',
packages/react-devtools-shared/src/backend/agent.js
+2 -7
@@ -149,7 +149,7 @@ export default class Agent extends EventEmitter<{
149 drawTraceUpdates: [Array<HostInstance>],
150 disableTraceUpdates: [],
151 getIfHasUnsupportedRendererVersion: [],
152 - updateHookSettings: [DevToolsHookSettings],
152 + updateHookSettings: [$ReadOnly<DevToolsHookSettings>],
153 getHookSettings: [],
154 }> {
155 _bridge: BackendBridge;
@@ -806,12 +806,7 @@ export default class Agent extends EventEmitter<{
806 updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
807 settings => {
808 // Propagate the settings, so Backend can subscribe to it and modify hook
809 - this.emit('updateHookSettings', {
810 - appendComponentStack: settings.appendComponentStack,
811 - breakOnConsoleErrors: settings.breakOnConsoleErrors,
812 - showInlineWarningsAndErrors: settings.showInlineWarningsAndErrors,
813 - hideConsoleLogsInStrictMode: settings.hideConsoleLogsInStrictMode,
814 - });
809 + this.emit('updateHookSettings', settings);
810 };
811
812 getHookSettings: () => void = () => {
packages/react-devtools-shared/src/backend/types.js
+1 -1
@@ -524,7 +524,7 @@ export type DevToolsHook = {
524 // Testing
525 dangerous_setTargetConsoleForTesting?: (fakeConsole: Object) => void,
526
527 - settings?: DevToolsHookSettings,
527 + settings?: $ReadOnly<DevToolsHookSettings>,
528 ...
529 };
530
packages/react-devtools-shared/src/devtools/store.js
+3
@@ -96,6 +96,7 @@ export default class Store extends EventEmitter<{
96 componentFilters: [],
97 error: [Error],
98 hookSettings: [$ReadOnly<DevToolsHookSettings>],
99 + settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
100 mutated: [[Array<number>, Map<number, number>]],
101 recordChangeDescriptions: [],
102 roots: [],
@@ -1519,7 +1520,9 @@ export default class Store extends EventEmitter<{
1520 updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
1521 settings => {
1522 this._hookSettings = settings;
1523 +
1524 this._bridge.send('updateHookSettings', settings);
1525 + this.emit('settingsUpdated', settings);
1526 };
1527
1528 onHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =