Skip to main content
This guide walks you through creating a new migration recipe for Node.js Userland Migrations.

Recipe Components

Every recipe consists of several required files that work together: | File | Purpose | |------|---------|| | README.md | Description, purpose, and usage instructions | | package.json | Package manifest with dependencies | | src/workflow.ts | Main transformation logic using jssg API | | codemod.yaml | Codemod metadata and configuration | | workflow.yaml | Workflow definition with transformation steps | | tests/ | Test suite with input and expected output files | | tsconfig.json | TypeScript configuration |
The workflow.ts naming is conventional but can be changed. For multi-step codemods, use descriptive names like enroll-to-set-timeout.ts or cleanup-imports.ts.

Creating a Recipe

1

Create the recipe directory

Create a new directory under recipes/ with a descriptive name:
mkdir recipes/my-migration
cd recipes/my-migration
2

Create package.json

Set up the package manifest:
package.json
{
  "name": "@nodejs/my-migration",
  "version": "1.0.0",
  "description": "Migrate deprecated API to modern equivalent",
  "type": "module",
  "scripts": {
    "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/nodejs/userland-migrations.git",
    "directory": "recipes/my-migration"
  },
  "author": "Your Name",
  "license": "MIT",
  "devDependencies": {
    "@codemod.com/jssg-types": "^1.5.0"
  },
  "dependencies": {
    "@nodejs/codemod-utils": "*"
  }
}
3

Create codemod.yaml

Define the codemod metadata:
codemod.yaml
schema_version: "1.0"
name: "@nodejs/my-migration"
version: "1.0.0"
description: Migrate deprecated API to modern equivalent
author: Your Name
license: MIT
workflow: workflow.yaml
category: migration

targets:
  languages:
    - javascript
    - typescript

keywords:
  - transformation
  - migration

registry:
  access: public
  visibility: public
4

Create workflow.yaml

Define the transformation workflow:
workflow.yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json

version: "1"

nodes:
  - id: apply-transforms
    name: Apply AST Transformations
    type: automatic
    steps:
      - name: Migrate deprecated API to modern equivalent
        js-ast-grep:
          js_file: src/workflow.ts
          base_path: .
          include:
            - "**/*.cjs"
            - "**/*.js"
            - "**/*.jsx"
            - "**/*.mjs"
            - "**/*.cts"
            - "**/*.mts"
            - "**/*.ts"
            - "**/*.tsx"
          exclude:
            - "**/node_modules/**"
          language: typescript
5

Write the transformation

Create src/workflow.ts with your transformation logic:
src/workflow.ts
import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement';
import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call';
import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path';
import type { SgRoot, Edit } from '@codemod.com/jssg-types/main';
import type Js from '@codemod.com/jssg-types/langs/javascript';

/**
 * Transform function that migrates deprecated API usage
 * to the modern equivalent.
 *
 * Handles:
 * 1. Updates import/require statements
 * 2. Transforms function calls
 * 3. Preserves original variable names
 */
export default function transform(root: SgRoot<Js>): string | null {
  const rootNode = root.root();
  const edits: Edit[] = [];

  // Find imports and requires for the target module
  const allStatements = [
    ...getNodeImportStatements(root, 'os'),
    ...getNodeRequireCalls(root, 'os'),
  ];

  // No imports found, skip transformation
  if (!allStatements.length) return null;

  for (const statement of allStatements) {
    // Step 1: Update import/require statements
    const namedImports = statement.find({
      rule: { kind: 'named_imports' },
    });

    if (namedImports) {
      const originalText = namedImports.text();
      if (originalText.includes('oldAPI')) {
        const newText = originalText.replace(/\boldAPI\b/g, 'newAPI');
        edits.push(namedImports.replace(newText));
      }
    }
  }

  // Step 2: Find and replace function calls
  const functionCalls = rootNode.findAll({
    rule: { pattern: 'oldAPI' },
  });

  for (const call of functionCalls) {
    edits.push(call.replace('newAPI'));
  }

  if (!edits.length) return null;

  return rootNode.commitEdits(edits);
}
6

Create tests

Set up test input and expected output files:
mkdir -p tests/input tests/expected
Create test cases in tests/input/:
tests/input/file-0.js
const { oldAPI } = require('os');

const result = oldAPI();
Create expected output in tests/expected/:
tests/expected/file-0.js
const { newAPI } = require('os');

const result = newAPI();
7

Add README documentation

Create README.md with usage examples showing before/after code:
README.md
# My Migration

This recipe migrates deprecated oldAPI to the modern newAPI.

See DEP0XXX in Node.js deprecations documentation.

## Example

Before:
import { oldAPI } from 'node:os';
const result = oldAPI()

After:
import { newAPI } from 'node:os';
const result = newAPI()

Multi-Step Transformations

For complex migrations requiring multiple transformation passes, create separate files for each step:
recipes/timers-deprecations/
├── src/
│   ├── enroll-to-set-timeout.ts
│   ├── unenroll-to-clear-timer.ts
│   ├── active-to-standard-timer.ts
│   ├── unref-active-to-unref.ts
│   └── cleanup-imports.ts
└── workflow.yaml
Define each step in workflow.yaml:
workflow.yaml
version: "1"

nodes:
  - id: apply-transforms
    name: Apply AST Transformations
    type: automatic
    steps:
      - name: Replace timers.enroll() with setTimeout()
        js-ast-grep:
          js_file: src/enroll-to-set-timeout.ts
          base_path: .
          include:
            - "**/*.js"
            - "**/*.ts"
          exclude:
            - "**/node_modules/**"
          language: typescript

      - name: Replace timers.unenroll() with standard clear APIs
        js-ast-grep:
          js_file: src/unenroll-to-clear-timer.ts
          base_path: .
          include:
            - "**/*.js"
            - "**/*.ts"
          exclude:
            - "**/node_modules/**"
          language: typescript

      - name: Clean up node:timers imports and requires
        js-ast-grep:
          js_file: src/cleanup-imports.ts
          base_path: .
          include:
            - "**/*.js"
            - "**/*.ts"
          exclude:
            - "**/node_modules/**"
          language: typescript

Common Patterns

Finding Imports and Requires

import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement';
import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call';

// Find all ways the module is imported
const allStatements = [
  ...getNodeImportStatements(root, 'util'),
  ...getNodeRequireCalls(root, 'util'),
];

Resolving Bindings

import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path';

for (const node of allStatements) {
  // Resolves 'util.log' to actual binding name (e.g., 'u.log')
  const binding = resolveBindingPath(node, '$.log');
  
  if (binding) {
    // Find all calls using this binding
    const matches = rootNode.findAll({
      rule: { pattern: `${binding}($$$ARG)` },
    });
  }
}

Removing Bindings

import { removeBinding } from '@nodejs/codemod-utils/ast-grep/remove-binding';
import { removeLines } from '@nodejs/codemod-utils/ast-grep/remove-lines';

const result = removeBinding(node, bindingName);

if (result?.edit) {
  edits.push(result.edit);
}

if (result?.lineToRemove) {
  linesToRemove.push(result.lineToRemove);
}

// Apply edits and clean up empty lines
const sourceCode = rootNode.commitEdits(edits);
return removeLines(sourceCode, linesToRemove);

Testing Your Recipe

Run tests for your recipe:
cd recipes/my-migration
npm test
This uses the jssg test runner to compare input files against expected output.

Next Steps

Testing Guide

Learn about testing requirements and best practices

Development Workflow

Understand the PR process and pre-commit checks

Build docs developers (and LLMs) love