Writing your very first codemod with jscodeshift
If you’re a JavaScript developer, and you have perused Twitter, GitHub, or Medium in the last year, you’ve likely heard of jscodeshift. If you haven’t, you’re missing out!
jscodeshift is a fantastic toolkit, authored by Christoph Pojer and Felix Kling, that allows you to programmatically make massive changes to your JavaScript codebase, using an approachable API that feels syntactically similar to jQuery.
In this post, it is my goal to provide you with the confidence and knowledge you need to author your very first codemod. To get you to that place, we’ll need to start with a bit of vocabulary.
Vocabulary
codemod: Code that is written with the sole intent of transforming other code. An example would be a piece of code that takes a normal function, and rewrites it to be an arrow function.
AST: A tree representation of a piece of source code. When working with JavaScript tools that allow you to inspect/modify an AST, this will most commonly be exposed to you in the form of nested JavaScript objects.
Node: The representation of a single construct within an AST. An example of a node could be a node for a FunctionExpression. A node will often have many other nodes nested within it.
Path: An object that wraps a single node, and exposes an API to make modifying/inspecting information about the node simpler.
Collection: A group of path objects that exposes helpers to transform all contained paths, or traverse them further. Collections are very similar to the object returned from jQuery’s $() function.
recast: A fantastic library (authored by Ben Newman) that takes an AST and transforms it back into source code. The beauty of recast is that it tries to preserve as much of your codes existing formatting as possible.
ast-types: Another great library authored by Ben Newman. It exposes 2 kinds of helpers that you’ll be using: functions that allow you to verify assumptions about nodes, and functions that allow you to compose new nodes to be added to an existing AST.
Now that we have that out of the way, it’s time to move on to the fun part!
Baby’s First codemod
To help with authoring your first codemod, you’ll want to visit AST Explorer, which is an indispensable tool for anyone looking to author a codemod of their own.
For our very first codemod, we’ll start simple: let’s build one to replace all calls to console.log with console.warn.
Step 1: Open AST Explorer. From the Transform menu at the top of the page, select jscodeshift.
Step 2: In the top-left panel, add the following example code:
console.log('I wish this was console.warn');
Step 3: In the bottom-left panel, add the following code:
export default function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source); const consoleLogCalls = root.find(j.CallExpression, {
callee: {
object: {
name: 'console'
},
property: {
name: 'log'
}
}
}); consoleLogCalls.forEach(p => {
p.node.callee.property.name = 'warn';
}); return root.toSource();
};
If you followed the 3 simple steps above correctly, you should now see your newly transformed code in the bottom-right panel
Unfortunately, copying + pasting code from a blog post isn’t the best way for you to learn. Let’s walk through our new codemod, piece-by-piece, and figure out exactly how this works.
Breaking it Down
export default function transformer(file, api) {
A codemod is simply a module that exports a function with 2 parameters: file and api.
const j = api.jscodeshift;
The jscodeshift function(commonly aliased as j) will be used frequently in our codemod, to do everything from creating an AST from source, to traversing that AST.
const root = j(file.source);
Calling the j function with file.source will convert our target files source code into an AST. This AST will be wrapped in a jscodeshift collection.
const consoleLogCalls = root.find(j.CallExpression, {
callee: {
object: {
name: 'console'
},
property: {
name: 'log'
}
}
});
The code above is how we can obtain a collection of all calls to console.log in our source. We do this by using the find method that is exposed on all collections. In this case, we are calling find on our root collection, which tells jscodeshift that we want to search the entire source.
The find function takes 2 arguments:
TypeDefinition: All available type definitions are exposed as properties on the j function. The simplest way to identify the correct type definition you need is to highlight the code you are looking to transform in the top-left panel of AST Explorer. Then, in the top-right panel, look for the type property for the corresponding node. Any type property you see in the AST will always have a corresponding property of the same name on j.
(Optional) Filter: A filter can be either a function that returns true/false, or an object that can be used to partially match against all nodes found for the specified TypeDefinition. In our example above, we use an object that matches unique pieces of the callee property.
consoleLogCalls.forEach(p => {
p.node.callee.property.name = 'warn';
});
A collection exposes many of the common methods you would expect to see on Array.prototype. In this case, we will use forEach to tell jscodeshift that we would like to work with each path in the collection, one at a time.
This is where the most exciting part of our codemod happens: we’re making a modification to our code, with other code! Although collections and paths expose many helpful APIs to declaratively modify the AST, we can always grab a reference to the node wrapped in a path, and mutate it directly (which is exactly what we’ll do now, for the sake of simplicity). In this example, we are renaming the callee’s property.name, to change it from log to warn.
return root.toSource();
The final piece of our codemod! jscodeshift expects that a codemod will return a string representation of the new source code when transformation is complete. To obtain a string from our AST, we can call toSource() on the root collection we created at the beginning of this exercise. The toSource() function calls recast directly, and accepts an object with any options that can be provided to recast.
And that’s it! You now understand the basics of writing a codemod with jscodeshift!
Helpful Tips
In no particular order, here are some helpful tips to guide you on your way to codemod mastery.
- For a list of all available methods exposed on collections and nodes, take a look at the jscodeshift source (nodes and collections). You can also console.log a collection or node in AST Explorer, and use your browser’s DevTools to inspect the prototype chain. If you would like to see better documentation of the methods available, ask how you can help on this issue.
- When you need to create new nodes to add/replace existing nodes in the AST, you will want to use the recast builder functions exposed on j. A builder will always be named similarly to a TypeDefinition, but with the first letter being lowercase. For example, the builder for a CallExpression would be j.callExpression(). The easiest way to get started using builders is to start typing out a call to one in AST Explorer. You’ll be prompted with hints that tell you what parameters are expected.
- When writing a new codemod, you’ll often want to do some exploratory digging through the code base you plan to modify. You can do this by writing a codemod that doesn’t make any modifications, but instead calls api.stats(‘some message here’) to log information. When you run the jscodeshift binary with the dry command line argument, it will inform you of how many times the stats function was called with each unique value.
Further Learning Resources
If you’d like to dig deeper into learning about authoring codemods, the following resources will send you down the right path:
- react-codemod: Codemods authored by the React core team, to help with migrations to newer React APIs.
- How to write a codemod: An excellent blog post that walks you through writing a codemod to transform string concatenation to ES2015 template strings.
- Write code to Rewrite your code: Another great blog post, with several real-world examples.
Conclusion
My hope with this post is that you now feel equipped to continue with more hands-on learning writing your own codemods. I plan to write a follow-up blog post in the next few weeks with more in-depth examples of writing larger transformations, and using more of the built in jscodeshift helpers.