diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index dfc3db15138c6..54a6b9b48a99d 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -26,6 +26,8 @@ import type { import type { DevToolsHook, DevToolsHookSettings, + ReloadAndProfileConfig, + ReloadAndProfileConfigPersistence, } from 'react-devtools-shared/src/backend/types'; import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; @@ -40,6 +42,7 @@ type ConnectOptions = { websocket?: ?WebSocket, onSettingsUpdated?: (settings: $ReadOnly) => void, isReloadAndProfileSupported?: boolean, + reloadAndProfileConfigPersistence?: ReloadAndProfileConfigPersistence, }; let savedComponentFilters: Array = @@ -60,8 +63,9 @@ export function initialize( maybeSettingsOrSettingsPromise?: | DevToolsHookSettings | Promise, + reloadAndProfileConfig?: ReloadAndProfileConfig, ) { - installHook(window, maybeSettingsOrSettingsPromise); + installHook(window, maybeSettingsOrSettingsPromise, reloadAndProfileConfig); } export function connectToDevTools(options: ?ConnectOptions) { @@ -82,6 +86,7 @@ export function connectToDevTools(options: ?ConnectOptions) { isAppActive = () => true, onSettingsUpdated, isReloadAndProfileSupported = getIsReloadAndProfileSupported(), + reloadAndProfileConfigPersistence, } = options || {}; const protocol = useHttps ? 'wss' : 'ws'; @@ -175,7 +180,7 @@ export function connectToDevTools(options: ?ConnectOptions) { // TODO (npm-packages) Warn if "isBackendStorageAPISupported" // $FlowFixMe[incompatible-call] found when upgrading Flow - const agent = new Agent(bridge); + const agent = new Agent(bridge, reloadAndProfileConfigPersistence); if (onSettingsUpdated != null) { agent.addListener('updateHookSettings', onSettingsUpdated); } @@ -315,6 +320,7 @@ type ConnectWithCustomMessagingOptions = { resolveRNStyle?: ResolveNativeStyle, onSettingsUpdated?: (settings: $ReadOnly) => void, isReloadAndProfileSupported?: boolean, + reloadAndProfileConfigPersistence?: ReloadAndProfileConfigPersistence, }; export function connectWithCustomMessagingProtocol({ @@ -325,6 +331,7 @@ export function connectWithCustomMessagingProtocol({ resolveRNStyle, onSettingsUpdated, isReloadAndProfileSupported = getIsReloadAndProfileSupported(), + reloadAndProfileConfigPersistence, }: ConnectWithCustomMessagingOptions): Function { const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (hook == null) { @@ -361,7 +368,7 @@ export function connectWithCustomMessagingProtocol({ bridge.send('overrideComponentFilters', savedComponentFilters); } - const agent = new Agent(bridge); + const agent = new Agent(bridge, reloadAndProfileConfigPersistence); if (onSettingsUpdated != null) { agent.addListener('updateHookSettings', onSettingsUpdated); } diff --git a/packages/react-devtools-fusebox/src/frontend.d.ts b/packages/react-devtools-fusebox/src/frontend.d.ts index 8a62ad54e504c..74a88c36a85f8 100644 --- a/packages/react-devtools-fusebox/src/frontend.d.ts +++ b/packages/react-devtools-fusebox/src/frontend.d.ts @@ -19,9 +19,12 @@ export type Bridge = { }; export type Store = Object; export type BrowserTheme = 'dark' | 'light'; +export type Config = { + supportsReloadAndProfile?: boolean, +}; export function createBridge(wall: Wall): Bridge; -export function createStore(bridge: Bridge): Store; +export function createStore(bridge: Bridge, config?: Config): Store; export type Source = { sourceURL: string, diff --git a/packages/react-devtools-shared/src/attachRenderer.js b/packages/react-devtools-shared/src/attachRenderer.js index 3138f00cad615..cd7a348b65d71 100644 --- a/packages/react-devtools-shared/src/attachRenderer.js +++ b/packages/react-devtools-shared/src/attachRenderer.js @@ -13,6 +13,7 @@ import type { DevToolsHook, RendererID, } from 'react-devtools-shared/src/backend/types'; +import type {ReloadAndProfileConfig} from './backend/types'; import {attach as attachFlight} from 'react-devtools-shared/src/backend/flight/renderer'; import {attach as attachFiber} from 'react-devtools-shared/src/backend/fiber/renderer'; @@ -29,6 +30,7 @@ export default function attachRenderer( id: RendererID, renderer: ReactRenderer, global: Object, + reloadAndProfileConfig: ReloadAndProfileConfig, ): RendererInterface | void { // only attach if the renderer is compatible with the current version of the backend if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) { @@ -48,7 +50,13 @@ export default function attachRenderer( renderer.currentDispatcherRef != null ) { // react-reconciler v16+ - rendererInterface = attachFiber(hook, id, renderer, global); + rendererInterface = attachFiber( + hook, + id, + renderer, + global, + reloadAndProfileConfig, + ); } else if (renderer.ComponentTree) { // react-dom v15 rendererInterface = attachLegacy(hook, id, renderer, global); diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index a1e96bfcdeb39..88450fe29ebc9 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -8,17 +8,7 @@ */ import EventEmitter from '../events'; -import { - SESSION_STORAGE_LAST_SELECTION_KEY, - SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, - SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, - __DEBUG__, -} from '../constants'; -import { - sessionStorageGetItem, - sessionStorageRemoveItem, - sessionStorageSetItem, -} from 'react-devtools-shared/src/storage'; +import {SESSION_STORAGE_LAST_SELECTION_KEY, __DEBUG__} from '../constants'; import setupHighlighter from './views/Highlighter'; import { initialize as setupTraceUpdates, @@ -36,9 +26,16 @@ import type { RendererID, RendererInterface, DevToolsHookSettings, + ReloadAndProfileConfigPersistence, } from './types'; import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; import {isReactNativeEnvironment} from './utils'; +import {defaultReloadAndProfileConfigPersistence} from '../utils'; +import { + sessionStorageGetItem, + sessionStorageRemoveItem, + sessionStorageSetItem, +} from '../storage'; const debug = (methodName: string, ...args: Array) => { if (__DEBUG__) { @@ -159,21 +156,27 @@ export default class Agent extends EventEmitter<{ _persistedSelection: PersistedSelection | null = null; _persistedSelectionMatch: PathMatch | null = null; _traceUpdatesEnabled: boolean = false; + _reloadAndProfileConfigPersistence: ReloadAndProfileConfigPersistence; - constructor(bridge: BackendBridge) { + constructor( + bridge: BackendBridge, + reloadAndProfileConfigPersistence?: ReloadAndProfileConfigPersistence = defaultReloadAndProfileConfigPersistence, + ) { super(); - if ( - sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true' - ) { + this._reloadAndProfileConfigPersistence = reloadAndProfileConfigPersistence; + const {getReloadAndProfileConfig, setReloadAndProfileConfig} = + reloadAndProfileConfigPersistence; + const reloadAndProfileConfig = getReloadAndProfileConfig(); + if (reloadAndProfileConfig.shouldReloadAndProfile) { this._recordChangeDescriptions = - sessionStorageGetItem( - SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, - ) === 'true'; + reloadAndProfileConfig.recordChangeDescriptions; this._isProfiling = true; - sessionStorageRemoveItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY); - sessionStorageRemoveItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY); + setReloadAndProfileConfig({ + shouldReloadAndProfile: false, + recordChangeDescriptions: false, + }); } const persistedSelectionString = sessionStorageGetItem( @@ -671,11 +674,10 @@ export default class Agent extends EventEmitter<{ reloadAndProfile: (recordChangeDescriptions: boolean) => void = recordChangeDescriptions => { - sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true'); - sessionStorageSetItem( - SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, - recordChangeDescriptions ? 'true' : 'false', - ); + this._reloadAndProfileConfigPersistence.setReloadAndProfileConfig({ + shouldReloadAndProfile: true, + recordChangeDescriptions, + }); // This code path should only be hit if the shell has explicitly told the Store that it supports profiling. // In that case, the shell must also listen for this specific message to know when it needs to reload the app. diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 22a7afcc4c632..4fac8839ae799 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -42,7 +42,6 @@ import { utfEncodeString, filterOutLocationComponentFilters, } from 'react-devtools-shared/src/utils'; -import {sessionStorageGetItem} from 'react-devtools-shared/src/storage'; import { formatConsoleArgumentsToSingleString, gt, @@ -61,8 +60,6 @@ import { __DEBUG__, PROFILING_FLAG_BASIC_SUPPORT, PROFILING_FLAG_TIMELINE_SUPPORT, - SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, - SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, TREE_OPERATION_REORDER_CHILDREN, @@ -106,6 +103,7 @@ import { supportsOwnerStacks, supportsConsoleTasks, } from './DevToolsFiberComponentStack'; +import type {ReloadAndProfileConfig} from '../types'; // $FlowFixMe[method-unbinding] const toString = Object.prototype.toString; @@ -865,6 +863,7 @@ export function attach( rendererID: number, renderer: ReactRenderer, global: Object, + reloadAndProfileConfig: ReloadAndProfileConfig, ): RendererInterface { // Newer versions of the reconciler package also specific reconciler version. // If that version number is present, use it. @@ -5213,13 +5212,10 @@ export function attach( } // Automatically start profiling so that we don't miss timing info from initial "mount". - if ( - sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === 'true' - ) { - startProfiling( - sessionStorageGetItem(SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY) === - 'true', - ); + if (reloadAndProfileConfig.shouldReloadAndProfile) { + const shouldRecordChangeDescriptions = + reloadAndProfileConfig.recordChangeDescriptions; + startProfiling(shouldRecordChangeDescriptions); } function getNearestFiber(devtoolsInstance: DevToolsInstance): null | Fiber { diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index c3110dc517fdc..c6f743546e49e 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -485,6 +485,20 @@ export type DevToolsBackend = { setupNativeStyleEditor?: SetupNativeStyleEditor, }; +export type ReloadAndProfileConfig = { + shouldReloadAndProfile: boolean, + recordChangeDescriptions: boolean, +}; + +// Linter doesn't speak Flow's `Partial` type +// eslint-disable-next-line no-undef +type PartialReloadAndProfileConfig = Partial; + +export type ReloadAndProfileConfigPersistence = { + setReloadAndProfileConfig: (config: PartialReloadAndProfileConfig) => void, + getReloadAndProfileConfig: () => ReloadAndProfileConfig, +}; + export type DevToolsHook = { listeners: {[key: string]: Array, ...}, rendererInterfaces: Map, diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 1916a8c93822c..3b45c7417d4e0 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -16,6 +16,7 @@ import type { RendererInterface, DevToolsBackend, DevToolsHookSettings, + ReloadAndProfileConfig, } from './backend/types'; import { @@ -26,6 +27,7 @@ import { import attachRenderer from './attachRenderer'; import formatConsoleArguments from 'react-devtools-shared/src/backend/utils/formatConsoleArguments'; import formatWithStyles from 'react-devtools-shared/src/backend/utils/formatWithStyles'; +import {defaultReloadAndProfileConfigPersistence} from './utils'; // React's custom built component stack strings match "\s{4}in" // Chrome's prefix matches "\s{4}at" @@ -54,6 +56,7 @@ export function installHook( maybeSettingsOrSettingsPromise?: | DevToolsHookSettings | Promise, + reloadAndProfileConfig?: ReloadAndProfileConfig = defaultReloadAndProfileConfigPersistence.getReloadAndProfileConfig(), ): DevToolsHook | null { if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { return null; @@ -207,7 +210,13 @@ export function installHook( reactBuildType, }); - const rendererInterface = attachRenderer(hook, id, renderer, target); + const rendererInterface = attachRenderer( + hook, + id, + renderer, + target, + reloadAndProfileConfig, + ); if (rendererInterface != null) { hook.rendererInterfaces.set(id, rendererInterface); hook.emit('renderer-attached', {id, rendererInterface}); diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index a9ebeaaa129da..715834334f360 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -36,6 +36,8 @@ import { TREE_OPERATION_UPDATE_TREE_BASE_DURATION, LOCAL_STORAGE_COMPONENT_FILTER_PREFERENCES_KEY, LOCAL_STORAGE_OPEN_IN_EDITOR_URL, + SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, + SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, } from './constants'; import { ComponentFilterElementType, @@ -50,7 +52,12 @@ import { ElementTypeMemo, ElementTypeVirtual, } from 'react-devtools-shared/src/frontend/types'; -import {localStorageGetItem, localStorageSetItem} from './storage'; +import { + localStorageGetItem, + localStorageSetItem, + sessionStorageGetItem, + sessionStorageSetItem, +} from './storage'; import {meta} from './hydration'; import isArray from './isArray'; @@ -62,6 +69,10 @@ import type { } from 'react-devtools-shared/src/frontend/types'; import type {SerializedElement as SerializedElementBackend} from 'react-devtools-shared/src/backend/types'; import {isSynchronousXHRSupported} from './backend/utils'; +import type { + ReloadAndProfileConfig, + ReloadAndProfileConfigPersistence, +} from './backend/types'; // $FlowFixMe[method-unbinding] const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -978,3 +989,35 @@ export function getIsReloadAndProfileSupported(): boolean { return isBackendStorageAPISupported && isSynchronousXHRSupported(); } + +export const defaultReloadAndProfileConfigPersistence: ReloadAndProfileConfigPersistence = + { + setReloadAndProfileConfig({ + shouldReloadAndProfile, + recordChangeDescriptions, + }): void { + if (shouldReloadAndProfile != null) { + sessionStorageSetItem( + SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, + shouldReloadAndProfile ? 'true' : 'false', + ); + } + if (recordChangeDescriptions != null) { + sessionStorageSetItem( + SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, + recordChangeDescriptions ? 'true' : 'false', + ); + } + }, + getReloadAndProfileConfig(): ReloadAndProfileConfig { + return { + shouldReloadAndProfile: + sessionStorageGetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY) === + 'true', + recordChangeDescriptions: + sessionStorageGetItem( + SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY, + ) === 'true', + }; + }, + };