Skip to main content

lib/npm/alias.ex

defmodule NPM.Alias do
  @moduledoc """
  Handle npm package aliases.

  npm supports aliasing packages with the `npm:` prefix syntax:

      "my-react": "npm:react@^18.0.0"

  This allows installing a package under a different name, useful for
  running multiple versions of the same package side by side.
  """

  @doc """
  Parse an alias specifier.

  Returns `{:alias, package, range}` if the specifier uses `npm:` prefix,
  or `{:normal, range}` otherwise.

  ## Examples

      iex> NPM.Alias.parse("npm:react@^18.0.0")
      {:alias, "react", "^18.0.0"}

      iex> NPM.Alias.parse("npm:@scope/pkg@1.0.0")
      {:alias, "@scope/pkg", "1.0.0"}

      iex> NPM.Alias.parse("^1.0.0")
      {:normal, "^1.0.0"}
  """
  @spec parse(String.t()) :: {:alias, String.t(), String.t()} | {:normal, String.t()}
  def parse("npm:" <> rest) do
    case parse_aliased(rest) do
      {pkg, range} -> {:alias, pkg, range}
      :error -> {:normal, "npm:" <> rest}
    end
  end

  def parse(range), do: {:normal, range}

  @doc """
  Check if a dependency specifier is an alias.
  """
  @spec alias?(String.t()) :: boolean()
  def alias?("npm:" <> _), do: true
  def alias?(_), do: false

  @doc """
  Extract the real package name from an alias specifier.

  Returns the original name if not an alias.
  """
  @spec real_name(String.t(), String.t()) :: String.t()
  def real_name(alias_name, "npm:" <> rest) do
    case parse_aliased(rest) do
      {pkg, _range} -> pkg
      :error -> alias_name
    end
  end

  def real_name(name, _range), do: name

  defp parse_aliased("@" <> rest) do
    case String.split(rest, "@", parts: 2) do
      [scope_and_name, range] -> {"@" <> scope_and_name, range}
      _ -> :error
    end
  end

  defp parse_aliased(rest) do
    case String.split(rest, "@", parts: 2) do
      [name, range] when name != "" -> {name, range}
      _ -> :error
    end
  end
end