mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[DevTools] Add "suspended by" Section to Component Inspector Sidebar (#34012)
This collects the ReactAsyncInfo between instances. It associates it
with the parent. Typically this would be a Server Component's Promise
return value but it can also be Promises in a fragment. It can also be
associated with a client component when you pass a Promise into the
child position e.g. `<div>{promise}</div>` then it's associated with the
div. If an instance is filtered, then it gets associated with the parent
of that's unfiltered.
The stack trace currently isn't source mapped. I'll do that in a follow
up.
We also need to add a "short name" from the Promise for the description
(e.g. url). I'll also add a little marker showing the relative time span
of each entry.
<img width="447" height="591" alt="Screenshot 2025-07-26 at 7 56 00 PM"
src="https://github.com/user-attachments/assets/7c966540-7b1b-4568-8cb9-f25cefd5a918"
/>
<img width="446" height="570" alt="Screenshot 2025-07-26 at 7 55 23 PM"
src="https://github.com/user-attachments/assets/4eac235b-e735-41e8-9c6e-a7633af64e4b"
/>
This commit is contained in:
committed by
GitHub
parent
cc015840ef
commit
4a58b63865
@@ -7,7 +7,11 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactComponentInfo, ReactDebugInfo} from 'shared/ReactTypes';
|
||||
import type {
|
||||
ReactComponentInfo,
|
||||
ReactDebugInfo,
|
||||
ReactAsyncInfo,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
import {
|
||||
ComponentFilterDisplayName,
|
||||
@@ -135,6 +139,7 @@ import type {
|
||||
ReactRenderer,
|
||||
RendererInterface,
|
||||
SerializedElement,
|
||||
SerializedAsyncInfo,
|
||||
WorkTagMap,
|
||||
CurrentDispatcherRef,
|
||||
LegacyDispatcherRef,
|
||||
@@ -165,6 +170,7 @@ type FiberInstance = {
|
||||
source: null | string | Error | ReactFunctionLocation, // source location of this component function, or owned child stack
|
||||
logCount: number, // total number of errors/warnings last seen
|
||||
treeBaseDuration: number, // the profiled time of the last render of this subtree
|
||||
suspendedBy: null | Array<ReactAsyncInfo>, // things that suspended in the children position of this component
|
||||
data: Fiber, // one of a Fiber pair
|
||||
};
|
||||
|
||||
@@ -178,6 +184,7 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
|
||||
source: null,
|
||||
logCount: 0,
|
||||
treeBaseDuration: 0,
|
||||
suspendedBy: null,
|
||||
data: fiber,
|
||||
};
|
||||
}
|
||||
@@ -193,6 +200,7 @@ type FilteredFiberInstance = {
|
||||
source: null | string | Error | ReactFunctionLocation, // always null here.
|
||||
logCount: number, // total number of errors/warnings last seen
|
||||
treeBaseDuration: number, // the profiled time of the last render of this subtree
|
||||
suspendedBy: null | Array<ReactAsyncInfo>, // not used
|
||||
data: Fiber, // one of a Fiber pair
|
||||
};
|
||||
|
||||
@@ -207,6 +215,7 @@ function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {
|
||||
source: null,
|
||||
logCount: 0,
|
||||
treeBaseDuration: 0,
|
||||
suspendedBy: null,
|
||||
data: fiber,
|
||||
}: any);
|
||||
}
|
||||
@@ -225,6 +234,7 @@ type VirtualInstance = {
|
||||
source: null | string | Error | ReactFunctionLocation, // source location of this server component, or owned child stack
|
||||
logCount: number, // total number of errors/warnings last seen
|
||||
treeBaseDuration: number, // the profiled time of the last render of this subtree
|
||||
suspendedBy: null | Array<ReactAsyncInfo>, // things that blocked the server component's child from rendering
|
||||
// The latest info for this instance. This can be updated over time and the
|
||||
// same info can appear in more than once ServerComponentInstance.
|
||||
data: ReactComponentInfo,
|
||||
@@ -242,6 +252,7 @@ function createVirtualInstance(
|
||||
source: null,
|
||||
logCount: 0,
|
||||
treeBaseDuration: 0,
|
||||
suspendedBy: null,
|
||||
data: debugEntry,
|
||||
};
|
||||
}
|
||||
@@ -2354,6 +2365,21 @@ export function attach(
|
||||
// the current parent here as well.
|
||||
let reconcilingParent: null | DevToolsInstance = null;
|
||||
|
||||
function insertSuspendedBy(asyncInfo: ReactAsyncInfo): void {
|
||||
const parentInstance = reconcilingParent;
|
||||
if (parentInstance === null) {
|
||||
// Suspending at the root is not attributed to any particular component
|
||||
// TODO: It should be attributed to the shell.
|
||||
return;
|
||||
}
|
||||
const suspendedBy = parentInstance.suspendedBy;
|
||||
if (suspendedBy === null) {
|
||||
parentInstance.suspendedBy = [asyncInfo];
|
||||
} else if (suspendedBy.indexOf(asyncInfo) === -1) {
|
||||
suspendedBy.push(asyncInfo);
|
||||
}
|
||||
}
|
||||
|
||||
function insertChild(instance: DevToolsInstance): void {
|
||||
const parentInstance = reconcilingParent;
|
||||
if (parentInstance === null) {
|
||||
@@ -2515,6 +2541,17 @@ export function attach(
|
||||
if (fiber._debugInfo) {
|
||||
for (let i = 0; i < fiber._debugInfo.length; i++) {
|
||||
const debugEntry = fiber._debugInfo[i];
|
||||
if (debugEntry.awaited) {
|
||||
// Async Info
|
||||
const asyncInfo: ReactAsyncInfo = (debugEntry: any);
|
||||
if (level === virtualLevel) {
|
||||
// Track any async info between the previous virtual instance up until to this
|
||||
// instance and add it to the parent. This can add the same set multiple times
|
||||
// so we assume insertSuspendedBy dedupes.
|
||||
insertSuspendedBy(asyncInfo);
|
||||
}
|
||||
if (previousVirtualInstance) continue;
|
||||
}
|
||||
if (typeof debugEntry.name !== 'string') {
|
||||
// Not a Component. Some other Debug Info.
|
||||
continue;
|
||||
@@ -2768,6 +2805,7 @@ export function attach(
|
||||
// Move all the children of this instance to the remaining set.
|
||||
remainingReconcilingChildren = instance.firstChild;
|
||||
instance.firstChild = null;
|
||||
instance.suspendedBy = null;
|
||||
try {
|
||||
// Unmount the remaining set.
|
||||
unmountRemainingChildren();
|
||||
@@ -2968,6 +3006,7 @@ export function attach(
|
||||
// We'll move them back one by one, and anything that remains is deleted.
|
||||
remainingReconcilingChildren = virtualInstance.firstChild;
|
||||
virtualInstance.firstChild = null;
|
||||
virtualInstance.suspendedBy = null;
|
||||
try {
|
||||
if (
|
||||
updateVirtualChildrenRecursively(
|
||||
@@ -3019,6 +3058,17 @@ export function attach(
|
||||
if (nextChild._debugInfo) {
|
||||
for (let i = 0; i < nextChild._debugInfo.length; i++) {
|
||||
const debugEntry = nextChild._debugInfo[i];
|
||||
if (debugEntry.awaited) {
|
||||
// Async Info
|
||||
const asyncInfo: ReactAsyncInfo = (debugEntry: any);
|
||||
if (level === virtualLevel) {
|
||||
// Track any async info between the previous virtual instance up until to this
|
||||
// instance and add it to the parent. This can add the same set multiple times
|
||||
// so we assume insertSuspendedBy dedupes.
|
||||
insertSuspendedBy(asyncInfo);
|
||||
}
|
||||
if (previousVirtualInstance) continue;
|
||||
}
|
||||
if (typeof debugEntry.name !== 'string') {
|
||||
// Not a Component. Some other Debug Info.
|
||||
continue;
|
||||
@@ -3343,6 +3393,7 @@ export function attach(
|
||||
// We'll move them back one by one, and anything that remains is deleted.
|
||||
remainingReconcilingChildren = fiberInstance.firstChild;
|
||||
fiberInstance.firstChild = null;
|
||||
fiberInstance.suspendedBy = null;
|
||||
}
|
||||
try {
|
||||
if (
|
||||
@@ -4051,6 +4102,42 @@ export function attach(
|
||||
return null;
|
||||
}
|
||||
|
||||
function serializeAsyncInfo(
|
||||
asyncInfo: ReactAsyncInfo,
|
||||
index: number,
|
||||
parentInstance: DevToolsInstance,
|
||||
): SerializedAsyncInfo {
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
const ioOwnerInstance = findNearestOwnerInstance(
|
||||
parentInstance,
|
||||
ioInfo.owner,
|
||||
);
|
||||
const awaitOwnerInstance = findNearestOwnerInstance(
|
||||
parentInstance,
|
||||
asyncInfo.owner,
|
||||
);
|
||||
return {
|
||||
awaited: {
|
||||
name: ioInfo.name,
|
||||
start: ioInfo.start,
|
||||
end: ioInfo.end,
|
||||
value: ioInfo.value == null ? null : ioInfo.value,
|
||||
env: ioInfo.env == null ? null : ioInfo.env,
|
||||
owner:
|
||||
ioOwnerInstance === null
|
||||
? null
|
||||
: instanceToSerializedElement(ioOwnerInstance),
|
||||
stack: ioInfo.stack == null ? null : ioInfo.stack,
|
||||
},
|
||||
env: asyncInfo.env == null ? null : asyncInfo.env,
|
||||
owner:
|
||||
awaitOwnerInstance === null
|
||||
? null
|
||||
: instanceToSerializedElement(awaitOwnerInstance),
|
||||
stack: asyncInfo.stack == null ? null : asyncInfo.stack,
|
||||
};
|
||||
}
|
||||
|
||||
// Fast path props lookup for React Native style editor.
|
||||
// Could use inspectElementRaw() but that would require shallow rendering hooks components,
|
||||
// and could also mess with memoization.
|
||||
@@ -4342,6 +4429,13 @@ export function attach(
|
||||
nativeTag = getNativeTag(fiber.stateNode);
|
||||
}
|
||||
|
||||
// This set is an edge case where if you pass a promise to a Client Component into a children
|
||||
// position without a Server Component as the direct parent. E.g. <div>{promise}</div>
|
||||
// In this case, this becomes associated with the Client/Host Component where as normally
|
||||
// you'd expect these to be associated with the Server Component that awaited the data.
|
||||
// TODO: Prepend other suspense sources like css, images and use().
|
||||
const suspendedBy = fiberInstance.suspendedBy;
|
||||
|
||||
return {
|
||||
id: fiberInstance.id,
|
||||
|
||||
@@ -4398,6 +4492,13 @@ export function attach(
|
||||
? []
|
||||
: Array.from(componentLogsEntry.warnings.entries()),
|
||||
|
||||
suspendedBy:
|
||||
suspendedBy === null
|
||||
? []
|
||||
: suspendedBy.map((info, index) =>
|
||||
serializeAsyncInfo(info, index, fiberInstance),
|
||||
),
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
|
||||
@@ -4451,6 +4552,9 @@ export function attach(
|
||||
const componentLogsEntry =
|
||||
componentInfoToComponentLogsMap.get(componentInfo);
|
||||
|
||||
// Things that Suspended this Server Component (use(), awaits and direct child promises)
|
||||
const suspendedBy = virtualInstance.suspendedBy;
|
||||
|
||||
return {
|
||||
id: virtualInstance.id,
|
||||
|
||||
@@ -4490,6 +4594,14 @@ export function attach(
|
||||
componentLogsEntry === undefined
|
||||
? []
|
||||
: Array.from(componentLogsEntry.warnings.entries()),
|
||||
|
||||
suspendedBy:
|
||||
suspendedBy === null
|
||||
? []
|
||||
: suspendedBy.map((info, index) =>
|
||||
serializeAsyncInfo(info, index, virtualInstance),
|
||||
),
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
|
||||
@@ -4534,7 +4646,7 @@ export function attach(
|
||||
|
||||
function createIsPathAllowed(
|
||||
key: string | null,
|
||||
secondaryCategory: 'hooks' | null,
|
||||
secondaryCategory: 'suspendedBy' | 'hooks' | null,
|
||||
) {
|
||||
// This function helps prevent previously-inspected paths from being dehydrated in updates.
|
||||
// This is important to avoid a bad user experience where expanded toggles collapse on update.
|
||||
@@ -4566,6 +4678,13 @@ export function attach(
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case 'suspendedBy':
|
||||
if (path.length < 5) {
|
||||
// Never dehydrate anything above suspendedBy[index].awaited.value
|
||||
// Those are part of the internal meta data. We only dehydrate inside the Promise.
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -4789,36 +4908,42 @@ export function attach(
|
||||
type: 'not-found',
|
||||
};
|
||||
}
|
||||
const inspectedElement = mostRecentlyInspectedElement;
|
||||
|
||||
// Any time an inspected element has an update,
|
||||
// we should update the selected $r value as wel.
|
||||
// Do this before dehydration (cleanForBridge).
|
||||
updateSelectedElement(mostRecentlyInspectedElement);
|
||||
updateSelectedElement(inspectedElement);
|
||||
|
||||
// Clone before cleaning so that we preserve the full data.
|
||||
// This will enable us to send patches without re-inspecting if hydrated paths are requested.
|
||||
// (Reducing how often we shallow-render is a better DX for function components that use hooks.)
|
||||
const cleanedInspectedElement = {...mostRecentlyInspectedElement};
|
||||
const cleanedInspectedElement = {...inspectedElement};
|
||||
// $FlowFixMe[prop-missing] found when upgrading Flow
|
||||
cleanedInspectedElement.context = cleanForBridge(
|
||||
cleanedInspectedElement.context,
|
||||
inspectedElement.context,
|
||||
createIsPathAllowed('context', null),
|
||||
);
|
||||
// $FlowFixMe[prop-missing] found when upgrading Flow
|
||||
cleanedInspectedElement.hooks = cleanForBridge(
|
||||
cleanedInspectedElement.hooks,
|
||||
inspectedElement.hooks,
|
||||
createIsPathAllowed('hooks', 'hooks'),
|
||||
);
|
||||
// $FlowFixMe[prop-missing] found when upgrading Flow
|
||||
cleanedInspectedElement.props = cleanForBridge(
|
||||
cleanedInspectedElement.props,
|
||||
inspectedElement.props,
|
||||
createIsPathAllowed('props', null),
|
||||
);
|
||||
// $FlowFixMe[prop-missing] found when upgrading Flow
|
||||
cleanedInspectedElement.state = cleanForBridge(
|
||||
cleanedInspectedElement.state,
|
||||
inspectedElement.state,
|
||||
createIsPathAllowed('state', null),
|
||||
);
|
||||
// $FlowFixMe[prop-missing] found when upgrading Flow
|
||||
cleanedInspectedElement.suspendedBy = cleanForBridge(
|
||||
inspectedElement.suspendedBy,
|
||||
createIsPathAllowed('suspendedBy', 'suspendedBy'),
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
|
||||
@@ -755,6 +755,10 @@ export function attach(
|
||||
inspectedElement.state,
|
||||
createIsPathAllowed('state'),
|
||||
);
|
||||
inspectedElement.suspendedBy = cleanForBridge(
|
||||
inspectedElement.suspendedBy,
|
||||
createIsPathAllowed('suspendedBy'),
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
@@ -847,6 +851,9 @@ export function attach(
|
||||
errors,
|
||||
warnings,
|
||||
|
||||
// Not supported in legacy renderers.
|
||||
suspendedBy: [],
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ import type {
|
||||
import type {InitBackend} from 'react-devtools-shared/src/backend';
|
||||
import type {TimelineDataExport} from 'react-devtools-timeline/src/types';
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
||||
import type {ReactFunctionLocation, ReactStackTrace} from 'shared/ReactTypes';
|
||||
import type Agent from './agent';
|
||||
|
||||
type BundleType =
|
||||
@@ -232,6 +232,25 @@ export type PathMatch = {
|
||||
isFullMatch: boolean,
|
||||
};
|
||||
|
||||
// Serialized version of ReactIOInfo
|
||||
export type SerializedIOInfo = {
|
||||
name: string,
|
||||
start: number,
|
||||
end: number,
|
||||
value: null | Promise<mixed>,
|
||||
env: null | string,
|
||||
owner: null | SerializedElement,
|
||||
stack: null | ReactStackTrace,
|
||||
};
|
||||
|
||||
// Serialized version of ReactAsyncInfo
|
||||
export type SerializedAsyncInfo = {
|
||||
awaited: SerializedIOInfo,
|
||||
env: null | string,
|
||||
owner: null | SerializedElement,
|
||||
stack: null | ReactStackTrace,
|
||||
};
|
||||
|
||||
export type SerializedElement = {
|
||||
displayName: string | null,
|
||||
id: number,
|
||||
@@ -268,14 +287,17 @@ export type InspectedElement = {
|
||||
hasLegacyContext: boolean,
|
||||
|
||||
// Inspectable properties.
|
||||
context: Object | null,
|
||||
hooks: Object | null,
|
||||
props: Object | null,
|
||||
state: Object | null,
|
||||
context: Object | null, // DehydratedData or {[string]: mixed}
|
||||
hooks: Object | null, // DehydratedData or {[string]: mixed}
|
||||
props: Object | null, // DehydratedData or {[string]: mixed}
|
||||
state: Object | null, // DehydratedData or {[string]: mixed}
|
||||
key: number | string | null,
|
||||
errors: Array<[string, number]>,
|
||||
warnings: Array<[string, number]>,
|
||||
|
||||
// Things that suspended this Instances
|
||||
suspendedBy: Object, // DehydratedData or Array<SerializedAsyncInfo>
|
||||
|
||||
// List of owners
|
||||
owners: Array<SerializedElement> | null,
|
||||
source: ReactFunctionLocation | null,
|
||||
|
||||
36
packages/react-devtools-shared/src/backendAPI.js
vendored
36
packages/react-devtools-shared/src/backendAPI.js
vendored
@@ -16,6 +16,7 @@ import ElementPollingCancellationError from 'react-devtools-shared/src/errors/El
|
||||
import type {
|
||||
InspectedElement as InspectedElementBackend,
|
||||
InspectedElementPayload,
|
||||
SerializedAsyncInfo as SerializedAsyncInfoBackend,
|
||||
} from 'react-devtools-shared/src/backend/types';
|
||||
import type {
|
||||
BackendEvents,
|
||||
@@ -24,6 +25,7 @@ import type {
|
||||
import type {
|
||||
DehydratedData,
|
||||
InspectedElement as InspectedElementFrontend,
|
||||
SerializedAsyncInfo as SerializedAsyncInfoFrontend,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {InspectedElementPath} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
@@ -209,6 +211,32 @@ export function cloneInspectedElementWithPath(
|
||||
return clonedInspectedElement;
|
||||
}
|
||||
|
||||
function backendToFrontendSerializedAsyncInfo(
|
||||
asyncInfo: SerializedAsyncInfoBackend,
|
||||
): SerializedAsyncInfoFrontend {
|
||||
const ioInfo = asyncInfo.awaited;
|
||||
return {
|
||||
awaited: {
|
||||
name: ioInfo.name,
|
||||
start: ioInfo.start,
|
||||
end: ioInfo.end,
|
||||
value: ioInfo.value,
|
||||
env: ioInfo.env,
|
||||
owner:
|
||||
ioInfo.owner === null
|
||||
? null
|
||||
: backendToFrontendSerializedElementMapper(ioInfo.owner),
|
||||
stack: ioInfo.stack,
|
||||
},
|
||||
env: asyncInfo.env,
|
||||
owner:
|
||||
asyncInfo.owner === null
|
||||
? null
|
||||
: backendToFrontendSerializedElementMapper(asyncInfo.owner),
|
||||
stack: asyncInfo.stack,
|
||||
};
|
||||
}
|
||||
|
||||
export function convertInspectedElementBackendToFrontend(
|
||||
inspectedElementBackend: InspectedElementBackend,
|
||||
): InspectedElementFrontend {
|
||||
@@ -238,9 +266,13 @@ export function convertInspectedElementBackendToFrontend(
|
||||
key,
|
||||
errors,
|
||||
warnings,
|
||||
suspendedBy,
|
||||
nativeTag,
|
||||
} = inspectedElementBackend;
|
||||
|
||||
const hydratedSuspendedBy: null | Array<SerializedAsyncInfoBackend> =
|
||||
hydrateHelper(suspendedBy);
|
||||
|
||||
const inspectedElement: InspectedElementFrontend = {
|
||||
canEditFunctionProps,
|
||||
canEditFunctionPropsDeletePaths,
|
||||
@@ -272,6 +304,10 @@ export function convertInspectedElementBackendToFrontend(
|
||||
state: hydrateHelper(state),
|
||||
errors,
|
||||
warnings,
|
||||
suspendedBy:
|
||||
hydratedSuspendedBy == null // backwards compat
|
||||
? []
|
||||
: hydratedSuspendedBy.map(backendToFrontendSerializedAsyncInfo),
|
||||
nativeTag,
|
||||
};
|
||||
|
||||
|
||||
@@ -51,3 +51,41 @@
|
||||
.EditableValue {
|
||||
min-width: 1rem;
|
||||
}
|
||||
|
||||
.CollapsableRow {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.CollapsableRow:last-child {
|
||||
margin-bottom: -0.25rem;
|
||||
}
|
||||
|
||||
.CollapsableHeader {
|
||||
width: 100%;
|
||||
padding: 0.25rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.CollapsableHeaderIcon {
|
||||
flex: 0 0 1rem;
|
||||
margin-left: -0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
padding: 0;
|
||||
color: var(--color-expand-collapse-toggle);
|
||||
}
|
||||
|
||||
.CollapsableHeaderTitle {
|
||||
flex: 1 1 auto;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.CollapsableContent {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.PreviewContainer {
|
||||
padding: 0 0.25rem 0.25rem 0.25rem;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import {copy} from 'clipboard-js';
|
||||
import {toNormalUrl} from 'jsc-safe-url';
|
||||
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
@@ -21,6 +20,8 @@ import useOpenResource from '../useOpenResource';
|
||||
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
||||
import styles from './InspectedElementSourcePanel.css';
|
||||
|
||||
import formatLocationForDisplay from './formatLocationForDisplay';
|
||||
|
||||
type Props = {
|
||||
source: ReactFunctionLocation,
|
||||
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
|
||||
@@ -95,52 +96,21 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
|
||||
symbolicatedSource,
|
||||
);
|
||||
|
||||
const [, sourceURL, line] =
|
||||
const [, sourceURL, line, column] =
|
||||
symbolicatedSource == null ? source : symbolicatedSource;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.SourceOneLiner}
|
||||
data-testname="InspectedElementView-FormattedSourceString">
|
||||
{linkIsEnabled ? (
|
||||
<span className={styles.Link} onClick={viewSource}>
|
||||
{formatSourceForDisplay(sourceURL, line)}
|
||||
</span>
|
||||
) : (
|
||||
formatSourceForDisplay(sourceURL, line)
|
||||
)}
|
||||
<span
|
||||
className={linkIsEnabled ? styles.Link : null}
|
||||
title={sourceURL + ':' + line}
|
||||
onClick={viewSource}>
|
||||
{formatLocationForDisplay(sourceURL, line, column)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// This function is based on describeComponentFrame() in packages/shared/ReactComponentStackFrame
|
||||
function formatSourceForDisplay(sourceURL: string, line: number) {
|
||||
// Metro can return JSC-safe URLs, which have `//&` as a delimiter
|
||||
// https://www.npmjs.com/package/jsc-safe-url
|
||||
const sanitizedSourceURL = sourceURL.includes('//&')
|
||||
? toNormalUrl(sourceURL)
|
||||
: sourceURL;
|
||||
|
||||
// Note: this RegExp doesn't work well with URLs from Metro,
|
||||
// which provides bundle URL with query parameters prefixed with /&
|
||||
const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
|
||||
|
||||
let nameOnly = sanitizedSourceURL.replace(BEFORE_SLASH_RE, '');
|
||||
|
||||
// In DEV, include code for a common special case:
|
||||
// prefer "folder/index.js" instead of just "index.js".
|
||||
if (/^index\./.test(nameOnly)) {
|
||||
const match = sanitizedSourceURL.match(BEFORE_SLASH_RE);
|
||||
if (match) {
|
||||
const pathBeforeSlash = match[1];
|
||||
if (pathBeforeSlash) {
|
||||
const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
|
||||
nameOnly = folderName + '/' + nameOnly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${nameOnly}:${line}`;
|
||||
}
|
||||
|
||||
export default InspectedElementSourcePanel;
|
||||
|
||||
153
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
vendored
Normal file
153
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js
vendored
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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 {copy} from 'clipboard-js';
|
||||
import * as React from 'react';
|
||||
import {useState} from 'react';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import KeyValue from './KeyValue';
|
||||
import {serializeDataForCopy} from '../utils';
|
||||
import Store from '../../store';
|
||||
import styles from './InspectedElementSharedStyles.css';
|
||||
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
|
||||
import StackTraceView from './StackTraceView';
|
||||
import OwnerView from './OwnerView';
|
||||
|
||||
import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {SerializedAsyncInfo} from 'react-devtools-shared/src/frontend/types';
|
||||
|
||||
type RowProps = {
|
||||
bridge: FrontendBridge,
|
||||
element: Element,
|
||||
inspectedElement: InspectedElement,
|
||||
store: Store,
|
||||
asyncInfo: SerializedAsyncInfo,
|
||||
index: number,
|
||||
};
|
||||
|
||||
function SuspendedByRow({
|
||||
bridge,
|
||||
element,
|
||||
inspectedElement,
|
||||
store,
|
||||
asyncInfo,
|
||||
index,
|
||||
}: RowProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const name = asyncInfo.awaited.name;
|
||||
let stack;
|
||||
let owner;
|
||||
if (asyncInfo.stack === null || asyncInfo.stack.length === 0) {
|
||||
stack = asyncInfo.awaited.stack;
|
||||
owner = asyncInfo.awaited.owner;
|
||||
} else {
|
||||
stack = asyncInfo.stack;
|
||||
owner = asyncInfo.owner;
|
||||
}
|
||||
return (
|
||||
<div className={styles.CollapsableRow}>
|
||||
<Button
|
||||
className={styles.CollapsableHeader}
|
||||
onClick={() => setIsOpen(prevIsOpen => !prevIsOpen)}
|
||||
title={`${isOpen ? 'Collapse' : 'Expand'}`}>
|
||||
<ButtonIcon
|
||||
className={styles.CollapsableHeaderIcon}
|
||||
type={isOpen ? 'expanded' : 'collapsed'}
|
||||
/>
|
||||
<span className={styles.CollapsableHeaderTitle}>{name}</span>
|
||||
</Button>
|
||||
{isOpen && (
|
||||
<div className={styles.CollapsableContent}>
|
||||
<div className={styles.PreviewContainer}>
|
||||
<KeyValue
|
||||
alphaSort={true}
|
||||
bridge={bridge}
|
||||
canDeletePaths={false}
|
||||
canEditValues={false}
|
||||
canRenamePaths={false}
|
||||
depth={1}
|
||||
element={element}
|
||||
hidden={false}
|
||||
inspectedElement={inspectedElement}
|
||||
name={'Promise'}
|
||||
path={[index, 'awaited', 'value']}
|
||||
pathRoot="suspendedBy"
|
||||
store={store}
|
||||
value={asyncInfo.awaited.value}
|
||||
/>
|
||||
</div>
|
||||
{stack !== null && stack.length > 0 && (
|
||||
<StackTraceView stack={stack} />
|
||||
)}
|
||||
{owner !== null && owner.id !== inspectedElement.id ? (
|
||||
<OwnerView
|
||||
key={owner.id}
|
||||
displayName={owner.displayName || 'Anonymous'}
|
||||
hocDisplayNames={owner.hocDisplayNames}
|
||||
compiledWithForget={owner.compiledWithForget}
|
||||
id={owner.id}
|
||||
isInStore={store.containsElement(owner.id)}
|
||||
type={owner.type}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
bridge: FrontendBridge,
|
||||
element: Element,
|
||||
inspectedElement: InspectedElement,
|
||||
store: Store,
|
||||
};
|
||||
|
||||
export default function InspectedElementSuspendedBy({
|
||||
bridge,
|
||||
element,
|
||||
inspectedElement,
|
||||
store,
|
||||
}: Props): React.Node {
|
||||
const {suspendedBy} = inspectedElement;
|
||||
|
||||
// Skip the section if nothing suspended this component.
|
||||
if (suspendedBy == null || suspendedBy.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleCopy = withPermissionsCheck(
|
||||
{permissions: ['clipboardWrite']},
|
||||
() => copy(serializeDataForCopy(suspendedBy)),
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.HeaderRow}>
|
||||
<div className={styles.Header}>suspended by</div>
|
||||
<Button onClick={handleCopy} title="Copy to clipboard">
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
</div>
|
||||
{suspendedBy.map((asyncInfo, index) => (
|
||||
<SuspendedByRow
|
||||
key={index}
|
||||
index={index}
|
||||
asyncInfo={asyncInfo}
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,16 +2,6 @@
|
||||
font-family: var(--font-family-sans);
|
||||
}
|
||||
|
||||
.Owner {
|
||||
color: var(--color-component-name);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.InspectedElement {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
@@ -28,41 +18,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.Owner {
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
.Owner:focus {
|
||||
outline: none;
|
||||
background-color: var(--color-button-background-focus);
|
||||
}
|
||||
|
||||
.NotInStore {
|
||||
color: var(--color-dim);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.OwnerButton {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.OwnerContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
width: 100%;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.OwnerContent:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.OwnersMetaField {
|
||||
padding-left: 1.25rem;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Fragment, useCallback, useContext} from 'react';
|
||||
import {TreeDispatcherContext} from './TreeContext';
|
||||
import {Fragment, useContext} from 'react';
|
||||
import {BridgeContext, StoreContext} from '../context';
|
||||
import Button from '../Button';
|
||||
import InspectedElementBadges from './InspectedElementBadges';
|
||||
import InspectedElementContextTree from './InspectedElementContextTree';
|
||||
import InspectedElementErrorsAndWarningsTree from './InspectedElementErrorsAndWarningsTree';
|
||||
@@ -20,12 +18,11 @@ import InspectedElementPropsTree from './InspectedElementPropsTree';
|
||||
import InspectedElementStateTree from './InspectedElementStateTree';
|
||||
import InspectedElementStyleXPlugin from './InspectedElementStyleXPlugin';
|
||||
import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
|
||||
import InspectedElementSuspendedBy from './InspectedElementSuspendedBy';
|
||||
import NativeStyleEditor from './NativeStyleEditor';
|
||||
import ElementBadges from './ElementBadges';
|
||||
import {useHighlightHostInstance} from '../hooks';
|
||||
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
|
||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||
import InspectedElementSourcePanel from './InspectedElementSourcePanel';
|
||||
import OwnerView from './OwnerView';
|
||||
|
||||
import styles from './InspectedElementView.css';
|
||||
|
||||
@@ -156,6 +153,15 @@ export default function InspectedElementView({
|
||||
<NativeStyleEditor />
|
||||
</div>
|
||||
|
||||
<div className={styles.InspectedElementSection}>
|
||||
<InspectedElementSuspendedBy
|
||||
bridge={bridge}
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
store={store}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showRenderedBy && (
|
||||
<div
|
||||
className={styles.InspectedElementSection}
|
||||
@@ -196,57 +202,3 @@ export default function InspectedElementView({
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
type OwnerViewProps = {
|
||||
displayName: string,
|
||||
hocDisplayNames: Array<string> | null,
|
||||
compiledWithForget: boolean,
|
||||
id: number,
|
||||
isInStore: boolean,
|
||||
};
|
||||
|
||||
function OwnerView({
|
||||
displayName,
|
||||
hocDisplayNames,
|
||||
compiledWithForget,
|
||||
id,
|
||||
isInStore,
|
||||
}: OwnerViewProps) {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {highlightHostInstance, clearHighlightHostInstance} =
|
||||
useHighlightHostInstance();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
logEvent({
|
||||
event_name: 'select-element',
|
||||
metadata: {source: 'owner-view'},
|
||||
});
|
||||
dispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: id,
|
||||
});
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={id}
|
||||
className={styles.OwnerButton}
|
||||
disabled={!isInStore}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => highlightHostInstance(id)}
|
||||
onMouseLeave={clearHighlightHostInstance}>
|
||||
<span className={styles.OwnerContent}>
|
||||
<span
|
||||
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
|
||||
title={displayName}>
|
||||
{displayName}
|
||||
</span>
|
||||
|
||||
<ElementBadges
|
||||
hocDisplayNames={hocDisplayNames}
|
||||
compiledWithForget={compiledWithForget}
|
||||
/>
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
.Owner {
|
||||
color: var(--color-component-name);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.125rem 0.25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
.Owner:focus {
|
||||
outline: none;
|
||||
background-color: var(--color-button-background-focus);
|
||||
}
|
||||
|
||||
.OwnerButton {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.OwnerContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
width: 100%;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.OwnerContent:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.NotInStore {
|
||||
color: var(--color-dim);
|
||||
cursor: default;
|
||||
}
|
||||
72
packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js
vendored
Normal file
72
packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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 * as React from 'react';
|
||||
import {useCallback, useContext} from 'react';
|
||||
import {TreeDispatcherContext} from './TreeContext';
|
||||
import Button from '../Button';
|
||||
import ElementBadges from './ElementBadges';
|
||||
import {useHighlightHostInstance} from '../hooks';
|
||||
import {logEvent} from 'react-devtools-shared/src/Logger';
|
||||
|
||||
import styles from './OwnerView.css';
|
||||
|
||||
type OwnerViewProps = {
|
||||
displayName: string,
|
||||
hocDisplayNames: Array<string> | null,
|
||||
compiledWithForget: boolean,
|
||||
id: number,
|
||||
isInStore: boolean,
|
||||
};
|
||||
|
||||
export default function OwnerView({
|
||||
displayName,
|
||||
hocDisplayNames,
|
||||
compiledWithForget,
|
||||
id,
|
||||
isInStore,
|
||||
}: OwnerViewProps): React.Node {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {highlightHostInstance, clearHighlightHostInstance} =
|
||||
useHighlightHostInstance();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
logEvent({
|
||||
event_name: 'select-element',
|
||||
metadata: {source: 'owner-view'},
|
||||
});
|
||||
dispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: id,
|
||||
});
|
||||
}, [dispatch, id]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={id}
|
||||
className={styles.OwnerButton}
|
||||
disabled={!isInStore}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => highlightHostInstance(id)}
|
||||
onMouseLeave={clearHighlightHostInstance}>
|
||||
<span className={styles.OwnerContent}>
|
||||
<span
|
||||
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
|
||||
title={displayName}>
|
||||
{displayName}
|
||||
</span>
|
||||
|
||||
<ElementBadges
|
||||
hocDisplayNames={hocDisplayNames}
|
||||
compiledWithForget={compiledWithForget}
|
||||
/>
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
.StackTraceView {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.CallSite {
|
||||
display: block;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.Link {
|
||||
color: var(--color-link);
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
border-radius: 0.125rem;
|
||||
padding: 0px 2px;
|
||||
}
|
||||
|
||||
.Link:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
58
packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js
vendored
Normal file
58
packages/react-devtools-shared/src/devtools/views/Components/StackTraceView.js
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 * as React from 'react';
|
||||
|
||||
import useOpenResource from '../useOpenResource';
|
||||
|
||||
import styles from './StackTraceView.css';
|
||||
|
||||
import type {ReactStackTrace, ReactCallSite} from 'shared/ReactTypes';
|
||||
|
||||
import formatLocationForDisplay from './formatLocationForDisplay';
|
||||
|
||||
type CallSiteViewProps = {
|
||||
callSite: ReactCallSite,
|
||||
};
|
||||
|
||||
export function CallSiteView({callSite}: CallSiteViewProps): React.Node {
|
||||
const symbolicatedCallSite: null | ReactCallSite = null; // TODO
|
||||
const [linkIsEnabled, viewSource] = useOpenResource(
|
||||
callSite,
|
||||
symbolicatedCallSite,
|
||||
);
|
||||
const [functionName, url, line, column] =
|
||||
symbolicatedCallSite !== null ? symbolicatedCallSite : callSite;
|
||||
return (
|
||||
<div className={styles.CallSite}>
|
||||
{functionName}
|
||||
{' @ '}
|
||||
<span
|
||||
className={linkIsEnabled ? styles.Link : null}
|
||||
onClick={viewSource}
|
||||
title={url + ':' + line}>
|
||||
{formatLocationForDisplay(url, line, column)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
stack: ReactStackTrace,
|
||||
};
|
||||
|
||||
export default function StackTraceView({stack}: Props): React.Node {
|
||||
return (
|
||||
<div className={styles.StackTraceView}>
|
||||
{stack.map((callSite, index) => (
|
||||
<CallSiteView key={index} callSite={callSite} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
packages/react-devtools-shared/src/devtools/views/Components/formatLocationForDisplay.js
vendored
Normal file
44
packages/react-devtools-shared/src/devtools/views/Components/formatLocationForDisplay.js
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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 {toNormalUrl} from 'jsc-safe-url';
|
||||
|
||||
// This function is based on describeComponentFrame() in packages/shared/ReactComponentStackFrame
|
||||
export default function formatLocationForDisplay(
|
||||
sourceURL: string,
|
||||
line: number,
|
||||
column: number,
|
||||
): string {
|
||||
// Metro can return JSC-safe URLs, which have `//&` as a delimiter
|
||||
// https://www.npmjs.com/package/jsc-safe-url
|
||||
const sanitizedSourceURL = sourceURL.includes('//&')
|
||||
? toNormalUrl(sourceURL)
|
||||
: sourceURL;
|
||||
|
||||
// Note: this RegExp doesn't work well with URLs from Metro,
|
||||
// which provides bundle URL with query parameters prefixed with /&
|
||||
const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
|
||||
|
||||
let nameOnly = sanitizedSourceURL.replace(BEFORE_SLASH_RE, '');
|
||||
|
||||
// In DEV, include code for a common special case:
|
||||
// prefer "folder/index.js" instead of just "index.js".
|
||||
if (/^index\./.test(nameOnly)) {
|
||||
const match = sanitizedSourceURL.match(BEFORE_SLASH_RE);
|
||||
if (match) {
|
||||
const pathBeforeSlash = match[1];
|
||||
if (pathBeforeSlash) {
|
||||
const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
|
||||
nameOnly = folderName + '/' + nameOnly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${nameOnly}:${line}`;
|
||||
}
|
||||
@@ -121,7 +121,7 @@ function sanitize(data: Object): void {
|
||||
}
|
||||
|
||||
export function serializeDataForCopy(props: Object): string {
|
||||
const cloned = Object.assign({}, props);
|
||||
const cloned = isArray(props) ? props.slice(0) : Object.assign({}, props);
|
||||
|
||||
sanitize(cloned);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
Dehydrated,
|
||||
Unserializable,
|
||||
} from 'react-devtools-shared/src/hydration';
|
||||
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
||||
import type {ReactFunctionLocation, ReactStackTrace} from 'shared/ReactTypes';
|
||||
|
||||
export type BrowserTheme = 'dark' | 'light';
|
||||
|
||||
@@ -184,6 +184,25 @@ export type Element = {
|
||||
compiledWithForget: boolean,
|
||||
};
|
||||
|
||||
// Serialized version of ReactIOInfo
|
||||
export type SerializedIOInfo = {
|
||||
name: string,
|
||||
start: number,
|
||||
end: number,
|
||||
value: null | Promise<mixed>,
|
||||
env: null | string,
|
||||
owner: null | SerializedElement,
|
||||
stack: null | ReactStackTrace,
|
||||
};
|
||||
|
||||
// Serialized version of ReactAsyncInfo
|
||||
export type SerializedAsyncInfo = {
|
||||
awaited: SerializedIOInfo,
|
||||
env: null | string,
|
||||
owner: null | SerializedElement,
|
||||
stack: null | ReactStackTrace,
|
||||
};
|
||||
|
||||
export type SerializedElement = {
|
||||
displayName: string | null,
|
||||
id: number,
|
||||
@@ -239,6 +258,9 @@ export type InspectedElement = {
|
||||
errors: Array<[string, number]>,
|
||||
warnings: Array<[string, number]>,
|
||||
|
||||
// Things that suspended this Instances
|
||||
suspendedBy: Object,
|
||||
|
||||
// List of owners
|
||||
owners: Array<SerializedElement> | null,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user