> ## Documentation Index
> Fetch the complete documentation index at: https://docs.codemod.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Advanced Patterns

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:

```ts theme={null}
import type { Codemod } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

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

export default codemod;
```

### 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.
* `options.targetDir: string` — absolute target directory for the current run. Use it when you need the root path for resolving sibling files or reporting paths.

```ts theme={null}
import type { Codemod } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

const codemod: Codemod<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
  const targetDir = options.targetDir;
  return null;
};

export default codemod;
```

<Info>
  `options.matches` contains nodes that matched your selector (when `getSelector` is exported). Use them to short‑circuit or to avoid recomputing broad queries.
</Info>

<Info>
  When you only need a stable repo-relative path for the current file, prefer `root.relativeFilename()` over manually stripping `options.targetDir`.
</Info>

## 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.

```ts theme={null}
import { parseAsync } from "codemod:ast-grep";
import type { Codemod } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

const codemod: Codemod<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 codemod;
```

### 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.

<Info>
  **Enterprise Features**: In the [Codemod app](https://go.codemod.com/app), parameters are configured through a visual UI. The Codemod app provides centralized state management for large-scale refactoring across many repos.
</Info>

You can access parameters via `options.params`.

<CodeGroup>
  ```ts codemod.ts theme={null}
  import type { Codemod } from "codemod:ast-grep";
  import type TSX from "codemod:ast-grep/langs/tsx";

  const codemod: Codemod<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 codemod;
  ```

  ```yaml workflow.yaml theme={null}
  # --- Basic schema example ---
  version: "1"
  params:
    schema:
      library:
        name: "Library"
        description: "Internationalization library to use"
        type: string
        default: "next-intl"
      shardingMethod:
        name: "Sharding Method"
        type: string
        default: "directory"
      prSize:
        name: "PR Size"
        type: string
        default: "50"

  ---
  # --- Enum schema example (multi-document YAML) ---
  version: "1"
  params:
    schema:
      format:
        name: "Module format"
        type: string
        default: "esm"
        oneOf:
          - type: string
            enum: ["esm", "cjs"]
  ```
</CodeGroup>

<Tip>
  How to pass params when executing your workflow:
  <a href="/cli#param-param-key-value">Passing parameters (--param)</a>.
</Tip>

## 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`:

```ts theme={null}
import type { Codemod } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";    

const codemod: Codemod<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 codemod;
```

<Info>
  `options.matrixValues` is only defined when your workflow uses a matrix strategy. Otherwise it is `undefined`.
</Info>

<Tip>
  Prefer checking sharding against a project-relative path (e.g., `path.relative(process.cwd(), root.filename())`) to avoid mismatches across environments.
</Tip>

### Matrix Strategy Configuration

Matrix strategy is defined in your workflow:

```yaml workflow.yaml theme={null}
version: "1"
state:
  schema:
    shards:
      type: array
      items:
        type: object
        properties:
          team: { type: string }
          shard: { type: string }
          shardId: { type: string }
```

<Note>
  **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](/workflows/reference#matrix-strategy) for details.
</Note>

### 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`

```ts theme={null}
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)" } ] }
});
```

<Info>
  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.
</Info>

## 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

```ts theme={null}
import { setState, getState, unsetState, acquireLock } from "codemod:workflow";
```

<ParamField path="setState" type="<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.
</ParamField>

<ParamField path="getState" type="<T>(name: string) => T | undefined">
  Gets a named state value, or `undefined` if it doesn't exist.
</ParamField>

<ParamField path="unsetState" type="(name: string) => void">
  Removes a named state value. If the key was persisted, a removal is propagated to persistent storage.
</ParamField>

<ParamField path="acquireLock" type="(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.
</ParamField>

### Basic Usage

State is shared across all files processed in parallel within a single step:

```ts codemod.ts theme={null}
import type { Codemod } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";
import { setState, getState, acquireLock } from "codemod:workflow";

const codemod: Codemod<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 codemod;
```

### When to Use Locks

Use `acquireLock` when you need to read-modify-write shared state from parallel threads. Without it, concurrent `getState` → `setState` sequences can overwrite each other:

```ts theme={null}
// 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();
}
```

<Warning>
  Always call `release()` in a `finally` block. Failing to release a lock will block all other threads waiting on that key.
</Warning>

<Info>
  Simple `setState` calls that don't depend on the current value (e.g., `setState("status", "done")`) don't need locking — they are individually atomic.
</Info>

### 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:

<CodeGroup>
  ```yaml workflow.yaml theme={null}
  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
  ```

  ```ts scripts/scan.ts theme={null}
  import type { Codemod } from "codemod:ast-grep";
  import type TSX from "codemod:ast-grep/langs/tsx";
  import { setState, getState, acquireLock } from "codemod:workflow";

  const codemod: Codemod<TSX> = async (root) => {
    // Collect file paths that match criteria
    const release = acquireLock("files");
    try {
      const files = getState<string[]>("filesToProcess") ?? [];
      files.push(root.filename());
      setState("filesToProcess", files); // persisted by default
    } finally {
      release();
    }
    return null;
  };

  export default codemod;
  ```

  ```ts scripts/process.ts theme={null}
  import { getState } from "codemod:workflow";

  // This runs in a subsequent "run" step via jssg exec.
  // State from the previous js-ast-grep step is available.
  const files = getState<string[]>("filesToProcess") ?? [];
  console.log(`Processing ${files.length} files`);
  ```
</CodeGroup>

### 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):

```ts theme={null}
setState("processingQueue", queue, false); // not persisted
```

## Runtime Hooks

The `codemod:runtime` module lets a codemod send structured execution signals to the engine without relying on `console.log` parsing.

```ts theme={null}
import runtime from "codemod:runtime";
```

Use runtime hooks when you want to:

* show meaningful progress in workflow task logs
* warn without failing the task
* improve watchdog diagnostics by naming the active unit or file
* fail the current step explicitly instead of returning a generic exception
* stop cooperatively when the task has been canceled

### API Reference

<ParamField path="progress" type="(message: string, meta?: RuntimeMeta) => void">
  Emits a structured progress event. Progress updates appear in task logs and refresh the step heartbeat so long-running transforms can prove they are still alive.
</ParamField>

<ParamField path="warn" type="(message: string, meta?: RuntimeMeta) => void">
  Emits a non-fatal warning event. Warnings are appended to task logs but do not change task state.
</ParamField>

<ParamField path="setCurrentUnit" type="(unitId: string, meta?: RuntimeMeta) => void">
  Overrides the current logical execution unit for diagnostics. For file-based transforms, this is usually the relative file path.
</ParamField>

<ParamField path="failFile" type="(message: string, meta?: RuntimeMeta) => never">
  Fails the current file or unit immediately. The runtime currently treats this as terminal for the running task.
</ParamField>

<ParamField path="failStep" type="(message: string, meta?: RuntimeMeta) => never">
  Fails the current step immediately. This is always terminal for the running task.
</ParamField>

<ParamField path="isCanceled" type="() => boolean">
  Returns `true` if the current task has been canceled. Use it to stop long-running work cooperatively.
</ParamField>

### Example: Report progress and fail explicitly

```ts codemod.ts theme={null}
import type { Codemod } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";
import runtime from "codemod:runtime";

const codemod: Codemod<TSX> = async (root) => {
  const relativeFile = root.filename();

  runtime.setCurrentUnit(relativeFile);
  runtime.progress("Scanning file");

  try {
    // Your transform logic here
    runtime.progress("Generating translation keys", { file: relativeFile });
    return null;
  } catch (error) {
    runtime.failStep(`Transform failed for ${relativeFile}`, {
      file: relativeFile,
      cause: error instanceof Error ? error.message : String(error),
    });
  }
};

export default codemod;
```

### When to use hooks vs exceptions

* Use `runtime.progress(...)` and `runtime.warn(...)` for structured observability.
* Use `runtime.failStep(...)` when the codemod has decided the task should stop immediately.
* Plain thrown errors still fail the task if they escape the codemod, but hooks give better logs and clearer intent.
* The engine's idle timeout is only a backstop for silent hangs. Hooks are the preferred way to report semantic failures.

<Warning>
  `codemod:runtime` requires a runtime version that supports runtime hooks. If a codemod must run in mixed-version environments, prefer a dynamic import with a clear upgrade error message when the module is unavailable.
</Warning>

## 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.

```ts theme={null}
import type { Codemod } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";

const codemod: Codemod<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 codemod;
```

## 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.

```ts theme={null}
import { jssgTransform } from "codemod:ast-grep";
import type { Codemod, 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: Codemod<CSS> = async (root) => {
  // Rename the file from .less to .css
  root.rename(root.filename().replace('.less', '.css'));
  // Optionally transform the content
  return transformedContent;
};

const codemod: Codemod<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 codemod;
```

<Info>
  `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.
</Info>

### `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`).

```ts theme={null}
const codemod: Codemod<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](/jssg/reference#file-renaming) 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.

```ts theme={null}
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.

```ts theme={null}
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.

```ts theme={null}
// 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 codemod: Codemod<TSX> = async (root, options) => {}` with `export default codemod`**
2. **Import proper types** - use `Codemod`, `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

<Tip>
  If you're working on a large-scale enterprise migration, feel free to [reach out to us](https://go.codemod.com/contact) to learn more about pro and enterprise plans and features.
</Tip>
