Skip to main content

lib/npm/migration.ex

defmodule NPM.Migration do
  @moduledoc """
  Provides migration guidance between npm and lockfile versions.
  """

  @doc """
  Determines required lockfile version for an npm version.
  """
  @spec lockfile_version(String.t()) :: non_neg_integer()
  def lockfile_version(npm_version) do
    case major_version(npm_version) do
      n when n >= 9 -> 3
      n when n >= 7 -> 2
      _ -> 1
    end
  end

  @doc """
  Checks if a lockfile migration is needed.
  """
  @spec needs_migration?(non_neg_integer(), String.t()) :: boolean()
  def needs_migration?(current_lockfile_version, target_npm_version) do
    lockfile_version(target_npm_version) != current_lockfile_version
  end

  @doc """
  Returns breaking changes between npm major versions.
  """
  @spec breaking_changes(non_neg_integer(), non_neg_integer()) :: [String.t()]
  def breaking_changes(from, to) when from < to do
    from..to
    |> Enum.flat_map(&changes_for_version/1)
  end

  def breaking_changes(_, _), do: []

  @doc """
  Returns migration steps.
  """
  @spec steps(non_neg_integer(), non_neg_integer()) :: [String.t()]
  def steps(from_lockfile, to_lockfile) when from_lockfile < to_lockfile do
    base = ["Delete node_modules/", "Delete package-lock.json", "Run npm install"]

    if to_lockfile >= 3 do
      base ++ ["Verify package-lock.json uses lockfileVersion #{to_lockfile}"]
    else
      base
    end
  end

  def steps(from, to) when from == to, do: ["No migration needed."]
  def steps(_, _), do: ["Downgrade not recommended."]

  @doc """
  Formats migration guide.
  """
  @spec format_guide(non_neg_integer(), non_neg_integer()) :: String.t()
  def format_guide(from, to) do
    step_list =
      steps(from, to) |> Enum.with_index(1) |> Enum.map_join("\n", fn {s, i} -> "#{i}. #{s}" end)

    "Migration from lockfileVersion #{from} to #{to}:\n#{step_list}"
  end

  defp major_version(version) do
    case String.split(version, ".", parts: 2) do
      [major | _] -> String.to_integer(major)
      _ -> 0
    end
  end

  defp changes_for_version(7) do
    ["package-lock.json v2 format", "Automatic peer dependency installation"]
  end

  defp changes_for_version(9) do
    ["package-lock.json v3 format", "Dropped support for Node.js 14"]
  end

  defp changes_for_version(10) do
    ["package-lock.json v3 format", "Dropped support for Node.js 16"]
  end

  defp changes_for_version(_), do: []
end