defmodule NPM.Patch do
@moduledoc """
Manages patched packages (patch-package style).
Tracks patches applied to node_modules packages and
verifies they are still applied after install.
"""
@patches_dir "patches"
@doc """
Lists all patch files in the patches directory.
"""
@spec list(String.t()) :: [%{package: String.t(), file: String.t()}]
def list(project_dir \\ ".") do
dir = Path.join(project_dir, @patches_dir)
case File.ls(dir) do
{:ok, files} ->
files
|> Enum.filter(&String.ends_with?(&1, ".patch"))
|> Enum.map(fn file ->
%{package: extract_package_name(file), file: file}
end)
|> Enum.sort_by(& &1.package)
_ ->
[]
end
end
@doc """
Checks if a package has patches.
"""
@spec patched?(String.t(), String.t()) :: boolean()
def patched?(package_name, project_dir \\ ".") do
list(project_dir)
|> Enum.any?(&(&1.package == package_name))
end
@doc """
Returns the count of patches.
"""
@spec count(String.t()) :: non_neg_integer()
def count(project_dir \\ "."), do: list(project_dir) |> length()
@doc """
Extracts the package name from a patch filename.
Supports formats like `lodash+4.17.21.patch` and `@scope+pkg+1.0.0.patch`.
"""
@spec extract_package_name(String.t()) :: String.t()
def extract_package_name(filename) do
base = String.trim_trailing(filename, ".patch")
case String.split(base, "+") do
["@" <> scope, name | _rest] -> "@#{scope}/#{name}"
[name | _rest] -> name
_ -> base
end
end
@doc """
Generates a patch filename for a package.
"""
@spec filename(String.t(), String.t()) :: String.t()
def filename(package_name, version) do
safe_name = String.replace(package_name, "/", "+")
"#{safe_name}+#{version}.patch"
end
@doc """
Returns all patched package names.
"""
@spec patched_packages(String.t()) :: [String.t()]
def patched_packages(project_dir \\ ".") do
list(project_dir) |> Enum.map(& &1.package) |> Enum.uniq()
end
end