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.
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.
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. - 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.
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.
- Named (multiple specifiers): Removes only the specified specifiers, keeping the rest.
- 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, removeSideEffectForms?: boolean }— Remove the default import from a module. SetremoveSideEffectFormsto also remove a side-effect-onlyimport "module"orrequire("module")when no binding import exists.{ type: "namespace", from: string }— Remove the namespace import from a module.{ type: "named", specifiers: string[], from: string }— Remove specific named imports.
Edit | null — An edit to apply via rootNode.commitEdits(), or null if the import was not found.
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. - Works for ESM named imports, destructured
require(), and destructured dynamicimport(). - If
tois already imported in the same clause, the old specifier is dropped instead of duplicated.
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 }> }— Renamenametotoin the import fromfrom.
Edit | null — An edit to apply via rootNode.commitEdits(), or null if none of the named specifiers is found or specifiers is empty.
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: