- Understanding ASTs — This article requires a basic understanding of how to inspect, traverse, and manipulate Abstract Syntax Trees. Codemod Studio has a built-in AST tree explorer that's great for visualizing node structures interactively.
- Codemod CLI installed — You'll need the Codemod CLI available via
npx codemod. See the CLI docs if you haven't set it up yet.
Throughout this guide, we'll break down the thought process behind writing a real-world codemod—from identifying patterns and planning edge cases to implementing and testing the transform.
By the end, you will learn:
- How to write a codemod that solves a real-world problem using JSSG, Codemod's transformation engine.
- How to use ast-grep pattern matching and AST manipulation techniques.
- How to test and publish your codemod using the Codemod CLI.
Let's learn by example!
Before ES6, JavaScript codebases relied heavily on var for variable declarations. Due to var's scoping issues, let and const were introduced—but many codebases still haven't migrated.
Refactoring manually is tedious and error-prone. Simple find-and-replace won't work either, because there are edge cases where blindly swapping var for const would break your code.
This is a perfect job for a codemod. We'll build a no-vars transform that automatically converts var declarations to const or let wherever it's safe to do so.
Before:
Loading code sample...
After:
Loading code sample...
If you're new to codemod development, you might think this is as simple as: find all var declarations and replace them with const. But that would break your code in most cases.
Let's consider this snippet that covers several real-world patterns:
Loading code sample...
This covers the following cases:
varis declared and never mutatedvaris declared and mutatedvaris declared as a loop indexvaris declared inside a loop
Now, take a moment—which of these would break if we just replaced var with const?
Here's a summary of the patterns and their safe transforms:
Loading code sample...
The index is mutated (i++), so it must become let.
Loading code sample...
Reassigned variables must become let.
Loading code sample...
Loading code sample...
Safe to become const x = "foo".
Loading code sample...
Keep as `var`. Converting to let or const would cause a SyntaxError for duplicate declarations in the same scope.
Loading code sample...
Keep as `var`. The variable is used before its declaration, relying on var's hoisting behavior.
Loading code sample...
Keep as `var`. Converting to let/const would change the closure behavior since let creates a new binding per iteration.
Now that we have a concrete list of patterns, let's prepare test fixtures. We'll use the Codemod CLI's built-in testing framework.
Test input (tests/no-vars/input.js):
Loading code sample...
Expected output (tests/no-vars/expected.js):
Loading code sample...
With this plan in mind, let's build the codemod.
Start by scaffolding a new JSSG codemod package:
Loading code sample...
Pick JavaScript ast-grep (JSSG) codemod when prompted. This gives you:
Loading code sample...
Now open scripts/codemod.ts—this is where we'll write our transform.
Every JSSG codemod exports a default transform function:
Loading code sample...
The function receives a parsed AST (root) and returns either a string (modified source code) or null (no changes).
Our detection strategy:
- Find all
vardeclarations broadly - Filter out declarations that must stay as
var(hoisted, duplicated, closure-referenced) - Classify the remaining ones as
letorconst
We use ast-grep's pattern syntax to match all var declarations structurally. The pattern var $DECL matches any variable_declaration node whose first token is the var keyword — no text filtering needed:
Loading code sample...
Use Codemod Studio to explore AST node kinds for your target patterns. Paste sample code and use the built-in AST tree explorer to see the exact tree structure.
Now we need to identify var declarations that must stay as var. We'll write helper functions for each unsafe case.
Check if a variable is declared twice in the same scope:
Loading code sample...
Check if a variable is used before its declaration (hoisting):
Loading code sample...
Check if a variable is inside a loop that contains closures:
When a loop contains closures (nested functions), converting var to let/const changes semantics because let creates a new binding per iteration. To be safe, we keep all var declarations as-is when the enclosing loop contains any closures.
Loading code sample...
Helper to find the enclosing scope:
Loading code sample...
Now we classify the remaining var declarations as either let or const:
- If the variable is mutated (reassigned or updated), use
let - If the variable is a for-loop initializer (e.g.,
for (var i = ...)), uselet - Otherwise, use
const
Check if a variable is mutated:
Loading code sample...
Check if it's a for-loop initializer:
Loading code sample...
Here's the complete transform:
In tree-sitter's JavaScript grammar, for (var x of items) does not wrap var x in a variable_declaration node like a normal var statement would. Instead, var and the identifier are bare children of the for_in_statement. We handle this by matching structurally with for (var $X of $Y) $BODY and replacing only the keyword token at child(2) — no regex, no full-node text replacement. This is the kind of detail you'll discover when exploring AST structures in Codemod Studio—always verify your assumptions against the actual tree!
Note: When writing codemods, always consider having fallbacks for undesirable cases. Here we chose let as a fallback when const is not applicable, rather than keeping var, since let is arguably the better default.
Set up your test fixtures under the tests/ directory:
Loading code sample...
Then run the tests:
Loading code sample...
Use the --verbose flag for detailed output when debugging:
Loading code sample...
And if you've intentionally changed behavior, update the snapshots:
Loading code sample...
Once your tests pass, you can publish your codemod to the Codemod Registry so others can use it:
Loading code sample...
Anyone can then run your codemod with:
Loading code sample...
See the publishing guide for setting up CI/CD with trusted publishers.
After applying this transform, we successfully convert var declarations to const or let wherever it's safe—handling edge cases like hoisting, duplicate declarations, and closure references in loops.
Before:
Loading code sample...
After:
Loading code sample...
- Identify patterns methodically. Do a thorough code search and capture as many possible patterns as you can before writing a single line of transform code.
- Test before you transform. Create test fixtures using the captured patterns—include both cases that should and should not be transformed.
- Use JSSG and ast-grep patterns. JSSG's pattern matching makes it easy to express complex structural queries without manually walking the AST.
- Publish and share. Once your codemod is tested, publish it to the Codemod Registry so your team (or the community) can run it with a single command.
- JSSG Quickstart — Build your first JSSG codemod in minutes.
- JSSG API Reference — Full reference for node navigation, editing, and pattern matching.
- Testing Guide — Learn about snapshot testing, strictness levels, and CI integration.
- Codemod Studio — Generate and test codemods visually with AI assistance.