Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
129 changes: 92 additions & 37 deletions packages/mui-codemod/src/v5.0.0/jss-to-styled.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,39 @@ export default function transformer(file, api, options) {
return tagName === 'React.Suspense' || tagName === 'Suspense';
}

const KEYFRAMES_KEY = /^@keyframes\s+(.+)$/;

/**
* JSS declares keyframes with a string key, for example `'@keyframes pulse'`.
* It is not a class name, and the percentages it nests are not class names either, so the
* whole property has to be carried over untouched. Emotion reads it from the style object as is.
*
* Other at-rules such as `@media` do nest class names, so they need a different treatment and
* are deliberately left alone here.
*
* @param {import('jscodeshift').ObjectProperty} prop
*/
function isKeyframes(prop) {
const key = prop.key;
return (
!!key &&
(key.type === 'StringLiteral' || key.type === 'Literal') &&
typeof key.value === 'string' &&
KEYFRAMES_KEY.test(key.value.trim())
);
}

/**
* `'@keyframes pulse'` -> `'pulse'`
*
* @param {import('jscodeshift').ObjectExpression} objectExpression
*/
function getKeyframeNames(objectExpression) {
return objectExpression.properties
.filter(isKeyframes)
.map((prop) => KEYFRAMES_KEY.exec(prop.key.value.trim())[1].trim());
}

function createStyledComponent(componentName, styledComponentName, stylesFn) {
let styleArg = null;
const rootIsFragment = isTagNameFragment(componentName);
Expand Down Expand Up @@ -176,6 +209,9 @@ export default function transformer(file, api, options) {
function createClasses(objExpression, prevObj) {
const classes = prevObj || j.objectExpression([]);
objExpression.properties.forEach((prop) => {
if (isKeyframes(prop)) {
return;
}
if (!classesCount[prop.key.name]) {
classesCount[prop.key.name] = 1;
} else {
Expand Down Expand Up @@ -276,27 +312,44 @@ export default function transformer(file, api, options) {
const objectExpression = getObjectExpression(functionExpression);

if (objectExpression) {
objectExpression.properties.forEach((prop) => {
if (!stylesCount[prop.key.name]) {
stylesCount[prop.key.name] = 1;
} else {
stylesCount[prop.key.name] += 1;
}
const resolvedKey =
stylesCount[prop.key.name] === 1
? prop.key.name
: `${prop.key.name}${stylesCount[prop.key.name]}`;
const selector = rootKeys.includes(resolvedKey) ? '&.' : '& .';
prop.key = j.templateLiteral(
[
j.templateElement({ raw: selector, cooked: selector }, false),
j.templateElement({ raw: '', cooked: '' }, true),
],
[j.identifier(`classes.${resolvedKey}`)],
);
prop.computed = true;
return prop;
});
// JSS refers to a keyframe with `$name`, emotion refers to it by its bare name.
const keyframeNames = getKeyframeNames(objectExpression);
if (keyframeNames.length > 0) {
j(objectExpression)
.find(j.Literal)
.forEach(({ node }) => {
if (typeof node.value !== 'string') {
return;
}
keyframeNames.forEach((name) => {
node.value = node.value.replace(new RegExp(`\\$${name}\\b`, 'g'), name);
});
});
}

objectExpression.properties
.filter((prop) => !isKeyframes(prop))
.forEach((prop) => {
if (!stylesCount[prop.key.name]) {
stylesCount[prop.key.name] = 1;
} else {
stylesCount[prop.key.name] += 1;
}
const resolvedKey =
stylesCount[prop.key.name] === 1
? prop.key.name
: `${prop.key.name}${stylesCount[prop.key.name]}`;
const selector = rootKeys.includes(resolvedKey) ? '&.' : '& .';
prop.key = j.templateLiteral(
[
j.templateElement({ raw: selector, cooked: selector }, false),
j.templateElement({ raw: '', cooked: '' }, true),
],
[j.identifier(`classes.${resolvedKey}`)],
);
prop.computed = true;
return prop;
});
}

if (functionExpression.params) {
Expand Down Expand Up @@ -385,22 +438,24 @@ export default function transformer(file, api, options) {
withStylesComponents.push({
variableName: path.parent.parent.node.id.name,
classes: j.objectExpression(
objectExpression.properties.map((prop) => {
if (!componentClassesCount[prop.key.name]) {
componentClassesCount[prop.key.name] = 1;
} else {
componentClassesCount[prop.key.name] += 1;
}
const resolvedKey =
componentClassesCount[prop.key.name] === 1
? prop.key.name
: `${prop.key.name}${componentClassesCount[prop.key.name]}`;
return j.property(
'init',
j.identifier(prop.key.name),
j.memberExpression(j.identifier('classes'), j.identifier(resolvedKey)),
);
}),
objectExpression.properties
.filter((prop) => !isKeyframes(prop))
.map((prop) => {
if (!componentClassesCount[prop.key.name]) {
componentClassesCount[prop.key.name] = 1;
} else {
componentClassesCount[prop.key.name] += 1;
}
const resolvedKey =
componentClassesCount[prop.key.name] === 1
? prop.key.name
: `${prop.key.name}${componentClassesCount[prop.key.name]}`;
return j.property(
'init',
j.identifier(prop.key.name),
j.memberExpression(j.identifier('classes'), j.identifier(resolvedKey)),
);
}),
),
});
}
Expand Down
30 changes: 30 additions & 0 deletions packages/mui-codemod/src/v5.0.0/jss-to-styled.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,36 @@ describe('@mui/codemod', () => {
});
});

describe('bugs - #30801 keyframes', () => {
it('transforms as needed', () => {
const actual = transform(
{
source: read('./jss-to-styled.test/keyframes.actual.js'),
path: require.resolve('./jss-to-styled.test/keyframes.actual.js'),
},
{ jscodeshift },
{},
);

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

it('should be idempotent', () => {
const actual = transform(
{
source: read('./jss-to-styled.test/keyframes.expected.js'),
path: require.resolve('./jss-to-styled.test/keyframes.expected.js'),
},
{ jscodeshift },
{},
);

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

describe('bugs - #29363 multiple makeStyles with the same classKeys', () => {
it('transforms as needed', () => {
const actual = transform(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react';
import { makeStyles } from '@mui/styles';

const useStyles = makeStyles((theme) => ({
root: {
animation: '$pulse 1.5s ease-in-out infinite',
color: theme.palette.primary.main,
},
'@keyframes pulse': {
'0%': {
opacity: 1,
},
'100%': {
opacity: 0.4,
},
},
}));

export default function Demo() {
const classes = useStyles();
return <div className={classes.root}>Hello</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';
import { styled } from '@mui/material/styles';
const PREFIX = 'keyframes';

const classes = {
root: `${PREFIX}-root`
};

const Root = styled('div')((
{
theme
}
) => ({
[`&.${classes.root}`]: {
animation: 'pulse 1.5s ease-in-out infinite',
color: theme.palette.primary.main,
},

'@keyframes pulse': {
'0%': {
opacity: 1,
},
'100%': {
opacity: 0.4,
},
}
}));

export default function Demo() {

return <Root className={classes.root}>Hello</Root>;
}
Loading