souporserious

Evolving components with codemods

3 min read

It'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 === property
7 )
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 = toName
30 }
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 return
52 }
53 path.node.specifiers = path.node.specifiers
54 .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.name
61 )
62 return hasNewImportAlready
63 ? null
64 : t.importSpecifier(
65 t.identifier(newComponent.name),
66 t.identifier(newComponent.name)
67 )
68 } else {
69 return specifier
70 }
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.

Updated:
  • design
  • development
  • css
  • grid
  • flexbox
  • react
© 2022 Travis Arnold