Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[codemod] Add optimal-imports for v5 #27404

Merged
merged 14 commits into from
Jul 26, 2021
28 changes: 28 additions & 0 deletions docs/src/pages/guides/migration-v4/migration-v4.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,17 @@ declare module '@material-ui/styles' {
}
```

### @material-ui/core/colors

- Nested imports of more than 1 level are private. You can't import color from `@material-ui/core/colors/red`.

> ✅ This is handled in [🪄preset-safe codemod](#preset-safe).

```diff
-import red from '@material-ui/core/colors/red';
+import { red } from '@material-ui/core/colors';
```

### @material-ui/core/styles

#### createGenerateClassName
Expand Down Expand Up @@ -2644,6 +2655,23 @@ declare module "@material-ui/private-theming" {
}
```

### [Jest] SyntaxError: Unexpected token 'export'

In v5, `@material-ui/core/colors/red` is considered private and should not be used in your project. [More details about this error](https://github.com/mui-org/material-ui/issues/27296).

You can use this codemod (**recommended**) to fix all the import in your project:

```sh
npx @material-ui/codemod v5.0.0/optimal-imports <folder>
```

or fix it manually like this:

```diff
-import red from '@material-ui/core/colors/red';
+import { red } from '@material-ui/core/colors';
```

### makeStyles - TypeError: Cannot read property 'drawer' of undefined

This error occurs when calling `useStyles` (result of `makeStyles`) or `withStyles` outside of `<ThemeProvider>` scope like this:
Expand Down
133 changes: 65 additions & 68 deletions packages/material-ui-codemod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,74 +69,54 @@ npx @material-ui/codemod@next v5.0.0/preset-safe <path|folder>

The list includes these transformers

- [@material-ui/codemod](#material-uicodemod)
- [Setup & run](#setup--run)
- [jscodeshift options](#jscodeshift-options)
- [Recast Options](#recast-options)
- [Included scripts](#included-scripts)
- [v5.0.0](#v500)
- [🚀 `preset-safe`](#-preset-safe)
- [`adapter-v4`](#adapter-v4)
- [`autocomplete-rename-closeicon`](#autocomplete-rename-closeicon)
- [`autocomplete-rename-option`](#autocomplete-rename-option)
- [`avatar-circle-circular`](#avatar-circle-circular)
- [`badge-overlap-value`](#badge-overlap-value)
- [`box-borderradius-values`](#box-borderradius-values)
- [`box-rename-css`](#box-rename-css)
- [`box-rename-gap`](#box-rename-gap)
- [`button-color-prop`](#button-color-prop)
- [`chip-variant-prop`](#chip-variant-prop)
- [`circularprogress-variant`](#circularprogress-variant)
- [`collapse-rename-collapsedheight`](#collapse-rename-collapsedheight)
- [`component-rename-prop`](#component-rename-prop)
- [`core-styles-import`](#core-styles-import)
- [`create-theme`](#create-theme)
- [`dialog-props`](#dialog-props)
- [`dialog-title-props`](#dialog-title-props)
- [`emotion-prepend-cache`](#emotion-prepend-cache)
- [`expansion-panel-component`](#expansion-panel-component)
- [`fab-variant`](#fab-variant)
- [`fade-rename-alpha`](#fade-rename-alpha)
- [`grid-justify-justifycontent`](#grid-justify-justifycontent)
- [`grid-list-component`](#grid-list-component)
- [`icon-button-size`](#icon-button-size)
- [`jss-to-styled`](#jss-to-styled)
- [`link-underline-hover`](#link-underline-hover)
- [`material-ui-styles`](#material-ui-styles)
- [`material-ui-types`](#material-ui-types)
- [`modal-props`](#modal-props)
- [`moved-lab-modules`](#moved-lab-modules)
- [`pagination-round-circular`](#pagination-round-circular)
- [`root-ref`](#root-ref)
- [`skeleton-variant`](#skeleton-variant)
- [`styled-engine-provider`](#styled-engine-provider)
- [`table-props`](#table-props)
- [`tabs-scroll-buttons`](#tabs-scroll-buttons)
- [`textarea-minmax-rows`](#textarea-minmax-rows)
- [`theme-augment`](#theme-augment)
- [`theme-breakpoints`](#theme-breakpoints)
- [`theme-breakpoints-width`](#theme-breakpoints-width)
- [`theme-options`](#theme-options)
- [`theme-palette-mode`](#theme-palette-mode)
- [`theme-provider`](#theme-provider)
- [`theme-spacing`](#theme-spacing)
- [`theme-typography-round`](#theme-typography-round)
- [`transitions`](#transitions)
- [`use-autocomplete`](#use-autocomplete)
- [`use-transitionprops`](#use-transitionprops)
- [`variant-prop`](#variant-prop)
- [`with-mobile-dialog`](#with-mobile-dialog)
- [`with-width`](#with-width)
- [v4.0.0](#v400)
- [`theme-spacing-api`](#theme-spacing-api)
- [`optimal-imports`](#optimal-imports)
- [`top-level-imports`](#top-level-imports)
- [v1.0.0](#v100)
- [`import-path`](#import-path)
- [`color-imports`](#color-imports)
- [`svg-icon-imports`](#svg-icon-imports)
- [v0.15.0](#v0150)
- [`import-path`](#import-path-1)
- [`adapter-v4`](#adapter-v4)
- [`autocomplete-rename-closeicon`](#autocomplete-rename-closeicon)
- [`autocomplete-rename-option`](#autocomplete-rename-option)
- [`avatar-circle-circular`](#avatar-circle-circular)
- [`badge-overlap-value`](#badge-overlap-value)
- [`box-borderradius-values`](#box-borderradius-values)
- [`box-rename-css`](#box-rename-css)
- [`box-rename-gap`](#box-rename-gap)
- [`button-color-prop`](#button-color-prop)
- [`chip-variant-prop`](#chip-variant-prop)
- [`circularprogress-variant`](#circularprogress-variant)
- [`collapse-rename-collapsedheight`](#collapse-rename-collapsedheight)
- [`core-styles-import`](#core-styles-import)
- [`create-theme`](#create-theme)
- [`dialog-props`](#dialog-props)
- [`dialog-title-props`](#dialog-title-props)
- [`emotion-prepend-cache`](#emotion-prepend-cache)
- [`expansion-panel-component`](#expansion-panel-component)
- [`fab-variant`](#fab-variant)
- [`fade-rename-alpha`](#fade-rename-alpha)
- [`grid-justify-justifycontent`](#grid-justify-justifycontent)
- [`grid-list-component`](#grid-list-component)
- [`icon-button-size`](#icon-button-size)
- [`material-ui-styles`](#material-ui-styles)
- [`material-ui-types`](#material-ui-types)
- [`modal-props`](#modal-props)
- [`moved-lab-modules`](#moved-lab-modules)
- [`pagination-round-circular`](#pagination-round-circular)
- [`optimal-imports`](#optimal-imports)
- [`root-ref`](#root-ref)
- [`skeleton-variant`](#skeleton-variant)
- [`styled-engine-provider`](#styled-engine-provider)
- [`table-props`](#table-props)
- [`tabs-scroll-buttons`](#tabs-scroll-buttons)
- [`textarea-minmax-rows`](#textarea-minmax-rows)
- [`theme-augment`](#theme-augment)
- [`theme-breakpoints`](#theme-breakpoints)
- [`theme-breakpoints-width`](#theme-breakpoints-width)
- [`theme-options`](#theme-options)
- [`theme-palette-mode`](#theme-palette-mode)
- [`theme-provider`](#theme-provider)
- [`theme-spacing`](#theme-spacing)
- [`theme-typography-round`](#theme-typography-round)
- [`transitions`](#transitions)
- [`use-autocomplete`](#use-autocomplete)
- [`use-transitionprops`](#use-transitionprops)
- [`with-mobile-dialog`](#with-mobile-dialog)
- [`with-width`](#with-width)

#### `adapter-v4`

Expand Down Expand Up @@ -739,6 +719,23 @@ npx @material-ui/codemod@next v5.0.0/pagination-round-circular <path>

You can find more details about this breaking change in [the migration guide](https://next.material-ui.com/guides/migration-v4/#pagination).

#### `optimal-imports`

Fix private import paths.

```diff
-import red from '@material-ui/core/colors/red';
-import createTheme from '@material-ui/core/styles/createTheme';
+import { red } from '@material-ui/core/colors';
+import { createTheme } from '@material-ui/core/styles';
```

<!-- #default-branch-switch -->

```sh
npx @material-ui/codemod@next v5.0.0/optimal-imports <path>
```

#### `root-ref`

Removes `RootRef` from the codebase.
Expand Down
92 changes: 92 additions & 0 deletions packages/material-ui-codemod/src/v5.0.0/optimal-imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { dirname } from 'path';
import addImports from 'jscodeshift-add-imports';
import getJSExports from '../util/getJSExports';

// istanbul ignore next
if (process.env.NODE_ENV === 'test') {
const resolve = require.resolve;
require.resolve = (source) =>
resolve(source.replace(/^@material-ui\/core\/modern/, '../../../material-ui/src'));
}

export default function transformer(fileInfo, api, options) {
const j = api.jscodeshift;
const importModule = options.importModule || '@material-ui/core';
const targetModule = options.targetModule || '@material-ui/core';
const printOptions = options.printOptions || {
quote: 'single',
trailingComma: true,
};

const root = j(fileInfo.source);
const importRegExp = new RegExp(`^${importModule}/([^/]+/)+([^/]+)$`);

const resultSpecifiers = new Map();

const addSpecifier = (source, specifier) => {
if (!resultSpecifiers.has(source)) {
resultSpecifiers.set(source, []);
}
resultSpecifiers.get(source).push(specifier);
};

root.find(j.ImportDeclaration).forEach((path) => {
if (path.value.importKind && path.value.importKind !== 'value') return;
const importPath = path.value.source.value.replace(/(index)?(\.js)?$/, '');
const match = importPath.match(importRegExp);
if (!match) return;

const subpath = match[1].replace(/\/$/, '');

if (/^(internal)/.test(subpath)) return;
const targetImportPath = `${targetModule}/${subpath}`;

const whitelist = getJSExports(
require.resolve(`${importModule}/modern/${subpath}`, {
paths: [dirname(fileInfo.path)],
}),
);

path.node.specifiers.forEach((specifier, index) => {
if (!path.node.specifiers.length) return;

if (specifier.importKind && specifier.importKind !== 'value') return;
if (specifier.type === 'ImportNamespaceSpecifier') return;
const localName = specifier.local.name;
switch (specifier.type) {
case 'ImportNamespaceSpecifier':
return;
case 'ImportDefaultSpecifier': {
const moduleName = match[2];
if (!whitelist.has(moduleName) && moduleName !== 'withStyles') {
return;
}
addSpecifier(
targetImportPath,
j.importSpecifier(j.identifier(moduleName), j.identifier(localName)),
);
path.get('specifiers', index).prune();
break;
}
case 'ImportSpecifier':
if (!whitelist.has(specifier.imported.name)) return;
addSpecifier(targetImportPath, specifier);
path.get('specifiers', index).prune();
break;
default:
break;
}
});

if (!path.node.specifiers.length) path.prune();
});

addImports(
root,
[...resultSpecifiers.keys()].map((source) =>
j.importDeclaration(resultSpecifiers.get(source), j.stringLiteral(source)),
),
);

return root.toSource(printOptions);
}
42 changes: 42 additions & 0 deletions packages/material-ui-codemod/src/v5.0.0/optimal-imports.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import fs from 'fs';
import path from 'path';
import { expect } from 'chai';
import jscodeshift from 'jscodeshift';
import transform from './optimal-imports';
import readFile from '../util/readFile';

function trim(str) {
return str ? str.replace(/^\s+|\s+$/, '') : '';
}

function read(fileName) {
return readFile(path.join(__dirname, fileName));
}

describe('@material-ui/codemod', () => {
describe('v5.0.0', () => {
describe('optimal-imports', () => {
it('convert path as needed', () => {
const actual = transform(
{ source: read('./optimal-imports.test/actual.js'), path: require.resolve('./optimal-imports.test/actual.js') },
{ jscodeshift: jscodeshift },
{},
);

const expected = read('./optimal-imports.test/expected.js');
expect(trim(actual)).to.equal(trim(expected), 'The transformed version should be correct');
});

it('should be idempotent', () => {
const actual = transform(
{ source: read('./optimal-imports.test/expected.js'), path: require.resolve('./optimal-imports.test/expected.js') },
{ jscodeshift: jscodeshift },
{},
);

const expected = read('./optimal-imports.test/expected.js');
expect(trim(actual)).to.equal(trim(expected), 'The transformed version should be correct');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import MenuItem from '@material-ui/core/MenuItem';
import red from '@material-ui/core/colors/red';
import useTheme from '@material-ui/core/styles/useTheme';
import Tab from '@material-ui/core/Tab';
import { green as greenColor } from '@material-ui/core/colors';
import MuiTabs from '@material-ui/core/Tabs';
import createTheme from '@material-ui/core/styles/createTheme';
import yellow from '@material-ui/core/colors/yellow';
import TableContext from '@material-ui/core/Table/TableContext';
import SwitchBase from '@material-ui/core/internal/SwitchBase';
import Ripple from '@material-ui/core/ButtonBase/Ripple';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import MenuItem from '@material-ui/core/MenuItem';
import Tab from '@material-ui/core/Tab';
import { green as greenColor, red, yellow } from '@material-ui/core/colors';
import MuiTabs from '@material-ui/core/Tabs';
import TableContext from '@material-ui/core/Table/TableContext';
import SwitchBase from '@material-ui/core/internal/SwitchBase';
import Ripple from '@material-ui/core/ButtonBase/Ripple';
import { useTheme, createTheme } from '@material-ui/core/styles';
2 changes: 2 additions & 0 deletions packages/material-ui-codemod/src/v5.0.0/preset-safe.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import transformTypes from './material-ui-types';
import transformModalProps from './modal-props';
import transformMovedLabModules from './moved-lab-modules';
import transformPaginationRoundCircular from './pagination-round-circular';
import transformPrivateImports from './optimal-imports';
import transformRootRef from './root-ref';
import transformSkeletonVariant from './skeleton-variant';
import transformStyledEngineProvider from './styled-engine-provider';
Expand Down Expand Up @@ -80,6 +81,7 @@ export default function transformer(file, api, options) {
file.source = transformModalProps(file, api, options);
file.source = transformMovedLabModules(file, api, options);
file.source = transformPaginationRoundCircular(file, api, options);
file.source = transformPrivateImports(file, api, options);
file.source = transformRootRef(file, api, options);
file.source = transformSkeletonVariant(file, api, options);
file.source = transformStyledEngineProvider(file, api, options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
StylesProvider,
} from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import yellow from '@material-ui/core/colors/yellow';
import CircularProgress from '@material-ui/core/CircularProgress';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
Expand All @@ -33,6 +34,7 @@ const Test = withWidth()((props) => (
<Box
p={2}
bgcolor="divider"
color={yellow[600]}
borderRadius={16}
display="flex"
alignItems="center"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Grid from '@material-ui/core/Grid';
import Switch from '@material-ui/core/Switch';
import Typography from '@material-ui/core/Typography';
import TextField from '@material-ui/core/TextField';
import { yellow } from '@material-ui/core/colors';

// FIXME checkout https://material-ui.com/components/use-media-query/#migrating-from-withwidth
const withWidth = () => (WrappedComponent) => (props) => <WrappedComponent {...props} width="xs" />;
Expand All @@ -31,6 +32,7 @@ const Test = withWidth()((props) => (
<Box
p={2}
bgcolor="divider"
color={yellow[600]}
borderRadius="16px"
display="flex"
alignItems="center"
Expand Down