mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
e5287287aa
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.
123 lines
3.6 KiB
JavaScript
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;
|
|
}
|