defmodule NPM.Install.ScriptRunner do
alias NPM.Install.Lifecycle
@moduledoc """
Analyzes and validates npm scripts from package.json.
"""
@common_scripts ~w(start test build lint dev serve clean format typecheck)
@doc """
Extracts scripts from package.json.
"""
@spec extract(map()) :: map()
def extract(%{"scripts" => scripts}) when is_map(scripts), do: scripts
def extract(_), do: %{}
@doc """
Returns lifecycle scripts defined in the package.
"""
@spec lifecycle(map()) :: map()
def lifecycle(data) do
scripts = extract(data)
Map.take(scripts, Lifecycle.hook_names())
end
@doc """
Returns non-lifecycle (custom) scripts.
"""
@spec custom(map()) :: map()
def custom(data) do
scripts = extract(data)
lifecycle_names = Lifecycle.hook_names()
Map.reject(scripts, fn {k, _} -> k in lifecycle_names end)
end
@doc """
Checks if a specific script exists.
"""
@spec has_script?(map(), String.t()) :: boolean()
def has_script?(data, name), do: Map.has_key?(extract(data), name)
@doc """
Detects common script patterns.
"""
@spec detect_patterns(map()) :: [atom()]
def detect_patterns(data) do
scripts = extract(data)
patterns = [
{:has_test,
Map.has_key?(scripts, "test") and
scripts["test"] != "echo \"Error: no test specified\" && exit 1"},
{:has_build, Map.has_key?(scripts, "build")},
{:has_lint,
Map.has_key?(scripts, "lint") or
Enum.any?(scripts, fn {_, v} -> String.contains?(v, "eslint") end)},
{:has_typecheck,
Map.has_key?(scripts, "typecheck") or
Enum.any?(scripts, fn {_, v} -> String.contains?(v, "tsc") end)},
{:has_dev, Map.has_key?(scripts, "dev") or Map.has_key?(scripts, "start")}
]
patterns |> Enum.filter(&elem(&1, 1)) |> Enum.map(&elem(&1, 0))
end
@doc """
Returns all script names sorted.
"""
@spec names(map()) :: [String.t()]
def names(data), do: data |> extract() |> Map.keys() |> Enum.sort()
@doc """
Counts scripts.
"""
@spec count(map()) :: non_neg_integer()
def count(data), do: data |> extract() |> map_size()
@doc """
Returns common scripts that are missing.
"""
@spec missing_common(map()) :: [String.t()]
def missing_common(data) do
defined = extract(data) |> Map.keys() |> MapSet.new()
@common_scripts |> Enum.reject(&MapSet.member?(defined, &1))
end
end