From f8024b0686c87634b233262e8a05e4a37a292e87 Mon Sep 17 00:00:00 2001 From: Edmond Chui <1967998+EdmondChuiHW@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:39:28 +0100 Subject: [PATCH] refactor: allow custom impl of backend realod-to-profile support check (#31048) ## Summary In preparation to support reload-to-profile in Fusebox (#31021), we need a way to check capability of different backends, e.g. web vs React Native. ## How did you test this change? * Default, e.g. existing web impl = no-op * Custom impl: is called --- packages/react-devtools-core/src/backend.js | 18 +++++- .../src/contentScripts/backendManager.js | 3 +- packages/react-devtools-inline/src/backend.js | 3 +- .../src/backend/agent.js | 16 ++---- .../src/backend/index.js | 5 ++ packages/react-devtools-shared/src/bridge.js | 3 +- .../src/devtools/store.js | 56 +++++-------------- packages/react-devtools-shared/src/utils.js | 13 +++++ 8 files changed, 57 insertions(+), 60 deletions(-) diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index 1f2055832a3dd..dfc3db15138c6 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -13,7 +13,10 @@ import {installHook} from 'react-devtools-shared/src/hook'; import {initBackend} from 'react-devtools-shared/src/backend'; import {__DEBUG__} from 'react-devtools-shared/src/constants'; import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor'; -import {getDefaultComponentFilters} from 'react-devtools-shared/src/utils'; +import { + getDefaultComponentFilters, + getIsReloadAndProfileSupported, +} from 'react-devtools-shared/src/utils'; import type {BackendBridge} from 'react-devtools-shared/src/bridge'; import type { @@ -36,6 +39,7 @@ type ConnectOptions = { isAppActive?: () => boolean, websocket?: ?WebSocket, onSettingsUpdated?: (settings: $ReadOnly) => void, + isReloadAndProfileSupported?: boolean, }; let savedComponentFilters: Array = @@ -77,6 +81,7 @@ export function connectToDevTools(options: ?ConnectOptions) { retryConnectionDelay = 2000, isAppActive = () => true, onSettingsUpdated, + isReloadAndProfileSupported = getIsReloadAndProfileSupported(), } = options || {}; const protocol = useHttps ? 'wss' : 'ws'; @@ -184,7 +189,7 @@ export function connectToDevTools(options: ?ConnectOptions) { hook.emit('shutdown'); }); - initBackend(hook, agent, window); + initBackend(hook, agent, window, isReloadAndProfileSupported); // Setup React Native style editor if the environment supports it. if (resolveRNStyle != null || hook.resolveRNStyle != null) { @@ -309,6 +314,7 @@ type ConnectWithCustomMessagingOptions = { nativeStyleEditorValidAttributes?: $ReadOnlyArray, resolveRNStyle?: ResolveNativeStyle, onSettingsUpdated?: (settings: $ReadOnly) => void, + isReloadAndProfileSupported?: boolean, }; export function connectWithCustomMessagingProtocol({ @@ -318,6 +324,7 @@ export function connectWithCustomMessagingProtocol({ nativeStyleEditorValidAttributes, resolveRNStyle, onSettingsUpdated, + isReloadAndProfileSupported = getIsReloadAndProfileSupported(), }: ConnectWithCustomMessagingOptions): Function { const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (hook == null) { @@ -368,7 +375,12 @@ export function connectWithCustomMessagingProtocol({ hook.emit('shutdown'); }); - const unsubscribeBackend = initBackend(hook, agent, window); + const unsubscribeBackend = initBackend( + hook, + agent, + window, + isReloadAndProfileSupported, + ); const nativeStyleResolver: ResolveNativeStyle | void = resolveRNStyle || hook.resolveRNStyle; diff --git a/packages/react-devtools-extensions/src/contentScripts/backendManager.js b/packages/react-devtools-extensions/src/contentScripts/backendManager.js index 36a2ab19aa411..9d3ec414330ed 100644 --- a/packages/react-devtools-extensions/src/contentScripts/backendManager.js +++ b/packages/react-devtools-extensions/src/contentScripts/backendManager.js @@ -13,6 +13,7 @@ import type { } from 'react-devtools-shared/src/backend/types'; import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils'; import {COMPACT_VERSION_NAME} from 'react-devtools-extensions/src/utils'; +import {getIsReloadAndProfileSupported} from 'react-devtools-shared/src/utils'; let welcomeHasInitialized = false; @@ -140,7 +141,7 @@ function activateBackend(version: string, hook: DevToolsHook) { hook.emit('shutdown'); }); - initBackend(hook, agent, window); + initBackend(hook, agent, window, getIsReloadAndProfileSupported()); // Setup React Native style editor if a renderer like react-native-web has injected it. if (typeof setupNativeStyleEditor === 'function' && hook.resolveRNStyle) { diff --git a/packages/react-devtools-inline/src/backend.js b/packages/react-devtools-inline/src/backend.js index fca1535c4e5ba..41af7809be0b1 100644 --- a/packages/react-devtools-inline/src/backend.js +++ b/packages/react-devtools-inline/src/backend.js @@ -8,6 +8,7 @@ import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyl import type {BackendBridge} from 'react-devtools-shared/src/bridge'; import type {Wall} from 'react-devtools-shared/src/frontend/types'; +import {getIsReloadAndProfileSupported} from 'react-devtools-shared/src/utils'; function startActivation(contentWindow: any, bridge: BackendBridge) { const onSavedPreferences = (data: $FlowFixMe) => { @@ -66,7 +67,7 @@ function finishActivation(contentWindow: any, bridge: BackendBridge) { const hook = contentWindow.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (hook) { - initBackend(hook, agent, contentWindow); + initBackend(hook, agent, contentWindow, getIsReloadAndProfileSupported()); // Setup React Native style editor if a renderer like react-native-web has injected it. if (hook.resolveRNStyle) { diff --git a/packages/react-devtools-shared/src/backend/agent.js b/packages/react-devtools-shared/src/backend/agent.js index e55e8a6d41e2b..a1e96bfcdeb39 100644 --- a/packages/react-devtools-shared/src/backend/agent.js +++ b/packages/react-devtools-shared/src/backend/agent.js @@ -38,7 +38,7 @@ import type { DevToolsHookSettings, } from './types'; import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types'; -import {isSynchronousXHRSupported, isReactNativeEnvironment} from './utils'; +import {isReactNativeEnvironment} from './utils'; const debug = (methodName: string, ...args: Array) => { if (__DEBUG__) { @@ -242,16 +242,6 @@ export default class Agent extends EventEmitter<{ if (this._isProfiling) { bridge.send('profilingStatus', true); } - - // Notify the frontend if the backend supports the Storage API (e.g. localStorage). - // If not, features like reload-and-profile will not work correctly and must be disabled. - let isBackendStorageAPISupported = false; - try { - localStorage.getItem('test'); - isBackendStorageAPISupported = true; - } catch (error) {} - bridge.send('isBackendStorageAPISupported', isBackendStorageAPISupported); - bridge.send('isSynchronousXHRSupported', isSynchronousXHRSupported()); } get rendererInterfaces(): {[key: RendererID]: RendererInterface, ...} { @@ -675,6 +665,10 @@ export default class Agent extends EventEmitter<{ } }; + onReloadAndProfileSupportedByHost: () => void = () => { + this._bridge.send('isReloadAndProfileSupportedByBackend', true); + }; + reloadAndProfile: (recordChangeDescriptions: boolean) => void = recordChangeDescriptions => { sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true'); diff --git a/packages/react-devtools-shared/src/backend/index.js b/packages/react-devtools-shared/src/backend/index.js index 5893424b394c8..86714b7f61476 100644 --- a/packages/react-devtools-shared/src/backend/index.js +++ b/packages/react-devtools-shared/src/backend/index.js @@ -17,6 +17,7 @@ export function initBackend( hook: DevToolsHook, agent: Agent, global: Object, + isReloadAndProfileSupported: boolean, ): () => void { if (hook == null) { // DevTools didn't get injected into this page (maybe b'c of the contentType). @@ -94,6 +95,10 @@ export function initBackend( } }); + if (isReloadAndProfileSupported) { + agent.onReloadAndProfileSupportedByHost(); + } + return () => { subs.forEach(fn => fn()); }; diff --git a/packages/react-devtools-shared/src/bridge.js b/packages/react-devtools-shared/src/bridge.js index fcf9b2d21e88d..dde6e7c3ffff8 100644 --- a/packages/react-devtools-shared/src/bridge.js +++ b/packages/react-devtools-shared/src/bridge.js @@ -181,8 +181,7 @@ export type BackendEvents = { fastRefreshScheduled: [], getSavedPreferences: [], inspectedElement: [InspectedElementPayload], - isBackendStorageAPISupported: [boolean], - isSynchronousXHRSupported: [boolean], + isReloadAndProfileSupportedByBackend: [boolean], operations: [Array], ownersList: [OwnersList], overrideComponentFilters: [Array], diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index b1544126d8d6e..8af997d9287ef 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -138,16 +138,6 @@ export default class Store extends EventEmitter<{ // Should the React Native style editor panel be shown? _isNativeStyleEditorSupported: boolean = false; - // Can the backend use the Storage API (e.g. localStorage)? - // If not, features like reload-and-profile will not work correctly and must be disabled. - _isBackendStorageAPISupported: boolean = false; - - // Can DevTools use sync XHR requests? - // If not, features like reload-and-profile will not work correctly and must be disabled. - // This current limitation applies only to web extension builds - // and will need to be reconsidered in the future if we add support for reload to React Native. - _isSynchronousXHRSupported: boolean = false; - _nativeStyleEditorValidAttributes: $ReadOnlyArray | null = null; // Older backends don't support an explicit bridge protocol, @@ -178,10 +168,12 @@ export default class Store extends EventEmitter<{ // These options may be initially set by a configuration option when constructing the Store. _supportsInspectMatchingDOMElement: boolean = false; _supportsClickToInspect: boolean = false; - _supportsReloadAndProfile: boolean = false; _supportsTimeline: boolean = false; _supportsTraceUpdates: boolean = false; + _isReloadAndProfileFrontendSupported: boolean = false; + _isReloadAndProfileBackendSupported: boolean = false; + // These options default to false but may be updated as roots are added and removed. _rootSupportsBasicProfiling: boolean = false; _rootSupportsTimelineProfiling: boolean = false; @@ -234,7 +226,7 @@ export default class Store extends EventEmitter<{ this._supportsClickToInspect = true; } if (supportsReloadAndProfile) { - this._supportsReloadAndProfile = true; + this._isReloadAndProfileFrontendSupported = true; } if (supportsTimeline) { this._supportsTimeline = true; @@ -255,17 +247,13 @@ export default class Store extends EventEmitter<{ ); bridge.addListener('shutdown', this.onBridgeShutdown); bridge.addListener( - 'isBackendStorageAPISupported', - this.onBackendStorageAPISupported, + 'isReloadAndProfileSupportedByBackend', + this.onBackendReloadAndProfileSupported, ); bridge.addListener( 'isNativeStyleEditorSupported', this.onBridgeNativeStyleEditorSupported, ); - bridge.addListener( - 'isSynchronousXHRSupported', - this.onBridgeSynchronousXHRSupported, - ); bridge.addListener( 'unsupportedRendererVersion', this.onBridgeUnsupportedRendererVersion, @@ -469,13 +457,9 @@ export default class Store extends EventEmitter<{ } get supportsReloadAndProfile(): boolean { - // Does the DevTools shell support reloading and eagerly injecting the renderer interface? - // And if so, can the backend use the localStorage API and sync XHR? - // All of these are currently required for the reload-and-profile feature to work. return ( - this._supportsReloadAndProfile && - this._isBackendStorageAPISupported && - this._isSynchronousXHRSupported + this._isReloadAndProfileFrontendSupported && + this._isReloadAndProfileBackendSupported ); } @@ -1433,17 +1417,13 @@ export default class Store extends EventEmitter<{ ); bridge.removeListener('shutdown', this.onBridgeShutdown); bridge.removeListener( - 'isBackendStorageAPISupported', - this.onBackendStorageAPISupported, + 'isReloadAndProfileSupportedByBackend', + this.onBackendReloadAndProfileSupported, ); bridge.removeListener( 'isNativeStyleEditorSupported', this.onBridgeNativeStyleEditorSupported, ); - bridge.removeListener( - 'isSynchronousXHRSupported', - this.onBridgeSynchronousXHRSupported, - ); bridge.removeListener( 'unsupportedRendererVersion', this.onBridgeUnsupportedRendererVersion, @@ -1458,18 +1438,10 @@ export default class Store extends EventEmitter<{ } }; - onBackendStorageAPISupported: ( - isBackendStorageAPISupported: boolean, - ) => void = isBackendStorageAPISupported => { - this._isBackendStorageAPISupported = isBackendStorageAPISupported; - - this.emit('supportsReloadAndProfile'); - }; - - onBridgeSynchronousXHRSupported: ( - isSynchronousXHRSupported: boolean, - ) => void = isSynchronousXHRSupported => { - this._isSynchronousXHRSupported = isSynchronousXHRSupported; + onBackendReloadAndProfileSupported: ( + isReloadAndProfileSupported: boolean, + ) => void = isReloadAndProfileSupported => { + this._isReloadAndProfileBackendSupported = isReloadAndProfileSupported; this.emit('supportsReloadAndProfile'); }; diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index e24414812c5b3..a9ebeaaa129da 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -61,6 +61,7 @@ import type { LRUCache, } from 'react-devtools-shared/src/frontend/types'; import type {SerializedElement as SerializedElementBackend} from 'react-devtools-shared/src/backend/types'; +import {isSynchronousXHRSupported} from './backend/utils'; // $FlowFixMe[method-unbinding] const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -965,3 +966,15 @@ export function backendToFrontendSerializedElementMapper( export function normalizeUrl(url: string): string { return url.replace('/./', '/'); } + +export function getIsReloadAndProfileSupported(): boolean { + // Notify the frontend if the backend supports the Storage API (e.g. localStorage). + // If not, features like reload-and-profile will not work correctly and must be disabled. + let isBackendStorageAPISupported = false; + try { + localStorage.getItem('test'); + isBackendStorageAPISupported = true; + } catch (error) {} + + return isBackendStorageAPISupported && isSynchronousXHRSupported(); +}