
Jscodeshift
- 307 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
jscodeshift is an agent skill that guides developers through writing and running JavaScript/TypeScript codemods using Meta's jscodeshift AST toolkit for automated large-scale refactors.
About
jscodeshift is a developer skill for applying Meta's jscodeshift codemod toolkit to JavaScript and TypeScript codebases. The skill walks through parsing source into an AST with recast, navigating nodes via the jscodeshift collection API, and emitting transformed files from transform modules executed by the jscodeshift CLI. Developers reach for jscodeshift when grep-and-replace is unsafe—library migrations, API renames, import path updates, or pattern replacements across hundreds of files. The toolkit supports multiple parsers including babel, flow, ts, and tsx, and transform modules can be authored in TypeScript. Use it during build when a mechanical refactor must preserve formatting and run reproducibly across an entire repo.
- jscodeshift
Jscodeshift by the numbers
- 307 all-time installs (skills.sh)
- +18 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,296 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill jscodeshiftAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 307 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do you automate large-scale JavaScript refactors safely?
Use jscodeshift for development tasks
Who is it for?
Developers maintaining large JavaScript or TypeScript monorepos who need repeatable, AST-aware batch refactors instead of fragile regex replacements.
Skip if: Developers making one-off edits to a handful of files where IDE rename-refactor or manual changes are faster and sufficient.
When should I use this skill?
A developer asks to write a codemod, migrate a library API across many files, or batch-transform JavaScript/TypeScript AST patterns.
What you get
Transform modules, CLI execution logs, and refactored .js/.ts/.tsx source files with preserved formatting.
- transform modules
- refactored source files
By the numbers
- Supports 5 parsers: babel, babylon, flow, ts, and tsx
Files
Facebook/Meta jscodeshift Best Practices
Comprehensive best practices guide for jscodeshift codemod development, designed for AI agents and LLMs. Contains 40 rules across 8 categories, prioritized by impact from critical (parser configuration, AST traversal) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples, and specific impact metrics.
When to Apply
Reference these guidelines when:
- Writing new jscodeshift codemods for code migrations
- Debugging transform failures or unexpected behavior
- Optimizing codemod performance on large codebases
- Reviewing codemod code for correctness
- Testing codemods for edge cases and regressions
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Parser Configuration | CRITICAL | parser- |
| 2 | AST Traversal Patterns | CRITICAL | traverse- |
| 3 | Node Filtering | HIGH | filter- |
| 4 | AST Transformation | HIGH | transform- |
| 5 | Code Generation | MEDIUM | codegen- |
| 6 | Testing Strategies | MEDIUM | test- |
| 7 | Runner Optimization | LOW-MEDIUM | runner- |
| 8 | Advanced Patterns | LOW | advanced- |
Quick Reference
1. Parser Configuration (CRITICAL)
- `parser-typescript-config` - Use correct parser for TypeScript files
- `parser-flow-annotation` - Use Flow parser for Flow-typed code
- `parser-babel5-compat` - Avoid default babel5compat for modern syntax
- `parser-export-declaration` - Export parser from transform module
- `parser-astexplorer-match` - Match AST Explorer parser to jscodeshift parser
2. AST Traversal Patterns (CRITICAL)
- `traverse-find-specific-type` - Use specific node types in find() calls
- `traverse-two-pass-pattern` - Use two-pass pattern for complex transforms
- `traverse-early-return` - Return early when no transformation needed
- `traverse-find-filter-pattern` - Use find() with filter object over filter() chain
- `traverse-closest-scope` - Use closestScope() for scope-aware transforms
- `traverse-avoid-repeated-find` - Avoid repeated find() calls for same node type
3. Node Filtering (HIGH)
- `filter-path-parent-check` - Check parent path before transformation
- `filter-import-binding` - Track import bindings for accurate usage detection
- `filter-nullish-checks` - Add nullish checks before property access
- `filter-jsx-context` - Distinguish JSX context from regular JavaScript
- `filter-computed-properties` - Handle computed property keys in filters
4. AST Transformation (HIGH)
- `transform-builder-api` - Use builder API for creating AST nodes
- `transform-replacewith-callback` - Use replaceWith callback for context-aware transforms
- `transform-insert-import` - Insert imports at correct position
- `transform-preserve-comments` - Preserve comments when replacing nodes
- `transform-renameto` - Use renameTo for variable renaming
- `transform-remove-unused-imports` - Remove unused imports after transformation
5. Code Generation (MEDIUM)
- `codegen-tosource-options` - Configure toSource() for consistent formatting
- `codegen-preserve-style` - Preserve original code style with recast
- `codegen-template-literals` - Use template literals for complex node creation
- `codegen-print-width` - Set appropriate print width for long lines
6. Testing Strategies (MEDIUM)
- `test-inline-snapshots` - Use defineInlineTest for input/output verification
- `test-negative-cases` - Write negative test cases first
- `test-dry-run-exploration` - Use dry run mode for codebase exploration
- `test-fixture-files` - Use fixture files for complex test cases
- `test-parse-errors` - Test for parse error handling
7. Runner Optimization (LOW-MEDIUM)
- `runner-parallel-workers` - Configure worker count for optimal parallelization
- `runner-ignore-patterns` - Use ignore patterns to skip non-source files
- `runner-extensions-filter` - Filter files by extension
- `runner-batch-processing` - Process large codebases in batches
- `runner-verbose-output` - Use verbose output for debugging transforms
8. Advanced Patterns (LOW)
- `advanced-compose-transforms` - Compose multiple transforms into pipelines
- `advanced-scope-analysis` - Use scope analysis for safe variable transforms
- `advanced-multi-file-state` - Share state across files with options
- `advanced-custom-collections` - Create custom collection methods
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Full Compiled Document
For a single comprehensive document containing all rules, see AGENTS.md.
Reference Files
| File | Description |
|---|---|
| AGENTS.md | Complete compiled guide with all rules |
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |
jscodeshift
Version 0.1.0 Facebook/Meta January 2026
Note:
This document is mainly for agents and LLMs to follow when maintaining,
generating, or refactoring codebases. Humans may also find it useful,
but guidance here is optimized for automation and consistency by AI-assisted workflows.
---
Abstract
Comprehensive best practices guide for jscodeshift codemod development, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (parser configuration, AST traversal) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated codemod creation and code generation.
---
Table of Contents
1. Parser Configuration — CRITICAL
- 1.1 Avoid Default Babel5Compat Parser for Modern Syntax — CRITICAL (prevents parse failures on post-ES2015 features)
- 1.2 Export Parser from Transform Module — CRITICAL (prevents 100% of parser mismatch failures)
- 1.3 Match AST Explorer Parser to jscodeshift Parser — CRITICAL (prevents AST structure mismatches during development)
- 1.4 Use Correct Parser for TypeScript Files — CRITICAL (prevents 100% transform failures on TypeScript codebases)
- 1.5 Use Flow Parser for Flow-Typed Code — CRITICAL (prevents parse failures on Flow type annotations)
2. AST Traversal Patterns — CRITICAL
- 2.1 Avoid Repeated find() Calls for Same Node Type — CRITICAL (reduces traversal from N passes to 1 pass)
- 2.2 Return Early When No Transformation Needed — CRITICAL (10-100× faster on files with no matches)
- 2.3 Use closestScope() for Scope-Aware Transforms — CRITICAL (prevents incorrect transforms on shadowed variables)
- 2.4 Use find() with Filter Object Over filter() Chain — CRITICAL (2-5× faster than separate filter() calls)
- 2.5 Use Specific Node Types in find() Calls — CRITICAL (10-100× faster traversal on large files)
- 2.6 Use Two-Pass Pattern for Complex Transforms — CRITICAL (reduces O(n²) to O(n) on complex transformations)
3. Node Filtering — HIGH
- 3.1 Add Nullish Checks Before Property Access — HIGH (prevents runtime crashes on optional AST properties)
- 3.2 Check Parent Path Before Transformation — HIGH (prevents false positives on nested structures)
- 3.3 Distinguish JSX Context from Regular JavaScript — HIGH (prevents incorrect transforms in JSX attributes vs expressions)
- 3.4 Handle Computed Property Keys in Filters — HIGH (prevents missed transforms on dynamic object keys)
- 3.5 Track Import Bindings for Accurate Usage Detection — HIGH (prevents missed transforms due to import aliases)
4. AST Transformation — HIGH
- 4.1 Insert Imports at Correct Position — HIGH (maintains valid module structure and import ordering)
- 4.2 Preserve Comments When Replacing Nodes — HIGH (prevents loss of documentation and directives)
- 4.3 Remove Unused Imports After Transformation — HIGH (prevents dead imports causing build warnings or errors)
- 4.4 Use Builder API for Creating AST Nodes — HIGH (prevents malformed AST nodes that crash toSource())
- 4.5 Use renameTo for Variable Renaming — HIGH (prevents 100% of scope-related rename bugs)
- 4.6 Use replaceWith Callback for Context-Aware Transforms — HIGH (enables dynamic transformations based on original node)
5. Code Generation — MEDIUM
- 5.1 Configure toSource() for Consistent Formatting — MEDIUM (prevents unnecessary diffs and maintains code style)
- 5.2 Preserve Original Code Style with Recast — MEDIUM (minimizes diff size by keeping unchanged code intact)
- 5.3 Set Appropriate Print Width for Long Lines — MEDIUM (prevents overly long lines that break linting rules)
- 5.4 Use Template Literals for Complex Node Creation — MEDIUM (reduces node creation code by 70-90%)
6. Testing Strategies — MEDIUM
- 6.1 Test for Parse Error Handling — MEDIUM (prevents transform crashes on malformed files)
- 6.2 Use defineInlineTest for Input/Output Verification — MEDIUM (catches 95%+ transform regressions automatically)
- 6.3 Use Dry Run Mode for Codebase Exploration — MEDIUM (enables safe exploration without modifying files)
- 6.4 Use Fixture Files for Complex Test Cases — MEDIUM (catches 90%+ edge cases missed by inline tests)
- 6.5 Write Negative Test Cases First — MEDIUM (prevents unintended transformations before they happen)
7. Runner Optimization — LOW-MEDIUM
- 7.1 Configure Worker Count for Optimal Parallelization — LOW-MEDIUM (2-4× speedup on multi-core systems)
- 7.2 Filter Files by Extension — LOW-MEDIUM (prevents 100% of missed file type transforms)
- 7.3 Process Large Codebases in Batches — LOW-MEDIUM (prevents memory exhaustion on large codebases)
- 7.4 Use Ignore Patterns to Skip Non-Source Files — LOW-MEDIUM (prevents wasted processing on generated/vendor code)
- 7.5 Use Verbose Output for Debugging Transforms — LOW-MEDIUM (reduces debugging time by 50-80%)
8. Advanced Patterns — LOW
- 8.1 Compose Multiple Transforms into Pipelines — LOW (enables reusable, testable transform building blocks)
- 8.2 Create Custom Collection Methods — LOW (reduces query code by 50-80% through reuse)
- 8.3 Share State Across Files with Options — LOW (enables cross-file analysis and coordinated transforms)
- 8.4 Use Scope Analysis for Safe Variable Transforms — LOW (prevents 100% of scope-related transform bugs)
---
References
1. https://github.com/facebook/jscodeshift 2. https://jscodeshift.com/ 3. https://jscodeshift.com/build/api-reference/ 4. https://martinfowler.com/articles/codemods-api-refactoring.html 5. https://github.com/benjamn/ast-types 6. https://github.com/benjamn/recast 7. https://astexplorer.net/
---
Source Files
This document was compiled from individual reference files. For detailed editing or extension:
| File | Description |
|---|---|
| references/_sections.md | Category definitions and impact ordering |
| assets/templates/_template.md | Template for creating new rules |
| SKILL.md | Quick reference entry point |
| metadata.json | Version and reference URLs |
Rule Title Here
Brief explanation (1-3 sentences) of WHY this rule matters. Focus on performance implications, correctness issues, or cascade effects.
Incorrect (describe the problem/cost):
// Code demonstrating the anti-pattern
// Include a comment explaining the consequence
const result = problematicApproach();Correct (describe the benefit/solution):
// Code demonstrating the correct approach
// Minimal diff from incorrect - same variable names
const result = correctApproach();Alternative (optional, when applicable):
// Alternative approach for specific contexts
const result = alternativeApproach();When NOT to use this pattern (optional):
- Exception case 1
- Exception case 2
Benefits (optional):
- Benefit 1
- Benefit 2
Reference: Source Title
{
"version": "1.1.6",
"organization": "Facebook/Meta",
"technology": "jscodeshift",
"date": "January 2026",
"abstract": "Comprehensive best practices guide for jscodeshift codemod development, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (parser configuration, AST traversal) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated codemod creation and code generation.",
"references": [
"https://github.com/facebook/jscodeshift",
"https://jscodeshift.com/",
"https://jscodeshift.com/build/api-reference/",
"https://martinfowler.com/articles/codemods-api-refactoring.html",
"https://github.com/benjamn/ast-types",
"https://github.com/benjamn/recast",
"https://astexplorer.net/"
],
"category": "DevEx"
}
jscodeshift Best Practices Skill
A comprehensive best practices guide for jscodeshift codemod development, containing 40+ rules across 8 categories.
Overview
This skill provides performance optimization and correctness guidelines for writing jscodeshift codemods. Rules are prioritized by impact from critical (parser configuration, AST traversal) to incremental (advanced patterns).
Structure
jscodeshift/
├── SKILL.md # Entry point with quick reference
├── AGENTS.md # Compiled comprehensive guide
├── metadata.json # Version and reference information
├── README.md # This file
├── references/
│ ├── _sections.md # Category definitions
│ ├── parser-*.md # Parser configuration rules
│ ├── traverse-*.md # AST traversal rules
│ ├── filter-*.md # Node filtering rules
│ ├── transform-*.md # AST transformation rules
│ ├── codegen-*.md # Code generation rules
│ ├── test-*.md # Testing strategy rules
│ ├── runner-*.md # Runner optimization rules
│ └── advanced-*.md # Advanced pattern rules
└── assets/
└── templates/
└── _template.md # Rule templateGetting Started
Installation
pnpm installBuilding AGENTS.md
pnpm buildValidating the Skill
pnpm validateCreating a New Rule
1. Choose the appropriate category based on impact and lifecycle stage 2. Copy the template from assets/templates/_template.md 3. Name the file using the pattern: {prefix}-{description}.md 4. Fill in the frontmatter (title, impact, impactDescription, tags) 5. Write the rule content with incorrect/correct examples 6. Rebuild AGENTS.md with pnpm build 7. Validate with pnpm validate
Prefix Reference
| Prefix | Category | Impact |
|---|---|---|
parser- | Parser Configuration | CRITICAL |
traverse- | AST Traversal Patterns | CRITICAL |
filter- | Node Filtering | HIGH |
transform- | AST Transformation | HIGH |
codegen- | Code Generation | MEDIUM |
test- | Testing Strategies | MEDIUM |
runner- | Runner Optimization | LOW-MEDIUM |
advanced- | Advanced Patterns | LOW |
Rule File Structure
---
title: Rule Title
impact: CRITICAL|HIGH|MEDIUM|LOW-MEDIUM|LOW
impactDescription: Quantified impact (e.g., "2-10× improvement")
tags: prefix, technique, tool
---
## Rule Title
Brief explanation of WHY this matters (1-3 sentences).
**Incorrect (problem description):**
\`\`\`javascript
// Code showing the anti-pattern
\`\`\`
**Correct (benefit description):**
\`\`\`javascript
// Code showing the correct approach
\`\`\`
Reference: [Source](url)File Naming Convention
Rule files follow the pattern: {prefix}-{kebab-case-description}.md
- First part is the category prefix (e.g.,
parser-,traverse-) - Second part describes the rule in kebab-case
- Examples:
parser-typescript-config.md,traverse-two-pass-pattern.md
Impact Levels
| Level | Description | Example |
|---|---|---|
| CRITICAL | Affects all transformations, cascading failures | Wrong parser breaks all transforms |
| HIGH | Significant correctness or performance impact | Missing scope checks corrupt code |
| MEDIUM | Moderate impact on quality or efficiency | Style preservation, testing coverage |
| LOW-MEDIUM | Optimization for specific scenarios | Runner parallelization tuning |
| LOW | Advanced patterns for edge cases | Multi-file state coordination |
Scripts
| Script | Description |
|---|---|
pnpm build | Compiles references into AGENTS.md |
pnpm validate | Validates skill structure and content |
Contributing
1. Follow the rule template structure exactly 2. Include both incorrect and correct code examples 3. Quantify impact where possible (e.g., "2-10× improvement") 4. Reference authoritative sources 5. Ensure first tag matches category prefix 6. Run validation before submitting
Acknowledgments
- facebook/jscodeshift - Official jscodeshift repository
- jscodeshift.com - Official documentation
- Martin Fowler - Refactoring with Codemods
- recast - AST-to-AST transformation
- ast-types - AST node definitions
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
---
1. Parser Configuration (parser)
Impact: CRITICAL Description: Parser misconfiguration cascades to all transformations - wrong parser produces wrong AST which breaks every subsequent operation.
2. AST Traversal Patterns (traverse)
Impact: CRITICAL Description: Inefficient traversal creates O(n²) complexity on large codebases, turning seconds into minutes. Strategic find() usage is essential.
3. Node Filtering (filter)
Impact: HIGH Description: Poor filtering causes incorrect transformations or silent failures. Precise filtering catches edge cases and prevents false positives.
4. AST Transformation (transform)
Impact: HIGH Description: Builder API misuse creates invalid AST nodes that crash toSource() or produce syntactically incorrect code.
5. Code Generation (codegen)
Impact: MEDIUM Description: toSource() misconfiguration loses original formatting, causing unnecessary diffs and failed code reviews.
6. Testing Strategies (test)
Impact: MEDIUM Description: Inadequate testing allows regressions and misses edge cases, causing production incidents when codemods run at scale.
7. Runner Optimization (runner)
Impact: LOW-MEDIUM Description: Runner configuration affects parallelization and ignore patterns, impacting performance on large codebases.
8. Advanced Patterns (advanced)
Impact: LOW Description: Composition, scoping, and complex multi-transform patterns for sophisticated codemod architectures.
Compose Multiple Transforms into Pipelines
Complex codemods can be built from smaller, independently testable transforms. Composition enables reuse and easier maintenance.
Incorrect (monolithic transform):
// Single 200+ line transform that does everything
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// Rename imports... 50 lines
// Update function calls... 50 lines
// Remove deprecated code... 50 lines
// Clean up unused imports... 50 lines
return root.toSource();
};
// Hard to test, maintain, or reuse partsCorrect (composed small transforms):
// transforms/renameImports.js
function renameImports(root, j, config) {
root.find(j.ImportDeclaration, { source: { value: config.oldModule } })
.forEach(path => {
path.node.source.value = config.newModule;
});
return root;
}
// transforms/updateCalls.js
function updateCalls(root, j, config) {
root.find(j.CallExpression, { callee: { name: config.oldName } })
.replaceWith(path => j.callExpression(
j.identifier(config.newName),
path.node.arguments
));
return root;
}
// transforms/removeUnusedImports.js
function removeUnusedImports(root, j) { /* ... */ }
// Main transform composes all pieces
module.exports = function transformer(file, api, options) {
const j = api.jscodeshift;
let root = j(file.source);
// Pipeline of transforms
root = renameImports(root, j, options);
root = updateCalls(root, j, options);
root = removeUnusedImports(root, j);
return root.toSource();
};Factory pattern for configurable transforms:
function createMigrationTransform(migrations) {
return function transformer(file, api) {
const j = api.jscodeshift;
let root = j(file.source);
migrations.forEach(migration => {
root = migration(root, j);
});
return root.toSource();
};
}
module.exports = createMigrationTransform([
renameImports,
updateCalls,
removeUnusedImports
]);Reference: Refactoring with Codemods to Automate API Changes
Create Custom Collection Methods
jscodeshift allows registering custom collection methods for frequently used query patterns. This improves code reuse and readability.
Incorrect (repeating complex queries):
// Same complex query repeated in multiple transforms
root.find(j.CallExpression)
.filter(path => {
const callee = path.node.callee;
return callee.type === 'MemberExpression' &&
callee.object.name === 'React' &&
callee.property.name === 'createElement';
});
// Copy-pasted to every transform that needs itCorrect (custom collection method):
// Register once at module load
const jscodeshift = require('jscodeshift');
// Add custom method to collections
jscodeshift.registerMethods({
findReactCreateElement: function() {
return this.find(jscodeshift.CallExpression).filter(path => {
const callee = path.node.callee;
return callee.type === 'MemberExpression' &&
callee.object?.name === 'React' &&
callee.property?.name === 'createElement';
});
},
findHooks: function(hookName) {
return this.find(jscodeshift.CallExpression, {
callee: { name: hookName || /^use[A-Z]/ }
});
},
findComponentDefinitions: function() {
// Finds both function and arrow function components
return this.find(jscodeshift.FunctionDeclaration)
.filter(path => /^[A-Z]/.test(path.node.id?.name))
.concat(
this.find(jscodeshift.VariableDeclarator)
.filter(path =>
/^[A-Z]/.test(path.node.id?.name) &&
(path.node.init?.type === 'ArrowFunctionExpression' ||
path.node.init?.type === 'FunctionExpression')
)
);
}
});
// Usage in transforms
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// Clean, readable queries
root.findReactCreateElement()
.replaceWith(/* ... */);
root.findHooks('useState')
.forEach(/* ... */);
root.findComponentDefinitions()
.forEach(/* ... */);
return root.toSource();
};Note: Register methods in a shared setup file that all transforms import.
Reference: jscodeshift - registerMethods
Share State Across Files with Options
Some transformations need information from multiple files. Use the options object and external state files for cross-file coordination.
Incorrect (each file processed in isolation):
// transform.js - no cross-file awareness
module.exports = function transformer(file, api) {
// Can't know what was exported from other files
// Can't coordinate changes across files
};Correct (shared state via options):
// First pass: collect information
// collect-exports.js
const exports = {};
module.exports = function collector(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
root.find(j.ExportNamedDeclaration).forEach(path => {
exports[file.path] = exports[file.path] || [];
// Collect export names
path.node.specifiers?.forEach(spec => {
exports[file.path].push(spec.exported.name);
});
});
return undefined; // No changes, just collecting
};
module.exports.exports = exports; // Expose collected data// Second pass: use collected information
// transform.js
const collectedExports = require('./collect-exports').exports;
module.exports = function transformer(file, api, options) {
const j = api.jscodeshift;
const root = j(file.source);
// Use cross-file data
const availableExports = collectedExports[options.targetFile] || [];
root.find(j.ImportDeclaration, { source: { value: options.targetFile } })
.forEach(path => {
// Filter to only valid exports
path.node.specifiers = path.node.specifiers.filter(spec =>
availableExports.includes(spec.imported.name)
);
});
return root.toSource();
};Alternative (external state file):
# Step 1: Collect data
jscodeshift -t collect-exports.js src/ --dry
node -e "require('./collect-exports'); console.log(JSON.stringify(exports))" > state.json
# Step 2: Transform using collected data
jscodeshift -t transform.js src/ --state-file=state.jsonNote: For complex multi-file transforms, consider tools like codemod-cli that have built-in multi-pass support.
Reference: jscodeshift - Options
Use Scope Analysis for Safe Variable Transforms
ast-types provides scope analysis to track variable bindings. Use it for transforms that need to understand variable usage across scopes.
Incorrect (ignores scope boundaries):
// Attempts to inline a variable without scope awareness
root.find(j.VariableDeclarator, { id: { name: 'config' } })
.forEach(path => {
const initValue = path.node.init;
// Inlines ALL references, even in wrong scope
root.find(j.Identifier, { name: 'config' })
.replaceWith(initValue);
});
// Breaks when 'config' is shadowed in nested scopeCorrect (scope-aware transformation):
root.find(j.VariableDeclarator, { id: { name: 'config' } })
.forEach(declPath => {
const initValue = declPath.node.init;
const scope = declPath.scope;
// Get all bindings in this scope
const bindings = scope.getBindings();
const configBinding = bindings['config'];
if (!configBinding) return;
// Only transform references that belong to THIS binding
configBinding.forEach(refPath => {
// Skip the declaration itself
if (refPath === declPath.get('id')) return;
// Check if reference is in a scope where 'config' is shadowed
let currentScope = refPath.scope;
while (currentScope && currentScope !== scope) {
if (currentScope.getBindings()['config']) {
// Shadowed - don't transform this reference
return;
}
currentScope = currentScope.parent;
}
// Safe to inline
refPath.replace(initValue);
});
});Using scope.lookup():
root.find(j.Identifier, { name: 'target' })
.filter(path => {
// Find which scope owns this binding
const scope = path.scope.lookup('target');
// Only transform if binding is at module level
return scope && scope.isGlobal;
});Caveat: ast-types scope analysis treats let and const as function-scoped rather than block-scoped. For block-scoped variables, manually check scope boundaries.
Reference: ast-types Scope
Preserve Original Code Style with Recast
Recast preserves original formatting for unchanged code. Avoid operations that force full reprinting of the file.
Incorrect (forces full reprint):
// Modifying the program body array forces full reprint
const body = root.get().node.body;
body.push(newStatement);
body.shift(); // Removing first element
// OR: Converting to source and back
const source = root.toSource();
const newRoot = j(source); // Loses original formatting infoCorrect (modify through paths for minimal diff):
// Use insertAfter/insertBefore for additions
root.find(j.ImportDeclaration).at(-1)
.insertAfter(newImportDeclaration);
// Use path.prune() or remove() for deletions
root.find(j.ExpressionStatement)
.filter(path => isDebugStatement(path))
.remove();
// Use replaceWith for modifications
root.find(j.Identifier, { name: 'oldName' })
.replaceWith(j.identifier('newName'));Why recast preserves style:
// Recast tracks which nodes are modified
// Unmodified nodes print exactly as original source
// Only modified nodes go through the printer
// Original: const x = 1; // Weird spacing
// After rename:
root.find(j.Identifier, { name: 'x' })
.replaceWith(j.identifier('y'));
// Output: const y = 1; // Spacing preserved!Note: Building new nodes with j.identifier() etc. always uses default formatting since they have no original source.
Reference: recast - Why Recast?
Set Appropriate Print Width for Long Lines
New nodes created with builders use recast's default formatting. Set wrapColumn to match your project's line length limit.
Incorrect (default width causes long lines):
// Default wrapColumn is 74, but project uses 100
return root.toSource();
// Creates:
// import {
// ComponentA,
// ComponentB,
// ComponentC
// } from './components';
// When it could fit on one lineCorrect (match project line length):
// Match your prettier/eslint max-line-length
return root.toSource({
wrapColumn: 100 // Or 80, 120 depending on project
});
// Creates:
// import { ComponentA, ComponentB, ComponentC } from './components';Alternative (disable wrapping for specific nodes):
// For nodes that should stay on one line regardless
const importNode = j.importDeclaration(specifiers, source);
// Mark as single-line (recast-specific)
importNode.loc = null; // Forces reprint without original location
return root.toSource({ wrapColumn: Infinity }); // No wrappingMatching common tools:
| Tool | Default | Option |
|---|---|---|
| Prettier | 80 | printWidth |
| ESLint | varies | max-len |
| jscodeshift | 74 | wrapColumn |
Note: Consider running prettier/eslint after jscodeshift as a post-processing step rather than trying to match formatting exactly.
Reference: recast - Print Options
Use Template Literals for Complex Node Creation
jscodeshift's template feature parses code strings into AST nodes. This is more readable than nested builder calls for complex structures.
Incorrect (deeply nested builders):
// Creating: export const handler = async (req, res) => { return res.json(data); }
const node = j.exportNamedDeclaration(
j.variableDeclaration('const', [
j.variableDeclarator(
j.identifier('handler'),
j.arrowFunctionExpression(
[j.identifier('req'), j.identifier('res')],
j.blockStatement([
j.returnStatement(
j.callExpression(
j.memberExpression(j.identifier('res'), j.identifier('json')),
[j.identifier('data')]
)
)
]),
true // async
)
)
])
);Correct (template literal):
// Same result, much more readable
const node = j.template.statement`
export const handler = async (req, res) => {
return res.json(data);
}
`;Template with interpolation:
// Insert existing nodes into templates
const functionName = j.identifier('processUser');
const paramName = j.identifier('userId');
const node = j.template.statement`
export function ${functionName}(${paramName}) {
return fetchUser(${paramName});
}
`;Available template methods:
j.template.statement`...` // Single statement
j.template.statements`...` // Multiple statements
j.template.expression`...` // ExpressionNote: Templates are parsed at runtime. Complex templates add parsing overhead - use builders for simple nodes.
Reference: jscodeshift - Templates
Configure toSource() for Consistent Formatting
The toSource() method accepts options that control output formatting. Default options may produce inconsistent style with existing code.
Incorrect (default options create inconsistent style):
// Default toSource() uses its own formatting preferences
return root.toSource();
// Original: const x = {a: 1, b: 2}
// Output may become:
// const x = {
// a: 1,
// b: 2
// }Correct (explicit formatting options):
// Match project's code style
return root.toSource({
quote: 'single', // Use single quotes
trailingComma: true, // Add trailing commas
tabWidth: 2, // 2-space indentation
useTabs: false, // Spaces not tabs
lineTerminator: '\n' // Unix line endings
});Common options:
| Option | Values | Effect |
|---|---|---|
quote | 'single', 'double', 'auto' | String quote style |
trailingComma | true, false | Trailing commas in arrays/objects |
tabWidth | 2, 4, etc. | Indentation width |
useTabs | true, false | Tabs vs spaces |
lineTerminator | '\n', '\r\n' | Line ending style |
wrapColumn | number | Max line width for wrapping |
Alternative (project-level config):
// Create shared config
const printOptions = {
quote: 'single',
trailingComma: true,
tabWidth: 2
};
// Use in all transforms
module.exports = function transformer(file, api) {
// ... transformation
return root.toSource(printOptions);
};Reference: recast - Printing Options
Handle Computed Property Keys in Filters
Computed property keys ([expression]) don't have a name property. Filters assuming string keys miss computed properties.
Incorrect (assumes static key):
// Finds static property 'status' but misses computed ones
root.find(j.Property, {
key: { name: 'status' }
});
// Finds: { status: 'active' }
// Misses: { [STATUS_KEY]: 'active' }
// Misses: { ['stat' + 'us']: 'active' }Correct (handles both static and computed):
root.find(j.Property)
.filter(path => {
const key = path.node.key;
// Static identifier key
if (key.type === 'Identifier' && key.name === 'status') {
return true;
}
// String literal key (less common but valid)
if (key.type === 'Literal' && key.value === 'status') {
return true;
}
// Computed key - can only match if it's a simple identifier
if (path.node.computed && key.type === 'Identifier') {
// This is [STATUS_KEY], we can't know the runtime value
// Log for manual review or check known constants
return key.name === 'STATUS_KEY';
}
return false;
});Alternative (for object patterns/destructuring):
root.find(j.ObjectPattern)
.find(j.Property)
.filter(path => {
// In destructuring, check both key and value
// { status: localStatus } - key is 'status'
// { [STATUS_KEY]: localStatus } - computed
const key = path.node.key;
return !path.node.computed && key.name === 'status';
});Note: Computed keys are inherently dynamic. Consider logging them for manual review rather than attempting transformation.
Reference: Mozilla Parser API - Property
Track Import Bindings for Accurate Usage Detection
Import aliases mean the local name differs from the imported name. Track the actual binding name, not the imported module name.
Incorrect (assumes import name matches usage):
// Looking for 'useState' but import is aliased
root.find(j.CallExpression, {
callee: { name: 'useState' }
});
// Misses: import { useState as useReactState } from 'react';
// useReactState(0) is not foundCorrect (tracks actual binding name):
// First, find the actual local binding name
const localNames = new Set();
root.find(j.ImportDeclaration, { source: { value: 'react' } })
.find(j.ImportSpecifier, { imported: { name: 'useState' } })
.forEach(path => {
// local.name is the actual name used in code
localNames.add(path.node.local.name);
});
// Now find usages by actual local name
root.find(j.CallExpression)
.filter(path => {
const callee = path.node.callee;
return callee.type === 'Identifier' && localNames.has(callee.name);
})
.forEach(path => {
// Transform usage
});Alternative (handle all specifier types):
function getImportBindings(root, j, source, importedName) {
const bindings = new Set();
root.find(j.ImportDeclaration, { source: { value: source } })
.forEach(importPath => {
importPath.node.specifiers.forEach(spec => {
if (spec.type === 'ImportDefaultSpecifier' && importedName === 'default') {
bindings.add(spec.local.name);
} else if (spec.type === 'ImportSpecifier' && spec.imported.name === importedName) {
bindings.add(spec.local.name);
} else if (spec.type === 'ImportNamespaceSpecifier') {
bindings.add(`${spec.local.name}.${importedName}`);
}
});
});
return bindings;
}Reference: Refactoring with Codemods to Automate API Changes
Distinguish JSX Context from Regular JavaScript
JSX uses different node types than regular JavaScript. Transforms must handle both contexts or risk missing matches.
Incorrect (ignores JSX context):
// Only finds JavaScript function calls, misses JSX
root.find(j.CallExpression, {
callee: { name: 'formatDate' }
});
// Misses: <Component date={formatDate(value)} />
// The formatDate call IS found, but...
// This misses JSX attribute handling entirely:
root.find(j.Identifier, { name: 'onClick' });
// Does NOT find: <button onClick={handler}>Correct (handles both contexts):
// For function calls in JSX expressions - this works fine
root.find(j.CallExpression, {
callee: { name: 'formatDate' }
});
// For prop/attribute names - use JSX-specific types
root.find(j.JSXAttribute, {
name: { name: 'onClick' }
});
// For JSX element names
root.find(j.JSXIdentifier, { name: 'Button' })
.filter(path => {
// Only opening/closing element names, not attribute names
return path.parent.node.type === 'JSXOpeningElement' ||
path.parent.node.type === 'JSXClosingElement';
});JSX node type mapping:
| JavaScript | JSX Equivalent |
|---|---|
Identifier | JSXIdentifier |
MemberExpression | JSXMemberExpression |
| N/A | JSXAttribute |
| N/A | JSXSpreadAttribute |
| N/A | JSXExpressionContainer |
Note: CallExpressions inside JSX are regular CallExpressions - only the JSX-specific syntax uses JSX node types.
Reference: JSX AST Specification
Add Nullish Checks Before Property Access
AST nodes have optional properties that may be null or undefined. Accessing nested properties without checks crashes the transform.
Incorrect (assumes properties exist):
root.find(j.CallExpression)
.filter(path => {
// Crashes if callee is not MemberExpression
return path.node.callee.object.name === 'console';
});
// Throws: Cannot read property 'object' of undefined
// when callee is Identifier, not MemberExpressionCorrect (defensive property access):
root.find(j.CallExpression)
.filter(path => {
const callee = path.node.callee;
// Check type before accessing type-specific properties
if (callee.type !== 'MemberExpression') {
return false;
}
// Now safe to access MemberExpression properties
return callee.object?.name === 'console';
});Alternative (using optional chaining):
root.find(j.CallExpression)
.filter(path => {
// Optional chaining handles missing properties
return path.node.callee?.object?.name === 'console' &&
path.node.callee?.property?.name === 'log';
});Common optional properties:
| Node Type | Optional Property | When Missing |
|---|---|---|
| FunctionDeclaration | id | Anonymous function |
| ExportDefaultDeclaration | declaration.id | Inline expression |
| MemberExpression | object.name | Computed member |
| Property | key.name | Computed key [expr] |
| ArrowFunctionExpression | id | Always missing |
Reference: ast-types Node Definitions
Check Parent Path Before Transformation
Nodes can appear in multiple contexts. Check the parent path to ensure you're transforming the correct usage.
Incorrect (transforms all matching identifiers):
// Renames 'config' everywhere, including object keys and destructuring
root.find(j.Identifier, { name: 'config' })
.forEach(path => {
path.node.name = 'settings';
});
// Breaks: { config: value } becomes { settings: value }
// Breaks: const { config } = obj; pattern matching failsCorrect (checks parent context):
root.find(j.Identifier, { name: 'config' })
.filter(path => {
const parent = path.parent.node;
// Skip object property keys: { config: ... }
if (parent.type === 'Property' && parent.key === path.node) {
return false;
}
// Skip object property shorthand: { config }
if (parent.type === 'Property' && parent.shorthand) {
return false;
}
// Skip member expression properties: obj.config
if (parent.type === 'MemberExpression' && parent.property === path.node) {
return false;
}
return true;
})
.forEach(path => {
path.node.name = 'settings';
});Alternative (using path.name for position):
root.find(j.Identifier, { name: 'config' })
.filter(path => {
// path.name tells you which property of parent this node is
// 'object' = left side of member expression
// 'property' = right side of member expression
return path.name !== 'property' && path.name !== 'key';
});Reference: ast-types - NodePath
Match AST Explorer Parser to jscodeshift Parser
AST Explorer uses different default parsers than jscodeshift. Mismatched parsers produce different AST structures, causing transforms developed in AST Explorer to fail in production.
Incorrect (mismatched parsers):
// Developed in AST Explorer with @babel/parser
// Node type: OptionalMemberExpression
root.find(j.OptionalMemberExpression);
// But jscodeshift with 'tsx' parser produces:
// Node type: TSOptionalMemberExpression
// Transform finds nothing!Correct (matched parsers):
// For jscodeshift parser='tsx', use @typescript-eslint/parser in AST Explorer
// Both produce consistent node types
// For jscodeshift parser='babel', use @babel/parser in AST Explorer
// Both produce consistent node types
// AST Explorer settings → Transform: jscodeshift
// Parser: Match your module.exports.parser valueParser Mapping:
| jscodeshift parser | AST Explorer parser |
|---|---|
tsx | @typescript-eslint/parser |
ts | @typescript-eslint/parser |
babel | @babel/parser |
babylon | babylon7 |
flow | flow |
Note: Always verify node types by inspecting the actual AST in AST Explorer with the matching parser before writing traversal code.
Reference: AST Explorer
Avoid Default Babel5Compat Parser for Modern Syntax
jscodeshift defaults to babel5compat mode for backwards compatibility with old codemods. This breaks on modern syntax like optional chaining, nullish coalescing, and private class fields.
Incorrect (relying on default parser):
// transform.js - no parser specified
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// Fails on: const value = obj?.nested?.property ?? 'default';
return root.toSource();
};Correct (explicit modern babel parser):
// transform.js
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
return root.toSource();
};
module.exports.parser = 'babel';Alternative (babylon with plugins):
// parser-config.json
{
"sourceType": "module",
"plugins": [
"jsx",
"optionalChaining",
"nullishCoalescingOperator",
"classPrivateProperties",
"classPrivateMethods"
]
}jscodeshift --parser=babylon --parser-config=parser-config.json -t transform.js src/Reference: jscodeshift Issue #500 - Bringing jscodeshift up to date
Export Parser from Transform Module
Specifying the parser via CLI is error-prone and requires every developer to remember the flag. Export the parser from the transform module to ensure consistent parsing.
Incorrect (parser only via CLI):
// transform.js - no parser export
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
return root.toSource();
};
// Developers must remember: jscodeshift --parser=tsx -t transform.js src/
// Forgetting --parser=tsx breaks the entire runCorrect (parser exported from module):
// transform.js
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
return root.toSource();
};
// Parser is bundled with the transform - no CLI flag needed
module.exports.parser = 'tsx';Benefits:
- Transform is self-contained and portable
- No CLI flag required for correct behavior
- Reduces human error when running codemods
- Documentation and implementation stay together
Reference: jscodeshift - Specifying Parser in Transform
Use Flow Parser for Flow-Typed Code
Flow type annotations require the flow parser. Using babel or babylon parsers causes syntax errors on Flow-specific syntax like opaque type or $Exact.
Incorrect (babel parser on Flow code):
// transform.js - processes files with Flow annotations
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// Fails on: opaque type ID = string;
return root.toSource();
};Correct (Flow parser specified):
// transform.js
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
return root.toSource();
};
module.exports.parser = 'flow';Alternative (custom parser config):
// flow-parser-config.json
{
"enums": true,
"esproposal_decorators": "ignore",
"esproposal_class_static_fields": "enable"
}jscodeshift --parser=flow --parser-config=flow-parser-config.json -t transform.js src/Reference: jscodeshift - Parser Options
Use Correct Parser for TypeScript Files
jscodeshift defaults to the babel parser, which cannot parse TypeScript syntax. Specify the correct parser to avoid parse failures on every file.
Incorrect (default babel parser on TypeScript):
// transform.js
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// Fails: SyntaxError on TypeScript syntax
return root.toSource();
};Correct (TypeScript parser specified):
// transform.js
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
return root.toSource();
};
module.exports.parser = 'tsx'; // Handles both .ts and .tsx filesAlternative (CLI flag):
jscodeshift --parser=tsx --extensions=ts,tsx -t transform.js src/Note: Use tsx parser for mixed codebases - it handles both .ts and .tsx files correctly.
Reference: jscodeshift README - Parser
Process Large Codebases in Batches
Very large codebases can exhaust memory when jscodeshift tracks all files. Process in batches for better memory management.
Incorrect (process entire monorepo at once):
# May run out of memory on 10k+ file codebases
jscodeshift -t transform.js packages/
# Node.js heap fills up tracking all file resultsCorrect (batch by package or directory):
# Process package by package
for pkg in packages/*; do
echo "Processing $pkg"
jscodeshift -t transform.js "$pkg/src"
done
# Or use find with xargs for parallelism
find packages -name "src" -type d | xargs -P 4 -I {} \
jscodeshift -t transform.js {}Alternative (split by file count):
# Get all files, process in batches of 1000
find src -name "*.ts" -o -name "*.tsx" | \
split -l 1000 - /tmp/batch_
for batch in /tmp/batch_*; do
jscodeshift -t transform.js $(cat "$batch" | tr '\n' ' ')
rm "$batch"
doneMemory tuning:
# Increase Node.js heap size for large batches
NODE_OPTIONS="--max-old-space-size=8192" \
jscodeshift -t transform.js src/
# Reduce worker count to lower memory per batch
jscodeshift --cpus=2 -t transform.js src/Note: Monitor memory usage with --verbose flag and adjust batch size accordingly.
Reference: jscodeshift - Running on Large Codebases
Filter Files by Extension
By default, jscodeshift processes .js files. Specify extensions to include TypeScript, JSX, or exclude test files.
Incorrect (misses TypeScript files):
# Only processes .js files by default
jscodeshift -t transform.js src/
# Misses: src/utils.ts, src/Component.tsxCorrect (explicit extensions):
# Include TypeScript and JSX
jscodeshift --extensions=js,jsx,ts,tsx -t transform.js src/
# TypeScript only
jscodeshift --extensions=ts,tsx -t transform.js src/
# JavaScript without JSX
jscodeshift --extensions=js -t transform.js src/Combining with parser:
# Must specify both extensions and parser for TypeScript
jscodeshift \
--extensions=ts,tsx \
--parser=tsx \
-t transform.js src/Alternative (glob patterns for fine control):
# Only component files
jscodeshift -t transform.js "src/components/**/*.tsx"
# Exclude test files
jscodeshift -t transform.js "src/**/!(*.test|*.spec).ts"
# Multiple specific paths
jscodeshift -t transform.js src/utils src/hooks src/componentsExtension vs parser mismatch:
# WRONG: tsx parser can't parse .js files with Flow
jscodeshift --extensions=js --parser=tsx -t transform.js src/
# RIGHT: Match parser to file type
jscodeshift --extensions=ts,tsx --parser=tsx -t transform.js src/
jscodeshift --extensions=js --parser=babel -t transform.js src/Reference: jscodeshift CLI - Extensions
Use Ignore Patterns to Skip Non-Source Files
Running transforms on node_modules, build output, or generated files wastes time and may cause unexpected changes. Use ignore patterns.
Incorrect (processes everything):
# Processes node_modules, dist, etc.
jscodeshift -t transform.js .
# May take 10× longer and produce unwanted changesCorrect (ignore non-source directories):
# Use --ignore-pattern flag
jscodeshift \
--ignore-pattern="**/node_modules/**" \
--ignore-pattern="**/dist/**" \
--ignore-pattern="**/build/**" \
--ignore-pattern="**/*.min.js" \
-t transform.js src/Alternative (use gitignore):
# Automatically ignores everything in .gitignore
jscodeshift --gitignore -t transform.js .
# Combines with additional patterns
jscodeshift --gitignore --ignore-pattern="**/__mocks__/**" -t transform.js .Common patterns to ignore:
| Pattern | Purpose |
|---|---|
**/node_modules/** | Dependencies |
**/dist/** | Build output |
**/build/** | Build output |
**/*.min.js | Minified files |
**/*.bundle.js | Bundled files |
**/vendor/** | Third-party code |
**/__generated__/** | Generated code |
**/coverage/** | Test coverage |
Note: Always use --gitignore as a baseline, then add project-specific patterns.
Reference: jscodeshift - Ignore Patterns
Configure Worker Count for Optimal Parallelization
jscodeshift runs transforms in parallel across multiple workers. Configure worker count based on available CPU cores.
Incorrect (default worker count may be suboptimal):
# Default uses 1 worker per CPU core
jscodeshift -t transform.js src/
# On I/O-heavy transforms, this may leave cores idle
# On memory-heavy transforms, this may cause swappingCorrect (tune workers to workload):
# For CPU-intensive transforms (complex AST manipulation)
# Use core count - 1 to leave headroom
jscodeshift --cpus=7 -t transform.js src/ # On 8-core machine
# For I/O-intensive transforms (many small files)
# Can exceed core count since workers wait on I/O
jscodeshift --cpus=12 -t transform.js src/
# For memory-heavy transforms (large files)
# Reduce workers to avoid memory pressure
jscodeshift --cpus=4 -t transform.js src/Benchmarking approach:
# Time with different worker counts
time jscodeshift --cpus=1 -t transform.js src/
time jscodeshift --cpus=4 -t transform.js src/
time jscodeshift --cpus=8 -t transform.js src/
time jscodeshift --cpus=16 -t transform.js src/
# Find the sweet spot for your transform and codebaseAlternative (single-threaded for debugging):
# Run single-threaded for easier debugging
jscodeshift --cpus=1 -t transform.js src/
# Or completely disable workers
jscodeshift --run-in-band -t transform.js src/Reference: jscodeshift CLI Options
Use Verbose Output for Debugging Transforms
When transforms don't behave as expected, verbose output helps identify which files are processed and what changes are made.
Incorrect (silent failures):
# No output except final summary
jscodeshift -t transform.js src/
# Results:
# 0 errors
# 47 unmodified
# 0 ok
# Hard to debug why nothing changedCorrect (verbose and print output):
# Show each file being processed
jscodeshift --verbose=2 -t transform.js src/
# Output:
# Processing src/utils.ts
# Processing src/hooks.ts
# ...
# Also print transformed source to stdout
jscodeshift --dry --print -t transform.js src/
# Shows what changes WOULD be made without writingVerbose levels:
| Level | Output |
|---|---|
0 | Silent (errors only) |
1 | Summary (default) |
2 | File names as processed |
Combining flags for debugging:
# Full debugging output
jscodeshift \
--verbose=2 \ # Show files
--dry \ # Don't write changes
--print \ # Show transformed output
--cpus=1 \ # Single-threaded for ordered output
-t transform.js src/file.tsUsing console.log in transforms:
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
console.log(`Processing: ${file.path}`);
const root = j(file.source);
const matches = root.find(j.CallExpression, { callee: { name: 'target' } });
console.log(`Found ${matches.size()} matches`);
// Transform logic...
};Reference: jscodeshift CLI - Verbose
Use Dry Run Mode for Codebase Exploration
Before writing the transform, use dry run mode with api.stats() to understand the codebase patterns you'll encounter.
Incorrect (writing transform without exploration):
// Guessing at patterns without data
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// Assuming all calls look like: oldFunc(arg1, arg2)
root.find(j.CallExpression, { callee: { name: 'oldFunc' } })
.replaceWith(/* ... */);
return root.toSource();
};
// Misses: oldFunc.bind(this), obj.oldFunc(), etc.Correct (explore first with stats):
// exploration-codemod.js
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
// Count different call patterns
root.find(j.CallExpression).forEach(path => {
const callee = path.node.callee;
if (callee.type === 'Identifier' && callee.name === 'oldFunc') {
api.stats('Direct call: oldFunc()');
} else if (callee.type === 'MemberExpression') {
if (callee.property.name === 'oldFunc') {
api.stats(`Member call: ${callee.object.name || '?'}.oldFunc()`);
}
}
});
return undefined; // No changes
};Running exploration:
# --dry runs transform without writing files
# Stats are printed at the end
jscodeshift --dry --print -t exploration-codemod.js src/
# Output:
# Results:
# 0 errors
# 47 unmodified
# Stats:
# Direct call: oldFunc(): 23
# Member call: utils.oldFunc(): 12
# Member call: this.oldFunc(): 3Note: Understanding the actual patterns in your codebase prevents incomplete transforms.
Reference: jscodeshift README - Stats
Use Fixture Files for Complex Test Cases
For complex transformations involving multiple patterns, use fixture files that mirror real codebase structure.
Incorrect (oversimplified inline tests):
// Simple inline test doesn't reflect real complexity
defineInlineTest(
transform,
{},
`import { x } from 'y';`,
`import { x } from 'z';`,
'transforms import'
);
// But real files have hundreds of lines with edge casesCorrect (fixture files for comprehensive testing):
// __testfixtures__/complex-component.input.tsx
// __testfixtures__/complex-component.output.tsx
const { defineTest } = require('jscodeshift/dist/testUtils');
// Tests input.tsx → output.tsx transformation
defineTest(
__dirname,
'transform', // transform filename
null, // options
'complex-component', // fixture name (without .input/.output suffix)
{ parser: 'tsx' }
);Fixture file structure:
__tests__/
├── transform.test.js
└── __testfixtures__/
├── basic.input.js
├── basic.output.js
├── with-aliases.input.js
├── with-aliases.output.js
├── complex-component.input.tsx
└── complex-component.output.tsxCombining inline and fixture tests:
// Use inline for simple cases (quick to read)
defineInlineTest(transform, {}, 'simple input', 'simple output', 'simple case');
// Use fixtures for complex cases (realistic scenarios)
defineTest(__dirname, 'transform', null, 'real-world-component');
defineTest(__dirname, 'transform', null, 'edge-case-module');Reference: jscodeshift - testUtils
Use defineInlineTest for Input/Output Verification
jscodeshift provides defineInlineTest for testing transforms with inline input/output strings. This makes test cases self-documenting.
Incorrect (external file-based tests):
// Hard to see what the transform does
// test/__testfixtures__/transform.input.js
// test/__testfixtures__/transform.output.js
test('transform works', () => {
// Requires opening multiple files to understand test
});Correct (inline test with clear before/after):
const { defineInlineTest } = require('jscodeshift/dist/testUtils');
const transform = require('../transform');
defineInlineTest(
transform,
{}, // options
// Input
`
import { oldFunc } from 'old-module';
const result = oldFunc(data);
`,
// Expected output
`
import { newFunc } from 'new-module';
const result = newFunc(data);
`,
'renames oldFunc import to newFunc'
);Testing edge cases:
// Test: transform does NOT modify unrelated code
defineInlineTest(
transform,
{},
`
import { otherFunc } from 'other-module';
const result = otherFunc(data);
`,
`
import { otherFunc } from 'other-module';
const result = otherFunc(data);
`,
'leaves unrelated imports unchanged'
);
// Test: handles aliased imports
defineInlineTest(
transform,
{},
`
import { oldFunc as myFunc } from 'old-module';
const result = myFunc(data);
`,
`
import { newFunc as myFunc } from 'new-module';
const result = myFunc(data);
`,
'handles aliased imports'
);Reference: jscodeshift - Testing
Write Negative Test Cases First
Write tests for code that should NOT be transformed before writing positive tests. This catches overly aggressive transforms early.
Incorrect (only positive tests):
// Only tests what SHOULD change
defineInlineTest(
transform,
{},
`import { useState } from 'react';`,
`import { useState } from 'preact/hooks';`,
'transforms react to preact'
);
// But doesn't verify: Does it leave non-react imports alone?
// Does it handle aliased imports correctly?Correct (negative tests first):
// First: Verify what should NOT change
defineInlineTest(
transform,
{},
`import { useState } from 'preact/hooks';`,
`import { useState } from 'preact/hooks';`,
'leaves preact imports unchanged'
);
defineInlineTest(
transform,
{},
`import { useState } from './local-hooks';`,
`import { useState } from './local-hooks';`,
'leaves local imports unchanged'
);
defineInlineTest(
transform,
{},
`
const react = require('react');
const { useState } = react;
`,
`
const react = require('react');
const { useState } = react;
`,
'does not transform require() calls'
);
// Then: Positive test cases
defineInlineTest(
transform,
{},
`import { useState } from 'react';`,
`import { useState } from 'preact/hooks';`,
'transforms react to preact'
);Test case categories to cover:
1. Similar but different - Code that looks like target but isn't 2. Different context - Same identifier in different positions 3. Nested structures - Deeply nested matching patterns 4. Edge cases - Empty files, comments only, unusual formatting
Reference: Refactoring with Codemods to Automate API Changes
Test for Parse Error Handling
Codemods may encounter files with syntax errors or unsupported syntax. Handle parse errors gracefully instead of crashing.
Incorrect (crashes on parse errors):
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source); // Throws on syntax error
// Transform never runs if file doesn't parse
return root.toSource();
};
// Running on file with syntax error crashes entire batchCorrect (graceful error handling):
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
let root;
try {
root = j(file.source);
} catch (error) {
// Log error but don't crash - allows batch to continue
console.error(`Parse error in ${file.path}: ${error.message}`);
return undefined; // Skip this file
}
// Transform logic
const calls = root.find(j.CallExpression, { callee: { name: 'target' } });
if (calls.size() === 0) {
return undefined;
}
calls.replaceWith(/* ... */);
return root.toSource();
};Test for error handling:
const { applyTransform } = require('jscodeshift/dist/testUtils');
const transform = require('../transform');
test('handles syntax errors gracefully', () => {
const malformedCode = `
const x = {
incomplete: true,
// Missing closing brace
`;
// Should not throw
const result = applyTransform(transform, {}, { source: malformedCode });
// Returns undefined (no changes) instead of crashing
expect(result).toBeUndefined();
});Note: Always test with malformed input to ensure robustness when running on large codebases.
Reference: jscodeshift Error Handling
Use Builder API for Creating AST Nodes
Manually constructing AST node objects is error-prone. Use jscodeshift's builder methods which validate required properties.
Incorrect (manual object construction):
// Missing required properties, incorrect structure
const newNode = {
type: 'CallExpression',
callee: { type: 'Identifier', name: 'newFunc' },
arguments: args
// Missing: optional, typeParameters, etc.
};
path.replace(newNode);
// May crash toSource() or produce invalid codeCorrect (builder API):
// Builder validates structure and sets defaults
const newNode = j.callExpression(
j.identifier('newFunc'),
args
);
path.replace(newNode);Common builder methods:
// Identifiers and literals
j.identifier('name')
j.literal('string')
j.literal(42)
// Expressions
j.callExpression(callee, arguments)
j.memberExpression(object, property)
j.arrowFunctionExpression(params, body, expression)
// Statements
j.variableDeclaration('const', [declarator])
j.variableDeclarator(id, init)
j.returnStatement(argument)
j.expressionStatement(expression)
// Import/Export
j.importDeclaration(specifiers, source)
j.importSpecifier(imported, local)
j.importDefaultSpecifier(local)Note: Builder method names match AST node types with camelCase. CallExpression → j.callExpression().
Reference: ast-types Builders
Insert Imports at Correct Position
New imports must be inserted at the top of the file, after existing imports, and before any code. Incorrect positioning breaks module loading.
Incorrect (inserts at wrong position):
// Inserts at very top, before 'use strict' or existing imports
root.get().node.body.unshift(
j.importDeclaration(
[j.importDefaultSpecifier(j.identifier('newModule'))],
j.literal('new-module')
)
);
// May produce: import newModule from 'new-module'; 'use strict';Correct (insert after existing imports):
function addImport(root, j, source, specifiers) {
const imports = root.find(j.ImportDeclaration);
const newImport = j.importDeclaration(specifiers, j.literal(source));
if (imports.size() > 0) {
// Insert after last import
imports.at(-1).insertAfter(newImport);
} else {
// No imports exist - find first non-directive statement
const body = root.get().node.body;
let insertIndex = 0;
// Skip 'use strict' and other directives
while (insertIndex < body.length &&
body[insertIndex].type === 'ExpressionStatement' &&
body[insertIndex].directive) {
insertIndex++;
}
body.splice(insertIndex, 0, newImport);
}
}
// Usage
addImport(root, j, 'lodash', [
j.importSpecifier(j.identifier('debounce'))
]);Alternative (check if import exists first):
function ensureImport(root, j, source, importedName, localName = importedName) {
const existingImport = root.find(j.ImportDeclaration, {
source: { value: source }
});
if (existingImport.size() > 0) {
// Check if specifier already exists
const hasSpecifier = existingImport
.find(j.ImportSpecifier, { imported: { name: importedName } })
.size() > 0;
if (!hasSpecifier) {
// Add specifier to existing import
existingImport.forEach(path => {
path.node.specifiers.push(
j.importSpecifier(j.identifier(importedName), j.identifier(localName))
);
});
}
} else {
// Add new import declaration
addImport(root, j, source, [
j.importSpecifier(j.identifier(importedName), j.identifier(localName))
]);
}
}Reference: jscodeshift - Working with Imports
Preserve Comments When Replacing Nodes
Comments are attached to AST nodes. Replacing a node loses its comments unless explicitly preserved.
Incorrect (loses comments):
// Original: /* Important */ const config = getConfig();
root.find(j.VariableDeclaration)
.replaceWith(path => {
return j.variableDeclaration('let', path.node.declarations);
});
// Result: let config = getConfig();
// Comment /* Important */ is lost!Correct (preserves comments):
root.find(j.VariableDeclaration)
.replaceWith(path => {
const newNode = j.variableDeclaration('let', path.node.declarations);
// Copy leading and trailing comments
newNode.comments = path.node.comments;
return newNode;
});
// Result: /* Important */ let config = getConfig();Alternative (preserve all attached comments):
function preserveComments(oldNode, newNode) {
if (oldNode.comments) {
newNode.comments = oldNode.comments;
}
if (oldNode.leadingComments) {
newNode.leadingComments = oldNode.leadingComments;
}
if (oldNode.trailingComments) {
newNode.trailingComments = oldNode.trailingComments;
}
return newNode;
}
root.find(j.VariableDeclaration)
.replaceWith(path => {
const newNode = j.variableDeclaration('let', path.node.declarations);
return preserveComments(path.node, newNode);
});When NOT to preserve comments:
// When deleting code, comments should be removed too
root.find(j.CallExpression, { callee: { name: 'deprecatedFunc' } })
.remove(); // Comments on removed nodes are intentionally lostReference: recast - Preserving Original Formatting
Remove Unused Imports After Transformation
When transformations remove code, associated imports may become unused. Clean up imports to avoid build warnings and bundle bloat.
Incorrect (leaves orphaned imports):
// Removes all console.log calls
root.find(j.CallExpression, {
callee: { object: { name: 'console' }, property: { name: 'log' } }
}).remove();
// But if file had: import { debug } from './logger';
// And: console.log(debug(data));
// Now 'debug' is unused but still importedCorrect (clean up unused imports):
function removeUnusedImports(root, j) {
root.find(j.ImportDeclaration).forEach(importPath => {
const specifiers = importPath.node.specifiers;
// Check each imported binding
const usedSpecifiers = specifiers.filter(spec => {
const localName = spec.local.name;
// Count usages (excluding the import itself)
const usages = root.find(j.Identifier, { name: localName })
.filter(idPath => {
// Not the import specifier itself
return idPath.parent.node !== spec;
});
return usages.size() > 0;
});
if (usedSpecifiers.length === 0) {
// Remove entire import
importPath.prune();
} else if (usedSpecifiers.length < specifiers.length) {
// Remove only unused specifiers
importPath.node.specifiers = usedSpecifiers;
}
});
}
// Usage: call after main transformation
root.find(j.CallExpression, { callee: { name: 'oldFunc' } }).remove();
removeUnusedImports(root, j);Note: Run import cleanup as a separate pass after all transformations to catch all unused imports.
Reference: jscodeshift Recipes - Removing Imports
Use renameTo for Variable Renaming
The renameTo() method handles all references to a variable within its scope. Manual renaming misses references or incorrectly renames shadowed variables.
Incorrect (manual identifier replacement):
// Renames ALL 'data' identifiers, even unrelated ones
root.find(j.Identifier, { name: 'data' })
.forEach(path => {
path.node.name = 'payload';
});
// Breaks: function process(data) { return data; }
// Becomes: function process(payload) { return payload; }
// But these are different variables!Correct (renameTo handles scope):
// Only rename the specific variable declaration and its references
root.find(j.VariableDeclarator, { id: { name: 'data' } })
.renameTo('payload');
// Input:
// const data = fetch(); console.log(data);
// function process(data) { return data; }
//
// Output:
// const payload = fetch(); console.log(payload);
// function process(data) { return data; } // Unchanged - different scopeAlternative (for function parameters):
// Rename a function parameter
root.find(j.FunctionDeclaration, { id: { name: 'processUser' } })
.find(j.Identifier, { name: 'callback' })
.filter(path => path.parent.node.type === 'FunctionDeclaration')
.renameTo('onComplete');Limitation: renameTo() works on variable declarators. For other identifiers, use scope-aware manual transformation.
Reference: jscodeshift API - renameTo
Use replaceWith Callback for Context-Aware Transforms
The replaceWith() method accepts a callback that receives the path, enabling transformations that depend on the original node's properties.
Incorrect (static replacement ignores context):
// Always replaces with same node, loses original arguments
root.find(j.CallExpression, { callee: { name: 'oldFunc' } })
.replaceWith(j.callExpression(
j.identifier('newFunc'),
[] // Lost the original arguments!
));Correct (callback preserves context):
root.find(j.CallExpression, { callee: { name: 'oldFunc' } })
.replaceWith(path => {
// Access original node through path
return j.callExpression(
j.identifier('newFunc'),
path.node.arguments // Preserve original arguments
);
});Alternative (add argument to existing call):
// Add a new first argument while preserving existing ones
root.find(j.CallExpression, { callee: { name: 'translate' } })
.replaceWith(path => {
return j.callExpression(
path.node.callee,
[
j.identifier('locale'), // New first argument
...path.node.arguments // Original arguments
]
);
});Complex example (wrap in another call):
// Wrap: oldFunc(args) → wrapper(oldFunc(args))
root.find(j.CallExpression, { callee: { name: 'oldFunc' } })
.replaceWith(path => {
return j.callExpression(
j.identifier('wrapper'),
[path.node] // Original call becomes argument
);
});Reference: jscodeshift API - replaceWith
Avoid Repeated find() Calls for Same Node Type
Each find() call traverses the AST. Cache the collection when accessing the same node type multiple times.
Incorrect (traverses AST 3 times):
// First traversal
const hasRequireCalls = root.find(j.CallExpression, { callee: { name: 'require' } }).size() > 0;
// Second traversal - same nodes
const requirePaths = root.find(j.CallExpression, { callee: { name: 'require' } }).paths();
// Third traversal - same nodes again
root.find(j.CallExpression, { callee: { name: 'require' } })
.replaceWith(path => /* transform */);Correct (single traversal, cached):
// Single traversal, reuse collection
const requireCalls = root.find(j.CallExpression, { callee: { name: 'require' } });
const hasRequireCalls = requireCalls.size() > 0;
const requirePaths = requireCalls.paths();
requireCalls.replaceWith(path => /* transform */);Alternative (for conditional transforms):
const requireCalls = root.find(j.CallExpression, { callee: { name: 'require' } });
if (requireCalls.size() === 0) {
return null; // Early return, no changes
}
// Now safe to transform
requireCalls.replaceWith(/* ... */);Benefits:
- Each
find()is O(n) where n = AST nodes - Caching reduces 3×O(n) to 1×O(n)
- Collections are lazy - operations chain without intermediate traversals
Reference: jscodeshift - Collections
Use closestScope() for Scope-Aware Transforms
Variable names can be shadowed in nested scopes. Use closestScope() to ensure transforms only affect the correct binding.
Incorrect (ignores variable shadowing):
// Renames ALL 'data' identifiers, even shadowed ones
root.find(j.Identifier, { name: 'data' })
.forEach(path => {
path.node.name = 'payload';
});
// Input:
// const data = fetch();
// function process(data) { return data; }
//
// Broken output (shadows broken):
// const payload = fetch();
// function process(payload) { return payload; }Correct (scope-aware transformation):
// Only rename 'data' in module scope, not function parameters
root.find(j.VariableDeclarator, { id: { name: 'data' } })
.filter(path => {
// Check if this is at module scope
const scope = path.scope;
return scope.isGlobal || scope.path.node.type === 'Program';
})
.forEach(path => {
const binding = path.scope.getBindings()['data'];
// Rename all references to this specific binding
binding?.forEach(refPath => {
refPath.node.name = 'payload';
});
path.node.id.name = 'payload';
});Alternative (using closestScope):
root.find(j.Identifier, { name: 'oldName' })
.filter(path => {
// Only transform if in the target scope
const scope = path.closestScope();
return scope.node.type === 'FunctionDeclaration' &&
scope.node.id?.name === 'targetFunction';
});Note: Always consider scope when renaming identifiers or the codemod will corrupt variable bindings.
Reference: ast-types - Scope
Return Early When No Transformation Needed
Calling toSource() is expensive as it regenerates the entire file. Return early when no changes are needed to skip code generation entirely.
Incorrect (always calls toSource):
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
root.find(j.CallExpression, { callee: { name: 'oldFunction' } })
.replaceWith(path => j.callExpression(
j.identifier('newFunction'),
path.node.arguments
));
// toSource() called even when no changes made
return root.toSource();
};Correct (early return on no changes):
module.exports = function transformer(file, api) {
const j = api.jscodeshift;
const root = j(file.source);
const calls = root.find(j.CallExpression, { callee: { name: 'oldFunction' } });
// Skip toSource() if nothing to transform
if (calls.size() === 0) {
return null; // Signal: no changes
}
calls.replaceWith(path => j.callExpression(
j.identifier('newFunction'),
path.node.arguments
));
return root.toSource();
};Alternative (using undefined):
// Both null and undefined signal "no changes"
if (calls.size() === 0) {
return undefined;
}Benefits:
- Unchanged files skip expensive parsing and printing
- jscodeshift reports accurate "unchanged" counts
- Significant speedup on large codebases with sparse changes
Reference: jscodeshift README
Use find() with Filter Object Over filter() Chain
The find() method accepts a filter object as its second argument, filtering during traversal. This is faster than traversing first and filtering after.
Incorrect (find then filter chain):
// Traverses entire AST, then iterates result twice
root.find(j.CallExpression)
.filter(path => path.node.callee.type === 'MemberExpression')
.filter(path => path.node.callee.object.name === 'console')
.filter(path => path.node.callee.property.name === 'log');Correct (filter object in find):
// Single traversal with inline filtering
root.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
object: { name: 'console' },
property: { name: 'log' }
}
});When to use filter() after find():
// Use filter() for complex conditions that can't be expressed as object matchers
root.find(j.CallExpression, {
callee: { object: { name: 'console' } }
})
.filter(path => {
// Complex logic: method must be log, warn, or error
const method = path.node.callee.property.name;
return ['log', 'warn', 'error'].includes(method);
});Benefits:
- Filter object uses direct property comparison (fast)
- filter() callback invokes function for each node (slower)
- Combine both: filter object for structure, filter() for complex logic
Reference: jscodeshift API Reference
Use Specific Node Types in find() Calls
Using generic node types in find() traverses the entire AST. Specify the exact node type to reduce search space dramatically.
Incorrect (overly generic traversal):
// Finds ALL expressions, then filters - O(n) full AST walk
root.find(j.Expression)
.filter(path => path.node.type === 'CallExpression')
.filter(path => path.node.callee.name === 'require');Correct (specific type reduces search space):
// Finds only CallExpressions - skips irrelevant nodes
root.find(j.CallExpression, {
callee: { name: 'require' }
});Alternative (for member expressions):
// Instead of finding all Identifiers and filtering
// Incorrect:
root.find(j.Identifier).filter(path => path.parent.node.type === 'MemberExpression');
// Correct:
root.find(j.MemberExpression, {
object: { name: 'console' }
});Benefits:
- Reduces nodes visited by 90%+ on typical files
- Second argument to
find()filters during traversal, not after - jscodeshift short-circuits non-matching branches
Reference: jscodeshift API Reference - find()
Use Two-Pass Pattern for Complex Transforms
Nested find() calls inside forEach() create O(n²) complexity. Use a two-pass pattern: collect data first, then transform.
Incorrect (O(n²) nested traversal):
// For each import, searches entire AST again
root.find(j.ImportDeclaration)
.forEach(importPath => {
const importedNames = importPath.node.specifiers.map(s => s.local.name);
// O(n) traversal for EACH import = O(n²) total
root.find(j.Identifier)
.filter(idPath => importedNames.includes(idPath.node.name))
.forEach(idPath => {
// transform usage
});
});Correct (O(n) two-pass approach):
// Pass 1: Collect all data in single traversal
const importedBindings = new Map();
root.find(j.ImportDeclaration)
.forEach(importPath => {
importPath.node.specifiers.forEach(specifier => {
importedBindings.set(specifier.local.name, {
source: importPath.node.source.value,
imported: specifier.imported?.name || 'default'
});
});
});
// Pass 2: Transform using collected data
root.find(j.Identifier)
.filter(idPath => importedBindings.has(idPath.node.name))
.forEach(idPath => {
const binding = importedBindings.get(idPath.node.name);
// Transform using pre-collected data
});Benefits:
- Single traversal for collection, single traversal for transformation
- Map lookups are O(1) vs array includes() O(n)
- Scales linearly with file size
Reference: Martin Fowler - Refactoring with Codemods
Related skills
How it compares
Choose jscodeshift over manual refactors when changes must scale across many files with AST precision and reproducible transform scripts.
FAQ
What is jscodeshift used for?
jscodeshift is Meta's toolkit for running codemods over JavaScript and TypeScript files. Developers write transform modules that parse source into an AST, modify nodes programmatically, and regenerate code while preserving formatting via recast.
When should developers use codemods instead of find-and-replace?
jscodeshift codemods suit structural refactors—renaming exports, updating import paths, or migrating library APIs—where string replacement would miss edge cases. AST transforms understand syntax, not just text patterns.