Codemods are automated code transformation tools that modify your source code by analyzing and manipulating its Abstract Syntax Tree (AST). This page explains the technical foundation of Node.js Userland codemods.
Abstract Syntax Trees (AST)
An Abstract Syntax Tree is a structured representation of your code that the computer can understand and manipulate. When you run a codemod:
Parse
Your source code is parsed into an AST representation
Analyze
The codemod searches for specific patterns in the AST
Transform
Matching nodes are replaced with updated code
Generate
The modified AST is converted back to source code
Example: From Code to AST
Consider this simple code:
const { isArray } = require('util');
isArray([]);
The AST represents this as a tree structure with nodes like variable_declarator, object_pattern, call_expression, etc. The codemod can identify these patterns and transform them.
ast-grep: The Pattern Matching Engine
ast-grep is a powerful tool for searching and manipulating ASTs using pattern matching. It provides:
- Pattern-based search: Find code using intuitive patterns like
util.isArray($ARG)
- Structural matching: Match based on code structure, not just text
- Language awareness: Understands JavaScript and TypeScript syntax
ast-grep is the core engine powering Node.js Userland codemods. It handles the heavy lifting of AST parsing and traversal.
The jssg API
The jssg (JavaScript ast-grep) API is the interface codemods use to interact with ASTs. It’s provided by the Codemod.com platform.
Core Concepts
Every codemod exports a transform function:
import type { SgRoot, Edit } from "@codemod.com/jssg-types/main";
import type JS from "@codemod.com/jssg-types/langs/javascript";
export default function transform(root: SgRoot<JS>): string | null {
const rootNode = root.root();
const edits: Edit[] = [];
// Find nodes, build edits, commit changes
return rootNode.commitEdits(edits);
}
Key types:
SgRoot<JS>: The root object containing the parsed AST
SgNode<JS>: Individual AST nodes you can query and manipulate
Edit: Represents a single code modification
Range: Represents a location in the source code
Finding Nodes
Use pattern matching to locate code:
// Find all calls to util.isArray
const nodes = rootNode.findAll({
rule: { pattern: 'util.isArray($ARG)' }
});
// Find specific node kinds
const imports = rootNode.findAll({
rule: { kind: 'import_statement' }
});
Creating Edits
Build transformation edits:
for (const node of nodes) {
const arg = node.getMatch('ARG');
if (arg) {
// Replace util.isArray(x) with Array.isArray(x)
edits.push(node.replace(`Array.isArray(${arg.text()})`));
}
}
Committing Changes
Apply all edits and return the transformed code:
if (!edits.length) return null;
return rootNode.commitEdits(edits);
Always return null when no changes are made. This signals to the codemod runner that the file should not be modified.
Real-World Example
Here’s a simplified version of the util.types.isNativeError to Error.isError migration:
export default function transform(root: SgRoot<JS>): string | null {
const rootNode = root.root();
const edits: Edit[] = [];
// Find all util imports/requires
const statements = [
...getNodeRequireCalls(root, 'util'),
...getNodeImportStatements(root, 'util'),
];
if (!statements.length) return null;
for (const stmt of statements) {
// Resolve how types.isNativeError is accessed locally
const binding = resolveBindingPath(stmt, '$.types.isNativeError');
if (!binding) continue;
// Find all calls using this binding
const calls = rootNode.findAll({
rule: { pattern: binding }
});
// Replace with Error.isError
for (const call of calls) {
edits.push(call.replace('Error.isError'));
}
}
if (!edits.length) return null;
return rootNode.commitEdits(edits);
}
This example demonstrates:
- Import detection: Finding where
util is imported or required
- Binding resolution: Determining how
types.isNativeError is accessed (could be util.types.isNativeError, types.isNativeError, or isNativeError depending on the import style)
- Pattern matching: Locating all usages of the resolved binding
- Replacement: Transforming to the new API
Codemod Utilities
The @nodejs/codemod-utils package provides battle-tested helpers for common operations:
Import/Require Detection
import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies';
// Finds all import/require statements for 'fs'
const fsImports = getModuleDependencies(root, 'fs');
Binding Resolution
import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path';
// Given: const { readFile } = require('fs')
// Returns: 'readFile'
const binding = resolveBindingPath(node, '$.readFile');
// Given: const fs = require('fs')
// Returns: 'fs.readFile'
const binding = resolveBindingPath(node, '$.readFile');
Cleanup Operations
import { removeBinding } from '@nodejs/codemod-utils/ast-grep/remove-binding';
import { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines';
// Remove unused import binding
const result = removeBinding(importNode, 'isArray');
if (result?.edit) edits.push(result.edit);
// Remove entire lines (like unused imports)
const cleanCode = removeLines(sourceCode, linesToRemove);
Always use the utility functions from @nodejs/codemod-utils instead of writing your own. They handle edge cases like destructuring, aliasing, and dynamic imports.
Workflow Structure
Each codemod follows a standard workflow:
// 1. Import utilities and types
import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies';
import type { SgRoot, Edit } from "@codemod.com/jssg-types/main";
import type JS from "@codemod.com/jssg-types/langs/javascript";
// 2. Define the transform function
export default function transform(root: SgRoot<JS>): string | null {
const rootNode = root.root();
const edits: Edit[] = [];
// 3. Find relevant imports/requires
const imports = getModuleDependencies(root, 'module-name');
if (!imports.length) return null;
// 4. Locate and transform usage patterns
for (const importNode of imports) {
// ... find usages and build edits
}
// 5. Apply edits and return result
if (!edits.length) return null;
return rootNode.commitEdits(edits);
}
- Early exit: Return
null immediately if the file doesn’t contain relevant code
- Batch edits: Collect all edits before calling
commitEdits() once
- Efficient patterns: Use specific patterns to avoid searching the entire AST
// ❌ Inefficient: Commits after each edit
for (const node of nodes) {
const newCode = rootNode.commitEdits([node.replace(newText)]);
}
// ✅ Efficient: Batch all edits
const edits: Edit[] = [];
for (const node of nodes) {
edits.push(node.replace(newText));
}
return rootNode.commitEdits(edits);
Next Steps
When to Use Codemods
Learn when codemods are the right choice for your migration
Safety Best Practices
Ensure safe, reliable code transformations
Additional Resources