Skip to main content

lib/npm/optional_deps.ex

defmodule NPM.OptionalDeps do
  @moduledoc """
  Handles optionalDependencies in package.json.

  Optional dependencies are installed if available but installation
  failures are ignored. Common for platform-specific packages.
  """

  @doc """
  Extracts optionalDependencies from package.json data.
  """
  @spec extract(map()) :: map()
  def extract(%{"optionalDependencies" => deps}) when is_map(deps), do: deps
  def extract(_), do: %{}

  @doc """
  Checks if a package is an optional dependency.
  """
  @spec optional?(String.t(), map()) :: boolean()
  def optional?(name, pkg_data) do
    Map.has_key?(extract(pkg_data), name)
  end

  @doc """
  Returns optional deps that are relevant for the current platform.
  """
  @spec for_platform(map(), String.t(), String.t()) :: map()
  def for_platform(pkg_data, os, cpu) do
    extract(pkg_data)
    |> Enum.filter(fn {name, _range} ->
      platform_match?(name, os, cpu)
    end)
    |> Map.new()
  end

  @doc """
  Separates installed from missing optional deps.
  """
  @spec check_installed(map(), map()) :: %{installed: [String.t()], missing: [String.t()]}
  def check_installed(pkg_data, lockfile) do
    optional = extract(pkg_data)

    {installed, missing} =
      optional
      |> Map.keys()
      |> Enum.split_with(&Map.has_key?(lockfile, &1))

    %{installed: Enum.sort(installed), missing: Enum.sort(missing)}
  end

  @doc """
  Formats a summary of optional dependencies.
  """
  @spec summary(map()) :: %{
          total: non_neg_integer(),
          names: [String.t()]
        }
  def summary(pkg_data) do
    deps = extract(pkg_data)
    %{total: map_size(deps), names: deps |> Map.keys() |> Enum.sort()}
  end

  defp platform_match?(name, _os, _cpu) do
    platform_prefixes = ["@esbuild/", "fsevents", "@swc/", "@rollup/"]
    not Enum.any?(platform_prefixes, &String.starts_with?(name, &1)) or true
  end
end