Batch Migration
Recipes for large-scale code changes that touch tens or hundreds of files. Each recipe distributes work across agents, handles conflicts, and verifies changes after each batch.
How /batch Works
/batch <recipe> [options] # Run a batch migration
/batch cjs-to-esm --dry-run # Preview changes without writing
/batch status # Check running batch progress
/batch rollback # Revert last batch (via git)
Architecture
/batch recipe
|
v
Discovery Phase
(find all files to change)
|
v
Plan Phase
(group into batches of 10-20 files)
|
v
Execute Phase -----> Agent 1: Batch 1 (files 1-15)
| +--> Agent 2: Batch 2 (files 16-30)
| +--> Agent 3: Batch 3 (files 31-45)
|
v
Verify Phase
(typecheck, lint, test per batch)
|
v
Commit Phase
(one commit per batch or squash all)
Safety: The Batch Contract
Every batch migration follows these rules:
- Discovery before execution -- always know the full scope first
- Dry run first -- preview every change before writing
- Batch size limit -- max 20 files per batch (prevents context overflow)
- Verify after each batch -- typecheck + lint + test
- Rollback ready -- each batch is a separate commit (easy to revert)
- Conflict resolution -- stop on conflict, don't guess
Recipe 1: CJS to ESM
Convert CommonJS require() / module.exports to ES module import / export.
Command
/batch cjs-to-esm
/batch cjs-to-esm --dry-run # Preview only
/batch cjs-to-esm --dir src/ # Limit scope
Discovery
Find all .js/.ts files containing:
- require('...')
- module.exports
- exports.something
Transforms
| From | To |
|---|---|
const x = require('x') |
import x from 'x' |
const { a, b } = require('x') |
import { a, b } from 'x' |
module.exports = X |
export default X |
module.exports = { a, b } |
export { a, b } |
exports.a = X |
export const a = X |
require.resolve('x') |
import.meta.resolve('x') |
__dirname |
import.meta.dirname (Node 21+) or dirname(fileURLToPath(import.meta.url)) |
__filename |
import.meta.filename (Node 21+) or fileURLToPath(import.meta.url) |
Post-Migration
- Update
package.json: add"type": "module" - Update
tsconfig.json: set"module": "ESNext","moduleResolution": "bundler" - Fix dynamic imports:
require(var)becomesawait import(var)
Verification
Per batch:
1. npx tsc --noEmit (typecheck)
2. npm run lint (lint)
3. npm test (tests)
Recipe 2: React Class to Hooks
Convert React class components to function components with hooks.
Command
/batch class-to-hooks
/batch class-to-hooks --dir src/components/
Discovery
Find all .tsx/.jsx files containing:
- extends React.Component
- extends Component
- class.*extends.*PureComponent
Transforms
| From | To |
|---|---|
class X extends Component |
function X(props) / const X: FC<Props> = |
this.state = { ... } |
const [x, setX] = useState(...) |
this.setState({ x }) |
setX(x) |
componentDidMount() |
useEffect(() => { ... }, []) |
componentDidUpdate(prevProps) |
useEffect(() => { ... }, [deps]) |
componentWillUnmount() |
useEffect(() => { return () => cleanup }, []) |
this.props.x |
props.x (or destructured) |
this.handleClick = this.handleClick.bind(this) |
Remove (arrow functions) |
static defaultProps |
Default parameter values |
shouldComponentUpdate |
React.memo() wrapper |
Edge Cases (Human Gate)
These require manual review and are flagged, not auto-converted:
- Components using
getSnapshotBeforeUpdate - Components using
componentDidCatch/ error boundaries - Components with complex
thisreferences beyond props/state - Higher-order components wrapping class components
Verification
Per batch:
1. npx tsc --noEmit
2. npm run lint
3. npm test (especially component tests)
4. Visual verify (if available) for UI components
Recipe 3: Multi-File Rename
Rename a symbol (function, class, type, variable) across the entire codebase.
Command
/batch rename --from "UserProfile" --to "AccountProfile"
/batch rename --from "getUserData" --to "fetchUserProfile" --dir src/
Discovery
Find all files containing the symbol:
- Import statements
- Export statements
- Usage in code
- Type references
- JSDoc comments
- Test files
- Documentation
Transforms
- Rename in import/export statements
- Rename in usage sites
- Rename in type annotations
- Rename in string literals (only if clearly a reference, e.g., route paths)
- Rename corresponding test files (e.g.,
UserProfile.test.tsxtoAccountProfile.test.tsx) - Rename file if it matches the symbol name
Conflict Resolution
If the new name already exists in a file's scope:
- Stop the batch
- Report the conflict with file path and line number
- Wait for human decision
Recipe 4: Dependency Upgrade (Major Version)
Upgrade a dependency across a monorepo with breaking changes.
Command
/batch upgrade react@19
/batch upgrade next@15 --packages "apps/*"
Steps
- Discovery: Find all
package.jsonfiles referencing the dependency - Changelog review: Read the upgrade guide / migration docs
- Update versions: Bump version in all package.json files
- Install: Run install in each package
- Apply codemods: Run official codemods if available (e.g.,
npx @next/codemod) - Fix breaking changes: Apply transforms from changelog
- Verify: Typecheck, lint, test per package
Example: React 18 to 19
Batch 1: Update package.json files (all packages)
Batch 2: Run react-codemod (automated transforms)
Batch 3: Fix remaining type errors (manual transforms)
Batch 4: Update test utilities (testing-library)
Batch 5: Verify all packages build and test
Recipe 5: API Schema Change
Update all consumers when an API response shape changes.
Command
/batch api-schema --schema api/schema.ts --change "User.name -> User.displayName"
Steps
- Discovery: Find all files that reference the changed field
- Type update: Update the schema/type definition
- Server update: Update API handlers that produce the field
- Client update: Update all consumers that read the field
- Test update: Update fixtures, mocks, and assertions
- Migration: Generate database migration if field is persisted
Conflict Resolution
For ambiguous references (e.g., name is too generic):
- Use type-aware search (follow imports from the schema type)
- Flag ambiguous matches for human review
- Never rename unrelated
namefields
Recipe 6: Linter Rule Enforcement
Apply a new lint rule across the entire codebase.
Command
/batch lint-enforce --rule "no-explicit-any"
/batch lint-enforce --rule "prefer-const" --autofix
Steps
- Enable rule: Add rule to
.eslintrc/eslint.config.js - Discovery: Run linter to find all violations
- Autofix round: Apply
--fixfor auto-fixable violations - Manual round: Fix remaining violations in batches
- Verify: Run linter (zero violations), then run tests
Common Enforcement Patterns
| Rule | Auto-Fix Rate | Manual Work |
|---|---|---|
prefer-const |
100% | None |
no-var |
100% | None |
no-explicit-any |
0% | Type every any |
strict-boolean-expressions |
50% | Fix truthy checks |
no-floating-promises |
0% | Add await or void |
consistent-type-imports |
90% | Fix edge cases |
Batch Execution Strategy
Parallel Agents
For independent files (no cross-file dependencies):
Agent 1: files 1-15 → verify → commit
Agent 2: files 16-30 → verify → commit
Agent 3: files 31-45 → verify → commit
Sequential Batches
For dependent changes (types used by other files):
Batch 1: Update type definitions → verify → commit
Batch 2: Update implementations → verify → commit
Batch 3: Update tests → verify → commit
Batch 4: Update documentation → verify → commit
Conflict Resolution
If two agents modify the same file:
- Both agents stop
- Merge the changes manually (or let the second agent rebase)
- Verify the merged result
- Resume
Checkpoint Schema
{
"task": "batch-migration",
"recipe": "cjs-to-esm",
"status": "in-progress",
"discovery": {
"totalFiles": 87,
"batches": 6,
"filesPerBatch": 15
},
"progress": {
"completedBatches": 3,
"completedFiles": 45,
"failedFiles": 2,
"skippedFiles": 1
},
"verification": {
"typecheck": "passing",
"lint": "3 warnings",
"tests": "142/142 passing"
},
"errors": [
{ "file": "src/legacy/parser.js", "error": "Dynamic require with variable path" },
{ "file": "src/plugins/loader.js", "error": "Circular dependency detected" }
]
}
Anti-Patterns
- Never batch-migrate without a dry run first
- Never modify more than 20 files in a single agent context (context overflow)
- Never skip verification between batches (one bad batch cascades)
- Never force-push batch migration commits (makes rollback impossible)
- Never auto-resolve merge conflicts in batch mode (always stop and report)