Repository: phoenixframework/phoenix
Pull request #4612 (2021-11-25 20:36:08)

Clarify how to use `npm install`


Patch:
diff --git a/guides/deployment/gigalixir.md b/guides/deployment/gigalixir.md
index f75fefcad..8c85a6595 100644
--- a/guides/deployment/gigalixir.md
+++ b/guides/deployment/gigalixir.md
@@ -98,7 +98,7 @@ Phoenix v1.6 uses `esbuild` to compile your assets but all Gigalixir images come
 ```json
 {
   "scripts": {
-    "deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild"
+    "deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild*"
   }
 }
 ```
diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md
index 63137e308..bca425d81 100644
--- a/guides/deployment/heroku.md
+++ b/guides/deployment/heroku.md
@@ -105,7 +105,7 @@ erlang_version=24.0.3
 
 # Invoke assets.deploy defined in your mix.exs to deploy assets with esbuild
 # Note we nuke the esbuild executable from the image
-hook_post_compile="eval mix assets.deploy && rm -f _build/esbuild"
+hook_post_compile="eval mix assets.deploy && rm -f _build/esbuild*"
 ```
 
 Finally, let's tell the build pack how to start our webserver. Create a file named `Procfile` at the root of your project:
@@ -131,7 +131,7 @@ When using this buildpack, you want to delegate all asset bundling to `npm`. So
 {
   ...
   "scripts": {
-    "deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild"
+    "deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild*"
   }
   ...
 }
diff --git a/guides/ecto.md b/guides/ecto.md
index 5df20fb83..56697b627 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -139,6 +139,7 @@ config :hello, Hello.Repo,
   password: "postgres",
   database: "hello_dev",
   hostname: "localhost",
+  show_sensitive_data_on_connection_error: true,
   pool_size: 10
 ...
 ```
@@ -167,7 +168,7 @@ defmodule Hello.User do
   end
 
   @doc false
-  def changeset(%User{} = user, attrs) do
+  def changeset(user, attrs) do
     user
     |> cast(attrs, [:name, :email, :bio, :number_of_pets])
     |> validate_required([:name, :email, :bio, :number_of_pets])
@@ -404,7 +405,7 @@ iex> alias Hello.{Repo, User}
 [Hello.Repo, Hello.User]
 
 iex> Repo.insert(%User{email: "user1@example.com"})
-[debug] QUERY OK db=6.5ms decode=2.0ms queue=0.5ms idle=1358.3ms
+[debug] QUERY OK db=6.5ms queue=0.5ms idle=1358.3ms
 INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user1@example.com", ~N[2021-02-25 01:58:55], ~N[2021-02-25 01:58:55]]
 {:ok,
  %Hello.User{
@@ -434,7 +435,11 @@ INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETUR
  }}
 ```
 
-We started by aliasing our `User` and `Repo` modules for easy access. Next, we called [`Repo.insert/2`] and passed a user struct. Since we are in the `dev` environment, we can see the debug logs for the query our repository performed when inserting the underlying `%User{}` data. We received a two-element tuple back with `{:ok, %User{}}`, which lets us know the insertion was successful. With a couple of users inserted, let's fetch them back out of the repo.
+We started by aliasing our `User` and `Repo` modules for easy access. Next, we called [`Repo.insert/2`] with a User struct. Since we are in the `dev` environment, we can see the debug logs for the query our repository performed when inserting the underlying `%User{}` data. We received a two-element tuple back with `{:ok, %User{}}`, which lets us know the insertion was successful.
+
+We could also insert a user by passing a changeset to [`Repo.insert/2`]. If the changeset is valid, the repository will use an optimized database query to insert the record, and return a two-element tuple back, as above. If the changeset is not valid, we receive a two-element tuple consisting of `:error` plus the invalid changeset.
+
+With a couple of users inserted, let's fetch them back out of the repo.
 
 ```elixir
 iex> Repo.all(User)
@@ -491,7 +496,10 @@ Now we're starting to get a taste of Ecto's rich querying capabilities. We used
 iex> Repo.all(from u in User, select: %{u.id => u.email})
 [debug] QUERY OK source="users" db=0.9ms
 SELECT u0."id", u0."email" FROM "users" AS u0 []
-[%{3 => "user1@example.com"}, %{4 => "user2@example.com"}]
+[
+  %{1 => "user1@example.com"},
+  %{2 => "user2@example.com"}
+]
 ```
 
 That little query packed a big punch. It both fetched all user emails from the database and efficiently built a map of the results in one go. You should browse the [Ecto.Query documentation](https://hexdocs.pm/ecto/Ecto.Query.html#content) to see the breadth of supported query features.
diff --git a/guides/routing.md b/guides/routing.md
index 4a7f0a374..d744a1705 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -148,9 +148,9 @@ The `Phoenix.Router.resources/4` macro describes additional options for customiz
 
 ## Path helpers
 
-Path helpers are functions that are dynamically defined on the `Router.Helpers` module for an individual application. For us, that is `HelloWeb.Router.Helpers`. The name of each path helper is derived from the name of the controller used in the route definition. Our controller is `HelloWeb.PageController`, and `page_path` is the function which will return the path to the root of our application.
+Path helpers are dynamically defined functions. They allow us to retrieve the path corresponding to a given controller-action pair. The name of each path helper is derived from the name of the controller used in the route definition. For our controller `HelloWeb.PageController`, `page_path` is the function that will return the path, which in this case is the root of our application.
 
-That's a mouthful. Let's see it in action. Run `iex -S mix` at the root of the project. When we call the `page_path` function on our router helpers with the endpoint or connection and action as arguments, it returns the path to us.
+Let's see it in action. Run `iex -S mix` at the root of the project. When we call the `page_path` function on our router helpers with the endpoint or connection and action as arguments, it returns the path to us.
 
 ```elixir
 iex> HelloWeb.Router.Helpers.page_path(HelloWeb.Endpoint, :index)
@@ -163,6 +163,8 @@ This is significant because we can use the `page_path` function in a template to
 <%= link "Welcome Page!", to: Routes.page_path(@conn, :index) %>
 ```
 
+Note that path helpers are dynamically defined on the `Router.Helpers` module for an individual application. For us, that is `HelloWeb.Router.Helpers`.
+
 The reason we can use `Routes.page_path` instead of the full `HelloWeb.Router.Helpers.page_path` name is because `HelloWeb.Router.Helpers` is aliased as `Routes` by default in the `view_helpers/0` block defined inside `lib/hello_web.ex`. This definition is made available to our templates through `use HelloWeb, :view`.
 
 We can, of course, use `HelloWeb.Router.Helpers.page_path(@conn, :index)` instead, but the convention is to use the aliased version for conciseness. Note that the alias is only set automatically for use in views, controllers and templates - outside these, you need either the full name, or to alias it yourself inside the module definition with `alias HelloWeb.Router.Helpers, as: Routes`.
diff --git a/installer/templates/phx_assets/app.js b/installer/templates/phx_assets/app.js
index e1fe24004..a514cc43c 100644
--- a/installer/templates/phx_assets/app.js
+++ b/installer/templates/phx_assets/app.js
@@ -13,7 +13,7 @@ import "../css/app.css"
 //
 //     import "../vendor/some-package.js"
 //
-// Alternatively, you can `npm install some-package` and import
+// Alternatively, you can `npm install some-package --prefix assets` and import
 // them using a path starting with the package name:
 //
 //     import "some-package"
@@ -42,4 +42,4 @@ import "phoenix_html"
 // >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
 // >> liveSocket.disableLatencySim()
 <%= @live_comment %>window.liveSocket = liveSocket
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/lib/mix/tasks/phx.gen.auth/hashing_library.ex b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
index 022a58f40..934891c9b 100644
--- a/lib/mix/tasks/phx.gen.auth/hashing_library.ex
+++ b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
@@ -7,7 +7,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.HashingLibrary do
     lib = %__MODULE__{
       name: :bcrypt,
       module: Bcrypt,
-      mix_dependency: ~s|{:bcrypt_elixir, "~> 2.0\"}|,
+      mix_dependency: ~s|{:bcrypt_elixir, "~> 2.0"}|,
       test_config: """
       config :bcrypt_elixir, :log_rounds, 1
       """
@@ -20,7 +20,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.HashingLibrary do
     lib = %__MODULE__{
       name: :pbkdf2,
       module: Pbkdf2,
-      mix_dependency: ~s|{:pbkdf2_elixir, "~> 1.0\"}|,
+      mix_dependency: ~s|{:pbkdf2_elixir, "~> 1.0"}|,
       test_config: """
       config :pbkdf2_elixir, :rounds, 1
       """
@@ -33,7 +33,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.HashingLibrary do
     lib = %__MODULE__{
       name: :argon2,
       module: Argon2,
-      mix_dependency: ~s|{:argon2_elixir, "~> 2.0\"}|,
+      mix_dependency: ~s|{:argon2_elixir, "~> 2.0"}|,
       test_config: """
       config :argon2_elixir, t_cost: 1, m_cost: 8
       """
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 5ec138ace..543ba62fc 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.
@@ -858,6 +862,11 @@ defmodule Phoenix.Endpoint do
     * `:max_frame_size` - the maximum allowed frame size in bytes,
       defaults to "infinity"
 
+    * `:fullsweep_after` - the maximum number of garbage collections
+      before forcing a fullsweep for the socket process. You can set
+      it to `0` to force more frequent cleanups of your websocket
+      transport processes. Setting this option requires Erlang/OTP 24
+
     * `:compress` - whether to enable per message compression on
       all data frames, defaults to false
 
diff --git a/lib/phoenix/endpoint/cowboy2_handler.ex b/lib/phoenix/endpoint/cowboy2_handler.ex
index ccb8cbe35..cbedb17a8 100644
--- a/lib/phoenix/endpoint/cowboy2_handler.ex
+++ b/lib/phoenix/endpoint/cowboy2_handler.ex
@@ -24,10 +24,21 @@ defmodule Phoenix.Endpoint.Cowboy2Handler do
               cowboy_opts =
                 opts
                 |> Enum.flat_map(fn
-                  {:timeout, timeout} -> [idle_timeout: timeout]
-                  {:compress, _} = opt -> [opt]
-                  {:max_frame_size, _} = opt -> [opt]
-                  _other -> []
+                  {:timeout, timeout} ->
+                    [idle_timeout: timeout]
+
+                  {:compress, _} = opt ->
+                    [opt]
+
+                  {:max_frame_size, _} = opt ->
+                    [opt]
+
+                  {:fullsweep_after, value} ->
+                    :erlang.process_flag(:fullsweep_after, value)
+                    []
+
+                  _other ->
+                    []
                 end)
                 |> Map.new()
 
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
diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex
index aff2cd0ba..31b4134fc 100644
--- a/lib/phoenix/socket.ex
+++ b/lib/phoenix/socket.ex
@@ -88,6 +88,10 @@ defmodule Phoenix.Socket do
 
       send(socket.transport_pid, :garbage_collect)
 
+  Alternatively, you can configure your endpoint socket to trigger more
+  fullsweep garbage collections more frequently, by setting the `:fullsweep_after`
+  option for websockets. See `Phoenix.Endpoint.socket/3` for more info.
+
   ## Client-server communication
 
   The encoding of server data and the decoding of client data is done
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index e9811ae93..d609b3999 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -57,7 +57,7 @@ defmodule Phoenix.Socket.Transport do
 
         def child_spec(opts) do
           # We won't spawn any process, so let's return a dummy task
-          %{id: Task, start: {Task, :start_link, [fn -> :ok end]}, restart: :transient}
+          %{id: __MODULE__, start: {Task, :start_link, [fn -> :ok end]}, restart: :transient}
         end
 
         def connect(state) do


Test patch:
diff --git a/test/phoenix/endpoint/supervisor_test.exs b/test/phoenix/endpoint/supervisor_test.exs
index 1c1216277..a08119d63 100644
--- a/test/phoenix/endpoint/supervisor_test.exs
+++ b/test/phoenix/endpoint/supervisor_test.exs
@@ -13,7 +13,9 @@ defmodule Phoenix.Endpoint.SupervisorTest do
     config = Supervisor.config(:phoenix, SupervisorApp.Endpoint)
     assert config[:otp_app] == :phoenix
     assert config[:custom] == true
-    assert config[:render_errors] == [view: SupervisorApp.ErrorView, accepts: ~w(html), layout: false]
+
+    assert config[:render_errors] ==
+             [view: SupervisorApp.ErrorView, accepts: ~w(html), layout: false]
   end
 
   defmodule HTTPSEndpoint do
@@ -34,8 +36,8 @@ defmodule Phoenix.Endpoint.SupervisorTest do
 
   defmodule HTTPEnvVarEndpoint do
     def config(:https), do: false
-    def config(:http), do: [port: {:system,"PHOENIX_PORT"}]
-    def config(:url), do: [host: {:system,"PHOENIX_HOST"}]
+    def config(:http), do: [port: {:system, "PHOENIX_PORT"}]
+    def config(:url), do: [host: {:system, "PHOENIX_HOST"}]
     def config(:otp_app), do: :phoenix
   end
 
@@ -57,6 +59,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
@@ -76,6 +84,7 @@ defmodule Phoenix.Endpoint.SupervisorTest do
   test "static_path/2 returns file's path with lookup cache" do
     assert {:nocache, {"/phoenix.png", nil}} =
              Supervisor.static_lookup(HTTPEndpoint, "/phoenix.png")
+
     assert {:nocache, {"/images/unknown.png", nil}} =
              Supervisor.static_lookup(HTTPEndpoint, "/images/unknown.png")
   end
@@ -92,4 +101,43 @@ 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