| 1 | /** |
| 2 | * Install the hook on window, which is an event emitter. |
| 3 | * Note: this global hook __REACT_DEVTOOLS_GLOBAL_HOOK__ is a de facto public API. |
| 4 | * It's especially important to avoid creating direct dependency on the DevTools Backend. |
| 5 | * That's why we still inline the whole event emitter implementation, |
| 6 | * the string format implementation, and part of the console implementation here. |
| 7 | * |
| 8 | * @flow |
| 9 | */ |
| 10 | |
| 11 | import type { |
| 12 | DevToolsHook, |
| 13 | Handler, |
| 14 | ReactRenderer, |
| 15 | RendererID, |
| 16 | RendererInterface, |
| 17 | DevToolsBackend, |
| 18 | DevToolsHookSettings, |
| 19 | ProfilingSettings, |
| 20 | ReactBuildType, |
| 21 | } from './backend/types'; |
| 22 | import type {ComponentFilter} from './frontend/types'; |
| 23 | |
| 24 | import { |
| 25 | FIREFOX_CONSOLE_DIMMING_COLOR, |
| 26 | ANSI_STYLE_DIMMING_TEMPLATE, |
| 27 | ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK, |
| 28 | } from 'react-devtools-shared/src/constants'; |
| 29 | import attachRenderer from './attachRenderer'; |
| 30 | import formatConsoleArguments from 'react-devtools-shared/src/backend/utils/formatConsoleArguments'; |
| 31 | import formatWithStyles from 'react-devtools-shared/src/backend/utils/formatWithStyles'; |
| 32 | |
| 33 | // React's custom built component stack strings match "\s{4}in" |
| 34 | // Chrome's prefix matches "\s{4}at" |
| 35 | const PREFIX_REGEX = /\s{4}(in|at)\s{1}/; |
| 36 | // Firefox and Safari have no prefix ("") |
| 37 | // but we can fallback to looking for location info (e.g. "foo.js:12:345") |
| 38 | const ROW_COLUMN_NUMBER_REGEX = /:\d+:\d+(\n|$)/; |
| 39 | |
| 40 | function isStringComponentStack(text: string): boolean { |
| 41 | return PREFIX_REGEX.test(text) || ROW_COLUMN_NUMBER_REGEX.test(text); |
| 42 | } |
| 43 | |
| 44 | // We add a suffix to some frames that older versions of React didn't do. |
| 45 | // To compare if it's equivalent we strip out the suffix to see if they're |
| 46 | // still equivalent. Similarly, we sometimes use [] and sometimes () so we |
| 47 | // strip them to for the comparison. |
| 48 | const frameDiffs = / \(\<anonymous\>\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm; |
| 49 | function areStackTracesEqual(a: string, b: string): boolean { |
| 50 | return a.replace(frameDiffs, '') === b.replace(frameDiffs, ''); |
| 51 | } |
| 52 | |
| 53 | const targetConsole: Object = console; |
| 54 | |
| 55 | const defaultProfilingSettings: ProfilingSettings = { |
| 56 | recordChangeDescriptions: false, |
| 57 | }; |
| 58 | |
| 59 | export function installHook( |
| 60 | target: any, |
| 61 | componentFiltersOrComponentFiltersPromise: |
| 62 | | Array<ComponentFilter> |
| 63 | | Promise<Array<ComponentFilter>>, |
| 64 | maybeSettingsOrSettingsPromise?: |
| 65 | | DevToolsHookSettings |
| 66 | | Promise<DevToolsHookSettings>, |
| 67 | shouldStartProfilingNow: boolean = false, |
| 68 | profilingSettings: ProfilingSettings = defaultProfilingSettings, |
| 69 | ): DevToolsHook | null { |
| 70 | if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { |
| 71 | return null; |
| 72 | } |
| 73 | |
| 74 | function detectReactBuildType(renderer: ReactRenderer): ReactBuildType { |
| 75 | try { |
| 76 | if (typeof renderer.version === 'string') { |
| 77 | // React DOM Fiber (16+) |
| 78 | if (renderer.bundleType > 0) { |
| 79 | // This is not a production build. |
| 80 | // We are currently only using 0 (PROD) and 1 (DEV) |
| 81 | // but might add 2 (PROFILE) in the future. |
| 82 | return 'development'; |
| 83 | } |
| 84 | |
| 85 | // React 16 uses flat bundles. If we report the bundle as production |
| 86 | // version, it means we also minified and envified it ourselves. |
| 87 | return 'production'; |
| 88 | // Note: There is still a risk that the CommonJS entry point has not |
| 89 | // been envified or uglified. In this case the user would have *both* |
| 90 | // development and production bundle, but only the prod one would run. |
| 91 | // This would be really bad. We have a separate check for this because |
| 92 | // it happens *outside* of the renderer injection. See `checkDCE` below. |
| 93 | } |
| 94 | |
| 95 | // $FlowFixMe[method-unbinding] |
| 96 | const toString = Function.prototype.toString; |
| 97 | if (renderer.Mount && renderer.Mount._renderNewRootComponent) { |
| 98 | // React DOM Stack |
| 99 | const renderRootCode = toString.call( |
| 100 | renderer.Mount._renderNewRootComponent, |
| 101 | ); |
| 102 | // Filter out bad results (if that is even possible): |
| 103 | if (renderRootCode.indexOf('function') !== 0) { |
| 104 | // Hope for the best if we're not sure. |
| 105 | return 'production'; |
| 106 | } |
| 107 | // Check for React DOM Stack < 15.1.0 in development. |
| 108 | // If it contains "storedMeasure" call, it's wrapped in ReactPerf (DEV only). |
| 109 | // This would be true even if it's minified, as method name still matches. |
| 110 | if (renderRootCode.indexOf('storedMeasure') !== -1) { |
| 111 | return 'development'; |
| 112 | } |
| 113 | // For other versions (and configurations) it's not so easy. |
| 114 | // Let's quickly exclude proper production builds. |
| 115 | // If it contains a warning message, it's either a DEV build, |
| 116 | // or an PROD build without proper dead code elimination. |
| 117 | if (renderRootCode.indexOf('should be a pure function') !== -1) { |
| 118 | // Now how do we tell a DEV build from a bad PROD build? |
| 119 | // If we see NODE_ENV, we're going to assume this is a dev build |
| 120 | // because most likely it is referring to an empty shim. |
| 121 | if (renderRootCode.indexOf('NODE_ENV') !== -1) { |
| 122 | return 'development'; |
| 123 | } |
| 124 | // If we see "development", we're dealing with an envified DEV build |
| 125 | // (such as the official React DEV UMD). |
| 126 | if (renderRootCode.indexOf('development') !== -1) { |
| 127 | return 'development'; |
| 128 | } |
| 129 | // I've seen process.env.NODE_ENV !== 'production' being smartly |
| 130 | // replaced by `true` in DEV by Webpack. I don't know how that |
| 131 | // works but we can safely guard against it because `true` was |
| 132 | // never used in the function source since it was written. |
| 133 | if (renderRootCode.indexOf('true') !== -1) { |
| 134 | return 'development'; |
| 135 | } |
| 136 | // By now either it is a production build that has not been minified, |
| 137 | // or (worse) this is a minified development build using non-standard |
| 138 | // environment (e.g. "staging"). We're going to look at whether |
| 139 | // the function argument name is mangled: |
| 140 | if ( |
| 141 | // 0.13 to 15 |
| 142 | renderRootCode.indexOf('nextElement') !== -1 || |
| 143 | // 0.12 |
| 144 | renderRootCode.indexOf('nextComponent') !== -1 |
| 145 | ) { |
| 146 | // We can't be certain whether this is a development build or not, |
| 147 | // but it is definitely unminified. |
| 148 | return 'unminified'; |
| 149 | } else { |
| 150 | // This is likely a minified development build. |
| 151 | return 'development'; |
| 152 | } |
| 153 | } |
| 154 | // By now we know that it's envified and dead code elimination worked, |
| 155 | // but what if it's still not minified? (Is this even possible?) |
| 156 | // Let's check matches for the first argument name. |
| 157 | if ( |
| 158 | // 0.13 to 15 |
| 159 | renderRootCode.indexOf('nextElement') !== -1 || |
| 160 | // 0.12 |
| 161 | renderRootCode.indexOf('nextComponent') !== -1 |
| 162 | ) { |
| 163 | return 'unminified'; |
| 164 | } |
| 165 | // Seems like we're using the production version. |
| 166 | // However, the branch above is Stack-only so this is 15 or earlier. |
| 167 | return 'outdated'; |
| 168 | } |
| 169 | } catch (err) { |
| 170 | // Weird environments may exist. |
| 171 | // This code needs a higher fault tolerance |
| 172 | // because it runs even with closed DevTools. |
| 173 | // TODO: should we catch errors in all injected code, and not just this part? |
| 174 | } |
| 175 | return 'production'; |
| 176 | } |
| 177 | |
| 178 | function checkDCE(fn: Function) { |
| 179 | // This runs for production versions of React. |
| 180 | // Needs to be super safe. |
| 181 | try { |
| 182 | // $FlowFixMe[method-unbinding] |
| 183 | const toString = Function.prototype.toString; |
| 184 | const code = toString.call(fn); |
| 185 | |
| 186 | // This is a string embedded in the passed function under DEV-only |
| 187 | // condition. However the function executes only in PROD. Therefore, |
| 188 | // if we see it, dead code elimination did not work. |
| 189 | if (code.indexOf('^_^') > -1) { |
| 190 | // Remember to report during next injection. |
| 191 | hasDetectedBadDCE = true; |
| 192 | |
| 193 | // Bonus: throw an exception hoping that it gets picked up by a reporting system. |
| 194 | // Not synchronously so that it doesn't break the calling code. |
| 195 | setTimeout(function () { |
| 196 | throw new Error( |
| 197 | 'React is running in production mode, but dead code ' + |
| 198 | 'elimination has not been applied. Read how to correctly ' + |
| 199 | 'configure React for production: ' + |
| 200 | 'https://react.dev/link/perf-use-production-build', |
| 201 | ); |
| 202 | }); |
| 203 | } |
| 204 | } catch (err) {} |
| 205 | } |
| 206 | |
| 207 | // TODO: isProfiling should be stateful, and we should update it once profiling is finished |
| 208 | const isProfiling = shouldStartProfilingNow; |
| 209 | let uidCounter = 0; |
| 210 | function inject(renderer: ReactRenderer): number { |
| 211 | const id = ++uidCounter; |
| 212 | renderers.set(id, renderer); |
| 213 | |
| 214 | const reactBuildType: ReactBuildType = hasDetectedBadDCE |
| 215 | ? 'deadcode' |
| 216 | : detectReactBuildType(renderer); |
| 217 | |
| 218 | hook.emit('renderer', { |
| 219 | id, |
| 220 | renderer, |
| 221 | reactBuildType, |
| 222 | }); |
| 223 | |
| 224 | const rendererInterface = attachRenderer( |
| 225 | hook, |
| 226 | id, |
| 227 | renderer, |
| 228 | target, |
| 229 | isProfiling, |
| 230 | profilingSettings, |
| 231 | componentFiltersOrComponentFiltersPromise, |
| 232 | ); |
| 233 | if (rendererInterface != null) { |
| 234 | hook.rendererInterfaces.set(id, rendererInterface); |
| 235 | hook.emit('renderer-attached', {id, rendererInterface}); |
| 236 | } else { |
| 237 | hook.hasUnsupportedRendererAttached = true; |
| 238 | hook.emit('unsupported-renderer-version'); |
| 239 | } |
| 240 | |
| 241 | return id; |
| 242 | } |
| 243 | |
| 244 | let hasDetectedBadDCE = false; |
| 245 | |
| 246 | function sub(event: string, fn: Handler) { |
| 247 | hook.on(event, fn); |
| 248 | return () => hook.off(event, fn); |
| 249 | } |
| 250 | |
| 251 | function on(event: string, fn: Handler) { |
| 252 | if (!listeners[event]) { |
| 253 | listeners[event] = []; |
| 254 | } |
| 255 | listeners[event].push(fn); |
| 256 | } |
| 257 | |
| 258 | function off(event: string, fn: Handler) { |
| 259 | if (!listeners[event]) { |
| 260 | return; |
| 261 | } |
| 262 | const index = listeners[event].indexOf(fn); |
| 263 | if (index !== -1) { |
| 264 | listeners[event].splice(index, 1); |
| 265 | } |
| 266 | if (!listeners[event].length) { |
| 267 | delete listeners[event]; |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | function emit(event: string, data: any) { |
| 272 | if (listeners[event]) { |
| 273 | listeners[event].map(fn => fn(data)); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | function getFiberRoots(rendererID: RendererID) { |
| 278 | const roots = fiberRoots; |
| 279 | if (!roots[rendererID]) { |
| 280 | roots[rendererID] = new Set(); |
| 281 | } |
| 282 | return roots[rendererID]; |
| 283 | } |
| 284 | |
| 285 | function onCommitFiberUnmount(rendererID: RendererID, fiber: any) { |
| 286 | const rendererInterface = rendererInterfaces.get(rendererID); |
| 287 | if (rendererInterface != null) { |
| 288 | rendererInterface.handleCommitFiberUnmount(fiber); |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | function onCommitFiberRoot( |
| 293 | rendererID: RendererID, |
| 294 | root: any, |
| 295 | priorityLevel: void | number, |
| 296 | ) { |
| 297 | const mountedRoots = hook.getFiberRoots(rendererID); |
| 298 | const current = root.current; |
| 299 | const isKnownRoot = mountedRoots.has(root); |
| 300 | const isUnmounting = |
| 301 | current.memoizedState == null || current.memoizedState.element == null; |
| 302 | |
| 303 | // Keep track of mounted roots so we can hydrate when DevTools connect. |
| 304 | if (!isKnownRoot && !isUnmounting) { |
| 305 | mountedRoots.add(root); |
| 306 | } else if (isKnownRoot && isUnmounting) { |
| 307 | mountedRoots.delete(root); |
| 308 | } |
| 309 | const rendererInterface = rendererInterfaces.get(rendererID); |
| 310 | if (rendererInterface != null) { |
| 311 | rendererInterface.handleCommitFiberRoot(root, priorityLevel); |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | function onPostCommitFiberRoot(rendererID: RendererID, root: any) { |
| 316 | const rendererInterface = rendererInterfaces.get(rendererID); |
| 317 | if (rendererInterface != null) { |
| 318 | rendererInterface.handlePostCommitFiberRoot(root); |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | let isRunningDuringStrictModeInvocation = false; |
| 323 | function setStrictMode(rendererID: RendererID, isStrictMode: boolean) { |
| 324 | isRunningDuringStrictModeInvocation = isStrictMode; |
| 325 | |
| 326 | if (isStrictMode) { |
| 327 | patchConsoleForStrictMode(); |
| 328 | } else { |
| 329 | unpatchConsoleForStrictMode(); |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | const unpatchConsoleCallbacks = []; |
| 334 | // For StrictMode we patch console once we are running in StrictMode and unpatch right after it |
| 335 | // So patching could happen multiple times during the runtime |
| 336 | // Notice how we don't patch error or warn methods, because they are already patched in patchConsoleForErrorsAndWarnings |
| 337 | // This will only happen once, when hook is installed |
| 338 | function patchConsoleForStrictMode() { |
| 339 | // Don't patch console in case settings were not injected |
| 340 | if (!hook.settings) { |
| 341 | return; |
| 342 | } |
| 343 | |
| 344 | // Don't patch twice |
| 345 | if (unpatchConsoleCallbacks.length > 0) { |
| 346 | return; |
| 347 | } |
| 348 | |
| 349 | // At this point 'error', 'warn', and 'trace' methods are already patched |
| 350 | // by React DevTools hook to append component stacks and other possible features. |
| 351 | const consoleMethodsToOverrideForStrictMode = [ |
| 352 | 'group', |
| 353 | 'groupCollapsed', |
| 354 | 'info', |
| 355 | 'log', |
| 356 | ]; |
| 357 | |
| 358 | // eslint-disable-next-line no-for-of-loops/no-for-of-loops |
| 359 | for (const method of consoleMethodsToOverrideForStrictMode) { |
| 360 | const originalMethod = targetConsole[method]; |
| 361 | const overrideMethod: (...args: Array<any>) => void = ( |
| 362 | ...args: any[] |
| 363 | ) => { |
| 364 | const settings = hook.settings; |
| 365 | // Something unexpected happened, fallback to just printing the console message. |
| 366 | if (settings == null) { |
| 367 | originalMethod(...args); |
| 368 | return; |
| 369 | } |
| 370 | |
| 371 | if (settings.hideConsoleLogsInStrictMode) { |
| 372 | return; |
| 373 | } |
| 374 | |
| 375 | if (settings.disableSecondConsoleLogDimmingInStrictMode) { |
| 376 | // Don't dim the console logs |
| 377 | originalMethod(...args); |
| 378 | } else { |
| 379 | // Dim the text color of the double logs if we're not hiding them. |
| 380 | // Firefox doesn't support ANSI escape sequences |
| 381 | if (__IS_FIREFOX__) { |
| 382 | originalMethod( |
| 383 | ...formatWithStyles(args, FIREFOX_CONSOLE_DIMMING_COLOR), |
| 384 | ); |
| 385 | } else { |
| 386 | originalMethod( |
| 387 | ANSI_STYLE_DIMMING_TEMPLATE, |
| 388 | ...formatConsoleArguments(...args), |
| 389 | ); |
| 390 | } |
| 391 | } |
| 392 | }; |
| 393 | |
| 394 | targetConsole[method] = overrideMethod; |
| 395 | unpatchConsoleCallbacks.push(() => { |
| 396 | targetConsole[method] = originalMethod; |
| 397 | }); |
| 398 | } |
| 399 | } |
| 400 | |
| 401 | function unpatchConsoleForStrictMode() { |
| 402 | unpatchConsoleCallbacks.forEach(callback => callback()); |
| 403 | unpatchConsoleCallbacks.length = 0; |
| 404 | } |
| 405 | |
| 406 | // For Errors and Warnings we only patch console once |
| 407 | function patchConsoleForErrorsAndWarnings() { |
| 408 | // Don't patch console in case settings were not injected |
| 409 | if (!hook.settings) { |
| 410 | return; |
| 411 | } |
| 412 | |
| 413 | const consoleMethodsToOverrideForErrorsAndWarnings = [ |
| 414 | 'error', |
| 415 | 'trace', |
| 416 | 'warn', |
| 417 | ]; |
| 418 | |
| 419 | // eslint-disable-next-line no-for-of-loops/no-for-of-loops |
| 420 | for (const method of consoleMethodsToOverrideForErrorsAndWarnings) { |
| 421 | const originalMethod = targetConsole[method]; |
| 422 | const overrideMethod: (...args: Array<any>) => void = (...args) => { |
| 423 | const settings = hook.settings; |
| 424 | // Something unexpected happened, fallback to just printing the console message. |
| 425 | if (settings == null) { |
| 426 | originalMethod(...args); |
| 427 | return; |
| 428 | } |
| 429 | |
| 430 | if ( |
| 431 | isRunningDuringStrictModeInvocation && |
| 432 | settings.hideConsoleLogsInStrictMode |
| 433 | ) { |
| 434 | return; |
| 435 | } |
| 436 | |
| 437 | let injectedComponentStackAsFakeError = false; |
| 438 | let alreadyHasComponentStack = false; |
| 439 | if (settings.appendComponentStack) { |
| 440 | const lastArg = args.length > 0 ? args[args.length - 1] : null; |
| 441 | alreadyHasComponentStack = |
| 442 | typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack. |
| 443 | } |
| 444 | |
| 445 | const shouldShowInlineWarningsAndErrors = |
| 446 | settings.showInlineWarningsAndErrors && |
| 447 | (method === 'error' || method === 'warn'); |
| 448 | |
| 449 | // Search for the first renderer that has a current Fiber. |
| 450 | // We don't handle the edge case of stacks for more than one (e.g. interleaved renderers?) |
| 451 | // eslint-disable-next-line no-for-of-loops/no-for-of-loops |
| 452 | for (const rendererInterface of hook.rendererInterfaces.values()) { |
| 453 | const {onErrorOrWarning, getComponentStack} = rendererInterface; |
| 454 | try { |
| 455 | if (shouldShowInlineWarningsAndErrors) { |
| 456 | // patch() is called by two places: (1) the hook and (2) the renderer backend. |
| 457 | // The backend is what implements a message queue, so it's the only one that injects onErrorOrWarning. |
| 458 | if (onErrorOrWarning != null) { |
| 459 | onErrorOrWarning( |
| 460 | method as any as 'error' | 'warn', |
| 461 | args.slice(), |
| 462 | ); |
| 463 | } |
| 464 | } |
| 465 | } catch (error) { |
| 466 | // Don't let a DevTools or React internal error interfere with logging. |
| 467 | setTimeout(() => { |
| 468 | throw error; |
| 469 | }, 0); |
| 470 | } |
| 471 | |
| 472 | try { |
| 473 | if (settings.appendComponentStack && getComponentStack != null) { |
| 474 | // This needs to be directly in the wrapper so we can pop exactly one frame. |
| 475 | const topFrame = Error('react-stack-top-frame'); |
| 476 | const match = getComponentStack(topFrame); |
| 477 | if (match !== null) { |
| 478 | const {enableOwnerStacks, componentStack} = match; |
| 479 | // Empty string means we have a match but no component stack. |
| 480 | // We don't need to look in other renderers but we also don't add anything. |
| 481 | if (componentStack !== '') { |
| 482 | // Create a fake Error so that when we print it we get native source maps. Every |
| 483 | // browser will print the .stack property of the error and then parse it back for source |
| 484 | // mapping. Rather than print the internal slot. So it doesn't matter that the internal |
| 485 | // slot doesn't line up. |
| 486 | const fakeError = new Error(''); |
| 487 | // In Chromium, only the stack property is printed but in Firefox the <name>:<message> |
| 488 | // gets printed so to make the colon make sense, we name it so we print Stack: |
| 489 | // and similarly Safari leave an expandable slot. |
| 490 | if (__IS_CHROME__ || __IS_EDGE__) { |
| 491 | // Before sending the stack to Chrome DevTools for formatting, |
| 492 | // V8 will reconstruct this according to the template <name>: <message><stack-frames> |
| 493 | // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/inspector/value-mirror.cc;l=252-311;drc=bdc48d1b1312cc40c00282efb1c9c5f41dcdca9a |
| 494 | // It has to start with ^[\w.]*Error\b to trigger stack formatting. |
| 495 | fakeError.name = enableOwnerStacks |
| 496 | ? 'Error Stack' |
| 497 | : 'Error Component Stack'; // This gets printed |
| 498 | } else { |
| 499 | fakeError.name = enableOwnerStacks |
| 500 | ? 'Stack' |
| 501 | : 'Component Stack'; // This gets printed |
| 502 | } |
| 503 | // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack |
| 504 | // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it |
| 505 | // to our own stack. |
| 506 | fakeError.stack = |
| 507 | __IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__ |
| 508 | ? (enableOwnerStacks |
| 509 | ? 'Error Stack:' |
| 510 | : 'Error Component Stack:') + componentStack |
| 511 | : componentStack; |
| 512 | |
| 513 | if (alreadyHasComponentStack) { |
| 514 | // Only modify the component stack if it matches what we would've added anyway. |
| 515 | // Otherwise we assume it was a non-React stack. |
| 516 | if ( |
| 517 | areStackTracesEqual(args[args.length - 1], componentStack) |
| 518 | ) { |
| 519 | const firstArg = args[0]; |
| 520 | if ( |
| 521 | args.length > 1 && |
| 522 | typeof firstArg === 'string' && |
| 523 | firstArg.endsWith('%s') |
| 524 | ) { |
| 525 | args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param |
| 526 | } |
| 527 | args[args.length - 1] = fakeError; |
| 528 | injectedComponentStackAsFakeError = true; |
| 529 | } |
| 530 | } else { |
| 531 | args.push(fakeError); |
| 532 | injectedComponentStackAsFakeError = true; |
| 533 | } |
| 534 | } |
| 535 | |
| 536 | // Don't add stacks from other renderers. |
| 537 | break; |
| 538 | } |
| 539 | } |
| 540 | } catch (error) { |
| 541 | // Don't let a DevTools or React internal error interfere with logging. |
| 542 | setTimeout(() => { |
| 543 | throw error; |
| 544 | }, 0); |
| 545 | } |
| 546 | } |
| 547 | |
| 548 | if (settings.breakOnConsoleErrors) { |
| 549 | // --- Welcome to debugging with React DevTools --- |
| 550 | // This debugger statement means that you've enabled the "break on warnings" feature. |
| 551 | // Use the browser's Call Stack panel to step out of this override function |
| 552 | // to where the original warning or error was logged. |
| 553 | // eslint-disable-next-line no-debugger |
| 554 | debugger; |
| 555 | } |
| 556 | |
| 557 | if ( |
| 558 | isRunningDuringStrictModeInvocation && |
| 559 | !settings.disableSecondConsoleLogDimmingInStrictMode |
| 560 | ) { |
| 561 | // Dim the text color of the double logs if we're not hiding them. |
| 562 | // Firefox doesn't support ANSI escape sequences |
| 563 | if (__IS_FIREFOX__) { |
| 564 | let argsWithCSSStyles = formatWithStyles( |
| 565 | args, |
| 566 | FIREFOX_CONSOLE_DIMMING_COLOR, |
| 567 | ); |
| 568 | |
| 569 | if (injectedComponentStackAsFakeError) { |
| 570 | argsWithCSSStyles = [ |
| 571 | `${argsWithCSSStyles[0]} %o`, |
| 572 | ...argsWithCSSStyles.slice(1), |
| 573 | ]; |
| 574 | } |
| 575 | |
| 576 | originalMethod(...argsWithCSSStyles); |
| 577 | } else { |
| 578 | originalMethod( |
| 579 | injectedComponentStackAsFakeError |
| 580 | ? ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK |
| 581 | : ANSI_STYLE_DIMMING_TEMPLATE, |
| 582 | ...formatConsoleArguments(...args), |
| 583 | ); |
| 584 | } |
| 585 | } else { |
| 586 | originalMethod(...args); |
| 587 | } |
| 588 | }; |
| 589 | |
| 590 | targetConsole[method] = overrideMethod; |
| 591 | } |
| 592 | } |
| 593 | |
| 594 | // TODO: More meaningful names for "rendererInterfaces" and "renderers". |
| 595 | const fiberRoots: {[RendererID]: Set<mixed>} = {}; |
| 596 | const rendererInterfaces = new Map<RendererID, RendererInterface>(); |
| 597 | const listeners: {[string]: Array<Handler>} = {}; |
| 598 | const renderers = new Map<RendererID, ReactRenderer>(); |
| 599 | const backends = new Map<string, DevToolsBackend>(); |
| 600 | |
| 601 | const hook: DevToolsHook = { |
| 602 | rendererInterfaces, |
| 603 | listeners, |
| 604 | |
| 605 | backends, |
| 606 | |
| 607 | // Fast Refresh for web relies on this. |
| 608 | renderers, |
| 609 | hasUnsupportedRendererAttached: false, |
| 610 | |
| 611 | emit, |
| 612 | getFiberRoots, |
| 613 | inject, |
| 614 | on, |
| 615 | off, |
| 616 | sub, |
| 617 | |
| 618 | // This is a legacy flag. |
| 619 | // React v16 checks the hook for this to ensure DevTools is new enough. |
| 620 | supportsFiber: true, |
| 621 | |
| 622 | // React Flight Client checks the hook for this to ensure DevTools is new enough. |
| 623 | supportsFlight: true, |
| 624 | |
| 625 | // React calls these methods. |
| 626 | checkDCE, |
| 627 | onCommitFiberUnmount, |
| 628 | onCommitFiberRoot, |
| 629 | // React v18.0+ |
| 630 | onPostCommitFiberRoot, |
| 631 | setStrictMode, |
| 632 | }; |
| 633 | |
| 634 | if (maybeSettingsOrSettingsPromise == null) { |
| 635 | // Set default settings |
| 636 | hook.settings = { |
| 637 | appendComponentStack: true, |
| 638 | breakOnConsoleErrors: false, |
| 639 | showInlineWarningsAndErrors: true, |
| 640 | hideConsoleLogsInStrictMode: false, |
| 641 | disableSecondConsoleLogDimmingInStrictMode: false, |
| 642 | }; |
| 643 | patchConsoleForErrorsAndWarnings(); |
| 644 | } else { |
| 645 | Promise.resolve(maybeSettingsOrSettingsPromise) |
| 646 | .then(settings => { |
| 647 | hook.settings = settings; |
| 648 | hook.emit('settingsInitialized', settings); |
| 649 | |
| 650 | patchConsoleForErrorsAndWarnings(); |
| 651 | }) |
| 652 | .catch(() => { |
| 653 | targetConsole.error( |
| 654 | "React DevTools failed to get Console Patching settings. Console won't be patched and some console features will not work.", |
| 655 | ); |
| 656 | }); |
| 657 | } |
| 658 | |
| 659 | Object.defineProperty(target, '__REACT_DEVTOOLS_GLOBAL_HOOK__', { |
| 660 | // This property needs to be configurable for the test environment, |
| 661 | // else we won't be able to delete and recreate it between tests. |
| 662 | configurable: __DEV__, |
| 663 | enumerable: false, |
| 664 | get() { |
| 665 | return hook; |
| 666 | }, |
| 667 | } as Object); |
| 668 | |
| 669 | return hook; |
| 670 | } |