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