1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @flow
8
+ */
9
+
10
+import type {
11
+ DevToolsHook,
12
+ ReactRenderer,
13
+} from 'react-devtools-shared/src/backend/types';
14
+import {hasAssignedBackend} from 'react-devtools-shared/src/backend/utils';
15
+import {COMPACT_VERSION_NAME} from './utils';
16
+
17
+let welcomeHasInitialized = false;
18
+
19
+// $FlowFixMe[missing-local-annot]
20
+function welcome(event: $FlowFixMe) {
21
+ if (
22
+ event.source !== window ||
23
+ event.data.source !== 'react-devtools-content-script'
24
+ ) {
25
+ return;
26
+ }
27
+
28
+ // In some circumstances, this method is called more than once for a single welcome message.
29
+ // The exact circumstances of this are unclear, though it seems related to 3rd party event batching code.
30
+ //
31
+ // Regardless, call this method multiple times can cause DevTools to add duplicate elements to the Store
32
+ // (and throw an error) or worse yet, choke up entirely and freeze the browser.
33
+ //
34
+ // The simplest solution is to ignore the duplicate events.
35
+ // To be clear, this SHOULD NOT BE NECESSARY, since we remove the event handler below.
36
+ //
37
+ // See https://github.com/facebook/react/issues/24162
38
+ if (welcomeHasInitialized) {
39
+ console.warn(
40
+ 'React DevTools detected duplicate welcome "message" events from the content script.',
41
+ );
42
+ return;
43
+ }
44
+
45
+ welcomeHasInitialized = true;
46
+
47
+ window.removeEventListener('message', welcome);
48
+
49
+ setup(window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
50
+}
51
+
52
+window.addEventListener('message', welcome);
53
+
54
+function setup(hook: ?DevToolsHook) {
55
+ // this should not happen, but Chrome can be weird sometimes
56
+ if (hook == null) {
57
+ return;
58
+ }
59
+
60
+ // register renderers that have already injected themselves.
61
+ hook.renderers.forEach(renderer => {
62
+ registerRenderer(renderer);
63
+ });
64
+ updateRequiredBackends();
65
+
66
+ // register renderers that inject themselves later.
67
+ hook.sub('renderer', ({renderer}) => {
68
+ registerRenderer(renderer);
69
+ updateRequiredBackends();
70
+ });
71
+
72
+ // listen for backend installations.
73
+ hook.sub('devtools-backend-installed', version => {
74
+ activateBackend(version, hook);
75
+ updateRequiredBackends();
76
+ });
77
+}
78
+
79
+const requiredBackends = new Set<string>();
80
+
81
+function registerRenderer(renderer: ReactRenderer) {
82
+ let version = renderer.reconcilerVersion || renderer.version;
83
+ if (!hasAssignedBackend(version)) {
84
+ version = COMPACT_VERSION_NAME;
85
+ }
86
+ requiredBackends.add(version);
87
+}
88
+
89
+function activateBackend(version: string, hook: DevToolsHook) {
90
+ const backend = hook.backends.get(version);
91
+ if (!backend) {
92
+ throw new Error(`Could not find backend for version "${version}"`);
93
+ }
94
+ const {Agent, Bridge, initBackend, setupNativeStyleEditor} = backend;
95
+ const bridge = new Bridge({
96
+ listen(fn) {
97
+ const listener = (event: $FlowFixMe) => {
98
+ if (
99
+ event.source !== window ||
100
+ !event.data ||
101
+ event.data.source !== 'react-devtools-content-script' ||
102
+ !event.data.payload
103
+ ) {
104
+ return;
105
+ }
106
+ fn(event.data.payload);
107
+ };
108
+ window.addEventListener('message', listener);
109
+ return () => {
110
+ window.removeEventListener('message', listener);
111
+ };
112
+ },
113
+ send(event: string, payload: any, transferable?: Array<any>) {
114
+ window.postMessage(
115
+ {
116
+ source: 'react-devtools-bridge',
117
+ payload: {event, payload},
118
+ },
119
+ '*',
120
+ transferable,
121
+ );
122
+ },
123
+ });
124
+
125
+ const agent = new Agent(bridge);
126
+ agent.addListener('shutdown', () => {
127
+ // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
128
+ // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
129
+ hook.emit('shutdown');
130
+ });
131
+
132
+ initBackend(hook, agent, window);
133
+
134
+ // Setup React Native style editor if a renderer like react-native-web has injected it.
135
+ if (typeof setupNativeStyleEditor === 'function' && hook.resolveRNStyle) {
136
+ setupNativeStyleEditor(
137
+ bridge,
138
+ agent,
139
+ hook.resolveRNStyle,
140
+ hook.nativeStyleEditorValidAttributes,
141
+ );
142
+ }
143
+
144
+ // Let the frontend know that the backend has attached listeners and is ready for messages.
145
+ // This covers the case of syncing saved values after reloading/navigating while DevTools remain open.
146
+ bridge.send('extensionBackendInitialized');
147
+
148
+ // this backend is activated
149
+ requiredBackends.delete(version);
150
+}
151
+
152
+// tell the service worker which versions of backends are needed for the current page
153
+function updateRequiredBackends() {
154
+ window.postMessage(
155
+ {
156
+ source: 'react-devtools-backend-manager',
157
+ payload: {
158
+ type: 'react-devtools-required-backends',
159
+ versions: Array.from(requiredBackends),
160
+ },
161
+ },
162
+ '*',
163
+ );
164
+}