Repository: phoenixframework/phoenix
Pull request #4614 (2021-11-26 21:13:19)

add `:force_watchers` option to `Phoenix.Endpoint`
This PR adds support for running watchers such as `esbuild` on an endpoint even when `:server` configuration is set to `false`. 

This is especially useful when running multiple Phoenix projects under an umbrella application in an environment where you only have access to a single port. In such an environment, you will likely resort to a proxy such as [master_proxy](https://github.com/jesseshieh/master_proxy) and set all the your Endpoint configurations to `server: false`.

See https://github.com/phoenixframework/esbuild/issues/34 for a more detailed explanation of the problem.


Patch:
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 5ec138ace..0a7fd287d 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -158,9 +158,10 @@ defmodule Phoenix.Endpoint do
     * `:watchers` - a set of watchers to run alongside your server. It
       expects a list of tuples containing the executable and its arguments.
       Watchers are guaranteed to run in the application directory, but only
-      when the server is enabled. For example, the watcher below will run
-      the "watch" mode of the webpack build tool when the server starts.
-      You can configure it to whatever build tool or command you want:
+      when the server is enabled (unless `:force_watchers` configuration is
+      set to `true`). For example, the watcher below will run the "watch" mode
+      of the webpack build tool when the server starts. You can configure it
+      to whatever build tool or command you want:
 
           [
             node: [
@@ -181,6 +182,9 @@ defmodule Phoenix.Endpoint do
 
           [another: {Mod, :fun, [arg1, arg2]}]
 
+    * `:force_watchers` - when `true`, forces your watchers to start
+      even when the `:server` option is set to `false`.
+
     * `:live_reload` - configuration for the live reload option.
       Configuration requires a `:patterns` option which should be a list of
       file patterns to watch. When these files change, it will trigger a reload.
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index dba217701..a7502f2ef 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -156,7 +156,7 @@ defmodule Phoenix.Endpoint.Supervisor do
   end
 
   defp watcher_children(_mod, conf, server?) do
-    if server? do
+    if server? || conf[:force_watchers] do
       Enum.map(conf[:watchers], &{Phoenix.Endpoint.Watcher, &1})
     else
       []
@@ -207,7 +207,8 @@ defmodule Phoenix.Endpoint.Supervisor do
      cache_manifest_skip_vsn: false,
 
      # Supervisor config
-     watchers: []]
+     watchers: [],
+     force_watchers: false]
   end
 
   defp render_errors(module) do


Test patch:
diff --git a/test/phoenix/endpoint/supervisor_test.exs b/test/phoenix/endpoint/supervisor_test.exs
index 1c1216277..e355222a7 100644
--- a/test/phoenix/endpoint/supervisor_test.exs
+++ b/test/phoenix/endpoint/supervisor_test.exs
@@ -57,6 +57,12 @@ defmodule Phoenix.Endpoint.SupervisorTest do
     def __compile_config__(), do: [force_ssl: [rewrite_on: [:x_forwarded_proto]]]
   end
 
+  defmodule WatchersEndpoint do
+    def init(:supervisor, config), do: {:ok, config}
+    def __compile_config__(), do: []
+    def __sockets__(), do: []
+  end
+
   test "generates the static url based on the static host configuration" do
     static_host = {:cache, "http://static.example.com"}
     assert Supervisor.static_url(StaticURLEndpoint) == static_host
@@ -92,4 +98,35 @@ defmodule Phoenix.Endpoint.SupervisorTest do
                  "expected these options to be unchanged from compile time: [:force_ssl]",
                  fn -> Supervisor.init({:phoenix, ForceSslEndpoint, []}) end
   end
+
+  describe "watchers" do
+    @watchers [esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}]
+
+    test "init/1 starts watcher children when `:server` config is true" do
+      Application.put_env(:phoenix, WatchersEndpoint, [server: true, watchers: @watchers])
+      {:ok, {_, children}} = Supervisor.init({:phoenix, WatchersEndpoint, []})
+      assert Enum.any?(children, fn
+        %{start: {Phoenix.Endpoint.Watcher, :start_link, _config}} -> true
+        _ -> false
+      end)
+    end
+
+    test "init/1 doesnt start watchers when `:server` config is false" do
+      Application.put_env(:phoenix, WatchersEndpoint, [server: false, watchers: @watchers])
+      {:ok, {_, children}} = Supervisor.init({:phoenix, WatchersEndpoint, []})
+      refute Enum.any?(children, fn
+        %{start: {Phoenix.Endpoint.Watcher, :start_link, _config}} -> true
+        _ -> false
+      end)
+    end
+
+    test "init/1 starts watcher children when `:server` config is false and `:force_watchers` is true" do
+      Application.put_env(:phoenix, WatchersEndpoint, [server: false, force_watchers: true, watchers: @watchers])
+      {:ok, {_, children}} = Supervisor.init({:phoenix, WatchersEndpoint, []})
+      assert Enum.any?(children, fn
+        %{start: {Phoenix.Endpoint.Watcher, :start_link, _config}} -> true
+        _ -> false
+      end)
+    end
+  end
 end