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