Current section

Files

Jump to
phoenix_vite lib phoenix_vite igniter.ex
Raw

lib/phoenix_vite/igniter.ex

if Code.ensure_loaded?(Igniter) do
defmodule PhoenixVite.Igniter do
@moduledoc false
alias Sourceror.Zipper
@doc """
Create a minimal vite config at `assets/vite.config.mjs`
"""
def create_vite_config(igniter) do
Igniter.create_new_file(igniter, "assets/vite.config.mjs", """
import { defineConfig } from 'vite'
import { phoenixVitePlugin } from 'phoenix_vite'
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
server: {
port: 5173,
strictPort: true,
cors: { origin: "http://localhost:4000" },
},
optimizeDeps: {
// https://vitejs.dev/guide/dep-pre-bundling#monorepos-and-linked-dependencies
include: ["phoenix", "phoenix_html", "phoenix_live_view"],
},
build: {
manifest: true,
rollupOptions: {
input: ["js/app.js", "css/app.css"],
},
outDir: "../priv/static",
emptyOutDir: true,
},
// LV Colocated JS and Hooks
// https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.ColocatedJS.html#module-internals
resolve: {
alias: {
"@": ".",
"phoenix-colocated": `${process.env.MIX_BUILD_PATH}/phoenix-colocated`,
},
},
plugins: [
tailwindcss(),
phoenixVitePlugin({
pattern: /\\.(ex|heex)$/
})
]
});
""")
end
@doc """
Updates dev.exs up with endpoints static_url configuration pointing to vite dev server.
"""
def configure_dev_server_static_url_for_development(igniter, app_name, endpoint) do
Igniter.Project.Config.configure(igniter, "dev.exs", app_name, [endpoint, :static_url],
host: "localhost",
port: 5173
)
end
@doc """
Wrap known generated static assets paths with `static_url` calls
"""
def update_generator_static_assets(igniter, web_module, endpoint) do
web_folder = Macro.underscore(web_module)
app_layout_1_8 = Path.join(["lib", web_folder, "components/layouts.ex"])
app_layout_1_7 = Path.join(["lib", web_folder, "components/layouts/app.html.heex"])
cond do
Igniter.exists?(igniter, app_layout_1_8) ->
update_logo_path_with_static_url(igniter, endpoint, app_layout_1_8)
Igniter.exists?(igniter, app_layout_1_7) ->
update_logo_path_with_static_url(igniter, endpoint, app_layout_1_7)
true ->
igniter
end
end
defp update_logo_path_with_static_url(igniter, endpoint, path) do
Igniter.update_file(igniter, path, fn source ->
Rewrite.Source.update(source, :content, fn content ->
String.replace(
content,
~s|~p"/images/logo.svg"|,
~s|static_url(#{inspect(endpoint)}, ~p"/images/logo.svg")|
)
end)
end)
end
@doc """
Disable phoenix cache_static_manifest in favor of using vites manifest.
"""
def use_only_vite_assets_caching(igniter, app_name, endpoint) do
igniter
|> Igniter.Project.Config.configure("prod.exs", app_name, [endpoint], [],
updater: fn zipper ->
with :error <- Igniter.Code.Keyword.remove_keyword_key(zipper, :cache_static_manifest) do
{:ok, zipper}
end
end
)
|> Igniter.Project.Config.configure_runtime_env(
:prod,
app_name,
[endpoint, :cache_static_manifest_latest],
{:code,
Sourceror.parse_string!("PhoenixVite.cache_static_manifest_latest(#{inspect(app_name)})")}
)
end
@doc """
Remove assets patterns from phoenix_live_reload
Assets reloading is handled by the vite dev server, not phoenix_live_reload
"""
def use_only_vite_reloading_for_assets(igniter, app_name, endpoint) do
Igniter.update_elixir_file(igniter, "config/dev.exs", fn zipper ->
with {:ok, zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:config,
3,
fn function_call ->
Igniter.Code.Function.argument_equals?(function_call, 0, app_name) &&
Igniter.Code.Function.argument_equals?(function_call, 1, endpoint) &&
Igniter.Code.Function.argument_matches_predicate?(
function_call,
2,
fn zipper ->
Igniter.Code.Keyword.keyword_has_path?(zipper, [:live_reload, :patterns])
end
)
end
) do
Igniter.Code.Function.update_nth_argument(zipper, 2, fn zipper ->
Igniter.Code.Keyword.put_in_keyword(
zipper,
[:live_reload, :patterns],
[],
fn zipper ->
Igniter.Code.List.remove_from_list(zipper, fn zipper ->
with {:sigil_r, _, [{:<<>>, _, [regex]}, []]} <- Zipper.node(zipper),
true <- String.contains?(regex, "priv/static") do
true
else
_ -> false
end
end)
end
)
end)
end
end)
end
@doc """
Add module preload polyfill to JS.
See https://vite.dev/guide/backend-integration.html
"""
def add_module_preload_polyfill(igniter) do
Igniter.update_file(igniter, "assets/js/app.js", fn source ->
Rewrite.Source.update(source, :content, fn content ->
~s|import "vite/modulepreload-polyfill";\n| <> content
end)
end)
end
@doc """
Move all static files to `assets/public` to be handles by vite.
"""
def use_vite_public_folder_for_static_assets(igniter) do
igniter
|> Igniter.mkdir("assets/public")
|> Igniter.include_glob("priv/static/**/*")
|> then(fn igniter ->
Enum.reduce(igniter.rewrite.sources, igniter, fn {path, _}, igniter ->
case Path.split(path) do
["priv", "static", "assets" | _] ->
igniter
["priv", "static" | rest] ->
Igniter.move_file(igniter, path, Path.join(["assets", "public" | rest]))
_ ->
igniter
end
end)
end)
|> Igniter.update_file(".gitignore", fn source ->
Rewrite.Source.update(source, :content, fn content ->
String.replace(
content,
"""
/priv/static/assets/
# Ignore digested assets cache.
/priv/static/cache_manifest.json
""",
"""
/priv/static/*
"""
)
end)
end)
end
@doc """
Handle browsers favicon requests by redirecting to the vite dev server.
"""
def add_favicon_handling_plug(igniter, endpoint) do
Igniter.Project.Module.find_and_update_module!(igniter, endpoint, fn zipper ->
with {:ok, zipper} <-
Igniter.Code.Common.move_to_last(zipper, &match?({:use, _, _}, Zipper.node(&1))),
zipper = Igniter.Code.Common.add_code(zipper, "import PhoenixVite.Plug"),
{:ok, zipper} <-
Igniter.Code.Common.move_to_last(
zipper,
&match?({:socket, _, [_, _, _]}, Zipper.node(&1))
) do
plug = """
plug :favicon, dev_server: {PhoenixVite.Components, :has_vite_watcher?, [__MODULE__]}
"""
{:ok, Igniter.Code.Common.add_code(zipper, plug)}
end
end)
end
@doc """
Replace static assets relations in root layout with dynamic vite ones.
"""
def link_root_layout_to_vite(igniter, app_name, endpoint, web_module) do
web_folder = Macro.underscore(web_module)
root_layout = Path.join(["lib", web_folder, "components/layouts/root.html.heex"])
Igniter.update_file(igniter, root_layout, fn source ->
Rewrite.Source.update(source, :content, fn content ->
String.replace(
content,
"""
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
<script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
</script>
""",
"""
<PhoenixVite.Components.assets
names={["js/app.js", "css/app.css"]}
manifest={{#{inspect(app_name)}, "priv/static/.vite/manifest.json"}}
dev_server={PhoenixVite.Components.has_vite_watcher?(#{inspect(endpoint)})}
to_url={fn p -> static_url(@conn, p) end}
/>
"""
)
end)
end)
end
@doc """
Remove esbuild and tailwind applications and integrations
"""
def remove_default_assets_handling(igniter, app_name, endpoint) do
Enum.reduce([:esbuild, :tailwind], igniter, fn dependency, igniter ->
igniter =
if endpoint do
path = [endpoint, :watchers]
Igniter.Project.Config.configure(igniter, "dev.exs", app_name, path, [],
updater: fn zipper ->
with :error <- Igniter.Code.Keyword.remove_keyword_key(zipper, dependency) do
{:ok, zipper}
end
end
)
else
igniter
end
igniter
|> Igniter.Project.Config.remove_application_configuration("config.exs", dependency)
|> Igniter.Project.Deps.remove_dep(dependency)
|> Igniter.add_task("deps.clean", ["--unlock", "--unused", "#{dependency}"])
end)
end
@doc """
Use package.json to pull in dependencies
"""
def adjust_js_dependency_management(igniter) do
igniter
|> Igniter.create_new_file("assets/package.json", """
{
"dependencies": {
"phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view",
"topbar": "^3.0.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.0",
"daisyui": "^5.0.0",
"phoenix_vite": "file:../deps/phoenix_vite",
"tailwindcss": "^4.1.0",
"vite": "^6.3.0"
}
}
""")
|> Igniter.update_file("assets/js/app.js", fn source ->
Rewrite.Source.update(source, :content, fn content ->
String.replace(content, "../vendor/topbar", "topbar")
end)
end)
|> Igniter.update_file("assets/css/app.css", fn source ->
Rewrite.Source.update(source, :content, fn content ->
content
|> String.replace("../vendor/daisyui-theme", "daisyui/theme")
|> String.replace("../vendor/daisyui", "daisyui")
end)
end)
|> Igniter.rm("assets/vendor/topbar.js")
|> Igniter.rm("assets/vendor/daisyui.js")
|> Igniter.rm("assets/vendor/daisyui-theme.js")
end
@doc """
Add :bun dependency to project integrated with vite
"""
def add_bun(igniter, app_name, endpoint) do
igniter
|> Igniter.Project.Deps.add_dep(
{:bun, "~> 1.5 and >= 1.5.1", runtime: quote(do: Mix.env() == :dev)},
append?: true
)
|> Igniter.Project.Config.configure("config.exs", :bun, [:version], "1.2.16")
|> Igniter.Project.Config.configure(
"config.exs",
:bun,
[:assets],
{:code, Sourceror.parse_string!(~s|[args: [], cd: Path.expand("../assets", __DIR__)]|)}
)
|> Igniter.Project.Config.configure(
"config.exs",
:bun,
[:vite],
{:code,
Sourceror.parse_string!(
~s|[args: ~w(x vite), cd: Path.expand("../assets", __DIR__), env: %{"MIX_BUILD_PATH" => Mix.Project.build_path()}]|
)}
)
|> Igniter.Project.Config.configure(
"dev.exs",
app_name,
[endpoint, :watchers, :vite],
{:code, Sourceror.parse_string!(~s|{Bun, :install_and_run, [:vite, ~w(dev)]}|)}
)
|> Igniter.Project.TaskAliases.modify_existing_alias("assets.setup", fn zipper ->
alias = Sourceror.parse_string!(~s|["bun.install --if-missing", "bun assets install"]|)
{:ok, Igniter.Code.Common.replace_code(zipper, alias)}
end)
|> Igniter.Project.TaskAliases.modify_existing_alias("assets.build", fn zipper ->
alias = Sourceror.parse_string!(~s|["bun vite build"]|)
{:ok, Igniter.Code.Common.replace_code(zipper, alias)}
end)
|> Igniter.Project.TaskAliases.modify_existing_alias("assets.deploy", fn zipper ->
alias = Sourceror.parse_string!(~s|["assets.build"]|)
{:ok, Igniter.Code.Common.replace_code(zipper, alias)}
end)
|> Igniter.add_task("deps.get")
|> Igniter.add_task("assets.setup")
end
@doc """
Integrate vite with local node/npm installation
"""
def add_local_node(igniter, app_name, endpoint) do
igniter
|> Igniter.Project.Config.configure(
"config.exs",
:phoenix_vite,
[PhoenixVite.Npm, :assets],
{:code, Sourceror.parse_string!(~s|[args: [], cd: Path.expand("../assets", __DIR__)]|)}
)
|> Igniter.Project.Config.configure(
"config.exs",
:phoenix_vite,
[PhoenixVite.Npm, :vite],
{:code,
Sourceror.parse_string!(
~s|[args: ~w(exec -- vite), cd: Path.expand("../assets", __DIR__), env: %{"MIX_BUILD_PATH" => Mix.Project.build_path()}]|
)}
)
|> Igniter.Project.Config.configure(
"dev.exs",
app_name,
[endpoint, :watchers, :vite],
{:code, Sourceror.parse_string!(~s|{PhoenixVite.Npm, :run, [:vite, ~w(dev)]}|)}
)
|> Igniter.Project.TaskAliases.modify_existing_alias("assets.setup", fn zipper ->
alias = Sourceror.parse_string!(~s|["phoenix_vite.npm assets install"]|)
{:ok, Igniter.Code.Common.replace_code(zipper, alias)}
end)
|> Igniter.Project.TaskAliases.modify_existing_alias("assets.build", fn zipper ->
alias = Sourceror.parse_string!(~s|["phoenix_vite.npm vite build"]|)
{:ok, Igniter.Code.Common.replace_code(zipper, alias)}
end)
|> Igniter.Project.TaskAliases.modify_existing_alias("assets.deploy", fn zipper ->
alias = Sourceror.parse_string!(~s|["assets.build"]|)
{:ok, Igniter.Code.Common.replace_code(zipper, alias)}
end)
|> Igniter.add_task("assets.setup")
end
end
end