jscodeshift Codemods
Core Philosophy: Transform AST nodes, not text. Let recast handle printing to preserve formatting and structure.
When to Use
Use codemods for:
-
API migrations - Library upgrades (React Router v5→v6, enzyme→RTL)
-
Pattern standardization - Enforce coding conventions across codebase
-
Deprecation removal - Remove deprecated APIs systematically
-
Large-scale refactoring - Rename functions, restructure imports, update patterns
Don't use codemods for:
-
One-off changes (faster to do manually)
-
Changes requiring semantic understanding (business logic)
-
Non-deterministic transformations
Codemod Workflow
Copy this checklist and track your progress:
Codemod Progress:
- Phase 1: Identify Patterns
- Collect before/after examples from real code
- Document transformation rules
- Identify edge cases
- Phase 2: Create Test Fixtures
- Create input fixture with pattern to transform
- Create expected output fixture
- Verify test fails (TDD)
- Phase 3: Implement Transform
- Find target nodes
- Apply transformation
- Return modified source
- Phase 4: Handle Edge Cases
- Add fixtures for edge cases
- Handle already-transformed code (idempotency)
- Handle missing dependencies
- Phase 5: Validate at Scale
- Dry run on target codebase
- Review sample of changes
- Run with --fail-on-error
Project Structure
Standard codemod project layout:
codemods/ ├── my-transform.ts # Transform implementation ├── tests/ │ └── my-transform-test.ts # Test file └── testfixtures/ ├── my-transform.input.ts # Input fixture ├── my-transform.output.ts # Expected output ├── edge-case.input.ts # Additional fixtures └── edge-case.output.ts
Transform Module Anatomy
Every transform exports a function with this signature:
import type { API, FileInfo, Options } from "jscodeshift";
export default function transform( fileInfo: FileInfo, api: API, options: Options ): string | null | undefined { const j = api.jscodeshift; const root = j(fileInfo.source);
// Find and transform nodes root .find(j.Identifier, { name: "oldName" }) .forEach((path) => { path.node.name = "newName"; });
// Return transformed source, null to skip, or undefined for no change return root.toSource(); }
Return values:
Return Meaning
string
Transformed source code
null
Skip this file (no output)
undefined
No changes made
Key objects:
Object Purpose
fileInfo.source
Original file contents
fileInfo.path
File path being transformed
api.jscodeshift
The jscodeshift library (usually aliased as j )
api.stats
Collect statistics during dry runs
api.report
Print to stdout
Testing with defineTest
jscodeshift provides fixture-based testing utilities:
// tests/my-transform-test.ts jest.autoMockOff(); const defineTest = require("jscodeshift/dist/testUtils").defineTest;
// Basic test - uses my-transform.input.ts → my-transform.output.ts defineTest(__dirname, "my-transform");
// Named fixtures for edge cases defineTest(__dirname, "my-transform", null, "already-transformed"); defineTest(__dirname, "my-transform", null, "missing-import"); defineTest(__dirname, "my-transform", null, "multiple-occurrences");
Fixture naming:
testfixtures/ ├── my-transform.input.ts # Default input ├── my-transform.output.ts # Default output ├── already-transformed.input.ts # Named fixture input ├── already-transformed.output.ts # Named fixture output
Running tests:
Run all codemod tests
npx jest codemods/tests/
Run specific transform tests
npx jest codemods/tests/my-transform-test.ts
Run with verbose output
npx jest codemods/tests/my-transform-test.ts --verbose
Collection API Quick Reference
The jscodeshift Collection API provides chainable methods:
Method Purpose Example
find(type, filter?)
Find nodes by type root.find(j.CallExpression, { callee: { name: 'foo' } })
filter(predicate)
Filter collection .filter(path => path.node.arguments.length > 0)
forEach(callback)
Iterate and mutate .forEach(path => { path.node.name = 'new' })
replaceWith(node)
Replace matched nodes .replaceWith(j.identifier('newName'))
remove()
Remove matched nodes .remove()
insertBefore(node)
Insert before each match .insertBefore(j.importDeclaration(...))
insertAfter(node)
Insert after each match .insertAfter(j.expressionStatement(...))
closest(type)
Find nearest ancestor .closest(j.FunctionDeclaration)
get()
Get first path .get()
paths()
Get all paths as array .paths()
size()
Count matches .size()
Chaining pattern:
root .find(j.CallExpression, { callee: { name: "oldFunction" } }) .filter((path) => path.node.arguments.length === 2) .forEach((path) => { path.node.callee.name = "newFunction"; });
Common Node Types
Node Type Represents Example Code
Identifier
Variable/function names foo , myVar
CallExpression
Function calls foo() , obj.method()
MemberExpression
Property access obj.prop , arr[0]
ImportDeclaration
Import statements import { x } from 'y'
ImportSpecifier
Named imports { x } in import
ImportDefaultSpecifier
Default imports x in import x from
VariableDeclaration
Variable declarations const x = 1
VariableDeclarator
Individual variable x = 1 part
FunctionDeclaration
Named functions function foo() {}
ArrowFunctionExpression
Arrow functions () => {}
ObjectExpression
Object literals { a: 1, b: 2 }
ArrayExpression
Array literals [1, 2, 3]
Literal
Primitive values 'string' , 42 , true
StringLiteral
String values 'hello'
Common Transformation Patterns
Rename Import Source
// Change: import { x } from 'old-package' // To: import { x } from 'new-package'
root .find(j.ImportDeclaration, { source: { value: "old-package" } }) .forEach((path) => { path.node.source.value = "new-package"; });
Rename Named Import
// Change: import { oldName } from 'package' // To: import { newName } from 'package'
root .find(j.ImportSpecifier, { imported: { name: "oldName" } }) .forEach((path) => { path.node.imported.name = "newName"; // Also rename local if not aliased if (path.node.local.name === "oldName") { path.node.local.name = "newName"; } });
Add Import If Missing
// Add: import { newThing } from 'package'
const existingImport = root.find(j.ImportDeclaration, { source: { value: "package" }, });
if (existingImport.size() === 0) { // Add new import at top of file const newImport = j.importDeclaration( [j.importSpecifier(j.identifier("newThing"))], j.literal("package") );
root.find(j.Program).get("body", 0).insertBefore(newImport); }
Rename Function Calls
// Change: oldFunction(arg) // To: newFunction(arg)
root .find(j.CallExpression, { callee: { name: "oldFunction" } }) .forEach((path) => { path.node.callee.name = "newFunction"; });
Transform Function Arguments
// Change: doThing(a, b, c) // To: doThing({ a, b, c })
root .find(j.CallExpression, { callee: { name: "doThing" } }) .filter((path) => path.node.arguments.length === 3) .forEach((path) => { const [a, b, c] = path.node.arguments; path.node.arguments = [ j.objectExpression([ j.property("init", j.identifier("a"), a), j.property("init", j.identifier("b"), b), j.property("init", j.identifier("c"), c), ]), ]; });
Track Variable Usage Across Scope
// Find what variable an import is bound to, then find all usages
root.find(j.ImportSpecifier, { imported: { name: "useHistory" } }).forEach((path) => { const localName = path.node.local.name; // Could be aliased
// Find all calls using this variable root .find(j.CallExpression, { callee: { name: localName } }) .forEach((callPath) => { // Transform each usage }); });
Replace Entire Expression
// Change: history.push('/path') // To: navigate('/path')
root .find(j.CallExpression, { callee: { type: "MemberExpression", object: { name: "history" }, property: { name: "push" }, }, }) .replaceWith((path) => { return j.callExpression(j.identifier("navigate"), path.node.arguments); });
Anti-Patterns
Over-Matching
// BAD: Matches ANY identifier named 'foo' root.find(j.Identifier, { name: "foo" });
// GOOD: Match specific context (function calls named 'foo') root.find(j.CallExpression, { callee: { name: "foo" } });
Ignoring Scope
// BAD: Assumes 'history' always means the router history root.find(j.Identifier, { name: "history" });
// GOOD: Verify it came from the expected import const historyImport = root.find(j.ImportSpecifier, { imported: { name: "useHistory" }, }); if (historyImport.size() === 0) return; // Skip file
Not Checking Idempotency
// BAD: Adds import every time, even if already present root.find(j.Program).get("body", 0).insertBefore(newImport);
// GOOD: Check first const existingImport = root.find(j.ImportDeclaration, { source: { value: "package" }, }); if (existingImport.size() === 0) { root.find(j.Program).get("body", 0).insertBefore(newImport); }
Destructive Transforms
// BAD: Rebuilds node from scratch, loses comments and formatting path.replace( j.callExpression(j.identifier("newFn"), [j.literal("arg")]) );
// GOOD: Mutate existing node to preserve metadata path.node.callee.name = "newFn";
Testing Only Happy Path
// BAD: Only one test fixture defineTest(__dirname, "my-transform");
// GOOD: Cover edge cases defineTest(__dirname, "my-transform"); defineTest(__dirname, "my-transform", null, "already-transformed"); defineTest(__dirname, "my-transform", null, "aliased-import"); defineTest(__dirname, "my-transform", null, "no-matching-code");
Debugging Transforms
Dry Run with Print
See output without writing files
npx jscodeshift -t my-transform.ts target/ --dry --print
Log Node Structure
root.find(j.CallExpression).forEach((path) => { console.log(JSON.stringify(path.node, null, 2)); });
Verbose Mode
Show transformation stats
npx jscodeshift -t my-transform.ts target/ --verbose=2
Fail on Errors
Exit with code 1 if any file fails
npx jscodeshift -t my-transform.ts target/ --fail-on-error
CLI Quick Reference
Basic usage
npx jscodeshift -t transform.ts src/
TypeScript/TSX files
npx jscodeshift -t transform.ts src/ --parser=tsx --extensions=ts,tsx
Dry run (no changes)
npx jscodeshift -t transform.ts src/ --dry
Print output to stdout
npx jscodeshift -t transform.ts src/ --print
Limit parallelism
npx jscodeshift -t transform.ts src/ --cpus=4
Ignore patterns
npx jscodeshift -t transform.ts src/ --ignore-pattern="**/*.test.ts"
Integration
Complementary skills:
-
writing-tests - For test-first codemod development
-
systematic-debugging - When transforms produce unexpected results
-
verification-before-completion - Verify codemod works before claiming done
Language-specific patterns:
- React/TypeScript: See references/react-typescript.md for JSX transforms, hook migrations, and component patterns