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