Skip to content

Add rule to enforce importing components with sx prop from @primer/styled-react #382

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/smart-rocks-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-plugin-primer-react": minor
---

Add rule `use-styled-react-import` to enforce importing components with sx prop from @primer/styled-react
128 changes: 128 additions & 0 deletions docs/rules/use-styled-react-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# use-styled-react-import

💼 This rule is _disabled_ in the ✅ `recommended` config.

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

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

Enforce importing components that use `sx` prop from `@primer/styled-react` instead of `@primer/react`, and vice versa.

## Rule Details

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`.

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.

It also moves certain types and utilities to the styled-react package.

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

- ActionList
- ActionMenu
- Box
- Breadcrumbs
- Button
- Flash
- FormControl
- Heading
- IconButton
- Label
- Link
- LinkButton
- PageLayout
- Text
- TextInput
- Truncate
- Octicon
- Dialog

### Types and utilities that should always be imported from `@primer/styled-react`:

- `BoxProps` (type)
- `SxProp` (type)
- `BetterSystemStyleObject` (type)
- `sx` (utility)

## Examples

### ❌ Incorrect

```jsx
import {Button, Link} from '@primer/react'

const Component = () => <Button sx={{color: 'red'}}>Click me</Button>
```

```jsx
import {Box} from '@primer/react'

const Component = () => <Box sx={{padding: 2}}>Content</Box>
```

```jsx
import {sx} from '@primer/react'
```

```jsx
import {Button} from '@primer/styled-react'

const Component = () => <Button>Click me</Button>
```

```jsx
import {Button} from '@primer/react'

const Component1 = () => <Button>Click me</Button>
const Component2 = () => <Button sx={{color: 'red'}}>Styled me</Button>
```

### ✅ Correct

```jsx
import {Link} from '@primer/react'
import {Button} from '@primer/styled-react'

const Component = () => <Button sx={{color: 'red'}}>Click me</Button>
```

```jsx
import {Box} from '@primer/styled-react'

const Component = () => <Box sx={{padding: 2}}>Content</Box>
```

```jsx
import {sx} from '@primer/styled-react'
```

```jsx
// Components without sx prop can stay in @primer/react
import {Button} from '@primer/react'

const Component = () => <Button>Click me</Button>
```

```jsx
// Components imported from styled-react but used without sx prop should be moved back
import {Button} from '@primer/react'

const Component = () => <Button>Click me</Button>
```

```jsx
// When a component is used both ways, use an alias for the styled version
import {Button} from '@primer/react'
import {Button as StyledButton} from '@primer/styled-react'

const Component1 = () => <Button>Click me</Button>
const Component2 = () => <StyledButton sx={{color: 'red'}}>Styled me</StyledButton>
```

## Options

This rule has no options.

## When Not To Use It

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.
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = {
'prefer-action-list-item-onselect': require('./rules/prefer-action-list-item-onselect'),
'enforce-css-module-identifier-casing': require('./rules/enforce-css-module-identifier-casing'),
'enforce-css-module-default-import': require('./rules/enforce-css-module-default-import'),
'use-styled-react-import': require('./rules/use-styled-react-import'),
},
configs: {
recommended: require('./configs/recommended'),
Expand Down
251 changes: 251 additions & 0 deletions src/rules/__tests__/use-styled-react-import.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
const rule = require('../use-styled-react-import')
const {RuleTester} = require('eslint')

const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
})

ruleTester.run('use-styled-react-import', rule, {
valid: [
// Valid: Component used without sx prop
`import { Button } from '@primer/react'
const Component = () => <Button>Click me</Button>`,

// Valid: Component with sx prop imported from styled-react
`import { Button } from '@primer/styled-react'
const Component = () => <Button sx={{ color: 'red' }}>Click me</Button>`,

// Valid: Utilities imported from styled-react
`import { sx } from '@primer/styled-react'`,

// Valid: Component not in the styled list
`import { Avatar } from '@primer/react'
const Component = () => <Avatar sx={{ color: 'red' }} />`,

// Valid: Component not imported from @primer/react
`import { Button } from '@github-ui/button'
const Component = () => <Button sx={{ color: 'red' }} />`,

// Valid: Mixed imports - component without sx prop
`import { Button, Text } from '@primer/react'
const Component = () => <Button>Click me</Button>`,

// Valid: Component without sx prop imported from styled-react (when not used)
`import { Button } from '@primer/styled-react'`,
],
invalid: [
// Invalid: Box with sx prop imported from @primer/react
{
code: `import { Box } from '@primer/react'
const Component = () => <Box sx={{ color: 'red' }}>Content</Box>`,
output: `import { Box } from '@primer/styled-react'
const Component = () => <Box sx={{ color: 'red' }}>Content</Box>`,
errors: [
{
messageId: 'useStyledReactImport',
data: {componentName: 'Box'},
},
],
},

// Invalid: Button with sx prop imported from @primer/react
{
code: `import { Button } from '@primer/react'
const Component = () => <Button sx={{ margin: 2 }}>Click me</Button>`,
output: `import { Button } from '@primer/styled-react'
const Component = () => <Button sx={{ margin: 2 }}>Click me</Button>`,
errors: [
{
messageId: 'useStyledReactImport',
data: {componentName: 'Button'},
},
],
},

// Invalid: Multiple components, one with sx prop
{
code: `import { Button, Box, Avatar } from '@primer/react'
const Component = () => (
<div>
<Button>Regular button</Button>
<Box sx={{ padding: 2 }}>Styled box</Box>
<Avatar />
</div>
)`,
output: `import { Button, Avatar } from '@primer/react'
import { Box } from '@primer/styled-react'
const Component = () => (
<div>
<Button>Regular button</Button>
<Box sx={{ padding: 2 }}>Styled box</Box>
<Avatar />
</div>
)`,
errors: [
{
messageId: 'useStyledReactImport',
data: {componentName: 'Box'},
},
],
},

// Invalid: Utility import from @primer/react that should be from styled-react
{
code: `import { sx } from '@primer/react'`,
output: `import { sx } from '@primer/styled-react'`,
errors: [
{
messageId: 'moveToStyledReact',
data: {importName: 'sx'},
},
],
},

// Invalid: Button and Link, only Button uses sx
{
code: `import { Button, Link } from '@primer/react'
const Component = () => <Button sx={{ color: 'red' }}>Click me</Button>`,
output: `import { Link } from '@primer/react'
import { Button } from '@primer/styled-react'
const Component = () => <Button sx={{ color: 'red' }}>Click me</Button>`,
errors: [
{
messageId: 'useStyledReactImport',
data: {componentName: 'Button'},
},
],
},

// Invalid: Button imported from styled-react but used without sx prop
{
code: `import { Button } from '@primer/styled-react'
const Component = () => <Button>Click me</Button>`,
output: `import { Button } from '@primer/react'
const Component = () => <Button>Click me</Button>`,
errors: [
{
messageId: 'usePrimerReactImport',
data: {componentName: 'Button'},
},
],
},

// Invalid: <Link /> and <StyledButton /> imported from styled-react but used without sx prop
{
code: `import { Button } from '@primer/react'
import { Button as StyledButton, Link } from '@primer/styled-react'
const Component = () => (
<div>
<Link />
<Button>Regular button</Button>
<StyledButton>Styled button</StyledButton>
</div>
)`,
output: `import { Button, Link } from '@primer/react'

const Component = () => (
<div>
<Link />
<Button>Regular button</Button>
<Button>Styled button</Button>
</div>
)`,
errors: [
{
messageId: 'usePrimerReactImport',
data: {componentName: 'Button'},
},
{
messageId: 'usePrimerReactImport',
data: {componentName: 'Link'},
},
{
messageId: 'usePrimerReactImport',
data: {componentName: 'Button'},
},
],
},

// Invalid: Box imported from styled-react but used without sx prop
{
code: `import { Box } from '@primer/styled-react'
const Component = () => <Box>Content</Box>`,
output: `import { Box } from '@primer/react'
const Component = () => <Box>Content</Box>`,
errors: [
{
messageId: 'usePrimerReactImport',
data: {componentName: 'Box'},
},
],
},

// Invalid: Multiple components from styled-react, one used without sx
{
code: `import { Button, Box } from '@primer/styled-react'
const Component = () => (
<div>
<Button>Regular button</Button>
<Box sx={{ padding: 2 }}>Styled box</Box>
</div>
)`,
output: `import { Box } from '@primer/styled-react'
import { Button } from '@primer/react'
const Component = () => (
<div>
<Button>Regular button</Button>
<Box sx={{ padding: 2 }}>Styled box</Box>
</div>
)`,
errors: [
{
messageId: 'usePrimerReactImport',
data: {componentName: 'Button'},
},
],
},

// Invalid: Button used both with and without sx prop - should use alias
{
code: `import { Button, Link } from '@primer/react'
const Component = () => (
<div>
<Link sx={{ color: 'red' }} />
<Button>Regular button</Button>
<Button sx={{ color: 'red' }}>Styled button</Button>
</div>
)`,
output: `import { Button } from '@primer/react'
import { Button as StyledButton, Link } from '@primer/styled-react'
const Component = () => (
<div>
<Link sx={{ color: 'red' }} />
<Button>Regular button</Button>
<StyledButton sx={{ color: 'red' }}>Styled button</StyledButton>
</div>
)`,
errors: [
{
messageId: 'useStyledReactImportWithAlias',
data: {componentName: 'Button', aliasName: 'StyledButton'},
},
{
messageId: 'useStyledReactImport',
data: {componentName: 'Link'},
},
{
messageId: 'useAliasedComponent',
data: {componentName: 'Button', aliasName: 'StyledButton'},
},
],
},
],
})
Loading