@backstage/eslint-plugin 0.1.7-next.0 → 0.1.8

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,19 @@
1
1
  # @backstage/eslint-plugin
2
2
 
3
+ ## 0.1.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 65ec043: add some `pickers` fixes
8
+
9
+ ## 0.1.7
10
+
11
+ ### Patch Changes
12
+
13
+ - 9ef572d: fix lint rule fixer for more than one `Component + Prop`
14
+ - 3a7eee7: eslint autofix for mui ThemeProvider
15
+ - d55828d: add fixer logic for import aliases
16
+
3
17
  ## 0.1.7-next.0
4
18
 
5
19
  ### Patch Changes
@@ -41,3 +41,50 @@ import {
41
41
  import Typography from '@material-ui/core/Typography';
42
42
  import Box from '@material-ui/core/Box';
43
43
  ```
44
+
45
+ ## --fix known issues
46
+
47
+ This rule provides automatic fixes for the imports, but it has some known issues:
48
+
49
+ ### Non Props types import
50
+
51
+ The fix will handle correctly 3 groups of imports:
52
+
53
+ - Any import from related to styles (i.e `makeStyles`, `styled`, `WithStyles`) will be auto fixed to the `@material-ui/core/styles` import.
54
+ - Any import with `Props` suffix will be auto fixed to actual component for example `DialogProps` will be imported from `@material-ui/core/Dialog`.
55
+ - Any other import will be considered as a component import and will be auto fixed to the actual component import.
56
+
57
+ This means that some types of imports without `Props` suffix will be wrongly auto fixed to the component import, for example this fix will be wrong:
58
+
59
+ ```diff
60
+ - import { Alert, Color } from '@material-ui/lab';
61
+ + import Alert from '@material-ui/lab/Alert';
62
+ + import Color from '@material-ui/lab/Color'; // this import is wrong
63
+ ```
64
+
65
+ The correct import should look like this:
66
+
67
+ ```diff
68
+ - import { Alert, Color } from '@material-ui/lab';
69
+ + import Alert, {Color} from '@material-ui/lab/Alert';
70
+ ```
71
+
72
+ Because `Color` is a type coming from the Alert component.
73
+
74
+ ### No default export available
75
+
76
+ Some components do not have a default export, for example `@material-ui/pickers/DateTimePicker` does not have a default export, so the fix will not work for these cases.
77
+
78
+ The fix will be wrong for this import:
79
+
80
+ ```diff
81
+ - import { DateTimePicker } from '@material-ui/pickers';
82
+ + import DateTimePicker from '@material-ui/pickers/DateTimePicker'; // this default import does not exist
83
+ ```
84
+
85
+ The correct import should look like this:
86
+
87
+ ```diff
88
+ - import { DateTimePicker } from '@material-ui/pickers';
89
+ + import { DateTimePicker } from '@material-ui/pickers/DateTimePicker'; // this is the correct import
90
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/eslint-plugin",
3
- "version": "0.1.7-next.0",
3
+ "version": "0.1.8",
4
4
  "description": "Backstage ESLint plugin",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -22,7 +22,8 @@
22
22
  "minimatch": "^9.0.0"
23
23
  },
24
24
  "devDependencies": {
25
- "@backstage/cli": "^0.26.3-next.1",
25
+ "@backstage/cli": "^0.26.5",
26
+ "@types/estree": "^1.0.5",
26
27
  "eslint": "^8.33.0"
27
28
  }
28
29
  }
@@ -16,16 +16,91 @@
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
+ // colorManipulator
31
+ 'hexToRgb',
32
+ 'rgbToHex',
33
+ 'hslToRgb',
34
+ 'decomposeColor',
35
+ 'recomposeColor',
36
+ 'getContrastRatio',
37
+ 'getLuminance',
38
+ 'emphasize',
39
+ 'fade',
40
+ 'alpha',
41
+ 'darken',
42
+ 'lighten',
43
+ // transitions
44
+ 'easing',
45
+ 'duration',
46
+ // styles
47
+ 'createTheme',
48
+ 'unstable_createMuiStrictModeTheme',
49
+ 'createMuiTheme',
50
+ 'ThemeOptions',
51
+ 'Theme',
52
+ 'Direction',
53
+ 'PaletteColorOptions',
54
+ 'SimplePaletteColorOptions',
55
+ 'createStyles',
56
+ 'TypographyStyle',
57
+ 'TypographyVariant',
20
58
  'makeStyles',
59
+ 'responsiveFontSizes',
60
+ 'ComponentsPropsList',
61
+ 'useTheme',
21
62
  'withStyles',
22
- 'createStyles',
63
+ 'WithStyles',
64
+ 'StyleRules',
65
+ 'StyleRulesCallback',
66
+ 'StyledComponentProps',
67
+ 'withTheme',
68
+ 'WithTheme',
23
69
  'styled',
24
- 'useTheme',
25
- 'Theme',
70
+ 'ComponentCreator',
71
+ 'StyledProps',
72
+ 'createGenerateClassName',
73
+ 'jssPreset',
74
+ 'ServerStyleSheets',
75
+ 'StylesProvider',
76
+ 'MuiThemeProvider',
26
77
  'ThemeProvider',
78
+ 'ThemeProviderProps',
27
79
  ];
28
80
 
81
+ /**
82
+ * filter function to keep only ImportSpecifier nodes
83
+ * @param {import('estree').ImportSpecifier | import('estree').ImportDefaultSpecifier| import('estree').ImportNamespaceSpecifier} specifier
84
+ * @returns {specifier is import('estree').ImportSpecifier}
85
+ */
86
+ function importSpecifiersFilter(specifier) {
87
+ return specifier.type === 'ImportSpecifier';
88
+ }
89
+
90
+ /**
91
+ * Gets the value of the named import depending on if it has an alias or not
92
+ * @param {FixerValues} values
93
+ * @returns {string}
94
+ * @example
95
+ * `import { ${getNamedImportValue({ value: 'SvgIcon', alias: 'Icon' })} } from 'x'` // import { Icon as SvgIcon } from 'x'
96
+ * `import { ${getNamedImportValue({ value: 'SvgIcon' })} } from 'x'` // import { SvgIcon } from 'x'
97
+ */
98
+ function getNamedImportValue(values) {
99
+ return values.alias
100
+ ? `${values.value} as ${values.alias}`
101
+ : `${values.value}`;
102
+ }
103
+
29
104
  /** @type {import('eslint').Rule.RuleModule} */
30
105
  module.exports = {
31
106
  meta: {
@@ -69,27 +144,43 @@ module.exports = {
69
144
  const replacements = [];
70
145
  const styles = [];
71
146
 
72
- const specifiers = node.specifiers.filter(
73
- s => s.type === 'ImportSpecifier',
147
+ const specifiers = node.specifiers.filter(importSpecifiersFilter);
148
+
149
+ const specifiersMap = specifiers.map(
150
+ /**
151
+ * transform ImportSpecifier to FixerValues to have a simpler object to work with
152
+ * @returns {FixerValues}
153
+ */
154
+ s => {
155
+ const value = s.imported.name;
156
+ const alias = s.local.name === value ? undefined : s.local.name;
157
+
158
+ const propsMatch =
159
+ /^([A-Z]\w+)Props$/.exec(value) ??
160
+ (node.source.value === '@material-ui/pickers'
161
+ ? /^Keyboard([A-Z]\w+Picker)$/.exec(value)
162
+ : null);
163
+
164
+ const emitProp = propsMatch !== null;
165
+ const emitComponent = !emitProp;
166
+ const emitComponentAndProp =
167
+ emitProp &&
168
+ specifiers.find(s => s.imported.name === propsMatch[1])?.local
169
+ .name;
170
+
171
+ return {
172
+ emitComponent: emitComponent || Boolean(emitComponentAndProp),
173
+ emitProp,
174
+ value,
175
+ componentValue: propsMatch ? propsMatch[1] : undefined,
176
+ componentAlias: emitComponentAndProp
177
+ ? emitComponentAndProp
178
+ : undefined,
179
+ alias,
180
+ };
181
+ },
74
182
  );
75
183
 
76
- const specifiersMap = specifiers.map(s => {
77
- const value = s.local.name;
78
- const propsMatch = /^([A-Z]\w+)Props$/.exec(value);
79
-
80
- const emitProp = propsMatch !== null;
81
- const emitComponent = !emitProp;
82
- const emitComponentAndProp =
83
- emitProp && specifiers.some(s => s.local.name === propsMatch[1]);
84
-
85
- return {
86
- emitComponent: emitComponent || emitComponentAndProp,
87
- emitProp,
88
- value,
89
- componentValue: propsMatch ? propsMatch[1] : undefined,
90
- };
91
- });
92
-
93
184
  // Filter out duplicates where we have both component and component+prop
94
185
  const filteredMap = specifiersMap.filter(
95
186
  f => !specifiersMap.some(s => f.value === s.componentValue),
@@ -104,27 +195,36 @@ module.exports = {
104
195
  // Just Component
105
196
  if (specifier.emitComponent && !specifier.emitProp) {
106
197
  if (KNOWN_STYLES.includes(specifier.value)) {
107
- styles.push(specifier.value);
198
+ styles.push(getNamedImportValue(specifier));
108
199
  } else {
109
- const replacement = `import ${specifier.value} from '${node.source.value}/${specifier.value}';`;
200
+ const replacement = `import ${
201
+ specifier.alias ?? specifier.value
202
+ } from '${node.source.value}/${specifier.value}';`;
110
203
  replacements.push(replacement);
111
204
  }
112
205
  }
113
206
 
114
207
  // Just Prop
115
208
  if (specifier.emitProp && !specifier.emitComponent) {
116
- const replacement = `import { ${specifier.value} } from '@material-ui/core/${specifier.componentValue}';`;
209
+ const replacement = `import { ${getNamedImportValue(
210
+ specifier,
211
+ )} } from '${node.source.value}/${specifier.componentValue}';`;
117
212
  replacements.push(replacement);
118
213
  }
119
214
 
120
215
  // Component and Prop
121
216
  if (specifier.emitComponent && specifier.emitProp) {
122
217
  replacements.push(
123
- `import ${specifier.componentValue}, { ${specifier.value} } from '@material-ui/core/${specifier.componentValue}';`,
218
+ `import ${
219
+ specifier.componentAlias ?? specifier.componentValue
220
+ }, { ${getNamedImportValue(specifier)} } from '${
221
+ node.source.value
222
+ }/${specifier.componentValue}';`,
124
223
  );
125
224
  }
126
225
  }
127
226
 
227
+ // if we imports that should be moved to styles we added them here
128
228
  if (styles.length > 0) {
129
229
  const stylesReplacement = `import { ${styles.join(
130
230
  ', ',
@@ -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
  },
@@ -62,6 +65,11 @@ import Typography from '@material-ui/core/Typography';`,
62
65
  errors: [{ messageId: 'topLevelImport' }],
63
66
  output: `import { ThemeProvider } from '@material-ui/core/styles';`,
64
67
  },
68
+ {
69
+ code: `import { WithStyles } from '@material-ui/core';`,
70
+ errors: [{ messageId: 'topLevelImport' }],
71
+ output: `import { WithStyles } from '@material-ui/core/styles';`,
72
+ },
65
73
  {
66
74
  code: `import { Grid, GridProps, Theme, makeStyles } from '@material-ui/core';`,
67
75
  errors: [{ messageId: 'topLevelImport' }],
@@ -83,6 +91,10 @@ import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`,
83
91
  Grid,
84
92
  makeStyles,
85
93
  ThemeProvider,
94
+ WithStyles,
95
+ Tooltip as MaterialTooltip,
96
+ alpha,
97
+ easing
86
98
  } from '@material-ui/core';`,
87
99
  errors: [{ messageId: 'topLevelImport' }],
88
100
  output: `import Box from '@material-ui/core/Box';
@@ -90,7 +102,8 @@ import DialogActions from '@material-ui/core/DialogActions';
90
102
  import DialogContent from '@material-ui/core/DialogContent';
91
103
  import DialogTitle from '@material-ui/core/DialogTitle';
92
104
  import Grid from '@material-ui/core/Grid';
93
- import { makeStyles, ThemeProvider } from '@material-ui/core/styles';`,
105
+ import MaterialTooltip from '@material-ui/core/Tooltip';
106
+ import { makeStyles, ThemeProvider, WithStyles, alpha, easing } from '@material-ui/core/styles';`,
94
107
  },
95
108
  {
96
109
  code: `import { Box, Button, makeStyles } from '@material-ui/core';`,
@@ -100,11 +113,11 @@ import Button from '@material-ui/core/Button';
100
113
  import { makeStyles } from '@material-ui/core/styles';`,
101
114
  },
102
115
  {
103
- code: `import { Paper, Typography, styled, withStyles } from '@material-ui/core';`,
116
+ code: `import { Paper, Typography, styled, withStyles, alpha, duration} from '@material-ui/core';`,
104
117
  errors: [{ messageId: 'topLevelImport' }],
105
118
  output: `import Paper from '@material-ui/core/Paper';
106
119
  import Typography from '@material-ui/core/Typography';
107
- import { styled, withStyles } from '@material-ui/core/styles';`,
120
+ import { styled, withStyles, alpha, duration } from '@material-ui/core/styles';`,
108
121
  },
109
122
  {
110
123
  code: `import { styled } from '@material-ui/core';`,
@@ -121,5 +134,37 @@ import { styled, withStyles } from '@material-ui/core/styles';`,
121
134
  errors: [{ messageId: 'topLevelImport' }],
122
135
  output: `import { TabProps } from '@material-ui/core/Tab';`,
123
136
  },
137
+ {
138
+ code: `import { Tooltip as MaterialTooltip, } from '@material-ui/core';`,
139
+ errors: [{ messageId: 'topLevelImport' }],
140
+ output: `import MaterialTooltip from '@material-ui/core/Tooltip';`,
141
+ },
142
+ {
143
+ code: `import { SvgIcon as Icon, SvgIconProps as IconProps } from '@material-ui/core';`,
144
+ errors: [{ messageId: 'topLevelImport' }],
145
+ output: `import Icon, { SvgIconProps as IconProps } from '@material-ui/core/SvgIcon';`,
146
+ },
147
+ {
148
+ code: `import { SvgIconProps as IconProps } from '@material-ui/core';`,
149
+ errors: [{ messageId: 'topLevelImport' }],
150
+ output: `import { SvgIconProps as IconProps } from '@material-ui/core/SvgIcon';`,
151
+ },
152
+ {
153
+ code: `import { styled as s } from '@material-ui/core';`,
154
+ errors: [{ messageId: 'topLevelImport' }],
155
+ output: `import { styled as s } from '@material-ui/core/styles';`,
156
+ },
157
+ {
158
+ code: `import { TreeItem, TreeItemProps, TreeView, AlertProps } from '@material-ui/lab';`,
159
+ errors: [{ messageId: 'topLevelImport' }],
160
+ output: `import TreeItem, { TreeItemProps } from '@material-ui/lab/TreeItem';
161
+ import TreeView from '@material-ui/lab/TreeView';
162
+ import { AlertProps } from '@material-ui/lab/Alert';`,
163
+ },
164
+ {
165
+ code: `import { KeyboardDatePicker } from '@material-ui/pickers';`,
166
+ errors: [{ messageId: 'topLevelImport' }],
167
+ output: `import { KeyboardDatePicker } from '@material-ui/pickers/DatePicker';`,
168
+ },
124
169
  ],
125
170
  });