From 2c4f9122636f0726cdcf28290efddf71a44b587b Mon Sep 17 00:00:00 2001 From: Ryan Cogswell <287804+ryancogswell@users.noreply.github.com> Date: Wed, 11 May 2022 02:20:09 -0500 Subject: [PATCH] [codemod] Add jss to tss-react codemod (#31802) --- .gitignore | 2 + .../guides/migration-v4/migration-v4.md | 152 +++--- packages/mui-codemod/README.md | 82 ++++ .../src/v5.0.0/jss-to-tss-react.js | 452 ++++++++++++++++++ .../src/v5.0.0/jss-to-tss-react.test.js | 153 ++++++ .../actual-from-material-ui-core-styles.js | 31 ++ .../actual-from-material-ui-core.js | 48 ++ .../actual-from-mui-styles.js | 33 ++ .../actual-mixins-pattern.js | 35 ++ .../actual-todo-comments.js | 51 ++ .../actual-typescript-docs-example-params.tsx | 40 ++ .../actual-typescript-docs-example.tsx | 38 ++ .../actual-typescript.tsx | 45 ++ .../actual-withStyles.js | 82 ++++ .../expected-from-material-ui-core-styles.js | 31 ++ .../expected-from-material-ui-core.js | 49 ++ .../expected-from-mui-styles.js | 33 ++ .../expected-mixins-pattern.js | 34 ++ .../expected-todo-comments.js | 59 +++ ...xpected-typescript-docs-example-params.tsx | 43 ++ .../expected-typescript-docs-example.tsx | 37 ++ .../expected-typescript.tsx | 48 ++ .../expected-withStyles.js | 85 ++++ 23 files changed, 1589 insertions(+), 74 deletions(-) create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-material-ui-core-styles.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-material-ui-core.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-mui-styles.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-mixins-pattern.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-todo-comments.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript-docs-example-params.tsx create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript-docs-example.tsx create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript.tsx create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-withStyles.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-material-ui-core-styles.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-material-ui-core.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-mui-styles.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-mixins-pattern.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-todo-comments.js create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript-docs-example-params.tsx create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript-docs-example.tsx create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript.tsx create mode 100644 packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-withStyles.js diff --git a/.gitignore b/.gitignore index 48190e100a0371..823978752e3e16 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ # However, in order to prevent issues, they are ignored here. .DS_STORE .idea +# IntelliJ IDEA module file +*.iml .vscode/* !.vscode/launch.json *.log diff --git a/docs/data/material/guides/migration-v4/migration-v4.md b/docs/data/material/guides/migration-v4/migration-v4.md index a6a48b4607d32e..e4e104e16585b0 100644 --- a/docs/data/material/guides/migration-v4/migration-v4.md +++ b/docs/data/material/guides/migration-v4/migration-v4.md @@ -2917,7 +2917,7 @@ Note: This API will not work if you are [using `styled-components` as underlying ::: The API is similar to JSS `makeStyles` but, under the hood, it uses `@emotion/react`. -It is also features a much better TypeScript support than v4's `makeStyles`. +It also features much better TypeScript support than v4's `makeStyles`. In order to use it, you'll need to add it to your project's dependencies: @@ -2951,7 +2951,15 @@ yarn add tss-react ); ``` -Then here is one example: +#### Codemod + +We provide [a codemod](https://github.com/mui/material-ui/blob/master/packages/mui-codemod/README.md#jss-to-tss-react) to help migrate JSS styles to the `tss-react` API. + +```sh +npx @mui/codemod v5.0.0/jss-to-tss-react +``` + +**Example transformation**: ```diff import React from 'react'; @@ -2988,15 +2996,17 @@ Then here is one example: export default Apply; ``` -If you were using the `$` syntax, the transformation would look like this: +If you were using the `$` syntax and `clsx` to combine multiple CSS classes, +the transformation would look like this: ```diff import * as React from 'react'; --import makeStyles from '@material-ui/styles/makeStyles'; +-import { makeStyles } from '@material-ui/core/styles'; +-import clsx from 'clsx'; +import { makeStyles } from 'tss-react/mui'; --const useStyles = makeStyles((theme) => { -+const useStyles = makeStyles()((_theme, _params, classes) => ({ +-const useStyles = makeStyles((theme) => ({ ++const useStyles = makeStyles()((theme, _params, classes) => ({ parent: { padding: 30, - '&:hover $child': { @@ -3004,19 +3014,30 @@ If you were using the `$` syntax, the transformation would look like this: backgroundColor: 'red', }, }, + small: {}, child: { backgroundColor: 'blue', + height: 50, +- '&$small': { ++ [`&.${classes.small}`]: { + backgroundColor: 'lightblue', + height: 30 + } }, - }); + })); function App() { - const classes = useStyles(); -+ const { classes } = useStyles(); - ++ const { classes, cx } = useStyles(); return (
-
- Background turns red when the mouse is hover the parent +
+ Background turns red when the mouse hovers over the parent. +
+-
++
+ Background turns red when the mouse hovers over the parent. + I am smaller than the other child.
); @@ -3026,11 +3047,12 @@ If you were using the `$` syntax, the transformation would look like this: ``` :::warning -**Note:** In plain JS projects (not using TypeScript), remove ``. +**Note:** In plain JS projects (not using TypeScript), remove ``. ::: -Now, a comprehensive example using both the `$` syntax, `useStyles()` parameters -and [an explicit name for the stylesheet](https://github.com/garronej/tss-react#naming-the-stylesheets-useful-for-debugging). +Now, a comprehensive example using the `$` syntax, `useStyles()` parameters, +merging in classes from a `classes` prop ([see doc](https://docs.tss-react.dev/your-own-classes-prop)), +and [an explicit name for the stylesheet](https://docs.tss-react.dev/page-1/makestyles-usestyles#naming-the-stylesheets-useful-for-debugging-and-theme-style-overrides). ```diff -import clsx from 'clsx'; @@ -3038,35 +3060,41 @@ and [an explicit name for the stylesheet](https://github.com/garronej/tss-react# +import { makeStyles } from 'tss-react/mui'; -const useStyles = makeStyles((theme) => createStyles< -- 'root' | 'small' | 'child', { color: 'primary' | 'secondary' } -->({ -+const useStyles = makeStyles< -+ { color: 'primary' | 'secondary' }, 'child' | 'small' -+>({ name: 'App' })((theme, { color }, classes) => ({ -- root: ({ color })=> ({ +- 'root' | 'small' | 'child', {color: 'primary' | 'secondary', padding: number} +-> +-({ +- root: ({color, padding}) => ({ ++const useStyles = makeStyles<{color: 'primary' | 'secondary', padding: number}, 'child' | 'small'>({name: 'App'})((theme, { color, padding }, classes) => ({ + root: { - padding: 30, -- '&:hover .child': { + padding: padding, +- '&:hover $child': { + [`&:hover .${classes.child}`]: { backgroundColor: theme.palette[color].main, } - }), + }, - small: {}, - child: { - border: '1px solid black', - height: 50, -- '&.small': { + small: {}, + child: { + border: '1px solid black', + height: 50, +- '&$small': { + [`&.${classes.small}`]: { - height: 30 - } - } --}, { name: 'App' }); + height: 30 + } + } +-}), {name: 'App'}); +})); - function App() { -- const classes = useStyles({ color: 'primary' }); -+ const { classes, cx } = useStyles({ color: 'primary' }); + function App({classes: classesProp}: {classes?: any}) { +- const classes = useStyles({color: 'primary', padding: 30, classes: classesProp}); ++ const { classes, cx } = useStyles({ ++ color: 'primary', ++ padding: 30 ++ }, { ++ props: { ++ classes: classesProp ++ } ++ }); return (
@@ -3078,30 +3106,40 @@ and [an explicit name for the stylesheet](https://github.com/garronej/tss-react# The Background take the primary theme color when the mouse hovers the parent. I am smaller than the other child.
-
- ); - } +
+ ); +} - export default App; +export default App; ``` +After running the codemod, search your code for "TODO jss-to-tss-react codemod" to find cases that +the codemod could not handle reliably; though there may be cases beyond those with TODO comments that +are not handled fully by the codemod particularly if parts of the styles are returned by functions. +If the styles buried within a function use the `$` syntax or `useStyles` params, then those styles won't +be migrated appropriately. + :::error **WARNING**: You should drop [`clsx`](https://www.npmjs.com/package/clsx) in favor of [`cx`](https://emotion.sh/docs/@emotion/css#cx). The key advantage of `cx` is that it detects emotion generated class names ensuring styles are overwritten in the correct order. +The default precedence of styles from multiple CSS classes is different between JSS and tss-react and some manual re-ordering of `cx` parameters +may be necessary (see [this issue comment](https://github.com/mui/material-ui/pull/31802#issuecomment-1093478971) for more details). ::: :::warning **Note**: To ensure that your class names always includes the actual name of your components, you can provide the `name` as an implicitly named key (`name: { App }`). [See doc](https://docs.tss-react.dev/page-1/makestyles-usestyles#naming-the-stylesheets-useful-for-debugging-and-theme-style-overrides). +You may end up with eslint warnings [like this one](https://user-images.githubusercontent.com/6702424/148657837-eae48942-fb86-4516-abe4-5dc10f44f0be.png) if you deconstruct more than one item. +Don't hesitate to disable `eslint(prefer-const)`, [like this](https://github.com/thieryw/gitlanding/blob/b2b0c71d95cfd353979c86dfcfa1646ef1665043/.eslintrc.js#L17) in a regular project, or [like this](https://github.com/InseeFrLab/onyxia-web/blob/a264ec6a6a7110cb1a17b2e22cc0605901db6793/package.json#L133) in a CRA. ::: #### `withStyles()` -`tss-react` also features a [type-safe implementation](https://github.com/garronej/tss-react#withstyles) of [v4's `withStyles()`](https://v4.mui.com/styles/api/#withstyles-styles-options-higher-order-component). +`tss-react` also features a [type-safe implementation](https://docs.tss-react.dev/page-1/withstyles) of [v4's `withStyles()`](https://v4.mui.com/styles/api/#withstyles-styles-options-higher-order-component). :::info **Note:** The equivalent of the `$` syntax is also supported in tss's `withStyles()`. -[See doc](https://github.com/garronej/tss-react#nested-selector-with-the-withstyles-api). +[See doc](https://docs.tss-react.dev/nested-selectors#withstyles). ::: ```diff @@ -3131,40 +3169,6 @@ The key advantage of `cx` is that it detects emotion generated class names ensur export default MyCustomButton; ``` -#### Overriding styles - `classes` prop - -[Documentation of the feature in v4](https://v4.mui.com/styles/advanced/#makestyles) - [Equivalent in `tss-react`](https://docs.tss-react.dev/your-own-classes-prop) - -```diff --import { makeStyles } from '@material-ui/core/styles'; -+import { makeStyles } from 'tss-react/mui'; - --const useStyles = makeStyles({ -+const useStyles = makeStyles()({ - root: {}, // a style rule - label: {}, // a nested style rule -}); - -function Nested(props) { -- const classes = useStyles(props); -+ const { classes } = useStyles(undefined, { props }); -//NOTE: Only the classes will be read from props, you could write { props: { classes: props.classes } } -//Example with types: https://docs.tss-react.dev/your-own-classes-prop - - return ( - - ); -} - -function Parent() { - return -} -``` - #### Theme style overrides [Global theme overrides](https://v4.mui.com/customization/components/#global-theme-override) is supported out of the box by TSS. diff --git a/packages/mui-codemod/README.md b/packages/mui-codemod/README.md index d8134f04822106..9b685b4b35466e 100644 --- a/packages/mui-codemod/README.md +++ b/packages/mui-codemod/README.md @@ -605,6 +605,88 @@ You can find more details about this breaking change in [the migration guide](ht > **Note:** This approach converts the first element in the return statement into styled component but also increases CSS specificity to override nested children. > This codemod should be adopted after handling all breaking changes, [check out the migration documentation](https://mui.com/material-ui/guides/migration-v4/) +#### `jss-to-tss-react` + +Migrate JSS styling with `makeStyles` or `withStyles` to the corresponding `tss-react` API. + +```diff +-import clsx from 'clsx'; +-import {makeStyles, createStyles} from '@material-ui/core/styles'; ++import { makeStyles } from 'tss-react/mui'; + +-const useStyles = makeStyles((theme) => createStyles< +- 'root' | 'small' | 'child', {color: 'primary' | 'secondary', padding: number} +-> +-({ +- root: ({color, padding}) => ({ ++const useStyles = makeStyles<{color: 'primary' | 'secondary', padding: number}, 'child' | 'small'>({name: 'App'})((theme, { color, padding }, classes) => ({ ++ root: { + padding: padding, +- '&:hover $child': { ++ [`&:hover .${classes.child}`]: { + backgroundColor: theme.palette[color].main, + } +- }), ++ }, + small: {}, + child: { + border: '1px solid black', + height: 50, +- '&$small': { ++ [`&.${classes.small}`]: { + height: 30 + } + } +-}), {name: 'App'}); ++})); + + function App({classes: classesProp}: {classes?: any}) { +- const classes = useStyles({color: 'primary', padding: 30, classes: classesProp}); ++ const { classes, cx } = useStyles({ ++ color: 'primary', ++ padding: 30 ++ }, { ++ props: { ++ classes: classesProp ++ } ++ }); + + return ( +
+
+ The Background take the primary theme color when the mouse hovers the parent. +
+-
++
+ The Background take the primary theme color when the mouse hovers the parent. + I am smaller than the other child. +
+
+ ); +} + +export default App; +``` + +```sh +npx @mui/codemod v5.0.0/jss-to-tss-react +``` + +The following scenarios are not currently handled by this codemod and will be marked with a +"TODO jss-to-tss-react codemod" comment: + +- If the hook returned by `makeStyles` (e.g. `useStyles`) is exported and used in another file, + the usages in other files will not be converted. +- Arrow functions as the value for a CSS prop will not be converted. Arrow functions **are** + supported at the rule level, though with some caveats listed below. +- In order for arrow functions at the rule level to be converted, the parameter must use object + destructuring (e.g. `root: ({color, padding}) => (...)`). If the parameter is not destructured + (e.g. `root: (props) => (...)`), it will not be converted. +- If an arrow function at the rule level contains a code block (i.e. contains an explicit `return` + statement) rather than just an object expression, it will not be converted. + +You can find more details about migrating from JSS to tss-react in [the migration guide](https://mui.com/guides/migration-v4/#2-use-tss-react). + #### `link-underline-hover` Apply `underline="hover"` to `` that does not define `underline` prop (to get the same behavior as in v4). diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.js new file mode 100644 index 00000000000000..5e109e82883a13 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.js @@ -0,0 +1,452 @@ +const ruleEndRegEx = /[^a-zA-Z0-9_]+/; + +function transformNestedKeys(j, comments, propValueNode, ruleRegEx, nestedKeys) { + propValueNode.properties.forEach((prop) => { + if (prop.value?.type === 'ObjectExpression') { + if (typeof prop.key.value === 'string') { + let ruleIndex = prop.key.value.search(ruleRegEx); + let searchStartIndex = 0; + const elements = []; + const identifiers = []; + while (ruleIndex >= 0) { + const valueStartingAtRuleName = prop.key.value.substring(ruleIndex + 1); + const ruleEndIndex = valueStartingAtRuleName.search(ruleEndRegEx); + const ruleName = + ruleEndIndex >= 0 + ? prop.key.value.substring(ruleIndex + 1, ruleIndex + 1 + ruleEndIndex) + : valueStartingAtRuleName; + if (!nestedKeys.includes(ruleName)) { + nestedKeys.push(ruleName); + } + const before = prop.key.value.substring(searchStartIndex, ruleIndex); + elements.push(j.templateElement({ raw: `${before}.`, cooked: `${before}.` }, false)); + identifiers.push(j.identifier(`classes.${ruleName}`)); + searchStartIndex = ruleIndex + ruleName.length + 1; + const after = prop.key.value.substring(searchStartIndex); + ruleIndex = after.search(ruleRegEx); + if (ruleIndex >= 0) { + ruleIndex += searchStartIndex; + } else { + elements.push(j.templateElement({ raw: after, cooked: after }, false)); + } + } + if (identifiers.length > 0) { + prop.key = j.templateLiteral(elements, identifiers); + prop.computed = true; + } + } + transformNestedKeys(j, comments, prop.value, ruleRegEx, nestedKeys); + } else if (prop.value?.type === 'ArrowFunctionExpression') { + comments.push( + j.commentLine( + ' TODO jss-to-tss-react codemod: Unable to handle style definition reliably. ArrowFunctionExpression in CSS prop.', + true, + ), + ); + } + }); +} +function transformStylesExpression(j, comments, stylesExpression, nestedKeys, setStylesExpression) { + const ruleNames = []; + const paramNames = []; + let objectExpression; + if (stylesExpression.type === 'ObjectExpression') { + objectExpression = stylesExpression; + } else if (stylesExpression.type === 'ArrowFunctionExpression') { + if (stylesExpression.body.type === 'BlockStatement') { + const returnStatement = stylesExpression.body.body.find((b) => b.type === 'ReturnStatement'); + if (returnStatement.argument.type === 'ObjectExpression') { + objectExpression = returnStatement.argument; + } + } else if (stylesExpression.body.type === 'ObjectExpression') { + objectExpression = stylesExpression.body; + } + } + if (objectExpression !== undefined) { + objectExpression.properties.forEach((prop) => { + if (prop.key?.name) { + ruleNames.push(prop.key.name); + } + }); + let ruleRegExString = '('; + ruleNames.forEach((ruleName, index) => { + if (index > 0) { + ruleRegExString += '|'; + } + ruleRegExString += `\\$${ruleName}`; + }); + ruleRegExString += ')'; + const ruleRegEx = new RegExp(ruleRegExString, 'g'); + objectExpression.properties.forEach((prop) => { + if (prop.value) { + if (prop.value.type !== 'ObjectExpression') { + if ( + prop.value.type === 'ArrowFunctionExpression' && + prop.value.body.type === 'ObjectExpression' && + prop.value.params[0].type === 'ObjectPattern' + ) { + prop.value.params[0].properties.forEach((property) => { + const name = property.key.name; + if (!paramNames.includes(name)) { + paramNames.push(name); + } + }); + prop.value = prop.value.body; + } else { + let extraComment = `Unexpected value type of ${prop.value.type}.`; + if (prop.value.type === 'ArrowFunctionExpression') { + if (prop.value.body.type === 'ObjectExpression') { + let example = ''; + if (prop.value.params[0].type === 'Identifier') { + example = ' (e.g. `(props) => ({...})` instead of `({color}) => ({...})`)'; + } + extraComment = ` Arrow function has parameter type of ${prop.value.params[0].type} instead of ObjectPattern${example}.`; + } else { + extraComment = ` Arrow function has body type of ${prop.value.body.type} instead of ObjectExpression.`; + } + } + comments.push( + j.commentLine( + ` TODO jss-to-tss-react codemod: Unable to handle style definition reliably. Unsupported arrow function syntax.`, + true, + ), + ); + comments.push(j.commentLine(extraComment, true)); + return; + } + } + transformNestedKeys(j, comments, prop.value, ruleRegEx, nestedKeys); + } + }); + if (paramNames.length > 0 || nestedKeys.length > 0) { + let arrowFunction; + if (stylesExpression.type === 'ArrowFunctionExpression') { + arrowFunction = stylesExpression; + } else { + arrowFunction = j.arrowFunctionExpression([], objectExpression); + setStylesExpression(arrowFunction); + } + if (arrowFunction.params.length === 0) { + arrowFunction.params.push(j.identifier('_theme')); + } + let paramsString = '_params'; + if (paramNames.length > 0) { + paramsString = `{ ${paramNames.join(', ')} }`; + } + arrowFunction.params.push(j.identifier(paramsString)); + if (nestedKeys.length > 0) { + arrowFunction.params.push(j.identifier('classes')); + } + if (arrowFunction.body.type === 'ObjectExpression') { + // In some cases, some needed parentheses were being lost without this. + arrowFunction.body = j.parenthesizedExpression(objectExpression); + } + } + } +} + +function addCommentsToDeclaration(declaration, commentsToAdd) { + let commentsPath = declaration; + if (declaration.parentPath.node.type === 'ExportNamedDeclaration') { + commentsPath = declaration.parentPath; + } + if (!commentsPath.node.comments) { + commentsPath.node.comments = []; + } + commentsPath.node.comments.push(...commentsToAdd); +} +function addComments(j, path, commentsToAdd) { + j(path) + .closest(j.VariableDeclaration) + .forEach((declaration) => { + addCommentsToDeclaration(declaration, commentsToAdd); + }); +} + +/** + * @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 || { quote: 'single' }; + + let importsChanged = false; + let foundCreateStyles = false; + let foundMakeStyles = false; + let foundWithStyles = false; + /** + * transform imports + */ + root.find(j.ImportDeclaration).forEach((path) => { + const importSource = path.node.source.value; + if ( + importSource === '@material-ui/core/styles' || + importSource === '@material-ui/core' || + importSource === '@mui/styles' + ) { + const specifiersToMove = []; + const specifiersToStay = []; + path.node.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportSpecifier') { + if (specifier.imported.name === 'makeStyles') { + foundMakeStyles = true; + specifiersToMove.push(specifier); + } else if (specifier.imported.name === 'withStyles') { + foundWithStyles = true; + specifiersToMove.push(specifier); + } else if (specifier.imported.name === 'createStyles') { + foundCreateStyles = true; + } else { + specifiersToStay.push(specifier); + } + } + }); + + if (specifiersToMove.length > 0) { + path.replace( + j.importDeclaration(specifiersToMove, j.stringLiteral('tss-react/mui')), + specifiersToStay.length > 0 + ? j.importDeclaration(specifiersToStay, j.stringLiteral(importSource)) + : undefined, + ); + importsChanged = true; + } + } else if (importSource === '@material-ui/styles/makeStyles') { + foundMakeStyles = true; + path.replace( + j.importDeclaration( + [j.importSpecifier(j.identifier('makeStyles'))], + j.stringLiteral('tss-react/mui'), + ), + ); + importsChanged = true; + } else if (importSource === '@material-ui/styles/withStyles') { + foundWithStyles = true; + path.replace( + j.importDeclaration( + [j.importSpecifier(j.identifier('withStyles'))], + j.stringLiteral('tss-react/mui'), + ), + ); + importsChanged = true; + } + }); + if (!importsChanged) { + return file.source; + } + const isTypeScript = file.path.endsWith('.tsx') || file.path.endsWith('.ts'); + if (foundMakeStyles) { + let clsxOrClassnamesName = null; + root.find(j.ImportDeclaration).forEach((path) => { + const importSource = path.node.source.value; + if (importSource === 'clsx' || importSource === 'classnames') { + path.node.specifiers.forEach((specifier) => { + if (specifier.type === 'ImportDefaultSpecifier') { + clsxOrClassnamesName = specifier.local.name; + } + }); + j(path).remove(); + } + }); + /** + * Convert makeStyles syntax + */ + const styleHooks = []; + root + .find(j.CallExpression, { callee: { name: 'makeStyles' } }) + .forEach((path) => { + let paramsTypes = null; + if (foundCreateStyles) { + j(path) + .find(j.CallExpression, { callee: { name: 'createStyles' } }) + .replaceWith((createStylesPath) => { + if ( + isTypeScript && + createStylesPath.node.typeParameters && + createStylesPath.node.typeParameters.params.length > 1 + ) { + paramsTypes = createStylesPath.node.typeParameters.params[1]; + } + return createStylesPath.node.arguments[0]; + }); + } + const nestedKeys = []; + let makeStylesOptions = null; + if (path.node.arguments.length > 1) { + makeStylesOptions = path.node.arguments[1]; + } + let stylesExpression = path.node.arguments[0]; + const commentsToAdd = []; + transformStylesExpression( + j, + commentsToAdd, + path.node.arguments[0], + nestedKeys, + (newStylesExpression) => { + stylesExpression = newStylesExpression; + }, + ); + addComments(j, path, commentsToAdd); + let makeStylesIdentifier = 'makeStyles'; + if (isTypeScript && (nestedKeys.length > 0 || paramsTypes !== null)) { + let paramsTypeString = 'void'; + if (paramsTypes !== null) { + paramsTypeString = j(paramsTypes).toSource(printOptions); + } + let nestedKeysString = ''; + if (nestedKeys.length > 0) { + const nestedKeysUnion = nestedKeys.join("' | '"); + nestedKeysString = `, '${nestedKeysUnion}'`; + } + makeStylesIdentifier += `<${paramsTypeString}${nestedKeysString}>`; + } + j(path).replaceWith( + j.callExpression( + j.callExpression( + j.identifier(makeStylesIdentifier), + makeStylesOptions === null ? [] : [makeStylesOptions], + ), + [stylesExpression], + ), + ); + }) + .closest(j.VariableDeclarator) + .forEach((path) => { + styleHooks.push(path.node.id.name); + j(path) + .closest(j.ExportNamedDeclaration) + .forEach(() => { + const comments = [ + j.commentLine( + ` TODO jss-to-tss-react codemod: usages of this hook outside of this file will not be converted.`, + true, + ), + ]; + addComments(j, path, comments); + }); + }); + /** + * Convert classes assignment syntax in calls to the hook (e.g. useStyles) and + * convert usages of clsx or classnames to cx. + */ + styleHooks.forEach((hookName) => { + root + .find(j.CallExpression, { callee: { name: hookName } }) + .forEach((hookCall) => { + if (hookCall.node.arguments.length === 1) { + const hookArg = hookCall.node.arguments[0]; + if (hookArg.type === 'Identifier') { + const secondArg = j.objectExpression([]); + secondArg.properties.push( + j.objectProperty(j.identifier('props'), j.identifier(hookArg.name)), + ); + hookCall.node.arguments.push(secondArg); + } else if (hookArg.properties) { + const hookArgPropsMinusClasses = []; + let classesProp = null; + hookArg.properties.forEach((hookProp) => { + if (hookProp.key.name === 'classes') { + classesProp = hookProp; + } else { + hookArgPropsMinusClasses.push(hookProp); + } + }); + if (classesProp !== null) { + if (hookArgPropsMinusClasses.length === 0) { + hookCall.node.arguments[0] = j.identifier('undefined'); + } else { + hookArg.properties = hookArgPropsMinusClasses; + } + const secondArg = j.objectExpression([]); + secondArg.properties.push( + j.objectProperty( + j.identifier('props'), + j.objectExpression([ + j.objectProperty(j.identifier('classes'), classesProp.value), + ]), + ), + ); + hookCall.node.arguments.push(secondArg); + } + } + } + }) + .closest(j.VariableDeclarator) + .forEach((path) => { + let foundClsxOrClassnamesUsage = false; + const classesName = path.node.id.name; + const classesAssign = classesName === 'classes' ? 'classes' : `classes: ${classesName}`; + if (clsxOrClassnamesName !== null) { + j(path) + .closestScope() + .find(j.CallExpression, { callee: { name: clsxOrClassnamesName } }) + .forEach((callPath) => { + callPath.node.callee.name = 'cx'; + foundClsxOrClassnamesUsage = true; + }); + } + if (foundClsxOrClassnamesUsage) { + path.node.id.name = `{ ${classesAssign}, cx }`; + } else { + path.node.id.name = `{ ${classesAssign} }`; + } + }); + root.find(j.ExportDefaultDeclaration, { declaration: { name: hookName } }).forEach((path) => { + const comments = [ + j.commentLine( + ` TODO jss-to-tss-react codemod: usages of this hook outside of this file will not be converted.`, + true, + ), + ]; + addCommentsToDeclaration(path, comments); + }); + }); + } + if (foundWithStyles) { + /** + * Convert withStyles syntax + */ + const styleVariables = []; + root + .find(j.CallExpression, { + callee: { type: 'CallExpression', callee: { name: 'withStyles' } }, + }) + .replaceWith((path) => { + const withStylesCall = path.node.callee; + const styles = path.node.callee.arguments[0]; + if (styles.type === 'Identifier') { + styleVariables.push(styles.name); + } else { + const nestedKeys = []; + const commentsToAdd = []; + transformStylesExpression(j, commentsToAdd, styles, nestedKeys, (newStylesExpression) => { + path.node.callee.arguments[0] = newStylesExpression; + }); + addComments(j, path, commentsToAdd); + } + const component = path.node.arguments[0]; + withStylesCall.arguments.unshift(component); + return withStylesCall; + }); + styleVariables.forEach((styleVar) => { + root.find(j.VariableDeclarator, { id: { name: styleVar } }).forEach((path) => { + const nestedKeys = []; + const commentsToAdd = []; + transformStylesExpression( + j, + commentsToAdd, + path.node.init, + nestedKeys, + (newStylesExpression) => { + path.node.init = newStylesExpression; + }, + ); + addComments(j, path, commentsToAdd); + }); + }); + } + return root.toSource(printOptions); +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test.js new file mode 100644 index 00000000000000..d432fafdb744da --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test.js @@ -0,0 +1,153 @@ +import path from 'path'; +import { expect } from 'chai'; +import jscodeshiftWithDefaultParser from 'jscodeshift'; +import transform from './jss-to-tss-react'; +import readFile from '../util/readFile'; + +function read(fileName) { + return readFile(path.join(__dirname, fileName)); +} +const jscodeshift = jscodeshiftWithDefaultParser.withParser('tsx'); + +describe('@mui/codemod', () => { + describe('v5.0.0', () => { + describe('jss-to-tss-react', () => { + it('transforms @material-ui/core/styles makeStyles to use tss-react', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/actual-from-material-ui-core-styles.js'), + path: require.resolve('./jss-to-tss-react.test/actual-from-material-ui-core-styles.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./jss-to-tss-react.test/expected-from-material-ui-core-styles.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + it('should be idempotent', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/expected-from-material-ui-core-styles.js'), + path: require.resolve( + './jss-to-tss-react.test/expected-from-material-ui-core-styles.js', + ), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./jss-to-tss-react.test/expected-from-material-ui-core-styles.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + it('transforms @material-ui/core makeStyles to use tss-react', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/actual-from-material-ui-core.js'), + path: require.resolve('./jss-to-tss-react.test/actual-from-material-ui-core.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./jss-to-tss-react.test/expected-from-material-ui-core.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + it('adds todo comments for scenarios that are not supported', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/actual-todo-comments.js'), + path: require.resolve('./jss-to-tss-react.test/actual-todo-comments.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./jss-to-tss-react.test/expected-todo-comments.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + it('transforms makeStyles with style rules returned by function to use tss-react', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/actual-mixins-pattern.js'), + path: require.resolve('./jss-to-tss-react.test/actual-mixins-pattern.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./jss-to-tss-react.test/expected-mixins-pattern.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + it('transforms @mui/styles makeStyles to use tss-react', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/actual-from-mui-styles.js'), + path: require.resolve('./jss-to-tss-react.test/actual-from-mui-styles.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./jss-to-tss-react.test/expected-from-mui-styles.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + it('transforms typescript makeStyles with nested selectors to use tss-react', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/actual-typescript.tsx'), + path: require.resolve('./jss-to-tss-react.test/actual-typescript.tsx'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./jss-to-tss-react.test/expected-typescript.tsx'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + it('transforms typescript makeStyles example in docs to use tss-react', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/actual-typescript-docs-example.tsx'), + path: require.resolve('./jss-to-tss-react.test/actual-typescript-docs-example.tsx'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./jss-to-tss-react.test/expected-typescript-docs-example.tsx'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + it('transforms typescript makeStyles advanced example in docs with params to use tss-react', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/actual-typescript-docs-example-params.tsx'), + path: require.resolve( + './jss-to-tss-react.test/actual-typescript-docs-example-params.tsx', + ), + }, + { jscodeshift }, + {}, + ); + + const expected = read( + './jss-to-tss-react.test/expected-typescript-docs-example-params.tsx', + ); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + it('transforms withStyles to use tss-react', () => { + const actual = transform( + { + source: read('./jss-to-tss-react.test/actual-withStyles.js'), + path: require.resolve('./jss-to-tss-react.test/actual-withStyles.js'), + }, + { jscodeshift }, + {}, + ); + + const expected = read('./jss-to-tss-react.test/expected-withStyles.js'); + expect(actual).to.equal(expected, 'The transformed version should be correct'); + }); + }); + }); +}); diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-material-ui-core-styles.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-material-ui-core-styles.js new file mode 100644 index 00000000000000..8a9501fee8f1ea --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-material-ui-core-styles.js @@ -0,0 +1,31 @@ +import React from "react"; +import { makeStyles } from "@material-ui/core/styles"; + +/* +Sandboxes for verifying correct behavior: +JSS - https://codesandbox.io/s/case1-jss-dedp2f?file=/src/App.js +TSS - https://codesandbox.io/s/case1-tss-s0z7tx?file=/src/App.js + */ + +const useStyles = makeStyles({ + test: { + backgroundColor: "purple", + color: "white" + } +}, { name: "TestName" }); + +const useStyles2 = makeStyles(() => ({ + test2: { + backgroundColor: "blue", + color: "lime" + } +})); + +function InnerComponent() { + const classes = useStyles2(); + return
Inner Test
; +} +export default function ComponentUsingStyles(props) { + const classes = useStyles(); + return
Test
; +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-material-ui-core.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-material-ui-core.js new file mode 100644 index 00000000000000..d82aa6e6e6051f --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-material-ui-core.js @@ -0,0 +1,48 @@ +import React from "react"; +import { makeStyles } from "@material-ui/core"; +import clsx from "clsx"; + +const useStyles = makeStyles(() => ({ + test: { + backgroundColor: "purple", + color: "white", + "&$qualifier": { + textDecoration: "underline" + }, + "&$qualifier$qualifier2": { + fontStyle: "italic" + }, + "&$qualifier2 .testStuffInBetween $qualifier": { + color: "brown" + }, + "&$qualifier:hover": { + backgroundColor: "red" + }, + "&$qualifier2:not(:hover)": { + fontWeight: 700 + }, + }, + qualifier: {}, + qualifier2: {} +})); + +const useStyles2 = makeStyles({ + test2: { + backgroundColor: "blue", + color: "lime" + } +}); + +function InnerComponent() { + const classes = useStyles2(); + return
Inner Test
; +} +export default function ComponentUsingStyles(props) { + const classes = useStyles(props); + return <> +
Test
+
Qualifier Test
+
Qualifier 2 Test
+
Qualifier & Qualifier 2 Test
+ ; +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-mui-styles.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-mui-styles.js new file mode 100644 index 00000000000000..c38e7b8f810959 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-from-mui-styles.js @@ -0,0 +1,33 @@ +import React from "react"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles({ + test: { + backgroundColor: "purple", + color: "white" + } +}); + +const useStyles2 = makeStyles({ + test: { + backgroundColor: "purple", + color: "white", + "& $test2": { + backgroundColor: "lime", + color: "blue" + } + }, + test2: { + backgroundColor: "blue", + color: "lime" + } +}); + +function InnerComponent() { + const classes = useStyles2(); + return
Inner Test
; +} +export default function ComponentUsingStyles(props) { + const classes = useStyles(); + return
Test
; +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-mixins-pattern.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-mixins-pattern.js new file mode 100644 index 00000000000000..2384a5eedfcf45 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-mixins-pattern.js @@ -0,0 +1,35 @@ +import React from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import clsx from "clsx"; + +function mixins() { + return { + test: { + backgroundColor: "purple", + color: "white" + } + }; +} +function cssProps() { + return { + paddingLeft: "8px" + } +} + +const useStyles = makeStyles(theme => { + return mixins(); +}); + +const useStyles2 = makeStyles(theme => ({ + ...mixins(), + test2: { + color: "red", + ...cssProps() + } +})); + +export default function ComponentUsingStyles(props) { + const classes = useStyles(); + const classes2 = useStyles2(); + return
Test
; +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-todo-comments.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-todo-comments.js new file mode 100644 index 00000000000000..e2f3cb9c082e08 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-todo-comments.js @@ -0,0 +1,51 @@ +import React from "react"; +import { makeStyles } from "@material-ui/core"; +import clsx from "clsx"; + +const useStyles = makeStyles(() => ({ + test: { + backgroundColor: "purple", + color: "white", + "&$qualifier": { + textDecoration: props => (props.textDecoration) + }, + }, + qualifier: {}, +})); + +export const useExportedStyles = makeStyles({ + test: { + backgroundColor: "purple", + color: "white", + } +}); + +const useStyles2 = makeStyles({ + test2: props => ({ + backgroundColor: "blue", + color: "lime" + }) +}); + +const useStyles3 = makeStyles({ + test3: props => { + return { + backgroundColor: "blue", + color: "lime" + }; + } +}); + +function InnerComponent() { + const classes = useStyles2(); + return
Inner Test
; +} +function ComponentUsingStyles(props) { + const classes = useStyles(props); + return <> +
Test
+
Qualifier Test
+ ; +} + +export default useStyles3; diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript-docs-example-params.tsx b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript-docs-example-params.tsx new file mode 100644 index 00000000000000..9b21b8c13d93b9 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript-docs-example-params.tsx @@ -0,0 +1,40 @@ +import clsx from 'clsx'; +import {makeStyles, createStyles} from '@material-ui/core/styles'; + +const useStyles = makeStyles((theme) => createStyles< + 'root' | 'small' | 'child', {color: 'primary' | 'secondary', padding: number} +> +({ + root: ({color, padding}) => ({ + padding: padding, + '&:hover $child': { + backgroundColor: theme.palette[color].main, + } + }), + small: {}, + child: { + border: '1px solid black', + height: 50, + '&$small': { + height: 30 + } + } +}), {name: 'App'}); + +function App({classes: classesProp}: {classes?: any}) { + const classes = useStyles({color: 'primary', padding: 30, classes: classesProp}); + + return ( +
+
+ The Background take the primary theme color when the mouse hovers the parent. +
+
+ The Background take the primary theme color when the mouse hovers the parent. + I am smaller than the other child. +
+
+ ); +} + +export default App; diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript-docs-example.tsx b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript-docs-example.tsx new file mode 100644 index 00000000000000..feda7313a3dd2b --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript-docs-example.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; + +const useStyles = makeStyles((theme) => ({ + parent: { + padding: 30, + '&:hover $child': { + backgroundColor: 'red', + }, + }, + small: {}, + child: { + backgroundColor: 'blue', + height: 50, + '&$small': { + backgroundColor: 'lightblue', + height: 30 + } + }, +})); + +function App() { + const classes = useStyles(); + return ( +
+
+ Background turns red when the mouse hovers over the parent. +
+
+ Background turns red when the mouse hovers over the parent. + I am smaller than the other child. +
+
+ ); +} + +export default App; diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript.tsx b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript.tsx new file mode 100644 index 00000000000000..1748cd6b6d419f --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-typescript.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { makeStyles, createStyles, Theme } from "@material-ui/core/styles"; + +/* +Sandboxes for verifying correct behavior: +JSS - https://codesandbox.io/s/typescript-case-bt065c?file=/demo.tsx +TSS - https://codesandbox.io/s/typescript-case-7jwpms?file=/demo.tsx + */ + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + test: { + backgroundColor: "purple", + color: "white", + "& $test2": { + backgroundColor: "lime", + color: "blue" + } + }, + test2: { + backgroundColor: "blue", + color: "lime" + } + }) +); +export function MergeClassesNoParams({ classes: classesProp } : { classes?: any }) { + const classes = useStyles({classes: classesProp}); + return
Test useStyles without params but with classes prop
; +} + +function InnerComponent({ classes } : { classes: any }) { + return
Inner Test2
; +} +export default function ComponentUsingStyles() { + const classes = useStyles(); + return ( + <> +
+ Test + +
+
Outer Test2
+ + ); +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-withStyles.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-withStyles.js new file mode 100644 index 00000000000000..cf4528192fb6a7 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/actual-withStyles.js @@ -0,0 +1,82 @@ +import React from "react"; +import { withStyles } from "@material-ui/core"; +import clsx from "clsx"; + +const styles1 = { + test: { + backgroundColor: "purple", + color: "white" + } +}; + +function Comp1({ classes }) { + return
Comp1
; +} + +const StyledComp1 = withStyles(styles1)(Comp1); + +const styles2 = { + test: { + backgroundColor: "black", + color: "lime" + }, + test2: { + backgroundColor: "white", + color: "purple", + "&$test": { + backgroundColor: "pink", + color: "blue" + } + } +}; + +function Comp2({ classes }) { + return ( + <> +
Comp2 test
+ +
Comp2 test2
+
+ Comp2 test and test2 +
+ + ); +} + +const StyledComp2 = withStyles(styles2)(Comp2); + +function Comp3({ classes }) { + return
Inline Styles +
Nested Inline Styles
+
; +} +const StyledComp3a = withStyles({test: {backgroundColor: "yellow"}})(Comp3); +const StyledComp3b = withStyles({test: {backgroundColor: "yellow", color: "lime", "& $test2": {backgroundColor: "orange"}}, test2: {}})(Comp3); + +const styles3c = (theme) => { + const bgColor1 = theme.palette.primary.main; + const color1 = theme.palette.primary.contrastText; + const bgColor2 = theme.palette.secondary.main; + const color2 = theme.palette.secondary.contrastText; + return { + test: { + backgroundColor: bgColor1, + color: color1, + "& $test2": { + backgroundColor: bgColor2, + color: color2 + } + }, + test2: {} + }; +}; +const StyledComp3c = withStyles(styles3c)(Comp3); + +export default function App() { + return <> + + + + + ; +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-material-ui-core-styles.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-material-ui-core-styles.js new file mode 100644 index 00000000000000..3dde6ce7518223 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-material-ui-core-styles.js @@ -0,0 +1,31 @@ +import React from "react"; +import { makeStyles } from 'tss-react/mui'; + +/* +Sandboxes for verifying correct behavior: +JSS - https://codesandbox.io/s/case1-jss-dedp2f?file=/src/App.js +TSS - https://codesandbox.io/s/case1-tss-s0z7tx?file=/src/App.js + */ + +const useStyles = makeStyles({ name: "TestName" })({ + test: { + backgroundColor: "purple", + color: "white" + } +}); + +const useStyles2 = makeStyles()(() => ({ + test2: { + backgroundColor: "blue", + color: "lime" + } +})); + +function InnerComponent() { + const { classes } = useStyles2(); + return
Inner Test
; +} +export default function ComponentUsingStyles(props) { + const { classes } = useStyles(); + return
Test
; +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-material-ui-core.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-material-ui-core.js new file mode 100644 index 00000000000000..1c5fee4c7bc5fd --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-material-ui-core.js @@ -0,0 +1,49 @@ +import React from "react"; +import { makeStyles } from 'tss-react/mui'; + +const useStyles = makeStyles()((_theme, _params, classes) => ({ + test: { + backgroundColor: "purple", + color: "white", + [`&.${classes.qualifier}`]: { + textDecoration: "underline" + }, + [`&.${classes.qualifier}.${classes.qualifier2}`]: { + fontStyle: "italic" + }, + [`&.${classes.qualifier2} .testStuffInBetween .${classes.qualifier}`]: { + color: "brown" + }, + [`&.${classes.qualifier}:hover`]: { + backgroundColor: "red" + }, + [`&.${classes.qualifier2}:not(:hover)`]: { + fontWeight: 700 + }, + }, + qualifier: {}, + qualifier2: {} +})); + +const useStyles2 = makeStyles()({ + test2: { + backgroundColor: "blue", + color: "lime" + } +}); + +function InnerComponent() { + const { classes } = useStyles2(); + return
Inner Test
; +} +export default function ComponentUsingStyles(props) { + const { classes, cx } = useStyles(props, { + props: props + }); + return <> +
Test
+
Qualifier Test
+
Qualifier 2 Test
+
Qualifier & Qualifier 2 Test
+ ; +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-mui-styles.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-mui-styles.js new file mode 100644 index 00000000000000..1908a038c32952 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-from-mui-styles.js @@ -0,0 +1,33 @@ +import React from "react"; +import { makeStyles } from 'tss-react/mui'; + +const useStyles = makeStyles()({ + test: { + backgroundColor: "purple", + color: "white" + } +}); + +const useStyles2 = makeStyles()((_theme, _params, classes) => ({ + test: { + backgroundColor: "purple", + color: "white", + [`& .${classes.test2}`]: { + backgroundColor: "lime", + color: "blue" + } + }, + test2: { + backgroundColor: "blue", + color: "lime" + } +})); + +function InnerComponent() { + const { classes } = useStyles2(); + return
Inner Test
; +} +export default function ComponentUsingStyles(props) { + const { classes } = useStyles(); + return
Test
; +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-mixins-pattern.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-mixins-pattern.js new file mode 100644 index 00000000000000..9751979fed28fa --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-mixins-pattern.js @@ -0,0 +1,34 @@ +import React from "react"; +import { makeStyles } from 'tss-react/mui'; + +function mixins() { + return { + test: { + backgroundColor: "purple", + color: "white" + } + }; +} +function cssProps() { + return { + paddingLeft: "8px" + } +} + +const useStyles = makeStyles()(theme => { + return mixins(); +}); + +const useStyles2 = makeStyles()(theme => ({ + ...mixins(), + test2: { + color: "red", + ...cssProps() + } +})); + +export default function ComponentUsingStyles(props) { + const { classes, cx } = useStyles(); + const { classes: classes2 } = useStyles2(); + return
Test
; +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-todo-comments.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-todo-comments.js new file mode 100644 index 00000000000000..9503903a8c1d78 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-todo-comments.js @@ -0,0 +1,59 @@ +import React from "react"; +import { makeStyles } from 'tss-react/mui'; + +// TODO jss-to-tss-react codemod: Unable to handle style definition reliably. ArrowFunctionExpression in CSS prop. +const useStyles = makeStyles()((_theme, _params, classes) => ({ + test: { + backgroundColor: "purple", + color: "white", + [`&.${classes.qualifier}`]: { + textDecoration: props => (props.textDecoration) + }, + }, + qualifier: {}, +})); + +// TODO jss-to-tss-react codemod: usages of this hook outside of this file will not be converted. +export const useExportedStyles = makeStyles()({ + test: { + backgroundColor: "purple", + color: "white", + } +}); + +// TODO jss-to-tss-react codemod: Unable to handle style definition reliably. Unsupported arrow function syntax. +// Arrow function has parameter type of Identifier instead of ObjectPattern (e.g. `(props) => ({...})` instead of `({color}) => ({...})`). +const useStyles2 = makeStyles()({ + test2: props => ({ + backgroundColor: "blue", + color: "lime" + }) +}); + +// TODO jss-to-tss-react codemod: Unable to handle style definition reliably. Unsupported arrow function syntax. +// Arrow function has body type of BlockStatement instead of ObjectExpression. +const useStyles3 = makeStyles()({ + test3: props => { + return { + backgroundColor: "blue", + color: "lime" + }; + } +}); + +function InnerComponent() { + const { classes } = useStyles2(); + return
Inner Test
; +} +function ComponentUsingStyles(props) { + const { classes, cx } = useStyles(props, { + props: props + }); + return <> +
Test
+
Qualifier Test
+ ; +} + +// TODO jss-to-tss-react codemod: usages of this hook outside of this file will not be converted. +export default useStyles3; diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript-docs-example-params.tsx b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript-docs-example-params.tsx new file mode 100644 index 00000000000000..11951ce8c1a7c0 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript-docs-example-params.tsx @@ -0,0 +1,43 @@ +import { makeStyles } from 'tss-react/mui'; + +const useStyles = makeStyles<{color: 'primary' | 'secondary', padding: number}, 'child' | 'small'>({name: 'App'})((theme, { color, padding }, classes) => ({ + root: { + padding: padding, + [`&:hover .${classes.child}`]: { + backgroundColor: theme.palette[color].main, + } + }, + small: {}, + child: { + border: '1px solid black', + height: 50, + [`&.${classes.small}`]: { + height: 30 + } + } +})); + +function App({classes: classesProp}: {classes?: any}) { + const { classes, cx } = useStyles({ + color: 'primary', + padding: 30 + }, { + props: { + classes: classesProp + } + }); + + return ( +
+
+ The Background take the primary theme color when the mouse hovers the parent. +
+
+ The Background take the primary theme color when the mouse hovers the parent. + I am smaller than the other child. +
+
+ ); +} + +export default App; diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript-docs-example.tsx b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript-docs-example.tsx new file mode 100644 index 00000000000000..b906d0c65b7665 --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript-docs-example.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { makeStyles } from 'tss-react/mui'; + +const useStyles = makeStyles()((theme, _params, classes) => ({ + parent: { + padding: 30, + [`&:hover .${classes.child}`]: { + backgroundColor: 'red', + }, + }, + small: {}, + child: { + backgroundColor: 'blue', + height: 50, + [`&.${classes.small}`]: { + backgroundColor: 'lightblue', + height: 30 + } + }, +})); + +function App() { + const { classes, cx } = useStyles(); + return ( +
+
+ Background turns red when the mouse hovers over the parent. +
+
+ Background turns red when the mouse hovers over the parent. + I am smaller than the other child. +
+
+ ); +} + +export default App; diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript.tsx b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript.tsx new file mode 100644 index 00000000000000..bd296d2afac08c --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-typescript.tsx @@ -0,0 +1,48 @@ +import * as React from "react"; +import { makeStyles } from 'tss-react/mui'; +import { Theme } from '@material-ui/core/styles'; + +/* +Sandboxes for verifying correct behavior: +JSS - https://codesandbox.io/s/typescript-case-bt065c?file=/demo.tsx +TSS - https://codesandbox.io/s/typescript-case-7jwpms?file=/demo.tsx + */ + +const useStyles = makeStyles()((theme: Theme, _params, classes) => ({ + test: { + backgroundColor: "purple", + color: "white", + [`& .${classes.test2}`]: { + backgroundColor: "lime", + color: "blue" + } + }, + test2: { + backgroundColor: "blue", + color: "lime" + } +})); +export function MergeClassesNoParams({ classes: classesProp } : { classes?: any }) { + const { classes } = useStyles(undefined, { + props: { + classes: classesProp + } + }); + return
Test useStyles without params but with classes prop
; +} + +function InnerComponent({ classes } : { classes: any }) { + return
Inner Test2
; +} +export default function ComponentUsingStyles() { + const { classes } = useStyles(); + return ( + <> +
+ Test + +
+
Outer Test2
+ + ); +} diff --git a/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-withStyles.js b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-withStyles.js new file mode 100644 index 00000000000000..5e9320675c821b --- /dev/null +++ b/packages/mui-codemod/src/v5.0.0/jss-to-tss-react.test/expected-withStyles.js @@ -0,0 +1,85 @@ +import React from "react"; +import { withStyles } from 'tss-react/mui'; +import clsx from "clsx"; + +const styles1 = { + test: { + backgroundColor: "purple", + color: "white" + } +}; + +function Comp1({ classes }) { + return
Comp1
; +} + +const StyledComp1 = withStyles(Comp1, styles1); + +const styles2 = (_theme, _params, classes) => ({ + test: { + backgroundColor: "black", + color: "lime" + }, + test2: { + backgroundColor: "white", + color: "purple", + [`&.${classes.test}`]: { + backgroundColor: "pink", + color: "blue" + } + } +}); + +function Comp2({ classes }) { + return ( + <> +
Comp2 test
+ +
Comp2 test2
+
+ Comp2 test and test2 +
+ + ); +} + +const StyledComp2 = withStyles(Comp2, styles2); + +function Comp3({ classes }) { + return
Inline Styles +
Nested Inline Styles
+
; +} +const StyledComp3a = withStyles(Comp3, {test: {backgroundColor: "yellow"}}); +const StyledComp3b = withStyles( + Comp3, + (_theme, _params, classes) => ({test: {backgroundColor: "yellow", color: "lime", [`& .${classes.test2}`]: {backgroundColor: "orange"}}, test2: {}}) +); + +const styles3c = (theme, _params, classes) => { + const bgColor1 = theme.palette.primary.main; + const color1 = theme.palette.primary.contrastText; + const bgColor2 = theme.palette.secondary.main; + const color2 = theme.palette.secondary.contrastText; + return { + test: { + backgroundColor: bgColor1, + color: color1, + [`& .${classes.test2}`]: { + backgroundColor: bgColor2, + color: color2 + } + }, + test2: {} + }; +}; +const StyledComp3c = withStyles(Comp3, styles3c); + +export default function App() { + return <> + + + + + ; +}