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);