defmodule NPM.Package.Fund do
@moduledoc """
Discovers and aggregates funding information from installed packages.
Reads the `funding` field from package manifests to help users
support open source maintainers.
"""
@type funding_info :: %{
package: String.t(),
version: String.t(),
type: String.t() | nil,
url: String.t()
}
@doc """
Extracts funding info from a package.json data map.
The `funding` field can be a string (URL), a map with `type` and `url`,
or a list of such entries.
"""
@spec extract(map()) :: [funding_info()]
def extract(%{"name" => name, "version" => version} = data) do
case data["funding"] do
nil -> []
url when is_binary(url) -> [%{package: name, version: version, type: nil, url: url}]
%{"url" => url} = f -> [%{package: name, version: version, type: f["type"], url: url}]
list when is_list(list) -> Enum.flat_map(list, &parse_funding_entry(name, version, &1))
_ -> []
end
end
def extract(_), do: []
@doc """
Collects funding info from all packages in a node_modules directory.
"""
@spec collect(String.t()) :: [funding_info()]
def collect(node_modules_dir) do
case File.ls(node_modules_dir) do
{:ok, entries} ->
entries
|> Enum.flat_map(&read_package_funding(node_modules_dir, &1))
|> Enum.sort_by(& &1.package)
_ ->
[]
end
end
@doc """
Groups funding entries by funding URL.
"""
@spec group_by_url([funding_info()]) :: %{String.t() => [funding_info()]}
def group_by_url(entries) do
Enum.group_by(entries, & &1.url)
end
@doc """
Returns a summary of funding information.
"""
@spec summary([funding_info()]) :: %{
packages_with_funding: non_neg_integer(),
unique_urls: non_neg_integer(),
types: [String.t()]
}
def summary(entries) do
%{
packages_with_funding: entries |> Enum.map(& &1.package) |> Enum.uniq() |> length(),
unique_urls: entries |> Enum.map(& &1.url) |> Enum.uniq() |> length(),
types:
entries |> Enum.map(& &1.type) |> Enum.reject(&is_nil/1) |> Enum.uniq() |> Enum.sort()
}
end
defp parse_funding_entry(name, version, url) when is_binary(url) do
[%{package: name, version: version, type: nil, url: url}]
end
defp parse_funding_entry(name, version, %{"url" => url} = f) do
[%{package: name, version: version, type: f["type"], url: url}]
end
defp parse_funding_entry(_, _, _), do: []
defp read_package_funding(nm_dir, entry) do
pkg_json = Path.join([nm_dir, entry, "package.json"])
case File.read(pkg_json) do
{:ok, content} ->
data = NPM.JSON.decode!(content)
extract(data)
_ ->
[]
end
rescue
_ -> []
end
end