@@ -14,13 +14,14 @@ import {enableHookNameParsing} from 'react-devtools-feature-flags';
1414import { SourceMapConsumer } from 'source-map' ;
1515import {
1616 checkNodeLocation ,
17- getASTFromSourceMap ,
1817 getFilteredHookASTNodes ,
1918 getHookName ,
2019 getPotentialHookDeclarationsFromAST ,
2120 isConfirmedHookDeclaration ,
2221 isNonDeclarativePrimitiveHook ,
22+ mapCompiledLineNumberToOriginalLineNumber ,
2323} from './astUtils' ;
24+ import { sourceMapsAreAppliedToErrors } from './ErrorTester' ;
2425
2526import type {
2627 HooksNode ,
@@ -29,18 +30,35 @@ import type {
2930} from 'react-debug-tools/src/ReactDebugHooks' ;
3031import type { HookNames } from 'react-devtools-shared/src/hookNamesCache' ;
3132import type { Thenable } from 'shared/ReactTypes' ;
32- import type { SourceConsumer } from './astUtils' ;
33+ import type { SourceConsumer , SourceMap } from './astUtils' ;
3334
3435const SOURCE_MAP_REGEX = / ? s o u r c e M a p p i n g U R L = ( [ ^ \s ' " ] + ) / gm;
3536const ABSOLUTE_URL_REGEX = / ^ h t t p s ? : \/ \/ / i;
3637const MAX_SOURCE_LENGTH = 100_000_000 ;
3738
3839type HookSourceData = { |
40+ // Generated by react-debug-tools.
3941 hookSource : HookSource ,
42+
43+ // AST for original source code; typically comes from a consumed source map.
44+ originalSourceAST : mixed ,
45+
46+ // Source code (React components or custom hooks) containing primitive hook calls.
47+ // If no source map has been provided, this code will be the same as runtimeSourceCode.
48+ originalSourceCode : string | null ,
49+
50+ // Compiled code (React components or custom hooks) containing primitive hook calls.
51+ runtimeSourceCode : string | null ,
52+
53+ // APIs from source-map for parsing source maps (if detected).
4054 sourceConsumer : SourceConsumer | null ,
41- sourceContents : string | null ,
55+
56+ // External URL of source map.
57+ // Sources without source maps (or with inline source maps) won't have this.
4258 sourceMapURL : string | null ,
43- sourceMapContents : string | null ,
59+
60+ // Parsed source map object.
61+ sourceMapContents : SourceMap | null ,
4462| } ;
4563
4664export default async function parseHookNames (
@@ -72,8 +90,10 @@ export default async function parseHookNames(
7290 if ( ! fileNameToHookSourceData . has ( fileName ) ) {
7391 fileNameToHookSourceData . set ( fileName , {
7492 hookSource,
93+ originalSourceAST : null ,
94+ originalSourceCode : null ,
95+ runtimeSourceCode : null ,
7596 sourceConsumer : null ,
76- sourceContents : null ,
7797 sourceMapURL : null ,
7898 sourceMapContents : null ,
7999 } ) ;
@@ -85,7 +105,7 @@ export default async function parseHookNames(
85105
86106 return loadSourceFiles ( fileNameToHookSourceData )
87107 . then ( ( ) => extractAndLoadSourceMaps ( fileNameToHookSourceData ) )
88- . then ( ( ) => parseSourceMaps ( fileNameToHookSourceData ) )
108+ . then ( ( ) => parseSourceAST ( fileNameToHookSourceData ) )
89109 . then ( ( ) => findHookNames ( hooksList , fileNameToHookSourceData ) ) ;
90110}
91111
@@ -108,11 +128,10 @@ function extractAndLoadSourceMaps(
108128) : Promise < * > {
109129 const promises = [ ] ;
110130 fileNameToHookSourceData . forEach ( hookSourceData => {
111- const sourceMappingURLs = ( ( hookSourceData . sourceContents : any ) : string ) . match (
112- SOURCE_MAP_REGEX ,
113- ) ;
131+ const runtimeSourceCode = ( ( hookSourceData . runtimeSourceCode : any ) : string ) ;
132+ const sourceMappingURLs = runtimeSourceCode . match ( SOURCE_MAP_REGEX ) ;
114133 if ( sourceMappingURLs == null ) {
115- // Maybe file has not been transformed; let's try to parse it as-is.
134+ // Maybe file has not been transformed; we'll try to parse it as-is in parseSourceAST() .
116135 } else {
117136 for ( let i = 0 ; i < sourceMappingURLs . length ; i ++ ) {
118137 const sourceMappingURL = sourceMappingURLs [ i ] ;
@@ -217,63 +236,50 @@ function findHookNames(
217236 return null ; // Should not be reachable.
218237 }
219238
220- let hooksFromAST ;
221- let potentialReactHookASTNode ;
222- let sourceCode ;
223-
224239 const sourceConsumer = hookSourceData . sourceConsumer ;
225- if ( sourceConsumer ) {
226- const astData = getASTFromSourceMap (
240+
241+ let originalSourceLineNumber ;
242+ if ( sourceMapsAreAppliedToErrors || ! sourceConsumer ) {
243+ // Either the current environment automatically applies source maps to errors,
244+ // or the current code had no source map to begin with.
245+ // Either way, we don't need to convert the Error stack frame locations.
246+ originalSourceLineNumber = lineNumber ;
247+ } else {
248+ originalSourceLineNumber = mapCompiledLineNumberToOriginalLineNumber (
227249 sourceConsumer ,
228250 lineNumber ,
229251 columnNumber ,
230252 ) ;
253+ }
231254
232- if ( astData === null ) {
233- return null ;
234- }
235-
236- const { sourceFileAST, line, source} = astData ;
237-
238- sourceCode = source ;
239- hooksFromAST = getPotentialHookDeclarationsFromAST ( sourceFileAST ) ;
240-
241- // Iterate through potential hooks and try to find the current hook.
242- // potentialReactHookASTNode will contain declarations of the form const X = useState(0);
243- // where X could be an identifier or an array pattern (destructuring syntax)
244- potentialReactHookASTNode = hooksFromAST . find ( node => {
245- const nodeLocationCheck = checkNodeLocation ( node , line ) ;
246- const hookDeclaractionCheck = isConfirmedHookDeclaration ( node ) ;
247- return nodeLocationCheck && hookDeclaractionCheck ;
248- } ) ;
249- } else {
250- sourceCode = hookSourceData . sourceContents ;
251-
252- // There's no source map to parse here so we can use the source contents directly.
253- const ast = parse ( sourceCode , {
254- sourceType : 'unambiguous' ,
255- plugins : [ 'jsx' , 'typescript' ] ,
256- } ) ;
257- hooksFromAST = getPotentialHookDeclarationsFromAST ( ast ) ;
258- const line = ( ( hookSource . lineNumber : any ) : number ) ;
259- potentialReactHookASTNode = hooksFromAST . find ( node => {
260- const nodeLocationCheck = checkNodeLocation ( node , line ) ;
261- const hookDeclaractionCheck = isConfirmedHookDeclaration ( node ) ;
262- return nodeLocationCheck && hookDeclaractionCheck ;
263- } ) ;
255+ if ( originalSourceLineNumber === null ) {
256+ return null ;
264257 }
265258
266- if ( ! sourceCode || ! potentialReactHookASTNode ) {
259+ const hooksFromAST = getPotentialHookDeclarationsFromAST (
260+ hookSourceData . originalSourceAST ,
261+ ) ;
262+ const potentialReactHookASTNode = hooksFromAST . find ( node => {
263+ const nodeLocationCheck = checkNodeLocation (
264+ node ,
265+ ( ( originalSourceLineNumber : any ) : number ) ,
266+ ) ;
267+ const hookDeclaractionCheck = isConfirmedHookDeclaration ( node ) ;
268+ return nodeLocationCheck && hookDeclaractionCheck ;
269+ } ) ;
270+
271+ if ( ! potentialReactHookASTNode ) {
267272 return null ;
268273 }
269274
270275 // nodesAssociatedWithReactHookASTNode could directly be used to obtain the hook variable name
271276 // depending on the type of potentialReactHookASTNode
272277 try {
278+ const originalSourceCode = ( ( hookSourceData . originalSourceCode : any ) : string ) ;
273279 const nodesAssociatedWithReactHookASTNode = getFilteredHookASTNodes (
274280 potentialReactHookASTNode ,
275281 hooksFromAST ,
276- sourceCode ,
282+ originalSourceCode ,
277283 ) ;
278284
279285 return getHookName (
@@ -305,19 +311,19 @@ function loadSourceFiles(
305311 const promises = [ ] ;
306312 fileNameToHookSourceData . forEach ( ( hookSourceData , fileName ) => {
307313 promises . push (
308- fetchFile ( fileName ) . then ( sourceContents => {
309- if ( sourceContents . length > MAX_SOURCE_LENGTH ) {
314+ fetchFile ( fileName ) . then ( runtimeSourceCode => {
315+ if ( runtimeSourceCode . length > MAX_SOURCE_LENGTH ) {
310316 throw Error ( 'Source code too large to parse' ) ;
311317 }
312318
313- hookSourceData . sourceContents = sourceContents ;
319+ hookSourceData . runtimeSourceCode = runtimeSourceCode ;
314320 } ) ,
315321 ) ;
316322 } ) ;
317323 return Promise . all ( promises ) ;
318324}
319325
320- async function parseSourceMaps (
326+ async function parseSourceAST (
321327 fileNameToHookSourceData : Map < string , HookSourceData > ,
322328) : Promise < * > {
323329 // SourceMapConsumer.initialize() does nothing when running in Node (aka Jest)
@@ -332,19 +338,41 @@ async function parseSourceMaps(
332338
333339 const promises = [ ] ;
334340 fileNameToHookSourceData . forEach ( hookSourceData => {
335- if ( hookSourceData . sourceMapContents !== null ) {
341+ const { runtimeSourceCode, sourceMapContents} = hookSourceData ;
342+ if ( sourceMapContents !== null ) {
336343 // Parse and extract the AST from the source map.
337344 promises . push (
338345 SourceMapConsumer . with (
339- hookSourceData . sourceMapContents ,
346+ sourceMapContents ,
340347 null ,
341348 ( sourceConsumer : SourceConsumer ) => {
342349 hookSourceData . sourceConsumer = sourceConsumer ;
350+
351+ // Now that the source map has been loaded,
352+ // extract the original source for later.
353+ const source = sourceMapContents . sources [ 0 ] ;
354+ const originalSourceCode = sourceConsumer . sourceContentFor (
355+ source ,
356+ true ,
357+ ) ;
358+
359+ // Save the original source and parsed AST for later.
360+ // TODO (named hooks) Cache this across components, per source/file name.
361+ hookSourceData . originalSourceCode = originalSourceCode ;
362+ hookSourceData . originalSourceAST = parse ( originalSourceCode , {
363+ sourceType : 'unambiguous' ,
364+ plugins : [ 'jsx' , 'typescript' ] ,
365+ } ) ;
343366 } ,
344367 ) ,
345368 ) ;
346369 } else {
347- // There's no source map to parse here so we can skip this step.
370+ // There's no source map to parse here so we can just parse the original source itself.
371+ hookSourceData . originalSourceCode = runtimeSourceCode ;
372+ hookSourceData . originalSourceAST = parse ( runtimeSourceCode , {
373+ sourceType : 'unambiguous' ,
374+ plugins : [ 'jsx' , 'typescript' ] ,
375+ } ) ;
348376 }
349377 } ) ;
350378 return Promise . all ( promises ) ;
0 commit comments