Skip to content

Commit 93e4b76

Browse files
authored
Add rule to enforce importing components with sx prop from @primer/styled-react (#382)
* feat: Add rule to enforce importing components with sx prop from @primer/styled-react * Update test for eslint v9 * feat: Enhance use-styled-react-import rule to enforce correct imports for components without sx prop * feat: Update use-styled-react-import rule to handle components used with and without sx prop, including aliasing for conflicts * Update eslint-plugin-primer-react version and rule * Add test case for import from github-ui * 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 * feat: Refactor import handling in use-styled-react-import rule to consolidate styled-react imports into a single statement with aliasing support * feat: Enhance use-styled-react-import rule to support alias mapping for styled-react imports and improve error reporting for components used without sx prop
1 parent 38aab58 commit 93e4b76

File tree

5 files changed

+868
-0
lines changed

5 files changed

+868
-0
lines changed

.changeset/smart-rocks-fail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-primer-react": minor
3+
---
4+
5+
Add rule `use-styled-react-import` to enforce importing components with sx prop from @primer/styled-react

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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# use-styled-react-import
2+
3+
💼 This rule is _disabled_ in the ✅ `recommended` config.
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
9+
Enforce importing components that use `sx` prop from `@primer/styled-react` instead of `@primer/react`, and vice versa.
10+
11+
## Rule Details
12+
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.
18+
19+
### Components that should be imported from `@primer/styled-react` when used with `sx`:
20+
21+
- ActionList
22+
- ActionMenu
23+
- Box
24+
- Breadcrumbs
25+
- Button
26+
- Flash
27+
- FormControl
28+
- Heading
29+
- IconButton
30+
- Label
31+
- Link
32+
- LinkButton
33+
- PageLayout
34+
- Text
35+
- TextInput
36+
- Truncate
37+
- Octicon
38+
- Dialog
39+
40+
### Types and utilities that should always be imported from `@primer/styled-react`:
41+
42+
- `BoxProps` (type)
43+
- `SxProp` (type)
44+
- `BetterSystemStyleObject` (type)
45+
- `sx` (utility)
46+
47+
## Examples
48+
49+
### ❌ Incorrect
50+
51+
```jsx
52+
import {Button, Link} from '@primer/react'
53+
54+
const Component = () => <Button sx={{color: 'red'}}>Click me</Button>
55+
```
56+
57+
```jsx
58+
import {Box} from '@primer/react'
59+
60+
const Component = () => <Box sx={{padding: 2}}>Content</Box>
61+
```
62+
63+
```jsx
64+
import {sx} from '@primer/react'
65+
```
66+
67+
```jsx
68+
import {Button} from '@primer/styled-react'
69+
70+
const Component = () => <Button>Click me</Button>
71+
```
72+
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+
80+
### ✅ Correct
81+
82+
```jsx
83+
import {Link} from '@primer/react'
84+
import {Button} from '@primer/styled-react'
85+
86+
const Component = () => <Button sx={{color: 'red'}}>Click me</Button>
87+
```
88+
89+
```jsx
90+
import {Box} from '@primer/styled-react'
91+
92+
const Component = () => <Box sx={{padding: 2}}>Content</Box>
93+
```
94+
95+
```jsx
96+
import {sx} from '@primer/styled-react'
97+
```
98+
99+
```jsx
100+
// Components without sx prop can stay in @primer/react
101+
import {Button} from '@primer/react'
102+
103+
const Component = () => <Button>Click me</Button>
104+
```
105+
106+
```jsx
107+
// Components imported from styled-react but used without sx prop should be moved back
108+
import {Button} from '@primer/react'
109+
110+
const Component = () => <Button>Click me</Button>
111+
```
112+
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+
122+
## Options
123+
124+
This rule has no options.
125+
126+
## When Not To Use It
127+
128+
This rule is specifically for migrating components that use the `sx` prop to the temporary `@primer/styled-react` package. If you're not using the `sx` prop or not participating in this migration, you can disable this rule.

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module.exports = {
1919
'prefer-action-list-item-onselect': require('./rules/prefer-action-list-item-onselect'),
2020
'enforce-css-module-identifier-casing': require('./rules/enforce-css-module-identifier-casing'),
2121
'enforce-css-module-default-import': require('./rules/enforce-css-module-default-import'),
22+
'use-styled-react-import': require('./rules/use-styled-react-import'),
2223
},
2324
configs: {
2425
recommended: require('./configs/recommended'),
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
const rule = require('../use-styled-react-import')
2+
const {RuleTester} = require('eslint')
3+
4+
const ruleTester = new RuleTester({
5+
languageOptions: {
6+
ecmaVersion: 'latest',
7+
sourceType: 'module',
8+
parserOptions: {
9+
ecmaFeatures: {
10+
jsx: true,
11+
},
12+
},
13+
},
14+
})
15+
16+
ruleTester.run('use-styled-react-import', rule, {
17+
valid: [
18+
// Valid: Component used without sx prop
19+
`import { Button } from '@primer/react'
20+
const Component = () => <Button>Click me</Button>`,
21+
22+
// Valid: Component with sx prop imported from styled-react
23+
`import { Button } from '@primer/styled-react'
24+
const Component = () => <Button sx={{ color: 'red' }}>Click me</Button>`,
25+
26+
// Valid: Utilities imported from styled-react
27+
`import { sx } from '@primer/styled-react'`,
28+
29+
// Valid: Component not in the styled list
30+
`import { Avatar } from '@primer/react'
31+
const Component = () => <Avatar sx={{ color: 'red' }} />`,
32+
33+
// Valid: Component not imported from @primer/react
34+
`import { Button } from '@github-ui/button'
35+
const Component = () => <Button sx={{ color: 'red' }} />`,
36+
37+
// Valid: Mixed imports - component without sx prop
38+
`import { Button, Text } from '@primer/react'
39+
const Component = () => <Button>Click me</Button>`,
40+
41+
// Valid: Component without sx prop imported from styled-react (when not used)
42+
`import { Button } from '@primer/styled-react'`,
43+
],
44+
invalid: [
45+
// Invalid: Box with sx prop imported from @primer/react
46+
{
47+
code: `import { Box } from '@primer/react'
48+
const Component = () => <Box sx={{ color: 'red' }}>Content</Box>`,
49+
output: `import { Box } from '@primer/styled-react'
50+
const Component = () => <Box sx={{ color: 'red' }}>Content</Box>`,
51+
errors: [
52+
{
53+
messageId: 'useStyledReactImport',
54+
data: {componentName: 'Box'},
55+
},
56+
],
57+
},
58+
59+
// Invalid: Button with sx prop imported from @primer/react
60+
{
61+
code: `import { Button } from '@primer/react'
62+
const Component = () => <Button sx={{ margin: 2 }}>Click me</Button>`,
63+
output: `import { Button } from '@primer/styled-react'
64+
const Component = () => <Button sx={{ margin: 2 }}>Click me</Button>`,
65+
errors: [
66+
{
67+
messageId: 'useStyledReactImport',
68+
data: {componentName: 'Button'},
69+
},
70+
],
71+
},
72+
73+
// Invalid: Multiple components, one with sx prop
74+
{
75+
code: `import { Button, Box, Avatar } from '@primer/react'
76+
const Component = () => (
77+
<div>
78+
<Button>Regular button</Button>
79+
<Box sx={{ padding: 2 }}>Styled box</Box>
80+
<Avatar />
81+
</div>
82+
)`,
83+
output: `import { Button, Avatar } from '@primer/react'
84+
import { Box } from '@primer/styled-react'
85+
const Component = () => (
86+
<div>
87+
<Button>Regular button</Button>
88+
<Box sx={{ padding: 2 }}>Styled box</Box>
89+
<Avatar />
90+
</div>
91+
)`,
92+
errors: [
93+
{
94+
messageId: 'useStyledReactImport',
95+
data: {componentName: 'Box'},
96+
},
97+
],
98+
},
99+
100+
// Invalid: Utility import from @primer/react that should be from styled-react
101+
{
102+
code: `import { sx } from '@primer/react'`,
103+
output: `import { sx } from '@primer/styled-react'`,
104+
errors: [
105+
{
106+
messageId: 'moveToStyledReact',
107+
data: {importName: 'sx'},
108+
},
109+
],
110+
},
111+
112+
// Invalid: Button and Link, only Button uses sx
113+
{
114+
code: `import { Button, Link } from '@primer/react'
115+
const Component = () => <Button sx={{ color: 'red' }}>Click me</Button>`,
116+
output: `import { Link } from '@primer/react'
117+
import { Button } from '@primer/styled-react'
118+
const Component = () => <Button sx={{ color: 'red' }}>Click me</Button>`,
119+
errors: [
120+
{
121+
messageId: 'useStyledReactImport',
122+
data: {componentName: 'Button'},
123+
},
124+
],
125+
},
126+
127+
// Invalid: Button imported from styled-react but used without sx prop
128+
{
129+
code: `import { Button } from '@primer/styled-react'
130+
const Component = () => <Button>Click me</Button>`,
131+
output: `import { Button } from '@primer/react'
132+
const Component = () => <Button>Click me</Button>`,
133+
errors: [
134+
{
135+
messageId: 'usePrimerReactImport',
136+
data: {componentName: 'Button'},
137+
},
138+
],
139+
},
140+
141+
// Invalid: <Link /> and <StyledButton /> imported from styled-react but used without sx prop
142+
{
143+
code: `import { Button } from '@primer/react'
144+
import { Button as StyledButton, Link } from '@primer/styled-react'
145+
const Component = () => (
146+
<div>
147+
<Link />
148+
<Button>Regular button</Button>
149+
<StyledButton>Styled button</StyledButton>
150+
</div>
151+
)`,
152+
output: `import { Button, Link } from '@primer/react'
153+
154+
const Component = () => (
155+
<div>
156+
<Link />
157+
<Button>Regular button</Button>
158+
<Button>Styled button</Button>
159+
</div>
160+
)`,
161+
errors: [
162+
{
163+
messageId: 'usePrimerReactImport',
164+
data: {componentName: 'Button'},
165+
},
166+
{
167+
messageId: 'usePrimerReactImport',
168+
data: {componentName: 'Link'},
169+
},
170+
{
171+
messageId: 'usePrimerReactImport',
172+
data: {componentName: 'Button'},
173+
},
174+
],
175+
},
176+
177+
// Invalid: Box imported from styled-react but used without sx prop
178+
{
179+
code: `import { Box } from '@primer/styled-react'
180+
const Component = () => <Box>Content</Box>`,
181+
output: `import { Box } from '@primer/react'
182+
const Component = () => <Box>Content</Box>`,
183+
errors: [
184+
{
185+
messageId: 'usePrimerReactImport',
186+
data: {componentName: 'Box'},
187+
},
188+
],
189+
},
190+
191+
// Invalid: Multiple components from styled-react, one used without sx
192+
{
193+
code: `import { Button, Box } from '@primer/styled-react'
194+
const Component = () => (
195+
<div>
196+
<Button>Regular button</Button>
197+
<Box sx={{ padding: 2 }}>Styled box</Box>
198+
</div>
199+
)`,
200+
output: `import { Box } from '@primer/styled-react'
201+
import { Button } from '@primer/react'
202+
const Component = () => (
203+
<div>
204+
<Button>Regular button</Button>
205+
<Box sx={{ padding: 2 }}>Styled box</Box>
206+
</div>
207+
)`,
208+
errors: [
209+
{
210+
messageId: 'usePrimerReactImport',
211+
data: {componentName: 'Button'},
212+
},
213+
],
214+
},
215+
216+
// Invalid: Button used both with and without sx prop - should use alias
217+
{
218+
code: `import { Button, Link } from '@primer/react'
219+
const Component = () => (
220+
<div>
221+
<Link sx={{ color: 'red' }} />
222+
<Button>Regular button</Button>
223+
<Button sx={{ color: 'red' }}>Styled button</Button>
224+
</div>
225+
)`,
226+
output: `import { Button } from '@primer/react'
227+
import { Button as StyledButton, Link } from '@primer/styled-react'
228+
const Component = () => (
229+
<div>
230+
<Link sx={{ color: 'red' }} />
231+
<Button>Regular button</Button>
232+
<StyledButton sx={{ color: 'red' }}>Styled button</StyledButton>
233+
</div>
234+
)`,
235+
errors: [
236+
{
237+
messageId: 'useStyledReactImportWithAlias',
238+
data: {componentName: 'Button', aliasName: 'StyledButton'},
239+
},
240+
{
241+
messageId: 'useStyledReactImport',
242+
data: {componentName: 'Link'},
243+
},
244+
{
245+
messageId: 'useAliasedComponent',
246+
data: {componentName: 'Button', aliasName: 'StyledButton'},
247+
},
248+
],
249+
},
250+
],
251+
})

0 commit comments

Comments
 (0)