Skip to main content

lib/npm/node/bin.ex

defmodule NPM.Node.Bin do
  @moduledoc """
  Parses and manages package bin field entries.

  The `bin` field in package.json maps command names to script files.
  Supports both string shorthand and map formats.
  """

  @doc """
  Extracts bin entries from package.json data.

  Returns a map of command name → script path.
  """
  @spec extract(map()) :: %{String.t() => String.t()}
  def extract(%{"bin" => bin, "name" => name}) when is_binary(bin) do
    %{name => bin}
  end

  def extract(%{"bin" => bin}) when is_map(bin), do: bin
  def extract(%{"directories" => %{"bin" => dir}}), do: %{"__dir__" => dir}
  def extract(_), do: %{}

  @doc """
  Lists all command names provided by a package.
  """
  @spec commands(map()) :: [String.t()]
  def commands(pkg_data) do
    pkg_data |> extract() |> Map.keys() |> Enum.sort()
  end

  @doc """
  Checks if a package provides any binaries.
  """
  @spec has_bin?(map()) :: boolean()
  def has_bin?(pkg_data), do: extract(pkg_data) != %{}

  @doc """
  Resolves the script path for a given command.
  """
  @spec resolve(String.t(), map()) :: String.t() | nil
  def resolve(command, pkg_data) do
    Map.get(extract(pkg_data), command)
  end

  @doc """
  Counts the number of binaries a package provides.
  """
  @spec count(map()) :: non_neg_integer()
  def count(pkg_data), do: pkg_data |> extract() |> map_size()

  @doc """
  Scans installed packages and collects all available binaries.
  """
  @spec all_bins(String.t()) :: %{String.t() => String.t()}
  def all_bins(node_modules_dir) do
    case File.ls(node_modules_dir) do
      {:ok, entries} ->
        entries
        |> Enum.reject(&String.starts_with?(&1, "."))
        |> Enum.flat_map(&read_pkg_bins(node_modules_dir, &1))
        |> Map.new()

      _ ->
        %{}
    end
  end

  defp read_pkg_bins(nm_dir, name) do
    pkg_path = Path.join([nm_dir, name, "package.json"])

    case File.read(pkg_path) do
      {:ok, content} ->
        data = NPM.JSON.decode!(content)
        bins = extract(data)
        Enum.map(bins, fn {cmd, script} -> {cmd, Path.join([nm_dir, name, script])} end)

      _ ->
        []
    end
  rescue
    _ -> []
  end
end