diff --git a/packages/react-devtools-shared/src/__tests__/utils-test.js b/packages/react-devtools-shared/src/__tests__/utils-test.js index 9e781e072ba75..f35cacc73308f 100644 --- a/packages/react-devtools-shared/src/__tests__/utils-test.js +++ b/packages/react-devtools-shared/src/__tests__/utils-test.js @@ -26,6 +26,7 @@ import { REACT_STRICT_MODE_TYPE as StrictMode, } from 'shared/ReactSymbols'; import {createElement} from 'react'; +import {symbolicateSource} from '../symbolicateSource'; describe('utils', () => { describe('getDisplayName', () => { @@ -385,6 +386,35 @@ describe('utils', () => { }); }); + describe('symbolicateSource', () => { + const source = `"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.f = f; +function f() { } +//# sourceMappingURL=`; + const result = { + column: 16, + line: 1, + sourceURL: 'http://test/a.mts', + }; + 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"}`, + 'http://test/a.mjs': `${source}a.mjs.map`, + 'http://test/b.mjs': `${source}./a.mjs.map`, + 'http://test/c.mjs': `${source}http://test/a.mjs.map`, + 'http://test/d.mjs': `${source}/a.mjs.map`, + }; + const fetchFileWithCaching = async (url: string) => fs[url] || null; + it('should parse source map urls', async () => { + const run = url => symbolicateSource(fetchFileWithCaching, url, 4, 10); + await expect(run('http://test/a.mjs')).resolves.toStrictEqual(result); + await expect(run('http://test/b.mjs')).resolves.toStrictEqual(result); + await expect(run('http://test/c.mjs')).resolves.toStrictEqual(result); + await expect(run('http://test/d.mjs')).resolves.toStrictEqual(result); + }); + }); + describe('formatConsoleArguments', () => { it('works with empty arguments list', () => { expect(formatConsoleArguments(...[])).toEqual([]); diff --git a/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js index 779cbb3b2f0d1..8e6503685bdb5 100644 --- a/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js +++ b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js @@ -24,8 +24,8 @@ type SearchPosition = { type ResultPosition = { column: number, line: number, - sourceContent: string, - sourceURL: string, + sourceContent: string | null, + sourceURL: string | null, }; export type SourceMapConsumerType = { @@ -118,18 +118,11 @@ function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) { const line = nearestEntry[2] + 1; const column = nearestEntry[3]; - if (sourceContent === null || sourceURL === null) { - // TODO maybe fall back to the runtime source instead of throwing? - throw Error( - `Could not find original source for line:${lineNumber} and column:${columnNumber}`, - ); - } - return { column, line, - sourceContent: ((sourceContent: any): string), - sourceURL: ((sourceURL: any): string), + sourceContent: ((sourceContent: any): string | null), + sourceURL: ((sourceURL: any): string | null), }; } diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js index 40bfed48ba53f..15423c12416cc 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js @@ -276,6 +276,11 @@ function parseSourceAST( columnNumber, lineNumber, }); + if (sourceContent === null || sourceURL === null) { + throw Error( + `Could not find original source for line:${lineNumber} and column:${columnNumber}`, + ); + } originalSourceColumnNumber = column; originalSourceLineNumber = line; diff --git a/packages/react-devtools-shared/src/symbolicateSource.js b/packages/react-devtools-shared/src/symbolicateSource.js index d28ed42e59db0..9430e88b3fc15 100644 --- a/packages/react-devtools-shared/src/symbolicateSource.js +++ b/packages/react-devtools-shared/src/symbolicateSource.js @@ -39,7 +39,7 @@ export async function symbolicateSourceWithCache( } const SOURCE_MAP_ANNOTATION_PREFIX = 'sourceMappingURL='; -async function symbolicateSource( +export async function symbolicateSource( fetchFileWithCaching: FetchFileWithCaching, sourceURL: string, lineNumber: number, // 1-based @@ -63,11 +63,12 @@ async function symbolicateSource( const sourceMapAnnotationStartIndex = resourceLine.indexOf( SOURCE_MAP_ANNOTATION_PREFIX, ); - const sourceMapURL = resourceLine.slice( + 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, ); @@ -84,29 +85,33 @@ async function symbolicateSource( columnNumber, // 1-based }); + if (possiblyURL === null) { + return null; + } try { - void new URL(possiblyURL); // This is a valid URL + // sourceMapURL = https://react.dev/script.js.map + void new URL(possiblyURL); // test if it is a valid URL const normalizedURL = normalizeUrl(possiblyURL); return {sourceURL: normalizedURL, line, column}; } catch (e) { // This is not valid URL - if (possiblyURL.startsWith('/')) { + if ( + // sourceMapURL = /file + possiblyURL.startsWith('/') || + // sourceMapURL = C:\\... + possiblyURL.slice(1).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; - + // possiblyURL = x.js.map, sourceMapURL = https://react.dev/script.js.map + const absoluteSourcePath = new URL( + possiblyURL, + sourceMapURL, + ).toString(); return {sourceURL: absoluteSourcePath, line, column}; } } catch (e) { diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 5b0903883c301..ffbc9e390d5a4 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -1017,7 +1017,7 @@ export function backendToFrontendSerializedElementMapper( }; } -// This is a hacky one to just support this exact case. +// Chrome normalizes urls like webpack-internals:// but new URL don't, so cannot use new URL here. export function normalizeUrl(url: string): string { return url.replace('/./', '/'); }