Skip to main content

lib/mix/tasks/npm.stats.ex

defmodule Mix.Tasks.Npm.Stats do
  @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, project} <- NPM.Workspace.read_all(),
         {:ok, all_deps} <- NPM.Workspace.install_dependencies(project),
         {:ok, lockfile} when lockfile != %{} <- NPM.Lockfile.read() do
      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