Automating code changes with codemods
What is a codemod?
A codemod is a tool/library that can be used to assist with large-scale codebase refactors that can be partially automated but might still require some human oversight and intervention.
Here is a scenario, let's say you manage a library internally in your company and you need to introduce a breaking change that changes the name of an export and also requires the usage of this export to be changed. This could be a change in the number of parameters, the addition of a curried function for the export, the need for a new type of config step, or anything else that requires some significant change to its usage. You might either decide against this change since it is just too significant to make or go ahead with it and let developers figure out how to adopt this change through a painful find-and-replace process which might not be so intuitive depending on how breaking the change is.
Below is an example of a task you might have needed to do in the past, which is migrating from using React.createClass
to React class components.
// component.jsx
const Counter = React.createClass({
getInitialState() {
return {
count: 0
};
},
increment() {
this.setState(prevState => ({ count: prevState.count + 1 }));
},
render() {
return (
<div>
<button onClick={this.increment}>Increment</button>
<span>{this.state.count}</span>
</div>
);
}
});
to this
// component.jsx
class Counter extends React.Component {
state = {
count: 0
}
increment() {
this.setState(prevState => ({ count: prevState.count + 1 }));
}
render() {
return (
<div>
<button onClick={this.increment}>Increment</button>
<span>{this.state.count}</span>
</div>
);
}
}
Migrating a couple of files in this scenario seems intuitive but not straightforward. It could end up being a repetitive and very annoying task to do.
Using a codemod, transforming the code can be simplified using this script which was provided by the React team.
npx react-codemod create-element-to-jsx <path>
This would do most of the work in terms of changing the imports and the usage of a javascript class leaving out only certain edge cases in some files for you to handle.
Writing codemods for Javascript codebases using jscodeshift
jscodeshift is an open-source toolkit from the Facebook team that allows you to go through a list of source files, transform them, and replace the source files with the result of the transformation. During the transform process, you parse the source into an abstract syntax tree (AST), make your changes to the AST structure, and then regenerate the new source code from the modified AST.
AST - An abstract syntax tree is a data structure used in computer science to represent the structure of a program or code snippet
jscodeshift
is built on top of these two packages
recast - used for the conversion of source code to an AST tree
ast-types - used for the interaction of the nodes in the AST tree
Now we are going to write a simple codemod to transform javascript files using jscodeshift
.
The first step would be to install jscodeshift
globally from npm
npm i -g jscodeshift
We would need a source file that needs to be transformed. For our example, let us create a sample JS file that imports lodash
with a default export and uses some of its utilities.
// array-utils.js
import _ from 'lodash';
export function flattenArray(array) {
const flattenedArray = _.flatten(array);
return flattenedArray;
}
export function uniqArray(array) {
const uniqArray = _.uniq(array);
return uniqArray;
}
Next, for our transformation, we want to replace the use of lodash
with lodash-es
by replacing the imports of the utilities with separate default exports for each of the utilities in use. We would need to write a transformation script. For what we need to achieve we can have the following logic;
// transform.js
module.exports = function (fileInfo, api) {
const j = api.jscodeshift;
// Parse the source code to an AST
const ast = j(fileInfo.source);
// Set to store all lodash methods used in the file
const lodashMethodsUsed = new Set();
// Find all lodash method usages to determine which methods are needed
ast
.find(j.MemberExpression, {
object: {
type: "Identifier",
name: "_",
},
})
.forEach((path) => {
if (path.node.property.type === "Identifier") {
lodashMethodsUsed.add(path.node.property.name);
}
});
// Replace the original lodash import with lodash-es imports
ast
.find(j.ImportDeclaration, {
source: {
type: "Literal",
value: "lodash",
},
})
.forEach((path) => {
j(path).remove();
});
lodashMethodsUsed.forEach((method) => {
const newImport = j.importDeclaration(
[j.importDefaultSpecifier(j.identifier(method))],
j.literal(`lodash-es/${method}`)
);
ast.get().node.program.body.unshift(newImport);
});
// Replace the original lodash method calls with the new imports
lodashMethodsUsed.forEach((method) => {
ast
.find(j.CallExpression, {
callee: {
type: "MemberExpression",
object: {
type: "Identifier",
name: "_",
},
property: {
type: "Identifier",
name: method,
},
},
})
.replaceWith((nodePath) => {
return j.callExpression(j.identifier(method), nodePath.node.arguments);
});
});
// Return the modified AST source code
return ast.toSource();
};
Knowing what kind of expressions to target in your source code AST can be tricky. One helpful tip would be to use an AST Explorer like astexplorer.net that can be used to view your entire source code as an AST.
Now we can apply the transformation to the source file using the command below
jscodeshift -t transform.js array-utils.js
Our source file should now have a code that should be the same as below
import uniq from "lodash-es/uniq";
import flatten from "lodash-es/flatten";
export function flattenArray(array) {
const flattenedArray = flatten(array);
return flattenedArray;
}
export function uniqArray(array) {
const uniqArray = uniq(array);
return uniqArray;
}
jscodeshift
can be used to apply transformations to a folder which could be the folder of a project if you want to apply a transformation to all files within the project.