Skip to main content

lib/npm/engines.ex

defmodule NPM.Engines do
  @moduledoc """
  Parses and analyzes the `engines` field from package.json.
  """

  @known_engines ~w(node npm yarn pnpm)

  @doc """
  Extracts engines from package.json data.
  """
  @spec extract(map()) :: map()
  def extract(%{"engines" => engines}) when is_map(engines), do: engines
  def extract(_), do: %{}

  @doc """
  Returns the node version constraint.
  """
  @spec node_range(map()) :: String.t() | nil
  def node_range(data), do: extract(data) |> Map.get("node")

  @doc """
  Returns the npm version constraint.
  """
  @spec npm_range(map()) :: String.t() | nil
  def npm_range(data), do: extract(data) |> Map.get("npm")

  @doc """
  Checks if a package specifies any engine constraints.
  """
  @spec has_engines?(map()) :: boolean()
  def has_engines?(data), do: extract(data) != %{}

  @doc """
  Returns the most restrictive node range across packages.
  """
  @spec strictest_node([map()]) :: String.t() | nil
  def strictest_node(packages) do
    packages
    |> Enum.map(&node_range/1)
    |> Enum.reject(&is_nil/1)
    |> case do
      [] -> nil
      ranges -> Enum.join(ranges, " ")
    end
  end

  @doc """
  Lists all unique engine names used across packages.
  """
  @spec used_engines([map()]) :: [String.t()]
  def used_engines(packages) do
    packages
    |> Enum.flat_map(&(extract(&1) |> Map.keys()))
    |> Enum.uniq()
    |> Enum.sort()
  end

  @doc """
  Returns unknown (non-standard) engine constraints.
  """
  @spec unknown_engines(map()) :: [String.t()]
  def unknown_engines(data) do
    data
    |> extract()
    |> Map.keys()
    |> Enum.reject(&(&1 in @known_engines))
    |> Enum.sort()
  end

  @doc """
  Summarizes engine constraints across all packages.
  """
  @spec summary([map()]) :: map()
  def summary(packages) do
    engines = used_engines(packages)
    with_engines = Enum.count(packages, &has_engines?/1)

    %{
      total_packages: length(packages),
      with_engines: with_engines,
      without_engines: length(packages) - with_engines,
      engines_used: engines,
      node_range: strictest_node(packages)
    }
  end
end