eslint-plugin-primer-react 9.3.2-rc.8eef5f9 → 9.4.0-rc.a59f32a
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
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# eslint-plugin-primer-react
|
|
2
2
|
|
|
3
|
-
## 9.
|
|
3
|
+
## 9.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#705](https://github.com/primer/eslint-plugin-primer-react/pull/705) [`9b282ba`](https://github.com/primer/eslint-plugin-primer-react/commit/9b282babfb4436dee689eaeeec568b1f6b3ce8b4) Thanks [@rickyzhangca](https://github.com/rickyzhangca)! - Expand `no-deprecated-props` to cover known deprecated Primer props, including compound components and aliased imports.
|
|
4
8
|
|
|
5
9
|
### Patch Changes
|
|
6
10
|
|
|
@@ -1,48 +1,31 @@
|
|
|
1
1
|
## Rule Details
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Reports known deprecated Primer React props. Supports compound components, import
|
|
4
|
+
aliases, and namespace imports without TypeScript type information.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
### Incorrect
|
|
6
7
|
|
|
7
8
|
```jsx
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
<ActionList>
|
|
13
|
-
<ActionList.Group title="Group heading">
|
|
14
|
-
<ActionList.Item>Item 1</ActionList.Item>
|
|
15
|
-
</ActionList.Group>
|
|
16
|
-
</ActionList>
|
|
17
|
-
)
|
|
9
|
+
import {Autocomplete, Banner} from '@primer/react'
|
|
10
|
+
|
|
11
|
+
const input = <Autocomplete.Input openOnFocus />
|
|
12
|
+
const banner = <Banner icon={InfoIcon} />
|
|
18
13
|
```
|
|
19
14
|
|
|
20
|
-
|
|
15
|
+
### Correct
|
|
21
16
|
|
|
22
17
|
```jsx
|
|
23
|
-
|
|
24
|
-
import {ActionList} from '@primer/react'
|
|
25
|
-
|
|
26
|
-
const App = () => (
|
|
27
|
-
<ActionList>
|
|
28
|
-
<ActionList.Group>
|
|
29
|
-
<ActionList.GroupHeading as="h2">Group heading</ActionList.GroupHeading>
|
|
30
|
-
<ActionList.Item>Item 1</ActionList.Item>
|
|
31
|
-
</ActionList.Group>
|
|
32
|
-
</ActionList>
|
|
33
|
-
)
|
|
34
|
-
```
|
|
18
|
+
import {Autocomplete, Banner} from '@primer/react'
|
|
35
19
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
import {ActionList} from '@primer/react'
|
|
39
|
-
|
|
40
|
-
const App = () => (
|
|
41
|
-
<ActionList role="lisbox">
|
|
42
|
-
<ActionList.Group>
|
|
43
|
-
<ActionList.GroupHeading>Group heading</ActionList.GroupHeading>
|
|
44
|
-
<ActionList.Item>Item 1</ActionList.Item>
|
|
45
|
-
</ActionList.Group>
|
|
46
|
-
</ActionList>
|
|
47
|
-
)
|
|
20
|
+
const input = <Autocomplete.Input />
|
|
21
|
+
const banner = <Banner leadingVisual={InfoIcon} />
|
|
48
22
|
```
|
|
23
|
+
|
|
24
|
+
Removing `openOnFocus` changes when suggestions open; it has no replacement prop.
|
|
25
|
+
|
|
26
|
+
The [prop list](../../src/rules/no-deprecated-props.js) is maintained manually.
|
|
27
|
+
The rule checks explicit JSX props, not spreads, nested objects, wrappers,
|
|
28
|
+
re-exports, or `React.createElement`.
|
|
29
|
+
|
|
30
|
+
Only `ActionList.Group.title` has an autofix, for paired tags with string or
|
|
31
|
+
expression values. Other migrations require manual changes.
|
package/package.json
CHANGED
|
@@ -65,59 +65,154 @@ ruleTester.run('no-deprecated-props', rule, {
|
|
|
65
65
|
</ActionList>`,
|
|
66
66
|
],
|
|
67
67
|
invalid: [
|
|
68
|
+
[
|
|
69
|
+
`<ActionList.Group title="Group heading 1"></ActionList.Group>`,
|
|
70
|
+
`<ActionList.Group><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
`<ActionList.Group title="Group heading 1" sx={{padding: 2}}></ActionList.Group>`,
|
|
74
|
+
`<ActionList.Group sx={{padding: 2}}><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
`<ActionList.Group variant="filled" title="Group heading 1"></ActionList.Group>`,
|
|
78
|
+
`<ActionList.Group variant="filled"><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
|
|
79
|
+
],
|
|
80
|
+
...['titleVariable', "'Title'", "condition ? 'Title' : undefined"].map(title => [
|
|
81
|
+
`<ActionList.Group title={${title}}></ActionList.Group>`,
|
|
82
|
+
`<ActionList.Group><ActionList.GroupHeading>{${title}}</ActionList.GroupHeading></ActionList.Group>`,
|
|
83
|
+
]),
|
|
84
|
+
].map(([code, output]) => ({
|
|
85
|
+
code: `import {ActionList} from '@primer/react';\n${code}`,
|
|
86
|
+
output: `import {ActionList} from '@primer/react';\n${output}`,
|
|
87
|
+
errors: [{messageId: 'titlePropDeprecated'}],
|
|
88
|
+
})),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const deprecatedProps = [
|
|
92
|
+
['ActionList.Item', 'as'],
|
|
93
|
+
['Autocomplete.Input', 'openOnFocus'],
|
|
94
|
+
['Autocomplete.Overlay', 'overlayProps'],
|
|
95
|
+
['Banner', 'icon'],
|
|
96
|
+
['CounterLabel', 'scheme'],
|
|
97
|
+
['IconButton', 'keyshortcuts'],
|
|
98
|
+
['Link', 'hoverColor'],
|
|
99
|
+
['PageLayout.Header', 'dividerWhenNarrow'],
|
|
100
|
+
['PageLayout.Pane', 'dividerWhenNarrow'],
|
|
101
|
+
['PageLayout.Pane', 'positionWhenNarrow'],
|
|
102
|
+
['PageLayout.Footer', 'dividerWhenNarrow'],
|
|
103
|
+
['SegmentedControl.Button', 'icon'],
|
|
104
|
+
['SegmentedControl.Button', 'leadingIcon'],
|
|
105
|
+
['SelectPanel', 'footer'],
|
|
106
|
+
['Spinner', 'aria-label'],
|
|
107
|
+
['StateLabel', 'variant'],
|
|
108
|
+
['TextInput.Action', 'children'],
|
|
109
|
+
['TextInput.Action', 'variant'],
|
|
110
|
+
['Textarea', 'contrast'],
|
|
111
|
+
['ThemeProvider', 'preventSSRMismatch'],
|
|
112
|
+
['UnderlineNav.Item', 'icon'],
|
|
113
|
+
...['TextInput', 'TextInputWithTokens', 'Autocomplete.Input'].flatMap(component =>
|
|
114
|
+
['icon', 'contrast', 'variant', 'width', 'minWidth', 'maxWidth'].map(prop => [component, prop]),
|
|
115
|
+
),
|
|
116
|
+
...['Button', 'LinkButton', 'ActionMenu.Button'].flatMap(component =>
|
|
117
|
+
['leadingIcon', 'trailingIcon'].map(prop => [component, prop]),
|
|
118
|
+
),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
ruleTester.run('no-deprecated-props catalogue', rule, {
|
|
122
|
+
valid: [
|
|
123
|
+
`import {ActionList} from '@primer/react'; <ActionList.LinkItem as="a" />`,
|
|
124
|
+
`import {Autocomplete} from '@primer/react'; <Autocomplete.Input onFocus={onFocus} />`,
|
|
125
|
+
`import {Autocomplete} from '@primer/react'; <Autocomplete.Overlay width="small" />`,
|
|
126
|
+
`import {Banner} from '@primer/react'; <Banner leadingVisual={Icon} />`,
|
|
127
|
+
`import {CounterLabel} from '@primer/react'; <CounterLabel variant="secondary" />`,
|
|
128
|
+
`import {IconButton} from '@primer/react'; <IconButton icon={Icon} keybindingHint="g" />`,
|
|
129
|
+
`import {Link} from '@primer/react'; <Link className="hover" />`,
|
|
130
|
+
`import {PageLayout} from '@primer/react'; <PageLayout.Pane divider={{narrow: 'line'}} position={{narrow: 'end'}} />`,
|
|
131
|
+
`import {SegmentedControl} from '@primer/react'; <SegmentedControl.Button leadingVisual={Icon} />`,
|
|
132
|
+
`import {SelectPanel} from '@primer/react'; <SelectPanel secondaryAction={action} />`,
|
|
133
|
+
`import {Spinner} from '@primer/react'; <Spinner srText="Loading" />`,
|
|
134
|
+
`import {StateLabel} from '@primer/react'; <StateLabel size="small" />`,
|
|
135
|
+
`import {TextInput} from '@primer/react'; <TextInput leadingVisual={Icon} size="small" style={{width: 100}} />`,
|
|
136
|
+
`import {TextInput} from '@primer/react'; <TextInput.Action icon={Icon}>{/* no children */}</TextInput.Action>`,
|
|
137
|
+
`import {UnderlineNav} from '@primer/react'; <UnderlineNav.Item leadingVisual={Icon} />`,
|
|
138
|
+
`import {Banner} from '@primer/react-brand'; <Banner icon={Icon} />`,
|
|
139
|
+
`import * as Primer from '@primer/react-brand'; <Primer.Autocomplete.Input openOnFocus />`,
|
|
140
|
+
`import {SelectPanel} from '@primer/react/experimental'; <SelectPanel footer={footer} />`,
|
|
141
|
+
`import {ActionList} from '@primer/react/deprecated'; <ActionList.Item as="a" />`,
|
|
142
|
+
`import {ActionList} from './local'; <ActionList.Group title="Local title" />`,
|
|
143
|
+
`const Autocomplete = local; <Autocomplete.Input openOnFocus />`,
|
|
144
|
+
`import {Banner} from '@primer/react'; function Example(Banner) { return <Banner icon={Icon} /> }`,
|
|
145
|
+
`import * as Primer from '@primer/react'; function Example(Primer) { return <Primer.Banner icon={Icon} /> }`,
|
|
146
|
+
`import {Autocomplete} from '@primer/react'; function Example(Autocomplete) { return <Autocomplete.Input openOnFocus /> }`,
|
|
147
|
+
`import {Banner} from '@primer/react'; <Banner {...props} />`,
|
|
148
|
+
`import * as Primer from '@primer/react'; <Primer.toString icon={Icon} />`,
|
|
149
|
+
`import {Banner} from '@primer/react'; <Banner toString={value} />`,
|
|
150
|
+
`<ActionList.Group title="Unresolved component" />`,
|
|
151
|
+
`<svg:path title="Not a component" />`,
|
|
152
|
+
],
|
|
153
|
+
invalid: [
|
|
154
|
+
...deprecatedProps.flatMap(([component, prop]) => {
|
|
155
|
+
const [root, ...members] = component.split('.')
|
|
156
|
+
const alias = ['Alias', ...members].join('.')
|
|
157
|
+
return [
|
|
158
|
+
`import {${root}} from '@primer/react'; <${component} ${prop}={value} />`,
|
|
159
|
+
`import {${root} as Alias} from '@primer/react'; <${alias} ${prop}={value} />`,
|
|
160
|
+
`import * as Primer from '@primer/react'; <Primer.${component} ${prop}={value} />`,
|
|
161
|
+
].map(code => ({
|
|
162
|
+
code,
|
|
163
|
+
output: null,
|
|
164
|
+
errors: [{messageId: 'deprecatedProp'}],
|
|
165
|
+
}))
|
|
166
|
+
}),
|
|
68
167
|
{
|
|
69
|
-
code:
|
|
70
|
-
output:
|
|
71
|
-
errors: [
|
|
72
|
-
{
|
|
73
|
-
messageId: 'titlePropDeprecated',
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
code: `<ActionList.Group title="Group heading 1" sx={{padding: 2}}></ActionList.Group>`,
|
|
79
|
-
output: `<ActionList.Group sx={{padding: 2}}><ActionList.GroupHeading>Group heading 1</ActionList.GroupHeading></ActionList.Group>`,
|
|
80
|
-
errors: [
|
|
81
|
-
{
|
|
82
|
-
messageId: 'titlePropDeprecated',
|
|
83
|
-
},
|
|
84
|
-
],
|
|
168
|
+
code: `import {Autocomplete as A} from '@primer/react'; <A.Input openOnFocus={false} />`,
|
|
169
|
+
output: null,
|
|
170
|
+
errors: [{messageId: 'deprecatedProp'}],
|
|
85
171
|
},
|
|
86
172
|
{
|
|
87
|
-
code:
|
|
88
|
-
output:
|
|
89
|
-
errors: [
|
|
90
|
-
{
|
|
91
|
-
messageId: 'titlePropDeprecated',
|
|
92
|
-
},
|
|
93
|
-
],
|
|
173
|
+
code: `import {TextInput} from '@primer/react'; <TextInput icon={Icon} contrast />`,
|
|
174
|
+
output: null,
|
|
175
|
+
errors: [{messageId: 'deprecatedProp'}, {messageId: 'deprecatedProp'}],
|
|
94
176
|
},
|
|
177
|
+
...['trailingIcon', 'trailingText'].map(prop => ({
|
|
178
|
+
code: `import {ActionList as Legacy} from '@primer/react/deprecated'; <Legacy.Item ${prop}={value} />`,
|
|
179
|
+
output: null,
|
|
180
|
+
errors: [{messageId: 'deprecatedProp'}],
|
|
181
|
+
})),
|
|
182
|
+
...['icon', 'contrast', 'variant', 'width', 'minWidth', 'maxWidth'].map(prop => ({
|
|
183
|
+
code: `import {FilteredActionList} from '@primer/react/experimental'; <FilteredActionList.Input ${prop}={value} />`,
|
|
184
|
+
output: null,
|
|
185
|
+
errors: [{messageId: 'deprecatedProp'}],
|
|
186
|
+
})),
|
|
95
187
|
{
|
|
96
|
-
code:
|
|
97
|
-
output:
|
|
98
|
-
errors: [
|
|
99
|
-
{
|
|
100
|
-
messageId: 'titlePropDeprecated',
|
|
101
|
-
},
|
|
102
|
-
],
|
|
188
|
+
code: `import {ThemeProvider} from '@primer/react/next'; <ThemeProvider preventSSRMismatch />`,
|
|
189
|
+
output: null,
|
|
190
|
+
errors: [{messageId: 'deprecatedProp'}],
|
|
103
191
|
},
|
|
192
|
+
...['Text', '{value}', '<span />', '<>Text</>'].map(children => ({
|
|
193
|
+
code: `import {TextInput} from '@primer/react'; <TextInput.Action>${children}</TextInput.Action>`,
|
|
194
|
+
output: null,
|
|
195
|
+
errors: [{messageId: 'deprecatedProp'}],
|
|
196
|
+
})),
|
|
104
197
|
{
|
|
105
|
-
code:
|
|
106
|
-
output:
|
|
107
|
-
errors: [
|
|
108
|
-
{
|
|
109
|
-
messageId: 'titlePropDeprecated',
|
|
110
|
-
},
|
|
111
|
-
],
|
|
198
|
+
code: `import {ActionList as List} from '@primer/react'; <List.Group title="Title"></List.Group>`,
|
|
199
|
+
output: `import {ActionList as List} from '@primer/react'; <List.Group><List.GroupHeading>Title</List.GroupHeading></List.Group>`,
|
|
200
|
+
errors: [{messageId: 'titlePropDeprecated'}],
|
|
112
201
|
},
|
|
113
202
|
{
|
|
114
|
-
code:
|
|
115
|
-
output:
|
|
116
|
-
errors: [
|
|
117
|
-
{
|
|
118
|
-
messageId: 'titlePropDeprecated',
|
|
119
|
-
},
|
|
120
|
-
],
|
|
203
|
+
code: `import * as Primer from '@primer/react'; <Primer.ActionList.Group title="A & B"></Primer.ActionList.Group>`,
|
|
204
|
+
output: `import * as Primer from '@primer/react'; <Primer.ActionList.Group><Primer.ActionList.GroupHeading>A & B</Primer.ActionList.GroupHeading></Primer.ActionList.Group>`,
|
|
205
|
+
errors: [{messageId: 'titlePropDeprecated'}],
|
|
121
206
|
},
|
|
207
|
+
...['title', 'title="Title"', 'title={title}'].map(attribute => ({
|
|
208
|
+
code: `import {ActionList} from '@primer/react'; <ActionList.Group ${attribute} />`,
|
|
209
|
+
output: null,
|
|
210
|
+
errors: [{messageId: 'titlePropDeprecated'}],
|
|
211
|
+
})),
|
|
212
|
+
...['title', 'title="{literal}"'].map(attribute => ({
|
|
213
|
+
code: `import {ActionList} from '@primer/react'; <ActionList.Group ${attribute}></ActionList.Group>`,
|
|
214
|
+
output: null,
|
|
215
|
+
errors: [{messageId: 'titlePropDeprecated'}],
|
|
216
|
+
})),
|
|
122
217
|
],
|
|
123
218
|
})
|
|
@@ -1,6 +1,128 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
-
const {
|
|
3
|
-
const
|
|
2
|
+
const {getVariableDeclaration} = require('../utils/get-variable-declaration')
|
|
3
|
+
const url = require('../url')
|
|
4
|
+
|
|
5
|
+
const textInputProps = {
|
|
6
|
+
icon: 'Use `leadingVisual` or `trailingVisual` instead.',
|
|
7
|
+
contrast: 'Omit this prop to use the default background.',
|
|
8
|
+
variant: 'Use `size` instead.',
|
|
9
|
+
width: 'Set `width` using CSS or `style` instead.',
|
|
10
|
+
minWidth: 'Set `min-width` using CSS or `style` instead.',
|
|
11
|
+
maxWidth: 'Set `max-width` using CSS or `style` instead.',
|
|
12
|
+
}
|
|
13
|
+
const buttonProps = {
|
|
14
|
+
leadingIcon: 'Use `leadingVisual` instead.',
|
|
15
|
+
trailingIcon: 'Use `trailingVisual` instead.',
|
|
16
|
+
}
|
|
17
|
+
const narrowDivider = 'Use `divider` with a responsive value instead.'
|
|
18
|
+
const deprecations = {
|
|
19
|
+
'ActionList.Group': {title: 'Use `ActionList.GroupHeading` instead.'},
|
|
20
|
+
'ActionList.Item': {as: 'Use `ActionList.LinkItem` for links; `as` has no effect on `ActionList.Item`.'},
|
|
21
|
+
'ActionMenu.Button': buttonProps,
|
|
22
|
+
'Autocomplete.Input': {
|
|
23
|
+
...textInputProps,
|
|
24
|
+
openOnFocus: 'Remove this prop. Review whether opening the menu on focus is still needed.',
|
|
25
|
+
},
|
|
26
|
+
'Autocomplete.Overlay': {overlayProps: 'Pass overlay props directly to `Autocomplete.Overlay` instead.'},
|
|
27
|
+
Banner: {icon: 'Use `leadingVisual` instead.'},
|
|
28
|
+
Button: buttonProps,
|
|
29
|
+
CounterLabel: {scheme: 'Use `variant` instead.'},
|
|
30
|
+
IconButton: {keyshortcuts: 'Use `keybindingHint` instead.'},
|
|
31
|
+
Link: {hoverColor: 'Style hover color using CSS instead.'},
|
|
32
|
+
LinkButton: buttonProps,
|
|
33
|
+
'PageLayout.Header': {dividerWhenNarrow: narrowDivider},
|
|
34
|
+
'PageLayout.Pane': {
|
|
35
|
+
dividerWhenNarrow: narrowDivider,
|
|
36
|
+
positionWhenNarrow: 'Use `position` with a responsive value instead.',
|
|
37
|
+
},
|
|
38
|
+
'PageLayout.Footer': {dividerWhenNarrow: narrowDivider},
|
|
39
|
+
'SegmentedControl.Button': {
|
|
40
|
+
icon: 'Use `leadingVisual` instead.',
|
|
41
|
+
leadingIcon: 'Use `leadingVisual` instead.',
|
|
42
|
+
},
|
|
43
|
+
SelectPanel: {footer: 'Use `secondaryAction` instead.'},
|
|
44
|
+
Spinner: {'aria-label': 'Use `srText` instead.'},
|
|
45
|
+
StateLabel: {variant: 'Use `size` with `small` or `medium` instead.'},
|
|
46
|
+
TextInput: textInputProps,
|
|
47
|
+
'TextInput.Action': {
|
|
48
|
+
children: 'Use an icon-only action with the `icon` prop instead.',
|
|
49
|
+
variant: 'Omit this prop to use the invisible button variant.',
|
|
50
|
+
},
|
|
51
|
+
TextInputWithTokens: textInputProps,
|
|
52
|
+
Textarea: {contrast: 'Omit this prop to use the default background.'},
|
|
53
|
+
ThemeProvider: {preventSSRMismatch: 'Remove this prop; it no longer has any effect.'},
|
|
54
|
+
'UnderlineNav.Item': {icon: 'Use `leadingVisual` instead.'},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entrypoints = new Map([
|
|
58
|
+
['@primer/react', deprecations],
|
|
59
|
+
[
|
|
60
|
+
'@primer/react/deprecated',
|
|
61
|
+
{
|
|
62
|
+
'ActionList.Item': {
|
|
63
|
+
trailingIcon: 'Use `trailingVisual` instead.',
|
|
64
|
+
trailingText: 'Use `trailingVisual` instead.',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
'@primer/react/experimental',
|
|
70
|
+
{
|
|
71
|
+
'FilteredActionList.Input': textInputProps,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
'@primer/react/next',
|
|
76
|
+
{
|
|
77
|
+
ThemeProvider: deprecations.ThemeProvider,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
function getPrimerComponent(node, scope) {
|
|
83
|
+
const members = []
|
|
84
|
+
let root = node.name
|
|
85
|
+
while (root.type === 'JSXMemberExpression') {
|
|
86
|
+
members.unshift(root.property.name)
|
|
87
|
+
root = root.object
|
|
88
|
+
}
|
|
89
|
+
if (root.type !== 'JSXIdentifier') return null
|
|
90
|
+
|
|
91
|
+
const definition = getVariableDeclaration(scope, {name: root.name})
|
|
92
|
+
if (definition?.type !== 'ImportBinding') return null
|
|
93
|
+
const entrypoint = definition.parent.source.value
|
|
94
|
+
const specifier = definition.node
|
|
95
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
96
|
+
members.unshift(specifier.imported.name ?? specifier.imported.value)
|
|
97
|
+
} else if (specifier.type !== 'ImportNamespaceSpecifier') {
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const component = members.join('.')
|
|
102
|
+
const catalogue = entrypoints.get(entrypoint)
|
|
103
|
+
if (!catalogue || !Object.hasOwn(catalogue, component)) return null
|
|
104
|
+
return {component, props: catalogue[component]}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fixGroupTitle(fixer, node, title, sourceCode) {
|
|
108
|
+
if (!title.value || node.selfClosing) return null
|
|
109
|
+
let groupTitle
|
|
110
|
+
if (title.value.type === 'Literal') {
|
|
111
|
+
groupTitle = sourceCode.getText(title.value).slice(1, -1)
|
|
112
|
+
if (/[{}<>]/.test(groupTitle)) return null
|
|
113
|
+
} else if (title.value.type === 'JSXExpressionContainer' && title.value.expression.type !== 'JSXEmptyExpression') {
|
|
114
|
+
groupTitle = sourceCode.getText(title.value)
|
|
115
|
+
} else {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
const heading = `${sourceCode.getText(node.name).slice(0, -'Group'.length)}GroupHeading`
|
|
119
|
+
const start = title.range[0]
|
|
120
|
+
const whitespace = sourceCode.text[start - 1]
|
|
121
|
+
return [
|
|
122
|
+
fixer.removeRange([whitespace === ' ' ? start - 1 : start, title.range[1]]),
|
|
123
|
+
fixer.insertTextAfter(node, `<${heading}>${groupTitle}</${heading}>`),
|
|
124
|
+
]
|
|
125
|
+
}
|
|
4
126
|
|
|
5
127
|
/**
|
|
6
128
|
* @type {import('eslint').Rule.RuleModule}
|
|
@@ -9,51 +131,56 @@ module.exports = {
|
|
|
9
131
|
meta: {
|
|
10
132
|
type: 'suggestion',
|
|
11
133
|
docs: {
|
|
12
|
-
description:
|
|
13
|
-
'Avoid using deprecated `title` prop on `ActionList.Group` component. Use `ActionList.GroupHeading` instead.',
|
|
134
|
+
description: 'Avoid deprecated Primer React component props.',
|
|
14
135
|
recommended: true,
|
|
15
|
-
url:
|
|
136
|
+
url: url(module),
|
|
16
137
|
},
|
|
17
138
|
fixable: 'code',
|
|
18
139
|
schema: [],
|
|
19
140
|
messages: {
|
|
20
141
|
titlePropDeprecated: 'The `title` prop is deprecated. Please use `ActionList.GroupHeading` instead.',
|
|
142
|
+
deprecatedProp: 'The `{{prop}}` prop on `{{component}}` is deprecated. {{hint}}',
|
|
21
143
|
},
|
|
22
144
|
},
|
|
23
145
|
create(context) {
|
|
24
146
|
return {
|
|
25
147
|
JSXOpeningElement(node) {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
148
|
+
const primer = getPrimerComponent(node, context.sourceCode.getScope(node))
|
|
149
|
+
if (!primer) return
|
|
150
|
+
for (const attribute of node.attributes) {
|
|
151
|
+
if (attribute.type !== 'JSXAttribute' || attribute.name.type !== 'JSXIdentifier') continue
|
|
152
|
+
const prop = attribute.name.name
|
|
153
|
+
if (!Object.hasOwn(primer.props, prop)) continue
|
|
154
|
+
if (primer.component === 'ActionList.Group' && prop === 'title') {
|
|
155
|
+
context.report({
|
|
156
|
+
node: attribute,
|
|
157
|
+
messageId: 'titlePropDeprecated',
|
|
158
|
+
fix: fixer => fixGroupTitle(fixer, node, attribute, context.sourceCode),
|
|
159
|
+
})
|
|
160
|
+
} else {
|
|
161
|
+
context.report({
|
|
162
|
+
node: attribute,
|
|
163
|
+
messageId: 'deprecatedProp',
|
|
164
|
+
data: {component: primer.component, prop, hint: primer.props[prop]},
|
|
165
|
+
})
|
|
166
|
+
}
|
|
29
167
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
168
|
+
if (
|
|
169
|
+
Object.hasOwn(primer.props, 'children') &&
|
|
170
|
+
!node.selfClosing &&
|
|
171
|
+
node.parent.children.some(
|
|
172
|
+
child =>
|
|
173
|
+
(child.type === 'JSXText' && child.value.trim() !== '') ||
|
|
174
|
+
(child.type === 'JSXExpressionContainer' && child.expression.type !== 'JSXEmptyExpression') ||
|
|
175
|
+
child.type === 'JSXElement' ||
|
|
176
|
+
child.type === 'JSXFragment' ||
|
|
177
|
+
child.type === 'JSXSpreadChild',
|
|
178
|
+
)
|
|
179
|
+
) {
|
|
33
180
|
context.report({
|
|
34
181
|
node,
|
|
35
|
-
messageId: '
|
|
36
|
-
|
|
37
|
-
// Group title is a string literal i.e. title="title"
|
|
38
|
-
if (title.value.type === 'Literal') {
|
|
39
|
-
groupTitle = title.value.value
|
|
40
|
-
// Group title is a JSX expression i.e. title={title}
|
|
41
|
-
} else if (title.value.type === 'JSXExpressionContainer') {
|
|
42
|
-
groupTitle = context.sourceCode.getText(title.value)
|
|
43
|
-
} else {
|
|
44
|
-
// we don't provide fix for cases where the title prop is not a string literal or JSX expression
|
|
45
|
-
return []
|
|
46
|
-
}
|
|
47
|
-
const start = title.range[0]
|
|
48
|
-
const end = title.range[1]
|
|
49
|
-
return [
|
|
50
|
-
fixer.removeRange([start - 1, end]), // remove the space before the title as well
|
|
51
|
-
fixer.insertTextAfterRange(
|
|
52
|
-
[node.range[1], node.range[1]],
|
|
53
|
-
`<ActionList.GroupHeading>${groupTitle}</ActionList.GroupHeading>`,
|
|
54
|
-
),
|
|
55
|
-
]
|
|
56
|
-
},
|
|
182
|
+
messageId: 'deprecatedProp',
|
|
183
|
+
data: {component: primer.component, prop: 'children', hint: primer.props.children},
|
|
57
184
|
})
|
|
58
185
|
}
|
|
59
186
|
},
|