mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Stacked on https://github.com/facebook/react/pull/31131. See last commit. This is a clean-up and a pre-requisite for next changes: 1. `ReloadAndProfileConfig` is now split into boolean value and settings object. This is mainly because I will add one more setting soon, and also because settings might be persisted for a longer time than the flag which signals if the Backend was reloaded for profiling. Ideally, this settings should probably be moved to the global Hook object, same as we did for console patching. 2. Host is now responsible for reseting the cached values, Backend will execute provided `onReloadAndProfileFlagsReset` callback.
211 lines
6.4 KiB
JavaScript
211 lines
6.4 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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 {
|
|
DevToolsHook,
|
|
ReactRenderer,
|
|
} 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';
|
|
import {
|
|
getIfReloadedAndProfiling,
|
|
onReloadAndProfile,
|
|
onReloadAndProfileFlagsReset,
|
|
} from 'react-devtools-shared/src/utils';
|
|
|
|
let welcomeHasInitialized = false;
|
|
const requiredBackends = new Set<string>();
|
|
|
|
function welcome(event: $FlowFixMe) {
|
|
if (
|
|
event.source !== window ||
|
|
event.data.source !== 'react-devtools-content-script'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// In some circumstances, this method is called more than once for a single welcome message.
|
|
// The exact circumstances of this are unclear, though it seems related to 3rd party event batching code.
|
|
//
|
|
// Regardless, call this method multiple times can cause DevTools to add duplicate elements to the Store
|
|
// (and throw an error) or worse yet, choke up entirely and freeze the browser.
|
|
//
|
|
// The simplest solution is to ignore the duplicate events.
|
|
// To be clear, this SHOULD NOT BE NECESSARY, since we remove the event handler below.
|
|
//
|
|
// See https://github.com/facebook/react/issues/24162
|
|
if (welcomeHasInitialized) {
|
|
console.warn(
|
|
'React DevTools detected duplicate welcome "message" events from the content script.',
|
|
);
|
|
return;
|
|
}
|
|
|
|
welcomeHasInitialized = true;
|
|
|
|
window.removeEventListener('message', welcome);
|
|
|
|
setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
|
|
}
|
|
|
|
function setup(hook: ?DevToolsHook) {
|
|
// this should not happen, but Chrome can be weird sometimes
|
|
if (hook == null) {
|
|
return;
|
|
}
|
|
|
|
// register renderers that have already injected themselves.
|
|
hook.renderers.forEach(renderer => {
|
|
registerRenderer(renderer, hook);
|
|
});
|
|
|
|
// Activate and remove from required all present backends, registered within the hook
|
|
hook.backends.forEach((_, backendVersion) => {
|
|
requiredBackends.delete(backendVersion);
|
|
activateBackend(backendVersion, hook);
|
|
});
|
|
|
|
updateRequiredBackends();
|
|
|
|
// register renderers that inject themselves later.
|
|
const unsubscribeRendererListener = hook.sub('renderer', ({renderer}) => {
|
|
registerRenderer(renderer, hook);
|
|
updateRequiredBackends();
|
|
});
|
|
|
|
// listen for backend installations.
|
|
const unsubscribeBackendInstallationListener = hook.sub(
|
|
'devtools-backend-installed',
|
|
version => {
|
|
activateBackend(version, hook);
|
|
updateRequiredBackends();
|
|
},
|
|
);
|
|
|
|
const unsubscribeShutdownListener: () => void = hook.sub('shutdown', () => {
|
|
unsubscribeRendererListener();
|
|
unsubscribeBackendInstallationListener();
|
|
unsubscribeShutdownListener();
|
|
});
|
|
}
|
|
|
|
function registerRenderer(renderer: ReactRenderer, hook: DevToolsHook) {
|
|
let version = renderer.reconcilerVersion || renderer.version;
|
|
if (!hasAssignedBackend(version)) {
|
|
version = COMPACT_VERSION_NAME;
|
|
}
|
|
|
|
// Check if required backend is already activated, no need to require again
|
|
if (!hook.backends.has(version)) {
|
|
requiredBackends.add(version);
|
|
}
|
|
}
|
|
|
|
function activateBackend(version: string, hook: DevToolsHook) {
|
|
const backend = hook.backends.get(version);
|
|
if (!backend) {
|
|
throw new Error(`Could not find backend for version "${version}"`);
|
|
}
|
|
|
|
const {Agent, Bridge, initBackend, setupNativeStyleEditor} = backend;
|
|
const bridge = new Bridge({
|
|
listen(fn) {
|
|
const listener = (event: $FlowFixMe) => {
|
|
if (
|
|
event.source !== window ||
|
|
!event.data ||
|
|
event.data.source !== 'react-devtools-content-script' ||
|
|
!event.data.payload
|
|
) {
|
|
return;
|
|
}
|
|
fn(event.data.payload);
|
|
};
|
|
window.addEventListener('message', listener);
|
|
return () => {
|
|
window.removeEventListener('message', listener);
|
|
};
|
|
},
|
|
send(event: string, payload: any, transferable?: Array<any>) {
|
|
window.postMessage(
|
|
{
|
|
source: 'react-devtools-bridge',
|
|
payload: {event, payload},
|
|
},
|
|
'*',
|
|
transferable,
|
|
);
|
|
},
|
|
});
|
|
|
|
const agent = new Agent(
|
|
bridge,
|
|
getIfReloadedAndProfiling(),
|
|
onReloadAndProfile,
|
|
);
|
|
// Agent read flags successfully, we can count it as successful launch
|
|
// Clean up flags, so that next reload won't start profiling
|
|
onReloadAndProfileFlagsReset();
|
|
|
|
agent.addListener('shutdown', () => {
|
|
// If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
|
|
// and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
|
|
hook.emit('shutdown');
|
|
delete window.__REACT_DEVTOOLS_BACKEND_MANAGER_INJECTED__;
|
|
});
|
|
|
|
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) {
|
|
setupNativeStyleEditor(
|
|
bridge,
|
|
agent,
|
|
hook.resolveRNStyle,
|
|
hook.nativeStyleEditorValidAttributes,
|
|
);
|
|
}
|
|
|
|
// Let the frontend know that the backend has attached listeners and is ready for messages.
|
|
// This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
|
|
bridge.send('extensionBackendInitialized');
|
|
|
|
// this backend is activated
|
|
requiredBackends.delete(version);
|
|
}
|
|
|
|
// tell the service worker which versions of backends are needed for the current page
|
|
function updateRequiredBackends() {
|
|
if (requiredBackends.size === 0) {
|
|
return;
|
|
}
|
|
|
|
window.postMessage(
|
|
{
|
|
source: 'react-devtools-backend-manager',
|
|
payload: {
|
|
type: 'require-backends',
|
|
versions: Array.from(requiredBackends),
|
|
},
|
|
},
|
|
'*',
|
|
);
|
|
}
|
|
|
|
/*
|
|
* Make sure this is executed only once in case Frontend is reloaded multiple times while Backend is initializing
|
|
* We can't use `reactDevToolsAgent` field on a global Hook object, because it only cleaned up after both Frontend and Backend initialized
|
|
*/
|
|
if (!window.__REACT_DEVTOOLS_BACKEND_MANAGER_INJECTED__) {
|
|
window.__REACT_DEVTOOLS_BACKEND_MANAGER_INJECTED__ = true;
|
|
|
|
window.addEventListener('message', welcome);
|
|
}
|