defmodule NPM.SideEffects do
@moduledoc """
Analyzes the `sideEffects` field from package.json.
Used by bundlers (webpack, rollup, esbuild) for tree-shaking.
"""
@doc """
Extracts the sideEffects value.
"""
@spec get(map()) :: boolean() | [String.t()] | nil
def get(%{"sideEffects" => val}), do: val
def get(_), do: nil
@doc """
Checks if the package is fully tree-shakeable (sideEffects: false).
"""
@spec tree_shakeable?(map()) :: boolean()
def tree_shakeable?(%{"sideEffects" => false}), do: true
def tree_shakeable?(_), do: false
@doc """
Checks if the package declares any side effects.
"""
@spec has_side_effects?(map()) :: boolean()
def has_side_effects?(%{"sideEffects" => false}), do: false
def has_side_effects?(%{"sideEffects" => []}), do: false
def has_side_effects?(_), do: true
@doc """
Returns files with side effects (when sideEffects is an array).
"""
@spec files_with_side_effects(map()) :: [String.t()]
def files_with_side_effects(%{"sideEffects" => files}) when is_list(files), do: files
def files_with_side_effects(_), do: []
@doc """
Checks if a specific file has side effects.
"""
@spec file_has_side_effects?(String.t(), map()) :: boolean()
def file_has_side_effects?(_file, %{"sideEffects" => false}), do: false
def file_has_side_effects?(file, %{"sideEffects" => patterns}) when is_list(patterns) do
Enum.any?(patterns, &file_matches?(&1, file))
end
def file_has_side_effects?(_, _), do: true
@doc """
Counts tree-shakeable packages.
"""
@spec stats([map()]) :: map()
def stats(packages) do
shakeable = Enum.count(packages, &tree_shakeable?/1)
with_array = Enum.count(packages, &is_list(get(&1)))
%{
total: length(packages),
tree_shakeable: shakeable,
partial: with_array,
unknown: length(packages) - shakeable - with_array
}
end
defp file_matches?(pattern, file) do
if String.contains?(pattern, "*") do
regex_str = pattern |> Regex.escape() |> String.replace("\\*", ".*")
Regex.match?(~r/^#{regex_str}$/, file)
else
file == pattern
end
end
end