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.
schema_version: "1.0"name: "@nodejs/my-migration"version: "1.0.0"description: Migrate deprecated API to modern equivalentauthor: Your Namelicense: MITworkflow: workflow.yamlcategory: migrationtargets: languages: - javascript - typescriptkeywords: - transformation - migrationregistry: access: public visibility: public
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 MigrationThis recipe migrates deprecated oldAPI to the modern newAPI.See DEP0XXX in Node.js deprecations documentation.## ExampleBefore:import { oldAPI } from 'node:os';const result = oldAPI()After:import { newAPI } from 'node:os';const result = newAPI()
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 importedconst allStatements = [ ...getNodeImportStatements(root, 'util'), ...getNodeRequireCalls(root, 'util'),];