Skip to main content

lib/npm/config/npmrc/merge.ex

defmodule NPM.Config.Npmrc.Merge do
  alias NPM.Config.Npmrc

  @moduledoc """
  Multi-layer .npmrc resolution (project → user → global).

  Merges configuration from multiple .npmrc files with proper precedence.
  """

  @doc """
  Resolves merged configuration from all .npmrc layers.

  Project-level overrides user-level, which overrides global.
  """
  @spec resolve(String.t()) :: map()
  def resolve(project_dir) do
    layers(project_dir)
    |> Enum.map(&read_layer/1)
    |> Enum.reduce(%{}, &Map.merge(&2, &1))
  end

  @doc """
  Returns the ordered list of .npmrc file paths (global → user → project).
  """
  @spec layers(String.t()) :: [String.t()]
  def layers(project_dir) do
    [
      global_path(),
      user_path(),
      Path.join(project_dir, ".npmrc")
    ]
  end

  @doc """
  Reads and parses a single .npmrc file.
  """
  @spec read_layer(String.t()) :: map()
  def read_layer(path) do
    case File.read(path) do
      {:ok, content} -> Npmrc.parse(content)
      _ -> %{}
    end
  end

  @doc """
  Returns which layers exist and contribute config.
  """
  @spec active_layers(String.t()) :: [%{path: String.t(), keys: [String.t()]}]
  def active_layers(project_dir) do
    layers(project_dir)
    |> Enum.flat_map(fn path ->
      config = read_layer(path)

      if map_size(config) > 0 do
        [%{path: path, keys: Map.keys(config) |> Enum.sort()}]
      else
        []
      end
    end)
  end

  @doc """
  Traces where a specific config key comes from.
  """
  @spec trace(String.t(), String.t()) :: {:ok, String.t(), String.t()} | :not_found
  def trace(project_dir, key) do
    layers(project_dir)
    |> Enum.reverse()
    |> Enum.find_value(fn path ->
      config = read_layer(path)
      if Map.has_key?(config, key), do: {:ok, path, config[key]}
    end) || :not_found
  end

  defp global_path, do: "/etc/npmrc"

  defp user_path do
    Path.join(System.user_home() || "/tmp", ".npmrc")
  end
end