
Codemod
- 274 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
codemod is an agent skill that teaches JSSG, ast-grep, and workflow best practices for writing, reviewing, and debugging safe AST-based code transformations and automated migrations.
About
codemod is an agent skill from pproenca/dot-skills that distills best practices for writing, reviewing, and debugging automated code transformations with Codemod, JSSG, ast-grep, and YAML-driven workflows. It packages 48 prioritized rules across 11 categories spanning AST understanding, pattern efficiency, parsing strategy, node traversal, semantic analysis, edit operations, workflow design, testing, state management, security, and package structure. Developers reach for codemod when authoring new migrations, debugging pattern matching failures, designing CI bulk-refactor pipelines, or auditing transforms for idempotency and cross-file semantic safety. The skill acts as a checklist emphasizing parser selection, batched edits, resumable workflow state, and test fixtures before running transformations on production repositories.
- codemod
Codemod by the numbers
- 274 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,406 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 codemodAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 274 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do you write safe AST codemod migrations?
Use codemod for development tasks
Who is it for?
Developers building JSSG or ast-grep codemods for large-scale refactors who need safety and performance guardrails.
Skip if: One-off manual edits, runtime application debugging, or OpenAPI client generation tasks covered by Orval skills.
When should I use this skill?
The user writes, reviews, or debugs codemods, ast-grep rules, JSSG transforms, or automated migration workflows.
What you get
Reviewed codemod source, workflow YAML configuration, test fixtures, and validated AST transformation rules.
- codemod rule set
- workflow configuration
- transform test fixtures
By the numbers
- Contains 48 rules across 11 categories
- Covers JSSG, ast-grep, and YAML workflow transformations
Files
Codemod Best Practices
Comprehensive best practices guide for Codemod (JSSG, ast-grep, workflows), designed for AI agents and LLMs. Contains 48 rules across 11 categories, prioritized by impact to guide automated refactoring and code generation.
When to Apply
Reference these guidelines when:
- Writing new codemods with JSSG or ast-grep
- Designing workflow configurations for migrations
- Debugging pattern matching or AST traversal issues
- Reviewing codemod code for performance and safety
- Setting up test fixtures for transform validation
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | AST Understanding | CRITICAL | ast- |
| 2 | Pattern Efficiency | CRITICAL | pattern- |
| 3 | Parsing Strategy | CRITICAL | parse- |
| 4 | Node Traversal | HIGH | traverse- |
| 5 | Semantic Analysis | HIGH | semantic- |
| 6 | Edit Operations | MEDIUM-HIGH | edit- |
| 7 | Workflow Design | MEDIUM-HIGH | workflow- |
| 8 | Testing Strategy | MEDIUM | test- |
| 9 | State Management | MEDIUM | state- |
| 10 | Security and Capabilities | LOW-MEDIUM | security- |
| 11 | Package Structure | LOW | pkg- |
Quick Reference
1. AST Understanding (CRITICAL)
- `ast-explore-before-writing` - Use AST Explorer before writing patterns
- `ast-understand-named-vs-anonymous` - Understand named vs anonymous nodes
- `ast-use-kind-for-precision` - Use kind constraint for precision
- `ast-field-access-for-structure` - Use field access for structural queries
- `ast-check-null-before-access` - Check null before property access
2. Pattern Efficiency (CRITICAL)
- `pattern-use-meta-variables` - Use meta variables for flexible matching
- `pattern-avoid-overly-generic` - Avoid overly generic patterns
- `pattern-combine-with-rules` - Combine patterns with rule operators
- `pattern-use-constraints` - Use constraints for reusable matching logic
- `pattern-use-relational-patterns` - Use relational patterns for context
- `pattern-ensure-idempotency` - Ensure patterns are idempotent
3. Parsing Strategy (CRITICAL)
- `parse-select-correct-parser` - Select the correct parser for file type
- `parse-handle-embedded-languages` - Handle embedded languages with parseAsync
- `parse-provide-pattern-context` - Provide context for ambiguous patterns
- `parse-early-return-non-applicable` - Early return for non-applicable files
4. Node Traversal (HIGH)
- `traverse-use-find-vs-findall` - Use find() for single match, findAll() for multiple
- `traverse-single-pass-collection` - Collect multiple patterns in single traversal
- `traverse-use-stopby-for-depth` - Use stopBy to control traversal depth
- `traverse-use-siblings-efficiently` - Use sibling navigation efficiently
- `traverse-cache-repeated-lookups` - Cache repeated node lookups
5. Semantic Analysis (HIGH)
- `semantic-use-file-scope-first` - Use file scope semantic analysis first
- `semantic-check-null-results` - Handle null semantic analysis results
- `semantic-verify-file-ownership` - Verify file ownership before cross-file edits
- `semantic-cache-cross-file-results` - Cache semantic analysis results
6. Edit Operations (MEDIUM-HIGH)
- `edit-batch-before-commit` - Batch edits before committing
- `edit-preserve-formatting` - Preserve surrounding formatting in edits
- `edit-handle-overlapping-ranges` - Handle overlapping edit ranges
- `edit-use-flatmap-for-conditional` - Use flatMap for conditional edits
- `edit-add-imports-correctly` - Add imports at correct position
7. Workflow Design (MEDIUM-HIGH)
- `workflow-order-nodes-by-dependency` - Order nodes by dependency
- `workflow-use-matrix-for-parallelism` - Use matrix strategy for parallelism
- `workflow-use-manual-gates` - Use manual gates for critical steps
- `workflow-validate-before-run` - Validate workflows before running
- `workflow-use-conditional-steps` - Use conditional steps for dynamic workflows
8. Testing Strategy (MEDIUM)
- `test-use-fixture-pairs` - Use input/expected fixture pairs
- `test-cover-edge-cases` - Cover edge cases in test fixtures
- `test-use-strictness-levels` - Choose appropriate test strictness level
- `test-update-fixtures-intentionally` - Update test fixtures intentionally
- `test-run-on-subset-first` - Test on file subset before full run
9. State Management (MEDIUM)
- `state-use-for-resumability` - Use state for resumable migrations
- `state-make-transforms-idempotent` - Make transforms idempotent for safe reruns
- `state-log-progress-for-observability` - Log progress for long-running migrations
10. Security and Capabilities (LOW-MEDIUM)
- `security-minimize-capabilities` - Minimize requested capabilities
- `security-validate-external-inputs` - Validate external inputs before use
- `security-review-before-running-third-party` - Review third-party codemods before running
11. Package Structure (LOW)
- `pkg-use-semantic-versioning` - Use semantic versioning for packages
- `pkg-write-descriptive-metadata` - Write descriptive package metadata
- `pkg-organize-by-convention` - Organize package by convention
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 complete guide with all rules expanded, see AGENTS.md.
Codemod
Version 0.1.0 Codemod Community 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 Codemod (JSSG, ast-grep, workflows), designed for AI agents and LLMs. Contains 48 rules across 11 categories, prioritized by impact from critical (AST understanding, pattern efficiency, parsing strategy) to incremental (security, package structure). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
---
Table of Contents
1. AST Understanding — CRITICAL
- 1.1 Check Null Before Property Access — CRITICAL (prevents runtime crashes in transforms)
- 1.2 Understand Named vs Anonymous Nodes — CRITICAL (eliminates 80% of pattern matching failures)
- 1.3 Use AST Explorer Before Writing Patterns — CRITICAL (prevents hours of debugging invalid patterns)
- 1.4 Use Field Access for Structural Queries — CRITICAL (enables precise child selection in complex nodes)
- 1.5 Use kind Constraint for Precision — CRITICAL (reduces false positives by 10x)
2. Pattern Efficiency — CRITICAL
- 2.1 Avoid Overly Generic Patterns — CRITICAL (reduces matching time from minutes to seconds)
- 2.2 Combine Patterns with Rule Operators — CRITICAL (enables complex matching without multiple passes)
- 2.3 Ensure Patterns Are Idempotent — CRITICAL (prevents infinite transformation loops)
- 2.4 Use Constraints for Reusable Matching Logic — CRITICAL (eliminates pattern duplication across rules)
- 2.5 Use Meta Variables for Flexible Matching — CRITICAL (enables pattern reuse across variations)
- 2.6 Use Relational Patterns for Context — CRITICAL (enables context-aware matching without manual filtering)
3. Parsing Strategy — CRITICAL
- 3.1 Early Return for Non-Applicable Files — CRITICAL (10-100x speedup by skipping irrelevant files)
- 3.2 Handle Embedded Languages with parseAsync — CRITICAL (enables transformations in template literals and CSS-in-JS)
- 3.3 Provide Context for Ambiguous Patterns — CRITICAL (prevents 100% of ambiguous pattern failures)
- 3.4 Select the Correct Parser for File Type — CRITICAL (prevents 100% transform failures from AST mismatch)
4. Node Traversal — HIGH
- 4.1 Cache Repeated Node Lookups — HIGH (eliminates redundant traversals in transform loops)
- 4.2 Collect Multiple Patterns in Single Traversal — HIGH (reduces N traversals to 1 traversal)
- 4.3 Use find() for Single Match, findAll() for Multiple — HIGH (find() short-circuits, reducing traversal by up to 99%)
- 4.4 Use Sibling Navigation Efficiently — HIGH (O(1) sibling access vs O(n) re-traversal)
- 4.5 Use stopBy to Control Traversal Depth — HIGH (prevents unbounded searches in deeply nested code)
5. Semantic Analysis — HIGH
- 5.1 Cache Semantic Analysis Results — HIGH (avoids redundant cross-file resolution)
- 5.2 Handle Null Semantic Analysis Results — HIGH (prevents crashes when symbols are unresolvable)
- 5.3 Use File Scope Semantic Analysis First — HIGH (10-100x faster than workspace scope for local transforms)
- 5.4 Verify File Ownership Before Cross-File Edits — HIGH (prevents editing node_modules and external files)
6. Edit Operations — MEDIUM-HIGH
- 6.1 Add Imports at Correct Position — MEDIUM-HIGH (maintains valid module structure)
- 6.2 Batch Edits Before Committing — MEDIUM-HIGH (prevents edit conflicts and improves performance)
- 6.3 Handle Overlapping Edit Ranges — MEDIUM-HIGH (prevents corrupted output from conflicting edits)
- 6.4 Preserve Surrounding Formatting in Edits — MEDIUM-HIGH (maintains code style consistency)
- 6.5 Use flatMap for Conditional Edits — MEDIUM-HIGH (eliminates null filtering, reduces code by 30%)
7. Workflow Design — MEDIUM-HIGH
- 7.1 Order Nodes by Dependency — MEDIUM-HIGH (prevents failed transforms due to missing prerequisites)
- 7.2 Use Conditional Steps for Dynamic Workflows — MEDIUM-HIGH (reduces execution time by 30-70% for partial migrations)
- 7.3 Use Manual Gates for Critical Steps — MEDIUM-HIGH (prevents runaway migrations with human checkpoints)
- 7.4 Use Matrix Strategy for Parallelism — MEDIUM-HIGH (3-10x speedup for independent transformations)
- 7.5 Validate Workflows Before Running — MEDIUM-HIGH (prevents 100% of schema and dependency errors)
8. Testing Strategy — MEDIUM
- 8.1 Choose Appropriate Test Strictness Level — MEDIUM (reduces false test failures by 50-90%)
- 8.2 Cover Edge Cases in Test Fixtures — MEDIUM (prevents production failures on unusual code)
- 8.3 Test on File Subset Before Full Run — MEDIUM (catches errors 10-100x faster before full run)
- 8.4 Update Test Fixtures Intentionally — MEDIUM (prevents accidental regressions from auto-updates)
- 8.5 Use Input/Expected Fixture Pairs — MEDIUM (enables repeatable, automated validation)
9. State Management — MEDIUM
- 9.1 Log Progress for Long-Running Migrations — MEDIUM (enables monitoring and debugging of multi-hour migrations)
- 9.2 Make Transforms Idempotent for Safe Reruns — MEDIUM (prevents infinite loops and double-transformation)
- 9.3 Use State for Resumable Migrations — MEDIUM (enables restart from failure point in long migrations)
10. Security and Capabilities — LOW-MEDIUM
- 10.1 Minimize Requested Capabilities — LOW-MEDIUM (reduces attack surface for untrusted codemods)
- 10.2 Review Third-Party Codemods Before Running — LOW-MEDIUM (prevents malicious code execution from untrusted sources)
- 10.3 Validate External Inputs Before Use — LOW-MEDIUM (prevents injection attacks from malicious input)
11. Package Structure — LOW
- 11.1 Organize Package by Convention — LOW (enables tooling support and contributor onboarding)
- 11.2 Use Semantic Versioning for Packages — LOW (enables safe dependency management and updates)
- 11.3 Write Descriptive Package Metadata — LOW (3-5x better search ranking in registry)
---
References
1. https://docs.codemod.com 2. https://ast-grep.github.io 3. https://github.com/codemod/codemod 4. https://github.com/facebook/jscodeshift 5. https://martinfowler.com/articles/codemods-api-refactoring.html 6. https://www.hypermod.io/docs/guides/best-practices
---
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 in Imperative Form
Brief explanation (1-3 sentences) of WHY this matters. Focus on performance, correctness, or maintainability implications.
Incorrect (description of the problem):
// Production-realistic code example showing the anti-pattern
// Include comments explaining the cost/issueCorrect (description of the solution):
// Production-realistic code example showing the correct approach
// Use same variable names as incorrect example
// Include comments explaining the benefitWhen NOT to use this pattern:
- Exception 1
- Exception 2
Reference: Reference Title
{
"version": "1.1.6",
"organization": "Codemod Community",
"technology": "Codemod",
"date": "January 2026",
"abstract": "Comprehensive best practices guide for Codemod (JSSG, ast-grep, workflows), designed for AI agents and LLMs. Contains 48 rules across 11 categories, prioritized by impact from critical (AST understanding, pattern efficiency, parsing strategy) to incremental (security, package structure). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.",
"references": [
"https://docs.codemod.com",
"https://ast-grep.github.io",
"https://github.com/codemod/codemod",
"https://github.com/facebook/jscodeshift",
"https://martinfowler.com/articles/codemods-api-refactoring.html",
"https://www.hypermod.io/docs/guides/best-practices"
],
"category": "DevEx"
}
Codemod Best Practices
Best practices for writing efficient, safe, and maintainable code transformations with Codemod (JSSG, ast-grep, workflows).
Overview
This skill provides 48 rules across 11 categories to help you write better codemods:
| Impact | Categories |
|---|---|
| CRITICAL | AST Understanding, Pattern Efficiency, Parsing Strategy |
| HIGH | Node Traversal, Semantic Analysis |
| MEDIUM-HIGH | Edit Operations, Workflow Design |
| MEDIUM | Testing Strategy, State Management |
| LOW-MEDIUM/LOW | Security, Package Structure |
Structure
codemod/
├── SKILL.md # Entry point with quick reference
├── AGENTS.md # Compiled comprehensive guide
├── metadata.json # Version and references
├── README.md # This file
├── references/
│ ├── _sections.md # Category definitions
│ └── {prefix}-*.md # Individual rules (48 total)
└── assets/
└── templates/
└── _template.md # Rule templateGetting Started
# Install dependencies (if modifying this skill)
pnpm install
# Install codemod CLI
npm install -g codemod
# Initialize a new codemod project
npx codemod init
# Build this skill (if modifying)
pnpm build
# Validate this skill
pnpm validateCreating a New Rule
1. Choose the appropriate category from references/_sections.md 2. Create a new file: references/{prefix}-{description}.md 3. Follow the template in assets/templates/_template.md 4. Run validation to check formatting
| Category | Prefix | When to Use |
|---|---|---|
| AST Understanding | ast- | AST structure, tree-sitter concepts |
| Pattern Efficiency | pattern- | Pattern syntax, matching optimization |
| Parsing Strategy | parse- | Parser selection, language handling |
| Node Traversal | traverse- | Navigation, search optimization |
| Semantic Analysis | semantic- | Cross-file analysis, symbol resolution |
| Edit Operations | edit- | Code modification, formatting |
| Workflow Design | workflow- | YAML configuration, orchestration |
| Testing Strategy | test- | Fixtures, validation approaches |
| State Management | state- | Progress tracking, resumability |
| Security | security- | Capabilities, permissions |
| Package Structure | pkg- | Metadata, organization |
Rule File Structure
---
title: Rule Title in Imperative Form
impact: CRITICAL|HIGH|MEDIUM-HIGH|MEDIUM|LOW-MEDIUM|LOW
impactDescription: Quantified impact (e.g., "10x speedup")
tags: prefix, keyword1, keyword2
---
## Rule Title
Brief explanation of WHY this matters (1-3 sentences).
**Incorrect (what's wrong):**
\`\`\`typescript
// Code example showing the problem
\`\`\`
**Correct (what's right):**
\`\`\`typescript
// Code example showing the solution
\`\`\`
Reference: [Link](URL)File Naming Convention
Files follow the pattern: {prefix}-{description}.md
prefix: Category identifier (3-8 chars) from_sections.mddescription: Kebab-case description of the rule
Examples:
ast-explore-before-writing.mdpattern-avoid-overly-generic.mdworkflow-use-matrix-for-parallelism.md
Impact Levels
| Level | Definition | Examples |
|---|---|---|
| CRITICAL | Foundational issues that cascade through entire pipeline | Wrong parser, inefficient patterns |
| HIGH | Significant performance or correctness impact | Traversal optimization, semantic analysis |
| MEDIUM-HIGH | Important for reliability and maintainability | Edit batching, workflow design |
| MEDIUM | Good practices that prevent common issues | Testing, state management |
| LOW-MEDIUM | Security and safety considerations | Capabilities, input validation |
| LOW | Organization and discoverability | Package structure, metadata |
Scripts
# Validate skill structure
node ../../scripts/validate-skill.js ./
# Build AGENTS.md from references
node ../../scripts/build-agents-md.js ./Contributing
1. Follow the rule template exactly 2. Ensure examples are production-realistic 3. Quantify impact where possible 4. Include authoritative references 5. Run validation before submitting
Acknowledgments
Based on official Codemod documentation and community best practices:
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. AST Understanding (ast)
Impact: CRITICAL Description: Understanding AST structure is foundational for all transformations. Wrong tree interpretation leads to incorrect matches, missed transformations, and broken code.
2. Pattern Efficiency (pattern)
Impact: CRITICAL Description: Patterns are evaluated millions of times across large codebases. Inefficient or overly generic patterns create multiplicative performance problems.
3. Parsing Strategy (parse)
Impact: CRITICAL Description: Parser selection determines AST structure. Wrong parser choice cascades through the entire pipeline, producing invalid matches and failed transforms.
4. Node Traversal (traverse)
Impact: HIGH Description: Efficient navigation reduces O(n^2) to O(n) operations. Proper traversal strategies prevent redundant work across large codebases.
5. Semantic Analysis (semantic)
Impact: HIGH Description: Cross-file symbol resolution enables safe refactoring. Understanding definitions and references prevents breaking changes during migrations.
6. Edit Operations (edit)
Impact: MEDIUM-HIGH Description: Proper edit batching prevents conflicts and preserves formatting. Edit ordering affects transform reliability in multi-step operations.
7. Workflow Design (workflow)
Impact: MEDIUM-HIGH Description: Workflow structure determines parallelization, state management, and resumability. Poor design leads to failed migrations and manual intervention.
8. Testing Strategy (test)
Impact: MEDIUM Description: Comprehensive testing prevents production incidents. Fixture-based validation catches edge cases before deployment.
9. State Management (state)
Impact: MEDIUM Description: Proper state handling enables resumable, idempotent migrations. State persistence is critical for large-scale, multi-day transformations.
10. Security and Capabilities (security)
Impact: LOW-MEDIUM Description: JSSG's deny-by-default security model requires explicit capability grants. Minimal permissions reduce attack surface for untrusted codemods.
11. Package Structure (pkg)
Impact: LOW Description: Proper packaging enables discoverability, version management, and CI/CD integration. Well-structured packages are reusable and maintainable.
Check Null Before Property Access
AST navigation methods return null when nodes don't exist. Always check for null before accessing properties to prevent runtime crashes.
Incorrect (assumes nodes exist):
const transform: Transform<TSX> = (root) => {
const calls = root.findAll({ rule: { kind: "call_expression" } });
const edits = calls.map(call => {
// Crashes if callee is computed: obj[method]()
const methodName = call.field("function").field("property").text();
// Crashes if no arguments
const firstArg = call.field("arguments").children()[0].text();
return call.replace(`newMethod(${firstArg})`);
});
return root.commitEdits(edits);
};Correct (null-safe access):
const transform: Transform<TSX> = (root) => {
const calls = root.findAll({ rule: { kind: "call_expression" } });
const edits = calls.flatMap(call => {
const callee = call.field("function");
if (!callee) return [];
const property = callee.field("property");
if (!property) return [];
const args = call.field("arguments");
const firstArg = args?.children()[0];
if (!firstArg) return [];
const methodName = property.text();
return [call.replace(`newMethod(${firstArg.text()})`)];
});
return root.commitEdits(edits);
};Best practices:
- Use optional chaining (
?.) for exploratory access - Use explicit null checks before transformations
- Return empty arrays from
flatMapfor invalid nodes - Use TypeScript's narrowing with
ifstatements
Reference: JSSG API Reference
Use AST Explorer Before Writing Patterns
Always visualize the AST structure using AST Explorer before writing patterns. The tree structure often differs from what you expect based on source code appearance.
Incorrect (guessing AST structure):
const transform: Transform<TSX> = (root) => {
// Assumes 'const x = 1' has a direct 'identifier' child
const matches = root.findAll({
rule: { pattern: "const $NAME = $VALUE" }
});
// Pattern fails because 'const' creates a lexical_declaration
// with variable_declarator children, not direct identifiers
return null;
};Correct (verified in AST Explorer):
const transform: Transform<TSX> = (root) => {
// Verified: lexical_declaration > variable_declarator > name, value
const matches = root.findAll({
rule: {
kind: "variable_declarator",
has: { field: "name", pattern: "$NAME" }
}
});
// Pattern matches actual tree structure
return null;
};Workflow: 1. Paste target code in astexplorer.net 2. Select the correct parser (tree-sitter for ast-grep) 3. Click nodes to see their kind and field names 4. Write patterns matching the actual structure
Reference: ast-grep Pattern Syntax
Use Field Access for Structural Queries
Tree-sitter nodes have named fields that provide semantic access to children. Use field() and field constraints instead of index-based child access.
Incorrect (index-based access):
const transform: Transform<TSX> = (root) => {
const functions = root.findAll({ rule: { kind: "function_declaration" } });
for (const fn of functions) {
// Index-based access is fragile
const name = fn.children()[0]; // Might be 'async' keyword
const params = fn.children()[1]; // Might be name if no async
// Breaks with async functions, generators, type annotations
}
return null;
};Correct (field-based access):
const transform: Transform<TSX> = (root) => {
const functions = root.findAll({ rule: { kind: "function_declaration" } });
for (const fn of functions) {
// Field access is semantic and stable
const name = fn.field("name"); // Always the function name
const params = fn.field("parameters"); // Always the params list
const body = fn.field("body"); // Always the function body
if (name && params) {
console.log(`Function ${name.text()} has ${params.children().length} params`);
}
}
return null;
};Common field names:
- Functions:
name,parameters,body,return_type - Variables:
name,value,type - Calls:
function,arguments - Classes:
name,body,superclass
Reference: JSSG API Reference
Understand Named vs Anonymous Nodes
Tree-sitter distinguishes between named nodes (semantic) and anonymous nodes (punctuation, keywords). ast-grep patterns skip anonymous nodes by default, which affects pattern matching behavior.
Incorrect (matching anonymous nodes explicitly):
const transform: Transform<TSX> = (root) => {
// Tries to match punctuation literally
const matches = root.findAll({
rule: { pattern: "{ $KEY: $VALUE }" }
});
// Fails because '{', ':', '}' are anonymous nodes
// ast-grep skips them by default
return null;
};Correct (matching named nodes only):
const transform: Transform<TSX> = (root) => {
// Match the named 'object' node with pair children
const matches = root.findAll({
rule: {
kind: "object",
has: {
kind: "pair",
has: [
{ field: "key", pattern: "$KEY" },
{ field: "value", pattern: "$VALUE" }
]
}
}
});
return null;
};Named vs Anonymous:
- Named:
function_declaration,identifier,string(semantic meaning) - Anonymous:
{,},:,;,const(syntax punctuation)
Use node.isNamed() to check node type programmatically.
Reference: ast-grep Core Concepts
Use kind Constraint for Precision
Combine pattern matching with kind constraints to eliminate false positives. Patterns alone can match unintended code structures with similar text.
Incorrect (pattern without kind constraint):
const transform: Transform<TSX> = (root) => {
// Pattern matches too broadly
const matches = root.findAll({
rule: { pattern: "console.log($ARG)" }
});
// Also matches: const console = { log: fn }; console.log(x)
// And: "console.log(test)" in strings
// And: // console.log(debug) in comments
return null;
};Correct (pattern with kind constraint):
const transform: Transform<TSX> = (root) => {
// Constrain to actual call expressions only
const matches = root.findAll({
rule: {
kind: "call_expression",
pattern: "console.log($ARG)"
}
});
// Only matches real console.log() calls
// Ignores strings, comments, and shadowed variables
return null;
};Common kind values:
call_expression- function callsmember_expression- property access (a.b)arrow_function- arrow functionsfunction_declaration- named functionsjsx_element- JSX tags
Reference: ast-grep Pattern Syntax
Add Imports at Correct Position
When adding new imports, insert them at the correct position relative to existing imports. Respect import ordering conventions.
Incorrect (appending to end):
const transform: Transform<TSX> = (root) => {
const needsLogger = root.find({
rule: { pattern: "logger.$METHOD($$$)" }
});
if (!needsLogger) return null;
const source = root.root().text();
// Appending import at end breaks module structure
return source + '\nimport { logger } from "utils/logger";';
// Import appears after code - invalid syntax!
};Correct (inserting with existing imports):
const transform: Transform<TSX> = (root) => {
const needsLogger = root.find({
rule: { pattern: "logger.$METHOD($$$)" }
});
if (!needsLogger) return null;
// Check if import already exists
const existingImport = root.find({
rule: { pattern: 'import { logger } from "utils/logger"' }
});
if (existingImport) return null; // Already imported
// Find last import statement
const imports = root.findAll({ rule: { kind: "import_statement" } });
const lastImport = imports[imports.length - 1];
if (lastImport) {
// Insert after last import
const range = lastImport.range();
const source = root.root().text();
const before = source.slice(0, range.end);
const after = source.slice(range.end);
return before + '\nimport { logger } from "utils/logger";' + after;
}
// No imports exist - add at top after any comments/directives
const firstNode = root.root().children()[0];
if (firstNode) {
const range = firstNode.range();
const source = root.root().text();
return 'import { logger } from "utils/logger";\n\n' + source;
}
return null;
};Import ordering conventions: 1. Node built-ins (fs, path) 2. External packages (react, lodash) 3. Internal aliases (@/utils, ~/lib) 4. Relative imports (./, ../)
Reference: JSSG API Reference
Batch Edits Before Committing
Collect all edits into an array and call commitEdits() once at the end. Multiple commits can cause conflicts and performance degradation.
Incorrect (committing inside loop):
const transform: Transform<TSX> = (root) => {
const consoleCalls = root.findAll({
rule: { pattern: "console.log($$$ARGS)" }
});
let result = root.root().text();
for (const call of consoleCalls) {
// Each commit regenerates the entire source string
result = root.commitEdits([call.replace("logger.info()")]);
// Edits applied sequentially - O(n²) string operations
// Later edits may use stale positions
}
return result;
};Correct (batched commits):
const transform: Transform<TSX> = (root) => {
const consoleCalls = root.findAll({
rule: { pattern: "console.log($$$ARGS)" }
});
// Collect all edits first
const edits = consoleCalls.map(call => {
const args = call.getMultipleMatches("ARGS");
const argsText = args.map(a => a.text()).join(", ");
return call.replace(`logger.info(${argsText})`);
});
// Single commit with all edits
return root.commitEdits(edits);
// Edits applied atomically - O(n) string operations
// Position calculations are accurate
};Why batching matters:
- Single string reconstruction pass
- Correct position calculations for overlapping ranges
- Atomic application (all or nothing)
- Better performance for large edit sets
Reference: JSSG API Reference
Handle Overlapping Edit Ranges
When multiple edits target overlapping source ranges, later edits may corrupt earlier ones. Detect and resolve conflicts before committing.
Incorrect (overlapping edits):
const transform: Transform<TSX> = (root) => {
// Find both outer and inner expressions
const outer = root.findAll({
rule: { pattern: "outer($INNER)" }
});
const inner = root.findAll({
rule: { pattern: "inner($ARG)" }
});
// Both match overlapping ranges in: outer(inner(x))
const edits = [
...outer.map(o => o.replace("newOuter()")),
...inner.map(i => i.replace("newInner()"))
];
// Result is corrupted: overlapping replacements
return root.commitEdits(edits);
};Correct (conflict detection):
const transform: Transform<TSX> = (root) => {
const outer = root.findAll({
rule: { pattern: "outer($INNER)" }
});
const inner = root.findAll({
rule: { pattern: "inner($ARG)" }
});
// Collect edits with range info
const outerEdits = outer.map(o => ({
node: o,
range: o.range(),
edit: o.replace("newOuter()")
}));
const innerEdits = inner.map(i => ({
node: i,
range: i.range(),
edit: i.replace("newInner()")
}));
// Filter out inner edits that overlap with outer
const nonOverlapping = innerEdits.filter(inner =>
!outerEdits.some(outer =>
rangesOverlap(inner.range, outer.range)
)
);
const finalEdits = [
...outerEdits.map(e => e.edit),
...nonOverlapping.map(e => e.edit)
];
return root.commitEdits(finalEdits);
};
function rangesOverlap(a: Range, b: Range): boolean {
return a.start < b.end && b.start < a.end;
}Strategies for overlapping edits:
- Prefer outer/parent edits over inner/child
- Process innermost first if preserving hierarchy
- Skip conflicting edits with filter
- Transform parent to include child changes
Reference: JSSG API Reference
Preserve Surrounding Formatting in Edits
When replacing nodes, preserve the surrounding whitespace and formatting to maintain code style consistency.
Incorrect (ignoring formatting context):
const transform: Transform<TSX> = (root) => {
const functions = root.findAll({
rule: { pattern: "function $NAME() { $$$BODY }" }
});
const edits = functions.map(fn => {
const name = fn.getMatch("NAME")?.text();
// Hardcoded formatting ignores original style
return fn.replace(`const ${name} = () => {}`);
// Original: function foo() { ... }
// Result: const foo = () => {}
// Lost: extra spacing, newlines, etc.
});
return root.commitEdits(edits);
};Correct (preserving formatting):
const transform: Transform<TSX> = (root) => {
const functions = root.findAll({
rule: { pattern: "function $NAME() { $$$BODY }" }
});
const edits = functions.map(fn => {
const name = fn.getMatch("NAME");
const body = fn.getMultipleMatches("BODY");
if (!name) return fn.replace(fn.text());
// Preserve body formatting exactly
const bodyText = body.map(b => b.text()).join("");
// Match the original node's formatting
const original = fn.text();
const leadingSpace = original.match(/^(\s*)/)?.[1] || "";
return fn.replace(`${leadingSpace}const ${name.text()} = () => {${bodyText}}`);
});
return root.commitEdits(edits);
};Better: Use getTransformed for captured nodes:
const transform: Transform<TSX> = (root) => {
const functions = root.findAll({
rule: { pattern: "function $NAME() { $$$BODY }" }
});
const edits = functions.map(fn => {
// getTransformed preserves original text exactly
const nameText = fn.getTransformed("NAME") || "anonymous";
const bodyText = fn.getTransformed("BODY") || "";
return fn.replace(`const ${nameText} = () => {${bodyText}}`);
});
return root.commitEdits(edits);
};Reference: JSSG API Reference
Use flatMap for Conditional Edits
Use flatMap instead of map + filter when some nodes don't produce edits. Return empty arrays for skipped nodes.
Incorrect (map with nulls):
const transform: Transform<TSX> = (root) => {
const calls = root.findAll({
rule: { pattern: "api.$METHOD($$$ARGS)" }
});
const edits = calls.map(call => {
const method = call.getMatch("METHOD");
if (!method) return null;
const methodName = method.text();
// Only transform deprecated methods
if (!deprecatedMethods.includes(methodName)) {
return null;
}
return call.replace(`newApi.${methodName}()`);
});
// Must filter out nulls
return root.commitEdits(edits.filter(Boolean) as Edit[]);
// Type assertion needed, filter doesn't narrow
};Correct (flatMap with empty arrays):
const transform: Transform<TSX> = (root) => {
const calls = root.findAll({
rule: { pattern: "api.$METHOD($$$ARGS)" }
});
const edits = calls.flatMap(call => {
const method = call.getMatch("METHOD");
if (!method) return []; // Skip gracefully
const methodName = method.text();
if (!deprecatedMethods.includes(methodName)) {
return []; // Not deprecated, skip
}
// Return array with single edit
return [call.replace(`newApi.${methodName}()`)];
});
// No filtering needed, proper types
return root.commitEdits(edits);
};Benefits of flatMap:
- No null/undefined handling
- Proper TypeScript types without assertions
- Can return multiple edits per node if needed
- Cleaner functional style
Pattern:
nodes.flatMap(node => {
if (shouldSkip(node)) return [];
if (needsMultipleEdits(node)) return [edit1, edit2];
return [singleEdit];
});Reference: JSSG API Reference
Early Return for Non-Applicable Files
Check file applicability before performing expensive traversals. Return early when files cannot possibly contain relevant patterns.
Incorrect (full traversal for all files):
const transform: Transform<TSX> = (root) => {
// Full AST traversal on every file
const matches = root.findAll({
rule: { pattern: "React.Component" }
});
if (matches.length === 0) {
return null; // Wasted traversal time
}
// Transform logic...
return root.commitEdits([]);
};
// 1000 files × full traversal = slowCorrect (early return with quick check):
const transform: Transform<TSX> = (root) => {
const source = root.root().text();
// Quick string checks before expensive traversal
if (!source.includes("React.Component") &&
!source.includes("extends Component")) {
return null; // Skip file entirely
}
// Only traverse files that might match
const matches = root.findAll({
rule: { pattern: "React.Component" }
});
// Transform logic...
return root.commitEdits([]);
};
// 50 relevant files × full traversal = fastBetter: use getSelector export:
// Pre-filter files at the engine level
export const getSelector = {
rule: {
any: [
{ pattern: "React.Component" },
{ pattern: "extends Component" }
]
}
};
const transform: Transform<TSX> = (root, options) => {
// Only called for files that match selector
// options.matches contains pre-matched nodes
const matches = options.matches || [];
// Transform logic...
return root.commitEdits([]);
};Early return strategies:
- String
includes()for keywords getSelectorexport for engine-level filtering- Filename checks for path-specific transforms
Reference: JSSG Advanced Patterns
Handle Embedded Languages with parseAsync
Code often contains embedded languages (CSS in styled-components, SQL in template literals, GraphQL queries). Use parseAsync to create sub-parsers for these contexts.
Incorrect (treating embedded code as strings):
const transform: Transform<TSX> = (root) => {
const styledComponents = root.findAll({
rule: { pattern: "styled.$TAG`$$$CSS`" }
});
const edits = styledComponents.map(match => {
const css = match.getMatch("CSS")?.text() || "";
// Regex-based CSS transformation - fragile, misses edge cases
const newCss = css.replace(/color:\s*red/g, "color: blue");
return match.replace(`styled.${match.getMatch("TAG")?.text()}\`${newCss}\``);
});
return root.commitEdits(edits);
};Correct (parsing embedded CSS):
import { parseAsync } from "codemod:ast-grep";
const transform: Transform<TSX> = async (root) => {
const styledComponents = root.findAll({
rule: { pattern: "styled.$TAG`$$$CSS`" }
});
const edits = await Promise.all(styledComponents.map(async match => {
const cssText = match.getMatch("CSS")?.text() || "";
// Parse CSS content as actual CSS
const cssRoot = await parseAsync("css", cssText);
const colorDecls = cssRoot.root().findAll({
rule: { pattern: "color: red" }
});
if (colorDecls.length === 0) return null;
// Transform CSS using AST
const cssEdits = colorDecls.map(decl => decl.replace("color: blue"));
const newCss = cssRoot.commitEdits(cssEdits);
const tag = match.getMatch("TAG")?.text();
return match.replace(`styled.${tag}\`${newCss}\``);
}));
return root.commitEdits(edits.filter(Boolean) as Edit[]);
};Embedded language scenarios:
styled-components/emotion→ CSS parser- Template literal SQL → SQL parser (if supported)
- GraphQL tagged templates → GraphQL parser
- HTML in template strings → HTML parser
Reference: JSSG Advanced Patterns
Provide Context for Ambiguous Patterns
Some code snippets are syntactically ambiguous without context. Use the pattern object form to provide surrounding context that disambiguates the pattern.
Incorrect (ambiguous pattern):
const transform: Transform<TSX> = (root) => {
// Is '{a, b}' an object or destructuring?
const matches = root.findAll({
rule: { pattern: "{ $A, $B }" }
});
// Parser guesses wrong, matches fail silently
// Is '() => x' a return or function body?
const arrows = root.findAll({
rule: { pattern: "() => $EXPR" }
});
// Ambiguous without statement context
return null;
};Correct (context-providing pattern object):
const transform: Transform<TSX> = (root) => {
// Explicit: match object literal destructuring in assignment
const destructuring = root.findAll({
rule: {
pattern: {
context: "const { $A, $B } = obj",
selector: "object_pattern"
}
}
});
// Explicit: match object literal in expression position
const objectLiterals = root.findAll({
rule: {
pattern: {
context: "const x = { $A, $B }",
selector: "object"
}
}
});
// Explicit: arrow function with implicit return
const arrows = root.findAll({
rule: {
pattern: {
context: "const fn = () => $EXPR",
selector: "arrow_function"
}
}
});
return null;
};When to use pattern object:
- Destructuring patterns (
{ a, b }vs{ a: 1 }) - Arrow functions (implicit vs block body)
- JSX fragments vs comparison operators
- Generic syntax (
<T>type vs JSX)
Reference: ast-grep Pattern Parse
Select the Correct Parser for File Type
Parser selection determines AST structure. Using the wrong parser produces an invalid or incomplete AST that causes all downstream pattern matching to fail silently.
Incorrect (wrong parser for file type):
# Using 'javascript' parser for TypeScript files
npx codemod jssg run ./transform.ts ./src --language javascript
# TypeScript-specific syntax is parsed incorrectly:
# - Type annotations become syntax errors
# - Generic parameters are misinterpreted
# - Interface declarations are skipped// Transform fails to match typed code
const matches = root.findAll({
rule: { pattern: "const $NAME: string = $VALUE" }
});
// Returns empty - 'javascript' parser doesn't understand ': string'Correct (matching parser to file type):
# Use 'tsx' for .tsx files (includes .ts and .js support)
npx codemod jssg run ./transform.ts ./src --language tsx
# Use 'typescript' for .ts files without JSX
npx codemod jssg run ./transform.ts ./src --language typescriptimport type { Transform } from "codemod:ast-grep";
import type TSX from "codemod:ast-grep/langs/tsx";
// Explicitly type the transform for proper autocomplete
const transform: Transform<TSX> = (root) => {
const matches = root.findAll({
rule: { pattern: "const $NAME: string = $VALUE" }
});
// Correctly matches TypeScript code
return null;
};Parser selection guide:
.tsxfiles →tsx(TypeScript + JSX).tsfiles →typescriptortsx.jsxfiles →jsx(JavaScript + JSX).jsfiles →javascriptorjsx
Reference: JSSG Quickstart
Avoid Overly Generic Patterns
Generic patterns match too many nodes, causing performance degradation and false positives. Add constraints to narrow the search space.
Incorrect (too generic):
const transform: Transform<TSX> = (root) => {
// Matches EVERY function call in the codebase
const matches = root.findAll({
rule: { pattern: "$FN($$$ARGS)" }
});
// On a 10k file codebase: matches millions of nodes
// Takes minutes to process
// Then filters in JS - wasteful
const consoleCalls = matches.filter(m =>
m.getMatch("FN")?.text().startsWith("console")
);
return null;
};Correct (specific pattern):
const transform: Transform<TSX> = (root) => {
// Pattern specifies exact target
const matches = root.findAll({
rule: {
kind: "call_expression",
pattern: "console.$METHOD($$$ARGS)"
}
});
// Matches only console.* calls
// 1000x fewer matches, milliseconds to process
return null;
};Specificity guidelines:
- Include literal text where known (object names, method prefixes)
- Add
kindconstraints to limit node types - Use
inside/hasto require structural context - Avoid standalone
$VARpatterns without context
Reference: ast-grep Match Algorithm
Combine Patterns with Rule Operators
Use rule operators (any, all, not) to compose complex matching logic in a single pass instead of multiple separate queries.
Incorrect (multiple passes):
const transform: Transform<TSX> = (root) => {
// Three separate traversals
const logCalls = root.findAll({ rule: { pattern: "console.log($$$)" } });
const warnCalls = root.findAll({ rule: { pattern: "console.warn($$$)" } });
const errorCalls = root.findAll({ rule: { pattern: "console.error($$$)" } });
// Combine results manually
const allCalls = [...logCalls, ...warnCalls, ...errorCalls];
// 3x traversal time, complex deduplication needed
return null;
};Correct (single pass with rule operators):
const transform: Transform<TSX> = (root) => {
// Single traversal with 'any' operator
const allCalls = root.findAll({
rule: {
any: [
{ pattern: "console.log($$$ARGS)" },
{ pattern: "console.warn($$$ARGS)" },
{ pattern: "console.error($$$ARGS)" }
]
}
});
// Or use pattern with meta variable
const consoleCalls = root.findAll({
rule: {
pattern: "console.$METHOD($$$ARGS)",
all: [
{ kind: "call_expression" },
{ not: { pattern: "console.table($$$)" } }
]
}
});
return null;
};Rule operators:
any: [rules]- matches if ANY rule matches (OR)all: [rules]- matches if ALL rules match (AND)not: rule- matches if rule does NOT matchmatches: "name"- references named utility rule
Reference: JSSG API Reference
Ensure Patterns Are Idempotent
Patterns should match only pre-transformation code, never post-transformation code. Running a codemod twice should produce the same result as running it once.
Incorrect (non-idempotent pattern):
const transform: Transform<TSX> = (root) => {
// Pattern matches both old and new format
const matches = root.findAll({
rule: { pattern: "logger($$$ARGS)" }
});
const edits = matches.map(match => {
const args = match.getMultipleMatches("ARGS");
// Wraps logger() calls with timestamp
return match.replace(`logger(Date.now(), ${args.map(a => a.text()).join(", ")})`);
});
// Running twice: logger(x) -> logger(Date.now(), x) -> logger(Date.now(), Date.now(), x)
return root.commitEdits(edits);
};Correct (idempotent pattern):
const transform: Transform<TSX> = (root) => {
// Pattern specifically excludes already-transformed code
const matches = root.findAll({
rule: {
pattern: "logger($$$ARGS)",
not: {
// Skip if first arg is Date.now()
has: {
field: "arguments",
has: {
kind: "call_expression",
pattern: "Date.now()"
}
}
}
}
});
const edits = matches.map(match => {
const args = match.getMultipleMatches("ARGS");
return match.replace(`logger(Date.now(), ${args.map(a => a.text()).join(", ")})`);
});
// Running twice: logger(x) -> logger(Date.now(), x) -> no change
return root.commitEdits(edits);
};Idempotency strategies:
- Use
notto exclude already-transformed patterns - Check for sentinel values or markers
- Match specific old API signatures only
- Test by running codemod twice on same input
Reference: Hypermod Best Practices
Use Constraints for Reusable Matching Logic
Define named constraints for commonly used matching conditions. Reference them with matches to keep patterns DRY and maintainable.
Incorrect (duplicated pattern logic):
const transform: Transform<TSX> = (root) => {
// Same string type check repeated everywhere
const stringConcats = root.findAll({
rule: {
pattern: "$LEFT + $RIGHT",
all: [
{ has: { pattern: "$LEFT", any: [
{ kind: "string" },
{ kind: "template_string" }
]}},
{ has: { pattern: "$RIGHT", any: [
{ kind: "string" },
{ kind: "template_string" }
]}}
]
}
});
// Repeated in 10 other rules...
return null;
};Correct (reusable constraints):
const transform: Transform<TSX> = (root) => {
const stringConcats = root.findAll({
rule: {
pattern: "$LEFT + $RIGHT"
},
constraints: {
LEFT: { matches: "STRING_LIKE" },
RIGHT: { matches: "STRING_LIKE" }
},
utils: {
STRING_LIKE: {
any: [
{ kind: "string" },
{ kind: "template_string" },
{ kind: "string_fragment" }
]
}
}
});
// Reuse STRING_LIKE in other rules
return null;
};Constraint patterns:
- Define common type checks in
utils - Reference with
matches: "UTIL_NAME" - Use in
constraintsto bind meta variables - Share across multiple rules in the same transform
Reference: JSSG API Reference
Use Meta Variables for Flexible Matching
Meta variables ($NAME, $$$ARGS) capture arbitrary AST nodes, enabling flexible patterns that match code variations. Use single $ for one node, triple $$$ for multiple.
Incorrect (hardcoded literals):
const transform: Transform<TSX> = (root) => {
// Only matches exact string "error"
const matches = root.findAll({
rule: { pattern: 'console.error("error")' }
});
// Misses: console.error(message), console.error(err.message)
// Misses: console.error("Error:", details)
return null;
};Correct (meta variables for flexibility):
const transform: Transform<TSX> = (root) => {
// $ARG captures any single argument
const singleArg = root.findAll({
rule: { pattern: "console.error($ARG)" }
});
// $$$ARGS captures zero or more arguments
const anyArgs = root.findAll({
rule: { pattern: "console.error($$$ARGS)" }
});
// Access captured values
for (const match of anyArgs) {
const args = match.getMultipleMatches("ARGS");
console.log(`Found ${args.length} arguments`);
}
return null;
};Meta variable syntax:
$NAME- captures exactly one node$$$NAME- captures zero or more nodes$_- anonymous single capture (don't need value)$$$- anonymous multiple capture
Reference: ast-grep Pattern Syntax
Use Relational Patterns for Context
Relational patterns (inside, has, precedes, follows) match nodes based on their structural context. Use them to avoid manual post-filtering.
Incorrect (manual context filtering):
const transform: Transform<TSX> = (root) => {
// Find all setState calls
const setStateCalls = root.findAll({
rule: { pattern: "this.setState($$$ARGS)" }
});
// Manually filter to those inside useEffect
const inUseEffect = setStateCalls.filter(call => {
let parent = call.parent();
while (parent) {
if (parent.text().includes("useEffect")) return true;
parent = parent.parent();
}
return false;
});
// Slow, error-prone, misses edge cases
return null;
};Correct (relational pattern):
const transform: Transform<TSX> = (root) => {
// Single query with context requirement
const setStateInEffect = root.findAll({
rule: {
pattern: "this.setState($$$ARGS)",
inside: {
kind: "call_expression",
pattern: "useEffect($$$)"
}
}
});
// Correct, fast, handles all nesting levels
return null;
};Relational operators:
inside: rule- node is descendant of matching ancestorhas: rule- node has matching descendantprecedes: rule- node appears before siblingfollows: rule- node appears after siblingstopBy: "neighbor" | "end"- controls search depth
Reference: ast-grep Relational Patterns
Organize Package by Convention
Follow the standard codemod package structure. Consistent organization enables tooling support and helps contributors navigate.
Incorrect (ad-hoc structure):
my-codemod/
├── transform.js # Where are tests?
├── config.json # Non-standard config
└── utils/ # Unclear purpose
└── helper.jsCorrect (standard structure):
my-codemod/
├── codemod.yaml # Package metadata
├── workflow.yaml # Workflow definition
├── scripts/ # JSSG transform files
│ ├── main.ts
│ └── helpers/
│ └── patterns.ts
├── rules/ # YAML ast-grep rules
│ └── deprecated-api.yaml
├── tests/ # Test fixtures
│ ├── basic-case/
│ │ ├── input.tsx
│ │ └── expected.tsx
│ └── edge-case/
│ ├── input.tsx
│ └── expected.tsx
├── README.md # Usage documentation
└── CHANGELOG.md # Version historyDirectory purposes:
| Directory | Purpose |
|---|---|
scripts/ | TypeScript/JavaScript transforms (JSSG) |
rules/ | Declarative YAML ast-grep rules |
tests/ | Input/expected fixture pairs |
| Root | Metadata and documentation |
Workflow referencing:
# workflow.yaml
version: "1"
nodes:
- id: transform
steps:
- type: js-ast-grep
codemod: ./scripts/main.ts # Relative to package root
- type: ast-grep
rule: ./rules/deprecated-api.yamlReference: Codemod Package Structure
Use Semantic Versioning for Packages
Follow semantic versioning (semver) for codemod packages. Version numbers communicate compatibility and change scope to consumers.
Incorrect (arbitrary versioning):
# codemod.yaml - meaningless version
schema_version: "1.0"
name: react-migration
version: "42" # What does this mean?
# Or:
version: "2024.01.15" # Date-based, no compatibility infoCorrect (semantic versioning):
# codemod.yaml - semver
schema_version: "1.0"
name: react-migration
version: "1.2.3"
# 1 = Major (breaking changes to transform behavior)
# 2 = Minor (new features, backward compatible)
# 3 = Patch (bug fixes, no behavior change)Version bump guidelines:
| Change Type | Version Bump | Example |
|---|---|---|
| Fix bug in existing pattern | Patch: 1.2.3 → 1.2.4 | Fix edge case handling |
| Add new transformation rule | Minor: 1.2.3 → 1.3.0 | Support new API pattern |
| Change output format | Major: 1.2.3 → 2.0.0 | Different code style |
| Remove pattern support | Major: 1.2.3 → 2.0.0 | Drop legacy format |
Pre-release versions:
version: "2.0.0-beta.1" # Pre-release testing
version: "2.0.0-rc.1" # Release candidatePublishing workflow:
# Validate before version bump
npx codemod jssg test ./transform.ts
# Update version in codemod.yaml
# Commit and tag
git tag v1.2.4
git push --tags
# Publish
npx codemod publishReference: Codemod Package Structure
Write Descriptive Package Metadata
Write clear descriptions and keywords in codemod.yaml. Good metadata helps users find your codemod in registry search.
Incorrect (minimal metadata):
# codemod.yaml - unhelpful
schema_version: "1.0"
name: my-codemod
version: "1.0.0"
# No description, author, keywords
# Users can't tell what it doesCorrect (comprehensive metadata):
# codemod.yaml - discoverable
schema_version: "1.0"
name: "@myorg/react-18-to-19"
version: "1.0.0"
description: |
Migrates React 18 applications to React 19.
Handles: useEffect cleanup, Suspense boundaries,
Server Components imports, and deprecated API removal.
author: "Team Name <team@example.com>"
license: "MIT"
category: "migration"
targets:
languages:
- TypeScript
- JavaScript
frameworks:
- React
keywords:
- upgrade
- breaking-change
- v18-to-v19
- react
- server-components
- suspense
repository:
url: "https://github.com/myorg/codemods"
directory: "packages/react-18-to-19"Keyword best practices:
- Include version tags:
v18-to-v19 - Include transformation type:
upgrade,migration - Include framework name:
react,nextjs - Include specific features:
server-components,suspense
Registry discoverability:
# Good keywords enable search
npx codemod search "react 19 upgrade"
# Finds: @myorg/react-18-to-19
npx codemod search "server components migration"
# Also finds: @myorg/react-18-to-19Reference: Codemod Package Structure
Minimize Requested Capabilities
JSSG uses deny-by-default security. Only request capabilities your codemod actually needs. Each capability expands the attack surface.
Incorrect (requesting all capabilities):
# codemod.yaml - over-permissioned
schema_version: "1.0"
name: simple-rename
capabilities:
- fs # Not needed for simple AST transform
- fetch # Not needed
- child_process # Definitely not needed!// Transform doesn't use any capabilities
const transform: Transform<TSX> = (root) => {
const matches = root.findAll({
rule: { pattern: "oldName" }
});
const edits = matches.map(m => m.replace("newName"));
return root.commitEdits(edits);
};Correct (minimal capabilities):
# codemod.yaml - least-privilege
schema_version: "1.0"
name: simple-rename
# No capabilities needed for pure AST transforms
# capabilities: [] (implicit)# codemod.yaml - only what's needed
schema_version: "1.0"
name: config-migrator
capabilities:
- fs # Only fs, needed to read config file
# No fetch or child_processWhen each capability is needed:
fs- Reading config files, writing reportsfetch- Downloading schemas, API validationchild_process- Running external tools (rare)
CLI equivalent:
# Only enable specific capability
npx codemod jssg run ./transform.ts ./src --allow-fs
# NOT: --allow-fs --allow-fetch --allow-child-processReference: JSSG Security
Review Third-Party Codemods Before Running
Inspect third-party codemod source code before running. Codemods with capabilities can execute arbitrary operations on your system.
Incorrect (running without review):
# Running random codemod from registry
npx codemod @unknown-author/mysterious-migration
# What does it do? What permissions does it have?
# Could be mining crypto, stealing credentials, etc.Correct (review first):
# 1. Search and inspect metadata
npx codemod search "react upgrade"
# Review: author, downloads, last update, capabilities
# 2. Check requested capabilities
cat node_modules/@org/codemod/codemod.yaml
# capabilities:
# - fs # Why does it need filesystem?
# - fetch # Why network access?
# - child_process # RED FLAG - why shell access?
# 3. Read the source code
cat node_modules/@org/codemod/scripts/transform.ts
# Look for suspicious:
# - eval(), Function()
# - fetch() to unknown URLs
# - execSync() with dynamic input
# - fs.writeFile() outside project
# 4. Run only after review
npx codemod @trusted-org/reviewed-migrationWarning signs in codemods:
- Requests
child_processcapability - Fetches from non-official URLs
- Writes files outside project directory
- Obfuscated or minified source
- No test suite or documentation
Trusted sources:
- Official framework maintainers
- Well-known organizations
- Codemods with visible source and tests
- High download counts and recent updates
Reference: JSSG Security
Validate External Inputs Before Use
When codemods accept external input (parameters, config files), validate before use. Untrusted input can cause injection attacks.
Incorrect (unsanitized parameter use):
const transform: Transform<TSX> = async (root, options) => {
const targetModule = options.params?.module;
// Direct use of user input in pattern - dangerous!
const matches = root.findAll({
rule: { pattern: `import { $$$NAMES } from "${targetModule}"` }
});
// User input in shell command - injection vulnerability!
const { execSync } = await import("child_process");
execSync(`npm info ${targetModule}`); // Dangerous!
return null;
};Correct (validated inputs):
const transform: Transform<TSX> = async (root, options) => {
const targetModule = options.params?.module;
// Validate module name format
if (!targetModule || !/^[@a-z0-9\-\/]+$/i.test(targetModule)) {
console.error(`Invalid module name: ${targetModule}`);
return null;
}
// Safe to use in pattern after validation
const matches = root.findAll({
rule: { pattern: `import { $$$NAMES } from "${targetModule}"` }
});
// Escape for shell if needed
const safeModule = targetModule.replace(/[^a-zA-Z0-9@\/-]/g, "");
const { execSync } = await import("child_process");
execSync(`npm info "${safeModule}"`); // Quoted and sanitized
return null;
};Input validation patterns:
- Module names:
/^[@a-z0-9\-\/]+$/i - File paths: Resolve and check within project root
- Identifiers:
/^[a-zA-Z_][a-zA-Z0-9_]*$/ - Always escape shell arguments
Reference: JSSG Security
Cache Semantic Analysis Results
Semantic analysis operations are expensive. Cache results when analyzing multiple related symbols to avoid redundant cross-file resolution.
Incorrect (repeated analysis):
const transform: Transform<TSX> = async (root) => {
const apiCalls = root.findAll({
rule: { pattern: "$API.$METHOD($$$ARGS)" }
});
const edits = await Promise.all(apiCalls.map(async call => {
const apiId = call.getMatch("API");
if (!apiId) return null;
// Each call re-resolves the same API symbol
const def = apiId.definition(); // Expensive!
// Each call re-finds all references
const refs = apiId.references(); // Very expensive!
return call.replace(`newApi.${call.getMatch("METHOD")?.text()}()`);
}));
return root.commitEdits(edits.filter(Boolean) as Edit[]);
};Correct (cached analysis):
const transform: Transform<TSX> = async (root) => {
// Cache for definitions by symbol text
const defCache = new Map<string, DefinitionResult | null>();
// Cache for references by definition location
const refCache = new Map<string, FileReference[]>();
const apiCalls = root.findAll({
rule: { pattern: "$API.$METHOD($$$ARGS)" }
});
const edits = await Promise.all(apiCalls.map(async call => {
const apiId = call.getMatch("API");
if (!apiId) return null;
const apiName = apiId.text();
// Check cache before expensive operation
if (!defCache.has(apiName)) {
defCache.set(apiName, apiId.definition());
}
const def = defCache.get(apiName);
if (def) {
const defKey = `${def.root.filename()}:${def.node.range().start}`;
if (!refCache.has(defKey)) {
refCache.set(defKey, def.node.references() || []);
}
}
return call.replace(`newApi.${call.getMatch("METHOD")?.text()}()`);
}));
return root.commitEdits(edits.filter(Boolean) as Edit[]);
};What to cache:
definition()results by symbol namereferences()results by definition location- Cross-file root objects for repeated writes
- Import resolution results
Reference: JSSG Semantic Analysis
Handle Null Semantic Analysis Results
Semantic analysis methods return null when symbols cannot be resolved. Always handle null results gracefully - not all code can be statically analyzed.
Incorrect (assumes resolution succeeds):
const transform: Transform<TSX> = (root) => {
const identifiers = root.findAll({ rule: { kind: "identifier" } });
const edits = identifiers.map(id => {
// Crashes on unresolvable symbols
const def = id.definition();
const defNode = def.node; // TypeError: Cannot read property 'node' of null
// External imports, globals, and dynamic code return null
return id.replace(defNode.text().toUpperCase());
});
return root.commitEdits(edits);
};Correct (null-safe semantic access):
const transform: Transform<TSX> = (root) => {
const identifiers = root.findAll({ rule: { kind: "identifier" } });
const edits = identifiers.flatMap(id => {
const def = id.definition();
// Handle unresolvable symbols
if (!def) {
// Could be: external import, global, dynamic, or analysis limitation
console.log(`Could not resolve: ${id.text()}`);
return [];
}
// Check definition kind for appropriate handling
if (def.kind === "external") {
// Symbol defined in node_modules or external file
return [];
}
if (def.kind === "import") {
// Symbol imported from another file
const importDef = def.node;
// Handle import-specific logic
}
return [id.replace(def.node.text().toUpperCase())];
});
return root.commitEdits(edits);
};Definition kinds:
"local"- defined in same file"import"- imported from another file"external"- from node_modules or outside projectnull- unresolvable (dynamic, global, etc.)
Reference: JSSG Semantic Analysis
Use File Scope Semantic Analysis First
Start with file-scope semantic analysis, which is fast and requires no configuration. Only upgrade to workspace scope when cross-file resolution is necessary.
Incorrect (workspace scope for local variables):
# workflow.yaml - unnecessary workspace scope
nodes:
- id: rename-locals
steps:
- type: js-ast-grep
codemod: ./scripts/rename.ts
semantic_analysis: workspace # Overkill for file-local transforms
# Indexes entire project even for local renamesconst transform: Transform<TSX> = (root) => {
const localVars = root.findAll({
rule: { pattern: "const $NAME = $VALUE" }
});
// Only renaming within this file
// Workspace indexing was wasted work
return null;
};Correct (file scope for local, workspace for cross-file):
# workflow.yaml - appropriate scoping
nodes:
- id: rename-locals
steps:
- type: js-ast-grep
codemod: ./scripts/rename-locals.ts
semantic_analysis: file # Fast, local-only
- id: rename-exports
depends_on: [rename-locals]
steps:
- type: js-ast-grep
codemod: ./scripts/rename-exports.ts
semantic_analysis: workspace # Needed for cross-file refs// rename-exports.ts - needs workspace scope
const transform: Transform<TSX> = async (root) => {
const exportedFn = root.find({
rule: { pattern: "export function $NAME($$$PARAMS) { $$$BODY }" }
});
if (!exportedFn) return null;
// Cross-file reference finding requires workspace scope
const refs = exportedFn.field("name")?.references();
// refs contains references from other files
return null;
};When to use workspace scope:
- Renaming exported symbols
- Finding all usages across project
- Analyzing import/export relationships
- Refactoring public APIs
Reference: JSSG Semantic Analysis
Verify File Ownership Before Cross-File Edits
When using semantic analysis for cross-file transformations, verify that target files are within your project before editing. Never modify node_modules or external dependencies.
Incorrect (editing without ownership check):
const transform: Transform<TSX> = async (root) => {
const exportedFn = root.find({
rule: { pattern: "export function deprecatedApi($$$)" }
});
if (!exportedFn) return null;
const refs = exportedFn.field("name")?.references() || [];
// Blindly edits all references
for (const fileRef of refs) {
for (const ref of fileRef.refs) {
// Might edit node_modules!
fileRef.root.write(
fileRef.root.commitEdits([ref.replace("newApi")])
);
}
}
return null;
};Correct (ownership verification):
const transform: Transform<TSX> = async (root) => {
const projectRoot = process.cwd();
const exportedFn = root.find({
rule: { pattern: "export function deprecatedApi($$$)" }
});
if (!exportedFn) return null;
const refs = exportedFn.field("name")?.references() || [];
for (const fileRef of refs) {
const filePath = fileRef.root.filename();
// Skip external files
if (!filePath.startsWith(projectRoot)) {
console.log(`Skipping external: ${filePath}`);
continue;
}
// Skip node_modules
if (filePath.includes("node_modules")) {
console.log(`Skipping dependency: ${filePath}`);
continue;
}
// Skip generated files
if (filePath.includes("/dist/") || filePath.includes("/build/")) {
continue;
}
// Safe to edit
for (const ref of fileRef.refs) {
fileRef.root.write(
fileRef.root.commitEdits([ref.replace("newApi")])
);
}
}
return null;
};File ownership checks:
startsWith(projectRoot)- within project!includes("node_modules")- not a dependency!includes("/dist/")- not generated code!endsWith(".d.ts")- not type declarations
Reference: JSSG Semantic Analysis
Log Progress for Long-Running Migrations
Add progress logging for transforms that process many files. Logs help monitor progress and debug issues.
Incorrect (silent processing):
const transform: Transform<TSX> = (root) => {
const matches = root.findAll({
rule: { pattern: "oldApi($$$ARGS)" }
});
const edits = matches.map(m => m.replace("newApi()"));
return root.commitEdits(edits);
};
// Running on 5000 files:
// ... silence for 30 minutes ...
// No idea if it's working, stuck, or almost doneCorrect (progress logging):
const transform: Transform<TSX> = (root, options) => {
const filename = root.filename();
// Log file being processed
console.log(`Processing: ${filename}`);
const matches = root.findAll({
rule: { pattern: "oldApi($$$ARGS)" }
});
if (matches.length === 0) {
console.log(` No matches in ${filename}`);
return null;
}
console.log(` Found ${matches.length} matches`);
const edits = matches.map((m, i) => {
const line = m.range().start.line;
console.log(` [${i + 1}/${matches.length}] Line ${line}: ${m.text().slice(0, 50)}...`);
return m.replace("newApi()");
});
console.log(` Transformed ${edits.length} occurrences`);
return root.commitEdits(edits);
};
// Output:
// Processing: src/components/Header.tsx
// Found 3 matches
// [1/3] Line 15: oldApi(user)...
// [2/3] Line 28: oldApi(config)...
// [3/3] Line 42: oldApi()...
// Transformed 3 occurrencesLogging best practices:
- Log filename at start of each file
- Log match counts for debugging
- Include line numbers for review
- Use consistent format for parsing
- Consider verbosity flag for detail control
Reference: JSSG Advanced Patterns
Make Transforms Idempotent for Safe Reruns
Transforms should produce the same result when run multiple times. This allows safe reruns after partial failures.
Incorrect (non-idempotent transform):
const transform: Transform<TSX> = (root) => {
const imports = root.findAll({ rule: { kind: "import_statement" } });
// Adds comment on every run
const edits = imports.map(imp =>
imp.replace(`// Migrated\n${imp.text()}`)
);
// Run twice:
// Before: import x from 'y';
// After 1: // Migrated
// import x from 'y';
// After 2: // Migrated
// // Migrated
// import x from 'y';
return root.commitEdits(edits);
};Correct (idempotent transform):
const transform: Transform<TSX> = (root) => {
const imports = root.findAll({ rule: { kind: "import_statement" } });
const edits = imports.flatMap(imp => {
const text = imp.text();
// Check if already migrated
const prev = imp.prev();
if (prev?.text().includes("// Migrated")) {
return []; // Skip already-processed imports
}
return [imp.replace(`// Migrated\n${text}`)];
});
// Run twice:
// Before: import x from 'y';
// After 1: // Migrated
// import x from 'y';
// After 2: (no change)
return root.commitEdits(edits);
};Idempotency patterns:
- Check for transformation markers before applying
- Use
notin patterns to exclude already-transformed code - Track processed files in workflow state
- Design transforms to match only pre-transformation patterns
Reference: Hypermod Best Practices
Use State for Resumable Migrations
Persist migration progress in workflow state. When migrations fail mid-way, you can resume from the last successful point.
Incorrect (no state tracking):
# workflow.yaml - no progress tracking
version: "1"
nodes:
- id: migrate-all
steps:
- type: js-ast-grep
codemod: ./scripts/migrate.ts
# Processes all 5000 files
# Fails at file 3000
# Must restart from beginningCorrect (state-tracked progress):
# workflow.yaml - resumable migration
version: "1"
state:
processed_files: []
failed_files: []
current_batch: 0
nodes:
- id: list-files
steps:
- type: run
command: find ./src -name "*.tsx" | sort
output: all_files
- id: process-batch
depends_on: [list-files]
strategy:
type: matrix
from_state: all_files
steps:
- type: js-ast-grep
codemod: ./scripts/migrate.ts
target: ${{ matrix.value }}
on_success: processed_files@=${{ matrix.value }}
on_failure: failed_files@=${{ matrix.value }}Resume after failure:
# Check status
npx codemod workflow status -w ./workflow.yaml
# Shows: 3000/5000 files processed
# Resume from last state
npx codemod workflow resume -w ./workflow.yaml
# Continues from file 3001State operations:
KEY=VALUE- set valueKEY@=VALUE- append to arrayKEY.nested=VALUE- set nested property
Reference: Codemod Workflow Reference
Cover Edge Cases in Test Fixtures
Create fixtures for edge cases that production code might contain. Real codebases have unusual patterns that simple examples miss.
Incorrect (only happy path):
tests/
└── basic-case/
├── input.tsx # Simple, clean code
└── expected.tsx
# Misses: comments, formatting, edge casesCorrect (comprehensive edge cases):
tests/
├── basic-case/
│ ├── input.tsx
│ └── expected.tsx
├── with-inline-comments/
│ ├── input.tsx # Code with // comments
│ └── expected.tsx
├── with-block-comments/
│ ├── input.tsx # Code with /* */ comments
│ └── expected.tsx
├── multiline-expression/
│ ├── input.tsx # Spans multiple lines
│ └── expected.tsx
├── already-transformed/
│ ├── input.tsx # Should be no-op
│ └── expected.tsx # Same as input
├── mixed-patterns/
│ ├── input.tsx # Some match, some don't
│ └── expected.tsx
├── empty-file/
│ ├── input.tsx # Empty content
│ └── expected.tsx
├── syntax-edge-cases/
│ ├── input.tsx # Optional chaining, nullish coalescing
│ └── expected.tsx
└── typescript-specific/
├── input.tsx # Generics, type assertions
└── expected.tsxEdge cases to always test:
- Empty files
- Files with only comments
- Already-transformed code (idempotency)
- Code with unusual formatting
- TypeScript-specific syntax
- JSX variations
- Dynamic/computed expressions
Reference: JSSG Testing
Test on File Subset Before Full Run
Run transforms on a small subset of files first. Validate results manually before applying to the entire codebase.
Incorrect (full run immediately):
# Run on entire codebase first time
npx codemod jssg run ./transform.ts ./src --language tsx
# 1,847 files modified
# Discover bug after 10 minutes
# Must revert everything and restartCorrect (incremental validation):
# 1. Test with fixture tests first
npx codemod jssg test ./transform.ts --language tsx
# 2. Run on single file
npx codemod jssg run ./transform.ts ./src/components/Button.tsx --language tsx
cat ./src/components/Button.tsx # Review output
# 3. Run on small directory
npx codemod jssg run ./transform.ts ./src/components --language tsx
git diff # Review all changes
# 4. Run on representative sample
find ./src -name "*.tsx" | head -20 | xargs dirname | sort -u | head -5
npx codemod jssg run ./transform.ts ./src/pages --language tsx
# 5. Full run after validation
npx codemod jssg run ./transform.ts ./src --language tsxSubset selection strategies:
- Start with smallest files
- Include files with known edge cases
- Test each file type (
.ts,.tsx,.js) - Include files from different teams/modules
Quick revert if needed:
# Git makes it easy to undo
git checkout -- src/
# Or for unstaged changes
git stashReference: JSSG CLI
Update Test Fixtures Intentionally
Use the -u flag to update expected files, but always review changes before committing. Auto-updated fixtures can hide regressions.
Incorrect (blindly updating):
# Tests fail after transform change
npx codemod jssg test ./transform.ts
# 3 tests failed
# Blindly accept all changes
npx codemod jssg test ./transform.ts -u
# 3 fixtures updated
git add -A && git commit -m "fix tests"
# Might have committed regressions!Correct (review before committing):
# Tests fail after transform change
npx codemod jssg test ./transform.ts
# ✗ basic-transform: output differs from expected
# Update fixtures
npx codemod jssg test ./transform.ts -u
# Updated: tests/basic-transform/expected.tsx
# Review what changed
git diff tests/
# Verify changes are intentional
# - Is the new output correct?
# - Does it match the intended behavior change?
# - Are there unexpected side effects?
# Only then commit
git add tests/ && git commit -m "Update fixtures for new format"Fixture review checklist:
- [ ] New output is semantically correct
- [ ] Formatting matches project style
- [ ] No unintended side effects
- [ ] Comments are preserved appropriately
- [ ] Edge cases still handled correctly
CI protection:
# Fail CI if fixtures need updating
- run: npx codemod jssg test ./transform.ts
# Don't use -u in CI - force explicit updatesReference: JSSG Testing
Use Input/Expected Fixture Pairs
Organize tests as paired input/expected files. The test runner compares actual output against expected files for automated validation.
Incorrect (ad-hoc testing):
// Manual testing in console
const result = transform(parse("tsx", "const x = 1"));
console.log(result); // "Looks right..."
// No persistent record, not reproducibleCorrect (fixture-based testing):
tests/
├── basic-transform/
│ ├── input.tsx
│ └── expected.tsx
├── handles-async/
│ ├── input.tsx
│ └── expected.tsx
├── preserves-comments/
│ ├── input.tsx
│ └── expected.tsx
└── no-op-when-already-migrated/
├── input.tsx
└── expected.tsx// tests/basic-transform/input.tsx
const user = await fetchUser();
const posts = await fetchPosts();// tests/basic-transform/expected.tsx
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);Run tests:
npx codemod jssg test ./transform.ts --language tsx
# Output:
# ✓ basic-transform
# ✓ handles-async
# ✓ preserves-comments
# ✓ no-op-when-already-migrated
# 4 tests passedTest naming conventions:
- Describe the scenario:
handles-nested-callbacks - Describe expected behavior:
converts-require-to-import - Describe edge cases:
preserves-dynamic-imports
Reference: JSSG Testing
Choose Appropriate Test Strictness Level
Use the --strictness flag to control how output is compared to expected. Stricter levels catch more issues but may fail on formatting differences.
Incorrect (wrong strictness for transform type):
# Using strict mode for a transform that reorders imports
npx codemod jssg test ./import-sorter.ts --strictness strict
# Test fails even though output is semantically correct:
# Expected: import { a, b } from 'x';
# Actual: import { b, a } from 'x';
# These are functionally identical but strict mode failsCorrect (appropriate strictness for transform type):
# Use loose mode for transforms that may reorder elements
npx codemod jssg test ./import-sorter.ts --strictness loose
# Passes: ignores import ordering differences
# Use strict mode for formatting-sensitive transforms
npx codemod jssg test ./preserve-whitespace.ts --strictness strict
# Catches: any whitespace changes that shouldn't happen
# Use ast mode for semantic transforms
npx codemod jssg test ./api-migration.ts --strictness ast
# Passes: as long as AST is equivalentStrictness level guide:
| Level | Compares | Use When |
|---|---|---|
strict | Exact string | Formatting must be preserved |
cst | Syntax tree | Whitespace changes acceptable |
ast | Abstract tree | Only semantics matter |
loose | Semantic | Reordering is acceptable |
Recommendation: Start with strict, relax only when the transform naturally produces equivalent but differently-formatted output.
Reference: JSSG Testing
Cache Repeated Node Lookups
When transforming multiple nodes that share context, cache common lookups to avoid repeated traversals.
Incorrect (repeated lookups):
const transform: Transform<TSX> = (root) => {
const apiCalls = root.findAll({
rule: { pattern: "api.$METHOD($$$ARGS)" }
});
const edits = apiCalls.map(call => {
// Each iteration re-traverses to find imports
const hasErrorImport = root.find({
rule: { pattern: 'import { ApiError } from "api"' }
});
// Each iteration re-traverses to find config
const config = root.find({
rule: { pattern: "const config = $VALUE" }
});
// N calls × 2 traversals = 2N unnecessary traversals
return call.replace(`wrappedApi.${call.getMatch("METHOD")?.text()}()`);
});
return root.commitEdits(edits);
};Correct (cached lookups):
const transform: Transform<TSX> = (root) => {
// Cache lookups before the loop
const hasErrorImport = root.find({
rule: { pattern: 'import { ApiError } from "api"' }
});
const config = root.find({
rule: { pattern: "const config = $VALUE" }
});
const apiCalls = root.findAll({
rule: { pattern: "api.$METHOD($$$ARGS)" }
});
// Reuse cached values
const edits = apiCalls.map(call => {
if (!hasErrorImport) {
// Use cached result
}
return call.replace(`wrappedApi.${call.getMatch("METHOD")?.text()}()`);
});
return root.commitEdits(edits);
};What to cache:
- Import statements (checked for many nodes)
- Configuration declarations
- Type definitions
- Any context used across multiple transformations
Reference: JSSG API Reference
Collect Multiple Patterns in Single Traversal
When you need to find multiple different patterns, combine them into a single query with any instead of making separate traversals.
Incorrect (multiple traversals):
const transform: Transform<TSX> = (root) => {
// 4 separate traversals of the AST
const requires = root.findAll({ rule: { pattern: "require($PATH)" } });
const imports = root.findAll({ rule: { kind: "import_statement" } });
const exports = root.findAll({ rule: { kind: "export_statement" } });
const dynamicImports = root.findAll({ rule: { pattern: "import($PATH)" } });
// Each traversal walks the entire tree
// 4N time complexity
return null;
};Correct (single traversal):
const transform: Transform<TSX> = (root) => {
// Single traversal collecting all patterns
const moduleStatements = root.findAll({
rule: {
any: [
{ pattern: "require($PATH)" },
{ kind: "import_statement" },
{ kind: "export_statement" },
{ pattern: "import($PATH)" }
]
}
});
// Categorize after collection
const requires = moduleStatements.filter(n => n.text().startsWith("require"));
const imports = moduleStatements.filter(n => n.kind() === "import_statement");
const exports = moduleStatements.filter(n => n.kind() === "export_statement");
const dynamicImports = moduleStatements.filter(n =>
n.kind() === "call_expression" && n.text().includes("import(")
);
// 1N time complexity + fast array filtering
return null;
};When to combine:
- Searching for related patterns (all module syntax)
- Collecting nodes for analysis (all function definitions)
- Building a manifest (all API usages)
Reference: JSSG API Reference
Use find() for Single Match, findAll() for Multiple
Use find() when you only need the first match - it stops traversal immediately. Use findAll() only when you need all occurrences.
Incorrect (findAll when only first needed):
const transform: Transform<TSX> = (root) => {
// Finds ALL imports, then takes first
const imports = root.findAll({
rule: { kind: "import_statement" }
});
// Only needed the first import location for insertion
const firstImport = imports[0];
if (!firstImport) return null;
// findAll traversed entire file unnecessarily
return null;
};Correct (find for single match):
const transform: Transform<TSX> = (root) => {
// Stops at first match
const firstImport = root.find({
rule: { kind: "import_statement" }
});
if (!firstImport) return null;
// Short-circuited traversal - much faster for large files
return null;
};Use find() when:
- Checking if any match exists
- Finding insertion point (first/last import)
- Validating presence of a pattern
- Getting a single representative node
Use findAll() when:
- Transforming all occurrences
- Counting matches
- Collecting nodes for batch operations
Reference: JSSG API Reference
Use Sibling Navigation Efficiently
Use next(), prev(), nextAll(), and prevAll() for sibling navigation instead of re-traversing from parent. Sibling methods are O(1) operations.
Incorrect (re-traversing from parent):
const transform: Transform<TSX> = (root) => {
const statements = root.findAll({ rule: { kind: "expression_statement" } });
const edits = statements.flatMap(stmt => {
// Re-traverse parent to find siblings
const parent = stmt.parent();
if (!parent) return [];
const siblings = parent.children();
const index = siblings.findIndex(s => s.id() === stmt.id());
const nextSibling = siblings[index + 1];
// O(n) per statement = O(n²) total
if (nextSibling?.kind() === "comment") {
return [stmt.replace(stmt.text() + " // has comment")];
}
return [];
});
return root.commitEdits(edits);
};Correct (direct sibling access):
const transform: Transform<TSX> = (root) => {
const statements = root.findAll({ rule: { kind: "expression_statement" } });
const edits = statements.flatMap(stmt => {
// O(1) sibling access
const nextSibling = stmt.next();
if (nextSibling?.kind() === "comment") {
return [stmt.replace(stmt.text() + " // has comment")];
}
return [];
});
return root.commitEdits(edits);
};Sibling navigation methods:
next()- immediately following siblingprev()- immediately preceding siblingnextAll()- all following siblingsprevAll()- all preceding siblings
Reference: JSSG API Reference
Use stopBy to Control Traversal Depth
Relational patterns (inside, has) traverse unbounded by default. Use stopBy to limit search depth and improve performance.
Incorrect (unbounded search):
const transform: Transform<TSX> = (root) => {
// Searches through ALL ancestors up to root
const awaitInTry = root.findAll({
rule: {
kind: "await_expression",
inside: {
kind: "try_statement"
}
// Without stopBy, climbs entire ancestor chain
// In deeply nested code, this is expensive
}
});
return null;
};Correct (bounded search):
const transform: Transform<TSX> = (root) => {
// Stop at nearest function boundary
const awaitInTry = root.findAll({
rule: {
kind: "await_expression",
inside: {
kind: "try_statement",
stopBy: {
any: [
{ kind: "function_declaration" },
{ kind: "arrow_function" },
{ kind: "method_definition" }
]
}
}
}
});
// Or use "neighbor" to check only immediate parent
const directChild = root.findAll({
rule: {
kind: "identifier",
inside: {
kind: "variable_declarator",
stopBy: "neighbor" // Only checks direct parent
}
}
});
return null;
};stopBy options:
"neighbor"- check only immediate parent/children"end"- search to tree boundary (default){ kind: "x" }- stop at specific node type- Rule object - stop when rule matches
Reference: JSSG API Reference
Order Nodes by Dependency
Define explicit depends_on relationships between workflow nodes. The engine executes nodes in topological order based on dependencies.
Incorrect (implicit ordering):
# workflow.yaml - assumes sequential execution
version: "1"
nodes:
- id: add-types
steps:
- type: js-ast-grep
codemod: ./scripts/add-types.ts
- id: update-imports
# No depends_on - might run before add-types!
steps:
- type: js-ast-grep
codemod: ./scripts/update-imports.ts
# Fails if types aren't added yet
- id: run-tests
steps:
- type: run
command: npm test
# Might run before transforms completeCorrect (explicit dependencies):
# workflow.yaml - explicit DAG
version: "1"
nodes:
- id: add-types
steps:
- type: js-ast-grep
codemod: ./scripts/add-types.ts
- id: update-imports
depends_on: [add-types] # Explicit dependency
steps:
- type: js-ast-grep
codemod: ./scripts/update-imports.ts
- id: fix-lint
depends_on: [update-imports]
steps:
- type: run
command: npx eslint --fix .
- id: run-tests
depends_on: [fix-lint] # Waits for all transforms
steps:
- type: run
command: npm testDependency patterns:
- Transform order:
[parse] → [transform] → [format] → [test] - Parallel-safe nodes can omit mutual dependencies
- Use arrays for multiple dependencies:
depends_on: [a, b] - Cyclic dependencies are detected and rejected
Reference: Codemod Workflow Reference
Use Conditional Steps for Dynamic Workflows
Use if expressions to conditionally execute steps based on state, parameters, or previous results.
Incorrect (always running all steps):
# workflow.yaml - runs everything regardless
version: "1"
nodes:
- id: migrate
steps:
- type: js-ast-grep
codemod: ./scripts/react-18.ts
- type: js-ast-grep
codemod: ./scripts/react-19.ts
# Runs even if not needed
- type: run
command: npm run typecheck
# Runs even if no changes were madeCorrect (conditional execution):
# workflow.yaml - smart step execution
version: "1"
params:
react_version:
type: string
default: "19"
skip_typecheck:
type: boolean
default: false
nodes:
- id: migrate
steps:
- type: js-ast-grep
codemod: ./scripts/react-18.ts
if: ${{ params.react_version == "18" }}
- type: js-ast-grep
codemod: ./scripts/react-19.ts
if: ${{ params.react_version == "19" }}
- type: run
command: npm run typecheck
if: ${{ !params.skip_typecheck }}Conditional based on state:
version: "1"
state:
has_typescript: false
nodes:
- id: detect-typescript
steps:
- type: run
command: test -f tsconfig.json && echo "true" || echo "false"
output: has_typescript
- id: type-migration
depends_on: [detect-typescript]
steps:
- type: js-ast-grep
codemod: ./scripts/add-types.ts
if: ${{ state.has_typescript == "true" }}Conditional expressions:
${{ params.x == "value" }}${{ state.flag == true }}${{ !params.skip }}${{ matrix.value == "special" }}
Reference: Codemod Workflow Reference
Use Manual Gates for Critical Steps
Add manual approval gates before destructive or irreversible operations. Gates pause execution until human approval.
Incorrect (fully automatic):
# workflow.yaml - no human checkpoints
version: "1"
nodes:
- id: migrate-database
steps:
- type: run
command: npm run db:migrate
# Runs immediately, no review
- id: deploy-production
depends_on: [migrate-database]
steps:
- type: run
command: npm run deploy:prod
# Deploys without approval!Correct (manual gates):
# workflow.yaml - human checkpoints
version: "1"
nodes:
- id: migrate-database
steps:
- type: run
command: npm run db:migrate:dry-run
# Dry run first
- id: review-migration
type: manual # Pauses for approval
depends_on: [migrate-database]
- id: apply-migration
depends_on: [review-migration]
steps:
- type: run
command: npm run db:migrate
- id: review-deployment
type: manual # Another checkpoint
depends_on: [apply-migration]
- id: deploy-production
depends_on: [review-deployment]
steps:
- type: run
command: npm run deploy:prodResume after approval:
# Check workflow status
npx codemod workflow status
# Resume after manual review
npx codemod workflow resume -w ./workflow.yamlWhen to use manual gates:
- Before database migrations
- Before production deployments
- After large-scale transforms (review diffs)
- Before irreversible operations
Reference: Codemod Workflow Reference
Use Matrix Strategy for Parallelism
Use matrix strategies to parallelize transforms across teams, directories, or configurations. Independent work items run concurrently.
Incorrect (sequential processing):
# workflow.yaml - processes teams one by one
version: "1"
nodes:
- id: migrate-team-a
steps:
- type: js-ast-grep
codemod: ./scripts/migrate.ts
target: ./packages/team-a
- id: migrate-team-b
depends_on: [migrate-team-a] # Unnecessary wait
steps:
- type: js-ast-grep
codemod: ./scripts/migrate.ts
target: ./packages/team-b
- id: migrate-team-c
depends_on: [migrate-team-b]
steps:
- type: js-ast-grep
codemod: ./scripts/migrate.ts
target: ./packages/team-c
# Total time: A + B + CCorrect (parallel matrix execution):
# workflow.yaml - parallel team processing
version: "1"
state:
teams:
- team-a
- team-b
- team-c
nodes:
- id: migrate-teams
strategy:
type: matrix
from_state: teams
steps:
- type: js-ast-grep
codemod: ./scripts/migrate.ts
target: ./packages/${{ matrix.value }}
# Total time: max(A, B, C)
- id: run-tests
depends_on: [migrate-teams]
steps:
- type: run
command: npm testAccess matrix values in transforms:
const transform: Transform<TSX> = (root, options) => {
const team = options.matrixValues?.value;
// Apply team-specific rules
if (team === "team-a") {
// Special handling for team-a
}
return null;
};Matrix use cases:
- Team/directory sharding
- Multi-variant transforms (different configs)
- Language-specific processing
- Repository-parallel execution
Reference: Codemod Workflow Reference
Validate Workflows Before Running
Always run workflow validate before executing workflows. Validation catches schema errors, missing dependencies, and cyclic references.
Incorrect (running without validation):
# Directly run without checking
npx codemod workflow run -w ./workflow.yaml
# Errors discovered mid-execution:
# - Missing codemod file at step 3
# - Invalid YAML syntax at line 47
# - Cyclic dependency between nodes
# - Unknown step type "jscodeshift"Correct (validate first):
# Validate workflow configuration
npx codemod workflow validate -w ./workflow.yaml
# Output shows all issues:
# ✓ Schema validation passed
# ✓ All codemod files exist
# ✓ No cyclic dependencies
# ✗ Error: Unknown step type "jscodeshift" at node "migrate"
# Hint: Did you mean "js-ast-grep"?
# Fix errors, then run
npx codemod workflow run -w ./workflow.yamlValidation checks:
- YAML syntax and schema compliance
- Node dependency DAG (no cycles)
- Referenced files exist (codemods, rules)
- Step types are valid
- Parameter schemas match usage
CI integration:
# .github/workflows/validate.yml
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx codemod workflow validate -w ./workflow.yamlReference: Codemod CLI
Related skills
How it compares
Pick codemod for AST migration authoring rather than Orval, which focuses on OpenAPI TypeScript client generation.
FAQ
How many rules does the codemod skill include?
The codemod skill includes 48 rules across 11 categories prioritized from CRITICAL AST understanding through LOW package structure. Categories cover pattern efficiency, parsing, traversal, semantics, edits, workflows, testing, state, and security.
What tools does the codemod skill target?
The codemod skill targets Codemod workflows with JSSG and ast-grep for AST-based transformations. It guides parser selection, idempotent edit patterns, YAML workflow orchestration, and test fixture validation for automated migrations.