defmodule NPM.Compat do
@moduledoc """
Checks compatibility of packages with a target Node.js version.
Analyzes the `engines.node` field across all packages to determine
if they are compatible with a specific Node.js version.
"""
@doc """
Checks if a package's engines field is compatible with a node version.
"""
@spec compatible?(map(), String.t()) :: boolean()
def compatible?(data, node_version) do
case NPM.Engines.node_range(data) do
nil -> true
range -> NPMSemver.matches?(node_version, range)
end
end
@doc """
Finds incompatible packages for a target node version.
"""
@spec incompatible([{String.t(), map()}], String.t()) :: [{String.t(), String.t()}]
def incompatible(packages, node_version) do
packages
|> Enum.flat_map(&check_node_compat(&1, node_version))
|> Enum.sort_by(&elem(&1, 0))
end
defp check_node_compat({name, data}, node_version) do
case NPM.Engines.node_range(data) do
nil -> []
range -> if NPMSemver.matches?(node_version, range), do: [], else: [{name, range}]
end
end
@doc """
Returns compatibility summary.
"""
@spec summary([{String.t(), map()}], String.t()) :: map()
def summary(packages, node_version) do
with_engines = Enum.count(packages, fn {_, d} -> NPM.Engines.has_engines?(d) end)
incompat = incompatible(packages, node_version)
%{
target: node_version,
total: length(packages),
with_engines: with_engines,
compatible: with_engines - length(incompat),
incompatible: length(incompat),
incompatible_packages: incompat
}
end
@doc """
Formats compatibility report.
"""
@spec format_report(map()) :: String.t()
def format_report(%{incompatible: 0} = summary) do
"All #{summary.with_engines} packages with engine constraints are compatible with Node.js #{summary.target}."
end
def format_report(summary) do
header = "#{summary.incompatible} packages incompatible with Node.js #{summary.target}:\n"
details =
Enum.map_join(summary.incompatible_packages, "\n", fn {name, range} ->
" #{name} requires node #{range}"
end)
header <> details
end
end