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 -