Repository: phoenixframework/phoenix
Pull request #5575 (2023-09-12 13:03:26)

Fix tailwind config for umbrella apps
Previously, the tailwind config assumed that an app name ended in `_web` and used that for the config to import.

This commit changes it so that the app name is used instead.

This closes https://github.com/phoenixframework/phoenix/issues/5403


Patch:
diff --git a/installer/templates/phx_assets/tailwind.config.js b/installer/templates/phx_assets/tailwind.config.js
index 856af57c6..38f3d1106 100644
--- a/installer/templates/phx_assets/tailwind.config.js
+++ b/installer/templates/phx_assets/tailwind.config.js
@@ -8,8 +8,8 @@ const path = require("path")
 module.exports = {
   content: [
     "./js/**/*.js",
-    "../lib/*_web.ex",
-    "../lib/*_web/**/*.*ex"
+    "../lib/<%= @lib_web_name || @app_name %>.ex",
+    "../lib/<%= @lib_web_name || @app_name %>/**/*.ex"
   ],
   theme: {
     extend: {
diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs
index 990ec27a4..c80f690f2 100644
--- a/installer/templates/phx_single/config/config.exs
+++ b/installer/templates/phx_single/config/config.exs
@@ -38,7 +38,7 @@ config :esbuild,
   default: [
     args:
       ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
-    cd: Path.expand("../assets", __DIR__),
+    cd: Path.expand("..<%= if @in_umbrella, do: "/apps/#{@app_name}" %>/assets", __DIR__),
     env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
   ]<% end %><%= if @css do %>
 
@@ -51,7 +51,7 @@ config :tailwind,
       --input=css/app.css
       --output=../priv/static/assets/app.css
     ),
-    cd: Path.expand("../assets", __DIR__)
+    cd: Path.expand("..<%= if @in_umbrella, do: "/apps/#{@app_name}" %>/assets", __DIR__),
   ]<% end %>
 
 # Configures Elixir's Logger
diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex
index 95584a031..670322864 100644
--- a/lib/mix/phoenix/schema.ex
+++ b/lib/mix/phoenix/schema.ex
@@ -36,8 +36,8 @@ defmodule Mix.Phoenix.Schema do
             route_prefix: nil,
             api_route_prefix: nil,
             migration_module: nil,
-            fixture_unique_functions: %{},
-            fixture_params: %{},
+            fixture_unique_functions: [],
+            fixture_params: [],
             prefix: nil
 
   @valid_types [
@@ -69,15 +69,15 @@ defmodule Mix.Phoenix.Schema do
   end
 
   def new(schema_name, schema_plural, cli_attrs, opts) do
-    ctx_app   = opts[:context_app] || Mix.Phoenix.context_app()
-    otp_app   = Mix.Phoenix.otp_app()
-    opts      = Keyword.merge(Application.get_env(otp_app, :generators, []), opts)
-    base      = Mix.Phoenix.context_base(ctx_app)
-    basename  = Phoenix.Naming.underscore(schema_name)
-    module    = Module.concat([base, schema_name])
-    repo      = opts[:repo] || Module.concat([base, "Repo"])
-    file      = Mix.Phoenix.context_lib_path(ctx_app, basename <> ".ex")
-    table     = opts[:table] || schema_plural
+    ctx_app = opts[:context_app] || Mix.Phoenix.context_app()
+    otp_app = Mix.Phoenix.otp_app()
+    opts = Keyword.merge(Application.get_env(otp_app, :generators, []), opts)
+    base = Mix.Phoenix.context_base(ctx_app)
+    basename = Phoenix.Naming.underscore(schema_name)
+    module = Module.concat([base, schema_name])
+    repo = opts[:repo] || Module.concat([base, "Repo"])
+    file = Mix.Phoenix.context_lib_path(ctx_app, basename <> ".ex")
+    table = opts[:table] || schema_plural
     {cli_attrs, uniques, redacts} = extract_attr_flags(cli_attrs)
     {assocs, attrs} = partition_attrs_and_assocs(module, attrs(cli_attrs))
     types = types(attrs)
@@ -96,11 +96,13 @@ defmodule Mix.Phoenix.Schema do
     collection = if schema_plural == singular, do: singular <> "_collection", else: schema_plural
     string_attr = string_attr(types)
     create_params = params(attrs, :create)
+
     default_params_key =
       case Enum.at(create_params, 0) do
         {key, _} -> key
         nil -> :some_field
       end
+
     fixture_unique_functions = fixture_unique_functions(singular, uniques, attrs)
 
     %Schema{
@@ -141,7 +143,7 @@ defmodule Mix.Phoenix.Schema do
       context_app: ctx_app,
       generate?: generate?,
       migration_module: migration_module(),
-      fixture_unique_functions: fixture_unique_functions,
+      fixture_unique_functions: Enum.sort(fixture_unique_functions),
       fixture_params: fixture_params(attrs, fixture_unique_functions),
       prefix: opts[:prefix]
     }
@@ -158,11 +160,12 @@ defmodule Mix.Phoenix.Schema do
   end
 
   def extract_attr_flags(cli_attrs) do
-    {attrs, uniques, redacts} = Enum.reduce(cli_attrs, {[], [], []}, fn attr, {attrs, uniques, redacts} ->
-      [attr_name | rest] = String.split(attr, ":")
-      attr_name = String.to_atom(attr_name)
-      split_flags(Enum.reverse(rest), attr_name, attrs, uniques, redacts)
-    end)
+    {attrs, uniques, redacts} =
+      Enum.reduce(cli_attrs, {[], [], []}, fn attr, {attrs, uniques, redacts} ->
+        [attr_name | rest] = String.split(attr, ":")
+        attr_name = String.to_atom(attr_name)
+        split_flags(Enum.reverse(rest), attr_name, attrs, uniques, redacts)
+      end)
 
     {Enum.reverse(attrs), uniques, redacts}
   end
@@ -174,7 +177,7 @@ defmodule Mix.Phoenix.Schema do
     do: split_flags(rest, name, attrs, uniques, [name | redacts])
 
   defp split_flags(rest, name, attrs, uniques, redacts),
-    do: {[Enum.join([name | Enum.reverse(rest)], ":") | attrs ], uniques, redacts}
+    do: {[Enum.join([name | Enum.reverse(rest)], ":") | attrs], uniques, redacts}
 
   @doc """
   Parses the attrs as received by generators.
@@ -247,8 +250,10 @@ defmodule Mix.Phoenix.Schema do
     end)
   end
 
-  def type_and_opts_for_schema({:enum, opts}), do: ~s|Ecto.Enum, values: #{inspect Keyword.get(opts, :values)}|
-  def type_and_opts_for_schema(other), do: inspect other
+  def type_and_opts_for_schema({:enum, opts}),
+    do: ~s|Ecto.Enum, values: #{inspect(Keyword.get(opts, :values))}|
+
+  def type_and_opts_for_schema(other), do: inspect(other)
 
   def maybe_redact_field(true), do: ", redact: true"
   def maybe_redact_field(false), do: ""
@@ -261,11 +266,13 @@ defmodule Mix.Phoenix.Schema do
     |> Map.fetch!(field)
     |> inspect_value(value)
   end
+
   defp inspect_value(:decimal, value), do: "Decimal.new(\"#{value}\")"
   defp inspect_value(_type, value), do: inspect(value)
 
   defp list_to_attr([key]), do: {String.to_atom(key), :string}
   defp list_to_attr([key, value]), do: {String.to_atom(key), String.to_atom(value)}
+
   defp list_to_attr([key, comp, value]) do
     {String.to_atom(key), {String.to_atom(comp), String.to_atom(value)}}
   end
@@ -274,55 +281,103 @@ defmodule Mix.Phoenix.Schema do
 
   defp type_to_default(key, t, :create) do
     case t do
-        {:array, type} -> build_array_values(type, :create)
-        {:enum, values} -> build_enum_values(values, :create)
-        :integer        -> 42
-        :float          -> 120.5
-        :decimal        -> "120.5"
-        :boolean        -> true
-        :map            -> %{}
-        :text           -> "some #{key}"
-        :date           -> Date.add(Date.utc_today(), -1)
-        :time           -> ~T[14:00:00]
-        :time_usec      -> ~T[14:00:00.000000]
-        :uuid           -> "7488a646-e31f-11e4-aace-600308960662"
-        :utc_datetime   -> DateTime.add(build_utc_datetime(), -@one_day_in_seconds, :second, Calendar.UTCOnlyTimeZoneDatabase)
-        :utc_datetime_usec -> DateTime.add(build_utc_datetime_usec(), -@one_day_in_seconds, :second, Calendar.UTCOnlyTimeZoneDatabase)
-        :naive_datetime -> NaiveDateTime.add(build_utc_naive_datetime(), -@one_day_in_seconds)
-        :naive_datetime_usec -> NaiveDateTime.add(build_utc_naive_datetime_usec(), -@one_day_in_seconds)
-        _  -> "some #{key}"
+      {:array, type} ->
+        build_array_values(type, :create)
+
+      {:enum, values} ->
+        build_enum_values(values, :create)
+
+      :integer ->
+        42
+
+      :float ->
+        120.5
+
+      :decimal ->
+        "120.5"
+
+      :boolean ->
+        true
+
+      :map ->
+        %{}
+
+      :text ->
+        "some #{key}"
+
+      :date ->
+        Date.add(Date.utc_today(), -1)
+
+      :time ->
+        ~T[14:00:00]
+
+      :time_usec ->
+        ~T[14:00:00.000000]
+
+      :uuid ->
+        "7488a646-e31f-11e4-aace-600308960662"
+
+      :utc_datetime ->
+        DateTime.add(
+          build_utc_datetime(),
+          -@one_day_in_seconds,
+          :second,
+          Calendar.UTCOnlyTimeZoneDatabase
+        )
+
+      :utc_datetime_usec ->
+        DateTime.add(
+          build_utc_datetime_usec(),
+          -@one_day_in_seconds,
+          :second,
+          Calendar.UTCOnlyTimeZoneDatabase
+        )
+
+      :naive_datetime ->
+        NaiveDateTime.add(build_utc_naive_datetime(), -@one_day_in_seconds)
+
+      :naive_datetime_usec ->
+        NaiveDateTime.add(build_utc_naive_datetime_usec(), -@one_day_in_seconds)
+
+      _ ->
+        "some #{key}"
     end
   end
+
   defp type_to_default(key, t, :update) do
     case t do
-        {:array, type}  -> build_array_values(type, :update)
-        {:enum, values} -> build_enum_values(values, :update)
-        :integer        -> 43
-        :float          -> 456.7
-        :decimal        -> "456.7"
-        :boolean        -> false
-        :map            -> %{}
-        :text           -> "some updated #{key}"
-        :date           -> Date.utc_today()
-        :time           -> ~T[15:01:01]
-        :time_usec      -> ~T[15:01:01.000000]
-        :uuid           -> "7488a646-e31f-11e4-aace-600308960668"
-        :utc_datetime   -> build_utc_datetime()
-        :utc_datetime_usec -> build_utc_datetime_usec()
-        :naive_datetime -> build_utc_naive_datetime()
-        :naive_datetime_usec -> build_utc_naive_datetime_usec()
-        _               -> "some updated #{key}"
+      {:array, type} -> build_array_values(type, :update)
+      {:enum, values} -> build_enum_values(values, :update)
+      :integer -> 43
+      :float -> 456.7
+      :decimal -> "456.7"
+      :boolean -> false
+      :map -> %{}
+      :text -> "some updated #{key}"
+      :date -> Date.utc_today()
+      :time -> ~T[15:01:01]
+      :time_usec -> ~T[15:01:01.000000]
+      :uuid -> "7488a646-e31f-11e4-aace-600308960668"
+      :utc_datetime -> build_utc_datetime()
+      :utc_datetime_usec -> build_utc_datetime_usec()
+      :naive_datetime -> build_utc_naive_datetime()
+      :naive_datetime_usec -> build_utc_naive_datetime_usec()
+      _ -> "some updated #{key}"
     end
   end
 
   defp build_array_values(:string, :create),
-    do: Enum.map([1,2], &("option#{&1}"))
+    do: Enum.map([1, 2], &"option#{&1}")
+
   defp build_array_values(:integer, :create),
-    do: [1,2]
+    do: [1, 2]
+
   defp build_array_values(:string, :update),
     do: ["option1"]
+
   defp build_array_values(:integer, :update),
     do: [1]
+
   defp build_array_values(_, _),
     do: []
 
@@ -354,21 +409,26 @@ defmodule Mix.Phoenix.Schema do
   """
 
   defp validate_attr!({name, :datetime}), do: validate_attr!({name, :naive_datetime})
+
   defp validate_attr!({name, :array}) do
-    Mix.raise """
+    Mix.raise("""
     Phoenix generators expect the type of the array to be given to #{name}:array.
     For example:
 
         mix phx.gen.schema Post posts settings:array:string
-    """
+    """)
   end
-  defp validate_attr!({_name, :enum}), do: Mix.raise @enum_missing_value_error
+
+  defp validate_attr!({_name, :enum}), do: Mix.raise(@enum_missing_value_error)
   defp validate_attr!({_name, type} = attr) when type in @valid_types, do: attr
   defp validate_attr!({_name, {:enum, _vals}} = attr), do: attr
   defp validate_attr!({_name, {type, _}} = attr) when type in @valid_types, do: attr
+
   defp validate_attr!({_, type}) do
-    Mix.raise "Unknown type `#{inspect type}` given to generator. " <>
-              "The supported types are: #{@valid_types |> Enum.sort() |> Enum.join(", ")}"
+    Mix.raise(
+      "Unknown type `#{inspect(type)}` given to generator. " <>
+        "The supported types are: #{@valid_types |> Enum.sort() |> Enum.join(", ")}"
+    )
   end
 
   defp partition_attrs_and_assocs(schema_module, attrs) do
@@ -376,14 +436,17 @@ defmodule Mix.Phoenix.Schema do
       Enum.split_with(attrs, fn
         {_, {:references, _}} ->
           true
+
         {key, :references} ->
-          Mix.raise """
+          Mix.raise("""
           Phoenix generators expect the table to be given to #{key}:references.
           For example:
 
               mix phx.gen.schema Comment comments body:text post_id:references:posts
-          """
-        _ -> false
+          """)
+
+        _ ->
+          false
       end)
 
     assocs =
@@ -399,8 +462,8 @@ defmodule Mix.Phoenix.Schema do
 
   defp schema_defaults(attrs) do
     Enum.into(attrs, %{}, fn
-      {key, :boolean}  -> {key, ", default: false"}
-      {key, _}         -> {key, ""}
+      {key, :boolean} -> {key, ", default: false"}
+      {key, _} -> {key, ""}
     end)
   end
 
@@ -428,9 +491,10 @@ defmodule Mix.Phoenix.Schema do
 
   defp schema_type(:text), do: :string
   defp schema_type(:uuid), do: Ecto.UUID
+
   defp schema_type(val) do
     if Code.ensure_loaded?(Ecto.Type) and not Ecto.Type.primitive?(val) do
-      Mix.raise "Unknown type `#{val}` given to generator"
+      Mix.raise("Unknown type `#{val}` given to generator")
     else
       val
     end
@@ -444,14 +508,14 @@ defmodule Mix.Phoenix.Schema do
     |> Enum.uniq_by(fn {key, _} -> key end)
     |> Enum.map(fn
       {key, false} -> "create index(:#{table}, [:#{key}])"
-      {key, true}  -> "create unique_index(:#{table}, [:#{key}])"
+      {key, true} -> "create unique_index(:#{table}, [:#{key}])"
     end)
   end
 
   defp migration_defaults(attrs) do
     Enum.into(attrs, %{}, fn
-      {key, :boolean}  -> {key, ", default: false, null: false"}
-      {key, _}         -> {key, ""}
+      {key, :boolean} -> {key, ", default: false, null: false"}
+      {key, _} -> {key, ""}
     end)
   end
 
@@ -482,7 +546,7 @@ defmodule Mix.Phoenix.Schema do
   defp migration_module do
     case Application.get_env(:ecto_sql, :migration_module, Ecto.Migration) do
       migration_module when is_atom(migration_module) -> migration_module
-      other -> Mix.raise "Expected :migration_module to be a module, got: #{inspect(other)}"
+      other -> Mix.raise("Expected :migration_module to be a module, got: #{inspect(other)}")
     end
   end
 
@@ -521,17 +585,19 @@ defmodule Mix.Phoenix.Schema do
             {function_def, true}
         end
 
-
       {attr, {function_name, function_def, needs_impl?}}
     end)
   end
 
   defp fixture_params(attrs, fixture_unique_functions) do
-    Map.new(attrs, fn {attr, type} ->
-      case Map.fetch(fixture_unique_functions, attr) do
-        {:ok, {function_name, _function_def, _needs_impl?}} ->
+    attrs
+    |> Enum.sort()
+    |> Enum.map(fn {attr, type} ->
+      case fixture_unique_functions do
+        %{^attr => {function_name, _function_def, _needs_impl?}} ->
           {attr, "#{function_name}()"}
-        :error ->
+
+        %{} ->
           {attr, inspect(type_to_default(attr, type, :create))}
       end
     end)
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index 934a08fa1..7bbf7aed4 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -78,16 +78,26 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   alias Mix.Phoenix.{Context, Schema}
   alias Mix.Tasks.Phx.Gen
 
-  @switches [binary_id: :boolean, table: :string, web: :string,
-             schema: :boolean, context: :boolean, context_app: :string,
-             merge_with_existing_context: :boolean, prefix: :string, live: :boolean]
+  @switches [
+    binary_id: :boolean,
+    table: :string,
+    web: :string,
+    schema: :boolean,
+    context: :boolean,
+    context_app: :string,
+    merge_with_existing_context: :boolean,
+    prefix: :string,
+    live: :boolean
+  ]
 
   @default_opts [schema: true, context: true]
 
   @doc false
   def run(args) do
     if Mix.Project.umbrella?() do
-      Mix.raise "mix phx.gen.context must be invoked from within your *_web application root directory"
+      Mix.raise(
+        "mix phx.gen.context must be invoked from within your *_web application root directory"
+      )
     end
 
     {context, schema} = build(args)
@@ -120,6 +130,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do
 
   defp parse_opts(args) do
     {opts, parsed, invalid} = OptionParser.parse(args, switches: @switches)
+
     merged_opts =
       @default_opts
       |> Keyword.merge(opts)
@@ -127,7 +138,9 @@ defmodule Mix.Tasks.Phx.Gen.Context do
 
     {merged_opts, parsed, invalid}
   end
+
   defp put_context_app(opts, nil), do: opts
+
   defp put_context_app(opts, string) do
     Keyword.put(opts, :context_app, String.to_atom(string))
   end
@@ -154,7 +167,10 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   @doc false
   def ensure_context_file_exists(%Context{file: file} = context, paths, binding) do
     unless Context.pre_existing?(context) do
-      Mix.Generator.create_file(file, Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/context.ex", binding))
+      Mix.Generator.create_file(
+        file,
+        Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/context.ex", binding)
+      )
     end
   end
 
@@ -162,7 +178,10 @@ defmodule Mix.Tasks.Phx.Gen.Context do
     ensure_context_file_exists(context, paths, binding)
 
     paths
-    |> Mix.Phoenix.eval_from("priv/templates/phx.gen.context/#{schema_access_template(context)}", binding)
+    |> Mix.Phoenix.eval_from(
+      "priv/templates/phx.gen.context/#{schema_access_template(context)}",
+      binding
+    )
     |> inject_eex_before_final_end(file, binding)
   end
 
@@ -173,7 +192,10 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   @doc false
   def ensure_test_file_exists(%Context{test_file: test_file} = context, paths, binding) do
     unless Context.pre_existing_tests?(context) do
-      Mix.Generator.create_file(test_file, Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/context_test.exs", binding))
+      Mix.Generator.create_file(
+        test_file,
+        Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/context_test.exs", binding)
+      )
     end
   end
 
@@ -186,13 +208,24 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   end
 
   @doc false
-  def ensure_test_fixtures_file_exists(%Context{test_fixtures_file: test_fixtures_file} = context, paths, binding) do
+  def ensure_test_fixtures_file_exists(
+        %Context{test_fixtures_file: test_fixtures_file} = context,
+        paths,
+        binding
+      ) do
     unless Context.pre_existing_test_fixtures?(context) do
-      Mix.Generator.create_file(test_fixtures_file, Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/fixtures_module.ex", binding))
+      Mix.Generator.create_file(
+        test_fixtures_file,
+        Mix.Phoenix.eval_from(paths, "priv/templates/phx.gen.context/fixtures_module.ex", binding)
+      )
     end
   end
 
-  defp inject_test_fixture(%Context{test_fixtures_file: test_fixtures_file} = context, paths, binding) do
+  defp inject_test_fixture(
+         %Context{test_fixtures_file: test_fixtures_file} = context,
+         paths,
+         binding
+       ) do
     ensure_test_fixtures_file_exists(context, paths, binding)
 
     paths
@@ -214,20 +247,14 @@ defmodule Mix.Tasks.Phx.Gen.Context do
       )
 
     if Enum.any?(fixture_functions_needing_implementations) do
-      Mix.shell.info(
-        """
-
-        Some of the generated database columns are unique. Please provide
-        unique implementations for the following fixture function(s) in
-        #{context.test_fixtures_file}:
-
-        #{
-          fixture_functions_needing_implementations
-          |> Enum.map_join(&indent(&1, 2))
-          |> String.trim_trailing()
-        }
-        """
-      )
+      Mix.shell().info("""
+
+      Some of the generated database columns are unique. Please provide
+      unique implementations for the following fixture function(s) in
+      #{context.test_fixtures_file}:
+
+      #{fixture_functions_needing_implementations |> Enum.map_join(&indent(&1, 2)) |> String.trim_trailing()}
+      """)
     end
   end
 
@@ -237,11 +264,11 @@ defmodule Mix.Tasks.Phx.Gen.Context do
     string
     |> String.split("\n")
     |> Enum.map_join(fn line ->
-        if String.trim(line) == "" do
-          "\n"
-        else
-          indent_string <> line <> "\n"
-        end
+      if String.trim(line) == "" do
+        "\n"
+      else
+        indent_string <> line <> "\n"
+      end
     end)
   end
 
@@ -283,27 +310,38 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   defp validate_args!([context, schema, _plural | _] = args, help) do
     cond do
       not Context.valid?(context) ->
-        help.raise_with_help "Expected the context, #{inspect context}, to be a valid module name"
+        help.raise_with_help(
+          "Expected the context, #{inspect(context)}, to be a valid module name"
+        )
+
       not Schema.valid?(schema) ->
-        help.raise_with_help "Expected the schema, #{inspect schema}, to be a valid module name"
+        help.raise_with_help("Expected the schema, #{inspect(schema)}, to be a valid module name")
+
       context == schema ->
-        help.raise_with_help "The context and schema should have different names"
+        help.raise_with_help("The context and schema should have different names")
+
       context == Mix.Phoenix.base() ->
-        help.raise_with_help "Cannot generate context #{context} because it has the same name as the application"
+        help.raise_with_help(
+          "Cannot generate context #{context} because it has the same name as the application"
+        )
+
       schema == Mix.Phoenix.base() ->
-        help.raise_with_help "Cannot generate schema #{schema} because it has the same name as the application"
+        help.raise_with_help(
+          "Cannot generate schema #{schema} because it has the same name as the application"
+        )
+
       true ->
         args
     end
   end
 
   defp validate_args!(_, help) do
-    help.raise_with_help "Invalid arguments"
+    help.raise_with_help("Invalid arguments")
   end
 
   @doc false
   def raise_with_help(msg) do
-    Mix.raise """
+    Mix.raise("""
     #{msg}
 
     mix phx.gen.html, phx.gen.json, phx.gen.live, and phx.gen.context
@@ -319,11 +357,12 @@ defmodule Mix.Tasks.Phx.Gen.Context do
     The context serves as the API boundary for the given resource.
     Multiple resources may belong to a context and a resource may be
     split over distinct contexts (such as Accounts.User and Payments.User).
-    """
+    """)
   end
 
   @doc false
   def prompt_for_code_injection(%Context{generate?: false}), do: :ok
+
   def prompt_for_code_injection(%Context{} = context) do
     if Context.pre_existing?(context) && !merge_with_existing_context?(context) do
       System.halt()
diff --git a/lib/phoenix/code_reloader.ex b/lib/phoenix/code_reloader.ex
index 931a7ccc5..d1c48fb54 100644
--- a/lib/phoenix/code_reloader.ex
+++ b/lib/phoenix/code_reloader.ex
@@ -33,7 +33,9 @@ defmodule Phoenix.CodeReloader do
 
   ## Options
 
-    * `:reloadable_args` - additional CLI args to pass to the compiler tasks
+    * `:reloadable_args` - additional CLI args to pass to the compiler tasks.
+      Defaults to `["--no-all-warnings"]` so only warnings related to the
+      files being compiled are printed
 
   """
   @spec reload(module, keyword) :: :ok | {:error, binary()}
@@ -64,22 +66,29 @@ defmodule Phoenix.CodeReloader do
     primary: "#EB532D",
     accent: "#a0b0c0",
     text_color: "#304050",
-    logo: "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K",
+    logo:
+      "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNzEgNDgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgoJPHBhdGggZD0ibTI2LjM3MSAzMy40NzctLjU1Mi0uMWMtMy45Mi0uNzI5LTYuMzk3LTMuMS03LjU3LTYuODI5LS43MzMtMi4zMjQuNTk3LTQuMDM1IDMuMDM1LTQuMTQ4IDEuOTk1LS4wOTIgMy4zNjIgMS4wNTUgNC41NyAyLjM5IDEuNTU3IDEuNzIgMi45ODQgMy41NTggNC41MTQgNS4zMDUgMi4yMDIgMi41MTUgNC43OTcgNC4xMzQgOC4zNDcgMy42MzQgMy4xODMtLjQ0OCA1Ljk1OC0xLjcyNSA4LjM3MS0zLjgyOC4zNjMtLjMxNi43NjEtLjU5MiAxLjE0NC0uODg2bC0uMjQxLS4yODRjLTIuMDI3LjYzLTQuMDkzLjg0MS02LjIwNS43MzUtMy4xOTUtLjE2LTYuMjQtLjgyOC04Ljk2NC0yLjU4Mi0yLjQ4Ni0xLjYwMS00LjMxOS0zLjc0Ni01LjE5LTYuNjExLS43MDQtMi4zMTUuNzM2LTMuOTM0IDMuMTM1LTMuNi45NDguMTMzIDEuNzQ2LjU2IDIuNDYzIDEuMTY1LjU4My40OTMgMS4xNDMgMS4wMTUgMS43MzggMS40OTMgMi44IDIuMjUgNi43MTIgMi4zNzUgMTAuMjY1LS4wNjgtNS44NDItLjAyNi05LjgxNy0zLjI0LTEzLjMwOC03LjMxMy0xLjM2Ni0xLjU5NC0yLjctMy4yMTYtNC4wOTUtNC43ODUtMi42OTgtMy4wMzYtNS42OTItNS43MS05Ljc5LTYuNjIzQzEyLjgtLjYyMyA3Ljc0NS4xNCAyLjg5MyAyLjM2MSAxLjkyNiAyLjgwNC45OTcgMy4zMTkgMCA0LjE0OWMuNDk0IDAgLjc2My4wMDYgMS4wMzIgMCAyLjQ0Ni0uMDY0IDQuMjggMS4wMjMgNS42MDIgMy4wMjQuOTYyIDEuNDU3IDEuNDE1IDMuMTA0IDEuNzYxIDQuNzk4LjUxMyAyLjUxNS4yNDcgNS4wNzguNTQ0IDcuNjA1Ljc2MSA2LjQ5NCA0LjA4IDExLjAyNiAxMC4yNiAxMy4zNDYgMi4yNjcuODUyIDQuNTkxIDEuMTM1IDcuMTcyLjU1NVpNMTAuNzUxIDMuODUyYy0uOTc2LjI0Ni0xLjc1Ni0uMTQ4LTIuNTYtLjk2MiAxLjM3Ny0uMzQzIDIuNTkyLS40NzYgMy44OTctLjUyOC0uMTA3Ljg0OC0uNjA3IDEuMzA2LTEuMzM2IDEuNDlabTMyLjAwMiAzNy45MjRjLS4wODUtLjYyNi0uNjItLjkwMS0xLjA0LTEuMjI4LTEuODU3LTEuNDQ2LTQuMDMtMS45NTgtNi4zMzMtMi0xLjM3NS0uMDI2LTIuNzM1LS4xMjgtNC4wMzEtLjYxLS41OTUtLjIyLTEuMjYtLjUwNS0xLjI0NC0xLjI3Mi4wMTUtLjc4LjY5My0xIDEuMzEtMS4xODQuNTA1LS4xNSAxLjAyNi0uMjQ3IDEuNi0uMzgyLTEuNDYtLjkzNi0yLjg4Ni0xLjA2NS00Ljc4Ny0uMy0yLjk5MyAxLjIwMi01Ljk0MyAxLjA2LTguOTI2LS4wMTctMS42ODQtLjYwOC0zLjE3OS0xLjU2My00LjczNS0yLjQwOGwtLjA0My4wM2EyLjk2IDIuOTYgMCAwIDAgLjA0LS4wMjljLS4wMzgtLjExNy0uMTA3LS4xMi0uMTk3LS4wNTRsLjEyMi4xMDdjMS4yOSAyLjExNSAzLjAzNCAzLjgxNyA1LjAwNCA1LjI3MSAzLjc5MyAyLjggNy45MzYgNC40NzEgMTIuNzg0IDMuNzNBNjYuNzE0IDY2LjcxNCAwIDAgMSAzNyA0MC44NzdjMS45OC0uMTYgMy44NjYuMzk4IDUuNzUzLjg5OVptLTkuMTQtMzAuMzQ1Yy0uMTA1LS4wNzYtLjIwNi0uMjY2LS40Mi0uMDY5IDEuNzQ1IDIuMzYgMy45ODUgNC4wOTggNi42ODMgNS4xOTMgNC4zNTQgMS43NjcgOC43NzMgMi4wNyAxMy4yOTMuNTEgMy41MS0xLjIxIDYuMDMzLS4wMjggNy4zNDMgMy4zOC4xOS0zLjk1NS0yLjEzNy02LjgzNy01Ljg0My03LjQwMS0yLjA4NC0uMzE4LTQuMDEuMzczLTUuOTYyLjk0LTUuNDM0IDEuNTc1LTEwLjQ4NS43OTgtMTUuMDk0LTIuNTUzWm0yNy4wODUgMTUuNDI1Yy43MDguMDU5IDEuNDE2LjEyMyAyLjEyNC4xODUtMS42LTEuNDA1LTMuNTUtMS41MTctNS41MjMtMS40MDQtMy4wMDMuMTctNS4xNjcgMS45MDMtNy4xNCAzLjk3Mi0xLjczOSAxLjgyNC0zLjMxIDMuODctNS45MDMgNC42MDQuMDQzLjA3OC4wNTQuMTE3LjA2Ni4xMTcuMzUuMDA1LjY5OS4wMjEgMS4wNDcuMDA1IDMuNzY4LS4xNyA3LjMxNy0uOTY1IDEwLjE0LTMuNy44OS0uODYgMS42ODUtMS44MTcgMi41NDQtMi43MS43MTYtLjc0NiAxLjU4NC0xLjE1OSAyLjY0NS0xLjA3Wm0tOC43NTMtNC42N2MtMi44MTIuMjQ2LTUuMjU0IDEuNDA5LTcuNTQ4IDIuOTQzLTEuNzY2IDEuMTgtMy42NTQgMS43MzgtNS43NzYgMS4zNy0uMzc0LS4wNjYtLjc1LS4xMTQtMS4xMjQtLjE3bC0uMDEzLjE1NmMuMTM1LjA3LjI2NS4xNTEuNDA1LjIwNy4zNTQuMTQuNzAyLjMwOCAxLjA3LjM5NSA0LjA4My45NzEgNy45OTIuNDc0IDExLjUxNi0xLjgwMyAyLjIyMS0xLjQzNSA0LjUyMS0xLjcwNyA3LjAxMy0xLjMzNi4yNTIuMDM4LjUwMy4wODMuNzU2LjEwNy4yMzQuMDIyLjQ3OS4yNTUuNzk1LjAwMy0yLjE3OS0xLjU3NC00LjUyNi0yLjA5Ni03LjA5NC0xLjg3MlptLTEwLjA0OS05LjU0NGMxLjQ3NS4wNTEgMi45NDMtLjE0MiA0LjQ4Ni0xLjA1OS0uNDUyLjA0LS42NDMuMDQtLjgyNy4wNzYtMi4xMjYuNDI0LTQuMDMzLS4wNC01LjczMy0xLjM4My0uNjIzLS40OTMtMS4yNTctLjk3NC0xLjg4OS0xLjQ1Ny0yLjUwMy0xLjkxNC01LjM3NC0yLjU1NS04LjUxNC0yLjUuMDUuMTU0LjA1NC4yNi4xMDguMzE1IDMuNDE3IDMuNDU1IDcuMzcxIDUuODM2IDEyLjM2OSA2LjAwOFptMjQuNzI3IDE3LjczMWMtMi4xMTQtMi4wOTctNC45NTItMi4zNjctNy41NzgtLjUzNyAxLjczOC4wNzggMy4wNDMuNjMyIDQuMTAxIDEuNzI4LjM3NC4zODguNzYzLjc2OCAxLjE4MiAxLjEwNiAxLjYgMS4yOSA0LjMxMSAxLjM1MiA1Ljg5Ni4xNTUtMS44NjEtLjcyNi0xLjg2MS0uNzI2LTMuNjAxLTIuNDUyWm0tMjEuMDU4IDE2LjA2Yy0xLjg1OC0zLjQ2LTQuOTgxLTQuMjQtOC41OS00LjAwOGE5LjY2NyA5LjY2NyAwIDAgMSAyLjk3NyAxLjM5Yy44NC41ODYgMS41NDcgMS4zMTEgMi4yNDMgMi4wNTUgMS4zOCAxLjQ3MyAzLjUzNCAyLjM3NiA0Ljk2MiAyLjA3LS42NTYtLjQxMi0xLjIzOC0uODQ4LTEuNTkyLTEuNTA3Wm0xNy4yOS0xOS4zMmMwLS4wMjMuMDAxLS4wNDUuMDAzLS4wNjhsLS4wMDYuMDA2LjAwNi0uMDA2LS4wMzYtLjAwNC4wMjEuMDE4LjAxMi4wNTNabS0yMCAxNC43NDRhNy42MSA3LjYxIDAgMCAwLS4wNzItLjA0MS4xMjcuMTI3IDAgMCAwIC4wMTUuMDQzYy4wMDUuMDA4LjAzOCAwIC4wNTgtLjAwMlptLS4wNzItLjA0MS0uMDA4LS4wMzQtLjAwOC4wMS4wMDgtLjAxLS4wMjItLjAwNi4wMDUuMDI2LjAyNC4wMTRaIgogICAgICAgICAgICBmaWxsPSIjRkQ0RjAwIiAvPgo8L3N2Zz4K",
     monospace_font: "menlo, consolas, monospace"
   }
 
   @doc """
   API used by Plug to start the code reloader.
   """
-  def init(opts), do: Keyword.put_new(opts, :reloader, &Phoenix.CodeReloader.reload/1)
+  def init(opts) do
+    case Keyword.pop(opts, :reloader) do
+      {nil, opts} -> &Phoenix.CodeReloader.reload(&1, opts)
+      {fun, _opts} -> fun
+    end
+  end
 
   @doc """
   API used by Plug to invoke the code reloader on every request.
   """
-  def call(conn, opts) do
-    case opts[:reloader].(conn.private.phoenix_endpoint) do
+  def call(conn, reloader) do
+    case reloader.(conn.private.phoenix_endpoint) do
       :ok ->
         conn
+
       {:error, output} ->
         conn
         |> put_resp_content_type("text/html")
@@ -270,7 +279,7 @@ defmodule Phoenix.CodeReloader do
 
   defp format_output(output) do
     output
-    |> String.trim
-    |> Plug.HTML.html_escape
+    |> String.trim()
+    |> Plug.HTML.html_escape()
   end
 end
diff --git a/lib/phoenix/code_reloader/server.ex b/lib/phoenix/code_reloader/server.ex
index e1146c4a1..ec18b3c4c 100644
--- a/lib/phoenix/code_reloader/server.ex
+++ b/lib/phoenix/code_reloader/server.ex
@@ -61,7 +61,7 @@ defmodule Phoenix.CodeReloader.Server do
   def handle_call({:reload!, endpoint, opts}, from, state) do
     compilers = endpoint.config(:reloadable_compilers)
     apps = endpoint.config(:reloadable_apps) || default_reloadable_apps()
-    args = Keyword.get(opts, :reloadable_args, [])
+    args = Keyword.get(opts, :reloadable_args, ["--no-all-warnings"])
 
     # We do a backup of the endpoint in case compilation fails.
     # If so we can bring it back to finish the request handling.
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index 277d9732b..5a1397144 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -294,9 +294,7 @@ defmodule Phoenix.Socket.Transport do
   """
   def code_reload(conn, endpoint, opts) do
     if Keyword.get(opts, :code_reloader, endpoint.config(:code_reloader)) do
-      # If the WebSocket reconnects, then often the page has already been reloaded
-      # and we don't want to print warnings twice, so we disable all warnings.
-      Phoenix.CodeReloader.reload(endpoint, reloadable_args: ~w(--no-all-warnings))
+      Phoenix.CodeReloader.reload(endpoint)
     end
 
     conn


Test patch:
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index 75d8744d1..d9b96c5e6 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -55,6 +55,7 @@ defmodule Mix.Tasks.Phx.NewTest do
       assert_file("phx_blog/config/config.exs", fn file ->
         assert file =~ "ecto_repos: [PhxBlog.Repo]"
         assert file =~ "config :phoenix, :json_library, Jason"
+        assert file =~ ~s[cd: Path.expand("../assets", __DIR__),]
         refute file =~ "namespace: PhxBlog"
         refute file =~ "config :phx_blog, :generators"
       end)
@@ -149,7 +150,11 @@ defmodule Mix.Tasks.Phx.NewTest do
 
       # tailwind
       assert_file("phx_blog/assets/css/app.css")
-      assert_file("phx_blog/assets/tailwind.config.js")
+      assert_file "phx_blog/assets/tailwind.config.js", fn file ->
+        assert file =~ "phx_blog_web.ex"
+        assert file =~ "phx_blog_web/**/*.ex"
+      end
+
       assert_file("phx_blog/assets/vendor/heroicons/LICENSE.md")
       assert_file("phx_blog/assets/vendor/heroicons/UPGRADE.md")
       assert_file("phx_blog/assets/vendor/heroicons/optimized/24/outline/cake.svg")
@@ -602,6 +607,11 @@ defmodule Mix.Tasks.Phx.NewTest do
         refute_file("phx_blog/config/config.exs")
       end)
 
+      assert_file("config/config.exs", fn file ->
+        assert file =~ "PhxBlogWeb.Endpoint"
+        assert file =~ ~s[cd: Path.expand("../apps/phx_blog/assets", __DIR__),]
+      end)
+
       assert_file("config/config.exs", "PhxBlogWeb.Endpoint")
     end)
   end
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index 1ca8a943f..5dca16ba0 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -183,6 +183,11 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
       assert_file(web_path(@app, ".gitignore"), ~r/\n$/)
       assert_file(web_path(@app, "assets/css/app.css"))
 
+      assert_file web_path(@app, "assets/tailwind.config.js"), fn file ->
+        assert file =~ "phx_umb_web.ex"
+        assert file =~ "phx_umb_web/**/*.ex"
+      end
+
       assert_file(web_path(@app, "priv/static/favicon.ico"))
 
       refute File.exists?(web_path(@app, "priv/static/assets/app.css"))
diff --git a/installer/test/phx_new_web_test.exs b/installer/test/phx_new_web_test.exs
index 2f13f4cda..7b6e7114c 100644
--- a/installer/test/phx_new_web_test.exs
+++ b/installer/test/phx_new_web_test.exs
@@ -63,4 +63,15 @@ defmodule Mix.Tasks.Phx.New.WebTest do
       assert_received {:mix_shell, :info, ["Start your Phoenix app" <> _]}
     end
   end
+
+  test "app_name is included in tailwind config" do
+    in_tmp_umbrella_project "new with defaults", fn ->
+      Mix.Tasks.Phx.New.Web.run(["testweb"])
+
+      assert_file "testweb/assets/tailwind.config.js", fn file ->
+        assert file =~ "testweb.ex"
+        assert file =~ "testweb/**/*.ex"
+      end
+    end
+  end
 end
diff --git a/test/phoenix/socket/v1_json_serializer_test.exs b/test/phoenix/socket/v1_json_serializer_test.exs
index 7f0cfc9eb..62652d3f2 100644
--- a/test/phoenix/socket/v1_json_serializer_test.exs
+++ b/test/phoenix/socket/v1_json_serializer_test.exs
@@ -7,8 +7,6 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do
   @serializer V1.JSONSerializer
   @v1_msg_json "{\"event\":\"e\",\"payload\":\"m\",\"ref\":null,\"topic\":\"t\"}"
   @v1_bad_json "[null,null,\"t\",\"e\",{\"m\":1}]"
-  @v1_reply_json "{\"event\":\"phx_reply\",\"payload\":{\"response\":null,\"status\":null},\"ref\":\"null\",\"topic\":\"t\"}"
-  @v1_fastlane_json "{\"event\":\"e\",\"payload\":\"m\",\"ref\":null,\"topic\":\"t\"}"
 
   def encode!(serializer, msg) do
     {:socket_push, :text, encoded} = serializer.encode!(msg)
@@ -24,12 +22,26 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do
 
   test "encode!/1 encodes `Phoenix.Socket.Message` as JSON" do
     msg = %Message{topic: "t", event: "e", payload: "m"}
-    assert encode!(@serializer, msg) == @v1_msg_json
+    encoded = encode!(@serializer, msg)
+
+    assert Jason.decode!(encoded) == %{
+             "event" => "e",
+             "payload" => "m",
+             "ref" => nil,
+             "topic" => "t"
+           }
   end
 
   test "encode!/1 encodes `Phoenix.Socket.Reply` as JSON" do
     msg = %Reply{topic: "t", ref: "null"}
-    assert encode!(@serializer, msg) == @v1_reply_json
+    encoded = encode!(@serializer, msg)
+
+    assert Jason.decode!(encoded) == %{
+             "event" => "phx_reply",
+             "payload" => %{"response" => nil, "status" => nil},
+             "ref" => "null",
+             "topic" => "t"
+           }
   end
 
   test "decode!/2 decodes `Phoenix.Socket.Message` from JSON" do
@@ -47,6 +59,13 @@ defmodule Phoenix.Socket.V1.JSONSerializerTest do
 
   test "fastlane!/1 encodes a broadcast into a message as JSON" do
     msg = %Broadcast{topic: "t", event: "e", payload: "m"}
-    assert fastlane!(@serializer, msg) == @v1_fastlane_json
+    encoded = fastlane!(@serializer, msg)
+
+    assert Jason.decode!(encoded) == %{
+             "event" => "e",
+             "payload" => "m",
+             "ref" => nil,
+             "topic" => "t"
+           }
   end
 end
diff --git a/test/phoenix/socket/v2_json_serializer_test.exs b/test/phoenix/socket/v2_json_serializer_test.exs
index 4723eb727..c534ff47d 100644
--- a/test/phoenix/socket/v2_json_serializer_test.exs
+++ b/test/phoenix/socket/v2_json_serializer_test.exs
@@ -4,7 +4,6 @@ defmodule Phoenix.Socket.V2.JSONSerializerTest do
 
   @serializer V2.JSONSerializer
   @v2_fastlane_json "[null,null,\"t\",\"e\",{\"m\":1}]"
-  @v2_reply_json "[null,null,\"t\",\"phx_reply\",{\"response\":{\"m\":1},\"status\":null}]"
   @v2_msg_json "[null,null,\"t\",\"e\",{\"m\":1}]"
 
   @client_push <<
@@ -101,7 +100,15 @@ defmodule Phoenix.Socket.V2.JSONSerializerTest do
 
   test "encode!/1 encodes `Phoenix.Socket.Reply` as JSON" do
     msg = %Reply{topic: "t", payload: %{m: 1}}
-    assert encode!(@serializer, msg) == @v2_reply_json
+    encoded = encode!(@serializer, msg)
+
+    assert Jason.decode!(encoded) == [
+             nil,
+             nil,
+             "t",
+             "phx_reply",
+             %{"response" => %{"m" => 1}, "status" => nil}
+           ]
   end
 
   test "decode!/2 decodes `Phoenix.Socket.Message` from JSON" do
@@ -225,7 +232,6 @@ defmodule Phoenix.Socket.V2.JSONSerializerTest do
           payload: {:binary, <<101, 102, 103>>}
         })
       end
-
     end
   end