Skip to content

Commit

Permalink
[codemod][base] Write a migration script for removal of component p…
Browse files Browse the repository at this point in the history
…rop from components (#36831)
  • Loading branch information
hbjORbj authored Apr 20, 2023
1 parent 46a0bd0 commit 3ad1be6
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 0 deletions.
15 changes: 15 additions & 0 deletions packages/mui-codemod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ npx @mui/codemod <transform> <path> --jscodeshift="--printOptions='{\"quote\":\"

### v5.0.0

#### `base-remove-component-prop`

Remove `component` prop from all Base UI components by transferring its value into `slots.root`.

This change only affects Base UI components.

```diff
- <Input component={CustomRoot} />
+ <Input slots={{ root: CustomRoot }} />
```

```sh
npx @mui/codemod v5.0.0/base-remove-component-prop <path>
```

#### `rename-css-variables`

Updates the names of the CSS variables of the Joy UI components to adapt to the new naming standards of the CSS variables for components.
Expand Down
85 changes: 85 additions & 0 deletions packages/mui-codemod/src/v5.0.0/base-remove-component-prop.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* @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;

const transformed = root
.find(j.ImportDeclaration)
// Process only Base UI components
.filter(({ node }) => node.source.value.startsWith('@mui/base'))
.forEach((path) => {
path.node.specifiers.forEach((elementNode) => {
root.findJSXElements(elementNode.local.name).forEach((elementPath) => {
if (elementPath.node.type !== 'JSXElement') {
return;
}

const attributeNodes = [];

let slotPropNodeInserted = false;
let slotsPropNode;
elementPath.node.openingElement.attributes.forEach((attributeNode) => {
if (attributeNode.type !== 'JSXAttribute') {
return;
}
const attributeName = attributeNode.name.name;
if (attributeName !== 'component' && attributeName !== 'slots') {
attributeNodes.push(attributeNode);
return;
}

if (attributeName === 'component') {
const valueNode = attributeNode.value.expression || attributeNode.value;
const rootObject = j.objectProperty(j.identifier('root'), valueNode);
if (slotsPropNode && slotsPropNode.value.expression) {
slotsPropNode.value.expression.properties.push(rootObject);
} else {
slotsPropNode = j.jsxAttribute(
j.jsxIdentifier('slots'),
j.jsxExpressionContainer(j.objectExpression([rootObject])),
);
if (!slotPropNodeInserted) {
slotPropNodeInserted = true;
attributeNodes.push(slotsPropNode);
}
}

if (file.path.endsWith('.ts') || file.path.endsWith('.tsx')) {
elementPath.node.openingElement.name.name +=
valueNode.type === 'Literal' && valueNode.value && valueNode.raw
? `<${valueNode.raw}>`
: `<typeof ${valueNode.name}>`;
}
}

if (attributeName === 'slots') {
if (
slotsPropNode &&
slotsPropNode.value.expression &&
attributeNode.value.expression
) {
slotsPropNode.value.expression.properties = [
...slotsPropNode.value.expression.properties,
...attributeNode.value.expression.properties,
];
} else {
slotsPropNode = attributeNode;
}
if (!slotPropNodeInserted) {
slotPropNodeInserted = true;
attributeNodes.push(slotsPropNode);
}
}
});

elementPath.node.openingElement.attributes = attributeNodes;
});
});
});

return transformed.toSource(printOptions);
}
43 changes: 43 additions & 0 deletions packages/mui-codemod/src/v5.0.0/base-remove-component-prop.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import path from 'path';
import { expect } from 'chai';
import jscodeshift from 'jscodeshift';
import transform from './base-remove-component-prop';
import readFile from '../util/readFile';

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

describe('@mui/codemod', () => {
describe('v5.0.0', () => {
describe('base-remove-component-prop', () => {
it('transforms props as needed', () => {
const actual = transform(
{
source: read('./base-remove-component-prop.test/actual.tsx'),
path: require.resolve('./base-remove-component-prop.test/actual.tsx'),
},
{ jscodeshift },
{},
);

const expected = read('./base-remove-component-prop.test/expected.tsx');
expect(actual).to.equal(expected, 'The transformed version should be correct');
});
});

it('does not add generics if js is used', () => {
const actual = transform(
{
source: read('./base-remove-component-prop.test/actual.jsx'),
path: require.resolve('./base-remove-component-prop.test/actual.jsx'),
},
{ jscodeshift },
{},
);

const expected = read('./base-remove-component-prop.test/expected.jsx');
expect(actual).to.equal(expected, 'The transformed version should be correct');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import MaterialInput from '@mui/material/Input';
import Input from '@mui/base/Input';
import Switch from '@mui/base/Switch';
import Badge from '@mui/base/Badge';

<MaterialInput component={CustomRoot} />;

<Input component={CustomRoot} />;

<Input component={CustomRoot}></Input>;

<Switch
component={CustomRoot}
randomProp="1"
randomProp2="2"
randomProp3="3"
slotProps={{ root: { className: 'root' } }}
/>;

<Badge
slots={{ badge: CustomBadge }}
component={CustomRoot}
randomProp="1"
randomProp2="2"
randomProp3="3"
slotProps={{ badge: { className: 'badge' } }}
/>;

<Input component='a' href='url'></Input>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @ts-nocheck
import MaterialInput from '@mui/material/Input';
import Input from '@mui/base/Input';
import Switch from '@mui/base/Switch';
import Badge from '@mui/base/Badge';

<MaterialInput component={CustomRoot} />;

<Input component={CustomRoot} />;

<Input component={CustomRoot}></Input>;

<Switch
component={CustomRoot}
randomProp="1"
randomProp2="2"
randomProp3="3"
slotProps={{ root: { className: 'root' } }}
/>;

<Badge
slots={{ badge: CustomBadge }}
component={CustomRoot}
randomProp="1"
randomProp2="2"
randomProp3="3"
slotProps={{ badge: { className: 'badge' } }}
/>;

<Input component='a' href='url'></Input>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import MaterialInput from '@mui/material/Input';
import Input from '@mui/base/Input';
import Switch from '@mui/base/Switch';
import Badge from '@mui/base/Badge';

<MaterialInput component={CustomRoot} />;

<Input slots={{
root: CustomRoot
}} />;

<Input slots={{
root: CustomRoot
}}></Input>;

<Switch
slots={{
root: CustomRoot
}}
randomProp="1"
randomProp2="2"
randomProp3="3"
slotProps={{ root: { className: 'root' } }}
/>;

<Badge
slots={{
badge: CustomBadge,
root: CustomRoot
}}
randomProp="1"
randomProp2="2"
randomProp3="3"
slotProps={{ badge: { className: 'badge' } }} />;

<Input slots={{
root: 'a'
}} href='url'></Input>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// @ts-nocheck
import MaterialInput from '@mui/material/Input';
import Input from '@mui/base/Input';
import Switch from '@mui/base/Switch';
import Badge from '@mui/base/Badge';

<MaterialInput component={CustomRoot} />;

<Input<typeof CustomRoot> slots={{
root: CustomRoot
}} />;

<Input<typeof CustomRoot> slots={{
root: CustomRoot
}}></Input>;

<Switch<typeof CustomRoot>
slots={{
root: CustomRoot
}}
randomProp="1"
randomProp2="2"
randomProp3="3"
slotProps={{ root: { className: 'root' } }}
/>;

<Badge<typeof CustomRoot>
slots={{
badge: CustomBadge,
root: CustomRoot
}}
randomProp="1"
randomProp2="2"
randomProp3="3"
slotProps={{ badge: { className: 'badge' } }} />;

<Input<'a'> slots={{
root: 'a'
}} href='url'></Input>;

0 comments on commit 3ad1be6

Please sign in to comment.