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