eslint-plugin-primer-react 8.6.1 → 9.0.0-rc.e1d461c
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 +9 -0
- package/README.md +1 -3
- package/package.json +2 -4
- package/src/configs/recommended.js +2 -2
- package/src/index.js +1 -3
- package/docs/rules/no-system-props.md +0 -67
- package/docs/rules/no-unnecessary-components.md +0 -69
- package/docs/rules/use-styled-react-import.md +0 -175
- package/src/rules/__tests__/no-system-props.test.js +0 -190
- package/src/rules/__tests__/no-unnecessary-components.test.js +0 -240
- package/src/rules/__tests__/use-styled-react-import.test.js +0 -537
- package/src/rules/no-system-props.js +0 -206
- package/src/rules/no-unnecessary-components.js +0 -160
- package/src/rules/use-styled-react-import.js +0 -435
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
const {isPrimerComponent} = require('../utils/is-primer-component')
|
|
2
|
-
const {isHTMLElement} = require('../utils/is-html-element')
|
|
3
|
-
const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
|
|
4
|
-
const {pick} = require('@styled-system/props')
|
|
5
|
-
const {some, last} = require('lodash')
|
|
6
|
-
|
|
7
|
-
// Components for which we allow all styled system props
|
|
8
|
-
const alwaysExcludedComponents = new Set([
|
|
9
|
-
'BaseStyles', // BaseStyles will be deprecated eventually
|
|
10
|
-
])
|
|
11
|
-
|
|
12
|
-
// Excluded by default, but optionally included:
|
|
13
|
-
const utilityComponents = new Set(['Box', 'Text'])
|
|
14
|
-
|
|
15
|
-
// Components for which we allow a set of prop names
|
|
16
|
-
const excludedComponentProps = new Map([
|
|
17
|
-
['ActionMenu.Overlay', new Set(['width', 'height', 'maxHeight', 'position', 'top', 'right', 'bottom', 'left'])],
|
|
18
|
-
['ActionMenu.Button', new Set(['alignContent'])],
|
|
19
|
-
['Autocomplete.Overlay', new Set(['width', 'height', 'maxHeight', 'position', 'top', 'right', 'bottom', 'left'])],
|
|
20
|
-
['AnchoredOverlay', new Set(['width', 'height'])],
|
|
21
|
-
['Avatar', new Set(['size'])],
|
|
22
|
-
['AvatarToken', new Set(['size'])],
|
|
23
|
-
['Blankslate', new Set(['border'])],
|
|
24
|
-
['Breadcrumbs', new Set(['overflow'])],
|
|
25
|
-
['Button', new Set(['alignContent'])],
|
|
26
|
-
['CircleOcticon', new Set(['size'])],
|
|
27
|
-
['ConfirmationDialog', new Set(['width', 'height'])],
|
|
28
|
-
['Dialog', new Set(['width', 'height', 'position'])],
|
|
29
|
-
['IssueLabelToken', new Set(['size'])],
|
|
30
|
-
['Overlay', new Set(['width', 'height', 'maxHeight', 'position', 'top', 'right', 'bottom', 'left'])],
|
|
31
|
-
['ProgressBar', new Set(['bg'])],
|
|
32
|
-
['Spinner', new Set(['size'])],
|
|
33
|
-
['SplitPageLayout.Header', new Set(['padding'])],
|
|
34
|
-
['SplitPageLayout.Footer', new Set(['padding'])],
|
|
35
|
-
['SplitPageLayout.Pane', new Set(['padding', 'position', 'width'])],
|
|
36
|
-
['SplitPageLayout.Content', new Set(['padding', 'width'])],
|
|
37
|
-
['SplitPageLayout.Sidebar', new Set(['padding', 'position', 'width'])],
|
|
38
|
-
['StyledOcticon', new Set(['size'])],
|
|
39
|
-
['Octicon', new Set(['size', 'color'])],
|
|
40
|
-
['PointerBox', new Set(['bg'])],
|
|
41
|
-
['TextInput', new Set(['size'])],
|
|
42
|
-
['TextInputWithTokens', new Set(['size', 'maxHeight'])],
|
|
43
|
-
['Token', new Set(['size'])],
|
|
44
|
-
['PageLayout', new Set(['padding'])],
|
|
45
|
-
['PageLayout.Header', new Set(['padding'])],
|
|
46
|
-
['PageLayout.Footer', new Set(['padding'])],
|
|
47
|
-
['PageLayout.Pane', new Set(['padding', 'position', 'width'])],
|
|
48
|
-
['PageLayout.Content', new Set(['padding', 'width'])],
|
|
49
|
-
['ProgressBar', new Set(['bg'])],
|
|
50
|
-
['ProgressBar.Item', new Set(['bg'])],
|
|
51
|
-
['PointerBox', new Set(['bg'])],
|
|
52
|
-
['Truncate', new Set(['maxWidth'])],
|
|
53
|
-
['Stack', new Set(['padding', 'gap'])],
|
|
54
|
-
['SkeletonBox', new Set(['width', 'height'])],
|
|
55
|
-
])
|
|
56
|
-
|
|
57
|
-
const alwaysExcludedProps = new Set(['variant', 'size'])
|
|
58
|
-
|
|
59
|
-
module.exports = {
|
|
60
|
-
meta: {
|
|
61
|
-
type: 'suggestion',
|
|
62
|
-
fixable: 'code',
|
|
63
|
-
schema: [
|
|
64
|
-
{
|
|
65
|
-
properties: {
|
|
66
|
-
skipImportCheck: {type: 'boolean'},
|
|
67
|
-
includeUtilityComponents: {type: 'boolean'},
|
|
68
|
-
ignoreNames: {type: 'array'},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
messages: {
|
|
73
|
-
noSystemProps: 'Styled-system props are deprecated ({{ componentName }} called with props: {{ propNames }})',
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
create(context) {
|
|
77
|
-
// If `skipImportCheck` is true, this rule will check for deprecated styled system props
|
|
78
|
-
// used in any components (not just ones that are imported from `@primer/react`).
|
|
79
|
-
const skipImportCheck = context.options[0] ? context.options[0].skipImportCheck : false
|
|
80
|
-
const includeUtilityComponents = context.options[0] ? context.options[0].includeUtilityComponents : false
|
|
81
|
-
const ignoreNames = context.options[0] ? context.options[0].ignoreNames || [] : []
|
|
82
|
-
|
|
83
|
-
const excludedComponents = new Set([
|
|
84
|
-
...alwaysExcludedComponents,
|
|
85
|
-
...(includeUtilityComponents ? [] : utilityComponents),
|
|
86
|
-
])
|
|
87
|
-
|
|
88
|
-
const sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
JSXOpeningElement(jsxNode) {
|
|
92
|
-
if (skipImportCheck) {
|
|
93
|
-
// if we skip checking if component is imported from primer,
|
|
94
|
-
// we need to atleast skip html elements
|
|
95
|
-
if (isHTMLElement(jsxNode)) return
|
|
96
|
-
} else {
|
|
97
|
-
// skip if component is not imported from primer/react
|
|
98
|
-
if (!isPrimerComponent(jsxNode.name, sourceCode.getScope(jsxNode))) return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const componentName = getJSXOpeningElementName(jsxNode)
|
|
102
|
-
if (ignoreNames.length && ignoreNames.includes(componentName)) return
|
|
103
|
-
|
|
104
|
-
if (excludedComponents.has(componentName)) return
|
|
105
|
-
|
|
106
|
-
// Create an object mapping from prop name to the AST node for that attribute
|
|
107
|
-
const propsByNameObject = jsxNode.attributes.reduce((object, attribute) => {
|
|
108
|
-
// We don't do anything about spreads for now — only named attributes
|
|
109
|
-
if (attribute.type === 'JSXAttribute') {
|
|
110
|
-
object[attribute.name.name] = attribute
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return object
|
|
114
|
-
}, {})
|
|
115
|
-
|
|
116
|
-
// Create an array of system prop attribute nodes
|
|
117
|
-
let systemProps = Object.values(pick(propsByNameObject))
|
|
118
|
-
|
|
119
|
-
const excludedProps = excludedComponentProps.has(componentName)
|
|
120
|
-
? new Set([...alwaysExcludedProps, ...excludedComponentProps.get(componentName)])
|
|
121
|
-
: alwaysExcludedProps
|
|
122
|
-
|
|
123
|
-
// Filter out our exceptional props
|
|
124
|
-
systemProps = systemProps.filter(prop => {
|
|
125
|
-
return !excludedProps.has(prop.name.name)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
if (systemProps.length !== 0) {
|
|
129
|
-
context.report({
|
|
130
|
-
node: jsxNode,
|
|
131
|
-
messageId: 'noSystemProps',
|
|
132
|
-
data: {
|
|
133
|
-
componentName,
|
|
134
|
-
propNames: systemProps.map(a => a.name.name).join(', '),
|
|
135
|
-
},
|
|
136
|
-
fix(fixer) {
|
|
137
|
-
const existingSxProp = jsxNode.attributes.find(
|
|
138
|
-
attribute => attribute.type === 'JSXAttribute' && attribute.name.name === 'sx',
|
|
139
|
-
)
|
|
140
|
-
const systemPropstylesMap = stylesMapFromPropNodes(systemProps, context)
|
|
141
|
-
if (existingSxProp && existingSxProp.value.expression.type !== 'ObjectExpression') {
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const stylesToAdd = existingSxProp
|
|
146
|
-
? excludeSxEntriesFromStyleMap(systemPropstylesMap, existingSxProp)
|
|
147
|
-
: systemPropstylesMap
|
|
148
|
-
|
|
149
|
-
return [
|
|
150
|
-
// Remove the bad props
|
|
151
|
-
...systemProps.map(node => fixer.remove(node)),
|
|
152
|
-
...(stylesToAdd.size > 0
|
|
153
|
-
? [
|
|
154
|
-
existingSxProp
|
|
155
|
-
? // Update an existing sx prop
|
|
156
|
-
fixer.insertTextAfter(
|
|
157
|
-
last(existingSxProp.value.expression.properties),
|
|
158
|
-
`, ${objectEntriesStringFromStylesMap(stylesToAdd)}`,
|
|
159
|
-
)
|
|
160
|
-
: // Insert new sx prop
|
|
161
|
-
fixer.insertTextAfter(last(jsxNode.attributes), sxPropTextFromStylesMap(systemPropstylesMap)),
|
|
162
|
-
]
|
|
163
|
-
: []),
|
|
164
|
-
]
|
|
165
|
-
},
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const sxPropTextFromStylesMap = styles => {
|
|
174
|
-
return ` sx={{${objectEntriesStringFromStylesMap(styles)}}}`
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const objectEntriesStringFromStylesMap = styles => {
|
|
178
|
-
return [...styles].map(([name, value]) => `${name}: ${value}`).join(', ')
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Given an array of styled prop attributes, return a mapping from attribute to expression
|
|
182
|
-
const stylesMapFromPropNodes = (systemProps, context) => {
|
|
183
|
-
return new Map(
|
|
184
|
-
systemProps.map(a => [
|
|
185
|
-
a.name.name,
|
|
186
|
-
a.value === null ? 'true' : a.value.raw || context.getSourceCode().getText(a.value.expression),
|
|
187
|
-
]),
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Given a style map and an existing sx prop, return a style map containing
|
|
192
|
-
// only the entries that aren't already overridden by an sx object entry
|
|
193
|
-
const excludeSxEntriesFromStyleMap = (stylesMap, sxProp) => {
|
|
194
|
-
if (
|
|
195
|
-
!sxProp.value ||
|
|
196
|
-
sxProp.value.type !== 'JSXExpressionContainer' ||
|
|
197
|
-
sxProp.value.expression.type !== 'ObjectExpression'
|
|
198
|
-
) {
|
|
199
|
-
return stylesMap
|
|
200
|
-
}
|
|
201
|
-
return new Map(
|
|
202
|
-
[...stylesMap].filter(([key]) => {
|
|
203
|
-
return !some(sxProp.value.expression.properties, p => p.type === 'Property' && p.key.name === key)
|
|
204
|
-
}),
|
|
205
|
-
)
|
|
206
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
|
|
3
|
-
const {ESLintUtils} = require('@typescript-eslint/utils')
|
|
4
|
-
const {IndexKind} = require('typescript')
|
|
5
|
-
const {pick: pickStyledSystemProps} = require('@styled-system/props')
|
|
6
|
-
const {isPrimerComponent} = require('../utils/is-primer-component')
|
|
7
|
-
|
|
8
|
-
/** @typedef {import('@typescript-eslint/types').TSESTree.JSXAttribute} JSXAttribute */
|
|
9
|
-
|
|
10
|
-
const components = {
|
|
11
|
-
Box: {
|
|
12
|
-
replacement: 'div',
|
|
13
|
-
messageId: 'unecessaryBox',
|
|
14
|
-
message: 'Prefer plain HTML elements over `Box` when not using `sx` for styling.',
|
|
15
|
-
allowedProps: new Set(['sx']), // + styled-system props
|
|
16
|
-
},
|
|
17
|
-
Text: {
|
|
18
|
-
replacement: 'span',
|
|
19
|
-
messageId: 'unecessarySpan',
|
|
20
|
-
message: 'Prefer plain HTML elements over `Text` when not using `sx` for styling.',
|
|
21
|
-
allowedProps: new Set(['sx', 'size', 'weight']), // + styled-system props
|
|
22
|
-
},
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const elementNameRegex = /^[a-z]\w*$/
|
|
26
|
-
const componentNameRegex = /^[A-Z][\w._]*$/
|
|
27
|
-
|
|
28
|
-
/** @param {string} propName */
|
|
29
|
-
const isStyledSystemProp = propName => propName in pickStyledSystemProps({[propName]: propName})
|
|
30
|
-
|
|
31
|
-
const rule = ESLintUtils.RuleCreator.withoutDocs({
|
|
32
|
-
meta: {
|
|
33
|
-
docs: {
|
|
34
|
-
description:
|
|
35
|
-
'`Box` and `Text` should only be used to provide access to the `sx` styling system and have a performance cost. If `sx` props are not being used, prefer `div` and `span` instead.',
|
|
36
|
-
},
|
|
37
|
-
messages: {
|
|
38
|
-
[components.Box.messageId]: components.Box.message,
|
|
39
|
-
[components.Text.messageId]: components.Text.message,
|
|
40
|
-
},
|
|
41
|
-
type: 'problem',
|
|
42
|
-
schema: [
|
|
43
|
-
{
|
|
44
|
-
type: 'object',
|
|
45
|
-
properties: {
|
|
46
|
-
skipImportCheck: {
|
|
47
|
-
type: 'boolean',
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
additionalProperties: false,
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
fixable: 'code',
|
|
54
|
-
},
|
|
55
|
-
defaultOptions: [{skipImportCheck: false}],
|
|
56
|
-
create(context) {
|
|
57
|
-
return {
|
|
58
|
-
JSXElement({openingElement, closingElement}) {
|
|
59
|
-
const {name, attributes} = openingElement
|
|
60
|
-
|
|
61
|
-
// Ensure this is one of the components we are looking for. Note this doesn't account for import aliases; this
|
|
62
|
-
// is intentional to avoid having to do the scope tree traversal for every component of every name, which would
|
|
63
|
-
// be needlessly expensive. We just ignore aliased imports.
|
|
64
|
-
if (name.type !== 'JSXIdentifier' || !(name.name in components)) return
|
|
65
|
-
const componentConfig = components[/** @type {keyof typeof components} */ (name.name)]
|
|
66
|
-
|
|
67
|
-
// Only continue if the variable declaration is an import from @primer/react. Otherwise it could, for example,
|
|
68
|
-
// be an import from @primer/brand, which would be valid without sx.
|
|
69
|
-
const skipImportCheck = context.options[0]?.skipImportCheck
|
|
70
|
-
const isPrimer = skipImportCheck || isPrimerComponent(name, context.sourceCode.getScope(openingElement))
|
|
71
|
-
if (!isPrimer) return
|
|
72
|
-
|
|
73
|
-
/** @param {string} name */
|
|
74
|
-
const isAllowedProp = name => componentConfig.allowedProps.has(name) || isStyledSystemProp(name)
|
|
75
|
-
|
|
76
|
-
// Validate the attributes and ensure an allowed prop is present or spreaded in
|
|
77
|
-
/** @type {typeof attributes[number] | undefined | null} */
|
|
78
|
-
let asProp = undefined
|
|
79
|
-
for (const attribute of attributes) {
|
|
80
|
-
// If there is a spread type, check if the type of the spreaded value has an allowed property
|
|
81
|
-
if (attribute.type === 'JSXSpreadAttribute') {
|
|
82
|
-
const services = ESLintUtils.getParserServices(context)
|
|
83
|
-
const typeChecker = services.program.getTypeChecker()
|
|
84
|
-
|
|
85
|
-
const spreadType = services.getTypeAtLocation(attribute.argument)
|
|
86
|
-
|
|
87
|
-
// Check if the spread type has a string index signature - this could hide an allowed property
|
|
88
|
-
if (typeChecker.getIndexTypeOfType(spreadType, IndexKind.String) !== undefined) return
|
|
89
|
-
|
|
90
|
-
const spreadPropNames = typeChecker.getPropertiesOfType(spreadType).map(prop => prop.getName())
|
|
91
|
-
|
|
92
|
-
// If an allowed prop gets spread in, this is a valid use of the component
|
|
93
|
-
if (spreadPropNames.some(isAllowedProp)) return
|
|
94
|
-
|
|
95
|
-
// If there is an `as` inside the spread object, we can't autofix reliably
|
|
96
|
-
if (spreadPropNames.includes('as')) asProp = null
|
|
97
|
-
|
|
98
|
-
continue
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Has an allowed prop, so should keep using this component
|
|
102
|
-
if (attribute.name.type === 'JSXIdentifier' && isAllowedProp(attribute.name.name)) return
|
|
103
|
-
|
|
104
|
-
// If there is an `as` prop we will need to account for that when autofixing
|
|
105
|
-
if (attribute.name.type === 'JSXIdentifier' && attribute.name.name === 'as') asProp = attribute
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Determine a replacement component name accounting for the `as` prop if present
|
|
109
|
-
/** @type {string | null} */
|
|
110
|
-
let replacement = componentConfig.replacement
|
|
111
|
-
if (asProp === null) {
|
|
112
|
-
// {...{as: 'something-unusable'}}
|
|
113
|
-
replacement = null
|
|
114
|
-
} else if (asProp?.type === 'JSXAttribute') {
|
|
115
|
-
// as={ComponentReference}
|
|
116
|
-
if (asProp.value?.type === 'JSXExpressionContainer' && asProp.value.expression.type === 'Identifier') {
|
|
117
|
-
// can't just use expression.name here because we want the whole expression if it's A.B
|
|
118
|
-
const expressionStr = context.sourceCode.getText(asProp.value.expression)
|
|
119
|
-
replacement = componentNameRegex.test(expressionStr) ? expressionStr : null
|
|
120
|
-
}
|
|
121
|
-
// as={'tagName'} (surprisingly common, we really should enable `react/jsx-curly-brace-presence`)
|
|
122
|
-
else if (
|
|
123
|
-
asProp.value?.type === 'JSXExpressionContainer' &&
|
|
124
|
-
asProp.value.expression.type === 'Literal' &&
|
|
125
|
-
typeof asProp.value.expression.value === 'string' &&
|
|
126
|
-
elementNameRegex.test(asProp.value.expression.value)
|
|
127
|
-
) {
|
|
128
|
-
replacement = asProp.value.expression.value
|
|
129
|
-
}
|
|
130
|
-
// as="tagName"
|
|
131
|
-
else if (
|
|
132
|
-
asProp.value?.type === 'Literal' &&
|
|
133
|
-
typeof asProp.value.value === 'string' &&
|
|
134
|
-
elementNameRegex.test(asProp.value.value)
|
|
135
|
-
) {
|
|
136
|
-
replacement = asProp.value.value
|
|
137
|
-
}
|
|
138
|
-
// too complex to autofix
|
|
139
|
-
else {
|
|
140
|
-
replacement = null
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
context.report({
|
|
145
|
-
node: name,
|
|
146
|
-
messageId: componentConfig.messageId,
|
|
147
|
-
fix: replacement
|
|
148
|
-
? function* (fixer) {
|
|
149
|
-
yield fixer.replaceText(name, replacement)
|
|
150
|
-
if (closingElement) yield fixer.replaceText(closingElement.name, replacement)
|
|
151
|
-
if (asProp) yield fixer.remove(asProp)
|
|
152
|
-
}
|
|
153
|
-
: undefined,
|
|
154
|
-
})
|
|
155
|
-
},
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
module.exports = {...rule, components}
|