| 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, ReactAsyncInfo} from 'shared/ReactTypes'; |
| 11 | import type {LazyComponent} from 'react/src/ReactLazy'; |
| 12 | |
| 13 | import { |
| 14 | describeBuiltInComponentFrame, |
| 15 | describeFunctionComponentFrame, |
| 16 | describeClassComponentFrame, |
| 17 | describeDebugInfoFrame, |
| 18 | } from 'shared/ReactComponentStackFrame'; |
| 19 | |
| 20 | import { |
| 21 | REACT_FORWARD_REF_TYPE, |
| 22 | REACT_MEMO_TYPE, |
| 23 | REACT_LAZY_TYPE, |
| 24 | REACT_SUSPENSE_LIST_TYPE, |
| 25 | REACT_SUSPENSE_TYPE, |
| 26 | REACT_VIEW_TRANSITION_TYPE, |
| 27 | } from 'shared/ReactSymbols'; |
| 28 | |
| 29 | import {enableViewTransition} from 'shared/ReactFeatureFlags'; |
| 30 | |
| 31 | import {formatOwnerStack} from 'shared/ReactOwnerStackFrames'; |
| 32 | |
| 33 | export type ComponentStackNode = { |
| 34 | parent: null | ComponentStackNode, |
| 35 | type: |
| 36 | | symbol |
| 37 | | string |
| 38 | | Function |
| 39 | | LazyComponent<any, any> |
| 40 | | ReactComponentInfo |
| 41 | | ReactAsyncInfo, |
| 42 | owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only |
| 43 | stack?: null | string | Error, // DEV only |
| 44 | }; |
| 45 | |
| 46 | function shouldConstruct(Component: any) { |
| 47 | return Component.prototype && Component.prototype.isReactComponent; |
| 48 | } |
| 49 | |
| 50 | function describeComponentStackByType( |
| 51 | type: |
| 52 | | symbol |
| 53 | | string |
| 54 | | Function |
| 55 | | LazyComponent<any, any> |
| 56 | | ReactComponentInfo, |
| 57 | ): string { |
| 58 | if (typeof type === 'string') { |
| 59 | return describeBuiltInComponentFrame(type); |
| 60 | } |
| 61 | if (typeof type === 'function') { |
| 62 | if (shouldConstruct(type)) { |
| 63 | return describeClassComponentFrame(type); |
| 64 | } else { |
| 65 | return describeFunctionComponentFrame(type); |
| 66 | } |
| 67 | } |
| 68 | // $FlowFixMe[invalid-compare] |
| 69 | if (typeof type === 'object' && type !== null) { |
| 70 | switch (type.$$typeof) { |
| 71 | case REACT_FORWARD_REF_TYPE: { |
| 72 | return describeFunctionComponentFrame((type as any).render); |
| 73 | } |
| 74 | case REACT_MEMO_TYPE: { |
| 75 | return describeFunctionComponentFrame((type as any).type); |
| 76 | } |
| 77 | case REACT_LAZY_TYPE: { |
| 78 | const lazyComponent: LazyComponent<any, any> = type as any; |
| 79 | const payload = lazyComponent._payload; |
| 80 | const init = lazyComponent._init; |
| 81 | try { |
| 82 | type = init(payload); |
| 83 | } catch (x) { |
| 84 | // TODO: When we support Thenables as component types we should rename this. |
| 85 | return describeBuiltInComponentFrame('Lazy'); |
| 86 | } |
| 87 | return describeComponentStackByType(type); |
| 88 | } |
| 89 | } |
| 90 | if (typeof type.name === 'string') { |
| 91 | return describeDebugInfoFrame(type.name, type.env, type.debugLocation); |
| 92 | } |
| 93 | } |
| 94 | switch (type) { |
| 95 | case REACT_SUSPENSE_LIST_TYPE: { |
| 96 | return describeBuiltInComponentFrame('SuspenseList'); |
| 97 | } |
| 98 | case REACT_SUSPENSE_TYPE: { |
| 99 | return describeBuiltInComponentFrame('Suspense'); |
| 100 | } |
| 101 | case REACT_VIEW_TRANSITION_TYPE: |
| 102 | if (enableViewTransition) { |
| 103 | return describeBuiltInComponentFrame('ViewTransition'); |
| 104 | } |
| 105 | } |
| 106 | return ''; |
| 107 | } |
| 108 | |
| 109 | export function getStackByComponentStackNode( |
| 110 | componentStack: ComponentStackNode, |
| 111 | ): string { |
| 112 | try { |
| 113 | let info = ''; |
| 114 | let node: ComponentStackNode = componentStack; |
| 115 | do { |
| 116 | info += describeComponentStackByType(node.type); |
| 117 | // $FlowFixMe[incompatible-type] we bail out when we get a null |
| 118 | node = node.parent; |
| 119 | } while (node); |
| 120 | return info; |
| 121 | } catch (x) { |
| 122 | return '\nError generating stack: ' + x.message + '\n' + x.stack; |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | function describeFunctionComponentFrameWithoutLineNumber(fn: Function): string { |
| 127 | // We use this because we don't actually want to describe the line of the component |
| 128 | // but just the component name. |
| 129 | const name = fn ? fn.displayName || fn.name : ''; |
| 130 | return name ? describeBuiltInComponentFrame(name) : ''; |
| 131 | } |
| 132 | |
| 133 | export function getOwnerStackByComponentStackNodeInDev( |
| 134 | componentStack: ComponentStackNode, |
| 135 | ): string { |
| 136 | if (!__DEV__) { |
| 137 | return ''; |
| 138 | } |
| 139 | try { |
| 140 | let info = ''; |
| 141 | |
| 142 | // The owner stack of the current component will be where it was created, i.e. inside its owner. |
| 143 | // There's no actual name of the currently executing component. Instead, that is available |
| 144 | // on the regular stack that's currently executing. However, for built-ins there is no such |
| 145 | // named stack frame and it would be ignored as being internal anyway. Therefore we add |
| 146 | // add one extra frame just to describe the "current" built-in component by name. |
| 147 | // Similarly, if there is no owner at all, then there's no stack frame so we add the name |
| 148 | // of the root component to the stack to know which component is currently executing. |
| 149 | if (typeof componentStack.type === 'string') { |
| 150 | info += describeBuiltInComponentFrame(componentStack.type); |
| 151 | } else if (typeof componentStack.type === 'function') { |
| 152 | if (!componentStack.owner) { |
| 153 | // Only if we have no other data about the callsite do we add |
| 154 | // the component name as the single stack frame. |
| 155 | info += describeFunctionComponentFrameWithoutLineNumber( |
| 156 | componentStack.type, |
| 157 | ); |
| 158 | } |
| 159 | } else { |
| 160 | if (!componentStack.owner) { |
| 161 | info += describeComponentStackByType(componentStack.type); |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | let owner: void | null | ComponentStackNode | ReactComponentInfo = |
| 166 | componentStack; |
| 167 | |
| 168 | while (owner) { |
| 169 | let ownerStack: ?string = null; |
| 170 | if (owner.debugStack != null) { |
| 171 | // Server Component |
| 172 | // TODO: Should we stash this somewhere for caching purposes? |
| 173 | ownerStack = formatOwnerStack(owner.debugStack); |
| 174 | owner = owner.owner; |
| 175 | } else { |
| 176 | // Client Component |
| 177 | const node: ComponentStackNode = owner as any; |
| 178 | if (node.stack != null) { |
| 179 | if (typeof node.stack !== 'string') { |
| 180 | ownerStack = node.stack = formatOwnerStack(node.stack); |
| 181 | } else { |
| 182 | ownerStack = node.stack; |
| 183 | } |
| 184 | } |
| 185 | owner = owner.owner; |
| 186 | } |
| 187 | // If we don't actually print the stack if there is no owner of this JSX element. |
| 188 | // In a real app it's typically not useful since the root app is always controlled |
| 189 | // by the framework. These also tend to have noisy stacks because they're not rooted |
| 190 | // in a React render but in some imperative bootstrapping code. It could be useful |
| 191 | // if the element was created in module scope. E.g. hoisted. We could add a a single |
| 192 | // stack frame for context for example but it doesn't say much if that's a wrapper. |
| 193 | if (owner && ownerStack) { |
| 194 | info += '\n' + ownerStack; |
| 195 | } |
| 196 | } |
| 197 | return info; |
| 198 | } catch (x) { |
| 199 | return '\nError generating stack: ' + x.message + '\n' + x.stack; |
| 200 | } |
| 201 | } |