eslint-plugin-primer-react 4.0.2 → 4.0.3-rc.d597dea

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": "4.0.2",
3
+ "version": "4.0.3-rc.d597dea",
4
4
  "description": "ESLint rules for Primer React",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -24,13 +24,13 @@
24
24
  "@changesets/changelog-github": "^0.4.0",
25
25
  "@changesets/cli": "^2.16.0",
26
26
  "@github/prettier-config": "0.0.4",
27
- "@primer/primitives": "^7.11.14",
28
- "eslint": "^8.0.1",
27
+ "@primer/primitives": "^7.14.0",
28
+ "eslint": "^8.42.0",
29
29
  "jest": "^27.0.6"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@primer/primitives": ">=4.6.2",
33
- "eslint": "^8.0.1"
33
+ "eslint": "^8.42.0"
34
34
  },
35
35
  "prettier": "@github/prettier-config",
36
36
  "dependencies": {
@@ -14,6 +14,7 @@ module.exports = {
14
14
  'primer-react/no-deprecated-colors': 'warn',
15
15
  'primer-react/no-system-props': 'warn',
16
16
  'primer-react/a11y-tooltip-interactive-trigger': 'error',
17
+ 'primer-react/new-color-css-vars': 'error',
17
18
  'primer-react/a11y-explicit-heading': 'error'
18
19
  },
19
20
  settings: {
package/src/index.js CHANGED
@@ -4,6 +4,7 @@ module.exports = {
4
4
  'no-deprecated-colors': require('./rules/no-deprecated-colors'),
5
5
  'no-system-props': require('./rules/no-system-props'),
6
6
  'a11y-tooltip-interactive-trigger': require('./rules/a11y-tooltip-interactive-trigger'),
7
+ 'new-color-css-vars': require('./rules/new-color-css-vars'),
7
8
  'a11y-explicit-heading': require('./rules/a11y-explicit-heading')
8
9
  },
9
10
  configs: {
@@ -0,0 +1,132 @@
1
+ const rule = require('../new-color-css-vars')
2
+ const {RuleTester} = require('eslint')
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 'latest',
7
+ sourceType: 'module',
8
+ ecmaFeatures: {
9
+ jsx: true
10
+ }
11
+ }
12
+ })
13
+
14
+ ruleTester.run('no-color-css-vars', rule, {
15
+ valid: [
16
+ {
17
+ code: `{color: 'fg.default'}`
18
+ },
19
+ {
20
+ code: `<circle stroke="var(--color-border-default)" strokeWidth="2" />`
21
+ },
22
+ {
23
+ code: `<circle fill="var(--color-border-default)" strokeWidth="2" />`
24
+ },
25
+ {
26
+ code: `<div style={{ color: 'var(--color-border-default)' }}></div>`
27
+ },
28
+ {
29
+ code: `<Blankslate border></Blankslate>`
30
+ }
31
+ ],
32
+ invalid: [
33
+ {
34
+ code: `<Button sx={{color: 'var(--color-fg-muted)'}}>Test</Button>`,
35
+ output: `<Button sx={{color: 'var(--fgColor-muted, var(--color-fg-muted))'}}>Test</Button>`,
36
+ errors: [
37
+ {
38
+ message: 'Replace var(--color-fg-muted) with var(--fgColor-muted, var(--color-fg-muted))'
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ code: `
44
+ <Box sx={{
45
+ '&:hover [data-component="copy-link"] button, &:focus [data-component="copy-link"] button': {
46
+ color: 'var(--color-accent-fg)'
47
+ }
48
+ }}>
49
+ </Box>`,
50
+ output: `
51
+ <Box sx={{
52
+ '&:hover [data-component="copy-link"] button, &:focus [data-component="copy-link"] button': {
53
+ color: 'var(--fgColor-accent, var(--color-accent-fg))'
54
+ }
55
+ }}>
56
+ </Box>`,
57
+ errors: [
58
+ {
59
+ message: 'Replace var(--color-accent-fg) with var(--fgColor-accent, var(--color-accent-fg))'
60
+ }
61
+ ]
62
+ },
63
+ {
64
+ code: `<Box sx={{boxShadow: '0 0 0 2px var(--color-canvas-subtle)'}} />`,
65
+ output: `<Box sx={{boxShadow: '0 0 0 2px var(--bgColor-muted, var(--color-canvas-subtle))'}} />`,
66
+ errors: [
67
+ {
68
+ message: 'Replace var(--color-canvas-subtle) with var(--bgColor-muted, var(--color-canvas-subtle))'
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ code: `<Box sx={{border: 'solid 2px var(--color-border-default)'}} />`,
74
+ output: `<Box sx={{border: 'solid 2px var(--borderColor-default, var(--color-border-default))'}} />`,
75
+ errors: [
76
+ {
77
+ message: 'Replace var(--color-border-default) with var(--borderColor-default, var(--color-border-default))'
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ code: `<Box sx={{backgroundColor: 'var(--color-canvas-default)'}} />`,
83
+ output: `<Box sx={{backgroundColor: 'var(--bgColor-default, var(--color-canvas-default))'}} />`,
84
+ errors: [
85
+ {
86
+ message: 'Replace var(--color-canvas-default) with var(--bgColor-default, var(--color-canvas-default))'
87
+ }
88
+ ]
89
+ },
90
+ {
91
+ name: 'variable in scope',
92
+ code: `
93
+ const baseStyles = { color: 'var(--color-fg-muted)' }
94
+ export const Fixture = <Button sx={baseStyles}>Test</Button>
95
+ `,
96
+ output: `
97
+ const baseStyles = { color: 'var(--fgColor-muted, var(--color-fg-muted))' }
98
+ export const Fixture = <Button sx={baseStyles}>Test</Button>
99
+ `,
100
+ errors: [
101
+ {
102
+ message: 'Replace var(--color-fg-muted) with var(--fgColor-muted, var(--color-fg-muted))'
103
+ }
104
+ ]
105
+ },
106
+ {
107
+ name: 'merge in sx',
108
+ code: `
109
+ import {merge} from '@primer/react'
110
+ export const Fixture = props => <Button sx={merge({color: 'var(--color-fg-muted)'}, props.sx)}>Test</Button>
111
+ `,
112
+ output: `
113
+ import {merge} from '@primer/react'
114
+ export const Fixture = props => <Button sx={merge({color: 'var(--fgColor-muted, var(--color-fg-muted))'}, props.sx)}>Test</Button>
115
+ `,
116
+ errors: [
117
+ {
118
+ message: 'Replace var(--color-fg-muted) with var(--fgColor-muted, var(--color-fg-muted))'
119
+ }
120
+ ]
121
+ },
122
+ {
123
+ code: `<Box sx={{borderColor: 'var(--color-border-default)'}} />`,
124
+ output: `<Box sx={{borderColor: 'var(--borderColor-default, var(--color-border-default))'}} />`,
125
+ errors: [
126
+ {
127
+ message: 'Replace var(--color-border-default) with var(--borderColor-default, var(--color-border-default))'
128
+ }
129
+ ]
130
+ }
131
+ ]
132
+ })
@@ -0,0 +1,106 @@
1
+ const cssVars = require('../utils/css-variable-map.json')
2
+
3
+ module.exports = {
4
+ meta: {
5
+ type: 'suggestion',
6
+ hasSuggestions: true,
7
+ fixable: 'code',
8
+ docs: {
9
+ description: 'Upgrade legacy CSS variables to Primitives v8 in sx prop'
10
+ },
11
+ schema: [
12
+ {
13
+ type: 'object',
14
+ properties: {
15
+ skipImportCheck: {
16
+ type: 'boolean'
17
+ },
18
+ checkAllStrings: {
19
+ type: 'boolean'
20
+ }
21
+ },
22
+ additionalProperties: false
23
+ }
24
+ ]
25
+ },
26
+ /** @param {import('eslint').Rule.RuleContext} context */
27
+ create(context) {
28
+ const styledSystemProps = [
29
+ 'bg',
30
+ 'backgroundColor',
31
+ 'color',
32
+ 'borderColor',
33
+ 'borderTopColor',
34
+ 'borderRightColor',
35
+ 'borderBottomColor',
36
+ 'borderLeftColor',
37
+ 'border',
38
+ 'boxShadow',
39
+ 'caretColor'
40
+ ]
41
+
42
+ return {
43
+ /** @param {import('eslint').Rule.Node} node */
44
+ JSXAttribute(node) {
45
+ if (node.name.name === 'sx') {
46
+ if (node.value.expression.type === 'ObjectExpression') {
47
+ // example: sx={{ color: 'fg.default' }} or sx={{ ':hover': {color: 'fg.default'} }}
48
+ const rawText = context.sourceCode.getText(node.value)
49
+ checkForVariables(node.value, rawText)
50
+ } else if (node.value.expression.type === 'Identifier') {
51
+ // example: sx={baseStyles}
52
+ const variableScope = context.sourceCode.getScope(node.value.expression)
53
+ const variable = variableScope.set.get(node.value.expression.name)
54
+
55
+ // if variable is not defined in scope, give up (could be imported from different file)
56
+ if (!variable) return
57
+
58
+ const variableDeclarator = variable.identifiers[0].parent
59
+ const rawText = context.sourceCode.getText(variableDeclarator)
60
+ checkForVariables(variableDeclarator, rawText)
61
+ } else {
62
+ // worth a try!
63
+ const rawText = context.sourceCode.getText(node.value)
64
+ checkForVariables(node.value, rawText)
65
+ }
66
+ } else if (
67
+ styledSystemProps.includes(node.name.name) &&
68
+ node.value &&
69
+ node.value.type === 'Literal' &&
70
+ typeof node.value.value === 'string'
71
+ ) {
72
+ checkForVariables(node.value, node.value.value)
73
+ }
74
+ }
75
+ }
76
+
77
+ function checkForVariables(node, rawText) {
78
+ // performance optimisation: exit early
79
+ if (!rawText.includes('var')) return
80
+
81
+ Object.keys(cssVars).forEach(cssVar => {
82
+ if (Array.isArray(cssVars[cssVar])) {
83
+ cssVars[cssVar].forEach(cssVarObject => {
84
+ const regex = new RegExp(`var\\(${cssVar}\\)`, 'g')
85
+ if (
86
+ cssVarObject.props.some(prop => rawText.includes(prop)) &&
87
+ regex.test(rawText) &&
88
+ !rawText.includes(cssVarObject.replacement)
89
+ ) {
90
+ const fixedString = rawText.replace(regex, `var(${cssVarObject.replacement}, var(${cssVar}))`)
91
+ if (!rawText.includes(fixedString)) {
92
+ context.report({
93
+ node,
94
+ message: `Replace var(${cssVar}) with var(${cssVarObject.replacement}, var(${cssVar}))`,
95
+ fix: function(fixer) {
96
+ return fixer.replaceText(node, node.type === 'Literal' ? `"${fixedString}"` : fixedString)
97
+ }
98
+ })
99
+ }
100
+ }
101
+ })
102
+ }
103
+ })
104
+ }
105
+ }
106
+ }