Repository: phoenixframework/phoenix
Pull request #3710 (2020-03-18 14:43:14)

Include install dependencies step in Release documentation
<3 <3 Ya'll are awesome thank you for everything <3 <3

When walking through the process of building a release for the first time, we've
seen a few people copy these steps out and expect them to run.

On a new server they fail, as `npm install` is not listed.

This causes a "webpack not found" error, which can be confusing (especially to
those without experience with JavaScript and its build process).


Patch:
diff --git a/guides/deployment/releases.md b/guides/deployment/releases.md
index bda63b36d..f76fc8bfd 100644
--- a/guides/deployment/releases.md
+++ b/guides/deployment/releases.md
@@ -37,6 +37,9 @@ Then load dependencies to compile code and assets:
 $ mix deps.get --only prod
 $ MIX_ENV=prod mix compile
 
+# Install / update  JavaScript dependencies
+$ npm install --prefix ./assets
+
 # Compile assets
 $ npm run deploy --prefix ./assets
 $ mix phx.digest
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_single/mix.exs b/installer/templates/phx_single/mix.exs
index 59acdaecb..dc9709f59 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -41,7 +41,7 @@ defmodule <%= app_module %>.MixProject do
       {:phoenix_ecto, "~> 4.1"},
       {:ecto_sql, "~> 3.1"},
       {<%= inspect adapter_app %>, ">= 0.0.0"},<% end %><%= if html do %><%= if live do %>
-      {:phoenix_live_view, "~> 0.9.0"},
+      {:phoenix_live_view, "~> 0.10.0"},
       {:floki, ">= 0.0.0", only: :test},<% end %>
       {:phoenix_html, "~> 2.11"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %>
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
index 6d95272bf..8b18add7a 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
@@ -39,7 +39,7 @@ defmodule <%= web_namespace %>.MixProject do
     [
       <%= phoenix_dep %>,<%= if ecto do %>
       {:phoenix_ecto, "~> 4.0"},<% end %><%= if html do %><%= if live do %>
-      {:phoenix_live_view, "~> 0.9.0"},
+      {:phoenix_live_view, "~> 0.10.0"},
       {:floki, ">= 0.0.0", only: :test},<% end %>
       {:phoenix_html, "~> 2.11"},
       {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %>
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 83e05dcb2..8b41e1a04 100644
--- a/lib/mix/tasks/phx.gen.html.ex
+++ b/lib/mix/tasks/phx.gen.html.ex
@@ -160,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.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/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 6d3ff25cc..b1934b01a 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -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 db8679d03..69b3db8d4 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -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