defmodule Mix.Tasks.Npm.Licenses do @shortdoc "Show licenses for installed packages" @moduledoc """ List the license of each installed package. mix npm.licenses mix npm.licenses --summary Reads the `license` field from each package's `package.json`. ## Options * `--summary` — show a count of each license type """ use Mix.Task @impl true def run(args) do Application.ensure_all_started(:req) {opts, _, _} = OptionParser.parse(args, strict: [summary: :boolean]) licenses = collect_licenses("node_modules") if opts[:summary] do print_summary(licenses) else print_all(licenses) end end @doc false def collect_licenses(node_modules_dir) do node_modules_dir |> list_packages() |> Enum.map(fn name -> pkg_json = Path.join([node_modules_dir, name, "package.json"]) license = read_license(pkg_json) {name, license} end) |> Enum.sort_by(&elem(&1, 0)) end defp print_all(licenses) do Enum.each(licenses, fn {name, license} -> Mix.shell().info("#{name}: #{license || "UNKNOWN"}") end) end defp print_summary(licenses) do licenses |> Enum.map(fn {_, license} -> license || "UNKNOWN" end) |> Enum.frequencies() |> Enum.sort_by(fn {_, count} -> -count end) |> Enum.each(fn {license, count} -> Mix.shell().info("#{license}: #{count}") end) end defp read_license(path) do case File.read(path) do {:ok, content} -> data = NPM.JSON.decode!(content) Map.get(data, "license") {:error, _} -> nil end end defp list_packages(dir) do case File.ls(dir) do {:ok, entries} -> entries |> Enum.reject(&String.starts_with?(&1, ".")) |> Enum.flat_map(&expand_entry(dir, &1)) {:error, _} -> [] end end defp expand_entry(dir, "@" <> _ = scope) do case File.ls(Path.join(dir, scope)) do {:ok, pkgs} -> Enum.map(pkgs, &"#{scope}/#{&1}") {:error, _} -> [] end end defp expand_entry(_dir, entry), do: [entry] end