@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.0",
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
- 'createStyles',
47
+ 'WithStyles',
48
+ 'StyleRules',
49
+ 'StyleRulesCallback',
50
+ 'StyledComponentProps',
51
+ 'withTheme',
52
+ 'WithTheme',
23
53
  'styled',
24
- 'useTheme',
25
- 'Theme',
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
- s => s.type === 'ImportSpecifier',
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
- const specifiersMap = specifiers.map(s => {
76
- const propsMatch = /^([A-Z]\w+)Props$/.exec(s.local.name);
142
+ const propsMatch = /^([A-Z]\w+)Props$/.exec(value);
77
143
 
78
- return {
79
- emitComponent: !(propsMatch !== null),
80
- emitProp: propsMatch !== null,
81
- value: s.local.name,
82
- propValue: propsMatch ? propsMatch[1] : undefined,
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 components = specifiersMap
92
- .filter(f => {
93
- return f.emitComponent;
94
- })
95
- .map(m => m.value);
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 ${specifier.local.name} from '${node.source.value}/${specifier.local.name}';`;
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
- } else if (
131
- specifiersMap.some(s => s.emitProp) &&
132
- specifiersMap.some(s => s.emitComponent)
133
- ) {
134
- // 3 - Component and Prop
135
- replacements.push(
136
- `import ${components[0]}, { ${props.join(
137
- ', ',
138
- )} } from '@material-ui/core/${components[0]}';`,
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 { makeStyles } from '@material-ui/core/styles';`,
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
  });