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