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 JSON5 from 'json5';
    
  11. 
    
  12. import type {Element} from 'react-devtools-shared/src/frontend/types';
    
  13. import type {StateContext} from './views/Components/TreeContext';
    
  14. import type Store from './store';
    
  15. 
    
  16. export function printElement(
    
  17.   element: Element,
    
  18.   includeWeight: boolean = false,
    
  19. ): string {
    
  20.   let prefix = ' ';
    
  21.   if (element.children.length > 0) {
    
  22.     prefix = element.isCollapsed ? '' : '';
    
  23.   }
    
  24. 
    
  25.   let key = '';
    
  26.   if (element.key !== null) {
    
  27.     key = ` key="${element.key}"`;
    
  28.   }
    
  29. 
    
  30.   let hocDisplayNames = null;
    
  31.   if (element.hocDisplayNames !== null) {
    
  32.     hocDisplayNames = [...element.hocDisplayNames];
    
  33.   }
    
  34. 
    
  35.   const hocs =
    
  36.     hocDisplayNames === null ? '' : ` [${hocDisplayNames.join('][')}]`;
    
  37. 
    
  38.   let suffix = '';
    
  39.   if (includeWeight) {
    
  40.     suffix = ` (${element.isCollapsed ? 1 : element.weight})`;
    
  41.   }
    
  42. 
    
  43.   return `${'  '.repeat(element.depth + 1)}${prefix} <${
    
  44.     element.displayName || 'null'
    
  45.   }${key}>${hocs}${suffix}`;
    
  46. }
    
  47. 
    
  48. export function printOwnersList(
    
  49.   elements: Array<Element>,
    
  50.   includeWeight: boolean = false,
    
  51. ): string {
    
  52.   return elements
    
  53.     .map(element => printElement(element, includeWeight))
    
  54.     .join('\n');
    
  55. }
    
  56. 
    
  57. export function printStore(
    
  58.   store: Store,
    
  59.   includeWeight: boolean = false,
    
  60.   state: StateContext | null = null,
    
  61. ): string {
    
  62.   const snapshotLines = [];
    
  63. 
    
  64.   let rootWeight = 0;
    
  65. 
    
  66.   function printSelectedMarker(index: number): string {
    
  67.     if (state === null) {
    
  68.       return '';
    
  69.     }
    
  70.     return state.selectedElementIndex === index ? `` : ' ';
    
  71.   }
    
  72. 
    
  73.   function printErrorsAndWarnings(element: Element): string {
    
  74.     const {errorCount, warningCount} =
    
  75.       store.getErrorAndWarningCountForElementID(element.id);
    
  76.     if (errorCount === 0 && warningCount === 0) {
    
  77.       return '';
    
  78.     }
    
  79.     return ` ${errorCount > 0 ? '' : ''}${warningCount > 0 ? '' : ''}`;
    
  80.   }
    
  81. 
    
  82.   const ownerFlatTree = state !== null ? state.ownerFlatTree : null;
    
  83.   if (ownerFlatTree !== null) {
    
  84.     snapshotLines.push(
    
  85.       '[owners]' + (includeWeight ? ` (${ownerFlatTree.length})` : ''),
    
  86.     );
    
  87.     ownerFlatTree.forEach((element, index) => {
    
  88.       const printedSelectedMarker = printSelectedMarker(index);
    
  89.       const printedElement = printElement(element, false);
    
  90.       const printedErrorsAndWarnings = printErrorsAndWarnings(element);
    
  91.       snapshotLines.push(
    
  92.         `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
    
  93.       );
    
  94.     });
    
  95.   } else {
    
  96.     const errorsAndWarnings = store._errorsAndWarnings;
    
  97.     if (errorsAndWarnings.size > 0) {
    
  98.       let errorCount = 0;
    
  99.       let warningCount = 0;
    
  100.       errorsAndWarnings.forEach(entry => {
    
  101.         errorCount += entry.errorCount;
    
  102.         warningCount += entry.warningCount;
    
  103.       });
    
  104. 
    
  105.       snapshotLines.push(`${errorCount}, ⚠ ${warningCount}`);
    
  106.     }
    
  107. 
    
  108.     store.roots.forEach(rootID => {
    
  109.       const {weight} = ((store.getElementByID(rootID): any): Element);
    
  110.       const maybeWeightLabel = includeWeight ? ` (${weight})` : '';
    
  111. 
    
  112.       // Store does not (yet) expose a way to get errors/warnings per root.
    
  113.       snapshotLines.push(`[root]${maybeWeightLabel}`);
    
  114. 
    
  115.       for (let i = rootWeight; i < rootWeight + weight; i++) {
    
  116.         const element = store.getElementAtIndex(i);
    
  117. 
    
  118.         if (element == null) {
    
  119.           throw Error(`Could not find element at index "${i}"`);
    
  120.         }
    
  121. 
    
  122.         const printedSelectedMarker = printSelectedMarker(i);
    
  123.         const printedElement = printElement(element, includeWeight);
    
  124.         const printedErrorsAndWarnings = printErrorsAndWarnings(element);
    
  125.         snapshotLines.push(
    
  126.           `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
    
  127.         );
    
  128.       }
    
  129. 
    
  130.       rootWeight += weight;
    
  131.     });
    
  132. 
    
  133.     // Make sure the pretty-printed test align with the Store's reported number of total rows.
    
  134.     if (rootWeight !== store.numElements) {
    
  135.       throw Error(
    
  136.         `Inconsistent Store state. Individual root weights ("${rootWeight}") do not match total weight ("${store.numElements}")`,
    
  137.       );
    
  138.     }
    
  139. 
    
  140.     // If roots have been unmounted, verify that they've been removed from maps.
    
  141.     // This helps ensure the Store doesn't leak memory.
    
  142.     store.assertExpectedRootMapSizes();
    
  143.   }
    
  144. 
    
  145.   return snapshotLines.join('\n');
    
  146. }
    
  147. 
    
  148. // We use JSON.parse to parse string values
    
  149. // e.g. 'foo' is not valid JSON but it is a valid string
    
  150. // so this method replaces e.g. 'foo' with "foo"
    
  151. export function sanitizeForParse(value: any): any | string {
    
  152.   if (typeof value === 'string') {
    
  153.     if (
    
  154.       value.length >= 2 &&
    
  155.       value.charAt(0) === "'" &&
    
  156.       value.charAt(value.length - 1) === "'"
    
  157.     ) {
    
  158.       return '"' + value.slice(1, value.length - 1) + '"';
    
  159.     }
    
  160.   }
    
  161.   return value;
    
  162. }
    
  163. 
    
  164. export function smartParse(value: any): any | void | number {
    
  165.   switch (value) {
    
  166.     case 'Infinity':
    
  167.       return Infinity;
    
  168.     case 'NaN':
    
  169.       return NaN;
    
  170.     case 'undefined':
    
  171.       return undefined;
    
  172.     default:
    
  173.       return JSON5.parse(sanitizeForParse(value));
    
  174.   }
    
  175. }
    
  176. 
    
  177. export function smartStringify(value: any): string {
    
  178.   if (typeof value === 'number') {
    
  179.     if (Number.isNaN(value)) {
    
  180.       return 'NaN';
    
  181.     } else if (!Number.isFinite(value)) {
    
  182.       return 'Infinity';
    
  183.     }
    
  184.   } else if (value === undefined) {
    
  185.     return 'undefined';
    
  186.   }
    
  187. 
    
  188.   return JSON.stringify(value);
    
  189. }
    
  190. 
    
  191. // [url, row, column]
    
  192. export type Stack = [string, number, number];
    
  193. 
    
  194. const STACK_DELIMETER = /\n\s+at /;
    
  195. const STACK_SOURCE_LOCATION = /([^\s]+) \((.+):(.+):(.+)\)/;
    
  196. 
    
  197. export function stackToComponentSources(
    
  198.   stack: string,
    
  199. ): Array<[string, ?Stack]> {
    
  200.   const out: Array<[string, ?Stack]> = [];
    
  201.   stack
    
  202.     .split(STACK_DELIMETER)
    
  203.     .slice(1)
    
  204.     .forEach(entry => {
    
  205.       const match = STACK_SOURCE_LOCATION.exec(entry);
    
  206.       if (match) {
    
  207.         const [, component, url, row, column] = match;
    
  208.         out.push([component, [url, parseInt(row, 10), parseInt(column, 10)]]);
    
  209.       } else {
    
  210.         out.push([entry, null]);
    
  211.       }
    
  212.     });
    
  213.   return out;
    
  214. }