@@ -14,13 +14,14 @@ import {enableHookNameParsing} from 'react-devtools-feature-flags';
14
14
import { SourceMapConsumer } from 'source-map' ;
15
15
import {
16
16
checkNodeLocation ,
17
- getASTFromSourceMap ,
18
17
getFilteredHookASTNodes ,
19
18
getHookName ,
20
19
getPotentialHookDeclarationsFromAST ,
21
20
isConfirmedHookDeclaration ,
22
21
isNonDeclarativePrimitiveHook ,
22
+ mapCompiledLineNumberToOriginalLineNumber ,
23
23
} from './astUtils' ;
24
+ import { sourceMapsAreAppliedToErrors } from './ErrorTester' ;
24
25
25
26
import type {
26
27
HooksNode ,
@@ -29,18 +30,35 @@ import type {
29
30
} from 'react-debug-tools/src/ReactDebugHooks' ;
30
31
import type { HookNames } from 'react-devtools-shared/src/hookNamesCache' ;
31
32
import type { Thenable } from 'shared/ReactTypes' ;
32
- import type { SourceConsumer } from './astUtils' ;
33
+ import type { SourceConsumer , SourceMap } from './astUtils' ;
33
34
34
35
const SOURCE_MAP_REGEX = / ? s o u r c e M a p p i n g U R L = ( [ ^ \s ' " ] + ) / gm;
35
36
const ABSOLUTE_URL_REGEX = / ^ h t t p s ? : \/ \/ / i;
36
37
const MAX_SOURCE_LENGTH = 100_000_000 ;
37
38
38
39
type HookSourceData = { |
40
+ // Generated by react-debug-tools.
39
41
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).
40
54
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.
42
58
sourceMapURL : string | null ,
43
- sourceMapContents : string | null ,
59
+
60
+ // Parsed source map object.
61
+ sourceMapContents : SourceMap | null ,
44
62
| } ;
45
63
46
64
export default async function parseHookNames (
@@ -72,8 +90,10 @@ export default async function parseHookNames(
72
90
if ( ! fileNameToHookSourceData . has ( fileName ) ) {
73
91
fileNameToHookSourceData . set ( fileName , {
74
92
hookSource,
93
+ originalSourceAST : null ,
94
+ originalSourceCode : null ,
95
+ runtimeSourceCode : null ,
75
96
sourceConsumer : null ,
76
- sourceContents : null ,
77
97
sourceMapURL : null ,
78
98
sourceMapContents : null ,
79
99
} ) ;
@@ -85,7 +105,7 @@ export default async function parseHookNames(
85
105
86
106
return loadSourceFiles ( fileNameToHookSourceData )
87
107
. then ( ( ) => extractAndLoadSourceMaps ( fileNameToHookSourceData ) )
88
- . then ( ( ) => parseSourceMaps ( fileNameToHookSourceData ) )
108
+ . then ( ( ) => parseSourceAST ( fileNameToHookSourceData ) )
89
109
. then ( ( ) => findHookNames ( hooksList , fileNameToHookSourceData ) ) ;
90
110
}
91
111
@@ -108,11 +128,10 @@ function extractAndLoadSourceMaps(
108
128
) : Promise < * > {
109
129
const promises = [ ] ;
110
130
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 ) ;
114
133
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() .
116
135
} else {
117
136
for ( let i = 0 ; i < sourceMappingURLs . length ; i ++ ) {
118
137
const sourceMappingURL = sourceMappingURLs [ i ] ;
@@ -217,63 +236,50 @@ function findHookNames(
217
236
return null ; // Should not be reachable.
218
237
}
219
238
220
- let hooksFromAST ;
221
- let potentialReactHookASTNode ;
222
- let sourceCode ;
223
-
224
239
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 (
227
249
sourceConsumer ,
228
250
lineNumber ,
229
251
columnNumber ,
230
252
) ;
253
+ }
231
254
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 ;
264
257
}
265
258
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 ) {
267
272
return null ;
268
273
}
269
274
270
275
// nodesAssociatedWithReactHookASTNode could directly be used to obtain the hook variable name
271
276
// depending on the type of potentialReactHookASTNode
272
277
try {
278
+ const originalSourceCode = ( ( hookSourceData . originalSourceCode : any ) : string ) ;
273
279
const nodesAssociatedWithReactHookASTNode = getFilteredHookASTNodes (
274
280
potentialReactHookASTNode ,
275
281
hooksFromAST ,
276
- sourceCode ,
282
+ originalSourceCode ,
277
283
) ;
278
284
279
285
return getHookName (
@@ -305,19 +311,19 @@ function loadSourceFiles(
305
311
const promises = [ ] ;
306
312
fileNameToHookSourceData . forEach ( ( hookSourceData , fileName ) => {
307
313
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 ) {
310
316
throw Error ( 'Source code too large to parse' ) ;
311
317
}
312
318
313
- hookSourceData . sourceContents = sourceContents ;
319
+ hookSourceData . runtimeSourceCode = runtimeSourceCode ;
314
320
} ) ,
315
321
) ;
316
322
} ) ;
317
323
return Promise . all ( promises ) ;
318
324
}
319
325
320
- async function parseSourceMaps (
326
+ async function parseSourceAST (
321
327
fileNameToHookSourceData : Map < string , HookSourceData > ,
322
328
) : Promise < * > {
323
329
// SourceMapConsumer.initialize() does nothing when running in Node (aka Jest)
@@ -332,19 +338,41 @@ async function parseSourceMaps(
332
338
333
339
const promises = [ ] ;
334
340
fileNameToHookSourceData . forEach ( hookSourceData => {
335
- if ( hookSourceData . sourceMapContents !== null ) {
341
+ const { runtimeSourceCode, sourceMapContents} = hookSourceData ;
342
+ if ( sourceMapContents !== null ) {
336
343
// Parse and extract the AST from the source map.
337
344
promises . push (
338
345
SourceMapConsumer . with (
339
- hookSourceData . sourceMapContents ,
346
+ sourceMapContents ,
340
347
null ,
341
348
( sourceConsumer : SourceConsumer ) => {
342
349
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
+ } ) ;
343
366
} ,
344
367
) ,
345
368
) ;
346
369
} 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
+ } ) ;
348
376
}
349
377
} ) ;
350
378
return Promise . all ( promises ) ;
0 commit comments