Skip to main content

lib/npm/registry/scope.ex

defmodule NPM.Registry.Scope do
  @moduledoc """
  Resolve the registry URL for scoped packages.

  npm supports per-scope registry configuration in `.npmrc`:

      @mycompany:registry=https://npm.mycompany.com/

  This allows private packages under `@mycompany/*` to resolve
  from a custom registry while other packages use the default.
  """

  @doc """
  Get the registry URL for a package name.

  Checks `.npmrc` for scope-specific registry configuration,
  falls back to the default registry.
  """
  @spec registry_for(String.t()) :: String.t()
  def registry_for("@" <> _ = name) do
    scope = name |> String.split("/") |> hd()
    scope_config_key = "#{scope}:registry"

    case read_config(scope_config_key) do
      nil -> NPM.Registry.registry_url()
      url -> String.trim_trailing(url, "/")
    end
  end

  def registry_for(_name), do: NPM.Registry.registry_url()

  @doc """
  Get all configured scope registries from `.npmrc`.

  Returns a map of `scope => registry_url`.
  """
  @spec all_scopes :: %{String.t() => String.t()}
  def all_scopes do
    case File.read(".npmrc") do
      {:ok, content} ->
        content
        |> NPM.Config.parse_npmrc()
        |> Enum.filter(fn {key, _} -> String.match?(key, ~r/^@[^:]+:registry$/) end)
        |> Map.new(fn {key, url} ->
          scope = key |> String.split(":") |> hd()
          {scope, String.trim_trailing(url, "/")}
        end)

      {:error, _} ->
        %{}
    end
  end

  @doc """
  Check if a package name is scoped.
  """
  @spec scoped?(String.t()) :: boolean()
  def scoped?("@" <> _), do: true
  def scoped?(_), do: false

  @doc """
  Extract the scope from a scoped package name.

  Returns `nil` for unscoped packages.
  """
  @spec scope(String.t()) :: String.t() | nil
  def scope("@" <> _ = name) do
    name |> String.split("/") |> hd()
  end

  def scope(_), do: nil

  defp read_config(key) do
    case File.read(".npmrc") do
      {:ok, content} ->
        content |> NPM.Config.parse_npmrc() |> Map.get(key)

      {:error, _} ->
        nil
    end
  end
end