UNPKG

@mui/codemod

Version:
545 lines (528 loc) 20.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = transformer; const nodePath = require('path'); /** * @param {import('jscodeshift').FileInfo} file * @param {import('jscodeshift').API} api */ function transformer(file, api, options) { const j = api.jscodeshift; const root = j(file.source); /** * @param {string} filePath * @example computePrefixFromPath('/a/b/c/Anonymous.tsx') === 'Anonymous' * @example computePrefixFromPath('/a/b/c/Anonymous.server.tsx') === 'Anonymous' */ function computePrefixFromPath(filePath) { return nodePath.basename(filePath, nodePath.extname(filePath)).split('.')[0]; } /** * * @param {import('jscodeshift').CallExpression} withStylesCall */ function getPrefix(withStylesCall) { let prefix; // 1. check from withStylesFn if (withStylesCall && withStylesCall.arguments[1] && withStylesCall.arguments[1].properties) { const name = withStylesCall.arguments[1].properties.find(prop => prop.key.name === 'name'); prefix = name.value.value; } if (!prefix) { // 2. check name from export default root.find(j.ExportDefaultDeclaration).forEach(path => { prefix = path.node.declaration.name; }); } if (!prefix) { // 3. use name export that is Capitalize root.find(j.ExportNamedDeclaration).forEach(path => { if (path.node.declaration.type !== 'VariableDeclaration') { return; } const name = path.node.declaration.declarations[0].id.name; if (!prefix && name.match(/^[A-Z]/)) { prefix = name; } }); } if (!prefix) { prefix = computePrefixFromPath(file.path); } return prefix; } function getFirstJsxName() { const matches = file.source.match(/<\/?(\w*)[\s\S]*?>/gm); if (matches) { const closingTag = matches.slice(-1)[0]; // Self closing tag if (closingTag.endsWith('/>') && closingTag !== '</>') { const end = closingTag.indexOf(' ') > 0 ? closingTag.indexOf(' ') : closingTag.length - 1; return closingTag.substring(1, end); } return closingTag.substring(2, closingTag.length - 1); } return null; } function getRootClassKeys() { const name = getFirstJsxName(); if (name) { const rootClassKeys = []; root.findJSXElements(name).at(0).forEach(path => { const existingClassName = path.node.openingElement.attributes.find(attr => attr.name && attr.name.name === 'className'); if (existingClassName) { if (existingClassName.value.type === 'StringLiteral') { // className="string" } if (existingClassName.value.type === 'JSXExpressionContainer') { if (existingClassName.value.expression.type === 'StringLiteral') { // className={'string'} } if (existingClassName.value.expression.type === 'MemberExpression') { // className={classes.root} if (existingClassName.value.expression.object.name === 'classes') { rootClassKeys.push(existingClassName.value.expression.property.name); } } if (existingClassName.value.expression.type === 'CallExpression') { // className={clsx(classes.root)} existingClassName.value.expression.arguments.forEach(arg => { if (arg.type === 'MemberExpression') { if (arg.object.name === 'classes') { rootClassKeys.push(arg.property.name); } } if (arg.type === 'ObjectExpression') { arg.properties.forEach(prop => { if (prop.key.object && prop.key.object.name === 'classes') { rootClassKeys.push(prop.key.property.name); } }); } }); } } } }); return rootClassKeys; } return []; } function isTagNameFragment(tagName) { return tagName === 'React.Fragment' || tagName === 'Fragment' || tagName === ''; } function isTagNameSuspense(tagName) { return tagName === 'React.Suspense' || tagName === 'Suspense'; } function createStyledComponent(componentName, styledComponentName, stylesFn) { let styleArg = null; const rootIsFragment = isTagNameFragment(componentName); if (rootIsFragment) { // The root is React.Fragment styleArg = j.stringLiteral('div'); } else if (componentName.match(/^[A-Z]/)) { // The root is a component styleArg = j.identifier(componentName); } else { styleArg = j.stringLiteral(componentName); } const declaration = j.variableDeclaration('const', [j.variableDeclarator(j.identifier(styledComponentName), j.callExpression(j.callExpression(j.identifier('styled'), [styleArg]), [stylesFn]))]); if (rootIsFragment) { declaration.comments = [j.commentLine(' TODO jss-to-styled codemod: The Fragment root was replaced by div. Change the tag if needed.')]; } return declaration; } const classesCount = {}; /** * * @param {import('jscodeshift').ObjectExpression} objExpression * @param {import('jscodeshift').ObjectExpression} prevObj */ function createClasses(objExpression, prevObj) { const classes = prevObj || j.objectExpression([]); objExpression.properties.forEach(prop => { if (!classesCount[prop.key.name]) { classesCount[prop.key.name] = 1; } else { classesCount[prop.key.name] += 1; } const resolvedKey = classesCount[prop.key.name] === 1 ? prop.key.name : `${prop.key.name}${classesCount[prop.key.name]}`; classes.properties.push(j.objectProperty(j.identifier(resolvedKey), j.templateLiteral([j.templateElement({ raw: '', cooked: '' }, false), j.templateElement({ raw: `-${resolvedKey}`, cooked: `-${resolvedKey}` }, true)], [j.identifier('PREFIX')]))); }); return classes; } /** * * @param {import('jscodeshift').ArrowFunctionExpression | import('jscodeshift').FunctionDeclaration} functionExpression */ function getReturnStatement(functionExpression) { if (functionExpression.type === 'ObjectExpression') { return functionExpression; } if (functionExpression.type === 'ArrowFunctionExpression') { if (functionExpression.body.type === 'BlockStatement') { const returnStatement = functionExpression.body.body.find(b => b.type === 'ReturnStatement'); return returnStatement.argument; } if (functionExpression.body.type === 'ObjectExpression') { return functionExpression.body; } if (functionExpression.body.type === 'CallExpression') { if (functionExpression.body.callee.name === 'createStyles') { return functionExpression.body.arguments[0]; } } } if (functionExpression.type === 'FunctionDeclaration') { const returnStatement = functionExpression.body.body.find(b => b.type === 'ReturnStatement'); return returnStatement.argument; } if (functionExpression.type === 'CallExpression') { if (functionExpression.callee.name === 'createStyles') { return functionExpression.arguments[0]; } } return null; } /** * @param {import('jscodeshift').ObjectExpression | import('jscodeshift').ArrowFunctionExpression | import('jscodeshift').FunctionDeclaration} expression */ function getObjectExpression(expression) { let objectExpression; if (expression.type === 'ObjectExpression') { objectExpression = expression; } if (expression.type === 'ArrowFunctionExpression') { if (expression.body.type === 'BlockStatement') { const returnStatement = expression.body.body.find(b => b.type === 'ReturnStatement'); objectExpression = returnStatement.argument; } if (expression.body.type === 'ObjectExpression') { expression.body.extra.parenthesized = false; objectExpression = expression.body; } } if (expression.type === 'FunctionDeclaration') { expression.type = 'FunctionExpression'; const returnStatement = expression.body.body.find(b => b.type === 'ReturnStatement'); objectExpression = returnStatement.argument; } return objectExpression; } const stylesCount = {}; /** * * @param {import('jscodeshift').ObjectExpression | import('jscodeshift').ArrowFunctionExpression | import('jscodeshift').FunctionDeclaration} functionExpression * @param {string[]} rootKeys * @param {import('jscodeshift').ObjectExpression | import('jscodeshift').ArrowFunctionExpression | import('jscodeshift').FunctionDeclaration} prevStyleArg */ function convertToStyledArg(functionExpression, rootKeys, prevStyleArg) { 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; }); } if (functionExpression.params) { functionExpression.params = functionExpression.params.map(param => { if (param.type === 'ObjectPattern') { return j.objectPattern([j.objectProperty(j.identifier('theme'), param)]); } const prop = j.objectProperty(param, param); prop.shorthand = true; return j.objectPattern([prop]); }); } if (prevStyleArg) { const prevObjectExpression = getObjectExpression(prevStyleArg); if (objectExpression) { // merge object prevObjectExpression.properties = [...prevObjectExpression.properties, ...objectExpression.properties]; } if (functionExpression.params && prevStyleArg.type === 'ObjectExpression') { // turn prevStyleArg to ArrowFunction prevStyleArg = j.arrowFunctionExpression(functionExpression.params, prevStyleArg); } return prevStyleArg; } return functionExpression; } const printOptions = options.printOptions || { quote: 'single' }; /** * find withStyles, makeStyles arg variable * ex. withStyles(styles, options) * - styles: can be variable or () => { ... } * - options: { name } */ /** * get the styles ASP * - ArrowFunctionExpression * - FunctionDeclaration */ const withStylesCall = root.find(j.CallExpression, { callee: { name: 'withStyles' } }).nodes()[0]; const makeStylesCall = root.find(j.CallExpression, { callee: { name: 'makeStyles' } }).nodes()[0]; if (!withStylesCall && !makeStylesCall) { return file.source; } const rootJsxName = getFirstJsxName(); if (isTagNameSuspense(rootJsxName)) { return file.source; } const styledComponentName = rootJsxName.match(/^[A-Z]/) && !isTagNameFragment(rootJsxName) ? `Styled${rootJsxName}`.replace('.', '') : 'Root'; const prefix = getPrefix(withStylesCall || makeStylesCall); const rootClassKeys = getRootClassKeys(); const result = {}; const componentClassesCount = {}; const withStylesComponents = []; if (withStylesCall) { let stylesFnName; root.find(j.CallExpression, { callee: { name: 'withStyles' } }).forEach(path => { const arg = path.node.arguments[0]; if (arg.type === 'Identifier') { stylesFnName = arg.name; } const objectExpression = getReturnStatement(arg); if (objectExpression) { // do this first, because objectExpression will be mutated in `createClasses` below. if (path.parent.parent && path.parent.parent.node.id) { // save withStylesComponent name, to add classes on JSX 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))); })) }); } result.classes = createClasses(objectExpression, result.classes); result.styledArg = convertToStyledArg(arg, rootClassKeys, result.styledArg); } }); root.find(j.VariableDeclarator, { id: { name: stylesFnName } }).forEach(path => { let fnArg = path.node.init; const objectExpression = getReturnStatement(fnArg); if (fnArg.type === 'ArrowFunctionExpression') { if (fnArg.body.type === 'CallExpression') { if (fnArg.body.callee.name === 'createStyles') { fnArg.body = fnArg.body.arguments[0]; } } } if (fnArg.type === 'CallExpression') { if (fnArg.callee.name === 'createStyles') { fnArg = fnArg.arguments[0]; } } if (objectExpression) { result.classes = createClasses(objectExpression, result.classes); result.styledArg = convertToStyledArg(fnArg, rootClassKeys); } }).remove(); root.find(j.FunctionDeclaration, { id: { name: stylesFnName } }).forEach(path => { const returnStatement = path.node.body.body.find(b => b.type === 'ReturnStatement'); result.classes = createClasses(returnStatement.argument, result.classes); result.styledArg = convertToStyledArg(path.node, rootClassKeys); }).remove(); } if (makeStylesCall) { let stylesFnName; root.find(j.CallExpression, { callee: { name: 'makeStyles' } }).at(0).forEach(path => { let arg = path.node.arguments[0]; if (arg.type === 'Identifier') { stylesFnName = arg.name; } const objectExpression = getReturnStatement(arg); if (arg.type === 'ArrowFunctionExpression') { if (arg.body.type === 'CallExpression') { if (arg.body.callee.name === 'createStyles') { arg.body = arg.body.arguments[0]; } } } if (arg.type === 'CallExpression') { if (arg.callee.name === 'createStyles') { arg = arg.arguments[0]; } } if (objectExpression) { result.classes = createClasses(objectExpression, result.classes); result.styledArg = convertToStyledArg(arg, rootClassKeys); } }); root.find(j.VariableDeclarator, { id: { name: stylesFnName } }).at(0).forEach(path => { const objectExpression = getReturnStatement(path.node.init); if (objectExpression) { result.classes = createClasses(objectExpression, result.classes); result.styledArg = convertToStyledArg(path.node.init, rootClassKeys); } }).remove(); root.find(j.FunctionDeclaration, { id: { name: stylesFnName } }).at(0).forEach(path => { const returnStatement = path.node.body.body.find(b => b.type === 'ReturnStatement'); result.classes = createClasses(returnStatement.argument, result.classes); result.styledArg = convertToStyledArg(path.node, rootClassKeys); }).remove(); root.find(j.VariableDeclaration).filter(path => path.node.declarations.some(d => d.id.name === 'useStyles')).remove(); } /** * create `classes` * create styled `Root` */ root.find(j.ImportDeclaration).at(-1).forEach(path => { path.insertAfter(j.variableDeclaration('const', [j.variableDeclarator(j.identifier('PREFIX'), j.stringLiteral(prefix))]), j.variableDeclaration('const', [j.variableDeclarator(j.identifier('classes'), result.classes)]), createStyledComponent(rootJsxName, styledComponentName, result.styledArg)); }); function transformJsxRootToStyledComponent(path) { if (path.node.openingFragment) { path.node.type = 'JSXElement'; path.node.openingElement = { type: 'JSXOpeningElement', name: styledComponentName }; path.node.closingElement = { type: 'JSXClosingElement', name: styledComponentName }; } else if (path.node.openingElement && path.node.openingElement.name && path.node.openingElement.name.name === undefined) { path.node.openingElement.name = styledComponentName; if (path.node.closingElement) { path.node.closingElement.name = styledComponentName; } } else { path.node.openingElement.name.name = styledComponentName; if (path.node.closingElement) { path.node.closingElement.name.name = styledComponentName; } } } /** * apply <StyledComponent /> */ if (rootJsxName === '') { root.find(j.JSXFragment).at(0).forEach(transformJsxRootToStyledComponent); } else if (rootJsxName.indexOf('.') > 0) { let converted = false; root.find(j.JSXElement).forEach(path => { if (!converted && path.node.openingElement.name.type === 'JSXMemberExpression') { const tagName = `${path.node.openingElement.name.object.name}.${path.node.openingElement.name.property.name}`; if (tagName === rootJsxName) { converted = true; transformJsxRootToStyledComponent(path); } } }); } else { root.findJSXElements(rootJsxName).at(0).forEach(transformJsxRootToStyledComponent); } /** * Attach classes to components created by withStyles * ex. const Button1 = withStyles(...)(Button) */ withStylesComponents.forEach(data => { root.find(j.JSXOpeningElement, { name: { name: data.variableName } }).forEach(path => { if (!path.node.attributes) { path.node.attributes = []; } path.node.attributes.push(j.jsxAttribute(j.jsxIdentifier('classes'), j.jsxExpressionContainer(data.classes))); }); }); /** * import styled if not exist */ const imports = root.find(j.ImportDeclaration).filter(({ node }) => node.source.value.match(/^@material-ui\/core\/styles$/)).forEach(({ node }) => { const existed = node.specifiers.find(s => s.imported.name === 'styled'); if (!existed) { node.specifiers.push(j.importSpecifier(j.identifier('styled'))); } }); if (!imports.size()) { root.find(j.ImportDeclaration).at(0).forEach(path => path.insertAfter(j.importDeclaration([j.importSpecifier(j.identifier('styled'))], j.literal('@mui/material/styles')))); } /** * remove import */ root.find(j.ImportDeclaration).filter(path => path.node.source.value.match(/^(@material-ui|@mui)\/styles\/?(withStyles|makeStyles|createStyles)?$/)).forEach(path => { path.node.specifiers = path.node.specifiers.filter(s => s.local.name !== 'withStyles' && s.local.name !== 'makeStyles' && s.local.name !== 'createStyles'); }).filter(path => !path.node.specifiers.length).remove(); /** * remove withStyles calls that create new component */ root.find(j.CallExpression, { callee: { name: 'withStyles' } }).forEach(path => { if (path.parent.parent.parent.node.type === 'VariableDeclaration' && path.parent.parent.parent.parent.node.type !== 'ExportNamedDeclaration' && path.parent.node.arguments[0].type === 'Identifier') { path.parent.parent.node.init = j.identifier(path.parent.node.arguments[0].name); } }); return root.toSource(printOptions).replace(/withStyles\([^)]*\),?/gm, '').replace(/([^=]{.*)classes[^.],?(.*})/gm, '$1$2').replace(/^.*useStyles(.*);?/gm, ''); }