defmodule Mix.Tasks.Npm.Publish do
@shortdoc "Publish package to the npm registry"
@moduledoc """
Publish the current package to the npm registry.
mix npm.publish
mix npm.publish --tag beta
mix npm.publish --access public
mix npm.publish --dry-run
Requires `NPM_TOKEN` environment variable or `.npmrc` auth config.
## Options
* `--tag` — publish with a dist-tag (default: `latest`)
* `--access` — package access level: `public` or `restricted`
* `--dry-run` — show what would be published without uploading
"""
use Mix.Task
@impl true
def run(args) do
Application.ensure_all_started(:req)
{opts, _, _} =
OptionParser.parse(args, strict: [tag: :string, access: :string, dry_run: :boolean])
tag = Keyword.get(opts, :tag, "latest")
access = Keyword.get(opts, :access, "public")
dry_run = Keyword.get(opts, :dry_run, false)
with {:ok, content} <- File.read("package.json"),
data <- NPM.JSON.decode!(content),
:ok <- validate_publish(data) do
name = data["name"]
version = data["version"]
if dry_run do
print_dry_run(name, version, tag, access)
else
publish(name, version, tag, access)
end
else
{:error, :enoent} ->
Mix.shell().error("No package.json found.")
{:error, reason} ->
Mix.shell().error("Publish failed: #{inspect(reason)}")
end
end
defp validate_publish(data) do
cond do
not Map.has_key?(data, "name") ->
{:error, "package.json must have a name field"}
not Map.has_key?(data, "version") ->
{:error, "package.json must have a version field"}
true ->
:ok
end
end
defp print_dry_run(name, version, tag, access) do
Mix.shell().info("Dry run — would publish:")
Mix.shell().info(" package: #{name}")
Mix.shell().info(" version: #{version}")
Mix.shell().info(" tag: #{tag}")
Mix.shell().info(" access: #{access}")
end
defp publish(name, version, tag, access) do
token = NPM.Config.auth_token()
if is_nil(token) do
Mix.shell().error(
"NPM_TOKEN not set. Set it, configure :duskmoon_npm, :token, or add auth to .npmrc"
)
{:error, :no_token}
else
registry = NPM.Registry.registry_url()
url = "#{registry}/#{NPM.Registry.encode_package(name)}"
Mix.shell().info(
"Publishing #{name}@#{version} to #{registry} (tag: #{tag}, access: #{access})..."
)
body = build_publish_body(name, version, tag, access)
case Req.put(url, body: body, headers: auth_headers(token)) do
{:ok, %{status: s}} when s in [200, 201] ->
Mix.shell().info("Published #{name}@#{version}")
:ok
{:ok, %{status: status, body: resp_body}} ->
Mix.shell().error("Publish failed (#{status}): #{inspect(resp_body)}")
{:error, {:http, status}}
{:error, reason} ->
Mix.shell().error("Publish failed: #{inspect(reason)}")
{:error, reason}
end
end
end
defp build_publish_body(name, version, tag, access) do
:json.encode(%{
"name" => name,
"version" => version,
"dist-tags" => %{tag => version},
"access" => access
})
end
defp auth_headers(token) do
[authorization: "Bearer #{token}", "content-type": "application/json"]
end
end