As a library developer, you may create a popular utility that hundreds of
thousands of developers rely on daily, such as lodash or React. Over time,
usage patterns might emerge that go beyond your initial design. When this
happens, you may need to extend an API by adding parameters or modifying
function signatures to fix edge cases. The challenge lies in rolling out
these breaking changes without disrupting your users’ workflows.
This is where codemods come in—a powerful tool for automating
large-scale code transformations, allowing developers to introduce breaking
API changes, refactor legacy codebases, and maintain code hygiene with
minimal manual effort.
In this article, we’ll explore what codemods are and the tools you can
use to create them, such as jscodeshift, hypermod.io, and codemod.com. We’ll walk through real-world examples,
from cleaning up feature toggles to refactoring component hierarchies.
You’ll also learn how to break down complex transformations into smaller,
testable pieces—a practice known as codemod composition—to ensure
flexibility and maintainability.
By the end, you’ll see how codemods can become a vital part of your
toolkit for managing large-scale codebases, helping you keep your code clean
and maintainable while handling even the most challenging refactoring
tasks.
Breaking Changes in APIs
Returning to the scenario of the library developer, after the initial
release, new usage patterns emerge, prompting the need to extend an
For simple changes, a basic find-and-replace in the IDE might work. In
more complex cases, you might resort to using tools like sed
or awk
. However, when your library is widely adopted, the
scope of such changes becomes harder to manage. You can’t be sure how
extensively the modification will impact your users, and the last thing
you want is to break existing functionality that doesn’t need
updating.
A common approach is to announce the breaking change, release a new
version, and ask users to migrate at their own pace. But this workflow,
while familiar, often doesn’t scale well, especially for major shifts.
Consider React’s transition from class components to function components
with hooks—a paradigm shift that took years for large codebases to fully
adopt. By the time teams managed to migrate, more breaking changes were
often already on the horizon.
For library developers, this situation creates a burden. Maintaining
multiple older versions to support users who haven’t migrated is both
costly and time-consuming. For users, frequent changes risk eroding trust.
They may hesitate to upgrade or start exploring more stable alternatives,
which perpetuating the cycle.
But what if you could help users manage these changes automatically?
What if you could release a tool alongside your update that refactors
their code for them—renaming functions, updating parameter order, and
removing unused code without requiring manual intervention?
That’s where codemods come in. Several libraries, including React
and Next.js, have already embraced codemods to smooth the path for version
bumps. For example, React provides codemods to handle the migration from
older API patterns, like the old Context API, to newer ones.
So, what exactly is the codemod we’re talking about here?
What is a Codemod?
A codemod (code modification) is an automated script used to transform
code to follow new APIs, syntax, or coding standards. Codemods use
Abstract Syntax Tree (AST) manipulation to apply consistent, large-scale
changes across codebases. Initially developed at Facebook, codemods helped
engineers manage refactoring tasks for large projects like React. As
Facebook scaled, maintaining the codebase and updating APIs became
increasingly difficult, prompting the development of codemods.
Manually updating thousands of files across different repositories was
inefficient and error-prone, so the concept of codemods—automated scripts
that transform code—was introduced to tackle this problem.
The process typically involves three main steps:
- Parsing the code into an AST, where each part of the code is
represented as a tree structure. - Modifying the tree by applying a transformation, such as renaming a
function or changing parameters. - Rewriting the modified tree back into the source code.
By using this approach, codemods ensure that changes are applied
consistently across every file in a codebase, reducing the chance of human
error. Codemods can also handle complex refactoring scenarios, such as
changes to deeply nested structures or removing deprecated API usage.
If we visualize the process, it would look something like this:
Figure 1: The three steps of a typical codemod process
The idea of a program that can “understand” your code and then perform
automatic transformations isn’t new. That’s how your IDE works when you
run refactorings like
Essentially, your IDE parses the source code into ASTs and applies
predefined transformations to the tree, saving the result back into your
files.
For modern IDEs, many things happen under the hood to ensure changes
are applied correctly and efficiently, such as determining the scope of
the change and resolving conflicts like variable name collisions. Some
refactorings even prompt you to input parameters, such as when using
order of parameters or default values before finalizing the change.
Use jscodeshift in JavaScript Codebases
Let’s look at a concrete example to understand how we could run a
codemod in a JavaScript project. The JavaScript community has several
tools that make this work feasible, including parsers that convert source
code into an AST, as well as transpilers that can transform the tree into
other formats (this is how TypeScript works). Additionally, there are
tools that help apply codemods to entire repositories automatically.
One of the most popular tools for writing codemods is jscodeshift, a toolkit maintained by Facebook.
It simplifies the creation of codemods by providing a powerful API to
manipulate ASTs. With jscodeshift, developers can search for specific
patterns in the code and apply transformations at scale.
You can use jscodeshift
to identify and replace deprecated API calls
with updated versions across an entire project.
Let’s break down a typical workflow for composing a codemod
manually.
Clean a Stale Feature Toggle
Let’s start with a simple yet practical example to demonstrate the
power of codemods. Imagine you’re using a feature
toggle in your
codebase to control the release of unfinished or experimental features.
Once the feature is live in production and working as expected, the next
logical step is to clean up the toggle and any related logic.
For instance, consider the following code:
const data = featureToggle('feature-new-product-list') ? { name: 'Product' } : undefined;
Once the feature is fully released and no longer needs a toggle, this
can be simplified to:
const data = { name: 'Product' };
The task involves finding all instances of featureToggle
in the
codebase, checking whether the toggle refers to
feature-new-product-list
, and removing the conditional logic surrounding
it. At the same time, other feature toggles (like
feature-search-result-refinement
, which may still be in development)
should remain untouched. The codemod needs to understand the structure
of the code to apply changes selectively.
Understanding the AST
Before we dive into writing the codemod, let’s break down how this
specific code snippet looks in an AST. You can use tools like AST
Explorer to visualize how source code and AST
are mapped. It’s helpful to understand the node types you’re interacting
with before applying any changes.
The image below shows the syntax tree in terms of ECMAScript syntax. It
contains nodes like Identifier
(for variables), StringLiteral
(for the
toggle name), and more abstract nodes like CallExpression
and
ConditionalExpression
.
Figure 2: The Abstract Syntax Tree representation of the feature toggle check
In this AST representation, the variable data
is assigned using a
ConditionalExpression
. The test part of the expression calls
featureToggle('feature-new-product-list')
. If the test returns true
,
the consequent branch assigns { name: 'Product' }
to data
. If
false
, the alternate branch assigns undefined
.
For a task with clear input and output, I prefer writing tests first,
then implementing the codemod. I start by defining a negative case to
ensure we don’t accidentally change things we want to leave untouched,
followed by a real case that performs the actual conversion. I begin with
a simple scenario, implement it, then add a variation (like checking if
featureToggle is called inside an if statement), implement that case, and
ensure all tests pass.
This approach aligns well with Test-Driven Development (TDD), even
if you don’t practice TDD regularly. Knowing exactly what the
transformation’s inputs and outputs are before coding improves safety and
efficiency, especially when tweaking codemods.
With jscodeshift, you can write tests to verify how the codemod
behaves:
const transform = require("../remove-feature-new-product-list"); defineInlineTest( transform, {}, ` const data = featureToggle('feature-new-product-list') ? { name: 'Product' } : undefined; `, ` const data = { name: 'Product' }; `, "delete the toggle feature-new-product-list in conditional operator" );
The defineInlineTest
function from jscodeshift allows you to define
the input, expected output, and a string describing the test’s intent.
Now, running the test with a normal jest
command will fail because the
codemod isn’t written yet.
The corresponding negative case would ensure the code remains unchanged
for other feature toggles:
defineInlineTest( transform, {}, ` const data = featureToggle('feature-search-result-refinement') ? { name: 'Product' } : undefined; `, ` const data = featureToggle('feature-search-result-refinement') ? { name: 'Product' } : undefined; `, "do not change other feature toggles" );
Writing the Codemod
Let’s start by defining a simple transform function. Create a file
called transform.js
with the following code structure:
module.exports = function(fileInfo, api, options) { const j = api.jscodeshift; const root = j(fileInfo.source); // manipulate the tree nodes here return root.toSource(); };
This function reads the file into a tree and uses jscodeshift’s API to
query, modify, and update the nodes. Finally, it converts the AST back to
source code with .toSource()
.
Now we can start implementing the transform steps:
- Find all instances of
featureToggle
. - Verify that the argument passed is
'feature-new-product-list'
. - Replace the entire conditional expression with the consequent part,
effectively removing the toggle.
Here’s how we achieve this using jscodeshift
:
module.exports = function (fileInfo, api, options) { const j = api.jscodeshift; const root = j(fileInfo.source); // Find ConditionalExpression where the test is featureToggle('feature-new-product-list') root .find(j.ConditionalExpression, { test: { callee: { name: "featureToggle" }, arguments: [{ value: "feature-new-product-list" }], }, }) .forEach((path) => { // Replace the ConditionalExpression with the 'consequent' j(path).replaceWith(path.node.consequent); }); return root.toSource(); };
The codemod above:
- Finds
ConditionalExpression
nodes where the test calls
featureToggle('feature-new-product-list')
. - Replaces the entire conditional expression with the consequent (i.e.,
{
), removing the toggle logic and leaving simplified code
name: 'Product' }
behind.
This example demonstrates how easy it is to create a useful
transformation and apply it to a large codebase, significantly reducing
manual effort.
You’ll need to write more test cases to handle variations like
if-else
statements, logical expressions (e.g.,
!featureToggle('feature-new-product-list')
), and so on to make the
codemod robust in real-world scenarios.
Once the codemod is ready, you can test it out on a target codebase,
such as the one you’re working on. jscodeshift provides a command-line
tool that you can use to apply the codemod and report the results.
$ jscodeshift -t transform-name src/
After validating the results, check that all functional tests still
pass and that nothing breaks—even if you’re introducing a breaking change.
Once satisfied, you can commit the changes and raise a pull request as
part of your normal workflow.
Codemods Improve Code Quality and Maintainability
Codemods aren’t just useful for managing breaking API changes—they can
significantly improve code quality and maintainability. As codebases
evolve, they often accumulate technical debt, including outdated feature
toggles, deprecated methods, or tightly coupled components. Manually
refactoring these areas can be time-consuming and error-prone.
By automating refactoring tasks, codemods help keep your codebase clean
and free of legacy patterns. Regularly applying codemods allows you to
enforce new coding standards, remove unused code, and modernize your
codebase without having to manually modify every file.