Skip to main content

lib/npm/lockfile/stats.ex

defmodule NPM.Lockfile.Stats do
  @moduledoc """
  Computes statistics about the lockfile itself.
  """

  @doc """
  Computes lockfile metadata.
  """
  @spec compute(String.t()) :: {:ok, map()} | {:error, term()}
  def compute(path) do
    case File.stat(path) do
      {:ok, stat} ->
        {:ok,
         %{
           size: stat.size,
           modified: stat.mtime,
           size_human: format_size(stat.size)
         }}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc """
  Computes lockfile content stats.
  """
  @spec content_stats(map()) :: map()
  def content_stats(lockfile) do
    total = map_size(lockfile)
    with_integrity = Enum.count(lockfile, fn {_, e} -> has_field?(e, :integrity) end)
    with_deps = Enum.count(lockfile, fn {_, e} -> has_deps?(e) end)

    %{
      total_packages: total,
      with_integrity: with_integrity,
      with_deps: with_deps,
      leaf_packages: total - with_deps,
      integrity_pct: if(total > 0, do: Float.round(with_integrity / total * 100, 1), else: 0.0)
    }
  end

  @doc """
  Estimates disk size of node_modules from lockfile.
  """
  @spec estimated_size(map()) :: non_neg_integer()
  def estimated_size(lockfile) do
    map_size(lockfile) * 50_000
  end

  @doc """
  Formats size in human-readable form.
  """
  @spec format_size(non_neg_integer()) :: String.t()
  defdelegate format_size(bytes), to: NPM.FormatUtil

  defp has_field?(entry, field) do
    case Map.get(entry, field) || Map.get(entry, to_string(field)) do
      nil -> false
      "" -> false
      _ -> true
    end
  end

  defp has_deps?(%{dependencies: deps}) when is_map(deps) and map_size(deps) > 0, do: true
  defp has_deps?(%{"dependencies" => deps}) when is_map(deps) and map_size(deps) > 0, do: true
  defp has_deps?(_), do: false
end