jscodeshift-codemods

Core Philosophy: Transform AST nodes, not text. Let recast handle printing to preserve formatting and structure.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "jscodeshift-codemods" with this command: npx skills add third774/dotfiles/third774-dotfiles-jscodeshift-codemods

jscodeshift Codemods

Core Philosophy: Transform AST nodes, not text. Let recast handle printing to preserve formatting and structure.

When to Use

Use codemods for:

  • API migrations - Library upgrades (React Router v5→v6, enzyme→RTL)

  • Pattern standardization - Enforce coding conventions across codebase

  • Deprecation removal - Remove deprecated APIs systematically

  • Large-scale refactoring - Rename functions, restructure imports, update patterns

Don't use codemods for:

  • One-off changes (faster to do manually)

  • Changes requiring semantic understanding (business logic)

  • Non-deterministic transformations

Codemod Workflow

Copy this checklist and track your progress:

Codemod Progress:

  • Phase 1: Identify Patterns
    • Collect before/after examples from real code
    • Document transformation rules
    • Identify edge cases
  • Phase 2: Create Test Fixtures
    • Create input fixture with pattern to transform
    • Create expected output fixture
    • Verify test fails (TDD)
  • Phase 3: Implement Transform
    • Find target nodes
    • Apply transformation
    • Return modified source
  • Phase 4: Handle Edge Cases
    • Add fixtures for edge cases
    • Handle already-transformed code (idempotency)
    • Handle missing dependencies
  • Phase 5: Validate at Scale
    • Dry run on target codebase
    • Review sample of changes
    • Run with --fail-on-error

Project Structure

Standard codemod project layout:

codemods/ ├── my-transform.ts # Transform implementation ├── tests/ │ └── my-transform-test.ts # Test file └── testfixtures/ ├── my-transform.input.ts # Input fixture ├── my-transform.output.ts # Expected output ├── edge-case.input.ts # Additional fixtures └── edge-case.output.ts

Transform Module Anatomy

Every transform exports a function with this signature:

import type { API, FileInfo, Options } from "jscodeshift";

export default function transform( fileInfo: FileInfo, api: API, options: Options ): string | null | undefined { const j = api.jscodeshift; const root = j(fileInfo.source);

// Find and transform nodes root .find(j.Identifier, { name: "oldName" }) .forEach((path) => { path.node.name = "newName"; });

// Return transformed source, null to skip, or undefined for no change return root.toSource(); }

Return values:

Return Meaning

string

Transformed source code

null

Skip this file (no output)

undefined

No changes made

Key objects:

Object Purpose

fileInfo.source

Original file contents

fileInfo.path

File path being transformed

api.jscodeshift

The jscodeshift library (usually aliased as j )

api.stats

Collect statistics during dry runs

api.report

Print to stdout

Testing with defineTest

jscodeshift provides fixture-based testing utilities:

// tests/my-transform-test.ts jest.autoMockOff(); const defineTest = require("jscodeshift/dist/testUtils").defineTest;

// Basic test - uses my-transform.input.ts → my-transform.output.ts defineTest(__dirname, "my-transform");

// Named fixtures for edge cases defineTest(__dirname, "my-transform", null, "already-transformed"); defineTest(__dirname, "my-transform", null, "missing-import"); defineTest(__dirname, "my-transform", null, "multiple-occurrences");

Fixture naming:

testfixtures/ ├── my-transform.input.ts # Default input ├── my-transform.output.ts # Default output ├── already-transformed.input.ts # Named fixture input ├── already-transformed.output.ts # Named fixture output

Running tests:

Run all codemod tests

npx jest codemods/tests/

Run specific transform tests

npx jest codemods/tests/my-transform-test.ts

Run with verbose output

npx jest codemods/tests/my-transform-test.ts --verbose

Collection API Quick Reference

The jscodeshift Collection API provides chainable methods:

Method Purpose Example

find(type, filter?)

Find nodes by type root.find(j.CallExpression, { callee: { name: 'foo' } })

filter(predicate)

Filter collection .filter(path => path.node.arguments.length > 0)

forEach(callback)

Iterate and mutate .forEach(path => { path.node.name = 'new' })

replaceWith(node)

Replace matched nodes .replaceWith(j.identifier('newName'))

remove()

Remove matched nodes .remove()

insertBefore(node)

Insert before each match .insertBefore(j.importDeclaration(...))

insertAfter(node)

Insert after each match .insertAfter(j.expressionStatement(...))

closest(type)

Find nearest ancestor .closest(j.FunctionDeclaration)

get()

Get first path .get()

paths()

Get all paths as array .paths()

size()

Count matches .size()

Chaining pattern:

root .find(j.CallExpression, { callee: { name: "oldFunction" } }) .filter((path) => path.node.arguments.length === 2) .forEach((path) => { path.node.callee.name = "newFunction"; });

Common Node Types

Node Type Represents Example Code

Identifier

Variable/function names foo , myVar

CallExpression

Function calls foo() , obj.method()

MemberExpression

Property access obj.prop , arr[0]

ImportDeclaration

Import statements import { x } from 'y'

ImportSpecifier

Named imports { x } in import

ImportDefaultSpecifier

Default imports x in import x from

VariableDeclaration

Variable declarations const x = 1

VariableDeclarator

Individual variable x = 1 part

FunctionDeclaration

Named functions function foo() {}

ArrowFunctionExpression

Arrow functions () => {}

ObjectExpression

Object literals { a: 1, b: 2 }

ArrayExpression

Array literals [1, 2, 3]

Literal

Primitive values 'string' , 42 , true

StringLiteral

String values 'hello'

Common Transformation Patterns

Rename Import Source

// Change: import { x } from 'old-package' // To: import { x } from 'new-package'

root .find(j.ImportDeclaration, { source: { value: "old-package" } }) .forEach((path) => { path.node.source.value = "new-package"; });

Rename Named Import

// Change: import { oldName } from 'package' // To: import { newName } from 'package'

root .find(j.ImportSpecifier, { imported: { name: "oldName" } }) .forEach((path) => { path.node.imported.name = "newName"; // Also rename local if not aliased if (path.node.local.name === "oldName") { path.node.local.name = "newName"; } });

Add Import If Missing

// Add: import { newThing } from 'package'

const existingImport = root.find(j.ImportDeclaration, { source: { value: "package" }, });

if (existingImport.size() === 0) { // Add new import at top of file const newImport = j.importDeclaration( [j.importSpecifier(j.identifier("newThing"))], j.literal("package") );

root.find(j.Program).get("body", 0).insertBefore(newImport); }

Rename Function Calls

// Change: oldFunction(arg) // To: newFunction(arg)

root .find(j.CallExpression, { callee: { name: "oldFunction" } }) .forEach((path) => { path.node.callee.name = "newFunction"; });

Transform Function Arguments

// Change: doThing(a, b, c) // To: doThing({ a, b, c })

root .find(j.CallExpression, { callee: { name: "doThing" } }) .filter((path) => path.node.arguments.length === 3) .forEach((path) => { const [a, b, c] = path.node.arguments; path.node.arguments = [ j.objectExpression([ j.property("init", j.identifier("a"), a), j.property("init", j.identifier("b"), b), j.property("init", j.identifier("c"), c), ]), ]; });

Track Variable Usage Across Scope

// Find what variable an import is bound to, then find all usages

root.find(j.ImportSpecifier, { imported: { name: "useHistory" } }).forEach((path) => { const localName = path.node.local.name; // Could be aliased

// Find all calls using this variable root .find(j.CallExpression, { callee: { name: localName } }) .forEach((callPath) => { // Transform each usage }); });

Replace Entire Expression

// Change: history.push('/path') // To: navigate('/path')

root .find(j.CallExpression, { callee: { type: "MemberExpression", object: { name: "history" }, property: { name: "push" }, }, }) .replaceWith((path) => { return j.callExpression(j.identifier("navigate"), path.node.arguments); });

Anti-Patterns

Over-Matching

// BAD: Matches ANY identifier named 'foo' root.find(j.Identifier, { name: "foo" });

// GOOD: Match specific context (function calls named 'foo') root.find(j.CallExpression, { callee: { name: "foo" } });

Ignoring Scope

// BAD: Assumes 'history' always means the router history root.find(j.Identifier, { name: "history" });

// GOOD: Verify it came from the expected import const historyImport = root.find(j.ImportSpecifier, { imported: { name: "useHistory" }, }); if (historyImport.size() === 0) return; // Skip file

Not Checking Idempotency

// BAD: Adds import every time, even if already present root.find(j.Program).get("body", 0).insertBefore(newImport);

// GOOD: Check first const existingImport = root.find(j.ImportDeclaration, { source: { value: "package" }, }); if (existingImport.size() === 0) { root.find(j.Program).get("body", 0).insertBefore(newImport); }

Destructive Transforms

// BAD: Rebuilds node from scratch, loses comments and formatting path.replace( j.callExpression(j.identifier("newFn"), [j.literal("arg")]) );

// GOOD: Mutate existing node to preserve metadata path.node.callee.name = "newFn";

Testing Only Happy Path

// BAD: Only one test fixture defineTest(__dirname, "my-transform");

// GOOD: Cover edge cases defineTest(__dirname, "my-transform"); defineTest(__dirname, "my-transform", null, "already-transformed"); defineTest(__dirname, "my-transform", null, "aliased-import"); defineTest(__dirname, "my-transform", null, "no-matching-code");

Debugging Transforms

Dry Run with Print

See output without writing files

npx jscodeshift -t my-transform.ts target/ --dry --print

Log Node Structure

root.find(j.CallExpression).forEach((path) => { console.log(JSON.stringify(path.node, null, 2)); });

Verbose Mode

Show transformation stats

npx jscodeshift -t my-transform.ts target/ --verbose=2

Fail on Errors

Exit with code 1 if any file fails

npx jscodeshift -t my-transform.ts target/ --fail-on-error

CLI Quick Reference

Basic usage

npx jscodeshift -t transform.ts src/

TypeScript/TSX files

npx jscodeshift -t transform.ts src/ --parser=tsx --extensions=ts,tsx

Dry run (no changes)

npx jscodeshift -t transform.ts src/ --dry

Print output to stdout

npx jscodeshift -t transform.ts src/ --print

Limit parallelism

npx jscodeshift -t transform.ts src/ --cpus=4

Ignore patterns

npx jscodeshift -t transform.ts src/ --ignore-pattern="**/*.test.ts"

Integration

Complementary skills:

  • writing-tests - For test-first codemod development

  • systematic-debugging - When transforms produce unexpected results

  • verification-before-completion - Verify codemod works before claiming done

Language-specific patterns:

  • React/TypeScript: See references/react-typescript.md for JSX transforms, hook migrations, and component patterns

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

documenting-code-comments

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

adversarial-code-review

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

customizing-opencode

No summary provided by upstream source.

Repository SourceNeeds Review