eslint-plugin-primer-react 8.2.0-rc.18e9359 → 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.
@@ -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.18e9359",
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",
@@ -70,7 +70,7 @@ ruleTester.run('use-styled-react-import', rule, {
70
70
  ],
71
71
  },
72
72
 
73
- // Invalid: FormControl used both with and without sx prop - should use alias
73
+ // Invalid: FormControl used both with and without sx prop - should move to styled-react
74
74
  {
75
75
  code: `import { FormControl } from '@primer/react'
76
76
  const Component = () => (
@@ -81,24 +81,19 @@ ruleTester.run('use-styled-react-import', rule, {
81
81
  </FormControl>
82
82
  </div>
83
83
  )`,
84
- output: `import { FormControl } from '@primer/react'
85
- import { FormControl as StyledFormControl } from '@primer/styled-react'
84
+ output: `import { FormControl } from '@primer/styled-react'
86
85
  const Component = () => (
87
86
  <div>
88
87
  <FormControl></FormControl>
89
- <StyledFormControl sx={{ color: 'red' }}>
90
- <StyledFormControl.Label visuallyHidden>Label</StyledFormControl.Label>
91
- </StyledFormControl>
88
+ <FormControl sx={{ color: 'red' }}>
89
+ <FormControl.Label visuallyHidden>Label</FormControl.Label>
90
+ </FormControl>
92
91
  </div>
93
92
  )`,
94
93
  errors: [
95
94
  {
96
- messageId: 'useStyledReactImportWithAlias',
97
- data: {componentName: 'FormControl', aliasName: 'StyledFormControl'},
98
- },
99
- {
100
- messageId: 'useAliasedComponent',
101
- data: {componentName: 'FormControl', aliasName: 'StyledFormControl'},
95
+ messageId: 'useStyledReactImport',
96
+ data: {componentName: 'FormControl'},
102
97
  },
103
98
  ],
104
99
  },
@@ -117,6 +112,41 @@ import { FormControl as StyledFormControl } from '@primer/styled-react'
117
112
  ],
118
113
  },
119
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
+
120
150
  // Invalid: Multiple components, one with sx prop
121
151
  {
122
152
  code: `import { Button, Box, Avatar } from '@primer/react'
@@ -185,7 +215,7 @@ import { Button } from '@primer/styled-react'
185
215
  ],
186
216
  },
187
217
 
188
- // 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
189
219
  {
190
220
  code: `import { Button } from '@primer/react'
191
221
  import { Button as StyledButton, Link } from '@primer/styled-react'
@@ -202,7 +232,7 @@ import { Button as StyledButton, Link } from '@primer/styled-react'
202
232
  <div>
203
233
  <Link />
204
234
  <Button>Regular button</Button>
205
- <Button>Styled button</Button>
235
+ <StyledButton>Styled button</StyledButton>
206
236
  </div>
207
237
  )`,
208
238
  errors: [
@@ -214,10 +244,6 @@ import { Button as StyledButton, Link } from '@primer/styled-react'
214
244
  messageId: 'usePrimerReactImport',
215
245
  data: {componentName: 'Link'},
216
246
  },
217
- {
218
- messageId: 'usePrimerReactImport',
219
- data: {componentName: 'Button'},
220
- },
221
247
  ],
222
248
  },
223
249
 
@@ -260,7 +286,7 @@ import { Button } from '@primer/react'
260
286
  ],
261
287
  },
262
288
 
263
- // 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
264
290
  {
265
291
  code: `import { Button, Link } from '@primer/react'
266
292
  const Component = () => (
@@ -270,28 +296,23 @@ import { Button } from '@primer/react'
270
296
  <Button sx={{ color: 'red' }}>Styled button</Button>
271
297
  </div>
272
298
  )`,
273
- output: `import { Button } from '@primer/react'
274
- import { Button as StyledButton, Link } from '@primer/styled-react'
299
+ output: `import { Link, Button } from '@primer/styled-react'
275
300
  const Component = () => (
276
301
  <div>
277
302
  <Link sx={{ color: 'red' }} />
278
303
  <Button>Regular button</Button>
279
- <StyledButton sx={{ color: 'red' }}>Styled button</StyledButton>
304
+ <Button sx={{ color: 'red' }}>Styled button</Button>
280
305
  </div>
281
306
  )`,
282
307
  errors: [
283
308
  {
284
- messageId: 'useStyledReactImportWithAlias',
285
- data: {componentName: 'Button', aliasName: 'StyledButton'},
309
+ messageId: 'useStyledReactImport',
310
+ data: {componentName: 'Button'},
286
311
  },
287
312
  {
288
313
  messageId: 'useStyledReactImport',
289
314
  data: {componentName: 'Link'},
290
315
  },
291
- {
292
- messageId: 'useAliasedComponent',
293
- data: {componentName: 'Button', aliasName: 'StyledButton'},
294
- },
295
316
  ],
296
317
  },
297
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,13 +117,8 @@ 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
134
-
135
120
  // For compound components like "ActionList.Item", we need to check the parent component
136
- const parentComponentName = originalComponentName.includes('.')
137
- ? originalComponentName.split('.')[0]
138
- : originalComponentName
121
+ const parentComponentName = componentName.includes('.') ? componentName.split('.')[0] : componentName
139
122
 
140
123
  // Track all used components that are in our styled components list
141
124
  if (styledComponents.has(parentComponentName)) {
@@ -148,19 +131,8 @@ module.exports = {
148
131
 
149
132
  if (hasSxProp) {
150
133
  componentsWithSx.add(parentComponentName)
151
- jsxElementsWithSx.push({node, componentName: originalComponentName, openingElement})
152
134
  } else {
153
135
  componentsWithoutSx.add(parentComponentName)
154
-
155
- // If this is an aliased component without sx, we need to track it for renaming
156
- if (aliasMapping.has(componentName)) {
157
- jsxElementsWithoutSx.push({
158
- node,
159
- localName: componentName,
160
- originalName: parentComponentName,
161
- openingElement,
162
- })
163
- }
164
136
  }
165
137
  }
166
138
  },
@@ -173,23 +145,16 @@ module.exports = {
173
145
  for (const componentName of componentsWithSx) {
174
146
  const importInfo = primerReactImports.get(componentName)
175
147
  if (importInfo && !styledReactImports.has(componentName)) {
176
- const hasConflict = componentsWithoutSx.has(componentName)
177
148
  const {node: importNode} = importInfo
178
149
 
179
150
  if (!importNodeChanges.has(importNode)) {
180
151
  importNodeChanges.set(importNode, {
181
152
  toMove: [],
182
- toAlias: [],
183
153
  originalSpecifiers: [...importNode.specifiers],
184
154
  })
185
155
  }
186
156
 
187
- const changes = importNodeChanges.get(importNode)
188
- if (hasConflict) {
189
- changes.toAlias.push(componentName)
190
- } else {
191
- changes.toMove.push(componentName)
192
- }
157
+ importNodeChanges.get(importNode).toMove.push(componentName)
193
158
  }
194
159
  }
195
160
 
@@ -197,15 +162,12 @@ module.exports = {
197
162
  for (const componentName of componentsWithSx) {
198
163
  const importInfo = primerReactImports.get(componentName)
199
164
  if (importInfo && !styledReactImports.has(componentName)) {
200
- // Check if this component is also used without sx prop (conflict scenario)
201
- const hasConflict = componentsWithoutSx.has(componentName)
202
-
203
165
  context.report({
204
166
  node: importInfo.specifier,
205
- messageId: hasConflict ? 'useStyledReactImportWithAlias' : 'useStyledReactImport',
206
- data: hasConflict ? {componentName, aliasName: `Styled${componentName}`} : {componentName},
167
+ messageId: 'useStyledReactImport',
168
+ data: {componentName},
207
169
  fix(fixer) {
208
- const {node: importNode, specifier} = importInfo
170
+ const {node: importNode} = importInfo
209
171
  const changes = importNodeChanges.get(importNode)
210
172
 
211
173
  if (!changes) {
@@ -213,10 +175,7 @@ module.exports = {
213
175
  }
214
176
 
215
177
  // Only apply the fix once per import node (for the first component processed)
216
- const isFirstComponent =
217
- changes.originalSpecifiers[0] === specifier ||
218
- (changes.toMove.length > 0 && changes.toMove[0] === componentName) ||
219
- (changes.toAlias.length > 0 && changes.toAlias[0] === componentName)
178
+ const isFirstComponent = changes.toMove[0] === componentName
220
179
 
221
180
  if (!isFirstComponent) {
222
181
  return null
@@ -228,27 +187,13 @@ module.exports = {
228
187
  // Find specifiers that remain in original import
229
188
  const remainingSpecifiers = changes.originalSpecifiers.filter(spec => {
230
189
  const name = spec.imported.name
231
- // Keep components that are not being moved (only aliased components stay for non-sx usage)
232
190
  return !componentsToMove.has(name)
233
191
  })
234
192
 
235
- // If no components remain, replace with new imports directly
193
+ // If no components remain, replace with new import directly
236
194
  if (remainingSpecifiers.length === 0) {
237
- // Build the new imports to replace the original
238
- const newImports = []
239
-
240
- // Add imports for moved components
241
- for (const componentName of changes.toMove) {
242
- newImports.push(`import { ${componentName} } from '@primer/styled-react'`)
243
- }
244
-
245
- // Add aliased imports for conflicted components
246
- for (const componentName of changes.toAlias) {
247
- const aliasName = `Styled${componentName}`
248
- newImports.push(`import { ${componentName} as ${aliasName} } from '@primer/styled-react'`)
249
- }
250
-
251
- 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'`))
252
197
  } else {
253
198
  // Otherwise, update the import to only include remaining components
254
199
  const remainingNames = remainingSpecifiers.map(spec => spec.imported.name)
@@ -256,28 +201,11 @@ module.exports = {
256
201
  fixer.replaceText(importNode, `import { ${remainingNames.join(', ')} } from '@primer/react'`),
257
202
  )
258
203
 
259
- // Combine all styled-react imports into a single import statement
260
- const styledReactImports = []
261
-
262
- // Add aliased components first
263
- for (const componentName of changes.toAlias) {
264
- const aliasName = `Styled${componentName}`
265
- styledReactImports.push(`${componentName} as ${aliasName}`)
266
- }
267
-
268
- // Add moved components second
269
- for (const componentName of changes.toMove) {
270
- styledReactImports.push(componentName)
271
- }
272
-
273
- if (styledReactImports.length > 0) {
274
- fixes.push(
275
- fixer.insertTextAfter(
276
- importNode,
277
- `\nimport { ${styledReactImports.join(', ')} } from '@primer/styled-react'`,
278
- ),
279
- )
280
- }
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
+ )
281
209
  }
282
210
 
283
211
  return fixes
@@ -286,31 +214,6 @@ module.exports = {
286
214
  }
287
215
  }
288
216
 
289
- // Report on JSX elements that should use aliased components
290
- for (const {node: jsxNode, componentName, openingElement} of jsxElementsWithSx) {
291
- const hasConflict = componentsWithoutSx.has(componentName)
292
- const isImportedFromPrimerReact = primerReactImports.has(componentName)
293
-
294
- if (hasConflict && isImportedFromPrimerReact && !styledReactImports.has(componentName)) {
295
- const aliasName = `Styled${componentName}`
296
- context.report({
297
- node: openingElement,
298
- messageId: 'useAliasedComponent',
299
- data: {componentName, aliasName},
300
- fix(fixer) {
301
- const sourceCode = context.getSourceCode()
302
- const jsxText = sourceCode.getText(jsxNode)
303
-
304
- // Replace all instances of the component name (both main component and compound components)
305
- const componentPattern = new RegExp(`\\b${componentName}(?=\\.|\\s|>)`, 'g')
306
- const aliasedText = jsxText.replace(componentPattern, aliasName)
307
-
308
- return fixer.replaceText(jsxNode, aliasedText)
309
- },
310
- })
311
- }
312
- }
313
-
314
217
  // Group styled-react imports that need to be moved to primer-react
315
218
  const styledReactImportNodeChanges = new Map()
316
219
 
@@ -432,30 +335,6 @@ module.exports = {
432
335
  }
433
336
  }
434
337
 
435
- // Report and fix JSX elements that use aliased components without sx prop
436
- for (const {node: jsxNode, originalName, openingElement} of jsxElementsWithoutSx) {
437
- if (!componentsWithSx.has(originalName) && styledReactImports.has(originalName)) {
438
- context.report({
439
- node: openingElement,
440
- messageId: 'usePrimerReactImport',
441
- data: {componentName: originalName},
442
- fix(fixer) {
443
- const fixes = []
444
-
445
- // Replace the aliased component name with the original component name in JSX opening tag
446
- fixes.push(fixer.replaceText(openingElement.name, originalName))
447
-
448
- // Replace the aliased component name in JSX closing tag if it exists
449
- if (jsxNode.closingElement) {
450
- fixes.push(fixer.replaceText(jsxNode.closingElement.name, originalName))
451
- }
452
-
453
- return fixes
454
- },
455
- })
456
- }
457
- }
458
-
459
338
  // Also report for types and utilities that should always be from styled-react
460
339
  for (const [importName, importInfo] of primerReactImports) {
461
340
  if ((styledTypes.has(importName) || styledUtilities.has(importName)) && !styledReactImports.has(importName)) {