Skip to content

Support configuring version per profile #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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
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]
: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 """
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you required to have a default profile? I don't think so... so this will likely be a breaking change in practice.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do not have a default profile, this will now resolve to the path for the version configured 'globally' (aka configured_version/0), so that shouldn't be a breaking change.

The only "problem" is that after updating, people will have to rerun mix tailwind.install, because the path to the binary has changed to include the version string.

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
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
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