Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bcd6707
Support configuring version per profile
RobinBoers Apr 25, 2025
9f9a237
Rename back to `configured_version`
RobinBoers Apr 25, 2025
2669b81
Don't change default argument order
RobinBoers Apr 25, 2025
57b26bc
`configured_version` -> `configured_version!`
RobinBoers Apr 25, 2025
b300964
Collect & filter required versions before installing
RobinBoers Apr 25, 2025
087dd35
Update lib/tailwind.ex
RobinBoers Apr 25, 2025
c2c42c4
Update lib/tailwind.ex
RobinBoers Apr 25, 2025
dc5d41f
Filter profiles for keyword lists
RobinBoers Apr 25, 2025
f8bf6b9
Mark `@doc false` on `install/2`
RobinBoers Apr 25, 2025
eac9354
Improve documentation
RobinBoers Apr 25, 2025
f002055
Fix broken test
RobinBoers Apr 25, 2025
790a893
Fix flaky test
RobinBoers Apr 25, 2025
17436fd
Resolve merge conflicts
RobinBoers May 21, 2026
7115673
Fix wrong docstring
RobinBoers May 22, 2026
fec5d16
Move `config_keys` to module attribute
RobinBoers May 22, 2026
459d3ac
Make targets version-aware
RobinBoers May 22, 2026
c79c938
Refactor run function for performance
RobinBoers May 22, 2026
4faea82
Fix `mix tailwind.install` not working if no profiles are configured
RobinBoers May 22, 2026
cc80df1
Fix typo
RobinBoers May 22, 2026
7ab3784
Add docstring to `bin_version/1`
RobinBoers May 22, 2026
ad64d70
Fix hardcoded path in warning
RobinBoers May 22, 2026
cf5293a
Make `File.chmod!` also a bang
RobinBoers May 22, 2026
ea29b6d
Fix bug where computing `bin_path` on unsupported platforms crashes, …
RobinBoers May 22, 2026
913df53
Make `configured_target/1` private
RobinBoers May 22, 2026
1dd71d8
Refactor `configured_target/1` to be consistent with `configured_vers…
RobinBoers May 22, 2026
28d1de0
Raise on boot when `:path` is set and one or more profiles configure …
RobinBoers Jun 3, 2026
170bb6c
Update lib/tailwind.ex
SteffenDE Jun 3, 2026
7a4b934
Apply suggestion from @SteffenDE
SteffenDE Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions lib/mix/tasks/tailwind.install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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
Comment thread
RobinBoers marked this conversation as resolved.
Outdated
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
Expand Down
87 changes: 54 additions & 33 deletions lib/tailwind.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]
Comment thread
SteffenDE marked this conversation as resolved.
Outdated
:tailwind |> Application.get_all_env() |> Keyword.drop(config_keys)
Comment thread
RobinBoers marked this conversation as resolved.
Outdated
end

@doc """
Returns the configured tailwind version.
"""
def configured_version do
def global_version do
Comment thread
RobinBoers marked this conversation as resolved.
Outdated
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.
Comment thread
RobinBoers marked this conversation as resolved.
Outdated
"""
def configured_version!(profile) when is_atom(profile) do
:tailwind
|> Application.fetch_env!(profile)
|> Keyword.get(:version, global_version())
end
Comment thread
RobinBoers marked this conversation as resolved.
Outdated

@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
Comment thread
RobinBoers marked this conversation as resolved.

@doc """
Expand Down Expand Up @@ -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
Expand All @@ -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
Comment thread
SteffenDE marked this conversation as resolved.
Outdated
path = bin_path(profile)

with true <- File.exists?(path),
{out, 0} <- System.cmd(path, ["--help"]),
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
Comment thread
RobinBoers marked this conversation as resolved.
Outdated
url = get_url(profile, base_url)
bin_path = bin_path(profile)
binary = fetch_body!(url)
File.mkdir_p!(Path.dirname(bin_path))

Expand All @@ -255,7 +276,7 @@ defmodule Tailwind do
# tailwindcss-macos-arm64
# tailwindcss-macos-x64
# tailwindcss-windows-x64.exe
defp target do
defp system_target do
Comment thread
SteffenDE marked this conversation as resolved.
Outdated
arch_str = :erlang.system_info(:system_architecture)
target_triple = arch_str |> List.to_string() |> String.split("-")

Expand Down Expand Up @@ -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
""
Expand Down Expand Up @@ -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}]}}}
Expand Down Expand Up @@ -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