7
* @flow
8
*/
9
10
+import type {ReactComponentInfo} from 'shared/ReactTypes';
11
+
12
import type {DevToolsHook, ReactRenderer, RendererInterface} from '../types';
13
14
+import {getOwnerStackByComponentInfoInDev} from './DevToolsComponentInfoStack';
15
+
16
+import {formatOwnerStack} from '../shared/DevToolsOwnerStack';
17
+
18
+import {componentInfoToComponentLogsMap} from '../shared/DevToolsServerComponentLogs';
19
+
20
+import {formatConsoleArgumentsToSingleString} from 'react-devtools-shared/src/backend/utils';
21
+
22
import {
23
patchConsoleUsingWindowValues,
24
registerRenderer as registerRendererWithConsole,
25
} from '../console';
26
27
+function supportsConsoleTasks(componentInfo: ReactComponentInfo): boolean {
28
+ // If this ReactComponentInfo supports native console.createTask then we are already running
29
+ // inside a native async stack trace if it's active - meaning the DevTools is open.
30
+ // Ideally we'd detect if this task was created while the DevTools was open or not.
31
+ return !!componentInfo.debugTask;
32
+}
33
+
34
export function attach(
35
hook: DevToolsHook,
36
rendererID: number,
37
renderer: ReactRenderer,
38
global: Object,
39
): RendererInterface {
40
+ const {getCurrentComponentInfo} = renderer;
41
+
42
+ function getComponentStack(
43
+ topFrame: Error,
44
+ ): null | {enableOwnerStacks: boolean, componentStack: string} {
45
+ if (getCurrentComponentInfo === undefined) {
46
+ // Expected this to be part of the renderer. Ignore.
47
+ return null;
48
+ }
49
+ const current = getCurrentComponentInfo();
50
+ if (current === null) {
51
+ // Outside of our render scope.
52
+ return null;
53
+ }
54
+
55
+ if (supportsConsoleTasks(current)) {
56
+ // This will be handled natively by console.createTask. No need for
57
+ // DevTools to add it.
58
+ return null;
59
+ }
60
+
61
+ const enableOwnerStacks = current.debugStack != null;
62
+ let componentStack = '';
63
+ if (enableOwnerStacks) {
64
+ // Prefix the owner stack with the current stack. I.e. what called
65
+ // console.error. While this will also be part of the native stack,
66
+ // it is hidden and not presented alongside this argument so we print
67
+ // them all together.
68
+ const topStackFrames = formatOwnerStack(topFrame);
69
+ if (topStackFrames) {
70
+ componentStack += '\n' + topStackFrames;
71
+ }
72
+ componentStack += getOwnerStackByComponentInfoInDev(current);
73
+ }
74
+ return {enableOwnerStacks, componentStack};
75
+ }
76
+
77
+ // Called when an error or warning is logged during render, commit, or passive (including unmount functions).
78
+ function onErrorOrWarning(
79
+ type: 'error' | 'warn',
80
+ args: $ReadOnlyArray<any>,
81
+ ): void {
82
+ if (getCurrentComponentInfo === undefined) {
83
+ // Expected this to be part of the renderer. Ignore.
84
+ return;
85
+ }
86
+ const componentInfo = getCurrentComponentInfo();
87
+ if (componentInfo === null) {
88
+ // Outside of our render scope.
89
+ return;
90
+ }
91
+
92
+ if (
93
+ args.length > 3 &&
94
+ typeof args[0] === 'string' &&
95
+ args[0].startsWith('%c%s%c ') &&
96
+ typeof args[1] === 'string' &&
97
+ typeof args[2] === 'string' &&
98
+ typeof args[3] === 'string'
99
+ ) {
100
+ // This looks like the badge we prefixed to the log. Our UI doesn't support formatted logs.
101
+ // We remove the formatting. If the environment of the log is the same as the environment of
102
+ // the component (the common case) we remove the badge completely otherwise leave it plain
103
+ const format = args[0].slice(7);
104
+ const env = args[2].trim();
105
+ args = args.slice(4);
106
+ if (env !== componentInfo.env) {
107
+ args.unshift('[' + env + '] ' + format);
108
+ } else {
109
+ args.unshift(format);
110
+ }
111
+ }
112
+
113
+ // We can't really use this message as a unique key, since we can't distinguish
114
+ // different objects in this implementation. We have to delegate displaying of the objects
115
+ // to the environment, the browser console, for example, so this is why this should be kept
116
+ // as an array of arguments, instead of the plain string.
117
+ // [Warning: %o, {...}] and [Warning: %o, {...}] will be considered as the same message,
118
+ // even if objects are different
119
+ const message = formatConsoleArgumentsToSingleString(...args);
120
+
121
+ // Track the warning/error for later.
122
+ let componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo);
123
+ if (componentLogsEntry === undefined) {
124
+ componentLogsEntry = {
125
+ errors: new Map(),
126
+ errorsCount: 0,
127
+ warnings: new Map(),
128
+ warningsCount: 0,
129
+ };
130
+ componentInfoToComponentLogsMap.set(componentInfo, componentLogsEntry);
131
+ }
132
+
133
+ const messageMap =
134
+ type === 'error'
135
+ ? componentLogsEntry.errors
136
+ : componentLogsEntry.warnings;
137
+ const count = messageMap.get(message) || 0;
138
+ messageMap.set(message, count + 1);
139
+ if (type === 'error') {
140
+ componentLogsEntry.errorsCount++;
141
+ } else {
142
+ componentLogsEntry.warningsCount++;
143
+ }
144
+
145
+ // The changes will be flushed later when we commit this tree to Fiber.
146
+ }
147
+
148
patchConsoleUsingWindowValues();
24
- registerRendererWithConsole(); // TODO: Fill in the impl
149
+ registerRendererWithConsole(onErrorOrWarning, getComponentStack);
150
151
return {
152
cleanup() {},