@mui/codemod
Version:
Codemod scripts for Material UI.
411 lines (407 loc) • 18.9 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = sxV6;
var _getReturnExpression = _interopRequireDefault(require("../../util/getReturnExpression"));
var _migrateToVariants = require("../../util/migrateToVariants");
/**
*
* @param {import('jscodeshift').MemberExpression | import('jscodeshift').Identifier} node
*/
function getCssVarName(node) {
let varName = '-';
while (node.type === 'MemberExpression') {
varName += `-${node.object?.name || node.property?.name || 'unknown'}`;
if (node.object.type === 'MemberExpression') {
node = node.object;
} else {
node = node.property;
}
}
varName += `-${node.name || 'unknown'}`;
return varName;
}
/**
* @param {import('jscodeshift').FileInfo} file
* @param {import('jscodeshift').API} api
*/
function sxV6(file, api, options) {
const j = api.jscodeshift;
const root = j(file.source);
const printOptions = options.printOptions;
const createBuildStyle = (0, _migrateToVariants.getCreateBuildStyle)(j);
const appendPaletteModeStyles = (0, _migrateToVariants.getAppendPaletteModeStyles)(j);
const buildArrowFunctionAST = (0, _migrateToVariants.getBuildArrowFunctionAST)(j);
/**
*
* @param {import('jscodeshift').Identifier} node
*/
function replaceUndefined(node, replacement = j.nullLiteral()) {
if (node?.type === 'Identifier' && node.name === 'undefined') {
return replacement;
}
return node;
}
let shouldTransform = false;
root.find(j.JSXAttribute, {
name: {
name: 'sx'
},
value: {
type: 'JSXExpressionContainer'
}
}).forEach(path => {
/**
* @type {[import('jscodeshift').StringLiteral, import('jscodeshift').Expression][]}
*/
const cssVars = [];
const conditionalExpressions = []; // for ensuring the sequence of styles
let currentIndex = 0;
const sxContainer = path.node.value;
if (['ArrowFunctionExpression', 'ObjectExpression', 'ArrayExpression'].includes(sxContainer.expression.type)) {
shouldTransform = true;
(sxContainer.expression.type === 'ArrayExpression' ? sxContainer.expression.elements : [sxContainer.expression]).forEach((item, index) => {
currentIndex = index;
recurseObjectExpression({
root: item,
replaceRoot: newRoot => {
sxContainer.expression = newRoot;
},
node: item,
buildStyle: createBuildStyle()
});
});
if (cssVars.length) {
const cssVarsObject = j.objectExpression(cssVars.map(([varName, value]) => j.objectProperty(varName, value)));
if (path.parent.node.type === 'JSXOpeningElement') {
const styleAttribute = path.parent.node.attributes.find(attribute => attribute.type === 'JSXAttribute' && attribute.name.name === 'style');
const spreadAttribute = path.parent.node.attributes.find(attribute => attribute.type === 'JSXSpreadAttribute');
if (styleAttribute) {
if (styleAttribute.value.expression.type === 'ObjectExpression') {
styleAttribute.value.expression.properties = [...cssVarsObject.properties, ...styleAttribute.value.expression.properties];
} else if (styleAttribute.value.expression.type === 'Identifier' || styleAttribute.value.expression.type === 'MemberExpression') {
styleAttribute.value.expression = j.objectExpression([...cssVarsObject.properties, j.spreadElement(styleAttribute.value.expression)]);
}
} else if (spreadAttribute) {
path.parent.node.attributes.push(j.jsxAttribute(j.jsxIdentifier('style'), j.jsxExpressionContainer(j.objectExpression([...cssVarsObject.properties, j.spreadElement(j.memberExpression(spreadAttribute.argument, j.identifier('style')))]))));
} else {
path.parent.node.attributes.push(j.jsxAttribute(j.jsxIdentifier('style'), j.jsxExpressionContainer(cssVarsObject)));
}
}
}
if (conditionalExpressions.length && sxContainer.expression.type === 'ArrayExpression') {
// insert the conditional expressions in the correct order
let cumulativeIndex = 0;
conditionalExpressions.forEach(([index, newElement]) => {
sxContainer.expression.elements.splice(index + 1 + cumulativeIndex, 0, newElement);
cumulativeIndex += 1;
});
}
if (sxContainer.expression.type === 'ArrayExpression') {
sxContainer.expression.elements = sxContainer.expression.elements.filter(item => item.type !== 'ObjectExpression' || item.properties.length > 0);
}
}
function wrapSxInArray(newElement) {
if (sxContainer.expression.type === 'ObjectExpression' || sxContainer.expression.type === 'ArrowFunctionExpression') {
sxContainer.expression = j.arrayExpression([sxContainer.expression]);
}
if (sxContainer.expression.type === 'ArrayExpression') {
// store in a list to be added later to ensure the sequence of styles
conditionalExpressions.push([currentIndex, newElement]);
}
}
function rootThemeCallback(data) {
if (data.root.type === 'ObjectExpression') {
data.replaceRoot?.(buildArrowFunctionAST([j.identifier('theme')], data.root));
} else if (data.root.type === 'ArrayExpression') {
data.root.elements.forEach((item, index) => {
if (item === data.node) {
data.root.elements[index] = buildArrowFunctionAST([j.identifier('theme')], data.root);
}
});
}
}
/**
*
* @param {{ node: import('jscodeshift').Expression }} data
*/
function recurseObjectExpression(data) {
if (data.node.type === 'ArrowFunctionExpression') {
const returnExpression = (0, _getReturnExpression.default)(data.node);
if (returnExpression) {
if (returnExpression.type === 'MemberExpression' && returnExpression.property?.type === 'ConditionalExpression') {
recurseObjectExpression({
...data,
node: j.conditionalExpression(returnExpression.property.test, {
...returnExpression,
property: returnExpression.property.consequent
}, {
...returnExpression,
property: returnExpression.property.alternate
})
});
} else if (returnExpression.type === 'TemplateLiteral') {
const firstExpression = returnExpression.expressions[0];
if (firstExpression?.type === 'ConditionalExpression') {
recurseObjectExpression({
...data,
node: j.conditionalExpression(firstExpression.test, {
...returnExpression,
expressions: [firstExpression.consequent, ...(returnExpression.expressions || []).slice(1)]
}, {
...returnExpression,
expressions: [firstExpression.alternate, ...(returnExpression.expressions || []).slice(1)]
})
});
} else {
recurseObjectExpression({
...data,
node: returnExpression
});
}
} else if (returnExpression.type === 'CallExpression' && (0, _migrateToVariants.getObjectKey)(returnExpression.callee)?.name === 'theme' || returnExpression.type === 'MemberExpression' && (0, _migrateToVariants.getObjectKey)(returnExpression)?.name === 'theme' || returnExpression.type === 'BinaryExpression' && ((0, _migrateToVariants.getObjectKey)(returnExpression.left)?.name === 'theme' || (0, _migrateToVariants.getObjectKey)(returnExpression.right)?.name === 'theme')) {
data.replaceValue?.(returnExpression);
rootThemeCallback(data);
} else {
recurseObjectExpression({
...data,
node: returnExpression
});
}
}
}
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;
},
deleteSelf: () => {
(0, _migrateToVariants.removeProperty)(data.node, prop);
if (data.node.properties.length === 0) {
data.deleteSelf?.();
}
},
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' ? (0, _migrateToVariants.getObjectKey)(data.node.argument.left.left)?.name : (0, _migrateToVariants.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
});
}
if (data.deleteSelf) {
data.deleteSelf();
} else {
(0, _migrateToVariants.removeProperty)(data.parentNode, data.node);
}
return;
}
if (data.node.argument.right.type === 'ObjectExpression') {
recurseObjectExpression({
...data,
node: data.node.argument.right,
root: data.node.argument.right,
replaceRoot: newRoot => {
data.node.argument.right = newRoot;
}
});
}
wrapSxInArray(j.logicalExpression(data.node.argument.operator, data.node.argument.left, data.buildStyle(data.node.argument.right)));
if (data.deleteSelf) {
data.deleteSelf();
} else {
(0, _migrateToVariants.removeProperty)(data.parentNode, data.node);
}
}
if (data.node.argument.type === 'ConditionalExpression') {
const isSxSpread = data.node.argument.test.type === 'CallExpression' && data.node.argument.test.callee.type === 'MemberExpression' && data.node.argument.test.callee.object.name === 'Array' && data.node.argument.test.callee.property.name === 'isArray' || data.node.argument.consequent.type === 'Identifier' && data.node.argument.consequent.name === 'sx' || data.node.argument.alternate.type === 'Identifier' && data.node.argument.alternate.name === 'sx';
if (!isSxSpread) {
recurseObjectExpression({
...data,
node: data.node.argument,
parentNode: data.node
});
wrapSxInArray(data.node.argument);
if (data.deleteSelf) {
data.deleteSelf();
} else {
(0, _migrateToVariants.removeProperty)(data.parentNode, data.node);
}
}
}
if (data.node.argument.type === 'CallExpression') {
if ((0, _migrateToVariants.getObjectKey)(data.node.argument.callee)?.name === 'theme' && data.node.argument.callee.property?.name?.startsWith('apply')) {
const objIndex = data.node.argument.arguments.findIndex(arg => arg.type === 'ObjectExpression');
recurseObjectExpression({
...data,
node: data.node.argument.arguments[objIndex],
buildStyle: styleExpression => {
const newArguments = [...data.node.argument.arguments];
newArguments[objIndex] = styleExpression;
return j.arrowFunctionExpression([j.identifier('theme')], {
...data.node.argument,
arguments: newArguments
});
}
});
}
}
}
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') {
if (data.parentNode?.type === 'ObjectExpression' && (data.node.test?.type === 'BinaryExpression' || data.node.test?.type === 'Identifier')) {
if (data.node.consequent.type !== 'ObjectExpression' && data.node.alternate.type !== 'ObjectExpression') {
if ((0, _migrateToVariants.isThemePaletteMode)(data.node.test.left)) {
const consequentKey = (0, _migrateToVariants.getObjectKey)(data.node.consequent);
if (consequentKey.type === 'Identifier' && consequentKey.name !== 'theme') {
const varName = getCssVarName(data.node.consequent);
cssVars.push([j.stringLiteral(varName), data.node.consequent]);
data.node.consequent = j.stringLiteral(`var(${varName})`);
}
const alternateKey = (0, _migrateToVariants.getObjectKey)(data.node.alternate);
if (alternateKey.type === 'Identifier' && alternateKey.name !== 'theme') {
const varName = getCssVarName(data.node.alternate);
cssVars.push([j.stringLiteral(varName), data.node.alternate]);
data.node.alternate = j.stringLiteral(`var(${varName})`);
}
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, replaceUndefined(data.node.consequent)));
}
data.replaceValue?.(replaceUndefined(data.node.alternate));
rootThemeCallback(data);
} else {
wrapSxInArray(j.conditionalExpression(data.node.test, data.buildStyle?.(replaceUndefined(data.node.consequent)), data.buildStyle?.(replaceUndefined(data.node.alternate))));
if (data.deleteSelf) {
data.deleteSelf();
} else {
(0, _migrateToVariants.removeProperty)(data.parentNode, data.node);
}
}
}
}
}
}
if (data.node.type === 'TemplateLiteral') {
if (data.parentNode?.type === 'ObjectExpression') {
const modeStyles = {};
data.node.expressions.forEach((expression, index) => {
if (expression.type === 'MemberExpression') {
const memberKey = (0, _migrateToVariants.getObjectKey)(expression);
if (memberKey.type === 'Identifier' && memberKey.name !== 'theme') {
const varName = getCssVarName(expression);
cssVars.push([j.stringLiteral(varName), expression]);
data.node.expressions[index] = j.stringLiteral(`var(${varName})`);
}
} else {
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) {
data.replaceValue?.(data.node);
}
}
}
if (data.node.expressions?.some(expression => (0, _migrateToVariants.getObjectKey)(expression)?.name === 'theme' || expression.type === 'CallExpression' && (0, _migrateToVariants.getObjectKey)(expression.callee)?.name === 'theme')) {
rootThemeCallback(data);
}
}
}
});
const transformed = root.toSource(printOptions);
if (shouldTransform) {
// recast adds extra newlines that we don't want, https://github.com/facebook/jscodeshift/issues/249
// need to remove them manually
const lines = [];
let isInStyled = false;
let spaceMatch;
transformed.split('\n').forEach(line => {
if (!isInStyled) {
lines.push(line);
} else if (line !== '') {
if (spaceMatch && line.match(/^\s+/)?.[0] === spaceMatch?.[0] && line.endsWith('}')) {
isInStyled = false;
spaceMatch = null;
}
lines.push(line);
}
if (line.includes('sx=') && !line.match(/sx=\{\{[^}]+\}\}/)) {
isInStyled = true;
spaceMatch = line.match(/^\s+/);
}
});
return lines.join('\n');
}
return transformed;
}