diff --git a/packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js b/packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js new file mode 100644 index 0000000000000..a2d303e543ac0 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js @@ -0,0 +1,72 @@ +/** + * 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 + */ + +// Do not add / import anything to this file. +// This function could be used from multiple places, including hook. + +// Skips CSS and object arguments, inlines other in the first argument as a template string +export default function formatConsoleArguments( + maybeMessage: any, + ...inputArgs: $ReadOnlyArray +): $ReadOnlyArray { + if (inputArgs.length === 0 || typeof maybeMessage !== 'string') { + return [maybeMessage, ...inputArgs]; + } + + const args = inputArgs.slice(); + + let template = ''; + let argumentsPointer = 0; + for (let i = 0; i < maybeMessage.length; ++i) { + const currentChar = maybeMessage[i]; + if (currentChar !== '%') { + template += currentChar; + continue; + } + + const nextChar = maybeMessage[i + 1]; + ++i; + + // Only keep CSS and objects, inline other arguments + switch (nextChar) { + case 'c': + case 'O': + case 'o': { + ++argumentsPointer; + template += `%${nextChar}`; + + break; + } + case 'd': + case 'i': { + const [arg] = args.splice(argumentsPointer, 1); + template += parseInt(arg, 10).toString(); + + break; + } + case 'f': { + const [arg] = args.splice(argumentsPointer, 1); + template += parseFloat(arg).toString(); + + break; + } + case 's': { + const [arg] = args.splice(argumentsPointer, 1); + template += arg.toString(); + + break; + } + + default: + template += `%${nextChar}`; + } + } + + return [template, ...args]; +} diff --git a/packages/react-devtools-shared/src/backend/utils/formatWithStyles.js b/packages/react-devtools-shared/src/backend/utils/formatWithStyles.js new file mode 100644 index 0000000000000..b258141e353f0 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/utils/formatWithStyles.js @@ -0,0 +1,67 @@ +/** + * 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 + */ + +// Do not add / import anything to this file. +// This function could be used from multiple places, including hook. + +// 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 default function formatWithStyles( + inputArgs: $ReadOnlyArray, + style?: string, +): $ReadOnlyArray { + 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]; + } +} diff --git a/packages/react-devtools-shared/src/backend/utils.js b/packages/react-devtools-shared/src/backend/utils/index.js similarity index 72% rename from packages/react-devtools-shared/src/backend/utils.js rename to packages/react-devtools-shared/src/backend/utils/index.js index e763bdd759759..1e7934af9835b 100644 --- a/packages/react-devtools-shared/src/backend/utils.js +++ b/packages/react-devtools-shared/src/backend/utils/index.js @@ -9,12 +9,15 @@ */ import {compareVersions} from 'compare-versions'; -import {dehydrate} from '../hydration'; +import {dehydrate} from 'react-devtools-shared/src/hydration'; import isArray from 'shared/isArray'; import type {Source} from 'react-devtools-shared/src/shared/types'; import type {DehydratedData} from 'react-devtools-shared/src/frontend/types'; +export {default as formatWithStyles} from './formatWithStyles'; +export {default as formatConsoleArguments} from './formatConsoleArguments'; + // 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 { @@ -164,125 +167,6 @@ export function serializeToString(data: any): string { ); } -// NOTE: KEEP IN SYNC with src/hook.js -// 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, - style?: string, -): $ReadOnlyArray { - 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]; - } -} - -// NOTE: KEEP IN SYNC with src/hook.js -// Skips CSS and object arguments, inlines other in the first argument as a template string -export function formatConsoleArguments( - maybeMessage: any, - ...inputArgs: $ReadOnlyArray -): $ReadOnlyArray { - if (inputArgs.length === 0 || typeof maybeMessage !== 'string') { - return [maybeMessage, ...inputArgs]; - } - - const args = inputArgs.slice(); - - let template = ''; - let argumentsPointer = 0; - for (let i = 0; i < maybeMessage.length; ++i) { - const currentChar = maybeMessage[i]; - if (currentChar !== '%') { - template += currentChar; - continue; - } - - const nextChar = maybeMessage[i + 1]; - ++i; - - // Only keep CSS and objects, inline other arguments - switch (nextChar) { - case 'c': - case 'O': - case 'o': { - ++argumentsPointer; - template += `%${nextChar}`; - - break; - } - case 'd': - case 'i': { - const [arg] = args.splice(argumentsPointer, 1); - template += parseInt(arg, 10).toString(); - - break; - } - case 'f': { - const [arg] = args.splice(argumentsPointer, 1); - template += parseFloat(arg).toString(); - - break; - } - case 's': { - const [arg] = args.splice(argumentsPointer, 1); - template += arg.toString(); - - break; - } - - default: - template += `%${nextChar}`; - } - } - - return [template, ...args]; -} - // 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 diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index ac56d13d6ab4e..2f0698b8e1353 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -24,6 +24,8 @@ import { ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK, } from 'react-devtools-shared/src/constants'; import attachRenderer from './attachRenderer'; +import formatConsoleArguments from 'react-devtools-shared/src/backend/utils/formatConsoleArguments'; +import formatWithStyles from 'react-devtools-shared/src/backend/utils/formatWithStyles'; // React's custom built component stack strings match "\s{4}in" // Chrome's prefix matches "\s{4}at" @@ -190,100 +192,6 @@ export function installHook( } catch (err) {} } - // NOTE: KEEP IN SYNC with src/backend/utils.js - function formatWithStyles(inputArgs: Array, style?: string): Array { - 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]; - } - } - // NOTE: KEEP IN SYNC with src/backend/utils.js - function formatConsoleArguments( - maybeMessage: any, - ...inputArgs: $ReadOnlyArray - ): $ReadOnlyArray { - if (inputArgs.length === 0 || typeof maybeMessage !== 'string') { - return [maybeMessage, ...inputArgs]; - } - - const args = inputArgs.slice(); - - let template = ''; - let argumentsPointer = 0; - for (let i = 0; i < maybeMessage.length; ++i) { - const currentChar = maybeMessage[i]; - if (currentChar !== '%') { - template += currentChar; - continue; - } - - const nextChar = maybeMessage[i + 1]; - ++i; - - // Only keep CSS and objects, inline other arguments - switch (nextChar) { - case 'c': - case 'O': - case 'o': { - ++argumentsPointer; - template += `%${nextChar}`; - - break; - } - case 'd': - case 'i': { - const [arg] = args.splice(argumentsPointer, 1); - template += parseInt(arg, 10).toString(); - - break; - } - case 'f': { - const [arg] = args.splice(argumentsPointer, 1); - template += parseFloat(arg).toString(); - - break; - } - case 's': { - const [arg] = args.splice(argumentsPointer, 1); - template += arg.toString(); - } - } - } - - return [template, ...args]; - } - let uidCounter = 0; function inject(renderer: ReactRenderer): number { const id = ++uidCounter; @@ -658,13 +566,16 @@ export function installHook( // Dim the text color of the double logs if we're not hiding them. // Firefox doesn't support ANSI escape sequences if (__IS_FIREFOX__) { - const argsWithCSSStyles = formatWithStyles( + let argsWithCSSStyles = formatWithStyles( args, FIREFOX_CONSOLE_DIMMING_COLOR, ); if (injectedComponentStackAsFakeError) { - argsWithCSSStyles[0] = `${argsWithCSSStyles[0]} %o`; + argsWithCSSStyles = [ + `${argsWithCSSStyles[0]} %o`, + ...argsWithCSSStyles.slice(1), + ]; } originalMethod(...argsWithCSSStyles);