diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js index 95c4fe817d528..a9d22b5a881be 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -20,6 +20,7 @@ import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/wit import StackTraceView from './StackTraceView'; import OwnerView from './OwnerView'; import {meta} from '../../../hydration'; +import useInferredName from '../useInferredName'; import type { InspectedElement, @@ -101,21 +102,7 @@ function SuspendedByRow({ }: RowProps) { const [isOpen, setIsOpen] = useState(false); const ioInfo = asyncInfo.awaited; - let name = ioInfo.name; - if (name === '' || name === 'Promise') { - // If all we have is a generic name, we can try to infer a better name from - // the stack. We only do this if the stack has more than one frame since - // otherwise it's likely to just be the name of the component which isn't better. - const bestStack = ioInfo.stack || asyncInfo.stack; - if (bestStack !== null && bestStack.length > 1) { - // TODO: Ideally we'd get the name from the last ignore listed frame before the - // first visible frame since this is the same algorithm as the Flight server uses. - // Ideally, we'd also get the name from the source mapped entry instead of the - // original entry. However, that would require suspending the immediate display - // of these rows to first do source mapping before we can show the name. - name = bestStack[0][0]; - } - } + const name = useInferredName(asyncInfo); const description = ioInfo.description; const longName = description === '' ? name : name + ' (' + description + ')'; const shortDescription = getShortDescription(name, description); diff --git a/packages/react-devtools-shared/src/devtools/views/useInferredName.js b/packages/react-devtools-shared/src/devtools/views/useInferredName.js new file mode 100644 index 0000000000000..f0822e126d701 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/useInferredName.js @@ -0,0 +1,78 @@ +import {use, useContext, useDeferredValue} from 'react'; + +import type {ReactCallSite} from 'shared/ReactTypes'; + +import type {SourceMappedLocation} from 'react-devtools-shared/src/symbolicateSource'; + +import type {SerializedAsyncInfo} from 'react-devtools-shared/src/frontend/types'; + +import FetchFileWithCachingContext from './Components/FetchFileWithCachingContext'; + +import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource'; + +export default function useInferredName( + asyncInfo: SerializedAsyncInfo, +): string { + const fetchFileWithCaching = useContext(FetchFileWithCachingContext); + const name = asyncInfo.awaited.name; + let inferNameFromStack = null; + if (!name || name === 'Promise') { + // If all we have is a generic name, we can try to infer a better name from + // the stack. We only do this if the stack has more than one frame since + // otherwise it's likely to just be the name of the component which isn't better. + const bestStack = asyncInfo.awaited.stack || asyncInfo.stack; + if (bestStack !== null && bestStack.length > 1) { + inferNameFromStack = bestStack; + } + } + // Start by not source mapping and just taking the first name and upgrade to + // the better name asynchronously if we find one. Most of the time it'll just be + // the top of the stack. + const shouldSourceMap = useDeferredValue(inferNameFromStack !== null, false); + if (inferNameFromStack !== null) { + if (shouldSourceMap) { + let bestMatch = ''; + for (let i = 0; i < inferNameFromStack.length; i++) { + const callSite: ReactCallSite = inferNameFromStack[i]; + const [virtualFunctionName, virtualURL, virtualLine, virtualColumn] = + callSite; + const symbolicatedCallSite: null | SourceMappedLocation = + fetchFileWithCaching !== null + ? use( + symbolicateSourceWithCache( + fetchFileWithCaching, + virtualURL, + virtualLine, + virtualColumn, + ), + ) + : null; + if (symbolicatedCallSite === null) { + // If we can't source map, we treat it as first party code. We called whatever was + // the previous callsite. + if (bestMatch === '') { + return virtualFunctionName || name; + } else { + return bestMatch; + } + } else if (!symbolicatedCallSite.ignored) { + if (bestMatch === '') { + // If we had no good stack frames for internal calls, just use the last + // first party function name. + return symbolicatedCallSite[0] || virtualFunctionName || name; + } else { + return bestMatch; + } + } else { + // This line was ignore listed, it might be the one we called into from first party. + bestMatch = symbolicatedCallSite[0] || virtualFunctionName; + } + } + return name; + } else { + return inferNameFromStack[0][0]; + } + } else { + return name; + } +}