Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions installer/lib/mix/tasks/phx.new.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ defmodule Mix.Tasks.Phx.New do
Please check the adapter docs for more information
and requirements. Defaults to "bandit".

* `--no-assets` - equivalent to `--no-esbuild` and `--no-tailwind`
* `--no-assets` - equivalent to `--no-esbuild`, `--no-tailwind`, and disables Volt

* `--no-dashboard` - do not include Phoenix.LiveDashboard

Expand All @@ -63,6 +63,8 @@ defmodule Mix.Tasks.Phx.New do
are left-in as reference for the subsequent styling of your layout
and components

* `--volt` - use Volt instead of esbuild and tailwind for assets

* `--binary-id` - use `binary_id` as primary key type in Ecto schemas

* `--verbose` - use verbose output
Expand Down Expand Up @@ -158,6 +160,7 @@ defmodule Mix.Tasks.Phx.New do
assets: :boolean,
esbuild: :boolean,
tailwind: :boolean,
volt: :boolean,
ecto: :boolean,
app: :string,
module: :string,
Expand Down Expand Up @@ -266,6 +269,7 @@ defmodule Mix.Tasks.Phx.New do

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

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

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

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

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

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

defp check_volt_options!(opts) do
if opts[:assets] != false && opts[:volt] &&
(opts[:esbuild] == false || opts[:tailwind] == false) do
Mix.raise("--volt cannot be combined with --no-esbuild or --no-tailwind")
end
end

defp install_mix(project, install?) do
if install? do
cmd(project, "mix deps.get")
Expand Down
35 changes: 30 additions & 5 deletions installer/lib/phx_new/generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,9 @@ defmodule Phx.New.Generator do
dashboard = Keyword.get(opts, :dashboard, true)
gettext = Keyword.get(opts, :gettext, true)
assets = Keyword.get(opts, :assets, true)
esbuild = Keyword.get(opts, :esbuild, assets)
tailwind = Keyword.get(opts, :tailwind, assets)
volt = assets and Keyword.get(opts, :volt, false)
esbuild = not volt and Keyword.get(opts, :esbuild, assets)
tailwind = not volt and Keyword.get(opts, :tailwind, assets)
mailer = Keyword.get(opts, :mailer, true)
dev = Keyword.get(opts, :dev, false)
from_elixir_install = Keyword.get(opts, :from_elixir_install, false)
Expand Down Expand Up @@ -282,6 +283,26 @@ defmodule Phx.New.Generator do
:error -> adapter_config
end

asset_builders =
if volt,
do: [:volt],
else: Enum.filter([tailwind && :tailwind, esbuild && :esbuild], & &1)

{assets_setup, assets_build, assets_deploy} =
if volt do
volt_profile = if project.in_umbrella?, do: " #{project.web_app}", else: ""

{[],
["compile", "volt.build#{volt_profile} --tailwind"],
["volt.build#{volt_profile} --tailwind", "phx.digest"]}
else
{
Enum.map(asset_builders, &"#{&1}.install --if-missing"),
["compile" | Enum.map(asset_builders, &"#{&1} #{project.web_app}")],
Enum.map(asset_builders, &"#{&1} #{project.web_app} --minify") ++ ["phx.digest"]
}
end

binding = [
app_name: project.app,
app_module: inspect(project.app_mod),
Expand All @@ -301,9 +322,13 @@ defmodule Phx.New.Generator do
signing_salt: random_string(8),
lv_signing_salt: random_string(8),
in_umbrella: project.in_umbrella?,
asset_builders: Enum.filter([tailwind && :tailwind, esbuild && :esbuild], & &1),
javascript: esbuild,
css: tailwind,
asset_builders: asset_builders,
assets_setup: assets_setup,
assets_build: assets_build,
assets_deploy: assets_deploy,
javascript: volt or esbuild,
css: volt or tailwind,
volt: volt,
mailer: mailer,
ecto: ecto,
html: html,
Expand Down
42 changes: 29 additions & 13 deletions installer/lib/phx_new/interactive.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ defmodule Phx.New.Interactive do
{"api", "API-only"}
]

@asset_options [
{"esbuild", "Esbuild + Tailwind"},
{"volt", "Volt"},
{"none", "None"}
]

def run do
catch_abort(fn ->
info([:green, "\nInitialize your Phoenix project (press Ctrl+C to abort)\n", :reset])
Expand All @@ -29,7 +35,7 @@ defmodule Phx.New.Interactive do
yes?("Use binary_id as primary key type?", false)
end

%{html: html, live: live, assets: assets} = prompt_web()
%{html: html, live: live, assets: assets, volt: volt} = prompt_web()
dashboard = yes?("Include LiveDashboard (monitoring)?")
mailer = yes?("Include Swoosh (mailer)?")
gettext = yes?("Include Gettext (i18n)?")
Expand All @@ -43,7 +49,8 @@ defmodule Phx.New.Interactive do
dashboard: dashboard,
mailer: mailer,
gettext: gettext,
assets: assets
assets: assets,
volt: volt
]
|> maybe_put_database(database)

Expand Down Expand Up @@ -78,13 +85,19 @@ defmodule Phx.New.Interactive do

defp prompt_web do
case prompt_choice("Web interface?", @web_options, "live") do
"api" -> %{html: false, live: false, assets: false}
"html" -> %{html: true, live: false, assets: prompt_assets?()}
"live" -> %{html: true, live: true, assets: prompt_assets?()}
"api" -> %{html: false, live: false, assets: false, volt: false}
"html" -> Map.merge(%{html: true, live: false}, prompt_assets())
"live" -> Map.merge(%{html: true, live: true}, prompt_assets())
end
end

defp prompt_assets?, do: yes?("Include Esbuild + Tailwind?")
defp prompt_assets do
case prompt_choice("Assets?", @asset_options, "esbuild") do
"esbuild" -> %{assets: true, volt: false}
"volt" -> %{assets: true, volt: true}
"none" -> %{assets: false, volt: false}
end
end

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

defp web_summary(opts) do
case {opts[:html], opts[:live], opts[:assets]} do
{true, true, true} -> "LiveView"
{true, true, false} -> "LiveView (no Esbuild, no Tailwind)"
{true, false, true} -> "HTML"
{true, false, false} -> "HTML (no Esbuild, no Tailwind)"
{false, _, _} -> "API-only"
case {opts[:html], opts[:live], opts[:assets], opts[:volt]} do
{true, true, true, true} -> "LiveView (Volt)"
{true, true, true, _} -> "LiveView"
{true, true, false, _} -> "LiveView (no Esbuild, no Tailwind)"
{true, false, true, true} -> "HTML (Volt)"
{true, false, true, _} -> "HTML"
{true, false, false, _} -> "HTML (no Esbuild, no Tailwind)"
{false, _, _, _} -> "API-only"
end
end

Expand All @@ -166,7 +181,8 @@ defmodule Phx.New.Interactive do
{!opts[:dashboard], "--no-dashboard"},
{!opts[:mailer], "--no-mailer"},
{!opts[:gettext], "--no-gettext"},
{!opts[:assets], "--no-assets"}
{!opts[:assets], "--no-assets"},
{opts[:volt], "--volt"}
],
condition,
do: flag
Expand Down
4 changes: 2 additions & 2 deletions installer/templates/phx_assets/app.js.eex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//
// import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
// If you have dependencies that try to import CSS, <%= if @volt, do: "Volt", else: "esbuild" %> will generate a separate `app.css` file.
// To load it, simply add a second `<link>` to your `root.html.heex` file.
<%= if @html do %>
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
Expand Down Expand Up @@ -52,7 +52,7 @@ import "phoenix_html"
// 1. stream server logs to the browser console
// 2. click on elements to jump to their definitions in your code editor
//
<%= @live_comment %>if (process.env.NODE_ENV === "development") {
<%= @live_comment %>if (<%= if @volt, do: "import.meta.env.DEV", else: "process.env.NODE_ENV === \"development\"" %>) {
<%= @live_comment %> window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
<%= @live_comment %> // Enable server log streaming to client.
<%= @live_comment %> // Disable with reloader.disableServerLogs()
Expand Down
4 changes: 2 additions & 2 deletions installer/templates/phx_assets/tsconfig.json.eex
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// This file is needed on most editors to enable the intelligent autocompletion
// of LiveView's JavaScript API methods. You can safely delete it if you don't need it.
//
// Note: This file assumes a basic esbuild setup without node_modules.
// We include a generic paths alias to deps to mimic how esbuild resolves
// Note: This file assumes a basic <%= if @volt, do: "Volt", else: "esbuild" %> setup without node_modules.
// We include a generic paths alias to deps to mimic how <%= if @volt, do: "Volt", else: "esbuild" %> resolves
// the Phoenix and LiveView JavaScript assets.
// If you have a package.json in your project, you should remove the
// paths configuration and instead add the phoenix dependencies to the
Expand Down
20 changes: 18 additions & 2 deletions installer/templates/phx_single/config/config.exs.eex
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,23 @@ config :<%= @app_name %>, <%= @endpoint_module %>,
#
# For production it's recommended to configure a different adapter
# at the `config/runtime.exs`.
config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Local<% end %><%= if @javascript do %>
config :<%= @app_name %>, <%= @app_module %>.Mailer, adapter: Swoosh.Adapters.Local<% end %><%= if @volt do %>

# Configure Volt
config :volt,
entry: "assets/js/app.js",
outdir: "priv/static/assets",
target: :es2022,
hash: false,
sourcemap: :hidden,
resolve_dirs: [Path.expand("../deps", __DIR__), Mix.Project.build_path()],
tailwind: [
css: "assets/css/app.css",
sources: [
%{base: "lib/", pattern: "**/*.{ex,heex,eex}"},
%{base: "assets/", pattern: "**/*.{js,ts,jsx,tsx}"}
]
]<% else %><%= if @javascript do %>

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

# Configure Elixir's Logger
config :logger, :default_formatter,
Expand Down
10 changes: 8 additions & 2 deletions installer/templates/phx_single/config/dev.exs.eex
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ config :<%= @app_name %>, <%= @endpoint_module %>,<%= if @inside_docker_env? do
code_reloader: true,
debug_errors: true,
secret_key_base: "<%= @secret_key_base_dev %>",
watchers: <%= if @javascript or @css do %>[<%= if @javascript do %>
watchers: <%= if @volt do %>[
volt: {Mix.Tasks.Volt.Dev, :run, [~w(--tailwind)]}
]<% else %><%= if @javascript or @css do %>[<%= if @javascript do %>
esbuild: {Esbuild, :install_and_run, [:<%= @app_name %>, ~w(--sourcemap=inline --watch)]}<%= if @css, do: "," %><% end %><%= if @css do %>
tailwind: {Tailwind, :install_and_run, [:<%= @app_name %>, ~w(--watch)]}<% end %>
]<% else %>[]<% end %>
]<% else %>[]<% end %><% end %><%= if @volt do %>

config :volt, :server,
prefix: "/assets",
watch_dirs: ["lib/"]<% end %>

# ## SSL Support
#
Expand Down
6 changes: 3 additions & 3 deletions installer/templates/phx_single/formatter.exs.eex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[
import_deps: [<%= if @ecto do %>:ecto, :ecto_sql, <% end %>:phoenix],<%= if @ecto do %>
subdirectories: ["priv/*/migrations"],<% end %><%= if @html do %>
plugins: [Phoenix.LiveView.HTMLFormatter],<% end %>
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 %>]
subdirectories: ["priv/*/migrations"],<% end %><%= if @html or @volt do %>
plugins: [<%= Enum.join(Enum.filter([@html && "Phoenix.LiveView.HTMLFormatter", @volt && "Volt.Formatter"], & &1), ", ") %>],<% end %>
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 %>]
]
13 changes: 6 additions & 7 deletions installer/templates/phx_single/mix.exs.eex
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ defmodule <%= @app_module %>.MixProject do
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_view, "~> 1.1.0"},
{:lazy_html, ">= 0.1.0", only: :test},<% end %><%= if @dashboard do %>
{:phoenix_live_dashboard, "~> 0.8.3"},<% end %><%= if @javascript do %>
{:phoenix_live_dashboard, "~> 0.8.3"},<% end %><%= if @volt do %>
{:volt, "~> 0.11.0"},<% else %><%= if @javascript do %>
{:esbuild, "~> 0.10", runtime: Mix.env() == :dev},<% end %><%= if @css do %>
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},<% end %><% end %><%= if @css do %>
{:heroicons,
github: "tailwindlabs/heroicons",
tag: "v2.2.0",
Expand Down Expand Up @@ -85,11 +86,9 @@ defmodule <%= @app_module %>.MixProject do
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]<% end %><%= if @asset_builders != [] do %>,
"assets.setup": <%= inspect Enum.map(@asset_builders, &"#{&1}.install --if-missing") %>,
"assets.build": <%= inspect ["compile" | Enum.map(@asset_builders, &"#{&1} #{@app_name}")] %>,
"assets.deploy": [
<%= Enum.map(@asset_builders, &" \"#{&1} #{@app_name} --minify\",\n") ++ [" \"phx.digest\""] %>
]<% end %>,
"assets.setup": <%= inspect @assets_setup %>,
"assets.build": <%= inspect @assets_build %>,
"assets.deploy": <%= inspect @assets_deploy %><% end %>,
precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"]
]
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,23 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,
layout: false
],
pubsub_server: <%= @app_module %>.PubSub,
live_view: [signing_salt: "<%= @lv_signing_salt %>"]<%= if @javascript do %>
live_view: [signing_salt: "<%= @lv_signing_salt %>"]<%= if @volt do %>

# Configure Volt
config :volt, :<%= @web_app_name %>,
entry: "assets/js/app.js",
outdir: "priv/static/assets",
target: :es2022,
hash: false,
sourcemap: :hidden,
resolve_dirs: [Path.expand("../deps", __DIR__), Mix.Project.build_path()],
tailwind: [
css: "assets/css/app.css",
sources: [
%{base: "lib/", pattern: "**/*.{ex,heex,eex}"},
%{base: "assets/", pattern: "**/*.{js,ts,jsx,tsx}"}
]
]<% else %><%= if @javascript do %>

# Configure esbuild (the version is required)
config :esbuild,
Expand All @@ -36,4 +52,4 @@ config :tailwind,
--output=priv/static/assets/css/app.css
),
cd: Path.expand("../apps/<%= @web_app_name %>", __DIR__)
]<% end %>
]<% end %><% end %>
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,<%= if @inside_docker_env?
code_reloader: true,
debug_errors: true,
secret_key_base: "<%= @secret_key_base_dev %>",
watchers: <%= if @javascript or @css do %>[<%= if @javascript do %>
watchers: <%= if @volt do %>[
volt: {Mix.Tasks.Volt.Dev, :run, [~w(<%= @web_app_name %> --tailwind)]}
]<% else %><%= if @javascript or @css do %>[<%= if @javascript do %>
esbuild: {Esbuild, :install_and_run, [:<%= @web_app_name %>, ~w(--sourcemap=inline --watch)]}<%= if @css, do: "," %><% end %><%= if @css do %>
tailwind: {Tailwind, :install_and_run, [:<%= @web_app_name %>, ~w(--watch)]}<% end %>
]<% else %>[]<% end %>
]<% else %>[]<% end %><% end %>

# ## SSL Support
#
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[
import_deps: [:phoenix],<%= if @html do %>
plugins: [Phoenix.LiveView.HTMLFormatter],<% end %>
inputs: [<%= if @html do %>"*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"<% else %>"*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"<% end %>]
import_deps: [:phoenix],<%= if @html or @volt do %>
plugins: [<%= Enum.join(Enum.filter([@html && "Phoenix.LiveView.HTMLFormatter", @volt && "Volt.Formatter"], & &1), ", ") %>],<% end %>
inputs: [<%= if @html do %>"*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"<% else %>"*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"<% end %><%= if @volt do %>, "assets/**/*.{js,ts,jsx,tsx}"<% end %>]
]
Loading