defmodule Mix.Tasks.Npm.Info do @shortdoc "Show info about an npm package" @moduledoc """ Show information about an npm package from the registry. mix npm.info lodash mix npm.info express@4.21.2 Displays the package name, latest version, description, and dependencies. """ use Mix.Task @impl true def run([spec]) do Application.ensure_all_started(:req) {name, version} = parse_spec(spec) show_info(name, version) end def run(_) do Mix.shell().error("Usage: mix npm.info ") end defp parse_spec(spec) do case spec do "@" <> rest -> case String.split(rest, "@", parts: 2) do [scoped, version] -> {"@" <> scoped, version} [scoped] -> {"@" <> scoped, nil} end _ -> case String.split(spec, "@", parts: 2) do [name, version] -> {name, version} [name] -> {name, nil} end end end defp show_info(name, version) do case NPM.Registry.get_packument(name) do {:ok, packument} -> print_packument(packument, version) {:error, :not_found} -> Mix.shell().error("Package #{name} not found.") {:error, reason} -> Mix.shell().error("Failed to fetch package: #{inspect(reason)}") end end defp print_packument(packument, nil) do versions = Map.keys(packument.versions) latest = latest_stable(versions) version_count = length(versions) Mix.shell().info("#{packument.name}") Mix.shell().info(" latest: #{latest || "unknown"}") Mix.shell().info(" versions: #{version_count}") if latest do print_version_info(packument, latest) end end defp print_packument(packument, version) do if Map.has_key?(packument.versions, version) do Mix.shell().info("#{packument.name}@#{version}") print_version_info(packument, version) else Mix.shell().error("Version #{version} not found for #{packument.name}") Mix.shell().info( "Available: #{packument.versions |> Map.keys() |> Enum.take(-5) |> Enum.join(", ")}" ) end end defp print_version_info(packument, version) do info = Map.get(packument.versions, version) if info, do: print_deps(info.dependencies) end defp print_deps(deps) when map_size(deps) == 0 do Mix.shell().info(" dependencies: none") end defp print_deps(deps) do Mix.shell().info(" dependencies:") Enum.each(Enum.sort(deps), fn {dep_name, range} -> Mix.shell().info(" #{dep_name}: #{range}") end) end defp latest_stable(versions) do versions |> Enum.flat_map(&parse_stable/1) |> Enum.sort_by(&elem(&1, 0), Version) |> last_version_string() end defp parse_stable(v) do case Version.parse(v) do {:ok, %{pre: []} = ver} -> [{ver, v}] _ -> [] end end defp last_version_string([]), do: nil defp last_version_string(list), do: list |> List.last() |> elem(1) end