@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 +12 -0
- package/README.md +6 -5
- package/docs/rules/no-top-level-material-ui-4-imports.md +43 -0
- package/index.js +1 -0
- package/knip-report.md +9 -0
- package/package.json +2 -2
- package/rules/no-top-level-material-ui-4-imports.js +156 -0
- package/src/no-forbidden-package-imports.test.ts +5 -0
- package/src/no-relative-monorepo-imports.test.ts +5 -0
- package/src/no-top-level-material-ui-4-imports.test.ts +107 -0
- package/src/no-undeclared-imports.test.ts +5 -0
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
|
|
39
|
-
|
|
|
40
|
-
| [@backstage/no-forbidden-package-imports](./docs/rules/no-forbidden-package-imports.md)
|
|
41
|
-
| [@backstage/no-relative-monorepo-imports](./docs/rules/no-relative-monorepo-imports.md)
|
|
42
|
-
| [@backstage/no-undeclared-imports](./docs/rules/no-undeclared-imports.md)
|
|
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
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
|
+
"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.
|
|
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({
|