defmodule NPM.Package.Spec do @moduledoc """ Parse npm package specifiers into structured data. npm supports multiple specifier formats: lodash # name only (latest) lodash@^4.0 # name with range @scope/pkg@1.0 # scoped with range npm:react@^18 # alias file:../local # file reference git+https://... # git reference https://...tgz # URL tarball """ @type t :: %{ raw: String.t(), name: String.t(), range: String.t() | nil, type: :registry | :alias | :file | :git | :url } @doc """ Parse a package specifier string. """ @spec parse(String.t()) :: t() def parse("npm:" <> _ = spec) do case NPM.Alias.parse(spec) do {:alias, name, range} -> %{raw: spec, name: name, range: range, type: :alias} {:normal, _} -> %{raw: spec, name: spec, range: nil, type: :registry} end end def parse("file:" <> _ = spec) do %{raw: spec, name: spec, range: nil, type: :file} end def parse("git+" <> _ = spec) do %{raw: spec, name: spec, range: nil, type: :git} end def parse("git://" <> _ = spec) do %{raw: spec, name: spec, range: nil, type: :git} end def parse("github:" <> _ = spec) do %{raw: spec, name: spec, range: nil, type: :git} end def parse("http://" <> _ = spec) do %{raw: spec, name: spec, range: nil, type: :url} end def parse("https://" <> _ = spec) do %{raw: spec, name: spec, range: nil, type: :url} end def parse("@" <> _ = spec) do case String.split(spec, "@", parts: 3) do ["", scope_name, range] -> %{raw: spec, name: "@#{scope_name}", range: range, type: :registry} _ -> %{raw: spec, name: spec, range: nil, type: :registry} end end def parse(spec) do case String.split(spec, "@", parts: 2) do [name, range] when name != "" -> %{raw: spec, name: name, range: range, type: :registry} _ -> %{raw: spec, name: spec, range: nil, type: :registry} end end @doc """ Check if a specifier targets the registry. """ @spec registry?(t()) :: boolean() def registry?(%{type: :registry}), do: true def registry?(_), do: false @doc """ Format a spec back to a string like `name@range`. """ @spec to_string(t()) :: String.t() def to_string(%{name: name, range: nil}), do: name def to_string(%{name: name, range: range}), do: "#{name}@#{range}" end