1. /**
    
  2. /**
    
  3.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  4.  *
    
  5.  * This source code is licensed under the MIT license found in the
    
  6.  * LICENSE file in the root directory of this source tree.
    
  7.  *
    
  8.  * @flow
    
  9.  */
    
  10. 
    
  11. import {compareVersions} from 'compare-versions';
    
  12. import {dehydrate} from '../hydration';
    
  13. import isArray from 'shared/isArray';
    
  14. 
    
  15. import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
    
  16. 
    
  17. // TODO: update this to the first React version that has a corresponding DevTools backend
    
  18. const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
    
  19. export function hasAssignedBackend(version?: string): boolean {
    
  20.   if (version == null || version === '') {
    
  21.     return false;
    
  22.   }
    
  23.   return gte(version, FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER);
    
  24. }
    
  25. 
    
  26. export function cleanForBridge(
    
  27.   data: Object | null,
    
  28.   isPathAllowed: (path: Array<string | number>) => boolean,
    
  29.   path: Array<string | number> = [],
    
  30. ): DehydratedData | null {
    
  31.   if (data !== null) {
    
  32.     const cleanedPaths: Array<Array<string | number>> = [];
    
  33.     const unserializablePaths: Array<Array<string | number>> = [];
    
  34.     const cleanedData = dehydrate(
    
  35.       data,
    
  36.       cleanedPaths,
    
  37.       unserializablePaths,
    
  38.       path,
    
  39.       isPathAllowed,
    
  40.     );
    
  41. 
    
  42.     return {
    
  43.       data: cleanedData,
    
  44.       cleaned: cleanedPaths,
    
  45.       unserializable: unserializablePaths,
    
  46.     };
    
  47.   } else {
    
  48.     return null;
    
  49.   }
    
  50. }
    
  51. 
    
  52. export function copyWithDelete(
    
  53.   obj: Object | Array<any>,
    
  54.   path: Array<string | number>,
    
  55.   index: number = 0,
    
  56. ): Object | Array<any> {
    
  57.   const key = path[index];
    
  58.   const updated = isArray(obj) ? obj.slice() : {...obj};
    
  59.   if (index + 1 === path.length) {
    
  60.     if (isArray(updated)) {
    
  61.       updated.splice(((key: any): number), 1);
    
  62.     } else {
    
  63.       delete updated[key];
    
  64.     }
    
  65.   } else {
    
  66.     // $FlowFixMe[incompatible-use] number or string is fine here
    
  67.     updated[key] = copyWithDelete(obj[key], path, index + 1);
    
  68.   }
    
  69.   return updated;
    
  70. }
    
  71. 
    
  72. // This function expects paths to be the same except for the final value.
    
  73. // e.g. ['path', 'to', 'foo'] and ['path', 'to', 'bar']
    
  74. export function copyWithRename(
    
  75.   obj: Object | Array<any>,
    
  76.   oldPath: Array<string | number>,
    
  77.   newPath: Array<string | number>,
    
  78.   index: number = 0,
    
  79. ): Object | Array<any> {
    
  80.   const oldKey = oldPath[index];
    
  81.   const updated = isArray(obj) ? obj.slice() : {...obj};
    
  82.   if (index + 1 === oldPath.length) {
    
  83.     const newKey = newPath[index];
    
  84.     // $FlowFixMe[incompatible-use] number or string is fine here
    
  85.     updated[newKey] = updated[oldKey];
    
  86.     if (isArray(updated)) {
    
  87.       updated.splice(((oldKey: any): number), 1);
    
  88.     } else {
    
  89.       delete updated[oldKey];
    
  90.     }
    
  91.   } else {
    
  92.     // $FlowFixMe[incompatible-use] number or string is fine here
    
  93.     updated[oldKey] = copyWithRename(obj[oldKey], oldPath, newPath, index + 1);
    
  94.   }
    
  95.   return updated;
    
  96. }
    
  97. 
    
  98. export function copyWithSet(
    
  99.   obj: Object | Array<any>,
    
  100.   path: Array<string | number>,
    
  101.   value: any,
    
  102.   index: number = 0,
    
  103. ): Object | Array<any> {
    
  104.   if (index >= path.length) {
    
  105.     return value;
    
  106.   }
    
  107.   const key = path[index];
    
  108.   const updated = isArray(obj) ? obj.slice() : {...obj};
    
  109.   // $FlowFixMe[incompatible-use] number or string is fine here
    
  110.   updated[key] = copyWithSet(obj[key], path, value, index + 1);
    
  111.   return updated;
    
  112. }
    
  113. 
    
  114. export function getEffectDurations(root: Object): {
    
  115.   effectDuration: any | null,
    
  116.   passiveEffectDuration: any | null,
    
  117. } {
    
  118.   // Profiling durations are only available for certain builds.
    
  119.   // If available, they'll be stored on the HostRoot.
    
  120.   let effectDuration = null;
    
  121.   let passiveEffectDuration = null;
    
  122.   const hostRoot = root.current;
    
  123.   if (hostRoot != null) {
    
  124.     const stateNode = hostRoot.stateNode;
    
  125.     if (stateNode != null) {
    
  126.       effectDuration =
    
  127.         stateNode.effectDuration != null ? stateNode.effectDuration : null;
    
  128.       passiveEffectDuration =
    
  129.         stateNode.passiveEffectDuration != null
    
  130.           ? stateNode.passiveEffectDuration
    
  131.           : null;
    
  132.     }
    
  133.   }
    
  134.   return {effectDuration, passiveEffectDuration};
    
  135. }
    
  136. 
    
  137. export function serializeToString(data: any): string {
    
  138.   if (data === undefined) {
    
  139.     return 'undefined';
    
  140.   }
    
  141. 
    
  142.   const cache = new Set<mixed>();
    
  143.   // Use a custom replacer function to protect against circular references.
    
  144.   return JSON.stringify(
    
  145.     data,
    
  146.     (key, value) => {
    
  147.       if (typeof value === 'object' && value !== null) {
    
  148.         if (cache.has(value)) {
    
  149.           return;
    
  150.         }
    
  151.         cache.add(value);
    
  152.       }
    
  153.       if (typeof value === 'bigint') {
    
  154.         return value.toString() + 'n';
    
  155.       }
    
  156.       return value;
    
  157.     },
    
  158.     2,
    
  159.   );
    
  160. }
    
  161. 
    
  162. // Formats an array of args with a style for console methods, using
    
  163. // the following algorithm:
    
  164. //     1. The first param is a string that contains %c
    
  165. //          - Bail out and return the args without modifying the styles.
    
  166. //            We don't want to affect styles that the developer deliberately set.
    
  167. //     2. The first param is a string that doesn't contain %c but contains
    
  168. //        string formatting
    
  169. //          - [`%c${args[0]}`, style, ...args.slice(1)]
    
  170. //          - Note: we assume that the string formatting that the developer uses
    
  171. //            is correct.
    
  172. //     3. The first param is a string that doesn't contain string formatting
    
  173. //        OR is not a string
    
  174. //          - Create a formatting string where:
    
  175. //                 boolean, string, symbol -> %s
    
  176. //                 number -> %f OR %i depending on if it's an int or float
    
  177. //                 default -> %o
    
  178. export function formatWithStyles(
    
  179.   inputArgs: $ReadOnlyArray<any>,
    
  180.   style?: string,
    
  181. ): $ReadOnlyArray<any> {
    
  182.   if (
    
  183.     inputArgs === undefined ||
    
  184.     inputArgs === null ||
    
  185.     inputArgs.length === 0 ||
    
  186.     // Matches any of %c but not %%c
    
  187.     (typeof inputArgs[0] === 'string' && inputArgs[0].match(/([^%]|^)(%c)/g)) ||
    
  188.     style === undefined
    
  189.   ) {
    
  190.     return inputArgs;
    
  191.   }
    
  192. 
    
  193.   // Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f)
    
  194.   const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g;
    
  195.   if (typeof inputArgs[0] === 'string' && inputArgs[0].match(REGEXP)) {
    
  196.     return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)];
    
  197.   } else {
    
  198.     const firstArg = inputArgs.reduce((formatStr, elem, i) => {
    
  199.       if (i > 0) {
    
  200.         formatStr += ' ';
    
  201.       }
    
  202.       switch (typeof elem) {
    
  203.         case 'string':
    
  204.         case 'boolean':
    
  205.         case 'symbol':
    
  206.           return (formatStr += '%s');
    
  207.         case 'number':
    
  208.           const formatting = Number.isInteger(elem) ? '%i' : '%f';
    
  209.           return (formatStr += formatting);
    
  210.         default:
    
  211.           return (formatStr += '%o');
    
  212.       }
    
  213.     }, '%c');
    
  214.     return [firstArg, style, ...inputArgs];
    
  215.   }
    
  216. }
    
  217. 
    
  218. // based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
    
  219. // based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
    
  220. // Implements s, d, i and f placeholders
    
  221. // NOTE: KEEP IN SYNC with src/hook.js
    
  222. export function format(
    
  223.   maybeMessage: any,
    
  224.   ...inputArgs: $ReadOnlyArray<any>
    
  225. ): string {
    
  226.   const args = inputArgs.slice();
    
  227. 
    
  228.   let formatted: string = String(maybeMessage);
    
  229. 
    
  230.   // If the first argument is a string, check for substitutions.
    
  231.   if (typeof maybeMessage === 'string') {
    
  232.     if (args.length) {
    
  233.       const REGEXP = /(%?)(%([jds]))/g;
    
  234. 
    
  235.       formatted = formatted.replace(REGEXP, (match, escaped, ptn, flag) => {
    
  236.         let arg = args.shift();
    
  237.         switch (flag) {
    
  238.           case 's':
    
  239.             arg += '';
    
  240.             break;
    
  241.           case 'd':
    
  242.           case 'i':
    
  243.             arg = parseInt(arg, 10).toString();
    
  244.             break;
    
  245.           case 'f':
    
  246.             arg = parseFloat(arg).toString();
    
  247.             break;
    
  248.         }
    
  249.         if (!escaped) {
    
  250.           return arg;
    
  251.         }
    
  252.         args.unshift(arg);
    
  253.         return match;
    
  254.       });
    
  255.     }
    
  256.   }
    
  257. 
    
  258.   // Arguments that remain after formatting.
    
  259.   if (args.length) {
    
  260.     for (let i = 0; i < args.length; i++) {
    
  261.       formatted += ' ' + String(args[i]);
    
  262.     }
    
  263.   }
    
  264. 
    
  265.   // Update escaped %% values.
    
  266.   formatted = formatted.replace(/%{2,2}/g, '%');
    
  267. 
    
  268.   return String(formatted);
    
  269. }
    
  270. 
    
  271. export function isSynchronousXHRSupported(): boolean {
    
  272.   return !!(
    
  273.     window.document &&
    
  274.     window.document.featurePolicy &&
    
  275.     window.document.featurePolicy.allowsFeature('sync-xhr')
    
  276.   );
    
  277. }
    
  278. 
    
  279. export function gt(a: string = '', b: string = ''): boolean {
    
  280.   return compareVersions(a, b) === 1;
    
  281. }
    
  282. 
    
  283. export function gte(a: string = '', b: string = ''): boolean {
    
  284.   return compareVersions(a, b) > -1;
    
  285. }