[DevTools] Unify by using ReactFunctionLocation type instead of Source (#33955)

In RSC and other stacks now we use a lot of `ReactFunctionLocation` type
to represent the location of a function. I.e. the location of the
beginning of the function (the enclosing line/col) that is represented
by the "Source" of the function. This is also what the parent Component
Stacks represents.

As opposed to `ReactCallSite` which is what normal stack traces and
owner stacks represent. I.e. the line/column number of the callsite into
the next function.

We can start sharing more code by using the `ReactFunctionLocation` type
to represent the component source location and it also helps clarify
which ones are function locations and which ones are callsites as we
start adding more stack traces (e.g. for async debug info and owner
stack traces).
This commit is contained in:
Sebastian Markbåge 2025-07-22 10:53:08 -04:00 committed by GitHub
parent bb4418d647
commit 7513996f20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 158 additions and 167 deletions

View File

@ -26,7 +26,7 @@ import {
import {localStorageSetItem} from 'react-devtools-shared/src/storage';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
export type StatusTypes = 'server-connected' | 'devtools-connected' | 'error';
export type StatusListener = (message: string, status: StatusTypes) => void;
@ -144,29 +144,27 @@ async function fetchFileWithCaching(url: string) {
}
function canViewElementSourceFunction(
_source: Source,
symbolicatedSource: Source | null,
_source: ReactFunctionLocation,
symbolicatedSource: ReactFunctionLocation | null,
): boolean {
if (symbolicatedSource == null) {
return false;
}
const [, sourceURL, ,] = symbolicatedSource;
return doesFilePathExist(symbolicatedSource.sourceURL, projectRoots);
return doesFilePathExist(sourceURL, projectRoots);
}
function viewElementSourceFunction(
_source: Source,
symbolicatedSource: Source | null,
_source: ReactFunctionLocation,
symbolicatedSource: ReactFunctionLocation | null,
): void {
if (symbolicatedSource == null) {
return;
}
launchEditor(
symbolicatedSource.sourceURL,
symbolicatedSource.line,
projectRoots,
);
const [, sourceURL, line] = symbolicatedSource;
launchEditor(sourceURL, line, projectRoots);
}
function onDisconnected() {

View File

@ -124,7 +124,7 @@ function createBridgeAndStore() {
};
const viewElementSourceFunction = (source, symbolicatedSource) => {
const {sourceURL, line, column} = symbolicatedSource
const [, sourceURL, line, column] = symbolicatedSource
? symbolicatedSource
: source;

View File

@ -28,22 +28,23 @@ export type Config = {
export function createBridge(wall: Wall): Bridge;
export function createStore(bridge: Bridge, config?: Config): Store;
export type Source = {
sourceURL: string,
line: number,
column: number,
};
export type ReactFunctionLocation = [
string, // function name
string, // file name TODO: model nested eval locations as nested arrays
number, // enclosing line number
number, // enclosing column number
];
export type ViewElementSource = (
source: Source,
symbolicatedSource: Source | null,
source: ReactFunctionLocation,
symbolicatedSource: ReactFunctionLocation | null,
) => void;
export type ViewAttributeSource = (
id: number,
path: Array<string | number>,
) => void;
export type CanViewElementSource = (
source: Source,
symbolicatedSource: Source | null,
source: ReactFunctionLocation,
symbolicatedSource: ReactFunctionLocation | null,
) => boolean;
export type InitializationOptions = {

View File

@ -12,7 +12,7 @@ import {
getDisplayNameForReactElement,
isPlainObject,
} from 'react-devtools-shared/src/utils';
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
import {stackToComponentLocations} from 'react-devtools-shared/src/devtools/utils';
import {
formatConsoleArguments,
formatConsoleArgumentsToSingleString,
@ -63,14 +63,17 @@ describe('utils', () => {
it('should parse a component stack trace', () => {
expect(
stackToComponentSources(`
stackToComponentLocations(`
at Foobar (http://localhost:3000/static/js/bundle.js:103:74)
at a
at header
at div
at App`),
).toEqual([
['Foobar', ['http://localhost:3000/static/js/bundle.js', 103, 74]],
[
'Foobar',
['Foobar', 'http://localhost:3000/static/js/bundle.js', 103, 74],
],
['a', null],
['header', null],
['div', null],
@ -315,12 +318,12 @@ describe('utils', () => {
'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' +
'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n',
),
).toEqual({
sourceURL:
'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
line: 1,
column: 10389,
});
).toEqual([
'',
'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
1,
10389,
]);
});
it('should construct the source from highest available frame', () => {
@ -338,12 +341,12 @@ describe('utils', () => {
' at tt (https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165520)\n' +
' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)',
),
).toEqual({
sourceURL:
'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
line: 5,
column: 9236,
});
).toEqual([
'',
'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
5,
9236,
]);
});
it('should construct the source from frame, which has only url specified', () => {
@ -353,12 +356,12 @@ describe('utils', () => {
' at a\n' +
' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n',
),
).toEqual({
sourceURL:
'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
line: 5,
column: 9236,
});
).toEqual([
'',
'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
5,
9236,
]);
});
it('should parse sourceURL correctly if it includes parentheses', () => {
@ -368,12 +371,12 @@ describe('utils', () => {
' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' +
' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)',
),
).toEqual({
sourceURL:
'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
line: 307,
column: 11,
});
).toEqual([
'',
'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
307,
11,
]);
});
it('should support Firefox stack', () => {
@ -383,12 +386,12 @@ describe('utils', () => {
'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' +
'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513',
),
).toEqual({
sourceURL:
'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
line: 1,
column: 165558,
});
).toEqual([
'',
'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
1,
165558,
]);
});
});
@ -398,11 +401,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.f = f;
function f() { }
//# sourceMappingURL=`;
const result = {
column: 16,
line: 1,
sourceURL: 'http://test/a.mts',
};
const result = ['', 'http://test/a.mts', 1, 16];
const fs = {
'http://test/a.mts': `export function f() {}`,
'http://test/a.mjs.map': `{"version":3,"file":"a.mjs","sourceRoot":"","sources":["a.mts"],"names":[],"mappings":";;AAAA,cAAsB;AAAtB,SAAgB,CAAC,KAAI,CAAC"}`,

View File

@ -145,7 +145,7 @@ import type {
ElementType,
Plugins,
} from 'react-devtools-shared/src/frontend/types';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import {getSourceLocationByFiber} from './DevToolsFiberComponentStack';
import {formatOwnerStack} from '../shared/DevToolsOwnerStack';
@ -162,7 +162,7 @@ type FiberInstance = {
parent: null | DevToolsInstance,
firstChild: null | DevToolsInstance,
nextSibling: null | DevToolsInstance,
source: null | string | Error | Source, // source location of this component function, or owned child stack
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
data: Fiber, // one of a Fiber pair
@ -190,7 +190,7 @@ type FilteredFiberInstance = {
parent: null | DevToolsInstance,
firstChild: null | DevToolsInstance,
nextSibling: null | DevToolsInstance,
source: null | string | Error | Source, // always null here.
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
data: Fiber, // one of a Fiber pair
@ -222,7 +222,7 @@ type VirtualInstance = {
parent: null | DevToolsInstance,
firstChild: null | DevToolsInstance,
nextSibling: null | DevToolsInstance,
source: null | string | Error | Source, // source location of this server component, or owned child stack
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
// The latest info for this instance. This can be updated over time and the
@ -5805,7 +5805,7 @@ export function attach(
function getSourceForFiberInstance(
fiberInstance: FiberInstance,
): Source | null {
): ReactFunctionLocation | null {
// Favor the owner source if we have one.
const ownerSource = getSourceForInstance(fiberInstance);
if (ownerSource !== null) {
@ -5830,7 +5830,9 @@ export function attach(
return source;
}
function getSourceForInstance(instance: DevToolsInstance): Source | null {
function getSourceForInstance(
instance: DevToolsInstance,
): ReactFunctionLocation | null {
let unresolvedSource = instance.source;
if (unresolvedSource === null) {
// We don't have any source yet. We can try again later in case an owned child mounts later.

View File

@ -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 {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type Agent from './agent';
type BundleType =
@ -281,7 +281,7 @@ export type InspectedElement = {
// List of owners
owners: Array<SerializedElement> | null,
source: Source | null,
source: ReactFunctionLocation | null,
type: ElementType,

View File

@ -12,7 +12,7 @@ import {compareVersions} from 'compare-versions';
import {dehydrate} from 'react-devtools-shared/src/hydration';
import isArray from 'shared/isArray';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
export {default as formatWithStyles} from './formatWithStyles';
@ -258,9 +258,12 @@ export const isReactNativeEnvironment = (): boolean => {
return window.document == null;
};
function extractLocation(
url: string,
): null | {sourceURL: string, line?: string, column?: string} {
function extractLocation(url: string): null | {
functionName?: string,
sourceURL: string,
line?: string,
column?: string,
} {
if (url.indexOf(':') === -1) {
return null;
}
@ -275,12 +278,15 @@ function extractLocation(
return null;
}
const functionName = ''; // TODO: Parse this in the regexp.
const [, , sourceURL, line, column] = locationParts;
return {sourceURL, line, column};
return {functionName, sourceURL, line, column};
}
const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
function parseSourceFromChromeStack(stack: string): Source | null {
function parseSourceFromChromeStack(
stack: string,
): ReactFunctionLocation | null {
const frames = stack.split('\n');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const frame of frames) {
@ -297,19 +303,22 @@ function parseSourceFromChromeStack(stack: string): Source | null {
continue;
}
const {sourceURL, line = '1', column = '1'} = location;
const {functionName, sourceURL, line = '1', column = '1'} = location;
return {
return [
functionName || '',
sourceURL,
line: parseInt(line, 10),
column: parseInt(column, 10),
};
parseInt(line, 10),
parseInt(column, 10),
];
}
return null;
}
function parseSourceFromFirefoxStack(stack: string): Source | null {
function parseSourceFromFirefoxStack(
stack: string,
): ReactFunctionLocation | null {
const frames = stack.split('\n');
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const frame of frames) {
@ -325,13 +334,14 @@ function parseSourceFromFirefoxStack(stack: string): Source | null {
continue;
}
const {sourceURL, line = '1', column = '1'} = location;
const {functionName, sourceURL, line = '1', column = '1'} = location;
return {
return [
functionName || '',
sourceURL,
line: parseInt(line, 10),
column: parseInt(column, 10),
};
parseInt(line, 10),
parseInt(column, 10),
];
}
return null;
@ -339,7 +349,7 @@ function parseSourceFromFirefoxStack(stack: string): Source | null {
export function parseSourceFromComponentStack(
componentStack: string,
): Source | null {
): ReactFunctionLocation | null {
if (componentStack.match(CHROME_STACK_REGEXP)) {
return parseSourceFromChromeStack(componentStack);
}
@ -347,13 +357,13 @@ export function parseSourceFromComponentStack(
return parseSourceFromFirefoxStack(componentStack);
}
let collectedLocation: Source | null = null;
let collectedLocation: ReactFunctionLocation | null = null;
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
let result: null | Source = null;
let result: null | ReactFunctionLocation = null;
// Collect structured stack traces from the callsites.
// We mirror how V8 serializes stack frames and how we later parse them.
for (let i = 0; i < structuredStackTrace.length; i++) {
@ -386,11 +396,7 @@ function collectStackTrace(
// Skip eval etc. without source url. They don't have location.
continue;
}
result = {
sourceURL,
line: line,
column: col,
};
result = [name, sourceURL, line, col];
}
}
// At the same time we generate a string stack trace just in case someone
@ -404,7 +410,9 @@ function collectStackTrace(
return stack;
}
export function parseSourceFromOwnerStack(error: Error): Source | null {
export function parseSourceFromOwnerStack(
error: Error,
): ReactFunctionLocation | null {
// First attempt to collected the structured data using prepareStackTrace.
collectedLocation = null;
const previousPrepare = Error.prepareStackTrace;

View File

@ -260,9 +260,9 @@ export function convertInspectedElementBackendToFrontend(
rendererPackageName,
rendererVersion,
rootType,
// Previous backend implementations (<= 5.0.1) have a different interface for Source, with fileName.
// This gates the source features for only compatible backends: >= 5.0.2
source: source && source.sourceURL ? source : null,
// Previous backend implementations (<= 6.1.5) have a different interface for Source.
// This gates the source features for only compatible backends: >= 6.1.6
source: Array.isArray(source) ? source : null,
type,
owners:
owners === null

View File

@ -9,6 +9,7 @@
import JSON5 from 'json5';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {Element} from 'react-devtools-shared/src/frontend/types';
import type {StateContext} from './views/Components/TreeContext';
import type Store from './store';
@ -188,16 +189,13 @@ export function smartStringify(value: any): string {
return JSON.stringify(value);
}
// [url, row, column]
export type Stack = [string, number, number];
const STACK_DELIMETER = /\n\s+at /;
const STACK_SOURCE_LOCATION = /([^\s]+) \((.+):(.+):(.+)\)/;
export function stackToComponentSources(
export function stackToComponentLocations(
stack: string,
): Array<[string, ?Stack]> {
const out: Array<[string, ?Stack]> = [];
): Array<[string, ?ReactFunctionLocation]> {
const out: Array<[string, ?ReactFunctionLocation]> = [];
stack
.split(STACK_DELIMETER)
.slice(1)
@ -205,7 +203,10 @@ export function stackToComponentSources(
const match = STACK_SOURCE_LOCATION.exec(entry);
if (match) {
const [, component, url, row, column] = match;
out.push([component, [url, parseInt(row, 10), parseInt(column, 10)]]);
out.push([
component,
[component, url, parseInt(row, 10), parseInt(column, 10)],
]);
} else {
out.push([entry, null]);
}

View File

@ -28,7 +28,7 @@ import Skeleton from './Skeleton';
import styles from './InspectedElement.css';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
export type Props = {};
@ -50,7 +50,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const symbolicatedSourcePromise: null | Promise<Source | null> =
const symbolicatedSourcePromise: null | Promise<ReactFunctionLocation | null> =
React.useMemo(() => {
if (inspectedElement == null) return null;
if (fetchFileWithCaching == null) return Promise.resolve(null);
@ -58,7 +58,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
const {source} = inspectedElement;
if (source == null) return Promise.resolve(null);
const {sourceURL, line, column} = source;
const [, sourceURL, line, column] = source;
return symbolicateSourceWithCache(
fetchFileWithCaching,
sourceURL,

View File

@ -19,12 +19,12 @@ import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/wit
import ViewElementSourceContext from './ViewElementSourceContext';
import type {Source as InspectedElementSource} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import styles from './InspectedElementSourcePanel.css';
type Props = {
source: InspectedElementSource,
symbolicatedSourcePromise: Promise<InspectedElementSource | null>,
source: ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
};
function InspectedElementSourcePanel({
@ -62,7 +62,7 @@ function InspectedElementSourcePanel({
function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
const symbolicatedSource = React.use(symbolicatedSourcePromise);
if (symbolicatedSource == null) {
const {sourceURL, line, column} = source;
const [, sourceURL, line, column] = source;
const handleCopy = withPermissionsCheck(
{permissions: ['clipboardWrite']},
() => copy(`${sourceURL}:${line}:${column}`),
@ -75,7 +75,7 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
);
}
const {sourceURL, line, column} = symbolicatedSource;
const [, sourceURL, line, column] = symbolicatedSource;
const handleCopy = withPermissionsCheck(
{permissions: ['clipboardWrite']},
() => copy(`${sourceURL}:${line}:${column}`),
@ -109,14 +109,8 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
}
}, [source, symbolicatedSource]);
let sourceURL, line;
if (symbolicatedSource == null) {
sourceURL = source.sourceURL;
line = source.line;
} else {
sourceURL = symbolicatedSource.sourceURL;
line = symbolicatedSource.line;
}
const [, sourceURL, line] =
symbolicatedSource == null ? source : symbolicatedSource;
return (
<div

View File

@ -35,7 +35,7 @@ import type {
} from 'react-devtools-shared/src/frontend/types';
import type {HookNames} from 'react-devtools-shared/src/frontend/types';
import type {ToggleParseHookNames} from './InspectedElementContext';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
type Props = {
element: Element,
@ -43,7 +43,7 @@ type Props = {
inspectedElement: InspectedElement,
parseHookNames: boolean,
toggleParseHookNames: ToggleParseHookNames,
symbolicatedSourcePromise: Promise<Source | null>,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
};
export default function InspectedElementView({

View File

@ -14,7 +14,7 @@ import Button from '../Button';
import ViewElementSourceContext from './ViewElementSourceContext';
import Skeleton from './Skeleton';
import type {Source as InspectedElementSource} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {
CanViewElementSource,
ViewElementSource,
@ -24,8 +24,8 @@ const {useCallback, useContext} = React;
type Props = {
canViewSource: ?boolean,
source: ?InspectedElementSource,
symbolicatedSourcePromise: Promise<InspectedElementSource | null> | null,
source: ?ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
};
function InspectedElementViewSourceButton({
@ -52,8 +52,8 @@ function InspectedElementViewSourceButton({
type ActualSourceButtonProps = {
canViewSource: ?boolean,
source: ?InspectedElementSource,
symbolicatedSourcePromise: Promise<InspectedElementSource | null> | null,
source: ?ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
canViewElementSourceFunction: CanViewElementSource | null,
viewElementSourceFunction: ViewElementSource | null,
};

View File

@ -11,22 +11,22 @@ import * as React from 'react';
import Button from 'react-devtools-shared/src/devtools/views/Button';
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
type Props = {
editorURL: string,
source: Source,
symbolicatedSourcePromise: Promise<Source | null>,
source: ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
};
function checkConditions(
editorURL: string,
source: Source,
source: ReactFunctionLocation,
): {url: URL | null, shouldDisableButton: boolean} {
try {
const url = new URL(editorURL);
let sourceURL = source.sourceURL;
let [, sourceURL, ,] = source;
// Check if sourceURL is a correct URL, which has a protocol specified
if (sourceURL.includes('://')) {

View File

@ -50,21 +50,21 @@ import type {FetchFileWithCaching} from './Components/FetchFileWithCachingContex
import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
export type TabID = 'components' | 'profiler';
export type ViewElementSource = (
source: Source,
symbolicatedSource: Source | null,
source: ReactFunctionLocation,
symbolicatedSource: ReactFunctionLocation | null,
) => void;
export type ViewAttributeSource = (
id: number,
path: Array<string | number>,
) => void;
export type CanViewElementSource = (
source: Source,
symbolicatedSource: Source | null,
source: ReactFunctionLocation,
symbolicatedSource: ReactFunctionLocation | null,
) => boolean;
export type Props = {

View File

@ -19,7 +19,7 @@ import {
formatTimestamp,
getSchedulingEventLabel,
} from 'react-devtools-timeline/src/utils/formatting';
import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
import {stackToComponentLocations} from 'react-devtools-shared/src/devtools/utils';
import {copy} from 'clipboard-js';
import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
@ -63,9 +63,9 @@ function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
</Button>
</div>
<ul className={styles.List}>
{stackToComponentSources(componentStack).map(
([displayName, stack], index) => {
if (stack == null) {
{stackToComponentLocations(componentStack).map(
([displayName, location], index) => {
if (location == null) {
return (
<li key={index}>
<Button
@ -79,16 +79,14 @@ function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
// TODO: We should support symbolication here as well, but
// symbolicating the whole stack can be expensive
const [sourceURL, line, column] = stack;
const source = {sourceURL, line, column};
const canViewSource =
canViewElementSourceFunction == null ||
canViewElementSourceFunction(source, null);
canViewElementSourceFunction(location, null);
const viewSource =
!canViewSource || viewElementSourceFunction == null
? () => null
: () => viewElementSourceFunction(source, null);
: () => viewElementSourceFunction(location, null);
return (
<li key={index}>

View File

@ -18,7 +18,7 @@ import type {
Dehydrated,
Unserializable,
} from 'react-devtools-shared/src/hydration';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
export type BrowserTheme = 'dark' | 'light';
@ -246,7 +246,7 @@ export type InspectedElement = {
owners: Array<SerializedElement> | null,
// Location of component in source code.
source: Source | null,
source: ReactFunctionLocation | null,
type: ElementType,

View File

@ -1,14 +0,0 @@
/**
* 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
*/
export type Source = {
sourceURL: string,
line: number,
column: number,
};

View File

@ -9,17 +9,20 @@
import SourceMapConsumer from 'react-devtools-shared/src/hooks/SourceMapConsumer';
import type {Source} from 'react-devtools-shared/src/shared/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
const symbolicationCache: Map<string, Promise<Source | null>> = new Map();
const symbolicationCache: Map<
string,
Promise<ReactFunctionLocation | null>,
> = new Map();
export async function symbolicateSourceWithCache(
fetchFileWithCaching: FetchFileWithCaching,
sourceURL: string,
line: number, // 1-based
column: number, // 1-based
): Promise<Source | null> {
): Promise<ReactFunctionLocation | null> {
const key = `${sourceURL}:${line}:${column}`;
const cachedPromise = symbolicationCache.get(key);
if (cachedPromise != null) {
@ -43,7 +46,7 @@ export async function symbolicateSource(
sourceURL: string,
lineNumber: number, // 1-based
columnNumber: number, // 1-based
): Promise<Source | null> {
): Promise<ReactFunctionLocation | null> {
const resource = await fetchFileWithCaching(sourceURL).catch(() => null);
if (resource == null) {
return null;
@ -75,6 +78,7 @@ export async function symbolicateSource(
try {
const parsedSourceMap = JSON.parse(sourceMap);
const consumer = SourceMapConsumer(parsedSourceMap);
const functionName = ''; // TODO: Parse function name from sourceContent.
const {
sourceURL: possiblyURL,
line,
@ -91,7 +95,7 @@ export async function symbolicateSource(
// sourceMapURL = https://react.dev/script.js.map
void new URL(possiblyURL); // test if it is a valid URL
return {sourceURL: possiblyURL, line, column};
return [functionName, possiblyURL, line, column];
} catch (e) {
// This is not valid URL
if (
@ -101,7 +105,7 @@ export async function symbolicateSource(
possiblyURL.slice(1).startsWith(':\\\\')
) {
// This is an absolute path
return {sourceURL: possiblyURL, line, column};
return [functionName, possiblyURL, line, column];
}
// This is a relative path
@ -110,7 +114,7 @@ export async function symbolicateSource(
possiblyURL,
sourceMapURL,
).toString();
return {sourceURL: absoluteSourcePath, line, column};
return [functionName, absoluteSourcePath, line, column];
}
} catch (e) {
return null;