Skip to main content
This guide covers advanced JSSG features that enable you to build more sophisticated and efficient codemods. You’ll learn how to parse code dynamically, optimize performance with selectors, use parameters for flexibility, and leverage matrix values for complex workflows.

Transform Function Contract

Every JSSG codemod must export a default function with this exact signature:
import type { Transform } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

const transform: Transform<TSX> = async (root, options) => {
  // Your transformation logic
};

export default transform;

Transform Options

The options object provides execution context:
  • options.language: string — language of the current file (e.g., ts, tsx). Use it to branch logic or choose language‑sensitive patterns.
  • options.params?: Record<string, string> — user parameters defined in the workflow. Drive behavior switches, defaults, and environment‑specific choices.
  • options.matches?: any[] — pre‑filtered nodes from the exported selector (if any). Use to skip broad queries inside the transform.
  • options.matrixValues?: Record<string, unknown> — values from matrix strategy execution. Use for sharding, multi‑variant runs, or team‑scoped processing.
import type { Transform } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

const transform: Transform<TSX> = async (root, options) => {
  const lang = options.language;
  const params = options.params;
  const preFilteredMatches = options.matches; // may be undefined if no selector
  const matrix = options.matrixValues; // undefined without matrix strategy
  return null;
};

export default transform;
options.matches contains nodes that matched your selector (when getSelector is exported). Use them to short‑circuit or to avoid recomputing broad queries.

Dynamic Parsing

Dynamic parsing allows you to parse and analyze different types of code within a single transform. This is particularly powerful when working with embedded languages like CSS-in-JS, HTML templates, or SQL queries within JavaScript code. Use dynamic parsing when your codemod needs to analyze multiple languages or when you’re working with template literals containing different syntax.
import type { parseAsync, GetParser, Transform } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

const transform: Transform<TSX> = async (root) => {
  // Find all styled-component template literals
  const styledComponents = root.root().findAll({
    rule: { pattern: "styled.$COMPONENT`$ARG`" }
  });

  for (const component of styledComponents) {
    const cssText = component.getMatch("ARG")?.text();
    if (!cssText) continue;

    // Parse the CSS content dynamically
    const cssRoot = await parseAsync("css", cssText);

    // Find vendor-prefixed properties that have modern equivalents
    const vendorPrefixedDeclarations = cssRoot.findAll({
      rule: {
        kind: "declaration",
        has: {
          kind: "property_name",
          regex: "^(-webkit-|-moz-|-ms-|-o-)"
        },
        inside: {
          kind: "block",
          has: {
            kind: "declaration",
            has: {
              kind: "property_name",
              regex: "^(border-radius|box-shadow|transition|transform|background|flex-direction)$"
            }
          }
        }
      },
    });

    // Log findings for analysis
    console.log(
      "Found vendor prefixes:",
      vendorPrefixedDeclarations.map((node) => node.text())
    );
  }

  return null;
};

export default transform;

Return Semantics

  • string: Modified code (if identical to input, treated as unmodified)
  • null: No changes needed
  • undefined: Same as null
  • Other types: Runtime error

Parameters and Configuration

Parameters make your codemods flexible and reusable by allowing you to pass configuration values at runtime. This enables you to create codemods that adapt their behavior based on user input or different execution contexts.
Enterprise Features: In the Codemod app, parameters are configured through a visual UI. The Codemod app provides centralized state management for large-scale refactoring across many repos.
You can access parameters via options.params.
import type { parseAsync, GetParser, Transform } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

const transform: Transform<TSX> = async (root, options) => {
  // Access user-configured parameters
  const library = options.params?.library || "next-intl";
  const shardingMethod = options.params?.shardingMethod || "directory";
  const prSize = options.params?.prSize || "50";
  
  // Use parameters in your transformation logic
  if (library === "react-i18next") {
    // Apply react-i18next specific transformations
  }
};

export default transform;
How to pass params when executing your workflow: Passing parameters (—param).

Matrix Strategy Integration

Matrix strategies enable you to run the same codemod with different configurations or to split large codebases into manageable chunks for parallel processing. Matrix values are particularly useful for:
  • Sharding: Distributing work across multiple processes or machines
  • Multi-variant transforms: Running the same logic with different parameters
  • Team-based processing: Applying different rules based on team ownership

Accessing Matrix Values

Access matrix values via options.matrixValues:
import type { parseAsync, GetParser, Transform } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";    

const transform: Transform<TSX> = async (root, options) => {
  // Access matrix values passed from the workflow (if matrix strategy is used)
  const team = (options.matrixValues as any)?.team;
  const shardId = (options.matrixValues as any)?.shardId;

  const filename = root.filename();

  // fitsInShard is an example sharding predicate provided by your orchestrator or custom code
  if (options.matrixValues && fitsInShard(filename, options.matrixValues)) {
    console.log(`Processing ${filename} for team ${team} in shard ${shardId}`);

    // Apply your transformations here and return committed edits
    // const edits = [...];
    // return root.root().commitEdits(edits);
  }

  // Skip files that don't belong to this shard (or when no matrix values are present)
  return null;
};

export default transform;
options.matrixValues is only defined when your workflow uses a matrix strategy. Otherwise it is undefined.
Prefer checking sharding against a project-relative path (e.g., path.relative(process.cwd(), root.filename())) to avoid mismatches across environments.

Matrix Strategy Configuration

Matrix strategy is defined in your workflow:
workflow.yaml
version: "1"
state:
  schema:
    shards:
      type: array
      items:
        type: object
        properties:
          team: { type: string }
          shard: { type: string }
          shardId: { type: string }
Parameters vs Matrix Values: options.params contains user-configured workflow settings, while options.matrixValues contains values from matrix strategy (e.g., team, shard, shardId from the shards state array). See Matrix Strategy for details.

Selectors (getSelector)

By default, JSSG processes every file in your project, which can be inefficient for large codebases. When a selector is exported, you can pre-filter files based on specific patterns, so that the engine only calls your transform for files that match it.
  • JSSG scans files using the selector before calling your transform
  • Files without matches are skipped, reducing work
  • Matched nodes are available via options.matches
import type { GetSelector } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

export const getSelector: GetSelector<TSX> = () => ({
  rule: { any: [ { pattern: "console.log($$$ARGS)" }, { pattern: "console.warn($$$ARGS)" } ] }
});
If the selector finds no matches in a file, your transform is not invoked for that file. This reduces both file processing and runtime initialization overhead. Prefer selectors for broad filtering; use find/findAll inside the transform for precise node selection.

State Management

The codemod:workflow module provides shared state that is accessible across all parallel file executions within a step, and can be persisted across workflow steps.

API Reference

import { setState, getState, unsetState, acquireLock } from "codemod:workflow";
setState
<T>(name: string, value: T, persist?: boolean) => void
Sets a named state value. Values are stored as native JavaScript objects (no manual JSON serialization needed). When persist is true (the default), the value is saved at the end of the execution batch and available to subsequent workflow steps.
getState
<T>(name: string) => T | undefined
Gets a named state value, or undefined if it doesn’t exist.
unsetState
(name: string) => void
Removes a named state value. If the key was persisted, a removal is propagated to persistent storage.
acquireLock
(name: string) => () => void
Acquires a named mutex lock. Returns a release function that must be called when done. While held, other threads’ getState, setState, and unsetState calls on the same key block until released.

Basic Usage

State is shared across all files processed in parallel within a single step:
codemod.ts
import type { Transform } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";
import { setState, getState, acquireLock } from "codemod:workflow";

const transform: Transform<TSX> = async (root) => {
  const rootNode = root.root();
  const imports = rootNode.findAll({ rule: { kind: "import_statement" } });

  // Safely accumulate results across parallel file executions
  const release = acquireLock("results");
  try {
    const results = getState<string[]>("importedFiles") ?? [];
    results.push(root.filename());
    setState("importedFiles", results);
  } finally {
    release();
  }

  return null;
};

export default transform;

When to Use Locks

Use acquireLock when you need to read-modify-write shared state from parallel threads. Without it, concurrent getStatesetState sequences can overwrite each other:
// Without lock — RACE CONDITION:
// Thread A reads count=0, Thread B reads count=0,
// both write count=1 instead of count=2
const count = getState<number>("count") ?? 0;
setState("count", count + 1);

// With lock — SAFE:
const release = acquireLock("count");
try {
  const count = getState<number>("count") ?? 0;
  setState("count", count + 1);
} finally {
  release();
}
Always call release() in a finally block. Failing to release a lock will block all other threads waiting on that key.
Simple setState calls that don’t depend on the current value (e.g., setState("status", "done")) don’t need locking — they are individually atomic.

Persisting State Across Workflow Steps

State set with persist: true (the default) is automatically saved after a js-ast-grep step completes. Subsequent steps in the same workflow node can read this state:
nodes:
  - id: analyze
    steps:
      - name: "Scan files"
        js-ast-grep:
          js_file: scripts/scan.ts
          language: "tsx"
      - name: "Process results"
        run: |
          codemod jssg exec $CODEMOD_PATH/scripts/process.ts

Transient State

Pass persist: false to keep state only for the current step execution (useful for in-memory coordination that doesn’t need to survive across steps):
setState("processingQueue", queue, false); // not persisted

Multi-Repo Orchestration

Use multi-repo orchestration when your codemod must coordinate changes across multiple repositories—such as updating shared libraries, enforcing consistency, or applying repo-specific logic. This approach streamlines large-scale migrations and ensures changes are applied uniformly.
import type { Transform } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

const transform: Transform<TSX> = async (root, options) => {
  const filePath = root.filename();
  const repoName = options.matrixValues?.repo_name;
  
  // Apply repo-specific transformations
  if (repoName === "frontend-app") {
    // Frontend-specific logic
  } else if (repoName === "backend-api") {
    // Backend-specific logic
  }
};

export default transform;

Multi-File Transforms

Sometimes a codemod needs to modify multiple files in a coordinated way. For example, renaming a .less file to .css and updating every import that references it. JSSG provides two mechanisms for this:

jssgTransform — Transform Secondary Files

Use jssgTransform() to apply a transform function to another file from within your main transform. The secondary file’s changes are collected and applied atomically alongside the primary file’s changes.
import { jssgTransform } from "codemod:ast-grep";
import type { Transform, Edit } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";
import type CSS from "codemod:ast-grep/langs/css";

const migrateStyles: Transform<CSS> = async (root) => {
  // Rename the file from .less to .css
  root.rename(root.filename().replace('.less', '.css'));
  // Optionally transform the content
  return transformedContent;
};

const transform: Transform<TSX> = async (root) => {
  const rootNode = root.root();
  const edits: Edit[] = [];

  // Find imports referencing .less files
  const lessImports = rootNode.findAll({
    rule: { pattern: "import $SOURCE" },
  });

  for (const imp of lessImports) {
    const source = imp.getMatch("SOURCE");
    if (!source?.text().includes(".less")) continue;

    const lessPath = source.text().slice(1, -1);

    // Transform the secondary file
    await jssgTransform(migrateStyles, lessPath, "css");

    // Update the import path in this file
    edits.push(source.replace(`"${lessPath.replace('.less', '.css')}"`));
  }

  return edits.length > 0 ? rootNode.commitEdits(edits) : null;
};

export default transform;
jssgTransform is a no-op in test mode — it returns null without reading or writing files. This means test cases only verify the primary transform. To test secondary transforms, write separate test cases for them.

root.rename() — Rename the Current File

Use root.rename(newPath) to rename the file currently being processed. This is useful for file extension conversions (.js.ts, .less.css, .cjs.mjs).
const transform: Transform<CSS> = async (root) => {
  // Rename .less → .css
  root.rename(root.filename().replace('.less', '.css'));

  // Return modified content, or null for rename-only
  return null;
};
Rules:
  • Relative paths resolve against the file’s parent directory.
  • Absolute paths are used as-is.
  • The resolved path must stay within the target directory.
  • rename() can only be called once per file.
See the API Reference for the full behavior matrix.

Advanced Pattern Composition

Rule References with Utils

Reference named sub‑rules in utils and attach them via matches to keep complex patterns DRY and composable.
import type { RuleConfig } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

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

Define reusable named fragments in constraints and reuse them across rules to centralize pattern logic.
import type { RuleConfig } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

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 bound relational searches (e.g., to immediate neighbors or the end of a parent) for correctness and performance.
// Search only immediate neighbors
{
  has: {
    stopBy: "neighbor",
    kind: "jsx_attribute"
  }
}

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

Best Practices

Consistent Patterns

  1. Use const transform: Transform<TSX> = async (root, options) => {} with export default transform
  2. Import proper types - use Transform, GetSelector, and language-specific types from codemod:ast-grep
  3. Handle edge cases - always check for null/undefined values and use try-catch for async operations
  4. Use early returns - skip processing when possible, especially for sharding
  5. Batch operations - collect edits before committing
  6. Export getSelector - provide a selector function for performance optimization
  7. Optimization strategies - prefer early returns, single traversals, and batching edits; use specific patterns to reduce backtracking and false positives

Enterprise Considerations

  • Idempotent transforms: Ensure transforms can be run multiple times safely
  • Progress reporting: Log progress for long-running migrations
  • Error handling: Gracefully handle unexpected input
  • Performance: Optimize for large codebases
  • Coordination: Use state for multi-repo coordination
If you’re working on a large-scale enterprise migration, feel free to reach out to us to learn more about pro and enterprise plans and features.