Utilities for resolving how global API paths should be accessed based on import patterns, and for safely removing or updating bindings.
Overview
These utilities handle the complexity of:
- Determining how imported functions are accessed locally (namespace, destructured, aliased)
- Safely removing unused imports while preserving other bindings
- Updating or replacing import bindings
- Handling both ES modules and CommonJS
Binding Path Resolution
resolveBindingPath
Resolves how a global API path should be accessed based on the import pattern.
import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path';
// Example 1: Destructured import
// Given: const { types } = require('node:util');
const path = resolveBindingPath(node, '$.types.isNativeError');
// Returns: 'types.isNativeError'
// Example 2: Namespace import
// Given: const util = require('node:util');
const path = resolveBindingPath(node, '$.types.isNativeError');
// Returns: 'util.types.isNativeError'
// Example 3: Aliased import
// Given: import { types as utilTypes } from 'node:util';
const path = resolveBindingPath(node, '$.types.isNativeError');
// Returns: 'utilTypes.isNativeError'
The AST node representing the import or require statement. Must be one of:
lexical_declaration
variable_declarator
import_statement
import_clause
The expected dotted path to resolve (e.g., ”.types.isNativeError").The‘` represents the module namespace.
Returns: string | undefined - The local access path that should be used in code, or undefined if the binding is not found
The $ in the path parameter represents the root module namespace and gets replaced with the actual local binding name.
Advanced Examples
// Given: const util = require('node:util');
const path = resolveBindingPath(node, '$.types.isNativeError');
// Returns: 'util.types.isNativeError'
const path2 = resolveBindingPath(node, '$.isDeepStrictEqual');
// Returns: 'util.isDeepStrictEqual'
// Given: import { types } from 'node:util';
const path = resolveBindingPath(node, '$.types.isNativeError');
// Returns: 'types.isNativeError'
// Given: const { types: utilTypes } = require('node:util');
const path = resolveBindingPath(node, '$.types.isNativeError');
// Returns: 'utilTypes.isNativeError'
// Given: const isNativeError = require('node:util').types.isNativeError;
const path = resolveBindingPath(node, '$.types.isNativeError');
// Returns: undefined (specific member access, not a binding pattern)
Binding Removal
removeBinding
Removes a specific binding from imports/requires, or removes the entire statement if it’s the only binding.
import { removeBinding } from '@nodejs/codemod-utils/ast-grep/remove-binding';
// Example 1: Remove one binding from multiple
// Given: const { types, isNativeError } = require('node:util');
const result = removeBinding(node, 'isNativeError');
// Result: { edit: Edit } transforms to: const { types } = require('node:util');
// Example 2: Remove the only binding
// Given: const { isNativeError } = require('node:util');
const result = removeBinding(node, 'isNativeError');
// Result: { lineToRemove: Range } to remove entire statement
// Example 3: Remove namespace binding
// Given: const util = require('node:util');
const result = removeBinding(node, 'util');
// Result: { lineToRemove: Range } to remove entire statement
The AST node representing the import or require statement
The name of the binding to remove (e.g., “isNativeError”)
Optional configuration objectIf provided, the binding will only be removed if it is unusedoptions.usageCheck.ignoredRanges
Array of ranges to ignore when checking for usage
Custom root node for usage checking. Defaults to the file root.
Returns: { edit?: Edit, lineToRemove?: Range } | undefined
- If multiple bindings exist: returns
{ edit } to modify the destructuring pattern
- If single binding: returns
{ lineToRemove } with the line range to remove
- If binding not found: returns
undefined
Usage Check Example
const result = removeBinding(node, 'isNativeError', {
usageCheck: {
ignoredRanges: [node.range()] // Ignore references in the import itself
}
});
if (result?.edit) {
context.applyEdit(result.edit);
} else if (result?.lineToRemove) {
// Remove the entire import line
const cleaned = removeLines(sourceCode, [result.lineToRemove]);
context.writeFile(filePath, cleaned);
}
Binding Updates
updateBinding
Updates a specific binding from imports/requires. Can replace, add, or remove bindings.
import { updateBinding } from '@nodejs/codemod-utils/ast-grep/update-binding';
// Example 1: Replace a binding
// Given: const { isNativeError } = require('node:util');
const result = updateBinding(node, { old: 'isNativeError', new: 'types' });
// Result: const { types } = require('node:util');
// Example 2: Add a new binding
// Given: const { isNativeError } = require('node:util');
const result = updateBinding(node, { old: undefined, new: 'types' });
// Result: const { isNativeError, types } = require('node:util');
// Example 3: Remove a binding (same as removeBinding)
// Given: const { isNativeError, types } = require('node:util');
const result = updateBinding(node, { old: 'isNativeError', new: undefined });
// Result: const { types } = require('node:util');
// Example 4: Many-to-many replacement
// Given: const { fips } = require('node:crypto');
const result = updateBinding(node, {
old: 'fips',
new: ['getFips', 'setFips']
});
// Result: const { getFips, setFips } = require('node:crypto');
The AST node representing the import or require statement
Configuration object for the update operationThe name of the binding to update or remove. Omit to add a new binding.
The new binding name(s) to replace the old one. Can be:
- A single string for one-to-one replacement
- An array of strings for one-to-many replacement
undefined to remove the binding
Optional usage checking configurationoptions.usageCheck.ignoredRanges
Ranges to ignore when checking if a binding is used
Custom root node for usage checking
Returns: { edit?: Edit, lineToRemove?: Range } | undefined
Advanced Update Patterns
One-to-Many
Adding Multiple
Rename Alias
// Replacing deprecated single export with multiple new exports
// Given: const { fips } = require('node:crypto');
updateBinding(node, {
old: 'fips',
new: ['getFips', 'setFips']
});
// Result: const { getFips, setFips } = require('node:crypto');
// Adding multiple new bindings
// Given: import { readFile } from 'fs';
updateBinding(node, {
new: ['writeFile', 'appendFile']
});
// Result: import { readFile, writeFile, appendFile } from 'fs';
// Given: import { types as utilTypes } from 'node:util';
updateBinding(node, { old: 'types', new: 'nodeTypes' });
// Result: import { nodeTypes } from 'node:util';
Complete Migration Example
import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies';
import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path';
import { updateBinding } from '@nodejs/codemod-utils/ast-grep/update-binding';
const ast = context.getAST();
const utilImports = getModuleDependencies(ast, 'util');
for (const importNode of utilImports) {
// Check if using deprecated isNativeError
const oldPath = resolveBindingPath(importNode, '$.isNativeError');
if (oldPath) {
// Need to replace isNativeError with types.isNativeError
const result = updateBinding(importNode, {
old: 'isNativeError',
new: 'types'
});
if (result?.edit) {
context.applyEdit(result.edit);
// Now update all usage sites
const usages = ast.findAll({
rule: { kind: 'identifier', pattern: 'isNativeError' }
});
for (const usage of usages) {
context.applyEdit(usage.replace('types.isNativeError'));
}
}
}
}