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`.
This commit is contained in:
Ruslan Lesiutin
2024-09-18 18:26:39 +01:00
committed by GitHub
parent e33acfd67f
commit f37c7bc653
12 changed files with 100 additions and 52 deletions
@@ -42,6 +42,7 @@
},
"permissions": [
"scripting",
"storage",
"tabs"
],
"host_permissions": [
@@ -42,6 +42,7 @@
},
"permissions": [
"scripting",
"storage",
"tabs"
],
"host_permissions": [
@@ -49,6 +49,7 @@
},
"permissions": [
"scripting",
"storage",
"tabs"
],
"host_permissions": [
@@ -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: ['<all_urls>'],
persistAcrossSessions: true,
runAt: 'document_start',
},
];
async function dynamicallyInjectContentScripts() {
@@ -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},
});
@@ -1,10 +1,43 @@
import {installHook} from 'react-devtools-shared/src/hook';
// avoid double execution
if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
installHook(window);
let resolveHookSettingsInjection;
// detect react
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__')) {
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
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on(
'renderer',
function ({reactBuildType}) {
+4 -6
View File
@@ -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();
@@ -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;
@@ -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',
+2 -7
View File
@@ -149,7 +149,7 @@ export default class Agent extends EventEmitter<{
drawTraceUpdates: [Array<HostInstance>],
disableTraceUpdates: [],
getIfHasUnsupportedRendererVersion: [],
updateHookSettings: [DevToolsHookSettings],
updateHookSettings: [$ReadOnly<DevToolsHookSettings>],
getHookSettings: [],
}> {
_bridge: BackendBridge;
@@ -806,12 +806,7 @@ export default class Agent extends EventEmitter<{
updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => 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 = () => {
+1 -1
View File
@@ -524,7 +524,7 @@ export type DevToolsHook = {
// Testing
dangerous_setTargetConsoleForTesting?: (fakeConsole: Object) => void,
settings?: DevToolsHookSettings,
settings?: $ReadOnly<DevToolsHookSettings>,
...
};
+3
View File
@@ -96,6 +96,7 @@ export default class Store extends EventEmitter<{
componentFilters: [],
error: [Error],
hookSettings: [$ReadOnly<DevToolsHookSettings>],
settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
mutated: [[Array<number>, Map<number, number>]],
recordChangeDescriptions: [],
roots: [],
@@ -1519,7 +1520,9 @@ export default class Store extends EventEmitter<{
updateHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =
settings => {
this._hookSettings = settings;
this._bridge.send('updateHookSettings', settings);
this.emit('settingsUpdated', settings);
};
onHookSettings: (settings: $ReadOnly<DevToolsHookSettings>) => void =