diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap index e7fe89f4e2573..83d769aaccd60 100644 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap @@ -486,7 +486,11 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected "hooks": null, "props": { "array_buffer": {}, + "array_of_arrays": [ + {} + ], "big_int": {}, + "data_view": {}, "date": {}, "fn": {}, "html_element": {}, @@ -503,6 +507,9 @@ exports[`InspectedElementContext should support complex data types: 1: Inspected "0": {}, "1": {} }, + "object_of_objects": { + "inner": {} + }, "react_element": {}, "set": { "0": "abc", diff --git a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js b/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js index aaf807485f950..0e7aa1788d929 100644 --- a/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js +++ b/packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js @@ -511,13 +511,19 @@ describe('InspectedElementContext', () => { const Example = () => null; + const arrayOfArrays = [[['abc', 123, true], []]]; const div = document.createElement('div'); const exampleFunction = () => {}; const setShallow = new Set(['abc', 123]); const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]); const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]); const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]); + const objectOfObjects = { + inner: {string: 'abc', number: 213, boolean: true}, + }; const typedArray = Int8Array.from([100, -100, 0]); + const arrayBuffer = typedArray.buffer; + const dataView = new DataView(arrayBuffer); const immutableMap = Immutable.fromJS({ a: [{hello: 'there'}, 'fixed', true], b: 123, @@ -531,15 +537,18 @@ describe('InspectedElementContext', () => { await utils.actAsync(() => ReactDOM.render( } set={setShallow} set_of_sets={setOfSets} @@ -579,13 +588,16 @@ describe('InspectedElementContext', () => { const { array_buffer, + array_of_arrays, big_int, + data_view, date, fn, html_element, immutable, map, map_of_maps, + object_of_objects, react_element, set, set_of_sets, @@ -597,54 +609,111 @@ describe('InspectedElementContext', () => { expect(array_buffer[meta.inspectable]).toBe(false); expect(array_buffer[meta.name]).toBe('ArrayBuffer'); expect(array_buffer[meta.type]).toBe('array_buffer'); + expect(array_buffer[meta.preview_short]).toBe('ArrayBuffer(3)'); + expect(array_buffer[meta.preview_long]).toBe('ArrayBuffer(3)'); + + expect(array_of_arrays[0][meta.size]).toBe(2); + expect(array_of_arrays[0][meta.inspectable]).toBe(true); + expect(array_of_arrays[0][meta.name]).toBe('Array'); + expect(array_of_arrays[0][meta.type]).toBe('array'); + expect(array_of_arrays[0][meta.preview_long]).toBe('[Array(3), Array(0)]'); + expect(array_of_arrays[0][meta.preview_short]).toBe('Array(2)'); expect(big_int[meta.inspectable]).toBe(false); expect(big_int[meta.name]).toBe('123'); expect(big_int[meta.type]).toBe('bigint'); + expect(big_int[meta.preview_long]).toBe('123n'); + expect(big_int[meta.preview_short]).toBe('123n'); + + expect(data_view[meta.size]).toBe(3); + expect(data_view[meta.inspectable]).toBe(false); + expect(data_view[meta.name]).toBe('DataView'); + expect(data_view[meta.type]).toBe('data_view'); + expect(data_view[meta.preview_long]).toBe('DataView(3)'); + expect(data_view[meta.preview_short]).toBe('DataView(3)'); expect(date[meta.inspectable]).toBe(false); expect(date[meta.type]).toBe('date'); + expect(date[meta.preview_long]).toBe( + 'Wed Dec 31 1969 16:00:00 GMT-0800 (Pacific Standard Time)', + ); + expect(date[meta.preview_short]).toBe( + 'Wed Dec 31 1969 16:00:00 GMT-0800 (Pacific Standard Time)', + ); expect(fn[meta.inspectable]).toBe(false); expect(fn[meta.name]).toBe('exampleFunction'); expect(fn[meta.type]).toBe('function'); + expect(fn[meta.preview_long]).toBe('exampleFunction'); + expect(fn[meta.preview_short]).toBe('exampleFunction'); expect(html_element[meta.inspectable]).toBe(false); expect(html_element[meta.name]).toBe('DIV'); expect(html_element[meta.type]).toBe('html_element'); + expect(html_element[meta.preview_long]).toBe('
'); + expect(html_element[meta.preview_short]).toBe('
'); expect(immutable[meta.inspectable]).toBeUndefined(); // Complex type expect(immutable[meta.name]).toBe('Map'); expect(immutable[meta.type]).toBe('iterator'); + expect(immutable[meta.preview_long]).toBe( + 'Map(3) {"a" => List(3), "b" => 123, "c" => Map(2)}', + ); + expect(immutable[meta.preview_short]).toBe('Map(3)'); expect(map[meta.inspectable]).toBeUndefined(); // Complex type expect(map[meta.name]).toBe('Map'); expect(map[meta.type]).toBe('iterator'); expect(map[0][meta.type]).toBe('array'); + expect(map[meta.preview_long]).toBe( + 'Map(2) {"name" => "Brian", "food" => "sushi"}', + ); + expect(map[meta.preview_short]).toBe('Map(2)'); expect(map_of_maps[meta.inspectable]).toBeUndefined(); // Complex type expect(map_of_maps[meta.name]).toBe('Map'); expect(map_of_maps[meta.type]).toBe('iterator'); expect(map_of_maps[0][meta.type]).toBe('array'); + expect(map_of_maps[meta.preview_long]).toBe( + 'Map(2) {"first" => Map(2), "second" => Map(2)}', + ); + expect(map_of_maps[meta.preview_short]).toBe('Map(2)'); + + expect(object_of_objects.inner[meta.size]).toBe(3); + expect(object_of_objects.inner[meta.inspectable]).toBe(true); + expect(object_of_objects.inner[meta.name]).toBe(''); + expect(object_of_objects.inner[meta.type]).toBe('object'); + expect(object_of_objects.inner[meta.preview_long]).toBe( + '{boolean: true, number: 213, string: "abc"}', + ); + expect(object_of_objects.inner[meta.preview_short]).toBe('{…}'); expect(react_element[meta.inspectable]).toBe(false); expect(react_element[meta.name]).toBe('span'); expect(react_element[meta.type]).toBe('react_element'); + expect(react_element[meta.preview_long]).toBe(''); + expect(react_element[meta.preview_short]).toBe(''); expect(set[meta.inspectable]).toBeUndefined(); // Complex type expect(set[meta.name]).toBe('Set'); expect(set[meta.type]).toBe('iterator'); expect(set[0]).toBe('abc'); expect(set[1]).toBe(123); + expect(set[meta.preview_long]).toBe('Set(2) {"abc", 123}'); + expect(set[meta.preview_short]).toBe('Set(2)'); expect(set_of_sets[meta.inspectable]).toBeUndefined(); // Complex type expect(set_of_sets[meta.name]).toBe('Set'); expect(set_of_sets[meta.type]).toBe('iterator'); expect(set_of_sets['0'][meta.inspectable]).toBe(true); + expect(set_of_sets[meta.preview_long]).toBe('Set(2) {Set(3), Set(3)}'); + expect(set_of_sets[meta.preview_short]).toBe('Set(2)'); expect(symbol[meta.inspectable]).toBe(false); expect(symbol[meta.name]).toBe('Symbol(symbol)'); expect(symbol[meta.type]).toBe('symbol'); + expect(symbol[meta.preview_long]).toBe('Symbol(symbol)'); + expect(symbol[meta.preview_short]).toBe('Symbol(symbol)'); expect(typed_array[meta.inspectable]).toBeUndefined(); // Complex type expect(typed_array[meta.size]).toBe(3); @@ -653,6 +722,8 @@ describe('InspectedElementContext', () => { expect(typed_array[0]).toBe(100); expect(typed_array[1]).toBe(-100); expect(typed_array[2]).toBe(0); + expect(typed_array[meta.preview_long]).toBe('Int8Array(3) [100, -100, 0]'); + expect(typed_array[meta.preview_short]).toBe('Int8Array(3)'); done(); }); diff --git a/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap b/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap index 2c35d681bc864..c1407ceeed6ad 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap +++ b/packages/react-devtools-shared/src/__tests__/legacy/__snapshots__/inspectElement-test.js.snap @@ -127,7 +127,11 @@ Object { "hooks": null, "props": { "array_buffer": {}, + "array_of_arrays": [ + {} + ], "big_int": {}, + "data_view": {}, "date": {}, "fn": {}, "html_element": {}, @@ -144,6 +148,9 @@ Object { "0": {}, "1": {} }, + "object_of_objects": { + "inner": {} + }, "react_element": {}, "set": { "0": "abc", diff --git a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js index 8c8336a93ed96..8c595d734f2fd 100644 --- a/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js +++ b/packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js @@ -147,13 +147,19 @@ describe('InspectedElementContext', () => { const Example = () => null; + const arrayOfArrays = [[['abc', 123, true], []]]; const div = document.createElement('div'); const exampleFunction = () => {}; const setShallow = new Set(['abc', 123]); const mapShallow = new Map([['name', 'Brian'], ['food', 'sushi']]); const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]); const mapOfMaps = new Map([['first', mapShallow], ['second', mapShallow]]); + const objectOfObjects = { + inner: {string: 'abc', number: 213, boolean: true}, + }; const typedArray = Int8Array.from([100, -100, 0]); + const arrayBuffer = typedArray.buffer; + const dataView = new DataView(arrayBuffer); const immutableMap = Immutable.fromJS({ a: [{hello: 'there'}, 'fixed', true], b: 123, @@ -166,15 +172,18 @@ describe('InspectedElementContext', () => { act(() => ReactDOM.render( } set={setShallow} set_of_sets={setOfSets} @@ -192,13 +201,16 @@ describe('InspectedElementContext', () => { const { array_buffer, + array_of_arrays, big_int, + data_view, date, fn, html_element, immutable, map, map_of_maps, + object_of_objects, react_element, set, set_of_sets, @@ -210,11 +222,25 @@ describe('InspectedElementContext', () => { expect(array_buffer[meta.inspectable]).toBe(false); expect(array_buffer[meta.name]).toBe('ArrayBuffer'); expect(array_buffer[meta.type]).toBe('array_buffer'); + expect(array_buffer[meta.preview_short]).toBe('ArrayBuffer(3)'); + expect(array_buffer[meta.preview_long]).toBe('ArrayBuffer(3)'); + + expect(array_of_arrays[0][meta.size]).toBe(2); + expect(array_of_arrays[0][meta.inspectable]).toBe(true); + expect(array_of_arrays[0][meta.name]).toBe('Array'); + expect(array_of_arrays[0][meta.type]).toBe('array'); + expect(array_of_arrays[0][meta.preview_long]).toBe('[Array(3), Array(0)]'); + expect(array_of_arrays[0][meta.preview_short]).toBe('Array(2)'); expect(big_int[meta.inspectable]).toBe(false); expect(big_int[meta.name]).toBe('123'); expect(big_int[meta.type]).toBe('bigint'); + expect(data_view[meta.size]).toBe(3); + expect(data_view[meta.inspectable]).toBe(false); + expect(data_view[meta.name]).toBe('DataView'); + expect(data_view[meta.type]).toBe('data_view'); + expect(date[meta.inspectable]).toBe(false); expect(date[meta.type]).toBe('date'); @@ -240,6 +266,15 @@ describe('InspectedElementContext', () => { expect(map_of_maps[meta.type]).toBe('iterator'); expect(map_of_maps[0][meta.type]).toBe('array'); + expect(object_of_objects.inner[meta.size]).toBe(3); + expect(object_of_objects.inner[meta.inspectable]).toBe(true); + expect(object_of_objects.inner[meta.name]).toBe(''); + expect(object_of_objects.inner[meta.type]).toBe('object'); + expect(object_of_objects.inner[meta.preview_long]).toBe( + '{boolean: true, number: 213, string: "abc"}', + ); + expect(object_of_objects.inner[meta.preview_short]).toBe('{…}'); + expect(react_element[meta.inspectable]).toBe(false); expect(react_element[meta.name]).toBe('span'); expect(react_element[meta.type]).toBe('react_element'); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js index 76b1a2e57b897..07e7337d501c5 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js @@ -133,6 +133,7 @@ export default function KeyValue({ } else { if (Array.isArray(value)) { const hasChildren = value.length > 0; + const displayName = getMetaValueLabel(value); children = value.map((innerValue, index) => ( {name} - - Array{' '} - {hasChildren ? '' : (empty)} - + {displayName}
, ); } else { @@ -180,9 +178,7 @@ export default function KeyValue({ } const hasChildren = entries.length > 0; - const displayName = value.hasOwnProperty(meta.unserializable) - ? getMetaValueLabel(value) - : 'Object'; + const displayName = getMetaValueLabel(value); let areChildrenReadOnly = isReadOnly || !!value[meta.readonly]; children = entries.map>(([key, keyValue]) => ( @@ -215,10 +211,7 @@ export default function KeyValue({ onClick={hasChildren ? toggleIsOpen : undefined}> {name} - - {`${displayName || ''} `} - {hasChildren ? '' : (empty)} - + {displayName}
, ); } diff --git a/packages/react-devtools-shared/src/devtools/views/utils.js b/packages/react-devtools-shared/src/devtools/views/utils.js index e52bedbace49b..16d3bd2881b5a 100644 --- a/packages/react-devtools-shared/src/devtools/views/utils.js +++ b/packages/react-devtools-shared/src/devtools/views/utils.js @@ -9,6 +9,7 @@ import escapeStringRegExp from 'escape-string-regexp'; import {meta} from '../../hydration'; +import {formatDataForPreview} from '../../utils'; import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks'; @@ -92,32 +93,10 @@ export function createRegExp(string: string): RegExp { } export function getMetaValueLabel(data: Object): string | null { - const name = data[meta.name]; - const type = data[meta.type]; - - switch (type) { - case 'html_element': - return name ? `<${name.toLowerCase()} />` : ''; - case 'react_element': - return `<${name} />`; - case 'function': - return `${name || 'fn'}()`; - case 'object': - return 'Object'; - case 'date': - case 'symbol': - return name; - case 'bigint': - return `${name}n`; - case 'iterator': - return `${name}(…)`; - case 'array_buffer': - case 'data_view': - case 'array': - case 'typed_array': - return `${name}[${data[meta.size]}]`; - default: - return null; + if (data.hasOwnProperty(meta.preview_long)) { + return data[meta.preview_long]; + } else { + return formatDataForPreview(data, true); } } diff --git a/packages/react-devtools-shared/src/hydration.js b/packages/react-devtools-shared/src/hydration.js index a4ddf7f84ffce..7aa61f5ba6270 100644 --- a/packages/react-devtools-shared/src/hydration.js +++ b/packages/react-devtools-shared/src/hydration.js @@ -9,22 +9,12 @@ import Symbol from 'es6-symbol'; import { - isElement, - typeOf, - AsyncMode, - ConcurrentMode, - ContextConsumer, - ContextProvider, - ForwardRef, - Fragment, - Lazy, - Memo, - Portal, - Profiler, - StrictMode, - Suspense, -} from 'react-is'; -import {getDisplayName, getInObject, setInObject} from './utils'; + getDataType, + getDisplayNameForReactElement, + getInObject, + formatDataForPreview, + setInObject, +} from './utils'; import type {DehydratedData} from 'react-devtools-shared/src/devtools/views/Components/types'; @@ -32,6 +22,8 @@ export const meta = { inspectable: Symbol('inspectable'), inspected: Symbol('inspected'), name: Symbol('name'), + preview_long: Symbol('preview_long'), + preview_short: Symbol('preview_short'), readonly: Symbol('readonly'), size: Symbol('size'), type: Symbol('type'), @@ -41,6 +33,8 @@ export const meta = { export type Dehydrated = {| inspectable: boolean, name: string | null, + preview_long: string | null, + preview_short: string | null, readonly?: boolean, size?: number, type: string, @@ -52,6 +46,8 @@ export type Dehydrated = {| // while preserving the original type and name. export type Unserializable = { name: string | null, + preview_long: string | null, + preview_short: string | null, readonly?: boolean, size?: number, type: string, @@ -66,84 +62,6 @@ export type Unserializable = { // but may decrease the responsiveness of expanding objects/arrays to inspect further. const LEVEL_THRESHOLD = 2; -type PropType = - | 'array' - | 'array_buffer' - | 'bigint' - | 'boolean' - | 'data_view' - | 'date' - | 'function' - | 'html_element' - | 'infinity' - | 'iterator' - | 'nan' - | 'null' - | 'number' - | 'object' - | 'react_element' - | 'string' - | 'symbol' - | 'typed_array' - | 'undefined' - | 'unknown'; - -/** - * Get a enhanced/artificial type string based on the object instance - */ -function getDataType(data: Object): PropType { - if (data === null) { - return 'null'; - } else if (data === undefined) { - return 'undefined'; - } - - if (isElement(data)) { - return 'react_element'; - } - - if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) { - return 'html_element'; - } - - const type = typeof data; - switch (type) { - case 'bigint': - return 'bigint'; - case 'boolean': - return 'boolean'; - case 'function': - return 'function'; - case 'number': - if (Number.isNaN(data)) { - return 'nan'; - } else if (!Number.isFinite(data)) { - return 'infinity'; - } else { - return 'number'; - } - case 'object': - if (Array.isArray(data)) { - return 'array'; - } else if (ArrayBuffer.isView(data)) { - return data instanceof DataView ? 'data_view' : 'typed_array'; - } else if (data instanceof ArrayBuffer) { - return 'array_buffer'; - } else if (typeof data[Symbol.iterator] === 'function') { - return 'iterator'; - } else if (Object.prototype.toString.call(data) === '[object Date]') { - return 'date'; - } - return 'object'; - case 'string': - return 'string'; - case 'symbol': - return 'symbol'; - default: - return 'unknown'; - } -} - /** * Generate the dehydrated metadata for complex object instances */ @@ -159,6 +77,8 @@ function createDehydrated( const dehydrated: Dehydrated = { inspectable, type, + preview_long: formatDataForPreview(data, true), + preview_short: formatDataForPreview(data, false), name: !data.constructor || data.constructor.name === 'Object' ? '' @@ -219,6 +139,8 @@ export function dehydrate( cleaned.push(path); return { inspectable: false, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), name: data.tagName, type, }; @@ -227,6 +149,8 @@ export function dehydrate( cleaned.push(path); return { inspectable: false, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), name: data.name, type, }; @@ -238,6 +162,8 @@ export function dehydrate( cleaned.push(path); return { inspectable: false, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), name: data.toString(), type, }; @@ -246,6 +172,8 @@ export function dehydrate( cleaned.push(path); return { inspectable: false, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), name: data.toString(), type, }; @@ -256,7 +184,9 @@ export function dehydrate( cleaned.push(path); return { inspectable: false, - name: getDisplayNameForReactElement(data), + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), + name: getDisplayNameForReactElement(data) || 'Unknown', type, }; @@ -266,6 +196,8 @@ export function dehydrate( cleaned.push(path); return { inspectable: false, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), name: type === 'data_view' ? 'DataView' : 'ArrayBuffer', size: data.byteLength, type, @@ -298,6 +230,8 @@ export function dehydrate( type: type, readonly: true, size: type === 'typed_array' ? data.length : undefined, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), name: !data.constructor || data.constructor.name === 'Object' ? '' @@ -331,6 +265,8 @@ export function dehydrate( cleaned.push(path); return { inspectable: false, + preview_short: formatDataForPreview(data, false), + preview_long: formatDataForPreview(data, true), name: data.toString(), type, }; @@ -381,6 +317,8 @@ export function fillInPath( delete target[meta.inspectable]; delete target[meta.inspected]; delete target[meta.name]; + delete target[meta.preview_long]; + delete target[meta.preview_short]; delete target[meta.readonly]; delete target[meta.size]; delete target[meta.type]; @@ -431,6 +369,8 @@ export function hydrate( replaced[meta.inspectable] = !!value.inspectable; replaced[meta.inspected] = false; replaced[meta.name] = value.name; + replaced[meta.preview_long] = value.preview_long; + replaced[meta.preview_short] = value.preview_short; replaced[meta.size] = value.size; replaced[meta.readonly] = !!value.readonly; replaced[meta.type] = value.type; @@ -471,6 +411,16 @@ function upgradeUnserializable(destination: Object, source: Object) { enumerable: false, value: source.name, }, + [meta.preview_long]: { + configurable: true, + enumerable: false, + value: source.preview_long, + }, + [meta.preview_short]: { + configurable: true, + enumerable: false, + value: source.preview_short, + }, [meta.size]: { configurable: true, enumerable: false, @@ -495,48 +445,10 @@ function upgradeUnserializable(destination: Object, source: Object) { delete destination.inspected; delete destination.name; + delete destination.preview_long; + delete destination.preview_short; delete destination.size; delete destination.readonly; delete destination.type; delete destination.unserializable; } - -export function getDisplayNameForReactElement( - element: React$Element, -): string | null { - const elementType = typeOf(element); - switch (elementType) { - case AsyncMode: - case ConcurrentMode: - return 'ConcurrentMode'; - case ContextConsumer: - return 'ContextConsumer'; - case ContextProvider: - return 'ContextProvider'; - case ForwardRef: - return 'ForwardRef'; - case Fragment: - return 'Fragment'; - case Lazy: - return 'Lazy'; - case Memo: - return 'Memo'; - case Portal: - return 'Portal'; - case Profiler: - return 'Profiler'; - case StrictMode: - return 'StrictMode'; - case Suspense: - return 'Suspense'; - default: - const {type} = element; - if (typeof type === 'string') { - return type; - } else if (type != null) { - return getDisplayName(type, 'Anonymous'); - } else { - return 'Element'; - } - } -} diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 5c22b92100b62..fc21e3ebdb5fd 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -9,6 +9,22 @@ import Symbol from 'es6-symbol'; import LRU from 'lru-cache'; +import { + isElement, + typeOf, + AsyncMode, + ConcurrentMode, + ContextConsumer, + ContextProvider, + ForwardRef, + Fragment, + Lazy, + Memo, + Portal, + Profiler, + StrictMode, + Suspense, +} from 'react-is'; import { TREE_OPERATION_ADD, TREE_OPERATION_REMOVE, @@ -28,6 +44,8 @@ import { ElementTypeMemo, } from 'react-devtools-shared/src/types'; import {localStorageGetItem, localStorageSetItem} from './storage'; +import {alphaSortEntries} from './devtools/views/utils'; +import {meta} from './hydration'; import type {ComponentFilter, ElementType} from './types'; @@ -305,3 +323,309 @@ export function setInObject( } } } + +export type DataType = + | 'array' + | 'array_buffer' + | 'bigint' + | 'boolean' + | 'data_view' + | 'date' + | 'function' + | 'html_element' + | 'infinity' + | 'iterator' + | 'nan' + | 'null' + | 'number' + | 'object' + | 'react_element' + | 'string' + | 'symbol' + | 'typed_array' + | 'undefined' + | 'unknown'; + +/** + * Get a enhanced/artificial type string based on the object instance + */ +export function getDataType(data: Object): DataType { + if (data === null) { + return 'null'; + } else if (data === undefined) { + return 'undefined'; + } + + if (isElement(data)) { + return 'react_element'; + } + + if (typeof HTMLElement !== 'undefined' && data instanceof HTMLElement) { + return 'html_element'; + } + + const type = typeof data; + switch (type) { + case 'bigint': + return 'bigint'; + case 'boolean': + return 'boolean'; + case 'function': + return 'function'; + case 'number': + if (Number.isNaN(data)) { + return 'nan'; + } else if (!Number.isFinite(data)) { + return 'infinity'; + } else { + return 'number'; + } + case 'object': + if (Array.isArray(data)) { + return 'array'; + } else if (ArrayBuffer.isView(data)) { + return data.constructor.hasOwnProperty('BYTES_PER_ELEMENT') + ? 'typed_array' + : 'data_view'; + } else if (data.constructor.name === 'ArrayBuffer') { + // HACK This ArrayBuffer check is gross; is there a better way? + // We could try to create a new DataView with the value. + // If it doesn't error, we know it's an ArrayBuffer, + // but this seems kind of awkward and expensive. + return 'array_buffer'; + } else if (typeof data[Symbol.iterator] === 'function') { + return 'iterator'; + } else if (Object.prototype.toString.call(data) === '[object Date]') { + return 'date'; + } + return 'object'; + case 'string': + return 'string'; + case 'symbol': + return 'symbol'; + default: + return 'unknown'; + } +} + +export function getDisplayNameForReactElement( + element: React$Element, +): string | null { + const elementType = typeOf(element); + switch (elementType) { + case AsyncMode: + case ConcurrentMode: + return 'ConcurrentMode'; + case ContextConsumer: + return 'ContextConsumer'; + case ContextProvider: + return 'ContextProvider'; + case ForwardRef: + return 'ForwardRef'; + case Fragment: + return 'Fragment'; + case Lazy: + return 'Lazy'; + case Memo: + return 'Memo'; + case Portal: + return 'Portal'; + case Profiler: + return 'Profiler'; + case StrictMode: + return 'StrictMode'; + case Suspense: + return 'Suspense'; + default: + const {type} = element; + if (typeof type === 'string') { + return type; + } else if (type != null) { + return getDisplayName(type, 'Anonymous'); + } else { + return 'Element'; + } + } +} + +const MAX_PREVIEW_STRING_LENGTH = 50; + +function truncateForDisplay( + string: string, + length: number = MAX_PREVIEW_STRING_LENGTH, +) { + if (string.length > length) { + return string.substr(0, length) + '…'; + } else { + return string; + } +} + +// Attempts to mimic Chrome's inline preview for values. +// For example, the following value... +// { +// foo: 123, +// bar: "abc", +// baz: [true, false], +// qux: { ab: 1, cd: 2 } +// }; +// +// Would show a preview of... +// {foo: 123, bar: "abc", baz: Array(2), qux: {…}} +// +// And the following value... +// [ +// 123, +// "abc", +// [true, false], +// { foo: 123, bar: "abc" } +// ]; +// +// Would show a preview of... +// [123, "abc", Array(2), {…}] +export function formatDataForPreview( + data: any, + showFormattedValue: boolean, +): string { + if (data != null && data.hasOwnProperty(meta.type)) { + return showFormattedValue + ? data[meta.preview_long] + : data[meta.preview_short]; + } + + const type = getDataType(data); + + switch (type) { + case 'html_element': + return `<${truncateForDisplay(data.tagName.toLowerCase())} />`; + case 'function': + return truncateForDisplay(data.name); + case 'string': + return `"${data}"`; + case 'bigint': + return truncateForDisplay(data.toString() + 'n'); + case 'symbol': + return truncateForDisplay(data.toString()); + case 'react_element': + return `<${truncateForDisplay( + getDisplayNameForReactElement(data) || 'Unknown', + )} />`; + case 'array_buffer': + return `ArrayBuffer(${data.byteLength})`; + case 'data_view': + return `DataView(${data.buffer.byteLength})`; + case 'array': + if (showFormattedValue) { + let formatted = ''; + for (let i = 0; i < data.length; i++) { + if (i > 0) { + formatted += ', '; + } + formatted += formatDataForPreview(data[i], false); + if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { + // Prevent doing a lot of unnecessary iteration... + break; + } + } + return `[${truncateForDisplay(formatted)}]`; + } else { + const length = data.hasOwnProperty(meta.size) + ? data[meta.size] + : data.length; + return `Array(${length})`; + } + case 'typed_array': + const shortName = `${data.constructor.name}(${data.length})`; + if (showFormattedValue) { + let formatted = ''; + for (let i = 0; i < data.length; i++) { + if (i > 0) { + formatted += ', '; + } + formatted += data[i]; + if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { + // Prevent doing a lot of unnecessary iteration... + break; + } + } + return `${shortName} [${truncateForDisplay(formatted)}]`; + } else { + return shortName; + } + case 'iterator': + const name = data.constructor.name; + if (showFormattedValue) { + // TRICKY + // Don't use [...spread] syntax for this purpose. + // This project uses @babel/plugin-transform-spread in "loose" mode which only works with Array values. + // Other types (e.g. typed arrays, Sets) will not spread correctly. + const array = Array.from(data); + + let formatted = ''; + for (let i = 0; i < array.length; i++) { + const entryOrEntries = array[i]; + + if (i > 0) { + formatted += ', '; + } + + // TRICKY + // Browsers display Maps and Sets differently. + // To mimic their behavior, detect if we've been given an entries tuple. + // Map(2) {"abc" => 123, "def" => 123} + // Set(2) {"abc", 123} + if (Array.isArray(entryOrEntries)) { + const key = formatDataForPreview(entryOrEntries[0], true); + const value = formatDataForPreview(entryOrEntries[1], false); + formatted += `${key} => ${value}`; + } else { + formatted += formatDataForPreview(entryOrEntries, false); + } + + if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { + // Prevent doing a lot of unnecessary iteration... + break; + } + } + + return `${name}(${data.size}) {${truncateForDisplay(formatted)}}`; + } else { + return `${name}(${data.size})`; + } + case 'date': + return data.toString(); + case 'object': + if (showFormattedValue) { + const keys = Object.keys(data).sort(alphaSortEntries); + + let formatted = ''; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (i > 0) { + formatted += ', '; + } + formatted += `${key}: ${formatDataForPreview(data[key], false)}`; + if (formatted.length > MAX_PREVIEW_STRING_LENGTH) { + // Prevent doing a lot of unnecessary iteration... + break; + } + } + return `{${truncateForDisplay(formatted)}}`; + } else { + return '{…}'; + } + case 'boolean': + case 'number': + case 'infinity': + case 'nan': + case 'null': + case 'undefined': + return data; + default: + try { + return truncateForDisplay('' + data); + } catch (error) { + return 'unserializable'; + } + } +} diff --git a/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js b/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js index cd6ecc9d1c230..effe7f14a348f 100644 --- a/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js +++ b/packages/react-devtools-shell/src/app/InspectableElements/UnserializableProps.js @@ -15,6 +15,8 @@ const map = new Map([['name', 'Brian'], ['food', 'sushi']]); const setOfSets = new Set([new Set(['a', 'b', 'c']), new Set([1, 2, 3])]); const mapOfMaps = new Map([['first', map], ['second', map]]); const typedArray = Int8Array.from([100, -100, 0]); +const arrayBuffer = typedArray.buffer; +const dataView = new DataView(arrayBuffer); const immutable = Immutable.fromJS({ a: [{hello: 'there'}, 'fixed', true], b: 123, @@ -27,6 +29,8 @@ const immutable = Immutable.fromJS({ export default function UnserializableProps() { return (