Skip to content

Commit 881b330

Browse files
committed
feat: Enhance use-styled-react-import rule to enforce correct imports for components without sx prop
1 parent e33e87d commit 881b330

File tree

3 files changed

+131
-5
lines changed

3 files changed

+131
-5
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77
<!-- end auto-generated rule header -->
88

9-
Enforce importing components that use `sx` prop from `@primer/styled-react` instead of `@primer/react`.
9+
Enforce importing components that use `sx` prop from `@primer/styled-react` instead of `@primer/react`, and vice versa.
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`. 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`. It also moves certain types and utilities to the styled-react package.
1414

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

@@ -60,6 +60,12 @@ const Component = () => <Box sx={{padding: 2}}>Content</Box>
6060
import {sx} from '@primer/react'
6161
```
6262

63+
```jsx
64+
import {Button} from '@primer/styled-react'
65+
66+
const Component = () => <Button>Click me</Button>
67+
```
68+
6369
### ✅ Correct
6470

6571
```jsx
@@ -86,6 +92,13 @@ import {Button} from '@primer/react'
8692
const Component = () => <Button>Click me</Button>
8793
```
8894

95+
```jsx
96+
// Components imported from styled-react but used without sx prop should be moved back
97+
import {Button} from '@primer/react'
98+
99+
const Component = () => <Button>Click me</Button>
100+
```
101+
89102
## Options
90103

91104
This rule has no options.

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

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ ruleTester.run('use-styled-react-import', rule, {
3333
// Valid: Mixed imports - component without sx prop
3434
`import { Button, Text } from '@primer/react'
3535
const Component = () => <Button>Click me</Button>`,
36+
37+
// Valid: Component without sx prop imported from styled-react (when not used)
38+
`import { Button } from '@primer/styled-react'`,
3639
],
3740
invalid: [
3841
// Invalid: Box with sx prop imported from @primer/react
@@ -116,5 +119,58 @@ import { Button } from '@primer/styled-react'
116119
},
117120
],
118121
},
122+
123+
// Invalid: Button imported from styled-react but used without sx prop
124+
{
125+
code: `import { Button } from '@primer/styled-react'
126+
const Component = () => <Button>Click me</Button>`,
127+
output: `import { Button } from '@primer/react'
128+
const Component = () => <Button>Click me</Button>`,
129+
errors: [
130+
{
131+
messageId: 'usePrimerReactImport',
132+
data: {componentName: 'Button'},
133+
},
134+
],
135+
},
136+
137+
// Invalid: Box imported from styled-react but used without sx prop
138+
{
139+
code: `import { Box } from '@primer/styled-react'
140+
const Component = () => <Box>Content</Box>`,
141+
output: `import { Box } from '@primer/react'
142+
const Component = () => <Box>Content</Box>`,
143+
errors: [
144+
{
145+
messageId: 'usePrimerReactImport',
146+
data: {componentName: 'Box'},
147+
},
148+
],
149+
},
150+
151+
// Invalid: Multiple components from styled-react, one used without sx
152+
{
153+
code: `import { Button, Box } from '@primer/styled-react'
154+
const Component = () => (
155+
<div>
156+
<Button>Regular button</Button>
157+
<Box sx={{ padding: 2 }}>Styled box</Box>
158+
</div>
159+
)`,
160+
output: `import { Box } from '@primer/styled-react'
161+
import { Button } from '@primer/react'
162+
const Component = () => (
163+
<div>
164+
<Button>Regular button</Button>
165+
<Box sx={{ padding: 2 }}>Styled box</Box>
166+
</div>
167+
)`,
168+
errors: [
169+
{
170+
messageId: 'usePrimerReactImport',
171+
data: {componentName: 'Button'},
172+
},
173+
],
174+
},
119175
],
120176
})

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

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ module.exports = {
4747
messages: {
4848
useStyledReactImport: 'Import {{ componentName }} from "@primer/styled-react" when using with sx prop',
4949
moveToStyledReact: 'Move {{ importName }} import to "@primer/styled-react"',
50+
usePrimerReactImport: 'Import {{ componentName }} from "@primer/react" when not using sx prop',
5051
},
5152
},
5253
create(context) {
5354
const componentsWithSx = new Set()
55+
const allUsedComponents = new Set() // Track all used components
5456
const primerReactImports = new Map() // Map of component name to import node
55-
const styledReactImports = new Set() // Set of components already imported from styled-react
57+
const styledReactImports = new Map() // Map of components imported from styled-react to import node
5658

5759
return {
5860
ImportDeclaration(node) {
@@ -73,10 +75,11 @@ module.exports = {
7375
}
7476
}
7577
} else if (importSource === '@primer/styled-react') {
76-
// Track what's already imported from styled-react
78+
// Track what's imported from styled-react
7779
for (const specifier of node.specifiers) {
7880
if (specifier.type === 'ImportSpecifier') {
79-
styledReactImports.add(specifier.imported.name)
81+
const importedName = specifier.imported.name
82+
styledReactImports.set(importedName, {node, specifier})
8083
}
8184
}
8285
}
@@ -85,6 +88,11 @@ module.exports = {
8588
JSXOpeningElement(node) {
8689
const componentName = getJSXOpeningElementName(node)
8790

91+
// Track all used components that are in our styled components list
92+
if (styledComponents.has(componentName)) {
93+
allUsedComponents.add(componentName)
94+
}
95+
8896
// Check if this component has an sx prop
8997
const hasSxProp = node.attributes.some(
9098
attr => attr.type === 'JSXAttribute' && attr.name && attr.name.name === 'sx',
@@ -146,6 +154,55 @@ module.exports = {
146154
}
147155
}
148156

157+
// Report errors for components used WITHOUT sx prop that are imported from @primer/styled-react
158+
for (const componentName of allUsedComponents) {
159+
// If component is used but NOT with sx prop, and it's imported from styled-react
160+
if (!componentsWithSx.has(componentName) && styledReactImports.has(componentName)) {
161+
const importInfo = styledReactImports.get(componentName)
162+
context.report({
163+
node: importInfo.specifier,
164+
messageId: 'usePrimerReactImport',
165+
data: {componentName},
166+
fix(fixer) {
167+
const {node: importNode, specifier} = importInfo
168+
const otherSpecifiers = importNode.specifiers.filter(s => s !== specifier)
169+
170+
// If this is the only import, replace the whole import
171+
if (otherSpecifiers.length === 0) {
172+
return fixer.replaceText(importNode, `import { ${componentName} } from '@primer/react'`)
173+
}
174+
175+
// Otherwise, remove from current import and add new import
176+
const fixes = []
177+
178+
// Remove the specifier from current import
179+
if (importNode.specifiers.length === 1) {
180+
fixes.push(fixer.remove(importNode))
181+
} else {
182+
const isFirst = importNode.specifiers[0] === specifier
183+
const isLast = importNode.specifiers[importNode.specifiers.length - 1] === specifier
184+
185+
if (isFirst) {
186+
const nextSpecifier = importNode.specifiers[1]
187+
fixes.push(fixer.removeRange([specifier.range[0], nextSpecifier.range[0]]))
188+
} else if (isLast) {
189+
const prevSpecifier = importNode.specifiers[importNode.specifiers.length - 2]
190+
fixes.push(fixer.removeRange([prevSpecifier.range[1], specifier.range[1]]))
191+
} else {
192+
const nextSpecifier = importNode.specifiers[importNode.specifiers.indexOf(specifier) + 1]
193+
fixes.push(fixer.removeRange([specifier.range[0], nextSpecifier.range[0]]))
194+
}
195+
}
196+
197+
// Add new import
198+
fixes.push(fixer.insertTextAfter(importNode, `\nimport { ${componentName} } from '@primer/react'`))
199+
200+
return fixes
201+
},
202+
})
203+
}
204+
}
205+
149206
// Also report for types and utilities that should always be from styled-react
150207
for (const [importName, importInfo] of primerReactImports) {
151208
if ((styledTypes.has(importName) || styledUtilities.has(importName)) && !styledReactImports.has(importName)) {

0 commit comments

Comments
 (0)