Current section
Files
Jump to
Current section
Files
lib/npm/node/exec.ex
defmodule NPM.Node.Exec do @moduledoc """ npx-style execution — resolve and run binaries from installed packages. Looks up binaries in `node_modules/.bin/` or resolves from package bin fields to find the right executable. """ @doc """ Finds the path to a binary command in node_modules/.bin/. """ @spec which(String.t(), String.t()) :: {:ok, String.t()} | {:error, :not_found} def which(command, node_modules_dir \\ "node_modules") do bin_dir = Path.join(node_modules_dir, ".bin") bin_path = Path.join(bin_dir, command) if File.exists?(bin_path) do {:ok, bin_path} else find_in_packages(command, node_modules_dir) end end @doc """ Lists all available binaries from installed packages. """ @spec available(String.t()) :: [String.t()] def available(node_modules_dir \\ "node_modules") do bin_dir = Path.join(node_modules_dir, ".bin") case File.ls(bin_dir) do {:ok, entries} -> Enum.sort(entries) _ -> list_package_bins(node_modules_dir) end end @doc """ Checks if a command is available in node_modules. """ @spec available?(String.t(), String.t()) :: boolean() def available?(command, node_modules_dir \\ "node_modules") do case which(command, node_modules_dir) do {:ok, _} -> true _ -> false end end @doc """ Resolves a package name from a command by checking bin fields. """ @spec package_for(String.t(), String.t()) :: {:ok, String.t()} | {:error, :not_found} def package_for(command, node_modules_dir \\ "node_modules") do case File.ls(node_modules_dir) do {:ok, entries} -> entries |> Enum.find_value(fn entry -> find_command_in_package(node_modules_dir, entry, command) end) |> case do nil -> {:error, :not_found} name -> {:ok, name} end _ -> {:error, :not_found} end end @doc """ Builds the execution environment with node_modules/.bin in PATH. """ @spec env(String.t()) :: [{String.t(), String.t()}] def env(node_modules_dir \\ "node_modules") do bin_dir = Path.expand(Path.join(node_modules_dir, ".bin")) node_modules_dir = Path.expand(node_modules_dir) current_path = System.get_env("PATH") || "" node_path = [node_modules_dir, System.get_env("NODE_PATH")] |> Enum.reject(&is_nil/1) |> Enum.join(":") [ {"PATH", "#{bin_dir}:#{current_path}"}, {"NODE_PATH", node_path} ] end defp find_in_packages(command, node_modules_dir) do pkg_path = Path.join([node_modules_dir, command, "package.json"]) case File.read(pkg_path) do {:ok, content} -> data = NPM.JSON.decode!(content) resolve_bin(data, command, node_modules_dir) _ -> {:error, :not_found} end rescue # JSON decode, missing keys, or unexpected package.json shape _ -> {:error, :not_found} end defp resolve_bin(%{"bin" => bin}, _command, nm_dir) when is_binary(bin) do {:ok, Path.join(nm_dir, bin)} end defp resolve_bin(%{"bin" => bin, "name" => name}, command, nm_dir) when is_map(bin) do case Map.get(bin, command) || Map.get(bin, name) do nil -> {:error, :not_found} path -> {:ok, Path.join([nm_dir, name, path])} end end defp resolve_bin(_, _, _), do: {:error, :not_found} defp find_command_in_package(nm_dir, entry, command) do if String.starts_with?(entry, ".") or String.starts_with?(entry, "@") do nil else check_package_bin(nm_dir, entry, command) end end defp check_package_bin(nm_dir, pkg_name, command) do pkg_path = Path.join([nm_dir, pkg_name, "package.json"]) case File.read(pkg_path) do {:ok, content} -> data = NPM.JSON.decode!(content) if has_command?(data, command), do: pkg_name, else: nil _ -> nil end rescue _ -> nil end defp has_command?(%{"bin" => bin}, command) when is_map(bin), do: Map.has_key?(bin, command) defp has_command?(%{"bin" => _, "name" => name}, command), do: name == command defp has_command?(_, _), do: false defp list_package_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_package_bins(node_modules_dir, &1)) |> Enum.sort() _ -> [] end end defp read_package_bins(nm_dir, pkg_name) do pkg_path = Path.join([nm_dir, pkg_name, "package.json"]) case File.read(pkg_path) do {:ok, content} -> data = NPM.JSON.decode!(content) extract_bin_names(data) _ -> [] end rescue _ -> [] end defp extract_bin_names(%{"bin" => bin}) when is_map(bin), do: Map.keys(bin) defp extract_bin_names(%{"bin" => _, "name" => name}), do: [name] defp extract_bin_names(_), do: []end