Files
react/packages/react-devtools-shared/src/symbolicateSource.js
T
Sebastian Markbåge 0825d019be [DevTools] Prefer I/O stack and show await stack after only if it's a different owner (#34101)
Stacked on #34094.

This shows the I/O stack if available. If it's not available or if it
has a different owner (like if it was passed in) then we show the
`"awaited at:"` stack below it so you can see where it started and where
it was awaited. If it's the same owner this tends to be unnecessary
noise. We could maybe be smarter if the stacks are very different then
you might want to show both even with the same owner.

<img width="517" height="478" alt="Screenshot 2025-08-04 at 11 57 28 AM"
src="https://github.com/user-attachments/assets/2dbfbed4-4671-4a5f-8e6e-ebec6fe8a1b7"
/>

Additionally, this adds an inferred await if there's no owner and no
stack for the await. The inferred await of a function/class component is
just the owner. No stack. Because the stack trace would be the return
value. This will also be the case if you use throw-a-Promise. The
inferred await in the child position of a built-in is the JSX location
of that await like if you pass a promise to a child. This inference
already happens when you pass a Promise from RSC so in this case it
already has an await - so this is mainly for client promises.
2025-08-06 11:21:01 -04:00

130 lines
3.9 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 SourceMapConsumer from 'react-devtools-shared/src/hooks/SourceMapConsumer';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
const symbolicationCache: Map<
string,
Promise<ReactFunctionLocation | null>,
> = new Map();
export function symbolicateSourceWithCache(
fetchFileWithCaching: FetchFileWithCaching,
sourceURL: string,
line: number, // 1-based
column: number, // 1-based
): Promise<ReactFunctionLocation | null> {
const key = `${sourceURL}:${line}:${column}`;
const cachedPromise = symbolicationCache.get(key);
if (cachedPromise != null) {
return cachedPromise;
}
const promise = symbolicateSource(
fetchFileWithCaching,
sourceURL,
line,
column,
);
symbolicationCache.set(key, promise);
return promise;
}
const SOURCE_MAP_ANNOTATION_PREFIX = 'sourceMappingURL=';
export async function symbolicateSource(
fetchFileWithCaching: FetchFileWithCaching,
sourceURL: string,
lineNumber: number, // 1-based
columnNumber: number, // 1-based
): Promise<ReactFunctionLocation | null> {
const resource = await fetchFileWithCaching(sourceURL).catch(() => null);
if (resource == null) {
return null;
}
const resourceLines = resource.split(/[\r\n]+/);
for (let i = resourceLines.length - 1; i >= 0; --i) {
const resourceLine = resourceLines[i];
// In case there is empty last line
if (!resourceLine) continue;
// Not an annotation? Stop looking for a source mapping url.
if (!resourceLine.startsWith('//#')) break;
if (resourceLine.includes(SOURCE_MAP_ANNOTATION_PREFIX)) {
const sourceMapAnnotationStartIndex = resourceLine.indexOf(
SOURCE_MAP_ANNOTATION_PREFIX,
);
const sourceMapAt = resourceLine.slice(
sourceMapAnnotationStartIndex + SOURCE_MAP_ANNOTATION_PREFIX.length,
resourceLine.length,
);
const sourceMapURL = new URL(sourceMapAt, sourceURL).toString();
const sourceMap = await fetchFileWithCaching(sourceMapURL).catch(
() => null,
);
if (sourceMap != null) {
try {
const parsedSourceMap = JSON.parse(sourceMap);
const consumer = SourceMapConsumer(parsedSourceMap);
const functionName = ''; // TODO: Parse function name from sourceContent.
const {
sourceURL: possiblyURL,
line,
column,
} = consumer.originalPositionFor({
lineNumber, // 1-based
columnNumber, // 1-based
});
if (possiblyURL === null) {
return null;
}
try {
// sourceMapURL = https://react.dev/script.js.map
void new URL(possiblyURL); // test if it is a valid URL
return [functionName, possiblyURL, line, column];
} catch (e) {
// This is not valid URL
if (
// sourceMapURL = /file
possiblyURL.startsWith('/') ||
// sourceMapURL = C:\\...
possiblyURL.slice(1).startsWith(':\\\\')
) {
// This is an absolute path
return [functionName, possiblyURL, line, column];
}
// This is a relative path
// possiblyURL = x.js.map, sourceMapURL = https://react.dev/script.js.map
const absoluteSourcePath = new URL(
possiblyURL,
sourceMapURL,
).toString();
return [functionName, absoluteSourcePath, line, column];
}
} catch (e) {
return null;
}
}
return null;
}
}
return null;
}