Skip to main content

lib/npm/file_size.ex

defmodule NPM.FileSize do
  @moduledoc """
  Analyzes file sizes within installed packages.

  Identifies large files, breaks down by extension, and helps
  optimize node_modules size.
  """

  @doc """
  Analyzes files in a package directory.
  """
  @spec analyze(String.t()) :: [%{path: String.t(), size: non_neg_integer()}]
  def analyze(package_dir) do
    case list_files(package_dir) do
      {:ok, files} ->
        Enum.map(files, fn file ->
          %{path: file, size: file_size(Path.join(package_dir, file))}
        end)
        |> Enum.sort_by(& &1.size, :desc)

      _ ->
        []
    end
  end

  @doc """
  Returns the N largest files in a package.
  """
  @spec largest(String.t(), non_neg_integer()) :: [%{path: String.t(), size: non_neg_integer()}]
  def largest(package_dir, n \\ 10) do
    analyze(package_dir) |> Enum.take(n)
  end

  @doc """
  Groups file sizes by extension.
  """
  @spec by_extension(String.t()) :: %{String.t() => non_neg_integer()}
  def by_extension(package_dir) do
    analyze(package_dir)
    |> Enum.group_by(fn %{path: path} ->
      case Path.extname(path) do
        "" -> "(none)"
        ext -> ext
      end
    end)
    |> Map.new(fn {ext, files} ->
      {ext, files |> Enum.map(& &1.size) |> Enum.sum()}
    end)
  end

  @doc """
  Returns total size of all files in a package.
  """
  @spec total(String.t()) :: non_neg_integer()
  def total(package_dir) do
    analyze(package_dir) |> Enum.map(& &1.size) |> Enum.sum()
  end

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

  defp list_files(dir) do
    if File.dir?(dir) do
      files =
        dir
        |> Path.join("**/*")
        |> Path.wildcard()
        |> Enum.filter(&File.regular?/1)
        |> Enum.map(&Path.relative_to(&1, dir))

      {:ok, files}
    else
      {:error, :not_dir}
    end
  end

  defp file_size(path) do
    case File.stat(path) do
      {:ok, %{size: size}} -> size
      _ -> 0
    end
  end
end