defmodule NPM.Scripts do
@moduledoc """
Reads and manages npm scripts from package.json.
Provides the `npm run` functionality — listing, filtering,
and determining execution order of scripts defined in package.json.
"""
@well_known ~w(start test build dev lint format clean prepare pretest posttest prebuild postbuild)
@doc """
Reads all scripts from a package.json file.
"""
@spec read(String.t()) :: {:ok, map()} | {:error, term()}
def read(package_json_path) do
case File.read(package_json_path) do
{:ok, content} ->
data = NPM.JSON.decode!(content)
{:ok, data["scripts"] || %{}}
error ->
error
end
end
@doc """
Lists all available script names.
"""
@spec list(map()) :: [String.t()]
def list(scripts), do: scripts |> Map.keys() |> Enum.sort()
@doc """
Checks if a script exists.
"""
@spec has?(map(), String.t()) :: boolean()
def has?(scripts, name), do: Map.has_key?(scripts, name)
@doc """
Returns pre/post hooks for a script.
"""
@spec hooks_for(map(), String.t()) :: %{pre: String.t() | nil, post: String.t() | nil}
def hooks_for(scripts, name) do
%{pre: scripts["pre#{name}"], post: scripts["post#{name}"]}
end
@doc """
Categorizes scripts into well-known and custom.
"""
@spec categorize(map()) :: %{well_known: [String.t()], custom: [String.t()]}
def categorize(scripts) do
known_set = MapSet.new(@well_known)
names = Map.keys(scripts)
%{
well_known: names |> Enum.filter(&MapSet.member?(known_set, &1)) |> Enum.sort(),
custom: names |> Enum.reject(&MapSet.member?(known_set, &1)) |> Enum.sort()
}
end
@doc """
Returns scripts matching a pattern.
"""
@spec filter(map(), String.t()) :: map()
def filter(scripts, pattern) do
regex = Regex.compile!(pattern, "i")
Map.filter(scripts, fn {name, _cmd} -> Regex.match?(regex, name) end)
end
@doc """
Formats a scripts map for display.
"""
@spec format(map()) :: String.t()
def format(scripts) when map_size(scripts) == 0, do: "No scripts defined."
def format(scripts) do
scripts
|> Enum.sort_by(&elem(&1, 0))
|> Enum.map_join("\n", fn {name, cmd} -> " #{name}: #{cmd}" end)
end
@doc """
Returns the execution order for a script, including pre/post hooks.
"""
@spec execution_order(map(), String.t()) :: [String.t()]
def execution_order(scripts, name) do
pre = if has?(scripts, "pre#{name}"), do: ["pre#{name}"], else: []
main = if has?(scripts, name), do: [name], else: []
post = if has?(scripts, "post#{name}"), do: ["post#{name}"], else: []
pre ++ main ++ post
end
end