Once upon a time, there was a JavaScript codebase. This codebase used the function expression syntax:
const returnTheNumberOne = () => 1;
For reasons that aren’t relevant to this blog post, I needed to replace this with the function declaration syntax:
function returnTheNumberOne()
{
return 1;
}
Usually some clever find-and-replace (read: regular expressions), mixed with a bit of manual labor, would take care of this quickly enough. I started down that path with a few minutes of tinkering.
Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems. Jamie Zawinski
It didn’t take me long to realize I needed to find a better way:
Enter JSCodeShift, stage left. JSCodeShift turns JavaScript files into an abstract-syntax tree (AST). This provides an object model that can be used to do more complicated transforms than could be done on context-less text.
The wonderful AST Explorer will show what the AST looks like for both our input and target. I’ve marked up the ASTs from the two code snippets above to show how they are related. This should help explain what needs to be transformed and how.
A codemod is JavaScript code which will operate on the nodes in the AST. Typically, it will find nodes that match some pattern and replace them with something else. At a high level, it could look something like this:
module.exports = (file, api) {
const j = api.codeshift;
const root = j(file.source);
function replaceNode(nodePath) {
...
}
return root.find(...)
.replaceWith(replaceNode)
.toSource();
}
Using the AST for the input code, the target is any VariableDeclaration node which contains an ArrowFunctionExpression node.
return root.find(j.VariableDeclaration, {
declarations: [{
init: {
type: 'ArrowFunctionExpression'
}
}]
})
.replaceWith(...)
.toSource();
According to the AST for the target code, what we found will be replaced with a FunctionDeclaration node. If we created this node by hand, we would have to manage a lot of AST-related details: line numbers, object references, parent relationships, etc. Luckily JSCodeShift provides builder functions such as j.functionDeclaration(...)
to do that for us. According to the FunctionDeclaration definition, that node is built with id
, params
, and body
. We can get those from the VariableDeclaration and ArrowFunctionExpression nodes we found above.
function replaceNode(nodePath) {
const { node } = nodePath;
const variableDeclarator = node.declarations[0];
const arrowFunctionExpression = variableDeclarator.init;
return j.functionDeclaration(
variableDeclarator.id,
arrowFunctionExpression.params,
arrowFunctionExpression.body);
}
return root.find(...)
.replaceWith(replaceNode)
.toSource();
It can take a little while to write a codemod, so a find/replace can still be faster in simple cases. There are also some cases in which small text changes result in significant changes to the AST that would require writing a second codemod. Analyzing the AST from this snippet is left as an exercise to the reader:
const helper = require('helper');
const helper = require('helper').foo;
Also, the way code comments are handled surprised me: they are properties of the closest node in the AST. That means they must be copied manually if you are changing that node.
JSCodeShift makes difficult or complicated code transforms much easier. The resulting code is far more readable than a regex. And when best practices change in the future, the codemod is easy to revisit, understand, and update for the new standard. The neverending march of progress just got easier.
JSCodeShift is a powerful new tool in our toolbox, though not one we’ll use every day.
Links used in this post: