Skip to main content

Quick Reference

Core Types

  • SgRoot<L> - Represents a parsed file
  • SgNode<L> - Represents any AST node
  • Edit - Single text replacement
  • RuleConfig - Pattern matching configuration

Essential Methods

  • root.root() - Get root AST node
  • root.filename() - Get file path
  • node.find(rule) - Find first match
  • node.findAll(rule) - Find all matches
  • node.replace(text) - Create edit
  • node.commitEdits(edits) - Apply edits

Runtime and built-ins

jssg is a JavaScript runtime similar to Node.js with most Node globals/modules (for example, fs, process). It is powered by QuickJS, uses LLRT for Node module compatibility, and oxc for module resolution. ast-grep is available as a built-in module. See Parse Helpers for parsing functions.

Transform Function

Every jssg codemod exports a transform function:
import type { SgRoot, SgNode } from "@codemod.com/jssg-types/main";
import type TSX from "codemod:ast-grep/langs/tsx";

export default function transform(
  root: SgRoot<TSX>, 
  options: {
    language: string;
    params?: Record<string, string>;
    matches?: SgNode[];
    matrixValues?: Record<string, unknown>;
  }
): string | null {
  // Your transformation logic
}
Types vs values: SgRoot and SgNode are TypeScript types (not runtime values). At runtime, the codemod:ast-grep module provides functions such as parse and parseAsync (see Parse Helpers).
Return values:
  • string - Modified code (if identical to input, treated as unmodified)
  • null - No changes needed
  • undefined - Same as null
  • Other types - Runtime error

SgRoot API

The root object provides access to the parsed file:
root()
SgNode
Get the root AST node of the file.
filename()
string
Get the file path or “anonymous” for ad-hoc parsing.
const rootNode = root.root();
const filePath = root.filename();

SgNode API

find(matcher)
SgNode | null
Find the first matching descendant node.
findAll(matcher)
SgNode[]
Find all matching descendant nodes.
parent()
SgNode | null
Get the parent node.
children()
SgNode[]
Get all child nodes.
child(index)
SgNode | null
Get child at specific index.
next()
SgNode | null
Get next sibling node.
nextAll()
SgNode[]
Get all following sibling nodes.
prev()
SgNode | null
Get previous sibling node.
prevAll()
SgNode[]
Get all previous sibling nodes.
ancestors()
SgNode[]
Get all ancestor nodes up to root.
getRoot()
SgRoot
Get the root SgRoot object.

Node Properties

text()
string
Get the text content of this node.
kind()
string
Get the node type (e.g., “function_declaration”, “arrow_function”).
range()
Range
Get the source position range.
is(kind)
boolean
Check if node is of specific type.
isLeaf()
boolean
Check if node has no children.
isNamed()
boolean
Check if node is a named AST node.
isNamedLeaf()
boolean
Check if node is a named leaf node.
id()
number
Get the unique identifier of this node.

Field Access

field(name)
SgNode | null
Get first child in named field.
fieldChildren(name)
SgNode[]
Get all children in named field.
// Access function parameters
const params = node.field("parameters");
const firstParam = params?.child(0);

// Check node type
if (node.is("function_declaration")) {
  const name = node.field("name")?.text();
}

Pattern Matching

matches(matcher)
boolean
Test if current node matches a pattern.
inside(matcher)
boolean
Check if node is inside a matching ancestor.
has(matcher)
boolean
Check if node has matching descendant.
precedes(matcher)
boolean
Check if node precedes a matching sibling.
follows(matcher)
boolean
Check if node follows a matching sibling.

Capture Methods

getMatch(name)
SgNode | null
Get captured node by name from pattern.
getMultipleMatches(name)
SgNode[]
Get all captured nodes with same name.
getTransformed(name)
string | null
Get transformed text of captured node.

Editing Methods

replace(text)
Edit
Create a replacement edit for this node.
commitEdits(edits)
string
Apply array of edits and return new code.

Pattern Matching

jssg uses ast-grep patterns to find code structures. Patterns are more powerful than regex because they understand code syntax.

Basic Patterns

// Find function calls
{ pattern: "console.log($ARG)" }

// Find variable declarations
{ pattern: "const $NAME = $VALUE" }

// Find imports
{ pattern: "import $NAME from $SOURCE" }

Pattern Syntax

  • $NAME - Capture a single node (e.g., $ARG, $NAME)
  • $$$ARGS - Capture multiple nodes (e.g., $$$ARGS, $$$PARAMS)
  • $... - Match any number of nodes
  • $NAME:kind - Capture only specific node types

Rule Configuration

Rules are authored as plain JavaScript objects with the same semantics as ast-grep’s YAML rule config. See: ast‑grep YAML Rule Config and Rule Config Guide.
// Simple pattern
{ rule: { pattern: "console.log($ARG)" } }

// Multiple patterns
{ rule: { 
  any: [
    { pattern: "console.log($ARG)" },
    { pattern: "console.warn($ARG)" }
  ]
}}

// With node type restriction
{ rule: { 
  pattern: "$FUNC($$$ARGS)",
  kind: "call_expression"
}}

// With regex constraint
{ rule: {
  pattern: "$HOOK($$$ARGS)",
  where: {
    HOOK: { regex: "^use[A-Z]" }
  }
}}

Relational Patterns

// Find nodes inside other nodes
{ rule: { 
  pattern: "console.log($ARG)",
  inside: { pattern: "function $NAME($$$PARAMS)" }
}}

// Find nodes that have specific children
{ rule: {
  pattern: "function $NAME($$$PARAMS)",
  has: { pattern: "console.log($$$ARGS)" }
}}

Advanced Pattern Composition

Rule References

Use matches to reference named rules defined in utils:
const myRule: RuleConfig<TSX> = {
  rule: {
    matches: "jsx-element-hardcoded-human-language",
    inside: { pattern: "function $NAME()" }
  },
  utils: {
    "jsx-element-hardcoded-human-language": {
      any: [
        { kind: "jsx_element" },
        { kind: "jsx_self_closing_element" }
      ]
    }
  }
};

Constraint System

Use constraints to define reusable rule patterns and reference them with matches:
const stringLikeRule: RuleConfig<TSX> = {
  constraints: {
    STR: {
      any: [{ kind: "string" }, { kind: "template_string" }]
    },
    NUM: {
      kind: "number"
    }
  },
  utils: {
    "string-like": {
      any: [
        { matches: "STR" },
        { pattern: "$STR + $NUM" }
      ]
    }
  },
  rule: {
    matches: "string-like"
  }
};

Traversal Control

Use stopBy to control how deep the search traverses:
// Search only immediate neighbors
{
  has: {
    stopBy: "neighbor",
    kind: "jsx_attribute"
  }
}

// Search until end of parent
{
  inside: {
    stopBy: "end",
    pattern: "function $NAME()"
  }
}

// Stop at specific node types
{
  inside: {
    stopBy: {
      not: {
        kind: "variable_declarator"
      }
    },
    pattern: "target"
  }
}

Select (optional)

Use a selector to skip files that don’t contain your target shape:
export function getSelector() {
  return { rule: { any: [{ pattern: "console.log($ARG)" }, { pattern: "console.debug($ARG)" }] } };
}
When a selector is provided, the engine pre‑computes matches; if none are found, the file is skipped (your transform won’t be called).

Basic Example

Here’s a complete example that replaces console.* calls with logger.*:
import type { SgRoot, SgNode } from "@codemod.com/jssg-types/main";
import type TSX from "codemod:ast-grep/langs/tsx";

export default function transform(root: SgRoot<TSX>): string | null {
  const rootNode = root.root();
  
  // Find all console.* calls
  const matches = rootNode.findAll({
    rule: { 
      any: [
        { pattern: "console.log($$$ARGS)" },
        { pattern: "console.warn($$$ARGS)" },
        { pattern: "console.error($$$ARGS)" }
      ]
    }
  });
  
  if (matches.length === 0) {
    return null; // No changes needed
  }
  
  // Create edits
  const edits = matches.map((node: SgNode<TSX>) => {
    const callee = node.field("function");
    const method = callee?.field("property")?.text() || "log";
    const args = node.getMultipleMatches("ARGS")
      .map(arg => arg.text())
      .join(", ");
    
    return node.replace(`logger.${method}(${args})`);
  });
  
  return rootNode.commitEdits(edits);
}
Before:
function example() {
  console.log("Hello");
  console.warn("Warning");
}
After:
function example() {
  logger.log("Hello");
  logger.warn("Warning");
}

Types

Core Types

Edit
{ startPos: number; endPos: number; insertedText: string }
Single text replacement range.
Position
{ line: number; column: number; index: number }
Source position.
Range
{ start: Position; end: Position }
Source span.

Edit Interface

interface Edit {
  /** The start position of the edit */
  startPos: number;
  /** The end position of the edit */
  endPos: number;
  /** The text to be inserted */
  insertedText: string;
}
Creating edits:
// Manual creation
const edit: Edit = {
  startPos: node.range().start.index,
  endPos: node.range().end.index,
  insertedText: "new code"
};

// Using node.replace() (recommended)
const edit = node.replace("new code");

Best Practices

1. Be Explicit with Transformations Always include explicit checks before accessing node properties:
// ✅ Good: Explicit validation
const param = node.field("parameters")?.child(0);
if (!param || !param.is("required_parameter")) {
  return null;
}

// ❌ Avoid: Assumptions without checks
const param = node.field("parameters").child(0); // May throw
2. Handle Edge Cases Gracefully Your codemod should handle various code styles:
// Handle different import styles
const imports = rootNode.findAll({
  rule: {
    any: [
      { pattern: "import $NAME from $SOURCE" }, // Default import
      { pattern: "import { $$$NAMES } from $SOURCE" }, // Named imports
      { pattern: "import * as $NAME from $SOURCE" }, // Namespace import
      { pattern: "const $NAME = require($SOURCE)" }, // CommonJS
    ],
  },
});
3. Use Type-Safe Patterns Leverage TypeScript’s type system:
import type { SgNode } from "@codemod.com/jssg-types/main";
import type TSX from "codemod:ast-grep/langs/tsx";

// Type-safe node type checking
function isReactComponent(
  node: SgNode<TSX>
): node is SgNode<TSX, "function_declaration" | "arrow_function"> {
  if (!isFunctionLike(node)) return false;
  
  // Check if it returns JSX
  const returnStatements = node.findAll({
    rule: { pattern: "return $EXPR" },
  });
  
  return returnStatements.some((ret) => {
    const expr = ret.getMatch("EXPR");
    return expr?.kind() === "jsx_element" || expr?.kind() === "jsx_fragment";
  });
}
4. Write Idempotent Transforms Match only the pre-change shape to avoid re-editing:
// ✅ Good: Matches original shape
{ pattern: "console.log($ARG)" }

// ❌ Avoid: Matches already transformed code
{ pattern: "logger.log($ARG)" }
5. Optimize Performance For large codebases:
export default function transform(root: SgRoot<TSX>): string | null {
  const rootNode = root.root();
  
  // Early return for non-applicable files
  if (!root.filename().endsWith(".tsx")) {
    return null;
  }
  
  // Single traversal for multiple patterns
  const edits: Edit[] = [];
  
  rootNode
    .findAll({
      rule: {
        any: [
          { pattern: "console.log($$$ARGS)" },
          { pattern: "console.warn($$$ARGS)" },
          { pattern: "console.error($$$ARGS)" },
        ],
      },
    })
    .forEach((node) => {
      // Process all patterns in one pass
      edits.push(node.replace(`logger.${method}(${args})`));
    });
  
  return edits.length > 0 ? rootNode.commitEdits(edits) : null;
}

Parse Helpers

Use parse/parseAsync for ad‑hoc checks, small tools, or tests outside a workflow:
parse(lang, src)
SgRoot
Parse raw source into an SgRoot.
parseAsync(lang, src)
Promise<SgRoot
Async variant of parse.
import { parse } from "codemod:ast-grep";

export default function main() {
  const root = parse("tsx", "<div>{fn(value)}</div>");
  const count = root.root().findAll({ rule: { pattern: "fn($X)" } }).length;
  console.log("matches:", count);
  return null;
}

Async Transformations

jssg supports async operations for complex transformations:
export default async function transform(root: SgRoot<TSX>): Promise<string | null> {
  const filePath = root.filename();
  
  // Async file system operations
  const config = await loadProjectConfig(filePath);
  
  // Async API calls or analysis
  const metadata = await analyzeImports(root.root());
  
  // Use gathered data in transformation
  const edits = await generateEdits(root.root(), config, metadata);
  
  return root.root().commitEdits(edits);
}
Common async patterns:
  • Loading configuration files
  • Analyzing project structure
  • Making API calls for metadata
  • Cross-file analysis

Advanced Patterns

Multi-File Context

Access information from other files in your project:
import { findProjectRoot, analyzeExports } from "./utils/project-analysis";

export default async function transform(root: SgRoot<TSX>): Promise<string | null> {
  const projectRoot = await findProjectRoot(root.filename());
  
  // Analyze exports from related files
  const componentExports = await analyzeExports(projectRoot, "src/components");
  
  // Use cross-file information in transformation
  const edits = transformImports(root.root(), componentExports);
  
  return root.root().commitEdits(edits);
}

Custom Node Matchers

Create reusable, complex matchers:
// utils/matchers.ts
export function createReactHookMatcher() {
  return {
    rule: {
      pattern: "$HOOK($$$ARGS)",
      where: {
        HOOK: {
          regex: "^use[A-Z]", // Matches useEffect, useState, etc.
        },
      },
    },
  };
}

// Usage in codemod
const hooks = rootNode.findAll(createReactHookMatcher());

Real-world Example: Next.js Route Props

Here’s a complete example that adds type annotations to Next.js page components:
import type { SgRoot, SgNode } from "@codemod.com/jssg-types/main";
import type TSX from "codemod:ast-grep/langs/tsx";

export default function transform(root: SgRoot<TSX>): string | null {
  const rootNode = root.root();

  // Find the default export
  const defaultExport = rootNode.find({
    rule: { pattern: "export default $COMPONENT" }
  });
  
  if (!defaultExport) return null;

  const component = defaultExport.getMatch("COMPONENT");
  if (!component.is("arrow_function") && !component.is("function_declaration")) {
    return null; // Not a function component
  }

  // Check for parameters that need typing
  const params = component.field("parameters");
  const secondParam = params?.child(1);
  
  if (!secondParam?.is("required_parameter")) {
    return null; // No second parameter
  }

  // Check if already has type annotation
  const typeAnnotation = secondParam.field("type")?.child(1);
  if (typeAnnotation) {
    return null; // Already typed
  }

  // Determine component type based on filename
  const filePath = root.filename();
  const isLayout = filePath.endsWith("/layout.tsx");
  const typeName = isLayout ? "LayoutProps" : "PageProps";

  // Add type annotation
  const edit = secondParam.replace(`${secondParam.text()}: ${typeName}`);
  return rootNode.commitEdits([edit]);
}
This example demonstrates:
  • Type guards: Checking node types before operations
  • Field access: Safely accessing AST fields with optional chaining
  • Conditional logic: Different behavior based on file context
  • Complex patterns: Finding and transforming specific code structures
  • Real-world patterns: Practical Next.js-specific transformations

Performance Optimization

For large codebases, optimize your traversal:
export default function transform(root: SgRoot<TSX>): string | null {
  const rootNode = root.root();
  
  // Early return for non-applicable files
  if (!root.filename().endsWith(".tsx")) {
    return null;
  }
  
  // Single traversal for multiple patterns
  const edits: Edit[] = [];
  
  rootNode
    .findAll({
      rule: {
        any: [
          { pattern: "console.log($$$ARGS)" },
          { pattern: "console.warn($$$ARGS)" },
          { pattern: "console.error($$$ARGS)" },
        ],
      },
    })
    .forEach((node) => {
      const callee = node.field("function");
      const method = callee?.field("property")?.text() || "log";
      const args = node.getMultipleMatches("ARGS")
        .map(arg => arg.text())
        .join(", ");
      edits.push(node.replace(`logger.${method}(${args})`));
    });
  
  return edits.length > 0 ? rootNode.commitEdits(edits) : null;
}
Optimization tips:
  • Early returns to skip non-applicable files
  • Single traversal for multiple patterns
  • Batch all edits before committing
  • Use specific patterns over generic ones
// Range‑aware decision: skip very long calls
export default function transform(root: any) {
  const node = root.root().find({ rule: { pattern: "longCall($A, $B, $C, $D)" } });
  if (!node) return null;
  const r = node.range();
  if (r.end.column - r.start.column > 120) return null;
  return root.root().commitEdits([node.replace("shortCall()")]);
}

Decision Guides

Use getSelector to skip entire files quickly; use find/findAll inside files to select nodes precisely.
Prefer pattern for clarity, add kind for stricter scopes, use relations for context, reserve regex as a last resort.
Return null to indicate “skipped”; return the same string when edits result in identical output — both are treated as unmodified by the engine.

Troubleshooting

Issue: Your codemod runs but doesn’t make changes.

Debug steps:
Issue: TypeScript compilation errors.

Solution: Ensure proper type imports and annotations:
Issue: Tests fail unexpectedly.

Debug with verbose output:

Update snapshots if changes are intended:
Issue: Patterns don’t match expected code.

Use the ast-grep playground to test patterns:
  1. Visit https://ast-grep.github.io/playground/
  2. Paste your code sample
  3. Test different patterns
  4. Use the AST viewer to understand structure
For large codebases:
  1. Early returns: Skip files that don’t need transformation
  2. Efficient patterns: Use specific patterns over generic ones
  3. Batch operations: Collect all edits before committing
  4. Limit traversals: Combine related searches
Language kinds and editor IntelliSense are available via codemod:ast-grep/langs/*. E.g., import { TSX } from "codemod:ast-grep/langs/tsx".
I