1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import {
    
  11.   getDataType,
    
  12.   getDisplayNameForReactElement,
    
  13.   getAllEnumerableKeys,
    
  14.   getInObject,
    
  15.   formatDataForPreview,
    
  16.   setInObject,
    
  17. } from 'react-devtools-shared/src/utils';
    
  18. 
    
  19. import type {
    
  20.   DehydratedData,
    
  21.   InspectedElementPath,
    
  22. } from 'react-devtools-shared/src/frontend/types';
    
  23. 
    
  24. export const meta = {
    
  25.   inspectable: (Symbol('inspectable'): symbol),
    
  26.   inspected: (Symbol('inspected'): symbol),
    
  27.   name: (Symbol('name'): symbol),
    
  28.   preview_long: (Symbol('preview_long'): symbol),
    
  29.   preview_short: (Symbol('preview_short'): symbol),
    
  30.   readonly: (Symbol('readonly'): symbol),
    
  31.   size: (Symbol('size'): symbol),
    
  32.   type: (Symbol('type'): symbol),
    
  33.   unserializable: (Symbol('unserializable'): symbol),
    
  34. };
    
  35. 
    
  36. export type Dehydrated = {
    
  37.   inspectable: boolean,
    
  38.   name: string | null,
    
  39.   preview_long: string | null,
    
  40.   preview_short: string | null,
    
  41.   readonly?: boolean,
    
  42.   size?: number,
    
  43.   type: string,
    
  44. };
    
  45. 
    
  46. // Typed arrays and other complex iteratable objects (e.g. Map, Set, ImmutableJS) need special handling.
    
  47. // These objects can't be serialized without losing type information,
    
  48. // so a "Unserializable" type wrapper is used (with meta-data keys) to send nested values-
    
  49. // while preserving the original type and name.
    
  50. export type Unserializable = {
    
  51.   name: string | null,
    
  52.   preview_long: string | null,
    
  53.   preview_short: string | null,
    
  54.   readonly?: boolean,
    
  55.   size?: number,
    
  56.   type: string,
    
  57.   unserializable: boolean,
    
  58.   [string | number]: any,
    
  59. };
    
  60. 
    
  61. // This threshold determines the depth at which the bridge "dehydrates" nested data.
    
  62. // Dehydration means that we don't serialize the data for e.g. postMessage or stringify,
    
  63. // unless the frontend explicitly requests it (e.g. a user clicks to expand a props object).
    
  64. //
    
  65. // Reducing this threshold will improve the speed of initial component inspection,
    
  66. // but may decrease the responsiveness of expanding objects/arrays to inspect further.
    
  67. const LEVEL_THRESHOLD = 2;
    
  68. 
    
  69. /**
    
  70.  * Generate the dehydrated metadata for complex object instances
    
  71.  */
    
  72. function createDehydrated(
    
  73.   type: string,
    
  74.   inspectable: boolean,
    
  75.   data: Object,
    
  76.   cleaned: Array<Array<string | number>>,
    
  77.   path: Array<string | number>,
    
  78. ): Dehydrated {
    
  79.   cleaned.push(path);
    
  80. 
    
  81.   const dehydrated: Dehydrated = {
    
  82.     inspectable,
    
  83.     type,
    
  84.     preview_long: formatDataForPreview(data, true),
    
  85.     preview_short: formatDataForPreview(data, false),
    
  86.     name:
    
  87.       !data.constructor || data.constructor.name === 'Object'
    
  88.         ? ''
    
  89.         : data.constructor.name,
    
  90.   };
    
  91. 
    
  92.   if (type === 'array' || type === 'typed_array') {
    
  93.     dehydrated.size = data.length;
    
  94.   } else if (type === 'object') {
    
  95.     dehydrated.size = Object.keys(data).length;
    
  96.   }
    
  97. 
    
  98.   if (type === 'iterator' || type === 'typed_array') {
    
  99.     dehydrated.readonly = true;
    
  100.   }
    
  101. 
    
  102.   return dehydrated;
    
  103. }
    
  104. 
    
  105. /**
    
  106.  * Strip out complex data (instances, functions, and data nested > LEVEL_THRESHOLD levels deep).
    
  107.  * The paths of the stripped out objects are appended to the `cleaned` list.
    
  108.  * On the other side of the barrier, the cleaned list is used to "re-hydrate" the cleaned representation into
    
  109.  * an object with symbols as attributes, so that a sanitized object can be distinguished from a normal object.
    
  110.  *
    
  111.  * Input: {"some": {"attr": fn()}, "other": AnInstance}
    
  112.  * Output: {
    
  113.  *   "some": {
    
  114.  *     "attr": {"name": the fn.name, type: "function"}
    
  115.  *   },
    
  116.  *   "other": {
    
  117.  *     "name": "AnInstance",
    
  118.  *     "type": "object",
    
  119.  *   },
    
  120.  * }
    
  121.  * and cleaned = [["some", "attr"], ["other"]]
    
  122.  */
    
  123. export function dehydrate(
    
  124.   data: Object,
    
  125.   cleaned: Array<Array<string | number>>,
    
  126.   unserializable: Array<Array<string | number>>,
    
  127.   path: Array<string | number>,
    
  128.   isPathAllowed: (path: Array<string | number>) => boolean,
    
  129.   level: number = 0,
    
  130. ): $PropertyType<DehydratedData, 'data'> {
    
  131.   const type = getDataType(data);
    
  132. 
    
  133.   let isPathAllowedCheck;
    
  134. 
    
  135.   switch (type) {
    
  136.     case 'html_element':
    
  137.       cleaned.push(path);
    
  138.       return {
    
  139.         inspectable: false,
    
  140.         preview_short: formatDataForPreview(data, false),
    
  141.         preview_long: formatDataForPreview(data, true),
    
  142.         name: data.tagName,
    
  143.         type,
    
  144.       };
    
  145. 
    
  146.     case 'function':
    
  147.       cleaned.push(path);
    
  148.       return {
    
  149.         inspectable: false,
    
  150.         preview_short: formatDataForPreview(data, false),
    
  151.         preview_long: formatDataForPreview(data, true),
    
  152.         name:
    
  153.           typeof data.name === 'function' || !data.name
    
  154.             ? 'function'
    
  155.             : data.name,
    
  156.         type,
    
  157.       };
    
  158. 
    
  159.     case 'string':
    
  160.       isPathAllowedCheck = isPathAllowed(path);
    
  161.       if (isPathAllowedCheck) {
    
  162.         return data;
    
  163.       } else {
    
  164.         return data.length <= 500 ? data : data.slice(0, 500) + '...';
    
  165.       }
    
  166. 
    
  167.     case 'bigint':
    
  168.       cleaned.push(path);
    
  169.       return {
    
  170.         inspectable: false,
    
  171.         preview_short: formatDataForPreview(data, false),
    
  172.         preview_long: formatDataForPreview(data, true),
    
  173.         name: data.toString(),
    
  174.         type,
    
  175.       };
    
  176. 
    
  177.     case 'symbol':
    
  178.       cleaned.push(path);
    
  179.       return {
    
  180.         inspectable: false,
    
  181.         preview_short: formatDataForPreview(data, false),
    
  182.         preview_long: formatDataForPreview(data, true),
    
  183.         name: data.toString(),
    
  184.         type,
    
  185.       };
    
  186. 
    
  187.     // React Elements aren't very inspector-friendly,
    
  188.     // and often contain private fields or circular references.
    
  189.     case 'react_element':
    
  190.       cleaned.push(path);
    
  191.       return {
    
  192.         inspectable: false,
    
  193.         preview_short: formatDataForPreview(data, false),
    
  194.         preview_long: formatDataForPreview(data, true),
    
  195.         name: getDisplayNameForReactElement(data) || 'Unknown',
    
  196.         type,
    
  197.       };
    
  198. 
    
  199.     // ArrayBuffers error if you try to inspect them.
    
  200.     case 'array_buffer':
    
  201.     case 'data_view':
    
  202.       cleaned.push(path);
    
  203.       return {
    
  204.         inspectable: false,
    
  205.         preview_short: formatDataForPreview(data, false),
    
  206.         preview_long: formatDataForPreview(data, true),
    
  207.         name: type === 'data_view' ? 'DataView' : 'ArrayBuffer',
    
  208.         size: data.byteLength,
    
  209.         type,
    
  210.       };
    
  211. 
    
  212.     case 'array':
    
  213.       isPathAllowedCheck = isPathAllowed(path);
    
  214.       if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
    
  215.         return createDehydrated(type, true, data, cleaned, path);
    
  216.       }
    
  217.       return data.map((item, i) =>
    
  218.         dehydrate(
    
  219.           item,
    
  220.           cleaned,
    
  221.           unserializable,
    
  222.           path.concat([i]),
    
  223.           isPathAllowed,
    
  224.           isPathAllowedCheck ? 1 : level + 1,
    
  225.         ),
    
  226.       );
    
  227. 
    
  228.     case 'html_all_collection':
    
  229.     case 'typed_array':
    
  230.     case 'iterator':
    
  231.       isPathAllowedCheck = isPathAllowed(path);
    
  232.       if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
    
  233.         return createDehydrated(type, true, data, cleaned, path);
    
  234.       } else {
    
  235.         const unserializableValue: Unserializable = {
    
  236.           unserializable: true,
    
  237.           type: type,
    
  238.           readonly: true,
    
  239.           size: type === 'typed_array' ? data.length : undefined,
    
  240.           preview_short: formatDataForPreview(data, false),
    
  241.           preview_long: formatDataForPreview(data, true),
    
  242.           name:
    
  243.             !data.constructor || data.constructor.name === 'Object'
    
  244.               ? ''
    
  245.               : data.constructor.name,
    
  246.         };
    
  247. 
    
  248.         // TRICKY
    
  249.         // Don't use [...spread] syntax for this purpose.
    
  250.         // This project uses @babel/plugin-transform-spread in "loose" mode which only works with Array values.
    
  251.         // Other types (e.g. typed arrays, Sets) will not spread correctly.
    
  252.         Array.from(data).forEach(
    
  253.           (item, i) =>
    
  254.             (unserializableValue[i] = dehydrate(
    
  255.               item,
    
  256.               cleaned,
    
  257.               unserializable,
    
  258.               path.concat([i]),
    
  259.               isPathAllowed,
    
  260.               isPathAllowedCheck ? 1 : level + 1,
    
  261.             )),
    
  262.         );
    
  263. 
    
  264.         unserializable.push(path);
    
  265. 
    
  266.         return unserializableValue;
    
  267.       }
    
  268. 
    
  269.     case 'opaque_iterator':
    
  270.       cleaned.push(path);
    
  271.       return {
    
  272.         inspectable: false,
    
  273.         preview_short: formatDataForPreview(data, false),
    
  274.         preview_long: formatDataForPreview(data, true),
    
  275.         name: data[Symbol.toStringTag],
    
  276.         type,
    
  277.       };
    
  278. 
    
  279.     case 'date':
    
  280.       cleaned.push(path);
    
  281.       return {
    
  282.         inspectable: false,
    
  283.         preview_short: formatDataForPreview(data, false),
    
  284.         preview_long: formatDataForPreview(data, true),
    
  285.         name: data.toString(),
    
  286.         type,
    
  287.       };
    
  288. 
    
  289.     case 'regexp':
    
  290.       cleaned.push(path);
    
  291.       return {
    
  292.         inspectable: false,
    
  293.         preview_short: formatDataForPreview(data, false),
    
  294.         preview_long: formatDataForPreview(data, true),
    
  295.         name: data.toString(),
    
  296.         type,
    
  297.       };
    
  298. 
    
  299.     case 'object':
    
  300.       isPathAllowedCheck = isPathAllowed(path);
    
  301. 
    
  302.       if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
    
  303.         return createDehydrated(type, true, data, cleaned, path);
    
  304.       } else {
    
  305.         const object: {
    
  306.           [string]: $PropertyType<DehydratedData, 'data'>,
    
  307.         } = {};
    
  308.         getAllEnumerableKeys(data).forEach(key => {
    
  309.           const name = key.toString();
    
  310.           object[name] = dehydrate(
    
  311.             data[key],
    
  312.             cleaned,
    
  313.             unserializable,
    
  314.             path.concat([name]),
    
  315.             isPathAllowed,
    
  316.             isPathAllowedCheck ? 1 : level + 1,
    
  317.           );
    
  318.         });
    
  319.         return object;
    
  320.       }
    
  321. 
    
  322.     case 'class_instance':
    
  323.       isPathAllowedCheck = isPathAllowed(path);
    
  324. 
    
  325.       if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
    
  326.         return createDehydrated(type, true, data, cleaned, path);
    
  327.       }
    
  328. 
    
  329.       const value: Unserializable = {
    
  330.         unserializable: true,
    
  331.         type,
    
  332.         readonly: true,
    
  333.         preview_short: formatDataForPreview(data, false),
    
  334.         preview_long: formatDataForPreview(data, true),
    
  335.         name: data.constructor.name,
    
  336.       };
    
  337. 
    
  338.       getAllEnumerableKeys(data).forEach(key => {
    
  339.         const keyAsString = key.toString();
    
  340. 
    
  341.         value[keyAsString] = dehydrate(
    
  342.           data[key],
    
  343.           cleaned,
    
  344.           unserializable,
    
  345.           path.concat([keyAsString]),
    
  346.           isPathAllowed,
    
  347.           isPathAllowedCheck ? 1 : level + 1,
    
  348.         );
    
  349.       });
    
  350. 
    
  351.       unserializable.push(path);
    
  352. 
    
  353.       return value;
    
  354. 
    
  355.     case 'infinity':
    
  356.     case 'nan':
    
  357.     case 'undefined':
    
  358.       // Some values are lossy when sent through a WebSocket.
    
  359.       // We dehydrate+rehydrate them to preserve their type.
    
  360.       cleaned.push(path);
    
  361.       return {type};
    
  362. 
    
  363.     default:
    
  364.       return data;
    
  365.   }
    
  366. }
    
  367. 
    
  368. export function fillInPath(
    
  369.   object: Object,
    
  370.   data: DehydratedData,
    
  371.   path: InspectedElementPath,
    
  372.   value: any,
    
  373. ) {
    
  374.   const target = getInObject(object, path);
    
  375.   if (target != null) {
    
  376.     if (!target[meta.unserializable]) {
    
  377.       delete target[meta.inspectable];
    
  378.       delete target[meta.inspected];
    
  379.       delete target[meta.name];
    
  380.       delete target[meta.preview_long];
    
  381.       delete target[meta.preview_short];
    
  382.       delete target[meta.readonly];
    
  383.       delete target[meta.size];
    
  384.       delete target[meta.type];
    
  385.     }
    
  386.   }
    
  387. 
    
  388.   if (value !== null && data.unserializable.length > 0) {
    
  389.     const unserializablePath = data.unserializable[0];
    
  390.     let isMatch = unserializablePath.length === path.length;
    
  391.     for (let i = 0; i < path.length; i++) {
    
  392.       if (path[i] !== unserializablePath[i]) {
    
  393.         isMatch = false;
    
  394.         break;
    
  395.       }
    
  396.     }
    
  397.     if (isMatch) {
    
  398.       upgradeUnserializable(value, value);
    
  399.     }
    
  400.   }
    
  401. 
    
  402.   setInObject(object, path, value);
    
  403. }
    
  404. 
    
  405. export function hydrate(
    
  406.   object: any,
    
  407.   cleaned: Array<Array<string | number>>,
    
  408.   unserializable: Array<Array<string | number>>,
    
  409. ): Object {
    
  410.   cleaned.forEach((path: Array<string | number>) => {
    
  411.     const length = path.length;
    
  412.     const last = path[length - 1];
    
  413.     const parent = getInObject(object, path.slice(0, length - 1));
    
  414.     if (!parent || !parent.hasOwnProperty(last)) {
    
  415.       return;
    
  416.     }
    
  417. 
    
  418.     const value = parent[last];
    
  419. 
    
  420.     if (!value) {
    
  421.       return;
    
  422.     } else if (value.type === 'infinity') {
    
  423.       parent[last] = Infinity;
    
  424.     } else if (value.type === 'nan') {
    
  425.       parent[last] = NaN;
    
  426.     } else if (value.type === 'undefined') {
    
  427.       parent[last] = undefined;
    
  428.     } else {
    
  429.       // Replace the string keys with Symbols so they're non-enumerable.
    
  430.       const replaced: {[key: symbol]: boolean | string, ...} = {};
    
  431.       replaced[meta.inspectable] = !!value.inspectable;
    
  432.       replaced[meta.inspected] = false;
    
  433.       replaced[meta.name] = value.name;
    
  434.       replaced[meta.preview_long] = value.preview_long;
    
  435.       replaced[meta.preview_short] = value.preview_short;
    
  436.       replaced[meta.size] = value.size;
    
  437.       replaced[meta.readonly] = !!value.readonly;
    
  438.       replaced[meta.type] = value.type;
    
  439. 
    
  440.       parent[last] = replaced;
    
  441.     }
    
  442.   });
    
  443.   unserializable.forEach((path: Array<string | number>) => {
    
  444.     const length = path.length;
    
  445.     const last = path[length - 1];
    
  446.     const parent = getInObject(object, path.slice(0, length - 1));
    
  447.     if (!parent || !parent.hasOwnProperty(last)) {
    
  448.       return;
    
  449.     }
    
  450. 
    
  451.     const node = parent[last];
    
  452. 
    
  453.     const replacement = {
    
  454.       ...node,
    
  455.     };
    
  456. 
    
  457.     upgradeUnserializable(replacement, node);
    
  458. 
    
  459.     parent[last] = replacement;
    
  460.   });
    
  461.   return object;
    
  462. }
    
  463. 
    
  464. function upgradeUnserializable(destination: Object, source: Object) {
    
  465.   Object.defineProperties(destination, {
    
  466.     // $FlowFixMe[invalid-computed-prop]
    
  467.     [meta.inspected]: {
    
  468.       configurable: true,
    
  469.       enumerable: false,
    
  470.       value: !!source.inspected,
    
  471.     },
    
  472.     // $FlowFixMe[invalid-computed-prop]
    
  473.     [meta.name]: {
    
  474.       configurable: true,
    
  475.       enumerable: false,
    
  476.       value: source.name,
    
  477.     },
    
  478.     // $FlowFixMe[invalid-computed-prop]
    
  479.     [meta.preview_long]: {
    
  480.       configurable: true,
    
  481.       enumerable: false,
    
  482.       value: source.preview_long,
    
  483.     },
    
  484.     // $FlowFixMe[invalid-computed-prop]
    
  485.     [meta.preview_short]: {
    
  486.       configurable: true,
    
  487.       enumerable: false,
    
  488.       value: source.preview_short,
    
  489.     },
    
  490.     // $FlowFixMe[invalid-computed-prop]
    
  491.     [meta.size]: {
    
  492.       configurable: true,
    
  493.       enumerable: false,
    
  494.       value: source.size,
    
  495.     },
    
  496.     // $FlowFixMe[invalid-computed-prop]
    
  497.     [meta.readonly]: {
    
  498.       configurable: true,
    
  499.       enumerable: false,
    
  500.       value: !!source.readonly,
    
  501.     },
    
  502.     // $FlowFixMe[invalid-computed-prop]
    
  503.     [meta.type]: {
    
  504.       configurable: true,
    
  505.       enumerable: false,
    
  506.       value: source.type,
    
  507.     },
    
  508.     // $FlowFixMe[invalid-computed-prop]
    
  509.     [meta.unserializable]: {
    
  510.       configurable: true,
    
  511.       enumerable: false,
    
  512.       value: !!source.unserializable,
    
  513.     },
    
  514.   });
    
  515. 
    
  516.   delete destination.inspected;
    
  517.   delete destination.name;
    
  518.   delete destination.preview_long;
    
  519.   delete destination.preview_short;
    
  520.   delete destination.size;
    
  521.   delete destination.readonly;
    
  522.   delete destination.type;
    
  523.   delete destination.unserializable;
    
  524. }