main
js 477 lines 15.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 {OMITTED_PROP_ERROR} from 'shared/ReactFlightPropertyAccess';
11
12 import hasOwnProperty from 'shared/hasOwnProperty';
13 import isArray from 'shared/isArray';
14 import {REACT_ELEMENT_TYPE} from './ReactSymbols';
15 import getComponentNameFromType from './getComponentNameFromType';
16
17 const EMPTY_ARRAY = 0;
18 const COMPLEX_ARRAY = 1;
19 const PRIMITIVE_ARRAY = 2; // Primitive values only that are accepted by JSON.stringify
20 const ENTRIES_ARRAY = 3; // Tuple arrays of string and value (like Headers, Map, etc)
21
22 // Showing wider objects in the devtools is not useful.
23 const OBJECT_WIDTH_LIMIT = 100;
24
25 function getArrayKind(array: Object): 0 | 1 | 2 | 3 {
26 let kind: 0 | 1 | 2 | 3 = EMPTY_ARRAY;
27 for (let i = 0; i < array.length && i < OBJECT_WIDTH_LIMIT; i++) {
28 const value = array[i];
29 if (typeof value === 'object' && value !== null) {
30 if (
31 isArray(value) &&
32 value.length === 2 &&
33 typeof value[0] === 'string'
34 ) {
35 // Key value tuple
36 if (kind !== EMPTY_ARRAY && kind !== ENTRIES_ARRAY) {
37 return COMPLEX_ARRAY;
38 }
39 kind = ENTRIES_ARRAY;
40 } else {
41 return COMPLEX_ARRAY;
42 }
43 } else if (typeof value === 'function') {
44 return COMPLEX_ARRAY;
45 } else if (typeof value === 'string' && value.length > 50) {
46 return COMPLEX_ARRAY;
47 } else if (kind !== EMPTY_ARRAY && kind !== PRIMITIVE_ARRAY) {
48 return COMPLEX_ARRAY;
49 } else if (typeof value === 'bigint') {
50 return COMPLEX_ARRAY;
51 } else {
52 kind = PRIMITIVE_ARRAY;
53 }
54 }
55 return kind;
56 }
57
58 export function addObjectToProperties(
59 object: Object,
60 properties: Array<[string, string]>,
61 indent: number,
62 prefix: string,
63 ): void {
64 if (ArrayBuffer.isView(object)) {
65 // Typed arrays (e.g. Uint8Array, Float32Array) can hold millions of
66 // elements. Enumerating them with for...in forces the engine to
67 // materialize a key for every index, which can freeze the page. Their
68 // contents aren't useful to show here so we skip them. DataView has no
69 // enumerable properties to show anyway.
70 return;
71 }
72 let addedProperties = 0;
73 for (const key in object) {
74 if (hasOwnProperty.call(object, key) && key[0] !== '_') {
75 addedProperties++;
76 const value = object[key];
77 addValueToProperties(key, value, properties, indent, prefix);
78 if (addedProperties >= OBJECT_WIDTH_LIMIT) {
79 properties.push([
80 prefix +
81 '\xa0\xa0'.repeat(indent) +
82 'Only ' +
83 OBJECT_WIDTH_LIMIT +
84 ' properties are shown. React will not log more properties of this object.',
85 '',
86 ]);
87 break;
88 }
89 }
90 }
91 }
92
93 function readReactElementTypeof(value: Object): mixed {
94 // Prevents dotting into $$typeof in opaque origin windows.
95 return '$$typeof' in value && hasOwnProperty.call(value, '$$typeof')
96 ? value.$$typeof
97 : undefined;
98 }
99
100 export function addValueToProperties(
101 propertyName: string,
102 value: mixed,
103 properties: Array<[string, string]>,
104 indent: number,
105 prefix: string,
106 ): void {
107 let desc;
108 switch (typeof value) {
109 case 'object':
110 if (value === null) {
111 desc = 'null';
112 break;
113 } else {
114 if (readReactElementTypeof(value) === REACT_ELEMENT_TYPE) {
115 // JSX
116 const typeName = getComponentNameFromType(value.type) || '\u2026';
117 const key = value.key;
118 const props: any = value.props;
119 const propsKeys = Object.keys(props);
120 const propsLength = propsKeys.length;
121 if (key == null && propsLength === 0) {
122 desc = '<' + typeName + ' />';
123 break;
124 }
125 if (
126 indent < 3 ||
127 (propsLength === 1 && propsKeys[0] === 'children' && key == null)
128 ) {
129 desc = '<' + typeName + ' \u2026 />';
130 break;
131 }
132 properties.push([
133 prefix + '\xa0\xa0'.repeat(indent) + propertyName,
134 '<' + typeName,
135 ]);
136 if (key !== null) {
137 addValueToProperties('key', key, properties, indent + 1, prefix);
138 }
139 let hasChildren = false;
140 let addedProperties = 0;
141 for (const propKey in props) {
142 addedProperties++;
143 if (propKey === 'children') {
144 if (
145 props.children != null &&
146 (!isArray(props.children) || props.children.length > 0)
147 ) {
148 hasChildren = true;
149 }
150 } else if (
151 hasOwnProperty.call(props, propKey) &&
152 propKey[0] !== '_'
153 ) {
154 addValueToProperties(
155 propKey,
156 props[propKey],
157 properties,
158 indent + 1,
159 prefix,
160 );
161 }
162
163 if (addedProperties >= OBJECT_WIDTH_LIMIT) {
164 break;
165 }
166 }
167 properties.push([
168 '',
169 hasChildren ? '>\u2026</' + typeName + '>' : '/>',
170 ]);
171 return;
172 }
173 // $FlowFixMe[method-unbinding]
174 const objectToString = Object.prototype.toString.call(value);
175 let objectName = objectToString.slice(8, objectToString.length - 1);
176 if (ArrayBuffer.isView(value)) {
177 // Typed arrays can hold millions of elements. Showing the type and
178 // length is more useful than enumerating every index (which is also
179 // prohibitively slow, see addObjectToProperties). DataView is the
180 // only view without a length; show just its type.
181 const length = (value as any).length;
182 desc =
183 typeof length === 'number'
184 ? objectName + '(' + length + ')'
185 : objectName;
186 break;
187 }
188 if (objectName === 'Array') {
189 const array: Array<any> = value as any;
190 const didTruncate = array.length > OBJECT_WIDTH_LIMIT;
191 const kind = getArrayKind(array);
192 if (kind === PRIMITIVE_ARRAY || kind === EMPTY_ARRAY) {
193 desc = JSON.stringify(
194 didTruncate
195 ? array.slice(0, OBJECT_WIDTH_LIMIT).concat('')
196 : array,
197 );
198 break;
199 } else if (kind === ENTRIES_ARRAY) {
200 properties.push([
201 prefix + '\xa0\xa0'.repeat(indent) + propertyName,
202 '',
203 ]);
204 for (let i = 0; i < array.length && i < OBJECT_WIDTH_LIMIT; i++) {
205 const entry = array[i];
206 addValueToProperties(
207 entry[0],
208 entry[1],
209 properties,
210 indent + 1,
211 prefix,
212 );
213 }
214 if (didTruncate) {
215 addValueToProperties(
216 OBJECT_WIDTH_LIMIT.toString(),
217 '',
218 properties,
219 indent + 1,
220 prefix,
221 );
222 }
223 return;
224 }
225 }
226 if (objectName === 'Promise') {
227 if (value.status === 'fulfilled') {
228 // Print the inner value
229 const idx = properties.length;
230 addValueToProperties(
231 propertyName,
232 value.value,
233 properties,
234 indent,
235 prefix,
236 );
237 if (properties.length > idx) {
238 // Wrap the value or type in Promise descriptor.
239 const insertedEntry = properties[idx];
240 insertedEntry[1] =
241 'Promise<' + (insertedEntry[1] || 'Object') + '>';
242 return;
243 }
244 } else if (value.status === 'rejected') {
245 // Print the inner error
246 const idx = properties.length;
247 addValueToProperties(
248 propertyName,
249 value.reason,
250 properties,
251 indent,
252 prefix,
253 );
254 if (properties.length > idx) {
255 // Wrap the value or type in Promise descriptor.
256 const insertedEntry = properties[idx];
257 insertedEntry[1] = 'Rejected Promise<' + insertedEntry[1] + '>';
258 return;
259 }
260 }
261 properties.push([
262 '\xa0\xa0'.repeat(indent) + propertyName,
263 'Promise',
264 ]);
265 return;
266 }
267 if (objectName === 'Object') {
268 const proto: any = Object.getPrototypeOf(value);
269 if (proto && typeof proto.constructor === 'function') {
270 objectName = proto.constructor.name;
271 }
272 }
273 properties.push([
274 prefix + '\xa0\xa0'.repeat(indent) + propertyName,
275 objectName === 'Object' ? (indent < 3 ? '' : '\u2026') : objectName,
276 ]);
277 if (indent < 3) {
278 addObjectToProperties(value, properties, indent + 1, prefix);
279 }
280 return;
281 }
282 case 'function':
283 const functionName = value.name;
284 if (
285 functionName === '' ||
286 // e.g. proxied functions or classes with a static property "name" that's not a string
287 typeof functionName !== 'string'
288 ) {
289 desc = '() => {}';
290 } else {
291 desc = functionName + '() {}';
292 }
293 break;
294 case 'string':
295 if (value === OMITTED_PROP_ERROR) {
296 desc = '\u2026'; // ellipsis
297 } else {
298 desc = JSON.stringify(
299 value.length >= 1024
300 ? value.slice(0, 1023) + '\u2026' // ellipsis
301 : value,
302 );
303 }
304 break;
305 case 'undefined':
306 desc = 'undefined';
307 break;
308 case 'boolean':
309 desc = value ? 'true' : 'false';
310 break;
311 default:
312 // eslint-disable-next-line react-internal/safe-string-coercion
313 desc = String(value);
314 }
315 properties.push([prefix + '\xa0\xa0'.repeat(indent) + propertyName, desc]);
316 }
317
318 const REMOVED = '-\xa0';
319 const ADDED = '+\xa0';
320 const UNCHANGED = '\u2007\xa0';
321
322 export function addObjectDiffToProperties(
323 prev: Object,
324 next: Object,
325 properties: Array<[string, string]>,
326 indent: number,
327 ): boolean {
328 // Note: We diff even non-owned properties here but things that are shared end up just the same.
329 // If a property is added or removed, we just emit the property name and omit the value it had.
330 // Mainly for performance. We need to minimize to only relevant information.
331 let isDeeplyEqual = true;
332 let prevPropertiesChecked = 0;
333 for (const key in prev) {
334 if (prevPropertiesChecked > OBJECT_WIDTH_LIMIT) {
335 properties.push([
336 'Previous object has more than ' +
337 OBJECT_WIDTH_LIMIT +
338 ' properties. React will not attempt to diff objects with too many properties.',
339 '',
340 ]);
341 isDeeplyEqual = false;
342 break;
343 }
344
345 if (!(key in next)) {
346 properties.push([REMOVED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
347 isDeeplyEqual = false;
348 }
349 prevPropertiesChecked++;
350 }
351
352 let nextPropertiesChecked = 0;
353 for (const key in next) {
354 if (nextPropertiesChecked > OBJECT_WIDTH_LIMIT) {
355 properties.push([
356 'Next object has more than ' +
357 OBJECT_WIDTH_LIMIT +
358 ' properties. React will not attempt to diff objects with too many properties.',
359 '',
360 ]);
361 isDeeplyEqual = false;
362 break;
363 }
364
365 if (key in prev) {
366 const prevValue = prev[key];
367 const nextValue = next[key];
368 if (prevValue !== nextValue) {
369 if (indent === 0 && key === 'children') {
370 // Omit any change inside the top level children prop since it's expected to change
371 // with any change to children of the component and their props will be logged
372 // elsewhere but still mark it as a cause of render.
373 const line = '\xa0\xa0'.repeat(indent) + key;
374 properties.push([REMOVED + line, '\u2026'], [ADDED + line, '\u2026']);
375 isDeeplyEqual = false;
376 continue;
377 }
378 if (indent >= 3) {
379 // Just fallthrough to print the two values if we're deep.
380 // This will skip nested properties of the objects.
381 } else if (
382 typeof prevValue === 'object' &&
383 typeof nextValue === 'object' &&
384 prevValue !== null &&
385 nextValue !== null &&
386 readReactElementTypeof(prevValue) ===
387 readReactElementTypeof(nextValue)
388 ) {
389 if (readReactElementTypeof(nextValue) === REACT_ELEMENT_TYPE) {
390 if (
391 prevValue.type === nextValue.type &&
392 prevValue.key === nextValue.key
393 ) {
394 // If the only thing that has changed is the props of a nested element, then
395 // we omit the props because it is likely to be represented as a diff elsewhere.
396 const typeName =
397 getComponentNameFromType(nextValue.type) || '\u2026';
398 const line = '\xa0\xa0'.repeat(indent) + key;
399 const desc = '<' + typeName + ' \u2026 />';
400 properties.push([REMOVED + line, desc], [ADDED + line, desc]);
401 isDeeplyEqual = false;
402 continue;
403 }
404 } else {
405 // $FlowFixMe[method-unbinding]
406 const prevKind = Object.prototype.toString.call(prevValue);
407 // $FlowFixMe[method-unbinding]
408 const nextKind = Object.prototype.toString.call(nextValue);
409 if (
410 prevKind === nextKind &&
411 (nextKind === '[object Object]' || nextKind === '[object Array]')
412 ) {
413 // Diff nested object
414 const entry = [
415 UNCHANGED + '\xa0\xa0'.repeat(indent) + key,
416 nextKind === '[object Array]' ? 'Array' : '',
417 ];
418 properties.push(entry);
419 const prevLength = properties.length;
420 const nestedEqual = addObjectDiffToProperties(
421 prevValue,
422 nextValue,
423 properties,
424 indent + 1,
425 );
426 if (!nestedEqual) {
427 isDeeplyEqual = false;
428 } else if (prevLength === properties.length) {
429 // Nothing notably changed inside the nested object. So this is only a change in reference
430 // equality. Let's note it.
431 entry[1] =
432 'Referentially unequal but deeply equal objects. Consider memoization.';
433 }
434 continue;
435 }
436 }
437 } else if (
438 typeof prevValue === 'function' &&
439 typeof nextValue === 'function' &&
440 prevValue.name === nextValue.name &&
441 prevValue.length === nextValue.length
442 ) {
443 // $FlowFixMe[method-unbinding]
444 const prevSrc = Function.prototype.toString.call(prevValue);
445 // $FlowFixMe[method-unbinding]
446 const nextSrc = Function.prototype.toString.call(nextValue);
447 if (prevSrc === nextSrc) {
448 // This looks like it might be the same function but different closures.
449 let desc;
450 if (nextValue.name === '') {
451 desc = '() => {}';
452 } else {
453 desc = nextValue.name + '() {}';
454 }
455 properties.push([
456 UNCHANGED + '\xa0\xa0'.repeat(indent) + key,
457 desc +
458 ' Referentially unequal function closure. Consider memoization.',
459 ]);
460 continue;
461 }
462 }
463
464 // Otherwise, emit the change in property and the values.
465 addValueToProperties(key, prevValue, properties, indent, REMOVED);
466 addValueToProperties(key, nextValue, properties, indent, ADDED);
467 isDeeplyEqual = false;
468 }
469 } else {
470 properties.push([ADDED + '\xa0\xa0'.repeat(indent) + key, '\u2026']);
471 isDeeplyEqual = false;
472 }
473
474 nextPropertiesChecked++;
475 }
476 return isDeeplyEqual;
477 }