eslint-plugin-primer-react 8.2.0-rc.b646af2 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,10 @@
6
6
 
7
7
  - [#407](https://github.com/primer/eslint-plugin-primer-react/pull/407) [`2f25480`](https://github.com/primer/eslint-plugin-primer-react/commit/2f25480c3341c1d1afb6fc040c5c5deee416d71c) Thanks [@jonrohan](https://github.com/jonrohan)! - Make `use-styled-react-import` rule configurable
8
8
 
9
+ ### Patch Changes
10
+
11
+ - [#406](https://github.com/primer/eslint-plugin-primer-react/pull/406) [`d72e8c4`](https://github.com/primer/eslint-plugin-primer-react/commit/d72e8c4c172d1c37da201a20cde0b7b2bd9ab283) Thanks [@jonrohan](https://github.com/jonrohan)! - Fixes for `use-styled-react-import` rule for compound components.
12
+
9
13
  ## 8.1.0
10
14
 
11
15
  ### Minor Changes
@@ -12,7 +12,7 @@ Enforce importing components that use `sx` prop from `@primer/styled-react` inst
12
12
 
13
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
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.
15
+ When a component is used with the `sx` prop anywhere in the file, the entire component import is moved to `@primer/styled-react`, simplifying the import structure.
16
16
 
17
17
  It also moves certain types and utilities to the styled-react package.
18
18
 
@@ -111,12 +111,11 @@ const Component = () => <Button>Click me</Button>
111
111
  ```
112
112
 
113
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'
114
+ // When a component is used with sx prop anywhere, import from styled-react
115
+ import {Button} from '@primer/styled-react'
117
116
 
118
117
  const Component1 = () => <Button>Click me</Button>
119
- const Component2 = () => <StyledButton sx={{color: 'red'}}>Styled me</StyledButton>
118
+ const Component2 = () => <Button sx={{color: 'red'}}>Styled me</Button>
120
119
  ```
121
120
 
122
121
  ## Options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "8.2.0-rc.b646af2",
3
+ "version": "8.2.0",
4
4
  "description": "ESLint rules for Primer React",
5
5
  "main": "src/index.js",
6
6
  "engines": {
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@styled-system/props": "^5.1.5",
35
- "@typescript-eslint/utils": "8.43.0",
35
+ "@typescript-eslint/utils": "^8.39.0",
36
36
  "eslint-plugin-github": "^6.0.0",
37
37
  "eslint-plugin-jsx-a11y": "^6.7.1",
38
38
  "eslint-traverse": "^1.0.0",
@@ -56,6 +56,48 @@ ruleTester.run('use-styled-react-import', rule, {
56
56
  ],
57
57
  },
58
58
 
59
+ // Invalid: ActionList.Item with sx prop and ActionList imported from @primer/react
60
+ {
61
+ code: `import { ActionList } from '@primer/react'
62
+ const Component = () => <ActionList.Item sx={{ color: 'red' }}>Content</ActionList.Item>`,
63
+ output: `import { ActionList } from '@primer/styled-react'
64
+ const Component = () => <ActionList.Item sx={{ color: 'red' }}>Content</ActionList.Item>`,
65
+ errors: [
66
+ {
67
+ messageId: 'useStyledReactImport',
68
+ data: {componentName: 'ActionList'},
69
+ },
70
+ ],
71
+ },
72
+
73
+ // Invalid: FormControl used both with and without sx prop - should move to styled-react
74
+ {
75
+ code: `import { FormControl } from '@primer/react'
76
+ const Component = () => (
77
+ <div>
78
+ <FormControl></FormControl>
79
+ <FormControl sx={{ color: 'red' }}>
80
+ <FormControl.Label visuallyHidden>Label</FormControl.Label>
81
+ </FormControl>
82
+ </div>
83
+ )`,
84
+ output: `import { FormControl } from '@primer/styled-react'
85
+ const Component = () => (
86
+ <div>
87
+ <FormControl></FormControl>
88
+ <FormControl sx={{ color: 'red' }}>
89
+ <FormControl.Label visuallyHidden>Label</FormControl.Label>
90
+ </FormControl>
91
+ </div>
92
+ )`,
93
+ errors: [
94
+ {
95
+ messageId: 'useStyledReactImport',
96
+ data: {componentName: 'FormControl'},
97
+ },
98
+ ],
99
+ },
100
+
59
101
  // Invalid: Button with sx prop imported from @primer/react
60
102
  {
61
103
  code: `import { Button } from '@primer/react'
@@ -70,6 +112,41 @@ ruleTester.run('use-styled-react-import', rule, {
70
112
  ],
71
113
  },
72
114
 
115
+ // Invalid: ActionList used without sx, ActionList.Item used with sx - should move ActionList to styled-react
116
+ {
117
+ code: `import { ActionList, ActionMenu } from '@primer/react'
118
+ const Component = () => (
119
+ <ActionMenu>
120
+ <ActionMenu.Overlay>
121
+ <ActionList>
122
+ <ActionList.Item sx={{ paddingLeft: 'calc(1 * var(--base-size-12))' }}>
123
+ Item
124
+ </ActionList.Item>
125
+ </ActionList>
126
+ </ActionMenu.Overlay>
127
+ </ActionMenu>
128
+ )`,
129
+ output: `import { ActionMenu } from '@primer/react'
130
+ import { ActionList } from '@primer/styled-react'
131
+ const Component = () => (
132
+ <ActionMenu>
133
+ <ActionMenu.Overlay>
134
+ <ActionList>
135
+ <ActionList.Item sx={{ paddingLeft: 'calc(1 * var(--base-size-12))' }}>
136
+ Item
137
+ </ActionList.Item>
138
+ </ActionList>
139
+ </ActionMenu.Overlay>
140
+ </ActionMenu>
141
+ )`,
142
+ errors: [
143
+ {
144
+ messageId: 'useStyledReactImport',
145
+ data: {componentName: 'ActionList'},
146
+ },
147
+ ],
148
+ },
149
+
73
150
  // Invalid: Multiple components, one with sx prop
74
151
  {
75
152
  code: `import { Button, Box, Avatar } from '@primer/react'
@@ -138,7 +215,7 @@ import { Button } from '@primer/styled-react'
138
215
  ],
139
216
  },
140
217
 
141
- // Invalid: <Link /> and <StyledButton /> imported from styled-react but used without sx prop
218
+ // Invalid: <Link /> and <Button /> imported from styled-react but used without sx prop
142
219
  {
143
220
  code: `import { Button } from '@primer/react'
144
221
  import { Button as StyledButton, Link } from '@primer/styled-react'
@@ -155,7 +232,7 @@ import { Button as StyledButton, Link } from '@primer/styled-react'
155
232
  <div>
156
233
  <Link />
157
234
  <Button>Regular button</Button>
158
- <Button>Styled button</Button>
235
+ <StyledButton>Styled button</StyledButton>
159
236
  </div>
160
237
  )`,
161
238
  errors: [
@@ -167,10 +244,6 @@ import { Button as StyledButton, Link } from '@primer/styled-react'
167
244
  messageId: 'usePrimerReactImport',
168
245
  data: {componentName: 'Link'},
169
246
  },
170
- {
171
- messageId: 'usePrimerReactImport',
172
- data: {componentName: 'Button'},
173
- },
174
247
  ],
175
248
  },
176
249
 
@@ -213,7 +286,7 @@ import { Button } from '@primer/react'
213
286
  ],
214
287
  },
215
288
 
216
- // Invalid: Button used both with and without sx prop - should use alias
289
+ // Invalid: Button and Link used with sx prop - should move both to styled-react
217
290
  {
218
291
  code: `import { Button, Link } from '@primer/react'
219
292
  const Component = () => (
@@ -223,28 +296,23 @@ import { Button } from '@primer/react'
223
296
  <Button sx={{ color: 'red' }}>Styled button</Button>
224
297
  </div>
225
298
  )`,
226
- output: `import { Button } from '@primer/react'
227
- import { Button as StyledButton, Link } from '@primer/styled-react'
299
+ output: `import { Link, Button } from '@primer/styled-react'
228
300
  const Component = () => (
229
301
  <div>
230
302
  <Link sx={{ color: 'red' }} />
231
303
  <Button>Regular button</Button>
232
- <StyledButton sx={{ color: 'red' }}>Styled button</StyledButton>
304
+ <Button sx={{ color: 'red' }}>Styled button</Button>
233
305
  </div>
234
306
  )`,
235
307
  errors: [
236
308
  {
237
- messageId: 'useStyledReactImportWithAlias',
238
- data: {componentName: 'Button', aliasName: 'StyledButton'},
309
+ messageId: 'useStyledReactImport',
310
+ data: {componentName: 'Button'},
239
311
  },
240
312
  {
241
313
  messageId: 'useStyledReactImport',
242
314
  data: {componentName: 'Link'},
243
315
  },
244
- {
245
- messageId: 'useAliasedComponent',
246
- data: {componentName: 'Button', aliasName: 'StyledButton'},
247
- },
248
316
  ],
249
317
  },
250
318
  ],
@@ -68,9 +68,6 @@ module.exports = {
68
68
  ],
69
69
  messages: {
70
70
  useStyledReactImport: 'Import {{ componentName }} from "@primer/styled-react" when using with sx prop',
71
- useStyledReactImportWithAlias:
72
- 'Import {{ componentName }} as {{ aliasName }} from "@primer/styled-react" when using with sx prop (conflicts with non-sx usage)',
73
- useAliasedComponent: 'Use {{ aliasName }} instead of {{ componentName }} when using sx prop',
74
71
  moveToStyledReact: 'Move {{ importName }} import to "@primer/styled-react"',
75
72
  usePrimerReactImport: 'Import {{ componentName }} from "@primer/react" when not using sx prop',
76
73
  },
@@ -86,9 +83,6 @@ module.exports = {
86
83
  const allUsedComponents = new Set() // Track all used components
87
84
  const primerReactImports = new Map() // Map of component name to import node
88
85
  const styledReactImports = new Map() // Map of components imported from styled-react to import node
89
- const aliasMapping = new Map() // Map local name to original component name for aliased imports
90
- const jsxElementsWithSx = [] // Track JSX elements that use sx prop
91
- const jsxElementsWithoutSx = [] // Track JSX elements that don't use sx prop
92
86
 
93
87
  return {
94
88
  ImportDeclaration(node) {
@@ -113,13 +107,7 @@ module.exports = {
113
107
  for (const specifier of node.specifiers) {
114
108
  if (specifier.type === 'ImportSpecifier') {
115
109
  const importedName = specifier.imported.name
116
- const localName = specifier.local.name
117
110
  styledReactImports.set(importedName, {node, specifier})
118
-
119
- // Track alias mapping for styled-react imports
120
- if (localName !== importedName) {
121
- aliasMapping.set(localName, importedName)
122
- }
123
111
  }
124
112
  }
125
113
  }
@@ -129,12 +117,12 @@ module.exports = {
129
117
  const openingElement = node.openingElement
130
118
  const componentName = getJSXOpeningElementName(openingElement)
131
119
 
132
- // Check if this is an aliased component from styled-react
133
- const originalComponentName = aliasMapping.get(componentName) || componentName
120
+ // For compound components like "ActionList.Item", we need to check the parent component
121
+ const parentComponentName = componentName.includes('.') ? componentName.split('.')[0] : componentName
134
122
 
135
123
  // Track all used components that are in our styled components list
136
- if (styledComponents.has(originalComponentName)) {
137
- allUsedComponents.add(originalComponentName)
124
+ if (styledComponents.has(parentComponentName)) {
125
+ allUsedComponents.add(parentComponentName)
138
126
 
139
127
  // Check if this component has an sx prop
140
128
  const hasSxProp = openingElement.attributes.some(
@@ -142,20 +130,9 @@ module.exports = {
142
130
  )
143
131
 
144
132
  if (hasSxProp) {
145
- componentsWithSx.add(originalComponentName)
146
- jsxElementsWithSx.push({node, componentName: originalComponentName, openingElement})
133
+ componentsWithSx.add(parentComponentName)
147
134
  } else {
148
- componentsWithoutSx.add(originalComponentName)
149
-
150
- // If this is an aliased component without sx, we need to track it for renaming
151
- if (aliasMapping.has(componentName)) {
152
- jsxElementsWithoutSx.push({
153
- node,
154
- localName: componentName,
155
- originalName: originalComponentName,
156
- openingElement,
157
- })
158
- }
135
+ componentsWithoutSx.add(parentComponentName)
159
136
  }
160
137
  }
161
138
  },
@@ -168,23 +145,16 @@ module.exports = {
168
145
  for (const componentName of componentsWithSx) {
169
146
  const importInfo = primerReactImports.get(componentName)
170
147
  if (importInfo && !styledReactImports.has(componentName)) {
171
- const hasConflict = componentsWithoutSx.has(componentName)
172
148
  const {node: importNode} = importInfo
173
149
 
174
150
  if (!importNodeChanges.has(importNode)) {
175
151
  importNodeChanges.set(importNode, {
176
152
  toMove: [],
177
- toAlias: [],
178
153
  originalSpecifiers: [...importNode.specifiers],
179
154
  })
180
155
  }
181
156
 
182
- const changes = importNodeChanges.get(importNode)
183
- if (hasConflict) {
184
- changes.toAlias.push(componentName)
185
- } else {
186
- changes.toMove.push(componentName)
187
- }
157
+ importNodeChanges.get(importNode).toMove.push(componentName)
188
158
  }
189
159
  }
190
160
 
@@ -192,15 +162,12 @@ module.exports = {
192
162
  for (const componentName of componentsWithSx) {
193
163
  const importInfo = primerReactImports.get(componentName)
194
164
  if (importInfo && !styledReactImports.has(componentName)) {
195
- // Check if this component is also used without sx prop (conflict scenario)
196
- const hasConflict = componentsWithoutSx.has(componentName)
197
-
198
165
  context.report({
199
166
  node: importInfo.specifier,
200
- messageId: hasConflict ? 'useStyledReactImportWithAlias' : 'useStyledReactImport',
201
- data: hasConflict ? {componentName, aliasName: `Styled${componentName}`} : {componentName},
167
+ messageId: 'useStyledReactImport',
168
+ data: {componentName},
202
169
  fix(fixer) {
203
- const {node: importNode, specifier} = importInfo
170
+ const {node: importNode} = importInfo
204
171
  const changes = importNodeChanges.get(importNode)
205
172
 
206
173
  if (!changes) {
@@ -208,10 +175,7 @@ module.exports = {
208
175
  }
209
176
 
210
177
  // Only apply the fix once per import node (for the first component processed)
211
- const isFirstComponent =
212
- changes.originalSpecifiers[0] === specifier ||
213
- (changes.toMove.length > 0 && changes.toMove[0] === componentName) ||
214
- (changes.toAlias.length > 0 && changes.toAlias[0] === componentName)
178
+ const isFirstComponent = changes.toMove[0] === componentName
215
179
 
216
180
  if (!isFirstComponent) {
217
181
  return null
@@ -223,27 +187,13 @@ module.exports = {
223
187
  // Find specifiers that remain in original import
224
188
  const remainingSpecifiers = changes.originalSpecifiers.filter(spec => {
225
189
  const name = spec.imported.name
226
- // Keep components that are not being moved (only aliased components stay for non-sx usage)
227
190
  return !componentsToMove.has(name)
228
191
  })
229
192
 
230
- // If no components remain, replace with new imports directly
193
+ // If no components remain, replace with new import directly
231
194
  if (remainingSpecifiers.length === 0) {
232
- // Build the new imports to replace the original
233
- const newImports = []
234
-
235
- // Add imports for moved components
236
- for (const componentName of changes.toMove) {
237
- newImports.push(`import { ${componentName} } from '@primer/styled-react'`)
238
- }
239
-
240
- // Add aliased imports for conflicted components
241
- for (const componentName of changes.toAlias) {
242
- const aliasName = `Styled${componentName}`
243
- newImports.push(`import { ${componentName} as ${aliasName} } from '@primer/styled-react'`)
244
- }
245
-
246
- fixes.push(fixer.replaceText(importNode, newImports.join('\n')))
195
+ const movedComponents = changes.toMove.join(', ')
196
+ fixes.push(fixer.replaceText(importNode, `import { ${movedComponents} } from '@primer/styled-react'`))
247
197
  } else {
248
198
  // Otherwise, update the import to only include remaining components
249
199
  const remainingNames = remainingSpecifiers.map(spec => spec.imported.name)
@@ -251,56 +201,11 @@ module.exports = {
251
201
  fixer.replaceText(importNode, `import { ${remainingNames.join(', ')} } from '@primer/react'`),
252
202
  )
253
203
 
254
- // Combine all styled-react imports into a single import statement
255
- const styledReactImports = []
256
-
257
- // Add aliased components first
258
- for (const componentName of changes.toAlias) {
259
- const aliasName = `Styled${componentName}`
260
- styledReactImports.push(`${componentName} as ${aliasName}`)
261
- }
262
-
263
- // Add moved components second
264
- for (const componentName of changes.toMove) {
265
- styledReactImports.push(componentName)
266
- }
267
-
268
- if (styledReactImports.length > 0) {
269
- fixes.push(
270
- fixer.insertTextAfter(
271
- importNode,
272
- `\nimport { ${styledReactImports.join(', ')} } from '@primer/styled-react'`,
273
- ),
274
- )
275
- }
276
- }
277
-
278
- return fixes
279
- },
280
- })
281
- }
282
- }
283
-
284
- // Report on JSX elements that should use aliased components
285
- for (const {node: jsxNode, componentName, openingElement} of jsxElementsWithSx) {
286
- const hasConflict = componentsWithoutSx.has(componentName)
287
- const isImportedFromPrimerReact = primerReactImports.has(componentName)
288
-
289
- if (hasConflict && isImportedFromPrimerReact && !styledReactImports.has(componentName)) {
290
- const aliasName = `Styled${componentName}`
291
- context.report({
292
- node: openingElement,
293
- messageId: 'useAliasedComponent',
294
- data: {componentName, aliasName},
295
- fix(fixer) {
296
- const fixes = []
297
-
298
- // Replace the component name in the JSX opening tag
299
- fixes.push(fixer.replaceText(openingElement.name, aliasName))
300
-
301
- // Replace the component name in the JSX closing tag if it exists
302
- if (jsxNode.closingElement) {
303
- fixes.push(fixer.replaceText(jsxNode.closingElement.name, aliasName))
204
+ // Add new styled-react import
205
+ const movedComponents = changes.toMove.join(', ')
206
+ fixes.push(
207
+ fixer.insertTextAfter(importNode, `\nimport { ${movedComponents} } from '@primer/styled-react'`),
208
+ )
304
209
  }
305
210
 
306
211
  return fixes
@@ -430,30 +335,6 @@ module.exports = {
430
335
  }
431
336
  }
432
337
 
433
- // Report and fix JSX elements that use aliased components without sx prop
434
- for (const {node: jsxNode, originalName, openingElement} of jsxElementsWithoutSx) {
435
- if (!componentsWithSx.has(originalName) && styledReactImports.has(originalName)) {
436
- context.report({
437
- node: openingElement,
438
- messageId: 'usePrimerReactImport',
439
- data: {componentName: originalName},
440
- fix(fixer) {
441
- const fixes = []
442
-
443
- // Replace the aliased component name with the original component name in JSX opening tag
444
- fixes.push(fixer.replaceText(openingElement.name, originalName))
445
-
446
- // Replace the aliased component name in JSX closing tag if it exists
447
- if (jsxNode.closingElement) {
448
- fixes.push(fixer.replaceText(jsxNode.closingElement.name, originalName))
449
- }
450
-
451
- return fixes
452
- },
453
- })
454
- }
455
- }
456
-
457
338
  // Also report for types and utilities that should always be from styled-react
458
339
  for (const [importName, importInfo] of primerReactImports) {
459
340
  if ((styledTypes.has(importName) || styledUtilities.has(importName)) && !styledReactImports.has(importName)) {