From 995d2809b818d34ddea8e30e717cedf979e99053 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Thu, 21 Dec 2023 20:08:08 -0600 Subject: [PATCH 1/7] Added new @backstage/no-top-level-mui4-imports rule Signed-off-by: Andre Wanlin --- .changeset/warm-maps-scream.md | 7 + .changeset/young-rules-repeat.md | 5 + docs/tutorials/migrate-to-mui5.md | 2 +- packages/eslint-plugin/README.md | 1 + .../docs/rules/no-top-level-mui4-imports.md | 43 ++++++ packages/eslint-plugin/index.js | 1 + .../rules/no-top-level-mui4-imports.js | 129 ++++++++++++++++++ .../src/no-top-level-mui4-imports.test.ts | 100 ++++++++++++++ plugins/azure-devops/.eslintrc.js | 6 +- .../AzureGitTagsIcon/AzureGitTagsIcon.tsx | 2 +- .../AzurePipelinesIcon/AzurePipelinesIcon.tsx | 2 +- .../AzurePullRequestsIcon.tsx | 2 +- .../src/components/BuildTable/BuildTable.tsx | 3 +- .../components/GitTagTable/GitTagTable.tsx | 2 +- .../PullRequestStatusButtonGroup.tsx | 3 +- .../PullRequestTable/PullRequestTable.tsx | 3 +- .../lib/PullRequestCard/PullRequestCard.tsx | 4 +- .../lib/PullRequestGrid/PullRequestGrid.tsx | 2 +- .../PullRequestGridColumn.tsx | 4 +- .../src/components/ReadmeCard/ReadmeCard.tsx | 4 +- plugins/devtools/.eslintrc.js | 6 +- .../Content/ConfigContent/ConfigContent.tsx | 10 +- .../ExternalDependenciesContent.tsx | 16 +-- .../Content/InfoContent/BackstageLogoIcon.tsx | 2 +- .../Content/InfoContent/InfoContent.tsx | 24 ++-- .../DevToolsLayout/DevToolsLayout.tsx | 2 +- plugins/linguist/.eslintrc.js | 6 +- .../components/LinguistCard/LinguistCard.tsx | 15 +- 28 files changed, 351 insertions(+), 55 deletions(-) create mode 100644 .changeset/warm-maps-scream.md create mode 100644 .changeset/young-rules-repeat.md create mode 100644 packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md create mode 100644 packages/eslint-plugin/rules/no-top-level-mui4-imports.js create mode 100644 packages/eslint-plugin/src/no-top-level-mui4-imports.test.ts diff --git a/.changeset/warm-maps-scream.md b/.changeset/warm-maps-scream.md new file mode 100644 index 0000000000..3ea5d93b03 --- /dev/null +++ b/.changeset/warm-maps-scream.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-azure-devops': patch +'@backstage/plugin-devtools': patch +'@backstage/plugin-linguist': patch +--- + +Updated imports from named to default imports to help with the Material UI v4 to v5 migration diff --git a/.changeset/young-rules-repeat.md b/.changeset/young-rules-repeat.md new file mode 100644 index 0000000000..5c9d790214 --- /dev/null +++ b/.changeset/young-rules-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/eslint-plugin': patch +--- + +Added new `@backstage/no-top-level-mui4-imports` rule that forbids top level imports from Material UI v4 packages diff --git a/docs/tutorials/migrate-to-mui5.md b/docs/tutorials/migrate-to-mui5.md index 96129ceb8a..1b21c8b419 100644 --- a/docs/tutorials/migrate-to-mui5.md +++ b/docs/tutorials/migrate-to-mui5.md @@ -56,7 +56,7 @@ For current known issues with the Material UI v5 migration, follow our [Mileston To migrate your plugin to Material UI v5, you can build on the resources available. -1. Manually fix the imports from named to default imports to match the new [linting rules for minimizing bundle size](https://mui.com/material-ui/guides/minimizing-bundle-size). +1. Manually fix the imports from named to default imports to match the new [linting rules for minimizing bundle size](https://mui.com/material-ui/guides/minimizing-bundle-size). Note: you can use the [new `@backstage/no-top-level-mui4-imports` ESLint](https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md) rule to help with this. 2. Run the migration `codemod` for the path of the specific plugin: `npx @mui/codemod v5.0.0/preset-safe plugins/`. 3. Take a look at possible `TODO:` items the `codemod` could not fix. 4. Remove types & methods from `@backstage/theme` which are marked as `@deprecated`. diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index c311670603..1304239caf 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -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. | diff --git a/packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md b/packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md new file mode 100644 index 0000000000..85f5821b01 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md @@ -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'; +``` diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js index 20a0ff2d18..6fdc4f89fb 100644 --- a/packages/eslint-plugin/index.js +++ b/packages/eslint-plugin/index.js @@ -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'), }, }; diff --git a/packages/eslint-plugin/rules/no-top-level-mui4-imports.js b/packages/eslint-plugin/rules/no-top-level-mui4-imports.js new file mode 100644 index 0000000000..c3367a2346 --- /dev/null +++ b/packages/eslint-plugin/rules/no-top-level-mui4-imports.js @@ -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; + }, + }); + }, + }), +}; diff --git a/packages/eslint-plugin/src/no-top-level-mui4-imports.test.ts b/packages/eslint-plugin/src/no-top-level-mui4-imports.test.ts new file mode 100644 index 0000000000..16605aa46f --- /dev/null +++ b/packages/eslint-plugin/src/no-top-level-mui4-imports.test.ts @@ -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';`, + }, + ], +}); diff --git a/plugins/azure-devops/.eslintrc.js b/plugins/azure-devops/.eslintrc.js index e2a53a6ad2..9932358b45 100644 --- a/plugins/azure-devops/.eslintrc.js +++ b/plugins/azure-devops/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + '@backstage/no-top-level-mui4-imports': 'error', + }, +}); diff --git a/plugins/azure-devops/src/components/AzureGitTagsIcon/AzureGitTagsIcon.tsx b/plugins/azure-devops/src/components/AzureGitTagsIcon/AzureGitTagsIcon.tsx index c8b63e3259..0d317e3ce1 100644 --- a/plugins/azure-devops/src/components/AzureGitTagsIcon/AzureGitTagsIcon.tsx +++ b/plugins/azure-devops/src/components/AzureGitTagsIcon/AzureGitTagsIcon.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SvgIcon, SvgIconProps } from '@material-ui/core'; +import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; import React from 'react'; diff --git a/plugins/azure-devops/src/components/AzurePipelinesIcon/AzurePipelinesIcon.tsx b/plugins/azure-devops/src/components/AzurePipelinesIcon/AzurePipelinesIcon.tsx index bd19331f6e..10cdb3a905 100644 --- a/plugins/azure-devops/src/components/AzurePipelinesIcon/AzurePipelinesIcon.tsx +++ b/plugins/azure-devops/src/components/AzurePipelinesIcon/AzurePipelinesIcon.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SvgIcon, SvgIconProps } from '@material-ui/core'; +import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; import React from 'react'; diff --git a/plugins/azure-devops/src/components/AzurePullRequestsIcon/AzurePullRequestsIcon.tsx b/plugins/azure-devops/src/components/AzurePullRequestsIcon/AzurePullRequestsIcon.tsx index 99e3f74e90..e298f8bc25 100644 --- a/plugins/azure-devops/src/components/AzurePullRequestsIcon/AzurePullRequestsIcon.tsx +++ b/plugins/azure-devops/src/components/AzurePullRequestsIcon/AzurePullRequestsIcon.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { SvgIcon, SvgIconProps } from '@material-ui/core'; +import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; /** @public */ export const AzurePullRequestsIcon = (props: SvgIconProps) => ( diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx index 6707929a6c..09eba5f5a2 100644 --- a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Box, Typography } from '@material-ui/core'; +import Box from '@material-ui/core/Box'; +import Typography from '@material-ui/core/Typography'; import { BuildResult, BuildRun, diff --git a/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx b/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx index c6abba55e1..f34a1929fe 100644 --- a/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx +++ b/plugins/azure-devops/src/components/GitTagTable/GitTagTable.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Box } from '@material-ui/core'; +import Box from '@material-ui/core/Box'; import { Link, ResponseErrorPanel, diff --git a/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/PullRequestStatusButtonGroup.tsx b/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/PullRequestStatusButtonGroup.tsx index f516bfb0e5..1b0b9a8afe 100644 --- a/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/PullRequestStatusButtonGroup.tsx +++ b/plugins/azure-devops/src/components/PullRequestStatusButtonGroup/PullRequestStatusButtonGroup.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Button, ButtonGroup } from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import ButtonGroup from '@material-ui/core/ButtonGroup'; import { PullRequestStatus } from '@backstage/plugin-azure-devops-common'; import React from 'react'; diff --git a/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx b/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx index b37ba6e73c..d672102945 100644 --- a/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx +++ b/plugins/azure-devops/src/components/PullRequestTable/PullRequestTable.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Box, Chip } from '@material-ui/core'; +import Box from '@material-ui/core/Box'; +import Chip from '@material-ui/core/Chip'; import { Link, ResponseErrorPanel, diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.tsx index 98237cb2a3..dd9fff86b5 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.tsx +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestCard/PullRequestCard.tsx @@ -15,7 +15,9 @@ */ import { Avatar, Link } from '@backstage/core-components'; -import { Card, CardContent, CardHeader } from '@material-ui/core'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; import Typography from '@material-ui/core/Typography'; import { AutoCompleteIcon } from '../AutoCompleteIcon'; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/PullRequestGrid.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/PullRequestGrid.tsx index 8b9c49beb4..0244ea3bb1 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/PullRequestGrid.tsx +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGrid/PullRequestGrid.tsx @@ -17,7 +17,7 @@ import { PullRequestGridColumn } from '../PullRequestGridColumn'; import { PullRequestGroup } from '../types'; import React from 'react'; -import { styled } from '@material-ui/core'; +import { styled } from '@material-ui/core/styles'; const GridDiv = styled('div')(({ theme }) => ({ display: 'flex', diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/PullRequestGridColumn.tsx b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/PullRequestGridColumn.tsx index 94a648188d..f06216ac76 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/PullRequestGridColumn.tsx +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/PullRequestGridColumn/PullRequestGridColumn.tsx @@ -14,7 +14,9 @@ * limitations under the License. */ -import { Paper, Typography, styled, withStyles } from '@material-ui/core'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; +import { styled, withStyles } from '@material-ui/core/styles'; import { PullRequestCard } from '../PullRequestCard'; import { PullRequestGroup } from '../types'; diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx index 420fe6ecf4..300c9e0d7c 100644 --- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx +++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx @@ -14,7 +14,9 @@ * limitations under the License. */ -import { Box, Button, makeStyles } from '@material-ui/core'; +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import { makeStyles } from '@material-ui/core/styles'; import { InfoCard, Progress, diff --git a/plugins/devtools/.eslintrc.js b/plugins/devtools/.eslintrc.js index e2a53a6ad2..9932358b45 100644 --- a/plugins/devtools/.eslintrc.js +++ b/plugins/devtools/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + '@backstage/no-top-level-mui4-imports': 'error', + }, +}); diff --git a/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx b/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx index 66129b5dae..aba46f85b7 100644 --- a/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx +++ b/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx @@ -15,16 +15,16 @@ */ import { Progress, WarningPanel } from '@backstage/core-components'; +import Box from '@material-ui/core/Box'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; import { - Box, createStyles, makeStyles, - Paper, Theme, - Typography, useTheme, -} from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; +} from '@material-ui/core/styles'; +import Alert from '@material-ui/lab/Alert'; import React from 'react'; import ReactJson from 'react-json-view'; import { useConfig } from '../../../hooks'; diff --git a/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx b/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx index a5dd825fe5..c6a46ea8fb 100644 --- a/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx +++ b/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx @@ -23,16 +23,12 @@ import { TableColumn, } from '@backstage/core-components'; import { ExternalDependency } from '@backstage/plugin-devtools-common'; -import { - Box, - createStyles, - Grid, - makeStyles, - Paper, - Theme, - Typography, -} from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; +import Box from '@material-ui/core/Box'; +import Grid from '@material-ui/core/Grid'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import Alert from '@material-ui/lab/Alert'; import React from 'react'; import { useExternalDependencies } from '../../../hooks'; diff --git a/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx b/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx index 32f35600f2..8a9df77546 100644 --- a/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx +++ b/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SvgIcon, SvgIconProps } from '@material-ui/core'; +import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; import React from 'react'; diff --git a/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx b/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx index 0ed43d2fee..fcbea53603 100644 --- a/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx +++ b/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx @@ -15,20 +15,16 @@ */ import { Progress } from '@backstage/core-components'; -import { - Avatar, - Box, - createStyles, - Divider, - List, - ListItem, - ListItemAvatar, - ListItemText, - makeStyles, - Paper, - Theme, -} from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; +import Avatar from '@material-ui/core/Avatar'; +import Box from '@material-ui/core/Box'; +import Divider from '@material-ui/core/Divider'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; +import ListItemAvatar from '@material-ui/core/ListItemAvatar'; +import ListItemText from '@material-ui/core/ListItemText'; +import Paper from '@material-ui/core/Paper'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import Alert from '@material-ui/lab/Alert'; import React from 'react'; import { useInfo } from '../../../hooks'; import { InfoDependenciesTable } from './InfoDependenciesTable'; diff --git a/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx index ae9a337ced..bce0837ef1 100644 --- a/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx +++ b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx @@ -19,7 +19,7 @@ import { attachComponentData, useElementFilter, } from '@backstage/core-plugin-api'; -import { TabProps } from '@material-ui/core'; +import { TabProps } from '@material-ui/core/Tab'; import { default as React } from 'react'; /** @public */ diff --git a/plugins/linguist/.eslintrc.js b/plugins/linguist/.eslintrc.js index e2a53a6ad2..9932358b45 100644 --- a/plugins/linguist/.eslintrc.js +++ b/plugins/linguist/.eslintrc.js @@ -1 +1,5 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + '@backstage/no-top-level-mui4-imports': 'error', + }, +}); diff --git a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx index 6014487d30..6a07a958c2 100644 --- a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx +++ b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx @@ -14,15 +14,12 @@ * limitations under the License. */ -import { - Box, - Chip, - Tooltip, - Typography, - makeStyles, - Grid, - useTheme, -} from '@material-ui/core'; +import Box from '@material-ui/core/Box'; +import Chip from '@material-ui/core/Chip'; +import Tooltip from '@material-ui/core/Tooltip'; +import Typography from '@material-ui/core/Typography'; +import Grid from '@material-ui/core/Grid'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; import { InfoCard, Progress } from '@backstage/core-components'; import Alert from '@material-ui/lab/Alert'; import { DateTime } from 'luxon'; From 37867e0c2c17540ea97f158d89a03908d3d7a789 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 22 Dec 2023 06:56:15 -0600 Subject: [PATCH 2/7] Renamed based on feedback Signed-off-by: Andre Wanlin --- .changeset/young-rules-repeat.md | 2 +- docs/tutorials/migrate-to-mui5.md | 2 +- packages/eslint-plugin/README.md | 12 ++++++------ ...orts.md => no-top-level-material-ui-4-imports.md} | 4 ++-- packages/eslint-plugin/index.js | 2 +- ...orts.js => no-top-level-material-ui-4-imports.js} | 2 +- ...ts => no-top-level-material-ui-4-imports.test.ts} | 2 +- plugins/azure-devops/.eslintrc.js | 2 +- plugins/devtools/.eslintrc.js | 2 +- plugins/linguist/.eslintrc.js | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) rename packages/eslint-plugin/docs/rules/{no-top-level-mui4-imports.md => no-top-level-material-ui-4-imports.md} (83%) rename packages/eslint-plugin/rules/{no-top-level-mui4-imports.js => no-top-level-material-ui-4-imports.js} (98%) rename packages/eslint-plugin/src/{no-top-level-mui4-imports.test.ts => no-top-level-material-ui-4-imports.test.ts} (98%) diff --git a/.changeset/young-rules-repeat.md b/.changeset/young-rules-repeat.md index 5c9d790214..6fb17f0608 100644 --- a/.changeset/young-rules-repeat.md +++ b/.changeset/young-rules-repeat.md @@ -2,4 +2,4 @@ '@backstage/eslint-plugin': patch --- -Added new `@backstage/no-top-level-mui4-imports` rule that forbids top level imports from Material UI v4 packages +Added new `@backstage/no-top-level-material-ui-4-imports` rule that forbids top level imports from Material UI v4 packages diff --git a/docs/tutorials/migrate-to-mui5.md b/docs/tutorials/migrate-to-mui5.md index 1b21c8b419..aaf8cff7f5 100644 --- a/docs/tutorials/migrate-to-mui5.md +++ b/docs/tutorials/migrate-to-mui5.md @@ -56,7 +56,7 @@ For current known issues with the Material UI v5 migration, follow our [Mileston To migrate your plugin to Material UI v5, you can build on the resources available. -1. Manually fix the imports from named to default imports to match the new [linting rules for minimizing bundle size](https://mui.com/material-ui/guides/minimizing-bundle-size). Note: you can use the [new `@backstage/no-top-level-mui4-imports` ESLint](https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md) rule to help with this. +1. Manually fix the imports from named to default imports to match the new [linting rules for minimizing bundle size](https://mui.com/material-ui/guides/minimizing-bundle-size). Note: you can use the [new `@backstage/no-top-level-material-ui-4-imports` ESLint](https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md) rule to help with this. 2. Run the migration `codemod` for the path of the specific plugin: `npx @mui/codemod v5.0.0/preset-safe plugins/`. 3. Take a look at possible `TODO:` items the `codemod` could not fix. 4. Remove types & methods from `@backstage/theme` which are marked as `@deprecated`. diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 1304239caf..600b3268e1 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -35,9 +35,9 @@ rules: { The following rules are provided by this plugin: -| Rule | Description | -| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| [@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. | +| Rule | Description | +| --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| [@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-material-ui-4-imports](./docs/rules/no-top-level-material-ui-4-imports.md) | Forbid top level import from Material UI v4 packages. | diff --git a/packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md b/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md similarity index 83% rename from packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md rename to packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md index 85f5821b01..a00eb73ffd 100644 --- a/packages/eslint-plugin/docs/rules/no-top-level-mui4-imports.md +++ b/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md @@ -1,4 +1,4 @@ -# @backstage/no-top-level-mui4-imports +# @backstage/no-top-level-material-ui-4-imports Forbid top level import from Material UI v4 packages. @@ -7,7 +7,7 @@ Forbid top level import from Material UI v4 packages. Add the rules as follows, it has no options: ```js -"@backstage/no-top-level-mui4-imports": ["error"] +"@backstage/no-top-level-material-ui-4-imports": ["error"] ``` ## Rule Details diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js index 6fdc4f89fb..af1a8891c3 100644 --- a/packages/eslint-plugin/index.js +++ b/packages/eslint-plugin/index.js @@ -29,6 +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'), + 'no-top-level-material-ui-4-imports': require('./rules/no-top-level-material-ui-4-imports'), }, }; diff --git a/packages/eslint-plugin/rules/no-top-level-mui4-imports.js b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js similarity index 98% rename from packages/eslint-plugin/rules/no-top-level-mui4-imports.js rename to packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js index c3367a2346..dad034e6f2 100644 --- a/packages/eslint-plugin/rules/no-top-level-mui4-imports.js +++ b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js @@ -37,7 +37,7 @@ module.exports = { }, 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', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md', }, }, create: context => ({ diff --git a/packages/eslint-plugin/src/no-top-level-mui4-imports.test.ts b/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts similarity index 98% rename from packages/eslint-plugin/src/no-top-level-mui4-imports.test.ts rename to packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts index 16605aa46f..cb68a529e9 100644 --- a/packages/eslint-plugin/src/no-top-level-mui4-imports.test.ts +++ b/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts @@ -15,7 +15,7 @@ */ import { RuleTester } from 'eslint'; -import rule from '../rules/no-top-level-mui4-imports'; +import rule from '../rules/no-top-level-material-ui-4-imports'; const ruleTester = new RuleTester({ parserOptions: { diff --git a/plugins/azure-devops/.eslintrc.js b/plugins/azure-devops/.eslintrc.js index 9932358b45..e487f765b2 100644 --- a/plugins/azure-devops/.eslintrc.js +++ b/plugins/azure-devops/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - '@backstage/no-top-level-mui4-imports': 'error', + '@backstage/no-top-level-material-ui-4-imports': 'error', }, }); diff --git a/plugins/devtools/.eslintrc.js b/plugins/devtools/.eslintrc.js index 9932358b45..e487f765b2 100644 --- a/plugins/devtools/.eslintrc.js +++ b/plugins/devtools/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - '@backstage/no-top-level-mui4-imports': 'error', + '@backstage/no-top-level-material-ui-4-imports': 'error', }, }); diff --git a/plugins/linguist/.eslintrc.js b/plugins/linguist/.eslintrc.js index 9932358b45..e487f765b2 100644 --- a/plugins/linguist/.eslintrc.js +++ b/plugins/linguist/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { rules: { - '@backstage/no-top-level-mui4-imports': 'error', + '@backstage/no-top-level-material-ui-4-imports': 'error', }, }); From 74f57943876fc780d25c813e717e9167cc5f37a9 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 22 Dec 2023 07:24:54 -0600 Subject: [PATCH 3/7] Pair of adjustment based on feedback Signed-off-by: Andre Wanlin --- .../rules/no-top-level-material-ui-4-imports.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js index dad034e6f2..1cbc56cf9b 100644 --- a/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js +++ b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js @@ -42,6 +42,12 @@ module.exports = { }, create: context => ({ ImportDeclaration: node => { + // Anatomy of a Node + // Example: import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon'; + // Specifiers are the part between the `import` and `from`, in the example that would be `SvgIcon, { SvgIconProps }` + // Source is the part after the `from`, in the example that would be `'@material-ui/core/SvgIcon'` + // Source value gets you `@material-ui/core/SvgIcon` without the quotes, where as Source raw gets it as is + // Return if empty import if (node.specifiers.length === 0) return; // Return if empty source value @@ -52,11 +58,9 @@ module.exports = { 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.specifiers.length >= 1 && node.source.value?.split('/').length === 3 ) return; @@ -87,9 +91,10 @@ module.exports = { ); for (const specifier of specifiers) { - if (specifier.local.name === 'TabProps') { + const propsMatch = /^([A-Z]\w+)Props$/.exec(specifier.local.name); + if (propsMatch) { replacements.push( - `import { TabProps } from '@material-ui/core/Tab';`, + `import { ${specifier.local.name} } from '@material-ui/core/${propsMatch[1]}';`, ); } else if ( specifier.local.name === 'SvgIcon' || From 834d2f530eb818a8134569cff611ded32bbb1fec Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 22 Dec 2023 11:41:47 -0600 Subject: [PATCH 4/7] Applied a map to avoid hard coded cases Signed-off-by: Andre Wanlin --- .../no-top-level-material-ui-4-imports.js | 87 ++++++++++++++----- ...no-top-level-material-ui-4-imports.test.ts | 5 ++ 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js index 1cbc56cf9b..76a928a5e5 100644 --- a/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js +++ b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js @@ -84,37 +84,80 @@ module.exports = { fix: fixer => { const replacements = []; const styles = []; - const svgIcon = []; const specifiers = node.specifiers.filter( s => s.type === 'ImportSpecifier', ); - for (const specifier of specifiers) { - const propsMatch = /^([A-Z]\w+)Props$/.exec(specifier.local.name); + const specifiersMap = specifiers.map(s => { + const propsTest = /^([A-Z]\w+)Props$/.test(s.local.name); + const propsMatch = /^([A-Z]\w+)Props$/.exec(s.local.name); + let propValue; if (propsMatch) { - replacements.push( - `import { ${specifier.local.name} } from '@material-ui/core/${propsMatch[1]}';`, - ); - } 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); + propValue = propsMatch[1]; } - } + return { + emitComponent: !propsTest, + emitProp: propsTest, + value: s.local.name, + propValue, + }; + }); - if (svgIcon.length > 0) { - if (svgIcon.every(s => ['SvgIcon', 'SvgIconProps'].includes(s))) { - replacements.push( - `import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`, - ); + // We have 3 cases: + // 1 - Just Prop: import { TabProps } from '@material-ui/core'; + // 2 - Just Component: import { Box } from '@material-ui/core'; + // 3 - Component and Prop: import { SvgIcon, SvgIconProps } from '@material-ui/core'; + + const components = specifiersMap + .filter(f => { + return f.emitComponent; + }) + .map(m => m.value); + const props = specifiersMap + .filter(f => { + return f.emitProp; + }) + .map(m => m.value); + + if ( + specifiersMap.some(s => s.emitProp) && + !specifiersMap.some(s => s.emitComponent) + ) { + // 1 - Just Prop + const propValue = specifiersMap + .filter(f => { + return f.emitProp; + }) + .map(m => m.propValue); + replacements.push( + `import { ${props.join(', ')} } from '@material-ui/core/${ + propValue[0] + }';`, + ); + } else if ( + !specifiersMap.some(s => s.emitProp) && + specifiersMap.some(s => s.emitComponent) + ) { + // 2 - Just Component + for (const specifier of specifiers) { + 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); + } } + } else if ( + specifiersMap.some(s => s.emitProp) && + specifiersMap.some(s => s.emitComponent) + ) { + // 3 - Component and Prop + replacements.push( + `import ${components[0]}, { ${props.join( + ', ', + )} } from '@material-ui/core/${components[0]}';`, + ); } if (styles.length > 0) { diff --git a/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts b/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts index cb68a529e9..ac0b3770e0 100644 --- a/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts +++ b/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts @@ -91,6 +91,11 @@ import { styled, withStyles } from '@material-ui/core/styles';`, errors: [{ messageId: 'topLevelImport' }], output: `import { styled } from '@material-ui/core/styles';`, }, + { + code: `import { SvgIcon, SvgIconProps } from '@material-ui/core';`, + errors: [{ messageId: 'topLevelImport' }], + output: `import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`, + }, { code: `import { TabProps } from '@material-ui/core';`, errors: [{ messageId: 'topLevelImport' }], From 7965c57e86b1afeaffbcd33dfed3f0efa61d98c9 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 22 Dec 2023 12:56:47 -0600 Subject: [PATCH 5/7] Removed third level or deeper reporting Signed-off-by: Andre Wanlin --- .../rules/no-top-level-material-ui-4-imports.js | 10 ++-------- .../src/no-top-level-material-ui-4-imports.test.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js index 76a928a5e5..dc2a555e24 100644 --- a/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js +++ b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js @@ -32,8 +32,6 @@ module.exports = { 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.', @@ -65,15 +63,11 @@ module.exports = { ) return; - // Report third level or deeper imports + // Return if third level or deeper imports if ( - node.specifiers.length === 1 && + node.specifiers.length >= 1 && node.source.value.split('/').length > 3 ) { - context.report({ - node, - messageId: 'thirdLevelImport', - }); return; } diff --git a/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts b/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts index ac0b3770e0..568bdb8cc8 100644 --- a/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts +++ b/packages/eslint-plugin/src/no-top-level-material-ui-4-imports.test.ts @@ -38,6 +38,12 @@ ruleTester.run('path-imports-rule', rule, { { code: `import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';`, }, + { + code: `import { StyleRules } from '@material-ui/core/styles/withStyles';`, + }, + { + code: `import { CreateCSSProperties, StyledComponentProps } from '@material-ui/core/styles/withStyles';`, + }, ], invalid: [ { @@ -68,10 +74,6 @@ 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' }], From aeed5347e1da9813aa3149011a364a0d5ce5038e Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 22 Dec 2023 13:05:08 -0600 Subject: [PATCH 6/7] Updated API Reports Signed-off-by: Andre Wanlin --- plugins/azure-devops/api-report.md | 2 +- plugins/devtools/api-report.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md index 38c1eed279..cfff4348ee 100644 --- a/plugins/azure-devops/api-report.md +++ b/plugins/azure-devops/api-report.md @@ -22,7 +22,7 @@ import { Readme } from '@backstage/plugin-azure-devops-common'; import { ReadmeConfig } from '@backstage/plugin-azure-devops-common'; import { RepoBuild } from '@backstage/plugin-azure-devops-common'; import { RepoBuildOptions } from '@backstage/plugin-azure-devops-common'; -import { SvgIconProps } from '@material-ui/core'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; import { Team } from '@backstage/plugin-azure-devops-common'; // @public (undocumented) diff --git a/plugins/devtools/api-report.md b/plugins/devtools/api-report.md index 21754d7c1e..bba36a5a9a 100644 --- a/plugins/devtools/api-report.md +++ b/plugins/devtools/api-report.md @@ -10,7 +10,7 @@ import { default as default_2 } from 'react'; import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; -import { TabProps } from '@material-ui/core'; +import { TabProps } from '@material-ui/core/Tab'; // @public (undocumented) export const ConfigContent: () => React_2.JSX.Element; From b07ff647d6b2b972e4dbbb0f3cc3f103849faebd Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 13 Jan 2024 15:05:12 -0600 Subject: [PATCH 7/7] Clean-up based on feedback Signed-off-by: Andre Wanlin --- .../no-top-level-material-ui-4-imports.md | 4 +-- .../no-top-level-material-ui-4-imports.js | 27 +++++-------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md b/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md index a00eb73ffd..a105014abc 100644 --- a/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md +++ b/packages/eslint-plugin/docs/rules/no-top-level-material-ui-4-imports.md @@ -7,12 +7,12 @@ Forbid top level import from Material UI v4 packages. Add the rules as follows, it has no options: ```js -"@backstage/no-top-level-material-ui-4-imports": ["error"] +'@backstage/no-top-level-material-ui-4-imports': 'error' ``` ## Rule Details -TBD - Not sure what should go here +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. ### Fail diff --git a/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js index dc2a555e24..6a726545fc 100644 --- a/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js +++ b/packages/eslint-plugin/rules/no-top-level-material-ui-4-imports.js @@ -57,19 +57,8 @@ module.exports = { // 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 proper import eg. `import Box from '@material-ui/core/Box'` - if ( - node.specifiers.length >= 1 && - node.source.value?.split('/').length === 3 - ) - return; - - // Return if third level or deeper imports - if ( - node.specifiers.length >= 1 && - node.source.value.split('/').length > 3 - ) { - return; - } + // Or if third level or deeper imports + if (node.source.value?.split('/').length >= 3) return; // Report all other imports context.report({ @@ -84,17 +73,13 @@ module.exports = { ); const specifiersMap = specifiers.map(s => { - const propsTest = /^([A-Z]\w+)Props$/.test(s.local.name); const propsMatch = /^([A-Z]\w+)Props$/.exec(s.local.name); - let propValue; - if (propsMatch) { - propValue = propsMatch[1]; - } + return { - emitComponent: !propsTest, - emitProp: propsTest, + emitComponent: !(propsMatch !== null), + emitProp: propsMatch !== null, value: s.local.name, - propValue, + propValue: propsMatch ? propsMatch[1] : undefined, }; });