@@ -61,6 +61,12 @@ type AnnotationPlugin = PluginObj<AnnotationPluginPass>;
6161export default function componentNameAnnotatePlugin ( { types : t } : typeof Babel ) : AnnotationPlugin {
6262 return {
6363 visitor : {
64+ Program : {
65+ enter ( path , state ) {
66+ const fragmentContext = collectFragmentContext ( path ) ;
67+ state [ 'sentryFragmentContext' ] = fragmentContext ;
68+ }
69+ } ,
6470 FunctionDeclaration ( path , state ) {
6571 if ( ! path . node . id || ! path . node . id . name ) {
6672 return ;
@@ -69,14 +75,17 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
6975 return ;
7076 }
7177
78+ const fragmentContext = state [ 'sentryFragmentContext' ] as FragmentContext | undefined ;
79+
7280 functionBodyPushAttributes (
7381 state . opts [ "annotate-fragments" ] === true ,
7482 t ,
7583 path ,
7684 path . node . id . name ,
7785 sourceFileNameFromState ( state ) ,
7886 attributeNamesFromState ( state ) ,
79- state . opts . ignoredComponents ?? [ ]
87+ state . opts . ignoredComponents ?? [ ] ,
88+ fragmentContext
8089 ) ;
8190 } ,
8291 ArrowFunctionExpression ( path , state ) {
@@ -97,14 +106,17 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
97106 return ;
98107 }
99108
109+ const fragmentContext = state [ 'sentryFragmentContext' ] as FragmentContext | undefined ;
110+
100111 functionBodyPushAttributes (
101112 state . opts [ "annotate-fragments" ] === true ,
102113 t ,
103114 path ,
104115 parent . id . name ,
105116 sourceFileNameFromState ( state ) ,
106117 attributeNamesFromState ( state ) ,
107- state . opts . ignoredComponents ?? [ ]
118+ state . opts . ignoredComponents ?? [ ] ,
119+ fragmentContext
108120 ) ;
109121 } ,
110122 ClassDeclaration ( path , state ) {
@@ -120,6 +132,8 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
120132
121133 const ignoredComponents = state . opts . ignoredComponents ?? [ ] ;
122134
135+ const fragmentContext = state [ 'sentryFragmentContext' ] as FragmentContext | undefined ;
136+
123137 render . traverse ( {
124138 ReturnStatement ( returnStatement ) {
125139 const arg = returnStatement . get ( "argument" ) ;
@@ -135,7 +149,8 @@ export default function componentNameAnnotatePlugin({ types: t }: typeof Babel):
135149 name . node && name . node . name ,
136150 sourceFileNameFromState ( state ) ,
137151 attributeNamesFromState ( state ) ,
138- ignoredComponents
152+ ignoredComponents ,
153+ fragmentContext
139154 ) ;
140155 } ,
141156 } ) ;
@@ -151,7 +166,8 @@ function functionBodyPushAttributes(
151166 componentName : string ,
152167 sourceFileName : string | undefined ,
153168 attributeNames : string [ ] ,
154- ignoredComponents : string [ ]
169+ ignoredComponents : string [ ] ,
170+ fragmentContext ?: FragmentContext
155171) : void {
156172 let jsxNode : Babel . NodePath ;
157173
@@ -200,7 +216,8 @@ function functionBodyPushAttributes(
200216 componentName ,
201217 sourceFileName ,
202218 attributeNames ,
203- ignoredComponents
219+ ignoredComponents ,
220+ fragmentContext
204221 ) ;
205222 }
206223 const alternate = arg . get ( "alternate" ) ;
@@ -212,7 +229,8 @@ function functionBodyPushAttributes(
212229 componentName ,
213230 sourceFileName ,
214231 attributeNames ,
215- ignoredComponents
232+ ignoredComponents ,
233+ fragmentContext
216234 ) ;
217235 }
218236 return ;
@@ -236,7 +254,8 @@ function functionBodyPushAttributes(
236254 componentName ,
237255 sourceFileName ,
238256 attributeNames ,
239- ignoredComponents
257+ ignoredComponents ,
258+ fragmentContext
240259 ) ;
241260}
242261
@@ -247,7 +266,8 @@ function processJSX(
247266 componentName : string | null ,
248267 sourceFileName : string | undefined ,
249268 attributeNames : string [ ] ,
250- ignoredComponents : string [ ]
269+ ignoredComponents : string [ ] ,
270+ fragmentContext ?: FragmentContext
251271) : void {
252272 if ( ! jsxNode ) {
253273 return ;
@@ -264,7 +284,8 @@ function processJSX(
264284 componentName ,
265285 sourceFileName ,
266286 attributeNames ,
267- ignoredComponents
287+ ignoredComponents ,
288+ fragmentContext
268289 ) ;
269290 } ) ;
270291
@@ -300,7 +321,8 @@ function processJSX(
300321 componentName ,
301322 sourceFileName ,
302323 attributeNames ,
303- ignoredComponents
324+ ignoredComponents ,
325+ fragmentContext
304326 ) ;
305327 } else {
306328 processJSX (
@@ -310,7 +332,8 @@ function processJSX(
310332 null ,
311333 sourceFileName ,
312334 attributeNames ,
313- ignoredComponents
335+ ignoredComponents ,
336+ fragmentContext
314337 ) ;
315338 }
316339 } ) ;
@@ -322,11 +345,12 @@ function applyAttributes(
322345 componentName : string | null ,
323346 sourceFileName : string | undefined ,
324347 attributeNames : string [ ] ,
325- ignoredComponents : string [ ]
348+ ignoredComponents : string [ ] ,
349+ fragmentContext ?: FragmentContext
326350) : void {
327351 const [ componentAttributeName , elementAttributeName , sourceFileAttributeName ] = attributeNames ;
328352
329- if ( isReactFragment ( t , openingElement ) ) {
353+ if ( isReactFragment ( t , openingElement , fragmentContext ) ) {
330354 return ;
331355 }
332356 // e.g., Raw JSX text like the `A` in `<h1>a</h1>`
@@ -443,18 +467,106 @@ function attributeNamesFromState(state: AnnotationPluginPass): [string, string,
443467 return [ webComponentName , webElementName , webSourceFileName ] ;
444468}
445469
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 (<>)
447551 if ( openingElement . isJSXFragment ( ) ) {
448552 return true ;
449553 }
450554
451555 const elementName = getPathName ( t , openingElement ) ;
452556
557+ // Direct fragment references
453558 if ( elementName === "Fragment" || elementName === "React.Fragment" ) {
454559 return true ;
455560 }
456561
457562 // 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
458570 if (
459571 openingElement . node &&
460572 "name" in openingElement . node &&
@@ -463,10 +575,6 @@ function isReactFragment(t: typeof Babel.types, openingElement: Babel.NodePath):
463575 "type" in openingElement . node . name &&
464576 openingElement . node . name . type === "JSXMemberExpression"
465577 ) {
466- if ( ! ( "name" in openingElement . node ) ) {
467- return false ;
468- }
469-
470578 const nodeName = openingElement . node . name ;
471579 if ( typeof nodeName !== "object" || ! nodeName ) {
472580 return false ;
@@ -487,9 +595,23 @@ function isReactFragment(t: typeof Babel.types, openingElement: Babel.NodePath):
487595 const objectName = "name" in nodeNameObject && nodeNameObject . name ;
488596 const propertyName = "name" in nodeNameProperty && nodeNameProperty . name ;
489597
598+ // React.Fragment check
490599 if ( objectName === "React" && propertyName === "Fragment" ) {
491600 return true ;
492601 }
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+ }
493615 }
494616 }
495617
0 commit comments