@mui/codemod
Version:
Codemod scripts for Material UI.
191 lines (189 loc) • 7.02 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = replaceComponentsWithSlots;
var _findComponentJSX = _interopRequireDefault(require("../../util/findComponentJSX"));
var _findComponentDefaultProps = _interopRequireDefault(require("../../util/findComponentDefaultProps"));
var _assignObject = _interopRequireDefault(require("../../util/assignObject"));
var _appendAttribute = _interopRequireDefault(require("../../util/appendAttribute"));
function componentsKeyToSlotsKey(str) {
return str[0].toLowerCase() + str.slice(1);
}
function replaceJsxComponentsProp(j, elementPath) {
const element = elementPath.node;
const index = element.openingElement.attributes.findIndex(attr => attr.type === 'JSXAttribute' && attr.name.name === 'components');
if (index !== -1) {
const removed = element.openingElement.attributes.splice(index, 1);
const camelCaseComponents = removed[0].value.expression.properties.reduce((acc, prop) => {
return {
...acc,
[componentsKeyToSlotsKey(prop.key.name)]: prop.value
};
}, {});
let hasNode = false;
element.openingElement.attributes.forEach(attr => {
if (attr.name?.name === 'slots') {
hasNode = true;
const slots = attr.value.expression.properties.reduce((acc, prop) => {
return {
...acc,
[prop.key.name]: prop.value
};
}, {});
Object.entries(camelCaseComponents).forEach(([slot, value]) => {
if (!slots[slot]) {
(0, _assignObject.default)(j, {
target: attr,
key: slot,
expression: value
});
}
});
}
});
if (!hasNode) {
(0, _appendAttribute.default)(j, {
target: element,
attributeName: 'slots',
expression: j.objectExpression(Object.entries(camelCaseComponents).map(([slot, value]) => {
return j.objectProperty(j.identifier(slot), value);
}))
});
}
}
}
function replaceJsxComponentsPropsProp(j, element) {
const index = element.openingElement.attributes.findIndex(attr => attr.type === 'JSXAttribute' && attr.name.name === 'componentsProps');
if (index !== -1) {
const removed = element.openingElement.attributes.splice(index, 1);
let hasNode = false;
element.openingElement.attributes.forEach(attr => {
if (attr.name?.name === 'slotProps') {
hasNode = true;
const slotProps = attr.value.expression.properties.reduce((acc, prop) => {
return {
...acc,
[prop.key.name]: prop.value
};
}, {});
removed[0].value.expression.properties.forEach(prop => {
if (!slotProps[prop.key.name]) {
(0, _assignObject.default)(j, {
target: attr,
key: prop.key.name,
expression: prop.value
});
} else {
attr.value.expression.properties = attr.value.expression.properties.filter(p => p?.key?.name !== prop.key.name);
(0, _assignObject.default)(j, {
target: attr,
key: prop.key.name,
expression: j.objectExpression([j.spreadElement(prop.value), j.spreadElement(slotProps[prop.key.name])])
});
}
});
}
});
if (!hasNode) {
(0, _appendAttribute.default)(j, {
target: element,
attributeName: 'slotProps',
expression: removed[0].value.expression
});
}
}
}
function replaceDefaultPropsComponentsProp(j, defaultPropsPathCollection) {
defaultPropsPathCollection.find(j.ObjectProperty, {
key: {
name: 'components'
}
}).forEach(path => {
const {
properties: defaultPropsProperties
} = path.parent.value;
const components = path.value.value.properties.reduce((acc, prop) => {
return {
...acc,
[componentsKeyToSlotsKey(prop.key.name)]: prop.value
};
}, {});
const existingSlots = defaultPropsProperties.find(prop => prop.key.name === 'slots');
const slots = existingSlots ? existingSlots.value.properties.reduce((acc, prop) => {
return {
...acc,
[prop.key.name]: prop.value
};
}, {}) : {};
const updatedSlots = j.objectExpression(Object.entries({
...components,
...slots
}).map(([slot, value]) => {
return j.objectProperty(j.identifier(slot), value);
}));
if (existingSlots) {
existingSlots.value = updatedSlots;
} else {
defaultPropsProperties.push(j.property('init', j.identifier('slots'), updatedSlots));
}
path.prune();
});
}
function replaceDefaultPropsComponentsPropsProp(j, defaultPropsPathCollection) {
defaultPropsPathCollection.find(j.ObjectProperty, {
key: {
name: 'componentsProps'
}
}).forEach(path => {
const {
properties: defaultPropsProperties
} = path.parent.value;
const components = path.value.value.properties.reduce((acc, prop) => {
return {
...acc,
[prop.key.name]: prop.value
};
}, {});
const existingSlots = defaultPropsProperties.find(prop => prop.key.name === 'slotProps');
const slots = existingSlots ? existingSlots.value.properties.reduce((acc, prop) => {
return {
...acc,
[prop.key.name]: components[prop.key.name] ? j.objectExpression([j.spreadElement(components[prop.key.name]), j.spreadElement(prop.value)]) : prop.value
};
}, {}) : {};
const updatedSlots = j.objectExpression(Object.entries({
...components,
...slots
}).map(([slot, value]) => {
return j.objectProperty(j.identifier(slot), value);
}));
if (existingSlots) {
existingSlots.value = updatedSlots;
} else {
defaultPropsProperties.push(j.property('init', j.identifier('slotProps'), updatedSlots));
}
path.prune();
});
}
/**
* Replaces components and componentsProps props with slots and slotProps.
* Handles local object and variable declaration.
* If the slots prop exists, it will add the components to the slots.
* If there are duplicated values, the slots values will be used.
*
* @param {import('jscodeshift')} j
* @param {{ element: import('jscodeshift').JSXElement, packageName?: string }} options
*
* @example <Component componentsProps={{ root: { 'testid': 'root-id'} }} /> => <Component slotProps={{ root: { 'testid': 'root-id'} }} />
*/
function replaceComponentsWithSlots(j, options) {
(0, _findComponentJSX.default)(j, options, elementPath => {
replaceJsxComponentsProp(j, elementPath);
replaceJsxComponentsPropsProp(j, elementPath.node);
});
const defaultPropsPathCollection = (0, _findComponentDefaultProps.default)(j, options);
replaceDefaultPropsComponentsProp(j, defaultPropsPathCollection);
replaceDefaultPropsComponentsPropsProp(j, defaultPropsPathCollection);
}