# `NPM.PackageResolver`
[🔗](https://github.com/elixir-volt/npm_ex/blob/v0.5.3/lib/npm/package_resolver.ex#L1)

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"

# `bare?`

```elixir
@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`

```elixir
@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?`

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

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

# `relative?`

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

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

# `relative_import_path`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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"]`)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
