@samitouri / QOS-React-1 / commits / fce4606657

chore[react-devtools]: extract some utils into separate modules to unify implementations (#30597)

Stacked on https://github.com/facebook/react/pull/30596. See [this commit](https://github.com/facebook/react/pull/30597/commits/4ba5e784bbfdcd69021e2d84c75ffe26fcb698f4). Moving `formatWithStyles` and `formatConsoleArguments` to its own modules, so that we can finally have a single implementation for these and stop inlining them in RDT global hook object.

Ruslan Lesiutin committed Sep 18, 2024 at 18:16 UTC fce46066571e7bf3ab6ce5bfe5fd3a615e098421
4 files changed +150 -216
packages/react-devtools-shared/src/backend/utils/formatConsoleArguments.js new
+72
@@ -0,0 +1,72 @@
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 +// Do not add / import anything to this file.
11 +// This function could be used from multiple places, including hook.
12 +
13 +// Skips CSS and object arguments, inlines other in the first argument as a template string
14 +export default function formatConsoleArguments(
15 + maybeMessage: any,
16 + ...inputArgs: $ReadOnlyArray<any>
17 +): $ReadOnlyArray<any> {
18 + if (inputArgs.length === 0 || typeof maybeMessage !== 'string') {
19 + return [maybeMessage, ...inputArgs];
20 + }
21 +
22 + const args = inputArgs.slice();
23 +
24 + let template = '';
25 + let argumentsPointer = 0;
26 + for (let i = 0; i < maybeMessage.length; ++i) {
27 + const currentChar = maybeMessage[i];
28 + if (currentChar !== '%') {
29 + template += currentChar;
30 + continue;
31 + }
32 +
33 + const nextChar = maybeMessage[i + 1];
34 + ++i;
35 +
36 + // Only keep CSS and objects, inline other arguments
37 + switch (nextChar) {
38 + case 'c':
39 + case 'O':
40 + case 'o': {
41 + ++argumentsPointer;
42 + template += `%${nextChar}`;
43 +
44 + break;
45 + }
46 + case 'd':
47 + case 'i': {
48 + const [arg] = args.splice(argumentsPointer, 1);
49 + template += parseInt(arg, 10).toString();
50 +
51 + break;
52 + }
53 + case 'f': {
54 + const [arg] = args.splice(argumentsPointer, 1);
55 + template += parseFloat(arg).toString();
56 +
57 + break;
58 + }
59 + case 's': {
60 + const [arg] = args.splice(argumentsPointer, 1);
61 + template += arg.toString();
62 +
63 + break;
64 + }
65 +
66 + default:
67 + template += `%${nextChar}`;
68 + }
69 + }
70 +
71 + return [template, ...args];
72 +}
packages/react-devtools-shared/src/backend/utils/formatWithStyles.js new
+67
@@ -0,0 +1,67 @@
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 +// Do not add / import anything to this file.
11 +// This function could be used from multiple places, including hook.
12 +
13 +// Formats an array of args with a style for console methods, using
14 +// the following algorithm:
15 +// 1. The first param is a string that contains %c
16 +// - Bail out and return the args without modifying the styles.
17 +// We don't want to affect styles that the developer deliberately set.
18 +// 2. The first param is a string that doesn't contain %c but contains
19 +// string formatting
20 +// - [`%c${args[0]}`, style, ...args.slice(1)]
21 +// - Note: we assume that the string formatting that the developer uses
22 +// is correct.
23 +// 3. The first param is a string that doesn't contain string formatting
24 +// OR is not a string
25 +// - Create a formatting string where:
26 +// boolean, string, symbol -> %s
27 +// number -> %f OR %i depending on if it's an int or float
28 +// default -> %o
29 +export default function formatWithStyles(
30 + inputArgs: $ReadOnlyArray<any>,
31 + style?: string,
32 +): $ReadOnlyArray<any> {
33 + if (
34 + inputArgs === undefined ||
35 + inputArgs === null ||
36 + inputArgs.length === 0 ||
37 + // Matches any of %c but not %%c
38 + (typeof inputArgs[0] === 'string' && inputArgs[0].match(/([^%]|^)(%c)/g)) ||
39 + style === undefined
40 + ) {
41 + return inputArgs;
42 + }
43 +
44 + // Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f)
45 + const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g;
46 + if (typeof inputArgs[0] === 'string' && inputArgs[0].match(REGEXP)) {
47 + return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)];
48 + } else {
49 + const firstArg = inputArgs.reduce((formatStr, elem, i) => {
50 + if (i > 0) {
51 + formatStr += ' ';
52 + }
53 + switch (typeof elem) {
54 + case 'string':
55 + case 'boolean':
56 + case 'symbol':
57 + return (formatStr += '%s');
58 + case 'number':
59 + const formatting = Number.isInteger(elem) ? '%i' : '%f';
60 + return (formatStr += formatting);
61 + default:
62 + return (formatStr += '%o');
63 + }
64 + }, '%c');
65 + return [firstArg, style, ...inputArgs];
66 + }
67 +}
packages/react-devtools-shared/src/backend/utils/index.js renamed
+4 -120
@@ -9,12 +9,15 @@
9 */
10
11 import {compareVersions} from 'compare-versions';
12 -import {dehydrate} from '../hydration';
12 +import {dehydrate} from 'react-devtools-shared/src/hydration';
13 import isArray from 'shared/isArray';
14
15 import type {Source} from 'react-devtools-shared/src/shared/types';
16 import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
17
18 +export {default as formatWithStyles} from './formatWithStyles';
19 +export {default as formatConsoleArguments} from './formatConsoleArguments';
20 +
21 // TODO: update this to the first React version that has a corresponding DevTools backend
22 const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
23 export function hasAssignedBackend(version?: string): boolean {
@@ -164,125 +167,6 @@ export function serializeToString(data: any): string {
167 );
168 }
169
167 -// NOTE: KEEP IN SYNC with src/hook.js
168 -// Formats an array of args with a style for console methods, using
169 -// the following algorithm:
170 -// 1. The first param is a string that contains %c
171 -// - Bail out and return the args without modifying the styles.
172 -// We don't want to affect styles that the developer deliberately set.
173 -// 2. The first param is a string that doesn't contain %c but contains
174 -// string formatting
175 -// - [`%c${args[0]}`, style, ...args.slice(1)]
176 -// - Note: we assume that the string formatting that the developer uses
177 -// is correct.
178 -// 3. The first param is a string that doesn't contain string formatting
179 -// OR is not a string
180 -// - Create a formatting string where:
181 -// boolean, string, symbol -> %s
182 -// number -> %f OR %i depending on if it's an int or float
183 -// default -> %o
184 -export function formatWithStyles(
185 - inputArgs: $ReadOnlyArray<any>,
186 - style?: string,
187 -): $ReadOnlyArray<any> {
188 - if (
189 - inputArgs === undefined ||
190 - inputArgs === null ||
191 - inputArgs.length === 0 ||
192 - // Matches any of %c but not %%c
193 - (typeof inputArgs[0] === 'string' && inputArgs[0].match(/([^%]|^)(%c)/g)) ||
194 - style === undefined
195 - ) {
196 - return inputArgs;
197 - }
198 -
199 - // Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f)
200 - const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g;
201 - if (typeof inputArgs[0] === 'string' && inputArgs[0].match(REGEXP)) {
202 - return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)];
203 - } else {
204 - const firstArg = inputArgs.reduce((formatStr, elem, i) => {
205 - if (i > 0) {
206 - formatStr += ' ';
207 - }
208 - switch (typeof elem) {
209 - case 'string':
210 - case 'boolean':
211 - case 'symbol':
212 - return (formatStr += '%s');
213 - case 'number':
214 - const formatting = Number.isInteger(elem) ? '%i' : '%f';
215 - return (formatStr += formatting);
216 - default:
217 - return (formatStr += '%o');
218 - }
219 - }, '%c');
220 - return [firstArg, style, ...inputArgs];
221 - }
222 -}
223 -
224 -// NOTE: KEEP IN SYNC with src/hook.js
225 -// Skips CSS and object arguments, inlines other in the first argument as a template string
226 -export function formatConsoleArguments(
227 - maybeMessage: any,
228 - ...inputArgs: $ReadOnlyArray<any>
229 -): $ReadOnlyArray<any> {
230 - if (inputArgs.length === 0 || typeof maybeMessage !== 'string') {
231 - return [maybeMessage, ...inputArgs];
232 - }
233 -
234 - const args = inputArgs.slice();
235 -
236 - let template = '';
237 - let argumentsPointer = 0;
238 - for (let i = 0; i < maybeMessage.length; ++i) {
239 - const currentChar = maybeMessage[i];
240 - if (currentChar !== '%') {
241 - template += currentChar;
242 - continue;
243 - }
244 -
245 - const nextChar = maybeMessage[i + 1];
246 - ++i;
247 -
248 - // Only keep CSS and objects, inline other arguments
249 - switch (nextChar) {
250 - case 'c':
251 - case 'O':
252 - case 'o': {
253 - ++argumentsPointer;
254 - template += `%${nextChar}`;
255 -
256 - break;
257 - }
258 - case 'd':
259 - case 'i': {
260 - const [arg] = args.splice(argumentsPointer, 1);
261 - template += parseInt(arg, 10).toString();
262 -
263 - break;
264 - }
265 - case 'f': {
266 - const [arg] = args.splice(argumentsPointer, 1);
267 - template += parseFloat(arg).toString();
268 -
269 - break;
270 - }
271 - case 's': {
272 - const [arg] = args.splice(argumentsPointer, 1);
273 - template += arg.toString();
274 -
275 - break;
276 - }
277 -
278 - default:
279 - template += `%${nextChar}`;
280 - }
281 - }
282 -
283 - return [template, ...args];
284 -}
285 -
170 // based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
171 // based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
172 // Implements s, d, i and f placeholders
packages/react-devtools-shared/src/hook.js
+7 -96
@@ -24,6 +24,8 @@ import {
24 ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK,
25 } from 'react-devtools-shared/src/constants';
26 import attachRenderer from './attachRenderer';
27 +import formatConsoleArguments from 'react-devtools-shared/src/backend/utils/formatConsoleArguments';
28 +import formatWithStyles from 'react-devtools-shared/src/backend/utils/formatWithStyles';
29
30 // React's custom built component stack strings match "\s{4}in"
31 // Chrome's prefix matches "\s{4}at"
@@ -190,100 +192,6 @@ export function installHook(
192 } catch (err) {}
193 }
194
193 - // NOTE: KEEP IN SYNC with src/backend/utils.js
194 - function formatWithStyles(inputArgs: Array<any>, style?: string): Array<any> {
195 - if (
196 - inputArgs === undefined ||
197 - inputArgs === null ||
198 - inputArgs.length === 0 ||
199 - // Matches any of %c but not %%c
200 - (typeof inputArgs[0] === 'string' &&
201 - inputArgs[0].match(/([^%]|^)(%c)/g)) ||
202 - style === undefined
203 - ) {
204 - return inputArgs;
205 - }
206 -
207 - // Matches any of %(o|O|d|i|s|f), but not %%(o|O|d|i|s|f)
208 - const REGEXP = /([^%]|^)((%%)*)(%([oOdisf]))/g;
209 - if (typeof inputArgs[0] === 'string' && inputArgs[0].match(REGEXP)) {
210 - return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)];
211 - } else {
212 - const firstArg = inputArgs.reduce((formatStr, elem, i) => {
213 - if (i > 0) {
214 - formatStr += ' ';
215 - }
216 - switch (typeof elem) {
217 - case 'string':
218 - case 'boolean':
219 - case 'symbol':
220 - return (formatStr += '%s');
221 - case 'number':
222 - const formatting = Number.isInteger(elem) ? '%i' : '%f';
223 - return (formatStr += formatting);
224 - default:
225 - return (formatStr += '%o');
226 - }
227 - }, '%c');
228 - return [firstArg, style, ...inputArgs];
229 - }
230 - }
231 - // NOTE: KEEP IN SYNC with src/backend/utils.js
232 - function formatConsoleArguments(
233 - maybeMessage: any,
234 - ...inputArgs: $ReadOnlyArray<any>
235 - ): $ReadOnlyArray<any> {
236 - if (inputArgs.length === 0 || typeof maybeMessage !== 'string') {
237 - return [maybeMessage, ...inputArgs];
238 - }
239 -
240 - const args = inputArgs.slice();
241 -
242 - let template = '';
243 - let argumentsPointer = 0;
244 - for (let i = 0; i < maybeMessage.length; ++i) {
245 - const currentChar = maybeMessage[i];
246 - if (currentChar !== '%') {
247 - template += currentChar;
248 - continue;
249 - }
250 -
251 - const nextChar = maybeMessage[i + 1];
252 - ++i;
253 -
254 - // Only keep CSS and objects, inline other arguments
255 - switch (nextChar) {
256 - case 'c':
257 - case 'O':
258 - case 'o': {
259 - ++argumentsPointer;
260 - template += `%${nextChar}`;
261 -
262 - break;
263 - }
264 - case 'd':
265 - case 'i': {
266 - const [arg] = args.splice(argumentsPointer, 1);
267 - template += parseInt(arg, 10).toString();
268 -
269 - break;
270 - }
271 - case 'f': {
272 - const [arg] = args.splice(argumentsPointer, 1);
273 - template += parseFloat(arg).toString();
274 -
275 - break;
276 - }
277 - case 's': {
278 - const [arg] = args.splice(argumentsPointer, 1);
279 - template += arg.toString();
280 - }
281 - }
282 - }
283 -
284 - return [template, ...args];
285 - }
286 -
195 let uidCounter = 0;
196 function inject(renderer: ReactRenderer): number {
197 const id = ++uidCounter;
@@ -658,13 +566,16 @@ export function installHook(
566 // Dim the text color of the double logs if we're not hiding them.
567 // Firefox doesn't support ANSI escape sequences
568 if (__IS_FIREFOX__) {
661 - const argsWithCSSStyles = formatWithStyles(
569 + let argsWithCSSStyles = formatWithStyles(
570 args,
571 FIREFOX_CONSOLE_DIMMING_COLOR,
572 );
573
574 if (injectedComponentStackAsFakeError) {
667 - argsWithCSSStyles[0] = `${argsWithCSSStyles[0]} %o`;
575 + argsWithCSSStyles = [
576 + `${argsWithCSSStyles[0]} %o`,
577 + ...argsWithCSSStyles.slice(1),
578 + ];
579 }
580
581 originalMethod(...argsWithCSSStyles);