From bcd67073bbdc8234871aa90e5977b3b24130338c Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 11:53:54 +0200 Subject: [PATCH 01/27] Support configuring version per profile --- lib/mix/tasks/tailwind.install.ex | 46 +++++++++------- lib/tailwind.ex | 87 +++++++++++++++++++------------ 2 files changed, 80 insertions(+), 53 deletions(-) diff --git a/lib/mix/tasks/tailwind.install.ex b/lib/mix/tasks/tailwind.install.ex index 0e8fe0a..5495084 100644 --- a/lib/mix/tasks/tailwind.install.ex +++ b/lib/mix/tasks/tailwind.install.ex @@ -2,6 +2,12 @@ defmodule Mix.Tasks.Tailwind.Install do @moduledoc """ Installs Tailwind executable and assets. + Usage: + + $ mix tailwind.install TASK_OPTIONS BASE_URL + + Example: + $ mix tailwind.install $ mix tailwind.install --if-missing @@ -15,9 +21,7 @@ defmodule Mix.Tasks.Tailwind.Install do binary (beware that we cannot guarantee the compatibility of any third party executable): - ```bash - $ mix tailwind.install https://people.freebsd.org/~dch/pub/tailwind/v3.2.6/tailwindcss-freebsd-x64 - ``` + $ mix tailwind.install https://people.freebsd.org/~dch/pub/tailwind/$version/tailwindcss-$target ## Options @@ -79,29 +83,31 @@ defmodule Mix.Tasks.Tailwind.Install do if opts[:runtime_config], do: Mix.Task.run("app.config") - if opts[:if_missing] && latest_version?() do - :ok - else - if Keyword.get(opts, :assets, true) do - File.mkdir_p!("assets/css") + for {profile, _} <- Tailwind.profiles() do + if opts[:if_missing] && latest_version?(profile) do + :ok + else + if Keyword.get(opts, :assets, true) do + File.mkdir_p!("assets/css") - prepare_app_css() - prepare_app_js() - end + prepare_app_css() + prepare_app_js() + end - if function_exported?(Mix, :ensure_application!, 1) do - Mix.ensure_application!(:inets) - Mix.ensure_application!(:ssl) - end + if function_exported?(Mix, :ensure_application!, 1) do + Mix.ensure_application!(:inets) + Mix.ensure_application!(:ssl) + end - Mix.Task.run("loadpaths") - Tailwind.install(base_url) + Mix.Task.run("loadpaths") + Tailwind.install(profile, base_url) + end end end - defp latest_version?() do - version = Tailwind.configured_version() - match?({:ok, ^version}, Tailwind.bin_version()) + defp latest_version?(profile) do + version = Tailwind.configured_version!(profile) + match?({:ok, ^version}, Tailwind.bin_version(profile)) end defp prepare_app_css do diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 63b3115..2d0a19b 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -81,20 +81,22 @@ defmodule Tailwind do """) end - configured_version = configured_version() - - case bin_version() do - {:ok, ^configured_version} -> - :ok - - {:ok, version} -> - Logger.warning(""" - Outdated tailwind version. Expected #{configured_version}, got #{version}. \ - Please run `mix tailwind.install` or update the version in your config files.\ - """) - - :error -> - :ok + for {profile, config} <- profiles() do + configured_version = Keyword.get(config, :version, global_version()) + + case bin_version(profile) do + {:ok, ^configured_version} -> + :ok + + {:ok, version} -> + Logger.warning(""" + Outdated tailwind version. Expected #{configured_version}, got #{version}. \ + Please run `mix tailwind.install` or update the version in your config files.\ + """) + + :error -> + :ok + end end end @@ -105,18 +107,36 @@ defmodule Tailwind do # Latest known version at the time of publishing. def latest_version, do: @latest_version + @doc false + def profiles do + config_keys = [:version_check, :version, :target, :path] + :tailwind |> Application.get_all_env() |> Keyword.drop(config_keys) + end + @doc """ Returns the configured tailwind version. """ - def configured_version do + def global_version do Application.get_env(:tailwind, :version, latest_version()) end + @doc """ + Returns the configured tailwind version for a specific profile. + + If not explicitly configured, falls back to `global_version/0`. + Raises if the given profile does not exist. + """ + def configured_version!(profile) when is_atom(profile) do + :tailwind + |> Application.fetch_env!(profile) + |> Keyword.get(:version, global_version()) + end + @doc """ Returns the configured tailwind target. By default, it is automatically detected. """ def configured_target do - Application.get_env(:tailwind, :target, target()) + Application.get_env(:tailwind, :target, system_target()) end @doc """ @@ -146,8 +166,8 @@ defmodule Tailwind do The executable may not be available if it was not yet installed. """ - def bin_path do - name = "tailwind-#{configured_target()}" + def bin_path(profile \\ :default) do + name = "tailwind-#{configured_target()}-#{configured_version!(profile)}" Application.get_env(:tailwind, :path) || if Code.ensure_loaded?(Mix.Project) do @@ -163,8 +183,8 @@ defmodule Tailwind do Returns `{:ok, version_string}` on success or `:error` when the executable is not available. """ - def bin_version do - path = bin_path() + def bin_version(profile \\ :default) do + path = bin_path(profile) with true <- File.exists?(path), {out, 0} <- System.cmd(path, ["--help"]), @@ -176,7 +196,7 @@ defmodule Tailwind do end @doc """ - Runs the given command with `args`. + Runs the tailwind CLI for the given `profile` with `args`. The given args will be appended to the configured args. The task output will be streamed directly to stdio. It @@ -198,7 +218,8 @@ defmodule Tailwind do stderr_to_stdout: true ] - bin_path() + profile + |> bin_path() |> System.cmd(args ++ extra_args, opts) |> elem(1) end @@ -213,8 +234,8 @@ defmodule Tailwind do Returns the same as `run/2`. """ def install_and_run(profile, args) do - unless File.exists?(bin_path()) do - install() + unless File.exists?(bin_path(profile)) do + install(profile) end run(profile, args) @@ -228,11 +249,11 @@ defmodule Tailwind do end @doc """ - Installs tailwind with `configured_version/0`. + Installs tailwind with `configured_version!/1`. """ - def install(base_url \\ default_base_url()) do - url = get_url(base_url) - bin_path = bin_path() + def install(profile \\ :default, base_url \\ default_base_url()) do + url = get_url(profile, base_url) + bin_path = bin_path(profile) binary = fetch_body!(url) File.mkdir_p!(Path.dirname(bin_path)) @@ -255,7 +276,7 @@ defmodule Tailwind do # tailwindcss-macos-arm64 # tailwindcss-macos-x64 # tailwindcss-windows-x64.exe - defp target do + defp system_target do arch_str = :erlang.system_info(:system_architecture) target_triple = arch_str |> List.to_string() |> String.split("-") @@ -303,7 +324,7 @@ defmodule Tailwind do # Tailwind CLI v4+ added explicit musl versions for Linux as # tailwind-linux-x64-musl # tailwind-linux-arm64-musl - if Version.match?(configured_version(), "~> 4.0") do + if Version.match?(global_version(), "~> 4.0") do "-musl" else "" @@ -356,7 +377,7 @@ defmodule Tailwind do You can see the available files for the configured version at: - https://github.com/tailwindlabs/tailwindcss/releases/tag/v#{configured_version()} + https://github.com/tailwindlabs/tailwindcss/releases/tag/v#{global_version()} """ {true, {:error, {:failed_connect, [{:to_address, _}, {inet, _, reason}]}}} @@ -420,9 +441,9 @@ defmodule Tailwind do :erlang.system_info(:otp_release) |> List.to_integer() end - defp get_url(base_url) do + defp get_url(profile, base_url) do base_url - |> String.replace("$version", configured_version()) + |> String.replace("$version", configured_version!(profile)) |> String.replace("$target", configured_target()) end end From 9f9a2373a3b28b46233a9ba21b0e6219a1b89b0c Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 13:17:27 +0200 Subject: [PATCH 02/27] Rename back to `configured_version` --- lib/tailwind.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 2d0a19b..310efbc 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -82,7 +82,7 @@ defmodule Tailwind do end for {profile, config} <- profiles() do - configured_version = Keyword.get(config, :version, global_version()) + configured_version = Keyword.get(config, :version, configured_version()) case bin_version(profile) do {:ok, ^configured_version} -> @@ -116,20 +116,20 @@ defmodule Tailwind do @doc """ Returns the configured tailwind version. """ - def global_version do + def configured_version do Application.get_env(:tailwind, :version, latest_version()) end @doc """ Returns the configured tailwind version for a specific profile. - If not explicitly configured, falls back to `global_version/0`. + If not explicitly configured, falls back to `configured_version/0`. Raises if the given profile does not exist. """ def configured_version!(profile) when is_atom(profile) do :tailwind |> Application.fetch_env!(profile) - |> Keyword.get(:version, global_version()) + |> Keyword.get(:version, configured_version()) end @doc """ @@ -324,7 +324,7 @@ defmodule Tailwind do # Tailwind CLI v4+ added explicit musl versions for Linux as # tailwind-linux-x64-musl # tailwind-linux-arm64-musl - if Version.match?(global_version(), "~> 4.0") do + if Version.match?(configured_version(), "~> 4.0") do "-musl" else "" @@ -377,7 +377,7 @@ defmodule Tailwind do You can see the available files for the configured version at: - https://github.com/tailwindlabs/tailwindcss/releases/tag/v#{global_version()} + https://github.com/tailwindlabs/tailwindcss/releases/tag/v#{configured_version()} """ {true, {:error, {:failed_connect, [{:to_address, _}, {inet, _, reason}]}}} From 2669b810334ada54f497dd8e55c726dabb513488 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 13:19:18 +0200 Subject: [PATCH 03/27] Don't change default argument order --- lib/mix/tasks/tailwind.install.ex | 2 +- lib/tailwind.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mix/tasks/tailwind.install.ex b/lib/mix/tasks/tailwind.install.ex index 5495084..9bfec48 100644 --- a/lib/mix/tasks/tailwind.install.ex +++ b/lib/mix/tasks/tailwind.install.ex @@ -100,7 +100,7 @@ defmodule Mix.Tasks.Tailwind.Install do end Mix.Task.run("loadpaths") - Tailwind.install(profile, base_url) + Tailwind.install(base_url, profile) end end end diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 310efbc..b97ebbe 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -235,7 +235,7 @@ defmodule Tailwind do """ def install_and_run(profile, args) do unless File.exists?(bin_path(profile)) do - install(profile) + install(default_base_url(), profile) end run(profile, args) @@ -251,7 +251,7 @@ defmodule Tailwind do @doc """ Installs tailwind with `configured_version!/1`. """ - def install(profile \\ :default, base_url \\ default_base_url()) do + def install(base_url \\ default_base_url(), profile \\ :default) do url = get_url(profile, base_url) bin_path = bin_path(profile) binary = fetch_body!(url) From 57b26bcae23cdcbffcc5179145124cea149590ae Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 13:21:53 +0200 Subject: [PATCH 04/27] `configured_version` -> `configured_version!` --- lib/mix/tasks/tailwind.install.ex | 2 +- lib/tailwind.ex | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/mix/tasks/tailwind.install.ex b/lib/mix/tasks/tailwind.install.ex index 9bfec48..2405eb7 100644 --- a/lib/mix/tasks/tailwind.install.ex +++ b/lib/mix/tasks/tailwind.install.ex @@ -106,7 +106,7 @@ defmodule Mix.Tasks.Tailwind.Install do end defp latest_version?(profile) do - version = Tailwind.configured_version!(profile) + version = Tailwind.configured_version(profile) match?({:ok, ^version}, Tailwind.bin_version(profile)) end diff --git a/lib/tailwind.ex b/lib/tailwind.ex index b97ebbe..6a5aef6 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -126,9 +126,9 @@ defmodule Tailwind do If not explicitly configured, falls back to `configured_version/0`. Raises if the given profile does not exist. """ - def configured_version!(profile) when is_atom(profile) do + def configured_version(profile) when is_atom(profile) do :tailwind - |> Application.fetch_env!(profile) + |> Application.get_env(profile, []) |> Keyword.get(:version, configured_version()) end @@ -167,7 +167,7 @@ defmodule Tailwind do The executable may not be available if it was not yet installed. """ def bin_path(profile \\ :default) do - name = "tailwind-#{configured_target()}-#{configured_version!(profile)}" + name = "tailwind-#{configured_target()}-#{configured_version(profile)}" Application.get_env(:tailwind, :path) || if Code.ensure_loaded?(Mix.Project) do @@ -249,7 +249,7 @@ defmodule Tailwind do end @doc """ - Installs tailwind with `configured_version!/1`. + Installs tailwind with `configured_version/1`. """ def install(base_url \\ default_base_url(), profile \\ :default) do url = get_url(profile, base_url) @@ -443,7 +443,7 @@ defmodule Tailwind do defp get_url(profile, base_url) do base_url - |> String.replace("$version", configured_version!(profile)) + |> String.replace("$version", configured_version(profile)) |> String.replace("$target", configured_target()) end end From b3009643176bdb4dd4f3fa4c753ccdfb2ef65534 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 13:40:38 +0200 Subject: [PATCH 05/27] Collect & filter required versions before installing --- lib/mix/tasks/tailwind.install.ex | 12 ++++++--- lib/tailwind.ex | 44 ++++++++++++++++++++++--------- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/lib/mix/tasks/tailwind.install.ex b/lib/mix/tasks/tailwind.install.ex index 2405eb7..4c7f9b4 100644 --- a/lib/mix/tasks/tailwind.install.ex +++ b/lib/mix/tasks/tailwind.install.ex @@ -83,8 +83,8 @@ defmodule Mix.Tasks.Tailwind.Install do if opts[:runtime_config], do: Mix.Task.run("app.config") - for {profile, _} <- Tailwind.profiles() do - if opts[:if_missing] && latest_version?(profile) do + for {version, latest?} <- collect_versions() do + if opts[:if_missing] && latest? do :ok else if Keyword.get(opts, :assets, true) do @@ -100,11 +100,17 @@ defmodule Mix.Tasks.Tailwind.Install do end Mix.Task.run("loadpaths") - Tailwind.install(base_url, profile) + Tailwind.install(base_url, version) end end end + defp collect_versions do + for {profile, _} <- Tailwind.profiles(), uniq: true do + {Tailwind.configured_version(profile), latest_version?(profile)} + end + end + defp latest_version?(profile) do version = Tailwind.configured_version(profile) match?({:ok, ^version}, Tailwind.bin_version(profile)) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 6a5aef6..c62f9f5 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -166,8 +166,10 @@ defmodule Tailwind do The executable may not be available if it was not yet installed. """ - def bin_path(profile \\ :default) do - name = "tailwind-#{configured_target()}-#{configured_version(profile)}" + def bin_path, do: bin_path(configured_version()) + + def bin_path(version) do + name = "tailwind-#{configured_target()}-#{version}" Application.get_env(:tailwind, :path) || if Code.ensure_loaded?(Mix.Project) do @@ -183,9 +185,20 @@ defmodule Tailwind do Returns `{:ok, version_string}` on success or `:error` when the executable is not available. """ - def bin_version(profile \\ :default) do - path = bin_path(profile) + def bin_version do + configured_version() + |> bin_path() + |> get_version() + end + + def bin_version(profile) when is_atom(profile) do + profile + |> configured_version() + |> bin_path() + |> get_version() + end + defp get_version(path) do with true <- File.exists?(path), {out, 0} <- System.cmd(path, ["--help"]), [vsn] <- Regex.run(~r/tailwindcss v([^\s]+)/, out, capture: :all_but_first) do @@ -219,6 +232,7 @@ defmodule Tailwind do ] profile + |> configured_version() |> bin_path() |> System.cmd(args ++ extra_args, opts) |> elem(1) @@ -233,9 +247,11 @@ defmodule Tailwind do Returns the same as `run/2`. """ - def install_and_run(profile, args) do - unless File.exists?(bin_path(profile)) do - install(default_base_url(), profile) + def install_and_run(profile, args) when is_atom(profile) do + version = configured_version(profile) + + unless File.exists?(bin_path(version)) do + install(default_base_url(), version) end run(profile, args) @@ -251,9 +267,13 @@ defmodule Tailwind do @doc """ Installs tailwind with `configured_version/1`. """ - def install(base_url \\ default_base_url(), profile \\ :default) do - url = get_url(profile, base_url) - bin_path = bin_path(profile) + def install(base_url \\ default_base_url()) do + install(base_url, configured_version()) + end + + def install(base_url, version) do + url = get_url(base_url, version) + bin_path = bin_path(version) binary = fetch_body!(url) File.mkdir_p!(Path.dirname(bin_path)) @@ -441,9 +461,9 @@ defmodule Tailwind do :erlang.system_info(:otp_release) |> List.to_integer() end - defp get_url(profile, base_url) do + defp get_url(base_url, version) do base_url - |> String.replace("$version", configured_version(profile)) + |> String.replace("$version", version) |> String.replace("$target", configured_target()) end end From 087dd35683b5ef9b77f2abab13fdf5f1f4a781d3 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 14:17:20 +0200 Subject: [PATCH 06/27] Update lib/tailwind.ex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/tailwind.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index c62f9f5..ab459b3 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -166,9 +166,7 @@ defmodule Tailwind do The executable may not be available if it was not yet installed. """ - def bin_path, do: bin_path(configured_version()) - - def bin_path(version) do + def bin_path(version \\ configured_version()) do name = "tailwind-#{configured_target()}-#{version}" Application.get_env(:tailwind, :path) || From c2c42c442c76061b0b974c5f85f246796d78daf5 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 14:17:39 +0200 Subject: [PATCH 07/27] Update lib/tailwind.ex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/tailwind.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index ab459b3..1b2c091 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -263,7 +263,7 @@ defmodule Tailwind do end @doc """ - Installs tailwind with `configured_version/1`. + Installs tailwind with `configured_version/0`. """ def install(base_url \\ default_base_url()) do install(base_url, configured_version()) From dc5d41f73f0c2d45a0aa075295c62c88619513a6 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 14:26:07 +0200 Subject: [PATCH 08/27] Filter profiles for keyword lists --- lib/tailwind.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 1b2c091..5e72403 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -110,7 +110,11 @@ defmodule Tailwind do @doc false def profiles do config_keys = [:version_check, :version, :target, :path] - :tailwind |> Application.get_all_env() |> Keyword.drop(config_keys) + + :tailwind + |> Application.get_all_env() + |> Keyword.drop(config_keys) + |> Enum.filter(&Keyword.keyword?(elem(&1, 1))) end @doc """ From f8bf6b9902c39106e5bddc86d182ca35234b478d Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 14:27:28 +0200 Subject: [PATCH 09/27] Mark `@doc false` on `install/2` --- lib/tailwind.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 5e72403..eace793 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -273,6 +273,7 @@ defmodule Tailwind do install(base_url, configured_version()) end + @doc false def install(base_url, version) do url = get_url(base_url, version) bin_path = bin_path(version) From eac9354e2ec3691658ae95fcf0d3166f98f7050a Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 14:37:49 +0200 Subject: [PATCH 10/27] Improve documentation --- lib/tailwind.ex | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index eace793..7086170 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -21,6 +21,9 @@ defmodule Tailwind do cd: Path.expand("../assets", __DIR__), ] + It is also possible to override the required tailwind CLI version on + profile-basis. + ## Tailwind configuration There are four global configurations for the tailwind application: @@ -146,7 +149,7 @@ defmodule Tailwind do @doc """ Returns the configuration for the given profile. - Returns nil if the profile does not exist. + Raises if the profile does not exist. """ def config_for!(profile) when is_atom(profile) do Application.get_env(:tailwind, profile) || @@ -166,7 +169,7 @@ defmodule Tailwind do end @doc """ - Returns the path to the executable. + Returns the path to the executable for the given `version`. The executable may not be available if it was not yet installed. """ @@ -182,9 +185,9 @@ defmodule Tailwind do end @doc """ - Returns the version of the tailwind executable. + Returns the version of the executable. - Returns `{:ok, version_string}` on success or `:error` when the executable + Returns `{:ok, vsn}` on success or `:error` when the executable is not available. """ def bin_version do @@ -245,7 +248,7 @@ defmodule Tailwind do end @doc """ - Installs, if not available, and then runs `tailwind`. + Installs, if not available, and then runs the tailwind CLI. Returns the same as `run/2`. """ @@ -260,7 +263,7 @@ defmodule Tailwind do end @doc """ - The default URL to install Tailwind from. + Returns the default URL to install Tailwind from. """ def default_base_url do "https://github.com/tailwindlabs/tailwindcss/releases/download/v$version/tailwindcss-$target" @@ -268,6 +271,9 @@ defmodule Tailwind do @doc """ Installs tailwind with `configured_version/0`. + + If given, the executable is downloaded from `base_url`, + otherwise, `default_base_url/0` is used. """ def install(base_url \\ default_base_url()) do install(base_url, configured_version()) From f002055eb4f0ea82dd55be94f4baaf892ab12c07 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 16:20:56 +0200 Subject: [PATCH 11/27] Fix broken test --- lib/mix/tasks/tailwind.install.ex | 55 ++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/mix/tasks/tailwind.install.ex b/lib/mix/tasks/tailwind.install.ex index 4c7f9b4..6489802 100644 --- a/lib/mix/tasks/tailwind.install.ex +++ b/lib/mix/tasks/tailwind.install.ex @@ -60,26 +60,15 @@ defmodule Mix.Tasks.Tailwind.Install do @impl true def run(args) do - valid_options = [runtime_config: :boolean, if_missing: :boolean, assets: :boolean] - - {opts, base_url} = - case OptionParser.parse_head!(args, strict: valid_options) do - {opts, []} -> - {opts, Tailwind.default_base_url()} - - {opts, [base_url]} -> - {opts, base_url} - - {_, _} -> - Mix.raise(""" - Invalid arguments to tailwind.install, expected one of: + if args |> try_install() |> was_successful?() do + :ok + else + :error + end + end - mix tailwind.install - mix tailwind.install 'https://github.com/tailwindlabs/tailwindcss/releases/download/v$version/tailwindcss-$target' - mix tailwind.install --runtime-config - mix tailwind.install --if-missing - """) - end + defp try_install(args) do + {opts, base_url} = parse_arguments(args) if opts[:runtime_config], do: Mix.Task.run("app.config") @@ -105,12 +94,36 @@ defmodule Mix.Tasks.Tailwind.Install do end end + defp parse_arguments(args) do + case OptionParser.parse_head!(args, strict: schema()) do + {opts, []} -> + {opts, Tailwind.default_base_url()} + + {opts, [base_url]} -> + {opts, base_url} + + {_, _} -> + Mix.raise(""" + Invalid arguments to tailwind.install, expected one of: + + mix tailwind.install + mix tailwind.install 'https://github.com/tailwindlabs/tailwindcss/releases/download/v$version/tailwindcss-$target' + mix tailwind.install --runtime-config + mix tailwind.install --if-missing + """) + end + end + defp collect_versions do for {profile, _} <- Tailwind.profiles(), uniq: true do {Tailwind.configured_version(profile), latest_version?(profile)} end end + defp was_successful?(results) do + Enum.all?(results, &(&1 == :ok)) + end + defp latest_version?(profile) do version = Tailwind.configured_version(profile) match?({:ok, ^version}, Tailwind.bin_version(profile)) @@ -145,4 +158,8 @@ defmodule Mix.Tasks.Tailwind.Install do :ok end end + + defp schema do + [runtime_config: :boolean, if_missing: :boolean, assets: :boolean] + end end From 790a89362fef51490f6c2aca94f14c9d6b104742 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 25 Apr 2025 16:31:27 +0200 Subject: [PATCH 12/27] Fix flaky test The `--if-missing` argument causes this test to be flaky. Previously, when you had downloaded another version, the newer version would automatically be missing and thus always be installed. Currently, that's no longer the case, because you can have multiple simultanous tailwind installs, so in this case (after the first test run), the newer version already exists. --- test/tailwind_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tailwind_test.exs b/test/tailwind_test.exs index f583bac..a7aeb24 100644 --- a/test/tailwind_test.exs +++ b/test/tailwind_test.exs @@ -39,7 +39,7 @@ defmodule TailwindTest do Application.delete_env(:tailwind, :version) - Mix.Task.rerun("tailwind.install", ["--if-missing"]) + Mix.Task.rerun("tailwind.install", []) assert File.read!("assets/css/app.css") =~ "tailwind" assert ExUnit.CaptureIO.capture_io(fn -> From 71156736674701da5df8a5e0f51ce087de437443 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 13:36:56 +0200 Subject: [PATCH 13/27] Fix wrong docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I changed the implementation from raising to non-raising after the suggestion from José, but forgot to also update the docstring. --- lib/tailwind.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index af3b21b..68f7b71 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -132,8 +132,8 @@ defmodule Tailwind do @doc """ Returns the configured tailwind version for a specific profile. - If not explicitly configured, falls back to `configured_version/0`. - Raises if the given profile does not exist. + If the profile does not exist or a version is not explicitly configured, + falls back to `configured_version/0`. """ def configured_version(profile) when is_atom(profile) do :tailwind From fec5d168e5181d4f71a5f910716750c3bc3cbcc9 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 13:50:06 +0200 Subject: [PATCH 14/27] Move `config_keys` to module attribute --- lib/tailwind.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 68f7b71..a990c9a 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -75,6 +75,8 @@ defmodule Tailwind do use Application require Logger + @config_keys [:version_check, :version, :target, :path] + @doc false def start(_, _) do if Application.get_env(:tailwind, :version_check, true) do @@ -114,11 +116,9 @@ defmodule Tailwind do @doc false def profiles do - config_keys = [:version_check, :version, :target, :path] - :tailwind |> Application.get_all_env() - |> Keyword.drop(config_keys) + |> Keyword.drop(@config_keys) |> Enum.filter(&Keyword.keyword?(elem(&1, 1))) end From 459d3ace0ae567229c8ea95ec1d6625bb07f5e0e Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 13:53:05 +0200 Subject: [PATCH 15/27] Make targets version-aware --- lib/tailwind.ex | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index a990c9a..3d26bcd 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -142,10 +142,22 @@ defmodule Tailwind do end @doc """ - Returns the configured tailwind target. By default, it is automatically detected. + Returns the configured tailwind target. By default, it is automatically detected + based on the globally configured version. """ def configured_target do - Application.get_env(:tailwind, :target, system_target()) + configured_target(configured_version()) + end + + @doc """ + Returns the configured tailwind target for the given `version`. + + The version is consulted only when no explicit `:target` is configured and the + target is auto-detected; some target suffixes (eg. the linux musl suffix) are + version-dependent. + """ + def configured_target(version) when is_binary(version) do + Application.get_env(:tailwind, :target, system_target(version)) end @doc """ @@ -176,7 +188,7 @@ defmodule Tailwind do The executable may not be available if it was not yet installed. """ def bin_path(version \\ configured_version()) do - name = "tailwind-#{configured_target()}-#{version}" + name = "tailwind-#{configured_target(version)}-#{version}" Application.get_env(:tailwind, :path) || if Code.ensure_loaded?(Mix.Project) do @@ -319,7 +331,7 @@ defmodule Tailwind do # tailwindcss-macos-arm64 # tailwindcss-macos-x64 # tailwindcss-windows-x64.exe - defp system_target do + defp system_target(version) do arch_str = :erlang.system_info(:system_architecture) target_triple = arch_str |> List.to_string() |> String.split("-") @@ -347,7 +359,7 @@ defmodule Tailwind do "freebsd-x64" {{:unix, :linux}, "aarch64", abi, 64} -> - "linux-arm64" <> maybe_add_abi_suffix(abi) + "linux-arm64" <> maybe_add_abi_suffix(abi, version) {{:unix, :linux}, "arm", _abi, 32} -> "linux-armv7" @@ -356,25 +368,25 @@ defmodule Tailwind do "linux-armv7" {{:unix, _osname}, arch, abi, 64} when arch in ~w(x86_64 amd64) -> - "linux-x64" <> maybe_add_abi_suffix(abi) + "linux-x64" <> maybe_add_abi_suffix(abi, version) {_os, _arch, _abi, _wordsize} -> raise "tailwind is not available for architecture: #{arch_str}" end end - defp maybe_add_abi_suffix("musl") do + defp maybe_add_abi_suffix("musl", version) do # Tailwind CLI v4+ added explicit musl versions for Linux as # tailwind-linux-x64-musl # tailwind-linux-arm64-musl - if Version.match?(configured_version(), "~> 4.0") do + if Version.match?(version, "~> 4.0") do "-musl" else "" end end - defp maybe_add_abi_suffix(_), do: "" + defp maybe_add_abi_suffix(_, _), do: "" defp fetch_body!(url, retry \\ true) when is_binary(url) do scheme = URI.parse(url).scheme @@ -494,6 +506,6 @@ defmodule Tailwind do defp get_url(base_url, version) do base_url |> String.replace("$version", version) - |> String.replace("$target", configured_target()) + |> String.replace("$target", configured_target(version)) end end From c79c938c84a890c62a198438d168887e9735c231 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 14:01:10 +0200 Subject: [PATCH 16/27] Refactor run function for performance --- lib/mix/tasks/tailwind.install.ex | 51 ++++++++++++++++++------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/mix/tasks/tailwind.install.ex b/lib/mix/tasks/tailwind.install.ex index 3488dd6..c9df5ff 100644 --- a/lib/mix/tasks/tailwind.install.ex +++ b/lib/mix/tasks/tailwind.install.ex @@ -40,31 +40,42 @@ defmodule Mix.Tasks.Tailwind.Install do @impl true def run(args) do - if args |> try_install() |> was_successful?() do - :ok - else - :error + {opts, base_url} = parse_arguments(args) + + if opts[:runtime_config] do + Mix.Task.run("app.config") + end + + case resolve_versions(opts) do + [] -> :ok + versions -> install_versions(base_url, versions) end end - defp try_install(args) do - {opts, base_url} = parse_arguments(args) + defp resolve_versions(opts) do + for {version, latest?} <- collect_versions(), + not (opts[:if_missing] && latest?) do + version + end + end - if opts[:runtime_config], do: Mix.Task.run("app.config") + defp install_versions(base_url, versions) do + ensure_install_ready() - for {version, latest?} <- collect_versions() do - if opts[:if_missing] && latest? do - :ok - else - if function_exported?(Mix, :ensure_application!, 1) do - Mix.ensure_application!(:inets) - Mix.ensure_application!(:ssl) - end + if Enum.all?(versions, &(Tailwind.install(base_url, &1) == :ok)) do + :ok + else + :error + end + end - Mix.Task.run("loadpaths") - Tailwind.install(base_url, version) - end + defp ensure_install_ready do + if function_exported?(Mix, :ensure_application!, 1) do + Mix.ensure_application!(:inets) + Mix.ensure_application!(:ssl) end + + Mix.Task.run("loadpaths") end defp parse_arguments(args) do @@ -93,10 +104,6 @@ defmodule Mix.Tasks.Tailwind.Install do end end - defp was_successful?(results) do - Enum.all?(results, &(&1 == :ok)) - end - defp latest_version?(profile) do version = Tailwind.configured_version(profile) match?({:ok, ^version}, Tailwind.bin_version(profile)) From 4faea8287ebaf6cb4fcc158f3d20f19ff5085ebc Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 14:06:10 +0200 Subject: [PATCH 17/27] Fix `mix tailwind.install` not working if no profiles are configured This now installs the given globally configured `version`, even if no profiles have been configured. This is not super useful because the `mix tailwind ` command won't be usable, but maybe a valid use case when users only want to install the binary and call it themselves. --- lib/mix/tasks/tailwind.install.ex | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/tailwind.install.ex b/lib/mix/tasks/tailwind.install.ex index c9df5ff..1776728 100644 --- a/lib/mix/tasks/tailwind.install.ex +++ b/lib/mix/tasks/tailwind.install.ex @@ -99,11 +99,22 @@ defmodule Mix.Tasks.Tailwind.Install do end defp collect_versions do - for {profile, _} <- Tailwind.profiles(), uniq: true do - {Tailwind.configured_version(profile), latest_version?(profile)} + case Tailwind.profiles() do + [] -> + [{Tailwind.configured_version(), latest_version?()}] + + profiles -> + for {profile, _} <- profiles, uniq: true do + {Tailwind.configured_version(profile), latest_version?(profile)} + end end end + defp latest_version? do + version = Tailwind.configured_version() + match?({:ok, ^version}, Tailwind.bin_version()) + end + defp latest_version?(profile) do version = Tailwind.configured_version(profile) match?({:ok, ^version}, Tailwind.bin_version(profile)) From cc80df1624abec8abf80c556a628dc15ca65301c Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 14:18:25 +0200 Subject: [PATCH 18/27] Fix typo --- lib/mix/tasks/tailwind.install.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/tailwind.install.ex b/lib/mix/tasks/tailwind.install.ex index 1776728..609d10e 100644 --- a/lib/mix/tasks/tailwind.install.ex +++ b/lib/mix/tasks/tailwind.install.ex @@ -54,7 +54,7 @@ defmodule Mix.Tasks.Tailwind.Install do defp resolve_versions(opts) do for {version, latest?} <- collect_versions(), - not (opts[:if_missing] && latest?) do + !(opts[:if_missing] && latest?) do version end end From 7ab37848a1b6d3e90e73718e8d02f260d1d81d71 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 14:31:07 +0200 Subject: [PATCH 19/27] Add docstring to `bin_version/1` --- lib/tailwind.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 3d26bcd..434780b 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -210,6 +210,12 @@ defmodule Tailwind do |> get_version() end + @doc """ + Returns the version of the executable for the given `profile`. + + Returns `{:ok, vsn}` on success or `:error` when the executable + is not available. + """ def bin_version(profile) when is_atom(profile) do profile |> configured_version() From ad64d70d583cee1e54adcfd7de8bbd7d5bedf6e8 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 14:33:02 +0200 Subject: [PATCH 20/27] Fix hardcoded path in warning --- lib/tailwind.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 434780b..8c95406 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -315,7 +315,7 @@ defmodule Tailwind do def install(base_url, version) do url = get_url(base_url, version) bin_path = bin_path(version) - binary = fetch_body!(url) + binary = fetch_body!(version, url) File.mkdir_p!(Path.dirname(bin_path)) # MacOS doesn't recompute code signing information if a binary @@ -394,7 +394,7 @@ defmodule Tailwind do defp maybe_add_abi_suffix(_, _), do: "" - defp fetch_body!(url, retry \\ true) when is_binary(url) do + defp fetch_body!(version, url, retry \\ true) when is_binary(url) do scheme = URI.parse(url).scheme url = String.to_charlist(url) Logger.debug("Downloading tailwind from #{url}") @@ -438,14 +438,14 @@ defmodule Tailwind do You can see the available files for the configured version at: - https://github.com/tailwindlabs/tailwindcss/releases/tag/v#{configured_version()} + https://github.com/tailwindlabs/tailwindcss/releases/tag/v#{version} """ {true, {:error, {:failed_connect, [{:to_address, _}, {inet, _, reason}]}}} when inet in [:inet, :inet6] and reason in [:ehostunreach, :enetunreach, :eprotonosupport, :nxdomain] -> :httpc.set_options(ipfamily: fallback(inet)) - fetch_body!(to_string(url), false) + fetch_body!(version, to_string(url), false) other -> raise """ @@ -458,7 +458,7 @@ defmodule Tailwind do your certificates are set via OTP ca certfile overide via SSL configuration. 2. Manually download the executable from the URL above and - place it at "_build/tailwind-#{configured_target()}" + place it at "#{bin_path(version)}" 3. Install and use Tailwind from npmJS. See our module documentation to learn more: https://hexdocs.pm/tailwind From cf5293ac94633a7b01ee1e62897aa28430981cc4 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 14:33:36 +0200 Subject: [PATCH 21/27] Make `File.chmod!` also a bang --- lib/tailwind.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 8c95406..8c05379 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -325,7 +325,9 @@ defmodule Tailwind do end File.write!(bin_path, binary, [:binary]) - File.chmod(bin_path, 0o755) + File.chmod!(bin_path, 0o755) + + :ok end # Available targets: From ea29b6d6572ad45dee77b1a40ed6dba9b3cee34d Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 14:33:51 +0200 Subject: [PATCH 22/27] Fix bug where computing `bin_path` on unsupported platforms crashes, even when `:path` is explicitly set --- lib/tailwind.ex | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 8c05379..272c11f 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -188,14 +188,17 @@ defmodule Tailwind do The executable may not be available if it was not yet installed. """ def bin_path(version \\ configured_version()) do + Application.get_env(:tailwind, :path) || default_bin_path(version) + end + + defp default_bin_path(version) do name = "tailwind-#{configured_target(version)}-#{version}" - Application.get_env(:tailwind, :path) || - if Code.ensure_loaded?(Mix.Project) do - Path.join(Path.dirname(Mix.Project.build_path()), name) - else - Path.expand("_build/#{name}") - end + if Code.ensure_loaded?(Mix.Project) do + Path.join(Path.dirname(Mix.Project.build_path()), name) + else + Path.expand("_build/#{name}") + end end @doc """ From 913df5326b1a126ede64185d0a9e816fa7146d89 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 14:35:10 +0200 Subject: [PATCH 23/27] Make `configured_target/1` private --- lib/tailwind.ex | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 272c11f..698c5f1 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -149,14 +149,7 @@ defmodule Tailwind do configured_target(configured_version()) end - @doc """ - Returns the configured tailwind target for the given `version`. - - The version is consulted only when no explicit `:target` is configured and the - target is auto-detected; some target suffixes (eg. the linux musl suffix) are - version-dependent. - """ - def configured_target(version) when is_binary(version) do + defp configured_target(version) when is_binary(version) do Application.get_env(:tailwind, :target, system_target(version)) end From 1dd71d814ca30a445c3fc1fd6c134eb5fe1bc9b9 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 22 May 2026 14:43:20 +0200 Subject: [PATCH 24/27] Refactor `configured_target/1` to be consistent with `configured_version/1` --- lib/tailwind.ex | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 698c5f1..39f0e8b 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -142,14 +142,26 @@ defmodule Tailwind do end @doc """ - Returns the configured tailwind target. By default, it is automatically detected - based on the globally configured version. + Returns the configured tailwind target. + + By default, it is automatically detected based on `configured_version/0`. """ def configured_target do - configured_target(configured_version()) + target_for_version(configured_version()) + end + + @doc """ + Returns the configured tailwind target for the given `profile`. + + By default, it is automatically detected based on `configured_version/1`. + """ + def configured_target(profile) when is_atom(profile) do + profile + |> configured_version() + |> target_for_version() end - defp configured_target(version) when is_binary(version) do + defp target_for_version(version) when is_binary(version) do Application.get_env(:tailwind, :target, system_target(version)) end @@ -185,7 +197,7 @@ defmodule Tailwind do end defp default_bin_path(version) do - name = "tailwind-#{configured_target(version)}-#{version}" + name = "tailwind-#{target_for_version(version)}-#{version}" if Code.ensure_loaded?(Mix.Project) do Path.join(Path.dirname(Mix.Project.build_path()), name) @@ -510,6 +522,6 @@ defmodule Tailwind do defp get_url(base_url, version) do base_url |> String.replace("$version", version) - |> String.replace("$target", configured_target(version)) + |> String.replace("$target", target_for_version(version)) end end From 28d1de0272408465fa21cf5cb07e29f46f1e24ca Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 3 Jun 2026 10:14:22 +0200 Subject: [PATCH 25/27] Raise on boot when `:path` is set and one or more profiles configure `:version` --- lib/tailwind.ex | 30 +++++++++++++++++++++++++++++- test/tailwind_test.exs | 24 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 39f0e8b..0e6e705 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -22,7 +22,8 @@ defmodule Tailwind do ] It is also possible to override the required tailwind CLI version on - profile-basis. + profile-basis. This is incompatible with the global `:path` setting, since `:path` points to a single executable. Configuring a profile + with its own `:version` while `:path` is set will raise at boot. ## Tailwind configuration @@ -79,6 +80,8 @@ defmodule Tailwind do @doc false def start(_, _) do + validate_config!() + if Application.get_env(:tailwind, :version_check, true) do unless Application.get_env(:tailwind, :version) do Logger.warning(""" @@ -165,6 +168,29 @@ defmodule Tailwind do Application.get_env(:tailwind, :target, system_target(version)) end + defp validate_config! do + if Application.get_env(:tailwind, :path) do + profiles_with_version = + for {profile, config} <- profiles(), + Keyword.has_key?(config, :version), + do: profile + + if profiles_with_version != [] do + raise """ + cannot configure per-profile :version when global :path is set. + + The global :path points to a single tailwind executable, so it is + incompatible with profiles that require different versions. + + Profile(s) with :version configured: #{inspect(profiles_with_version)} + + Either remove :path from your :tailwind config, or remove :version + from the offending profile(s). + """ + end + end + end + @doc """ Returns the configuration for the given profile. @@ -321,6 +347,8 @@ defmodule Tailwind do @doc false def install(base_url, version) do + validate_config!() + url = get_url(base_url, version) bin_path = bin_path(version) binary = fetch_body!(version, url) diff --git a/test/tailwind_test.exs b/test/tailwind_test.exs index 1b7aa78..c1fb77d 100644 --- a/test/tailwind_test.exs +++ b/test/tailwind_test.exs @@ -26,6 +26,30 @@ defmodule TailwindTest do end) =~ @version end + test "raises when :path is set and a profile overrides :version" do + Application.put_env(:tailwind, :path, "/tmp/does-not-matter") + Application.put_env(:tailwind, :pinned, version: "3.4.17", args: []) + + on_exit(fn -> + Application.delete_env(:tailwind, :path) + Application.delete_env(:tailwind, :pinned) + end) + + assert_raise RuntimeError, ~r/cannot configure per-profile :version/, fn -> + Tailwind.install("https://example.invalid/$version/$target", "3.4.17") + end + end + + test "does not raise when :path is set but no profile overrides :version" do + Application.put_env(:tailwind, :path, Tailwind.bin_path()) + + on_exit(fn -> Application.delete_env(:tailwind, :path) end) + + assert ExUnit.CaptureIO.capture_io(fn -> + assert Tailwind.run(:default, ["--help"]) == 0 + end) =~ @version + end + test "installs and updates with custom config" do Application.put_env(:tailwind, :version, "3.4.17") From 170bb6c4b203ba7f5c0cdc3c5dcb08338114e9cf Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Wed, 3 Jun 2026 11:32:23 +0200 Subject: [PATCH 26/27] Update lib/tailwind.ex --- lib/tailwind.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tailwind.ex b/lib/tailwind.ex index 0e6e705..9836cab 100644 --- a/lib/tailwind.ex +++ b/lib/tailwind.ex @@ -176,7 +176,7 @@ defmodule Tailwind do do: profile if profiles_with_version != [] do - raise """ + raise ArgumentError, """ cannot configure per-profile :version when global :path is set. The global :path points to a single tailwind executable, so it is From 7a4b934c9745ef1394642006579a74016e84708c Mon Sep 17 00:00:00 2001 From: Steffen Deusch Date: Wed, 3 Jun 2026 11:34:31 +0200 Subject: [PATCH 27/27] Apply suggestion from @SteffenDE --- test/tailwind_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tailwind_test.exs b/test/tailwind_test.exs index c1fb77d..b40ff1e 100644 --- a/test/tailwind_test.exs +++ b/test/tailwind_test.exs @@ -35,7 +35,7 @@ defmodule TailwindTest do Application.delete_env(:tailwind, :pinned) end) - assert_raise RuntimeError, ~r/cannot configure per-profile :version/, fn -> + assert_raise ArgumentError, ~r/cannot configure per-profile :version/, fn -> Tailwind.install("https://example.invalid/$version/$target", "3.4.17") end end