@samitouri / QOS-React-2 / commits / 7513996f20

[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).

Sebastian Markbåge committed Jul 22, 2025 at 10:53 UTC 7513996f20e34070141aa605fe282ca6986915a0
19 files changed +158 -167
packages/react-devtools-core/src/standalone.js
+9 -11
@@ -26,7 +26,7 @@ import {
26 import {localStorageSetItem} from 'react-devtools-shared/src/storage';
27
28 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
29 -import type {Source} from 'react-devtools-shared/src/shared/types';
29 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
30
31 export type StatusTypes = 'server-connected' | 'devtools-connected' | 'error';
32 export type StatusListener = (message: string, status: StatusTypes) => void;
@@ -144,29 +144,27 @@ async function fetchFileWithCaching(url: string) {
144 }
145
146 function canViewElementSourceFunction(
147 - _source: Source,
148 - symbolicatedSource: Source | null,
147 + _source: ReactFunctionLocation,
148 + symbolicatedSource: ReactFunctionLocation | null,
149 ): boolean {
150 if (symbolicatedSource == null) {
151 return false;
152 }
153 + const [, sourceURL, ,] = symbolicatedSource;
154
154 - return doesFilePathExist(symbolicatedSource.sourceURL, projectRoots);
155 + return doesFilePathExist(sourceURL, projectRoots);
156 }
157
158 function viewElementSourceFunction(
158 - _source: Source,
159 - symbolicatedSource: Source | null,
159 + _source: ReactFunctionLocation,
160 + symbolicatedSource: ReactFunctionLocation | null,
161 ): void {
162 if (symbolicatedSource == null) {
163 return;
164 }
165
165 - launchEditor(
166 - symbolicatedSource.sourceURL,
167 - symbolicatedSource.line,
168 - projectRoots,
169 - );
166 + const [, sourceURL, line] = symbolicatedSource;
167 + launchEditor(sourceURL, line, projectRoots);
168 }
169
170 function onDisconnected() {
packages/react-devtools-extensions/src/main/index.js
+1 -1
@@ -124,7 +124,7 @@ function createBridgeAndStore() {
124 };
125
126 const viewElementSourceFunction = (source, symbolicatedSource) => {
127 - const {sourceURL, line, column} = symbolicatedSource
127 + const [, sourceURL, line, column] = symbolicatedSource
128 ? symbolicatedSource
129 : source;
130
packages/react-devtools-fusebox/src/frontend.d.ts
+10 -9
@@ -28,22 +28,23 @@ export type Config = {
28 export function createBridge(wall: Wall): Bridge;
29 export function createStore(bridge: Bridge, config?: Config): Store;
30
31 -export type Source = {
32 - sourceURL: string,
33 - line: number,
34 - column: number,
35 -};
31 +export type ReactFunctionLocation = [
32 + string, // function name
33 + string, // file name TODO: model nested eval locations as nested arrays
34 + number, // enclosing line number
35 + number, // enclosing column number
36 +];
37 export type ViewElementSource = (
37 - source: Source,
38 - symbolicatedSource: Source | null,
38 + source: ReactFunctionLocation,
39 + symbolicatedSource: ReactFunctionLocation | null,
40 ) => void;
41 export type ViewAttributeSource = (
42 id: number,
43 path: Array<string | number>,
44 ) => void;
45 export type CanViewElementSource = (
45 - source: Source,
46 - symbolicatedSource: Source | null,
46 + source: ReactFunctionLocation,
47 + symbolicatedSource: ReactFunctionLocation | null,
48 ) => boolean;
49
50 export type InitializationOptions = {
packages/react-devtools-shared/src/__tests__/utils-test.js
+37 -38
@@ -12,7 +12,7 @@ import {
12 getDisplayNameForReactElement,
13 isPlainObject,
14 } from 'react-devtools-shared/src/utils';
15 -import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
15 +import {stackToComponentLocations} from 'react-devtools-shared/src/devtools/utils';
16 import {
17 formatConsoleArguments,
18 formatConsoleArgumentsToSingleString,
@@ -63,14 +63,17 @@ describe('utils', () => {
63
64 it('should parse a component stack trace', () => {
65 expect(
66 - stackToComponentSources(`
66 + stackToComponentLocations(`
67 at Foobar (http://localhost:3000/static/js/bundle.js:103:74)
68 at a
69 at header
70 at div
71 at App`),
72 ).toEqual([
73 - ['Foobar', ['http://localhost:3000/static/js/bundle.js', 103, 74]],
73 + [
74 + 'Foobar',
75 + ['Foobar', 'http://localhost:3000/static/js/bundle.js', 103, 74],
76 + ],
77 ['a', null],
78 ['header', null],
79 ['div', null],
@@ -315,12 +318,12 @@ describe('utils', () => {
318 'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' +
319 'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n',
320 ),
318 - ).toEqual({
319 - sourceURL:
320 - 'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
321 - line: 1,
322 - column: 10389,
323 - });
321 + ).toEqual([
322 + '',
323 + 'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js',
324 + 1,
325 + 10389,
326 + ]);
327 });
328
329 it('should construct the source from highest available frame', () => {
@@ -338,12 +341,12 @@ describe('utils', () => {
341 ' at tt (https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165520)\n' +
342 ' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)',
343 ),
341 - ).toEqual({
342 - sourceURL:
343 - 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
344 - line: 5,
345 - column: 9236,
346 - });
344 + ).toEqual([
345 + '',
346 + 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
347 + 5,
348 + 9236,
349 + ]);
350 });
351
352 it('should construct the source from frame, which has only url specified', () => {
@@ -353,12 +356,12 @@ describe('utils', () => {
356 ' at a\n' +
357 ' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n',
358 ),
356 - ).toEqual({
357 - sourceURL:
358 - 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
359 - line: 5,
360 - column: 9236,
361 - });
359 + ).toEqual([
360 + '',
361 + 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js',
362 + 5,
363 + 9236,
364 + ]);
365 });
366
367 it('should parse sourceURL correctly if it includes parentheses', () => {
@@ -368,12 +371,12 @@ describe('utils', () => {
371 ' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' +
372 ' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)',
373 ),
371 - ).toEqual({
372 - sourceURL:
373 - 'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
374 - line: 307,
375 - column: 11,
376 - });
374 + ).toEqual([
375 + '',
376 + 'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js',
377 + 307,
378 + 11,
379 + ]);
380 });
381
382 it('should support Firefox stack', () => {
@@ -383,12 +386,12 @@ describe('utils', () => {
386 'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' +
387 'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513',
388 ),
386 - ).toEqual({
387 - sourceURL:
388 - 'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
389 - line: 1,
390 - column: 165558,
391 - });
389 + ).toEqual([
390 + '',
391 + 'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js',
392 + 1,
393 + 165558,
394 + ]);
395 });
396 });
397
@@ -398,11 +401,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
401 exports.f = f;
402 function f() { }
403 //# sourceMappingURL=`;
401 - const result = {
402 - column: 16,
403 - line: 1,
404 - sourceURL: 'http://test/a.mts',
405 - };
404 + const result = ['', 'http://test/a.mts', 1, 16];
405 const fs = {
406 'http://test/a.mts': `export function f() {}`,
407 'http://test/a.mjs.map': `{"version":3,"file":"a.mjs","sourceRoot":"","sources":["a.mts"],"names":[],"mappings":";;AAAA,cAAsB;AAAtB,SAAgB,CAAC,KAAI,CAAC"}`,
packages/react-devtools-shared/src/backend/fiber/renderer.js
+8 -6
@@ -145,7 +145,7 @@ import type {
145 ElementType,
146 Plugins,
147 } from 'react-devtools-shared/src/frontend/types';
148 -import type {Source} from 'react-devtools-shared/src/shared/types';
148 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
149 import {getSourceLocationByFiber} from './DevToolsFiberComponentStack';
150 import {formatOwnerStack} from '../shared/DevToolsOwnerStack';
151
@@ -162,7 +162,7 @@ type FiberInstance = {
162 parent: null | DevToolsInstance,
163 firstChild: null | DevToolsInstance,
164 nextSibling: null | DevToolsInstance,
165 - source: null | string | Error | Source, // source location of this component function, or owned child stack
165 + source: null | string | Error | ReactFunctionLocation, // source location of this component function, or owned child stack
166 logCount: number, // total number of errors/warnings last seen
167 treeBaseDuration: number, // the profiled time of the last render of this subtree
168 data: Fiber, // one of a Fiber pair
@@ -190,7 +190,7 @@ type FilteredFiberInstance = {
190 parent: null | DevToolsInstance,
191 firstChild: null | DevToolsInstance,
192 nextSibling: null | DevToolsInstance,
193 - source: null | string | Error | Source, // always null here.
193 + source: null | string | Error | ReactFunctionLocation, // always null here.
194 logCount: number, // total number of errors/warnings last seen
195 treeBaseDuration: number, // the profiled time of the last render of this subtree
196 data: Fiber, // one of a Fiber pair
@@ -222,7 +222,7 @@ type VirtualInstance = {
222 parent: null | DevToolsInstance,
223 firstChild: null | DevToolsInstance,
224 nextSibling: null | DevToolsInstance,
225 - source: null | string | Error | Source, // source location of this server component, or owned child stack
225 + source: null | string | Error | ReactFunctionLocation, // source location of this server component, or owned child stack
226 logCount: number, // total number of errors/warnings last seen
227 treeBaseDuration: number, // the profiled time of the last render of this subtree
228 // The latest info for this instance. This can be updated over time and the
@@ -5805,7 +5805,7 @@ export function attach(
5805
5806 function getSourceForFiberInstance(
5807 fiberInstance: FiberInstance,
5808 - ): Source | null {
5808 + ): ReactFunctionLocation | null {
5809 // Favor the owner source if we have one.
5810 const ownerSource = getSourceForInstance(fiberInstance);
5811 if (ownerSource !== null) {
@@ -5830,7 +5830,9 @@ export function attach(
5830 return source;
5831 }
5832
5833 - function getSourceForInstance(instance: DevToolsInstance): Source | null {
5833 + function getSourceForInstance(
5834 + instance: DevToolsInstance,
5835 + ): ReactFunctionLocation | null {
5836 let unresolvedSource = instance.source;
5837 if (unresolvedSource === null) {
5838 // We don't have any source yet. We can try again later in case an owned child mounts later.
packages/react-devtools-shared/src/backend/types.js
+2 -2
@@ -32,7 +32,7 @@ import type {
32 import type {InitBackend} from 'react-devtools-shared/src/backend';
33 import type {TimelineDataExport} from 'react-devtools-timeline/src/types';
34 import type {BackendBridge} from 'react-devtools-shared/src/bridge';
35 -import type {Source} from 'react-devtools-shared/src/shared/types';
35 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
36 import type Agent from './agent';
37
38 type BundleType =
@@ -281,7 +281,7 @@ export type InspectedElement = {
281
282 // List of owners
283 owners: Array<SerializedElement> | null,
284 - source: Source | null,
284 + source: ReactFunctionLocation | null,
285
286 type: ElementType,
287
packages/react-devtools-shared/src/backend/utils/index.js
+34 -26
@@ -12,7 +12,7 @@ import {compareVersions} from 'compare-versions';
12 import {dehydrate} from 'react-devtools-shared/src/hydration';
13 import isArray from 'shared/isArray';
14
15 -import type {Source} from 'react-devtools-shared/src/shared/types';
15 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
16 import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
17
18 export {default as formatWithStyles} from './formatWithStyles';
@@ -258,9 +258,12 @@ export const isReactNativeEnvironment = (): boolean => {
258 return window.document == null;
259 };
260
261 -function extractLocation(
262 - url: string,
263 -): null | {sourceURL: string, line?: string, column?: string} {
261 +function extractLocation(url: string): null | {
262 + functionName?: string,
263 + sourceURL: string,
264 + line?: string,
265 + column?: string,
266 +} {
267 if (url.indexOf(':') === -1) {
268 return null;
269 }
@@ -275,12 +278,15 @@ function extractLocation(
278 return null;
279 }
280
281 + const functionName = ''; // TODO: Parse this in the regexp.
282 const [, , sourceURL, line, column] = locationParts;
279 - return {sourceURL, line, column};
283 + return {functionName, sourceURL, line, column};
284 }
285
286 const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m;
283 -function parseSourceFromChromeStack(stack: string): Source | null {
287 +function parseSourceFromChromeStack(
288 + stack: string,
289 +): ReactFunctionLocation | null {
290 const frames = stack.split('\n');
291 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
292 for (const frame of frames) {
@@ -297,19 +303,22 @@ function parseSourceFromChromeStack(stack: string): Source | null {
303 continue;
304 }
305
300 - const {sourceURL, line = '1', column = '1'} = location;
306 + const {functionName, sourceURL, line = '1', column = '1'} = location;
307
302 - return {
308 + return [
309 + functionName || '',
310 sourceURL,
304 - line: parseInt(line, 10),
305 - column: parseInt(column, 10),
306 - };
311 + parseInt(line, 10),
312 + parseInt(column, 10),
313 + ];
314 }
315
316 return null;
317 }
318
312 -function parseSourceFromFirefoxStack(stack: string): Source | null {
319 +function parseSourceFromFirefoxStack(
320 + stack: string,
321 +): ReactFunctionLocation | null {
322 const frames = stack.split('\n');
323 // eslint-disable-next-line no-for-of-loops/no-for-of-loops
324 for (const frame of frames) {
@@ -325,13 +334,14 @@ function parseSourceFromFirefoxStack(stack: string): Source | null {
334 continue;
335 }
336
328 - const {sourceURL, line = '1', column = '1'} = location;
337 + const {functionName, sourceURL, line = '1', column = '1'} = location;
338
330 - return {
339 + return [
340 + functionName || '',
341 sourceURL,
332 - line: parseInt(line, 10),
333 - column: parseInt(column, 10),
334 - };
342 + parseInt(line, 10),
343 + parseInt(column, 10),
344 + ];
345 }
346
347 return null;
@@ -339,7 +349,7 @@ function parseSourceFromFirefoxStack(stack: string): Source | null {
349
350 export function parseSourceFromComponentStack(
351 componentStack: string,
342 -): Source | null {
352 +): ReactFunctionLocation | null {
353 if (componentStack.match(CHROME_STACK_REGEXP)) {
354 return parseSourceFromChromeStack(componentStack);
355 }
@@ -347,13 +357,13 @@ export function parseSourceFromComponentStack(
357 return parseSourceFromFirefoxStack(componentStack);
358 }
359
350 -let collectedLocation: Source | null = null;
360 +let collectedLocation: ReactFunctionLocation | null = null;
361
362 function collectStackTrace(
363 error: Error,
364 structuredStackTrace: CallSite[],
365 ): string {
356 - let result: null | Source = null;
366 + let result: null | ReactFunctionLocation = null;
367 // Collect structured stack traces from the callsites.
368 // We mirror how V8 serializes stack frames and how we later parse them.
369 for (let i = 0; i < structuredStackTrace.length; i++) {
@@ -386,11 +396,7 @@ function collectStackTrace(
396 // Skip eval etc. without source url. They don't have location.
397 continue;
398 }
389 - result = {
390 - sourceURL,
391 - line: line,
392 - column: col,
393 - };
399 + result = [name, sourceURL, line, col];
400 }
401 }
402 // At the same time we generate a string stack trace just in case someone
@@ -404,7 +410,9 @@ function collectStackTrace(
410 return stack;
411 }
412
407 -export function parseSourceFromOwnerStack(error: Error): Source | null {
413 +export function parseSourceFromOwnerStack(
414 + error: Error,
415 +): ReactFunctionLocation | null {
416 // First attempt to collected the structured data using prepareStackTrace.
417 collectedLocation = null;
418 const previousPrepare = Error.prepareStackTrace;
packages/react-devtools-shared/src/backendAPI.js
+3 -3
@@ -260,9 +260,9 @@ export function convertInspectedElementBackendToFrontend(
260 rendererPackageName,
261 rendererVersion,
262 rootType,
263 - // Previous backend implementations (<= 5.0.1) have a different interface for Source, with fileName.
264 - // This gates the source features for only compatible backends: >= 5.0.2
265 - source: source && source.sourceURL ? source : null,
263 + // Previous backend implementations (<= 6.1.5) have a different interface for Source.
264 + // This gates the source features for only compatible backends: >= 6.1.6
265 + source: Array.isArray(source) ? source : null,
266 type,
267 owners:
268 owners === null
packages/react-devtools-shared/src/devtools/utils.js
+8 -7
@@ -9,6 +9,7 @@
9
10 import JSON5 from 'json5';
11
12 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
13 import type {Element} from 'react-devtools-shared/src/frontend/types';
14 import type {StateContext} from './views/Components/TreeContext';
15 import type Store from './store';
@@ -188,16 +189,13 @@ export function smartStringify(value: any): string {
189 return JSON.stringify(value);
190 }
191
191 -// [url, row, column]
192 -export type Stack = [string, number, number];
193 -
192 const STACK_DELIMETER = /\n\s+at /;
193 const STACK_SOURCE_LOCATION = /([^\s]+) \((.+):(.+):(.+)\)/;
194
197 -export function stackToComponentSources(
195 +export function stackToComponentLocations(
196 stack: string,
199 -): Array<[string, ?Stack]> {
200 - const out: Array<[string, ?Stack]> = [];
197 +): Array<[string, ?ReactFunctionLocation]> {
198 + const out: Array<[string, ?ReactFunctionLocation]> = [];
199 stack
200 .split(STACK_DELIMETER)
201 .slice(1)
@@ -205,7 +203,10 @@ export function stackToComponentSources(
203 const match = STACK_SOURCE_LOCATION.exec(entry);
204 if (match) {
205 const [, component, url, row, column] = match;
208 - out.push([component, [url, parseInt(row, 10), parseInt(column, 10)]]);
206 + out.push([
207 + component,
208 + [component, url, parseInt(row, 10), parseInt(column, 10)],
209 + ]);
210 } else {
211 out.push([entry, null]);
212 }
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+3 -3
@@ -28,7 +28,7 @@ import Skeleton from './Skeleton';
28
29 import styles from './InspectedElement.css';
30
31 -import type {Source} from 'react-devtools-shared/src/shared/types';
31 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
32
33 export type Props = {};
34
@@ -50,7 +50,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
50
51 const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
52
53 - const symbolicatedSourcePromise: null | Promise<Source | null> =
53 + const symbolicatedSourcePromise: null | Promise<ReactFunctionLocation | null> =
54 React.useMemo(() => {
55 if (inspectedElement == null) return null;
56 if (fetchFileWithCaching == null) return Promise.resolve(null);
@@ -58,7 +58,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
58 const {source} = inspectedElement;
59 if (source == null) return Promise.resolve(null);
60
61 - const {sourceURL, line, column} = source;
61 + const [, sourceURL, line, column] = source;
62 return symbolicateSourceWithCache(
63 fetchFileWithCaching,
64 sourceURL,
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSourcePanel.js
+7 -13
@@ -19,12 +19,12 @@ import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/wit
19
20 import ViewElementSourceContext from './ViewElementSourceContext';
21
22 -import type {Source as InspectedElementSource} from 'react-devtools-shared/src/shared/types';
22 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
23 import styles from './InspectedElementSourcePanel.css';
24
25 type Props = {
26 - source: InspectedElementSource,
27 - symbolicatedSourcePromise: Promise<InspectedElementSource | null>,
26 + source: ReactFunctionLocation,
27 + symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
28 };
29
30 function InspectedElementSourcePanel({
@@ -62,7 +62,7 @@ function InspectedElementSourcePanel({
62 function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
63 const symbolicatedSource = React.use(symbolicatedSourcePromise);
64 if (symbolicatedSource == null) {
65 - const {sourceURL, line, column} = source;
65 + const [, sourceURL, line, column] = source;
66 const handleCopy = withPermissionsCheck(
67 {permissions: ['clipboardWrite']},
68 () => copy(`${sourceURL}:${line}:${column}`),
@@ -75,7 +75,7 @@ function CopySourceButton({source, symbolicatedSourcePromise}: Props) {
75 );
76 }
77
78 - const {sourceURL, line, column} = symbolicatedSource;
78 + const [, sourceURL, line, column] = symbolicatedSource;
79 const handleCopy = withPermissionsCheck(
80 {permissions: ['clipboardWrite']},
81 () => copy(`${sourceURL}:${line}:${column}`),
@@ -109,14 +109,8 @@ function FormattedSourceString({source, symbolicatedSourcePromise}: Props) {
109 }
110 }, [source, symbolicatedSource]);
111
112 - let sourceURL, line;
113 - if (symbolicatedSource == null) {
114 - sourceURL = source.sourceURL;
115 - line = source.line;
116 - } else {
117 - sourceURL = symbolicatedSource.sourceURL;
118 - line = symbolicatedSource.line;
119 - }
112 + const [, sourceURL, line] =
113 + symbolicatedSource == null ? source : symbolicatedSource;
114
115 return (
116 <div
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js
+2 -2
@@ -35,7 +35,7 @@ import type {
35 } from 'react-devtools-shared/src/frontend/types';
36 import type {HookNames} from 'react-devtools-shared/src/frontend/types';
37 import type {ToggleParseHookNames} from './InspectedElementContext';
38 -import type {Source} from 'react-devtools-shared/src/shared/types';
38 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
39
40 type Props = {
41 element: Element,
@@ -43,7 +43,7 @@ type Props = {
43 inspectedElement: InspectedElement,
44 parseHookNames: boolean,
45 toggleParseHookNames: ToggleParseHookNames,
46 - symbolicatedSourcePromise: Promise<Source | null>,
46 + symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
47 };
48
49 export default function InspectedElementView({
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementViewSourceButton.js
+5 -5
@@ -14,7 +14,7 @@ import Button from '../Button';
14 import ViewElementSourceContext from './ViewElementSourceContext';
15 import Skeleton from './Skeleton';
16
17 -import type {Source as InspectedElementSource} from 'react-devtools-shared/src/shared/types';
17 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
18 import type {
19 CanViewElementSource,
20 ViewElementSource,
@@ -24,8 +24,8 @@ const {useCallback, useContext} = React;
24
25 type Props = {
26 canViewSource: ?boolean,
27 - source: ?InspectedElementSource,
28 - symbolicatedSourcePromise: Promise<InspectedElementSource | null> | null,
27 + source: ?ReactFunctionLocation,
28 + symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
29 };
30
31 function InspectedElementViewSourceButton({
@@ -52,8 +52,8 @@ function InspectedElementViewSourceButton({
52
53 type ActualSourceButtonProps = {
54 canViewSource: ?boolean,
55 - source: ?InspectedElementSource,
56 - symbolicatedSourcePromise: Promise<InspectedElementSource | null> | null,
55 + source: ?ReactFunctionLocation,
56 + symbolicatedSourcePromise: Promise<ReactFunctionLocation | null> | null,
57 canViewElementSourceFunction: CanViewElementSource | null,
58 viewElementSourceFunction: ViewElementSource | null,
59 };
packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js
+5 -5
@@ -11,22 +11,22 @@ import * as React from 'react';
11 import Button from 'react-devtools-shared/src/devtools/views/Button';
12 import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
13
14 -import type {Source} from 'react-devtools-shared/src/shared/types';
14 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
15
16 type Props = {
17 editorURL: string,
18 - source: Source,
19 - symbolicatedSourcePromise: Promise<Source | null>,
18 + source: ReactFunctionLocation,
19 + symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
20 };
21
22 function checkConditions(
23 editorURL: string,
24 - source: Source,
24 + source: ReactFunctionLocation,
25 ): {url: URL | null, shouldDisableButton: boolean} {
26 try {
27 const url = new URL(editorURL);
28
29 - let sourceURL = source.sourceURL;
29 + let [, sourceURL, ,] = source;
30
31 // Check if sourceURL is a correct URL, which has a protocol specified
32 if (sourceURL.includes('://')) {
packages/react-devtools-shared/src/devtools/views/DevTools.js
+5 -5
@@ -50,21 +50,21 @@ import type {FetchFileWithCaching} from './Components/FetchFileWithCachingContex
50 import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
51 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
52 import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
53 -import type {Source} from 'react-devtools-shared/src/shared/types';
53 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
54
55 export type TabID = 'components' | 'profiler';
56
57 export type ViewElementSource = (
58 - source: Source,
59 - symbolicatedSource: Source | null,
58 + source: ReactFunctionLocation,
59 + symbolicatedSource: ReactFunctionLocation | null,
60 ) => void;
61 export type ViewAttributeSource = (
62 id: number,
63 path: Array<string | number>,
64 ) => void;
65 export type CanViewElementSource = (
66 - source: Source,
67 - symbolicatedSource: Source | null,
66 + source: ReactFunctionLocation,
67 + symbolicatedSource: ReactFunctionLocation | null,
68 ) => boolean;
69
70 export type Props = {
packages/react-devtools-shared/src/devtools/views/Profiler/SidebarEventInfo.js
+6 -8
@@ -19,7 +19,7 @@ import {
19 formatTimestamp,
20 getSchedulingEventLabel,
21 } from 'react-devtools-timeline/src/utils/formatting';
22 -import {stackToComponentSources} from 'react-devtools-shared/src/devtools/utils';
22 +import {stackToComponentLocations} from 'react-devtools-shared/src/devtools/utils';
23 import {copy} from 'clipboard-js';
24 import {withPermissionsCheck} from 'react-devtools-shared/src/frontend/utils/withPermissionsCheck';
25
@@ -63,9 +63,9 @@ function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
63 </Button>
64 </div>
65 <ul className={styles.List}>
66 - {stackToComponentSources(componentStack).map(
67 - ([displayName, stack], index) => {
68 - if (stack == null) {
66 + {stackToComponentLocations(componentStack).map(
67 + ([displayName, location], index) => {
68 + if (location == null) {
69 return (
70 <li key={index}>
71 <Button
@@ -79,16 +79,14 @@ function SchedulingEventInfo({eventInfo}: SchedulingEventProps) {
79
80 // TODO: We should support symbolication here as well, but
81 // symbolicating the whole stack can be expensive
82 - const [sourceURL, line, column] = stack;
83 - const source = {sourceURL, line, column};
82 const canViewSource =
83 canViewElementSourceFunction == null ||
86 - canViewElementSourceFunction(source, null);
84 + canViewElementSourceFunction(location, null);
85
86 const viewSource =
87 !canViewSource || viewElementSourceFunction == null
88 ? () => null
91 - : () => viewElementSourceFunction(source, null);
89 + : () => viewElementSourceFunction(location, null);
90
91 return (
92 <li key={index}>
packages/react-devtools-shared/src/frontend/types.js
+2 -2
@@ -18,7 +18,7 @@ import type {
18 Dehydrated,
19 Unserializable,
20 } from 'react-devtools-shared/src/hydration';
21 -import type {Source} from 'react-devtools-shared/src/shared/types';
21 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
22
23 export type BrowserTheme = 'dark' | 'light';
24
@@ -246,7 +246,7 @@ export type InspectedElement = {
246 owners: Array<SerializedElement> | null,
247
248 // Location of component in source code.
249 - source: Source | null,
249 + source: ReactFunctionLocation | null,
250
251 type: ElementType,
252
packages/react-devtools-shared/src/shared/types.js deleted
-14
@@ -1,14 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -export type Source = {
11 - sourceURL: string,
12 - line: number,
13 - column: number,
14 -};
packages/react-devtools-shared/src/symbolicateSource.js
+11 -7
@@ -9,17 +9,20 @@
9
10 import SourceMapConsumer from 'react-devtools-shared/src/hooks/SourceMapConsumer';
11
12 -import type {Source} from 'react-devtools-shared/src/shared/types';
12 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
13 import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
14
15 -const symbolicationCache: Map<string, Promise<Source | null>> = new Map();
15 +const symbolicationCache: Map<
16 + string,
17 + Promise<ReactFunctionLocation | null>,
18 +> = new Map();
19
20 export async function symbolicateSourceWithCache(
21 fetchFileWithCaching: FetchFileWithCaching,
22 sourceURL: string,
23 line: number, // 1-based
24 column: number, // 1-based
22 -): Promise<Source | null> {
25 +): Promise<ReactFunctionLocation | null> {
26 const key = `${sourceURL}:${line}:${column}`;
27 const cachedPromise = symbolicationCache.get(key);
28 if (cachedPromise != null) {
@@ -43,7 +46,7 @@ export async function symbolicateSource(
46 sourceURL: string,
47 lineNumber: number, // 1-based
48 columnNumber: number, // 1-based
46 -): Promise<Source | null> {
49 +): Promise<ReactFunctionLocation | null> {
50 const resource = await fetchFileWithCaching(sourceURL).catch(() => null);
51 if (resource == null) {
52 return null;
@@ -75,6 +78,7 @@ export async function symbolicateSource(
78 try {
79 const parsedSourceMap = JSON.parse(sourceMap);
80 const consumer = SourceMapConsumer(parsedSourceMap);
81 + const functionName = ''; // TODO: Parse function name from sourceContent.
82 const {
83 sourceURL: possiblyURL,
84 line,
@@ -91,7 +95,7 @@ export async function symbolicateSource(
95 // sourceMapURL = https://react.dev/script.js.map
96 void new URL(possiblyURL); // test if it is a valid URL
97
94 - return {sourceURL: possiblyURL, line, column};
98 + return [functionName, possiblyURL, line, column];
99 } catch (e) {
100 // This is not valid URL
101 if (
@@ -101,7 +105,7 @@ export async function symbolicateSource(
105 possiblyURL.slice(1).startsWith(':\\\\')
106 ) {
107 // This is an absolute path
104 - return {sourceURL: possiblyURL, line, column};
108 + return [functionName, possiblyURL, line, column];
109 }
110
111 // This is a relative path
@@ -110,7 +114,7 @@ export async function symbolicateSource(
114 possiblyURL,
115 sourceMapURL,
116 ).toString();
113 - return {sourceURL: absoluteSourcePath, line, column};
117 + return [functionName, absoluteSourcePath, line, column];
118 }
119 } catch (e) {
120 return null;