Skip to content

Commit cfeeb4b

Browse files
lunaruanrickhanlonii
authored andcommitted
don't stringify objects for console log second render (#24373)
Fixes #24302 based on #24306. --- The current implementation for strict mode double logging stringiness and dims the second log. However, because we stringify everything, including objects, this causes objects to be logged as `[object Object]` etc. This PR creates a new function that formats console log arguments with a specified style. It does this by: 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 --- Co-authored-by: Billy Janitsch <[email protected]>
1 parent cfd116c commit cfeeb4b

File tree

5 files changed

+223
-71
lines changed

5 files changed

+223
-71
lines changed

packages/react-devtools-shared/src/__tests__/console-test.js

+47-20
Original file line numberDiff line numberDiff line change
@@ -494,20 +494,31 @@ describe('console', () => {
494494
);
495495
expect(mockLog.mock.calls[0]).toHaveLength(1);
496496
expect(mockLog.mock.calls[0][0]).toBe('log');
497-
expect(mockLog.mock.calls[1]).toHaveLength(2);
498-
expect(mockLog.mock.calls[1][0]).toBe('%clog');
497+
expect(mockLog.mock.calls[1]).toEqual([
498+
'%c%s',
499+
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
500+
'log',
501+
]);
499502

500503
expect(mockWarn).toHaveBeenCalledTimes(2);
501504
expect(mockWarn.mock.calls[0]).toHaveLength(1);
502505
expect(mockWarn.mock.calls[0][0]).toBe('warn');
503-
expect(mockWarn.mock.calls[1]).toHaveLength(2);
504-
expect(mockWarn.mock.calls[1][0]).toBe('%cwarn');
506+
expect(mockWarn.mock.calls[1]).toHaveLength(3);
507+
expect(mockWarn.mock.calls[1]).toEqual([
508+
'%c%s',
509+
`color: ${process.env.DARK_MODE_DIMMED_WARNING_COLOR}`,
510+
'warn',
511+
]);
505512

506513
expect(mockError).toHaveBeenCalledTimes(2);
507514
expect(mockError.mock.calls[0]).toHaveLength(1);
508515
expect(mockError.mock.calls[0][0]).toBe('error');
509-
expect(mockError.mock.calls[1]).toHaveLength(2);
510-
expect(mockError.mock.calls[1][0]).toBe('%cerror');
516+
expect(mockError.mock.calls[1]).toHaveLength(3);
517+
expect(mockError.mock.calls[1]).toEqual([
518+
'%c%s',
519+
`color: ${process.env.DARK_MODE_DIMMED_ERROR_COLOR}`,
520+
'error',
521+
]);
511522
});
512523

513524
it('should not double log if hideConsoleLogsInStrictMode is enabled in Strict mode', () => {
@@ -577,20 +588,32 @@ describe('console', () => {
577588
expect(mockLog).toHaveBeenCalledTimes(2);
578589
expect(mockLog.mock.calls[0]).toHaveLength(1);
579590
expect(mockLog.mock.calls[0][0]).toBe('log');
580-
expect(mockLog.mock.calls[1]).toHaveLength(2);
581-
expect(mockLog.mock.calls[1][0]).toBe('%clog');
591+
expect(mockLog.mock.calls[1]).toHaveLength(3);
592+
expect(mockLog.mock.calls[1]).toEqual([
593+
'%c%s',
594+
`color: ${process.env.DARK_MODE_DIMMED_LOG_COLOR}`,
595+
'log',
596+
]);
582597

583598
expect(mockWarn).toHaveBeenCalledTimes(2);
584599
expect(mockWarn.mock.calls[0]).toHaveLength(1);
585600
expect(mockWarn.mock.calls[0][0]).toBe('warn');
586-
expect(mockWarn.mock.calls[1]).toHaveLength(2);
587-
expect(mockWarn.mock.calls[1][0]).toBe('%cwarn');
601+
expect(mockWarn.mock.calls[1]).toHaveLength(3);
602+
expect(mockWarn.mock.calls[1]).toEqual([
603+
'%c%s',
604+
`color: ${process.env.DARK_MODE_DIMMED_WARNING_COLOR}`,
605+
'warn',
606+
]);
588607

589608
expect(mockError).toHaveBeenCalledTimes(2);
590609
expect(mockError.mock.calls[0]).toHaveLength(1);
591610
expect(mockError.mock.calls[0][0]).toBe('error');
592-
expect(mockError.mock.calls[1]).toHaveLength(2);
593-
expect(mockError.mock.calls[1][0]).toBe('%cerror');
611+
expect(mockError.mock.calls[1]).toHaveLength(3);
612+
expect(mockError.mock.calls[1]).toEqual([
613+
'%c%s',
614+
`color: ${process.env.DARK_MODE_DIMMED_ERROR_COLOR}`,
615+
'error',
616+
]);
594617
});
595618

596619
it('should not double log in Strict mode initial render for extension', () => {
@@ -666,22 +689,26 @@ describe('console', () => {
666689
expect(normalizeCodeLocInfo(mockWarn.mock.calls[0][1])).toEqual(
667690
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
668691
);
669-
expect(mockWarn.mock.calls[1]).toHaveLength(2);
670-
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][0])).toEqual(
671-
'%cwarn \n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
672-
);
692+
expect(mockWarn.mock.calls[1]).toHaveLength(4);
693+
expect(mockWarn.mock.calls[1][0]).toEqual('%c%s %s');
673694
expect(mockWarn.mock.calls[1][1]).toMatch('color: rgba(');
695+
expect(mockWarn.mock.calls[1][2]).toEqual('warn');
696+
expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][3]).trim()).toEqual(
697+
'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
698+
);
674699

675700
expect(mockError).toHaveBeenCalledTimes(2);
676701
expect(mockError.mock.calls[0]).toHaveLength(2);
677702
expect(normalizeCodeLocInfo(mockError.mock.calls[0][1])).toEqual(
678703
'\n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
679704
);
680-
expect(mockError.mock.calls[1]).toHaveLength(2);
681-
expect(normalizeCodeLocInfo(mockError.mock.calls[1][0])).toEqual(
682-
'%cerror \n in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
683-
);
705+
expect(mockError.mock.calls[1]).toHaveLength(4);
706+
expect(mockError.mock.calls[1][0]).toEqual('%c%s %s');
684707
expect(mockError.mock.calls[1][1]).toMatch('color: rgba(');
708+
expect(mockError.mock.calls[1][2]).toEqual('error');
709+
expect(normalizeCodeLocInfo(mockError.mock.calls[1][3]).trim()).toEqual(
710+
'in Child (at **)\n in Intermediate (at **)\n in Parent (at **)',
711+
);
685712
});
686713
});
687714

packages/react-devtools-shared/src/__tests__/utils-test.js

+81-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import {
1111
getDisplayName,
1212
getDisplayNameForReactElement,
1313
} from 'react-devtools-shared/src/utils';
14-
import {format} from 'react-devtools-shared/src/backend/utils';
14+
import {
15+
format,
16+
formatWithStyles,
17+
} from 'react-devtools-shared/src/backend/utils';
1518
import {
1619
REACT_SUSPENSE_LIST_TYPE as SuspenseList,
1720
REACT_STRICT_MODE_TYPE as StrictMode,
@@ -110,4 +113,81 @@ describe('utils', () => {
110113
expect(format(Symbol('abc'), 123)).toEqual('Symbol(abc) 123');
111114
});
112115
});
116+
117+
describe('formatWithStyles', () => {
118+
it('should format empty arrays', () => {
119+
expect(formatWithStyles([])).toEqual([]);
120+
expect(formatWithStyles([], 'gray')).toEqual([]);
121+
expect(formatWithStyles(undefined)).toEqual(undefined);
122+
});
123+
124+
it('should bail out of strings with styles', () => {
125+
expect(
126+
formatWithStyles(['%ca', 'color: green', 'b', 'c'], 'color: gray'),
127+
).toEqual(['%ca', 'color: green', 'b', 'c']);
128+
});
129+
130+
it('should format simple strings', () => {
131+
expect(formatWithStyles(['a'])).toEqual(['a']);
132+
133+
expect(formatWithStyles(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']);
134+
expect(formatWithStyles(['a'], 'color: gray')).toEqual([
135+
'%c%s',
136+
'color: gray',
137+
'a',
138+
]);
139+
expect(formatWithStyles(['a', 'b', 'c'], 'color: gray')).toEqual([
140+
'%c%s %s %s',
141+
'color: gray',
142+
'a',
143+
'b',
144+
'c',
145+
]);
146+
});
147+
148+
it('should format string substituions', () => {
149+
expect(
150+
formatWithStyles(['%s %s %s', 'a', 'b', 'c'], 'color: gray'),
151+
).toEqual(['%c%s %s %s', 'color: gray', 'a', 'b', 'c']);
152+
153+
// The last letter isn't gray here but I think it's not a big
154+
// deal, since there is a string substituion but it's incorrect
155+
expect(
156+
formatWithStyles(['%s %s', 'a', 'b', 'c'], 'color: gray'),
157+
).toEqual(['%c%s %s', 'color: gray', 'a', 'b', 'c']);
158+
});
159+
160+
it('should support multiple argument types', () => {
161+
const symbol = Symbol('a');
162+
expect(
163+
formatWithStyles(
164+
['abc', 123, 12.3, true, {hello: 'world'}, symbol],
165+
'color: gray',
166+
),
167+
).toEqual([
168+
'%c%s %i %f %s %o %s',
169+
'color: gray',
170+
'abc',
171+
123,
172+
12.3,
173+
true,
174+
{hello: 'world'},
175+
symbol,
176+
]);
177+
});
178+
179+
it('should properly format escaped string substituions', () => {
180+
expect(formatWithStyles(['%%s'], 'color: gray')).toEqual([
181+
'%c%s',
182+
'color: gray',
183+
'%%s',
184+
]);
185+
expect(formatWithStyles(['%%c'], 'color: gray')).toEqual([
186+
'%c%s',
187+
'color: gray',
188+
'%%c',
189+
]);
190+
expect(formatWithStyles(['%%c%c'], 'color: gray')).toEqual(['%%c%c']);
191+
});
192+
});
113193
});

packages/react-devtools-shared/src/backend/console.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
1111
import type {CurrentDispatcherRef, ReactRenderer, WorkTagMap} from './types';
1212
import type {BrowserTheme} from 'react-devtools-shared/src/devtools/views/DevTools';
13-
import {format} from './utils';
13+
import {format, formatWithStyles} from './utils';
1414

1515
import {getInternalReactConstants} from './renderer';
1616
import {getStackByFiberInDevAndProd} from './DevToolsFiberComponentStack';
@@ -38,7 +38,7 @@ const STYLE_DIRECTIVE_REGEX = /^%c/;
3838
// so the console color stays consistent
3939
function isStrictModeOverride(args: Array<string>, method: string): boolean {
4040
return (
41-
args.length === 2 &&
41+
args.length >= 2 &&
4242
STYLE_DIRECTIVE_REGEX.test(args[0]) &&
4343
args[1] === `color: ${getConsoleColor(method) || ''}`
4444
);
@@ -246,7 +246,8 @@ export function patch({
246246
);
247247
if (componentStack !== '') {
248248
if (isStrictModeOverride(args, method)) {
249-
args[0] = format(args[0], componentStack);
249+
args[0] = `${args[0]} %s`;
250+
args.push(componentStack);
250251
} else {
251252
args.push(componentStack);
252253
}
@@ -335,7 +336,7 @@ export function patchForStrictMode() {
335336
} else {
336337
const color = getConsoleColor(method);
337338
if (color) {
338-
originalMethod(`%c${format(...args)}`, `color: ${color}`);
339+
originalMethod(...formatWithStyles(args, `color: ${color}`));
339340
} else {
340341
throw Error('Console color is not defined');
341342
}

packages/react-devtools-shared/src/backend/utils.js

+56
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,62 @@ export function serializeToString(data: any): string {
156156
});
157157
}
158158

159+
// Formats an array of args with a style for console methods, using
160+
// the following algorithm:
161+
// 1. The first param is a string that contains %c
162+
// - Bail out and return the args without modifying the styles.
163+
// We don't want to affect styles that the developer deliberately set.
164+
// 2. The first param is a string that doesn't contain %c but contains
165+
// string formatting
166+
// - [`%c${args[0]}`, style, ...args.slice(1)]
167+
// - Note: we assume that the string formatting that the developer uses
168+
// is correct.
169+
// 3. The first param is a string that doesn't contain string formatting
170+
// OR is not a string
171+
// - Create a formatting string where:
172+
// boolean, string, symbol -> %s
173+
// number -> %f OR %i depending on if it's an int or float
174+
// default -> %o
175+
export function formatWithStyles(
176+
inputArgs: $ReadOnlyArray<any>,
177+
style?: string,
178+
): $ReadOnlyArray<any> {
179+
if (
180+
inputArgs === undefined ||
181+
inputArgs === null ||
182+
inputArgs.length === 0 ||
183+
// Matches any of %c but not %%c
184+
(typeof inputArgs[0] === 'string' && inputArgs[0].match(/([^%]|^)(%c)/g)) ||
185+
style === undefined
186+
) {
187+
return inputArgs;
188+
}
189+
190+
// Matches any of %(o|O|i|s|f), but not %%(o|O|i|s|f)
191+
const REGEXP = /([^%]|^)(%([oOdisf]))/g;
192+
if (inputArgs[0].match(REGEXP)) {
193+
return [`%c${inputArgs[0]}`, style, ...inputArgs.slice(1)];
194+
} else {
195+
const firstArg = inputArgs.reduce((formatStr, elem, i) => {
196+
if (i > 0) {
197+
formatStr += ' ';
198+
}
199+
switch (typeof elem) {
200+
case 'string':
201+
case 'boolean':
202+
case 'symbol':
203+
return (formatStr += '%s');
204+
case 'number':
205+
const formatting = Number.isInteger(elem) ? '%i' : '%f';
206+
return (formatStr += formatting);
207+
default:
208+
return (formatStr += '%o');
209+
}
210+
}, '%c');
211+
return [firstArg, style, ...inputArgs];
212+
}
213+
}
214+
159215
// based on https://github.com/tmpfs/format-util/blob/0e62d430efb0a1c51448709abd3e2406c14d8401/format.js#L1
160216
// based on https://developer.mozilla.org/en-US/docs/Web/API/console#Using_string_substitutions
161217
// Implements s, d, i and f placeholders

0 commit comments

Comments
 (0)