defmodule NPM.Dependency.Outdated.Report do
@moduledoc """
Generates formatted reports for outdated npm packages.
"""
@doc """
Formats outdated packages as a table similar to `npm outdated`.
"""
@spec format_table([map()]) :: String.t()
def format_table([]), do: "All packages are up to date."
def format_table(packages) do
header = %{
name: "Package",
current: "Current",
wanted: "Wanted",
latest: "Latest",
type: "Type"
}
rows = [header | packages]
widths = %{
name: rows |> Enum.map(&String.length(to_string(&1.name))) |> Enum.max(),
current: rows |> Enum.map(&String.length(to_string(&1.current))) |> Enum.max(),
wanted: rows |> Enum.map(&String.length(to_string(Map.get(&1, :wanted, "")))) |> Enum.max(),
latest: rows |> Enum.map(&String.length(to_string(&1.latest))) |> Enum.max()
}
[format_row(header, widths) | Enum.map(packages, &format_row(&1, widths))]
|> Enum.join("\n")
end
@doc """
Categorizes a list of outdated packages by severity.
"""
@spec categorize([map()]) :: map()
def categorize(packages) do
%{
major: Enum.filter(packages, &(&1.type == :major)),
minor: Enum.filter(packages, &(&1.type == :minor)),
patch: Enum.filter(packages, &(&1.type == :patch))
}
end
@doc """
Generates a summary line.
"""
@spec summary([map()]) :: String.t()
def summary([]), do: "All packages are up to date."
def summary(packages) do
cat = categorize(packages)
parts = []
parts = if cat.major != [], do: ["#{length(cat.major)} major" | parts], else: parts
parts = if cat.minor != [], do: ["#{length(cat.minor)} minor" | parts], else: parts
parts = if cat.patch != [], do: ["#{length(cat.patch)} patch" | parts], else: parts
"#{length(packages)} outdated: #{parts |> Enum.reverse() |> Enum.join(", ")}"
end
@doc """
Returns security-relevant outdated packages (major versions behind).
"""
@spec security_risk([map()]) :: [map()]
def security_risk(packages) do
Enum.filter(packages, &(&1.type == :major))
end
defp format_row(row, widths) do
name = String.pad_trailing(to_string(row.name), widths.name)
current = String.pad_trailing(to_string(row.current), widths.current)
wanted = String.pad_trailing(to_string(Map.get(row, :wanted, "")), widths.wanted)
latest = String.pad_trailing(to_string(row.latest), widths.latest)
"#{name} #{current} #{wanted} #{latest}"
end
end