Comparing ast-grep and jscodeshift

Comparing ast-grep and jscodeshift

If you’ve ever had to update thousands of files for a framework migration, API rename, or design-system refresh, you already know the pain: regex is too fragile, and manual edits are too slow.

Two tools show up over and over in these conversations: ast-grep and jscodeshift.

Both are good. Both can save days (or weeks). But they have very different ergonomics and strengths.

This guide is a practical, “what should I actually use?” comparison based on real codemod workflows.

TL;DR

  • Use jscodeshift when you need deep JavaScript/TypeScript transforms and want full control in JS code.
  • Use ast-grep when you want speed, multi-language matching, and easier pattern-first rules.
  • Important nuance: ast-grep is not just YAML rules. It also ships with a Node API (N-API binding), so you can write codemods in JavaScript/TypeScript instead of YAML when that feels more natural.
  • If your transformation crosses language boundaries (for example, JavaScript string literals containing HTML, or css-in-js template literals), ast-grep’s multi-language story is often more straightforward.

What is ast-grep?

ast-grep is an AST-based search and transform engine with a pattern-first workflow.

Most people first meet it via declarative rules (YAML/JSON), which is great for quickly codifying “find this shape, replace with that shape.” But ast-grep also has a Node API through N-API, which means you can drive transformations programmatically in JS/TS and avoid YAML entirely if that’s your preference.

Why teams like ast-grep

  • Fast scans and rewrites (Rust core)
  • Pattern matching that reads close to source code
  • Works across many languages (JS/TS, JSON, Rust, Go, Python, HTML, CSS, and more)
  • Can run as rules in CI, or as scripted transforms in Node

Quick declarative example

id: no-console-log
language: JavaScript
rule:
  pattern: console.log($ARG)
fix: console.debug($ARG)

Read-only

That’s enough to match and rewrite console.log(...) calls.

What is jscodeshift?

jscodeshift is the classic JavaScript codemod toolkit.

You write transformations in JavaScript, powered by recast + AST tooling. It’s imperative, explicit, and very flexible for JS/TS codebases.

Why teams like jscodeshift

  • Mature ecosystem and lots of existing codemods
  • Great for deep, custom JS/TS migrations
  • Recast-based printing that generally preserves style/comments well
  • Fully programmable transform logic in JavaScript

Quick jscodeshift example

export default function transformer(fileInfo, api) {
  const j = api.jscodeshift;

  return j(fileInfo.source)
    .find(j.CallExpression, {
      callee: {
        object: { type: 'Identifier', name: 'console' },
        property: { type: 'Identifier', name: 'log' },
      },
    })
    .replaceWith((path) =>
      j.callExpression(
        j.memberExpression(j.identifier('console'), j.identifier('debug')),
        path.value.arguments
      )
    )
    .toSource();
}

Read-only

Thorough comparison: where each tool shines

1) Authoring experience

  • jscodeshift: You write JavaScript. If your team already thinks in AST nodes and traversals, this is comfortable and powerful.
  • ast-grep: You can start with concise declarative rules, which lowers the barrier for many migrations. And when rules get complex, you can switch to the Node API (N-API) and keep everything in JS/TS.

In practice: ast-grep gives you both a quick declarative entry point and a programmable path without forcing YAML forever.

2) Language coverage

  • jscodeshift: Primarily JS/TS-focused.
  • ast-grep: Multi-language by design. This matters when a migration touches more than one syntax in the same repo.

In practice: if your codemod spans UI templates, styles, config files, and JS/TS app code, ast-grep usually requires fewer tool handoffs.

3) Performance and scale

  • ast-grep: Rust implementation is generally very fast for scanning and matching at repo scale.
  • jscodeshift: Capable and battle-tested, but often slower on very large codebases depending on transform complexity.

In practice: both can handle serious migrations, but ast-grep often feels faster in broad pattern-matching passes.

4) Transform complexity

  • jscodeshift: Excellent when you need highly specific AST logic, custom control flow, or stateful transformations.
  • ast-grep: Great for structural patterns and bulk rewrites; can still handle advanced workflows through rule composition and Node API scripting.

In practice: for “surgical JS-only AST programming,” jscodeshift remains a top choice.

5) Cross-language migrations (real-world edge)

This is where ast-grep can be unexpectedly strong.

It’s common to find language boundaries inside one file:

  • JS string literals that contain HTML
  • styled-components / emotion template literals containing CSS
  • Framework macros that embed SQL/GraphQL/CSS

With ast-grep, you can parse and transform those embedded languages as part of one workflow more naturally than a JS-only AST stack.

Working examples: switching languages in one migration

Below are practical examples that teams actually run into.

Example A: JavaScript string with embedded HTML

Input

const template = '<button class="btn btn-primary">Save</button>';

Read-only

Goal

Rename btn-primary to button-primary inside the HTML string.

ast-grep approach (Node API concept)

  1. Parse JavaScript and find string literals.
  2. For candidate literals that look like HTML, run an HTML ast-grep transform on the string value.
  3. Write the transformed HTML back into the JS string literal.

Pseudo-code shape:

// Conceptual flow using ast-grep Node API + HTML parsing
for (const jsString of findJavaScriptStringLiterals(source)) {
  if (!looksLikeHtml(jsString.value)) continue;

  const updatedHtml = transformWithAstGrep({
    language: 'html',
    code: jsString.value,
    find: '<button class="btn btn-primary">$$$</button>',
    replace: '<button class="btn button-primary">$$$</button>',
  });

  replaceStringLiteral(jsString, updatedHtml);
}

Read-only

This pattern is hard to do robustly with plain regex and awkward with JS-only AST tools unless you bolt on additional parsers manually.

Example B: css-in-js template literals

Input

const Button = styled.button`
  color: #fff;
  background: var(--brand-primary);
`;

Read-only

Goal

Migrate --brand-primary to --color-primary.

ast-grep approach

  1. Parse TS/JS and find tagged templates where tag is styled.* (or css).
  2. Parse template content as CSS.
  3. Apply CSS-level rewrite and write it back.

Pseudo-code shape:

for (const cssTemplate of findCssInJsTemplates(source)) {
  const nextCss = transformWithAstGrep({
    language: 'css',
    code: cssTemplate.content,
    find: 'var(--brand-primary)',
    replace: 'var(--color-primary)',
  });

  replaceTemplateContent(cssTemplate, nextCss);
}

Read-only

This is a great example of why ast-grep’s multi-language + N-API path can be more versatile than a single-language codemod tool.

So… which one should you pick?

Choose jscodeshift when

  • Your migration is purely JS/TS AST work
  • You want full imperative control and already have AST expertise
  • You rely on existing jscodeshift codemods in your ecosystem

Choose ast-grep when

  • You want fast structural matching and rewriting
  • You need one codemod workflow across multiple languages
  • You like declarative rules for simple cases, but still want to drop into JS/TS via Node API (N-API) for advanced logic
  • You’re dealing with embedded-language scenarios (HTML in strings, css-in-js, etc.)

Final take

This isn’t really a “winner takes all” situation.

  • jscodeshift is still excellent for deep JS/TS codemod engineering.
  • ast-grep is often the more versatile choice for modern repos where migrations cross language boundaries, and its N-API Node API closes much of the flexibility gap people assume exists.

If you’re building codemods for a large org, there’s a good chance you’ll want both in your toolbox—just used for different kinds of jobs.