Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[react devtools] Device storage support #25213

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion packages/react-devtools-shared/src/backend/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ import {
SESSION_STORAGE_LAST_SELECTION_KEY,
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
LOCAL_STORAGE_BROWSER_THEME,
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
__DEBUG__,
} from '../constants';
import {
sessionStorageGetItem,
sessionStorageRemoveItem,
sessionStorageSetItem,
} from 'react-devtools-shared/src/storage';
import {storeSettingInDeviceStorage} from 'react-devtools-shared/src/backend/deviceStorage';
import setupHighlighter from './views/Highlighter';
import {
initialize as setupTraceUpdates,
Expand Down Expand Up @@ -683,7 +689,27 @@ export default class Agent extends EventEmitter<{|
hideConsoleLogsInStrictMode: boolean,
browserTheme: BrowserTheme,
|}) => {
// If the frontend preference has change,
// Store the current settings in the device storage cache so that, if the app
// is restarted, we can access those settings before the DevTools frontend connects
storeSettingInDeviceStorage(
LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
JSON.stringify(appendComponentStack),
);
storeSettingInDeviceStorage(
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
JSON.stringify(breakOnConsoleErrors),
);
storeSettingInDeviceStorage(
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
JSON.stringify(showInlineWarningsAndErrors),
);
storeSettingInDeviceStorage(
LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
JSON.stringify(hideConsoleLogsInStrictMode),
);
storeSettingInDeviceStorage(LOCAL_STORAGE_BROWSER_THEME, browserTheme);

// If the frontend preferences have changed,
// or in the case of React Native- if the backend is just finding out the preference-
// then reinstall the console overrides.
// It's safe to call these methods multiple times, so we don't need to worry about that.
Expand Down
95 changes: 95 additions & 0 deletions packages/react-devtools-shared/src/backend/deviceStorage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import type {InjectedDeviceStorageMethods} from 'react-devtools-shared/src/backend/types';

export {initializeFromDeviceStorage} from './initialize';
import {initializeFromDeviceStorage} from './initialize';

// # How does React Native/React access settings during startup, potentially
// before the DevTools frontend has connected?
//
// During startup,
// - React Native reads those settings from device storage and writes them to
// the window object.
// - React does need to do this, as the DevTools extension (if installed) can
// execute code that runs before the site JS. This code reads these settings
// from the localStorage of the extension frontend and adds a <script /> tag
// that writes these settings to the window object.
//
// # When does RN/React attempt to access these settings?
//
// - When ReactDOM is required
// - In RN, when `injectDeviceStorageMethods` is called, which is during the same
// tick as the app initially renders, and
// - When the DevTools backend connects
//
// So, that means that `initializeFromDeviceStorage` should be safe to be called
// multiple times.

let methods: ?InjectedDeviceStorageMethods = null;

type EncounteredSettings = {[string]: string};
let mostRecentlyEncounteredSettings: ?EncounteredSettings = null;

// This function is called by React Native before the main app is loaded, as
// `window.__REACT_DEVTOOLS_GLOBAL_HOOK__.injectDeviceStorageMethods`. This is
// before the React DevTools frontend will have connected.
//
// The provided `setValueOnDevice` method allows us to cache most-recently-seen
// settings (e.g. whether to hide double console logs in strict mode).
//
// This behaves as follows:
// - when `injectDeviceStorageMethods` is first called, if the React DevTools
// frontend has connected, this caches on device any settings stored in (A)
// - when the React DevTools frontend connects and whenever a relevant setting
// is modified, `storeSettingInDeviceStorage` is (repeatedly) called.
// - if `injectDeviceStorageMethods` has been called, this will persist the
// relevant setting to device storage
// - if `injectDeviceStorageMethods` has not been called, this will store the
// the settings in memory (step A)
//
// Values stored via `setValueOnDevice` (i.e. in device storage) should not be
// copied to React DevTools' local storage.
export function injectDeviceStorageMethods(
injectedMethods: InjectedDeviceStorageMethods,
) {
// This is a no-op if called multiple times.
if (methods != null) {
return;
}
methods = injectedMethods;

if (mostRecentlyEncounteredSettings != null) {
// The DevTools front-end has connected and attempted to cache some
// settings. Let's cache them on device storage.
for (const key in mostRecentlyEncounteredSettings) {
const value = mostRecentlyEncounteredSettings[key];
try {
injectedMethods.setValueOnDevice(key, value);
} catch {}
}
mostRecentlyEncounteredSettings = null;
}

initializeFromDeviceStorage();
}

export function storeSettingInDeviceStorage(key: string, value: string) {
if (methods == null) {
// injectDeviceStorageMethods has not been called
mostRecentlyEncounteredSettings = mostRecentlyEncounteredSettings ?? {};
mostRecentlyEncounteredSettings[key] = value;
} else {
// injectDeviceStorageMethods has been called
try {
methods.setValueOnDevice(key, value);
} catch {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

import {patch as patchConsole} from 'react-devtools-shared/src/backend/console';
import type {InjectedDeviceStorageMethods} from 'react-devtools-shared/src/backend/types';
import {
LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
LOCAL_STORAGE_BROWSER_THEME,
} from 'react-devtools-shared/src/constants';
import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';

export function initializeFromDeviceStorage() {
patchConsole(getConsolePatchValues());
}

function getConsolePatchValues(): {
appendComponentStack: boolean,
breakOnConsoleErrors: boolean,
showInlineWarningsAndErrors: boolean,
hideConsoleLogsInStrictMode: boolean,
browserTheme: BrowserTheme,
} {
return {
appendComponentStack:
castBool(window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__) ?? true,
breakOnConsoleErrors:
castBool(window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__) ?? false,
showInlineWarningsAndErrors:
castBool(window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__) ??
true,
hideConsoleLogsInStrictMode:
castBool(window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__) ??
false,
browserTheme:
castBrowserTheme(window.__REACT_DEVTOOLS_BROWSER_THEME__) ?? 'dark',
};
}

function castBool(v: any): ?boolean {
if (v === true || v === false) {
return v;
}
}

function castBrowserTheme(v: any): ?BrowserTheme {
if (v === 'light' || v === 'dark' || v === 'auto') {
return v;
}
}
23 changes: 2 additions & 21 deletions packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import is from 'shared/objectIs';
import hasOwnProperty from 'shared/hasOwnProperty';
import {getStyleXData} from './StyleX/utils';
import {createProfilingHooks} from './profilingHooks';
import {initializeFromDeviceStorage} from 'react-devtools-shared/src/backend/deviceStorage';

import type {GetTimelineData, ToggleProfilingStatus} from './profilingHooks';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
Expand Down Expand Up @@ -813,27 +814,7 @@ export function attach(
// * Append component stacks to warnings and error messages
// * Disable logging during re-renders to inspect hooks (see inspectHooksOfFiber)
registerRendererWithConsole(renderer, onErrorOrWarning);

// The renderer interface can't read these preferences directly,
// because it is stored in localStorage within the context of the extension.
// It relies on the extension to pass the preference through via the global.
const appendComponentStack =
window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__ !== false;
const breakOnConsoleErrors =
window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__ === true;
const showInlineWarningsAndErrors =
window.__REACT_DEVTOOLS_SHOW_INLINE_WARNINGS_AND_ERRORS__ !== false;
const hideConsoleLogsInStrictMode =
window.__REACT_DEVTOOLS_HIDE_CONSOLE_LOGS_IN_STRICT_MODE__ === true;
const browserTheme = window.__REACT_DEVTOOLS_BROWSER_THEME__;

patchConsole({
appendComponentStack,
breakOnConsoleErrors,
showInlineWarningsAndErrors,
hideConsoleLogsInStrictMode,
browserTheme,
});
initializeFromDeviceStorage();

const debug = (
name: string,
Expand Down
7 changes: 7 additions & 0 deletions packages/react-devtools-shared/src/backend/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ export type DevToolsProfilingHooks = {|
markComponentPassiveEffectUnmountStopped: () => void,
|};

export type InjectedDeviceStorageMethods = {
setValueOnDevice: (key: string, value: string) => void,
};

export type DevToolsHook = {
listeners: {[key: string]: Array<Handler>, ...},
rendererInterfaces: Map<RendererID, RendererInterface>,
Expand Down Expand Up @@ -480,6 +484,9 @@ export type DevToolsHook = {
didError?: boolean,
) => void,

// React Native calls this
injectDeviceStorageMethods: (methods: InjectedDeviceStorageMethods) => void,

// Timeline internal module filtering
getInternalModuleRanges: () => Array<[string, string]>,
registerInternalModuleStart: (moduleStartError: Error) => void,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
import {useLocalStorage} from '../hooks';
import {BridgeContext} from '../context';
import {logEvent} from 'react-devtools-shared/src/Logger';
import {storeSettingInDeviceStorage} from 'react-devtools-shared/src/backend/deviceStorage';

import type {BrowserTheme} from '../DevTools';

Expand Down Expand Up @@ -73,8 +74,12 @@ SettingsContext.displayName = 'SettingsContext';
function useLocalStorageWithLog<T>(
key: string,
initialValue: T | (() => T),
onSet: ?(T) => void,
): [T, (value: T | (() => T)) => void] {
return useLocalStorage<T>(key, initialValue, (v, k) => {
if (onSet != null) {
onSet(v);
}
logEvent({
event_name: 'settings-changed',
metadata: {
Expand Down Expand Up @@ -113,21 +118,51 @@ function SettingsContextController({
const [theme, setTheme] = useLocalStorageWithLog<Theme>(
LOCAL_STORAGE_BROWSER_THEME,
'auto',
newTheme => {
switch (theme) {
case 'light':
storeSettingInDeviceStorage(
LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
JSON.stringify(newTheme),
);
case 'dark':
storeSettingInDeviceStorage(
LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
JSON.stringify(newTheme),
);
case 'auto':
return;
}
},
);

const [
appendComponentStack,
setAppendComponentStack,
] = useLocalStorageWithLog<boolean>(
LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
true,
newAppendComponentStack =>
storeSettingInDeviceStorage(
LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
JSON.stringify(newAppendComponentStack),
),
);

const [
breakOnConsoleErrors,
setBreakOnConsoleErrors,
] = useLocalStorageWithLog<boolean>(
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
false,
newBreakOnConsoleErrors => {
storeSettingInDeviceStorage(
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
JSON.stringify(newBreakOnConsoleErrors),
);
},
);

const [parseHookNames, setParseHookNames] = useLocalStorageWithLog<boolean>(
LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
false,
Expand All @@ -138,13 +173,25 @@ function SettingsContextController({
] = useLocalStorageWithLog<boolean>(
LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
false,
newHideConsoleLogsInStrictMode => {
storeSettingInDeviceStorage(
LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
JSON.stringify(newHideConsoleLogsInStrictMode),
);
},
);
const [
showInlineWarningsAndErrors,
setShowInlineWarningsAndErrors,
] = useLocalStorageWithLog<boolean>(
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
true,
newShowInlineWarningsAndErrors => {
storeSettingInDeviceStorage(
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
JSON.stringify(newShowInlineWarningsAndErrors),
);
},
);
const [
traceUpdatesEnabled,
Expand Down
Loading