Codemods & AST Transforms — Professional¶
Source: Facebook jscodeshift; OpenRewrite docs; Instagram/Meta LibCST
At the professional level codemods stop being one-off scripts and become infrastructure: they run in CI, they execute against monorepos with millions of lines, they produce diffs that teams have to review, and they live for years as the canonical way a change is rolled out. This page is about operating codemods at that scale — performance, CI integration, review strategy for generated diffs, the recipe ecosystems, maintenance, and failure handling.
1. Codemods in CI¶
There are two distinct jobs a codemod does in CI, and conflating them causes pain.
Job A — apply a migration (one-shot, gated)¶
A large migration is too big for one human-reviewable PR. The pattern:
- Run the codemod in a CI job that opens PRs, sharded by directory/team, not one giant PR. Meta and Google both shard mechanical migrations so each owning team reviews their slice.
- Each PR runs the full test suite — the codemod's correctness is verified by your existing tests, not by trust. A green build on a tightly-scoped diff is the safety contract.
- Land PRs incrementally. A failure in one shard doesn't block the others.
Job B — enforce a rule (continuous, blocking)¶
Once migrated, you don't want the old pattern creeping back. Run the codemod in check / dry-run mode on every PR and fail the build if it would change anything:
# CI gate: fail if the codemod would still modify any file (i.e. old pattern reintroduced)
jscodeshift -t ban-console-log.js src/ --dry --print > /tmp/out.txt
if [ -s /tmp/out.txt ]; then
echo "❌ console.log reintroduced — run the codemod or fix manually"; exit 1;
fi
OpenRewrite has this first-class via rewrite:dryRun failing the build when a recipe finds work to do. This turns a one-time migration into a permanent invariant — far stronger than a lint rule someone can disable, because it's the actual transform checking for the actual pattern.
When NOT to gate in CI: if the codemod is slow (type-aware, whole-project parse) and the rule is better expressed as a lint rule, use the lint rule for the gate and keep the codemod for the one-shot migration. Don't put a 3-minute OpenRewrite dry-run on the critical path of every PR if an ESLint rule catches the same thing in 200ms.
2. Performance on large repos¶
On a monorepo, naïve codemod runs are slow enough to matter (tens of minutes), and the wrong approach can be hours.
Parallelism. jscodeshift parallelizes across files by default (worker processes; tune with --cpus=N). Because each file transforms independently, this scales near-linearly — the win is real and free. OpenRewrite parses incrementally and caches the LST. LibCST/Bowler can be driven over a process pool.
Scope the file set. Don't run over node_modules, generated code, vendored directories, or files that can't contain the pattern. The cheapest speedup is not parsing files you don't need to:
jscodeshift -t mod.js src/ \
--ignore-pattern="**/*.test.js" \
--ignore-pattern="**/generated/**" \
--cpus=8
Pre-filter cheaply, then parse. Parsing is the expensive step. Use a fast textual grep to find candidate files (files that even mention the token), and only run the AST transform on those. A repo-wide rg "console\.log" -l narrows 50,000 files to 400 before you pay for a single parse. The regex here isn't doing the transform (that's still the AST's job) — it's just cheaply eliminating files that provably can't match.
Type-aware = expensive. ts-morph and OpenRewrite type-check the project; that dominates runtime. Run them once over the whole project (they share the parsed program) rather than per-file in a loop, and budget minutes, not seconds.
| Technique | Typical win | Caveat |
|---|---|---|
--cpus=N parallelism | near-linear | bounded by cores / IO |
| Ignore generated/vendor dirs | large | maintain the ignore list |
| grep pre-filter → parse candidates only | huge on big repos | the grep must not exclude real matches (keep it loose) |
| Reuse one type-checked program | essential for type-aware | needs a buildable project |
When NOT to optimize: a codemod you run once over a repo that finishes in 90 seconds doesn't need a grep pre-filter or worker tuning. Optimize the codemods that run in CI on every PR (Job B), where latency is paid forever. One-shot migrations: just let them run.
3. Reviewing machine-generated diffs¶
A 200-file codemod PR defeats normal line-by-line review — reviewers can't read 4,000 changed lines meaningfully, so they rubber-stamp, and the rubber-stamp is where bugs hide. The professional review strategy:
- Review the codemod, not (only) the diff. The transform logic and its fixture tests are what's actually being reviewed for correctness. If the codemod is right and well-tested, the diff is mechanically derived. Put effort there.
- Audit the diff for shape, not every line. Reviewers verify: is the change uniform?
git diff --statand spot-checking a sample of files confirms every site changed the same way. Non-uniform hunks are where the codemod mishandled a variant — those are what you read closely. - Lean on the lossless printer. Because the printer only moves lines you changed (see middle.md), the diff is the set of intended edits. If a file shows reformatting noise, that's a finding — investigate it.
- Trust the test suite as the real reviewer. The contract is: tightly-scoped diff + green full test run = safe. The tests verify behavior; the human verifies the transform is principled and the diff is uniform.
- Sample, don't skim everything. Read 5–10 representative files in full, including the gnarliest ones you can find (multi-line, commented, edge-case). If those are right and the rest are uniform, you're done.
When NOT to auto-merge: never auto-merge a codemod PR on green CI alone if the transform touches semantics (not just naming). Behavior-changing codemods need a human to confirm the intent was right, because the tests only catch behavior they cover. Pure renames with full lossless diffs are the only ones safe to fast-track.
4. Recipe ecosystems and codemods as living tools¶
The highest-leverage professional move is treating common migrations as shared, versioned, reusable recipes rather than throwaway scripts.
OpenRewrite is the canonical example: recipes are published artifacts, composed from sub-recipes, versioned, and run via the build. The org-level pattern is a recipe catalog — your company's conventions (logging API, error handling, banned methods, dependency upgrades) encoded as recipes anyone can run and CI can enforce. A migration written once is rerun forever as new code lands.
jscodeshift / LibCST ecosystems work the same socially: libraries ship codemods with breaking releases (react-codemod, next codemod, MUI, Jest). The professional expectation when you ship a breaking change to an internal library is to ship the codemod that migrates consumers — you don't push the migration cost onto every team, you automate it.
Maintaining codemods as living tools means:
- Version them with the thing they migrate. The
v2→v3codemod lives next to the v3 release and is tagged. - Keep their tests green in CI. A codemod whose fixtures rot is a codemod no one trusts to run.
- Document the escape hatches — which variants it flags for manual handling (see senior.md §2), so consumers know what to finish by hand.
When NOT to invest in a reusable recipe: a genuinely one-time, one-repo change doesn't need to be a published, versioned recipe — that's gold-plating. Reserve the recipe-as-artifact treatment for migrations that recur, that other teams run, or that CI must enforce going forward.
5. Failure handling¶
At scale, a codemod will hit files it can't handle. The difference between professional and amateur is what happens then.
- Parse failures must not be silent. A file with a syntax error (or a syntax the parser version doesn't support) can't be transformed. The codemod must report it, not skip it quietly — a silently-skipped file is an un-migrated site that looks migrated. jscodeshift reports per-file errors in its summary; surface and fail on them.
- Unhandled variants get flagged, not guessed. As in senior.md, when a site can't be transformed safely (spread args, dynamic dispatch, macro-generated code), emit a report line for a human, and don't emit wrong code.
- Partial application is fine; partial silence is not. It's acceptable for a codemod to handle 95% and list the 5%. It is never acceptable for it to handle 95%, mangle 4%, and tell you it did 100%.
- Make reruns safe. Because some shards will fail and get rerun, idempotency (middle.md §3) isn't optional at scale — it's what makes "just run it again" a safe instruction.
- Validate after applying. The post-apply gate is your test suite plus a compile/typecheck. A codemod that produces parseable-but-uncompilable code (wrong type, missing import) is caught here. Never trust a codemod's output without re-typechecking the result.
The mental model: a codemod operating on thousands of files is a distributed batch job. Treat its failures the way you'd treat any batch job — explicit error reporting, idempotent retries, a verification step, and a manifest of what it couldn't do.
Next¶
- interview.md — the questions that separate people who've operated codemods from those who've only read about them.
- tasks.md — write real codemods end to end.
- Org-wide recipe catalogs are a form of automated Refactoring to Patterns enforcement.
- The traversal at the heart of every recipe is, again, Visitor.
In this topic