UNPKG

@mui/codemod

Version:
586 lines (571 loc) 22.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = migrateToVariants; exports.getCreateBuildStyle = exports.getBuildArrowFunctionAST = exports.getAppendPaletteModeStyles = void 0; exports.getIdentifierKey = getIdentifierKey; exports.getObjectKey = getObjectKey; exports.getObjectToArrowFunction = void 0; exports.isThemePaletteMode = isThemePaletteMode; exports.removeProperty = removeProperty; const MAX_DEPTH = 20; /** * @param {import('jscodeshift').API['j']} j * @returns */ const getCreateBuildStyle = j => function createBuildStyle(key, upperBuildStyle, applyStylesMode) { if (applyStylesMode) { upperBuildStyle = styleExpression => j.objectExpression([j.spreadElement(j.callExpression(j.memberExpression(j.identifier('theme'), j.identifier('applyStyles')), [j.stringLiteral(applyStylesMode), styleExpression]))]); } return function buildStyle(styleExpression) { if (key) { if (key.type === 'Identifier' || key.type === 'StringLiteral') { return upperBuildStyle(j.objectExpression([j.objectProperty(key, styleExpression)])); } if (key.type === 'TemplateLiteral' || key.type === 'CallExpression') { return upperBuildStyle(j.objectExpression([{ ...j.objectProperty(key, styleExpression), computed: true }])); } } return upperBuildStyle ? upperBuildStyle(styleExpression) : styleExpression; }; }; /** * @param {import('jscodeshift').API['j']} j */ exports.getCreateBuildStyle = getCreateBuildStyle; const getAppendPaletteModeStyles = j => /** * * @param {{ properties: any[] }} node * @param {Record<string, any[] | import('jscodeshift').ObjectExpression>} modeStyles */ function appendPaletteModeStyles(node, modeStyles) { Object.entries(modeStyles).forEach(([mode, objectStyles]) => { node.properties.push(j.spreadElement(j.callExpression(j.memberExpression(j.identifier('theme'), j.identifier('applyStyles')), [j.stringLiteral(mode), Array.isArray(objectStyles) ? j.objectExpression(objectStyles) : objectStyles]))); }); }; /** * * @param {import('jscodeshift').MemberExpression | import('jscodeshift').Identifier} node */ exports.getAppendPaletteModeStyles = getAppendPaletteModeStyles; function getIdentifierKey(node) { if (node.type === 'MemberExpression') { return node.property; } return node; } /** * * @param {import('jscodeshift').UnaryExpression | import('jscodeshift').MemberExpression | import('jscodeshift').Identifier} node */ function getObjectKey(node) { let tempNode = { ...node }; while (tempNode.type === 'UnaryExpression') { tempNode = tempNode.argument; } while (tempNode.type === 'MemberExpression' || tempNode.type === 'OptionMemberExpression') { tempNode = tempNode.object; } return tempNode; } /** * * @param {import('jscodeshift').ObjectExpression} node */ function removeProperty(parentNode, child) { if (parentNode) { if (parentNode.type === 'ObjectExpression') { parentNode.properties = parentNode.properties.filter(prop => prop !== child && prop.value !== child); } } } /** * @param {import('jscodeshift').API['j']} j */ const getBuildArrowFunctionAST = j => /** * * @param {Set<string> | import('jscodeshift').Expression[]} params * @param {import('jscodeshift').BlockStatement} body * @returns */ function buildArrowFunctionAST(params, body) { const destructured = [...params].every(param => typeof param === 'string'); return j.arrowFunctionExpression(destructured ? [j.objectPattern([...params].map(k => ({ ...j.objectProperty(j.identifier(k), j.identifier(k)), shorthand: true })))] : params, body); }; /** * @param {import('jscodeshift').API['j']} j */ exports.getBuildArrowFunctionAST = getBuildArrowFunctionAST; const getObjectToArrowFunction = j => { const buildArrowFunctionAST = getBuildArrowFunctionAST(j); return ( /** * * @param {import('jscodeshift').ObjectExpression} objectExpression * @param {import('jscodeshift').BinaryExpression} additional */ function objectToArrowFunction(objectExpression, additional) { const paramKeys = new Set(); let left; objectExpression.properties.forEach((prop, index) => { paramKeys.add(prop.key.name); const result = j.binaryExpression('===', prop.key, prop.value); if (index === 0) { left = result; } else { left = j.logicalExpression('&&', left, result); } }); if (additional) { paramKeys.add(getObjectKey(additional.left).name); } return buildArrowFunctionAST(paramKeys, additional ? j.logicalExpression('&&', left, additional) : left); } ); }; /** * * @param {undefined | null | import('jscodeshift').Expression} node */ exports.getObjectToArrowFunction = getObjectToArrowFunction; function isThemePaletteMode(node) { return node?.type === 'MemberExpression' && node.object.type === 'MemberExpression' && node.object.object.name === 'theme' && node.object.property.name === 'palette' && node.property.name === 'mode'; } /** * * @param {import('jscodeshift').API['j']} j * @param {any[]} styles */ function migrateToVariants(j, styles) { const createBuildStyle = getCreateBuildStyle(j); const appendPaletteModeStyles = getAppendPaletteModeStyles(j); const buildArrowFunctionAST = getBuildArrowFunctionAST(j); const objectToArrowFunction = getObjectToArrowFunction(j); /** * A map of used variable with its original name */ const parameterMap = {}; /** * * @param {import('jscodeshift').Identifier | import('jscodeshift').BinaryExpression | import('jscodeshift').UnaryExpression | import('jscodeshift').MemberExpression} node */ function inverseBinaryExpression(node) { if (node.type === 'Identifier' || node.type === 'MemberExpression') { return j.unaryExpression('!', node); } if (node.operator === '===') { return { ...node, operator: '!==' }; } if (node.operator === '!==') { return { ...node, operator: '===' }; } if (node.operator === '!') { if (node.argument?.operator === '!') { return node.argument; } return j.unaryExpression('!', node); } return node; } function buildObjectAST(jsObject) { const result = j.objectExpression([]); Object.entries(jsObject).forEach(([key, value]) => { result.properties.push(j.objectProperty(j.identifier(key), value)); }); return result; } function resolveParamName(name) { if (typeof name !== 'string') { if (name.type === 'Identifier' && parameterMap[name.name]) { if (parameterMap[name.name].includes('-')) { return j.stringLiteral(parameterMap[name.name]); } return { ...name, name: parameterMap[name.name] }; } return name; } return parameterMap[name] || name; } /** * * @param {import('jscodeshift').LogicalExpression | import('jscodeshift').BinaryExpression | import('jscodeshift').UnaryExpression | import('jscodeshift').MemberExpression} node */ function buildProps(node) { const properties = []; const variables = new Set(); let isAllEqual = true; let tempNode = { ...node }; function assignProperties(_node) { if (_node.type === 'BinaryExpression') { variables.add(resolveParamName(getObjectKey(_node.left).name)); if (_node.operator === '===') { properties.push(j.objectProperty(resolveParamName(getIdentifierKey(_node.left)), _node.right)); } else { isAllEqual = false; } } if (_node.type === 'MemberExpression' || _node.type === 'Identifier') { isAllEqual = false; variables.add(resolveParamName(getObjectKey(_node).name)); } if (_node.type === 'UnaryExpression') { isAllEqual = false; if (_node.argument.type === 'UnaryExpression') { // handle `!!variable` variables.add(resolveParamName(getObjectKey(_node.argument.argument).name)); } else { // handle `!variable` variables.add(resolveParamName(getObjectKey(_node.argument).name)); } } } let counter = 0; if (tempNode.type !== 'LogicalExpression') { assignProperties(tempNode); } else { while (tempNode.type === 'LogicalExpression' && counter < MAX_DEPTH) { counter += 1; if (tempNode.operator !== '&&') { isAllEqual = false; } assignProperties(tempNode.right); if (tempNode.left.type !== 'LogicalExpression') { assignProperties(tempNode.left); break; } tempNode = { ...tempNode.left }; } } if (!isAllEqual) { return buildArrowFunctionAST(variables, node); } return j.objectExpression(properties); } function mergeProps(parentProps, currentProps) { if (parentProps.type === 'ObjectExpression' && currentProps.type === 'ObjectExpression') { return j.objectExpression([...parentProps.properties, ...currentProps.properties]); } const parentArrow = parentProps.type === 'ObjectExpression' ? objectToArrowFunction(parentProps) : parentProps; const currentArrow = currentProps.type === 'ObjectExpression' ? objectToArrowFunction(currentProps) : currentProps; const variables = new Set(); [...parentArrow.params[0].properties, ...currentArrow.params[0].properties].forEach(param => { variables.add(resolveParamName(param.key.name)); }); return buildArrowFunctionAST(variables, j.logicalExpression('&&', parentArrow.body, currentArrow.body)); } // 2. Find logical spread expressions to convert to variants styles.forEach(style => { const parameters = new Set(); style.params.forEach(param => { if (param.type === 'ObjectPattern') { param.properties.forEach(prop => { if (prop.type === 'ObjectProperty') { let paramName; if (prop.value.type === 'Identifier') { paramName = prop.value.name; } if (prop.value.type === 'AssignmentPattern') { paramName = prop.value.left.name; } if (paramName) { parameters.add(paramName); if (prop.key.type === 'Identifier') { parameterMap[paramName] = prop.key.name; } if (prop.key.type === 'StringLiteral') { parameterMap[paramName] = prop.key.value; } } } }); } if (param.type === 'Identifier') { parameters.add(param.name); } }); const variants = []; if (style.body.type === 'LogicalExpression') { if (style.params[0] && (style.params[0].type === 'Identifier' || style.params[0].type === 'ObjectPattern' && style.params[0].properties.some(prop => prop.key.name !== 'theme'))) { // case: ({ theme, ownerState }) => ownerState.variant === 'regular' && theme.mixins.toolbar style.body = j.objectExpression([j.objectProperty(j.identifier('variants'), j.arrayExpression([j.objectExpression([j.objectProperty(j.identifier('props'), buildProps(style.body.left)), j.objectProperty(j.identifier('style'), style.body.right)])]))]); } } else if (style.body.type === 'ConditionalExpression') { // skip ConditionalExpression } else { const expressions = []; if (style.body.type === 'ArrayExpression') { expressions.push(...style.body.elements); } else { expressions.push(style.body); } expressions.forEach(objectExpression => { let counter = 0; while (objectExpression.type !== 'ObjectExpression' && counter < MAX_DEPTH) { counter += 1; if (objectExpression.type === 'BlockStatement') { objectExpression = objectExpression.body.find(item => item.type === 'ReturnStatement').argument; } } recurseObjectExpression({ node: objectExpression, buildStyle: createBuildStyle() }); if (variants.length && objectExpression.type === 'ObjectExpression') { objectExpression.properties.push(j.objectProperty(j.identifier('variants'), j.arrayExpression(variants.filter(variant => { const props = variant.properties.find(prop => prop.key.name === 'props'); const styleVal = variant.properties.find(prop => prop.key.name === 'style'); return props && styleVal && (!styleVal.value.properties || styleVal.value.properties.length > 0) && (props.value.type === 'ArrowFunctionExpression' || props.value.properties.length > 0); })))); } }); } function recurseObjectExpression(data) { if (data.node.type === 'ObjectExpression') { const modeStyles = {}; // to collect styles from `theme.palette.mode === '...'` data.node.properties.forEach(prop => { if (prop.type === 'ObjectProperty') { recurseObjectExpression({ ...data, node: prop.value, parentNode: data.node, key: prop.key, buildStyle: createBuildStyle(prop.key, data.buildStyle), replaceValue: newValue => { prop.value = newValue; }, modeStyles }); } else { recurseObjectExpression({ ...data, node: prop, parentNode: data.node, buildStyle: createBuildStyle(prop.key, data.buildStyle) }); } }); appendPaletteModeStyles(data.node, modeStyles); } if (data.node.type === 'SpreadElement') { if (data.node.argument.type === 'LogicalExpression') { const paramName = data.node.argument.left.type === 'BinaryExpression' ? getObjectKey(data.node.argument.left.left)?.name : getObjectKey(data.node.argument.left)?.name; if (paramName === 'theme' && data.node.argument.left.right.type === 'StringLiteral') { if (data.node.argument.right.type === 'ObjectExpression') { const mode = data.node.argument.left.right.value; data.node.argument.right.properties.forEach(prop => { if (prop.type === 'ObjectProperty') { recurseObjectExpression({ ...data, node: prop.value, parentNode: data.node.argument.right, key: prop.key, buildStyle: createBuildStyle(prop.key, data.buildStyle, mode), replaceValue: newValue => { prop.value = newValue; } }); } else { recurseObjectExpression({ ...data, node: prop, parentNode: data.node.argument.right, buildStyle: createBuildStyle(prop.key, data.buildStyle, mode) }); } }); appendPaletteModeStyles(data.parentNode, { [mode]: data.node.argument.right }); } removeProperty(data.parentNode, data.node); return; } if (paramName && !parameters.has(paramName)) { return; } const scopeProps = buildProps(data.node.argument.left); const variant = { props: data.props ? mergeProps(data.props, scopeProps) : scopeProps, style: data.node.argument.right }; const lastLength = variants.push({}); // preserve the order of the recursive calls const modeStyles = {}; // to collect styles from `theme.palette.mode === '...'` if (variant.style.type === 'ObjectExpression') { variant.style.properties.forEach(prop => { if (prop.type === 'ObjectProperty') { recurseObjectExpression({ ...data, node: prop.value, parentNode: variant.style, props: variant.props, key: prop.key, buildStyle: createBuildStyle(prop.key, data.buildStyle), replaceValue: newValue => { prop.value = newValue; }, modeStyles }); } else { recurseObjectExpression({ ...data, node: prop, parentNode: variant.style, props: variant.props, buildStyle: createBuildStyle(prop.key, data.buildStyle) }); } }); } appendPaletteModeStyles(variant.style, modeStyles); variant.style = data.buildStyle(variant.style); variants[lastLength - 1] = buildObjectAST(variant); removeProperty(data.parentNode, data.node); } if (data.node.argument.type === 'ConditionalExpression') { recurseObjectExpression({ ...data, node: data.node.argument, parentNode: data.node }); removeProperty(data.parentNode, data.node); } } if (data.node.type === 'ConditionalExpression') { if (data.node.test.type === 'BinaryExpression' || data.node.test.type === 'UnaryExpression' || data.node.test.type === 'Identifier' || data.node.test.type === 'MemberExpression') { let leftName = getObjectKey(data.node.test)?.name; if (data.node.test.left) { leftName = getObjectKey(data.node.test.left)?.name; } if (data.node.test.argument) { leftName = getObjectKey(data.node.test.argument)?.name; } if (parameters.has(leftName) && leftName !== 'theme') { let props = buildProps(data.node.test); if (data.props) { props = mergeProps(data.props, props); } const styleVal = data.buildStyle(data.node.consequent); const variant = { props, style: styleVal }; variants.push(buildObjectAST(variant)); if (data.node.consequent.type === 'ObjectExpression' && data.node.alternate.type === 'ObjectExpression') { // create another variant with inverted condition let props2 = buildProps(inverseBinaryExpression(data.node.test)); if (data.props) { props2 = mergeProps(data.props, props2); } const styleVal2 = data.buildStyle(data.node.alternate); const variant2 = { props: props2, style: styleVal2 }; variants.push(buildObjectAST(variant2)); if (data.parentNode?.type === 'ObjectExpression') { removeProperty(data.parentNode, data.node); } } else { data.replaceValue?.(data.node.alternate); } } if (leftName === 'theme' && data.parentNode?.type === 'ObjectExpression' && data.node.test?.type === 'BinaryExpression' && isThemePaletteMode(data.node.test.left)) { if (data.node.consequent.type !== 'ObjectExpression' && data.node.alternate.type !== 'ObjectExpression') { if (data.modeStyles) { if (!data.modeStyles[data.node.test.right.value]) { data.modeStyles[data.node.test.right.value] = []; } data.modeStyles[data.node.test.right.value].push(j.objectProperty(data.key, data.node.consequent)); } data.replaceValue?.(data.node.alternate); } } } } if (data.node.type === 'TemplateLiteral') { if (data.parentNode?.type === 'ObjectExpression') { const modeStyles = {}; data.node.expressions.forEach((expression, index) => { recurseObjectExpression({ ...data, node: expression, parentNode: data.parentNode, buildStyle: createBuildStyle(data.key, data.buildStyle), replaceValue: newValue => { data.node.expressions[index] = newValue; }, modeStyles }); }); if (data.modeStyles) { Object.entries(modeStyles).forEach(([mode, objectStyles]) => { const clonedNode = { ...data.node, expressions: data.node.expressions.map(expression => ({ ...expression })) }; clonedNode.expressions = objectStyles.map(item => item.value); if (!data.modeStyles[mode]) { data.modeStyles[mode] = []; } data.modeStyles[mode].push(j.objectProperty(data.key, clonedNode)); }); if (data.key) { // to remove the arrow function // { ...: theme => `1px solid...` } to { ...: `1px solid...` } data.replaceValue?.(data.node); } } } } if (data.key && data.key.type === 'Identifier' && data.node.type === 'MemberExpression' && data.node.object.type === 'ObjectExpression' && parameters.has(getObjectKey(data.node.property).name)) { data.node.object.properties.forEach(prop => { variants.push(buildObjectAST({ props: j.objectExpression([j.objectProperty(getIdentifierKey(data.node.property), j.stringLiteral(prop.key.name))]), style: data.buildStyle(prop.value) })); }); removeProperty(data.parentNode, data.node); } } style.params.forEach(param => { if (param.type === 'ObjectPattern') { param.properties = param.properties.filter(prop => prop.key.name === 'theme'); } }); if (style.body.type === 'ObjectExpression') { // Remove empty `...theme.applyStyles('...', {})` style.body.properties = style.body.properties.filter(prop => { if (prop.argument?.callee?.object?.name === 'theme' && typeof prop.argument?.arguments[0]?.value === 'string' && !prop.argument?.arguments?.[1]?.properties?.length) { return false; } return true; }); } }); }