Skip to main content

lib/npm/config/npmrc.ex

defmodule NPM.Config.Npmrc do
  @moduledoc """
  Parses and analyzes .npmrc configuration files.

  Supports project, user, and global .npmrc locations.
  """

  @doc """
  Finds all .npmrc files for a project.
  """
  @spec locate(String.t()) :: [String.t()]
  def locate(project_dir \\ ".") do
    candidates = [
      Path.join(project_dir, ".npmrc"),
      Path.expand("~/.npmrc"),
      "/etc/npmrc"
    ]

    Enum.filter(candidates, &File.exists?/1)
  end

  @doc """
  Parses an .npmrc file into a map.
  """
  @spec parse(String.t()) :: map()
  def parse(content) when is_binary(content) do
    content
    |> String.split("\n")
    |> Enum.reject(&blank_or_comment?/1)
    |> Enum.flat_map(&parse_line/1)
    |> Map.new()
  end

  @doc """
  Reads and parses an .npmrc file from disk.
  """
  @spec read(String.t()) :: {:ok, map()} | {:error, :not_found}
  def read(path) do
    case File.read(path) do
      {:ok, content} -> {:ok, parse(content)}
      _ -> {:error, :not_found}
    end
  end

  @doc """
  Merges multiple .npmrc configs (later overrides earlier).
  """
  @spec merge([map()]) :: map()
  def merge(configs), do: Enum.reduce(configs, %{}, &Map.merge(&2, &1))

  @doc """
  Checks if auth token is configured.
  """
  @spec has_auth?(map()) :: boolean()
  def has_auth?(config) do
    Enum.any?(config, fn {key, _} ->
      String.contains?(key, "_authToken") or key == "_auth"
    end)
  end

  @doc """
  Extracts scoped registry configurations.
  """
  @spec scoped_registries(map()) :: [{String.t(), String.t()}]
  def scoped_registries(config) do
    config
    |> Enum.filter(fn {key, _} -> String.match?(key, ~r/^@.+:registry$/) end)
    |> Enum.map(fn {key, url} ->
      scope = key |> String.replace(":registry", "") |> String.trim_leading("@")
      {scope, url}
    end)
    |> Enum.sort_by(&elem(&1, 0))
  end

  @doc """
  Formats .npmrc config for display.
  """
  @spec format(map()) :: String.t()
  def format(config) when map_size(config) == 0, do: "Empty .npmrc"

  def format(config) do
    config
    |> Enum.sort_by(&elem(&1, 0))
    |> Enum.map_join("\n", fn {k, v} ->
      if String.contains?(k, "Token") or k == "_auth",
        do: "#{k} = [REDACTED]",
        else: "#{k} = #{v}"
    end)
  end

  defp blank_or_comment?(line) do
    trimmed = String.trim(line)
    trimmed == "" or String.starts_with?(trimmed, "#") or String.starts_with?(trimmed, ";")
  end

  defp parse_line(line) do
    case String.split(String.trim(line), "=", parts: 2) do
      [key, value] -> [{String.trim(key), String.trim(value)}]
      _ -> []
    end
  end
end