cf.completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Codemods & Babel plugins

Write Babel plugins and jscodeshift codemods that change code across a whole codebase, keep its formatting with recast, and test every transform.

By the end, you can
  • 01
    Write a Babel pluginUse a visitor, paths, types and options to change code on every build.
  • 02
    Write a codemodUse jscodeshift to change source files once, keeping their formatting with recast.
  • 03
    Test and run it safelyCheck transforms with fixtures and a second run, then dry-run them before writing files.

What a codemod is

A codemod is a program that changes code for you, across a whole codebase. When a library renames a function, you can run one codemod instead of editing 400 files by hand. Next.js, for example, ships its upgrade codemods as npx @next/codemod.

Codemods use the steps from the working with ASTs lesson: parse, find, change, print. This lesson uses the real tools: Babel plugins, jscodeshift and recast.

Rename a function with jscodeshiftPop out in the code editor (opens in a new tab)JavaScript
import jscodeshift from "jscodeshift";

const j = jscodeshift;
const source = 'const user = fetchUser(7); // the admin\nshow(fetchUser, "fetchUser");';
const output = j(source)
  .find(j.Identifier, { name: "fetchUser" })
  .replaceWith(() => j.identifier("getUser"))
  .toSource();
console.log(output);

Line 5 parses the code. Line 6 finds every Identifier named fetchUser, line 7 puts a new getUser identifier in each place, and line 8 prints the code.

It prints const user = getUser(7); // the admin and show(getUser, "fetchUser");. The comment is still there, and the string "fetchUser" is untouched: a string is not an identifier. Find and replace would have changed it.

Three file cards, api.js, cart.js and page.js, point into a function card labeled codemod. Out come the same three files: api.js changed, cart.js changed, page.js unchanged.api.jsapi.jschangedcart.jscart.jschangedpage.jspage.jsunchangedcodemod
A codemod reads every file in a project and makes the same kind of change wherever it finds a match. Here, renaming fetchUser changes two files and leaves the third alone.
Real-life analogyA style editor with a rulebook

Picture an editor who gets a rulebook for a long book. They go through every chapter, apply each rule where it fits, and leave quotes from other people alone. Before printing, they show the author every marked-up page. A codemod works the same way on code.

In real life: A rulebook: "write e-mail as email"
In JavaScript: A codemod's transform
In real life: Every chapter of the book
In JavaScript: Every file in the project
In real life: Changing words, never quotes
In JavaScript: Changing identifiers, never strings
In real life: Showing the author the marked-up pages first
In JavaScript: A dry run and a diff
The main idea

A codemod changes your source files once, using the tree, so it changes only the code it means to. A Babel plugin uses the same tree, but changes the output on every build and leaves your source alone.

Anatomy of a Babel plugin

A Babel plugin is a function. Babel calls it with its API, and the plugin returns an object with a visitor: one function for each node type it cares about. Babel parses the code, walks the tree, calls the visitor, and prints the result.

A plugin that removes console callsPop out in the code editor (opens in a new tab)JavaScript
import { transform } from "@babel/standalone";

function removeLogs({ types: t }) {
  return {
    name: "remove-logs",
    visitor: {
      CallExpression(path, state) {
        const callee = path.get("callee");
        if (!callee.isMemberExpression()) return;
        if (!t.isIdentifier(callee.node.object, { name: "console" })) return;
        if (state.opts.keep?.includes(callee.node.property.name)) return;
        if (path.parentPath.isExpressionStatement()) path.parentPath.remove();
        else path.replaceWith(t.unaryExpression("void", t.numericLiteral(0)));
      },
    },
  };
}

const code = [
  'console.log("start");',
  "const total = add(2, 3);",
  'console.error("oops");',
  'const result = console.log("done");',
].join("\n");
console.log(transform(code, { plugins: [[removeLogs, { keep: ["error"] }]] }).code);

Line 3 receives Babel's API and takes types from it, named t: helpers that check and build nodes. Line 7 runs for every CallExpression. It gets a path, the node plus its place in the tree, and state, which holds the plugin's options.

Lines 9 to 11 skip calls that aren't console.something, and methods listed in state.opts.keep. Line 12 removes the whole statement when the call stands alone. Line 13 replaces the call with void 0 when its value is used, because removing it would break the const.

Line 25 passes the plugin with its options, { keep: ["error"] }. It prints const total = add(2, 3);, then console.error("oops");, then const result = void 0;.

Step through the removeLogs plugin
Step 0 of 10Ready
Your turn: follow the blue line

A replay of the example code, not a view inside a JavaScript engine. The plugin's checks run for real on Acorn's tree of the same code.

Running in
  1. script
Next: line 25
import { transform } from "@babel/standalone"; function removeLogs({ types: t }) {  return {    name: "remove-logs",    visitor: {      CallExpression(path, state) {        const callee = path.get("callee");        if (!callee.isMemberExpression()) return;        if (!t.isIdentifier(callee.node.object, { name: "console" })) return;        if (state.opts.keep?.includes(callee.node.property.name)) return;        if (path.parentPath.isExpressionStatement()) path.parentPath.remove();        else path.replaceWith(t.unaryExpression("void", t.numericLiteral(0)));      },    },  };} const code = [  'console.log("start");',  "const total = add(2, 3);",  'console.error("oops");',  'const result = console.log("done");',].join("\n");
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Press Step to see the visitor run for each call, and which line decides what happens to it. This replays the example code; it isn't a view inside the JavaScript engine.

Three function cards in a row: parse, traverse, generate. A tag reading removeLogs, visitor hangs under traverse. Above the first: code. Above the last: new code.codenew codeparsetraversegenerateremoveLogsvisitor
Babel does the parsing and the printing. Your plugin only gives it a visitor, and Babel calls it for each matching node while it walks the tree.

Read each row of the table as: "on a path, this gives you this."

What a Babel path gives you
On a pathWhat it gives you
path.nodeThe node itself, such as a CallExpression.
path.parent, path.parentPathThe parent node, and the parent's path.
path.get("callee")The path of a child, by its field name.
path.isMemberExpression()Whether the node has that type.
path.remove()Takes the node out of the tree.
path.replaceWith(node)Puts another node in its place.
state.optsThe options the plugin was given in the config.

Plugins run on every build, so they suit changes that the source should not keep, such as compiling JSX. The transpilers lesson shows Babel in a build.

Codemods with jscodeshift

jscodeshift is a tool for running codemods. A transform is a function that gets one file and an API, and returns the new code. jscodeshift runs it on every file you give it.

A jscodeshift transformPop out in the code editor (opens in a new tab)JavaScript
import jscodeshift from "jscodeshift";

function transformer(file, api) {
  const j = api.jscodeshift;
  return j(file.source)
    .find(j.CallExpression, { callee: { name: "fetchUser" } })
    .forEach((path) => {
      path.node.callee.name = "getUser";
    })
    .toSource();
}

const source = "const user = fetchUser(7);   // the admin\nconst list = api.fetchUser(8);";
console.log(transformer({ path: "app.js", source }, { jscodeshift }));

Line 3 is the transform. file.source is the file's code, and api.jscodeshift is the library, named j on line 4. Line 6 finds calls whose callee has the name fetchUser: the filter object is compared field by field.

Line 8 renames each one, and line 10 prints the file. Line 14 calls the transform the way jscodeshift would. The first line now says getUser(7), and keeps its three spaces before the comment. api.fetchUser(8) is left alone: that callee is a MemberExpression, which has no name.

Read each row of the table as: "this method does this."

jscodeshift's collection methods
MethodWhat it does
j(source)Parses the code into a collection that holds the whole file.
.find(type, filter)Every node of that type whose fields match the filter.
.filter(fn)Keeps only the paths for which fn returns true.
.forEach(fn)Runs fn on each path, so you can change its node.
.replaceWith(fn)Puts the node that fn returns in each place.
.size()How many paths the collection holds.
.toSource()Prints the code, reusing the original text wherever nothing changed.

To run a transform on a project, use the command line. If the transform returns nothing, jscodeshift leaves that file alone. Read each row of the table as: "this command or option does this."

Running jscodeshift
Command or optionWhat it does
npx jscodeshift -t rename.js src/Runs the transform in rename.js on every file in src.
--dry or -dA dry run: no files are changed.
--print or -pPrints each changed file.
--parser=tsxReads TypeScript with JSX. The default parser is babel.
--from=fetchUserOptions jscodeshift doesn't know are passed to your transform, in options.

Preserving formatting

A codemod's output goes into a commit that people review. If it changes the spacing of every line, the real change is hard to find. Tools differ a lot here.

Babel prints everything againPop out in the code editor (opens in a new tab)JavaScript
import { transform } from "@babel/standalone";

const code = "let  cups = 2;   // two cups\nconsole.log( cups );";
console.log(transform(code, { plugins: [] }).code);

Line 3 has odd spacing on purpose. Line 4 runs Babel with no plugins at all, so nothing is meant to change. But Babel prints the whole tree from scratch, and it prints let cups = 2; // two cups and console.log(cups);: every extra space is gone.

recast reprints only what changedPop out in the code editor (opens in a new tab)JavaScript
import * as recast from "recast";

const code = "let  cups = 2;   // two cups\nconsole.log( cups );";
const ast = recast.parse(code);
ast.program.body[0].kind = "const";
console.log(recast.print(ast).code);
console.log(recast.prettyPrint(ast).code);

recast keeps the original text of each node. Line 5 changes let to const. Line 6, recast.print, reprints only that declaration, so console.log( cups ); keeps its spaces.

Line 7, recast.prettyPrint, prints the whole tree again instead, and console.log(cups) loses them. jscodeshift's toSource() uses recast.print. Many teams also run Prettier after a codemod.

A card with the untouched line console.log( cups ), with spaces inside the brackets, points to two function cards. Babel gives console.log(cups) without the spaces, labeled everything reprinted. Recast gives console.log( cups ) as it was, labeled untouched lines kept.console.log( cups );Babelrecastconsole.log(cups);console.log( cups );everything reprinteduntouched lines kept
The second line of the example isn't changed by anyone. Babel prints the whole tree again, so its spaces are gone. recast reuses the original text for every node you didn't touch, so the line stays exactly as it was.

Testing transforms

A codemod may touch thousands of files, so test it first. The usual test is a fixture: an input, and the output you expect. Good fixtures cover three things: what should change, what must not change, and a second run.

Three fixtures and a second runPop out in the code editor (opens in a new tab)JavaScript
import jscodeshift from "jscodeshift";

function transform(source) {
  const j = jscodeshift;
  return j(source)
    .find(j.BinaryExpression, { operator: "==" })
    .forEach((path) => {
      path.node.operator = "===";
    })
    .toSource();
}

const cases = [
  ["if (a == b) go();", "if (a === b) go();"],
  ["if (a != b) go();", "if (a !== b) go();"],
  ['const s = "a == b";', 'const s = "a == b";'],
];
for (const [input, expected] of cases) {
  const once = transform(input);
  console.log(once === expected ? "pass" : "FAIL", transform(once) === once ? "stable" : "unstable");
}

Lines 3 to 11 are a transform that turns == into ===. Lines 13 to 17 list fixtures: the input, then the expected output. Line 19 runs the transform, and line 20 compares the result, then runs it again on its own output.

It prints pass stable, FAIL stable, pass stable. The second fixture caught a bug: the transform forgot !=. The third proves a string is left alone. stable means a second run changes nothing, so running the codemod twice is safe. Exercise 4 fixes the bug.

input.js goes into a transform card, which gives output. output is compared with expected.js: pass. output goes into the transform again and gives the same output: stable.input.jstransformoutputexpected.jssame?passrun again: same outputstable
A fixture test gives the transform an input and compares the result with the expected code. Running it a second time checks that a finished file stays the same.

jscodeshift has helpers for Jest: defineInlineTest takes the input and expected output directly, and defineTest reads them from __testfixtures__/name.input.js and name.output.js.

Running a codemod safely

Before a codemod writes anything, do a dry run: see which files would change and how. Pick a codemod to dry-run it on a small project of three files.

Playground: dry-run a codemod on a small project
What would change2 of 3 files
api.jswould change
  • 1export function fetchUser(id) { (removed)
  • 1export function getUser(id) { (added)
cart.jswould change
  • 1import { fetchUser } from "./api.js"; (removed)
  • 1import { getUser } from "./api.js"; (added)
  • 5 return fetchUser(cart.userId); (removed)
  • 5 return getUser(cart.userId); (added)
page.jsunchanged
Try it yourself
Rename fetchUser to getUser: 2 of 3 files would change.

It renames the function, its import and its call. The string "fetchUser is old" in page.js isn't an identifier, so it stays.

A dry run shows what a codemod would change without writing any files. This one parses each file with Acorn and edits the text at node positions.

Each file shows its changed lines, removed in red and added in green. Look at Use === and !==: it skips cart.userId == null on purpose, because == null matches both null and undefined. A codemod must never change what code does.

A safe routine for real codebases:

  1. Commit or stash your work, so the codemod's changes are the only ones in the diff.
  2. Test the transform with fixtures, including code it must not touch.
  3. Dry-run it with --dry --print, then run it for real.
  4. Read the diff, run the project's tests and linter, and run Prettier if the team uses it.
Babel plugin, codemod or lint rule?
  • Remove console.log from the code you ship, but keep it in the source
  • Turn JSX into function calls
  • Replace __DEV__ with false in production builds
  • Rename an API in 400 files after a library upgrade
  • Move every import of a module to its new path
  • Turn old class components into function components
  • Warn when someone writes == in new code
  • Stop new uses of a deprecated function
Try it yourself
0 of 8 correct

Put each job with the tool that fits it: a plugin on every build, a codemod once on the source, or a lint rule while you write.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

Most mix-ups come from treating code as text, or a one-time change as a build step.

  • "A codemod is the same as a Babel plugin." No. A codemod changes the source once; a plugin changes the output on every build.
  • "Find and replace is good enough." It also changes strings, comments and other names. And a text replace can break code when it runs twice.
  • "Every AST tool keeps formatting." No. Babel prints everything again; recast keeps what didn't change.
  • "If the transform runs without errors, it's right." No. Test it with fixtures, including code it must not change.
  • "== and === are always safe to swap." No. x == null also matches undefined.

The table below puts three of these side by side. Read each row as: "people often think this, but this is true."

What is true about codemods
TopicWhat people thinkWhat is true
A codemodIt's the same as a Babel plugin.A codemod changes your source files once; a plugin changes the output on every build.
Find and replaceIt's good enough for a rename.It also changes strings, comments and other names, and running it twice can break code.
FormattingEvery AST tool keeps your formatting.Babel prints everything again; recast, used by jscodeshift, keeps what didn't change.

Practice exercises

Each exercise uses Babel or jscodeshift for real. Guess first, then run it.

Exercise 1 · Warm-upCount what find matches

What do lines 5 and 6 print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
import jscodeshift from "jscodeshift";

const j = jscodeshift;
const root = j("fetchUser(1);\napi.fetchUser(2);\nfetchUser(3);");
console.log(root.find(j.CallExpression, { callee: { name: "fetchUser" } }).size());
console.log(root.find(j.Identifier, { name: "fetchUser" }).size());

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upWhat is the parent?

    What does line 14 print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    import { transform } from "@babel/standalone";
    
    const seen = [];
    function spy() {
      return {
        visitor: {
          Identifier(path) {
            seen.push(path.node.name + ":" + path.parent.type);
          },
        },
      };
    }
    transform("total(price);", { plugins: [spy] });
    console.log(seen.join(" "));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWrite a var to let plugin

      Fill in the visitor on line 6. What is the first line your program prints?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      import { transform } from "@babel/standalone";
      
      function varToLet() {
        return {
          visitor: {
            // Add a VariableDeclaration visitor that changes var to let.
          },
        };
      }
      
      console.log(transform("var a = 1;\nconst b = 2;", { plugins: [varToLet] }).code);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix the failing fixture

        The second fixture fails. Change lines 6 and 8 so != becomes !== too. What does the second line print now?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        import jscodeshift from "jscodeshift";
        
        function transform(source) {
          const j = jscodeshift;
          return j(source)
            .find(j.BinaryExpression, { operator: "==" })
            .forEach((path) => {
              path.node.operator = "===";
            })
            .toSource();
        }
        
        const cases = [
          ["if (a == b) go();", "if (a === b) go();"],
          ["if (a != b) go();", "if (a !== b) go();"],
          ['const s = "a == b";', 'const s = "a == b";'],
        ];
        for (const [input, expected] of cases) {
          const once = transform(input);
          console.log(once === expected ? "pass" : "FAIL", transform(once) === once ? "stable" : "unstable");
        }

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeRename the import too

          This renames the call but not the import, which would break the file. Add a second find for the import. What is the first line your program prints?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          import jscodeshift from "jscodeshift";
          
          function transformer(file, api) {
            const j = api.jscodeshift;
            const root = j(file.source);
            root.find(j.CallExpression, { callee: { name: "fetchUser" } }).forEach((path) => {
              path.node.callee.name = "getUser";
            });
            return root.toSource();
          }
          
          const source = 'import { fetchUser } from "./api.js";\nfetchUser(7);';
          console.log(transformer({ path: "app.js", source }, { jscodeshift }));

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeTake the names from options

            Change lines 6 and 8 to use options.from and options.to. What is the second line your program prints?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            import jscodeshift from "jscodeshift";
            
            function transformer(file, api, options) {
              const j = api.jscodeshift;
              return j(file.source)
                .find(j.CallExpression, { callee: { name: "fetchUser" } })
                .forEach((path) => {
                  path.node.callee.name = "getUser";
                })
                .toSource();
            }
            
            const source = "fetchUser(1);\nloadCart(2);";
            console.log(transformer({ path: "app.js", source }, { jscodeshift }, { from: "loadCart", to: "getCart" }));

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask what runs once and what runs on every build, and what would happen if the transform ran twice. Each answer explains why it's right or wrong.

              The codemods quiz · 6 questionsScore: first tries count
              1. Question 1 of 6What does a Babel plugin function return?

                Choose an answer to see the explanation.

              2. Question 2 of 6In a visitor, what is the difference between path and path.node?

                Choose an answer to see the explanation.

              3. Question 3 of 6What does running this text replace twice print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const transform = (code) => code.replaceAll("==", "===");
                const once = transform("a == b");
                console.log(once, "|", transform(once));

                Choose an answer to see the explanation.

              4. Question 4 of 6What do these two fixture tests print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const transform = (code) => code.replace("var ", "let ");
                const cases = [
                  ["var a = 1;", "let a = 1;"],
                  ["var a = 1; var b = 2;", "let a = 1; let b = 2;"],
                ];
                console.log(cases.map(([input, expected]) => transform(input) === expected).join(" "));

                Choose an answer to see the explanation.

              5. Question 5 of 6Why does jscodeshift keep your comments and spacing?

                Choose an answer to see the explanation.

              6. Question 6 of 6What does npx jscodeshift --dry -t rename.js src/ do?

                Choose an answer to see the explanation.

              Key takeaways

              Codemods and plugins turn a tree change into a change across every file.

              • A codemod changes source files once; a Babel plugin changes the output on every build.
              • A Babel plugin returns a visitor. Each function gets a path, which can remove or replace its node.
              • A jscodeshift transform gets a file and returns new code, using find, forEach and toSource.
              • recast reprints only what changed, so your formatting survives; Babel prints everything again.
              • Test with fixtures, including code that must not change, and check that a second run changes nothing.
              • Dry-run first, read the diff, and never change what the code does.

              Remember the one-liner.
              Find by shape, change the tree, print only what changed, and test it twice.

              Coming next: How linters and formatters work. They walk the same trees to find problems and to print your code neatly.

              CompleteFrontend Clear concepts. Working examples.