Skip to content

Commit c7e25ae

Browse files
committed
feat: Update use-styled-react-import rule to handle components used with and without sx prop, including aliasing for conflicts
1 parent 881b330 commit c7e25ae

File tree

3 files changed

+136
-37
lines changed

3 files changed

+136
-37
lines changed

docs/rules/use-styled-react-import.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ Enforce importing components that use `sx` prop from `@primer/styled-react` inst
1010

1111
## Rule Details
1212

13-
This rule detects when certain Primer React components are used with the `sx` prop and ensures they are imported from the temporary `@primer/styled-react` package instead of `@primer/react`. When the same components are used without the `sx` prop, it ensures they are imported from `@primer/react` instead of `@primer/styled-react`. It also moves certain types and utilities to the styled-react package.
13+
This rule detects when certain Primer React components are used with the `sx` prop and ensures they are imported from the temporary `@primer/styled-react` package instead of `@primer/react`. When the same components are used without the `sx` prop, it ensures they are imported from `@primer/react` instead of `@primer/styled-react`.
14+
15+
When a component is used both with and without the `sx` prop in the same file, the rule will import the styled version with an alias (e.g., `StyledButton`) and update the JSX usage accordingly to avoid naming conflicts.
16+
17+
It also moves certain types and utilities to the styled-react package.
1418

1519
### Components that should be imported from `@primer/styled-react` when used with `sx`:
1620

@@ -66,6 +70,13 @@ import {Button} from '@primer/styled-react'
6670
const Component = () => <Button>Click me</Button>
6771
```
6872

73+
```jsx
74+
import {Button} from '@primer/react'
75+
76+
const Component1 = () => <Button>Click me</Button>
77+
const Component2 = () => <Button sx={{color: 'red'}}>Styled me</Button>
78+
```
79+
6980
### ✅ Correct
7081

7182
```jsx
@@ -99,6 +110,15 @@ import {Button} from '@primer/react'
99110
const Component = () => <Button>Click me</Button>
100111
```
101112

113+
```jsx
114+
// When a component is used both ways, use an alias for the styled version
115+
import {Button} from '@primer/react'
116+
import {Button as StyledButton} from '@primer/styled-react'
117+
118+
const Component1 = () => <Button>Click me</Button>
119+
const Component2 = () => <StyledButton sx={{color: 'red'}}>Styled me</StyledButton>
120+
```
121+
102122
## Options
103123

104124
This rule has no options.

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,5 +172,34 @@ import { Button } from '@primer/react'
172172
},
173173
],
174174
},
175+
176+
// Invalid: Button used both with and without sx prop - should use alias
177+
{
178+
code: `import { Button } from '@primer/react'
179+
const Component = () => (
180+
<div>
181+
<Button>Regular button</Button>
182+
<Button sx={{ color: 'red' }}>Styled button</Button>
183+
</div>
184+
)`,
185+
output: `import { Button } from '@primer/react'
186+
import { Button as StyledButton } from '@primer/styled-react'
187+
const Component = () => (
188+
<div>
189+
<Button>Regular button</Button>
190+
<StyledButton sx={{ color: 'red' }}>Styled button</StyledButton>
191+
</div>
192+
)`,
193+
errors: [
194+
{
195+
messageId: 'useStyledReactImportWithAlias',
196+
data: {componentName: 'Button', aliasName: 'StyledButton'},
197+
},
198+
{
199+
messageId: 'useAliasedComponent',
200+
data: {componentName: 'Button', aliasName: 'StyledButton'},
201+
},
202+
],
203+
},
175204
],
176205
})

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

Lines changed: 86 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,20 @@ module.exports = {
4646
schema: [],
4747
messages: {
4848
useStyledReactImport: 'Import {{ componentName }} from "@primer/styled-react" when using with sx prop',
49+
useStyledReactImportWithAlias:
50+
'Import {{ componentName }} as {{ aliasName }} from "@primer/styled-react" when using with sx prop (conflicts with non-sx usage)',
51+
useAliasedComponent: 'Use {{ aliasName }} instead of {{ componentName }} when using sx prop',
4952
moveToStyledReact: 'Move {{ importName }} import to "@primer/styled-react"',
5053
usePrimerReactImport: 'Import {{ componentName }} from "@primer/react" when not using sx prop',
5154
},
5255
},
5356
create(context) {
5457
const componentsWithSx = new Set()
58+
const componentsWithoutSx = new Set() // Track components used without sx
5559
const allUsedComponents = new Set() // Track all used components
5660
const primerReactImports = new Map() // Map of component name to import node
5761
const styledReactImports = new Map() // Map of components imported from styled-react to import node
62+
const jsxElementsWithSx = [] // Track JSX elements that use sx prop
5863

5964
return {
6065
ImportDeclaration(node) {
@@ -85,21 +90,25 @@ module.exports = {
8590
}
8691
},
8792

88-
JSXOpeningElement(node) {
89-
const componentName = getJSXOpeningElementName(node)
93+
JSXElement(node) {
94+
const openingElement = node.openingElement
95+
const componentName = getJSXOpeningElementName(openingElement)
9096

9197
// Track all used components that are in our styled components list
9298
if (styledComponents.has(componentName)) {
9399
allUsedComponents.add(componentName)
94-
}
95100

96-
// Check if this component has an sx prop
97-
const hasSxProp = node.attributes.some(
98-
attr => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'sx',
99-
)
101+
// Check if this component has an sx prop
102+
const hasSxProp = openingElement.attributes.some(
103+
attr => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'sx',
104+
)
100105

101-
if (hasSxProp && styledComponents.has(componentName)) {
102-
componentsWithSx.add(componentName)
106+
if (hasSxProp) {
107+
componentsWithSx.add(componentName)
108+
jsxElementsWithSx.push({node, componentName, openingElement})
109+
} else {
110+
componentsWithoutSx.add(componentName)
111+
}
103112
}
104113
},
105114

@@ -108,45 +117,86 @@ module.exports = {
108117
for (const componentName of componentsWithSx) {
109118
const importInfo = primerReactImports.get(componentName)
110119
if (importInfo && !styledReactImports.has(componentName)) {
120+
// Check if this component is also used without sx prop (conflict scenario)
121+
const hasConflict = componentsWithoutSx.has(componentName)
122+
111123
context.report({
112124
node: importInfo.specifier,
113-
messageId: 'useStyledReactImport',
114-
data: {componentName},
125+
messageId: hasConflict ? 'useStyledReactImportWithAlias' : 'useStyledReactImport',
126+
data: hasConflict ? {componentName, aliasName: `Styled${componentName}`} : {componentName},
115127
fix(fixer) {
116128
const {node: importNode, specifier} = importInfo
117129
const otherSpecifiers = importNode.specifiers.filter(s => s !== specifier)
118130

119-
// If this is the only import, replace the whole import
120-
if (otherSpecifiers.length === 0) {
121-
return fixer.replaceText(importNode, `import { ${componentName} } from '@primer/styled-react'`)
122-
}
123-
124-
// Otherwise, remove from current import and add new import
125-
const fixes = []
126-
127-
// Remove the specifier from current import
128-
if (importNode.specifiers.length === 1) {
129-
fixes.push(fixer.remove(importNode))
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+
)
130138
} else {
131-
const isFirst = importNode.specifiers[0] === specifier
132-
const isLast = importNode.specifiers[importNode.specifiers.length - 1] === specifier
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+
}
133144

134-
if (isFirst) {
135-
const nextSpecifier = importNode.specifiers[1]
136-
fixes.push(fixer.removeRange([specifier.range[0], nextSpecifier.range[0]]))
137-
} else if (isLast) {
138-
const prevSpecifier = importNode.specifiers[importNode.specifiers.length - 2]
139-
fixes.push(fixer.removeRange([prevSpecifier.range[1], specifier.range[1]]))
145+
// Otherwise, remove from current import and add new import
146+
const fixes = []
147+
148+
// Remove the specifier from current import
149+
if (importNode.specifiers.length === 1) {
150+
fixes.push(fixer.remove(importNode))
140151
} else {
141-
const nextSpecifier = importNode.specifiers[importNode.specifiers.indexOf(specifier) + 1]
142-
fixes.push(fixer.removeRange([specifier.range[0], nextSpecifier.range[0]]))
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+
}
143165
}
166+
167+
// Add new import
168+
fixes.push(
169+
fixer.insertTextAfter(importNode, `\nimport { ${componentName} } from '@primer/styled-react'`),
170+
)
171+
172+
return fixes
144173
}
174+
},
175+
})
176+
}
177+
}
145178

146-
// Add new import
147-
fixes.push(
148-
fixer.insertTextAfter(importNode, `\nimport { ${componentName} } from '@primer/styled-react'`),
149-
)
179+
// Report on JSX elements that should use aliased components
180+
for (const {node: jsxNode, componentName, openingElement} of jsxElementsWithSx) {
181+
const hasConflict = componentsWithoutSx.has(componentName)
182+
const isImportedFromPrimerReact = primerReactImports.has(componentName)
183+
184+
if (hasConflict && isImportedFromPrimerReact && !styledReactImports.has(componentName)) {
185+
const aliasName = `Styled${componentName}`
186+
context.report({
187+
node: openingElement,
188+
messageId: 'useAliasedComponent',
189+
data: {componentName, aliasName},
190+
fix(fixer) {
191+
const fixes = []
192+
193+
// Replace the component name in the JSX opening tag
194+
fixes.push(fixer.replaceText(openingElement.name, aliasName))
195+
196+
// Replace the component name in the JSX closing tag if it exists
197+
if (jsxNode.closingElement) {
198+
fixes.push(fixer.replaceText(jsxNode.closingElement.name, aliasName))
199+
}
150200

151201
return fixes
152202
},

0 commit comments

Comments
 (0)