Skip to content

Commit

Permalink
[codemod] Add codemods for optimal tree-shakeable imports (#16192)
Browse files Browse the repository at this point in the history
  • Loading branch information
jedwards1211 authored and oliviertassinari committed Jul 24, 2019
1 parent 0f57b9d commit fd24690
Show file tree
Hide file tree
Showing 13 changed files with 614 additions and 3 deletions.
31 changes: 31 additions & 0 deletions packages/material-ui-codemod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,34 @@ This codemod tries to perform a basic expression simplification which can be imp
+const spacing = theme.spacing(5) * 5;
```

#### `optimal-imports`

Converts all `@material-ui/core` imports more than 1 level deep to the optimal form for tree shaking:

```diff
-import withStyles from '@material-ui/core/styles/withStyles';
-import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
+import { withStyles, createMuiTheme } from '@material-ui/core/styles';
```

```sh
find src -name '*.js' -print | xargs jscodeshift -t node_modules/@material-ui/codemod/lib/v4.0.0/optimal-imports.js
```

#### `top-level-imports`

Converts all `@material-ui/core` submodule imports to the root module:

```diff
-import List from '@material-ui/core/List';
-import { withStyles } from '@material-ui/core/styles';
+import { List, withStyles } from '@material-ui/core';
```

```sh
find src -name '*.js' -print | xargs jscodeshift -t node_modules/@material-ui/codemod/lib/v4.0.0/top-level-imports.js
```

### v1.0.0

#### `import-path`
Expand All @@ -64,10 +92,12 @@ find src -name '*.js' -print | xargs jscodeshift -t node_modules/@material-ui/co
```

**Notice**: if you are migrating from pre-v1.0, and your imports use `material-ui`, you will need to manually find and replace all references to `material-ui` in your code to `@material-ui/core`. E.g.:

```diff
-import Typography from 'material-ui/Typography';
+import Typography from '@material-ui/core/Typography';
```

Subsequently, you can run the above `find ...` command to flatten your imports.

#### `color-imports`
Expand All @@ -87,6 +117,7 @@ find src -name '*.js' -print | xargs jscodeshift -t node_modules/@material-ui/co
```

**additional options**

```
jscodeshift -t <color-imports.js> <path> --importPath='mui/styles/colors' --targetPath='mui/colors'
```
Expand Down
5 changes: 5 additions & 0 deletions packages/material-ui-codemod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
},
"license": "MIT",
"homepage": "https://github.com/mui-org/material-ui/tree/master/packages/material-ui-codemod",
"dependencies": {
"@babel/core": "^7.4.5",
"@babel/traverse": "^7.4.5",
"jscodeshift-add-imports": "^1.0.1"
},
"devDependencies": {
"jscodeshift": "^0.6.0"
},
Expand Down
22 changes: 22 additions & 0 deletions packages/material-ui-codemod/src/util/getJSExports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import memoize from './memoize';
import { readFileSync } from 'fs';
import { parseSync } from '@babel/core';
import traverse from '@babel/traverse';

const getJSExports = memoize(file => {
const result = new Set();

const ast = parseSync(readFileSync(file, 'utf8'), {
filename: file,
});

traverse(ast, {
ExportSpecifier: ({ node: { exported } }) => {
result.add(exported.name);
},
});

return result;
});

export default getJSExports;
12 changes: 12 additions & 0 deletions packages/material-ui-codemod/src/util/memoize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const memoize = (func, resolver = a => a) => {
const cache = new Map();
return (...args) => {
const key = resolver(...args);
if (cache.has(key)) return cache.get(key);
const value = func(...args);
cache.set(key, value);
return value;
};
};

export default memoize;
92 changes: 92 additions & 0 deletions packages/material-ui-codemod/src/v4.0.0/optimal-imports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { dirname } from 'path';
import getJSExports from '../util/getJSExports';
import addImports from 'jscodeshift-add-imports';

// istanbul ignore next
if (process.env.NODE_ENV === 'test') {
const resolve = require.resolve;
require.resolve = source =>
resolve(source.replace(/^@material-ui\/core\/es/, '../../../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-utils)/.test(subpath)) return;
const targetImportPath = `${targetModule}/${subpath}`;

const whitelist = getJSExports(
require.resolve(`${importModule}/es/${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)) 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()]
.sort()
.map(source =>
j.importDeclaration(resultSpecifiers.get(source).sort(), j.stringLiteral(source)),
),
);

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

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

function read(fileName) {
return fs.readFileSync(path.join(__dirname, fileName), 'utf8').toString();
}

describe('@material-ui/codemod', () => {
describe('v4.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');

assert.strictEqual(
trim(actual),
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');

assert.strictEqual(
trim(actual),
trim(expected),
'The transformed version should be correct',
);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import { withTheme } from '@material-ui/core/styles';
import withStyles from '@material-ui/core/styles/withStyles';
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
import MenuItem from '@material-ui/core/MenuItem';
import Tab from '@material-ui/core/Tab';
import MuiTabs from '@material-ui/core/Tabs';
import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
import BottomNavigation from '@material-ui/core/BottomNavigation';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Card from '@material-ui/core/Card';
import CardMedia from '@material-ui/core/CardMedia';
import CardHeader from '@material-ui/core/CardHeader';
import MuiCollapse from '@material-ui/core/Collapse';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import List from '@material-ui/core/List';
import DialogTitle from '@material-ui/core/DialogTitle';
import Dialog from '@material-ui/core/Dialog';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import withMobileDialog from '@material-ui/core/withMobileDialog';
import Slide from '@material-ui/core/Slide';
import RadioGroup from '@material-ui/core/RadioGroup';
import Radio from '@material-ui/core/Radio';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import ExpansionPanelActions from '@material-ui/core/ExpansionPanelActions';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import GridListTile from '@material-ui/core/GridListTile';
import GridList from '@material-ui/core/GridList';
import CircularProgress from '@material-ui/core/CircularProgress';
import MuiLinearProgress from '@material-ui/core/LinearProgress';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormGroup from '@material-ui/core/FormGroup';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import Fade from '@material-ui/core/Fade';
import StepContent from '@material-ui/core/StepContent';
import StepButton from '@material-ui/core/StepButton';
import Step from '@material-ui/core/Step';
import Stepper from '@material-ui/core/Stepper';
import TableRow from '@material-ui/core/TableRow';
import TablePagination from '@material-ui/core/TablePagination';
import TableCell from '@material-ui/core/TableCell';
import TableBody from '@material-ui/core/TableBody';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import InputLabel from '@material-ui/core/InputLabel';
import Input from '@material-ui/core/Input';
import Grow from '@material-ui/core/Grow';
import TableFooter from '@material-ui/core/TableFooter';
import withWidth, { isWidthUp } from '@material-ui/core/withWidth';
import Zoom from '@material-ui/core/Zoom';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import ListSubheader from '@material-ui/core/ListSubheader';
// just testing that these get ignored
import TableContext from '@material-ui/core/Table/TableContext';
import TabScrollButton from '@material-ui/core/Tabs/TabScrollButton';
import SwitchBase from '@material-ui/core/internal/SwitchBase';
import createMount from '@material-ui/core/test-utils/createMount';
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import { withTheme, withStyles, createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles';
import MenuItem from '@material-ui/core/MenuItem';
import Tab from '@material-ui/core/Tab';
import MuiTabs from '@material-ui/core/Tabs';
import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
import BottomNavigation from '@material-ui/core/BottomNavigation';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Card from '@material-ui/core/Card';
import CardMedia from '@material-ui/core/CardMedia';
import CardHeader from '@material-ui/core/CardHeader';
import MuiCollapse from '@material-ui/core/Collapse';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import List from '@material-ui/core/List';
import DialogTitle from '@material-ui/core/DialogTitle';
import Dialog from '@material-ui/core/Dialog';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import withMobileDialog from '@material-ui/core/withMobileDialog';
import Slide from '@material-ui/core/Slide';
import RadioGroup from '@material-ui/core/RadioGroup';
import Radio from '@material-ui/core/Radio';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import ExpansionPanelActions from '@material-ui/core/ExpansionPanelActions';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import GridListTile from '@material-ui/core/GridListTile';
import GridList from '@material-ui/core/GridList';
import CircularProgress from '@material-ui/core/CircularProgress';
import MuiLinearProgress from '@material-ui/core/LinearProgress';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormGroup from '@material-ui/core/FormGroup';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import Fade from '@material-ui/core/Fade';
import StepContent from '@material-ui/core/StepContent';
import StepButton from '@material-ui/core/StepButton';
import Step from '@material-ui/core/Step';
import Stepper from '@material-ui/core/Stepper';
import TableRow from '@material-ui/core/TableRow';
import TablePagination from '@material-ui/core/TablePagination';
import TableCell from '@material-ui/core/TableCell';
import TableBody from '@material-ui/core/TableBody';
import Table from '@material-ui/core/Table';
import TableHead from '@material-ui/core/TableHead';
import InputLabel from '@material-ui/core/InputLabel';
import Input from '@material-ui/core/Input';
import Grow from '@material-ui/core/Grow';
import TableFooter from '@material-ui/core/TableFooter';
import withWidth, { isWidthUp } from '@material-ui/core/withWidth';
import Zoom from '@material-ui/core/Zoom';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import ListSubheader from '@material-ui/core/ListSubheader';
// just testing that these get ignored
import TableContext from '@material-ui/core/Table/TableContext';
import TabScrollButton from '@material-ui/core/Tabs/TabScrollButton';
import SwitchBase from '@material-ui/core/internal/SwitchBase';
import createMount from '@material-ui/core/test-utils/createMount';
Loading

0 comments on commit fd24690

Please sign in to comment.