Files
react/packages/react-devtools-shared/src/backend/fiber/DevToolsFiberComponentStack.js
T
Sebastian MarkbågeandGitHub e44685e4f1 [DevTools] Use Owner Stacks to Implement View Source of a Server Component (#30798)
We don't have the source location of Server Components on the client
because we don't want to eagerly do the throw trick for all Server
Components just in case. Unfortunately Node.js doesn't expose V8's API
to get a source location of a function.

We do have the owner stacks of the JSX that created it though and at
some point we'll also show that location in DevTools.

However, the realization is that if a Server Component is the owner of
any child. The owner stack of that child will have the owner component's
source location as its bottom stack frame.

The technique I'm implementing here is to track whenever a child mounts
we already have its owner. We track the first discovered owned child's
stack on the owner. Then when we ask for a Source location of the owner
do we parse that stack and extract the location of the bottom frame.
This doesn't give us a location necessarily in the top of the function
but somewhere in the function.

In this case the first owned child is the Container:

<img width="1107" alt="Screenshot 2024-08-22 at 10 24 42 PM"
src="https://github.com/user-attachments/assets/95f32850-24a5-4151-8ce6-b7b89db68aee">
<img width="648" alt="Screenshot 2024-08-22 at 10 24 20 PM"
src="https://github.com/user-attachments/assets/4bcba033-866f-4684-9beb-de09d189deff">

We can even use this technique for Fibers too. Currently I use this as a
fallback in case the error technique didn't work. This covers a case
where nothing errors but you still render a child. This case is actually
quite common:

```
function Foo() {
  return <Bar />;
}
```

However, for Fibers we could really just use the `inspect(function)`
technique which works for all cases. At least in Chrome.

Unfortunately, this technique doesn't work if a Component doesn't create
any new JSX but just renders its children. It also currently doesn't
work if the child is filtered since I only look up the owner if an
instance is not filtered. This means that the container in the fixture
can't view source by default since the host component is filtered:

```
export default function Container({children}) {
  return <div>{children}</div>;
}
```

<img width="1107" alt="Screenshot 2024-08-22 at 10 24 35 PM"
src="https://github.com/user-attachments/assets/c3f8f9c5-5add-4d35-9290-3a5079e82adc">
2024-08-26 20:50:43 -04:00

219 lines
7.1 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
*/
// This is a DevTools fork of ReactFiberComponentStack.
// 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 {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {CurrentDispatcherRef, WorkTagMap} from '../types';
import type {ReactComponentInfo} from 'shared/ReactTypes';
import {
describeBuiltInComponentFrame,
describeFunctionComponentFrame,
describeClassComponentFrame,
describeDebugInfoFrame,
} from '../shared/DevToolsComponentStackFrame';
import {formatOwnerStack} from '../shared/DevToolsOwnerStack';
export function describeFiber(
workTagMap: WorkTagMap,
workInProgress: Fiber,
currentDispatcherRef: CurrentDispatcherRef,
): string {
const {
HostHoistable,
HostSingleton,
HostComponent,
LazyComponent,
SuspenseComponent,
SuspenseListComponent,
FunctionComponent,
IndeterminateComponent,
SimpleMemoComponent,
ForwardRef,
ClassComponent,
} = workTagMap;
switch (workInProgress.tag) {
case HostHoistable:
case HostSingleton:
case HostComponent:
return describeBuiltInComponentFrame(workInProgress.type);
case LazyComponent:
// TODO: When we support Thenables as component types we should rename this.
return describeBuiltInComponentFrame('Lazy');
case SuspenseComponent:
return describeBuiltInComponentFrame('Suspense');
case SuspenseListComponent:
return describeBuiltInComponentFrame('SuspenseList');
case FunctionComponent:
case IndeterminateComponent:
case SimpleMemoComponent:
return describeFunctionComponentFrame(
workInProgress.type,
currentDispatcherRef,
);
case ForwardRef:
return describeFunctionComponentFrame(
workInProgress.type.render,
currentDispatcherRef,
);
case ClassComponent:
return describeClassComponentFrame(
workInProgress.type,
currentDispatcherRef,
);
default:
return '';
}
}
export function getStackByFiberInDevAndProd(
workTagMap: WorkTagMap,
workInProgress: Fiber,
currentDispatcherRef: CurrentDispatcherRef,
): string {
try {
let info = '';
let node: Fiber = workInProgress;
do {
info += describeFiber(workTagMap, node, currentDispatcherRef);
// Add any Server Component stack frames in reverse order.
const debugInfo = node._debugInfo;
if (debugInfo) {
for (let i = debugInfo.length - 1; i >= 0; i--) {
const entry = debugInfo[i];
if (typeof entry.name === 'string') {
info += describeDebugInfoFrame(entry.name, entry.env);
}
}
}
// $FlowFixMe[incompatible-type] we bail out when we get a null
node = node.return;
} while (node);
return info;
} catch (x) {
return '\nError generating stack: ' + x.message + '\n' + x.stack;
}
}
export function getSourceLocationByFiber(
workTagMap: WorkTagMap,
fiber: Fiber,
currentDispatcherRef: CurrentDispatcherRef,
): null | string {
// This is like getStackByFiberInDevAndProd but just the first stack frame.
try {
const info = describeFiber(workTagMap, fiber, currentDispatcherRef);
if (info !== '') {
return info.slice(1); // skip the leading newline
}
} catch (x) {
console.error(x);
}
return null;
}
export function supportsConsoleTasks(fiber: Fiber): boolean {
// If this Fiber supports native console.createTask then we are already running
// inside a native async stack trace if it's active - meaning the DevTools is open.
// Ideally we'd detect if this task was created while the DevTools was open or not.
return !!fiber._debugTask;
}
export function supportsOwnerStacks(fiber: Fiber): boolean {
// If this Fiber supports owner stacks then it'll have the _debugStack field.
// It might be null but that still means we should use the owner stack logic.
return fiber._debugStack !== undefined;
}
export function getOwnerStackByFiberInDev(
workTagMap: WorkTagMap,
workInProgress: Fiber,
currentDispatcherRef: CurrentDispatcherRef,
): string {
const {
HostHoistable,
HostSingleton,
HostText,
HostComponent,
SuspenseComponent,
SuspenseListComponent,
} = workTagMap;
try {
let info = '';
if (workInProgress.tag === HostText) {
// Text nodes never have an owner/stack because they're not created through JSX.
// We use the parent since text nodes are always created through a host parent.
workInProgress = (workInProgress.return: any);
}
// The owner stack of the current fiber 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.
switch (workInProgress.tag) {
case HostHoistable:
case HostSingleton:
case HostComponent:
info += describeBuiltInComponentFrame(workInProgress.type);
break;
case SuspenseComponent:
info += describeBuiltInComponentFrame('Suspense');
break;
case SuspenseListComponent:
info += describeBuiltInComponentFrame('SuspenseList');
break;
}
let owner: void | null | Fiber | ReactComponentInfo = workInProgress;
while (owner) {
if (typeof owner.tag === 'number') {
const fiber: Fiber = (owner: any);
owner = fiber._debugOwner;
let debugStack = fiber._debugStack;
// 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 && debugStack) {
if (typeof debugStack !== 'string') {
debugStack = formatOwnerStack(debugStack);
}
if (debugStack !== '') {
info += '\n' + debugStack;
}
}
} else if (owner.debugStack != null) {
// Server Component
const ownerStack: Error = owner.debugStack;
owner = owner.owner;
if (owner && ownerStack) {
info += '\n' + formatOwnerStack(ownerStack);
}
} else {
break;
}
}
return info;
} catch (x) {
return '\nError generating stack: ' + x.message + '\n' + x.stack;
}
}