Current section

Files

Jump to
salad_ui lib mix tasks salad.init.ex
Raw

lib/mix/tasks/salad.init.ex

defmodule Mix.Tasks.Salad.Init do
@moduledoc """
A Mix task for initializing SaladUI in a project and configuring it to use a color theme.
Commands:
mix salad.init : Add all necessary configuration to be able to install SaladUI components
mix salad.init --as-lib : Add all necessary configuration to use SaladUI in a library
mix salad.init --help : Print this help message
"""
use Mix.Task
import SaladUi.TasksHelpers
alias SaladUI.Patcher
@default_components_path "lib/%APP_NAME%_web/components"
@color_schemes ~w(zinc slate stone gray neutral red rose orange green blue yellow violet)
@default_color_scheme "gray"
@default_tailwind_animate_version "1.0.7"
@impl true
def run(argv) do
case argv do
[] -> execute_init()
["--as-lib"] -> execute_init(as_lib: true)
_ -> print_usage()
end
end
defp execute_init(opts \\ []) do
component_path = prompt_component_path()
color_scheme = prompt_color_scheme()
init(component_path, color_scheme, opts)
end
defp init(component_path, color_scheme, opts) do
env = Atom.to_string(Mix.env())
app_name = Mix.Project.config()[:app] |> Atom.to_string() |> String.downcase()
assets_path = build_assets_path(env)
application_file_path = Path.join(File.cwd!(), "lib/#{app_name}/application.ex")
File.mkdir_p!(component_path)
with :ok <- write_config(component_path),
:ok <- init_tw_merge_cache(application_file_path),
:ok <- patch_css(color_scheme, assets_path),
:ok <- patch_js(assets_path),
:ok <- copy_tailwind_colors(assets_path),
:ok <- patch_tailwind_config(opts),
:ok <- maybe_write_helpers_module(component_path, app_name, opts),
:ok <- maybe_write_component_module(component_path, app_name, opts),
:ok <- install_tailwind_animate(opts) do
if opts[:as_lib] do
Mix.shell().info("Done. Now you can use any component by `import SaladUI.<ComponentName>` in your project.")
else
Mix.shell().info("Done. Now you can add components by running mix salad.add <component_name>")
end
else
{:error, reason} -> Mix.shell().error("Error during setup: #{reason}")
end
end
defp prompt_component_path do
default_path = build_default_component_path()
"Enter the path to the components folder (#{default_path}):"
|> Mix.shell().prompt()
|> String.trim()
|> case do
"" -> default_path
path -> parse_path(path)
end
end
defp prompt_color_scheme do
prompt = "Select the color scheme to use (#{@default_color_scheme}):"
response = prompt |> Mix.shell().prompt() |> String.trim() |> String.downcase()
case response do
"" ->
Mix.shell().info("Using default color scheme: #{@default_color_scheme}")
@default_color_scheme
color_scheme when color_scheme in @color_schemes ->
Mix.shell().info("Using color scheme: #{color_scheme}")
color_scheme
_ ->
Mix.shell().error("Invalid color scheme")
prompt_color_scheme()
end
end
defp write_config(component_path) do
write_dev_config(component_path)
end
defp write_dev_config(component_path) do
Mix.shell().info("Writing components path to dev.exs")
dev_config_path = Path.join(File.cwd!(), "config/dev.exs")
components_config = [
salad_ui: %{
description: "Path to install SaladUI components",
values: [components_path: "Path.join(File.cwd!(), \"#{component_path}\")"]
}
]
patch_config(dev_config_path, components_config)
end
defp patch_config(config_path, config) do
if File.exists?(config_path) do
Patcher.patch_config(config_path, config)
:ok
else
{:error, "#{Path.basename(config_path)} not found"}
end
end
defp init_tw_merge_cache(application_file_path) do
cache_module = "TwMerge.Cache"
description = "Start TwMerge cache"
Mix.shell().info("Adding Tailwind merge cache to application supervisor")
if File.exists?(application_file_path) do
Patcher.patch_elixir_application(
application_file_path,
cache_module,
description
)
else
{:error, "application.ex not found"}
end
end
defp patch_css(color_scheme, assets_path) do
app_css_path = Path.join(File.cwd!(), "assets/css/app.css")
css_color_scheme_path = Path.join([assets_path, "colors", "#{color_scheme}.css"])
if File.exists?(app_css_path) do
Mix.shell().info("Patching app.css")
Patcher.patch_css_file(app_css_path, css_color_scheme_path)
:ok
else
{:error, "app.css not found"}
end
end
defp patch_js(assets_path) do
app_js_path = Path.join(File.cwd!(), "assets/js/app.js")
js_file_path = Path.join(assets_path, "server-events.js")
if File.exists?(app_js_path) do
Mix.shell().info("Patching app.js")
Patcher.patch_js_file(app_js_path, js_file_path)
:ok
else
{:error, "app.js not found"}
end
end
defp copy_tailwind_colors(assets_path) do
Mix.shell().info("Copying tailwind.colors.json to assets folder")
source_path = Path.join(assets_path, "tailwind.colors.json")
target_path = Path.join(File.cwd!(), "assets/tailwind.colors.json")
unless File.exists?(target_path) do
File.cp!(source_path, target_path)
end
:ok
end
defp patch_tailwind_config(opts) do
Mix.shell().info("Patching tailwind.config.js")
tailwind_config_path = Path.join(File.cwd!(), "assets/tailwind.config.js")
if File.exists?(tailwind_config_path) do
Patcher.patch_tailwind_config(tailwind_config_path, opts)
:ok
else
{:error, "tailwind.config.js not found"}
end
end
defp maybe_write_helpers_module(_component_path, _app_name, as_lib: true), do: :ok
defp maybe_write_helpers_module(component_path, app_name, _opts) do
Mix.shell().info("Writing helpers module")
source_path = Path.join(get_base_path(), "helpers.ex")
target_path = Path.join(component_path, "helpers.ex")
module_name = Macro.camelize(app_name)
source_code =
Regex.replace(
~r/defmodule SaladUI\.Helpers/,
File.read!(source_path),
"defmodule #{module_name}Web.ComponentHelpers"
)
File.write!(target_path, source_code)
end
defp maybe_write_component_module(_component_path, _app_name, as_lib: true), do: :ok
defp maybe_write_component_module(component_path, app_name, _opts) do
Mix.shell().info("Writing component module")
source_path = Path.join(:code.priv_dir(:salad_ui), "templates/component.eex")
target_path = Path.join(component_path, "component.ex")
module_name = Macro.camelize(app_name)
source_code = EEx.eval_file(source_path, module_name: module_name, assigns: %{module_name: module_name})
File.write!(target_path, source_code)
end
defp install_tailwind_animate(opts) do
tag = Keyword.get(opts, :tailwind_animate_version, @default_tailwind_animate_version)
Mix.shell().info("Downloading tailwindcss-animate.js v#{tag}")
url = "https://raw.githubusercontent.com/jamiebuilds/tailwindcss-animate/refs/tags/v#{tag}/index.js"
output_path = Keyword.get(opts, :output_path, Path.join(File.cwd!(), "assets/vendor/tailwindcss-animate.js"))
:inets.start()
:ssl.start()
case :httpc.request(:get, {url, []}, [], body_format: :binary) do
{:ok, {{_version, 200, _reason_phrase}, _headers, body}} ->
# Write the body to file
File.write!(output_path, body)
:ok
{:ok, {{_version, status_code, _reason_phrase}, _headers, _body}} ->
{:error, "Failed to download tailwindcss-animate with status #{status_code}"}
{:error, reason} ->
{:error, "Failed to download tailwindcss-animate.js: #{inspect(reason)}"}
end
end
defp print_usage, do: Mix.shell().info(@moduledoc)
defp parse_path(path) do
path
# remove trailing slash
|> String.replace(~r/\/$/, "")
# remove leading slash
|> String.replace(~r/^\.*\/?/, "")
end
defp build_assets_path(env) do
["_build", env, "lib/salad_ui/priv/static/assets"]
|> Path.join()
|> Path.expand()
end
defp build_default_component_path do
app_name = Mix.Project.config()[:app] |> Atom.to_string() |> String.downcase()
String.replace(@default_components_path, "%APP_NAME%", app_name)
end
end