Skip to content

Commit

Permalink
Add AccordionSummary classes deprecation codemods
Browse files Browse the repository at this point in the history
  • Loading branch information
DiegoAndai committed Feb 8, 2024
1 parent 7917d2a commit 75f5ff3
Show file tree
Hide file tree
Showing 16 changed files with 464 additions and 18 deletions.
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
80 changes: 73 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,75 @@ 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'];

// 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,59 @@
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;

root.find(j.ObjectProperty, { key: { name: 'contentGutters' } }).forEach((path) => {
if (
path.parent?.parent?.node.key?.name === 'styleOverrides' &&
path.parent?.parent?.parent?.parent?.node.key?.name === 'MuiAccordionSummary'
) {
path.replace(
j.property(
'init',
j.identifier('gutters'),
j.objectExpression([
j.objectProperty(j.stringLiteral('& .MuiAccordionSummary-content'), path.node.value),
]),
),
);
}
});

root
.find(j.Literal, (literal) => literal.value.match(/^.MuiAccordionSummary-contentGutters/))
.forEach((path) => {
path.replace(
j.literal(
path.value.value.replace(
/^.MuiAccordionSummary-contentGutters/,
'.MuiAccordionSummary-gutters .MuiAccordionSummary-content',
),
),
);
});

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

// this is a special case for contentGutters 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 childSelectorRegex = new RegExp(`^& ${deprecatedClass}`);
root
.find(j.Literal, (literal) => literal.value.match(childSelectorRegex))
.forEach((path) => {
path.replace(j.literal(path.value.value.replace(childSelectorRegex, 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-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-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-classes';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const deprecatedClass = '.MuiAccordionSummary-contentGutters';
const replacementSelector = '.MuiAccordionSummary-gutters .MuiAccordionSummary-content';

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

if (selector.match(directRegex)) {
rule.selector = selector.replace(directRegex, replacementSelector);
} else if (selector.match(childSelectorRegex)) {
// this is a special case for contentGutters 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
rule.selector = selector.replace(childSelectorRegex, 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,7 @@
.MuiAccordionSummary-contentGutters {
color: red;
}

.MuiAccordionSummary-root .MuiAccordionSummary-contentGutters {
color: red;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
fn({
MuiAccordionSummary: {
styleOverrides: {
contentGutters: {
color: 'red',
},
},
},
});

styled(Component)(() => {
return {
'.MuiAccordionSummary-contentGutters': {
color: 'red',
},
};
});

styled(Component)(() => {
return {
'& .MuiAccordionSummary-contentGutters': {
color: 'red',
},
};
});

<>
<AccordionSummary
sx={{
'.MuiAccordionSummary-contentGutters': {
color: 'red',
},
}}
/>

<AccordionSummary
sx={{
'& .MuiAccordionSummary-contentGutters': {
color: 'red',
},
}}
/>
</>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.MuiAccordionSummary-gutters .MuiAccordionSummary-content {
color: red;
}

.MuiAccordionSummary-root.MuiAccordionSummary-gutters .MuiAccordionSummary-content {
color: red;
}
Loading

0 comments on commit 75f5ff3

Please sign in to comment.