Skip to main content

lib/npm/bundle_deps.ex

defmodule NPM.BundleDeps do
  @moduledoc """
  Handles bundledDependencies in package.json.

  bundledDependencies are packages that are included in the tarball
  when publishing, rather than being fetched from the registry.
  """

  @doc """
  Extracts bundledDependencies from package.json data.

  Handles both `bundledDependencies` and `bundleDependencies` fields.
  """
  @spec extract(map()) :: [String.t()]
  def extract(%{"bundledDependencies" => deps}) when is_list(deps), do: deps
  def extract(%{"bundleDependencies" => deps}) when is_list(deps), do: deps
  def extract(%{"bundledDependencies" => true} = data), do: all_dep_names(data)
  def extract(%{"bundleDependencies" => true} = data), do: all_dep_names(data)
  def extract(_), do: []

  @doc """
  Checks if a package is bundled.
  """
  @spec bundled?(String.t(), map()) :: boolean()
  def bundled?(name, pkg_data) do
    name in extract(pkg_data)
  end

  @doc """
  Validates that all bundled deps are also declared as dependencies.
  """
  @spec validate(map()) :: {:ok, [String.t()]} | {:error, [String.t()]}
  def validate(pkg_data) do
    bundled = extract(pkg_data)
    declared = Map.keys(pkg_data["dependencies"] || %{})
    missing = Enum.reject(bundled, &(&1 in declared))

    if missing == [] do
      {:ok, bundled}
    else
      {:error, Enum.map(missing, &"#{&1} is bundled but not in dependencies")}
    end
  end

  @doc """
  Returns the count of bundled dependencies.
  """
  @spec count(map()) :: non_neg_integer()
  def count(pkg_data), do: pkg_data |> extract() |> length()

  defp all_dep_names(data) do
    data
    |> Map.get("dependencies", %{})
    |> Map.keys()
    |> Enum.sort()
  end
end