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

[mui-codemod][AccordionSummary] Add contentGutters deprecation codemods #41006

Merged
merged 18 commits into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,38 @@ The Accordion's `TransitionProps` was deprecated in favor of `slotProps.transiti
/>
```

## AccordionSummary

Use the [codemod](https://github.com/mui/material-ui/tree/HEAD/packages/mui-codemod#accordion-summary-classes) below to migrate the code as described in the following sections:

```bash
npx @mui/codemod@latest deprecations/accordion-summary-classes <path>
```

### .MuiAccordionSummary-contentGutters

The AccordionSummary's `.MuiAccordionSummary-contentGutters` class was deprecated in favor of the `.MuiAccordionSummary-gutters` and `.MuiAccordionSummary-content` classes.
Bear in mind that the `.MuiAccordionSummary-gutters` class is applied to the component's root, whereas the `.MuiAccordionSummary-contentGutters` and `.MuiAccordionSummary-content` classes are applied to the content element.

```diff
-.MuiAccordionSummary-root .MuiAccordionSummary-contentGutters
+.MuiAccordionSummary-root.MuiAccordionSummary-gutters .MuiAccordionSummary-content
DiegoAndai marked this conversation as resolved.
Show resolved Hide resolved
/>
```

```diff
MuiAccordionSummary: {
styleOverrides: {
root: {
- '& .MuiAccordionSummary-contentGutters': {
+ '&.MuiAccordionSummary-gutters .MuiAccordionSummary-content': {
color: 'red',
},
},
},
},
```

## Avatar

Use the [codemod](https://github.com/mui/material-ui/tree/HEAD/packages/mui-codemod#avatar-props) below to migrate the code as described in the following sections:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"description": "Styles applied to {{nodeName}} unless {{conditions}}.",
"nodeName": "the children wrapper element",
"conditions": "<code>disableGutters={true}</code>",
"deprecationInfo": "Combine the <a href=\"/material-ui/api/accordion-summary/#AccordionSummary-classes-gutters\">.MuiAccordionSummary-gutters</a> and <a href=\"/material-ui/api/accordion-summary/#AccordionSummary-classes-content\">.MuiAccordionSummary-content</a> classes instead."
"deprecationInfo": "Combine the <a href=\"/material-ui/api/accordion-summary/#AccordionSummary-classes-gutters\">.MuiAccordionSummary-gutters</a> and <a href=\"/material-ui/api/accordion-summary/#AccordionSummary-classes-content\">.MuiAccordionSummary-content</a> classes instead. <a href=\"/material-ui/migration/migrating-from-deprecated-apis/\">How to migrate</a>."
},
"disabled": {
"description": "State class applied to {{nodeName}} if {{conditions}}.",
Expand Down
16 changes: 12 additions & 4 deletions packages/mui-codemod/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,28 @@

## Understanding the codemod

The codemod is a tool that helps developers migrate thier codebase when we introduced changes in new version. The changes could be deprecations, enhancements, or breaking changes.
The codemod is a tool that helps developers migrate their codebase when we introduce changes in a new version. The changes could be deprecations, enhancements, or breaking changes.

The codemod is based on [jscodeshift](https://github.com/facebook/jscodeshift) which is a wrapper of [recast](https://github.com/benjamn/recast).
The codemods for JS files are based on [jscodeshift](https://github.com/facebook/jscodeshift) which is a wrapper of [recast](https://github.com/benjamn/recast).

The codemods for CSS files are based on [postcss](https://github.com/postcss/postcss).

## Adding a new codemod

1. Create a new folder in `packages/mui-codemod/src/*/*` with the name of the codemod.
2. The folder should include:
- `<codemod>.js` - the transform implementation
- `<codemod>.test.js` - tests for the codemod (use jscodeshift from the `testUtils` folder)
- `postcss-plugin.js` - the postcss plugin (optional)
- `postcss.config.js` - the postcss config file (optional)
- `<codemod>.test.js` - tests for the codemods (use jscodeshift from the `testUtils` folder)
- `test-cases` - folder with fixtures for the codemod
- `actual.js` - the input for the codemod
- `expected.js` - the expected output of the codemod
3. Use [astexplorer](https://astexplorer.net/) to check the AST types and properties (set </> to @babel/parser because we use [`tsx`](https://github.com/benjamn/recast/blob/master/parsers/babel.ts) as a default parser for our codemod).
- `actual.css` - the input for the postcss plugin (optional)
- `expected.css` - the expected output of the postcss plugin (optional)
3. Use [astexplorer](https://astexplorer.net/) to check the AST types and properties
- For JS codemods set </> to @babel/parser because we use [`tsx`](https://github.com/benjamn/recast/blob/master/parsers/babel.ts) as a default parser.
- For CSS codemods set </> to postcss
4. [Test the codemod locally](#local)
5. Add the codemod to README.md

Expand Down
30 changes: 30 additions & 0 deletions packages/mui-codemod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

This repository contains a collection of codemod scripts based for use with
[jscodeshift](https://github.com/facebook/jscodeshift) that help update the APIs.
Some of the codemods also run [postcss](https://github.com/postcss/postcss) plugins to update CSS files.

## Setup & run

Expand Down Expand Up @@ -91,6 +92,35 @@ A combination of all deprecations.
npx @mui/codemod@latest deprecations/accordion-props <path>
```

#### `accordion-summary-classes`

JS transforms:

```diff
MuiAccordionSummary: {
styleOverrides: {
root: {
- '& .MuiAccordionSummary-contentGutters': {
+ '&.MuiAccordionSummary-gutters .MuiAccordionSummary-content': {
color: 'red',
},
},
},
},
```

CSS transforms:

```diff
-.MuiAccordionSummary-root .MuiAccordionSummary-contentGutters
+.MuiAccordionSummary-root.MuiAccordionSummary-gutters .MuiAccordionSummary-content
/>
```

```bash
npx @mui/codemod@latest deprecations/accordion-summary-classes <path>
```

#### `avatar-props`

```diff
Expand Down
87 changes: 80 additions & 7 deletions packages/mui-codemod/codemod.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ const { promises: fs } = require('fs');
const path = require('path');
const yargs = require('yargs');
const jscodeshiftPackage = require('jscodeshift/package.json');
const postcssCliPackage = require('postcss-cli/package.json');

const jscodeshiftDirectory = path.dirname(require.resolve('jscodeshift'));
const jscodeshiftExecutable = path.join(jscodeshiftDirectory, jscodeshiftPackage.bin.jscodeshift);

async function runTransform(transform, files, flags, codemodFlags) {
const postcssCliDirectory = path.dirname(require.resolve('postcss-cli'));
const postcssExecutable = path.join(postcssCliDirectory, postcssCliPackage.bin.postcss);

async function runJscodeshiftTransform(transform, files, flags, codemodFlags) {
const paths = [
path.resolve(__dirname, './src', `${transform}/index.js`),
path.resolve(__dirname, './src', `${transform}.js`),
Expand Down Expand Up @@ -57,6 +61,8 @@ async function runTransform(transform, files, flags, codemodFlags) {
flags.parser || 'tsx',
'--ignore-pattern',
'**/node_modules/**',
'--ignore-pattern',
'**/*.css',
];

if (flags.dry) {
Expand All @@ -80,15 +86,82 @@ async function runTransform(transform, files, flags, codemodFlags) {
}
}

const parseCssFilePaths = async (files) => {
const cssFiles = await Promise.all(
files.map(async (filePath) => {
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
return `${filePath}/**/*.css`;
}
if (filePath.endsWith('.css')) {
return filePath;
}

return null;
}),
);

return cssFiles.filter(Boolean);
};

async function runPostcssTransform(transform, files) {
// local postcss plugins are loaded through config files https://github.com/postcss/postcss-load-config/issues/17#issuecomment-253125559
const paths = [
path.resolve(__dirname, './src', `${transform}/postcss.config.js`),
path.resolve(__dirname, './node', `${transform}/postcss.config.js`),
];

let configPath;
let error;
// eslint-disable-next-line no-restricted-syntax
for (const item of paths) {
try {
// eslint-disable-next-line no-await-in-loop
await fs.stat(item);
error = undefined;
configPath = item;
break;
} catch (srcPathError) {
error = srcPathError;
continue;
}
}

if (error) {
// don't throw if the file is not found, postcss transform is optional
if (error?.code !== 'ENOENT') {
throw error;
}
} else {
const cssPaths = await parseCssFilePaths(files);

if (cssPaths.length > 0) {
const args = [
postcssExecutable,
...cssPaths,
'--config',
configPath,
'--replace',
'--verbose',
];

// eslint-disable-next-line no-console -- debug information
console.log(`Executing command: postcss ${args.join(' ')}`);
const postcssProcess = childProcess.spawnSync('node', args, { stdio: 'inherit' });

if (postcssProcess.error) {
throw postcssProcess.error;
}
}
}
}

function run(argv) {
const { codemod, paths, ...flags } = argv;
const files = paths.map((filePath) => path.resolve(filePath));

return runTransform(
codemod,
paths.map((filePath) => path.resolve(filePath)),
flags,
argv._,
);
runJscodeshiftTransform(codemod, files, flags, argv._);
runPostcssTransform(codemod, files);
}

yargs
Expand Down
2 changes: 2 additions & 0 deletions packages/mui-codemod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"@babel/traverse": "^7.23.9",
"jscodeshift": "^0.13.1",
"jscodeshift-add-imports": "^1.0.10",
"postcss": "^8.4.33",
"postcss-cli": "^8.0.0",
"yargs": "^17.7.2"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { deprecatedClass, replacementSelector } from './postcss-plugin';

/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
export default function transformer(file, api, options) {
const j = api.jscodeshift;
const root = j(file.source);
const printOptions = options.printOptions;

// contentGutters is a special case as it's applied to the content child
// but gutters is applied to the parent element, so the gutter class needs to go on the parent

const selectorRegex = new RegExp(`^& ${deprecatedClass}`);
root
.find(
j.Literal,
(literal) => typeof literal.value === 'string' && literal.value.match(selectorRegex),
)
.forEach((path) => {
path.replace(j.literal(path.value.value.replace(selectorRegex, `&${replacementSelector}`)));
});

return root.toSource(printOptions);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import path from 'path';
import { expect } from 'chai';
import postcss from 'postcss';
import { jscodeshift } from '../../../testUtils';
import jsTransform from './accordion-summary-classes';
import { plugin as postcssPlugin } from './postcss-plugin';
import readFile from '../../util/readFile';

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

const postcssProcessor = postcss([postcssPlugin]);

describe('@mui/codemod', () => {
describe('deprecations', () => {
describe('accordion-summary-classes', () => {
describe('js-transform', () => {
it('transforms props as needed', () => {
const actual = jsTransform(
{ source: read('./test-cases/actual.js') },
{ jscodeshift },
{ printOptions: { quote: 'single', trailingComma: true } },
);

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

it('should be idempotent', () => {
const actual = jsTransform(
{ source: read('./test-cases/expected.js') },
{ jscodeshift },
{},
);

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

describe('css-transform', () => {
it('transforms classes as needed', async () => {
const actual = await postcssProcessor.process(read('./test-cases/actual.css'), {
from: undefined,
});

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

it('should be idempotent', async () => {
const actual = await postcssProcessor.process(read('./test-cases/expected.css'), {
from: undefined,
});

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

describe('test-cases', () => {
it('should not be the same', () => {
const actualJS = read('./test-cases/actual.js');
const expectedJS = read('./test-cases/expected.js');
expect(actualJS).not.to.equal(expectedJS, 'The actual and expected should be different');

const actualCSS = read('./test-cases/actual.css');
const expectedCSS = read('./test-cases/expected.css');
expect(actualCSS).not.to.equal(
expectedCSS,
'The actual and expected should be different',
);
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './accordion-summary-classes';
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const deprecatedClass = '.MuiAccordionSummary-contentGutters';
const replacementSelector = '.MuiAccordionSummary-gutters .MuiAccordionSummary-content';

const plugin = () => {
return {
postcssPlugin: `Replace ${deprecatedClass} with ${replacementSelector}`,
Rule(rule) {
const { selector } = rule;

// contentGutters is a special case as it's applied to the content child
// but gutters is applied to the parent element, so the gutter class needs to go on the parent

const selectorRegex = new RegExp(` ${deprecatedClass}`);
if (selector.match(selectorRegex)) {
rule.selector = selector.replace(selectorRegex, replacementSelector);
}
},
};
};
plugin.postcss = true;

module.exports = {
plugin,
deprecatedClass,
replacementSelector,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const { plugin } = require('./postcss-plugin');

module.exports = {
plugins: [plugin],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.MuiAccordionSummary-root .MuiAccordionSummary-contentGutters {
color: red;
}
Loading
Loading