# jscodeshift: running codemods across a JavaScript codebase without rewriting every file

> Facebook's codemod toolkit wraps recast in a runner that parallelises across files, keeps the original formatting of untouched lines, and passes any CLI flag through to the transform. Worth understanding where recast ends and string surgery begins.

**facebook/jscodeshift** — A JavaScript codemod toolkit.

- Repository: https://github.com/facebook/jscodeshift
- Website: https://jscodeshift.com
- Stars: 10,038 · Forks: 497
- Language: JavaScript
- License: MIT
- Published: 2026-10-06 · Updated: 2026-10-06 · Language: en
- Canonical page: https://hysenlabs.com/projects/facebook-jscodeshift

## What the runner adds on top of recast

jscodeshift describes itself as a toolkit for running codemods over multiple JavaScript or TypeScript files, and it lists two components. The first is a runner that executes your transform for each file and prints a summary of how many files were and were not transformed. The second is a wrapper around recast, described as an AST-to-AST transform tool that tries to preserve the style of the original code as much as possible.

That split matters when you are choosing an approach. A regex pass through a repository is fast and needs no parsing, but it cannot tell an identifier in a string from one in a comment, and it rewrites formatting on every line it touches. jscodeshift parses, queries the tree, and prints only the regions it changed, leaving surrounding whitespace, quotes and semicolons alone. That is why codemod output produces reviewable diffs instead of a repository-wide reformat.

The repository layout matches the description. `src/` holds the runner, `parser/` the parser selection logic, `bin/` the CLI entry point that `package.json` maps to `jscodeshift.js`, and `utils/` the supporting helpers. There is a `sample/` directory of example transforms and a `recipes/` directory, plus a `.changeset/` setup, which is how this project tracks releases.

## Installing and running the CLI

The install is a single global npm package, and it registers the `jscodeshift` binary:

```bash
npm install -g jscodeshift
```

Running it with no transform defaults to `./transform.js` in the current directory. Any CLI flag that is not a documented runner option is passed straight through to the transform as an entry in `options`, which the README shows with a concrete example:

```bash
jscodeshift -t myTransforms fileA fileB --foo=bar
```

In that case `options` contains `{foo: 'bar'}`. This is the mechanism that lets one transform serve several call sites, and it is the reason the README can say that all options reach the transformer even though the documented list is not exhaustive.

The `--help` output in the README is worth reading before writing a transform, because several defaults shape the result. `--extensions` defaults to `js`, so TypeScript and JSX files need `--extensions=ts,tsx` or whatever your files use. `--parser` defaults to `babel` and accepts `babel`, `babylon`, `flow`, `ts` or `tsx`. `-c, --cpus` defaults to `max(all - 1, 1)`, `-p, --print` sends output to stdout, `-d, --dry` writes nothing, `-s, --silent` suppresses stdout and stderr, and `--fail-on-error` turns found errors into a non-zero exit code, which is the flag that makes the tool usable as a CI gate. There are also `--gitignore` and `--ignore-pattern` and `--ignore-config` for skipping files, and `--stdin` to read the file list from standard input.

## The transform module contract is three arguments and a string

A transform is a module exporting a function. That is the entire interface:

```js
module.exports = function(fileInfo, api, options) {
  // transform `fileInfo.source` here
  return source;
};
```

`fileInfo` carries the `path` and the `source` of the file being processed. `api` exposes `jscodeshift`, the recast wrapper, plus `stats` and `report`. `options` is the passthrough object described above.

The return value decides the outcome per file: a string that differs from the input source counts as a successful transformation, and the remaining cases in the README's list determine whether a file is marked as unchanged or skipped. This is the part worth internalising, because it means a transform that returns the original string is not an error, it is the normal outcome for the majority of files in a typical codemod.

The `api` object exists for convenience and the README is unusually clear that you are not obliged to use it: any tool that can modify source is acceptable. That escape hatch is what lets codemods built on this runner do things that are not AST rewrites at all, such as injecting a banner into a file or delegating to another formatter.

`stats` only functions during a `--dry` run. It takes a string and counts how often your transform calls it with that value, and the CLI reports the totals at the end, which is a good way to find out how prevalent a construct is before committing to a change. `report` prints a string to stdout, and the README explains why it exists instead of calling `process.stdout` directly: interleaved output from many parallel workers gets mangled.

## Querying the AST with a jQuery-like chain

The jscodeshift wrapper gives you collection-style traversal, which is the part that makes a transform readable. The README's short example finds every declarator bound to a name and renames it, then prints the result:

```js
module.exports = function(fileInfo, api, options) {
  return api.jscodeshift(fileInfo.source)
    .findVariableDeclarators('foo')
    .renameTo('bar')
    .toSource();
}
```

The chain is three links: select a node type, call the operation, serialise. `toSource()` at the end is what hands the string back to the runner. Because selection returns collections rather than single nodes, most operations work across every match at once, and you can filter further with the rest of the collection API.

The reason to reach for this instead of a regex is exactly the case in the example. A repository containing `foo` in a comment, in a string literal, and as a variable would get all three rewritten by a text substitution. Here only the variable declarators are touched, and because recast reprints only the modified ranges, the comment and the string survive intact.

There is a real limit to that approach. The wrapper knows syntax, not meaning, so a transform cannot tell whether two identifiers that share a name refer to the same thing, and it cannot follow an import across modules to check whether a renamed export still matches. Those are the cases where a codemod has to be conservative and where review still matters.

## Running the runner from JavaScript instead of the shell

The programmatic entry point matters when a codemod is one step in a larger pipeline, since it lets you decide the file list, the options and the concurrency in code. The README shows the runner imported directly:

```js
const {run: jscodeshift} = require('jscodeshift/src/Runner')
const path = require('node:path');

const transformPath = path.resolve('transform.js')
const paths = ['foo.js', 'bar']
const options = {
  dry: true,
  print: true,
  verbose: 1,
}

const res = await jscodeshift(transformPath, paths, options)
console.log(res)
```

The result object carries `stats`, `timeElapsed`, and counters named `error`, `ok`, `nochange` and `skip`. Those four counters are the useful part for automation: `ok` and `nochange` tell you how many files the transform actually altered, `skip` covers files excluded by ignore rules or extensions, and `error` is where failures land. Note that the import path is `jscodeshift/src/Runner` rather than the package root, so the internal layout is part of the supported surface here.

You can also point the CLI at a URL instead of a local file, since the `--transform` flag accepts a local path or a URL, and a transform module can be written in TypeScript as of v0.6.1.

Dependencies at the current version line tell you what you inherit. `package.json` requires node 16 or newer and pulls in `@babel/core` and `@babel/parser` at `^7.24.7`, `recast` at `^0.23.11`, `flow-parser`, `picomatch` for glob matching, `neo-async`, `write-file-atomic`, and `@babel/preset-env` as an optional peer dependency. The Babel parser is the default, and the two most recent releases were both recast bumps: v17.2.0 fixed double-wrapped parentheses around JSX elements, and v17.3.0 allowed TypeScript type arguments on tagged template literals. A project whose recent releases are parser bumps rather than new features is telling you the hard part is parsing, not orchestration. The project is MIT licensed and the last push was on 2026-09-17.

## Conclusion

jscodeshift is the right tool when a change is too regular for a text search and too broad to hand-edit: renaming an import across thousands of files, rewriting an API call to a new signature, migrating a JSX prop. It gives you a jQuery-like AST query API, parallelism across cores, a dry run with occurrence counts, and formatting preservation through recast, which no regex pass offers. What it does not give you is semantic understanding, so a transform that has to reason about types or runtime behaviour will still need a human in the loop. Start with the sample transforms under `sample/`, write the transform to print with `--dry` and `--print`, and check the summary counts before letting it write.

## FAQ

### How do you install jscodeshift?

It is published on npm and installed globally with `npm install -g jscodeshift`, which registers the `jscodeshift` binary. The current version line requires node 16 or newer.

### How do you run jscodeshift on TypeScript files?

Pass the extensions and the parser, since `--extensions` defaults to `js` only and `--parser` defaults to `babel`. The parser flag accepts `babel`, `babylon`, `flow`, `ts` and `tsx`, so TypeScript support needs both flags set for your file naming.

### What is the difference between jscodeshift and writing a codemod with recast directly?

recast is the AST-to-AST library that preserves original formatting; jscodeshift wraps it with a file-by-file runner, parallelism across child processes, dry runs, ignore rules, occurrence counting and a summary report. If you only need to transform one string in memory, recast alone is enough. If you need to sweep a repository, the runner is the part doing the work.

### How do you pass custom options to a jscodeshift transform?

Any flag the runner does not recognise is forwarded to the transform and arrives in its `options` argument, so a call like `jscodeshift -t myTransforms fileA fileB --foo=bar` gives the transform `{foo: 'bar'}`. Documented runner options are also passed through, so a transform can read `dry` or `extensions` as well.

## Sources

- [facebook/jscodeshift on GitHub](https://github.com/facebook/jscodeshift)
- [License: MIT](https://github.com/facebook/jscodeshift/blob/main/LICENSE)
- [Project website](https://jscodeshift.com)
- [README](https://github.com/facebook/jscodeshift/blob/main/README.md)
- [Releases](https://github.com/facebook/jscodeshift/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/facebook-jscodeshift
