main
js 427 lines 12.8 KB
Raw
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 Agent from 'react-devtools-shared/src/backend/agent';
11 import Bridge from 'react-devtools-shared/src/bridge';
12 import {installHook} from 'react-devtools-shared/src/hook';
13 import {initBackend} from 'react-devtools-shared/src/backend';
14 import {__DEBUG__} from 'react-devtools-shared/src/constants';
15 import setupNativeStyleEditor from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
16 import {
17 getDefaultComponentFilters,
18 getIsReloadAndProfileSupported,
19 } from 'react-devtools-shared/src/utils';
20
21 import type {BackendBridge} from 'react-devtools-shared/src/bridge';
22 import type {
23 ComponentFilter,
24 Wall,
25 } from 'react-devtools-shared/src/frontend/types';
26 import type {
27 DevToolsHook,
28 DevToolsHookSettings,
29 ProfilingSettings,
30 } from 'react-devtools-shared/src/backend/types';
31 import type {ResolveNativeStyle} from 'react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor';
32
33 type ConnectOptions = {
34 host?: string,
35 nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
36 path?: string,
37 port?: number,
38 useHttps?: boolean,
39 resolveRNStyle?: ResolveNativeStyle,
40 retryConnectionDelay?: number,
41 isAppActive?: () => boolean,
42 websocket?: ?WebSocket,
43 onSettingsUpdated?: (settings: $ReadOnly<DevToolsHookSettings>) => void,
44 isReloadAndProfileSupported?: boolean,
45 isProfiling?: boolean,
46 onReloadAndProfile?: (recordChangeDescriptions: boolean) => void,
47 onReloadAndProfileFlagsReset?: () => void,
48 };
49
50 let savedComponentFilters: Array<ComponentFilter> =
51 getDefaultComponentFilters();
52
53 function debug(methodName: string, ...args: Array<mixed>) {
54 // $FlowFixMe[constant-condition]
55 if (__DEBUG__) {
56 console.log(
57 `%c[core/backend] %c${methodName}`,
58 'color: teal; font-weight: bold;',
59 'font-weight: bold;',
60 ...args,
61 );
62 }
63 }
64
65 export function initialize(
66 maybeSettingsOrSettingsPromise?:
67 | DevToolsHookSettings
68 | Promise<DevToolsHookSettings>,
69 shouldStartProfilingNow: boolean = false,
70 profilingSettings?: ProfilingSettings,
71 maybeComponentFiltersOrComponentFiltersPromise?:
72 | Array<ComponentFilter>
73 | Promise<Array<ComponentFilter>>,
74 ) {
75 const componentFiltersOrComponentFiltersPromise =
76 maybeComponentFiltersOrComponentFiltersPromise
77 ? maybeComponentFiltersOrComponentFiltersPromise
78 : savedComponentFilters;
79 installHook(
80 window,
81 componentFiltersOrComponentFiltersPromise,
82 maybeSettingsOrSettingsPromise,
83 shouldStartProfilingNow,
84 profilingSettings,
85 );
86 }
87
88 export function connectToDevTools(options: ?ConnectOptions) {
89 const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
90 if (hook == null) {
91 // DevTools didn't get injected into this page (maybe b'c of the contentType).
92 return;
93 }
94
95 const {
96 host = 'localhost',
97 nativeStyleEditorValidAttributes,
98 path = '',
99 useHttps = false,
100 port = 8097,
101 websocket,
102 resolveRNStyle = null as $FlowFixMe,
103 retryConnectionDelay = 2000,
104 isAppActive = () => true,
105 onSettingsUpdated,
106 isReloadAndProfileSupported = getIsReloadAndProfileSupported(),
107 isProfiling,
108 onReloadAndProfile,
109 onReloadAndProfileFlagsReset,
110 } = options || {};
111
112 const protocol = useHttps ? 'wss' : 'ws';
113 const prefixedPath = path !== '' && !path.startsWith('/') ? '/' + path : path;
114 let retryTimeoutID: TimeoutID | null = null;
115
116 function scheduleRetry() {
117 if (retryTimeoutID === null) {
118 // Two seconds because RN had issues with quick retries.
119 retryTimeoutID = setTimeout(
120 () => connectToDevTools(options),
121 retryConnectionDelay,
122 );
123 }
124 }
125
126 if (!isAppActive()) {
127 // If the app is in background, maybe retry later.
128 // Don't actually attempt to connect until we're in foreground.
129 scheduleRetry();
130 return;
131 }
132
133 let bridge: BackendBridge | null = null;
134
135 function shutdownBridge(): void {
136 const bridgeToShutdown = bridge;
137 if (bridgeToShutdown !== null) {
138 // Clear the active reference before shutdown flushes its final message
139 // through a potentially closed socket.
140 bridge = null;
141 bridgeToShutdown.shutdown();
142 }
143 }
144
145 const messageListeners = [];
146 const uri = protocol + '://' + host + ':' + port + prefixedPath;
147
148 // If existing websocket is passed, use it.
149 // This is necessary to support our custom integrations.
150 // See D6251744.
151 const ws = websocket ? websocket : new window.WebSocket(uri);
152 ws.onclose = handleClose;
153 ws.onerror = handleFailed;
154 ws.onmessage = handleMessage;
155 ws.onopen = function () {
156 bridge = new Bridge({
157 listen(fn) {
158 messageListeners.push(fn);
159 return () => {
160 const index = messageListeners.indexOf(fn);
161 if (index >= 0) {
162 messageListeners.splice(index, 1);
163 }
164 };
165 },
166 send(
167 event: string,
168 payload: mixed,
169 transferable?: $ReadOnlyArray<mixed>,
170 ) {
171 if (ws.readyState === ws.OPEN) {
172 // $FlowFixMe[constant-condition]
173 if (__DEBUG__) {
174 debug('wall.send()', event, payload);
175 }
176
177 ws.send(JSON.stringify({event, payload}));
178 } else {
179 // $FlowFixMe[constant-condition]
180 if (__DEBUG__) {
181 debug(
182 'wall.send()',
183 'Shutting down bridge because of closed WebSocket connection',
184 );
185 }
186
187 shutdownBridge();
188 scheduleRetry();
189 }
190 },
191 });
192 bridge.addListener(
193 'updateComponentFilters',
194 (componentFilters: Array<ComponentFilter>) => {
195 // Save filter changes in memory, in case DevTools is reloaded.
196 // In that case, the renderer will already be using the updated values.
197 // We'll lose these in between backend reloads but that can't be helped.
198 savedComponentFilters = componentFilters;
199 },
200 );
201
202 // TODO (npm-packages) Warn if "isBackendStorageAPISupported"
203 // $FlowFixMe[incompatible-type] found when upgrading Flow
204 const agent = new Agent(bridge, isProfiling, onReloadAndProfile);
205 if (typeof onReloadAndProfileFlagsReset === 'function') {
206 onReloadAndProfileFlagsReset();
207 }
208
209 if (onSettingsUpdated != null) {
210 agent.addListener('updateHookSettings', onSettingsUpdated);
211 }
212 agent.addListener('shutdown', () => {
213 if (onSettingsUpdated != null) {
214 agent.removeListener('updateHookSettings', onSettingsUpdated);
215 }
216
217 // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
218 // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
219 hook.emit('shutdown');
220 });
221
222 initBackend(hook, agent, window, isReloadAndProfileSupported);
223
224 // Setup React Native style editor if the environment supports it.
225 if (resolveRNStyle != null || hook.resolveRNStyle != null) {
226 setupNativeStyleEditor(
227 // $FlowFixMe[incompatible-type] found when upgrading Flow
228 bridge,
229 agent,
230 // $FlowFixMe[constant-condition]
231 (resolveRNStyle || hook.resolveRNStyle) as any as ResolveNativeStyle,
232 nativeStyleEditorValidAttributes ||
233 hook.nativeStyleEditorValidAttributes ||
234 null,
235 );
236 } else {
237 // Otherwise listen to detect if the environment later supports it.
238 // For example, Flipper does not eagerly inject these values.
239 // Instead it relies on the React Native Inspector to lazily inject them.
240 let lazyResolveRNStyle;
241 let lazyNativeStyleEditorValidAttributes;
242
243 const initAfterTick = () => {
244 if (bridge !== null) {
245 setupNativeStyleEditor(
246 bridge,
247 agent,
248 lazyResolveRNStyle,
249 lazyNativeStyleEditorValidAttributes,
250 );
251 }
252 };
253
254 if (!hook.hasOwnProperty('resolveRNStyle')) {
255 Object.defineProperty(hook, 'resolveRNStyle', {
256 enumerable: false,
257 get() {
258 return lazyResolveRNStyle;
259 },
260 set(value: $FlowFixMe) {
261 lazyResolveRNStyle = value;
262 initAfterTick();
263 },
264 } as Object);
265 }
266 if (!hook.hasOwnProperty('nativeStyleEditorValidAttributes')) {
267 Object.defineProperty(hook, 'nativeStyleEditorValidAttributes', {
268 enumerable: false,
269 get() {
270 return lazyNativeStyleEditorValidAttributes;
271 },
272 set(value: $FlowFixMe) {
273 lazyNativeStyleEditorValidAttributes = value;
274 initAfterTick();
275 },
276 } as Object);
277 }
278 }
279 };
280
281 function handleClose() {
282 // $FlowFixMe[constant-condition]
283 if (__DEBUG__) {
284 debug('WebSocket.onclose');
285 }
286
287 shutdownBridge();
288 scheduleRetry();
289 }
290
291 function handleFailed() {
292 // $FlowFixMe[constant-condition]
293 if (__DEBUG__) {
294 debug('WebSocket.onerror');
295 }
296
297 scheduleRetry();
298 }
299
300 function handleMessage(event: MessageEvent<>) {
301 let data;
302 try {
303 if (typeof event.data === 'string') {
304 data = JSON.parse(event.data);
305 // $FlowFixMe[constant-condition]
306 if (__DEBUG__) {
307 debug('WebSocket.onmessage', data);
308 }
309 } else {
310 throw Error();
311 }
312 } catch (e) {
313 console.error(
314 '[React DevTools] Failed to parse JSON: ' + (event.data as any),
315 );
316 return;
317 }
318 messageListeners.forEach(fn => {
319 try {
320 fn(data);
321 } catch (error) {
322 // jsc doesn't play so well with tracebacks that go into eval'd code,
323 // so the stack trace here will stop at the `eval()` call. Getting the
324 // message that caused the error is the best we can do for now.
325 console.log('[React DevTools] Error calling listener', data);
326 console.log('error:', error);
327 throw error;
328 }
329 });
330 }
331 }
332
333 type ConnectWithCustomMessagingOptions = {
334 onSubscribe: (cb: (message: mixed) => void) => void,
335 onUnsubscribe: (cb: (message: mixed) => void) => void,
336 onMessage: (event: string, payload: mixed) => void,
337 nativeStyleEditorValidAttributes?: $ReadOnlyArray<string>,
338 resolveRNStyle?: ResolveNativeStyle,
339 onSettingsUpdated?: (settings: $ReadOnly<DevToolsHookSettings>) => void,
340 isReloadAndProfileSupported?: boolean,
341 isProfiling?: boolean,
342 onReloadAndProfile?: (recordChangeDescriptions: boolean) => void,
343 onReloadAndProfileFlagsReset?: () => void,
344 };
345
346 export function connectWithCustomMessagingProtocol({
347 onSubscribe,
348 onUnsubscribe,
349 onMessage,
350 nativeStyleEditorValidAttributes,
351 resolveRNStyle,
352 onSettingsUpdated,
353 isReloadAndProfileSupported = getIsReloadAndProfileSupported(),
354 isProfiling,
355 onReloadAndProfile,
356 onReloadAndProfileFlagsReset,
357 }: ConnectWithCustomMessagingOptions): Function {
358 const hook: ?DevToolsHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
359 if (hook == null) {
360 // DevTools didn't get injected into this page (maybe b'c of the contentType).
361 return;
362 }
363
364 const wall: Wall = {
365 listen(fn: (message: mixed) => void) {
366 onSubscribe(fn);
367
368 return () => {
369 onUnsubscribe(fn);
370 };
371 },
372 send(event: string, payload: mixed) {
373 onMessage(event, payload);
374 },
375 };
376
377 const bridge: BackendBridge = new Bridge(wall);
378
379 bridge.addListener(
380 'updateComponentFilters',
381 (componentFilters: Array<ComponentFilter>) => {
382 // Save filter changes in memory, in case DevTools is reloaded.
383 // In that case, the renderer will already be using the updated values.
384 // We'll lose these in between backend reloads but that can't be helped.
385 savedComponentFilters = componentFilters;
386 },
387 );
388
389 const agent = new Agent(bridge, isProfiling, onReloadAndProfile);
390 if (typeof onReloadAndProfileFlagsReset === 'function') {
391 onReloadAndProfileFlagsReset();
392 }
393
394 if (onSettingsUpdated != null) {
395 agent.addListener('updateHookSettings', onSettingsUpdated);
396 }
397 agent.addListener('shutdown', () => {
398 if (onSettingsUpdated != null) {
399 agent.removeListener('updateHookSettings', onSettingsUpdated);
400 }
401
402 // If we received 'shutdown' from `agent`, we assume the `bridge` is already shutting down,
403 // and that caused the 'shutdown' event on the `agent`, so we don't need to call `bridge.shutdown()` here.
404 hook.emit('shutdown');
405 });
406
407 const unsubscribeBackend = initBackend(
408 hook,
409 agent,
410 window,
411 isReloadAndProfileSupported,
412 );
413
414 const nativeStyleResolver: ResolveNativeStyle | void =
415 resolveRNStyle || hook.resolveRNStyle;
416
417 if (nativeStyleResolver != null) {
418 const validAttributes =
419 nativeStyleEditorValidAttributes ||
420 hook.nativeStyleEditorValidAttributes ||
421 null;
422
423 setupNativeStyleEditor(bridge, agent, nativeStyleResolver, validAttributes);
424 }
425
426 return unsubscribeBackend;
427 }