main
js 381 lines 8.83 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 {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
11 import {backendToFrontendSerializedElementMapper} from 'react-devtools-shared/src/utils';
12 import Store from 'react-devtools-shared/src/devtools/store';
13 import TimeoutError from 'react-devtools-shared/src/errors/TimeoutError';
14 import ElementPollingCancellationError from 'react-devtools-shared/src/errors/ElementPollingCancellationError';
15
16 import type {
17 InspectedElement as InspectedElementBackend,
18 InspectedElementPayload,
19 SerializedAsyncInfo as SerializedAsyncInfoBackend,
20 } from 'react-devtools-shared/src/backend/types';
21 import type {
22 BackendEvents,
23 FrontendBridge,
24 } from 'react-devtools-shared/src/bridge';
25 import type {
26 DehydratedData,
27 InspectedElement as InspectedElementFrontend,
28 SerializedAsyncInfo as SerializedAsyncInfoFrontend,
29 } from 'react-devtools-shared/src/frontend/types';
30 import type {InspectedElementPath} from 'react-devtools-shared/src/frontend/types';
31
32 export function clearErrorsAndWarnings({
33 bridge,
34 store,
35 }: {
36 bridge: FrontendBridge,
37 store: Store,
38 }): void {
39 store.rootIDToRendererID.forEach(rendererID => {
40 bridge.send('clearErrorsAndWarnings', {rendererID});
41 });
42 }
43
44 export function clearErrorsForElement({
45 bridge,
46 id,
47 rendererID,
48 }: {
49 bridge: FrontendBridge,
50 id: number,
51 rendererID: number,
52 }): void {
53 bridge.send('clearErrorsForElementID', {
54 rendererID,
55 id,
56 });
57 }
58
59 export function clearWarningsForElement({
60 bridge,
61 id,
62 rendererID,
63 }: {
64 bridge: FrontendBridge,
65 id: number,
66 rendererID: number,
67 }): void {
68 bridge.send('clearWarningsForElementID', {
69 rendererID,
70 id,
71 });
72 }
73
74 export function copyInspectedElementPath({
75 bridge,
76 id,
77 path,
78 rendererID,
79 }: {
80 bridge: FrontendBridge,
81 id: number,
82 path: Array<string | number>,
83 rendererID: number,
84 }): void {
85 bridge.send('copyElementPath', {
86 id,
87 path,
88 rendererID,
89 });
90 }
91
92 export function inspectElement(
93 bridge: FrontendBridge,
94 forceFullData: boolean,
95 id: number,
96 path: InspectedElementPath | null,
97 rendererID: number,
98 shouldListenToPauseEvents: boolean,
99 ): Promise<InspectedElementPayload> {
100 const requestID = requestCounter++;
101 const promise = getPromiseForRequestID<InspectedElementPayload>(
102 requestID,
103 'inspectedElement',
104 bridge,
105 `Timed out while inspecting element ${id}.`,
106 shouldListenToPauseEvents,
107 );
108
109 bridge.send('inspectElement', {
110 forceFullData,
111 id,
112 path,
113 rendererID,
114 requestID,
115 });
116
117 return promise;
118 }
119
120 export function inspectScreen(
121 bridge: FrontendBridge,
122 forceFullData: boolean,
123 arbitraryRootID: number,
124 path: InspectedElementPath | null,
125 shouldListenToPauseEvents: boolean,
126 ): Promise<InspectedElementPayload> {
127 const requestID = requestCounter++;
128 const promise = getPromiseForRequestID<InspectedElementPayload>(
129 requestID,
130 'inspectedScreen',
131 bridge,
132 `Timed out while inspecting screen.`,
133 shouldListenToPauseEvents,
134 );
135
136 bridge.send('inspectScreen', {
137 requestID,
138 id: arbitraryRootID,
139 path,
140 forceFullData,
141 });
142
143 return promise;
144 }
145
146 let storeAsGlobalCount = 0;
147
148 export function storeAsGlobal({
149 bridge,
150 id,
151 path,
152 rendererID,
153 }: {
154 bridge: FrontendBridge,
155 id: number,
156 path: Array<string | number>,
157 rendererID: number,
158 }): void {
159 bridge.send('storeAsGlobal', {
160 count: storeAsGlobalCount++,
161 id,
162 path,
163 rendererID,
164 });
165 }
166
167 const TIMEOUT_DELAY = 10_000;
168
169 let requestCounter = 0;
170
171 function getPromiseForRequestID<T>(
172 requestID: number,
173 eventType: $Keys<BackendEvents>,
174 bridge: FrontendBridge,
175 timeoutMessage: string,
176 shouldListenToPauseEvents: boolean = false,
177 ): Promise<T> {
178 return new Promise((resolve, reject) => {
179 const cleanup = () => {
180 bridge.removeListener(eventType, onInspectedElement);
181 bridge.removeListener('shutdown', onShutdown);
182
183 if (shouldListenToPauseEvents) {
184 bridge.removeListener('pauseElementPolling', onDisconnect);
185 }
186
187 clearTimeout(timeoutID);
188 };
189
190 const onShutdown = () => {
191 cleanup();
192 reject(
193 new Error(
194 'Failed to inspect element. Try again or restart React DevTools.',
195 ),
196 );
197 };
198
199 const onDisconnect = () => {
200 cleanup();
201 reject(new ElementPollingCancellationError());
202 };
203
204 const onInspectedElement = (data: any) => {
205 if (data.responseID === requestID) {
206 cleanup();
207 resolve(data as T);
208 }
209 };
210
211 const onTimeout = () => {
212 cleanup();
213 reject(new TimeoutError(timeoutMessage));
214 };
215
216 bridge.addListener(eventType, onInspectedElement);
217 bridge.addListener('shutdown', onShutdown);
218
219 if (shouldListenToPauseEvents) {
220 bridge.addListener('pauseElementPolling', onDisconnect);
221 }
222
223 const timeoutID = setTimeout(onTimeout, TIMEOUT_DELAY);
224 });
225 }
226
227 export function cloneInspectedElementWithPath(
228 inspectedElement: InspectedElementFrontend,
229 path: Array<string | number>,
230 value: Object,
231 ): InspectedElementFrontend {
232 const hydratedValue = hydrateHelper(value, path);
233 const clonedInspectedElement = {...inspectedElement};
234
235 fillInPath(clonedInspectedElement, value, path, hydratedValue);
236
237 return clonedInspectedElement;
238 }
239
240 function backendToFrontendSerializedAsyncInfo(
241 asyncInfo: SerializedAsyncInfoBackend,
242 ): SerializedAsyncInfoFrontend {
243 const ioInfo = asyncInfo.awaited;
244 return {
245 awaited: {
246 name: ioInfo.name,
247 description: ioInfo.description,
248 start: ioInfo.start,
249 end: ioInfo.end,
250 byteSize: ioInfo.byteSize,
251 value: ioInfo.value,
252 env: ioInfo.env,
253 owner:
254 ioInfo.owner === null
255 ? null
256 : backendToFrontendSerializedElementMapper(ioInfo.owner),
257 stack: ioInfo.stack,
258 },
259 env: asyncInfo.env,
260 owner:
261 asyncInfo.owner === null
262 ? null
263 : backendToFrontendSerializedElementMapper(asyncInfo.owner),
264 stack: asyncInfo.stack,
265 };
266 }
267
268 export function convertInspectedElementBackendToFrontend(
269 inspectedElementBackend: InspectedElementBackend,
270 ): InspectedElementFrontend {
271 const {
272 canEditFunctionProps,
273 canEditFunctionPropsDeletePaths,
274 canEditFunctionPropsRenamePaths,
275 canEditHooks,
276 canEditHooksAndDeletePaths,
277 canEditHooksAndRenamePaths,
278 canToggleError,
279 isErrored,
280 canToggleSuspense,
281 isSuspended,
282 hasLegacyContext,
283 id,
284 type,
285 owners,
286 env,
287 source,
288 stack,
289 context,
290 hooks,
291 plugins,
292 props,
293 rendererPackageName,
294 rendererVersion,
295 rootType,
296 state,
297 key,
298 errors,
299 warnings,
300 suspendedBy,
301 suspendedByRange,
302 unknownSuspenders,
303 nativeTag,
304 } = inspectedElementBackend;
305
306 const hydratedSuspendedBy: null | Array<SerializedAsyncInfoBackend> =
307 hydrateHelper(suspendedBy);
308
309 const inspectedElement: InspectedElementFrontend = {
310 canEditFunctionProps,
311 canEditFunctionPropsDeletePaths,
312 canEditFunctionPropsRenamePaths,
313 canEditHooks,
314 canEditHooksAndDeletePaths,
315 canEditHooksAndRenamePaths,
316 canToggleError,
317 isErrored,
318 canToggleSuspense,
319 isSuspended,
320 hasLegacyContext,
321 id,
322 key,
323 plugins,
324 rendererPackageName,
325 rendererVersion,
326 rootType,
327 // Previous backend implementations (<= 6.1.5) have a different interface for Source.
328 // This gates the source features for only compatible backends: >= 6.1.6
329 source: Array.isArray(source) ? source : null,
330 stack: stack,
331 type,
332 owners:
333 owners === null
334 ? null
335 : owners.map(backendToFrontendSerializedElementMapper),
336 env,
337 context: hydrateHelper(context),
338 hooks: hydrateHelper(hooks),
339 props: hydrateHelper(props),
340 state: hydrateHelper(state),
341 errors,
342 warnings,
343 suspendedBy:
344 hydratedSuspendedBy == null // backwards compat
345 ? []
346 : hydratedSuspendedBy.map(backendToFrontendSerializedAsyncInfo),
347 suspendedByRange,
348 unknownSuspenders,
349 nativeTag,
350 };
351
352 return inspectedElement;
353 }
354
355 export function hydrateHelper(
356 dehydratedData: DehydratedData | null,
357 path: ?InspectedElementPath,
358 ): Object | null {
359 if (dehydratedData !== null) {
360 const {cleaned, data, unserializable} = dehydratedData;
361
362 if (path) {
363 const {length} = path;
364 if (length > 0) {
365 // Hydration helper requires full paths, but inspection dehydrates with relative paths.
366 // In that event it's important that we adjust the "cleaned" paths to match.
367 return hydrate(
368 data,
369 cleaned.map(cleanedPath => cleanedPath.slice(length)),
370 unserializable.map(unserializablePath =>
371 unserializablePath.slice(length),
372 ),
373 );
374 }
375 }
376
377 return hydrate(data, cleaned, unserializable);
378 } else {
379 return null;
380 }
381 }