Repository: phoenixframework/phoenix
Pull request #4622 (2021-12-07 21:44:48)

Reverse order of definition of channel handler functions
References #4618 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca9d1edca..780ea3c10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,29 @@ See the [upgrade guide](https://gist.github.com/chrismccord/2ab350f154235ad4a4d0
 
 Phoenix v1.6 requires Elixir v1.9+.
 
+## 1.6.5 (2021-12-16)
+
+### Enhancements
+  * [Endpoint] Support `check_origin: :conn` to enforce origin on the connection's host, port, and scheme
+
+## 1.6.4 (2021-12-08)
+
+### Bug Fixes
+  * Fix incorrect `phx.gen.release` output
+
+## 1.6.3 (2021-12-07)
+
+### Enhancements
+  * Add new `phx.gen.release` task for release and docker based deployments
+  * Add `fullsweep_after` option to the websocket transport
+  * Add `:force_watchers` option to `Phoenix.Endpoint` for running watchers even when web server is not started
+
+### Bug Fixes
+  * Fix Endpoint `log: false` failing to disable logging
+
+### JavaScript Client Bug Fixes
+  * Do not attempt to reconnect automatically if client gracefully closes connection
+
 ## 1.6.2 (2021-10-08)
 
 ### Bug Fixes
diff --git a/assets/js/phoenix/socket.js b/assets/js/phoenix/socket.js
index 653d5c4ce..dc4639820 100644
--- a/assets/js/phoenix/socket.js
+++ b/assets/js/phoenix/socket.js
@@ -78,7 +78,7 @@ import Timer from "./timer"
  *
  * Defaults to 20s (double the server long poll timer).
  *
- * @param {{Object|function)} [opts.params] - The optional params to pass when connecting
+ * @param {(Object|function)} [opts.params] - The optional params to pass when connecting
  * @param {string} [opts.binaryType] - The binary type to use for binary WebSocket frames.
  *
  * Defaults to "arraybuffer"
diff --git a/assets/package-lock.json b/assets/package-lock.json
index df943ff1a..c65dda4d8 100644
--- a/assets/package-lock.json
+++ b/assets/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "phoenix",
-  "version": "1.6.2",
+  "version": "1.6.4",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "phoenix",
-      "version": "1.6.2",
+      "version": "1.6.4",
       "license": "MIT",
       "devDependencies": {
         "@babel/cli": "7.14.3",
diff --git a/assets/package.json b/assets/package.json
index 1409b9a2e..177dd8b4d 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -1,6 +1,6 @@
 {
   "name": "phoenix",
-  "version": "1.6.2",
+  "version": "1.6.4",
   "description": "The official JavaScript client for the Phoenix web framework.",
   "license": "MIT",
   "main": "./assets/js/phoenix/index.js",
diff --git a/config/config.exs b/config/config.exs
index 783d73c57..38a43e8c1 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -18,7 +18,7 @@ if Mix.env() == :dev do
   end
 
   config :esbuild,
-    version: "0.13.5",
+    version: "0.14.0",
     module: esbuild.(~w(--format=esm --sourcemap --outfile=../priv/static/phoenix.esm.js)),
     main: esbuild.(~w(--format=cjs --sourcemap --outfile=../priv/static/phoenix.cjs.js)),
     cdn:
diff --git a/guides/contexts.md b/guides/contexts.md
index 6a66cf364..1944fc265 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -28,7 +28,7 @@ Phoenix includes the `mix phx.gen.html`, `mix phx.gen.json`, `mix phx.gen.live`,
 
 In order to run the context generators, we need to come up with a module name that groups the related functionality that we're building. In the [Ecto guide](ecto.html), we saw how we can use Changesets and Repos to validate and persist user schemas, but we didn't integrate this with our application at large. In fact, we didn't think about where a "user" in our application should live at all. Let's take a step back and think about the different parts of our system. We know that we'll have products to showcase on pages for sale, along with descriptions, pricing, etc. Along with selling products, we know we'll need to support carting, order checkout, and so on. While the products being purchased are related to the cart and checkout processes, showcasing a product and managing the *exhibition* of our products is distinctly different than tracking what a user has placed in their cart or how an order is placed. A `Catalog` context is a natural place for the management of our product details and the showcasing of those products we have for sale.
 
-> Naming things is hard. If you're stuck when trying to come up with a context name when the grouped functionality in your system isn't yet clear, you can simply use the plural form of the resource you're creating. For example, a `Products` context for managing products. As you grow your application and the parts of your system become clear, you can simply rename the context to a more refined name at a later time.
+> Naming things is hard. If you're stuck when trying to come up with a context name when the grouped functionality in your system isn't yet clear, you can simply use the plural form of the resource you're creating. For example, a `Products` context for managing products. As you grow your application and the parts of your system become clear, you can simply rename the context to a more refined one.
 
 To jump-start our catalog context, we'll use `mix phx.gen.html` which creates a context module that wraps up Ecto access for creating, updating, and deleting products, along with web files like controllers and templates for the web interface into our context. Run the following command at your project root:
 
@@ -325,7 +325,7 @@ Our basic catalog features are nice, but let's take it up a notch by categorizin
 
 For now, categories will contain only textual information. Our first order of business is to decide where categories live in the application. We have our `Catalog` context, which manages the exhibition of our products. Product categorization is a natural fit here. Phoenix is also smart enough to generate code inside an existing context, which makes adding new resources to a context a breeze. Run the following command at your project root:
 
-> Sometimes it may be tricky to determine if two resources belong to the same context or not. In those cases, prefer distinct contexts per resource and refactor later if necessary. Otherwise you can easily end-up with large contexts of loosely related entities. Also keep in mind that the fact two resources are related does not necessarily mean they belong to the same context, otherwise you would quickly end-up with one large context, as the majority of resources in an application are connected to each other. To sum it up: if you are unsure, you should prefer explicit modules (contexts) between resources.
+> Sometimes it may be tricky to determine if two resources belong to the same context or not. In those cases, prefer distinct contexts per resource and refactor later if necessary. Otherwise you can easily end-up with large contexts of loosely related entities. Also keep in mind that the fact two resources are related does not necessarily mean they belong to the same context, otherwise you would quickly end-up with one large context, as the majority of resources in an application are connected to each other. To sum it up: if you are unsure, you should prefer separate modules (contexts).
 
 ```console
 $ mix phx.gen.context Catalog Category categories \
@@ -662,9 +662,9 @@ Our database is ready to go with new `carts` and `cart_items` tables, but now we
 
 ### Cross-context data
 
-Dependencies in your software are often unavoidable, but we can do our best to limit them where possible and lessen the maintenance burden when a dependency is necessary. So far, we've done a great job isolating the two main contexts of our application from each other, but now we have a necessary dependency to handle.
+So far, we've done a great job isolating the two main contexts of our application from each other, but now we have a necessary dependency to handle.
 
-Our `Catalog.Product` resource serves to keep the responsibilities of representing a product inside the catalog, but ultimately for an item to exist in the cart, a product from the catalog must be present. Given this, our `ShoppingCart` context will have a data dependency on the `Catalog` context. With that in mind, we have two options. One is to expose APIs on the `Catalog` context that allows us to efficiently fetch product data for use in the `ShoppingCart` system, or we can use database joins to fetch the dependent data. Both are valid options given your tradeoffs and application size, but joining data from the database when you have a hard data dependency is just fine for a large class of applications. Additionally, joining data yourself results in increased application code complexity, worse performance, and potential data inconsistencies. Also, if you decide to break out coupled contexts into entirely separate applications and databases at a later time, you still gain the benefits of isolation. This is because your public context APIs will likely remain unchanged.
+Our `Catalog.Product` resource serves to keep the responsibilities of representing a product inside the catalog, but ultimately for an item to exist in the cart, a product from the catalog must be present. Given this, our `ShoppingCart` context will have a data dependency on the `Catalog` context. With that in mind, we have two options. One is to expose APIs on the `Catalog` context that allows us to efficiently fetch product data for use in the `ShoppingCart` system, which we would manually stitch together. Or we can use database joins to fetch the dependent data. Both are valid options given your tradeoffs and application size, but joining data from the database when you have a hard data dependency is just fine for a large class of applications and is the approach we will take here.
 
 Now that we know where our data dependencies exist, let's add our schema associations so we can tie shopping cart items to products. First, let's make a quick change to our cart schema in `lib/hello/shopping_cart/cart.ex` to associate a cart to its items:
 
@@ -1343,4 +1343,8 @@ As we explored the context API, you might have wondered:
 
 > If one of the goals of our context is to encapsulate Ecto Repo access, why does `create_user/1` return an `Ecto.Changeset` struct when we fail to create a user?
 
-The answer is we've decided to expose `%Ecto.Changeset{}` as a public *data-structure* in our application. We saw before how changesets allow us to track field changes, perform validations, and generate error messages. Its use here is decoupled from the private Repo access and Ecto changeset API internals. We're exposing a data structure that the caller understands which contains the rich information like field errors. Conveniently for us, the `phoenix_ecto` project implements the necessary `Phoenix.Param` and `Phoenix.HTML.FormData` protocols which know how to handle `%Ecto.Changeset{}`'s for things like form generation and error messages. You can also think about it as being as if you had defined your own `%Accounts.Changes{}` struct for the same purpose and implemented the Phoenix protocols for the web-layer integration.
+Although Changesets are part of Ecto, they are not tied to the database, and they can be used to map data from and to any source, which makes it a general and useful data structure for tracking field changes, perform validations, and generate error messages.
+
+For those reasons, `%Ecto.Changeset{}` is a good choice to model the data changes between your contexts and your web layer. Regardless if you are talking to an API or the database.
+
+Finally, note that your controllers and views are not hardcoded to work exclusively with Ecto either. Instead, Phoenix defines protocols such as `Phoenix.Param` and `Phoenix.HTML.FormData`, which allow any library to extend how Phoenix generates URL parameters or renders forms. Conveniently for us, the `phoenix_ecto` project implements those protocols, but you could as well bring your own data structures and implement them yourself.
diff --git a/guides/ecto.md b/guides/ecto.md
index 56697b627..394b42f39 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -137,8 +137,8 @@ When `phx.new` generated our application, it included some basic repository conf
 config :hello, Hello.Repo,
   username: "postgres",
   password: "postgres",
-  database: "hello_dev",
   hostname: "localhost",
+  database: "hello_dev",
   show_sensitive_data_on_connection_error: true,
   pool_size: 10
 ...
diff --git a/guides/mix_tasks.md b/guides/mix_tasks.md
index 5ebff708c..657a16ed4 100644
--- a/guides/mix_tasks.md
+++ b/guides/mix_tasks.md
@@ -580,10 +580,10 @@ Notice that this task has updated `config/config.exs`. If we take a look, we'll
 ```elixir
 . . .
 config :hello, OurCustom.Repo,
-  database: "hello_repo",
   username: "user",
   password: "pass",
-  hostname: "localhost"
+  hostname: "localhost",
+  database: "hello_repo",
 . . .
 ```
 
diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex
index b0ed82b42..5fbada487 100644
--- a/installer/lib/phx_new/generator.ex
+++ b/installer/lib/phx_new/generator.ex
@@ -324,16 +324,16 @@ defmodule Phx.New.Generator do
       dev: [
         username: user,
         password: pass,
-        database: "#{app}_dev",
         hostname: "localhost",
+        database: "#{app}_dev",
         show_sensitive_data_on_connection_error: true,
         pool_size: 10
       ],
       test: [
         username: user,
         password: pass,
-        database: {:literal, ~s|"#{app}_test\#{System.get_env("MIX_TEST_PARTITION")}"|},
         hostname: "localhost",
+        database: {:literal, ~s|"#{app}_test\#{System.get_env("MIX_TEST_PARTITION")}"|},
         pool: Ecto.Adapters.SQL.Sandbox,
         pool_size: 10,
       ],
diff --git a/installer/mix.exs b/installer/mix.exs
index 511870b6b..5540044e7 100644
--- a/installer/mix.exs
+++ b/installer/mix.exs
@@ -6,7 +6,7 @@ end
 defmodule Phx.New.MixProject do
   use Mix.Project
 
-  @version "1.6.2"
+  @version "1.6.4"
   @scm_url "https://github.com/phoenixframework/phoenix"
 
   # If the elixir requirement is updated, we need to update:
diff --git a/installer/templates/phx_single/config/config.exs b/installer/templates/phx_single/config/config.exs
index 19420a522..860d8aa9d 100644
--- a/installer/templates/phx_single/config/config.exs
+++ b/installer/templates/phx_single/config/config.exs
@@ -33,7 +33,7 @@ config :swoosh, :api_client, false<% end %><%= if @assets do %>
 
 # Configure esbuild (the version is required)
 config :esbuild,
-  version: "0.13.5",
+  version: "0.14.0",
   default: [
     args:
       ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
diff --git a/installer/templates/phx_single/config/runtime.exs b/installer/templates/phx_single/config/runtime.exs
index 9d7a0c97d..b7c7ebc9d 100644
--- a/installer/templates/phx_single/config/runtime.exs
+++ b/installer/templates/phx_single/config/runtime.exs
@@ -6,6 +6,12 @@ import Config
 # and secrets from environment variables or elsewhere. Do not define
 # any compile-time configuration in here, as it won't be applied.
 # The block below contains prod specific runtime configuration.
+
+# Start the phoenix server if environment is set and running in a  release
+if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
+  config :<%= @app_name %>, <%= @endpoint_module %>, server: true
+end
+
 if config_env() == :prod do
   # The secret key base is used to sign/encrypt cookies and other secrets.
   # A default value is used in config/dev.exs and config/test.exs but you
@@ -19,7 +25,6 @@ if config_env() == :prod do
       You can generate one by calling: mix phx.gen.secret
       """
 
-  server? = System.get_env("PHX_SERVER") != nil
   host = System.get_env("PHX_HOST") || "example.com"
   port = String.to_integer(System.get_env("PORT") || "4000")
 
@@ -33,7 +38,6 @@ if config_env() == :prod do
       ip: {0, 0, 0, 0, 0, 0, 0, 0},
       port: port
     ],
-    server: server?,
     secret_key_base: secret_key_base
 
   # ## Using releases
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
index 4d93c5b96..94e0d08a1 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
@@ -13,7 +13,7 @@ config :<%= @web_app_name %>, <%= @endpoint_module %>,
 
 # Configure esbuild (the version is required)
 config :esbuild,
-  version: "0.13.5",
+  version: "0.14.0",
   default: [
     args:
       ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
diff --git a/lib/mix/tasks/phx.gen.release.ex b/lib/mix/tasks/phx.gen.release.ex
index ee5952a1f..21afa6346 100644
--- a/lib/mix/tasks/phx.gen.release.ex
+++ b/lib/mix/tasks/phx.gen.release.ex
@@ -6,7 +6,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
   The following release files are created:
 
-    * `lib/app_name/release.exs` - A release module containing tasks for running
+    * `lib/app_name/release.ex` - A release module containing tasks for running
       migrations inside a release
 
     * `rel/overlays/bin/migrate` - A migrate script for conveniently invoking
@@ -45,6 +45,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
     app = Mix.Phoenix.otp_app()
     app_namespace = Mix.Phoenix.base()
+    web_namespace = app_namespace |> Mix.Phoenix.web_module() |> inspect()
 
     binding = [
       app_namespace: app_namespace,
@@ -75,6 +76,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
     File.chmod!("rel/overlays/bin/server", 0o755)
     File.chmod!("rel/overlays/bin/server.bat", 0o755)
+
     if ecto? do
       File.chmod!("rel/overlays/bin/migrate", 0o755)
       File.chmod!("rel/overlays/bin/migrate.bat", 0o755)
@@ -84,26 +86,26 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
     Your application is ready to be deployed in a release!
 
-        # To start your system
-        _build/dev/rel/#{app}/bin/#{app} start
+    See https://hexdocs.pm/mix/Mix.Tasks.Release.html for more information about Elixir releases.
+    #{if docker?, do: docker_instructions()}
+    Here are some useful release commands you can run in any release environment:
+
+        # To build a release
+        mix release
 
         # To start your system with the Phoenix server running
         _build/dev/rel/#{app}/bin/server
-    #{ecto? && ecto_instructions(app)}
-    Once the release is running:
+    #{if ecto?, do: ecto_instructions(app)}
+    Once the release is running you can connect to it remotely:
 
-        # To connect to it remotely
         _build/dev/rel/#{app}/bin/#{app} remote
 
-        # To stop it gracefully (you may also send SIGINT/SIGTERM)
-        _build/dev/rel/#{app}/bin/#{app} stop
-
     To list all commands:
 
         _build/dev/rel/#{app}/bin/#{app}
     """)
 
-    ecto? &&
+    if ecto? do
       post_install_instructions("config/runtime.exs", ~r/ECTO_IPV6/, """
       [warn] Conditional IPV6 support missing from runtime configuration.
 
@@ -115,17 +117,16 @@ defmodule Mix.Tasks.Phx.Gen.Release do
             ...,
             socket_options: maybe_ipv6
       """)
+    end
 
     post_install_instructions("config/runtime.exs", ~r/PHX_SERVER/, """
     [warn] Conditional server startup is missing from runtime configuration.
 
-    Add the following to your config/runtime.exs:
-
-        server? = System.get_env("PHX_SERVER") != nil
+    Add the following to the top of your config/runtime.exs:
 
-        config :#{app}, #{app_namespace}.Endpoint,
-          ...,
-          server: server?
+        if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
+          config :#{app}, #{web_namespace}.Endpoint, server: true
+        end
     """)
 
     post_install_instructions("config/runtime.exs", ~r/PHX_HOST/, """
@@ -135,7 +136,7 @@ defmodule Mix.Tasks.Phx.Gen.Release do
 
         host = System.get_env("PHX_HOST") || "example.com"
 
-        config :#{app}, #{app_namespace}.Endpoint,
+        config :#{app}, #{web_namespace}.Endpoint,
           ...,
           url: [host: host, port: 443]
     """)
@@ -149,6 +150,17 @@ defmodule Mix.Tasks.Phx.Gen.Release do
     """
   end
 
+  defp docker_instructions do
+    """
+
+    Using the generated Dockerfile, your release will be bundled into
+    a Docker image, ready for deployment on platforms that support Docker.
+
+    For more information about deploying with Docker see
+    https://hexdocs.pm/phoenix/releases.html#containers
+    """
+  end
+
   defp paths do
     [".", :phoenix]
   end
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 543ba62fc..de943ea20 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -798,6 +798,10 @@ defmodule Phoenix.Endpoint do
             "//*.other.com"
           ]
 
+      Or to accept any origin matching the request connection's host, port, and scheme:
+
+          check_origin: :conn
+
       Or a custom MFA function:
 
           check_origin: {MyAppWeb.Auth, :my_check_origin?, []}
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index d1ced76fa..815b76ab9 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -531,7 +531,8 @@ defmodule Phoenix.Router do
 
   defp build_pipes(name, pipe_through) do
     plugs = pipe_through |> Enum.reverse |> Enum.map(&{&1, [], true})
-    {conn, body} = Plug.Builder.compile(__ENV__, plugs, init_mode: Phoenix.plug_init_mode(), log_on_halt: :debug)
+    opts = [init_mode: Phoenix.plug_init_mode(), log_on_halt: :debug]
+    {conn, body} = Plug.Builder.compile(__ENV__, plugs, opts)
 
     quote do
       defp unquote(name)(unquote(conn)), do: unquote(body)
@@ -676,29 +677,35 @@ defmodule Phoenix.Router do
   See `pipeline/2` for more information.
   """
   defmacro plug(plug, opts \\ []) do
+    {plug, opts} = expand_plug_and_opts(plug, opts, __CALLER__)
+
+    quote do
+      if pipeline = @phoenix_pipeline do
+        @phoenix_pipeline [{unquote(plug), unquote(opts), true}|pipeline]
+      else
+        raise "cannot define plug at the router level, plug must be defined inside a pipeline"
+      end
+    end
+  end
+
+  defp expand_plug_and_opts(plug, opts, caller) do
     runtime? = Phoenix.plug_init_mode() == :runtime
 
     plug =
       if runtime? do
-        expand_alias(plug, __CALLER__)
+        expand_alias(plug, caller)
       else
         plug
       end
 
     opts =
       if runtime? and Macro.quoted_literal?(opts) do
-        Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
+        Macro.prewalk(opts, &expand_alias(&1, caller))
       else
         opts
       end
 
-    quote do
-      if pipeline = @phoenix_pipeline do
-        @phoenix_pipeline [{unquote(plug), unquote(opts), true}|pipeline]
-      else
-        raise "cannot define plug at the router level, plug must be defined inside a pipeline"
-      end
-    end
+    {plug, opts}
   end
 
   defp expand_alias({:__aliases__, _, _} = alias, env),
@@ -874,6 +881,13 @@ defmodule Phoenix.Router do
 
   """
   defmacro scope(options, do: context) do
+    options =
+      if Macro.quoted_literal?(options) do
+        Macro.prewalk(options, &expand_alias(&1, __CALLER__))
+      else
+        options
+      end
+
     do_scope(options, context)
   end
 
@@ -894,15 +908,23 @@ defmodule Phoenix.Router do
 
   """
   defmacro scope(path, options, do: context) do
-    options = Macro.expand(options, %{__CALLER__ | function: {:init, 1}})
+    options =
+      if Macro.quoted_literal?(options) do
+        Macro.prewalk(options, &expand_alias(&1, __CALLER__))
+      else
+        options
+      end
 
-    options = quote do
-      path = unquote(path)
-      case unquote(options) do
-        alias when is_atom(alias) -> [path: path, alias: alias]
-        options when is_list(options) -> Keyword.put(options, :path, path)
+    options =
+      quote do
+        path = unquote(path)
+
+        case unquote(options) do
+          alias when is_atom(alias) -> [path: path, alias: alias]
+          options when is_list(options) -> Keyword.put(options, :path, path)
+        end
       end
-    end
+
     do_scope(options, context)
   end
 
@@ -923,7 +945,7 @@ defmodule Phoenix.Router do
 
   """
   defmacro scope(path, alias, options, do: context) do
-    alias = Macro.expand(alias, %{__CALLER__ | function: {:init, 1}})
+    alias = expand_alias(alias, __CALLER__)
 
     options = quote do
       unquote(options)
@@ -985,7 +1007,7 @@ defmodule Phoenix.Router do
 
   """
   defmacro forward(path, plug, plug_opts \\ [], router_opts \\ []) do
-    plug = Macro.expand(plug, %{__CALLER__ | function: {:init, 1}})
+    {plug, plug_opts} = expand_plug_and_opts(plug, plug_opts, __CALLER__)
     router_opts = Keyword.put(router_opts, :as, nil)
 
     quote unquote: true, bind_quoted: [path: path, plug: plug] do
diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex
index 31b4134fc..88f8c4594 100644
--- a/lib/phoenix/socket.ex
+++ b/lib/phoenix/socket.ex
@@ -354,24 +354,24 @@ defmodule Phoenix.Socket do
   See `Phoenix.Channel` for more information
   """
   defmacro channel(topic_pattern, module, opts \\ []) do
-    # Tear the alias to simply store the root in the AST.
-    # This will make Elixir unable to track the dependency between
-    # endpoint <-> socket and avoid recompiling the endpoint
-    # (alongside the whole project) whenever the socket changes.
-    module = tear_alias(module)
+    module = expand_alias(module, __CALLER__)
+
+    opts =
+      if Macro.quoted_literal?(opts) do
+        Macro.prewalk(opts, &expand_alias(&1, __CALLER__))
+      else
+        opts
+      end
 
     quote do
       @phoenix_channels {unquote(topic_pattern), unquote(module), unquote(opts)}
     end
   end
 
-  defp tear_alias({:__aliases__, meta, [h|t]}) do
-    alias = {:__aliases__, meta, [h]}
-    quote do
-      Module.concat([unquote(alias)|unquote(t)])
-    end
-  end
-  defp tear_alias(other), do: other
+  defp expand_alias({:__aliases__, _, _} = alias, env),
+    do: Macro.expand(alias, %{env | function: {:channel, 3}})
+
+  defp expand_alias(other, _env), do: other
 
   @doc false
   @deprecated "transport/3 in Phoenix.Socket is deprecated and has no effect"
@@ -380,7 +380,10 @@ defmodule Phoenix.Socket do
   end
 
   defmacro __before_compile__(env) do
-    channels = Module.get_attribute(env.module, :phoenix_channels)
+    channels =
+      env.module
+      |> Module.get_attribute(:phoenix_channels, [])
+      |> Enum.reverse()
 
     channel_defs =
       for {topic_pattern, module, opts} <- channels do
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index d609b3999..2f1d113a4 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -353,7 +353,7 @@ defmodule Phoenix.Socket.Transport do
       is_nil(origin) or check_origin == false ->
         conn
 
-      origin_allowed?(check_origin, URI.parse(origin), endpoint) ->
+      origin_allowed?(check_origin, URI.parse(origin), endpoint, conn) ->
         conn
 
       true ->
@@ -567,8 +567,11 @@ defmodule Phoenix.Socket.Transport do
           {module, function, arguments} ->
             {module, function, arguments}
 
+          :conn ->
+            :conn
+
           invalid ->
-            raise ArgumentError, ":check_origin expects a boolean, list of hosts, or MFA tuple, got: #{inspect(invalid)}"
+            raise ArgumentError, ":check_origin expects a boolean, list of hosts, :conn, or MFA tuple, got: #{inspect(invalid)}"
         end
 
       {:cache, check_origin}
@@ -588,13 +591,15 @@ defmodule Phoenix.Socket.Transport do
     end
   end
 
-  defp origin_allowed?({module, function, arguments}, uri, _endpoint),
+  defp origin_allowed?({module, function, arguments}, uri, _endpoint, _conn),
     do: apply(module, function, [uri | arguments])
-  defp origin_allowed?(_check_origin, %{host: nil}, _endpoint),
+  defp origin_allowed?(:conn, uri, _endpoint, %Plug.Conn{} = conn),
+    do: uri.host == conn.host and uri.scheme == conn.scheme and uri.port == conn.port
+  defp origin_allowed?(_check_origin, %{host: nil}, _endpoint, _conn),
     do: false
-  defp origin_allowed?(true, uri, endpoint),
+  defp origin_allowed?(true, uri, endpoint, _conn),
     do: compare?(uri.host, host_to_binary(endpoint.config(:url)[:host]))
-  defp origin_allowed?(check_origin, uri, _endpoint) when is_list(check_origin),
+  defp origin_allowed?(check_origin, uri, _endpoint, _conn) when is_list(check_origin),
     do: origin_allowed?(uri, check_origin)
 
   defp origin_allowed?(uri, allowed_origins) do
diff --git a/lib/phoenix/token.ex b/lib/phoenix/token.ex
index 7fd3de623..551405a6f 100644
--- a/lib/phoenix/token.ex
+++ b/lib/phoenix/token.ex
@@ -197,8 +197,8 @@ defmodule Phoenix.Token do
   ## Options
 
     * `:max_age` - verifies the token only if it has been generated
-      "max age" ago in seconds. A reasonable value is 1 day (86400
-      seconds)
+      "max age" ago in seconds. Defaults to the max age signed in the
+      token (86400)
     * `:key_iterations` - option passed to `Plug.Crypto.KeyGenerator`
       when generating the encryption and signing keys. Defaults to 1000
     * `:key_length` - option passed to `Plug.Crypto.KeyGenerator`
diff --git a/mix.exs b/mix.exs
index 6559f6aeb..c28b2f7c2 100644
--- a/mix.exs
+++ b/mix.exs
@@ -8,7 +8,7 @@ defmodule Phoenix.MixProject do
     end
   end
 
-  @version "1.6.2"
+  @version "1.6.4"
   @scm_url "https://github.com/phoenixframework/phoenix"
 
   # If the elixir requirement is updated, we need to make the installer
diff --git a/package.json b/package.json
index c4f6b4c0e..29cdaba0e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "phoenix",
-  "version": "1.6.2",
+  "version": "1.6.4",
   "description": "The official JavaScript client for the Phoenix web framework.",
   "license": "MIT",
   "module": "./priv/static/phoenix.esm.js",
diff --git a/priv/templates/phx.gen.live/live_helpers.ex b/priv/templates/phx.gen.live/live_helpers.ex
index be9d96850..37cd9bbd4 100644
--- a/priv/templates/phx.gen.live/live_helpers.ex
+++ b/priv/templates/phx.gen.live/live_helpers.ex
@@ -46,7 +46,7 @@ defmodule <%= inspect context.web_module %>.LiveHelpers do
          <a id="close" href="#" class="phx-modal-close" phx-click={hide_modal()}>✖</a>
         <%% end %>
 
-        <%%= render_slot(@inner_block, assigns) %>
+        <%%= render_slot(@inner_block) %>
       </div>
     </div>
     """


Test patch:
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index 317e67d6c..ffbbd002e 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -52,7 +52,7 @@ defmodule Mix.Tasks.Phx.NewTest do
       end
 
       assert_file "phx_blog/config/prod.exs", fn file ->
-        assert file =~ "port: 80"
+        assert file =~ "port: 443"
       end
 
       assert_file "phx_blog/config/runtime.exs", ~r/ip: {0, 0, 0, 0, 0, 0, 0, 0}/
@@ -145,12 +145,15 @@ defmodule Mix.Tasks.Phx.NewTest do
       assert_file "phx_blog/config/test.exs", config
       assert_file "phx_blog/config/runtime.exs", fn file ->
         assert file =~ config
-        assert file =~ ~S|ipv6? = System.get_env("ECTO_IPV6") == "true"|
-        assert file =~ ~S|server? = System.get_env("PHX_SERVER") == "true"|
-        assert file =~ "host = System.get_env(\"PHX_HOST\") || \"example.com\""
-        assert file =~ ~S|url: [host: host, port: 80],|
-        assert file =~ ~S|server: server?|
-        assert file =~ ~S|socket_options: if(ipv6?, do: [:inet6], else: []),|
+        assert file =~ ~S|maybe_ipv6 = if System.get_env("ECTO_IPV6"), do: [:inet6], else: []|
+        assert file =~ ~S|socket_options: maybe_ipv6|
+        assert file =~ """
+        if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do
+          config :phx_blog, PhxBlogWeb.Endpoint, server: true
+        end
+        """
+        assert file =~ ~S[host = System.get_env("PHX_HOST") || "example.com"]
+        assert file =~ ~S|url: [host: host, port: 443],|
       end
       assert_file "phx_blog/config/test.exs", ~R/database: "phx_blog_test#\{System.get_env\("MIX_TEST_PARTITION"\)\}"/
       assert_file "phx_blog/lib/phx_blog/repo.ex", ~r"defmodule PhxBlog.Repo"
diff --git a/lib/phoenix/test/channel_test.ex b/lib/phoenix/test/channel_test.ex
index 057402695..68558af26 100644
--- a/lib/phoenix/test/channel_test.ex
+++ b/lib/phoenix/test/channel_test.ex
@@ -206,7 +206,7 @@ defmodule Phoenix.ChannelTest do
 
   """
   defmacro socket(socket_module) do
-    build_socket(socket_module, nil, [], __CALLER__)
+    socket(socket_module, nil, [], __CALLER__)
   end
 
   @doc """
@@ -218,22 +218,18 @@ defmodule Phoenix.ChannelTest do
 
   """
   defmacro socket(socket_module, socket_id, socket_assigns) do
-    build_socket(socket_module, socket_id, socket_assigns, __CALLER__)
+    socket(socket_module, socket_id, socket_assigns, __CALLER__)
   end
 
-  defp build_socket(socket, id, assigns, caller) do
+  defp socket(module, id, assigns, caller) do
     if endpoint = Module.get_attribute(caller.module, :endpoint) do
       quote do
-        %Socket{
-          assigns: Enum.into(unquote(assigns), %{}),
-          endpoint: unquote(endpoint),
-          handler: unquote(socket || first_socket!(endpoint)),
-          id: unquote(id),
-          pubsub_server: unquote(endpoint).config(:pubsub_server),
-          serializer: NoopSerializer,
-          transport: :channel_test,
-          transport_pid: self()
-        }
+        unquote(__MODULE__).__socket__(
+          unquote(module),
+          unquote(id),
+          unquote(assigns),
+          unquote(endpoint)
+        )
       end
     else
       raise "module attribute @endpoint not set for socket/2"
@@ -241,15 +237,17 @@ defmodule Phoenix.ChannelTest do
   end
 
   @doc false
-  @deprecated "Phoenix.ChannelTest.socket/0 is deprecated, please call socket/1 instead"
-  defmacro socket() do
-    build_socket(nil, nil, [], __CALLER__)
-  end
-
-  @doc false
-  @deprecated "Phoenix.ChannelTest.socket/2 is deprecated, please call socket/3 instead"
-  defmacro socket(id, assigns) do
-    build_socket(nil, id, assigns, __CALLER__)
+  def __socket__(socket, id, assigns, endpoint) do
+    %Socket{
+      assigns: Enum.into(assigns, %{}),
+      endpoint: endpoint,
+      handler: socket || first_socket!(endpoint),
+      id: id,
+      pubsub_server: endpoint.config(:pubsub_server),
+      serializer: NoopSerializer,
+      transport: {__MODULE__, fetch_test_supervisor!()},
+      transport_pid: self()
+    }
   end
 
   defp first_socket!(endpoint) do
@@ -259,6 +257,34 @@ defmodule Phoenix.ChannelTest do
     end
   end
 
+  defp fetch_test_supervisor!() do
+    case ExUnit.OnExitHandler.get_supervisor(self()) do
+      {:ok, nil} ->
+        opts = [strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1]
+        {:ok, sup} = Supervisor.start_link([], opts)
+        ExUnit.OnExitHandler.put_supervisor(self(), sup)
+        sup
+
+      {:ok, sup} ->
+        sup
+
+      :error ->
+        raise ArgumentError, "socket/1-3 can only be invoked from the test process"
+    end
+  end
+
+  @doc false
+  @deprecated "Phoenix.ChannelTest.socket/0 is deprecated, please call socket/1 instead"
+  defmacro socket() do
+    socket(nil, nil, [], __CALLER__)
+  end
+
+  @doc false
+  @deprecated "Phoenix.ChannelTest.socket/2 is deprecated, please call socket/3 instead"
+  defmacro socket(id, assigns) do
+    socket(nil, id, assigns, __CALLER__)
+  end
+
   @doc """
   Initiates a transport connection for the socket handler.
 
@@ -279,7 +305,7 @@ defmodule Phoenix.ChannelTest do
   def __connect__(endpoint, handler, params, connect_info) do
     map = %{
       endpoint: endpoint,
-      transport: :channel_test,
+      transport: {__MODULE__, fetch_test_supervisor!()},
       options: [serializer: [{NoopSerializer, "~> 1.0.0"}]],
       params: __stringify__(params),
       connect_info: connect_info
@@ -383,9 +409,11 @@ defmodule Phoenix.ChannelTest do
         match_topic_to_channel!(socket, topic)
       end
 
+    %Socket{transport: {__MODULE__, sup}} = socket
+
     starter =
       fn _, _, _, spec ->
-        Supervisor.start_child(fetch_test_supervisor!(), %{spec | id: make_ref()})
+        Supervisor.start_child(sup, %{spec | id: make_ref()})
       end
 
     case Server.join(socket, channel, message, [starter: starter] ++ opts) do
@@ -397,23 +425,6 @@ defmodule Phoenix.ChannelTest do
     end
   end
 
-  # TODO: replace with ExUnit.Case.fetch_test_supervisor!() when we require Elixir v1.11.
-  defp fetch_test_supervisor!() do
-    case ExUnit.OnExitHandler.get_supervisor(self()) do
-      {:ok, nil} ->
-        opts = [strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1]
-        {:ok, sup} = Supervisor.start_link([], opts)
-        ExUnit.OnExitHandler.put_supervisor(self(), sup)
-        sup
-
-      {:ok, sup} ->
-        sup
-
-      :error ->
-        raise ArgumentError, "fetch_test_supervisor!/0 can only be invoked from the test process"
-    end
-  end
-
   @doc """
   Pushes a message into the channel.
 
diff --git a/test/phoenix/socket/socket_test.exs b/test/phoenix/socket/socket_test.exs
index 8d789d5c0..fb72c5dd2 100644
--- a/test/phoenix/socket/socket_test.exs
+++ b/test/phoenix/socket/socket_test.exs
@@ -4,6 +4,24 @@ defmodule Phoenix.SocketTest do
   import Phoenix.Socket
   alias Phoenix.Socket.{Message, InvalidMessageError}
 
+  defmodule UserSocket do
+    use Phoenix.Socket
+
+    channel "food*", FoodChannel
+    channel "foo*", FooChannel
+
+    def connect(_params, socket, _connect_info), do: {:ok, socket}
+    def id(_socket), do: nil
+  end
+
+  describe "__channel__" do
+    test "returns the correct channel handler module" do
+      assert {FooChannel, []} == UserSocket.__channel__("foo:1")
+      assert {FoodChannel, []} == UserSocket.__channel__("food:1")
+      assert nil == UserSocket.__channel__("unknown")
+    end
+  end
+
   describe "messages" do
     test "from_map! converts a map with string keys into a %Message{}" do
       msg = Message.from_map!(%{"topic" => "c", "event" => "e", "payload" => "", "ref" => "r"})
diff --git a/test/phoenix/socket/transport_test.exs b/test/phoenix/socket/transport_test.exs
index c4af2c7ea..da8c15b70 100644
--- a/test/phoenix/socket/transport_test.exs
+++ b/test/phoenix/socket/transport_test.exs
@@ -51,11 +51,13 @@ defmodule Phoenix.Socket.TransportTest do
   ## Check origin
 
   describe "check_origin/4" do
-    defp check_origin(origin, opts) do
-      conn = conn(:get, "/") |> put_req_header("origin", origin)
+    defp check_origin(%Plug.Conn{} = conn, origin, opts) do
+      conn = put_req_header(conn, "origin", origin)
       Transport.check_origin(conn, make_ref(), Endpoint, opts)
     end
 
+    defp check_origin(origin, opts), do: check_origin(conn(:get, "/"), origin, opts)
+
     test "does not check origin if disabled" do
       refute check_origin("/", check_origin: false).halted
     end
@@ -131,6 +133,22 @@ defmodule Phoenix.Socket.TransportTest do
       refute check_origin("", check_origin: mfa).halted
     end
 
+    test "checks origin against :host" do
+      conn = %Plug.Conn{conn(:get, "/") | host: "example.com", scheme: "http", port: 80}
+      refute check_origin(conn, "http://example.com", check_origin: :conn).halted
+
+      assert check_origin(conn, "https://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "ws://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "wss://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "http://www.example.com", check_origin: :conn).halted
+      assert check_origin(conn, "http://www.another.com", check_origin: :conn).halted
+
+      conn = %Plug.Conn{conn(:get, "/") | host: "example.com", scheme: "https", port: 443}
+      refute check_origin(conn, "https://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "http://example.com", check_origin: :conn).halted
+      assert check_origin(conn, "https://example.com:4000", check_origin: :conn).halted
+    end
+
     test "does not halt invalid URIs when check_origin is disabled" do
       refute check_origin("file://", check_origin: false).halted
       refute check_origin("null", check_origin: false).halted
diff --git a/test/phoenix/test/channel_test.exs b/test/phoenix/test/channel_test.exs
index ad5508047..ae4627201 100644
--- a/test/phoenix/test/channel_test.exs
+++ b/test/phoenix/test/channel_test.exs
@@ -187,27 +187,23 @@ defmodule Phoenix.Test.ChannelTest do
   ## socket
 
   test "socket/1" do
-    assert socket(UserSocket) == %Socket{
+    assert %Socket{
              endpoint: @endpoint,
              handler: UserSocket,
              pubsub_server: Phoenix.Test.ChannelTest.PubSub,
-             transport: :channel_test,
-             transport_pid: self(),
              serializer: Phoenix.ChannelTest.NoopSerializer
-           }
+           } = socket(UserSocket)
   end
 
-  test "socket/2" do
-    assert socket(UserSocket, "user:id", %{hello: :world}) == %Socket{
+  test "socket/3" do
+    assert %Socket{
              id: "user:id",
              assigns: %{hello: :world},
              endpoint: @endpoint,
              pubsub_server: Phoenix.Test.ChannelTest.PubSub,
-             transport: :channel_test,
-             transport_pid: self(),
              serializer: Phoenix.ChannelTest.NoopSerializer,
              handler: UserSocket
-           }
+           } = socket(UserSocket, "user:id", %{hello: :world})
   end
 
   ## join
@@ -220,7 +216,7 @@ defmodule Phoenix.Test.ChannelTest do
     assert socket.endpoint == @endpoint
     assert socket.pubsub_server == Phoenix.Test.ChannelTest.PubSub
     assert socket.topic == "foo:socket"
-    assert socket.transport == :channel_test
+    assert {Phoenix.ChannelTest, _} = socket.transport
     assert socket.transport_pid == self()
     assert socket.serializer == Phoenix.ChannelTest.NoopSerializer
     assert socket.assigns == %{hello: :world, original: :assign}
@@ -233,6 +229,18 @@ defmodule Phoenix.Test.ChannelTest do
     assert dictionary[:"$callers"] == [self()]
   end
 
+  test "join/3 from another process" do
+    socket = socket(UserSocket, "id", original: :assign)
+
+    assert {:ok, socket, client} =
+             Task.async(fn ->
+               join(socket, Channel, "foo:socket")
+              end)
+              |> Task.await()
+
+    assert %{socket | joined: true} == client
+  end
+
   test "join/3 with error reply" do
     assert {:error, %{reason: "mybad"}} =
              join(socket(UserSocket), Channel, "foo:error", %{"error" => "mybad"})