defmodule NPM.Dependency.Sort do alias NPM.Dependency.Graph @moduledoc """ Topological sorting of packages for correct install/build order. """ @doc """ Topologically sorts packages so dependencies come before dependents. """ @spec sort(map()) :: {:ok, [String.t()]} | {:error, :cycle} def sort(adj) do in_degree = compute_in_degree(adj) queue = in_degree |> Enum.filter(fn {_, d} -> d == 0 end) |> Enum.map(&elem(&1, 0)) |> Enum.sort() topo_sort(adj, in_degree, queue, []) end @doc """ Returns install order — topological sort where leaves come first. """ @spec install_order(map()) :: [String.t()] def install_order(adj) do {:ok, order} = sort(adj) Enum.reverse(order) end @doc """ Returns build order levels — packages that can be built in parallel. """ @spec parallel_levels(map()) :: [[String.t()]] def parallel_levels(adj) do rev = Graph.reverse(adj) in_degree = compute_in_degree(rev) build_levels(rev, in_degree, []) end @doc """ Counts the number of levels (maximum parallelism depth). """ @spec level_count(map()) :: non_neg_integer() def level_count(adj), do: parallel_levels(adj) |> length() defp compute_in_degree(adj) do base = Map.new(Map.keys(adj), &{&1, 0}) Enum.reduce(adj, base, fn {_, deps}, acc -> Enum.reduce(deps, acc, fn dep, inner -> Map.update(inner, dep, 1, &(&1 + 1)) end) end) end defp topo_sort(_adj, _in_degree, [], result) do {:ok, Enum.reverse(result)} end defp topo_sort(adj, in_degree, [node | rest], result) do deps = Map.get(adj, node, []) new_in_degree = Enum.reduce(deps, in_degree, fn dep, acc -> Map.update!(acc, dep, &(&1 - 1)) end) new_ready = deps |> Enum.filter(fn dep -> Map.get(new_in_degree, dep) == 0 end) |> Enum.sort() topo_sort(adj, new_in_degree, Enum.sort(rest ++ new_ready), [node | result]) end defp build_levels(adj, in_degree, levels) do ready = in_degree |> Enum.filter(fn {_, d} -> d == 0 end) |> Enum.map(&elem(&1, 0)) |> Enum.sort() if ready == [] do Enum.reverse(levels) else new_in_degree = ready |> Enum.reduce(Map.drop(in_degree, ready), &decrement_deps(adj, &1, &2)) build_levels(adj, new_in_degree, [ready | levels]) end end defp decrement_deps(adj, node, in_degree) do adj |> Map.get(node, []) |> Enum.reduce(in_degree, fn dep, acc -> Map.update(acc, dep, 0, &max(&1 - 1, 0)) end) end end