refactor: allow custom impl of backend realod-to-profile support check (#31048)

<!--
  Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.

Before submitting a pull request, please make sure the following is
done:

1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
  2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
  9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
  10. If you haven't already, complete the CLA.

Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->

## 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?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->

* Default, e.g. existing web impl = no-op
* Custom impl: is called
This commit is contained in:
Edmond Chui
2024-09-26 12:39:28 +01:00
committed by GitHub
parent d66fa02a30
commit f8024b0686
8 changed files with 57 additions and 60 deletions

View File

@@ -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<DevToolsHookSettings>) => void,
isReloadAndProfileSupported?: boolean,
};
let savedComponentFilters: Array<ComponentFilter> =
@@ -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<string>,
resolveRNStyle?: ResolveNativeStyle,
onSettingsUpdated?: (settings: $ReadOnly<DevToolsHookSettings>) => 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;

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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<string>) => {
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');

View File

@@ -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());
};

View File

@@ -181,8 +181,7 @@ export type BackendEvents = {
fastRefreshScheduled: [],
getSavedPreferences: [],
inspectedElement: [InspectedElementPayload],
isBackendStorageAPISupported: [boolean],
isSynchronousXHRSupported: [boolean],
isReloadAndProfileSupportedByBackend: [boolean],
operations: [Array<number>],
ownersList: [OwnersList],
overrideComponentFilters: [Array<ComponentFilter>],

View File

@@ -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<string> | 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');
};

View File

@@ -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();
}