Repository: phoenixframework/phoenix
Pull request #3702 (2020-03-11 20:17:47)

Add phx.gen.live
Only thing left remaining is to flesh out the generated LiveView tests to fully exercise the CRUD functionality 


Patch:
diff --git a/assets/js/phoenix.js b/assets/js/phoenix.js
index 30af0cfd5..7b9dbe8a9 100644
--- a/assets/js/phoenix.js
+++ b/assets/js/phoenix.js
@@ -1200,9 +1200,7 @@ export class Ajax {
       let req = new XDomainRequest() // IE8, IE9
       this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
     } else {
-      let req = global.XMLHttpRequest ?
-                  new global.XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
-                  new ActiveXObject("Microsoft.XMLHTTP") // IE6, IE5
+      let req = new global.XMLHttpRequest(); // IE7+, Firefox, Chrome, Opera, Safari
       this.xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback)
     }
   }
diff --git a/installer/lib/phx_new/single.ex b/installer/lib/phx_new/single.ex
index 75b661cc5..b4f37ee4e 100644
--- a/installer/lib/phx_new/single.ex
+++ b/installer/lib/phx_new/single.ex
@@ -52,12 +52,11 @@ defmodule Phx.New.Single do
     {:eex, "phx_live/templates/layout/root.html.leex",       :project, "lib/:lib_web_name/templates/layout/root.html.leex"},
     {:eex, "phx_live/templates/layout/app.html.leex",        :project, "lib/:lib_web_name/templates/layout/app.html.leex"},
     {:eex, "phx_live/templates/layout/live.html.leex",       :project, "lib/:lib_web_name/templates/layout/live.html.leex"},
-    {:eex, "phx_web/templates/page/index.html.eex",          :project, "lib/:lib_web_name/templates/page/index.html.leex"},
     {:eex, "phx_web/views/layout_view.ex",                   :project, "lib/:lib_web_name/views/layout_view.ex"},
-    {:eex, "phx_web/views/page_view.ex",                     :project, "lib/:lib_web_name/views/page_view.ex"},
     {:eex, "phx_live/live/page_live.ex",                     :project, "lib/:lib_web_name/live/page_live.ex"},
+    {:eex, "phx_web/templates/page/index.html.eex",          :project, "lib/:lib_web_name/live/page_live.html.leex"},
     {:eex, "phx_test/views/layout_view_test.exs",            :project, "test/:lib_web_name/views/layout_view_test.exs"},
-    {:eex, "phx_test/live/home_live_test.exs",               :project, "test/:lib_web_name/live/home_live_test.exs"},
+    {:eex, "phx_test/live/page_live_test.exs",               :project, "test/:lib_web_name/live/page_live_test.exs"},
   ]
 
   template :ecto, [
diff --git a/installer/lib/phx_new/web.ex b/installer/lib/phx_new/web.ex
index 3677048c1..11192f5ad 100644
--- a/installer/lib/phx_new/web.ex
+++ b/installer/lib/phx_new/web.ex
@@ -53,12 +53,11 @@ defmodule Phx.New.Web do
     {:eex, "phx_live/templates/layout/root.html.leex", :web, "lib/:web_app/templates/layout/root.html.leex"},
     {:eex, "phx_live/templates/layout/app.html.leex",  :web, "lib/:web_app/templates/layout/app.html.leex"},
     {:eex, "phx_live/templates/layout/live.html.leex", :web, "lib/:web_app/templates/layout/live.html.leex"},
-    {:eex, "phx_web/templates/page/index.html.eex",    :web, "lib/:web_app/templates/page/index.html.leex"},
     {:eex, "phx_web/views/layout_view.ex",             :web, "lib/:web_app/views/layout_view.ex"},
-    {:eex, "phx_web/views/page_view.ex",               :web, "lib/:web_app/views/page_view.ex"},
     {:eex, "phx_live/live/page_live.ex",               :web, "lib/:web_app/live/page_live.ex"},
+    {:eex, "phx_web/templates/page/index.html.eex",    :web, "lib/:web_app/live/page_live.html.leex"},
     {:eex, "phx_test/views/layout_view_test.exs",      :web, "test/:web_app/views/layout_view_test.exs"},
-    {:eex, "phx_test/live/home_live_test.exs",         :web, "test/:web_app/live/home_live_test.exs"},
+    {:eex, "phx_test/live/page_live_test.exs",         :web, "test/:web_app/live/page_live_test.exs"},
   ]
 
   def prepare_project(%Project{app: app} = project) when not is_nil(app) do
diff --git a/installer/templates/phx_live/live/page_live.ex b/installer/templates/phx_live/live/page_live.ex
index 04dce2641..ca6ae5184 100644
--- a/installer/templates/phx_live/live/page_live.ex
+++ b/installer/templates/phx_live/live/page_live.ex
@@ -1,10 +1,6 @@
 defmodule <%= web_namespace %>.PageLive do
   use <%= web_namespace %>, :live_view
 
-  def render(assigns) do
-    <%= web_namespace %>.PageView.render("index.html", assigns)
-  end
-
   def mount(_params, _session, socket) do
     {:ok, assign(socket, query: "", results: %{})}
   end
diff --git a/installer/templates/phx_live/templates/layout/root.html.leex b/installer/templates/phx_live/templates/layout/root.html.leex
index 4fc1a1396..6a76c66a6 100644
--- a/installer/templates/phx_live/templates/layout/root.html.leex
+++ b/installer/templates/phx_live/templates/layout/root.html.leex
@@ -4,7 +4,7 @@
     <meta charset="utf-8"/>
     <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
     <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
-    <%%= live_title_tag assigns[:page_title] || <%= app_module %>, suffix: " · Phoenix Framework" %>
+    <%%= live_title_tag assigns[:page_title] || "<%= app_module %>", suffix: " · Phoenix Framework" %>
     <link rel="stylesheet" href="<%%= Routes.static_path(@conn, "/css/app.css") %>"/>
     <%%= csrf_meta_tag() %>
   </head>
diff --git a/installer/templates/phx_web/telemetry.ex b/installer/templates/phx_web/telemetry.ex
index 16139e54d..25e844454 100644
--- a/installer/templates/phx_web/telemetry.ex
+++ b/installer/templates/phx_web/telemetry.ex
@@ -16,7 +16,7 @@ defmodule <%= web_namespace %>.Telemetry do
     Supervisor.init(children, strategy: :one_for_one)
   end
 
-  defp metrics do
+  def metrics do
     [
       # Phoenix Metrics
       summary("phoenix.endpoint.stop.duration",
diff --git a/lib/mix/phoenix.ex b/lib/mix/phoenix.ex
index 47a9e0a11..a9d3fdcd8 100644
--- a/lib/mix/phoenix.ex
+++ b/lib/mix/phoenix.ex
@@ -319,7 +319,11 @@ defmodule Mix.Phoenix do
   Prompts to continue if any files exist.
   """
   def prompt_for_conflicts(generator_files) do
-    file_paths = Enum.map(generator_files, fn {_, _, path} -> path end)
+    file_paths =
+      Enum.flat_map(generator_files, fn
+        {:new_eex, _, _path} -> []
+        {_kind, _, path} -> [path]
+      end)
 
     case Enum.filter(file_paths, &File.exists?(&1)) do
       [] -> :ok
diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex
index 40405f842..8b41e1a04 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -18,7 +18,9 @@ defmodule Mix.Tasks.Phx.Gen.Html do
   > over distinct contexts (such as `Accounts.User` and `Payments.User`).
 
   The schema is responsible for mapping the database fields into an
-  Elixir struct.
+  Elixir struct. It is followed by an optional list of attributes,
+  with their respective names and types. See `mix phx.gen.schema`
+  for more information on attributes.
 
   Overall, this generator will add the following files to `lib/`:
 
@@ -28,6 +30,8 @@ defmodule Mix.Tasks.Phx.Gen.Html do
     * a controller in `lib/app_web/controllers/user_controller.ex`
     * default CRUD templates in `lib/app_web/templates/user`
 
+  ## The context app
+
   A migration file for the repository and test files for the context and
   controller features will also be generated.
 
@@ -58,42 +62,16 @@ defmodule Mix.Tasks.Phx.Gen.Html do
   Which would generate a `lib/app_web/controllers/sales/user_controller.ex` and
   `lib/app_web/views/sales/user_view.ex`.
 
-  ## Generating without a schema or context file
-
-  In some cases, you may wish to bootstrap HTML templates, controllers, and
-  controller tests, but leave internal implementation of the context or schema
-  to yourself. You can use the `--no-context` and `--no-schema` flags for
-  file generation control.
-
-  ## table
-
-  By default, the table name for the migration and schema will be
-  the plural name provided for the resource. To customize this value,
-  a `--table` option may be provided. For example:
-
-      mix phx.gen.html Accounts User users --table cms_users
-
-  ## binary_id
+  ## Customising the context, schema, tables and migrations
 
-  Generated migration can use `binary_id` for schema's primary key
-  and its references with option `--binary-id`.
+  In some cases, you may wish to bootstrap HTML templates, controllers,
+  and controller tests, but leave internal implementation of the context
+  or schema to yourself. You can use the `--no-context` and `--no-schema`
+  flags for file generation control.
 
-  ## Default options
-
-  This generator uses default options provided in the `:generators`
-  configuration of your application. These are the defaults:
-
-      config :your_app, :generators,
-        migration: true,
-        binary_id: false,
-        sample_binary_id: "11111111-1111-1111-1111-111111111111"
-
-  You can override those options per invocation by providing corresponding
-  switches, e.g. `--no-binary-id` to use normal ids despite the default
-  configuration or `--migration` to force generation of the migration.
-
-  Read the documentation for `phx.gen.schema` for more information on
-  attributes.
+  You can also change the table name or configure the migrations to
+  use binary ids for primary keys, see `mix phx.gen.schema` for more
+  information.
   """
   use Mix.Task
 
@@ -182,7 +160,8 @@ defmodule Mix.Tasks.Phx.Gen.Html do
     if context.generate?, do: Gen.Context.print_shell_instructions(context)
   end
 
-  defp inputs(%Schema{} = schema) do
+  @doc false
+  def inputs(%Schema{} = schema) do
     Enum.map(schema.attrs, fn
       {_, {:references, _}} ->
         {nil, nil, nil}
diff --git a/lib/mix/tasks/phx.gen.json.ex b/lib/mix/tasks/phx.gen.json.ex
index f6b28da2e..15c89ee3f 100644
--- a/lib/mix/tasks/phx.gen.json.ex
+++ b/lib/mix/tasks/phx.gen.json.ex
@@ -18,7 +18,9 @@ defmodule Mix.Tasks.Phx.Gen.Json do
   > over distinct contexts (such as `Accounts.User` and `Payments.User`).
 
   The schema is responsible for mapping the database fields into an
-  Elixir struct.
+  Elixir struct. It is followed by an optional list of attributes,
+  with their respective names and types. See `mix phx.gen.schema`
+  for more information on attributes.
 
   Overall, this generator will add the following files to `lib/`:
 
@@ -30,6 +32,8 @@ defmodule Mix.Tasks.Phx.Gen.Json do
   A migration file for the repository and test files for the context and
   controller features will also be generated.
 
+  ## The context app
+
   The location of the web files (controllers, views, templates, etc) in an
   umbrella application will vary based on the `:context_app` config located
   in your applications `:generators` configuration. When set, the Phoenix
@@ -57,42 +61,16 @@ defmodule Mix.Tasks.Phx.Gen.Json do
   Which would generate a `lib/app_web/controllers/sales/user_controller.ex` and
   `lib/app_web/views/sales/user_view.ex`.
 
-  ## Generating without a schema or context file
-
-  In some cases, you may wish to bootstrap JSON views, controllers, and
-  controller tests, but leave internal implementation of the context or schema
-  to yourself. You can use the `--no-context` and `--no-schema` flags for
-  file generation control.
-
-  ## table
-
-  By default, the table name for the migration and schema will be
-  the plural name provided for the resource. To customize this value,
-  a `--table` option may be provided. For example:
-
-      mix phx.gen.json Accounts User users --table cms_users
-
-  ## binary_id
-
-  Generated migration can use `binary_id` for schema's primary key
-  and its references with option `--binary-id`.
-
-  ## Default options
-
-  This generator uses default options provided in the `:generators`
-  configuration of your application. These are the defaults:
-
-      config :your_app, :generators,
-        migration: true,
-        binary_id: false,
-        sample_binary_id: "11111111-1111-1111-1111-111111111111"
+  ## Customising the context, schema, tables and migrations
 
-  You can override those options per invocation by providing corresponding
-  switches, e.g. `--no-binary-id` to use normal ids despite the default
-  configuration or `--migration` to force generation of the migration.
+  In some cases, you may wish to bootstrap JSON views, controllers,
+  and controller tests, but leave internal implementation of the context
+  or schema to yourself. You can use the `--no-context` and `--no-schema`
+  flags for file generation control.
 
-  Read the documentation for `phx.gen.schema` for more information on
-  attributes.
+  You can also change the table name or configure the migrations to
+  use binary ids for primary keys, see `mix phx.gen.schema` for more
+  information.
   """
 
   use Mix.Task
diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex
new file mode 100644
index 000000000..c3d92a4e2
--- /dev/null
+++ b/lib/mix/tasks/phx.gen.live.ex
@@ -0,0 +1,228 @@
+defmodule Mix.Tasks.Phx.Gen.Live do
+  @shortdoc "Generates LiveView, templates, and context for a resource"
+
+  @moduledoc """
+  Generates LiveView, templates, and context for a resource.
+
+      mix phx.gen.live Accounts User users name:string age:integer
+
+  The first argument is the context module followed by the schema module
+  and its plural name (used as the schema table name).
+
+  The context is an Elixir module that serves as an API boundary for
+  the given resource. A context often holds many related resources.
+  Therefore, if the context already exists, it will be augmented with
+  functions for the given resource.
+
+  When this command is run for the first time, a `ModalComponent` and
+  `LiveHelpers` module will be created, along with the resource level
+  LiveViews and components, including an `IndexLive`, `ShowLive`, `FormComponent`
+  for the new resource.
+
+  > Note: A resource may also be split
+  > over distinct contexts (such as `Accounts.User` and `Payments.User`).
+
+  The schema is responsible for mapping the database fields into an
+  Elixir struct. It is followed by an optional list of attributes,
+  with their respective names and types. See `mix phx.gen.schema`
+  for more information on attributes.
+
+  Overall, this generator will add the following files to `lib/`:
+
+    * a context module in `lib/app/accounts.ex` for the accounts API
+    * a schema in `lib/app/accounts/user.ex`, with an `users` table
+    * a view in `lib/app_web/views/user_view.ex`
+    * a LiveView in `lib/app_web/live/user_live/show_live.ex`
+    * a LiveView in `lib/app_web/live/user_live/index_live.ex`
+    * a LiveComponent in `lib/app_web/live/user_live/form_component.ex`
+    * a LiveComponent in `lib/app_web/live/modal_component.ex`
+    * a helpers modules in `lib/app_web/live/live_helpers.ex`
+
+  ## The context app
+
+  A migration file for the repository and test files for the context and
+  controller features will also be generated.
+
+  The location of the web files (LiveView's, views, templates, etc) in an
+  umbrella application will vary based on the `:context_app` config located
+  in your applications `:generators` configuration. When set, the Phoenix
+  generators will generate web files directly in your lib and test folders
+  since the application is assumed to be isolated to web specific functionality.
+  If `:context_app` is not set, the generators will place web related lib
+  and test files in a `web/` directory since the application is assumed
+  to be handling both web and domain specific functionality.
+  Example configuration:
+
+      config :my_app_web, :generators, context_app: :my_app
+
+  Alternatively, the `--context-app` option may be supplied to the generator:
+
+      mix phx.gen.live Sales User users --context-app warehouse
+
+  ## Web namespace
+
+  By default, the controller and view will be namespaced by the schema name.
+  You can customize the web module namespace by passing the `--web` flag with a
+  module name, for example:
+
+      mix phx.gen.live Sales User users --web Sales
+
+  Which would generate a LiveViews inside `lib/app_web/live/sales/user_live/` and a
+  view at `lib/app_web/views/sales/user_view.ex`.
+
+  ## Customising the context, schema, tables and migrations
+
+  In some cases, you may wish to bootstrap HTML templates, LiveViews,
+  and tests, but leave internal implementation of the context or schema
+  to yourself. You can use the `--no-context` and `--no-schema` flags
+  for file generation control.
+
+  You can also change the table name or configure the migrations to
+  use binary ids for primary keys, see `mix phx.gen.schema` for more
+  information.
+  """
+  use Mix.Task
+
+  alias Mix.Phoenix.{Context}
+  alias Mix.Tasks.Phx.Gen
+
+  @doc false
+  def run(args) do
+    if Mix.Project.umbrella?() do
+      Mix.raise "mix phx.gen.live can only be run inside an application directory"
+    end
+
+    {context, schema} = Gen.Context.build(args)
+    Gen.Context.prompt_for_code_injection(context)
+
+    binding = [context: context, schema: schema, inputs: Gen.Html.inputs(schema)]
+    paths = Mix.Phoenix.generator_paths()
+
+    prompt_for_conflicts(context)
+
+    context
+    |> copy_new_files(binding, paths)
+    |> maybe_inject_helpers()
+    |> print_shell_instructions()
+  end
+
+  defp prompt_for_conflicts(context) do
+    context
+    |> files_to_be_generated()
+    |> Kernel.++(context_files(context))
+    |> Mix.Phoenix.prompt_for_conflicts()
+  end
+  defp context_files(%Context{generate?: true} = context) do
+    Gen.Context.files_to_be_generated(context)
+  end
+  defp context_files(%Context{generate?: false}) do
+    []
+  end
+
+  defp files_to_be_generated(%Context{schema: schema, context_app: context_app}) do
+    web_prefix = Mix.Phoenix.web_path(context_app)
+    test_prefix = Mix.Phoenix.web_test_path(context_app)
+    web_path = to_string(schema.web_path)
+    live_subdir = "#{schema.singular}_live"
+
+    [
+      {:eex, "show.ex",         Path.join([web_prefix, "live", web_path, live_subdir, "show.ex"])},
+      {:eex, "index.ex",        Path.join([web_prefix, "live", web_path, live_subdir, "index.ex"])},
+      {:eex, "form.ex",         Path.join([web_prefix, "live", web_path, live_subdir, "form.ex"])},
+      {:eex, "form.html.leex",  Path.join([web_prefix, "live", web_path, live_subdir, "form.html.leex"])},
+      {:eex, "index.html.leex", Path.join([web_prefix, "live", web_path, live_subdir, "index.html.leex"])},
+      {:eex, "show.html.leex",  Path.join([web_prefix, "live", web_path, live_subdir, "show.html.leex"])},
+      {:eex, "live_test.exs",   Path.join([test_prefix, "live", web_path, "#{schema.singular}_live_test.exs"])},
+      {:new_eex, "modal.ex",        Path.join([web_prefix, "live", "modal.ex"])},
+      {:new_eex, "live_helpers.ex", Path.join([web_prefix, "live", "live_helpers.ex"])},
+    ]
+  end
+
+  defp copy_new_files(%Context{} = context, binding, paths) do
+    files = files_to_be_generated(context)
+    Mix.Phoenix.copy_from(paths, "priv/templates/phx.gen.live", binding, files)
+    if context.generate?, do: Gen.Context.copy_new_files(context, paths, binding)
+
+    context
+  end
+
+  defp maybe_inject_helpers(%Context{context_app: ctx_app} = context) do
+    web_prefix = Mix.Phoenix.web_path(ctx_app)
+    [lib_prefix, web_dir] = Path.split(web_prefix)
+    file_path = Path.join(lib_prefix, "#{web_dir}.ex")
+    file = File.read!(file_path)
+    inject = "import #{inspect(context.web_module)}.LiveHelpers"
+
+    if String.contains?(file, inject) do
+      :ok
+    else
+      do_inject_helpers(context, file, file_path, inject)
+    end
+
+    context
+  end
+
+  defp do_inject_helpers(context, file, file_path, inject) do
+    Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path)])
+
+    new_file = String.replace(file, "import Phoenix.LiveView.Helpers", "import Phoenix.LiveView.Helpers\n      #{inject}")
+    if file != new_file do
+      File.write!(file_path, new_file)
+    else
+      Mix.shell().info """
+
+      Add your #{inspect(context.web_module)}.LiveHelpers import to your view_helpers block in #{file_path}:
+
+          defp view_helpers do
+            quote do
+              # Use all HTML functionality (forms, tags, etc)
+              use Phoenix.HTML
+
+              # Import convenience functions for LiveView rendering
+              import Phoenix.LiveView.Helpers
+              #{inject}
+              ...
+            end
+          end
+      """
+    end
+  end
+
+  @doc false
+  def print_shell_instructions(%Context{schema: schema, context_app: ctx_app} = context) do
+    prefix = Module.concat(context.web_module, schema.web_namespace)
+    web_path = Mix.Phoenix.web_path(ctx_app)
+
+    if schema.web_namespace do
+      Mix.shell().info """
+
+      Add the live routes to your #{schema.web_namespace} :browser scope in #{web_path}/router.ex:
+
+          scope "/#{schema.web_path}", #{inspect prefix}, as: :#{schema.web_path} do
+            pipe_through :browser
+            ...
+
+      #{for line <- live_route_instructions(schema), do: "      #{line}"}
+          end
+      """
+    else
+      Mix.shell().info """
+
+      Add the live routes to your browser scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex:
+
+      #{for line <- live_route_instructions(schema), do: "    #{line}"}
+      """
+    end
+    if context.generate?, do: Gen.Context.print_shell_instructions(context)
+  end
+
+  defp live_route_instructions(schema) do
+    [
+      ~s|live "/#{schema.plural}", #{inspect(schema.alias)}Live.Index, :index\n|,
+      ~s|live "/#{schema.plural}/new", #{inspect(schema.alias)}Live.Index, :new\n|,
+      ~s|live "/#{schema.plural}/:id/edit", #{inspect(schema.alias)}Live.Index, :edit\n\n|,
+      ~s|live "/#{schema.plural}/:id", #{inspect(schema.alias)}Live.Show, :show\n|,
+      ~s|live "/#{schema.plural}/:id/show/edit", #{inspect(schema.alias)}Live.Show, :edit|
+    ]
+  end
+end
diff --git a/lib/phoenix/logger.ex b/lib/phoenix/logger.ex
index 3569157f6..c8ac0bc9d 100644
--- a/lib/phoenix/logger.ex
+++ b/lib/phoenix/logger.ex
@@ -1,7 +1,7 @@
 defmodule Phoenix.Logger do
   @moduledoc """
   Instrumenter to handle logging of various instrumentation events.
-  
+
   ## Instrumentation
 
   Phoenix uses the `:telemetry` library for instrumentation. The following events
@@ -27,8 +27,14 @@ defmodule Phoenix.Logger do
       * Metadata: `%{conn: Plug.Conn.t, route: binary, plug: module, plug_opts: term, path_params: map, pipe_through: [atom], log: Logger.level | false}`
       * Disable logging: Pass `log: false` to the router macro, for example: `get("/page", PageController, :index, log: false)`
 
+    * `[:phoenix, :router_dispatch, :exception]` - dispatched by `Phoenix.Router`
+      after exceptions on dispatching a route
+      * Measurement: `%{duration: native_time}`
+      * Metadata: `%{kind: :throw | :error | :exit, reason: term(), stacktrace: Exception.stacktrace()}`
+      * Disable logging: This event is not logged
+
     * `[:phoenix, :router_dispatch, :stop]` - dispatched by `Phoenix.Router`
-      after successfully dispatching to a matched route
+      after successfully dispatching a matched route
       * Measurement: `%{duration: native_time}`
       * Metadata: `%{conn: Plug.Conn.t, route: binary, plug: module, plug_opts: term, path_params: map, pipe_through: [atom], log: Logger.level | false}`
       * Disable logging: This event is not logged
@@ -47,7 +53,7 @@ defmodule Phoenix.Logger do
       * Measurement: `%{duration: native_time}`
       * Metadata: `%{params: term, socket: Phoenix.Socket.t}`
       * Disable logging: This event cannot be disabled
-  
+
     * `[:phoenix, :channel_handled_in]` - dispatched at the end of a channel handle in
       * Measurement: `%{duration: native_time}`
       * Metadata: `%{event: binary, params: term, socket: Phoenix.Socket.t}`
@@ -242,7 +248,6 @@ defmodule Phoenix.Logger do
       %{
         transport: transport,
         params: params,
-        connect_info: connect_info,
         user_socket: user_socket,
         result: result,
         serializer: serializer
@@ -257,8 +262,6 @@ defmodule Phoenix.Logger do
         inspect(transport),
         "\n  Serializer: ",
         inspect(serializer),
-        "\n  Connect Info: ",
-        inspect(connect_info),
         "\n  Parameters: ",
         inspect(filter_values(params))
       ]
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index fd01c2cdb..3e76b7444 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -360,14 +360,14 @@ defmodule Phoenix.Router do
           e in Plug.Conn.WrapperError ->
             measurements = %{duration: System.monotonic_time() - start}
             metadata = %{kind: :error, error: e, stacktrace: __STACKTRACE__}
-            :telemetry.execute([:phoenix, :router_dispatch, :failure], measurements, metadata)
+            :telemetry.execute([:phoenix, :router_dispatch, :exception], measurements, metadata)
             Plug.Conn.WrapperError.reraise(e)
         catch
-          :error, reason ->
+          kind, reason ->
             measurements = %{duration: System.monotonic_time() - start}
-            metadata = %{kind: :error, error: reason, stacktrace: __STACKTRACE__}
-            :telemetry.execute([:phoenix, :router_dispatch, :failure], measurements, metadata)
-            Plug.Conn.WrapperError.reraise(piped_conn, :error, reason, __STACKTRACE__)
+            metadata = %{kind: kind, error: reason, stacktrace: __STACKTRACE__}
+            :telemetry.execute([:phoenix, :router_dispatch, :exception], measurements, metadata)
+            Plug.Conn.WrapperError.reraise(piped_conn, kind, reason, __STACKTRACE__)
         end
     end
   end
@@ -544,7 +544,10 @@ defmodule Phoenix.Router do
 
   ## Options
 
-    * `:as` - configures the named helper exclusively
+    * `:as` - configures the named helper exclusively. If false, does not generate
+      a helper.
+    * `:alias` - configure if the scope alias should be applied to the route.
+      Defaults to true, disables scoping if false.
     * `:log` - the level to log the route dispatching under,
       may be set to false. Defaults to `:debug`
     * `:host` - a string containing the host scope, or prefix host scope,
@@ -818,11 +821,11 @@ defmodule Phoenix.Router do
 
   The supported options are:
 
-    * `:path` - a string containing the path scope
-    * `:as` - a string or atom containing the named helper scope
-    * `:alias` - an alias (atom) containing the controller scope.
-      When set, this value may be overridden per route by passing `alias: false`
-      to route definitions, such as `get`, `post`, etc.
+    * `:path` - a string containing the path scope.
+    * `:as` - a string or atom containing the named helper scope. When set to
+      false, it resets the nested helper scopes.
+    * `:alias` - an alias (atom) containing the controller scope. When set to
+      false, it resets all nested aliases.
     * `:host` - a string containing the host scope, or prefix host scope,
       ie `"foo.bar.com"`, `"foo."`
     * `:private` - a map of private data to merge into the connection when a route matches
diff --git a/lib/phoenix/router/scope.ex b/lib/phoenix/router/scope.ex
index 53dd33be3..67164e66d 100644
--- a/lib/phoenix/router/scope.ex
+++ b/lib/phoenix/router/scope.ex
@@ -91,6 +91,8 @@ defmodule Phoenix.Router.Scope do
   end
 
   def push(module, opts) when is_list(opts) do
+    top = get_top(module)
+
     path =
       if path = Keyword.get(opts, :path) do
         path |> validate_path() |> String.split("/", trim: true)
@@ -98,19 +100,18 @@ defmodule Phoenix.Router.Scope do
         []
       end
 
-    alias = Keyword.get(opts, :alias) |> List.wrap() |> Enum.map(&Atom.to_string/1)
-    as = Keyword.get(opts, :as) |> List.wrap()
+    alias = append_unless_false(top, opts, :alias, &Atom.to_string(&1))
+    as = append_unless_false(top, opts, :as, & &1)
     host = Keyword.get(opts, :host)
     private = Keyword.get(opts, :private, %{})
     assigns = Keyword.get(opts, :assigns, %{})
 
-    top = get_top(module)
     update_stack(module, fn stack -> [top | stack] end)
 
     put_top(module, %Scope{
       path: top.path ++ path,
-      alias: top.alias ++ alias,
-      as: top.as ++ as,
+      alias: alias,
+      as: as,
       host: host || top.host,
       pipes: top.pipes,
       private: Map.merge(top.private, private),
@@ -120,6 +121,14 @@ defmodule Phoenix.Router.Scope do
     })
   end
 
+  defp append_unless_false(top, opts, key, fun) do
+    case opts[key] do
+      false -> []
+      nil -> Map.fetch!(top, key)
+      other -> Map.fetch!(top, key) ++ [fun.(other)]
+    end
+  end
+
   @doc """
   Pops a scope from the module stack.
   """
diff --git a/priv/templates/phx.gen.context/access_no_schema.ex b/priv/templates/phx.gen.context/access_no_schema.ex
index 104175d62..71afc7768 100644
--- a/priv/templates/phx.gen.context/access_no_schema.ex
+++ b/priv/templates/phx.gen.context/access_no_schema.ex
@@ -84,6 +84,6 @@
       %Todo{...}
 
   """
-  def change_<%= schema.singular %>(%<%= inspect schema.alias %>{} = <%= schema.singular %>) do
+  def change_<%= schema.singular %>(%<%= inspect schema.alias %>{} = <%= schema.singular %>, _attrs \\ %{}) do
     raise "TODO"
   end
diff --git a/priv/templates/phx.gen.context/schema_access.ex b/priv/templates/phx.gen.context/schema_access.ex
index 38a3db8f6..bdee344d5 100644
--- a/priv/templates/phx.gen.context/schema_access.ex
+++ b/priv/templates/phx.gen.context/schema_access.ex
@@ -91,6 +91,6 @@
       %Ecto.Changeset{data: %<%= inspect schema.alias %>{}}
 
   """
-  def change_<%= schema.singular %>(%<%= inspect schema.alias %>{} = <%= schema.singular %>) do
-    <%= inspect schema.alias %>.changeset(<%= schema.singular %>, %{})
+  def change_<%= schema.singular %>(%<%= inspect schema.alias %>{} = <%= schema.singular %>, attrs \\ %{}) do
+    <%= inspect schema.alias %>.changeset(<%= schema.singular %>, attrs)
   end
diff --git a/priv/templates/phx.gen.live/form.ex b/priv/templates/phx.gen.live/form.ex
new file mode 100644
index 000000000..3ab2b2a67
--- /dev/null
+++ b/priv/templates/phx.gen.live/form.ex
@@ -0,0 +1,53 @@
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Form do
+  use <%= inspect context.web_module %>, :live_component
+
+  alias <%= inspect context.module %>
+
+  def update(%{<%= schema.singular %>: <%= schema.singular %>} = assigns, socket) do
+    changeset = <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>)
+
+    {:ok,
+     socket
+     |> assign(assigns)
+     |> assign(:changeset, changeset)}
+  end
+
+  def handle_event("validate", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
+    changeset =
+      socket.assigns.<%= schema.singular %>
+      |> <%= inspect context.alias %>.change_<%= schema.singular %>(<%= schema.singular %>_params)
+      |> Map.put(:action, :validate)
+
+    {:noreply, assign(socket, :changeset, changeset)}
+  end
+
+  def handle_event("save", %{"<%= schema.singular %>" => <%= schema.singular %>_params}, socket) do
+    save_<%= schema.singular %>(socket, socket.assigns.action, <%= schema.singular %>_params)
+  end
+
+  defp save_<%= schema.singular %>(socket, :edit, <%= schema.singular %>_params) do
+    case <%= inspect context.alias %>.update_<%= schema.singular %>(socket.assigns.<%= schema.singular %>, <%= schema.singular %>_params) do
+      {:ok, _<%= schema.singular %>} ->
+        {:noreply,
+         socket
+         |> put_flash(:info, "<%= schema.human_singular %> updated successfully")
+         |> push_redirect(to: socket.assigns.return_to)}
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        {:noreply, assign(socket, :changeset, changeset)}
+    end
+  end
+
+  defp save_<%= schema.singular %>(socket, :new, <%= schema.singular %>_params) do
+    case <%= inspect context.alias %>.create_<%= schema.singular %>(<%= schema.singular %>_params) do
+      {:ok, _<%= schema.singular %>} ->
+        {:noreply,
+         socket
+         |> put_flash(:info, "<%= schema.human_singular %> created successfully")
+         |> push_redirect(to: socket.assigns.return_to)}
+
+      {:error, %Ecto.Changeset{} = changeset} ->
+        {:noreply, assign(socket, changeset: changeset)}
+    end
+  end
+end
diff --git a/priv/templates/phx.gen.live/form.html.leex b/priv/templates/phx.gen.live/form.html.leex
new file mode 100644
index 000000000..d7f827f3f
--- /dev/null
+++ b/priv/templates/phx.gen.live/form.html.leex
@@ -0,0 +1,14 @@
+<h2><%%= @title %></h2>
+
+<%%= f = form_for @changeset, "#",
+  id: "form-#{@id}",
+  phx_target: "#form-#{@id}",
+  phx_change: "validate",
+  phx_submit: "save" %>
+<%= for {label, input, error} <- inputs, input do %>
+  <%= label %>
+  <%= input %>
+  <%= error %>
+<% end %>
+  <%%= submit "Save", phx_disable_with: "Saving..." %>
+</form>
diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex
new file mode 100644
index 000000000..d6f65356d
--- /dev/null
+++ b/priv/templates/phx.gen.live/index.ex
@@ -0,0 +1,46 @@
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Index do
+  use <%= inspect context.web_module %>, :live_view
+
+  alias <%= inspect context.module %>
+  alias <%= inspect schema.module %>
+
+  def mount(_params, _session, socket) do
+    {:ok, socket}
+  end
+
+  def handle_params(params, _url, socket) do
+    {:noreply, handle_action(socket.assigns.live_action, params, socket)}
+  end
+
+  defp handle_action(:edit, %{"id" => id}, socket) do
+    socket
+    |> assign(:page_title, "Edit <%= schema.human_singular %>")
+    |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))
+    |> assign_new(:<%= schema.plural %>, &fetch_<%= schema.plural %>/0)
+  end
+
+  defp handle_action(:new, _params, socket) do
+    socket
+    |> assign(:page_title, "New <%= schema.human_singular %>")
+    |> assign(:<%= schema.singular %>, %<%= inspect schema.alias %>{})
+    |> assign_new(:<%= schema.plural %>, &fetch_<%= schema.plural %>/0)
+  end
+
+  defp handle_action(:index, _params, socket) do
+    socket
+    |> assign(:page_title, "Listing <%= schema.human_plural %>")
+    |> assign(:<%= schema.singular %>, nil)
+    |> assign(:<%= schema.plural %>, fetch_<%= schema.plural %>())
+  end
+
+  def handle_event("delete", %{"id" => id}, socket) do
+    <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id)
+    {:ok, _} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>)
+
+    {:noreply, assign(socket, :<%= schema.plural %>, fetch_<%=schema.plural %>())}
+  end
+
+  defp fetch_<%= schema.plural %> do
+    <%= inspect context.alias %>.list_<%= schema.plural %>()
+  end
+end
diff --git a/priv/templates/phx.gen.live/index.html.leex b/priv/templates/phx.gen.live/index.html.leex
new file mode 100644
index 000000000..b209b4821
--- /dev/null
+++ b/priv/templates/phx.gen.live/index.html.leex
@@ -0,0 +1,35 @@
+<h1>Listing <%= schema.human_plural %></h1>
+
+<%%= if @live_action in [:new, :edit] do %>
+  <%%= live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Form,
+    id: @<%= schema.singular %>.id || :new,
+    title: @page_title,
+    action: @live_action,
+    <%= schema.singular %>: @<%= schema.singular %>,
+    return_to: Routes.<%= schema.singular %>_index_path(@socket, :index) %>
+<%% end %>
+
+<table>
+  <thead>
+    <tr>
+<%= for {k, _} <- schema.attrs do %>      <th><%= Phoenix.Naming.humanize(Atom.to_string(k)) %></th>
+<% end %>
+      <th></th>
+    </tr>
+  </thead>
+  <tbody>
+    <%%= for <%= schema.singular %> <- @<%= schema.plural %> do %>
+      <tr>
+<%= for {k, _} <- schema.attrs do %>        <td><%%= <%= schema.singular %>.<%= k %> %></td>
+<% end %>
+        <td>
+          <span><%%= live_redirect "Show", to: Routes.<%= schema.singular %>_show_path(@socket, :show, <%= schema.singular %>) %></span>
+          <span><%%= live_patch "Edit", to: Routes.<%= schema.singular %>_index_path(@socket, :edit, <%= schema.singular %>) %></span>
+          <span><%%= link "Delete", to: "#", phx_click: "delete", phx_value_id: <%= schema.singular %>.id, data: [confirm: "Are you sure?"] %></span>
+        </td>
+      </tr>
+    <%% end %>
+  </tbody>
+</table>
+
+<span><%%= live_patch "New <%= schema.human_singular %>", to: Routes.<%= schema.singular %>_index_path(@socket, :new) %></span>
diff --git a/priv/templates/phx.gen.live/live_helpers.ex b/priv/templates/phx.gen.live/live_helpers.ex
new file mode 100644
index 000000000..26e88e05b
--- /dev/null
+++ b/priv/templates/phx.gen.live/live_helpers.ex
@@ -0,0 +1,23 @@
+defmodule <%= inspect context.web_module %>.LiveHelpers do
+  import Phoenix.LiveView.Helpers
+
+  @doc """
+  Renders a component inside the `<%= inspect context.web_module %>.Modal` component.
+
+  The rendered modal receives a `:return_to` option to properly update
+  the URL when the modal is closed.
+
+  ## Examples
+
+      <%%= live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Form,
+        id: @<%= schema.singular %>.id || :new,
+        action: @live_action,
+        <%= schema.singular %>: @<%= schema.singular %>,
+        return_to: Routes.<%= schema.singular %>_index_path(@socket, :index) %>
+  """
+  def live_modal(socket, component, opts) do
+    path = Keyword.fetch!(opts, :return_to)
+    modal_opts = [id: :modal, return_to: path, component: component, opts: opts]
+    live_component(socket, <%= inspect context.web_module %>.Modal, modal_opts)
+  end
+end
diff --git a/priv/templates/phx.gen.live/modal.ex b/priv/templates/phx.gen.live/modal.ex
new file mode 100644
index 000000000..f6a940aba
--- /dev/null
+++ b/priv/templates/phx.gen.live/modal.ex
@@ -0,0 +1,24 @@
+defmodule <%= inspect context.web_module %>.Modal do
+  use <%= inspect context.web_module %>, :live_component
+
+  def render(assigns) do
+    ~L"""
+    <div id="<%%= @id %>" class="phx-modal"
+      phx-capture-click="close"
+      phx-window-keydown="close"
+      phx-key="escape"
+      phx-target="#<%%= @id %>"
+      phx-page-loading>
+
+      <div class="phx-modal-content">
+        <%%= live_patch raw("&times;"), to: @return_to, class: "phx-modal-close" %>
+        <%%= live_component @socket, @component, @opts %>
+      </div>
+    </div>
+    """
+  end
+
+  def handle_event("close", _, socket) do
+    {:noreply, push_patch(socket, to: socket.assigns.return_to)}
+  end
+end
diff --git a/priv/templates/phx.gen.live/show.ex b/priv/templates/phx.gen.live/show.ex
new file mode 100644
index 000000000..4fe01a928
--- /dev/null
+++ b/priv/templates/phx.gen.live/show.ex
@@ -0,0 +1,19 @@
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Show do
+  use <%= inspect context.web_module %>, :live_view
+
+  alias <%= inspect context.module %>
+
+  def mount(_params, _session, socket) do
+    {:ok, socket}
+  end
+
+  def handle_params(%{"id" => id}, _, socket) do
+    {:noreply,
+     socket
+     |> assign(:page_title, page_title(socket.assigns.live_action))
+     |> assign(:<%= schema.singular %>, <%= inspect context.alias %>.get_<%= schema.singular %>!(id))}
+  end
+
+  defp page_title(:show), do: "Show <%= schema.human_singular %>"
+  defp page_title(:edit), do: "Edit <%= schema.human_singular %>"
+end
diff --git a/priv/templates/phx.gen.live/show.html.leex b/priv/templates/phx.gen.live/show.html.leex
new file mode 100644
index 000000000..254adef73
--- /dev/null
+++ b/priv/templates/phx.gen.live/show.html.leex
@@ -0,0 +1,22 @@
+<h1>Show <%= schema.human_singular %></h1>
+
+<%%= if @live_action in [:edit] do %>
+  <%%= live_modal @socket, <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>Live.Form,
+    id: @<%= schema.singular %>.id,
+    title: @page_title,
+    action: @live_action,
+    <%= schema.singular %>: @<%= schema.singular %>,
+    return_to: Routes.<%= schema.singular %>_show_path(@socket, :show, @<%= schema.singular %>) %>
+<%% end %>
+
+<ul>
+<%= for {k, _} <- schema.attrs do %>
+  <li>
+    <strong><%= Phoenix.Naming.humanize(Atom.to_string(k)) %>:</strong>
+    <%%= @<%= schema.singular %>.<%= k %> %>
+  </li>
+<% end %>
+</ul>
+
+<span><%%= live_patch "Edit", to: Routes.<%= schema.singular %>_show_path(@socket, :edit, @<%= schema.singular %>), class: "button", phx_disable_with: "loading..." %></span>
+<span><%%= live_redirect "Back", to: Routes.<%= schema.singular %>_index_path(@socket, :index) %></span>


Test patch:
diff --git a/installer/templates/phx_test/live/home_live_test.exs b/installer/templates/phx_test/live/home_live_test.exs
deleted file mode 100644
index c9fdede09..000000000
--- a/installer/templates/phx_test/live/home_live_test.exs
+++ /dev/null
@@ -1,11 +0,0 @@
-defmodule <%= web_namespace %>.HomeLiveTest do
-  use <%= web_namespace %>.ConnCase, async: true
-
-  import Phoenix.LiveViewTest
-
-  test "disconnected and connected render", %{conn: conn} do
-    {:ok, home_live, disconneted_html} = live(conn, "/")
-    assert disconneted_html =~ "Welcome to Phoenix!"
-    assert render(home_live) =~ "Welcome to Phoenix!"
-  end
-end
diff --git a/installer/templates/phx_test/live/page_live_test.exs b/installer/templates/phx_test/live/page_live_test.exs
new file mode 100644
index 000000000..08df863c7
--- /dev/null
+++ b/installer/templates/phx_test/live/page_live_test.exs
@@ -0,0 +1,11 @@
+defmodule <%= web_namespace %>.PageLiveTest do
+  use <%= web_namespace %>.ConnCase
+
+  import Phoenix.LiveViewTest
+
+  test "disconnected and connected render", %{conn: conn} do
+    {:ok, page_live, disconneted_html} = live(conn, "/")
+    assert disconneted_html =~ "Welcome to Phoenix!"
+    assert render(page_live) =~ "Welcome to Phoenix!"
+  end
+end
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index fd73e31ff..b1934b01a 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -166,7 +166,7 @@ defmodule Mix.Tasks.Phx.NewTest do
         assert file =~ "{:telemetry_poller, measurements: periodic_measurements()"
         assert file =~ "defp periodic_measurements do"
         assert file =~ "# {PhxBlogWeb, :count_users, []}"
-        assert file =~ "defp metrics do"
+        assert file =~ "def metrics do"
         assert file =~ "summary(\"phoenix.endpoint.stop.duration\","
         assert file =~ "summary(\"phoenix.router_dispatch.stop.duration\","
         assert file =~ "# Database Metrics"
@@ -322,7 +322,7 @@ defmodule Mix.Tasks.Phx.NewTest do
         assert file =~ ~s|<%= live_title_tag assigns[:page_title]|
       end
 
-      assert_file "phx_blog/lib/phx_blog_web/templates/page/index.html.leex", fn file ->
+      assert_file "phx_blog/lib/phx_blog_web/live/page_live.html.leex", fn file ->
         assert file =~ ~s[Welcome]
       end
 
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index ee236d820..69b3db8d4 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -196,7 +196,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
         assert file =~ "{:telemetry_poller, measurements: periodic_measurements()"
         assert file =~ "defp periodic_measurements do"
         assert file =~ "# {PhxUmbWeb, :count_users, []}"
-        assert file =~ "defp metrics do"
+        assert file =~ "def metrics do"
         assert file =~ "summary(\"phoenix.endpoint.stop.duration\","
         assert file =~ "summary(\"phoenix.router_dispatch.stop.duration\","
         assert file =~ "# Database Metrics"
@@ -334,7 +334,7 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
         assert file =~ "defmodule PhxUmbWeb.PageLive do"
       end
 
-      assert_file web_path(@app, "lib/#{@app}_web/templates/page/index.html.leex"), fn file ->
+      assert_file web_path(@app, "lib/#{@app}_web/live/page_live.html.leex"), fn file ->
         assert file =~ ~s[Welcome]
       end
 
diff --git a/priv/templates/phx.gen.live/live_test.exs b/priv/templates/phx.gen.live/live_test.exs
new file mode 100644
index 000000000..a97ecfbcc
--- /dev/null
+++ b/priv/templates/phx.gen.live/live_test.exs
@@ -0,0 +1,77 @@
+defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web_namespace, schema.alias) %>LiveTest do
+  use <%= inspect context.web_module %>.ConnCase
+
+  import Phoenix.LiveViewTest
+
+  alias <%= inspect context.module %>
+
+  @create_attrs <%= inspect schema.params.create %>
+  @update_attrs <%= inspect schema.params.update %>
+  @invalid_attrs <%= inspect for {key, _} <- schema.params.create, into: %{}, do: {key, nil} %>
+
+  defp fixture(:<%= schema.singular %>) do
+    {:ok, <%= schema.singular %>} = <%= inspect context.alias %>.create_<%= schema.singular %>(@create_attrs)
+    <%= schema.singular %>
+  end
+
+  defp create_<%= schema.singular %>(_) do
+    <%= schema.singular %> = fixture(:<%= schema.singular %>)
+    %{<%= schema.singular %>: <%= schema.singular %>}
+  end
+
+  describe "index" do
+    setup [:create_<%= schema.singular %>]
+
+    test "lists all <%= schema.plural %>", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      {:ok, index_live, disconnected_html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index))
+      connected_html = render(index_live)
+
+      assert disconnected_html =~ "Listing <%= schema.human_plural %>"
+      assert connected_html =~ "Listing <%= schema.human_plural %>"
+
+      <%= if schema.string_attr do %>assert disconnected_html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %>
+      <%= if schema.string_attr do %>assert connected_html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %>
+    end
+
+    test "renders new <%= schema.singular %> form", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      {:ok, index_live, disconnected_html} = live(conn, Routes.<%= schema.singular %>_index_path(conn, :new))
+      connected_html = render(index_live)
+
+      <%= if schema.string_attr do %>assert disconnected_html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %>
+      <%= if schema.string_attr do %>assert connected_html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %>
+      assert disconnected_html =~ "New <%= schema.human_singular %>"
+      assert connected_html =~ "New <%= schema.human_singular %>"
+    end
+  end
+
+  describe "show" do
+    setup [:create_<%= schema.singular %>]
+
+    test "shows <%= schema.singular %>", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      {:ok, show_live, disconnected_html} = live(conn, Routes.<%= schema.singular %>_show_path(conn, :show, <%= schema.singular %>))
+      connected_html = render(show_live)
+
+      assert disconnected_html =~ "Show <%= schema.human_singular %>"
+      assert connected_html =~ "Show <%= schema.human_singular %>"
+    end
+
+    test "renders edit <%= schema.singular %> form", %{conn: conn, <%= schema.singular %>: <%= schema.singular %>} do
+      {:ok, show_live, disconnected_html} = live(conn, Routes.<%= schema.singular %>_show_path(conn, :edit, <%= schema.singular %>))
+      assert disconnected_html =~ "Edit <%= schema.human_singular %>"
+      assert render(show_live) =~ "Edit <%= schema.human_singular %>"
+      <%= if schema.string_attr do %>assert disconnected_html =~ <%= schema.singular %>.<%= schema.string_attr %><% end %>
+      <%= if schema.string_attr do %>assert render(show_live) =~ <%= schema.singular %>.<%= schema.string_attr %><% end %>
+
+      assert render_submit([show_live, "#form-#{<%= schema.singular %>.id}"], "save", %{"<%= schema.singular %>" => @invalid_attrs}) =~
+               "Edit <%= schema.human_singular %>"
+
+      <%= if schema.string_attr do %>assert {:error, {:redirect, path}} =
+               render_submit([show_live, "#form-#{<%= schema.singular %>.id}"], "save", %{"<%= schema.singular %>" => @update_attrs})
+
+      {:ok, _show_live, disconnected_html} = live(conn, path)
+      assert disconnected_html =~ "some updated <%= schema.string_attr %>"<% else %>
+      assert {:error, {:redirect, _path}} =
+               render_submit([show_live, "#form-#{<%= schema.singular %>.id}"], "save", %{"<%= schema.singular %>" => @update_attrs})<%end %>
+    end
+  end
+end
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
new file mode 100644
index 000000000..0be6ee213
--- /dev/null
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -0,0 +1,363 @@
+Code.require_file "../../../installer/test/mix_helper.exs", __DIR__
+
+defmodule Mix.Tasks.Phx.Gen.LiveTest do
+  use ExUnit.Case
+  import MixHelper
+  alias Mix.Tasks.Phx.Gen
+
+  setup do
+    Mix.Task.clear()
+    :ok
+  end
+
+  defp in_tmp_live_project(test, func) do
+    in_tmp_project(test, fn ->
+      File.mkdir_p!("lib")
+      File.touch!("lib/phoenix_web.ex")
+      File.touch!("lib/phoenix.ex")
+      func.()
+    end)
+  end
+
+  defp in_tmp_live_umbrella_project(test, func) do
+    in_tmp_umbrella_project(test, fn ->
+      File.mkdir_p!("phoenix/lib")
+      File.mkdir_p!("phoenix_web/lib")
+      File.touch!("phoenix/lib/phoenix.ex")
+      File.touch!("phoenix_web/lib/phoenix_web.ex")
+      func.()
+    end)
+  end
+
+  test "invalid mix arguments", config do
+    in_tmp_live_project config.test, fn ->
+      assert_raise Mix.Error, ~r/Expected the context, "blog", to be a valid module name/, fn ->
+        Gen.Live.run(~w(blog Post posts title:string))
+      end
+
+      assert_raise Mix.Error, ~r/Expected the schema, "posts", to be a valid module name/, fn ->
+        Gen.Live.run(~w(Post posts title:string))
+      end
+
+      assert_raise Mix.Error, ~r/The context and schema should have different names/, fn ->
+        Gen.Live.run(~w(Blog Blog blogs))
+      end
+
+      assert_raise Mix.Error, ~r/Invalid arguments/, fn ->
+        Gen.Live.run(~w(Blog.Post posts))
+      end
+
+      assert_raise Mix.Error, ~r/Invalid arguments/, fn ->
+        Gen.Live.run(~w(Blog Post))
+      end
+    end
+  end
+
+  test "generates live resource and handles existing contexts", config do
+    in_tmp_live_project config.test, fn ->
+      Gen.Live.run(~w(Blog Post posts title slug:unique votes:integer cost:decimal
+                      tags:array:text popular:boolean drafted_at:datetime
+                      published_at:utc_datetime
+                      published_at_usec:utc_datetime_usec
+                      deleted_at:naive_datetime
+                      deleted_at_usec:naive_datetime_usec
+                      alarm:time
+                      alarm_usec:time_usec
+                      secret:uuid announcement_date:date alarm:time
+                      weight:float user_id:references:users))
+
+      assert_file "lib/phoenix/blog/post.ex"
+      assert_file "lib/phoenix/blog.ex"
+      assert_file "test/phoenix/blog_test.exs"
+
+      assert_file "lib/phoenix_web/live/post_live/index.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.PostLive.Index"
+      end
+
+      assert_file "lib/phoenix_web/live/post_live/show.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.PostLive.Show"
+      end
+
+      assert_file "lib/phoenix_web/live/post_live/form.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.PostLive.Form"
+      end
+
+      assert_file "lib/phoenix_web/live/modal.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.Modal"
+      end
+
+      assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
+      assert_file path, fn file ->
+        assert file =~ "create table(:posts)"
+        assert file =~ "add :title, :string"
+        assert file =~ "create unique_index(:posts, [:slug])"
+      end
+
+      assert_file "lib/phoenix_web/live/post_live/index.html.leex", fn file ->
+        assert file =~ " Routes.post_index_path(@socket, :index)"
+      end
+
+      assert_file "lib/phoenix_web/live/post_live/show.html.leex", fn file ->
+        assert file =~ " Routes.post_index_path(@socket, :index)"
+      end
+
+      assert_file "lib/phoenix_web/live/post_live/form.html.leex", fn file ->
+        assert file =~ ~s(<%= text_input f, :title %>)
+        assert file =~ ~s(<%= number_input f, :votes %>)
+        assert file =~ ~s(<%= number_input f, :cost, step: "any" %>)
+        assert file =~ ~s(<%= multiple_select f, :tags, ["Option 1": "option1", "Option 2": "option2"] %>)
+        assert file =~ ~s(<%= checkbox f, :popular %>)
+        assert file =~ ~s(<%= datetime_select f, :drafted_at %>)
+        assert file =~ ~s(<%= datetime_select f, :published_at %>)
+        assert file =~ ~s(<%= datetime_select f, :deleted_at %>)
+        assert file =~ ~s(<%= date_select f, :announcement_date %>)
+        assert file =~ ~s(<%= time_select f, :alarm %>)
+        assert file =~ ~s(<%= text_input f, :secret %>)
+
+        assert file =~ ~s(<%= label f, :title %>)
+        assert file =~ ~s(<%= label f, :votes %>)
+        assert file =~ ~s(<%= label f, :cost %>)
+        assert file =~ ~s(<%= label f, :tags %>)
+        assert file =~ ~s(<%= label f, :popular %>)
+        assert file =~ ~s(<%= label f, :drafted_at %>)
+        assert file =~ ~s(<%= label f, :published_at %>)
+        assert file =~ ~s(<%= label f, :deleted_at %>)
+        assert file =~ ~s(<%= label f, :announcement_date %>)
+        assert file =~ ~s(<%= label f, :alarm %>)
+        assert file =~ ~s(<%= label f, :secret %>)
+
+        refute file =~ ~s(<%= label f, :user_id)
+        refute file =~ ~s(<%= number_input f, :user_id)
+      end
+
+      send self(), {:mix_shell_input, :yes?, true}
+      Gen.Live.run(~w(Blog Comment comments title:string))
+      assert_received {:mix_shell, :info, ["You are generating into an existing context" <> _]}
+
+      assert_file "lib/phoenix/blog/comment.ex"
+      assert_file "test/phoenix_web/live/comment_live_test.exs", fn file ->
+        assert file =~ "defmodule PhoenixWeb.CommentLiveTest"
+      end
+
+      assert [path] = Path.wildcard("priv/repo/migrations/*_create_comments.exs")
+      assert_file path, fn file ->
+        assert file =~ "create table(:comments)"
+        assert file =~ "add :title, :string"
+      end
+
+      assert_file "lib/phoenix_web/live/comment_live/index.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.CommentLive.Index"
+      end
+
+      assert_file "lib/phoenix_web/live/comment_live/show.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.CommentLive.Show"
+      end
+
+      assert_file "lib/phoenix_web/live/comment_live/form.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.CommentLive.Form"
+      end
+
+      assert_receive {:mix_shell, :info, ["""
+
+      Add the live routes to your browser scope in lib/phoenix_web/router.ex:
+
+          live "/comments", CommentLive.Index, :index
+          live "/comments/new", CommentLive.Index, :new
+          live "/comments/:id/edit", CommentLive.Index, :edit
+
+          live "/comments/:id", CommentLive.Show, :show
+          live "/comments/:id/show/edit", CommentLive.Show, :edit
+      """]}
+    end
+  end
+
+  test "with --web namespace generates namedspaced web modules and directories", config do
+    in_tmp_live_project config.test, fn ->
+      Gen.Live.run(~w(Blog Post posts title:string --web Blog))
+
+      assert_file "lib/phoenix/blog/post.ex"
+      assert_file "lib/phoenix/blog.ex"
+      assert_file "test/phoenix/blog_test.exs"
+
+      assert_file "lib/phoenix_web/live/blog/post_live/index.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.Blog.PostLive.Index"
+      end
+
+      assert_file "lib/phoenix_web/live/blog/post_live/show.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.Blog.PostLive.Show"
+      end
+
+      assert_file "lib/phoenix_web/live/blog/post_live/form.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.Blog.PostLive.Form"
+      end
+
+      assert_file "lib/phoenix_web/live/modal.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.Modal"
+      end
+
+      assert [path] = Path.wildcard("priv/repo/migrations/*_create_posts.exs")
+      assert_file path, fn file ->
+        assert file =~ "create table(:posts)"
+        assert file =~ "add :title, :string"
+      end
+
+      assert_file "lib/phoenix_web/live/blog/post_live/index.html.leex", fn file ->
+        assert file =~ " Routes.post_index_path(@socket, :index)"
+      end
+
+      assert_file "lib/phoenix_web/live/blog/post_live/show.html.leex", fn file ->
+        assert file =~ " Routes.post_index_path(@socket, :index)"
+      end
+
+      assert_file "lib/phoenix_web/live/blog/post_live/form.html.leex"
+
+      assert_receive {:mix_shell, :info, ["""
+
+      Add the live routes to your Blog :browser scope in lib/phoenix_web/router.ex:
+
+          scope "/blog", PhoenixWeb.Blog, as: :blog do
+            pipe_through :browser
+            ...
+
+            live "/posts", PostLive.Index, :index
+            live "/posts/new", PostLive.Index, :new
+            live "/posts/:id/edit", PostLive.Index, :edit
+
+            live "/posts/:id", PostLive.Show, :show
+            live "/posts/:id/show/edit", PostLive.Show, :edit
+          end
+      """]}
+    end
+  end
+
+  test "with --no-context skips context and schema file generation", config do
+    in_tmp_live_project config.test, fn ->
+      Gen.Live.run(~w(Blog Post posts title:string --no-context))
+
+      refute_file "lib/phoenix/blog.ex"
+      refute_file "lib/phoenix/blog/post.ex"
+      assert Path.wildcard("priv/repo/migrations/*.exs") == []
+
+      assert_file "lib/phoenix_web/live/post_live/index.ex"
+      assert_file "lib/phoenix_web/live/post_live/show.ex"
+      assert_file "lib/phoenix_web/live/post_live/form.ex"
+
+      assert_file "lib/phoenix_web/live/modal.ex", fn file ->
+        assert file =~ "defmodule PhoenixWeb.Modal"
+      end
+
+      assert_file "lib/phoenix_web/live/post_live/index.html.leex"
+      assert_file "lib/phoenix_web/live/post_live/show.html.leex"
+      assert_file "lib/phoenix_web/live/post_live/form.html.leex"
+      assert_file "test/phoenix_web/live/post_live_test.exs"
+    end
+  end
+
+  test "with --no-schema skips schema file generation", config do
+    in_tmp_live_project config.test, fn ->
+      Gen.Live.run(~w(Blog Post posts title:string --no-schema))
+
+      assert_file "lib/phoenix/blog.ex"
+      refute_file "lib/phoenix/blog/post.ex"
+      assert Path.wildcard("priv/repo/migrations/*.exs") == []
+
+      assert_file "lib/phoenix_web/live/post_live/index.ex"
+      assert_file "lib/phoenix_web/live/post_live/show.ex"
+      assert_file "lib/phoenix_web/live/post_live/form.ex"
+      assert_file "lib/phoenix_web/live/modal.ex"
+
+      assert_file "lib/phoenix_web/live/post_live/index.html.leex"
+      assert_file "lib/phoenix_web/live/post_live/show.html.leex"
+      assert_file "lib/phoenix_web/live/post_live/form.html.leex"
+      assert_file "test/phoenix_web/live/post_live_test.exs"
+    end
+  end
+
+  describe "inside umbrella" do
+    test "without context_app generators config uses web dir", config do
+      in_tmp_live_umbrella_project config.test, fn ->
+        File.cd!("phoenix_web")
+
+        Application.put_env(:phoenix, :generators, context_app: nil)
+        Gen.Live.run(~w(Accounts User users name:string))
+
+        assert_file "lib/phoenix/accounts.ex"
+        assert_file "lib/phoenix/accounts/user.ex"
+
+        assert_file "lib/phoenix_web/live/user_live/index.ex", fn file ->
+          assert file =~ "defmodule PhoenixWeb.UserLive.Index"
+          assert file =~ "use PhoenixWeb, :live_view"
+        end
+
+        assert_file "lib/phoenix_web/live/user_live/show.ex", fn file ->
+          assert file =~ "defmodule PhoenixWeb.UserLive.Show"
+          assert file =~ "use PhoenixWeb, :live_view"
+        end
+
+        assert_file "lib/phoenix_web/live/user_live/form.ex", fn file ->
+          assert file =~ "defmodule PhoenixWeb.UserLive.Form"
+          assert file =~ "use PhoenixWeb, :live_component"
+        end
+
+        assert_file "lib/phoenix_web/live/user_live/form.html.leex"
+
+        assert_file "lib/phoenix_web/live/modal.ex", fn file ->
+          assert file =~ "defmodule PhoenixWeb.Modal"
+        end
+
+        assert_file "test/phoenix_web/live/user_live_test.exs", fn file ->
+          assert file =~ "defmodule PhoenixWeb.UserLiveTest"
+        end
+      end
+    end
+
+    test "raises with false context_app", config do
+      in_tmp_live_umbrella_project config.test, fn ->
+        Application.put_env(:phoenix, :generators, context_app: false)
+        assert_raise Mix.Error, ~r/no context_app configured/, fn ->
+          Gen.Live.run(~w(Accounts User users name:string))
+        end
+      end
+    end
+
+    test "with context_app generators config does not use web dir", config do
+      in_tmp_live_umbrella_project config.test, fn ->
+        File.mkdir!("another_app")
+        Application.put_env(:phoenix, :generators, context_app: {:another_app, "another_app"})
+
+        File.cd!("phoenix")
+
+        Gen.Live.run(~w(Accounts User users name:string))
+
+        assert_file "another_app/lib/another_app/accounts.ex"
+        assert_file "another_app/lib/another_app/accounts/user.ex"
+
+
+        assert_file "lib/phoenix/live/user_live/index.ex", fn file ->
+          assert file =~ "defmodule Phoenix.UserLive.Index"
+          assert file =~ "use Phoenix, :live_view"
+        end
+
+        assert_file "lib/phoenix/live/user_live/show.ex", fn file ->
+          assert file =~ "defmodule Phoenix.UserLive.Show"
+          assert file =~ "use Phoenix, :live_view"
+        end
+
+        assert_file "lib/phoenix/live/user_live/form.ex", fn file ->
+          assert file =~ "defmodule Phoenix.UserLive.Form"
+          assert file =~ "use Phoenix, :live_component"
+        end
+
+        assert_file "lib/phoenix/live/modal.ex", fn file ->
+          assert file =~ "defmodule Phoenix.Modal"
+        end
+
+        assert_file "lib/phoenix/live/user_live/form.html.leex"
+
+        assert_file "test/phoenix/live/user_live_test.exs", fn file ->
+          assert file =~ "defmodule Phoenix.UserLiveTest"
+        end
+      end
+    end
+  end
+end
diff --git a/test/phoenix/integration/websocket_channels_test.exs b/test/phoenix/integration/websocket_channels_test.exs
index b892a61cc..add5f9aa2 100644
--- a/test/phoenix/integration/websocket_channels_test.exs
+++ b/test/phoenix/integration/websocket_channels_test.exs
@@ -347,7 +347,6 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do
 
         assert log =~ "CONNECTED TO Phoenix.Integration.WebSocketChannelsTest.UserSocket in "
         assert log =~ "  Transport: :websocket"
-        assert log =~ "  Connect Info: %{}"
         assert log =~ "  Serializer: #{inspect @serializer}"
         assert log =~ "  Parameters: %{\"logging\" => \"enabled\", \"vsn\" => #{inspect(@vsn)}}"
       end
diff --git a/test/phoenix/router/helpers_test.exs b/test/phoenix/router/helpers_test.exs
index f7549d9ed..0210d379d 100644
--- a/test/phoenix/router/helpers_test.exs
+++ b/test/phoenix/router/helpers_test.exs
@@ -51,6 +51,10 @@ defmodule Phoenix.Router.HelpersTest do
 
     scope "/admin/new", alias: Admin, as: "admin" do
       resources "/messages", MessageController
+
+      scope "/unscoped", as: false do
+        resources "/messages", MessageController, as: :my_admin_message
+      end
     end
 
     scope "/trails", trailing_slash: true do
@@ -375,6 +379,13 @@ defmodule Phoenix.Router.HelpersTest do
     assert Helpers.admin_message_path(__MODULE__, :show, 1) == "/admin/new/messages/1"
   end
 
+  test "scoped route helpers generated unscoped :as options" do
+    assert Helpers.my_admin_message_path(__MODULE__, :index, []) == "/admin/new/unscoped/messages"
+    assert Helpers.my_admin_message_path(__MODULE__, :index) == "/admin/new/unscoped/messages"
+    assert Helpers.my_admin_message_path(__MODULE__, :show, 1, []) == "/admin/new/unscoped/messages/1"
+    assert Helpers.my_admin_message_path(__MODULE__, :show, 1) == "/admin/new/unscoped/messages/1"
+  end
+
   test "scoped route helpers generated with trailing slashes" do
     assert Helpers.trail_path(__MODULE__, :index) == "/trails/"
     assert Helpers.trail_path(__MODULE__, :index, id: 5) == "/trails/?id=5"
diff --git a/test/phoenix/router/scope_test.exs b/test/phoenix/router/scope_test.exs
index 5e8653719..803960e81 100644
--- a/test/phoenix/router/scope_test.exs
+++ b/test/phoenix/router/scope_test.exs
@@ -51,9 +51,15 @@ defmodule Phoenix.Router.ScopedRoutingTest do
 
       scope "/v1", alias: V1 do
         resources "/users", UserController, only: [:delete], private: %{private_token: "baz"}
+
         get "/noalias", Api.V1.UserController, :proxy,
           private: %{proxy_to: {scoped_alias(__MODULE__, UserController), :show}},
           alias: false
+
+        scope "/scoped", alias: false do
+          get "/noalias", Api.V1.UserController, :proxy,
+          private: %{proxy_to: {scoped_alias(__MODULE__, Api.V1.UserController), :show}}
+        end
       end
     end
 
@@ -205,12 +211,18 @@ defmodule Phoenix.Router.ScopedRoutingTest do
     end
   end
 
-  test "alias false with expanded scoped alias" do
+  test "alias false with expanded scoped alias via option" do
     conn = call(Router, :get, "/api/v1/noalias")
     assert conn.status == 200
     assert conn.resp_body == "api v1 users show"
   end
 
+  test "alias false with expanded scoped alias via scope" do
+    conn = call(Router, :get, "/api/v1/scoped/noalias")
+    assert conn.status == 200
+    assert conn.resp_body == "api v1 users show"
+  end
+
   test "raises for reserved prefixes" do
     assert_raise ArgumentError, ~r/`static` is a reserved route prefix/, fn ->
       defmodule ErrorRouter do