@@ -61,6 +61,12 @@ type AnnotationPlugin = PluginObj<AnnotationPluginPass>;
61
61
export default function componentNameAnnotatePlugin ( { types : t } : typeof Babel ) : AnnotationPlugin {
62
62
return {
63
63
visitor : {
64
+ Program : {
65
+ enter ( path , state ) {
66
+ const fragmentContext = collectFragmentContext ( path ) ;
67
+ state [ 'sentryFragmentContext' ] = fragmentContext ;
68
+ }
69
+ } ,
64
70
FunctionDeclaration ( path , state ) {
65
71
if ( ! path . node . id || ! path . node . id . name ) {
66
72
return ;
@@ -69,14 +75,17 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
69
75
return ;
70
76
}
71
77
78
+ const fragmentContext = state [ 'sentryFragmentContext' ] as FragmentContext | undefined ;
79
+
72
80
functionBodyPushAttributes (
73
81
state . opts [ "annotate-fragments" ] === true ,
74
82
t ,
75
83
path ,
76
84
path . node . id . name ,
77
85
sourceFileNameFromState ( state ) ,
78
86
attributeNamesFromState ( state ) ,
79
- state . opts . ignoredComponents ?? [ ]
87
+ state . opts . ignoredComponents ?? [ ] ,
88
+ fragmentContext
80
89
) ;
81
90
} ,
82
91
ArrowFunctionExpression ( path , state ) {
@@ -97,14 +106,17 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
97
106
return ;
98
107
}
99
108
109
+ const fragmentContext = state [ 'sentryFragmentContext' ] as FragmentContext | undefined ;
110
+
100
111
functionBodyPushAttributes (
101
112
state . opts [ "annotate-fragments" ] === true ,
102
113
t ,
103
114
path ,
104
115
parent . id . name ,
105
116
sourceFileNameFromState ( state ) ,
106
117
attributeNamesFromState ( state ) ,
107
- state . opts . ignoredComponents ?? [ ]
118
+ state . opts . ignoredComponents ?? [ ] ,
119
+ fragmentContext
108
120
) ;
109
121
} ,
110
122
ClassDeclaration ( path , state ) {
@@ -120,6 +132,8 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
120
132
121
133
const ignoredComponents = state . opts . ignoredComponents ?? [ ] ;
122
134
135
+ const fragmentContext = state [ 'sentryFragmentContext' ] as FragmentContext | undefined ;
136
+
123
137
render . traverse ( {
124
138
ReturnStatement ( returnStatement ) {
125
139
const arg = returnStatement . get ( "argument" ) ;
@@ -135,7 +149,8 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
135
149
name . node && name . node . name ,
136
150
sourceFileNameFromState ( state ) ,
137
151
attributeNamesFromState ( state ) ,
138
- ignoredComponents
152
+ ignoredComponents ,
153
+ fragmentContext
139
154
) ;
140
155
} ,
141
156
} ) ;
@@ -151,7 +166,8 @@ function functionBodyPushAttributes(
151
166
componentName : string ,
152
167
sourceFileName : string | undefined ,
153
168
attributeNames : string [ ] ,
154
- ignoredComponents : string [ ]
169
+ ignoredComponents : string [ ] ,
170
+ fragmentContext ?: FragmentContext
155
171
) : void {
156
172
let jsxNode : Babel . NodePath ;
157
173
@@ -200,7 +216,8 @@ function functionBodyPushAttributes(
200
216
componentName ,
201
217
sourceFileName ,
202
218
attributeNames ,
203
- ignoredComponents
219
+ ignoredComponents ,
220
+ fragmentContext
204
221
) ;
205
222
}
206
223
const alternate = arg . get ( "alternate" ) ;
@@ -212,7 +229,8 @@ function functionBodyPushAttributes(
212
229
componentName ,
213
230
sourceFileName ,
214
231
attributeNames ,
215
- ignoredComponents
232
+ ignoredComponents ,
233
+ fragmentContext
216
234
) ;
217
235
}
218
236
return ;
@@ -236,7 +254,8 @@ function functionBodyPushAttributes(
236
254
componentName ,
237
255
sourceFileName ,
238
256
attributeNames ,
239
- ignoredComponents
257
+ ignoredComponents ,
258
+ fragmentContext
240
259
) ;
241
260
}
242
261
@@ -247,7 +266,8 @@ function processJSX(
247
266
componentName : string | null ,
248
267
sourceFileName : string | undefined ,
249
268
attributeNames : string [ ] ,
250
- ignoredComponents : string [ ]
269
+ ignoredComponents : string [ ] ,
270
+ fragmentContext ?: FragmentContext
251
271
) : void {
252
272
if ( ! jsxNode ) {
253
273
return ;
@@ -264,7 +284,8 @@ function processJSX(
264
284
componentName ,
265
285
sourceFileName ,
266
286
attributeNames ,
267
- ignoredComponents
287
+ ignoredComponents ,
288
+ fragmentContext
268
289
) ;
269
290
} ) ;
270
291
@@ -300,7 +321,8 @@ function processJSX(
300
321
componentName ,
301
322
sourceFileName ,
302
323
attributeNames ,
303
- ignoredComponents
324
+ ignoredComponents ,
325
+ fragmentContext
304
326
) ;
305
327
} else {
306
328
processJSX (
@@ -310,7 +332,8 @@ function processJSX(
310
332
null ,
311
333
sourceFileName ,
312
334
attributeNames ,
313
- ignoredComponents
335
+ ignoredComponents ,
336
+ fragmentContext
314
337
) ;
315
338
}
316
339
} ) ;
@@ -322,11 +345,12 @@ function applyAttributes(
322
345
componentName : string | null ,
323
346
sourceFileName : string | undefined ,
324
347
attributeNames : string [ ] ,
325
- ignoredComponents : string [ ]
348
+ ignoredComponents : string [ ] ,
349
+ fragmentContext ?: FragmentContext
326
350
) : void {
327
351
const [ componentAttributeName , elementAttributeName , sourceFileAttributeName ] = attributeNames ;
328
352
329
- if ( isReactFragment ( t , openingElement ) ) {
353
+ if ( isReactFragment ( t , openingElement , fragmentContext ) ) {
330
354
return ;
331
355
}
332
356
// e.g., Raw JSX text like the `A` in `<h1>a</h1>`
@@ -443,18 +467,106 @@ function attributeNamesFromState(state: AnnotationPluginPass): [string, string,
443
467
return [ webComponentName , webElementName , webSourceFileName ] ;
444
468
}
445
469
446
- function isReactFragment ( t : typeof Babel . types , openingElement : Babel . NodePath ) : boolean {
470
+ interface FragmentContext {
471
+ fragmentAliases : Set < string > ;
472
+ reactNamespaceAliases : Set < string > ;
473
+ }
474
+
475
+ function collectFragmentContext ( programPath : Babel . NodePath ) : FragmentContext {
476
+ const fragmentAliases = new Set < string > ( ) ;
477
+ const reactNamespaceAliases = new Set < string > ( [ 'React' ] ) ; // Default React namespace
478
+
479
+ programPath . traverse ( {
480
+ ImportDeclaration ( importPath ) {
481
+ const source = importPath . node . source . value ;
482
+
483
+ // Handle React imports
484
+ if ( source === 'react' || source === 'React' ) {
485
+ importPath . node . specifiers . forEach ( spec => {
486
+ if ( spec . type === 'ImportSpecifier' && spec . imported . type === 'Identifier' ) {
487
+ // import { Fragment } from 'react' -> Fragment
488
+ // import { Fragment as F } from 'react' -> F
489
+ if ( spec . imported . name === 'Fragment' ) {
490
+ fragmentAliases . add ( spec . local . name ) ;
491
+ }
492
+ } else if ( spec . type === 'ImportDefaultSpecifier' ) {
493
+ // import React from 'react' -> React
494
+ reactNamespaceAliases . add ( spec . local . name ) ;
495
+ } else if ( spec . type === 'ImportNamespaceSpecifier' ) {
496
+ // import * as React from 'react' -> React
497
+ reactNamespaceAliases . add ( spec . local . name ) ;
498
+ }
499
+ } ) ;
500
+ }
501
+ } ,
502
+
503
+ // Handle simple variable assignments only (avoid complex cases)
504
+ VariableDeclarator ( varPath ) {
505
+ if ( varPath . node . init ) {
506
+ const init = varPath . node . init ;
507
+
508
+ // Handle identifier assignments: const MyFragment = Fragment
509
+ if ( varPath . node . id . type === 'Identifier' ) {
510
+ // Handle: const MyFragment = Fragment (only if Fragment is a known alias)
511
+ if ( init . type === 'Identifier' && fragmentAliases . has ( init . name ) ) {
512
+ fragmentAliases . add ( varPath . node . id . name ) ;
513
+ }
514
+
515
+ // Handle: const MyFragment = React.Fragment (only for known React namespaces)
516
+ if ( init . type === 'MemberExpression' &&
517
+ init . object . type === 'Identifier' &&
518
+ init . property . type === 'Identifier' &&
519
+ reactNamespaceAliases . has ( init . object . name ) &&
520
+ init . property . name === 'Fragment' ) {
521
+ fragmentAliases . add ( varPath . node . id . name ) ;
522
+ }
523
+ }
524
+
525
+ // Handle destructuring assignments: const { Fragment } = React
526
+ if ( varPath . node . id . type === 'ObjectPattern' ) {
527
+ if ( init . type === 'Identifier' && reactNamespaceAliases . has ( init . name ) ) {
528
+ ( varPath . node . id as any ) . properties . forEach ( ( prop : any ) => {
529
+ if ( prop . type === 'ObjectProperty' &&
530
+ prop . key ?. type === 'Identifier' &&
531
+ prop . value ?. type === 'Identifier' &&
532
+ prop . key . name === 'Fragment' ) {
533
+ fragmentAliases . add ( prop . value . name ) ;
534
+ }
535
+ } ) ;
536
+ }
537
+ }
538
+ }
539
+ }
540
+ } ) ;
541
+
542
+ return { fragmentAliases, reactNamespaceAliases } ;
543
+ }
544
+
545
+ function isReactFragment (
546
+ t : typeof Babel . types ,
547
+ openingElement : Babel . NodePath ,
548
+ context ?: FragmentContext // Add this optional parameter
549
+ ) : boolean {
550
+ // Handle JSX fragments (<>)
447
551
if ( openingElement . isJSXFragment ( ) ) {
448
552
return true ;
449
553
}
450
554
451
555
const elementName = getPathName ( t , openingElement ) ;
452
556
557
+ // Direct fragment references
453
558
if ( elementName === "Fragment" || elementName === "React.Fragment" ) {
454
559
return true ;
455
560
}
456
561
457
562
// TODO: All these objects are typed as unknown, maybe an oversight in Babel types?
563
+
564
+ // Check if the element name is a known fragment alias
565
+ if ( context && elementName && context . fragmentAliases . has ( elementName ) ) {
566
+ return true ;
567
+ }
568
+
569
+ // Handle JSXMemberExpression
458
570
if (
459
571
openingElement . node &&
460
572
"name" in openingElement . node &&
@@ -463,10 +575,6 @@ function isReactFragment(t: typeof Babel.types, openingElement: Babel.NodePath):
463
575
"type" in openingElement . node . name &&
464
576
openingElement . node . name . type === "JSXMemberExpression"
465
577
) {
466
- if ( ! ( "name" in openingElement . node ) ) {
467
- return false ;
468
- }
469
-
470
578
const nodeName = openingElement . node . name ;
471
579
if ( typeof nodeName !== "object" || ! nodeName ) {
472
580
return false ;
@@ -487,9 +595,23 @@ function isReactFragment(t: typeof Babel.types, openingElement: Babel.NodePath):
487
595
const objectName = "name" in nodeNameObject && nodeNameObject . name ;
488
596
const propertyName = "name" in nodeNameProperty && nodeNameProperty . name ;
489
597
598
+ // React.Fragment check
490
599
if ( objectName === "React" && propertyName === "Fragment" ) {
491
600
return true ;
492
601
}
602
+
603
+ // Enhanced checks using context
604
+ if ( context ) {
605
+ // Check React.Fragment pattern with known React namespaces
606
+ if ( context . reactNamespaceAliases . has ( objectName as string ) && propertyName === "Fragment" ) {
607
+ return true ;
608
+ }
609
+
610
+ // Check MyFragment.Fragment pattern
611
+ if ( context . fragmentAliases . has ( objectName as string ) && propertyName === "Fragment" ) {
612
+ return true ;
613
+ }
614
+ }
493
615
}
494
616
}
495
617
0 commit comments