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