Skip to content

Commit f163375

Browse files
committed
feat: Enhance use-styled-react-import rule to support multiple components in imports, handling conflicts and aliasing for components used with and without sx prop
1 parent c597a0e commit f163375

File tree

2 files changed

+96
-37
lines changed

2 files changed

+96
-37
lines changed

src/rules/__tests__/use-styled-react-import.test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,17 +179,20 @@ import { Button } from '@primer/react'
179179

180180
// Invalid: Button used both with and without sx prop - should use alias
181181
{
182-
code: `import { Button } from '@primer/react'
182+
code: `import { Button, Link } from '@primer/react'
183183
const Component = () => (
184184
<div>
185+
<Link sx={{ color: 'red' }} />
185186
<Button>Regular button</Button>
186187
<Button sx={{ color: 'red' }}>Styled button</Button>
187188
</div>
188189
)`,
189190
output: `import { Button } from '@primer/react'
191+
import { Link } from '@primer/styled-react'
190192
import { Button as StyledButton } from '@primer/styled-react'
191193
const Component = () => (
192194
<div>
195+
<Link sx={{ color: 'red' }} />
193196
<Button>Regular button</Button>
194197
<StyledButton sx={{ color: 'red' }}>Styled button</StyledButton>
195198
</div>
@@ -199,6 +202,10 @@ import { Button as StyledButton } from '@primer/styled-react'
199202
messageId: 'useStyledReactImportWithAlias',
200203
data: {componentName: 'Button', aliasName: 'StyledButton'},
201204
},
205+
{
206+
messageId: 'useStyledReactImport',
207+
data: {componentName: 'Link'},
208+
},
202209
{
203210
messageId: 'useAliasedComponent',
204211
data: {componentName: 'Button', aliasName: 'StyledButton'},

src/rules/use-styled-react-import.js

Lines changed: 88 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,33 @@ module.exports = {
113113
},
114114

115115
'Program:exit': function () {
116+
// Group components by import node to handle multiple changes to same import
117+
const importNodeChanges = new Map()
118+
119+
// Collect all changes needed for components used with sx prop
120+
for (const componentName of componentsWithSx) {
121+
const importInfo = primerReactImports.get(componentName)
122+
if (importInfo && !styledReactImports.has(componentName)) {
123+
const hasConflict = componentsWithoutSx.has(componentName)
124+
const {node: importNode} = importInfo
125+
126+
if (!importNodeChanges.has(importNode)) {
127+
importNodeChanges.set(importNode, {
128+
toMove: [],
129+
toAlias: [],
130+
originalSpecifiers: [...importNode.specifiers],
131+
})
132+
}
133+
134+
const changes = importNodeChanges.get(importNode)
135+
if (hasConflict) {
136+
changes.toAlias.push(componentName)
137+
} else {
138+
changes.toMove.push(componentName)
139+
}
140+
}
141+
}
142+
116143
// Report errors for components used with sx prop that are imported from @primer/react
117144
for (const componentName of componentsWithSx) {
118145
const importInfo = primerReactImports.get(componentName)
@@ -126,51 +153,76 @@ module.exports = {
126153
data: hasConflict ? {componentName, aliasName: `Styled${componentName}`} : {componentName},
127154
fix(fixer) {
128155
const {node: importNode, specifier} = importInfo
129-
const otherSpecifiers = importNode.specifiers.filter(s => s !== specifier)
156+
const changes = importNodeChanges.get(importNode)
130157

131-
if (hasConflict) {
132-
// Use alias when there's a conflict - keep original import and add aliased import
133-
const aliasName = `Styled${componentName}`
134-
return fixer.insertTextAfter(
135-
importNode,
136-
`\nimport { ${componentName} as ${aliasName} } from '@primer/styled-react'`,
137-
)
138-
} else {
139-
// No conflict - use the original logic
140-
// If this is the only import, replace the whole import
141-
if (otherSpecifiers.length === 0) {
142-
return fixer.replaceText(importNode, `import { ${componentName} } from '@primer/styled-react'`)
143-
}
158+
if (!changes) {
159+
return null
160+
}
144161

145-
// Otherwise, remove from current import and add new import
146-
const fixes = []
162+
// Only apply the fix once per import node (for the first component processed)
163+
const isFirstComponent =
164+
changes.originalSpecifiers[0] === specifier ||
165+
(changes.toMove.length > 0 && changes.toMove[0] === componentName) ||
166+
(changes.toAlias.length > 0 && changes.toAlias[0] === componentName)
147167

148-
// Remove the specifier from current import
149-
if (importNode.specifiers.length === 1) {
150-
fixes.push(fixer.remove(importNode))
151-
} else {
152-
const isFirst = importNode.specifiers[0] === specifier
153-
const isLast = importNode.specifiers[importNode.specifiers.length - 1] === specifier
154-
155-
if (isFirst) {
156-
const nextSpecifier = importNode.specifiers[1]
157-
fixes.push(fixer.removeRange([specifier.range[0], nextSpecifier.range[0]]))
158-
} else if (isLast) {
159-
const prevSpecifier = importNode.specifiers[importNode.specifiers.length - 2]
160-
fixes.push(fixer.removeRange([prevSpecifier.range[1], specifier.range[1]]))
161-
} else {
162-
const nextSpecifier = importNode.specifiers[importNode.specifiers.indexOf(specifier) + 1]
163-
fixes.push(fixer.removeRange([specifier.range[0], nextSpecifier.range[0]]))
164-
}
168+
if (!isFirstComponent) {
169+
return null
170+
}
171+
172+
const fixes = []
173+
const componentsToMove = new Set(changes.toMove)
174+
175+
// Find specifiers that remain in original import
176+
const remainingSpecifiers = changes.originalSpecifiers.filter(spec => {
177+
const name = spec.imported.name
178+
// Keep components that are not being moved (only aliased components stay for non-sx usage)
179+
return !componentsToMove.has(name)
180+
})
181+
182+
// If no components remain, replace with new imports directly
183+
if (remainingSpecifiers.length === 0) {
184+
// Build the new imports to replace the original
185+
const newImports = []
186+
187+
// Add imports for moved components
188+
for (const componentName of changes.toMove) {
189+
newImports.push(`import { ${componentName} } from '@primer/styled-react'`)
190+
}
191+
192+
// Add aliased imports for conflicted components
193+
for (const componentName of changes.toAlias) {
194+
const aliasName = `Styled${componentName}`
195+
newImports.push(`import { ${componentName} as ${aliasName} } from '@primer/styled-react'`)
165196
}
166197

167-
// Add new import
198+
fixes.push(fixer.replaceText(importNode, newImports.join('\n')))
199+
} else {
200+
// Otherwise, update the import to only include remaining components
201+
const remainingNames = remainingSpecifiers.map(spec => spec.imported.name)
168202
fixes.push(
169-
fixer.insertTextAfter(importNode, `\nimport { ${componentName} } from '@primer/styled-react'`),
203+
fixer.replaceText(importNode, `import { ${remainingNames.join(', ')} } from '@primer/react'`),
170204
)
171205

172-
return fixes
206+
// Add new imports for moved components
207+
for (const componentName of changes.toMove) {
208+
fixes.push(
209+
fixer.insertTextAfter(importNode, `\nimport { ${componentName} } from '@primer/styled-react'`),
210+
)
211+
}
212+
213+
// Add new aliased imports for conflicted components
214+
for (const componentName of changes.toAlias) {
215+
const aliasName = `Styled${componentName}`
216+
fixes.push(
217+
fixer.insertTextAfter(
218+
importNode,
219+
`\nimport { ${componentName} as ${aliasName} } from '@primer/styled-react'`,
220+
),
221+
)
222+
}
173223
}
224+
225+
return fixes
174226
},
175227
})
176228
}

0 commit comments

Comments
 (0)