Repository: phoenixframework/phoenix
Pull request #5397 (2023-04-04 01:53:44)

Ignore ./cover/ folder
Found via `mix test --cover`


Patch:
diff --git a/.gitignore b/.gitignore
index 5d4b3de37..459b4dbf2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 /doc/
 /node_modules/
 /tmp/
+/cover/
 
 /assets/node_modules/
 
diff --git a/guides/asset_management.md b/guides/asset_management.md
index 6088e3a16..9a4ddaa92 100644
--- a/guides/asset_management.md
+++ b/guides/asset_management.md
@@ -89,55 +89,54 @@ $ yarn add ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view
 Next, add a custom JavaScript build script. We'll call the example `assets/build.js`:
 
 ```js
-const esbuild = require('esbuild')
+const esbuild = require("esbuild");
 
-const args = process.argv.slice(2)
-const watch = args.includes('--watch')
-const deploy = args.includes('--deploy')
+const args = process.argv.slice(2);
+const watch = args.includes('--watch');
+const deploy = args.includes('--deploy');
 
 const loader = {
   // Add loaders for images/fonts/etc, e.g. { '.svg': 'file' }
-}
+};
 
 const plugins = [
   // Add and configure plugins here
-]
+];
 
+// Define esbuild options
 let opts = {
-  entryPoints: ['js/app.js'],
+  entryPoints: ["js/app.js"],
   bundle: true,
-  target: 'es2017',
-  outdir: '../priv/static/assets',
-  logLevel: 'info',
-  loader,
-  plugins
-}
-
-if (watch) {
-  opts = {
-    ...opts,
-    watch,
-    sourcemap: 'inline'
-  }
-}
+  logLevel: "info",
+  target: "es2017",
+  outdir: "../priv/static/assets",
+  external: ["*.css", "fonts/*", "images/*"],
+  loader: loader,
+  plugins: plugins,
+};
 
 if (deploy) {
   opts = {
     ...opts,
-    minify: true
-  }
+    minify: true,
+  };
 }
 
-const promise = esbuild.build(opts)
-
 if (watch) {
-  promise.then(_result => {
-    process.stdin.on('close', () => {
-      process.exit(0)
+  opts = {
+    ...opts,
+    sourcemap: "inline",
+  };
+  esbuild
+    .context(opts)
+    .then((ctx) => {
+      ctx.watch();
     })
-
-    process.stdin.resume()
-  })
+    .catch((_error) => {
+      process.exit(1);
+    });
+} else {
+  esbuild.build(opts);
 }
 ```
 
diff --git a/guides/json_and_apis.md b/guides/json_and_apis.md
index e831f682c..4e3776589 100644
--- a/guides/json_and_apis.md
+++ b/guides/json_and_apis.md
@@ -258,7 +258,7 @@ With this knowledge in hand, we can explore the `FallbackController` (`lib/hello
     conn
     |> put_status(:unprocessable_entity)
     |> put_view(json: HelloWeb.ChangesetJSON)
-    |> render("error.json", changeset: changeset)
+    |> render(:error, changeset: changeset)
   end
 ```
 
@@ -316,12 +316,22 @@ mix help phx.new
 The output should contain the following:
 
 ```text
-  • --no-assets - do not generate the assets folder
+  • --no-assets - equivalent to --no-esbuild and --no-tailwind
+  • --no-dashboard - do not include Phoenix.LiveDashboard
   • --no-ecto - do not generate Ecto files
-  • --no-html - do not generate HTML views
+  • --no-esbuild - do not include esbuild dependencies and
+    assets. We do not recommend setting this option, unless for API
+    only applications, as doing so requires you to manually add and
+    track JavaScript dependencies
   • --no-gettext - do not generate gettext files
-  • --no-dashboard - do not include Phoenix.LiveDashboard
+  • --no-html - do not generate HTML views
+  • --no-live - comment out LiveView socket setup in
+    assets/js/app.js. Automatically disabled if --no-html is given
   • --no-mailer - do not generate Swoosh mailer files
+  • --no-tailwind - do not include tailwind dependencies and
+    assets. The generated markup will still include Tailwind CSS
+    classes, those are left-in as reference for the subsequent
+    styling of your layout and components
 ```
 
 The `--no-html` is the obvious one we want to use when creating any Phoenix application for an API in order to leave out all the unnecessary HTML scaffolding. You may also pass `--no-assets`, if you don't want any of the asset management bit, `--no-gettext` if you don't support internationalization, and so on.
diff --git a/installer/templates/phx_single/config/dev.exs b/installer/templates/phx_single/config/dev.exs
index ae2699692..94b6e6573 100644
--- a/installer/templates/phx_single/config/dev.exs
+++ b/installer/templates/phx_single/config/dev.exs
@@ -4,8 +4,8 @@ import Config
 # debugging and code reloading.
 #
 # The watchers configuration can be used to run external
-# watchers to your application. For example, we use it
-# with esbuild to bundle .js and .css sources.
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
 config :<%= @app_name %>, <%= @endpoint_module %>,
   # Binding to loopback ipv4 address prevents access from other machines.
   # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
index a01f32588..64a5e9723 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
@@ -2,8 +2,8 @@
 # debugging and code reloading.
 #
 # The watchers configuration can be used to run external
-# watchers to your application. For example, we use it
-# with esbuild to bundle .js and .css sources.
+# watchers to your application. For example, we can use it
+# to bundle .js and .css sources.
 config :<%= @web_app_name %>, <%= @endpoint_module %>,
   # Binding to loopback ipv4 address prevents access from other machines.
   # Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex
index d5d5de772..600611937 100644
--- a/lib/mix/tasks/phx.gen.live.ex
+++ b/lib/mix/tasks/phx.gen.live.ex
@@ -36,7 +36,7 @@ defmodule Mix.Tasks.Phx.Gen.Live do
     * a helpers module in `lib/app_web/live/live_helpers.ex` with a modal
 
   After file generation is complete, there will be output regarding required
-  updates to the lib/app_web/router.ex file.
+  updates to the `lib/app_web/router.ex` file.
 
       Add the live routes to your browser scope in lib/app_web/router.ex:
 
@@ -86,6 +86,8 @@ defmodule Mix.Tasks.Phx.Gen.Live do
   to yourself. You can use the `--no-context` and `--no-schema` flags
   for file generation control.
 
+      mix phx.gen.live Accounts User users --no-context --no-schema
+
   You can also change the table name or configure the migrations to
   use binary ids for primary keys, see `mix help phx.gen.schema` for more
   information.
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index d730bbb3c..b51ba0349 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -506,7 +506,7 @@ defmodule Phoenix.Controller do
     end
   end
 
-  @invalid_local_url_chars ["\\"]
+  @invalid_local_url_chars ["\\", "\%"]
   defp validate_local_url("//" <> _ = to), do: raise_invalid_url(to)
 
   defp validate_local_url("/" <> _ = to) do
@@ -755,7 +755,7 @@ defmodule Phoenix.Controller do
 
   Raises `Plug.Conn.AlreadySentError` if `conn` is already sent.
   """
-  @spec put_root_layout(Plug.Conn.t(), [{format :: atom, layout}]) ::
+  @spec put_root_layout(Plug.Conn.t(), [{format :: atom, layout}] | false) ::
           Plug.Conn.t()
   def put_root_layout(%Plug.Conn{state: state} = conn, layout) do
     if state in @unsent do
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 247f7704f..7af9c3991 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -837,16 +837,16 @@ defmodule Phoenix.Endpoint do
     * `:user_agent` - the value of the "user-agent" request header
 
     * `{:session, session_config}` - the session information from `Plug.Conn`.
-      The `session_config` is an exact copy of the arguments given to `Plug.Session`.
-      This requires the "_csrf_token" to be given as request parameter with
-      the value of `URI.encode_www_form(Plug.CSRFProtection.get_csrf_token())`
-      when connecting to the socket. It can also be a MFA to allow loading
-      config in runtime `{MyAppWeb.Auth, :get_session_config, []}`. Otherwise
-      the session will be `nil`.
-
-      `session_config` may take a `:csrf_token_key` option
-      which is useful when using `:protect_from_forgery` with a custom
-      `:session_key`. If not given, it defaults to `"_csrf_token"`.
+      The `session_config` is typically an exact copy of the arguments given
+      to `Plug.Session`. In order to validate the session, the "_csrf_token"
+      must be given as request parameter when connecting the socket with the
+      value of `URI.encode_www_form(Plug.CSRFProtection.get_csrf_token())`.
+      The CSRF token request parameter can be modified via the `:csrf_token_key`
+      option.
+
+      Additionally, `session_config` may be a MFA, such as
+      `{MyAppWeb.Auth, :get_session_config, []}`, to allow loading config in
+      runtime.
 
   Arbitrary keywords may also appear following the above valid keys, which
   is useful for passing custom connection information to the socket.


Test patch:
diff --git a/test/phoenix/controller/controller_test.exs b/test/phoenix/controller/controller_test.exs
index 49e683339..a99ea49d5 100644
--- a/test/phoenix/controller/controller_test.exs
+++ b/test/phoenix/controller/controller_test.exs
@@ -378,6 +378,14 @@ defmodule Phoenix.Controller.ControllerTest do
       assert_raise ArgumentError, ~r/unsafe/, fn ->
         redirect(conn(:get, "/"), to: "/\\example.com")
       end
+
+      assert_raise ArgumentError, ~r/expects a path/, fn ->
+        redirect(conn(:get, "/"), to: "//\\example.com")
+      end
+
+      assert_raise ArgumentError, ~r/unsafe/, fn ->
+        redirect(conn(:get, "/"), to: "/%09/example.com")
+      end
     end
 
     test "with :external" do