eslint-plugin-primer-react 8.2.1 → 8.3.0-rc.4b58a28

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.2.1",
3
+ "version": "8.3.0-rc.4b58a28",
4
4
  "description": "ESLint rules for Primer React",
5
5
  "main": "src/index.js",
6
6
  "engines": {
@@ -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.44.0",
49
+ "@typescript-eslint/rule-tester": "8.44.1",
50
50
  "eslint": "^9.0.0",
51
51
  "eslint-plugin-eslint-comments": "^3.2.0",
52
52
  "eslint-plugin-filenames": "^1.3.2",
@@ -3,6 +3,7 @@ const {RuleTester} = require('eslint')
3
3
 
4
4
  const ruleTester = new RuleTester({
5
5
  languageOptions: {
6
+ parser: require(require.resolve('@typescript-eslint/parser', {paths: [require.resolve('eslint-plugin-github')]})),
6
7
  ecmaVersion: 'latest',
7
8
  sourceType: 'module',
8
9
  parserOptions: {
@@ -25,6 +26,8 @@ ruleTester.run('use-styled-react-import', rule, {
25
26
 
26
27
  // Valid: Utilities imported from styled-react
27
28
  `import { sx } from '@primer/styled-react'`,
29
+ `import { useTheme } from '@primer/styled-react'`,
30
+ `import { sx, useTheme } from '@primer/styled-react'`,
28
31
 
29
32
  // Valid: Component not in the styled list
30
33
  `import { Avatar } from '@primer/react'
@@ -40,6 +43,14 @@ ruleTester.run('use-styled-react-import', rule, {
40
43
 
41
44
  // Valid: Component without sx prop imported from styled-react (when not used)
42
45
  `import { Button } from '@primer/styled-react'`,
46
+
47
+ // Valid: allowedComponents without sx prop imported from styled-react
48
+ `import { ThemeProvider, BaseStyles } from '@primer/styled-react'
49
+ const Component = ({children}) => <ThemeProvider><BaseStyles>{children}</BaseStyles></ThemeProvider>`,
50
+
51
+ // Valid: Component with sx prop AND allowedComponents
52
+ `import { ThemeProvider, Button } from '@primer/styled-react'
53
+ const Component = () => <ThemeProvider><Button sx={{ color: 'btn.bg'}}>Click me</Button></ThemeProvider>`,
43
54
  ],
44
55
  invalid: [
45
56
  // Invalid: Box with sx prop imported from @primer/react
@@ -205,6 +216,43 @@ import { Box } from '@primer/styled-react'
205
216
  },
206
217
  ],
207
218
  },
219
+ {
220
+ code: `import { useTheme } from '@primer/react'`,
221
+ output: `import { useTheme } from '@primer/styled-react'`,
222
+ errors: [
223
+ {
224
+ messageId: 'moveToStyledReact',
225
+ data: {importName: 'useTheme'},
226
+ },
227
+ ],
228
+ },
229
+ {
230
+ code: `import { sx, useTheme } from '@primer/react'`,
231
+ output: `import { sx, useTheme } from '@primer/styled-react'`,
232
+ errors: [
233
+ {
234
+ messageId: 'moveToStyledReact',
235
+ data: {importName: 'sx'},
236
+ },
237
+ {
238
+ messageId: 'moveToStyledReact',
239
+ data: {importName: 'useTheme'},
240
+ },
241
+ ],
242
+ },
243
+
244
+ // Invalid: Utility import from @primer/react that should be from styled-react, mixed with other imports
245
+ {
246
+ code: `import { sx, useAnchoredPosition } from '@primer/react'`,
247
+ output: `import { useAnchoredPosition } from '@primer/react'
248
+ import { sx } from '@primer/styled-react'`,
249
+ errors: [
250
+ {
251
+ messageId: 'moveToStyledReact',
252
+ data: {importName: 'sx'},
253
+ },
254
+ ],
255
+ },
208
256
 
209
257
  // Invalid: Button and Link, only Button uses sx
210
258
  {
@@ -335,6 +383,61 @@ import { Button } from '@primer/react'
335
383
  },
336
384
  ],
337
385
  },
386
+
387
+ // Invalid: ThemeProvider and BaseStyles - should move to styled-react
388
+ {
389
+ code: `
390
+ import { ThemeProvider, BaseStyles } from '@primer/react'
391
+ `,
392
+ output: `
393
+ import { ThemeProvider, BaseStyles } from '@primer/styled-react'
394
+ `,
395
+ errors: [
396
+ {
397
+ messageId: 'moveToStyledReact',
398
+ data: {importName: 'ThemeProvider'},
399
+ },
400
+ {
401
+ messageId: 'moveToStyledReact',
402
+ data: {importName: 'BaseStyles'},
403
+ },
404
+ ],
405
+ },
406
+
407
+ // Invalid: ThemeProvider, Button without sx prop - only ThemeProvider should be from styled-react
408
+ {
409
+ code: `
410
+ import { ThemeProvider, Button } from '@primer/react'
411
+ const Component = () => <ThemeProvider><Button>Click me</Button></ThemeProvider>
412
+ `,
413
+ output: `
414
+ import { Button } from '@primer/react'
415
+ import { ThemeProvider } from '@primer/styled-react'
416
+ const Component = () => <ThemeProvider><Button>Click me</Button></ThemeProvider>
417
+ `,
418
+ errors: [
419
+ {
420
+ messageId: 'moveToStyledReact',
421
+ data: {importName: 'ThemeProvider'},
422
+ },
423
+ ],
424
+ },
425
+
426
+ // Invalid: Utility and type imports from @primer/react that should be from styled-react
427
+ {
428
+ code: `import { sx, type SxProp } from '@primer/react'`,
429
+ output: `import { sx, type SxProp } from '@primer/styled-react'`,
430
+ errors: [
431
+ {
432
+ messageId: 'moveToStyledReact',
433
+ data: {importName: 'sx'},
434
+ },
435
+ {
436
+ messageId: 'moveToStyledReact',
437
+ data: {importName: 'SxProp'},
438
+ },
439
+ ],
440
+ },
338
441
  ],
339
442
  })
340
443
 
@@ -23,13 +23,17 @@ const defaultStyledComponents = [
23
23
  'Truncate',
24
24
  'Octicon',
25
25
  'Dialog',
26
+ 'ThemeProvider',
27
+ 'BaseStyles',
26
28
  ]
27
29
 
30
+ const componentsToAlwaysImportFromStyledReact = new Set(['ThemeProvider', 'BaseStyles'])
31
+
28
32
  // Default types that should be imported from @primer/styled-react
29
33
  const defaultStyledTypes = ['BoxProps', 'SxProp', 'BetterSystemStyleObject']
30
34
 
31
35
  // Default utilities that should be imported from @primer/styled-react
32
- const defaultStyledUtilities = ['sx']
36
+ const defaultStyledUtilities = ['sx', 'useTheme']
33
37
 
34
38
  /**
35
39
  * @type {import('eslint').Rule.RuleModule}
@@ -246,7 +250,11 @@ module.exports = {
246
250
  // Report errors for components used WITHOUT sx prop that are imported from @primer/styled-react
247
251
  for (const componentName of allUsedComponents) {
248
252
  // If component is used but NOT with sx prop, and it's imported from styled-react
249
- if (!componentsWithSx.has(componentName) && styledReactImports.has(componentName)) {
253
+ if (
254
+ !componentsWithSx.has(componentName) &&
255
+ styledReactImports.has(componentName) &&
256
+ !componentsToAlwaysImportFromStyledReact.has(componentName)
257
+ ) {
250
258
  const importInfo = styledReactImports.get(componentName)
251
259
  context.report({
252
260
  node: importInfo.specifier,
@@ -337,53 +345,67 @@ module.exports = {
337
345
  }
338
346
  }
339
347
 
340
- // Also report for types and utilities that should always be from styled-react
348
+ // Also report for types, utilities and components that should always be from styled-react
341
349
  for (const [importName, importInfo] of primerReactImports) {
342
- if ((styledTypes.has(importName) || styledUtilities.has(importName)) && !styledReactImports.has(importName)) {
350
+ if (
351
+ (styledTypes.has(importName) ||
352
+ styledUtilities.has(importName) ||
353
+ componentsToAlwaysImportFromStyledReact.has(importName)) &&
354
+ !styledReactImports.has(importName)
355
+ ) {
343
356
  context.report({
344
357
  node: importInfo.specifier,
345
358
  messageId: 'moveToStyledReact',
346
359
  data: {importName},
347
360
  fix(fixer) {
348
361
  const {node: importNode, specifier, importSource} = importInfo
349
- const otherSpecifiers = importNode.specifiers.filter(s => s !== specifier)
350
362
 
351
- // Convert @primer/react path to @primer/styled-react path
352
- const styledReactPath = importSource.replace('@primer/react', '@primer/styled-react')
363
+ const fixes = []
353
364
 
354
- // If this is the only import, replace the whole import
355
- if (otherSpecifiers.length === 0) {
356
- const prefix = styledTypes.has(importName) ? 'type ' : ''
357
- return fixer.replaceText(importNode, `import { ${prefix}${importName} } from '${styledReactPath}'`)
358
- }
365
+ // we consolidate all the fixes for the import in the first specifier
366
+ const isFirst = importNode.specifiers[0] === specifier
367
+ if (!isFirst) return null
359
368
 
360
- // Otherwise, remove from current import and add new import
361
- const fixes = []
369
+ const specifiersToMove = importNode.specifiers.filter(specifier => {
370
+ const name = specifier.imported.name
371
+ return (
372
+ styledUtilities.has(name) ||
373
+ styledTypes.has(name) ||
374
+ componentsToAlwaysImportFromStyledReact.has(name)
375
+ )
376
+ })
377
+
378
+ const remainingSpecifiers = importNode.specifiers.filter(specifier => {
379
+ return !specifiersToMove.includes(specifier)
380
+ })
381
+
382
+ // Convert @primer/react path to @primer/styled-react path
383
+ const styledReactPath = importSource.replace('@primer/react', '@primer/styled-react')
362
384
 
363
- // Remove the specifier from current import
364
- if (importNode.specifiers.length === 1) {
385
+ if (remainingSpecifiers.length === 0) {
386
+ // if there are no remaining specifiers, we can remove the whole import
365
387
  fixes.push(fixer.remove(importNode))
366
388
  } else {
367
- const isFirst = importNode.specifiers[0] === specifier
368
- const isLast = importNode.specifiers[importNode.specifiers.length - 1] === specifier
369
-
370
- if (isFirst) {
371
- const nextSpecifier = importNode.specifiers[1]
372
- fixes.push(fixer.removeRange([specifier.range[0], nextSpecifier.range[0]]))
373
- } else if (isLast) {
374
- const prevSpecifier = importNode.specifiers[importNode.specifiers.length - 2]
375
- fixes.push(fixer.removeRange([prevSpecifier.range[1], specifier.range[1]]))
376
- } else {
377
- const nextSpecifier = importNode.specifiers[importNode.specifiers.indexOf(specifier) + 1]
378
- fixes.push(fixer.removeRange([specifier.range[0], nextSpecifier.range[0]]))
379
- }
389
+ const remainingNames = remainingSpecifiers.map(spec =>
390
+ spec.importKind === 'type' ? `type ${spec.imported.name}` : spec.imported.name,
391
+ )
392
+ fixes.push(
393
+ fixer.replaceText(importNode, `import { ${remainingNames.join(', ')} } from '${importSource}'`),
394
+ )
380
395
  }
381
396
 
382
- // Add new import
383
- const prefix = styledTypes.has(importName) ? 'type ' : ''
384
- fixes.push(
385
- fixer.insertTextAfter(importNode, `\nimport { ${prefix}${importName} } from '${styledReactPath}'`),
386
- )
397
+ if (specifiersToMove.length > 0) {
398
+ const movedComponents = specifiersToMove.map(spec =>
399
+ spec.importKind === 'type' ? `type ${spec.imported.name}` : spec.imported.name,
400
+ )
401
+ const onNewLine = remainingSpecifiers.length > 0
402
+ fixes.push(
403
+ fixer.insertTextAfter(
404
+ importNode,
405
+ `${onNewLine ? '\n' : ''}import { ${movedComponents.join(', ')} } from '${styledReactPath}'`,
406
+ ),
407
+ )
408
+ }
387
409
 
388
410
  return fixes
389
411
  },