Skip to content
Ankit Chouhan

How to transform thousand of files confidently

Code Migration, JSCodeShift2 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.

Example of code transformation

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

  • Read your old source code.
  • Parse it and create an abstract syntax tree (AST).
  • Apply the transformation on AST.
  • Reconstruct source code from transformed AST and replace the original code.

Flow chart of AST transformation

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.

Previous code example

Our final result will look something like this.

Final transformed code

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 tsx
2
3export default function transformer(file, api) {
4
5 // jscodeshift cli will run this transformer function with two parameters.
6 // file: our initial source code
7 // api: an object exposes jscodeshift library and helper functions
8 const j = api.jscodeshift;
9
10 // keep root of our source code AST
11 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 Elements
20 root.find(j.JSXElement).forEach((path) => {
21 // Path also contains information about parentPath for other use cases
22 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_back
29
30 let attributes = openingElement.attributes;
31
32 // Normalize icon name to get transformed opening element name
33 // arrow_back => ArrowBack
34 const iconPascalCaseName = iconSnakeCaseName
35 .split("_")
36 .map((txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase())
37 .join("");
38
39 // Push this icon entry in import list
40 iconImports.push(iconPascalCaseName);
41
42 // Remove 'material-icons ' from className value
43
44 // We are looking for a attribute which has 'className' name
45 // Replace the current className value and remove the attribute
46 // 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 = classValues
55 .replace("material-icons ", "")
56 .replace("material-icons", "");
57 classNameAttribute.value.value = classNamePropValue;
58
59 // remove className props is value is empty
60 if (!classNamePropValue.trim()) {
61 attributes = attributes.filter(
62 (attribute) => attribute.name.name !== "className"
63 );
64 }
65 }
66
67 // Create new Icon component with old attributes
68 // jsxElement returns generated SVG icon component.
69
70 const svgIconComponent = j.jsxElement(
71 j.jsxOpeningElement(
72 j.jsxIdentifier(iconPascalCaseName),
73 attributes,
74 /*selfClosing*/ true
75 )
76 );
77
78 // Replace the <i/> node with svgIconComponent node
79 j(path).replaceWith(svgIconComponent);
80 }
81 });
82
83 // Create import statement for all used icon components
84 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 body
95 root.get().node.program.body.unshift(newImport);
96 }
97
98 // Return source code of transformed AST
99 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.

Ending Notes

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.

Resources

Here are some sources to get started with codemods.