@backstage/eslint-plugin 0.1.6 → 0.1.7
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,5 +1,20 @@
|
|
|
1
1
|
# @backstage/eslint-plugin
|
|
2
2
|
|
|
3
|
+
## 0.1.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9ef572d: fix lint rule fixer for more than one `Component + Prop`
|
|
8
|
+
- 3a7eee7: eslint autofix for mui ThemeProvider
|
|
9
|
+
- d55828d: add fixer logic for import aliases
|
|
10
|
+
|
|
11
|
+
## 0.1.7-next.0
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 9ef572d: fix lint rule fixer for more than one `Component + Prop`
|
|
16
|
+
- 3a7eee7: eslint autofix for mui ThemeProvider
|
|
17
|
+
|
|
3
18
|
## 0.1.6
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/eslint-plugin",
|
|
3
|
+
"version": "0.1.7",
|
|
3
4
|
"description": "Backstage ESLint plugin",
|
|
4
|
-
"version": "0.1.6",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"minimatch": "^9.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@backstage/cli": "^0.26.
|
|
25
|
+
"@backstage/cli": "^0.26.3",
|
|
26
|
+
"@types/estree": "^1.0.5",
|
|
26
27
|
"eslint": "^8.33.0"
|
|
27
28
|
}
|
|
28
29
|
}
|
|
@@ -16,15 +16,75 @@
|
|
|
16
16
|
|
|
17
17
|
// @ts-check
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} FixerValues
|
|
21
|
+
* @property {string} value
|
|
22
|
+
* @property {string} [alias]
|
|
23
|
+
* @property {string} [componentValue]
|
|
24
|
+
* @property {string} [componentAlias]
|
|
25
|
+
* @property {boolean} emitComponent
|
|
26
|
+
* @property {boolean} emitProp
|
|
27
|
+
*/
|
|
28
|
+
|
|
19
29
|
const KNOWN_STYLES = [
|
|
30
|
+
// TODO: add exports from colorManipulator and transitions
|
|
31
|
+
'createTheme',
|
|
32
|
+
'unstable_createMuiStrictModeTheme',
|
|
33
|
+
'createMuiTheme',
|
|
34
|
+
'ThemeOptions',
|
|
35
|
+
'Theme',
|
|
36
|
+
'Direction',
|
|
37
|
+
'PaletteColorOptions',
|
|
38
|
+
'SimplePaletteColorOptions',
|
|
39
|
+
'createStyles',
|
|
40
|
+
'TypographyStyle',
|
|
41
|
+
'TypographyVariant',
|
|
20
42
|
'makeStyles',
|
|
43
|
+
'responsiveFontSizes',
|
|
44
|
+
'ComponentsPropsList',
|
|
45
|
+
'useTheme',
|
|
21
46
|
'withStyles',
|
|
22
|
-
'
|
|
47
|
+
'WithStyles',
|
|
48
|
+
'StyleRules',
|
|
49
|
+
'StyleRulesCallback',
|
|
50
|
+
'StyledComponentProps',
|
|
51
|
+
'withTheme',
|
|
52
|
+
'WithTheme',
|
|
23
53
|
'styled',
|
|
24
|
-
'
|
|
25
|
-
'
|
|
54
|
+
'ComponentCreator',
|
|
55
|
+
'StyledProps',
|
|
56
|
+
'createGenerateClassName',
|
|
57
|
+
'jssPreset',
|
|
58
|
+
'ServerStyleSheets',
|
|
59
|
+
'StylesProvider',
|
|
60
|
+
'MuiThemeProvider',
|
|
61
|
+
'ThemeProvider',
|
|
62
|
+
'ThemeProviderProps',
|
|
26
63
|
];
|
|
27
64
|
|
|
65
|
+
/**
|
|
66
|
+
* filter function to keep only ImportSpecifier nodes
|
|
67
|
+
* @param {import('estree').ImportSpecifier | import('estree').ImportDefaultSpecifier| import('estree').ImportNamespaceSpecifier} specifier
|
|
68
|
+
* @returns {specifier is import('estree').ImportSpecifier}
|
|
69
|
+
*/
|
|
70
|
+
function importSpecifiersFilter(specifier) {
|
|
71
|
+
return specifier.type === 'ImportSpecifier';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the value of the named import depending on if it has an alias or not
|
|
76
|
+
* @param {FixerValues} values
|
|
77
|
+
* @returns {string}
|
|
78
|
+
* @example
|
|
79
|
+
* `import { ${getNamedImportValue({ value: 'SvgIcon', alias: 'Icon' })} } from 'x'` // import { Icon as SvgIcon } from 'x'
|
|
80
|
+
* `import { ${getNamedImportValue({ value: 'SvgIcon' })} } from 'x'` // import { SvgIcon } from 'x'
|
|
81
|
+
*/
|
|
82
|
+
function getNamedImportValue(values) {
|
|
83
|
+
return values.alias
|
|
84
|
+
? `${values.value} as ${values.alias}`
|
|
85
|
+
: `${values.value}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
28
88
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
29
89
|
module.exports = {
|
|
30
90
|
meta: {
|
|
@@ -68,77 +128,83 @@ module.exports = {
|
|
|
68
128
|
const replacements = [];
|
|
69
129
|
const styles = [];
|
|
70
130
|
|
|
71
|
-
const specifiers = node.specifiers.filter(
|
|
72
|
-
|
|
73
|
-
|
|
131
|
+
const specifiers = node.specifiers.filter(importSpecifiersFilter);
|
|
132
|
+
|
|
133
|
+
const specifiersMap = specifiers.map(
|
|
134
|
+
/**
|
|
135
|
+
* transform ImportSpecifier to FixerValues to have a simpler object to work with
|
|
136
|
+
* @returns {FixerValues}
|
|
137
|
+
*/
|
|
138
|
+
s => {
|
|
139
|
+
const value = s.imported.name;
|
|
140
|
+
const alias = s.local.name === value ? undefined : s.local.name;
|
|
74
141
|
|
|
75
|
-
|
|
76
|
-
const propsMatch = /^([A-Z]\w+)Props$/.exec(s.local.name);
|
|
142
|
+
const propsMatch = /^([A-Z]\w+)Props$/.exec(value);
|
|
77
143
|
|
|
78
|
-
|
|
79
|
-
emitComponent
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
144
|
+
const emitProp = propsMatch !== null;
|
|
145
|
+
const emitComponent = !emitProp;
|
|
146
|
+
const emitComponentAndProp =
|
|
147
|
+
emitProp &&
|
|
148
|
+
specifiers.find(s => s.imported.name === propsMatch[1])?.local
|
|
149
|
+
.name;
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
emitComponent: emitComponent || Boolean(emitComponentAndProp),
|
|
153
|
+
emitProp,
|
|
154
|
+
value,
|
|
155
|
+
componentValue: propsMatch ? propsMatch[1] : undefined,
|
|
156
|
+
componentAlias: emitComponentAndProp
|
|
157
|
+
? emitComponentAndProp
|
|
158
|
+
: undefined,
|
|
159
|
+
alias,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Filter out duplicates where we have both component and component+prop
|
|
165
|
+
const filteredMap = specifiersMap.filter(
|
|
166
|
+
f => !specifiersMap.some(s => f.value === s.componentValue),
|
|
167
|
+
);
|
|
85
168
|
|
|
86
169
|
// We have 3 cases:
|
|
87
170
|
// 1 - Just Prop: import { TabProps } from '@material-ui/core';
|
|
88
171
|
// 2 - Just Component: import { Box } from '@material-ui/core';
|
|
89
172
|
// 3 - Component and Prop: import { SvgIcon, SvgIconProps } from '@material-ui/core';
|
|
90
173
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const props = specifiersMap
|
|
97
|
-
.filter(f => {
|
|
98
|
-
return f.emitProp;
|
|
99
|
-
})
|
|
100
|
-
.map(m => m.value);
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
specifiersMap.some(s => s.emitProp) &&
|
|
104
|
-
!specifiersMap.some(s => s.emitComponent)
|
|
105
|
-
) {
|
|
106
|
-
// 1 - Just Prop
|
|
107
|
-
const propValue = specifiersMap
|
|
108
|
-
.filter(f => {
|
|
109
|
-
return f.emitProp;
|
|
110
|
-
})
|
|
111
|
-
.map(m => m.propValue);
|
|
112
|
-
replacements.push(
|
|
113
|
-
`import { ${props.join(', ')} } from '@material-ui/core/${
|
|
114
|
-
propValue[0]
|
|
115
|
-
}';`,
|
|
116
|
-
);
|
|
117
|
-
} else if (
|
|
118
|
-
!specifiersMap.some(s => s.emitProp) &&
|
|
119
|
-
specifiersMap.some(s => s.emitComponent)
|
|
120
|
-
) {
|
|
121
|
-
// 2 - Just Component
|
|
122
|
-
for (const specifier of specifiers) {
|
|
123
|
-
if (KNOWN_STYLES.includes(specifier.local.name)) {
|
|
124
|
-
styles.push(specifier.local.name);
|
|
174
|
+
for (const specifier of filteredMap) {
|
|
175
|
+
// Just Component
|
|
176
|
+
if (specifier.emitComponent && !specifier.emitProp) {
|
|
177
|
+
if (KNOWN_STYLES.includes(specifier.value)) {
|
|
178
|
+
styles.push(getNamedImportValue(specifier));
|
|
125
179
|
} else {
|
|
126
|
-
const replacement = `import ${
|
|
180
|
+
const replacement = `import ${
|
|
181
|
+
specifier.alias ?? specifier.value
|
|
182
|
+
} from '${node.source.value}/${specifier.value}';`;
|
|
127
183
|
replacements.push(replacement);
|
|
128
184
|
}
|
|
129
185
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
186
|
+
|
|
187
|
+
// Just Prop
|
|
188
|
+
if (specifier.emitProp && !specifier.emitComponent) {
|
|
189
|
+
const replacement = `import { ${getNamedImportValue(
|
|
190
|
+
specifier,
|
|
191
|
+
)} } from '@material-ui/core/${specifier.componentValue}';`;
|
|
192
|
+
replacements.push(replacement);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Component and Prop
|
|
196
|
+
if (specifier.emitComponent && specifier.emitProp) {
|
|
197
|
+
replacements.push(
|
|
198
|
+
`import ${
|
|
199
|
+
specifier.componentAlias ?? specifier.componentValue
|
|
200
|
+
}, { ${getNamedImportValue(
|
|
201
|
+
specifier,
|
|
202
|
+
)} } from '@material-ui/core/${specifier.componentValue}';`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
140
205
|
}
|
|
141
206
|
|
|
207
|
+
// if we imports that should be moved to styles we added them here
|
|
142
208
|
if (styles.length > 0) {
|
|
143
209
|
const stylesReplacement = `import { ${styles.join(
|
|
144
210
|
', ',
|
|
@@ -35,6 +35,9 @@ ruleTester.run('path-imports-rule', rule, {
|
|
|
35
35
|
{
|
|
36
36
|
code: `import { styled, withStyles } from '@material-ui/core/styles';`,
|
|
37
37
|
},
|
|
38
|
+
{
|
|
39
|
+
code: `import { WithStyles } from '@material-ui/core/styles';`,
|
|
40
|
+
},
|
|
38
41
|
{
|
|
39
42
|
code: `import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`,
|
|
40
43
|
},
|
|
@@ -57,6 +60,28 @@ import Typography from '@material-ui/core/Typography';`,
|
|
|
57
60
|
errors: [{ messageId: 'topLevelImport' }],
|
|
58
61
|
output: `import Box from '@material-ui/core/Box';`,
|
|
59
62
|
},
|
|
63
|
+
{
|
|
64
|
+
code: `import { ThemeProvider } from '@material-ui/core';`,
|
|
65
|
+
errors: [{ messageId: 'topLevelImport' }],
|
|
66
|
+
output: `import { ThemeProvider } from '@material-ui/core/styles';`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
code: `import { WithStyles } from '@material-ui/core';`,
|
|
70
|
+
errors: [{ messageId: 'topLevelImport' }],
|
|
71
|
+
output: `import { WithStyles } from '@material-ui/core/styles';`,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
code: `import { Grid, GridProps, Theme, makeStyles } from '@material-ui/core';`,
|
|
75
|
+
errors: [{ messageId: 'topLevelImport' }],
|
|
76
|
+
output: `import Grid, { GridProps } from '@material-ui/core/Grid';
|
|
77
|
+
import { Theme, makeStyles } from '@material-ui/core/styles';`,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
code: `import { Grid, GridProps, SvgIcon, SvgIconProps } from '@material-ui/core';`,
|
|
81
|
+
errors: [{ messageId: 'topLevelImport' }],
|
|
82
|
+
output: `import Grid, { GridProps } from '@material-ui/core/Grid';
|
|
83
|
+
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`,
|
|
84
|
+
},
|
|
60
85
|
{
|
|
61
86
|
code: `import {
|
|
62
87
|
Box,
|
|
@@ -65,6 +90,9 @@ import Typography from '@material-ui/core/Typography';`,
|
|
|
65
90
|
DialogTitle,
|
|
66
91
|
Grid,
|
|
67
92
|
makeStyles,
|
|
93
|
+
ThemeProvider,
|
|
94
|
+
WithStyles,
|
|
95
|
+
Tooltip as MaterialTooltip,
|
|
68
96
|
} from '@material-ui/core';`,
|
|
69
97
|
errors: [{ messageId: 'topLevelImport' }],
|
|
70
98
|
output: `import Box from '@material-ui/core/Box';
|
|
@@ -72,7 +100,8 @@ import DialogActions from '@material-ui/core/DialogActions';
|
|
|
72
100
|
import DialogContent from '@material-ui/core/DialogContent';
|
|
73
101
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
|
74
102
|
import Grid from '@material-ui/core/Grid';
|
|
75
|
-
import
|
|
103
|
+
import MaterialTooltip from '@material-ui/core/Tooltip';
|
|
104
|
+
import { makeStyles, ThemeProvider, WithStyles } from '@material-ui/core/styles';`,
|
|
76
105
|
},
|
|
77
106
|
{
|
|
78
107
|
code: `import { Box, Button, makeStyles } from '@material-ui/core';`,
|
|
@@ -103,5 +132,25 @@ import { styled, withStyles } from '@material-ui/core/styles';`,
|
|
|
103
132
|
errors: [{ messageId: 'topLevelImport' }],
|
|
104
133
|
output: `import { TabProps } from '@material-ui/core/Tab';`,
|
|
105
134
|
},
|
|
135
|
+
{
|
|
136
|
+
code: `import { Tooltip as MaterialTooltip, } from '@material-ui/core';`,
|
|
137
|
+
errors: [{ messageId: 'topLevelImport' }],
|
|
138
|
+
output: `import MaterialTooltip from '@material-ui/core/Tooltip';`,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
code: `import { SvgIcon as Icon, SvgIconProps as IconProps } from '@material-ui/core';`,
|
|
142
|
+
errors: [{ messageId: 'topLevelImport' }],
|
|
143
|
+
output: `import Icon, { SvgIconProps as IconProps } from '@material-ui/core/SvgIcon';`,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
code: `import { SvgIconProps as IconProps } from '@material-ui/core';`,
|
|
147
|
+
errors: [{ messageId: 'topLevelImport' }],
|
|
148
|
+
output: `import { SvgIconProps as IconProps } from '@material-ui/core/SvgIcon';`,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
code: `import { styled as s } from '@material-ui/core';`,
|
|
152
|
+
errors: [{ messageId: 'topLevelImport' }],
|
|
153
|
+
output: `import { styled as s } from '@material-ui/core/styles';`,
|
|
154
|
+
},
|
|
106
155
|
],
|
|
107
156
|
});
|