Evolving components with codemods
3 min readIt's certain that the way in which we write software won't be the same tomorrow or a decade from now. AST manipulation can help with just that.
Codemods are a wonderful tool to add to your toolbox. When creating APIs or even moving libraries, we can use codemods to make our lives easier and focus on more important tasks.
AST Transformation
Codemods solely work off of AST transformations. These are detailed bits of information that describe all sorts of charaterisittcs of your code like line positions, types, and much more.
Evolving APIs
When working through a component library or and sort of code for that matter The difference between good/great API design is thinking about how your code will evolve. You may find that certain component doesn’t work well or you might want to switch a codebase from using class names to CSS in JS.
Finding all components
First, we will map out some expectations of what we need our codemod to do:
- Remove the old component from imports
- Add the new component as an import if it does not exist yet
- Find every place the old component is defined and replace it with the new one
- Update any APIs that may have changed between both solutions
Handling imports
By visiting the ImportDeclration we can determine what we need to remove and add.
Swapping or adding props
When inspecting the AST you will find that props in JSX are referred to as attributes.
1export default function ({ types: t }) {2 function matchesComponentName(path, name) {3 const [object, property] = name.split('.')4 return (5 path.node.name.object.name === object &&6 path.node.name.property.name === property7 )8 }9 function changeComponentName(path, name) {10 path.node.name = t.jSXIdentifier(name)11 }12 function createAttribute(name, value) {13 const literalType =14 typeof value === 'number'15 ? t.jSXExpressionContainer(literalType(value))16 : t.stringLiteral(value)17 return t.jSXAttribute(t.jSXIdentifier(name), literalType)18 }19 return {20 visitor: {21 JSXOpeningElement(path) {22 componentsToChange.forEach(([from, to, propsToConvert]) => {23 if (matchesComponentName(path, from.name)) {24 changeComponentName(path, to.name)25 if (propsToConvert) {26 propsToConvert.forEach(([fromName, toName]) => {27 path.node.attributes.forEach((attribute, index) => {28 if (attribute.name.name === fromName) {29 attribute.name.name = toName30 }31 })32 })33 }34 if (to.props) {35 to.props.forEach(([prop, value]) => {36 path.node.attributes.unshift(createAttribute(prop, value))37 })38 }39 }40 })41 },42 JSXClosingElement(path) {43 componentsToChange.forEach(([from, to]) => {44 if (matchesComponentName(path, from.name)) {45 changeComponentName(path, to.name)46 }47 })48 },49 ImportDeclaration(path) {50 if (path.node.source.value !== '@planning-center/ui-kit') {51 return52 }53 path.node.specifiers = path.node.specifiers54 .map((specifier) => {55 if (oldComponentNames.includes(specifier.imported.name)) {56 const [oldComponent, newComponent] = componentsToChange[57 oldComponentNames.indexOf(specifier.imported.name)58 ]59 const hasNewImportAlready = path.node.specifiers.some(60 (specifier) => specifier.imported.name === newComponent.name61 )62 return hasNewImportAlready63 ? null64 : t.importSpecifier(65 t.identifier(newComponent.name),66 t.identifier(newComponent.name)67 )68 } else {69 return specifier70 }71 })72 .filter(Boolean)73 },74 },75 }76}
Summary
Codemods are easy to write for JSX and allow component libraries to evolve. If you’d like to dive further into AST transformation I highly reccommend Kent C Dodds FrontendMasters coures. I hope this gave you a glimpse into the power of codemods and how easy they can be to write.
- design
- development
- css
- grid
- flexbox
- react