Repository: phoenixframework/phoenix
Pull request #5954 (2024-10-17 08:44:32)

[docs] add code example on how to render functional component in controller
I didn't see many (any?) code examples in docs of how to render a functional component directly in controller. This PR adds that.


Patch:
diff --git a/guides/components.md b/guides/components.md
index f0a3c1805..f78aa38a3 100644
--- a/guides/components.md
+++ b/guides/components.md
@@ -62,11 +62,7 @@ Next we need to update `show.html.heex`:
 </section>
 ```
 
-When we reload `http://localhost:4000/hello/Frank`, we should see the same content as before.
-
-Since templates are embedded inside the `HelloHTML` module, we were able to invoke the view function simply as `<.greet messenger="..." />`.
-
-If the component was defined elsewhere, we can also type `<HelloWeb.HelloHTML.greet messenger="..." />`.
+When we reload `http://localhost:4000/hello/Frank`, we should see the same content as before. Since the `show.html.heex` template is embedded within the `HelloHTML` module, we were able to invoke the function component directly as `<.greet messenger="..." />`. If the component was defined elsewhere, we would need to give its full name: `<HelloWeb.HelloHTML.greet messenger="..." />`.
 
 By declaring attributes as required, Phoenix will warn at compile time if we call the `<.greet />` component without passing attributes. If an attribute is optional, you can specify the `:default` option with a value:
 
@@ -74,19 +70,22 @@ By declaring attributes as required, Phoenix will warn at compile time if we cal
 attr :messenger, :string, default: nil
 ```
 
-Although this is a quick example, it shows the different roles function components play in Phoenix:
-
-* Function components can be defined as functions that receive `assigns` as argument and call the `~H` sigil, as we did in `greet/1`
-
-* Function components can be embedded from template files, that's how we load `show.html.heex` into `HelloWeb.HelloHTML`
+Overall, function components are the essential building block of Phoenix rendering stack. The majority of the times, they are functions that receive a single argument called `assigns` and call the `~H` sigil, as we did in `greet/1`. They can also be invoked from templates, with compile-time validation of its attributes declared via `attr`.
 
-* Function components can declare which attributes are expected, which are validated at compilation time
+In fact, every template embedded into `HelloHTML` is a function component in itself. `show.html.heex` simply becomes a function component named `show`. This also means you can directly render function components directly from the controller, skipping the `show.html.heex` template:
 
-* Function components can be directly rendered from controllers
+```elixir
+def HelloWeb.HelloController do
+  use HelloWeb, :controller
 
-* Function components can be directly rendered from other function components, as we called `<.greet messenger={@messenger} />` from `show.html.heex`
+  def show(conn, %{"messenger" => messenger}) do
+    # Render the HelloWeb.HelloHTML.greet/1 component
+    render(conn, :greet, messenger: messenger)
+  end
+end
+```
 
-And there's more. Before we go deeper, let's fully understand the expressive power behind the HEEx template language.
+Next, let's fully understand the expressive power behind the HEEx template language.
 
 ## HEEx
 
diff --git a/guides/controllers.md b/guides/controllers.md
index 6ca334642..3bb93589e 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -47,7 +47,7 @@ defmodule HelloWeb.PageController do
   ...
 
   def index(conn, _params) do
-    render(conn, :index)
+    render(conn, :home)
   end
 end
 ```
diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs
index 60fe5b267..828252311 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -13,7 +13,8 @@ defmodule <%= @app_module %>.MixProject do
       elixirc_paths: elixirc_paths(Mix.env()),
       start_permanent: Mix.env() == :prod,
       aliases: aliases(),
-      deps: deps()
+      deps: deps(),
+      listeners: [Phoenix.CodeReloader]
     ]
   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 12f347b4d..620aa84ec 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
@@ -13,7 +13,8 @@ defmodule <%= @web_namespace %>.MixProject do
       elixirc_paths: elixirc_paths(Mix.env()),
       start_permanent: Mix.env() == :prod,
       aliases: aliases(),
-      deps: deps()
+      deps: deps(),
+      listeners: [Phoenix.CodeReloader]
     ]
   end
 
diff --git a/lib/mix/tasks/phx.gen.auth/hashing_library.ex b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
index cd1d2b7c7..2d37afc26 100644
--- a/lib/mix/tasks/phx.gen.auth/hashing_library.ex
+++ b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
@@ -40,7 +40,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.HashingLibrary do
     lib = %__MODULE__{
       name: :argon2,
       module: Argon2,
-      mix_dependency: ~s|{:argon2_elixir, "~> 3.0"}|,
+      mix_dependency: ~s|{:argon2_elixir, "~> 4.0"}|,
       test_config: """
       config :argon2_elixir, t_cost: 1, m_cost: 8
       """
diff --git a/lib/phoenix/code_reloader.ex b/lib/phoenix/code_reloader.ex
index 809d55525..433cc7547 100644
--- a/lib/phoenix/code_reloader.ex
+++ b/lib/phoenix/code_reloader.ex
@@ -31,6 +31,19 @@ defmodule Phoenix.CodeReloader do
 
   This function is a no-op and returns `:ok` if Mix is not available.
 
+  The reloader should also be configured as a Mix listener in project's
+  mix.exs file (since Elixir v1.18):
+
+      def project do
+        [
+          ...,
+          listeners: [Phoenix.CodeReloader]
+        ]
+      end
+
+  This way the reloader can notice whenever the project is compiled
+  concurrently.
+
   ## Options
 
     * `:reloadable_args` - additional CLI args to pass to the compiler tasks.
@@ -57,6 +70,10 @@ defmodule Phoenix.CodeReloader do
   @spec sync :: :ok
   defdelegate sync, to: Phoenix.CodeReloader.Server
 
+  @doc false
+  @spec child_spec(keyword) :: Supervisor.child_spec()
+  defdelegate child_spec(opts), to: Phoenix.CodeReloader.MixListener
+
   ## Plug
 
   @behaviour Plug
diff --git a/lib/phoenix/code_reloader/mix_listener.ex b/lib/phoenix/code_reloader/mix_listener.ex
new file mode 100644
index 000000000..f136314ee
--- /dev/null
+++ b/lib/phoenix/code_reloader/mix_listener.ex
@@ -0,0 +1,72 @@
+defmodule Phoenix.CodeReloader.MixListener do
+  @moduledoc false
+
+  use GenServer
+
+  @name __MODULE__
+
+  @spec start_link(keyword) :: GenServer.on_start()
+  def start_link(_opts) do
+    GenServer.start_link(__MODULE__, {}, name: @name)
+  end
+
+  @spec started? :: boolean()
+  def started? do
+    Process.whereis(Phoenix.CodeReloader.MixListener) != nil
+  end
+
+  @doc """
+  Unloads all modules invalidated by external compilations.
+
+  Only reloads modules from the given apps.
+  """
+  @spec purge([atom()]) :: :ok
+  def purge(apps) do
+    GenServer.call(@name, {:purge, apps}, :infinity)
+  end
+
+  @impl true
+  def init({}) do
+    {:ok, %{to_purge: %{}}}
+  end
+
+  @impl true
+  def handle_call({:purge, apps}, _from, state) do
+    for app <- apps, modules = state.to_purge[app] do
+      purge_modules(modules)
+    end
+
+    {:reply, :ok, %{state | to_purge: %{}}}
+  end
+
+  @impl true
+  def handle_info({:modules_compiled, info}, state) do
+    if info.os_pid == System.pid() do
+      # Ignore compilations from ourselves, because the modules are
+      # already updated in memory
+      {:noreply, state}
+    else
+      %{changed: changed, removed: removed} = info.modules_diff
+
+      state =
+        update_in(state.to_purge[info.app], fn to_purge ->
+          to_purge = to_purge || MapSet.new()
+          to_purge = Enum.into(changed, to_purge)
+          Enum.into(removed, to_purge)
+        end)
+
+      {:noreply, state}
+    end
+  end
+
+  def handle_info(_message, state) do
+    {:noreply, state}
+  end
+
+  defp purge_modules(modules) do
+    for module <- modules do
+      :code.purge(module)
+      :code.delete(module)
+    end
+  end
+end
diff --git a/lib/phoenix/code_reloader/server.ex b/lib/phoenix/code_reloader/server.ex
index 8f56f423c..16ff92bf1 100644
--- a/lib/phoenix/code_reloader/server.ex
+++ b/lib/phoenix/code_reloader/server.ex
@@ -63,23 +63,39 @@ defmodule Phoenix.CodeReloader.Server do
     apps = endpoint.config(:reloadable_apps) || default_reloadable_apps()
     args = Keyword.get(opts, :reloadable_args, ["--no-all-warnings"])
 
-    # We do a backup of the endpoint in case compilation fails.
-    # If so we can bring it back to finish the request handling.
-    backup = load_backup(endpoint)
     froms = all_waiting([from], endpoint)
 
-    {res, out} =
-      proxy_io(fn ->
-        try do
-          mix_compile(Code.ensure_loaded(Mix.Task), compilers, apps, args, state.timestamp)
-        catch
-          :exit, {:shutdown, 1} ->
-            :error
-
-          kind, reason ->
-            IO.puts(Exception.format(kind, reason, __STACKTRACE__))
-            :error
-        end
+    {backup, res, out} =
+      with_build_lock(fn ->
+        purge_fallback? =
+          if Phoenix.CodeReloader.MixListener.started?() do
+            Phoenix.CodeReloader.MixListener.purge(apps)
+            false
+          else
+            warn_missing_mix_listener()
+            true
+          end
+
+        # We do a backup of the endpoint in case compilation fails.
+        # If so we can bring it back to finish the request handling.
+        backup = load_backup(endpoint)
+
+        {res, out} =
+          proxy_io(fn ->
+            try do
+              task_loaded = Code.ensure_loaded(Mix.Task)
+              mix_compile(task_loaded, compilers, apps, args, state.timestamp, purge_fallback?)
+            catch
+              :exit, {:shutdown, 1} ->
+                :error
+
+              kind, reason ->
+                IO.puts(Exception.format(kind, reason, __STACKTRACE__))
+                :error
+            end
+          end)
+
+        {backup, res, out}
       end)
 
     reply =
@@ -175,7 +191,34 @@ defmodule Phoenix.CodeReloader.Server do
     defp purge_protocols(_path), do: :ok
   end
 
-  defp mix_compile({:module, Mix.Task}, compilers, apps_to_reload, compile_args, timestamp) do
+  defp warn_missing_mix_listener do
+    listeners_supported? = Version.match?(System.version(), ">= 1.18.0-dev")
+
+    if listeners_supported? do
+      IO.warn("""
+      a Mix listener expected by Phoenix.CodeReloader is missing.
+
+      Please add the listener to your mix.exs configuration, like so:
+
+          def project do
+            [
+              ...,
+              listeners: [Phoenix.CodeReloader]
+            ]
+          end
+
+      """)
+    end
+  end
+
+  defp mix_compile(
+         {:module, Mix.Task},
+         compilers,
+         apps_to_reload,
+         compile_args,
+         timestamp,
+         purge_fallback?
+       ) do
     config = Mix.Project.config()
     path = Mix.Project.consolidation_path(config)
 
@@ -184,8 +227,25 @@ defmodule Phoenix.CodeReloader.Server do
       purge_protocols(path)
     end
 
-    mix_compile_deps(Mix.Dep.cached(), apps_to_reload, compile_args, compilers, timestamp, path)
-    mix_compile_project(config[:app], apps_to_reload, compile_args, compilers, timestamp, path)
+    mix_compile_deps(
+      Mix.Dep.cached(),
+      apps_to_reload,
+      compile_args,
+      compilers,
+      timestamp,
+      path,
+      purge_fallback?
+    )
+
+    mix_compile_project(
+      config[:app],
+      apps_to_reload,
+      compile_args,
+      compilers,
+      timestamp,
+      path,
+      purge_fallback?
+    )
 
     if config[:consolidate_protocols] do
       # If we are consolidating protocols, we need to purge all of its modules
@@ -198,30 +258,46 @@ defmodule Phoenix.CodeReloader.Server do
     :ok
   end
 
-  defp mix_compile({:error, _reason}, _, _, _, _) do
+  defp mix_compile({:error, _reason}, _, _, _, _, _) do
     raise "the Code Reloader is enabled but Mix is not available. If you want to " <>
             "use the Code Reloader in production or inside an escript, you must add " <>
             ":mix to your applications list. Otherwise, you must disable code reloading " <>
             "in such environments"
   end
 
-  defp mix_compile_deps(deps, apps_to_reload, compile_args, compilers, timestamp, path) do
+  defp mix_compile_deps(
+         deps,
+         apps_to_reload,
+         compile_args,
+         compilers,
+         timestamp,
+         path,
+         purge_fallback?
+       ) do
     for dep <- deps, dep.app in apps_to_reload do
       Mix.Dep.in_dependency(dep, fn _ ->
-        mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
+        mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?)
       end)
     end
   end
 
-  defp mix_compile_project(nil, _, _, _, _, _), do: :ok
-
-  defp mix_compile_project(app, apps_to_reload, compile_args, compilers, timestamp, path) do
+  defp mix_compile_project(nil, _, _, _, _, _, _), do: :ok
+
+  defp mix_compile_project(
+         app,
+         apps_to_reload,
+         compile_args,
+         compilers,
+         timestamp,
+         path,
+         purge_fallback?
+       ) do
     if app in apps_to_reload do
-      mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
+      mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?)
     end
   end
 
-  defp mix_compile_unless_stale_config(compilers, compile_args, timestamp, path) do
+  defp mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?) do
     manifests = Mix.Tasks.Compile.Elixir.manifests()
     configs = Mix.Project.config_files()
     config = Mix.Project.config()
@@ -230,7 +306,8 @@ defmodule Phoenix.CodeReloader.Server do
       [] ->
         # If the manifests are more recent than the timestamp,
         # someone updated this app behind the scenes, so purge all beams.
-        if Mix.Utils.stale?(manifests, [timestamp]) do
+        # TODO: remove once we depend on Elixir 1.18
+        if purge_fallback? and Mix.Utils.stale?(manifests, [timestamp]) do
           purge_modules(Path.join(Mix.Project.app_path(config), "ebin"))
         end
 
@@ -351,4 +428,11 @@ defmodule Phoenix.CodeReloader.Server do
       Logger.configure(compile_time_application: logger_config_app)
     end
   end
+
+  # TODO: remove once we depend on Elixir 1.18
+  if Code.ensure_loaded?(Mix.Project) and function_exported?(Mix.Project, :with_build_lock, 1) do
+    defp with_build_lock(fun), do: Mix.Project.with_build_lock(fun)
+  else
+    defp with_build_lock(fun), do: fun.()
+  end
 end


Test patch:
diff --git a/integration_test/mix.exs b/integration_test/mix.exs
index 81012c0c8..4c45c7fcf 100644
--- a/integration_test/mix.exs
+++ b/integration_test/mix.exs
@@ -54,7 +54,7 @@ defmodule Phoenix.Integration.MixProject do
       {:swoosh, "~> 1.16"},
       {:bandit, "~> 1.0"},
       {:bcrypt_elixir, "~> 3.0"},
-      {:argon2_elixir, "~> 3.0"},
+      {:argon2_elixir, "~> 4.0"},
       {:pbkdf2_elixir, "~> 2.0"},
       {:tailwind, "~> 0.2"},
       {:heroicons,
diff --git a/integration_test/mix.lock b/integration_test/mix.lock
index 59445ecf5..59d00655d 100644
--- a/integration_test/mix.lock
+++ b/integration_test/mix.lock
@@ -1,10 +1,10 @@
 %{
-  "argon2_elixir": {:hex, :argon2_elixir, "3.2.1", "f47740bf9f2a39ffef79ba48eb25dea2ee37bcc7eadf91d49615591d1a6fce1a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "a813b78217394530b5fcf4c8070feee43df03ffef938d044019169c766315690"},
+  "argon2_elixir": {:hex, :argon2_elixir, "4.1.0", "2f242afe47c373663cb404eb75e792f749507075ed737b49685a9f2edcb401df", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ecb6f2ca2cca34b28e546224661bf2a85714516d2713c7313c5ffe8bdade7cf"},
   "bandit": {:hex, :bandit, "1.5.4", "8e56e7cfc06f3c57995be0d9bf4e45b972d8732f5c7e96ef8ec0735f52079527", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "04c2b38874769af67fe7f10034f606ad6dda1d8f80c4d7a0c616b347584d5aff"},
   "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
-  "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
+  "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
   "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
-  "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
+  "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"},
   "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
   "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
   "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
@@ -13,12 +13,12 @@
   "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.16.0", "1cdc8ea6319e7cb1bc273a36db0ecde69ad56b4dea3037689ad8c0afc6a91e16", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "73c9dd56830d67c951bc254c082cb0a7f9fa139d44866bc3186c8859d1b4d787"},
   "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
   "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
-  "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
+  "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
   "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
   "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
   "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
   "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
-  "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
+  "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
   "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
   "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"},
   "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index d0944d8f7..e252d1167 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -1050,7 +1050,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert_received {:mix_shell, :yes?, [@liveview_option_message]}
 
         assert_file("mix.exs", fn file ->
-          assert file =~ ~s|{:argon2_elixir, "~> 3.0"}|
+          assert file =~ ~s|{:argon2_elixir, "~> 4.0"}|
         end)
 
         assert_file("config/test.exs", fn file ->