Skip to content

Commit 5bc79db

Browse files
committed
Add Volt support to phx.new
1 parent 8cbe817 commit 5bc79db

18 files changed

Lines changed: 309 additions & 59 deletions

File tree

installer/lib/mix/tasks/phx.new.ex

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ defmodule Mix.Tasks.Phx.New do
3838
Please check the adapter docs for more information
3939
and requirements. Defaults to "bandit".
4040
41-
* `--no-assets` - equivalent to `--no-esbuild` and `--no-tailwind`
41+
* `--no-assets` - equivalent to `--no-esbuild`, `--no-tailwind`, and disables Volt
4242
4343
* `--no-dashboard` - do not include Phoenix.LiveDashboard
4444
@@ -63,6 +63,8 @@ defmodule Mix.Tasks.Phx.New do
6363
are left-in as reference for the subsequent styling of your layout
6464
and components
6565
66+
* `--volt` - use Volt instead of esbuild and tailwind for assets
67+
6668
* `--binary-id` - use `binary_id` as primary key type in Ecto schemas
6769
6870
* `--verbose` - use verbose output
@@ -158,6 +160,7 @@ defmodule Mix.Tasks.Phx.New do
158160
assets: :boolean,
159161
esbuild: :boolean,
160162
tailwind: :boolean,
163+
volt: :boolean,
161164
ecto: :boolean,
162165
app: :string,
163166
module: :string,
@@ -266,6 +269,7 @@ defmodule Mix.Tasks.Phx.New do
266269

267270
defp validate_project(%Project{opts: opts} = project, path) do
268271
check_app_name!(project.app, !!opts[:app])
272+
check_volt_options!(opts)
269273
check_directory_existence!(Map.fetch!(project, path))
270274
check_module_name_validity!(project.root_mod)
271275
check_module_name_availability!(project.root_mod)
@@ -297,17 +301,20 @@ defmodule Mix.Tasks.Phx.New do
297301

298302
if mix_step == [] do
299303
builders = Keyword.fetch!(project.binding, :asset_builders)
304+
installable_builders = Enum.reject(builders, &(&1 == :volt))
300305

301-
if builders != [] do
306+
if installable_builders != [] do
302307
Mix.shell().info([:green, "* running ", :reset, "mix assets.setup"])
303308

304309
# First compile only builders so we can install in parallel
305310
# TODO: Once we require Erlang/OTP 28, jason may no longer be required
306-
cmd(project, "mix deps.compile jason #{Enum.join(builders, " ")}", log: false)
311+
cmd(project, "mix deps.compile jason #{Enum.join(installable_builders, " ")}",
312+
log: false
313+
)
307314
end
308315

309316
tasks =
310-
Enum.map(builders, fn builder ->
317+
Enum.map(installable_builders, fn builder ->
311318
cmd = "mix do loadpaths --no-compile --no-listeners + #{builder}.install"
312319
Task.async(fn -> cmd(project, cmd, log: false, cd: project.web_path) end)
313320
end)
@@ -340,6 +347,13 @@ defmodule Mix.Tasks.Phx.New do
340347

341348
defp maybe_cd(path, func), do: path && File.cd!(path, func)
342349

350+
defp check_volt_options!(opts) do
351+
if opts[:assets] != false && opts[:volt] &&
352+
(opts[:esbuild] == false || opts[:tailwind] == false) do
353+
Mix.raise("--volt cannot be combined with --no-esbuild or --no-tailwind")
354+
end
355+
end
356+
343357
defp install_mix(project, install?) do
344358
if install? do
345359
cmd(project, "mix deps.get")

installer/lib/phx_new/generator.ex

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,9 @@ defmodule Phx.New.Generator do
244244
dashboard = Keyword.get(opts, :dashboard, true)
245245
gettext = Keyword.get(opts, :gettext, true)
246246
assets = Keyword.get(opts, :assets, true)
247-
esbuild = Keyword.get(opts, :esbuild, assets)
248-
tailwind = Keyword.get(opts, :tailwind, assets)
247+
volt = assets and Keyword.get(opts, :volt, false)
248+
esbuild = not volt and Keyword.get(opts, :esbuild, assets)
249+
tailwind = not volt and Keyword.get(opts, :tailwind, assets)
249250
mailer = Keyword.get(opts, :mailer, true)
250251
dev = Keyword.get(opts, :dev, false)
251252
from_elixir_install = Keyword.get(opts, :from_elixir_install, false)
@@ -282,6 +283,26 @@ defmodule Phx.New.Generator do
282283
:error -> adapter_config
283284
end
284285

286+
asset_builders =
287+
if volt,
288+
do: [:volt],
289+
else: Enum.filter([tailwind && :tailwind, esbuild && :esbuild], & &1)
290+
291+
{assets_setup, assets_build, assets_deploy} =
292+
if volt do
293+
volt_profile = if project.in_umbrella?, do: " #{project.web_app}", else: ""
294+
295+
{[],
296+
["compile", "volt.build#{volt_profile} --tailwind"],
297+
["volt.build#{volt_profile} --tailwind", "phx.digest"]}
298+
else
299+
{
300+
Enum.map(asset_builders, &"#{&1}.install --if-missing"),
301+
["compile" | Enum.map(asset_builders, &"#{&1} #{project.web_app}")],
302+
Enum.map(asset_builders, &"#{&1} #{project.web_app} --minify") ++ ["phx.digest"]
303+
}
304+
end
305+
285306
binding = [
286307
app_name: project.app,
287308
app_module: inspect(project.app_mod),
@@ -301,9 +322,13 @@ defmodule Phx.New.Generator do
301322
signing_salt: random_string(8),
302323
lv_signing_salt: random_string(8),
303324
in_umbrella: project.in_umbrella?,
304-
asset_builders: Enum.filter([tailwind && :tailwind, esbuild && :esbuild], & &1),
305-
javascript: esbuild,
306-
css: tailwind,
325+
asset_builders: asset_builders,
326+
assets_setup: assets_setup,
327+
assets_build: assets_build,
328+
assets_deploy: assets_deploy,
329+
javascript: volt or esbuild,
330+
css: volt or tailwind,
331+
volt: volt,
307332
mailer: mailer,
308333
ecto: ecto,
309334
html: html,

installer/lib/phx_new/interactive.ex

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ defmodule Phx.New.Interactive do
1515
{"api", "API-only"}
1616
]
1717

18+
@asset_options [
19+
{"esbuild", "Esbuild + Tailwind"},
20+
{"volt", "Volt"},
21+
{"none", "None"}
22+
]
23+
1824
def run do
1925
catch_abort(fn ->
2026
info([:green, "\nInitialize your Phoenix project (press Ctrl+C to abort)\n", :reset])
@@ -29,7 +35,7 @@ defmodule Phx.New.Interactive do
2935
yes?("Use binary_id as primary key type?", false)
3036
end
3137

32-
%{html: html, live: live, assets: assets} = prompt_web()
38+
%{html: html, live: live, assets: assets, volt: volt} = prompt_web()
3339
dashboard = yes?("Include LiveDashboard (monitoring)?")
3440
mailer = yes?("Include Swoosh (mailer)?")
3541
gettext = yes?("Include Gettext (i18n)?")
@@ -43,7 +49,8 @@ defmodule Phx.New.Interactive do
4349
dashboard: dashboard,
4450
mailer: mailer,
4551
gettext: gettext,
46-
assets: assets
52+
assets: assets,
53+
volt: volt
4754
]
4855
|> maybe_put_database(database)
4956

@@ -78,13 +85,19 @@ defmodule Phx.New.Interactive do
7885

7986
defp prompt_web do
8087
case prompt_choice("Web interface?", @web_options, "live") do
81-
"api" -> %{html: false, live: false, assets: false}
82-
"html" -> %{html: true, live: false, assets: prompt_assets?()}
83-
"live" -> %{html: true, live: true, assets: prompt_assets?()}
88+
"api" -> %{html: false, live: false, assets: false, volt: false}
89+
"html" -> Map.merge(%{html: true, live: false}, prompt_assets())
90+
"live" -> Map.merge(%{html: true, live: true}, prompt_assets())
8491
end
8592
end
8693

87-
defp prompt_assets?, do: yes?("Include Esbuild + Tailwind?")
94+
defp prompt_assets do
95+
case prompt_choice("Assets?", @asset_options, "esbuild") do
96+
"esbuild" -> %{assets: true, volt: false}
97+
"volt" -> %{assets: true, volt: true}
98+
"none" -> %{assets: false, volt: false}
99+
end
100+
end
88101

89102
defp prompt_choice(question, choices, default) do
90103
info("\n#{question}\n")
@@ -146,12 +159,14 @@ defmodule Phx.New.Interactive do
146159
end
147160

148161
defp web_summary(opts) do
149-
case {opts[:html], opts[:live], opts[:assets]} do
150-
{true, true, true} -> "LiveView"
151-
{true, true, false} -> "LiveView (no Esbuild, no Tailwind)"
152-
{true, false, true} -> "HTML"
153-
{true, false, false} -> "HTML (no Esbuild, no Tailwind)"
154-
{false, _, _} -> "API-only"
162+
case {opts[:html], opts[:live], opts[:assets], opts[:volt]} do
163+
{true, true, true, true} -> "LiveView (Volt)"
164+
{true, true, true, _} -> "LiveView"
165+
{true, true, false, _} -> "LiveView (no Esbuild, no Tailwind)"
166+
{true, false, true, true} -> "HTML (Volt)"
167+
{true, false, true, _} -> "HTML"
168+
{true, false, false, _} -> "HTML (no Esbuild, no Tailwind)"
169+
{false, _, _, _} -> "API-only"
155170
end
156171
end
157172

@@ -166,7 +181,8 @@ defmodule Phx.New.Interactive do
166181
{!opts[:dashboard], "--no-dashboard"},
167182
{!opts[:mailer], "--no-mailer"},
168183
{!opts[:gettext], "--no-gettext"},
169-
{!opts[:assets], "--no-assets"}
184+
{!opts[:assets], "--no-assets"},
185+
{opts[:volt], "--volt"}
170186
],
171187
condition,
172188
do: flag

installer/templates/phx_assets/app.js.eex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
//
1515
// import "some-package"
1616
//
17-
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
17+
// If you have dependencies that try to import CSS, <%= if @volt, do: "Volt", else: "esbuild" %> will generate a separate `app.css` file.
1818
// To load it, simply add a second `<link>` to your `root.html.heex` file.
1919
<%= if @html do %>
2020
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
@@ -52,7 +52,7 @@ import "phoenix_html"
5252
// 1. stream server logs to the browser console
5353
// 2. click on elements to jump to their definitions in your code editor
5454
//
55-
<%= @live_comment %>if (process.env.NODE_ENV === "development") {
55+
<%= @live_comment %>if (<%= if @volt, do: "import.meta.env.DEV", else: "process.env.NODE_ENV === \"development\"" %>) {
5656
<%= @live_comment %> window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
5757
<%= @live_comment %> // Enable server log streaming to client.
5858
<%= @live_comment %> // Disable with reloader.disableServerLogs()

installer/templates/phx_assets/tsconfig.json.eex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// This file is needed on most editors to enable the intelligent autocompletion
22
// of LiveView's JavaScript API methods. You can safely delete it if you don't need it.
33
//
4-
// Note: This file assumes a basic esbuild setup without node_modules.
5-
// We include a generic paths alias to deps to mimic how esbuild resolves
4+
// Note: This file assumes a basic <%= if @volt, do: "Volt", else: "esbuild" %> setup without node_modules.
5+
// We include a generic paths alias to deps to mimic how <%= if @volt, do: "Volt", else: "esbuild" %> resolves
66
// the Phoenix and LiveView JavaScript assets.
77
// If you have a package.json in your project, you should remove the
88
// paths configuration and instead add the phoenix dependencies to the

installer/templates/phx_single/config/config.exs.eex

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,23 @@ config :<%= @app_name %>, <%= @endpoint_module %>,
3131
#
3232
# For production it's recommended to configure a different adapter
3333
# at the `config/runtime.exs`.
34-
config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Local<% end %><%= if @javascript do %>
34+
config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Local<% end %><%= if @volt do %>
35+
36+
# Configure Volt
37+
config :volt,
38+
entry: "assets/js/app.js",
39+
outdir: "priv/static/assets",
40+
target: :es2022,
41+
hash: false,
42+
sourcemap: :hidden,
43+
resolve_dirs: [Path.expand("../deps", __DIR__), Mix.Project.build_path()],
44+
tailwind: [
45+
css: "assets/css/app.css",
46+
sources: [
47+
%{base: "lib/", pattern: "**/*.{ex,heex,eex}"},
48+
%{base: "assets/", pattern: "**/*.{js,ts,jsx,tsx}"}
49+
]
50+
]<% else %><%= if @javascript do %>
3551

3652
# Configure esbuild (the version is required)
3753
config :esbuild,
@@ -52,7 +68,7 @@ config :tailwind,
5268
--output=priv/static/assets/css/app.css
5369
),
5470
cd: Path.expand("..<%= if @in_umbrella, do: "/apps/#{@app_name}" %>", __DIR__),
55-
]<% end %>
71+
]<% end %><% end %>
5672

5773
# Configure Elixir's Logger
5874
config :logger, :default_formatter,

installer/templates/phx_single/config/dev.exs.eex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@ config :<%= @app_name %>, <%= @endpoint_module %>,<%= if @inside_docker_env? do
1818
code_reloader: true,
1919
debug_errors: true,
2020
secret_key_base: "<%= @secret_key_base_dev %>",
21-
watchers: <%= if @javascript or @css do %>[<%= if @javascript do %>
21+
watchers: <%= if @volt do %>[
22+
volt: {Mix.Tasks.Volt.Dev, :run, [~w(--tailwind)]}
23+
]<% else %><%= if @javascript or @css do %>[<%= if @javascript do %>
2224
esbuild: {Esbuild, :install_and_run, [:<%= @app_name %>, ~w(--sourcemap=inline --watch)]}<%= if @css, do: "," %><% end %><%= if @css do %>
2325
tailwind: {Tailwind, :install_and_run, [:<%= @app_name %>, ~w(--watch)]}<% end %>
24-
]<% else %>[]<% end %>
26+
]<% else %>[]<% end %><% end %><%= if @volt do %>
27+
28+
config :volt, :server,
29+
prefix: "/assets",
30+
watch_dirs: ["lib/"]<% end %>
2531

2632
# ## SSL Support
2733
#
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[
22
import_deps: [<%= if @ecto do %>:ecto, :ecto_sql, <% end %>:phoenix],<%= if @ecto do %>
3-
subdirectories: ["priv/*/migrations"],<% end %><%= if @html do %>
4-
plugins: [Phoenix.LiveView.HTMLFormatter],<% end %>
5-
inputs: [<%= if @html do %>"*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"<% else %>"*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"<% end %><%= if @ecto do %>, "priv/*/seeds.exs"<% end %>]
3+
subdirectories: ["priv/*/migrations"],<% end %><%= if @html or @volt do %>
4+
plugins: [<%= Enum.join(Enum.filter([@html && "Phoenix.LiveView.HTMLFormatter", @volt && "Volt.Formatter"], & &1), ", ") %>],<% end %>
5+
inputs: [<%= if @html do %>"*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"<% else %>"*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"<% end %><%= if @ecto do %>, "priv/*/seeds.exs"<% end %><%= if @volt do %>, "assets/**/*.{js,ts,jsx,tsx}"<% end %>]
66
]

installer/templates/phx_single/mix.exs.eex

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ defmodule <%= @app_module %>.MixProject do
5252
{:phoenix_live_reload, "~> 1.2", only: :dev},
5353
{:phoenix_live_view, "~> 1.1.0"},
5454
{:lazy_html, ">= 0.1.0", only: :test},<% end %><%= if @dashboard do %>
55-
{:phoenix_live_dashboard, "~> 0.8.3"},<% end %><%= if @javascript do %>
55+
{:phoenix_live_dashboard, "~> 0.8.3"},<% end %><%= if @volt do %>
56+
{:volt, "~> 0.11.0"},<% else %><%= if @javascript do %>
5657
{:esbuild, "~> 0.10", runtime: Mix.env() == :dev},<% end %><%= if @css do %>
57-
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
58+
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},<% end %><% end %><%= if @css do %>
5859
{:heroicons,
5960
github: "tailwindlabs/heroicons",
6061
tag: "v2.2.0",
@@ -85,11 +86,9 @@ defmodule <%= @app_module %>.MixProject do
8586
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
8687
"ecto.reset": ["ecto.drop", "ecto.setup"],
8788
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]<% end %><%= if @asset_builders != [] do %>,
88-
"assets.setup": <%= inspect Enum.map(@asset_builders, &"#{&1}.install --if-missing") %>,
89-
"assets.build": <%= inspect ["compile" | Enum.map(@asset_builders, &"#{&1} #{@app_name}")] %>,
90-
"assets.deploy": [
91-
<%= Enum.map(@asset_builders, &" \"#{&1} #{@app_name} --minify\",\n") ++ [" \"phx.digest\""] %>
92-
]<% end %>,
89+
"assets.setup": <%= inspect @assets_setup %>,
90+
"assets.build": <%= inspect @assets_build %>,
91+
"assets.deploy": <%= inspect @assets_deploy %><% end %>,
9392
precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"]
9493
]
9594
end

installer/templates/phx_umbrella/apps/app_name_web/config/config.exs.eex

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,23 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,
1515
layout: false
1616
],
1717
pubsub_server: <%= @app_module %>.PubSub,
18-
live_view: [signing_salt: "<%= @lv_signing_salt %>"]<%= if @javascript do %>
18+
live_view: [signing_salt: "<%= @lv_signing_salt %>"]<%= if @volt do %>
19+
20+
# Configure Volt
21+
config :volt, :<%= @web_app_name %>,
22+
entry: "assets/js/app.js",
23+
outdir: "priv/static/assets",
24+
target: :es2022,
25+
hash: false,
26+
sourcemap: :hidden,
27+
resolve_dirs: [Path.expand("../deps", __DIR__), Mix.Project.build_path()],
28+
tailwind: [
29+
css: "assets/css/app.css",
30+
sources: [
31+
%{base: "lib/", pattern: "**/*.{ex,heex,eex}"},
32+
%{base: "assets/", pattern: "**/*.{js,ts,jsx,tsx}"}
33+
]
34+
]<% else %><%= if @javascript do %>
1935

2036
# Configure esbuild (the version is required)
2137
config :esbuild,
@@ -36,4 +52,4 @@ config :tailwind,
3652
--output=priv/static/assets/css/app.css
3753
),
3854
cd: Path.expand("../apps/<%= @web_app_name %>", __DIR__)
39-
]<% end %>
55+
]<% end %><% end %>

0 commit comments

Comments
 (0)