Added new @backstage/no-top-level-mui4-imports rule

Signed-off-by: Andre Wanlin <awanlin@spotify.com>
This commit is contained in:
Andre Wanlin
2023-12-21 20:08:08 -06:00
parent 56d3373c82
commit 995d2809b8
28 changed files with 351 additions and 55 deletions
+1
View File
@@ -40,3 +40,4 @@ The following rules are provided by this plugin:
| [@backstage/no-forbidden-package-imports](./docs/rules/no-forbidden-package-imports.md) | Disallow internal monorepo imports from package subpaths that are not exported. |
| [@backstage/no-relative-monorepo-imports](./docs/rules/no-relative-monorepo-imports.md) | Forbid relative imports that reach outside of the package in a monorepo. |
| [@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`. |
| [@backstage/no-top-level-mui4-imports](./docs/rules/no-top-level-mui4-imports.md) | Forbid top level import from Material UI v4 packages. |
@@ -0,0 +1,43 @@
# @backstage/no-top-level-mui4-imports
Forbid top level import from Material UI v4 packages.
## Usage
Add the rules as follows, it has no options:
```js
"@backstage/no-top-level-mui4-imports": ["error"]
```
## Rule Details
TBD - Not sure what should go here
### Fail
```tsx
import { Box, Typography } from '@material-ui/core';
```
```tsx
import Box from '@material-ui/core';
```
```tsx
import {
Box,
DialogActions,
DialogContent,
DialogTitle,
Grid,
makeStyles,
} from '@material-ui/core';
```
### Pass
```tsx
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';
```
+1
View File
@@ -29,5 +29,6 @@ module.exports = {
'no-forbidden-package-imports': require('./rules/no-forbidden-package-imports'),
'no-relative-monorepo-imports': require('./rules/no-relative-monorepo-imports'),
'no-undeclared-imports': require('./rules/no-undeclared-imports'),
'no-top-level-mui4-imports': require('./rules/no-top-level-mui4-imports'),
},
};
@@ -0,0 +1,129 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
const KNOWN_STYLES = [
'makeStyles',
'withStyles',
'createStyles',
'styled',
'useTheme',
'Theme',
];
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
fixable: 'code',
messages: {
topLevelImport: 'Top level imports for Material UI are not allowed',
thirdLevelImport:
'Third level or deeper imports for Material UI are not allowed',
},
docs: {
description: 'Forbid top level import from Material UI v4 packages.',
url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md',
},
},
create: context => ({
ImportDeclaration: node => {
// Return if empty import
if (node.specifiers.length === 0) return;
// Return if empty source value
if (!node.source.value) return;
// Return if source value not a string
if (typeof node.source.value !== 'string') return;
// Return if import does not start with '@material-ui/'
if (!node.source.value.startsWith('@material-ui/')) return;
// Return if import is from '@material-ui/core/styles', as it's valid already
if (node.source.value === '@material-ui/core/styles') return;
// Return if import is from '@material-ui/core/SvgIcon', as it's valid already
if (node.source.value === '@material-ui/core/SvgIcon') return;
// Return if proper import eg. `import Box from '@material-ui/core/Box'`
if (
node.specifiers.length === 1 &&
node.source.value?.split('/').length === 3
)
return;
// Report third level or deeper imports
if (
node.specifiers.length === 1 &&
node.source.value.split('/').length > 3
) {
context.report({
node,
messageId: 'thirdLevelImport',
});
return;
}
// Report all other imports
context.report({
node,
messageId: 'topLevelImport',
fix: fixer => {
const replacements = [];
const styles = [];
const svgIcon = [];
const specifiers = node.specifiers.filter(
s => s.type === 'ImportSpecifier',
);
for (const specifier of specifiers) {
if (specifier.local.name === 'TabProps') {
replacements.push(
`import { TabProps } from '@material-ui/core/Tab';`,
);
} else if (
specifier.local.name === 'SvgIcon' ||
specifier.local.name === 'SvgIconProps'
) {
svgIcon.push(specifier.local.name);
} else if (KNOWN_STYLES.includes(specifier.local.name)) {
styles.push(specifier.local.name);
} else {
const replacement = `import ${specifier.local.name} from '${node.source.value}/${specifier.local.name}';`;
replacements.push(replacement);
}
}
if (svgIcon.length > 0) {
if (svgIcon.every(s => ['SvgIcon', 'SvgIconProps'].includes(s))) {
replacements.push(
`import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`,
);
}
}
if (styles.length > 0) {
const stylesReplacement = `import { ${styles.join(
', ',
)} } from '@material-ui/core/styles';`;
replacements.push(stylesReplacement);
}
const result = fixer.replaceText(node, replacements.join('\n'));
return result;
},
});
},
}),
};
@@ -0,0 +1,100 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RuleTester } from 'eslint';
import rule from '../rules/no-top-level-mui4-imports';
const ruleTester = new RuleTester({
parserOptions: {
sourceType: 'module',
ecmaVersion: 2021,
},
});
ruleTester.run('path-imports-rule', rule, {
valid: [
{
code: `import Typography from '@material-ui/core/Typography';`,
},
{
code: `import Box from '@material-ui/core/Box'`,
},
{
code: `import { styled, withStyles } from '@material-ui/core/styles';`,
},
{
code: `import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`,
},
],
invalid: [
{
code: `import { Box, Typography } from '@material-ui/core';`,
errors: [{ messageId: 'topLevelImport' }],
output: `import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';`,
},
{
code: `import { Box } from '@material-ui/core';`,
errors: [{ messageId: 'topLevelImport' }],
output: `import Box from '@material-ui/core/Box';`,
},
{
code: `import {
Box,
DialogActions,
DialogContent,
DialogTitle,
Grid,
makeStyles,
} from '@material-ui/core';`,
errors: [{ messageId: 'topLevelImport' }],
output: `import Box from '@material-ui/core/Box';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Grid from '@material-ui/core/Grid';
import { makeStyles } from '@material-ui/core/styles';`,
},
{
code: `import { TabIndicator } from '@material-ui/core/Tabs/TabIndicator';`,
errors: [{ messageId: 'thirdLevelImport' }],
},
{
code: `import { Box, Button, makeStyles } from '@material-ui/core';`,
errors: [{ messageId: 'topLevelImport' }],
output: `import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';
import { makeStyles } from '@material-ui/core/styles';`,
},
{
code: `import { Paper, Typography, styled, withStyles } from '@material-ui/core';`,
errors: [{ messageId: 'topLevelImport' }],
output: `import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import { styled, withStyles } from '@material-ui/core/styles';`,
},
{
code: `import { styled } from '@material-ui/core';`,
errors: [{ messageId: 'topLevelImport' }],
output: `import { styled } from '@material-ui/core/styles';`,
},
{
code: `import { TabProps } from '@material-ui/core';`,
errors: [{ messageId: 'topLevelImport' }],
output: `import { TabProps } from '@material-ui/core/Tab';`,
},
],
});