/**/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @flow*/import {compareVersions} from 'compare-versions';
import {dehydrate} from '../hydration';
import isArray from 'shared/isArray';
import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
// TODO: update this to the first React version that has a corresponding DevTools backendconst FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
export function hasAssignedBackend(version?: string): boolean {
if (version == null || version === '') {
return false;
}return gte(version, FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER);
}export function cleanForBridge(
data: Object | null,
isPathAllowed: (path: Array<string | number>) => boolean,
path: Array<string | number> = [],
): DehydratedData | null {
if (data !== null) {
const cleanedPaths: Array<Array<string | number>> = [];
const unserializablePaths: Array<Array<string | number>> = [];
const cleanedData = dehydrate(
data,
cleanedPaths,
unserializablePaths,
path,isPathAllowed,
);return {
data: cleanedData,
cleaned: cleanedPaths,
unserializable: unserializablePaths,
};} else {return null;}}export function copyWithDelete(
obj: Object | Array<any>,path: Array<string | number>,index: number = 0,
): Object | Array<any> {
const key = path[index];
const updated = isArray(obj) ? obj.slice() : {...obj};
if (index + 1 === path.length) {
if (isArray(updated)) {
updated.splice(((key: any): number), 1);
} else {
delete updated[key];
}} else {// $FlowFixMe[incompatible-use] number or string is fine here
updated[key] = copyWithDelete(obj[key], path, index + 1);
}return updated;
}// This function expects paths to be the same except for the final value.// e.g. ['path', 'to', 'foo'] and ['path', 'to', 'bar']export function copyWithRename(
obj: Object | Array<any>,oldPath: Array<string | number>,newPath: Array<string | number>,index: number = 0,
): Object | Array<any> {
const oldKey = oldPath[index];
const updated = isArray(obj) ? obj.slice() : {...obj};
if (index + 1 === oldPath.length) {
const newKey = newPath[index];
// $FlowFixMe[incompatible-use] number or string is fine here
updated[newKey] = updated[oldKey];
if (isArray(updated)) {
updated.splice(((oldKey: any): number), 1);
} else {
delete updated[oldKey];
}} else {// $FlowFixMe[incompatible-use] number or string is fine here
updated[oldKey] = copyWithRename(obj[oldKey], oldPath, newPath, index + 1);
}return updated;
}export function copyWithSet(
obj: Object | Array<any>,path: Array<string | number>,value: any,index: number = 0,
): Object | Array<any> {
if (index >= path.length) {
return value;
}const key = path[index];
const updated = isArray(obj) ? obj.slice() : {...obj};
// $FlowFixMe[incompatible-use] number or string is fine here
updated[key] = copyWithSet(obj[key], path, value, index + 1);
return updated;}export function getEffectDurations(root: Object): {
effectDuration: any | null,
passiveEffectDuration: any | null,
} {// Profiling durations are only available for certain builds.
// If available, they'll be stored on the HostRoot.
let effectDuration = null;let passiveEffectDuration = null;const hostRoot = root.current;if (hostRoot != null) {
const stateNode = hostRoot.stateNode;
if (stateNode != null) {
effectDuration =
stateNode.effectDuration != null ? stateNode.effectDuration : null;
passiveEffectDuration =
stateNode.passiveEffectDuration != null
? stateNode.passiveEffectDuration
: null;
}}return {effectDuration, passiveEffectDuration};
}export function serializeToString(data: any): string {
if (data === undefined) {
return 'undefined';
}const cache = new Set<mixed>();// Use a custom replacer function to protect against circular references.
return JSON.stringify(
data,(key, value) => {if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
return;
}cache.add(value);
}if (typeof value === 'bigint') {
return value.toString() + 'n';
}return value;},2,
);
}// Formats an array of args with a style for console methods, using// the following algorithm:// 1. The first param is a string that contains %c// - Bail out and return the args without modifying the styles.// We don't want to affect styles that the developer deliberately set.// 2. The first param is a string that doesn't contain %c but contains// string formatting// - [`%c${args[0]}`, style, ...args.slice(1)]// - Note: we assume that the string formatting that the developer uses// is correct.// 3. The first param is a string that doesn't contain string formatting// OR is not a string// - Create a formatting string where:// boolean, string, symbol -> %s// number -> %f OR %i depending on if it's an int or float// default -> %oexport function formatWithStyles(
inputArgs: $ReadOnlyArray<any>,style?: string,): $ReadOnlyArray<any> {
if (
inputArgs === undefined ||
inputArgs === null ||
inputArgs.length === 0 ||
// Matches any of %c but not %%c
(typeof inputArgs[0] === 'string' && inputArgs[0].match(/([^%]|^)(%c)/g)) ||
style === undefined
) {return inputArgs;
}// Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f)
const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g;
if (typeof inputArgs[0] === 'string' && inputArgs[0].match(REGEXP)) {
return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)];
} else {const firstArg = inputArgs.reduce((formatStr, elem, i) => {
if (i > 0) {
formatStr += ' ';
}switch (typeof elem) {
case 'string':
case 'boolean':
case 'symbol':
return (formatStr += '%s');
case 'number':
const formatting = Number.isInteger(elem) ? '%i' : '%f';
return (formatStr += formatting);
default:return (formatStr += '%o');
}}, '%c');
return [firstArg, style, ...inputArgs];
}}// based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1// based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions// Implements s, d, i and f placeholders// NOTE: KEEP IN SYNC with src/hook.jsexport function format(
maybeMessage: any,...inputArgs: $ReadOnlyArray<any>): string {
const args = inputArgs.slice();
let formatted: string = String(maybeMessage);
// If the first argument is a string, check for substitutions.
if (typeof maybeMessage === 'string') {
if (args.length) {
const REGEXP = /(%?)(%([jds]))/g;
formatted = formatted.replace(REGEXP, (match, escaped, ptn, flag) => {
let arg = args.shift();
switch (flag) {
case 's':
arg += '';
break;
case 'd':
case 'i':
arg = parseInt(arg, 10).toString();
break;
case 'f':
arg = parseFloat(arg).toString();
break;
}if (!escaped) {
return arg;
}args.unshift(arg);
return match;
});}}// Arguments that remain after formatting.
if (args.length) {
for (let i = 0; i < args.length; i++) {
formatted += ' ' + String(args[i]);
}}// Update escaped %% values.
formatted = formatted.replace(/%{2,2}/g, '%');
return String(formatted);
}export function isSynchronousXHRSupported(): boolean {
return !!(window.document &&window.document.featurePolicy &&window.document.featurePolicy.allowsFeature('sync-xhr')
);}export function gt(a: string = '', b: string = ''): boolean {
return compareVersions(a, b) === 1;
}export function gte(a: string = '', b: string = ''): boolean {
return compareVersions(a, b) > -1;
}