These utilities work with JavaScript, TypeScript, JSX, and TSX ASTs. They handle both ESM (
import) and CJS (require()) formats automatically.getImport
Locate an import of a given module and return its alias/identifier node.
Parameters
SgNode<T, 'program'>
required
The root program node to search within.
GetImportOptions
required
What to look for:
{ type: "default", from: string }— Find the default import from a module.{ type: "named", name: string, from: string }— Find a specific named import from a module.
GetImportResult<T> | null — An object containing:
Returns
null if the import is not found. A namespace import is returned only when the requested default or named binding is absent. A type-only binding is still returned, with isTypeOnly: true, so it is not treated as a value.
Supported Import Shapes
Example
getAllImports
Locate imports of a given module and return their alias/identifier nodes.
Parameters
SgNode<T, 'program'>
required
The root program node to search within.
GetImportOptions
required
What to look for:
{ type: "default", from: string }— Find all default imports from a module.{ type: "named", name: string, from: string }— Find all instances of a specific named import from a module.
ResolvedImport<T>[] — An array containing:
Returns an empty array if no imports are found.
{ type: "default" } includes every namespace import from that module even when a default import exists. A named query includes a namespace import only when that export is not imported by name.
For JavaScript, TypeScript, and TSX, dynamic import("module").then(...) callbacks
return the callback’s parameter binding. Identifiers in its body, nested function
parameters, and identifiers in TypeScript parameter default values are excluded.
Example
listImports
List the import statements and require() / import() declarations in a file. getAllImports asks for one exported name. listImports answers “what does this file import from these packages?”, which is the question a barrel split asks (lodash, date-fns, rxjs, @mui/material).
Parameters
SgNode<T, 'program'>
required
The root program node.
ListImportsOptions
{ from?: string | readonly string[] }. When set, only statements whose module specifier is exactly one of those strings are returned. "mod" does not match "mod/extra". Quotes on the specifier do not matter.specifiers[].isTypeOnly is true for both import type { Foo } and import { type Foo }. The statement flag isTypeOnly is only the first form.
The same statements removeImport can edit are listed. A multi-declarator const a = require("mod"), b = 1 is omitted, so a later deletion cannot drop b. const x = import("mod") without await is omitted. import("mod").then(...) is included.
addImport
Add an import to the program. Smart behavior:
- Skips if the import already exists
- Merges named specifiers into an existing import statement from the same source
- Creates a new import statement otherwise
- Inserts after the last existing import (or at file start if no imports exist)
- A new statement copies the quote and semicolon of the first module specifier already in the file. When the file has none, it copies the first string literal and the first semicolon-style statement, and keeps
'and;only when those are absent. A merge does not rewrite the existing clause. - A named specifier with
isTypeOnly: trueis written as inlinetype(import { type Foo }), notimport type. A value specifier is not merged into an existingimport typestatement. When a value import from the same module already exists, the specifier merges into that statement even if animport typeline comes first. Otherwise it becomes a second statement. Type-only specifiers merged intoimport typedo not repeat thetypekeyword.import type Fooorimport type * as nswith the same local name dropstypeinstead of adding a second statement. A different local name is a new statement. - A type-only binding does not count as the value.
import { type Foo }drops thetypemodifier, andimport type { Foo, Bar }becomesimport { Foo, type Bar }, when the local name matches. A different local name (import type { Foo as F }plus a valueFoo) stays a separate statement. - A named specifier already exists only when that export is bound to the same local name.
import { foo as bar }does not satisfyfoo, andimport * as nsdoes not satisfy a named specifier. A new statement is inserted after the last module-level import. Arequire()orimport()inside a function is not an insertion point.
SgNode<T, 'program'>
required
The root program node.
AddImportOptions
required
One of:
{ type: "default", name: string, from: string, moduleType?: "esm" | "cjs" }— Add a default import.{ type: "namespace", name: string, from: string }— Add a namespace import (import * as name).{ type: "named", specifiers: Array<{ name: string, alias?: string, isTypeOnly?: boolean }>, from: string, moduleType?: "esm" | "cjs" }— Add named imports.isTypeOnlyemits an inlinetypemodifier.
Edit | null — An edit to apply via rootNode.commitEdits(), or null if the import already exists.
Example
removeImport
Remove an import from the program. Smart behavior:
- Default/namespace: Removes the entire import statement. Pass
aliasto remove that local binding when the module is imported more than once. - Named (multiple specifiers): Removes only the specified specifiers, keeping the rest. The same export is removed from every matching statement. Pass
{ name, alias }instead of a string to remove one local binding. - Named (last specifier): Removes the entire import statement.
- Handles both ESM and CJS formats.
SgNode<T, 'program'>
required
The root program node.
RemoveImportOptions
required
One of:
{ type: "default", from: string, alias?: string, removeSideEffectForms?: boolean }— Remove the default import from a module. Setaliasto choose the local binding. SetremoveSideEffectFormsto also remove a side-effect-onlyimport "module"orrequire("module")when no binding import exists.{ type: "namespace", from: string, alias?: string }— Remove the namespace import from a module.{ type: "named", specifiers: Array<string | { name: string, alias?: string }>, from: string }— Remove specific named imports.aliasis the current local name.
Edit[] | null — A flat array of edits to pass to rootNode.commitEdits(), or null if the import was not found. Separated matches stay as separate array elements so text between them can still change. Spread into an Edit[] (do not wrap the array again).
Example
updateImport
Replace named specifiers of an existing import or require in place. The whole clause is one edit, so the rewrite does not overlap other specifier edits.
import { foo } from "mod"with{ name: "foo", to: "bar" }becomesimport { bar } from "mod".- An existing local alias is kept:
import { foo as f }becomesimport { bar as f }. Passaliasto override that local name. - Every matching clause is updated. Pass
localto rename one current binding when the same export is imported more than once (fooandfoo as bar, or two statements). Clauses separated by other statements come back as an array of edits. fooandfoo as barstay distinct. Renaming a sibling does not drop the alias.- Works for ESM named imports, destructured
require(), and destructured dynamicimport(). - If the resulting local name is already used in the same clause, the existing binding stays and the rename is dropped.
import { foo, baz as bar }renamed fromfootobarstaysimport { baz as bar }.
SgNode<T, 'program'>
required
The root program node.
UpdateImportOptions
required
Named imports only:
{ type: "named", from: string, specifiers: Array<{ name: string, to: string, alias?: string, local?: string }> }— Renamenametotoin the import fromfrom.localis the current local name to match.
Edit[] | null — A flat array of edits to pass to rootNode.commitEdits(), or null if none of the named specifiers is found or specifiers is empty. Separated clauses are separate elements. Pass the array directly (do not wrap it in another array).
Example
stringToExactRegexString
Escape a string and return a regex that matches only that exact text. The result is wrapped in ^...$, with regex metacharacters escaped.
getFileStyle
Quote, semicolon, and indent of a JavaScript or TypeScript file.
addImport uses the quote and semicolon when it creates a statement. Call getFileStyle yourself when you insert other code.
The first module specifier wins. Later imports, and strings elsewhere in the file, are ignored while a specifier exists. This does not read Prettier or EditorConfig.
getLineIndent
Leading spaces or tabs on the line a node starts on. Returns "" when any other character sits before the node on that line, so foo(bar) and an indented foo(bar) do not treat the preceding code as indent. The node’s position is a UTF-8 byte offset, so non-ASCII text earlier in the file does not shift the line. A carriage return is not part of the indent.
indentText
Prefix every non-empty line. Blank lines stay blank, so an inserted block does not grow trailing whitespace.
importBindingOf
The import that defines this use, or null when the name is local. This is the opposite of getImport: the call site is known and the package is not.
setup() is rewritten only when importedName is "setup" and source is the package you mean. A parameter named setup, or the same name imported from another package, returns a different binding or null.
The lookup stops at the import in the file that contains the use. import { server } from "./browser" reports source "./browser", not the package ./browser imported from. ns.graphql after import * as ns is not resolved; call this on ns and read the property text yourself.
Returns null when semantic analysis is unavailable. definition() can throw in that case; this function does not.
resolveDefinition
The node that defines this use, following one import into another project file.
- A local declaration returns that node.
- An import the workspace can resolve returns that file’s definition. In the semantic API this kind is
"external", which means another project file, not a package on npm. - An unresolved package import returns the import specifier.
const a = setup(); const b = aresolvesbtob, not tosetup(). A second hop is out of scope.
null when semantic analysis is unavailable.
resolvesToFactory
True when the binding is a variable initialized by a direct call, or new, of importedName from one of sources.
new QueryClient(), new PrismaClient(), and express() are the same shape. One hop includes a binding imported from another project file (export const server = setupServer() in browser.ts, server.listen() in the file that imports it).
These stay false:
const { listen } = setupServer()lib.setupServer()const a = setupServer(); const b = a- a shadowed local with the same spelling, such as
const server = app.listen()inside a function
renameReferences
Edits that rename references of one binding in currentFile. Pass root.filename().
Shadowed names stay. references() only returns this binding, so a parameter spelled the same as the import is not renamed. Comments, string literals, and property names such as record.cleanUrl are not references and stay. A shorthand property is an identifier, so { cleanUrl } becomes { getCleanUrlString } and the key changes. The import clause is not renamed; use updateImport for that.
Workspace references() also returns other files. Those edits are omitted. Writing them onto the file currently being committed targets the wrong tree. Cross-file renames stay with the caller, via root.write on the other file.
Returns [] when semantic analysis is unavailable.
Common Patterns
Verify Import Before Transforming
Always verify that a symbol is imported from the expected package before transforming its usage:Replace One Import With Another
Rename a named specifier in the same module withupdateImport:
removeImport and addImport:
Match Existing Module Style
getImport automatically detects the module type. Use moduleType when adding imports to match the existing style: