diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex
index 282de5c429..99710dc2d2 100644
--- a/installer/lib/phx_new/generator.ex
+++ b/installer/lib/phx_new/generator.ex
@@ -128,7 +128,7 @@ defmodule Phx.New.Generator do
project.binding[:javascript] && project.binding[:css] &&
@new_project_rules_files["assets.md"],
# generic usage rules
- "\n",
+ "",
[
"\n",
@rules_files["elixir.md"],
@@ -157,7 +157,7 @@ defmodule Phx.New.Generator do
@rules_files["liveview.md"],
"\n"
],
- ""
+ "\n"
]
|> Enum.reject(fn part -> part == nil or part == false end)
|> Enum.intersperse("\n\n")
diff --git a/installer/templates/phx_assets/app.js.eex b/installer/templates/phx_assets/app.js.eex
index f22e06639f..5992c28515 100644
--- a/installer/templates/phx_assets/app.js.eex
+++ b/installer/templates/phx_assets/app.js.eex
@@ -15,8 +15,8 @@
// import "some-package"
//
// If you have dependencies that try to import CSS, esbuild will generate a separate `app.css` file.
-// To load it, simply add a second `` to your `root.html.heex` file.
-<%= if @html do %>
+// To load it, simply add a second `` to your `root.html.heex` file.<%= if @html do %>
+
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
@@ -79,12 +79,11 @@ import "phoenix_html"
<%= @live_comment %>
<%= @live_comment %> window.liveReloader = reloader
<%= @live_comment %> })
-<%= @live_comment %>}
+<%= @live_comment %>}<%= if not @live do %>
-<%= if not @live do %>
// Handle flash close
document.querySelectorAll("[role=alert][data-flash]").forEach((el) => {
el.addEventListener("click", () => {
el.setAttribute("hidden", "")
})
-})<% end %><% end %>
\ No newline at end of file
+})<% end %><% end %>
diff --git a/installer/templates/phx_gettext/errors.pot.eex b/installer/templates/phx_gettext/errors.pot.eex
index 577244ef36..f820c2a5ec 100644
--- a/installer/templates/phx_gettext/errors.pot.eex
+++ b/installer/templates/phx_gettext/errors.pot.eex
@@ -6,8 +6,8 @@
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here has no
-## effect: edit them in PO (`.po`) files instead.
-<%= if @ecto do %>## From Ecto.Changeset.cast/4
+## effect: edit them in PO (`.po`) files instead.<%= if @ecto do %>
+## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
diff --git a/installer/templates/phx_single/gitignore.eex b/installer/templates/phx_single/gitignore.eex
index b32013e3a5..4d7f1c95f6 100644
--- a/installer/templates/phx_single/gitignore.eex
+++ b/installer/templates/phx_single/gitignore.eex
@@ -23,8 +23,8 @@ erl_crash.dump
/tmp/
# Ignore package tarball (built via "mix hex.build").
-<%= @app_name %>-*.tar
-<%= if @javascript or @css do %>
+<%= @app_name %>-*.tar<%= if @javascript or @css do %>
+
# Ignore assets that are produced by build tools.
/priv/static/assets/
@@ -33,9 +33,8 @@ erl_crash.dump
# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
-/assets/node_modules/
-<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
+/assets/node_modules/<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
+
# Database files
*.db
-*.db-*
-<% end %>
+*.db-*<% end %>
diff --git a/installer/templates/phx_static/default.css b/installer/templates/phx_static/default.css
index 40e1e8b932..3403a21a14 100644
--- a/installer/templates/phx_static/default.css
+++ b/installer/templates/phx_static/default.css
@@ -2587,4 +2587,3 @@
transform: rotate(360deg);
}
}
-
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/gitignore.eex b/installer/templates/phx_umbrella/apps/app_name_web/gitignore.eex
index 4f23a32d90..d6c802adf3 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/gitignore.eex
+++ b/installer/templates/phx_umbrella/apps/app_name_web/gitignore.eex
@@ -23,8 +23,8 @@ erl_crash.dump
/tmp/
# Ignore package tarball (built via "mix hex.build").
-<%= @web_app_name %>-*.tar
-<%= if @javascript or @css do %>
+<%= @web_app_name %>-*.tar<%= if @javascript or @css do %>
+
# Ignore assets that are produced by build tools.
/priv/static/assets/
@@ -33,9 +33,8 @@ erl_crash.dump
# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
-/assets/node_modules/
-<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
+/assets/node_modules/<% end %><%= if @adapter_app == :ecto_sqlite3 do %>
+
# Database files
*.db
-*.db-*
-<% end %>
+*.db-*<% end %>
diff --git a/installer/templates/phx_umbrella/config/test.exs.eex b/installer/templates/phx_umbrella/config/test.exs.eex
index a5ed60f0c8..9e99acc783 100644
--- a/installer/templates/phx_umbrella/config/test.exs.eex
+++ b/installer/templates/phx_umbrella/config/test.exs.eex
@@ -16,7 +16,7 @@ config :phoenix, :plug_init_mode, :runtime<%= if @html do %>
# Enable helpful, but potentially expensive runtime checks
config :phoenix_live_view,
enable_expensive_runtime_checks: true<% end %>
-
+
# Sort query params output of verified routes for robust url comparisons
config :phoenix,
- sort_verified_routes_query_params: true
\ No newline at end of file
+ sort_verified_routes_query_params: true
diff --git a/installer/templates/phx_umbrella/gitignore.eex b/installer/templates/phx_umbrella/gitignore.eex
index 59679f6efb..c403299c06 100644
--- a/installer/templates/phx_umbrella/gitignore.eex
+++ b/installer/templates/phx_umbrella/gitignore.eex
@@ -20,10 +20,8 @@ erl_crash.dump
*.ez
# Temporary files, for example, from tests.
-/tmp/
+/tmp/<%= if @adapter_app == :ecto_sqlite3 do %>
-<%= if @adapter_app == :ecto_sqlite3 do %>
# Database files
*.db
-*.db-*
-<% end %>
+*.db-*<% end %>
diff --git a/installer/test/mix_helper.exs b/installer/test/mix_helper.exs
index f11a094085..be5b4b5064 100644
--- a/installer/test/mix_helper.exs
+++ b/installer/test/mix_helper.exs
@@ -96,8 +96,21 @@ defmodule MixHelper do
def assert_file(file) do
assert File.regular?(file), "Expected #{file} to exist, but does not"
+ content = File.read!(file)
+
+ if not binary_file?(file) do
+ # \S\n\z matches non-whitespace followed by a single newline at EOF
+ assert content == "" or content =~ ~r/\S\n\z/,
+ "Expected #{file} to end with a single trailing newline"
+
+ refute content =~ "\n\n\n", "Expected #{file} to not contain consecutive blank lines"
+ end
+
+ content
end
+ defp binary_file?(file), do: Path.extname(file) in ~w(.ico .pem .png)
+
def refute_file(file) do
refute File.regular?(file), "Expected #{file} to not exist, but it does"
end
@@ -111,8 +124,8 @@ defmodule MixHelper do
assert_file(file, &assert(&1 =~ match))
is_function(match, 1) ->
- assert_file(file)
- match.(File.read!(file))
+ content = assert_file(file)
+ match.(content)
true ->
raise inspect({file, match})
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index 489dca741d..a347f2f771 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -147,7 +147,6 @@ defmodule Mix.Tasks.Phx.NewTest do
assert_file("phx_blog/.gitignore", fn file ->
assert file =~ "/priv/static/assets/"
assert file =~ "phx_blog-*.tar"
- assert file =~ ~r/\n$/
end)
assert_file("phx_blog/config/dev.exs", fn file ->
@@ -324,7 +323,6 @@ defmodule Mix.Tasks.Phx.NewTest do
# No assets
assert_file("phx_blog/.gitignore", fn file ->
refute file =~ "/priv/static/assets/"
- assert file =~ ~r/\n$/
end)
refute File.exists?("phx_blog/priv/static/images/logo.svg")
@@ -531,8 +529,6 @@ defmodule Mix.Tasks.Phx.NewTest do
refute file =~ "/priv/static/assets/"
end)
- assert_file("phx_blog/.gitignore")
- assert_file("phx_blog/.gitignore", ~r/\n$/)
assert_file("phx_blog/priv/static/assets/css/app.css")
assert_file("phx_blog/priv/static/assets/js/app.js")
assert_file("phx_blog/priv/static/favicon.ico")
@@ -592,7 +588,6 @@ defmodule Mix.Tasks.Phx.NewTest do
Mix.Tasks.Phx.New.run([project_path, "--app", @app_name, "--module", "PhoteuxBlog"])
assert_file("custom_path/.gitignore")
- assert_file("custom_path/.gitignore", ~r/\n$/)
assert_file("custom_path/mix.exs", ~r/app: :phx_blog/)
assert_file("custom_path/lib/phx_blog_web/endpoint.ex", ~r/app: :phx_blog/)
assert_file("custom_path/config/config.exs", ~r/namespace: PhoteuxBlog/)
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 5625e569b3..38d956ec5f 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -194,9 +194,10 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
end)
# assets
- assert_file(web_path(@app, ".gitignore"), "/priv/static/assets/")
- assert_file(web_path(@app, ".gitignore"), "#{@app}_web-*.tar")
- assert_file(web_path(@app, ".gitignore"), ~r/\n$/)
+ assert_file(web_path(@app, ".gitignore"), fn file ->
+ assert file =~ "/priv/static/assets/"
+ assert file =~ "#{@app}_web-*.tar"
+ end)
assert_file(web_path(@app, "assets/css/app.css"), fn file ->
assert file =~ "lib/phx_umb_web"
@@ -353,7 +354,6 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
# No assets
assert_file(web_path(@app, ".gitignore"), fn file ->
- assert file =~ ~r/\n$/
refute file =~ "/priv/static/assets/"
end)
@@ -516,8 +516,10 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
in_tmp("new with no_assets", fn ->
Mix.Tasks.Phx.New.run([@app, "--umbrella", "--no-assets"])
- refute File.read!(web_path(@app, ".gitignore")) |> String.contains?("/priv/static/assets/")
- assert_file(web_path(@app, ".gitignore"), ~r/\n$/)
+ assert_file(web_path(@app, ".gitignore"), fn file ->
+ refute file =~ "/priv/static/assets/"
+ end)
+
assert_file(web_path(@app, "priv/static/assets/js/app.js"))
assert_file(web_path(@app, "priv/static/assets/css/app.css"))
assert_file(web_path(@app, "priv/static/favicon.ico"))
@@ -883,7 +885,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
assert_file("another/lib/another/components/layouts/root.html.heex")
# assets
- assert_file("another/.gitignore", ~r/\n$/)
+ assert_file("another/.gitignore")
assert_file("another/priv/static/favicon.ico")
assert_file("another/assets/css/app.css")
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index 359133461f..47176e001d 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -262,6 +262,45 @@ defmodule Mix.Phoenix.Schema do
end)
end
+ def format_schema_body(schema, scope \\ nil) do
+ fields = format_fields_for_schema(schema)
+
+ assocs =
+ Enum.map_join(schema.assocs, "\n", fn {_, k, _, _} ->
+ type = if schema.binary_id, do: ":binary_id", else: ":id"
+ " field #{inspect(k)}, #{type}"
+ end)
+
+ scope_field =
+ if scope do
+ " field :#{scope.schema_key}, #{inspect(scope.schema_type)}"
+ else
+ ""
+ end
+
+ timestamp_opts =
+ if schema.timestamp_type != :naive_datetime do
+ "type: #{inspect(schema.timestamp_type)}"
+ else
+ ""
+ end
+
+ timestamps = " timestamps(#{timestamp_opts})"
+
+ schema_fields =
+ [fields, assocs, scope_field]
+ |> join_non_empty("\n")
+
+ [schema_fields, timestamps]
+ |> join_non_empty("\n\n")
+ end
+
+ defp join_non_empty(list, joiner) do
+ list
+ |> Enum.reject(&(&1 == ""))
+ |> Enum.join(joiner)
+ end
+
@doc """
Returns the required fields in the schema. Anything not in the `optionals` list
is considered required.
diff --git a/priv/templates/phx.gen.schema/schema.ex.eex b/priv/templates/phx.gen.schema/schema.ex.eex
index f1aa869144..b1647a1692 100644
--- a/priv/templates/phx.gen.schema/schema.ex.eex
+++ b/priv/templates/phx.gen.schema/schema.ex.eex
@@ -8,11 +8,7 @@ defmodule <%= inspect schema.module %> do
@foreign_key_type :binary_id<% else %><%= if schema.opts[:primary_key] do %>
@primary_key {:<%= schema.opts[:primary_key] %>, :id, autogenerate: true}<% end %><% end %>
schema <%= inspect schema.table %> do
-<%= Mix.Phoenix.Schema.format_fields_for_schema(schema) %>
-<%= for {_, k, _, _} <- schema.assocs do %> field <%= inspect k %>, <%= if schema.binary_id do %>:binary_id<% else %>:id<% end %>
-<% end %><%= if scope do %> field :<%= scope.schema_key %>, <%= inspect scope.schema_type %>
-<% end %>
- timestamps(<%= if schema.timestamp_type != :naive_datetime, do: "type: #{inspect schema.timestamp_type}" %>)
+<%= Mix.Phoenix.Schema.format_schema_body(schema, scope) %>
end
@doc false
diff --git a/priv/templates/phx.gen.socket/socket.ex.eex b/priv/templates/phx.gen.socket/socket.ex.eex
index d6af581b43..a4fbca117d 100644
--- a/priv/templates/phx.gen.socket/socket.ex.eex
+++ b/priv/templates/phx.gen.socket/socket.ex.eex
@@ -5,11 +5,8 @@ defmodule <%= module %>Socket do
#
# It's possible to control the websocket connection and
# assign values that can be accessed by your channel topics.
-
- ## Channels<%= if existing_channel do %>
-
- channel "<%= existing_channel[:singular] %>:*", <%= existing_channel[:module] %>Channel
-<% else %>
+<%= if existing_channel do %>
+ channel "<%= existing_channel[:singular] %>:*", <%= existing_channel[:module] %>Channel<% else %>
# Uncomment the following line to define a "room:*" topic
# pointing to the `<%= web_module %>.RoomChannel`:
#
@@ -20,9 +17,8 @@ defmodule <%= module %>Socket do
# mix phx.gen.channel Room
#
# See the [`Channels guide`](https://phoenix.hexdocs.pm/channels.html)
- # for further details.
+ # for further details.<% end %>
-<% end %>
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index 66574049f2..f6f631f2fe 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -1442,7 +1442,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
test "allows templates to be overridden", config do
in_tmp_phx_project(config.test, fn ->
File.mkdir_p!("priv/templates/phx.gen.auth")
- File.write!("priv/templates/phx.gen.auth/auth.ex.eex", "#it works!")
+ File.write!("priv/templates/phx.gen.auth/auth.ex.eex", "#it works!\n")
send(self(), {:mix_shell_input, :yes?, false})
diff --git a/test/mix/tasks/phx.gen.schema_test.exs b/test/mix/tasks/phx.gen.schema_test.exs
index 27389d28f3..72a12288f6 100644
--- a/test/mix/tasks/phx.gen.schema_test.exs
+++ b/test/mix/tasks/phx.gen.schema_test.exs
@@ -726,4 +726,51 @@ defmodule Mix.Tasks.Phx.Gen.SchemaTest do
)
end)
end
+
+ describe "format_schema_body/2" do
+ setup do
+ scope = %{schema_key: :user_id, schema_type: :id}
+ {:ok, scope: scope}
+ end
+
+ test "no fields, no assocs, no scope" do
+ schema = Schema.new("Blog.Post", "posts", [], [])
+ assert Schema.format_schema_body(schema) == " timestamps()"
+ end
+
+ test "with fields, no assocs, no scope" do
+ schema = Schema.new("Blog.Post", "posts", ["title:string"], [])
+ assert Schema.format_schema_body(schema) == " field :title, :string\n\n timestamps()"
+ end
+
+ test "no fields, with assocs, no scope" do
+ schema = Schema.new("Blog.Comment", "comments", ["post_id:references:posts"], [])
+ assert Schema.format_schema_body(schema) == " field :post_id, :id\n\n timestamps()"
+ end
+
+ test "no fields, no assocs, with scope", %{scope: scope} do
+ schema = Schema.new("Blog.Post", "posts", [], [])
+ assert Schema.format_schema_body(schema, scope) == " field :user_id, :id\n\n timestamps()"
+ end
+
+ test "with fields, with assocs, no scope" do
+ schema = Schema.new("Blog.Comment", "comments", ["title:string", "post_id:references:posts"], [])
+ assert Schema.format_schema_body(schema) == " field :title, :string\n field :post_id, :id\n\n timestamps()"
+ end
+
+ test "with fields, no assocs, with scope", %{scope: scope} do
+ schema = Schema.new("Blog.Post", "posts", ["title:string"], [])
+ assert Schema.format_schema_body(schema, scope) == " field :title, :string\n field :user_id, :id\n\n timestamps()"
+ end
+
+ test "no fields, with assocs, with scope", %{scope: scope} do
+ schema = Schema.new("Blog.Comment", "comments", ["post_id:references:posts"], [])
+ assert Schema.format_schema_body(schema, scope) == " field :post_id, :id\n field :user_id, :id\n\n timestamps()"
+ end
+
+ test "with fields, with assocs, with scope", %{scope: scope} do
+ schema = Schema.new("Blog.Comment", "comments", ["title:string", "post_id:references:posts"], [])
+ assert Schema.format_schema_body(schema, scope) == " field :title, :string\n field :post_id, :id\n field :user_id, :id\n\n timestamps()"
+ end
+ end
end
diff --git a/usage-rules/elixir.md b/usage-rules/elixir.md
index 63af87f1ad..79c4082236 100644
--- a/usage-rules/elixir.md
+++ b/usage-rules/elixir.md
@@ -52,4 +52,3 @@
assert_receive {:DOWN, ^ref, :process, ^pid, :normal}
- Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages
-