Skip to main content

lib/mix/tasks/npm.pack.ex

defmodule Mix.Tasks.Npm.Pack do
  @shortdoc "Create a tarball"

  @moduledoc """
  Create a tarball of the current package.

      mix npm.pack

  Creates a `.tgz` file from the project's `package.json`,
  including all files that would be published.
  """

  use Mix.Task

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

    case File.read("package.json") do
      {:ok, content} ->
        data = NPM.JSON.decode!(content)
        pack(data)

      {:error, :enoent} ->
        Mix.shell().error("No package.json found.")

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

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

  defp pack(data) do
    name = Map.get(data, "name", "package") |> String.replace("/", "-")
    version = Map.get(data, "version", "0.0.0")
    files = collect_files(data)
    tarball_name = "#{name}-#{version}.tgz"

    entries =
      Enum.map(files, fn file ->
        {~c"package/#{file}", ~c"#{file}"}
      end)

    case :erl_tar.create(~c"#{tarball_name}", entries, [:compressed]) do
      :ok ->
        stat = File.stat!(tarball_name)
        Mix.shell().info("Created #{tarball_name} (#{format_size(stat.size)})")

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

  defp collect_files(data) do
    case Map.get(data, "files") do
      patterns when is_list(patterns) ->
        Enum.flat_map(patterns, &Path.wildcard/1)
        |> Enum.concat(["package.json"])
        |> Enum.uniq()
        |> Enum.filter(&File.regular?/1)

      _ ->
        default_files()
    end
  end

  defp default_files do
    ["package.json", "README.md", "LICENSE", "CHANGELOG.md"]
    |> Enum.filter(&File.exists?/1)
  end

  defp format_size(bytes) when bytes < 1024, do: "#{bytes} B"
  defp format_size(bytes), do: "#{Float.round(bytes / 1024, 1)} KB"
end