> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/nodejs/userland-migrations/llms.txt
> Use this file to discover all available pages before exploring further.

# Creating Recipes

> Step-by-step guide to creating new migration recipes

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 |

<Note>
  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`.
</Note>

## Creating a Recipe

<Steps>
  <Step title="Create the recipe directory">
    Create a new directory under `recipes/` with a descriptive name:

    ```bash theme={null}
    mkdir recipes/my-migration
    cd recipes/my-migration
    ```
  </Step>

  <Step title="Create package.json">
    Set up the package manifest:

    ```json package.json theme={null}
    {
      "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": "*"
      }
    }
    ```
  </Step>

  <Step title="Create codemod.yaml">
    Define the codemod metadata:

    ```yaml codemod.yaml theme={null}
    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
    ```
  </Step>

  <Step title="Create workflow.yaml">
    Define the transformation workflow:

    ```yaml workflow.yaml theme={null}
    # 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
    ```
  </Step>

  <Step title="Write the transformation">
    Create `src/workflow.ts` with your transformation logic:

    ```typescript src/workflow.ts theme={null}
    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);
    }
    ```
  </Step>

  <Step title="Create tests">
    Set up test input and expected output files:

    ```bash theme={null}
    mkdir -p tests/input tests/expected
    ```

    Create test cases in `tests/input/`:

    ```javascript tests/input/file-0.js theme={null}
    const { oldAPI } = require('os');

    const result = oldAPI();
    ```

    Create expected output in `tests/expected/`:

    ```javascript tests/expected/file-0.js theme={null}
    const { newAPI } = require('os');

    const result = newAPI();
    ```
  </Step>

  <Step title="Add README documentation">
    Create `README.md` with usage examples showing before/after code:

    ```markdown README.md theme={null}
    # 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()
    ```
  </Step>
</Steps>

## 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`:

```yaml workflow.yaml theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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:

```bash theme={null}
cd recipes/my-migration
npm test
```

This uses the jssg test runner to compare input files against expected output.

## Next Steps

<CardGroup cols={2}>
  <Card title="Testing Guide" icon="flask" href="/contributing/testing">
    Learn about testing requirements and best practices
  </Card>

  <Card title="Development Workflow" icon="code-branch" href="/contributing/workflow">
    Understand the PR process and pre-commit checks
  </Card>
</CardGroup>
