/*** 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 Agent from 'react-devtools-shared/src/backend/agent';
import Bridge from 'react-devtools-shared/src/bridge';
import {installHook} from 'react-devtools-shared/src/hook';
import {initBackend} from 'react-devtools-shared/src/backend';
import {installConsoleFunctionsToWindow} from 'react-devtools-shared/src/backend/console';
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 {
initializeUsingCachedSettings,
cacheConsolePatchSettings,
type DevToolsSettingsManager,
} from './cachedSettings';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
import type {DevToolsHook} from 'react-devtools-shared/src/backend/types';
import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
type ConnectOptions = {
host?: string,
nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
port?: number,
useHttps?: boolean,
resolveRNStyle?: ResolveNativeStyle,
retryConnectionDelay?: number,
isAppActive?: () => boolean,
websocket?: ?WebSocket,
devToolsSettingsManager: ?DevToolsSettingsManager,
...};// Install a global variable to allow patching console early (during injection).// This provides React Native developers with components stacks even if they don't run DevTools.installConsoleFunctionsToWindow();
installHook(window);
const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
let savedComponentFilters: Array<ComponentFilter> =
getDefaultComponentFilters();
function debug(methodName: string, ...args: Array<mixed>) {
if (__DEBUG__) {
console.log(
`%c[core/backend] %c${methodName}`,
'color: teal; font-weight: bold;',
'font-weight: bold;',
...args,
);}}export function connectToDevTools(options: ?ConnectOptions) {
if (hook == null) {
// DevTools didn't get injected into this page (maybe b'c of the contentType).
return;
}const {
host = 'localhost',
nativeStyleEditorValidAttributes,
useHttps = false,
port = 8097,
websocket,
resolveRNStyle = (null: $FlowFixMe),
retryConnectionDelay = 2000,
isAppActive = () => true,
devToolsSettingsManager,
} = options || {};
const protocol = useHttps ? 'wss' : 'ws';
let retryTimeoutID: TimeoutID | null = null;
function scheduleRetry() {
if (retryTimeoutID === null) {
// Two seconds because RN had issues with quick retries.
retryTimeoutID = setTimeout(
() => connectToDevTools(options),
retryConnectionDelay,
);}}if (devToolsSettingsManager != null) {
try {
initializeUsingCachedSettings(devToolsSettingsManager);
} catch (e) {
// If we call a method on devToolsSettingsManager that throws, or if
// is invalid data read out, don't throw and don't interrupt initialization
console.error(e);
}}if (!isAppActive()) {
// If the app is in background, maybe retry later.
// Don't actually attempt to connect until we're in foreground.
scheduleRetry();
return;
}let bridge: BackendBridge | null = null;
const messageListeners = [];
const uri = protocol + '://' + host + ':' + port;
// If existing websocket is passed, use it.
// This is necessary to support our custom integrations.
// See D6251744.
const ws = websocket ? websocket : new window.WebSocket(uri);
ws.onclose = handleClose;
ws.onerror = handleFailed;
ws.onmessage = handleMessage;
ws.onopen = function () {
bridge = new Bridge({
listen(fn) {
messageListeners.push(fn);
return () => {
const index = messageListeners.indexOf(fn);
if (index >= 0) {
messageListeners.splice(index, 1);
}};},send(event: string, payload: any, transferable?: Array<any>) {
if (ws.readyState === ws.OPEN) {
if (__DEBUG__) {
debug('wall.send()', event, payload);
}ws.send(JSON.stringify({event, payload}));
} else {
if (__DEBUG__) {
debug(
'wall.send()',
'Shutting down bridge because of closed WebSocket connection',
);}if (bridge !== null) {
bridge.shutdown();
}scheduleRetry();
}},});bridge.addListener(
'updateComponentFilters',
(componentFilters: Array<ComponentFilter>) => {
// Save filter changes in memory, in case DevTools is reloaded.
// In that case, the renderer will already be using the updated values.
// We'll lose these in between backend reloads but that can't be helped.
savedComponentFilters = componentFilters;
},);if (devToolsSettingsManager != null && bridge != null) {
bridge.addListener('updateConsolePatchSettings', consolePatchSettings =>
cacheConsolePatchSettings(
devToolsSettingsManager,
consolePatchSettings,
),);}// The renderer interface doesn't read saved component filters directly,
// because they are generally stored in localStorage within the context of the extension.
// Because of this it relies on the extension to pass filters.
// In the case of the standalone DevTools being used with a website,
// saved filters are injected along with the backend script tag so we shouldn't override them here.
// This injection strategy doesn't work for React Native though.
// Ideally the backend would save the filters itself, but RN doesn't provide a sync storage solution.
// So for now we just fall back to using the default filters...
if (window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ == null) {
// $FlowFixMe[incompatible-use] found when upgrading Flow
bridge.send('overrideComponentFilters', savedComponentFilters);
}// TODO (npm-packages) Warn if "isBackendStorageAPISupported"
// $FlowFixMe[incompatible-call] found when upgrading Flow
const agent = new Agent(bridge);
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');
});initBackend(hook, agent, window);
// Setup React Native style editor if the environment supports it.
if (resolveRNStyle != null || hook.resolveRNStyle != null) {
setupNativeStyleEditor(
// $FlowFixMe[incompatible-call] found when upgrading Flow
bridge,
agent,
((resolveRNStyle || hook.resolveRNStyle: any): ResolveNativeStyle),
nativeStyleEditorValidAttributes ||
hook.nativeStyleEditorValidAttributes ||
null,
);} else {
// Otherwise listen to detect if the environment later supports it.
// For example, Flipper does not eagerly inject these values.
// Instead it relies on the React Native Inspector to lazily inject them.
let lazyResolveRNStyle;
let lazyNativeStyleEditorValidAttributes;
const initAfterTick = () => {
if (bridge !== null) {
setupNativeStyleEditor(
bridge,
agent,
lazyResolveRNStyle,
lazyNativeStyleEditorValidAttributes,
);}};if (!hook.hasOwnProperty('resolveRNStyle')) {
Object.defineProperty(
hook,
'resolveRNStyle',
({enumerable: false,
get() {
return lazyResolveRNStyle;
},set(value: $FlowFixMe) {
lazyResolveRNStyle = value;
initAfterTick();
},}: Object),
);}if (!hook.hasOwnProperty('nativeStyleEditorValidAttributes')) {
Object.defineProperty(
hook,
'nativeStyleEditorValidAttributes',
({enumerable: false,
get() {
return lazyNativeStyleEditorValidAttributes;
},set(value: $FlowFixMe) {
lazyNativeStyleEditorValidAttributes = value;
initAfterTick();
},}: Object),
);}}};function handleClose() {
if (__DEBUG__) {
debug('WebSocket.onclose');
}if (bridge !== null) {
bridge.emit('shutdown');
}scheduleRetry();
}function handleFailed() {
if (__DEBUG__) {
debug('WebSocket.onerror');
}scheduleRetry();
}function handleMessage(event: MessageEvent) {
let data;
try {
if (typeof event.data === 'string') {
data = JSON.parse(event.data);
if (__DEBUG__) {
debug('WebSocket.onmessage', data);
}} else {
throw Error();
}} catch (e) {
console.error(
'[React DevTools] Failed to parse JSON: ' + (event.data: any),
);return;
}messageListeners.forEach(fn => {
try {
fn(data);
} catch (error) {
// jsc doesn't play so well with tracebacks that go into eval'd code,
// so the stack trace here will stop at the `eval()` call. Getting the
// message that caused the error is the best we can do for now.
console.log('[React DevTools] Error calling listener', data);
console.log('error:', error);
throw error;
}});}}