/**
/**
* 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 backend
const 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 -> %o
export 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.js
export 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;
}