— Code Migration, JSCodeShift — 2 min read
Yes…we will be talking about codemods, what are codemods and how to write a transformation script?
Codemod may be a tool or a library that assists to do large-scale codebase refactoring that can be partially automated but still require human intervention.
Here is an example for you.
We need to transform the old-style ‘this’ binding to arrow functions. But we need to do this in a large-scale codebase say thousand of files. This might take days to do manually. What if we can write a script that takes this code as data, processes them and returns the transformed code. This process is called metaprogramming.
Let’s go through the theory once. This is what your script needs to do
Remember your compiler design classes, AST is the final result of the syntax analysis phase. You can read more about it here.
Whoops… that's a lot of work. Don’t worry, we have tools that will handle all the low-level work for us. We will be using an awesome tool called jscodeshift.
jscodeshift is a wrapper around recast which is an AST to AST transformer. It also let us know how many files have been transformed or skipped.
So now let’s get our hands dirty. We have font icons tags with a material-icons class attached. We need to migrate from font icons to SVG icons.
Our final result will look something like this.
In the transformed code 'i' tag is replaced with respective SVG icon components. Previous className and other props are still attached with the new SVG component. We also have an import statement from our icon library.
These transformations have been applied to thousands of javascript and typescript files in a real project.̉̉̉̉̉̉
You first need to install jscodeshift.
1npm install -g jscodeshift
Here is final script.
1//jscodeshift -t scripts/index.js src/**/*.(js|tsx) --parser tsx2
3export default function transformer(file, api) {4 5 // jscodeshift cli will run this transformer function with two parameters.6 // file: our initial source code7 // api: an object exposes jscodeshift library and helper functions8 const j = api.jscodeshift;9
10 // keep root of our source code AST11 const root = j(file.source);12 13 const iconImports = [];14 const exportPath = "material-icon-lib";15
16 // We are looking for a JSX element which has openingElement as 'i'17 // <i className="material-icon">arrow_back</i>18 19 // root.find will traverse the tree and returns the list of paths all JSX Elements20 root.find(j.JSXElement).forEach((path) => {21 // Path also contains information about parentPath for other use cases22 const { node } = path;23 const openingElement = node.openingElement;24
25 if (openingElement.name.name === "i") {26 const iconSnakeCaseName = node.children[0].value;27 // <i className="material-icon">arrow_back</i>28 // iconSnakeCaseName => arrow_back29 30 let attributes = openingElement.attributes;31
32 // Normalize icon name to get transformed opening element name33 // arrow_back => ArrowBack34 const iconPascalCaseName = iconSnakeCaseName35 .split("_")36 .map((txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())37 .join("");38
39 // Push this icon entry in import list40 iconImports.push(iconPascalCaseName);41
42 // Remove 'material-icons ' from className value43 44 // We are looking for a attribute which has 'className' name45 // Replace the current className value and remove the attribute46 // if value becomes empty.47 const classNameAttribute = attributes.find(48 (attribute) => attribute.name.name === "className"49 );50
51 const classValues = classNameAttribute.value.value;52
53 if (classNameAttribute && typeof classValues === "string") {54 const classNamePropValue = classValues55 .replace("material-icons ", "")56 .replace("material-icons", "");57 classNameAttribute.value.value = classNamePropValue;58
59 // remove className props is value is empty60 if (!classNamePropValue.trim()) {61 attributes = attributes.filter(62 (attribute) => attribute.name.name !== "className"63 );64 }65 }66
67 // Create new Icon component with old attributes68 // jsxElement returns generated SVG icon component.69 70 const svgIconComponent = j.jsxElement(71 j.jsxOpeningElement(72 j.jsxIdentifier(iconPascalCaseName),73 attributes,74 /*selfClosing*/ true75 )76 );77
78 // Replace the <i/> node with svgIconComponent node79 j(path).replaceWith(svgIconComponent);80 }81 });82
83 // Create import statement for all used icon components84 const importSpecifiers = [...new Set(iconImports)].map(85 (icon) => j.importSpecifier(j.identifier(icon))86 );87
88 if (importSpecifiers.length) {89 const newImport = j.importDeclaration(90 importSpecifiers,91 j.stringLiteral(exportPath)92 );93
94 // Add this import statement on the top of source body95 root.get().node.program.body.unshift(newImport);96 }97 98 // Return source code of transformed AST99 return root.toSource();100}
You can explore more and play with this example here. Try exploring the source code of some popular codemods like loadable-component, react-codemod and many more.
Most of the codemod works the same way. The key is to understand the generated AST of your language. Now you might guess how Babel transforms modern javascript for old browsers, how eslint/prettier checks for linting errors and many more. I’m excited about what you build out of these tools.
Here are some sources to get started with codemods.