defmodule Mix.Tasks.Npm.Diff do @shortdoc "Show changes between lockfile versions" @moduledoc """ Show what changed since the last `npm.lock` was committed. mix npm.diff Compares the current `npm.lock` with the git HEAD version and shows added, removed, and updated packages. """ use Mix.Task @impl true def run([]) do Application.ensure_all_started(:req) case read_git_lockfile() do {:ok, old_lockfile} -> case NPM.Lockfile.read() do {:ok, new_lockfile} -> print_diff(old_lockfile, new_lockfile) {:error, _} -> Mix.shell().error("Cannot read npm.lock") end {:error, :not_committed} -> Mix.shell().info("npm.lock is not tracked by git.") end end def run(_) do Mix.shell().error("Usage: mix npm.diff") end defp read_git_lockfile do case System.cmd("git", ["show", "HEAD:npm.lock"], stderr_to_stdout: true) do {content, 0} -> data = NPM.JSON.decode!(content) lockfile = NPM.Lockfile.parse_packages(Map.get(data, "packages", %{})) {:ok, lockfile} {_, _} -> {:error, :not_committed} end end defp print_diff(old, new) when old == new do Mix.shell().info("No changes in npm.lock") end defp print_diff(old, new) do added = Map.keys(new) -- Map.keys(old) removed = Map.keys(old) -- Map.keys(new) updated = for key <- Map.keys(new), Map.has_key?(old, key), old[key].version != new[key].version, do: key if added == [] and removed == [] and updated == [] do Mix.shell().info("No version changes in npm.lock") else Enum.each(Enum.sort(added), &Mix.shell().info("+ #{&1}@#{new[&1].version}")) Enum.each(Enum.sort(removed), &Mix.shell().info("- #{&1}@#{old[&1].version}")) Enum.each(Enum.sort(updated), fn key -> Mix.shell().info("↑ #{key} #{old[key].version} → #{new[key].version}") end) end end end