@backstage/eslint-plugin 0.1.4 → 0.1.5

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,17 @@
1
1
  # @backstage/eslint-plugin
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 995d280: Added new `@backstage/no-top-level-material-ui-4-imports` rule that forbids top level imports from Material UI v4 packages
8
+
9
+ ## 0.1.5-next.0
10
+
11
+ ### Patch Changes
12
+
13
+ - 995d280: Added new `@backstage/no-top-level-material-ui-4-imports` rule that forbids top level imports from Material UI v4 packages
14
+
3
15
  ## 0.1.4
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -35,8 +35,9 @@ rules: {
35
35
 
36
36
  The following rules are provided by this plugin:
37
37
 
38
- | Rule | Description |
39
- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
40
- | [@backstage/no-forbidden-package-imports](./docs/rules/no-forbidden-package-imports.md) | Disallow internal monorepo imports from package subpaths that are not exported. |
41
- | [@backstage/no-relative-monorepo-imports](./docs/rules/no-relative-monorepo-imports.md) | Forbid relative imports that reach outside of the package in a monorepo. |
42
- | [@backstage/no-undeclared-imports](./docs/rules/no-undeclared-imports.md) | Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`. |
38
+ | Rule | Description |
39
+ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
40
+ | [@backstage/no-forbidden-package-imports](./docs/rules/no-forbidden-package-imports.md) | Disallow internal monorepo imports from package subpaths that are not exported. |
41
+ | [@backstage/no-relative-monorepo-imports](./docs/rules/no-relative-monorepo-imports.md) | Forbid relative imports that reach outside of the package in a monorepo. |
42
+ | [@backstage/no-undeclared-imports](./docs/rules/no-undeclared-imports.md) | Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`. |
43
+ | [@backstage/no-top-level-material-ui-4-imports](./docs/rules/no-top-level-material-ui-4-imports.md) | Forbid top level import from Material UI v4 packages. |
@@ -0,0 +1,43 @@
1
+ # @backstage/no-top-level-material-ui-4-imports
2
+
3
+ Forbid top level import from Material UI v4 packages.
4
+
5
+ ## Usage
6
+
7
+ Add the rules as follows, it has no options:
8
+
9
+ ```js
10
+ '@backstage/no-top-level-material-ui-4-imports': 'error'
11
+ ```
12
+
13
+ ## Rule Details
14
+
15
+ Automatically fixes imports from named to default imports. This will help you comply with [Material UI recommendations](https://mui.com/material-ui/guides/minimizing-bundle-size/) and make migrating to Material UI v5 easier.
16
+
17
+ ### Fail
18
+
19
+ ```tsx
20
+ import { Box, Typography } from '@material-ui/core';
21
+ ```
22
+
23
+ ```tsx
24
+ import Box from '@material-ui/core';
25
+ ```
26
+
27
+ ```tsx
28
+ import {
29
+ Box,
30
+ DialogActions,
31
+ DialogContent,
32
+ DialogTitle,
33
+ Grid,
34
+ makeStyles,
35
+ } from '@material-ui/core';
36
+ ```
37
+
38
+ ### Pass
39
+
40
+ ```tsx
41
+ import Typography from '@material-ui/core/Typography';
42
+ import Box from '@material-ui/core/Box';
43
+ ```
package/index.js CHANGED
@@ -29,5 +29,6 @@ module.exports = {
29
29
  'no-forbidden-package-imports': require('./rules/no-forbidden-package-imports'),
30
30
  'no-relative-monorepo-imports': require('./rules/no-relative-monorepo-imports'),
31
31
  'no-undeclared-imports': require('./rules/no-undeclared-imports'),
32
+ 'no-top-level-material-ui-4-imports': require('./rules/no-top-level-material-ui-4-imports'),
32
33
  },
33
34
  };
package/knip-report.md ADDED
@@ -0,0 +1,9 @@
1
+ # Knip report
2
+
3
+ ## Unlisted dependencies (2)
4
+
5
+ | Name | Location | Severity |
6
+ | :----- | :----------------------------- | :------- |
7
+ | estree | rules/no-undeclared-imports.js | error |
8
+ | estree | lib/visitImports.js | error |
9
+
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@backstage/eslint-plugin",
3
3
  "description": "Backstage ESLint plugin",
4
- "version": "0.1.4",
4
+ "version": "0.1.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -22,7 +22,7 @@
22
22
  "minimatch": "^5.1.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@backstage/cli": "^0.25.0",
25
+ "@backstage/cli": "^0.25.2",
26
26
  "eslint": "^8.33.0"
27
27
  }
28
28
  }
@@ -0,0 +1,156 @@
1
+ /*
2
+ * Copyright 2023 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ // @ts-check
18
+
19
+ const KNOWN_STYLES = [
20
+ 'makeStyles',
21
+ 'withStyles',
22
+ 'createStyles',
23
+ 'styled',
24
+ 'useTheme',
25
+ 'Theme',
26
+ ];
27
+
28
+ /** @type {import('eslint').Rule.RuleModule} */
29
+ module.exports = {
30
+ meta: {
31
+ type: 'problem',
32
+ fixable: 'code',
33
+ messages: {
34
+ topLevelImport: 'Top level imports for Material UI are not allowed',
35
+ },
36
+ docs: {
37
+ description: 'Forbid top level import from Material UI v4 packages.',
38
+ url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md',
39
+ },
40
+ },
41
+ create: context => ({
42
+ ImportDeclaration: node => {
43
+ // Anatomy of a Node
44
+ // Example: import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
45
+ // Specifiers are the part between the `import` and `from`, in the example that would be `SvgIcon, { SvgIconProps }`
46
+ // Source is the part after the `from`, in the example that would be `'@material-ui/core/SvgIcon'`
47
+ // Source value gets you `@material-ui/core/SvgIcon` without the quotes, where as Source raw gets it as is
48
+
49
+ // Return if empty import
50
+ if (node.specifiers.length === 0) return;
51
+ // Return if empty source value
52
+ if (!node.source.value) return;
53
+ // Return if source value not a string
54
+ if (typeof node.source.value !== 'string') return;
55
+ // Return if import does not start with '@material-ui/'
56
+ if (!node.source.value.startsWith('@material-ui/')) return;
57
+ // Return if import is from '@material-ui/core/styles', as it's valid already
58
+ if (node.source.value === '@material-ui/core/styles') return;
59
+ // Return if proper import eg. `import Box from '@material-ui/core/Box'`
60
+ // Or if third level or deeper imports
61
+ if (node.source.value?.split('/').length >= 3) return;
62
+
63
+ // Report all other imports
64
+ context.report({
65
+ node,
66
+ messageId: 'topLevelImport',
67
+ fix: fixer => {
68
+ const replacements = [];
69
+ const styles = [];
70
+
71
+ const specifiers = node.specifiers.filter(
72
+ s => s.type === 'ImportSpecifier',
73
+ );
74
+
75
+ const specifiersMap = specifiers.map(s => {
76
+ const propsMatch = /^([A-Z]\w+)Props$/.exec(s.local.name);
77
+
78
+ return {
79
+ emitComponent: !(propsMatch !== null),
80
+ emitProp: propsMatch !== null,
81
+ value: s.local.name,
82
+ propValue: propsMatch ? propsMatch[1] : undefined,
83
+ };
84
+ });
85
+
86
+ // We have 3 cases:
87
+ // 1 - Just Prop: import { TabProps } from '@material-ui/core';
88
+ // 2 - Just Component: import { Box } from '@material-ui/core';
89
+ // 3 - Component and Prop: import { SvgIcon, SvgIconProps } from '@material-ui/core';
90
+
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);
125
+ } else {
126
+ const replacement = `import ${specifier.local.name} from '${node.source.value}/${specifier.local.name}';`;
127
+ replacements.push(replacement);
128
+ }
129
+ }
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
+ );
140
+ }
141
+
142
+ if (styles.length > 0) {
143
+ const stylesReplacement = `import { ${styles.join(
144
+ ', ',
145
+ )} } from '@material-ui/core/styles';`;
146
+ replacements.push(stylesReplacement);
147
+ }
148
+
149
+ const result = fixer.replaceText(node, replacements.join('\n'));
150
+
151
+ return result;
152
+ },
153
+ });
154
+ },
155
+ }),
156
+ };
@@ -25,6 +25,11 @@ const ERR = (name: string, path: string) => ({
25
25
  message: `${name} does not export ${path}`,
26
26
  });
27
27
 
28
+ // cwd must be restored
29
+ const origDir = process.cwd();
30
+ afterAll(() => {
31
+ process.chdir(origDir);
32
+ });
28
33
  process.chdir(FIXTURE);
29
34
 
30
35
  const ruleTester = new RuleTester({
@@ -28,6 +28,11 @@ const ERR_FORBIDDEN = (newImp: string) => ({
28
28
  message: `Relative imports of monorepo packages are forbidden, use '${newImp}' instead`,
29
29
  });
30
30
 
31
+ // cwd must be restored
32
+ const origDir = process.cwd();
33
+ afterAll(() => {
34
+ process.chdir(origDir);
35
+ });
31
36
  process.chdir(FIXTURE);
32
37
 
33
38
  const ruleTester = new RuleTester({
@@ -0,0 +1,107 @@
1
+ /*
2
+ * Copyright 2023 The Backstage Authors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { RuleTester } from 'eslint';
18
+ import rule from '../rules/no-top-level-material-ui-4-imports';
19
+
20
+ const ruleTester = new RuleTester({
21
+ parserOptions: {
22
+ sourceType: 'module',
23
+ ecmaVersion: 2021,
24
+ },
25
+ });
26
+
27
+ ruleTester.run('path-imports-rule', rule, {
28
+ valid: [
29
+ {
30
+ code: `import Typography from '@material-ui/core/Typography';`,
31
+ },
32
+ {
33
+ code: `import Box from '@material-ui/core/Box'`,
34
+ },
35
+ {
36
+ code: `import { styled, withStyles } from '@material-ui/core/styles';`,
37
+ },
38
+ {
39
+ code: `import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`,
40
+ },
41
+ {
42
+ code: `import { StyleRules } from '@material-ui/core/styles/withStyles';`,
43
+ },
44
+ {
45
+ code: `import { CreateCSSProperties, StyledComponentProps } from '@material-ui/core/styles/withStyles';`,
46
+ },
47
+ ],
48
+ invalid: [
49
+ {
50
+ code: `import { Box, Typography } from '@material-ui/core';`,
51
+ errors: [{ messageId: 'topLevelImport' }],
52
+ output: `import Box from '@material-ui/core/Box';
53
+ import Typography from '@material-ui/core/Typography';`,
54
+ },
55
+ {
56
+ code: `import { Box } from '@material-ui/core';`,
57
+ errors: [{ messageId: 'topLevelImport' }],
58
+ output: `import Box from '@material-ui/core/Box';`,
59
+ },
60
+ {
61
+ code: `import {
62
+ Box,
63
+ DialogActions,
64
+ DialogContent,
65
+ DialogTitle,
66
+ Grid,
67
+ makeStyles,
68
+ } from '@material-ui/core';`,
69
+ errors: [{ messageId: 'topLevelImport' }],
70
+ output: `import Box from '@material-ui/core/Box';
71
+ import DialogActions from '@material-ui/core/DialogActions';
72
+ import DialogContent from '@material-ui/core/DialogContent';
73
+ import DialogTitle from '@material-ui/core/DialogTitle';
74
+ import Grid from '@material-ui/core/Grid';
75
+ import { makeStyles } from '@material-ui/core/styles';`,
76
+ },
77
+ {
78
+ code: `import { Box, Button, makeStyles } from '@material-ui/core';`,
79
+ errors: [{ messageId: 'topLevelImport' }],
80
+ output: `import Box from '@material-ui/core/Box';
81
+ import Button from '@material-ui/core/Button';
82
+ import { makeStyles } from '@material-ui/core/styles';`,
83
+ },
84
+ {
85
+ code: `import { Paper, Typography, styled, withStyles } from '@material-ui/core';`,
86
+ errors: [{ messageId: 'topLevelImport' }],
87
+ output: `import Paper from '@material-ui/core/Paper';
88
+ import Typography from '@material-ui/core/Typography';
89
+ import { styled, withStyles } from '@material-ui/core/styles';`,
90
+ },
91
+ {
92
+ code: `import { styled } from '@material-ui/core';`,
93
+ errors: [{ messageId: 'topLevelImport' }],
94
+ output: `import { styled } from '@material-ui/core/styles';`,
95
+ },
96
+ {
97
+ code: `import { SvgIcon, SvgIconProps } from '@material-ui/core';`,
98
+ errors: [{ messageId: 'topLevelImport' }],
99
+ output: `import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`,
100
+ },
101
+ {
102
+ code: `import { TabProps } from '@material-ui/core';`,
103
+ errors: [{ messageId: 'topLevelImport' }],
104
+ output: `import { TabProps } from '@material-ui/core/Tab';`,
105
+ },
106
+ ],
107
+ });
@@ -53,6 +53,11 @@ const ERR_SWITCH_BACK = () => ({
53
53
  message: 'Switch back to import declaration',
54
54
  });
55
55
 
56
+ // cwd must be restored
57
+ const origDir = process.cwd();
58
+ afterAll(() => {
59
+ process.chdir(origDir);
60
+ });
56
61
  process.chdir(FIXTURE);
57
62
 
58
63
  const ruleTester = new RuleTester({