Skip to main content

lib/mix/tasks/npm.search.ex

defmodule Mix.Tasks.Npm.Search do
  @shortdoc "Search npm registry"

  @moduledoc """
  Search for packages on the npm registry.

      mix npm.search lodash
      mix npm.search react --limit 5

  Returns matching packages with descriptions.
  """

  use Mix.Task

  @impl true
  def run(args) do
    Application.ensure_all_started(:req)
    {opts, terms, _} = OptionParser.parse(args, strict: [limit: :integer])

    case terms do
      [] ->
        Mix.shell().error("Usage: mix npm.search <query> [--limit N]")

      _ ->
        query = Enum.join(terms, " ")
        limit = Keyword.get(opts, :limit, 10)
        search(query, limit)
    end
  end

  defp search(query, limit) do
    url = "#{NPM.Registry.registry_url()}/-/v1/search?text=#{URI.encode(query)}&size=#{limit}"

    case Req.get(url) do
      {:ok, %{status: 200, body: body}} ->
        print_results(body)

      {:ok, %{status: status}} ->
        Mix.shell().error("Search failed with status #{status}")

      {:error, reason} ->
        Mix.shell().error("Search failed: #{inspect(reason)}")
    end
  end

  defp print_results(%{"objects" => []}) do
    Mix.shell().info("No packages found.")
  end

  defp print_results(%{"objects" => objects}) do
    Enum.each(objects, fn %{"package" => pkg} ->
      name = Map.get(pkg, "name", "?")
      version = Map.get(pkg, "version", "?")
      description = Map.get(pkg, "description", "")

      Mix.shell().info(
        String.pad_trailing("#{name}@#{version}", 40) <>
          String.slice(description, 0, 60)
      )
    end)
  end

  defp print_results(_), do: Mix.shell().info("No results.")
end