Skip to main content
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:
1

Parse

Your source code is parsed into an AST representation
2

Analyze

The codemod searches for specific patterns in the AST
3

Transform

Matching nodes are replaced with updated code
4

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:
  1. Import detection: Finding where util is imported or required
  2. Binding resolution: Determining how types.isNativeError is accessed (could be util.types.isNativeError, types.isNativeError, or isNativeError depending on the import style)
  3. Pattern matching: Locating all usages of the resolved binding
  4. 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);
}

Performance Considerations

  • 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

Build docs developers (and LLMs) love