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.
@@ -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}