From 7a85173ca22081726dd2f772ce5ef7bc7ebe0e76 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 24 Jan 2024 13:36:18 +0700 Subject: [PATCH 01/12] init deprecations codemod --- .eslintignore | 1 + .../accordion-props/accordion-props.js | 11 +++++ .../accordion-props/accordion-props.test.js | 43 +++++++++++++++++++ .../accordion-props/test-cases/actual.js | 12 ++++++ .../accordion-props/test-cases/expected.js | 12 ++++++ .../src/deprecations/all/deprecations-all.js | 11 +++++ 6 files changed, 90 insertions(+) create mode 100644 packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js create mode 100644 packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js create mode 100644 packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js create mode 100644 packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js create mode 100644 packages/mui-codemod/src/deprecations/all/deprecations-all.js diff --git a/.eslintignore b/.eslintignore index 298ccbdd2f0041..2933e3842db111 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,6 +11,7 @@ /examples/material-ui-nextjs/src /packages/mui-codemod/lib /packages/mui-codemod/src/*/*.test/* +/packages/mui-codemod/src/**/test-cases/* /packages/mui-icons-material/fixtures /packages/mui-icons-material/legacy /packages/mui-icons-material/lib diff --git a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js new file mode 100644 index 00000000000000..9f3c083b86de65 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js @@ -0,0 +1,11 @@ +/** + * @param {import('jscodeshift').FileInfo} file + * @param {import('jscodeshift').API} api + */ +export default function transformer(file, api, options) { + const j = api.jscodeshift; + + const printOptions = options.printOptions; + + return j(file.source).toSource(printOptions); +} diff --git a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js new file mode 100644 index 00000000000000..6c6d4a71db002a --- /dev/null +++ b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js @@ -0,0 +1,43 @@ +import path from 'path'; +import { expect } from 'chai'; +import jscodeshift from 'jscodeshift'; +import transform from './accordion-props'; +import readFile from '../../util/readFile'; + +function read(fileName) { + return readFile(path.join(__dirname, fileName)); +} + +describe('@mui/codemod', () => { + describe('deprecations', () => { + describe('accordion-props', () => { + it('transforms props as needed', () => { + const actual = transform( + { + source: read('./test-cases/actual.js'), + path: require.resolve('./test-cases/actual.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./test-cases/expected.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + + it('should be idempotent', () => { + const actual = transform( + { + source: read('./test-cases/expected.js'), + path: require.resolve('./test-cases/expected.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./test-cases/expected.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + }); + }); +}); diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js new file mode 100644 index 00000000000000..05bc7e0cca8c5e --- /dev/null +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js @@ -0,0 +1,12 @@ +; +; + +// theme +fn({ + MuiAccordion: { + defaultProps: { + TransitionComponent: CustomTransition, + TransitionProps: { unmountOnExit: true }, + }, + }, +}); diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js new file mode 100644 index 00000000000000..05bc7e0cca8c5e --- /dev/null +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js @@ -0,0 +1,12 @@ +; +; + +// theme +fn({ + MuiAccordion: { + defaultProps: { + TransitionComponent: CustomTransition, + TransitionProps: { unmountOnExit: true }, + }, + }, +}); diff --git a/packages/mui-codemod/src/deprecations/all/deprecations-all.js b/packages/mui-codemod/src/deprecations/all/deprecations-all.js new file mode 100644 index 00000000000000..08e1d4510f02fd --- /dev/null +++ b/packages/mui-codemod/src/deprecations/all/deprecations-all.js @@ -0,0 +1,11 @@ +import transformAccordionProps from '../accordion-props/accordion-props'; + +/** + * @param {import('jscodeshift').FileInfo} file + * @param {import('jscodeshift').API} api + */ +export default function deprecationsAll(file, api, options) { + file.source = transformAccordionProps(file, api, options); + + return file.source; +} From 5c4c9a237f3c1767f0ddf8f9d56b83c80f8f73f3 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 24 Jan 2024 13:40:42 +0700 Subject: [PATCH 02/12] update readme --- packages/mui-codemod/README.md | 35 ++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/mui-codemod/README.md b/packages/mui-codemod/README.md index 0a1f256699e47c..acabaf5646f6cc 100644 --- a/packages/mui-codemod/README.md +++ b/packages/mui-codemod/README.md @@ -60,9 +60,40 @@ npx @mui/codemod@latest --jscodeshift="--printOptions='{\"quo ## Included scripts +- [Deprecation](#deprecations) +- [v5](#v500) +- [v4](#v400) +- [v1](#v100) +- [v0.15](#v0150) + +### Deprecations + +```bash +npx @mui/codemod@latest deprecations/all +``` + +#### `all` + +A combination of all deprecations. + +#### `accordion-props` + +```diff + +``` + +```bash +npx @mui/codemod@latest deprecations/accordion-props +``` + ### v5.0.0 -### `base-use-named-exports` +#### `base-use-named-exports` Base UI default exports were changed to named ones. Previously we had a mix of default and named ones. This was changed to improve consistency and avoid problems some bundlers have with default exports. @@ -81,7 +112,7 @@ This codemod updates the import and re-export statements. npx @mui/codemod@latest v5.0.0/base-use-named-exports ``` -### `base-remove-unstyled-suffix` +#### `base-remove-unstyled-suffix` The `Unstyled` suffix has been removed from all Base UI component names, including names of types and other related identifiers. From 39bf9464b25f04676967cc23088c99244bc2ee91 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Wed, 24 Jan 2024 17:03:10 +0700 Subject: [PATCH 03/12] add accordion-props --- packages/mui-codemod/codemod.js | 48 +++++--- .../accordion-props/accordion-props.js | 105 +++++++++++++++++- .../accordion-props/accordion-props.test.js | 30 +++++ .../src/deprecations/accordion-props/index.js | 1 + .../accordion-props/test-cases/actual.js | 22 ++-- .../accordion-props/test-cases/expected.js | 40 +++++-- .../test-cases/theme.actual.js | 8 ++ .../test-cases/theme.expected.js | 12 ++ 8 files changed, 224 insertions(+), 42 deletions(-) create mode 100644 packages/mui-codemod/src/deprecations/accordion-props/index.js create mode 100644 packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.actual.js create mode 100644 packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.expected.js diff --git a/packages/mui-codemod/codemod.js b/packages/mui-codemod/codemod.js index 20c0622c98f27a..5c9e19d739cc49 100755 --- a/packages/mui-codemod/codemod.js +++ b/packages/mui-codemod/codemod.js @@ -10,27 +10,39 @@ const jscodeshiftDirectory = path.dirname(require.resolve('jscodeshift')); const jscodeshiftExecutable = path.join(jscodeshiftDirectory, jscodeshiftPackage.bin.jscodeshift); async function runTransform(transform, files, flags, codemodFlags) { - const transformerSrcPath = path.resolve(__dirname, './src', `${transform}.js`); - const transformerBuildPath = path.resolve(__dirname, './node', `${transform}.js`); + const paths = [ + path.resolve(__dirname, './src', `${transform}/index.js`), + path.resolve(__dirname, './src', `${transform}.js`), + path.resolve(__dirname, './node', `${transform}/index.js`), + path.resolve(__dirname, './node', `${transform}.js`), + ]; + let transformerPath; - try { - await fs.stat(transformerSrcPath); - transformerPath = transformerSrcPath; - } catch (srcPathError) { + let error; + // eslint-disable-next-line no-restricted-syntax + for (const item of paths) { try { - await fs.stat(transformerBuildPath); - transformerPath = transformerBuildPath; - } catch (buildPathError) { - if (buildPathError.code === 'ENOENT') { - throw new Error( - `Transform '${transform}' not found. Check out ${path.resolve( - __dirname, - './README.md for a list of available codemods.', - )}`, - ); - } - throw buildPathError; + // eslint-disable-next-line no-await-in-loop + await fs.stat(item); + error = undefined; + transformerPath = item; + break; + } catch (srcPathError) { + error = srcPathError; + continue; + } + } + + if (error) { + if (error?.code === 'ENOENT') { + throw new Error( + `Transform '${transform}' not found. Check out ${path.resolve( + __dirname, + './README.md for a list of available codemods.', + )}`, + ); } + throw error; } const args = [ diff --git a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js index 9f3c083b86de65..18542dafe0e9b6 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js @@ -4,8 +4,109 @@ */ export default function transformer(file, api, options) { const j = api.jscodeshift; - + const root = j(file.source); const printOptions = options.printOptions; - return j(file.source).toSource(printOptions); + root.find(j.JSXAttribute, { name: { name: 'TransitionComponent' } }).forEach((path) => { + const slotsNode = /** @type import('jscodeshift').JSXOpeningElement */ ( + path.parent.node + ).attributes.find((attr) => attr.name?.name === 'slots'); + + if (slotsNode) { + const expContainer = /** @type import('jscodeshift').JSXExpressionContainer */ ( + slotsNode.value + ); + if (expContainer.expression.type === 'ObjectExpression') { + // case `slots={{ ... }}` + expContainer.expression.properties.push( + j.objectProperty(j.identifier('transition'), path.node.value.expression), + ); + } else if (expContainer.expression.type === 'Identifier') { + // case `slots={outerSlots} + expContainer.expression = j.objectExpression([ + j.spreadElement(j.identifier(expContainer.expression.name)), + j.objectProperty(j.identifier('transition'), path.node.value.expression), + ]); + } + } else { + path.insertAfter( + j.jsxAttribute( + j.jsxIdentifier('slots'), + j.jsxExpressionContainer( + j.objectExpression([ + j.objectProperty(j.identifier('transition'), path.node.value.expression), + ]), + ), + ), + ); + } + + // remove `TransitionComponent` prop + path.replace(); + }); + + root.find(j.JSXAttribute, { name: { name: 'TransitionProps' } }).forEach((path) => { + const slotPropsNode = /** @type import('jscodeshift').JSXOpeningElement */ ( + path.parent.node + ).attributes.find((attr) => attr.name?.name === 'slotProps'); + + if (slotPropsNode) { + // insert to `slotProps` prop + const expContainer = /** @type import('jscodeshift').JSXExpressionContainer */ ( + slotPropsNode.value + ); + if (expContainer.expression.type === 'ObjectExpression') { + // case `slotProps={{ ... }}` + expContainer.expression.properties.push( + j.objectProperty(j.identifier('transition'), path.node.value.expression), + ); + } else if (expContainer.expression.type === 'Identifier') { + // case `slotProps={outerSlotProps} + expContainer.expression = j.objectExpression([ + j.spreadElement(j.identifier(expContainer.expression.name)), + j.objectProperty(j.identifier('transition'), path.node.value.expression), + ]); + } + } else { + path.insertAfter( + j.jsxAttribute( + j.jsxIdentifier('slotProps'), + j.jsxExpressionContainer( + j.objectExpression([ + j.objectProperty(j.identifier('transition'), path.node.value.expression), + ]), + ), + ), + ); + } + + // remove `TransitionProps` prop + path.replace(); + }); + + root.find(j.Property, { key: { name: 'TransitionComponent' } }).forEach((path) => { + if (path.parent?.parent?.parent?.parent?.node.key?.name === 'MuiAccordion') { + path.replace( + j.property( + 'init', + j.identifier('slots'), + j.objectExpression([j.objectProperty(j.identifier('transition'), path.node.value)]), + ), + ); + } + }); + + root.find(j.Property, { key: { name: 'TransitionProps' } }).forEach((path) => { + if (path.parent?.parent?.parent?.parent?.node.key?.name === 'MuiAccordion') { + path.replace( + j.property( + 'init', + j.identifier('slotProps'), + j.objectExpression([j.objectProperty(j.identifier('transition'), path.node.value)]), + ), + ); + } + }); + + return root.toSource(printOptions); } diff --git a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js index 6c6d4a71db002a..89b724843ec406 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js @@ -39,5 +39,35 @@ describe('@mui/codemod', () => { expect(actual).to.equal(expected, 'The transformed version should be correct'); }); }); + + describe('[theme] accordion-props', () => { + it('transforms props as needed', () => { + const actual = transform( + { + source: read('./test-cases/theme.actual.js'), + path: require.resolve('./test-cases/theme.actual.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./test-cases/theme.expected.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + + it('should be idempotent', () => { + const actual = transform( + { + source: read('./test-cases/theme.expected.js'), + path: require.resolve('./test-cases/theme.expected.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./test-cases/theme.expected.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + }); }); }); diff --git a/packages/mui-codemod/src/deprecations/accordion-props/index.js b/packages/mui-codemod/src/deprecations/accordion-props/index.js new file mode 100644 index 00000000000000..d069f42dc1754a --- /dev/null +++ b/packages/mui-codemod/src/deprecations/accordion-props/index.js @@ -0,0 +1 @@ +export { default } from './accordion-props'; diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js index 05bc7e0cca8c5e..ed0da30e7d35d9 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js @@ -1,12 +1,14 @@ ; ; - -// theme -fn({ - MuiAccordion: { - defaultProps: { - TransitionComponent: CustomTransition, - TransitionProps: { unmountOnExit: true }, - }, - }, -}); +; +; diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js index 05bc7e0cca8c5e..787f5694d2c322 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js @@ -1,12 +1,28 @@ -; -; - -// theme -fn({ - MuiAccordion: { - defaultProps: { - TransitionComponent: CustomTransition, - TransitionProps: { unmountOnExit: true }, - }, - }, -}); +; +; +; +; diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.actual.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.actual.js new file mode 100644 index 00000000000000..b21358b649fa77 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.actual.js @@ -0,0 +1,8 @@ +fn({ + MuiAccordion: { + defaultProps: { + TransitionComponent: CustomTransition, + TransitionProps: { unmountOnExit: true }, + }, + }, +}); diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.expected.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.expected.js new file mode 100644 index 00000000000000..c3495144d05221 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.expected.js @@ -0,0 +1,12 @@ +fn({ + MuiAccordion: { + defaultProps: { + slots: { + transition: CustomTransition + }, + slotProps: { + transition: { unmountOnExit: true } + }, + }, + }, +}); From 78e70aa19a52055938dd59835455d9300c1337a4 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Fri, 26 Jan 2024 09:52:58 +0700 Subject: [PATCH 04/12] add missing index file --- packages/mui-codemod/src/deprecations/all/index.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/mui-codemod/src/deprecations/all/index.js diff --git a/packages/mui-codemod/src/deprecations/all/index.js b/packages/mui-codemod/src/deprecations/all/index.js new file mode 100644 index 00000000000000..dcd97bd9b0e340 --- /dev/null +++ b/packages/mui-codemod/src/deprecations/all/index.js @@ -0,0 +1 @@ +export { default } from './deprecations-all'; From a05e8a28bab67b3b6e329682603b356927ad8625 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Fri, 26 Jan 2024 10:52:08 +0700 Subject: [PATCH 05/12] fix jscodeshift import for test --- .../accordion-props/accordion-props.js | 4 +-- .../accordion-props/accordion-props.test.js | 30 ++++--------------- .../test-cases/theme.expected.js | 3 +- packages/mui-codemod/testUtils/index.js | 4 +++ 4 files changed, 13 insertions(+), 28 deletions(-) create mode 100644 packages/mui-codemod/testUtils/index.js diff --git a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js index 18542dafe0e9b6..c069c808bfceca 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js @@ -84,7 +84,7 @@ export default function transformer(file, api, options) { path.replace(); }); - root.find(j.Property, { key: { name: 'TransitionComponent' } }).forEach((path) => { + root.find(j.ObjectProperty, { key: { name: 'TransitionComponent' } }).forEach((path) => { if (path.parent?.parent?.parent?.parent?.node.key?.name === 'MuiAccordion') { path.replace( j.property( @@ -96,7 +96,7 @@ export default function transformer(file, api, options) { } }); - root.find(j.Property, { key: { name: 'TransitionProps' } }).forEach((path) => { + root.find(j.ObjectProperty, { key: { name: 'TransitionProps' } }).forEach((path) => { if (path.parent?.parent?.parent?.parent?.node.key?.name === 'MuiAccordion') { path.replace( j.property( diff --git a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js index 89b724843ec406..4c807f4721f71c 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.test.js @@ -1,6 +1,6 @@ import path from 'path'; import { expect } from 'chai'; -import jscodeshift from 'jscodeshift'; +import { jscodeshift } from '../../../testUtils'; import transform from './accordion-props'; import readFile from '../../util/readFile'; @@ -12,28 +12,14 @@ describe('@mui/codemod', () => { describe('deprecations', () => { describe('accordion-props', () => { it('transforms props as needed', () => { - const actual = transform( - { - source: read('./test-cases/actual.js'), - path: require.resolve('./test-cases/actual.js'), - }, - { jscodeshift }, - {}, - ); + const actual = transform({ source: read('./test-cases/actual.js') }, { jscodeshift }, {}); const expected = read('./test-cases/expected.js'); expect(actual).to.equal(expected, 'The transformed version should be correct'); }); it('should be idempotent', () => { - const actual = transform( - { - source: read('./test-cases/expected.js'), - path: require.resolve('./test-cases/expected.js'), - }, - { jscodeshift }, - {}, - ); + const actual = transform({ 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'); @@ -43,10 +29,7 @@ describe('@mui/codemod', () => { describe('[theme] accordion-props', () => { it('transforms props as needed', () => { const actual = transform( - { - source: read('./test-cases/theme.actual.js'), - path: require.resolve('./test-cases/theme.actual.js'), - }, + { source: read('./test-cases/theme.actual.js') }, { jscodeshift }, {}, ); @@ -57,10 +40,7 @@ describe('@mui/codemod', () => { it('should be idempotent', () => { const actual = transform( - { - source: read('./test-cases/theme.expected.js'), - path: require.resolve('./test-cases/theme.expected.js'), - }, + { source: read('./test-cases/theme.expected.js') }, { jscodeshift }, {}, ); diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.expected.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.expected.js index c3495144d05221..e98f56af5651d5 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.expected.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.expected.js @@ -4,9 +4,10 @@ fn({ slots: { transition: CustomTransition }, + slotProps: { transition: { unmountOnExit: true } - }, + } }, }, }); diff --git a/packages/mui-codemod/testUtils/index.js b/packages/mui-codemod/testUtils/index.js new file mode 100644 index 00000000000000..038f57962c8576 --- /dev/null +++ b/packages/mui-codemod/testUtils/index.js @@ -0,0 +1,4 @@ +/* eslint-disable import/prefer-default-export */ +import j from 'jscodeshift'; + +export const jscodeshift = j.withParser('tsx'); From 0db96f0e8a266955ca53c2ec4f2f43817e73cb6d Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Fri, 26 Jan 2024 10:56:48 +0700 Subject: [PATCH 06/12] added contributing guide --- packages/mui-codemod/CONTRIBUTING.md | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 packages/mui-codemod/CONTRIBUTING.md diff --git a/packages/mui-codemod/CONTRIBUTING.md b/packages/mui-codemod/CONTRIBUTING.md new file mode 100644 index 00000000000000..ffd1e3176c437f --- /dev/null +++ b/packages/mui-codemod/CONTRIBUTING.md @@ -0,0 +1,44 @@ +## 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 based on [jscodeshift](https://github.com/facebook/jscodeshift) which is a wrapper of [recast](https://github.com/benjamn/recast). + +## Adding new codemods + +1. Create a new folder in `packages/mui-codemod/src/*/*` with the name of the codemod. +2. The folder should include: + - `.js` - the transform implementation + - `.test.js` - tests for the codemod (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). +4. [Test the codemod locally](#local) +5. Add the codemod to README.md + +## Testing + +### Local + +Open the terminal at root directory and run the codemod to test the transformation, for example, testing the `accordion-props` codemod: + +```sh +node packages/mui-codemod/codemod deprecations/accordion-props packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.actual.js +``` + +### CI + +Open the CodeSandbox CI build and copy the link from the "Local Install Instructions" section. + +Run the codemod to test the transformation: + +```sh +npx @mui/codemod@ +``` + +For example: + +```sh +npx @mui/codemod@https://pkg.csb.dev/mui/material-ui/commit/39bf9464/@mui/codemod deprecations/accordion-props docs/src/modules/brandingTheme.ts +``` From e54092cf1cf97e44f7a61e262670bd3afa3a8c2e Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Fri, 26 Jan 2024 12:57:44 +0700 Subject: [PATCH 07/12] fix logic --- .../accordion-props/accordion-props.js | 123 ++++++++---------- .../accordion-props/test-cases/actual.js | 7 +- .../mui-codemod/src/util/appendAttribute.js | 11 ++ packages/mui-codemod/src/util/assignObject.js | 26 ++++ .../mui-codemod/src/util/findComponentJSX.js | 39 ++++++ 5 files changed, 137 insertions(+), 69 deletions(-) create mode 100644 packages/mui-codemod/src/util/appendAttribute.js create mode 100644 packages/mui-codemod/src/util/assignObject.js create mode 100644 packages/mui-codemod/src/util/findComponentJSX.js diff --git a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js index c069c808bfceca..8ae9cc0b1c7167 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js @@ -1,3 +1,7 @@ +import findComponentJSX from '../../util/findComponentJSX'; +import assignObject from '../../util/assignObject'; +import appendAttribute from '../../util/appendAttribute'; + /** * @param {import('jscodeshift').FileInfo} file * @param {import('jscodeshift').API} api @@ -7,81 +11,66 @@ export default function transformer(file, api, options) { const root = j(file.source); const printOptions = options.printOptions; - root.find(j.JSXAttribute, { name: { name: 'TransitionComponent' } }).forEach((path) => { - const slotsNode = /** @type import('jscodeshift').JSXOpeningElement */ ( - path.parent.node - ).attributes.find((attr) => attr.name?.name === 'slots'); - - if (slotsNode) { - const expContainer = /** @type import('jscodeshift').JSXExpressionContainer */ ( - slotsNode.value + findComponentJSX(j, { root, componentName: 'Accordion' }, (elementPath) => { + let expression = elementPath.node.openingElement.attributes.find( + (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'TransitionComponent', + )?.value?.expression; + if (expression) { + const slotsNode = elementPath.node.openingElement.attributes.find( + (attr) => attr.name?.name === 'slots', ); - if (expContainer.expression.type === 'ObjectExpression') { - // case `slots={{ ... }}` - expContainer.expression.properties.push( - j.objectProperty(j.identifier('transition'), path.node.value.expression), - ); - } else if (expContainer.expression.type === 'Identifier') { - // case `slots={outerSlots} - expContainer.expression = j.objectExpression([ - j.spreadElement(j.identifier(expContainer.expression.name)), - j.objectProperty(j.identifier('transition'), path.node.value.expression), - ]); + + if (slotsNode) { + assignObject(j, { + target: slotsNode, + key: 'transition', + expression, + }); + } else { + appendAttribute(j, { + target: elementPath.node, + attributeName: 'slots', + expression: j.objectExpression([ + j.objectProperty(j.identifier('transition'), expression), + ]), + }); } - } else { - path.insertAfter( - j.jsxAttribute( - j.jsxIdentifier('slots'), - j.jsxExpressionContainer( - j.objectExpression([ - j.objectProperty(j.identifier('transition'), path.node.value.expression), - ]), - ), - ), - ); + + elementPath.node.openingElement.attributes = + elementPath.node.openingElement.attributes.filter( + (attr) => attr.name.name === 'TransitionComponent', + ); } - // remove `TransitionComponent` prop - path.replace(); - }); + expression = elementPath.node.openingElement.attributes.find( + (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'TransitionProps', + )?.value?.expression; + if (expression) { + const slotsNode = elementPath.node.openingElement.attributes.find( + (attr) => attr.name?.name === 'slotProps', + ); - root.find(j.JSXAttribute, { name: { name: 'TransitionProps' } }).forEach((path) => { - const slotPropsNode = /** @type import('jscodeshift').JSXOpeningElement */ ( - path.parent.node - ).attributes.find((attr) => attr.name?.name === 'slotProps'); + if (slotsNode) { + assignObject(j, { + target: slotsNode, + key: 'transition', + expression, + }); + } else { + appendAttribute(j, { + target: elementPath.node, + attributeName: 'slotProps', + expression: j.objectExpression([ + j.objectProperty(j.identifier('transition'), expression), + ]), + }); + } - if (slotPropsNode) { - // insert to `slotProps` prop - const expContainer = /** @type import('jscodeshift').JSXExpressionContainer */ ( - slotPropsNode.value - ); - if (expContainer.expression.type === 'ObjectExpression') { - // case `slotProps={{ ... }}` - expContainer.expression.properties.push( - j.objectProperty(j.identifier('transition'), path.node.value.expression), + elementPath.node.openingElement.attributes = + elementPath.node.openingElement.attributes.filter( + (attr) => attr.name.name === 'TransitionProps', ); - } else if (expContainer.expression.type === 'Identifier') { - // case `slotProps={outerSlotProps} - expContainer.expression = j.objectExpression([ - j.spreadElement(j.identifier(expContainer.expression.name)), - j.objectProperty(j.identifier('transition'), path.node.value.expression), - ]); - } - } else { - path.insertAfter( - j.jsxAttribute( - j.jsxIdentifier('slotProps'), - j.jsxExpressionContainer( - j.objectExpression([ - j.objectProperty(j.identifier('transition'), path.node.value.expression), - ]), - ), - ), - ); } - - // remove `TransitionProps` prop - path.replace(); }); root.find(j.ObjectProperty, { key: { name: 'TransitionComponent' } }).forEach((path) => { diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js index ed0da30e7d35d9..bc3d9ce31411f3 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js @@ -1,12 +1,15 @@ +import Accordion from '@mui/material/Accordion'; +import { Accordion as MyAccordion } from '@mui/material'; + ; -; +; ; - }} /> + * @example push expression to `slots.transition` => }} /> + */ +export default function assignObject(j, options) { + const { target, expression, key } = options; + if (target && target.type === 'JSXOpeningElement') { + const expContainer = /** @type import('jscodeshift').JSXExpressionContainer */ (target.value); + + if (expContainer.expression.type === 'ObjectExpression') { + // case `={{ ... }}` + expContainer.expression.properties.push(j.objectProperty(j.identifier(key), expression)); + } else if (expContainer.expression.type === 'Identifier') { + // case `={outerVariable} + expContainer.expression = j.objectExpression([ + j.spreadElement(j.identifier(expContainer.expression.name)), + j.objectProperty(j.identifier(key), expression), + ]); + } + } +} diff --git a/packages/mui-codemod/src/util/findComponentJSX.js b/packages/mui-codemod/src/util/findComponentJSX.js new file mode 100644 index 00000000000000..08d6fa4d497710 --- /dev/null +++ b/packages/mui-codemod/src/util/findComponentJSX.js @@ -0,0 +1,39 @@ +/** + * Find all the JSXElements of a given component name. + * + * @param {import('jscodeshift')} j + * @param {{ root: import('jscodeshift').Collection; componentName: string }} options + * @param {(path: import('jscodeshift').ASTPath) => void} callback + * + */ +export default function findComponentJSX(j, options, callback) { + const { root, componentName } = options; + + // case 1: import ComponentName from '@mui/material/ComponentName'; + // case 2: import { ComponentName } from '@mui/material'; + // case 3: import { ComponentName as SomethingElse } from '@mui/material'; + + const importName = new Set(); + + root + .find(j.ImportDeclaration) + .filter((path) => + path.node.source.value.match(new RegExp(`^@material-ui/material/?(${componentName})?`)), + ) + .forEach((path) => { + path.node.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportDefaultSpecifier') { + importName.add(specifier.local.name); + } + if (specifier.type === 'ImportSpecifier' && specifier.imported.name === componentName) { + importName.add(specifier.local.name); + } + }); + }); + + [...importName].forEach((name) => { + root.findJSXElements(name).forEach((elementPath) => { + callback(elementPath); + }); + }); +} From 280ece1ef18539797bac8ebdeb4cabd16febd29b Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Fri, 26 Jan 2024 13:18:01 +0700 Subject: [PATCH 08/12] update test cases --- .../accordion-props/accordion-props.js | 74 +++++++++---------- .../accordion-props/test-cases/expected.js | 25 ++----- .../mui-codemod/src/util/findComponentJSX.js | 2 +- 3 files changed, 41 insertions(+), 60 deletions(-) diff --git a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js index 8ae9cc0b1c7167..dddc01c2288104 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/accordion-props.js @@ -12,64 +12,58 @@ export default function transformer(file, api, options) { const printOptions = options.printOptions; findComponentJSX(j, { root, componentName: 'Accordion' }, (elementPath) => { - let expression = elementPath.node.openingElement.attributes.find( + let index = elementPath.node.openingElement.attributes.findIndex( (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'TransitionComponent', - )?.value?.expression; - if (expression) { - const slotsNode = elementPath.node.openingElement.attributes.find( - (attr) => attr.name?.name === 'slots', - ); - - if (slotsNode) { - assignObject(j, { - target: slotsNode, - key: 'transition', - expression, - }); - } else { + ); + if (index !== -1) { + const removed = elementPath.node.openingElement.attributes.splice(index, 1); + let hasNode = false; + elementPath.node.openingElement.attributes.forEach((attr) => { + if (attr.name?.name === 'slots') { + hasNode = true; + assignObject(j, { + target: attr, + key: 'transition', + expression: removed[0].value.expression, + }); + } + }); + if (!hasNode) { appendAttribute(j, { target: elementPath.node, attributeName: 'slots', expression: j.objectExpression([ - j.objectProperty(j.identifier('transition'), expression), + j.objectProperty(j.identifier('transition'), removed[0].value.expression), ]), }); } - - elementPath.node.openingElement.attributes = - elementPath.node.openingElement.attributes.filter( - (attr) => attr.name.name === 'TransitionComponent', - ); } - expression = elementPath.node.openingElement.attributes.find( + index = elementPath.node.openingElement.attributes.findIndex( (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'TransitionProps', - )?.value?.expression; - if (expression) { - const slotsNode = elementPath.node.openingElement.attributes.find( - (attr) => attr.name?.name === 'slotProps', - ); - - if (slotsNode) { - assignObject(j, { - target: slotsNode, - key: 'transition', - expression, - }); - } else { + ); + if (index !== -1) { + const removed = elementPath.node.openingElement.attributes.splice(index, 1); + let hasNode = false; + elementPath.node.openingElement.attributes.forEach((attr) => { + if (attr.name?.name === 'slotProps') { + hasNode = true; + assignObject(j, { + target: attr, + key: 'transition', + expression: removed[0].value.expression, + }); + } + }); + if (!hasNode) { appendAttribute(j, { target: elementPath.node, attributeName: 'slotProps', expression: j.objectExpression([ - j.objectProperty(j.identifier('transition'), expression), + j.objectProperty(j.identifier('transition'), removed[0].value.expression), ]), }); } - - elementPath.node.openingElement.attributes = - elementPath.node.openingElement.attributes.filter( - (attr) => attr.name.name === 'TransitionProps', - ); } }); diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js index 787f5694d2c322..0686530e49cb22 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js @@ -1,28 +1,15 @@ +import Accordion from '@mui/material/Accordion'; +import { Accordion as MyAccordion } from '@mui/material'; + ; -; -; -; +; +; diff --git a/packages/mui-codemod/src/util/findComponentJSX.js b/packages/mui-codemod/src/util/findComponentJSX.js index 08d6fa4d497710..e079d004648f7e 100644 --- a/packages/mui-codemod/src/util/findComponentJSX.js +++ b/packages/mui-codemod/src/util/findComponentJSX.js @@ -18,7 +18,7 @@ export default function findComponentJSX(j, options, callback) { root .find(j.ImportDeclaration) .filter((path) => - path.node.source.value.match(new RegExp(`^@material-ui/material/?(${componentName})?`)), + path.node.source.value.match(new RegExp(`^@mui/material/?(${componentName})?`)), ) .forEach((path) => { path.node.specifiers.forEach((specifier) => { From 46f1f12122b4a8cbff3685c739e2318f386febdc Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Fri, 26 Jan 2024 20:24:55 +0700 Subject: [PATCH 09/12] fix guide --- packages/mui-codemod/CONTRIBUTING.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/mui-codemod/CONTRIBUTING.md b/packages/mui-codemod/CONTRIBUTING.md index ffd1e3176c437f..8debfcb046338b 100644 --- a/packages/mui-codemod/CONTRIBUTING.md +++ b/packages/mui-codemod/CONTRIBUTING.md @@ -1,10 +1,12 @@ +# Contributing + ## 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 based on [jscodeshift](https://github.com/facebook/jscodeshift) which is a wrapper of [recast](https://github.com/benjamn/recast). -## Adding new codemods +## 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: @@ -23,7 +25,7 @@ The codemod is based on [jscodeshift](https://github.com/facebook/jscodeshift) w Open the terminal at root directory and run the codemod to test the transformation, for example, testing the `accordion-props` codemod: -```sh +```bash node packages/mui-codemod/codemod deprecations/accordion-props packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.actual.js ``` @@ -33,12 +35,12 @@ Open the CodeSandbox CI build and copy the link from the "Local Install Instruct Run the codemod to test the transformation: -```sh +```bash npx @mui/codemod@ ``` For example: -```sh +```bash npx @mui/codemod@https://pkg.csb.dev/mui/material-ui/commit/39bf9464/@mui/codemod deprecations/accordion-props docs/src/modules/brandingTheme.ts ``` From 8a3e4baeadd16d0a995ba6003a29175ea80f47e0 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 29 Jan 2024 17:07:12 +0700 Subject: [PATCH 10/12] fix assignObject logic --- .../accordion-props/test-cases/actual.js | 37 ++++++++++++++----- .../accordion-props/test-cases/expected.js | 25 ++++++++++++- packages/mui-codemod/src/util/assignObject.js | 2 +- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js index bc3d9ce31411f3..d0f76993c9142a 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/actual.js @@ -1,17 +1,36 @@ import Accordion from '@mui/material/Accordion'; import { Accordion as MyAccordion } from '@mui/material'; -; -; +; +; ; + slots={{ + root: 'div', + transition: CustomTransition + }} + slotProps={{ + root: { className: 'foo' }, + transition: { unmountOnExit: true } + }} />; ; +// should skip non MUI components +; diff --git a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js index 0686530e49cb22..d0f76993c9142a 100644 --- a/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js +++ b/packages/mui-codemod/src/deprecations/accordion-props/test-cases/expected.js @@ -11,5 +11,26 @@ import { Accordion as MyAccordion } from '@mui/material'; }} slotProps={{ transition: transitionVars }} />; -; -; +; +; +// should skip non MUI components +; diff --git a/packages/mui-codemod/src/util/assignObject.js b/packages/mui-codemod/src/util/assignObject.js index edaf3ce1c791fa..f08a1a6fb91d21 100644 --- a/packages/mui-codemod/src/util/assignObject.js +++ b/packages/mui-codemod/src/util/assignObject.js @@ -9,7 +9,7 @@ */ export default function assignObject(j, options) { const { target, expression, key } = options; - if (target && target.type === 'JSXOpeningElement') { + if (target && target.type === 'JSXAttribute') { const expContainer = /** @type import('jscodeshift').JSXExpressionContainer */ (target.value); if (expContainer.expression.type === 'ObjectExpression') { From 465523218c09a45bb4b859a9de061b7141ec4c50 Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 29 Jan 2024 17:07:23 +0700 Subject: [PATCH 11/12] update CONTRIBUTING --- packages/mui-codemod/CONTRIBUTING.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/mui-codemod/CONTRIBUTING.md b/packages/mui-codemod/CONTRIBUTING.md index 8debfcb046338b..f4fc9feba22676 100644 --- a/packages/mui-codemod/CONTRIBUTING.md +++ b/packages/mui-codemod/CONTRIBUTING.md @@ -21,7 +21,16 @@ The codemod is based on [jscodeshift](https://github.com/facebook/jscodeshift) w ## Testing -### Local +I recommend to follow these steps to test the codemod: + +- Create an `actual.js` file with the code you want to transform. +- Run [local](#local) transformation to check if the codemod is correct. +- Copy the transformed code to `expected.js`. +- Run `pnpm tc ` to final check if the codemod is correct. + +💡 The reason that I don't recommend creating the `expected.js` and run the test with `pnpm` script is because the transformation is likely not pretty-printed and it's hard to compare the output with the expected output. + +### Local transformation (while developing) Open the terminal at root directory and run the codemod to test the transformation, for example, testing the `accordion-props` codemod: @@ -29,9 +38,9 @@ Open the terminal at root directory and run the codemod to test the transformati node packages/mui-codemod/codemod deprecations/accordion-props packages/mui-codemod/src/deprecations/accordion-props/test-cases/theme.actual.js ``` -### CI +### CI (after opening a PR) -Open the CodeSandbox CI build and copy the link from the "Local Install Instructions" section. +To simulate a consumer-facing experience on any project before merging the PR, open the CodeSandbox CI build and copy the link from the "Local Install Instructions" section. Run the codemod to test the transformation: From 3410a5ed516567a6b64f9b560f3762f84d78ba2a Mon Sep 17 00:00:00 2001 From: siriwatknp Date: Mon, 29 Jan 2024 21:11:18 +0700 Subject: [PATCH 12/12] trigger build