Current section
Files
Jump to
Current section
Files
lib/mix/tasks/uikit.setup.ex
defmodule Mix.Tasks.Uikit.Setup.Docs do
@moduledoc false
@spec short_doc() :: String.t()
def short_doc do
"Sets up UIkit in your Phoenix application — no Node.js required"
end
@spec example() :: String.t()
def example do
"mix uikit.setup"
end
@spec long_doc() :: String.t()
def long_doc do
"""
#{short_doc()}
UIkit's JS and SCSS source files are shipped with the `elixir_uikit` package.
This task configures your project to use them directly from `deps/elixir_uikit`
— nothing is vendored into your project.
This task will:
- Remove Tailwind CSS, DaisyUI, and heroicons
- Add and configure `dart_sass` with a load path into the dep's SCSS
- Configure esbuild aliases to resolve UIkit JS from the dep
- Set up `assets/js/app.js` with UIkit imports and LiveView hooks
- Create `assets/css/app.scss` with UIkit SCSS imports
- Add UIkit component imports to your web module
No Node.js or npm installation is required.
## Example
```sh
#{example()}
```
"""
end
end
if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.Uikit.Setup do
@shortdoc "#{__MODULE__.Docs.short_doc()}"
@moduledoc __MODULE__.Docs.long_doc()
use Igniter.Mix.Task
@impl Igniter.Mix.Task
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :elixir_uikit,
example: __MODULE__.Docs.example(),
installs: [{:dart_sass, "~> 0.7", only: :dev}]
}
end
@impl Igniter.Mix.Task
def igniter(igniter) do
app_name = Igniter.Project.Application.app_name(igniter)
igniter
|> Igniter.Project.Deps.add_dep({:dart_sass, "~> 0.7", only: :dev})
|> remove_tailwind()
|> configure_dart_sass(app_name)
|> setup_javascript()
|> setup_stylesheet()
|> add_component_imports()
|> Igniter.add_notice("""
UIkit has been set up successfully — no Node.js required!
Next steps:
1. Run `mix deps.get` to fetch dart_sass
2. Restart your Phoenix server
You can now use UIkit components in your templates:
<.uk_button variant="primary">Click me</.uk_button>
""")
end
# ── Remove Tailwind ──────────────────────────────────────────────────
defp remove_tailwind(igniter) do
igniter
|> Igniter.Project.Deps.remove_dep(:tailwind)
|> Igniter.Project.Deps.remove_dep(:heroicons)
|> remove_tailwind_config()
|> remove_tailwind_watcher()
|> remove_tailwind_aliases()
|> remove_tailwind_vendor_files()
end
defp remove_tailwind_config(igniter) do
Igniter.update_file(igniter, "config/config.exs", fn source ->
content = Rewrite.Source.get(source, :content)
new_content =
Regex.replace(
~r/\n*# Configure tailwind[^\n]*\nconfig :tailwind,\n(?:[ \t]+[^\n]*\n)*/,
content,
""
)
Rewrite.Source.update(source, :content, fn _ -> new_content end)
end)
end
defp remove_tailwind_watcher(igniter) do
Igniter.update_file(igniter, "config/dev.exs", fn source ->
content = Rewrite.Source.get(source, :content)
new_content =
content
|> String.replace(~r/,?\s*tailwind:\s*\{Tailwind[^}]*\}/s, "")
|> String.replace(~r/tailwind:\s*\{Tailwind[^}]*\},?\s*/s, "")
Rewrite.Source.update(source, :content, fn _ -> new_content end)
end)
end
defp remove_tailwind_aliases(igniter) do
Igniter.update_file(igniter, "mix.exs", fn source ->
content = Rewrite.Source.get(source, :content)
new_content = String.replace(content, ~r/"tailwind [^"]*"/, "\"sass default\"")
Rewrite.Source.update(source, :content, fn _ -> new_content end)
end)
end
defp remove_tailwind_vendor_files(igniter) do
~w(assets/vendor/heroicons.js assets/vendor/daisyui.js assets/vendor/daisyui-theme.js)
|> Enum.reduce(igniter, fn file, acc ->
acc = Igniter.include_existing_file(acc, file, required: false)
if Igniter.exists?(acc, file) do
%{acc | rms: [file | acc.rms]}
else
acc
end
end)
end
# ── Configure dart_sass ──────────────────────────────────────────────
defp configure_dart_sass(igniter, app_name) do
igniter
|> Igniter.Project.Config.configure(
"config.exs",
:dart_sass,
[:version],
"1.77.8",
updater: fn zipper -> {:ok, Igniter.Code.Common.replace_code(zipper, "1.77.8")} end
)
|> Igniter.Project.Config.configure(
"dev.exs",
:dart_sass,
[:default],
{:code,
quote do
[
args: [
"--load-path=../deps/elixir_uikit/priv/vendor/uikit/scss",
"css/app.scss",
"../priv/static/assets/css/app.css"
],
cd: Path.expand("../assets", __DIR__)
]
end},
updater: fn zipper ->
{:ok,
Igniter.Code.Common.replace_code(
zipper,
quote do
[
args: [
"--load-path=../deps/elixir_uikit/priv/vendor/uikit/scss",
"css/app.scss",
"../priv/static/assets/css/app.css"
],
cd: Path.expand("../assets", __DIR__)
]
end
)}
end
)
|> add_sass_watcher(app_name)
end
defp add_sass_watcher(igniter, app_name) do
Igniter.update_file(igniter, "config/dev.exs", fn source ->
content = Rewrite.Source.get(source, :content)
new_content =
if String.contains?(content, "watchers:") do
Regex.replace(
~r/(watchers:\s*\[)\n/,
content,
"\\1\n sass: {\n DartSass,\n :install_and_run,\n [:default, ~w(--embed-source-map --source-map-urls=absolute --watch)]\n },\n"
)
else
Regex.replace(
~r/(#{Regex.escape("#{app_name}Web.Endpoint,")})(\s*\n)/,
content,
"\\1\\2 watchers: [\n sass: {\n DartSass,\n :install_and_run,\n [:default, ~w(--embed-source-map --source-map-urls=absolute --watch)]\n }\n ],\\2"
)
end
Rewrite.Source.update(source, :content, fn _ -> new_content end)
end)
end
# ── JavaScript setup ─────────────────────────────────────────────────
defp setup_javascript(igniter) do
igniter
|> Igniter.include_existing_file("assets/js/app.js", required: false)
|> Igniter.update_file("assets/js/app.js", fn source ->
content = Rewrite.Source.get(source, :content)
new_content = add_uikit_js_imports(content)
Rewrite.Source.update(source, :content, fn _ -> new_content end)
end)
end
defp add_uikit_js_imports(content) do
lines = String.split(content, "\n")
last_import_idx =
lines
|> Enum.with_index()
|> Enum.filter(fn {line, _idx} -> Regex.match?(~r/^import\s/, line) end)
|> List.last()
|> case do
{_line, idx} -> idx
nil -> -1
end
uikit_lines = [
"",
"import UIkit from \"../../deps/elixir_uikit/priv/vendor/uikit/js/uikit.min.js\"",
"import Icons from \"../../deps/elixir_uikit/priv/vendor/uikit/js/uikit-icons.min.js\"",
"",
"UIkit.use(Icons)",
"window.UIkit = UIkit",
"",
"import UikitHooks, { onBeforeElUpdated } from \"../../deps/elixir_uikit/priv/static/js/elixir_uikit.js\""
]
{before, after_lines} = Enum.split(lines, last_import_idx + 1)
new_content = Enum.join(before ++ uikit_lines ++ after_lines, "\n")
new_content =
cond do
Regex.match?(~r/hooks:\s*\{\.\.\./, new_content) ->
Regex.replace(
~r/hooks:\s*(\{[^}]*\})/,
new_content,
fn _, inner -> "hooks: {#{String.slice(inner, 1..-2//1)}, ...UikitHooks}" end
)
Regex.match?(~r/hooks:\s*[a-zA-Z]/, new_content) ->
Regex.replace(
~r/hooks:\s*([a-zA-Z]\w*)/,
new_content,
"hooks: {...\\1, ...UikitHooks}"
)
Regex.match?(~r/new\s+LiveSocket\s*\(/, new_content) ->
Regex.replace(
~r/(new\s+LiveSocket\s*\(\s*"[^"]+"\s*,\s*Socket\s*,\s*\{)/,
new_content,
"\\1\n hooks: UikitHooks,"
)
true ->
new_content
end
# Inject the dom callback for UIkit LiveView compatibility
if Regex.match?(~r/dom:\s*\{/, new_content) do
# dom option already exists — user must integrate manually
new_content
else
Regex.replace(
~r/(new\s+LiveSocket\s*\(\s*"[^"]+"\s*,\s*Socket\s*,\s*\{)/,
new_content,
"\\1\n dom: { onBeforeElUpdated },"
)
end
end
# ── Stylesheet setup ─────────────────────────────────────────────────
defp setup_stylesheet(igniter) do
css_path = "assets/css/app.css"
igniter = Igniter.include_existing_file(igniter, css_path, required: false)
igniter =
if Igniter.exists?(igniter, css_path) do
%{igniter | rms: [css_path | igniter.rms]}
else
igniter
end
Igniter.create_or_update_file(
igniter,
"assets/css/app.scss",
uikit_scss_content(),
fn source ->
content = Rewrite.Source.get(source, :content)
new_content = uikit_scss_content() <> "\n" <> content
Rewrite.Source.update(source, :content, fn _ -> new_content end)
end
)
end
defp uikit_scss_content do
String.trim_leading("""
// UIkit (loaded via dart_sass --load-path)
@import "variables-theme";
@import "mixins-theme";
@import "uikit-theme";
/* Add your custom styles below */
""")
end
# ── Component imports ────────────────────────────────────────────────
defp add_component_imports(igniter) do
web_module = Igniter.Libs.Phoenix.web_module(igniter)
Igniter.Project.Module.find_and_update_module!(igniter, web_module, fn zipper ->
case Igniter.Code.Function.move_to_def(zipper, :html_helpers, 0) do
{:ok, zipper} ->
{:ok,
Igniter.Code.Common.add_code(zipper, """
import Uikit.Components
import Uikit.FormComponents
""")}
:error ->
{:ok, zipper}
end
end)
end
end
else
defmodule Mix.Tasks.Uikit.Setup do
@shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use"
@moduledoc __MODULE__.Docs.long_doc()
use Mix.Task
@impl Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'uikit.setup' requires igniter. Please install igniter and try again.
For more information, see: https://hexdocs.pm/igniter/readme.html#installation
""")
exit({:shutdown, 1})
end
end
end