Current section

Files

Jump to
lightning_css lib lightning_css.ex
Raw

lib/lightning_css.ex

defmodule LightningCSS do
@moduledoc """
Lightning CSS
"""
# Package: https://registry.npmjs.org/lightningcss-cli/latest
# Website: https://www.npmjs.com/package/lightningcss-cli
@latest_version "1.22.0"
use Application
require Logger
def start(_, _) do
unless Application.get_env(:lightning_css, :version) do
Logger.warning("""
lightning_css version is not configured. Please set it in your config files:
config :lightning_css, :version, "#{latest_version()}"
""")
end
configured_version = configured_version()
case bin_version() do
{:ok, ^configured_version} ->
:ok
{:ok, version} ->
Logger.warning("""
Outdated lightning_css version. Expected #{configured_version}, got #{version}. \
Please run `mix lightning_css.install` or update the version in your config files.\
""")
:error ->
:ok
end
Supervisor.start_link([], strategy: :one_for_one, name: __MODULE__.Supervisor)
end
@doc """
Returns the version of Lightning CSS that this package should use.
"""
@spec configured_version() :: String.t()
def configured_version do
Application.get_env(:lightning_css, :version, latest_version())
end
@doc """
Returns the configuration for the given profile.
Returns nil if the profile does not exist.
"""
def config_for!(profile) when is_atom(profile) do
Application.get_env(:lightning_css, profile) ||
raise ArgumentError, """
unknown lightning_css profile. Make sure the profile is defined in your config/config.exs file, such as:
config :lightning_css,
#{profile}: [
args: ~w(css/app.css --bundle --targets='last 10 versions' --output-dir=../priv/static/assets),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
"""
end
@doc """
Returns the most recent Lightning CSS version known by this package.
"""
@spec latest_version() :: String.t()
def latest_version() do
@latest_version
end
@doc """
Returns the version of the lightning_css executable.
Returns `{:ok, version_string}` on success or `:error` when the executable
is not available.
"""
def bin_version do
path = bin_path()
version_path = Path.dirname(path) |> Path.join("version")
with true <- File.exists?(version_path),
{:ok, result} <- File.read(version_path) do
{:ok, String.trim(result)}
else
_ -> :error
end
end
@doc """
Returns the path to the executable.
The executable may not be available if it was not yet installed.
"""
def bin_path do
name = "lightning_css-#{target()}"
Application.get_env(:lightning_css, :path) ||
if Code.ensure_loaded?(Mix.Project) do
Path.join(Path.dirname(Mix.Project.build_path()), name)
else
Path.expand("_build/#{name}")
end
end
# Available targets: https://registry.npmjs.org/lightningcss-cli/latest
# credo:disable-for-next-line
def target do
case :os.type() do
# Assuming it's an x86 CPU
{:win32, _} ->
wordsize = :erlang.system_info(:wordsize)
if wordsize == 8 do
"win32-x64"
else
"win32-ia32"
end
{:unix, osname} ->
arch_str = :erlang.system_info(:system_architecture)
[arch | _] = arch_str |> List.to_string() |> String.split("-")
case arch do
"amd64" -> "#{osname}-x64"
"x86_64" -> "#{osname}-x64"
"i686" -> "#{osname}-ia32"
"i386" -> "#{osname}-ia32"
"aarch64" -> "#{osname}-arm64"
"arm" when osname == :darwin -> "darwin-arm64"
"arm" -> "#{osname}-arm"
"armv7" <> _ -> "#{osname}-arm"
_ -> raise "lightning_css is not available for architecture: #{arch_str}"
end
end
end
@doc """
Runs the given command with `args`.
The given args will be appended to the configured args.
The task output will be streamed directly to stdio. It
returns the status of the underlying call.
"""
def run(profile, extra_args) when is_atom(profile) and is_list(extra_args) do
config = config_for!(profile)
args = config[:args] || []
if args == [] and extra_args == [] do
raise "no arguments passed to lightning_css"
end
opts = [
cd: config[:cd] || File.cwd!(),
env: config[:env] || %{},
into: IO.stream(:stdio, :line),
stderr_to_stdout: true
]
bin_path()
|> System.cmd(args ++ extra_args, opts)
|> elem(1)
end
@doc """
Installs, if not available, and then runs `lightning_css`.
Returns the same as `run/2`.
"""
def install_and_run(profile, args) do
File.exists?(bin_path()) || start_unique_install_worker()
run(profile, args)
end
defp start_unique_install_worker() do
ref =
__MODULE__.Supervisor
|> Supervisor.start_child(
Supervisor.child_spec({Task, &install/0}, restart: :transient, id: __MODULE__.Installer)
)
|> case do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end
|> Process.monitor()
receive do
{:DOWN, ^ref, _, _, _} -> :ok
end
end
@doc """
Installs lightning_css with `configured_version/0`.
"""
def install do
version = configured_version()
tmp_opts = if System.get_env("MIX_XDG"), do: %{os: :linux}, else: %{}
tmp_dir =
freshdir_p(:filename.basedir(:user_cache, "lightning_css", tmp_opts)) ||
freshdir_p(Path.join(System.tmp_dir!(), "lightning_css")) ||
raise "could not install lightning_css. Set MIX_XGD=1 and then set XDG_CACHE_HOME to the path you want to use as cache"
url = "https://registry.npmjs.org/lightningcss-cli-#{target()}/-/lightningcss-cli-#{target()}-#{version}.tgz"
tar = fetch_body!(url)
case :erl_tar.extract({:binary, tar}, [:compressed, cwd: to_charlist(tmp_dir)]) do
:ok -> :ok
other -> raise "couldn't unpack archive: #{inspect(other)}"
end
bin_path = bin_path()
File.mkdir_p!(Path.dirname(bin_path))
case :os.type() do
{:win32, _} ->
File.cp!(Path.join([tmp_dir, "package", "lightningcss.exe"]), bin_path)
File.write!(Path.join(Path.dirname(bin_path), "version"), version)
_ ->
File.cp!(Path.join([tmp_dir, "package", "lightningcss"]), bin_path)
File.write!(Path.join(Path.dirname(bin_path), "version"), version)
end
end
defp fetch_body!(url) do
scheme = URI.parse(url).scheme
url = String.to_charlist(url)
Logger.debug("Downloading lightning_css from #{url}")
{:ok, _} = Application.ensure_all_started(:inets)
{:ok, _} = Application.ensure_all_started(:ssl)
if proxy = proxy_for_scheme(scheme) do
%{host: host, port: port} = URI.parse(proxy)
Logger.debug("Using #{String.upcase(scheme)}_PROXY: #{proxy}")
set_option = if "https" == scheme, do: :https_proxy, else: :proxy
:httpc.set_options([{set_option, {{String.to_charlist(host), port}, []}}])
end
# https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/inets
cacertfile = cacertfile() |> String.to_charlist()
http_options =
[
ssl: [
verify: :verify_peer,
cacertfile: cacertfile,
depth: 2,
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
]
]
|> maybe_add_proxy_auth(scheme)
options = [body_format: :binary]
case :httpc.request(:get, {url, []}, http_options, options) do
{:ok, {{_, 200, _}, _headers, body}} ->
body
other ->
raise """
couldn't fetch #{url}: #{inspect(other)}
You may also install the "lightning_css" executable manually, \
see the docs: https://hexdocs.pm/lightning_css
"""
end
end
defp proxy_for_scheme("http") do
System.get_env("HTTP_PROXY") || System.get_env("http_proxy")
end
defp proxy_for_scheme("https") do
System.get_env("HTTPS_PROXY") || System.get_env("https_proxy")
end
defp maybe_add_proxy_auth(http_options, scheme) do
case proxy_auth(scheme) do
nil -> http_options
auth -> [{:proxy_auth, auth} | http_options]
end
end
defp proxy_auth(scheme) do
with proxy when is_binary(proxy) <- proxy_for_scheme(scheme),
%{userinfo: userinfo} when is_binary(userinfo) <- URI.parse(proxy),
[username, password] <- String.split(userinfo, ":") do
{String.to_charlist(username), String.to_charlist(password)}
else
_ -> nil
end
end
defp freshdir_p(path) do
with {:ok, _} <- File.rm_rf(path),
:ok <- File.mkdir_p(path) do
path
else
_ -> nil
end
end
defp cacertfile() do
Application.get_env(:lightning_css, :cacerts_path) || CAStore.file_path()
end
end