Skip to content
WhatsApp
AI StudioPricing
Development16 min read2026-09-06

Automated AST Refactoring: Writing Custom jscodeshift Codemods for Massive Codebases

Tired of manual refactoring? Learn how to inspect Abstract Syntax Trees, write custom jscodeshift codemods, automate breaking API migrations across 1,000+ files, and test your transforms with Vitest.

Marcus Vance
Staff Infrastructure Engineer
Automated AST Refactoring: Writing Custom jscodeshift Codemods for Massive Codebases

The Limits of Find-and-Replace in Enterprise Monorepos

Every software engineering organization eventually faces a massive breaking migration: deprecating a legacy Button component API, renaming a design token prop, or upgrading from TanStack Query v4 to v5 across 800 repository files.

Using regex search-and-replace for these refactors is perilous. Regex lacks contextual awareness: it cannot distinguish between a prop named 'size' on an Icon vs a 'size' prop on an Avatar, nor can it safely insert new import statements without corrupting whitespace.

Abstract Syntax Tree (AST) codemods solve this completely. A codemod parses source code into a structured tree representation, queries specific syntax nodes (such as JSXElement or ImportDeclaration), manipulates properties programmatically, and prints clean, formatted code back to disk.

In this guide, we learn how to inspect ASTs and write custom jscodeshift codemods that refactor hundreds of files in seconds.

Pro Tip: Use AST Explorer (astexplorer.net) with parser '@babel/preset-typescript' to inspect code syntax nodes interactively while authoring codemods.

How an Abstract Syntax Tree (AST) Represents Code

To a compiler, JavaScript code is not a string of text; it is a hierarchical tree of nodes:

A line like import { Button } from '@/components/ui' is parsed into an ImportDeclaration node, containing an ImportSpecifier (Button) and a StringLiteral source ('@/components/ui').

JSX like <Button variant='primary'>Click</Button> becomes a JSXElement node with JSXOpeningElement, JSXAttribute ('variant'), and JSXText children.

Once you understand this tree model, refactoring is simply tree traversal and node replacement.

Writing a Custom Codemod: Renaming Props Across Components

Let's author a practical codemod: our team is replacing the legacy prop isPrimary={true} on <Button> with the new modern design system prop variant='primary'.

transforms/migrateButtonProp.ts (jscodeshift Transform)
typescript
import type { API, FileInfo } from "jscodeshift";

export default function transformer(file: FileInfo, api: API) {
  const j = api.jscodeshift;
  const root = j(file.source);

  // 1. Find all JSX elements named 'Button'
  root.find(j.JSXElement, {
    openingElement: { name: { name: "Button" } },
  }).forEach((path) => {
    const { attributes } = path.node.openingElement;
    if (!attributes) return;

    // 2. Find and replace legacy 'isPrimary' prop
    path.node.openingElement.attributes = attributes.map((attr) => {
      if (
        attr.type === "JSXAttribute" &&
        attr.name.name === "isPrimary"
      ) {
        // Replace with variant="primary"
        return j.jsxAttribute(
          j.jsxIdentifier("variant"),
          j.stringLiteral("primary")
        );
      }
      return attr;
    });
  });

  // 3. Return formatted source code
  return root.toSource({ quote: "single" });
}

Automating Import Specifier Rewrites

Codemods can also update import paths. If Button was moved from '@/components/ui' to '@codexox/design-system', query ImportDeclaration nodes, verify that Button is imported, and rewrite the source literal cleanly.

transforms/migrateImports.ts (Rewriting Import Sources)
typescript
export function rewriteImport(j: any, root: any, componentName: string, newPath: string) {
  root.find(j.ImportDeclaration).forEach((path: any) => {
    const specifiers = path.node.specifiers;
    const hasComponent = specifiers?.some(
      (s: any) => s.local?.name === componentName
    );

    if (hasComponent) {
      path.node.source = j.stringLiteral(newPath);
    }
  });
}

Unit Testing Codemods with Vitest Snapshot Assertions

Never run an untested codemod across a 500,000-line monorepo. Author snapshot unit tests that feed input code snippets into your transform and assert that output matches expected targets.

Vitest provides instantaneous feedback, enabling developers to test complex JSX edge-cases, multiline comment preservation, and conditional expressions with complete confidence.

test/migrateButtonProp.test.ts (Vitest Codemod Suite)
typescript
import { describe, it, expect } from "vitest";
import jscodeshift from "jscodeshift";
import transformer from "../transforms/migrateButtonProp";

function applyTransform(inputSource: string): string {
  const api = {
    jscodeshift: jscodeshift.withParser("tsx"),
    j: jscodeshift.withParser("tsx"),
    stats: () => {},
    report: () => {},
  };
  return transformer({ source: inputSource, path: "test.tsx" }, api) || inputSource;
}

describe("migrateButtonProp AST codemod", () => {
  it("converts isPrimary={true} into variant='primary'", () => {
    const input = `<Button isPrimary={true}>Confirm Action</Button>;`;
    const output = applyTransform(input);
    expect(output).toBe(`<Button variant='primary'>Confirm Action</Button>;`);
  });

  it("leaves unrelated props intact without altering sibling formatting", () => {
    const input = `<Button isPrimary={true} size="large" onClick={handleClick}>Submit</Button>;`;
    const output = applyTransform(input);
    expect(output).toContain(`variant='primary'`);
    expect(output).toContain(`size="large"`);
  });
});

Running Codemods in Parallel Across Monorepos

Execute jscodeshift with multi-core parallelism using npx jscodeshift -t transforms/migrateButtonProp.ts src/ --extensions=ts,tsx --parser=tsx.

jscodeshift utilizes all available CPU cores to refactor thousands of files in under 10 seconds.

Complex Pattern Matching: Transforming Method Calls and State Hooks

Beyond simple JSX attribute replacements, AST transforms can refactor complex JavaScript logic. For instance, you can query CallExpression nodes to migrate legacy React class lifecycles (componentDidMount) or replace deprecated query client method calls with modern TanStack Query v5 syntax across an entire codebase.

Identify target MemberExpression identifiers (like queryClient.getQueryData), replace arguments with modern object signature shapes, and insert new import specifiers dynamically.

transforms/migrateQueryCall.ts (CallExpression Node Transform)
typescript
export function migrateQueryCall(j: any, root: any) {
  // Query: queryClient.getQueryData(['todos']) -> queryClient.getQueryData({ queryKey: ['todos'] })
  root.find(j.CallExpression, {
    callee: {
      object: { name: "queryClient" },
      property: { name: "getQueryData" },
    },
  }).forEach((path: any) => {
    const args = path.node.arguments;
    if (args.length === 1 && args[0].type === "ArrayExpression") {
      // Wrap bare array argument into { queryKey: [...] } object expression
      path.node.arguments = [
        j.objectExpression([
          j.objectProperty(j.identifier("queryKey"), args[0]),
        ]),
      ];
    }
  });
}
Frequently Asked Questions

What parser should be configured in jscodeshift for modern TypeScript and React?

Always specify --parser=tsx or configure babel-typescript in jscodeshift so that TypeScript generics and JSX syntax are parsed without syntax errors.

How does jscodeshift preserve original code formatting and comments?

jscodeshift uses recast under the hood, which performs non-destructive token tracking to preserve indentation, semicolons, and comments on unmodified lines.

Can codemods break git blame history?

Yes, large automated refactors create sweeping diffs. Teams should add the codemod commit SHA to a .git-blame-ignore-revs file to keep git blame pristine.

Can codemods format code with Prettier automatically?

Yes. It is common practice to run prettier --write on changed files immediately after the jscodeshift run completes.

What is the difference between jscodeshift and ts-morph?

jscodeshift is fast, lightweight, and focused on AST transforms across any JS/TS code. ts-morph wraps the full TypeScript type-checker compiler API, allowing you to query semantic types (like type-of variable).

How can developers safely preview codemod changes before modifying disk files?

Pass the --dry flag to jscodeshift (npx jscodeshift -d -p -t transform.ts src/) to print diff previews to the terminal without writing changes to disk.

How fast is jscodeshift on multi-thousand file enterprise codebases?

Because jscodeshift forks worker processes across all physical CPU threads, it typically processes 1,000 files in under 6 seconds, making it orders of magnitude faster than manual refactoring.

Topics:#AST Refactoring#jscodeshift#Codemods#Abstract Syntax Tree#TypeScript Tooling#Codebase Modernization#Automated Migration#Babel AST

Want this thinking applied to your product?

Spin up a prototype in minutes, or brief our team and get a scoped plan within 24 hours.