NPM.PackageResolver (NPM v0.5.3)

Copy Markdown View Source

Resolve bare and relative import specifiers to files on disk.

Implements the Node.js module resolution algorithm for use by bundlers, TypeScript tooling, and anything that needs to turn an import specifier into an absolute file path.

This is the npm domain half of resolution — specifier parsing, node_modules traversal, package.json entry points, and file extension probing. AST rewriting stays in the consumer (e.g. OXC).

Examples

iex> NPM.PackageResolver.split_specifier("@babel/core/lib/parse")
{"@babel/core", "./lib/parse"}

iex> NPM.PackageResolver.relative?("./utils")
true

iex> NPM.PackageResolver.node_builtin?("node:fs")
true

iex> NPM.PackageResolver.find_node_modules("/app/src")
"/app/node_modules"

Summary

Functions

Returns true for bare (package) specifiers — anything that is neither relative nor a Node.js built-in.

Walk up from dir to find the nearest node_modules directory.

Returns true for Node.js built-in modules (fs, node:path, etc.).

Returns true for relative specifiers (./, ../, or /).

Compute a relative import path from importer to target within project_root.

Resolve a full import specifier to an absolute file path.

Resolve the entry point of a package directory.

Split a bare specifier into {package_name, subpath | nil}.

Resolve a file path by probing extensions and index.* files.

Functions

bare?(specifier)

@spec bare?(String.t()) :: boolean()

Returns true for bare (package) specifiers — anything that is neither relative nor a Node.js built-in.

find_node_modules(dir)

@spec find_node_modules(String.t()) :: String.t() | nil

Walk up from dir to find the nearest node_modules directory.

Returns the absolute path or nil if none is found before the filesystem root.

node_builtin?(name)

@spec node_builtin?(String.t()) :: boolean()

Returns true for Node.js built-in modules (fs, node:path, etc.).

relative?(arg1)

@spec relative?(String.t()) :: boolean()

Returns true for relative specifiers (./, ../, or /).

relative_import_path(importer, target, project_root)

@spec relative_import_path(String.t(), String.t(), String.t()) :: String.t()

Compute a relative import path from importer to target within project_root.

Both paths must be absolute. Returns a POSIX-style relative path with a guaranteed ./ or ../ prefix, suitable for use as an import specifier.

Examples

iex> NPM.PackageResolver.relative_import_path(
...>   "/app/src/pages/home.js",
...>   "/app/src/utils/format.js",
...>   "/app"
...> )
"../utils/format.js"

iex> NPM.PackageResolver.relative_import_path(
...>   "/app/src/index.js",
...>   "/app/src/app.js",
...>   "/app"
...> )
"./app.js"

resolve(specifier, from_dir, opts \\ [])

@spec resolve(String.t(), String.t(), keyword()) ::
  {:ok, String.t()} | {:builtin, String.t()} | :error

Resolve a full import specifier to an absolute file path.

Handles bare, relative, and built-in specifiers:

  • Relative (./foo) — resolved against from_dir with extension probing
  • Built-in (node:fs) — returns {:builtin, name}
  • Bare (lodash/fp) — locates node_modules, then resolves the entry point

Options

  • :conditions — condition names for the exports field
  • :extensions — extensions for file probing

resolve_entry(package_dir, opts \\ [])

@spec resolve_entry(
  String.t(),
  keyword()
) :: {:ok, String.t()} | :error

Resolve the entry point of a package directory.

Reads package.json and checks (in order):

  1. exports field (via NPM.Exports) with the given subpath and conditions
  2. browser field (when "browser" is in conditions and value is a string)
  3. module field
  4. main field
  5. ./index.js fallback

Options

  • :subpath — export subpath to resolve (default: ".")
  • :conditions — condition names for the exports field (default: ["import", "default"])
  • :extensions — extensions for file probing (default: [".js", ".mjs", ".cjs", ".json"])

split_specifier(specifier)

@spec split_specifier(String.t()) :: {String.t(), String.t() | nil}

Split a bare specifier into {package_name, subpath | nil}.

Handles scoped packages correctly:

"lodash"                 {"lodash", nil}
"lodash/fp"              {"lodash", "./fp"}
"@babel/core"            {"@babel/core", nil}
"@babel/core/lib/parse"  {"@babel/core", "./lib/parse"}

try_resolve(base, opts \\ [])

@spec try_resolve(
  String.t(),
  keyword()
) :: {:ok, String.t()} | :error

Resolve a file path by probing extensions and index.* files.

Given a base path (without extension), tries each extension in order, then tries base/index.* for directory imports.

Options

  • :extensions — list of extensions to probe (default: [".js", ".mjs", ".cjs", ".json"])