@backstage/eslint-plugin 0.1.7-next.0 → 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,13 @@
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
+
3
11
  ## 0.1.7-next.0
4
12
 
5
13
  ### Patch Changes
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.7",
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.3",
26
+ "@types/estree": "^1.0.5",
26
27
  "eslint": "^8.33.0"
27
28
  }
28
29
  }
@@ -16,16 +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',
26
61
  'ThemeProvider',
62
+ 'ThemeProviderProps',
27
63
  ];
28
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
+
29
88
  /** @type {import('eslint').Rule.RuleModule} */
30
89
  module.exports = {
31
90
  meta: {
@@ -69,27 +128,39 @@ module.exports = {
69
128
  const replacements = [];
70
129
  const styles = [];
71
130
 
72
- const specifiers = node.specifiers.filter(
73
- s => s.type === 'ImportSpecifier',
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;
141
+
142
+ const propsMatch = /^([A-Z]\w+)Props$/.exec(value);
143
+
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
+ },
74
162
  );
75
163
 
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
164
  // Filter out duplicates where we have both component and component+prop
94
165
  const filteredMap = specifiersMap.filter(
95
166
  f => !specifiersMap.some(s => f.value === s.componentValue),
@@ -104,27 +175,36 @@ module.exports = {
104
175
  // Just Component
105
176
  if (specifier.emitComponent && !specifier.emitProp) {
106
177
  if (KNOWN_STYLES.includes(specifier.value)) {
107
- styles.push(specifier.value);
178
+ styles.push(getNamedImportValue(specifier));
108
179
  } else {
109
- const replacement = `import ${specifier.value} from '${node.source.value}/${specifier.value}';`;
180
+ const replacement = `import ${
181
+ specifier.alias ?? specifier.value
182
+ } from '${node.source.value}/${specifier.value}';`;
110
183
  replacements.push(replacement);
111
184
  }
112
185
  }
113
186
 
114
187
  // Just Prop
115
188
  if (specifier.emitProp && !specifier.emitComponent) {
116
- const replacement = `import { ${specifier.value} } from '@material-ui/core/${specifier.componentValue}';`;
189
+ const replacement = `import { ${getNamedImportValue(
190
+ specifier,
191
+ )} } from '@material-ui/core/${specifier.componentValue}';`;
117
192
  replacements.push(replacement);
118
193
  }
119
194
 
120
195
  // Component and Prop
121
196
  if (specifier.emitComponent && specifier.emitProp) {
122
197
  replacements.push(
123
- `import ${specifier.componentValue}, { ${specifier.value} } from '@material-ui/core/${specifier.componentValue}';`,
198
+ `import ${
199
+ specifier.componentAlias ?? specifier.componentValue
200
+ }, { ${getNamedImportValue(
201
+ specifier,
202
+ )} } from '@material-ui/core/${specifier.componentValue}';`,
124
203
  );
125
204
  }
126
205
  }
127
206
 
207
+ // if we imports that should be moved to styles we added them here
128
208
  if (styles.length > 0) {
129
209
  const stylesReplacement = `import { ${styles.join(
130
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
  },
@@ -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,8 @@ import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`,
83
91
  Grid,
84
92
  makeStyles,
85
93
  ThemeProvider,
94
+ WithStyles,
95
+ Tooltip as MaterialTooltip,
86
96
  } from '@material-ui/core';`,
87
97
  errors: [{ messageId: 'topLevelImport' }],
88
98
  output: `import Box from '@material-ui/core/Box';
@@ -90,7 +100,8 @@ import DialogActions from '@material-ui/core/DialogActions';
90
100
  import DialogContent from '@material-ui/core/DialogContent';
91
101
  import DialogTitle from '@material-ui/core/DialogTitle';
92
102
  import Grid from '@material-ui/core/Grid';
93
- import { makeStyles, ThemeProvider } from '@material-ui/core/styles';`,
103
+ import MaterialTooltip from '@material-ui/core/Tooltip';
104
+ import { makeStyles, ThemeProvider, WithStyles } from '@material-ui/core/styles';`,
94
105
  },
95
106
  {
96
107
  code: `import { Box, Button, makeStyles } from '@material-ui/core';`,
@@ -121,5 +132,25 @@ import { styled, withStyles } from '@material-ui/core/styles';`,
121
132
  errors: [{ messageId: 'topLevelImport' }],
122
133
  output: `import { TabProps } from '@material-ui/core/Tab';`,
123
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
+ },
124
155
  ],
125
156
  });