| 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 | } |