@samitouri / QOS-React-2 / commits / ce65df7092

Added support for unserializable types (e.g. Set/Map, Immutable)

Brian Vaughn committed Aug 17, 2019 at 10:20 UTC ce65df7092ed69b45aa5787b708c1516b2c07427
13 files changed +548 -114
shells/dev/app/InspectableElements/InspectableElements.js
+2 -2
@@ -1,10 +1,10 @@
1 // @flow
2
3 import React, { Fragment } from 'react';
4 +import UnserializableProps from './UnserializableProps';
5 import Contexts from './Contexts';
6 import CustomHooks from './CustomHooks';
7 import CustomObject from './CustomObject';
7 -import MapAndSet from './MapAndSet';
8 import NestedProps from './NestedProps';
9 import SimpleValues from './SimpleValues';
10
@@ -15,7 +15,7 @@ export default function InspectableElements() {
15 <Fragment>
16 <h1>Inspectable elements</h1>
17 <SimpleValues />
18 - <MapAndSet />
18 + <UnserializableProps />
19 <NestedProps />
20 <Contexts />
21 <CustomHooks />
shells/dev/app/InspectableElements/MapAndSet.js deleted
-19
@@ -1,19 +0,0 @@
1 -// @flow
2 -
3 -import React from 'react';
4 -
5 -const set = new Set();
6 -set.add('abc');
7 -set.add(123);
8 -
9 -const map = new Map();
10 -map.set('name', 'Brian');
11 -map.set('food', 'sushi');
12 -
13 -export default function MapAndSet() {
14 - return <ChildComponent map={map} set={set} />;
15 -}
16 -
17 -function ChildComponent(props: any) {
18 - return null;
19 -}
shells/dev/app/InspectableElements/UnserializableProps.js new
+35
@@ -0,0 +1,35 @@
1 +// @flow
2 +
3 +import React from 'react';
4 +import Immutable from 'immutable';
5 +
6 +const set = new Set(['abc', 123]);
7 +const map = new Map([['name', 'Brian'], ['food', 'sushi']]);
8 +const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
9 +const mapOfMaps = new Map([['first', map], ['second', map]]);
10 +const typedArray = Int8Array.from([100, -100, 0]);
11 +const immutable = Immutable.fromJS({
12 + a: [{ hello: 'there' }, 'fixed', true],
13 + b: 123,
14 + c: {
15 + '1': 'xyz',
16 + xyz: 1,
17 + },
18 +});
19 +
20 +export default function UnserializableProps() {
21 + return (
22 + <ChildComponent
23 + map={map}
24 + set={set}
25 + mapOfMaps={mapOfMaps}
26 + setOfSets={setOfSets}
27 + typedArray={typedArray}
28 + immutable={immutable}
29 + />
30 + );
31 +}
32 +
33 +function ChildComponent(props: any) {
34 + return null;
35 +}
src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+66 -5
@@ -1,5 +1,41 @@
1 // Jest Snapshot v1, https://goo.gl/fbAQLP
2
3 +exports[`InspectedElementContext should dehydrate complex nested values when requested: 1: Initially inspect element 1`] = `
4 +{
5 + "id": 2,
6 + "owners": null,
7 + "context": null,
8 + "hooks": null,
9 + "props": {
10 + "set_of_sets": {
11 + "0": {},
12 + "1": {}
13 + }
14 + },
15 + "state": null
16 +}
17 +`;
18 +
19 +exports[`InspectedElementContext should dehydrate complex nested values when requested: 2: Inspect props.set_of_sets.0 1`] = `
20 +{
21 + "id": 2,
22 + "owners": null,
23 + "context": null,
24 + "hooks": null,
25 + "props": {
26 + "set_of_sets": {
27 + "0": {
28 + "0": 1,
29 + "1": 2,
30 + "2": 3
31 + },
32 + "1": {}
33 + }
34 + },
35 + "state": null
36 +}
37 +`;
38 +
39 exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
40 {
41 "id": 2,
@@ -449,13 +485,38 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected
485 "context": null,
486 "hooks": null,
487 "props": {
452 - "html_element": {},
488 + "array_buffer": {},
489 + "date": {},
490 "fn": {},
454 - "symbol": {},
491 + "html_element": {},
492 + "immutable": {
493 + "0": {},
494 + "1": {},
495 + "2": {}
496 + },
497 + "map": {
498 + "0": {},
499 + "1": {}
500 + },
501 + "map_of_maps": {
502 + "0": {},
503 + "1": {}
504 + },
505 "react_element": {},
456 - "array_buffer": {},
457 - "typed_array": {},
458 - "date": {}
506 + "set": {
507 + "0": "abc",
508 + "1": 123
509 + },
510 + "set_of_sets": {
511 + "0": {},
512 + "1": {}
513 + },
514 + "symbol": {},
515 + "typed_array": {
516 + "0": 100,
517 + "1": -100,
518 + "2": 0
519 + }
520 },
521 "state": null
522 }
src/__tests__/inspectedElementContext-test.js
+139 -24
@@ -390,23 +390,42 @@ describe('InspectedElementContext', () => {
390 });
391
392 it('should support complex data types', async done => {
393 + const Immutable = require('immutable');
394 +
395 const Example = () => null;
396
397 const div = document.createElement('div');
396 - const exmapleFunction = () => {};
397 - const typedArray = new Uint8Array(3);
398 + const exampleFunction = () => {};
399 + const setShallow = new Set(['abc', 123]);
400 + const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
401 + const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
402 + const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
403 + const typedArray = Int8Array.from([100, -100, 0]);
404 + const immutableMap = Immutable.fromJS({
405 + a: [{ hello: 'there' }, 'fixed', true],
406 + b: 123,
407 + c: {
408 + '1': 'xyz',
409 + xyz: 1,
410 + },
411 + });
412
413 const container = document.createElement('div');
414 await utils.actAsync(() =>
415 ReactDOM.render(
416 <Example
417 + array_buffer={typedArray.buffer}
418 + date={new Date()}
419 + fn={exampleFunction}
420 html_element={div}
404 - fn={exmapleFunction}
405 - symbol={Symbol('symbol')}
421 + immutable={immutableMap}
422 + map={mapShallow}
423 + map_of_maps={mapOfMaps}
424 react_element={<span />}
407 - array_buffer={typedArray.buffer}
425 + set={setShallow}
426 + set_of_sets={setOfSets}
427 + symbol={Symbol('symbol')}
428 typed_array={typedArray}
409 - date={new Date()}
429 />,
430 container
431 )
@@ -441,36 +460,76 @@ describe('InspectedElementContext', () => {
460 expect(inspectedElement).toMatchSnapshot(`1: Inspected element ${id}`);
461
462 const {
444 - html_element,
463 + array_buffer,
464 + date,
465 fn,
446 - symbol,
466 + html_element,
467 + immutable,
468 + map,
469 + map_of_maps,
470 react_element,
448 - array_buffer,
471 + set,
472 + set_of_sets,
473 + symbol,
474 typed_array,
450 - date,
475 } = (inspectedElement: any).props;
476 +
477 + expect(array_buffer[meta.size]).toBe(3);
478 + expect(array_buffer[meta.inspectable]).toBe(false);
479 + expect(array_buffer[meta.name]).toBe('ArrayBuffer');
480 + expect(array_buffer[meta.type]).toBe('array_buffer');
481 +
482 + expect(date[meta.inspectable]).toBe(false);
483 + expect(date[meta.type]).toBe('date');
484 +
485 + expect(fn[meta.inspectable]).toBe(false);
486 + expect(fn[meta.name]).toBe('exampleFunction');
487 + expect(fn[meta.type]).toBe('function');
488 +
489 expect(html_element[meta.inspectable]).toBe(false);
490 expect(html_element[meta.name]).toBe('DIV');
491 expect(html_element[meta.type]).toBe('html_element');
455 - expect(fn[meta.inspectable]).toBe(false);
456 - expect(fn[meta.name]).toBe('exmapleFunction');
457 - expect(fn[meta.type]).toBe('function');
458 - expect(symbol[meta.inspectable]).toBe(false);
459 - expect(symbol[meta.name]).toBe('Symbol(symbol)');
460 - expect(symbol[meta.type]).toBe('symbol');
492 +
493 + expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type
494 + expect(immutable[meta.name]).toBe('Map');
495 + expect(immutable[meta.type]).toBe('iterator');
496 +
497 + expect(map[meta.inspectable]).toBeUndefined(); // Complex type
498 + expect(map[meta.name]).toBe('Map');
499 + expect(map[meta.type]).toBe('iterator');
500 + expect(map[0][meta.type]).toBe('array');
501 +
502 + expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type
503 + expect(map_of_maps[meta.name]).toBe('Map');
504 + expect(map_of_maps[meta.type]).toBe('iterator');
505 + expect(map_of_maps[0][meta.type]).toBe('array');
506 +
507 expect(react_element[meta.inspectable]).toBe(false);
508 expect(react_element[meta.name]).toBe('span');
509 expect(react_element[meta.type]).toBe('react_element');
464 - expect(array_buffer[meta.size]).toBe(3);
465 - expect(array_buffer[meta.inspectable]).toBe(false);
466 - expect(array_buffer[meta.name]).toBe('ArrayBuffer');
467 - expect(array_buffer[meta.type]).toBe('array_buffer');
510 +
511 + expect(set[meta.inspectable]).toBeUndefined(); // Complex type
512 + expect(set[meta.name]).toBe('Set');
513 + expect(set[meta.type]).toBe('iterator');
514 + expect(set[0]).toBe('abc');
515 + expect(set[1]).toBe(123);
516 +
517 + expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type
518 + expect(set_of_sets[meta.name]).toBe('Set');
519 + expect(set_of_sets[meta.type]).toBe('iterator');
520 + expect(set_of_sets['0'][meta.inspectable]).toBe(true);
521 +
522 + expect(symbol[meta.inspectable]).toBe(false);
523 + expect(symbol[meta.name]).toBe('Symbol(symbol)');
524 + expect(symbol[meta.type]).toBe('symbol');
525 +
526 + expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type
527 expect(typed_array[meta.size]).toBe(3);
469 - expect(typed_array[meta.inspectable]).toBe(false);
470 - expect(typed_array[meta.name]).toBe('Uint8Array');
528 + expect(typed_array[meta.name]).toBe('Int8Array');
529 expect(typed_array[meta.type]).toBe('typed_array');
472 - expect(date[meta.inspectable]).toBe(false);
473 - expect(date[meta.type]).toBe('date');
530 + expect(typed_array[0]).toBe(100);
531 + expect(typed_array[1]).toBe(-100);
532 + expect(typed_array[2]).toBe(0);
533
534 done();
535 });
@@ -663,6 +722,62 @@ describe('InspectedElementContext', () => {
722 done();
723 });
724
725 + it('should dehydrate complex nested values when requested', async done => {
726 + const Example = () => null;
727 +
728 + const container = document.createElement('div');
729 + await utils.actAsync(() =>
730 + ReactDOM.render(
731 + <Example
732 + set_of_sets={new Set([new Set([1, 2, 3]), new Set(['a', 'b', 'c'])])}
733 + />,
734 + container
735 + )
736 + );
737 +
738 + const id = ((store.getElementIDAtIndex(0): any): number);
739 +
740 + let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
741 + let inspectedElement = null;
742 +
743 + function Suspender({ target }) {
744 + const context = React.useContext(InspectedElementContext);
745 + getInspectedElementPath = context.getInspectedElementPath;
746 + inspectedElement = context.getInspectedElement(target);
747 + return null;
748 + }
749 +
750 + await utils.actAsync(
751 + () =>
752 + TestRenderer.create(
753 + <Contexts
754 + defaultSelectedElementID={id}
755 + defaultSelectedElementIndex={0}
756 + >
757 + <React.Suspense fallback={null}>
758 + <Suspender target={id} />
759 + </React.Suspense>
760 + </Contexts>
761 + ),
762 + false
763 + );
764 + expect(getInspectedElementPath).not.toBeNull();
765 + expect(inspectedElement).not.toBeNull();
766 + expect(inspectedElement).toMatchSnapshot('1: Initially inspect element');
767 +
768 + inspectedElement = null;
769 + TestUtils.act(() => {
770 + TestRenderer.act(() => {
771 + getInspectedElementPath(id, ['props', 'set_of_sets', 0]);
772 + jest.runOnlyPendingTimers();
773 + });
774 + });
775 + expect(inspectedElement).not.toBeNull();
776 + expect(inspectedElement).toMatchSnapshot('2: Inspect props.set_of_sets.0');
777 +
778 + done();
779 + });
780 +
781 it('should include updates for nested values that were previously hydrated', async done => {
782 const Example = () => null;
783
src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap
+30 -5
@@ -126,13 +126,38 @@ Object {
126 "context": {},
127 "hooks": null,
128 "props": {
129 - "html_element": {},
129 + "array_buffer": {},
130 + "date": {},
131 "fn": {},
131 - "symbol": {},
132 + "html_element": {},
133 + "immutable": {
134 + "0": {},
135 + "1": {},
136 + "2": {}
137 + },
138 + "map": {
139 + "0": {},
140 + "1": {}
141 + },
142 + "map_of_maps": {
143 + "0": {},
144 + "1": {}
145 + },
146 "react_element": {},
133 - "array_buffer": {},
134 - "typed_array": {},
135 - "date": {}
147 + "set": {
148 + "0": "abc",
149 + "1": 123
150 + },
151 + "set_of_sets": {
152 + "0": {},
153 + "1": {}
154 + },
155 + "symbol": {},
156 + "typed_array": {
157 + "0": 100,
158 + "1": -100,
159 + "2": 0
160 + }
161 },
162 "state": null
163 },
src/__tests__/legacy/inspectElement-test.js
+88 -25
@@ -23,7 +23,11 @@ describe('InspectedElementContext', () => {
23 dehydratedData: DehydratedData | null
24 ): Object | null {
25 if (dehydratedData !== null) {
26 - return hydrate(dehydratedData.data, dehydratedData.cleaned);
26 + return hydrate(
27 + dehydratedData.data,
28 + dehydratedData.cleaned,
29 + dehydratedData.unserializable
30 + );
31 } else {
32 return null;
33 }
@@ -132,22 +136,41 @@ describe('InspectedElementContext', () => {
136 });
137
138 it('should support complex data types', async done => {
139 + const Immutable = require('immutable');
140 +
141 const Example = () => null;
142
143 const div = document.createElement('div');
138 - const exmapleFunction = () => {};
139 - const typedArray = new Uint8Array(3);
144 + const exampleFunction = () => {};
145 + const setShallow = new Set(['abc', 123]);
146 + const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]);
147 + const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]);
148 + const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]);
149 + const typedArray = Int8Array.from([100, -100, 0]);
150 + const immutableMap = Immutable.fromJS({
151 + a: [{ hello: 'there' }, 'fixed', true],
152 + b: 123,
153 + c: {
154 + '1': 'xyz',
155 + xyz: 1,
156 + },
157 + });
158
159 act(() =>
160 ReactDOM.render(
161 <Example
162 + array_buffer={typedArray.buffer}
163 + date={new Date()}
164 + fn={exampleFunction}
165 html_element={div}
145 - fn={exmapleFunction}
146 - symbol={Symbol('symbol')}
166 + immutable={immutableMap}
167 + map={mapShallow}
168 + map_of_maps={mapOfMaps}
169 react_element={<span />}
148 - array_buffer={typedArray.buffer}
170 + set={setShallow}
171 + set_of_sets={setOfSets}
172 + symbol={Symbol('symbol')}
173 typed_array={typedArray}
150 - date={new Date()}
174 />,
175 document.createElement('div')
176 )
@@ -159,36 +182,76 @@ describe('InspectedElementContext', () => {
182 expect(inspectedElement).toMatchSnapshot('1: Initial inspection');
183
184 const {
162 - html_element,
185 + array_buffer,
186 + date,
187 fn,
164 - symbol,
188 + html_element,
189 + immutable,
190 + map,
191 + map_of_maps,
192 react_element,
166 - array_buffer,
193 + set,
194 + set_of_sets,
195 + symbol,
196 typed_array,
168 - date,
197 } = inspectedElement.value.props;
198 +
199 + expect(array_buffer[meta.size]).toBe(3);
200 + expect(array_buffer[meta.inspectable]).toBe(false);
201 + expect(array_buffer[meta.name]).toBe('ArrayBuffer');
202 + expect(array_buffer[meta.type]).toBe('array_buffer');
203 +
204 + expect(date[meta.inspectable]).toBe(false);
205 + expect(date[meta.type]).toBe('date');
206 +
207 + expect(fn[meta.inspectable]).toBe(false);
208 + expect(fn[meta.name]).toBe('exampleFunction');
209 + expect(fn[meta.type]).toBe('function');
210 +
211 expect(html_element[meta.inspectable]).toBe(false);
212 expect(html_element[meta.name]).toBe('DIV');
213 expect(html_element[meta.type]).toBe('html_element');
173 - expect(fn[meta.inspectable]).toBe(false);
174 - expect(fn[meta.name]).toBe('exmapleFunction');
175 - expect(fn[meta.type]).toBe('function');
176 - expect(symbol[meta.inspectable]).toBe(false);
177 - expect(symbol[meta.name]).toBe('Symbol(symbol)');
178 - expect(symbol[meta.type]).toBe('symbol');
214 +
215 + expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type
216 + expect(immutable[meta.name]).toBe('Map');
217 + expect(immutable[meta.type]).toBe('iterator');
218 +
219 + expect(map[meta.inspectable]).toBeUndefined(); // Complex type
220 + expect(map[meta.name]).toBe('Map');
221 + expect(map[meta.type]).toBe('iterator');
222 + expect(map[0][meta.type]).toBe('array');
223 +
224 + expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type
225 + expect(map_of_maps[meta.name]).toBe('Map');
226 + expect(map_of_maps[meta.type]).toBe('iterator');
227 + expect(map_of_maps[0][meta.type]).toBe('array');
228 +
229 expect(react_element[meta.inspectable]).toBe(false);
230 expect(react_element[meta.name]).toBe('span');
231 expect(react_element[meta.type]).toBe('react_element');
182 - expect(array_buffer[meta.size]).toBe(3);
183 - expect(array_buffer[meta.inspectable]).toBe(false);
184 - expect(array_buffer[meta.name]).toBe('ArrayBuffer');
185 - expect(array_buffer[meta.type]).toBe('array_buffer');
232 +
233 + expect(set[meta.inspectable]).toBeUndefined(); // Complex type
234 + expect(set[meta.name]).toBe('Set');
235 + expect(set[meta.type]).toBe('iterator');
236 + expect(set[0]).toBe('abc');
237 + expect(set[1]).toBe(123);
238 +
239 + expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type
240 + expect(set_of_sets[meta.name]).toBe('Set');
241 + expect(set_of_sets[meta.type]).toBe('iterator');
242 + expect(set_of_sets['0'][meta.inspectable]).toBe(true);
243 +
244 + expect(symbol[meta.inspectable]).toBe(false);
245 + expect(symbol[meta.name]).toBe('Symbol(symbol)');
246 + expect(symbol[meta.type]).toBe('symbol');
247 +
248 + expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type
249 expect(typed_array[meta.size]).toBe(3);
187 - expect(typed_array[meta.inspectable]).toBe(false);
188 - expect(typed_array[meta.name]).toBe('Uint8Array');
250 + expect(typed_array[meta.name]).toBe('Int8Array');
251 expect(typed_array[meta.type]).toBe('typed_array');
190 - expect(date[meta.inspectable]).toBe(false);
191 - expect(date[meta.type]).toBe('date');
252 + expect(typed_array[0]).toBe(100);
253 + expect(typed_array[1]).toBe(-100);
254 + expect(typed_array[2]).toBe(0);
255
256 done();
257 });
src/backend/utils.js
+12 -3
@@ -10,11 +10,20 @@ export function cleanForBridge(
10 path?: Array<string | number> = []
11 ): DehydratedData | null {
12 if (data !== null) {
13 - const cleaned = [];
13 + const cleanedPaths = [];
14 + const unserializablePaths = [];
15 + const cleanedData = dehydrate(
16 + data,
17 + cleanedPaths,
18 + unserializablePaths,
19 + path,
20 + isPathWhitelisted
21 + );
22
23 return {
16 - data: dehydrate(data, cleaned, path, isPathWhitelisted),
17 - cleaned,
24 + data: cleanedData,
25 + cleaned: cleanedPaths,
26 + unserializable: unserializablePaths,
27 };
28 } else {
29 return null;
src/devtools/views/Components/InspectedElementContext.js
+6 -3
@@ -134,7 +134,7 @@ function InspectedElementContextController({ children }: Props) {
134 const value = hydrateHelper(data.value, data.path);
135 const inspectedElement = { ...currentlyInspectedElement };
136
137 - fillInPath(inspectedElement, data.path, value);
137 + fillInPath(inspectedElement, data.value, data.path, value);
138
139 resource.write(element, inspectedElement);
140
@@ -289,7 +289,7 @@ function hydrateHelper(
289 path?: Array<string | number>
290 ): Object | null {
291 if (dehydratedData !== null) {
292 - let { cleaned, data } = dehydratedData;
292 + let { cleaned, data, unserializable } = dehydratedData;
293
294 if (path) {
295 const { length } = path;
@@ -297,10 +297,13 @@ function hydrateHelper(
297 // Hydration helper requires full paths, but inspection dehydrates with relative paths.
298 // In that event it's important that we adjust the "cleaned" paths to match.
299 cleaned = cleaned.map(cleanedPath => cleanedPath.slice(length));
300 + unserializable = unserializable.map(unserializablePath =>
301 + unserializablePath.slice(length)
302 + );
303 }
304 }
305
303 - return hydrate(data, cleaned);
306 + return hydrate(data, cleaned, unserializable);
307 } else {
308 return null;
309 }
src/devtools/views/Components/KeyValue.js
+19 -7
@@ -16,6 +16,7 @@ type KeyValueProps = {|
16 depth: number,
17 hidden?: boolean,
18 inspectPath?: InspectPath,
19 + isReadOnly?: boolean,
20 name: string,
21 overrideValueFn?: ?OverrideValueFn,
22 path: Array<any>,
@@ -25,6 +26,7 @@ type KeyValueProps = {|
26 export default function KeyValue({
27 depth,
28 inspectPath,
29 + isReadOnly,
30 hidden,
31 name,
32 overrideValueFn,
@@ -78,17 +80,18 @@ export default function KeyValue({
80 displayValue = 'undefined';
81 }
82
81 - const nameClassName =
82 - typeof overrideValueFn === 'function' ? styles.EditableName : styles.Name;
83 + const isEditable = typeof overrideValueFn === 'function' && !isReadOnly;
84
85 children = (
86 <div key="root" className={styles.Item} hidden={hidden} style={style}>
87 <div className={styles.ExpandCollapseToggleSpacer} />
87 - <span className={nameClassName}>{name}</span>
88 - {typeof overrideValueFn === 'function' ? (
88 + <span className={isEditable ? styles.EditableName : styles.Name}>
89 + {name}
90 + </span>
91 + {isEditable ? (
92 <EditableValue
93 dataType={dataType}
91 - overrideValueFn={overrideValueFn}
94 + overrideValueFn={((overrideValueFn: any): OverrideValueFn)}
95 path={path}
96 value={value}
97 />
@@ -97,7 +100,10 @@ export default function KeyValue({
100 )}
101 </div>
102 );
100 - } else if (value.hasOwnProperty(meta.type)) {
103 + } else if (
104 + value.hasOwnProperty(meta.type) &&
105 + !value.hasOwnProperty(meta.unserializable)
106 + ) {
107 children = (
108 <div key="root" className={styles.Item} hidden={hidden} style={style}>
109 {isInspectable ? (
@@ -123,6 +129,7 @@ export default function KeyValue({
129 key={index}
130 depth={depth + 1}
131 inspectPath={inspectPath}
132 + isReadOnly={isReadOnly}
133 hidden={hidden || !isOpen}
134 name={index}
135 overrideValueFn={overrideValueFn}
@@ -156,12 +163,17 @@ export default function KeyValue({
163 );
164 } else {
165 const hasChildren = Object.entries(value).length > 0;
166 + const displayName = value.hasOwnProperty(meta.unserializable)
167 + ? getMetaValueLabel(value)
168 + : 'Object';
169
170 + let areChildrenReadOnly = isReadOnly || !!value[meta.readonly];
171 children = Object.entries(value).map<Element<any>>(([name, value]) => (
172 <KeyValue
173 key={name}
174 depth={depth + 1}
175 inspectPath={inspectPath}
176 + isReadOnly={areChildrenReadOnly}
177 hidden={hidden || !isOpen}
178 name={name}
179 overrideValueFn={overrideValueFn}
@@ -188,7 +200,7 @@ export default function KeyValue({
200 {name}
201 </span>
202 <span>
191 - Object{' '}
203 + {`${displayName || ''} `}
204 {hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
205 </span>
206 </div>
src/devtools/views/Components/types.js
+1
@@ -83,5 +83,6 @@ export type InspectedElement = {|
83
84 export type DehydratedData = {|
85 cleaned: Array<Array<string | number>>,
86 + unserializable: Array<Array<string | number>>,
87 data: Object,
88 |};
src/hydration.js
+140 -15
@@ -18,6 +18,8 @@ import {
18 } from 'react-is';
19 import { getDisplayName, getInObject, setInObject } from './utils';
20
21 +import type { DehydratedData } from 'src/devtools/views/Components/types';
22 +
23 export const meta = {
24 inspectable: Symbol('inspectable'),
25 inspected: Symbol('inspected'),
@@ -25,6 +27,7 @@ export const meta = {
27 readonly: Symbol('readonly'),
28 size: Symbol('size'),
29 type: Symbol('type'),
30 + unserializable: Symbol('unserializable'),
31 };
32
33 type Dehydrated = {|
@@ -35,6 +38,18 @@ type Dehydrated = {|
38 type: string,
39 |};
40
41 +// Typed arrays and other complex iteratable objects (e.g. Map, Set, ImmutableJS) need special handling.
42 +// These objects can't be serialized without losing type information,
43 +// so a "Unserializable" type wrapper is used (with meta-data keys) to send nested values-
44 +// while preserving the original type and name.
45 +type Unserializable = {
46 + name: string | null,
47 + readonly?: boolean,
48 + size?: number,
49 + type: string,
50 + unserializable: boolean,
51 +};
52 +
53 // This threshold determines the depth at which the bridge "dehydrates" nested data.
54 // Dehydration means that we don't serialize the data for e.g. postMessage or stringify,
55 // unless the frontend explicitly requests it (e.g. a user clicks to expand a props object).
@@ -173,12 +188,19 @@ function createDehydrated(
188 export function dehydrate(
189 data: Object,
190 cleaned: Array<Array<string | number>>,
191 + unserializable: Array<Array<string | number>>,
192 path: Array<string | number>,
193 isPathWhitelisted: (path: Array<string | number>) => boolean,
194 level?: number = 0
179 -): string | Dehydrated | { [key: string]: string | Dehydrated } {
195 +):
196 + | string
197 + | Dehydrated
198 + | Unserializable
199 + | { [key: string]: string | Dehydrated | Unserializable } {
200 const type = getDataType(data);
201
202 + let isPathWhitelistedCheck;
203 +
204 switch (type) {
205 case 'html_element':
206 cleaned.push(path);
@@ -229,23 +251,56 @@ export function dehydrate(
251 };
252
253 case 'array':
232 - const arrayPathCheck = isPathWhitelisted(path);
233 - if (level >= LEVEL_THRESHOLD && !arrayPathCheck) {
254 + isPathWhitelistedCheck = isPathWhitelisted(path);
255 + if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
256 return createDehydrated(type, true, data, cleaned, path);
257 }
258 return data.map((item, i) =>
259 dehydrate(
260 item,
261 cleaned,
262 + unserializable,
263 path.concat([i]),
264 isPathWhitelisted,
242 - arrayPathCheck ? 1 : level + 1
265 + isPathWhitelistedCheck ? 1 : level + 1
266 )
267 );
268
269 case 'typed_array':
270 case 'iterator':
248 - return createDehydrated(type, false, data, cleaned, path);
271 + isPathWhitelistedCheck = isPathWhitelisted(path);
272 + if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
273 + return createDehydrated(type, true, data, cleaned, path);
274 + } else {
275 + const unserializableValue: Unserializable = {
276 + unserializable: true,
277 + type: type,
278 + readonly: true,
279 + size: type === 'typed_array' ? data.length : undefined,
280 + name:
281 + !data.constructor || data.constructor.name === 'Object'
282 + ? ''
283 + : data.constructor.name,
284 + };
285 +
286 + if (typeof data[Symbol.iterator]) {
287 + [...data].forEach(
288 + (item, i) =>
289 + (unserializableValue[i] = dehydrate(
290 + item,
291 + cleaned,
292 + unserializable,
293 + path.concat([i]),
294 + isPathWhitelisted,
295 + isPathWhitelistedCheck ? 1 : level + 1
296 + ))
297 + );
298 + }
299 +
300 + unserializable.push(path);
301 +
302 + return unserializableValue;
303 + }
304
305 case 'date':
306 cleaned.push(path);
@@ -256,8 +311,8 @@ export function dehydrate(
311 };
312
313 case 'object':
259 - const objectPathCheck = isPathWhitelisted(path);
260 - if (level >= LEVEL_THRESHOLD && !objectPathCheck) {
314 + isPathWhitelistedCheck = isPathWhitelisted(path);
315 + if (level >= LEVEL_THRESHOLD && !isPathWhitelistedCheck) {
316 return createDehydrated(type, true, data, cleaned, path);
317 } else {
318 const object = {};
@@ -265,9 +320,10 @@ export function dehydrate(
320 object[name] = dehydrate(
321 data[name],
322 cleaned,
323 + unserializable,
324 path.concat([name]),
325 isPathWhitelisted,
270 - objectPathCheck ? 1 : level + 1
326 + isPathWhitelistedCheck ? 1 : level + 1
327 );
328 }
329 return object;
@@ -290,24 +346,33 @@ export function dehydrate(
346
347 export function fillInPath(
348 object: Object,
349 + data: DehydratedData,
350 path: Array<string | number>,
351 value: any
352 ) {
353 const target = getInObject(object, path);
354 if (target != null) {
298 - delete target[meta.inspectable];
299 - delete target[meta.inspected];
300 - delete target[meta.name];
301 - delete target[meta.readonly];
302 - delete target[meta.size];
303 - delete target[meta.type];
355 + if (!target[meta.unserializable]) {
356 + delete target[meta.inspectable];
357 + delete target[meta.inspected];
358 + delete target[meta.name];
359 + delete target[meta.readonly];
360 + delete target[meta.size];
361 + delete target[meta.type];
362 + }
363 + }
364 +
365 + if (value !== null && data.unserializable.includes(path)) {
366 + upgradeUnserializable(value, value);
367 }
368 +
369 setInObject(object, path, value);
370 }
371
372 export function hydrate(
373 object: Object,
310 - cleaned: Array<Array<string | number>>
374 + cleaned: Array<Array<string | number>>,
375 + unserializable: Array<Array<string | number>>
376 ): Object {
377 cleaned.forEach((path: Array<string | number>) => {
378 const length = path.length;
@@ -338,9 +403,69 @@ export function hydrate(
403 parent[last] = replaced;
404 }
405 });
406 + unserializable.forEach((path: Array<string | number>) => {
407 + const length = path.length;
408 + const last = path[length - 1];
409 + const parent = getInObject(object, path.slice(0, length - 1));
410 + if (!parent || !parent.hasOwnProperty(last)) {
411 + return;
412 + }
413 +
414 + const node = parent[last];
415 +
416 + const replacement = {
417 + ...node,
418 + };
419 +
420 + upgradeUnserializable(replacement, node);
421 +
422 + parent[last] = replacement;
423 + });
424 return object;
425 }
426
427 +function upgradeUnserializable(destination: Object, source: Object) {
428 + Object.defineProperties(destination, {
429 + [meta.inspected]: {
430 + configurable: true,
431 + enumerable: false,
432 + value: !!source.inspected,
433 + },
434 + [meta.name]: {
435 + configurable: true,
436 + enumerable: false,
437 + value: source.name,
438 + },
439 + [meta.size]: {
440 + configurable: true,
441 + enumerable: false,
442 + value: source.size,
443 + },
444 + [meta.readonly]: {
445 + configurable: true,
446 + enumerable: false,
447 + value: !!source.readonly,
448 + },
449 + [meta.type]: {
450 + configurable: true,
451 + enumerable: false,
452 + value: source.type,
453 + },
454 + [meta.unserializable]: {
455 + configurable: true,
456 + enumerable: false,
457 + value: !!source.unserializable,
458 + },
459 + });
460 +
461 + delete destination.inspected;
462 + delete destination.name;
463 + delete destination.size;
464 + delete destination.readonly;
465 + delete destination.type;
466 + delete destination.unserializable;
467 +}
468 +
469 export function getDisplayNameForReactElement(
470 element: React$Element<any>
471 ): string | null {
src/utils.js
+10 -6
@@ -266,13 +266,17 @@ export function shallowDiffers(prev: Object, next: Object): boolean {
266
267 export function getInObject(object: Object, path: Array<string | number>): any {
268 return path.reduce((reduced: Object, attr: string | number): any => {
269 - if (typeof reduced === 'object' && reduced !== null) {
270 - return reduced[attr];
271 - } else if (Array.isArray(reduced)) {
272 - return reduced[attr];
273 - } else {
274 - return null;
269 + if (reduced) {
270 + if (hasOwnProperty.call(reduced, attr)) {
271 + return reduced[attr];
272 + }
273 + if (typeof reduced[Symbol.iterator] === 'function') {
274 + // Convert iterable to array and return array[index]
275 + return [...reduced][attr];
276 + }
277 }
278 +
279 + return null;
280 }, object);
281 }
282