defmodule Mix.Tasks.Npm.Stats do alias NPM.Package.JSON @shortdoc "Show dependency statistics" @moduledoc """ Display statistics about the npm dependency graph. mix npm.stats Shows: total packages, direct deps, transitive deps, max depth, leaf count, and fan-in/fan-out leaders. """ use Mix.Task alias NPM.Dependency.Graph @impl true def run([]) do Application.ensure_all_started(:req) with {:ok, %{dependencies: deps, dev_dependencies: dev_deps}} <- JSON.read_all(), {:ok, lockfile} when lockfile != %{} <- NPM.Lockfile.read() do all_deps = Map.merge(deps, dev_deps) adj = Graph.adjacency_list(lockfile) fin = Graph.fan_in(adj) fout = Graph.fan_out(adj) Mix.shell().info("NPM dependency statistics:") Mix.shell().info(" Total packages: #{map_size(lockfile)}") Mix.shell().info(" Direct deps: #{map_size(all_deps)}") Mix.shell().info(" Transitive deps: #{map_size(lockfile) - map_size(all_deps)}") Mix.shell().info(" Leaf packages: #{length(Graph.leaves(adj))}") print_top("Most depended on", fin) print_top("Most dependencies", fout) else {:ok, lockfile} when lockfile == %{} -> Mix.shell().info("No packages installed.") error -> Mix.shell().error("Error: #{inspect(error)}") end end def run(_) do Mix.shell().error("Usage: mix npm.stats") end defp print_top(label, map) do top = map |> Enum.sort_by(fn {_, v} -> -v end) |> Enum.take(5) |> Enum.filter(fn {_, v} -> v > 0 end) if top != [] do Mix.shell().info("\n #{label}:") Enum.each(top, fn {name, count} -> Mix.shell().info(" #{name}: #{count}") end) end end end