eslint-plugin-primer-react 8.1.0-rc.d5a1ac8 → 8.2.0-rc.1772227

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "8.1.0-rc.d5a1ac8",
3
+ "version": "8.2.0-rc.1772227",
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.39.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",
@@ -46,7 +46,7 @@
46
46
  "@github/markdownlint-github": "^0.6.3",
47
47
  "@github/prettier-config": "0.0.6",
48
48
  "@types/jest": "^30.0.0",
49
- "@typescript-eslint/rule-tester": "8.39.0",
49
+ "@typescript-eslint/rule-tester": "8.42.0",
50
50
  "eslint": "^9.0.0",
51
51
  "eslint-plugin-eslint-comments": "^3.2.0",
52
52
  "eslint-plugin-filenames": "^1.3.2",
@@ -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,27 +296,83 @@ 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
  },
316
+ ],
317
+ },
318
+ ],
319
+ })
320
+
321
+ // Test configuration options
322
+ ruleTester.run('use-styled-react-import with custom configuration', rule, {
323
+ valid: [
324
+ // Valid: Custom component not in default list
325
+ {
326
+ code: `import { CustomButton } from '@primer/react'
327
+ const Component = () => <CustomButton sx={{ color: 'red' }}>Click me</CustomButton>`,
328
+ options: [{}], // Using default configuration
329
+ },
330
+
331
+ // Valid: Custom component in custom list used without sx prop
332
+ {
333
+ code: `import { CustomButton } from '@primer/react'
334
+ const Component = () => <CustomButton>Click me</CustomButton>`,
335
+ options: [{styledComponents: ['CustomButton']}],
336
+ },
337
+
338
+ // Valid: Custom component with sx prop imported from styled-react
339
+ {
340
+ code: `import { CustomButton } from '@primer/styled-react'
341
+ const Component = () => <CustomButton sx={{ color: 'red' }}>Click me</CustomButton>`,
342
+ options: [{styledComponents: ['CustomButton']}],
343
+ },
344
+
345
+ // Valid: Box not in custom list, so sx usage is allowed from @primer/react
346
+ {
347
+ code: `import { Box } from '@primer/react'
348
+ const Component = () => <Box sx={{ color: 'red' }}>Content</Box>`,
349
+ options: [{styledComponents: ['CustomButton']}], // Box not included
350
+ },
351
+ ],
352
+ invalid: [
353
+ // Invalid: Custom component with sx prop should be from styled-react
354
+ {
355
+ code: `import { CustomButton } from '@primer/react'
356
+ const Component = () => <CustomButton sx={{ color: 'red' }}>Click me</CustomButton>`,
357
+ output: `import { CustomButton } from '@primer/styled-react'
358
+ const Component = () => <CustomButton sx={{ color: 'red' }}>Click me</CustomButton>`,
359
+ options: [{styledComponents: ['CustomButton']}],
360
+ errors: [
361
+ {
362
+ messageId: 'useStyledReactImport',
363
+ data: {componentName: 'CustomButton'},
364
+ },
365
+ ],
366
+ },
367
+ // Invalid: Custom utility should be from styled-react
368
+ {
369
+ code: `import { customSx } from '@primer/react'`,
370
+ output: `import { customSx } from '@primer/styled-react'`,
371
+ options: [{styledUtilities: ['customSx']}],
372
+ errors: [
244
373
  {
245
- messageId: 'useAliasedComponent',
246
- data: {componentName: 'Button', aliasName: 'StyledButton'},
374
+ messageId: 'moveToStyledReact',
375
+ data: {importName: 'customSx'},
247
376
  },
248
377
  ],
249
378
  },
@@ -48,6 +48,7 @@ const excludedComponentProps = new Map([
48
48
  ['PointerBox', new Set(['bg'])],
49
49
  ['Truncate', new Set(['maxWidth'])],
50
50
  ['Stack', new Set(['padding', 'gap'])],
51
+ ['SkeletonBox', new Set(['width', 'height'])],
51
52
  ])
52
53
 
53
54
  const alwaysExcludedProps = new Set(['variant', 'size'])
@@ -3,8 +3,8 @@
3
3
  const url = require('../url')
4
4
  const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
5
5
 
6
- // Components that should be imported from @primer/styled-react when used with sx prop
7
- const styledComponents = new Set([
6
+ // Default components that should be imported from @primer/styled-react when used with sx prop
7
+ const defaultStyledComponents = [
8
8
  'ActionList',
9
9
  'ActionMenu',
10
10
  'Box',
@@ -23,13 +23,13 @@ const styledComponents = new Set([
23
23
  'Truncate',
24
24
  'Octicon',
25
25
  'Dialog',
26
- ])
26
+ ]
27
27
 
28
- // Types that should be imported from @primer/styled-react
29
- const styledTypes = new Set(['BoxProps', 'SxProp', 'BetterSystemStyleObject'])
28
+ // Default types that should be imported from @primer/styled-react
29
+ const defaultStyledTypes = ['BoxProps', 'SxProp', 'BetterSystemStyleObject']
30
30
 
31
- // Utilities that should be imported from @primer/styled-react
32
- const styledUtilities = new Set(['sx'])
31
+ // Default utilities that should be imported from @primer/styled-react
32
+ const defaultStyledUtilities = ['sx']
33
33
 
34
34
  /**
35
35
  * @type {import('eslint').Rule.RuleModule}
@@ -43,25 +43,46 @@ module.exports = {
43
43
  url: url(module),
44
44
  },
45
45
  fixable: 'code',
46
- schema: [],
46
+ schema: [
47
+ {
48
+ type: 'object',
49
+ properties: {
50
+ styledComponents: {
51
+ type: 'array',
52
+ items: {type: 'string'},
53
+ description: 'Components that should be imported from @primer/styled-react when used with sx prop',
54
+ },
55
+ styledTypes: {
56
+ type: 'array',
57
+ items: {type: 'string'},
58
+ description: 'Types that should be imported from @primer/styled-react',
59
+ },
60
+ styledUtilities: {
61
+ type: 'array',
62
+ items: {type: 'string'},
63
+ description: 'Utilities that should be imported from @primer/styled-react',
64
+ },
65
+ },
66
+ additionalProperties: false,
67
+ },
68
+ ],
47
69
  messages: {
48
70
  useStyledReactImport: 'Import {{ componentName }} from "@primer/styled-react" when using with sx prop',
49
- useStyledReactImportWithAlias:
50
- 'Import {{ componentName }} as {{ aliasName }} from "@primer/styled-react" when using with sx prop (conflicts with non-sx usage)',
51
- useAliasedComponent: 'Use {{ aliasName }} instead of {{ componentName }} when using sx prop',
52
71
  moveToStyledReact: 'Move {{ importName }} import to "@primer/styled-react"',
53
72
  usePrimerReactImport: 'Import {{ componentName }} from "@primer/react" when not using sx prop',
54
73
  },
55
74
  },
56
75
  create(context) {
76
+ // Get configuration options or use defaults
77
+ const options = context.options[0] || {}
78
+ const styledComponents = new Set(options.styledComponents || defaultStyledComponents)
79
+ const styledTypes = new Set(options.styledTypes || defaultStyledTypes)
80
+ const styledUtilities = new Set(options.styledUtilities || defaultStyledUtilities)
57
81
  const componentsWithSx = new Set()
58
82
  const componentsWithoutSx = new Set() // Track components used without sx
59
83
  const allUsedComponents = new Set() // Track all used components
60
84
  const primerReactImports = new Map() // Map of component name to import node
61
85
  const styledReactImports = new Map() // Map of components imported from styled-react to import node
62
- const aliasMapping = new Map() // Map local name to original component name for aliased imports
63
- const jsxElementsWithSx = [] // Track JSX elements that use sx prop
64
- const jsxElementsWithoutSx = [] // Track JSX elements that don't use sx prop
65
86
 
66
87
  return {
67
88
  ImportDeclaration(node) {
@@ -86,13 +107,7 @@ module.exports = {
86
107
  for (const specifier of node.specifiers) {
87
108
  if (specifier.type === 'ImportSpecifier') {
88
109
  const importedName = specifier.imported.name
89
- const localName = specifier.local.name
90
110
  styledReactImports.set(importedName, {node, specifier})
91
-
92
- // Track alias mapping for styled-react imports
93
- if (localName !== importedName) {
94
- aliasMapping.set(localName, importedName)
95
- }
96
111
  }
97
112
  }
98
113
  }
@@ -102,12 +117,12 @@ module.exports = {
102
117
  const openingElement = node.openingElement
103
118
  const componentName = getJSXOpeningElementName(openingElement)
104
119
 
105
- // Check if this is an aliased component from styled-react
106
- 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
107
122
 
108
123
  // Track all used components that are in our styled components list
109
- if (styledComponents.has(originalComponentName)) {
110
- allUsedComponents.add(originalComponentName)
124
+ if (styledComponents.has(parentComponentName)) {
125
+ allUsedComponents.add(parentComponentName)
111
126
 
112
127
  // Check if this component has an sx prop
113
128
  const hasSxProp = openingElement.attributes.some(
@@ -115,20 +130,9 @@ module.exports = {
115
130
  )
116
131
 
117
132
  if (hasSxProp) {
118
- componentsWithSx.add(originalComponentName)
119
- jsxElementsWithSx.push({node, componentName: originalComponentName, openingElement})
133
+ componentsWithSx.add(parentComponentName)
120
134
  } else {
121
- componentsWithoutSx.add(originalComponentName)
122
-
123
- // If this is an aliased component without sx, we need to track it for renaming
124
- if (aliasMapping.has(componentName)) {
125
- jsxElementsWithoutSx.push({
126
- node,
127
- localName: componentName,
128
- originalName: originalComponentName,
129
- openingElement,
130
- })
131
- }
135
+ componentsWithoutSx.add(parentComponentName)
132
136
  }
133
137
  }
134
138
  },
@@ -141,23 +145,16 @@ module.exports = {
141
145
  for (const componentName of componentsWithSx) {
142
146
  const importInfo = primerReactImports.get(componentName)
143
147
  if (importInfo && !styledReactImports.has(componentName)) {
144
- const hasConflict = componentsWithoutSx.has(componentName)
145
148
  const {node: importNode} = importInfo
146
149
 
147
150
  if (!importNodeChanges.has(importNode)) {
148
151
  importNodeChanges.set(importNode, {
149
152
  toMove: [],
150
- toAlias: [],
151
153
  originalSpecifiers: [...importNode.specifiers],
152
154
  })
153
155
  }
154
156
 
155
- const changes = importNodeChanges.get(importNode)
156
- if (hasConflict) {
157
- changes.toAlias.push(componentName)
158
- } else {
159
- changes.toMove.push(componentName)
160
- }
157
+ importNodeChanges.get(importNode).toMove.push(componentName)
161
158
  }
162
159
  }
163
160
 
@@ -165,15 +162,12 @@ module.exports = {
165
162
  for (const componentName of componentsWithSx) {
166
163
  const importInfo = primerReactImports.get(componentName)
167
164
  if (importInfo && !styledReactImports.has(componentName)) {
168
- // Check if this component is also used without sx prop (conflict scenario)
169
- const hasConflict = componentsWithoutSx.has(componentName)
170
-
171
165
  context.report({
172
166
  node: importInfo.specifier,
173
- messageId: hasConflict ? 'useStyledReactImportWithAlias' : 'useStyledReactImport',
174
- data: hasConflict ? {componentName, aliasName: `Styled${componentName}`} : {componentName},
167
+ messageId: 'useStyledReactImport',
168
+ data: {componentName},
175
169
  fix(fixer) {
176
- const {node: importNode, specifier} = importInfo
170
+ const {node: importNode} = importInfo
177
171
  const changes = importNodeChanges.get(importNode)
178
172
 
179
173
  if (!changes) {
@@ -181,10 +175,7 @@ module.exports = {
181
175
  }
182
176
 
183
177
  // Only apply the fix once per import node (for the first component processed)
184
- const isFirstComponent =
185
- changes.originalSpecifiers[0] === specifier ||
186
- (changes.toMove.length > 0 && changes.toMove[0] === componentName) ||
187
- (changes.toAlias.length > 0 && changes.toAlias[0] === componentName)
178
+ const isFirstComponent = changes.toMove[0] === componentName
188
179
 
189
180
  if (!isFirstComponent) {
190
181
  return null
@@ -196,27 +187,13 @@ module.exports = {
196
187
  // Find specifiers that remain in original import
197
188
  const remainingSpecifiers = changes.originalSpecifiers.filter(spec => {
198
189
  const name = spec.imported.name
199
- // Keep components that are not being moved (only aliased components stay for non-sx usage)
200
190
  return !componentsToMove.has(name)
201
191
  })
202
192
 
203
- // If no components remain, replace with new imports directly
193
+ // If no components remain, replace with new import directly
204
194
  if (remainingSpecifiers.length === 0) {
205
- // Build the new imports to replace the original
206
- const newImports = []
207
-
208
- // Add imports for moved components
209
- for (const componentName of changes.toMove) {
210
- newImports.push(`import { ${componentName} } from '@primer/styled-react'`)
211
- }
212
-
213
- // Add aliased imports for conflicted components
214
- for (const componentName of changes.toAlias) {
215
- const aliasName = `Styled${componentName}`
216
- newImports.push(`import { ${componentName} as ${aliasName} } from '@primer/styled-react'`)
217
- }
218
-
219
- 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'`))
220
197
  } else {
221
198
  // Otherwise, update the import to only include remaining components
222
199
  const remainingNames = remainingSpecifiers.map(spec => spec.imported.name)
@@ -224,56 +201,11 @@ module.exports = {
224
201
  fixer.replaceText(importNode, `import { ${remainingNames.join(', ')} } from '@primer/react'`),
225
202
  )
226
203
 
227
- // Combine all styled-react imports into a single import statement
228
- const styledReactImports = []
229
-
230
- // Add aliased components first
231
- for (const componentName of changes.toAlias) {
232
- const aliasName = `Styled${componentName}`
233
- styledReactImports.push(`${componentName} as ${aliasName}`)
234
- }
235
-
236
- // Add moved components second
237
- for (const componentName of changes.toMove) {
238
- styledReactImports.push(componentName)
239
- }
240
-
241
- if (styledReactImports.length > 0) {
242
- fixes.push(
243
- fixer.insertTextAfter(
244
- importNode,
245
- `\nimport { ${styledReactImports.join(', ')} } from '@primer/styled-react'`,
246
- ),
247
- )
248
- }
249
- }
250
-
251
- return fixes
252
- },
253
- })
254
- }
255
- }
256
-
257
- // Report on JSX elements that should use aliased components
258
- for (const {node: jsxNode, componentName, openingElement} of jsxElementsWithSx) {
259
- const hasConflict = componentsWithoutSx.has(componentName)
260
- const isImportedFromPrimerReact = primerReactImports.has(componentName)
261
-
262
- if (hasConflict && isImportedFromPrimerReact && !styledReactImports.has(componentName)) {
263
- const aliasName = `Styled${componentName}`
264
- context.report({
265
- node: openingElement,
266
- messageId: 'useAliasedComponent',
267
- data: {componentName, aliasName},
268
- fix(fixer) {
269
- const fixes = []
270
-
271
- // Replace the component name in the JSX opening tag
272
- fixes.push(fixer.replaceText(openingElement.name, aliasName))
273
-
274
- // Replace the component name in the JSX closing tag if it exists
275
- if (jsxNode.closingElement) {
276
- 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
+ )
277
209
  }
278
210
 
279
211
  return fixes
@@ -403,30 +335,6 @@ module.exports = {
403
335
  }
404
336
  }
405
337
 
406
- // Report and fix JSX elements that use aliased components without sx prop
407
- for (const {node: jsxNode, originalName, openingElement} of jsxElementsWithoutSx) {
408
- if (!componentsWithSx.has(originalName) && styledReactImports.has(originalName)) {
409
- context.report({
410
- node: openingElement,
411
- messageId: 'usePrimerReactImport',
412
- data: {componentName: originalName},
413
- fix(fixer) {
414
- const fixes = []
415
-
416
- // Replace the aliased component name with the original component name in JSX opening tag
417
- fixes.push(fixer.replaceText(openingElement.name, originalName))
418
-
419
- // Replace the aliased component name in JSX closing tag if it exists
420
- if (jsxNode.closingElement) {
421
- fixes.push(fixer.replaceText(jsxNode.closingElement.name, originalName))
422
- }
423
-
424
- return fixes
425
- },
426
- })
427
- }
428
- }
429
-
430
338
  // Also report for types and utilities that should always be from styled-react
431
339
  for (const [importName, importInfo] of primerReactImports) {
432
340
  if ((styledTypes.has(importName) || styledUtilities.has(importName)) && !styledReactImports.has(importName)) {