@@ -59,7 +59,9 @@ module.exports = {
59
59
const allUsedComponents = new Set ( ) // Track all used components
60
60
const primerReactImports = new Map ( ) // Map of component name to import node
61
61
const styledReactImports = new Map ( ) // Map of components imported from styled-react to import node
62
+ const aliasMapping = new Map ( ) // Map local name to original component name for aliased imports
62
63
const jsxElementsWithSx = [ ] // Track JSX elements that use sx prop
64
+ const jsxElementsWithoutSx = [ ] // Track JSX elements that don't use sx prop
63
65
64
66
return {
65
67
ImportDeclaration ( node ) {
@@ -84,7 +86,13 @@ module.exports = {
84
86
for ( const specifier of node . specifiers ) {
85
87
if ( specifier . type === 'ImportSpecifier' ) {
86
88
const importedName = specifier . imported . name
89
+ const localName = specifier . local . name
87
90
styledReactImports . set ( importedName , { node, specifier} )
91
+
92
+ // Track alias mapping for styled-react imports
93
+ if ( localName !== importedName ) {
94
+ aliasMapping . set ( localName , importedName )
95
+ }
88
96
}
89
97
}
90
98
}
@@ -94,20 +102,33 @@ module.exports = {
94
102
const openingElement = node . openingElement
95
103
const componentName = getJSXOpeningElementName ( openingElement )
96
104
105
+ // Check if this is an aliased component from styled-react
106
+ const originalComponentName = aliasMapping . get ( componentName ) || componentName
107
+
97
108
// Track all used components that are in our styled components list
98
- if ( styledComponents . has ( componentName ) ) {
99
- allUsedComponents . add ( componentName )
109
+ if ( styledComponents . has ( originalComponentName ) ) {
110
+ allUsedComponents . add ( originalComponentName )
100
111
101
112
// Check if this component has an sx prop
102
113
const hasSxProp = openingElement . attributes . some (
103
114
attr => attr . type === 'JSXAttribute' && attr . name && attr . name . name === 'sx' ,
104
115
)
105
116
106
117
if ( hasSxProp ) {
107
- componentsWithSx . add ( componentName )
108
- jsxElementsWithSx . push ( { node, componentName, openingElement} )
118
+ componentsWithSx . add ( originalComponentName )
119
+ jsxElementsWithSx . push ( { node, componentName : originalComponentName , openingElement} )
109
120
} else {
110
- componentsWithoutSx . add ( componentName )
121
+ componentsWithoutSx . add ( originalComponentName )
122
+
123
+ // If this is an aliased component without sx, we need to track it for renaming
124
+ if ( aliasMapping . has ( componentName ) ) {
125
+ jsxElementsWithoutSx . push ( {
126
+ node,
127
+ localName : componentName ,
128
+ originalName : originalComponentName ,
129
+ openingElement,
130
+ } )
131
+ }
111
132
}
112
133
}
113
134
} ,
@@ -261,6 +282,32 @@ module.exports = {
261
282
}
262
283
}
263
284
285
+ // Group styled-react imports that need to be moved to primer-react
286
+ const styledReactImportNodeChanges = new Map ( )
287
+
288
+ // Collect components that need to be moved from styled-react to primer-react
289
+ for ( const componentName of allUsedComponents ) {
290
+ if ( ! componentsWithSx . has ( componentName ) && styledReactImports . has ( componentName ) ) {
291
+ const importInfo = styledReactImports . get ( componentName )
292
+ const { node : importNode } = importInfo
293
+
294
+ if ( ! styledReactImportNodeChanges . has ( importNode ) ) {
295
+ styledReactImportNodeChanges . set ( importNode , {
296
+ toMove : [ ] ,
297
+ originalSpecifiers : [ ...importNode . specifiers ] ,
298
+ } )
299
+ }
300
+
301
+ styledReactImportNodeChanges . get ( importNode ) . toMove . push ( componentName )
302
+ }
303
+ }
304
+
305
+ // Find existing primer-react import nodes to merge with
306
+ const primerReactImportNodes = new Set ( )
307
+ for ( const [ , { node} ] of primerReactImports ) {
308
+ primerReactImportNodes . add ( node )
309
+ }
310
+
264
311
// Report errors for components used WITHOUT sx prop that are imported from @primer /styled-react
265
312
for ( const componentName of allUsedComponents ) {
266
313
// If component is used but NOT with sx prop, and it's imported from styled-react
@@ -271,38 +318,108 @@ module.exports = {
271
318
messageId : 'usePrimerReactImport' ,
272
319
data : { componentName} ,
273
320
fix ( fixer ) {
274
- const { node : importNode , specifier } = importInfo
275
- const otherSpecifiers = importNode . specifiers . filter ( s => s !== specifier )
321
+ const { node : importNode } = importInfo
322
+ const changes = styledReactImportNodeChanges . get ( importNode )
276
323
277
- // If this is the only import, replace the whole import
278
- if ( otherSpecifiers . length === 0 ) {
279
- return fixer . replaceText ( importNode , `import { ${ componentName } } from '@primer/react'` )
324
+ if ( ! changes ) {
325
+ return null
326
+ }
327
+
328
+ // Only apply the fix once per import node (for the first component processed)
329
+ const isFirstComponent = changes . toMove [ 0 ] === componentName
330
+
331
+ if ( ! isFirstComponent ) {
332
+ return null
280
333
}
281
334
282
- // Otherwise, remove from current import and add new import
283
335
const fixes = [ ]
336
+ const componentsToMove = new Set ( changes . toMove )
284
337
285
- // Remove the specifier from current import
286
- if ( importNode . specifiers . length === 1 ) {
338
+ // Find specifiers that remain in styled-react import
339
+ const remainingSpecifiers = changes . originalSpecifiers . filter ( spec => {
340
+ const name = spec . imported . name
341
+ return ! componentsToMove . has ( name )
342
+ } )
343
+
344
+ // Check if there's an existing primer-react import to merge with
345
+ const existingPrimerReactImport = Array . from ( primerReactImportNodes ) [ 0 ]
346
+
347
+ if ( existingPrimerReactImport && remainingSpecifiers . length === 0 ) {
348
+ // Case: No remaining styled-react imports, merge with existing primer-react import
349
+ const existingSpecifiers = existingPrimerReactImport . specifiers . map ( spec => spec . imported . name )
350
+ const newSpecifiers = [ ...existingSpecifiers , ...changes . toMove ] . filter (
351
+ ( name , index , arr ) => arr . indexOf ( name ) === index ,
352
+ )
353
+
354
+ fixes . push (
355
+ fixer . replaceText (
356
+ existingPrimerReactImport ,
357
+ `import { ${ newSpecifiers . join ( ', ' ) } } from '@primer/react'` ,
358
+ ) ,
359
+ )
287
360
fixes . push ( fixer . remove ( importNode ) )
361
+ } else if ( existingPrimerReactImport && remainingSpecifiers . length > 0 ) {
362
+ // Case: Some styled-react imports remain, merge moved components with existing primer-react
363
+ const existingSpecifiers = existingPrimerReactImport . specifiers . map ( spec => spec . imported . name )
364
+ const newSpecifiers = [ ...existingSpecifiers , ...changes . toMove ] . filter (
365
+ ( name , index , arr ) => arr . indexOf ( name ) === index ,
366
+ )
367
+
368
+ fixes . push (
369
+ fixer . replaceText (
370
+ existingPrimerReactImport ,
371
+ `import { ${ newSpecifiers . join ( ', ' ) } } from '@primer/react'` ,
372
+ ) ,
373
+ )
374
+
375
+ const remainingNames = remainingSpecifiers . map ( spec => spec . imported . name )
376
+ fixes . push (
377
+ fixer . replaceText (
378
+ importNode ,
379
+ `import { ${ remainingNames . join ( ', ' ) } } from '@primer/styled-react'` ,
380
+ ) ,
381
+ )
382
+ } else if ( remainingSpecifiers . length === 0 ) {
383
+ // Case: No existing primer-react import, no remaining styled-react imports
384
+ const movedComponents = changes . toMove . join ( ', ' )
385
+ fixes . push ( fixer . replaceText ( importNode , `import { ${ movedComponents } } from '@primer/react'` ) )
288
386
} else {
289
- const isFirst = importNode . specifiers [ 0 ] === specifier
290
- const isLast = importNode . specifiers [ importNode . specifiers . length - 1 ] === specifier
387
+ // Case: No existing primer-react import, some styled-react imports remain
388
+ const remainingNames = remainingSpecifiers . map ( spec => spec . imported . name )
389
+ fixes . push (
390
+ fixer . replaceText (
391
+ importNode ,
392
+ `import { ${ remainingNames . join ( ', ' ) } } from '@primer/styled-react'` ,
393
+ ) ,
394
+ )
291
395
292
- if ( isFirst ) {
293
- const nextSpecifier = importNode . specifiers [ 1 ]
294
- fixes . push ( fixer . removeRange ( [ specifier . range [ 0 ] , nextSpecifier . range [ 0 ] ] ) )
295
- } else if ( isLast ) {
296
- const prevSpecifier = importNode . specifiers [ importNode . specifiers . length - 2 ]
297
- fixes . push ( fixer . removeRange ( [ prevSpecifier . range [ 1 ] , specifier . range [ 1 ] ] ) )
298
- } else {
299
- const nextSpecifier = importNode . specifiers [ importNode . specifiers . indexOf ( specifier ) + 1 ]
300
- fixes . push ( fixer . removeRange ( [ specifier . range [ 0 ] , nextSpecifier . range [ 0 ] ] ) )
301
- }
396
+ const movedComponents = changes . toMove . join ( ', ' )
397
+ fixes . push ( fixer . insertTextAfter ( importNode , `\nimport { ${ movedComponents } } from '@primer/react'` ) )
302
398
}
303
399
304
- // Add new import
305
- fixes . push ( fixer . insertTextAfter ( importNode , `\nimport { ${ componentName } } from '@primer/react'` ) )
400
+ return fixes
401
+ } ,
402
+ } )
403
+ }
404
+ }
405
+
406
+ // Report and fix JSX elements that use aliased components without sx prop
407
+ for ( const { node : jsxNode , originalName, openingElement} of jsxElementsWithoutSx ) {
408
+ if ( ! componentsWithSx . has ( originalName ) && styledReactImports . has ( originalName ) ) {
409
+ context . report ( {
410
+ node : openingElement ,
411
+ messageId : 'usePrimerReactImport' ,
412
+ data : { componentName : originalName } ,
413
+ fix ( fixer ) {
414
+ const fixes = [ ]
415
+
416
+ // Replace the aliased component name with the original component name in JSX opening tag
417
+ fixes . push ( fixer . replaceText ( openingElement . name , originalName ) )
418
+
419
+ // Replace the aliased component name in JSX closing tag if it exists
420
+ if ( jsxNode . closingElement ) {
421
+ fixes . push ( fixer . replaceText ( jsxNode . closingElement . name , originalName ) )
422
+ }
306
423
307
424
return fixes
308
425
} ,
0 commit comments