mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
If an aborted task is not rendering, then this is an async abort. Conceptually it's as if the abort happened inside the async gap. The abort reason's stack frame won't have that on the stack so instead we use the owner stack and debug task of any halted async debug info. One thing that's a bit awkward is that if you do have a sync abort and you use that error as the "reason" then that thing still has a sync stack in a different component. In another approach I was exploring having different error objects for each component but I don't think that's worth it.
201 lines
6.5 KiB
JavaScript
201 lines
6.5 KiB
JavaScript
/**
|
|
* 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
|
|
*/
|
|
|
|
import type {ReactComponentInfo, ReactAsyncInfo} from 'shared/ReactTypes';
|
|
import type {LazyComponent} from 'react/src/ReactLazy';
|
|
|
|
import {
|
|
describeBuiltInComponentFrame,
|
|
describeFunctionComponentFrame,
|
|
describeClassComponentFrame,
|
|
describeDebugInfoFrame,
|
|
} from 'shared/ReactComponentStackFrame';
|
|
|
|
import {
|
|
REACT_FORWARD_REF_TYPE,
|
|
REACT_MEMO_TYPE,
|
|
REACT_LAZY_TYPE,
|
|
REACT_SUSPENSE_LIST_TYPE,
|
|
REACT_SUSPENSE_TYPE,
|
|
REACT_VIEW_TRANSITION_TYPE,
|
|
} from 'shared/ReactSymbols';
|
|
|
|
import {enableViewTransition} from 'shared/ReactFeatureFlags';
|
|
|
|
import {formatOwnerStack} from 'shared/ReactOwnerStackFrames';
|
|
|
|
export type ComponentStackNode = {
|
|
parent: null | ComponentStackNode,
|
|
type:
|
|
| symbol
|
|
| string
|
|
| Function
|
|
| LazyComponent<any, any>
|
|
| ReactComponentInfo
|
|
| ReactAsyncInfo,
|
|
owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only
|
|
stack?: null | string | Error, // DEV only
|
|
};
|
|
|
|
function shouldConstruct(Component: any) {
|
|
return Component.prototype && Component.prototype.isReactComponent;
|
|
}
|
|
|
|
function describeComponentStackByType(
|
|
type:
|
|
| symbol
|
|
| string
|
|
| Function
|
|
| LazyComponent<any, any>
|
|
| ReactComponentInfo,
|
|
): string {
|
|
if (typeof type === 'string') {
|
|
return describeBuiltInComponentFrame(type);
|
|
}
|
|
if (typeof type === 'function') {
|
|
if (shouldConstruct(type)) {
|
|
return describeClassComponentFrame(type);
|
|
} else {
|
|
return describeFunctionComponentFrame(type);
|
|
}
|
|
}
|
|
if (typeof type === 'object' && type !== null) {
|
|
switch (type.$$typeof) {
|
|
case REACT_FORWARD_REF_TYPE: {
|
|
return describeFunctionComponentFrame((type: any).render);
|
|
}
|
|
case REACT_MEMO_TYPE: {
|
|
return describeFunctionComponentFrame((type: any).type);
|
|
}
|
|
case REACT_LAZY_TYPE: {
|
|
const lazyComponent: LazyComponent<any, any> = (type: any);
|
|
const payload = lazyComponent._payload;
|
|
const init = lazyComponent._init;
|
|
try {
|
|
type = init(payload);
|
|
} catch (x) {
|
|
// TODO: When we support Thenables as component types we should rename this.
|
|
return describeBuiltInComponentFrame('Lazy');
|
|
}
|
|
return describeComponentStackByType(type);
|
|
}
|
|
}
|
|
if (typeof type.name === 'string') {
|
|
return describeDebugInfoFrame(type.name, type.env, type.debugLocation);
|
|
}
|
|
}
|
|
switch (type) {
|
|
case REACT_SUSPENSE_LIST_TYPE: {
|
|
return describeBuiltInComponentFrame('SuspenseList');
|
|
}
|
|
case REACT_SUSPENSE_TYPE: {
|
|
return describeBuiltInComponentFrame('Suspense');
|
|
}
|
|
case REACT_VIEW_TRANSITION_TYPE:
|
|
if (enableViewTransition) {
|
|
return describeBuiltInComponentFrame('ViewTransition');
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
export function getStackByComponentStackNode(
|
|
componentStack: ComponentStackNode,
|
|
): string {
|
|
try {
|
|
let info = '';
|
|
let node: ComponentStackNode = componentStack;
|
|
do {
|
|
info += describeComponentStackByType(node.type);
|
|
// $FlowFixMe[incompatible-type] we bail out when we get a null
|
|
node = node.parent;
|
|
} while (node);
|
|
return info;
|
|
} catch (x) {
|
|
return '\nError generating stack: ' + x.message + '\n' + x.stack;
|
|
}
|
|
}
|
|
|
|
function describeFunctionComponentFrameWithoutLineNumber(fn: Function): string {
|
|
// We use this because we don't actually want to describe the line of the component
|
|
// but just the component name.
|
|
const name = fn ? fn.displayName || fn.name : '';
|
|
return name ? describeBuiltInComponentFrame(name) : '';
|
|
}
|
|
|
|
export function getOwnerStackByComponentStackNodeInDev(
|
|
componentStack: ComponentStackNode,
|
|
): string {
|
|
if (!__DEV__) {
|
|
return '';
|
|
}
|
|
try {
|
|
let info = '';
|
|
|
|
// The owner stack of the current component will be where it was created, i.e. inside its owner.
|
|
// There's no actual name of the currently executing component. Instead, that is available
|
|
// on the regular stack that's currently executing. However, for built-ins there is no such
|
|
// named stack frame and it would be ignored as being internal anyway. Therefore we add
|
|
// add one extra frame just to describe the "current" built-in component by name.
|
|
// Similarly, if there is no owner at all, then there's no stack frame so we add the name
|
|
// of the root component to the stack to know which component is currently executing.
|
|
if (typeof componentStack.type === 'string') {
|
|
info += describeBuiltInComponentFrame(componentStack.type);
|
|
} else if (typeof componentStack.type === 'function') {
|
|
if (!componentStack.owner) {
|
|
// Only if we have no other data about the callsite do we add
|
|
// the component name as the single stack frame.
|
|
info += describeFunctionComponentFrameWithoutLineNumber(
|
|
componentStack.type,
|
|
);
|
|
}
|
|
} else {
|
|
if (!componentStack.owner) {
|
|
info += describeComponentStackByType(componentStack.type);
|
|
}
|
|
}
|
|
|
|
let owner: void | null | ComponentStackNode | ReactComponentInfo =
|
|
componentStack;
|
|
|
|
while (owner) {
|
|
let ownerStack: ?string = null;
|
|
if (owner.debugStack != null) {
|
|
// Server Component
|
|
// TODO: Should we stash this somewhere for caching purposes?
|
|
ownerStack = formatOwnerStack(owner.debugStack);
|
|
owner = owner.owner;
|
|
} else {
|
|
// Client Component
|
|
const node: ComponentStackNode = (owner: any);
|
|
if (node.stack != null) {
|
|
if (typeof node.stack !== 'string') {
|
|
ownerStack = node.stack = formatOwnerStack(node.stack);
|
|
} else {
|
|
ownerStack = node.stack;
|
|
}
|
|
}
|
|
owner = owner.owner;
|
|
}
|
|
// If we don't actually print the stack if there is no owner of this JSX element.
|
|
// In a real app it's typically not useful since the root app is always controlled
|
|
// by the framework. These also tend to have noisy stacks because they're not rooted
|
|
// in a React render but in some imperative bootstrapping code. It could be useful
|
|
// if the element was created in module scope. E.g. hoisted. We could add a a single
|
|
// stack frame for context for example but it doesn't say much if that's a wrapper.
|
|
if (owner && ownerStack) {
|
|
info += '\n' + ownerStack;
|
|
}
|
|
}
|
|
return info;
|
|
} catch (x) {
|
|
return '\nError generating stack: ' + x.message + '\n' + x.stack;
|
|
}
|
|
}
|