/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ // This is a DevTools fork of ReactComponentStackFrame. // This fork enables DevTools to use the same "native" component stack format, // while still maintaining support for multiple renderer versions // (which use different values for ReactTypeOfWork). import type {CurrentDispatcherRef} from '../types'; // The shared console patching code is DEV-only. // We can't use it since DevTools only ships production builds. import {disableLogs, reenableLogs} from './DevToolsConsolePatching'; let prefix; export function describeBuiltInComponentFrame(name: string): string { if (prefix === undefined) { // Extract the VM specific prefix used by each line. try { throw Error(); } catch (x) { const match = x.stack.trim().match(/\n( *(at )?)/); prefix = (match && match[1]) || ''; } } let suffix = ''; if (__IS_CHROME__ || __IS_EDGE__ || __IS_NATIVE__) { suffix = ' ()'; } else if (__IS_FIREFOX__) { suffix = '@unknown:0:0'; } // We use the prefix to ensure our stacks line up with native stack frames. // We use a suffix to ensure it gets parsed natively. return '\n' + prefix + name + suffix; } export function describeDebugInfoFrame(name: string, env: ?string): string { return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : '')); } let reentry = false; let componentFrameCache; if (__DEV__) { const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map; componentFrameCache = new PossiblyWeakMap<$FlowFixMe, string>(); } export function describeNativeComponentFrame( fn: Function, construct: boolean, currentDispatcherRef: CurrentDispatcherRef, ): string { // If something asked for a stack inside a fake render, it should get ignored. if (!fn || reentry) { return ''; } if (__DEV__) { const frame = componentFrameCache.get(fn); if (frame !== undefined) { return frame; } } const previousPrepareStackTrace = Error.prepareStackTrace; // $FlowFixMe[incompatible-type] It does accept undefined. Error.prepareStackTrace = undefined; reentry = true; // Override the dispatcher so effects scheduled by this shallow render are thrown away. // // Note that unlike the code this was forked from (in ReactComponentStackFrame) // DevTools should override the dispatcher even when DevTools is compiled in production mode, // because the app itself may be in development mode and log errors/warnings. const previousDispatcher = currentDispatcherRef.H; currentDispatcherRef.H = null; disableLogs(); try { // NOTE: keep in sync with the implementation in ReactComponentStackFrame /** * Finding a common stack frame between sample and control errors can be * tricky given the different types and levels of stack trace truncation from * different JS VMs. So instead we'll attempt to control what that common * frame should be through this object method: * Having both the sample and control errors be in the function under the * `DescribeNativeComponentFrameRoot` property, + setting the `name` and * `displayName` properties of the function ensures that a stack * frame exists that has the method name `DescribeNativeComponentFrameRoot` in * it for both control and sample stacks. */ const RunInRootFrame = { DetermineComponentFrameRoot(): [?string, ?string] { let control; try { // This should throw. if (construct) { // Something should be setting the props in the constructor. const Fake = function () { throw Error(); }; // $FlowFixMe[prop-missing] Object.defineProperty(Fake.prototype, 'props', { set: function () { // We use a throwing setter instead of frozen or non-writable props // because that won't throw in a non-strict mode function. throw Error(); }, }); if (typeof Reflect === 'object' && Reflect.construct) { // We construct a different control for this case to include any extra // frames added by the construct call. try { Reflect.construct(Fake, []); } catch (x) { control = x; } Reflect.construct(fn, [], Fake); } else { try { Fake.call(); } catch (x) { control = x; } // $FlowFixMe[prop-missing] found when upgrading Flow fn.call(Fake.prototype); } } else { try { throw Error(); } catch (x) { control = x; } // TODO(luna): This will currently only throw if the function component // tries to access React/ReactDOM/props. We should probably make this throw // in simple components too const maybePromise = fn(); // If the function component returns a promise, it's likely an async // component, which we don't yet support. Attach a noop catch handler to // silence the error. // TODO: Implement component stacks for async client components? if (maybePromise && typeof maybePromise.catch === 'function') { maybePromise.catch(() => {}); } } } catch (sample) { // This is inlined manually because closure doesn't do it for us. if (sample && control && typeof sample.stack === 'string') { return [sample.stack, control.stack]; } } return [null, null]; }, }; // $FlowFixMe[prop-missing] RunInRootFrame.DetermineComponentFrameRoot.displayName = 'DetermineComponentFrameRoot'; const namePropDescriptor = Object.getOwnPropertyDescriptor( RunInRootFrame.DetermineComponentFrameRoot, 'name', ); // Before ES6, the `name` property was not configurable. if (namePropDescriptor && namePropDescriptor.configurable) { // V8 utilizes a function's `name` property when generating a stack trace. Object.defineProperty( RunInRootFrame.DetermineComponentFrameRoot, // Configurable properties can be updated even if its writable descriptor // is set to `false`. // $FlowFixMe[cannot-write] 'name', {value: 'DetermineComponentFrameRoot'}, ); } const [sampleStack, controlStack] = RunInRootFrame.DetermineComponentFrameRoot(); if (sampleStack && controlStack) { // This extracts the first frame from the sample that isn't also in the control. // Skipping one frame that we assume is the frame that calls the two. const sampleLines = sampleStack.split('\n'); const controlLines = controlStack.split('\n'); let s = 0; let c = 0; while ( s < sampleLines.length && !sampleLines[s].includes('DetermineComponentFrameRoot') ) { s++; } while ( c < controlLines.length && !controlLines[c].includes('DetermineComponentFrameRoot') ) { c++; } // We couldn't find our intentionally injected common root frame, attempt // to find another common root frame by search from the bottom of the // control stack... if (s === sampleLines.length || c === controlLines.length) { s = sampleLines.length - 1; c = controlLines.length - 1; while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) { // We expect at least one stack frame to be shared. // Typically this will be the root most one. However, stack frames may be // cut off due to maximum stack limits. In this case, one maybe cut off // earlier than the other. We assume that the sample is longer or the same // and there for cut off earlier. So we should find the root most frame in // the sample somewhere in the control. c--; } } for (; s >= 1 && c >= 0; s--, c--) { // Next we find the first one that isn't the same which should be the // frame that called our sample function and the control. if (sampleLines[s] !== controlLines[c]) { // In V8, the first line is describing the message but other VMs don't. // If we're about to return the first line, and the control is also on the same // line, that's a pretty good indicator that our sample threw at same line as // the control. I.e. before we entered the sample frame. So we ignore this result. // This can happen if you passed a class to function component, or non-function. if (s !== 1 || c !== 1) { do { s--; c--; // We may still have similar intermediate frames from the construct call. // The next one that isn't the same should be our match though. if (c < 0 || sampleLines[s] !== controlLines[c]) { // V8 adds a "new" prefix for native classes. Let's remove it to make it prettier. let frame = '\n' + sampleLines[s].replace(' at new ', ' at '); // If our component frame is labeled "" // but we have a user-provided "displayName" // splice it in to make the stack more readable. if (fn.displayName && frame.includes('')) { frame = frame.replace('', fn.displayName); } if (__DEV__) { if (typeof fn === 'function') { componentFrameCache.set(fn, frame); } } // Return the line we found. return frame; } } while (s >= 1 && c >= 0); } break; } } } } finally { reentry = false; Error.prepareStackTrace = previousPrepareStackTrace; currentDispatcherRef.H = previousDispatcher; reenableLogs(); } // Fallback to just using the name if we couldn't make it throw. const name = fn ? fn.displayName || fn.name : ''; const syntheticFrame = name ? describeBuiltInComponentFrame(name) : ''; if (__DEV__) { if (typeof fn === 'function') { componentFrameCache.set(fn, syntheticFrame); } } return syntheticFrame; } export function describeClassComponentFrame( ctor: Function, currentDispatcherRef: CurrentDispatcherRef, ): string { return describeNativeComponentFrame(ctor, true, currentDispatcherRef); } export function describeFunctionComponentFrame( fn: Function, currentDispatcherRef: CurrentDispatcherRef, ): string { return describeNativeComponentFrame(fn, false, currentDispatcherRef); }