Skip to main content

lib/mix/tasks/npm.shrinkwrap.ex

defmodule Mix.Tasks.Npm.Shrinkwrap do
  @shortdoc "Create npm-shrinkwrap.json from lockfile"

  @moduledoc """
  Create an `npm-shrinkwrap.json` from the current `npm.lock`.

      mix npm.shrinkwrap

  The shrinkwrap file locks exact versions for publishing. Unlike
  `npm.lock` (which is project-only), `npm-shrinkwrap.json` is
  included when the package is published to the registry.
  """

  use Mix.Task

  @impl true
  def run([]) do
    Application.ensure_all_started(:req)

    case NPM.Lockfile.read() do
      {:ok, lockfile} when lockfile == %{} ->
        Mix.shell().error("No npm.lock found. Run `mix npm.install` first.")

      {:ok, lockfile} ->
        shrinkwrap = build_shrinkwrap(lockfile)
        content = NPM.JSON.encode_pretty(shrinkwrap)
        File.write!("npm-shrinkwrap.json", content)
        Mix.shell().info("Created npm-shrinkwrap.json (#{map_size(lockfile)} packages)")

      {:error, reason} ->
        Mix.shell().error("Failed to read lockfile: #{inspect(reason)}")
    end
  end

  def run(_) do
    Mix.shell().error("Usage: mix npm.shrinkwrap")
  end

  defp build_shrinkwrap(lockfile) do
    deps =
      Map.new(lockfile, fn {name, entry} ->
        {name,
         %{
           "version" => entry.version,
           "integrity" => entry.integrity,
           "resolved" => entry.tarball
         }}
      end)

    %{
      "lockfileVersion" => 1,
      "dependencies" => deps
    }
  end
end