main
js 228 lines 6.83 KB
Raw
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 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 function supportsConsoleTasks(componentInfo: ReactComponentInfo): boolean {
23 // If this ReactComponentInfo supports native console.createTask then we are already running
24 // inside a native async stack trace if it's active - meaning the DevTools is open.
25 // Ideally we'd detect if this task was created while the DevTools was open or not.
26 return !!componentInfo.debugTask;
27 }
28
29 export function attach(
30 hook: DevToolsHook,
31 rendererID: number,
32 renderer: ReactRenderer,
33 global: Object,
34 ): RendererInterface {
35 const {getCurrentComponentInfo} = renderer;
36
37 function getComponentStack(
38 topFrame: Error,
39 ): null | {enableOwnerStacks: boolean, componentStack: string} {
40 if (getCurrentComponentInfo === undefined) {
41 // Expected this to be part of the renderer. Ignore.
42 return null;
43 }
44 const current = getCurrentComponentInfo();
45 if (current === null) {
46 // Outside of our render scope.
47 return null;
48 }
49
50 if (supportsConsoleTasks(current)) {
51 // This will be handled natively by console.createTask. No need for
52 // DevTools to add it.
53 return null;
54 }
55
56 const enableOwnerStacks = current.debugStack != null;
57 let componentStack = '';
58 if (enableOwnerStacks) {
59 // Prefix the owner stack with the current stack. I.e. what called
60 // console.error. While this will also be part of the native stack,
61 // it is hidden and not presented alongside this argument so we print
62 // them all together.
63 const topStackFrames = formatOwnerStack(topFrame);
64 if (topStackFrames) {
65 componentStack += '\n' + topStackFrames;
66 }
67 componentStack += getOwnerStackByComponentInfoInDev(current);
68 }
69 return {enableOwnerStacks, componentStack};
70 }
71
72 // Called when an error or warning is logged during render, commit, or passive (including unmount functions).
73 function onErrorOrWarning(
74 type: 'error' | 'warn',
75 args: $ReadOnlyArray<any>,
76 ): void {
77 if (getCurrentComponentInfo === undefined) {
78 // Expected this to be part of the renderer. Ignore.
79 return;
80 }
81 const componentInfo = getCurrentComponentInfo();
82 if (componentInfo === null) {
83 // Outside of our render scope.
84 return;
85 }
86
87 if (
88 args.length > 3 &&
89 typeof args[0] === 'string' &&
90 args[0].startsWith('%c%s%c ') &&
91 typeof args[1] === 'string' &&
92 typeof args[2] === 'string' &&
93 typeof args[3] === 'string'
94 ) {
95 // This looks like the badge we prefixed to the log. Our UI doesn't support formatted logs.
96 // We remove the formatting. If the environment of the log is the same as the environment of
97 // the component (the common case) we remove the badge completely otherwise leave it plain
98 const format = args[0].slice(7);
99 const env = args[2].trim();
100 args = args.slice(4);
101 if (env !== componentInfo.env) {
102 args.unshift('[' + env + '] ' + format);
103 } else {
104 args.unshift(format);
105 }
106 }
107
108 // We can't really use this message as a unique key, since we can't distinguish
109 // different objects in this implementation. We have to delegate displaying of the objects
110 // to the environment, the browser console, for example, so this is why this should be kept
111 // as an array of arguments, instead of the plain string.
112 // [Warning: %o, {...}] and [Warning: %o, {...}] will be considered as the same message,
113 // even if objects are different
114 const message = formatConsoleArgumentsToSingleString(...args);
115
116 // Track the warning/error for later.
117 let componentLogsEntry = componentInfoToComponentLogsMap.get(componentInfo);
118 if (componentLogsEntry === undefined) {
119 componentLogsEntry = {
120 errors: new Map(),
121 errorsCount: 0 as number,
122 warnings: new Map(),
123 warningsCount: 0 as number,
124 };
125 componentInfoToComponentLogsMap.set(componentInfo, componentLogsEntry);
126 }
127
128 const messageMap =
129 type === 'error'
130 ? componentLogsEntry.errors
131 : componentLogsEntry.warnings;
132 const count = messageMap.get(message) || 0;
133 messageMap.set(message, count + 1);
134 if (type === 'error') {
135 componentLogsEntry.errorsCount++;
136 } else {
137 componentLogsEntry.warningsCount++;
138 }
139
140 // The changes will be flushed later when we commit this tree to Fiber.
141 }
142
143 const supportsTogglingSuspense = false;
144
145 return {
146 cleanup() {},
147 clearErrorsAndWarnings() {},
148 clearErrorsForElementID() {},
149 clearWarningsForElementID() {},
150 getSerializedElementValueByPath() {},
151 deletePath() {},
152 findHostInstancesForElementID() {
153 return null;
154 },
155 findLastKnownRectsForID() {
156 return null;
157 },
158 flushInitialOperations() {},
159 getBestMatchForTrackedPath() {
160 return null;
161 },
162 getComponentStack,
163 getDisplayNameForElementID() {
164 return null;
165 },
166 getNearestMountedDOMNode() {
167 return null;
168 },
169 getElementIDForHostInstance() {
170 return null;
171 },
172 getSuspenseNodeIDForHostInstance() {
173 return null;
174 },
175 getInstanceAndStyle() {
176 return {
177 instance: null,
178 style: null,
179 };
180 },
181 getOwnersList() {
182 return null;
183 },
184 getPathForElement() {
185 return null;
186 },
187 getProfilingData() {
188 throw new Error('getProfilingData not supported by this renderer');
189 },
190 handleCommitFiberRoot() {},
191 handleCommitFiberUnmount() {},
192 handlePostCommitFiberRoot() {},
193 hasElementWithId() {
194 return false;
195 },
196 inspectElement(
197 requestID: number,
198 id: number,
199 path: Array<string | number> | null,
200 ) {
201 return {
202 id,
203 responseID: requestID,
204 type: 'not-found',
205 };
206 },
207 logElementToConsole() {},
208 getElementAttributeByPath() {},
209 getElementSourceFunctionById() {},
210 onErrorOrWarning,
211 overrideError() {},
212 overrideSuspense() {},
213 overrideSuspenseMilestone() {},
214 overrideValueAtPath() {},
215 renamePath() {},
216 renderer,
217 setTraceUpdatesEnabled() {},
218 setTrackedPath() {},
219 startProfiling() {},
220 stopProfiling() {},
221 storeAsGlobal() {},
222 supportsTogglingSuspense,
223 updateComponentFilters() {},
224 getEnvironmentNames() {
225 return [];
226 },
227 };
228 }