Improved inspected element props with inline previews (#17579)
* Improved inspected element props with inline previews This mimics the inline preview shown by the brower console and dramatically improves the UX when inspecting deep values. I also updated tests to add more coverage for this new functionality. * Cleaned up the DataView vs typed array check * Added early bailouts to DevTools when generating preview strings for iterables/objects/arrays, to avoid doing unnecessary work
Brian Vaughn committed
Dec 11, 2019 at 17:52 UTC
12c000412d05c4a6079b4f57f721a40b8cea374d
9 files changed
+508
-176
packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+7
@@ -486,7 +486,11 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
486
"hooks": null,
487
"props": {
488
"array_buffer": {},
489
+ "array_of_arrays": [
490
+ {}
491
+ ],
492
"big_int": {},
493
+ "data_view": {},
494
"date": {},
495
"fn": {},
496
"html_element": {},
@@ -503,6 +507,9 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
507
"0": {},
508
"1": {}
509
},
510
+ "object_of_objects": {
511
+ "inner": {}
512
+ },
513
"react_element": {},
514
"set": {
515
"0": "abc",
packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
+73
-2
@@ -511,13 +511,19 @@ describe('InspectedElementContext', () => {
511
512
const Example = () => null;
513
514
+ const arrayOfArrays = [[['abc', 123, true], []]];
515
const div = document.createElement('div');
516
const exampleFunction = () => {};
517
const setShallow = new Set(['abc', 123]);
518
const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
519
const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
520
const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
521
+ const objectOfObjects = {
522
+ inner: {string: 'abc', number: 213, boolean: true},
523
+ };
524
const typedArray = Int8Array.from([100, -100, 0]);
525
+ const arrayBuffer = typedArray.buffer;
526
+ const dataView = new DataView(arrayBuffer);
527
const immutableMap = Immutable.fromJS({
528
a: [{hello: 'there'}, 'fixed', true],
529
b: 123,
@@ -531,15 +537,18 @@ describe('InspectedElementContext', () => {
537
await utils.actAsync(() =>
538
ReactDOM.render(
539
<Example
534
- array_buffer={typedArray.buffer}
540
+ array_buffer={arrayBuffer}
541
+ array_of_arrays={arrayOfArrays}
542
// eslint-disable-next-line no-undef
543
big_int={BigInt(123)}
537
- date={new Date()}
544
+ data_view={dataView}
545
+ date={new Date(123)}
546
fn={exampleFunction}
547
html_element={div}
548
immutable={immutableMap}
549
map={mapShallow}
550
map_of_maps={mapOfMaps}
551
+ object_of_objects={objectOfObjects}
552
react_element={<span />}
553
set={setShallow}
554
set_of_sets={setOfSets}
@@ -579,13 +588,16 @@ describe('InspectedElementContext', () => {
588
589
const {
590
array_buffer,
591
+ array_of_arrays,
592
big_int,
593
+ data_view,
594
date,
595
fn,
596
html_element,
597
immutable,
598
map,
599
map_of_maps,
600
+ object_of_objects,
601
react_element,
602
set,
603
set_of_sets,
@@ -597,54 +609,111 @@ describe('InspectedElementContext', () => {
609
expect(array_buffer[meta.inspectable]).toBe(false);
610
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
611
expect(array_buffer[meta.type]).toBe('array_buffer');
612
+ expect(array_buffer[meta.preview_short]).toBe('ArrayBuffer(3)');
613
+ expect(array_buffer[meta.preview_long]).toBe('ArrayBuffer(3)');
614
+
615
+ expect(array_of_arrays[0][meta.size]).toBe(2);
616
+ expect(array_of_arrays[0][meta.inspectable]).toBe(true);
617
+ expect(array_of_arrays[0][meta.name]).toBe('Array');
618
+ expect(array_of_arrays[0][meta.type]).toBe('array');
619
+ expect(array_of_arrays[0][meta.preview_long]).toBe('[Array(3), Array(0)]');
620
+ expect(array_of_arrays[0][meta.preview_short]).toBe('Array(2)');
621
622
expect(big_int[meta.inspectable]).toBe(false);
623
expect(big_int[meta.name]).toBe('123');
624
expect(big_int[meta.type]).toBe('bigint');
625
+ expect(big_int[meta.preview_long]).toBe('123n');
626
+ expect(big_int[meta.preview_short]).toBe('123n');
627
+
628
+ expect(data_view[meta.size]).toBe(3);
629
+ expect(data_view[meta.inspectable]).toBe(false);
630
+ expect(data_view[meta.name]).toBe('DataView');
631
+ expect(data_view[meta.type]).toBe('data_view');
632
+ expect(data_view[meta.preview_long]).toBe('DataView(3)');
633
+ expect(data_view[meta.preview_short]).toBe('DataView(3)');
634
635
expect(date[meta.inspectable]).toBe(false);
636
expect(date[meta.type]).toBe('date');
637
+ expect(date[meta.preview_long]).toBe(
638
+ 'Wed Dec 31 1969 16:00:00 GMT-0800 (Pacific Standard Time)',
639
+ );
640
+ expect(date[meta.preview_short]).toBe(
641
+ 'Wed Dec 31 1969 16:00:00 GMT-0800 (Pacific Standard Time)',
642
+ );
643
644
expect(fn[meta.inspectable]).toBe(false);
645
expect(fn[meta.name]).toBe('exampleFunction');
646
expect(fn[meta.type]).toBe('function');
647
+ expect(fn[meta.preview_long]).toBe('exampleFunction');
648
+ expect(fn[meta.preview_short]).toBe('exampleFunction');
649
650
expect(html_element[meta.inspectable]).toBe(false);
651
expect(html_element[meta.name]).toBe('DIV');
652
expect(html_element[meta.type]).toBe('html_element');
653
+ expect(html_element[meta.preview_long]).toBe('<div />');
654
+ expect(html_element[meta.preview_short]).toBe('<div />');
655
656
expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type
657
expect(immutable[meta.name]).toBe('Map');
658
expect(immutable[meta.type]).toBe('iterator');
659
+ expect(immutable[meta.preview_long]).toBe(
660
+ 'Map(3) {"a" => List(3), "b" => 123, "c" => Map(2)}',
661
+ );
662
+ expect(immutable[meta.preview_short]).toBe('Map(3)');
663
664
expect(map[meta.inspectable]).toBeUndefined(); // Complex type
665
expect(map[meta.name]).toBe('Map');
666
expect(map[meta.type]).toBe('iterator');
667
expect(map[0][meta.type]).toBe('array');
668
+ expect(map[meta.preview_long]).toBe(
669
+ 'Map(2) {"name" => "Brian", "food" => "sushi"}',
670
+ );
671
+ expect(map[meta.preview_short]).toBe('Map(2)');
672
673
expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type
674
expect(map_of_maps[meta.name]).toBe('Map');
675
expect(map_of_maps[meta.type]).toBe('iterator');
676
expect(map_of_maps[0][meta.type]).toBe('array');
677
+ expect(map_of_maps[meta.preview_long]).toBe(
678
+ 'Map(2) {"first" => Map(2), "second" => Map(2)}',
679
+ );
680
+ expect(map_of_maps[meta.preview_short]).toBe('Map(2)');
681
+
682
+ expect(object_of_objects.inner[meta.size]).toBe(3);
683
+ expect(object_of_objects.inner[meta.inspectable]).toBe(true);
684
+ expect(object_of_objects.inner[meta.name]).toBe('');
685
+ expect(object_of_objects.inner[meta.type]).toBe('object');
686
+ expect(object_of_objects.inner[meta.preview_long]).toBe(
687
+ '{boolean: true, number: 213, string: "abc"}',
688
+ );
689
+ expect(object_of_objects.inner[meta.preview_short]).toBe('{…}');
690
691
expect(react_element[meta.inspectable]).toBe(false);
692
expect(react_element[meta.name]).toBe('span');
693
expect(react_element[meta.type]).toBe('react_element');
694
+ expect(react_element[meta.preview_long]).toBe('<span />');
695
+ expect(react_element[meta.preview_short]).toBe('<span />');
696
697
expect(set[meta.inspectable]).toBeUndefined(); // Complex type
698
expect(set[meta.name]).toBe('Set');
699
expect(set[meta.type]).toBe('iterator');
700
expect(set[0]).toBe('abc');
701
expect(set[1]).toBe(123);
702
+ expect(set[meta.preview_long]).toBe('Set(2) {"abc", 123}');
703
+ expect(set[meta.preview_short]).toBe('Set(2)');
704
705
expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type
706
expect(set_of_sets[meta.name]).toBe('Set');
707
expect(set_of_sets[meta.type]).toBe('iterator');
708
expect(set_of_sets['0'][meta.inspectable]).toBe(true);
709
+ expect(set_of_sets[meta.preview_long]).toBe('Set(2) {Set(3), Set(3)}');
710
+ expect(set_of_sets[meta.preview_short]).toBe('Set(2)');
711
712
expect(symbol[meta.inspectable]).toBe(false);
713
expect(symbol[meta.name]).toBe('Symbol(symbol)');
714
expect(symbol[meta.type]).toBe('symbol');
715
+ expect(symbol[meta.preview_long]).toBe('Symbol(symbol)');
716
+ expect(symbol[meta.preview_short]).toBe('Symbol(symbol)');
717
718
expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type
719
expect(typed_array[meta.size]).toBe(3);
@@ -653,6 +722,8 @@ describe('InspectedElementContext', () => {
722
expect(typed_array[0]).toBe(100);
723
expect(typed_array[1]).toBe(-100);
724
expect(typed_array[2]).toBe(0);
725
+ expect(typed_array[meta.preview_long]).toBe('Int8Array(3) [100, -100, 0]');
726
+ expect(typed_array[meta.preview_short]).toBe('Int8Array(3)');
727
728
done();
729
});
packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
+7
@@ -127,7 +127,11 @@ Object {
127
"hooks": null,
128
"props": {
129
"array_buffer": {},
130
+ "array_of_arrays": [
131
+ {}
132
+ ],
133
"big_int": {},
134
+ "data_view": {},
135
"date": {},
136
"fn": {},
137
"html_element": {},
@@ -144,6 +148,9 @@ Object {
148
"0": {},
149
"1": {}
150
},
151
+ "object_of_objects": {
152
+ "inner": {}
153
+ },
154
"react_element": {},
155
"set": {
156
"0": "abc",
packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
+37
-2
@@ -147,13 +147,19 @@ describe('InspectedElementContext', () => {
147
148
const Example = () => null;
149
150
+ const arrayOfArrays = [[['abc', 123, true], []]];
151
const div = document.createElement('div');
152
const exampleFunction = () => {};
153
const setShallow = new Set(['abc', 123]);
154
const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
155
const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
156
const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
157
+ const objectOfObjects = {
158
+ inner: {string: 'abc', number: 213, boolean: true},
159
+ };
160
const typedArray = Int8Array.from([100, -100, 0]);
161
+ const arrayBuffer = typedArray.buffer;
162
+ const dataView = new DataView(arrayBuffer);
163
const immutableMap = Immutable.fromJS({
164
a: [{hello: 'there'}, 'fixed', true],
165
b: 123,
@@ -166,15 +172,18 @@ describe('InspectedElementContext', () => {
172
act(() =>
173
ReactDOM.render(
174
<Example
169
- array_buffer={typedArray.buffer}
175
+ array_buffer={arrayBuffer}
176
+ array_of_arrays={arrayOfArrays}
177
// eslint-disable-next-line no-undef
178
big_int={BigInt(123)}
172
- date={new Date()}
179
+ data_view={dataView}
180
+ date={new Date(123)}
181
fn={exampleFunction}
182
html_element={div}
183
immutable={immutableMap}
184
map={mapShallow}
185
map_of_maps={mapOfMaps}
186
+ object_of_objects={objectOfObjects}
187
react_element={<span />}
188
set={setShallow}
189
set_of_sets={setOfSets}
@@ -192,13 +201,16 @@ describe('InspectedElementContext', () => {
201
202
const {
203
array_buffer,
204
+ array_of_arrays,
205
big_int,
206
+ data_view,
207
date,
208
fn,
209
html_element,
210
immutable,
211
map,
212
map_of_maps,
213
+ object_of_objects,
214
react_element,
215
set,
216
set_of_sets,
@@ -210,11 +222,25 @@ describe('InspectedElementContext', () => {
222
expect(array_buffer[meta.inspectable]).toBe(false);
223
expect(array_buffer[meta.name]).toBe('ArrayBuffer');
224
expect(array_buffer[meta.type]).toBe('array_buffer');
225
+ expect(array_buffer[meta.preview_short]).toBe('ArrayBuffer(3)');
226
+ expect(array_buffer[meta.preview_long]).toBe('ArrayBuffer(3)');
227
+
228
+ expect(array_of_arrays[0][meta.size]).toBe(2);
229
+ expect(array_of_arrays[0][meta.inspectable]).toBe(true);
230
+ expect(array_of_arrays[0][meta.name]).toBe('Array');
231
+ expect(array_of_arrays[0][meta.type]).toBe('array');
232
+ expect(array_of_arrays[0][meta.preview_long]).toBe('[Array(3), Array(0)]');
233
+ expect(array_of_arrays[0][meta.preview_short]).toBe('Array(2)');
234
235
expect(big_int[meta.inspectable]).toBe(false);
236
expect(big_int[meta.name]).toBe('123');
237
expect(big_int[meta.type]).toBe('bigint');
238
239
+ expect(data_view[meta.size]).toBe(3);
240
+ expect(data_view[meta.inspectable]).toBe(false);
241
+ expect(data_view[meta.name]).toBe('DataView');
242
+ expect(data_view[meta.type]).toBe('data_view');
243
+
244
expect(date[meta.inspectable]).toBe(false);
245
expect(date[meta.type]).toBe('date');
246
@@ -240,6 +266,15 @@ describe('InspectedElementContext', () => {
266
expect(map_of_maps[meta.type]).toBe('iterator');
267
expect(map_of_maps[0][meta.type]).toBe('array');
268
269
+ expect(object_of_objects.inner[meta.size]).toBe(3);
270
+ expect(object_of_objects.inner[meta.inspectable]).toBe(true);
271
+ expect(object_of_objects.inner[meta.name]).toBe('');
272
+ expect(object_of_objects.inner[meta.type]).toBe('object');
273
+ expect(object_of_objects.inner[meta.preview_long]).toBe(
274
+ '{boolean: true, number: 213, string: "abc"}',
275
+ );
276
+ expect(object_of_objects.inner[meta.preview_short]).toBe('{…}');
277
+
278
expect(react_element[meta.inspectable]).toBe(false);
279
expect(react_element[meta.name]).toBe('span');
280
expect(react_element[meta.type]).toBe('react_element');
packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
+4
-11
@@ -133,6 +133,7 @@ export default function KeyValue({
133
} else {
134
if (Array.isArray(value)) {
135
const hasChildren = value.length > 0;
136
+ const displayName = getMetaValueLabel(value);
137
138
children = value.map((innerValue, index) => (
139
<KeyValue
@@ -164,10 +165,7 @@ export default function KeyValue({
165
onClick={hasChildren ? toggleIsOpen : undefined}>
166
{name}
167
</span>
167
- <span>
168
- Array{' '}
169
- {hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
170
- </span>
168
+ <span className={styles.Value}>{displayName}</span>
169
</div>,
170
);
171
} else {
@@ -180,9 +178,7 @@ export default function KeyValue({
178
}
179
180
const hasChildren = entries.length > 0;
183
- const displayName = value.hasOwnProperty(meta.unserializable)
184
- ? getMetaValueLabel(value)
185
- : 'Object';
181
+ const displayName = getMetaValueLabel(value);
182
183
let areChildrenReadOnly = isReadOnly || !!value[meta.readonly];
184
children = entries.map<Element<any>>(([key, keyValue]) => (
@@ -215,10 +211,7 @@ export default function KeyValue({
211
onClick={hasChildren ? toggleIsOpen : undefined}>
212
{name}
213
</span>
218
- <span>
219
- {`${displayName || ''} `}
220
- {hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
221
- </span>
214
+ <span className={styles.Value}>{displayName}</span>
215
</div>,
216
);
217
}
packages/react-devtools-shared/src/devtools/views/utils.js
+5
-26
@@ -9,6 +9,7 @@
9
10
import escapeStringRegExp from 'escape-string-regexp';
11
import {meta} from '../../hydration';
12
+import {formatDataForPreview} from '../../utils';
13
14
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
15
@@ -92,32 +93,10 @@ export function createRegExp(string: string): RegExp {
93
}
94
95
export function getMetaValueLabel(data: Object): string | null {
95
- const name = data[meta.name];
96
- const type = data[meta.type];
97
-
98
- switch (type) {
99
- case 'html_element':
100
- return name ? `<${name.toLowerCase()} />` : '';
101
- case 'react_element':
102
- return `<${name} />`;
103
- case 'function':
104
- return `${name || 'fn'}()`;
105
- case 'object':
106
- return 'Object';
107
- case 'date':
108
- case 'symbol':
109
- return name;
110
- case 'bigint':
111
- return `${name}n`;
112
- case 'iterator':
113
- return `${name}(…)`;
114
- case 'array_buffer':
115
- case 'data_view':
116
- case 'array':
117
- case 'typed_array':
118
- return `${name}[${data[meta.size]}]`;
119
- default:
120
- return null;
96
+ if (data.hasOwnProperty(meta.preview_long)) {
97
+ return data[meta.preview_long];
98
+ } else {
99
+ return formatDataForPreview(data, true);
100
}
101
}
102
packages/react-devtools-shared/src/hydration.js
+47
-135
@@ -9,22 +9,12 @@
9
10
import Symbol from 'es6-symbol';
11
import {
12
- isElement,
13
- typeOf,
14
- AsyncMode,
15
- ConcurrentMode,
16
- ContextConsumer,
17
- ContextProvider,
18
- ForwardRef,
19
- Fragment,
20
- Lazy,
21
- Memo,
22
- Portal,
23
- Profiler,
24
- StrictMode,
25
- Suspense,
26
-} from 'react-is';
27
-import {getDisplayName, getInObject, setInObject} from './utils';
12
+ getDataType,
13
+ getDisplayNameForReactElement,
14
+ getInObject,
15
+ formatDataForPreview,
16
+ setInObject,
17
+} from './utils';
18
19
import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Components/types';
20
@@ -32,6 +22,8 @@ export const meta = {
22
inspectable: Symbol('inspectable'),
23
inspected: Symbol('inspected'),
24
name: Symbol('name'),
25
+ preview_long: Symbol('preview_long'),
26
+ preview_short: Symbol('preview_short'),
27
readonly: Symbol('readonly'),
28
size: Symbol('size'),
29
type: Symbol('type'),
@@ -41,6 +33,8 @@ export const meta = {
33
export type Dehydrated = {|
34
inspectable: boolean,
35
name: string | null,
36
+ preview_long: string | null,
37
+ preview_short: string | null,
38
readonly?: boolean,
39
size?: number,
40
type: string,
@@ -52,6 +46,8 @@ export type Dehydrated = {|
46
// while preserving the original type and name.
47
export type Unserializable = {
48
name: string | null,
49
+ preview_long: string | null,
50
+ preview_short: string | null,
51
readonly?: boolean,
52
size?: number,
53
type: string,
@@ -66,84 +62,6 @@ export type Unserializable = {
62
// but may decrease the responsiveness of expanding objects/arrays to inspect further.
63
const LEVEL_THRESHOLD = 2;
64
69
-type PropType =
70
- | 'array'
71
- | 'array_buffer'
72
- | 'bigint'
73
- | 'boolean'
74
- | 'data_view'
75
- | 'date'
76
- | 'function'
77
- | 'html_element'
78
- | 'infinity'
79
- | 'iterator'
80
- | 'nan'
81
- | 'null'
82
- | 'number'
83
- | 'object'
84
- | 'react_element'
85
- | 'string'
86
- | 'symbol'
87
- | 'typed_array'
88
- | 'undefined'
89
- | 'unknown';
90
-
91
-/**
92
- * Get a enhanced/artificial type string based on the object instance
93
- */
94
-function getDataType(data: Object): PropType {
95
- if (data === null) {
96
- return 'null';
97
- } else if (data === undefined) {
98
- return 'undefined';
99
- }
100
-
101
- if (isElement(data)) {
102
- return 'react_element';
103
- }
104
-
105
- if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
106
- return 'html_element';
107
- }
108
-
109
- const type = typeof data;
110
- switch (type) {
111
- case 'bigint':
112
- return 'bigint';
113
- case 'boolean':
114
- return 'boolean';
115
- case 'function':
116
- return 'function';
117
- case 'number':
118
- if (Number.isNaN(data)) {
119
- return 'nan';
120
- } else if (!Number.isFinite(data)) {
121
- return 'infinity';
122
- } else {
123
- return 'number';
124
- }
125
- case 'object':
126
- if (Array.isArray(data)) {
127
- return 'array';
128
- } else if (ArrayBuffer.isView(data)) {
129
- return data instanceof DataView ? 'data_view' : 'typed_array';
130
- } else if (data instanceof ArrayBuffer) {
131
- return 'array_buffer';
132
- } else if (typeof data[Symbol.iterator] === 'function') {
133
- return 'iterator';
134
- } else if (Object.prototype.toString.call(data) === '[object Date]') {
135
- return 'date';
136
- }
137
- return 'object';
138
- case 'string':
139
- return 'string';
140
- case 'symbol':
141
- return 'symbol';
142
- default:
143
- return 'unknown';
144
- }
145
-}
146
-
65
/**
66
* Generate the dehydrated metadata for complex object instances
67
*/
@@ -159,6 +77,8 @@ function createDehydrated(
77
const dehydrated: Dehydrated = {
78
inspectable,
79
type,
80
+ preview_long: formatDataForPreview(data, true),
81
+ preview_short: formatDataForPreview(data, false),
82
name:
83
!data.constructor || data.constructor.name === 'Object'
84
? ''
@@ -219,6 +139,8 @@ export function dehydrate(
139
cleaned.push(path);
140
return {
141
inspectable: false,
142
+ preview_short: formatDataForPreview(data, false),
143
+ preview_long: formatDataForPreview(data, true),
144
name: data.tagName,
145
type,
146
};
@@ -227,6 +149,8 @@ export function dehydrate(
149
cleaned.push(path);
150
return {
151
inspectable: false,
152
+ preview_short: formatDataForPreview(data, false),
153
+ preview_long: formatDataForPreview(data, true),
154
name: data.name,
155
type,
156
};
@@ -238,6 +162,8 @@ export function dehydrate(
162
cleaned.push(path);
163
return {
164
inspectable: false,
165
+ preview_short: formatDataForPreview(data, false),
166
+ preview_long: formatDataForPreview(data, true),
167
name: data.toString(),
168
type,
169
};
@@ -246,6 +172,8 @@ export function dehydrate(
172
cleaned.push(path);
173
return {
174
inspectable: false,
175
+ preview_short: formatDataForPreview(data, false),
176
+ preview_long: formatDataForPreview(data, true),
177
name: data.toString(),
178
type,
179
};
@@ -256,7 +184,9 @@ export function dehydrate(
184
cleaned.push(path);
185
return {
186
inspectable: false,
259
- name: getDisplayNameForReactElement(data),
187
+ preview_short: formatDataForPreview(data, false),
188
+ preview_long: formatDataForPreview(data, true),
189
+ name: getDisplayNameForReactElement(data) || 'Unknown',
190
type,
191
};
192
@@ -266,6 +196,8 @@ export function dehydrate(
196
cleaned.push(path);
197
return {
198
inspectable: false,
199
+ preview_short: formatDataForPreview(data, false),
200
+ preview_long: formatDataForPreview(data, true),
201
name: type === 'data_view' ? 'DataView' : 'ArrayBuffer',
202
size: data.byteLength,
203
type,
@@ -298,6 +230,8 @@ export function dehydrate(
230
type: type,
231
readonly: true,
232
size: type === 'typed_array' ? data.length : undefined,
233
+ preview_short: formatDataForPreview(data, false),
234
+ preview_long: formatDataForPreview(data, true),
235
name:
236
!data.constructor || data.constructor.name === 'Object'
237
? ''
@@ -331,6 +265,8 @@ export function dehydrate(
265
cleaned.push(path);
266
return {
267
inspectable: false,
268
+ preview_short: formatDataForPreview(data, false),
269
+ preview_long: formatDataForPreview(data, true),
270
name: data.toString(),
271
type,
272
};
@@ -381,6 +317,8 @@ export function fillInPath(
317
delete target[meta.inspectable];
318
delete target[meta.inspected];
319
delete target[meta.name];
320
+ delete target[meta.preview_long];
321
+ delete target[meta.preview_short];
322
delete target[meta.readonly];
323
delete target[meta.size];
324
delete target[meta.type];
@@ -431,6 +369,8 @@ export function hydrate(
369
replaced[meta.inspectable] = !!value.inspectable;
370
replaced[meta.inspected] = false;
371
replaced[meta.name] = value.name;
372
+ replaced[meta.preview_long] = value.preview_long;
373
+ replaced[meta.preview_short] = value.preview_short;
374
replaced[meta.size] = value.size;
375
replaced[meta.readonly] = !!value.readonly;
376
replaced[meta.type] = value.type;
@@ -471,6 +411,16 @@ function upgradeUnserializable(destination: Object, source: Object) {
411
enumerable: false,
412
value: source.name,
413
},
414
+ [meta.preview_long]: {
415
+ configurable: true,
416
+ enumerable: false,
417
+ value: source.preview_long,
418
+ },
419
+ [meta.preview_short]: {
420
+ configurable: true,
421
+ enumerable: false,
422
+ value: source.preview_short,
423
+ },
424
[meta.size]: {
425
configurable: true,
426
enumerable: false,
@@ -495,48 +445,10 @@ function upgradeUnserializable(destination: Object, source: Object) {
445
446
delete destination.inspected;
447
delete destination.name;
448
+ delete destination.preview_long;
449
+ delete destination.preview_short;
450
delete destination.size;
451
delete destination.readonly;
452
delete destination.type;
453
delete destination.unserializable;
454
}
503
-
504
-export function getDisplayNameForReactElement(
505
- element: React$Element<any>,
506
-): string | null {
507
- const elementType = typeOf(element);
508
- switch (elementType) {
509
- case AsyncMode:
510
- case ConcurrentMode:
511
- return 'ConcurrentMode';
512
- case ContextConsumer:
513
- return 'ContextConsumer';
514
- case ContextProvider:
515
- return 'ContextProvider';
516
- case ForwardRef:
517
- return 'ForwardRef';
518
- case Fragment:
519
- return 'Fragment';
520
- case Lazy:
521
- return 'Lazy';
522
- case Memo:
523
- return 'Memo';
524
- case Portal:
525
- return 'Portal';
526
- case Profiler:
527
- return 'Profiler';
528
- case StrictMode:
529
- return 'StrictMode';
530
- case Suspense:
531
- return 'Suspense';
532
- default:
533
- const {type} = element;
534
- if (typeof type === 'string') {
535
- return type;
536
- } else if (type != null) {
537
- return getDisplayName(type, 'Anonymous');
538
- } else {
539
- return 'Element';
540
- }
541
- }
542
-}
packages/react-devtools-shared/src/utils.js
+324
@@ -9,6 +9,22 @@
9
10
import Symbol from 'es6-symbol';
11
import LRU from 'lru-cache';
12
+import {
13
+ isElement,
14
+ typeOf,
15
+ AsyncMode,
16
+ ConcurrentMode,
17
+ ContextConsumer,
18
+ ContextProvider,
19
+ ForwardRef,
20
+ Fragment,
21
+ Lazy,
22
+ Memo,
23
+ Portal,
24
+ Profiler,
25
+ StrictMode,
26
+ Suspense,
27
+} from 'react-is';
28
import {
29
TREE_OPERATION_ADD,
30
TREE_OPERATION_REMOVE,
@@ -28,6 +44,8 @@ import {
44
ElementTypeMemo,
45
} from 'react-devtools-shared/src/types';
46
import {localStorageGetItem, localStorageSetItem} from './storage';
47
+import {alphaSortEntries} from './devtools/views/utils';
48
+import {meta} from './hydration';
49
50
import type {ComponentFilter, ElementType} from './types';
51
@@ -305,3 +323,309 @@ export function setInObject(
323
}
324
}
325
}
326
+
327
+export type DataType =
328
+ | 'array'
329
+ | 'array_buffer'
330
+ | 'bigint'
331
+ | 'boolean'
332
+ | 'data_view'
333
+ | 'date'
334
+ | 'function'
335
+ | 'html_element'
336
+ | 'infinity'
337
+ | 'iterator'
338
+ | 'nan'
339
+ | 'null'
340
+ | 'number'
341
+ | 'object'
342
+ | 'react_element'
343
+ | 'string'
344
+ | 'symbol'
345
+ | 'typed_array'
346
+ | 'undefined'
347
+ | 'unknown';
348
+
349
+/**
350
+ * Get a enhanced/artificial type string based on the object instance
351
+ */
352
+export function getDataType(data: Object): DataType {
353
+ if (data === null) {
354
+ return 'null';
355
+ } else if (data === undefined) {
356
+ return 'undefined';
357
+ }
358
+
359
+ if (isElement(data)) {
360
+ return 'react_element';
361
+ }
362
+
363
+ if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) {
364
+ return 'html_element';
365
+ }
366
+
367
+ const type = typeof data;
368
+ switch (type) {
369
+ case 'bigint':
370
+ return 'bigint';
371
+ case 'boolean':
372
+ return 'boolean';
373
+ case 'function':
374
+ return 'function';
375
+ case 'number':
376
+ if (Number.isNaN(data)) {
377
+ return 'nan';
378
+ } else if (!Number.isFinite(data)) {
379
+ return 'infinity';
380
+ } else {
381
+ return 'number';
382
+ }
383
+ case 'object':
384
+ if (Array.isArray(data)) {
385
+ return 'array';
386
+ } else if (ArrayBuffer.isView(data)) {
387
+ return data.constructor.hasOwnProperty('BYTES_PER_ELEMENT')
388
+ ? 'typed_array'
389
+ : 'data_view';
390
+ } else if (data.constructor.name === 'ArrayBuffer') {
391
+ // HACK This ArrayBuffer check is gross; is there a better way?
392
+ // We could try to create a new DataView with the value.
393
+ // If it doesn't error, we know it's an ArrayBuffer,
394
+ // but this seems kind of awkward and expensive.
395
+ return 'array_buffer';
396
+ } else if (typeof data[Symbol.iterator] === 'function') {
397
+ return 'iterator';
398
+ } else if (Object.prototype.toString.call(data) === '[object Date]') {
399
+ return 'date';
400
+ }
401
+ return 'object';
402
+ case 'string':
403
+ return 'string';
404
+ case 'symbol':
405
+ return 'symbol';
406
+ default:
407
+ return 'unknown';
408
+ }
409
+}
410
+
411
+export function getDisplayNameForReactElement(
412
+ element: React$Element<any>,
413
+): string | null {
414
+ const elementType = typeOf(element);
415
+ switch (elementType) {
416
+ case AsyncMode:
417
+ case ConcurrentMode:
418
+ return 'ConcurrentMode';
419
+ case ContextConsumer:
420
+ return 'ContextConsumer';
421
+ case ContextProvider:
422
+ return 'ContextProvider';
423
+ case ForwardRef:
424
+ return 'ForwardRef';
425
+ case Fragment:
426
+ return 'Fragment';
427
+ case Lazy:
428
+ return 'Lazy';
429
+ case Memo:
430
+ return 'Memo';
431
+ case Portal:
432
+ return 'Portal';
433
+ case Profiler:
434
+ return 'Profiler';
435
+ case StrictMode:
436
+ return 'StrictMode';
437
+ case Suspense:
438
+ return 'Suspense';
439
+ default:
440
+ const {type} = element;
441
+ if (typeof type === 'string') {
442
+ return type;
443
+ } else if (type != null) {
444
+ return getDisplayName(type, 'Anonymous');
445
+ } else {
446
+ return 'Element';
447
+ }
448
+ }
449
+}
450
+
451
+const MAX_PREVIEW_STRING_LENGTH = 50;
452
+
453
+function truncateForDisplay(
454
+ string: string,
455
+ length: number = MAX_PREVIEW_STRING_LENGTH,
456
+) {
457
+ if (string.length > length) {
458
+ return string.substr(0, length) + '…';
459
+ } else {
460
+ return string;
461
+ }
462
+}
463
+
464
+// Attempts to mimic Chrome's inline preview for values.
465
+// For example, the following value...
466
+// {
467
+// foo: 123,
468
+// bar: "abc",
469
+// baz: [true, false],
470
+// qux: { ab: 1, cd: 2 }
471
+// };
472
+//
473
+// Would show a preview of...
474
+// {foo: 123, bar: "abc", baz: Array(2), qux: {…}}
475
+//
476
+// And the following value...
477
+// [
478
+// 123,
479
+// "abc",
480
+// [true, false],
481
+// { foo: 123, bar: "abc" }
482
+// ];
483
+//
484
+// Would show a preview of...
485
+// [123, "abc", Array(2), {…}]
486
+export function formatDataForPreview(
487
+ data: any,
488
+ showFormattedValue: boolean,
489
+): string {
490
+ if (data != null && data.hasOwnProperty(meta.type)) {
491
+ return showFormattedValue
492
+ ? data[meta.preview_long]
493
+ : data[meta.preview_short];
494
+ }
495
+
496
+ const type = getDataType(data);
497
+
498
+ switch (type) {
499
+ case 'html_element':
500
+ return `<${truncateForDisplay(data.tagName.toLowerCase())} />`;
501
+ case 'function':
502
+ return truncateForDisplay(data.name);
503
+ case 'string':
504
+ return `"${data}"`;
505
+ case 'bigint':
506
+ return truncateForDisplay(data.toString() + 'n');
507
+ case 'symbol':
508
+ return truncateForDisplay(data.toString());
509
+ case 'react_element':
510
+ return `<${truncateForDisplay(
511
+ getDisplayNameForReactElement(data) || 'Unknown',
512
+ )} />`;
513
+ case 'array_buffer':
514
+ return `ArrayBuffer(${data.byteLength})`;
515
+ case 'data_view':
516
+ return `DataView(${data.buffer.byteLength})`;
517
+ case 'array':
518
+ if (showFormattedValue) {
519
+ let formatted = '';
520
+ for (let i = 0; i < data.length; i++) {
521
+ if (i > 0) {
522
+ formatted += ', ';
523
+ }
524
+ formatted += formatDataForPreview(data[i], false);
525
+ if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
526
+ // Prevent doing a lot of unnecessary iteration...
527
+ break;
528
+ }
529
+ }
530
+ return `[${truncateForDisplay(formatted)}]`;
531
+ } else {
532
+ const length = data.hasOwnProperty(meta.size)
533
+ ? data[meta.size]
534
+ : data.length;
535
+ return `Array(${length})`;
536
+ }
537
+ case 'typed_array':
538
+ const shortName = `${data.constructor.name}(${data.length})`;
539
+ if (showFormattedValue) {
540
+ let formatted = '';
541
+ for (let i = 0; i < data.length; i++) {
542
+ if (i > 0) {
543
+ formatted += ', ';
544
+ }
545
+ formatted += data[i];
546
+ if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
547
+ // Prevent doing a lot of unnecessary iteration...
548
+ break;
549
+ }
550
+ }
551
+ return `${shortName} [${truncateForDisplay(formatted)}]`;
552
+ } else {
553
+ return shortName;
554
+ }
555
+ case 'iterator':
556
+ const name = data.constructor.name;
557
+ if (showFormattedValue) {
558
+ // TRICKY
559
+ // Don't use [...spread] syntax for this purpose.
560
+ // This project uses @babel/plugin-transform-spread in "loose" mode which only works with Array values.
561
+ // Other types (e.g. typed arrays, Sets) will not spread correctly.
562
+ const array = Array.from(data);
563
+
564
+ let formatted = '';
565
+ for (let i = 0; i < array.length; i++) {
566
+ const entryOrEntries = array[i];
567
+
568
+ if (i > 0) {
569
+ formatted += ', ';
570
+ }
571
+
572
+ // TRICKY
573
+ // Browsers display Maps and Sets differently.
574
+ // To mimic their behavior, detect if we've been given an entries tuple.
575
+ // Map(2) {"abc" => 123, "def" => 123}
576
+ // Set(2) {"abc", 123}
577
+ if (Array.isArray(entryOrEntries)) {
578
+ const key = formatDataForPreview(entryOrEntries[0], true);
579
+ const value = formatDataForPreview(entryOrEntries[1], false);
580
+ formatted += `${key} => ${value}`;
581
+ } else {
582
+ formatted += formatDataForPreview(entryOrEntries, false);
583
+ }
584
+
585
+ if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
586
+ // Prevent doing a lot of unnecessary iteration...
587
+ break;
588
+ }
589
+ }
590
+
591
+ return `${name}(${data.size}) {${truncateForDisplay(formatted)}}`;
592
+ } else {
593
+ return `${name}(${data.size})`;
594
+ }
595
+ case 'date':
596
+ return data.toString();
597
+ case 'object':
598
+ if (showFormattedValue) {
599
+ const keys = Object.keys(data).sort(alphaSortEntries);
600
+
601
+ let formatted = '';
602
+ for (let i = 0; i < keys.length; i++) {
603
+ const key = keys[i];
604
+ if (i > 0) {
605
+ formatted += ', ';
606
+ }
607
+ formatted += `${key}: ${formatDataForPreview(data[key], false)}`;
608
+ if (formatted.length > MAX_PREVIEW_STRING_LENGTH) {
609
+ // Prevent doing a lot of unnecessary iteration...
610
+ break;
611
+ }
612
+ }
613
+ return `{${truncateForDisplay(formatted)}}`;
614
+ } else {
615
+ return '{…}';
616
+ }
617
+ case 'boolean':
618
+ case 'number':
619
+ case 'infinity':
620
+ case 'nan':
621
+ case 'null':
622
+ case 'undefined':
623
+ return data;
624
+ default:
625
+ try {
626
+ return truncateForDisplay('' + data);
627
+ } catch (error) {
628
+ return 'unserializable';
629
+ }
630
+ }
631
+}
packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js
+4
@@ -15,6 +15,8 @@ const map = new Map([['name', 'Brian'], ['food', 'sushi']]);
15
const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
16
const mapOfMaps = new Map([['first', map], ['second', map]]);
17
const typedArray = Int8Array.from([100, -100, 0]);
18
+const arrayBuffer = typedArray.buffer;
19
+const dataView = new DataView(arrayBuffer);
20
const immutable = Immutable.fromJS({
21
a: [{hello: 'there'}, 'fixed', true],
22
b: 123,
@@ -27,6 +29,8 @@ const immutable = Immutable.fromJS({
29
export default function UnserializableProps() {
30
return (
31
<ChildComponent
32
+ arrayBuffer={arrayBuffer}
33
+ dataView={dataView}
34
map={map}
35
set={set}
36
mapOfMaps={mapOfMaps}