defmodule Mix.Tasks.Npm.Fund do
@shortdoc "Show package funding info"
@moduledoc """
Show funding information for installed packages.
mix npm.fund
Queries the registry for funding URLs of all installed packages.
"""
use Mix.Task
@impl true
def run([]) do
Application.ensure_all_started(:req)
case NPM.Lockfile.read() do
{:ok, lockfile} when lockfile == %{} ->
Mix.shell().info("No packages installed.")
{:ok, lockfile} ->
show_funding(lockfile)
{:error, reason} ->
Mix.shell().error("Failed to read lockfile: #{inspect(reason)}")
end
end
def run(_) do
Mix.shell().error("Usage: mix npm.fund")
end
defp show_funding(lockfile) do
Mix.shell().info("Checking funding for #{map_size(lockfile)} packages...")
lockfile
|> Map.keys()
|> Enum.sort()
|> Task.async_stream(&fetch_funding/1, max_concurrency: 8, timeout: 30_000)
|> Enum.each(fn
{:ok, {name, urls}} when urls != [] ->
Mix.shell().info(" #{name}: #{Enum.join(urls, ", ")}")
_ ->
:ok
end)
end
defp fetch_funding(name) do
url = "#{NPM.Registry.registry_url()}/#{String.replace(name, "/", "%2f")}"
case Req.get(url) do
{:ok, %{status: 200, body: body}} ->
{name, extract_funding(body)}
_ ->
{name, []}
end
end
defp extract_funding(%{"funding" => funding}) when is_binary(funding), do: [funding]
defp extract_funding(%{"funding" => %{"url" => url}}), do: [url]
defp extract_funding(%{"funding" => funding_list}) when is_list(funding_list) do
Enum.flat_map(funding_list, fn
%{"url" => url} -> [url]
url when is_binary(url) -> [url]
_ -> []
end)
end
defp extract_funding(_), do: []
end