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