defmodule Mix.Tasks.Add do @moduledoc """ Add dependencies to your mix.exs file. ## Usage mix add PACKAGE [VERSION] [OPTIONS] ## Examples mix add phoenix mix add phoenix "~> 1.7" mix add jason "~> 1.4" --only dev,test mix add my_lib --github username/my_lib mix add my_lib --path ../my_lib ## Options * `--version`, `-v` - Specify version constraint (default: latest from hex.pm) * `--only` - Comma-separated list of environments (dev,test,prod) * `--runtime` - Include in runtime (default: true) * `--git` - Git repository URL * `--github` - GitHub repository (username/repo format) * `--path` - Local path dependency * `--branch` - Git branch * `--tag` - Git tag * `--ref` - Git ref * `--override` - Override existing dependency * `--organization` - Hex organization * `--repo` - Hex repository (default: hexpm) """ use Igniter.Mix.Task @impl Igniter.Mix.Task def info(_argv, _source) do %Igniter.Mix.Task.Info{ example: "mix add phoenix \"~> 1.7\"", positional: [:package, :version], schema: [ only: :string, runtime: :boolean, git: :string, github: :string, path: :string, branch: :string, tag: :string, ref: :string, override: :boolean, organization: :string, repo: :string ], aliases: [ v: :version ] } end @impl Igniter.Mix.Task def igniter(igniter) do package = igniter.args.positional[:package] if is_nil(package) do Igniter.add_issue(igniter, "No package name provided. Usage: mix add PACKAGE [VERSION]") else add_dependency(igniter, package) end end defp add_dependency(igniter, package) do dep_spec = build_dep_spec(igniter, package) opts = build_opts(igniter) igniter |> Igniter.Project.Deps.add_dep(dep_spec, opts) |> maybe_fetch_deps(package) end defp build_dep_spec(igniter, package) do package_atom = String.to_atom(package) version = igniter.args.positional[:version] base_spec = cond do path = igniter.args.options[:path] -> {package_atom, path: path} git = igniter.args.options[:git] -> git_opts = build_git_opts(igniter.args.options) {package_atom, git_opts ++ [git: git]} github = igniter.args.options[:github] -> git_opts = build_git_opts(igniter.args.options) {package_atom, git_opts ++ [github: github]} true -> version = version || fetch_latest_version(package) {package_atom, version || "~> 0.1"} end add_dep_options(base_spec, igniter.args.options) end defp build_git_opts(options) do [] |> maybe_add_opt(:branch, options[:branch]) |> maybe_add_opt(:tag, options[:tag]) |> maybe_add_opt(:ref, options[:ref]) end defp maybe_add_opt(opts, _key, nil), do: opts defp maybe_add_opt(opts, key, value), do: [{key, value} | opts] defp add_dep_options({name, spec}, options) when is_list(spec) do spec = spec |> maybe_add_opt(:override, options[:override]) |> maybe_add_opt(:organization, options[:organization]) |> maybe_add_opt(:repo, options[:repo]) |> maybe_add_runtime(options[:runtime]) |> maybe_add_only(options[:only]) {name, spec} end defp add_dep_options({name, version}, options) when is_binary(version) do opts = [] |> maybe_add_opt(:override, options[:override]) |> maybe_add_opt(:organization, options[:organization]) |> maybe_add_opt(:repo, options[:repo]) |> maybe_add_runtime(options[:runtime]) |> maybe_add_only(options[:only]) if opts == [] do {name, version} else {name, version, opts} end end defp maybe_add_runtime(opts, nil), do: opts defp maybe_add_runtime(opts, false), do: [{:runtime, false} | opts] defp maybe_add_runtime(opts, true), do: opts defp maybe_add_only(opts, nil), do: opts defp maybe_add_only(opts, ""), do: opts defp maybe_add_only(opts, envs) when is_binary(envs) do env_atoms = envs |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.map(&String.to_atom/1) [{:only, env_atoms} | opts] end defp build_opts(igniter) do if igniter.args.options[:override] do [on_exists: :overwrite] else [on_exists: :skip] end end defp fetch_latest_version(package) do case fetch_hex_info(package) do {:ok, version} -> version {:error, _} -> nil end end defp fetch_hex_info(package) do url = "https://hex.pm/api/packages/#{package}" case Req.get(url) do {:ok, %{status: 200, body: body}} -> latest = get_latest_release(body) {:ok, "~> #{latest}"} _ -> {:error, :not_found} end rescue _ -> {:error, :request_failed} end defp get_latest_release(%{"releases" => releases}) when is_list(releases) do releases |> Enum.reject(&(&1["retired"])) |> Enum.map(&(&1["version"])) |> Enum.sort(:desc) |> List.first() |> format_version() end defp get_latest_release(_), do: "0.1.0" defp format_version(nil), do: "0.1.0" defp format_version(version) do # Extract major.minor for ~> constraint case String.split(version, ".") do [major, minor | _] -> "#{major}.#{minor}" _ -> version end end defp maybe_fetch_deps(igniter, package) do igniter |> Igniter.add_notice(""" Added #{package} to mix.exs Run `mix deps.get` to fetch the dependency. """) end end