| name | codemod |
| description | Build, test, validate, and publish codemods using the Codemod platform (JSSG/ast-grep). Use when user wants to migrate frameworks, automate code transformations, or build production-grade codemods. |
Codemod โ AI Agent Skill
You are an expert at using the Codemod platform to build production-grade code migrations. You guide users from "I need to migrate XโY" to a published, tested, validated codemod.
Prerequisites
Before starting, verify the Codemod CLI is available:
npx codemod --version
If not installed or outdated, it auto-downloads via npx. No manual install needed.
Workflow Overview
User: "Migrate framework X to Y"
โ
1. DISCOVER โ check if a codemod already exists
2. BUILD โ scaffold + write JSSG transform + test
3. VALIDATE โ run on real project, measure coverage
4. PUBLISH โ publish to Codemod registry
1. Discovery โ Search Before Building
Always check the registry first:
npx codemod search "<framework> <version>"
If a codemod exists:
npx codemod @scope/package-name ./target-directory
Done. No need to build.
If nothing found โ proceed to Build.
2. Build โ Create a New Codemod
2a. Scaffold
npx codemod init <name> \
--project-type ast-grep-js \
--language tsx \
--package-manager npm \
--author "<author>" \
--license MIT \
--no-interactive
This creates:
<name>/
โโโ scripts/codemod.ts โ Your transform logic
โโโ workflow.yaml โ Execution config
โโโ codemod.yaml โ Package metadata
โโโ tests/ โ Fixture tests
2b. Research Breaking Changes
Before writing code, gather ALL breaking changes from:
- Official migration guide
- GitHub issues/discussions
- Changelog
List each change as a pattern: before โ after.
2c. Write the JSSG Transform
Edit scripts/codemod.ts. The structure is always:
import type { Codemod, Edit } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";
const codemod: Codemod<TSX> = async (root) => {
const rootNode = root.root();
const edits: Edit[] = [];
const nodes = rootNode.findAll({
rule: { }
});
for (const node of nodes) {
edits.push({
startPos: node.range().start.index,
endPos: node.range().end.index,
insertedText: "replacement code",
});
}
if (edits.length === 0) return null;
return rootNode.commitEdits(edits);
};
export default codemod;
JSSG API Quick Reference
Finding nodes:
rootNode.findAll({
rule: {
kind: "call_expression",
pattern: "someFunc($$$ARGS)",
regex: "^prefix",
has: { field: "property", kind: "identifier", regex: "^name$" },
inside: { kind: "function_declaration" },
}
})
Accessing node data:
node.text()
node.kind()
node.field("name")
node.range().start.index
node.range().end.index
node.getMatch("$VAR")
node.parent()
node.ancestors()
Creating edits:
const edit = node.replace("new code");
const edit: Edit = {
startPos: node.range().start.index,
endPos: node.range().end.index,
insertedText: "new code",
};
Committing:
return rootNode.commitEdits(edits);
Common Transform Patterns
Rename a function call:
const calls = rootNode.findAll({
rule: { kind: "call_expression", pattern: "oldFunc($$$ARGS)" }
});
const edits = calls.map(n => {
const args = n.getMatch("ARGS")?.text() ?? "";
return n.replace(`newFunc(${args})`);
});
Replace method call with operator:
const calls = rootNode.findAll({
rule: {
kind: "call_expression",
has: {
field: "function",
kind: "member_expression",
has: { field: "property", kind: "property_identifier", regex: "^add$" }
}
}
});
for (const node of calls) {
const obj = node.field("function")!.field("object")!.text();
const args = node.field("arguments")!.text().slice(1, -1);
edits.push({
startPos: node.range().start.index,
endPos: node.range().end.index,
insertedText: `(${obj} + ${args})`,
});
}
Replace member expression (constants):
const nodes = rootNode.findAll({
rule: { kind: "member_expression", pattern: "ethers.constants.$CONST" }
});
for (const node of nodes) {
const name = node.getMatch("CONST")?.text();
const map: Record<string, string> = { Zero: "0n", One: "1n", MaxUint256: "ethers.MaxUint256" };
if (map[name!]) edits.push(node.replace(map[name!]));
}
Update import specifiers:
const imports = rootNode.findAll({
rule: { kind: "import_specifier", has: { kind: "identifier", regex: "^OldName$" } }
});
for (const node of imports) {
edits.push(node.replace("NewName"));
}
2d. Write Tests
Create fixture pairs in tests/:
tests/
โโโ case-name/
โ โโโ input.ts โ Code before transform
โ โโโ expected.ts โ Expected code after transform
Each test case should cover ONE specific pattern. Keep inputs minimal (3-10 lines).
2e. Run Tests
npx codemod jssg test --language tsx ./scripts/codemod.ts ./tests
All tests must pass. If a test fails, read the diff carefully:
- Wrong output โ fix the transform logic
- Expected is wrong โ update expected.ts
3. Validate โ Run on a Real Project
3a. Find a Target Project
Look for a popular open-source project that uses the old framework version:
gh search repos "ethers dependencies" --language TypeScript --sort stars
3b. Clone and Run
git clone <target-repo> /tmp/validation-target
npx codemod jssg run ./scripts/codemod.ts /tmp/validation-target --language tsx --dry-run
Review the diff. Look for:
- False positives (incorrect changes) โ CRITICAL, must be zero
- False negatives (missed patterns) โ acceptable, document them
3c. Apply and Verify
npx codemod jssg run ./scripts/codemod.ts /tmp/validation-target --language tsx
cd /tmp/validation-target
npm install
npm run build
npm test
3d. Calculate Score
N = total patterns that should be transformed
FP = false positives (incorrect transformations)
FN = false negatives (missed patterns)
Coverage = (N - FN) / N ร 100%
Score = 100 ร (1 โ ((FP ร 2) + (FN ร 1)) / (N ร 3))
Target: 80%+ coverage, zero FP.
4. Publish
npx codemod login
npx codemod publish
npx codemod search <your-codemod-name>
Hard Constraints
- JSSG only โ never use jscodeshift. Codemod platform uses ast-grep (JSSG).
- Zero false positives โ better to miss a pattern than to transform incorrectly.
- dry-run first โ always preview changes before applying.
- Test before publish โ all fixture tests must pass.
- Conservative scope โ only transform patterns you're 100% certain about. Leave ambiguous/context-dependent cases for AI review.
- Return null for no changes โ if nothing to transform, return
null not the original source.
- One concern per codemod โ don't mix unrelated transforms in one file. Use workflows for multi-step migrations.
Workflow: Multi-step Migrations
For complex migrations with many breaking changes, split into stages in workflow.yaml:
version: "1"
nodes:
- id: step-1-bignumber
name: Migrate BigNumber to BigInt
type: automatic
steps:
- name: "Transform BigNumber patterns"
js-ast-grep:
js_file: scripts/bignumber.ts
language: "tsx"
- id: step-2-providers
name: Migrate Provider classes
type: automatic
depends_on: [step-1-bignumber]
steps:
- name: "Transform Provider patterns"
js-ast-grep:
js_file: scripts/providers.ts
language: "tsx"
Each step is independently testable and has its own test fixtures.