Skip to main content

lib/mix/tasks/npm.info.ex

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 <package[@version]>")
  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