main
js 265 lines 7.5 KB
Raw
1 /**
2 /**
3 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 *
8 * @flow
9 */
10
11 import {compareVersions} from 'compare-versions';
12 import {dehydrate} from 'react-devtools-shared/src/hydration';
13 import isArray from 'shared/isArray';
14
15 import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
16
17 export {default as formatWithStyles} from './formatWithStyles';
18 export {default as formatConsoleArguments} from './formatConsoleArguments';
19
20 // TODO: update this to the first React version that has a corresponding DevTools backend
21 const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
22 export function hasAssignedBackend(version?: string): boolean {
23 if (version == null || version === '') {
24 return false;
25 }
26 return gte(version, FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER);
27 }
28
29 export function cleanForBridge(
30 data: Object | null,
31 isPathAllowed: (path: Array<string | number>) => boolean,
32 path: Array<string | number> = [],
33 ): DehydratedData | null {
34 if (data !== null) {
35 const cleanedPaths: Array<Array<string | number>> = [];
36 const unserializablePaths: Array<Array<string | number>> = [];
37 const cleanedData = dehydrate(
38 data,
39 cleanedPaths,
40 unserializablePaths,
41 path,
42 isPathAllowed,
43 );
44
45 return {
46 data: cleanedData,
47 cleaned: cleanedPaths,
48 unserializable: unserializablePaths,
49 };
50 } else {
51 return null;
52 }
53 }
54
55 export function copyWithDelete(
56 obj: Object | Array<any>,
57 path: Array<string | number>,
58 index: number = 0,
59 ): Object | Array<any> {
60 const key = path[index];
61 const updated = isArray(obj) ? obj.slice() : {...obj};
62 if (index + 1 === path.length) {
63 if (isArray(updated)) {
64 updated.splice(key as any as number, 1);
65 } else {
66 delete updated[key];
67 }
68 } else {
69 // $FlowFixMe[incompatible-use] number or string is fine here
70 updated[key] = copyWithDelete(obj[key], path, index + 1);
71 }
72 return updated;
73 }
74
75 // This function expects paths to be the same except for the final value.
76 // e.g. ['path', 'to', 'foo'] and ['path', 'to', 'bar']
77 export function copyWithRename(
78 obj: Object | Array<any>,
79 oldPath: Array<string | number>,
80 newPath: Array<string | number>,
81 index: number = 0,
82 ): Object | Array<any> {
83 const oldKey = oldPath[index];
84 const updated = isArray(obj) ? obj.slice() : {...obj};
85 if (index + 1 === oldPath.length) {
86 const newKey = newPath[index];
87 // $FlowFixMe[incompatible-use] number or string is fine here
88 updated[newKey] = updated[oldKey];
89 if (isArray(updated)) {
90 updated.splice(oldKey as any as number, 1);
91 } else {
92 delete updated[oldKey];
93 }
94 } else {
95 // $FlowFixMe[incompatible-use] number or string is fine here
96 updated[oldKey] = copyWithRename(obj[oldKey], oldPath, newPath, index + 1);
97 }
98 return updated;
99 }
100
101 export function copyWithSet(
102 obj: Object | Array<any>,
103 path: Array<string | number>,
104 value: any,
105 index: number = 0,
106 ): Object | Array<any> {
107 if (index >= path.length) {
108 return value;
109 }
110 const key = path[index];
111 const updated = isArray(obj) ? obj.slice() : {...obj};
112 // $FlowFixMe[incompatible-use] number or string is fine here
113 updated[key] = copyWithSet(obj[key], path, value, index + 1);
114 return updated;
115 }
116
117 export function getEffectDurations(root: Object): {
118 effectDuration: any | null,
119 passiveEffectDuration: any | null,
120 } {
121 // Profiling durations are only available for certain builds.
122 // If available, they'll be stored on the HostRoot.
123 let effectDuration = null;
124 let passiveEffectDuration = null;
125 const hostRoot = root.current;
126 if (hostRoot != null) {
127 const stateNode = hostRoot.stateNode;
128 if (stateNode != null) {
129 effectDuration =
130 stateNode.effectDuration != null ? stateNode.effectDuration : null;
131 passiveEffectDuration =
132 stateNode.passiveEffectDuration != null
133 ? stateNode.passiveEffectDuration
134 : null;
135 }
136 }
137 return {effectDuration, passiveEffectDuration};
138 }
139
140 export function serializeToString(data: any): string {
141 if (data === undefined) {
142 return 'undefined';
143 }
144
145 if (typeof data === 'function') {
146 return data.toString();
147 }
148
149 const cache = new Set<mixed>();
150 // Use a custom replacer function to protect against circular references.
151 return JSON.stringify(
152 data,
153 (key: string, value: any) => {
154 if (typeof value === 'object' && value !== null) {
155 if (cache.has(value)) {
156 return;
157 }
158 cache.add(value);
159 }
160 if (typeof value === 'bigint') {
161 return value.toString() + 'n';
162 }
163 return value;
164 },
165 2,
166 );
167 }
168
169 function safeToString(val: any): string {
170 try {
171 return String(val);
172 } catch (err) {
173 if (typeof val === 'object') {
174 // An object with no prototype and no `[Symbol.toPrimitive]()`, `toString()`, and `valueOf()` methods would throw.
175 // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String#string_coercion
176 return '[object Object]';
177 }
178 throw err;
179 }
180 }
181
182 // based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
183 // based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
184 // Implements s, d, i and f placeholders
185 export function formatConsoleArgumentsToSingleString(
186 maybeMessage: any,
187 ...inputArgs: $ReadOnlyArray<any>
188 ): string {
189 const args = inputArgs.slice();
190
191 let formatted: string = safeToString(maybeMessage);
192
193 // If the first argument is a string, check for substitutions.
194 if (typeof maybeMessage === 'string') {
195 if (args.length) {
196 const REGEXP = /(%?)(%([jdisf]))/g;
197
198 // $FlowFixMe[incompatible-type]
199 formatted = formatted.replace(REGEXP, (match, escaped, ptn, flag) => {
200 let arg = args.shift();
201 switch (flag) {
202 case 's':
203 // $FlowFixMe[unsafe-addition]
204 arg += '';
205 break;
206 case 'd':
207 case 'i':
208 arg = parseInt(arg, 10).toString();
209 break;
210 case 'f':
211 arg = parseFloat(arg).toString();
212 break;
213 }
214 if (!escaped) {
215 return arg;
216 }
217 args.unshift(arg);
218 return match;
219 });
220 }
221 }
222
223 // Arguments that remain after formatting.
224 if (args.length) {
225 for (let i = 0; i < args.length; i++) {
226 formatted += ' ' + safeToString(args[i]);
227 }
228 }
229
230 // Update escaped %% values.
231 formatted = formatted.replace(/%{2,2}/g, '%');
232
233 return String(formatted);
234 }
235
236 export function isSynchronousXHRSupported(): boolean {
237 return !!(
238 window.document &&
239 window.document.featurePolicy &&
240 window.document.featurePolicy.allowsFeature('sync-xhr')
241 );
242 }
243
244 export function gt(a: string = '', b: string = ''): boolean {
245 return compareVersions(a, b) === 1;
246 }
247
248 export function gte(a: string = '', b: string = ''): boolean {
249 return compareVersions(a, b) > -1;
250 }
251
252 export const isReactNativeEnvironment = (): boolean => {
253 // We've been relying on this for such a long time
254 // We should probably define the client for DevTools on the backend side and share it with the frontend
255 return window.document == null;
256 };
257
258 // 0.123456789 => 0.123
259 // Expects high-resolution timestamp in milliseconds, like from performance.now()
260 // Mainly used for optimizing the size of serialized profiling payload
261 export function formatDurationToMicrosecondsGranularity(
262 duration: number,
263 ): number {
264 return Math.round(duration * 1000) / 1000;
265 }