@samitouri / QOS-React-2 / commits / 12c000412d

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}