Files
react/packages/react-devtools-shared/src/symbolicateSource.js
T
Ruslan Lesiutin e5287287aa feat[devtools]: symbolicate source for inspected element (#28471)
Stacked on https://github.com/facebook/react/pull/28351, please review
only the last commit.

Top-level description of the approach:
1. Once user selects an element from the tree, frontend asks backend to
return the inspected element, this is where we simulate an error
happening in `render` function of the component and then we parse the
error stack. As an improvement, we should probably migrate from custom
implementation of error stack parser to `error-stack-parser` from npm.
2. When frontend receives the inspected element and this object is being
propagated, we create a Promise for symbolicated source, which is then
passed down to all components, which are using `source`.
3. These components use `use` hook for this promise and are wrapped in
Suspense.

Caching:
1. For browser extension, we cache Promises based on requested resource
+ key + column, also added use of
`chrome.devtools.inspectedWindow.getResource` API.
2. For standalone case (RN), we cache based on requested resource url,
we cache the content of it.
2024-03-05 12:32:11 +00:00

123 lines
3.6 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 {normalizeUrl} from 'react-devtools-shared/src/utils';
import SourceMapConsumer from 'react-devtools-shared/src/hooks/SourceMapConsumer';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
const symbolicationCache: Map<string, Promise<Source | null>> = new Map();
export async function symbolicateSourceWithCache(
fetchFileWithCaching: FetchFileWithCaching,
sourceURL: string,
line: number, // 1-based
column: number, // 1-based
): Promise<Source | 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=';
async function symbolicateSource(
fetchFileWithCaching: FetchFileWithCaching,
sourceURL: string,
lineNumber: number, // 1-based
columnNumber: number, // 1-based
): Promise<Source | 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 sourceMapURL = resourceLine.slice(
sourceMapAnnotationStartIndex + SOURCE_MAP_ANNOTATION_PREFIX.length,
resourceLine.length,
);
const sourceMap = await fetchFileWithCaching(sourceMapURL).catch(
() => null,
);
if (sourceMap != null) {
try {
const parsedSourceMap = JSON.parse(sourceMap);
const consumer = SourceMapConsumer(parsedSourceMap);
const {
sourceURL: possiblyURL,
line,
column,
} = consumer.originalPositionFor({
lineNumber, // 1-based
columnNumber, // 1-based
});
try {
void new URL(possiblyURL); // This is a valid URL
const normalizedURL = normalizeUrl(possiblyURL);
return {sourceURL: normalizedURL, line, column};
} catch (e) {
// This is not valid URL
if (possiblyURL.startsWith('/')) {
// This is an absolute path
return {sourceURL: possiblyURL, line, column};
}
// This is a relative path
const [sourceMapAbsolutePathWithoutQueryParameters] =
sourceMapURL.split(/[?#&]/);
const absoluteSourcePath =
sourceMapAbsolutePathWithoutQueryParameters +
(sourceMapAbsolutePathWithoutQueryParameters.endsWith('/')
? ''
: '/') +
possiblyURL;
return {sourceURL: absoluteSourcePath, line, column};
}
} catch (e) {
return null;
}
}
return null;
}
}
return null;
}