defmodule NPM.InstallStrategy do
@moduledoc """
Determines and configures package installation strategy.
npm supports hoisted (default), nested, and isolated install strategies
configured via `.npmrc` or `--install-strategy` flag.
"""
@strategies ~w(hoisted nested shallow linked)a
@doc """
Detects the install strategy from config.
"""
@spec detect(map()) :: atom()
def detect(config) when is_map(config) do
case config["install-strategy"] || config["install_strategy"] do
"nested" -> :nested
"shallow" -> :shallow
"linked" -> :linked
_ -> :hoisted
end
end
@doc """
Returns all supported strategies.
"""
@spec strategies :: [atom()]
def strategies, do: @strategies
@doc """
Checks if a strategy is valid.
"""
@spec valid?(atom()) :: boolean()
def valid?(strategy), do: strategy in @strategies
@doc """
Describes a strategy.
"""
@spec describe(atom()) :: String.t()
def describe(:hoisted), do: "Hoist all dependencies to top-level node_modules (default)"
def describe(:nested), do: "Install dependencies nested in package folders"
def describe(:shallow), do: "Only install direct dependencies at top level"
def describe(:linked), do: "Symlink packages instead of copying"
def describe(_), do: "Unknown strategy"
@doc """
Returns recommended strategy for a project configuration.
"""
@spec recommend(map()) :: atom()
def recommend(pkg_data) do
cond do
has_many_workspaces?(pkg_data) -> :hoisted
has_conflicting_versions?(pkg_data) -> :nested
true -> :hoisted
end
end
@doc """
Returns the node_modules structure depth for a strategy.
"""
@spec max_depth(atom()) :: non_neg_integer() | :infinity
def max_depth(:hoisted), do: 1
def max_depth(:nested), do: :infinity
def max_depth(:shallow), do: 1
def max_depth(:linked), do: 1
def max_depth(_), do: 1
defp has_many_workspaces?(%{"workspaces" => ws}) when is_list(ws), do: length(ws) > 3
defp has_many_workspaces?(_), do: false
defp has_conflicting_versions?(%{"dependencies" => deps, "devDependencies" => dev_deps})
when is_map(deps) and is_map(dev_deps) do
common = MapSet.intersection(MapSet.new(Map.keys(deps)), MapSet.new(Map.keys(dev_deps)))
MapSet.size(common) > 0
end
defp has_conflicting_versions?(_), do: false
end