Skip to content
50 changes: 31 additions & 19 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,37 @@ 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 {version, latest?} <- collect_versions() do
if opts[:if_missing] && latest? 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)
Mix.Task.run("loadpaths")
Tailwind.install(base_url, version)
end
end
end

Mix.Task.run("loadpaths")
Tailwind.install(base_url)
defp collect_versions do
for {profile, _} <- Tailwind.profiles(), uniq: true do
{Tailwind.configured_version(profile), latest_version?(profile)}
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
99 changes: 70 additions & 29 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, configured_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
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 `configured_version/0`.
Raises if the given profile does not exist.
"""
def configured_version(profile) when is_atom(profile) do
:tailwind
|> Application.get_env(profile, [])
|> Keyword.get(:version, configured_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,10 @@ 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, 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
Expand All @@ -164,8 +186,19 @@ defmodule Tailwind do
is not available.
"""
def bin_version do
path = bin_path()
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
Expand All @@ -176,7 +209,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 +231,9 @@ defmodule Tailwind do
stderr_to_stdout: true
]

bin_path()
profile
|> configured_version()
|> bin_path()
|> System.cmd(args ++ extra_args, opts)
|> elem(1)
end
Expand All @@ -212,9 +247,11 @@ defmodule Tailwind do

Returns the same as `run/2`.
"""
def install_and_run(profile, args) do
unless File.exists?(bin_path()) do
install()
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)
Expand All @@ -228,11 +265,15 @@ 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()
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))

Expand All @@ -255,7 +296,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 @@ -420,9 +461,9 @@ defmodule Tailwind do
:erlang.system_info(:otp_release) |> List.to_integer()
end

defp get_url(base_url) do
defp get_url(base_url, version) do
base_url
|> String.replace("$version", configured_version())
|> String.replace("$version", version)
|> String.replace("$target", configured_target())
end
end