defmodule NPM.Lockfile.Merge do
@moduledoc """
Merge two lockfiles, preferring entries from the newer lockfile.
Used for workspace scenarios where multiple `package.json` files
produce separate lockfiles that need to be combined.
"""
@doc """
Merge two lockfiles. Entries in `newer` override entries in `base`.
Returns the merged lockfile map.
"""
@spec merge(NPM.Lockfile.t(), NPM.Lockfile.t()) :: NPM.Lockfile.t()
def merge(base, newer) do
Map.merge(base, newer)
end
@doc """
Merge with a custom conflict resolver.
The `resolver` function receives `(name, base_entry, newer_entry)`
and returns the winning entry.
"""
@spec merge(NPM.Lockfile.t(), NPM.Lockfile.t(), function()) :: NPM.Lockfile.t()
def merge(base, newer, resolver) do
Map.merge(base, newer, fn name, base_entry, newer_entry ->
resolver.(name, base_entry, newer_entry)
end)
end
@doc """
Find packages that differ between two lockfiles.
Returns `{added, removed, changed}` where:
- `added` — packages in `newer` but not `base`
- `removed` — packages in `base` but not `newer`
- `changed` — packages in both with different versions
"""
@spec diff(NPM.Lockfile.t(), NPM.Lockfile.t()) ::
{[String.t()], [String.t()], [{String.t(), String.t(), String.t()}]}
def diff(base, newer) do
base_keys = MapSet.new(Map.keys(base))
newer_keys = MapSet.new(Map.keys(newer))
added = MapSet.difference(newer_keys, base_keys) |> MapSet.to_list() |> Enum.sort()
removed = MapSet.difference(base_keys, newer_keys) |> MapSet.to_list() |> Enum.sort()
changed =
MapSet.intersection(base_keys, newer_keys)
|> Enum.filter(fn name -> base[name].version != newer[name].version end)
|> Enum.map(fn name -> {name, base[name].version, newer[name].version} end)
|> Enum.sort()
{added, removed, changed}
end
end